Explorar o código

Merge pull request #19557 from overleaf/rd-left-menu-dropdown

[web] Migrate the left menu on the project dashboard part #2

GitOrigin-RevId: 55b213352dd850650668bb4e507e11607aee0107
Rebeka Dekany %!s(int64=2) %!d(string=hai) anos
pai
achega
92a2debc9a

+ 1 - 0
services/web/frontend/extracted-translations.json

@@ -1383,6 +1383,7 @@
   "tc_switch_guests_tip": "",
   "tc_switch_user_tip": "",
   "tell_the_project_owner_and_ask_them_to_upgrade": "",
+  "template": "",
   "template_approved_by_publisher": "",
   "template_description": "",
   "template_title_taken_from_project_title": "",

+ 230 - 109
services/web/frontend/js/features/project-list/components/new-project-button.tsx

@@ -1,5 +1,8 @@
 import { type JSXElementConstructor, useCallback, useState } from 'react'
-import { Dropdown, MenuItem } from 'react-bootstrap'
+import {
+  Dropdown as BS3Dropdown,
+  MenuItem as BS3MenuItem,
+} from 'react-bootstrap'
 import { useTranslation } from 'react-i18next'
 import ControlledDropdown from '../../../shared/components/controlled-dropdown'
 import getMeta from '../../../utils/meta'
@@ -10,6 +13,15 @@ import AddAffiliation, { useAddAffiliation } from './add-affiliation'
 import { Nullable } from '../../../../../types/utils'
 import { sendMB } from '../../../infrastructure/event-tracking'
 import importOverleafModules from '../../../../macros/import-overleaf-module.macro'
+import BootstrapVersionSwitcher from '@/features/ui/components/bootstrap-5/bootstrap-version-switcher'
+import {
+  Dropdown,
+  DropdownDivider,
+  DropdownHeader,
+  DropdownItem,
+  DropdownMenu,
+  DropdownToggle,
+} from '@/features/ui/components/bootstrap-5/dropdown-menu'
 
 type SendTrackingEvent = {
   dropdownMenu: string
@@ -30,7 +42,6 @@ type NewProjectButtonProps = {
   id: string
   buttonText?: string
   className?: string
-  menuClassName?: string
   trackingKey?: string
   showAddAffiliationWidget?: boolean
 }
@@ -39,7 +50,6 @@ function NewProjectButton({
   id,
   buttonText,
   className,
-  menuClassName,
   trackingKey,
   showAddAffiliationWidget,
 }: NewProjectButtonProps) {
@@ -88,7 +98,7 @@ function NewProjectButton({
 
   const handleModalMenuClick = useCallback(
     (
-      e: React.MouseEvent<Record<string, unknown>>,
+      e: React.MouseEvent,
       { modalVariant, dropdownMenuEvent }: ModalMenuClickOptions
     ) => {
       // avoid invoking the "onClick" callback on the main dropdown button
@@ -105,10 +115,7 @@ function NewProjectButton({
   )
 
   const handlePortalTemplateClick = useCallback(
-    (
-      e: React.MouseEvent<Record<string, unknown>>,
-      institutionTemplateName: string
-    ) => {
+    (e: React.MouseEvent, institutionTemplateName: string) => {
       // avoid invoking the "onClick" callback on the main dropdown button
       e.stopPropagation()
 
@@ -122,10 +129,7 @@ function NewProjectButton({
   )
 
   const handleStaticTemplateClick = useCallback(
-    (
-      e: React.MouseEvent<Record<string, unknown>>,
-      templateTrackingKey: string
-    ) => {
+    (e: React.MouseEvent, templateTrackingKey: string) => {
       // avoid invoking the "onClick" callback on the main dropdown button
       e.stopPropagation()
 
@@ -142,113 +146,230 @@ function NewProjectButton({
   )
 
   const ImportProjectFromGithubMenu: JSXElementConstructor<{
-    onClick: (e: React.MouseEvent<Record<string, unknown>>) => void
+    onClick: (e: React.MouseEvent) => void
   }> = importProjectFromGithubMenu?.import.default
 
   return (
     <>
-      <ControlledDropdown
-        id={id}
-        className={className}
-        onMainButtonClick={handleMainButtonClick}
-      >
-        <Dropdown.Toggle
-          noCaret
-          className="new-project-button"
-          bsStyle="primary"
-        >
-          {buttonText || t('new_project')}
-        </Dropdown.Toggle>
-        <Dropdown.Menu className={menuClassName}>
-          <MenuItem
-            onClick={e =>
-              handleModalMenuClick(e, {
-                modalVariant: 'blank_project',
-                dropdownMenuEvent: 'blank-project',
-              })
-            }
-          >
-            {t('blank_project')}
-          </MenuItem>
-          <MenuItem
-            onClick={e =>
-              handleModalMenuClick(e, {
-                modalVariant: 'example_project',
-                dropdownMenuEvent: 'example-project',
-              })
-            }
-          >
-            {t('example_project')}
-          </MenuItem>
-          <MenuItem
-            onClick={e =>
-              handleModalMenuClick(e, {
-                modalVariant: 'upload_project',
-                dropdownMenuEvent: 'upload-project',
-              })
-            }
-          >
-            {t('upload_project')}
-          </MenuItem>
-          {ImportProjectFromGithubMenu && (
-            <ImportProjectFromGithubMenu
-              onClick={e =>
-                handleModalMenuClick(e, {
-                  modalVariant: 'import_from_github',
-                  dropdownMenuEvent: 'import-from-github',
-                })
-              }
-            />
-          )}
-          {portalTemplates.length > 0 ? (
-            <>
-              <MenuItem divider />
-              <MenuItem header>
-                {`${t('institution')} ${t('templates')}`}
-              </MenuItem>
-              {portalTemplates.map((portalTemplate, index) => (
-                <MenuItem
-                  key={`portal-template-${index}`}
-                  href={`${portalTemplate.url}#templates`}
+      <BootstrapVersionSwitcher
+        bs3={
+          <ControlledDropdown id={id} className={className}>
+            <BS3Dropdown.Toggle
+              noCaret
+              className="new-project-button"
+              bsStyle="primary"
+            >
+              {buttonText || t('new_project')}
+            </BS3Dropdown.Toggle>
+            <BS3Dropdown.Menu>
+              <BS3MenuItem
+                onClick={(e: React.MouseEvent) =>
+                  handleModalMenuClick(e, {
+                    modalVariant: 'blank_project',
+                    dropdownMenuEvent: 'blank-project',
+                  })
+                }
+              >
+                {t('blank_project')}
+              </BS3MenuItem>
+              <BS3MenuItem
+                onClick={(e: React.MouseEvent) =>
+                  handleModalMenuClick(e, {
+                    modalVariant: 'example_project',
+                    dropdownMenuEvent: 'example-project',
+                  })
+                }
+              >
+                {t('example_project')}
+              </BS3MenuItem>
+              <BS3MenuItem
+                onClick={(e: React.MouseEvent) =>
+                  handleModalMenuClick(e, {
+                    modalVariant: 'upload_project',
+                    dropdownMenuEvent: 'upload-project',
+                  })
+                }
+              >
+                {t('upload_project')}
+              </BS3MenuItem>
+              {ImportProjectFromGithubMenu && (
+                <ImportProjectFromGithubMenu
                   onClick={e =>
-                    handlePortalTemplateClick(e, portalTemplate.name)
+                    handleModalMenuClick(e, {
+                      modalVariant: 'import_from_github',
+                      dropdownMenuEvent: 'import-from-github',
+                    })
+                  }
+                />
+              )}
+              {portalTemplates.length > 0 ? (
+                <>
+                  <BS3MenuItem divider />
+                  <div aria-hidden="true" className="dropdown-header">
+                    {`${t('institution')} ${t('templates')}`}
+                  </div>
+                  {portalTemplates.map((portalTemplate, index) => (
+                    <BS3MenuItem
+                      key={`portal-template-${index}`}
+                      href={`${portalTemplate.url}#templates`}
+                      onClick={(e: React.MouseEvent) =>
+                        handlePortalTemplateClick(e, portalTemplate.name)
+                      }
+                    >
+                      {portalTemplate.name}
+                    </BS3MenuItem>
+                  ))}
+                </>
+              ) : null}
+
+              {templateLinks && templateLinks.length > 0 && (
+                <>
+                  <BS3MenuItem divider />
+                  <div aria-hidden="true" className="dropdown-header">
+                    {t('templates')}
+                  </div>
+                </>
+              )}
+              {templateLinks?.map((templateLink, index) => (
+                <BS3MenuItem
+                  key={`new-project-button-template-${index}`}
+                  href={templateLink.url}
+                  onClick={(e: React.MouseEvent) =>
+                    handleStaticTemplateClick(e, templateLink.trackingKey)
                   }
                 >
-                  {portalTemplate.name}
-                </MenuItem>
+                  {templateLink.name === 'view_all'
+                    ? t('view_all')
+                    : templateLink.name}
+                </BS3MenuItem>
               ))}
-            </>
-          ) : null}
-
-          {templateLinks && templateLinks.length > 0 && (
-            <>
-              <MenuItem divider />
-              <MenuItem header>{t('templates')}</MenuItem>
-            </>
-          )}
-          {templateLinks?.map((templateLink, index) => (
-            <MenuItem
-              key={`new-project-button-template-${index}`}
-              href={templateLink.url}
-              onClick={e =>
-                handleStaticTemplateClick(e, templateLink.trackingKey)
-              }
+              {showAddAffiliationWidget && enableAddAffiliationWidget ? (
+                <>
+                  <BS3MenuItem divider />
+                  <li className="add-affiliation-mobile-wrapper">
+                    <AddAffiliation className="is-mobile" />
+                  </li>
+                </>
+              ) : null}
+            </BS3Dropdown.Menu>
+          </ControlledDropdown>
+        }
+        bs5={
+          <Dropdown className={className} onSelect={handleMainButtonClick}>
+            <DropdownToggle
+              id={id}
+              className="new-project-button"
+              variant="primary"
             >
-              {templateLink.name === 'view_all'
-                ? t('view_all')
-                : templateLink.name}
-            </MenuItem>
-          ))}
-          {showAddAffiliationWidget && enableAddAffiliationWidget ? (
-            <>
-              <MenuItem divider />
-              <li className="add-affiliation-mobile-wrapper">
-                <AddAffiliation className="is-mobile" />
+              {buttonText || t('new_project')}
+            </DropdownToggle>
+            <DropdownMenu>
+              <li role="none">
+                <DropdownItem
+                  onClick={e =>
+                    handleModalMenuClick(e, {
+                      modalVariant: 'blank_project',
+                      dropdownMenuEvent: 'blank-project',
+                    })
+                  }
+                >
+                  {t('blank_project')}
+                </DropdownItem>
               </li>
-            </>
-          ) : null}
-        </Dropdown.Menu>
-      </ControlledDropdown>
+              <li role="none">
+                <DropdownItem
+                  onClick={e =>
+                    handleModalMenuClick(e, {
+                      modalVariant: 'example_project',
+                      dropdownMenuEvent: 'example-project',
+                    })
+                  }
+                >
+                  {t('example_project')}
+                </DropdownItem>
+              </li>
+              <li role="none">
+                <DropdownItem
+                  onClick={e =>
+                    handleModalMenuClick(e, {
+                      modalVariant: 'upload_project',
+                      dropdownMenuEvent: 'upload-project',
+                    })
+                  }
+                >
+                  {t('upload_project')}
+                </DropdownItem>
+              </li>
+              <li role="none">
+                {ImportProjectFromGithubMenu && (
+                  <ImportProjectFromGithubMenu
+                    onClick={e =>
+                      handleModalMenuClick(e, {
+                        modalVariant: 'import_from_github',
+                        dropdownMenuEvent: 'import-from-github',
+                      })
+                    }
+                  />
+                )}
+              </li>
+              {portalTemplates.length > 0 ? (
+                <>
+                  <DropdownDivider />
+                  <DropdownHeader aria-hidden="true">
+                    {`${t('institution')} ${t('templates')}`}
+                  </DropdownHeader>
+                  {portalTemplates.map((portalTemplate, index) => (
+                    <li role="none" key={`portal-template-${index}`}>
+                      <DropdownItem
+                        key={`portal-template-${index}`}
+                        href={`${portalTemplate.url}#templates`}
+                        onClick={e =>
+                          handlePortalTemplateClick(e, portalTemplate.name)
+                        }
+                        aria-label={`${portalTemplate.name} ${t('template')}`}
+                      >
+                        {portalTemplate.name}
+                      </DropdownItem>
+                    </li>
+                  ))}
+                </>
+              ) : null}
+
+              {templateLinks && templateLinks.length > 0 && (
+                <>
+                  <DropdownDivider />
+                  <DropdownHeader aria-hidden="true">
+                    {t('templates')}
+                  </DropdownHeader>
+                </>
+              )}
+              {templateLinks?.map((templateLink, index) => (
+                <li role="none" key={`new-project-button-template-${index}`}>
+                  <DropdownItem
+                    href={templateLink.url}
+                    onClick={e =>
+                      handleStaticTemplateClick(e, templateLink.trackingKey)
+                    }
+                    aria-label={`${templateLink.name} ${t('template')}`}
+                  >
+                    {templateLink.name === 'view_all'
+                      ? t('view_all')
+                      : templateLink.name}
+                  </DropdownItem>
+                </li>
+              ))}
+              {showAddAffiliationWidget && enableAddAffiliationWidget ? (
+                <>
+                  <DropdownDivider />
+                  <li className="add-affiliation-mobile-wrapper">
+                    <AddAffiliation className="is-mobile" />
+                  </li>
+                </>
+              ) : null}
+            </DropdownMenu>
+          </Dropdown>
+        }
+      />
       <NewProjectButtonModal modal={modal} onHide={() => setModal(null)} />
     </>
   )

+ 5 - 1
services/web/frontend/js/features/project-list/components/new-project-button/modal-content-new-project-form.tsx

@@ -101,8 +101,12 @@ function ModalContentNewProjectForm({ onCancel, template = 'none' }: Props) {
           variant="primary"
           onClick={createNewProject}
           disabled={projectName === '' || isLoading}
+          isLoading={isLoading}
+          bs3Props={{
+            loading: isLoading ? `${t('creating')}…` : t('create'),
+          }}
         >
-          {isLoading ? `${t('creating')}…` : t('create')}
+          {t('create')}
         </OLButton>
       </OLModalFooter>
     </>

+ 6 - 2
services/web/frontend/js/features/project-list/components/sidebar/tags-list.tsx

@@ -36,8 +36,12 @@ export default function TagsList() {
 
   return (
     <>
-      <li role="separator" className="separator">
-        <h2>{t('organize_projects')}</h2>
+      <li
+        className="dropdown-header"
+        aria-hidden="true"
+        data-testid="organize-projects"
+      >
+        {t('organize_projects')}
       </li>
       <li className="tag">
         <button type="button" className="tag-name" onClick={openCreateTagModal}>

+ 3 - 1
services/web/frontend/js/features/project-list/components/welcome-message-new/welcome-message-create-new-project-dropdown.tsx

@@ -267,7 +267,9 @@ function WelcomeMessageCreateNewProjectDropdown({
             {(portalTemplates?.length ?? 0) > 0 ? (
               <>
                 <DropdownDivider />
-                <DropdownHeader>{t('institution_templates')}</DropdownHeader>
+                <DropdownHeader aria-hidden="true">
+                  {t('institution_templates')}
+                </DropdownHeader>
                 {portalTemplates?.map((portalTemplate, index) => (
                   <DropdownItem
                     key={`portal-template-${index}`}

+ 1 - 0
services/web/frontend/js/features/ui/components/types/dropdown-menu-props.ts

@@ -36,6 +36,7 @@ export type DropdownItemProps = PropsWithChildren<{
 
 export type DropdownToggleProps = PropsWithChildren<{
   bsPrefix?: string
+  className?: string
   disabled?: boolean
   split?: boolean
   id?: string // necessary for assistive technologies

+ 17 - 17
services/web/frontend/stylesheets/app/project-list-react.less

@@ -212,6 +212,18 @@
   ul.project-list-filters {
     margin: @margin-sm @folders-menu-margin;
 
+    .dropdown-header {
+      margin-top: @folders-title-margin-top;
+      margin-bottom: @folders-title-margin-bottom;
+      font-size: @folders-title-font-size;
+      color: @folders-title-color;
+      text-transform: @folders-title-text-transform;
+      padding: 12.5px 16px;
+      font-weight: @folders-title-font-weight;
+      font-family: @font-family-sans-serif;
+      text-transform: uppercase;
+    }
+
     .subdued {
       color: @gray-light;
     }
@@ -263,17 +275,6 @@
       }
     }
 
-    h2 {
-      margin-top: @folders-title-margin-top;
-      margin-bottom: @folders-title-margin-bottom;
-      font-size: @folders-title-font-size;
-      color: @folders-title-color;
-      text-transform: @folders-title-text-transform;
-      padding: @folders-title-padding;
-      font-weight: @folders-title-font-weight;
-      font-family: @font-family-sans-serif;
-    }
-
     > li.tag {
       &.active {
         .tag-menu > button {
@@ -318,6 +319,11 @@
         align-items: center;
         word-wrap: anywhere;
 
+        .tag-list-icon {
+          vertical-align: sub;
+          font-weight: bold;
+        }
+
         span.name {
           padding-left: 0.5em;
           line-height: @folders-tag-line-height;
@@ -725,12 +731,6 @@
       // There is enough space for these on mobile devices (checked DE and EN translations).
       white-space: nowrap;
     }
-
-    .dropdown-header {
-      padding: 14px 20px;
-      font-size: 13px;
-      text-transform: uppercase;
-    }
   }
 
   .projects-types-menu-item {

+ 13 - 0
services/web/frontend/stylesheets/bootstrap-5/pages/project-list.scss

@@ -74,6 +74,10 @@
     }
   }
 
+  .new-project-button.dropdown-toggle::after {
+    display: none;
+  }
+
   .project-list-welcome-wrapper {
     width: 100%;
     padding-bottom: var(--spacing-08);
@@ -199,6 +203,13 @@
       }
     }
 
+    .dropdown-header {
+      @include body-sm;
+
+      padding: var(--spacing-05) var(--spacing-06);
+      text-transform: uppercase;
+    }
+
     > li.active {
       border-radius: 0;
 
@@ -598,6 +609,8 @@
       @extend .btn;
       @extend .btn-lg;
       @extend .btn-primary;
+
+      margin-bottom: var(--spacing-07);
     }
   }
 }

+ 5 - 1
services/web/frontend/stylesheets/components/dropdowns.less

@@ -96,6 +96,10 @@ button.dropdown-toggle.dropdown-toggle-no-background {
     outline: none;
     width: 100%;
   }
+
+  .dropdown-header {
+    color: @dropdown-header-color;
+  }
 }
 
 // This removes positioning, display and z-index, which is used just to style the menu in situations where something
@@ -199,7 +203,7 @@ button.dropdown-toggle.dropdown-toggle-no-background {
 // Dropdown section headers
 .dropdown-header {
   display: block;
-  padding: 3px 20px;
+  padding: 12.5px 15px;
   font-size: @font-size-small;
   line-height: @line-height-base;
   color: @dropdown-header-color;

+ 3 - 3
services/web/test/frontend/features/project-list/components/sidebar/tags-list.test.tsx

@@ -41,9 +41,9 @@ describe('<TagsList />', function () {
   })
 
   it('displays the tags list', function () {
-    screen.getByRole('heading', {
-      name: 'Organize Projects',
-    })
+    const header = screen.getByTestId('organize-projects')
+    expect(header.textContent).to.equal('Organize Projects')
+
     screen.getByRole('button', {
       name: 'New Tag',
     })