Ver Fonte

[web] Add icon menus in the Sidebar (#22318)

* Add sidebar icons and menus (Help, Account)

* Place the account dropdown menu above the tooltip

* Hide tooltip when dropdown menu is shown

* Fixup tooltip position

* Add Digital Science link

* Add aria-labels on the dropdown toggles

* Update sidebar items spacings

- Add space between notification and DS icons
- Balance margin/padding in the scrollable part

* Revert changing the side of .dev-tool-bar-open-button

* Remove `!important` in link color

* Add Help tooltip

* Revert dropdown-menu z-index change

* Move `AccountMenuItems` to its own file

* Revert "Remove `!important` in link color"

This reverts commit b2de83f815d05cd1e224604d4ca355986ba76b99.

GitOrigin-RevId: 286266ea5348384f2592f748d3ae32c452558988
Antoine Clausse há 1 ano atrás
pai
commit
62cfb68c1b

+ 89 - 5
services/web/frontend/js/features/project-list/components/sidebar/sidebar-ds-nav.tsx

@@ -1,15 +1,36 @@
+import { useState } from 'react'
 import NewProjectButton from '../new-project-button'
 import SidebarFilters from './sidebar-filters'
 import AddAffiliation, { useAddAffiliation } from '../add-affiliation'
 import SurveyWidget from '../survey-widget'
 import { usePersistedResize } from '../../../../shared/hooks/use-resize'
+import MaterialIcon from '@/shared/components/material-icon'
+import { Dropdown } from 'react-bootstrap-5'
+import getMeta from '@/utils/meta'
+import OLTooltip from '@/features/ui/components/ol/ol-tooltip'
+import { useTranslation } from 'react-i18next'
+import { NavDropdownMenuItems } from '@/features/ui/components/bootstrap-5/navbar/nav-dropdown-from-data'
+import { NavbarDropdownItemData } from '@/features/ui/components/types/navbar'
+import { useContactUsModal } from '@/shared/hooks/use-contact-us-modal'
+import { UserProvider } from '@/shared/context/user-context'
+import { AccountMenuItems } from '@/features/ui/components/bootstrap-5/navbar/account-menu-items'
 
 function SidebarDsNav() {
+  const { t } = useTranslation()
+  const [showAccountDropdown, setShowAccountDropdown] = useState(false)
+  const [showHelpDropdown, setShowHelpDropdown] = useState(false)
+  const { showModal: showContactUsModal, modal: contactUsModal } =
+    useContactUsModal({
+      autofillProjectUrl: false,
+    })
   const { show: showAddAffiliationWidget } = useAddAffiliation()
   const { mousePos, getHandleProps, getTargetProps } = usePersistedResize({
     name: 'project-sidebar',
   })
-
+  const { sessionUser, showSubscriptionLink, items } = getMeta('ol-navbar')
+  const helpItem = items.find(
+    item => item.text === 'help'
+  ) as NavbarDropdownItemData
   return (
     <div
       className="project-list-sidebar-wrapper-react d-none d-md-flex"
@@ -28,10 +49,73 @@ function SidebarDsNav() {
       <div className="project-list-sidebar-survey-wrapper">
         <SurveyWidget variant="light" />
       </div>
-      <div className="bg-warning">
-        Help / Profile
-        <br />
-        DS Nav
+      <div className="d-flex gap-3 mb-2">
+        {helpItem && (
+          <Dropdown
+            className="ds-nav-icon-dropdown"
+            onToggle={show => setShowHelpDropdown(show)}
+          >
+            <Dropdown.Toggle role="menuitem" aria-label={t('help')}>
+              <OLTooltip
+                description={t('help')}
+                id="help-icon"
+                overlayProps={{
+                  placement: 'top',
+                  show: showHelpDropdown ? false : undefined,
+                }}
+              >
+                <div>
+                  <MaterialIcon type="help" size="2x" />
+                </div>
+              </OLTooltip>
+            </Dropdown.Toggle>
+            <Dropdown.Menu as="ul" role="menu" align="end">
+              <NavDropdownMenuItems
+                dropdown={helpItem.dropdown}
+                showContactUsModal={showContactUsModal}
+              />
+            </Dropdown.Menu>
+          </Dropdown>
+        )}
+        {sessionUser && (
+          <>
+            <Dropdown
+              className="ds-nav-icon-dropdown"
+              onToggle={show => setShowAccountDropdown(show)}
+            >
+              <Dropdown.Toggle role="menuitem" aria-label={t('Account')}>
+                <OLTooltip
+                  description={t('Account')}
+                  id="open-account"
+                  overlayProps={{
+                    placement: 'top',
+                    show: showAccountDropdown ? false : undefined,
+                  }}
+                >
+                  <div>
+                    <MaterialIcon type="person" size="2x" />
+                  </div>
+                </OLTooltip>
+              </Dropdown.Toggle>
+              <Dropdown.Menu as="ul" role="menu" align="end">
+                <AccountMenuItems
+                  sessionUser={sessionUser}
+                  showSubscriptionLink={showSubscriptionLink}
+                />
+              </Dropdown.Menu>
+            </Dropdown>
+            <UserProvider>{contactUsModal}</UserProvider>
+          </>
+        )}
+      </div>
+      <div className="ds-nav-ds-link">
+        <a
+          target="_blank"
+          href="https://www.digital-science.com/"
+          rel="noopener"
+        >
+          Digital Science
+        </a>
       </div>
       <div
         {...getHandleProps({

+ 55 - 0
services/web/frontend/js/features/ui/components/bootstrap-5/navbar/account-menu-items.tsx

@@ -0,0 +1,55 @@
+import { Dropdown } from 'react-bootstrap-5'
+import { useTranslation } from 'react-i18next'
+import getMeta from '@/utils/meta'
+import type { NavbarSessionUser } from '@/features/ui/components/types/navbar'
+import DropdownListItem from '@/features/ui/components/bootstrap-5/dropdown-list-item'
+import NavDropdownDivider from './nav-dropdown-divider'
+import NavDropdownLinkItem from './nav-dropdown-link-item'
+
+export function AccountMenuItems({
+  sessionUser,
+  showSubscriptionLink,
+}: {
+  sessionUser: NavbarSessionUser
+  showSubscriptionLink: boolean
+}) {
+  const { t } = useTranslation()
+  const logOutFormId = 'logOutForm'
+
+  return (
+    <>
+      <Dropdown.Item as="li" disabled role="menuitem">
+        {sessionUser.email}
+      </Dropdown.Item>
+      <NavDropdownDivider />
+      <NavDropdownLinkItem href="/user/settings">
+        {t('Account Settings')}
+      </NavDropdownLinkItem>
+      {showSubscriptionLink ? (
+        <NavDropdownLinkItem href="/user/subscription">
+          {t('subscription')}
+        </NavDropdownLinkItem>
+      ) : null}
+      <NavDropdownDivider />
+      <DropdownListItem>
+        {
+          // The button is outside the form but still belongs to it via the
+          // form attribute. The reason to do this is that if the button is
+          // inside the form, screen readers will not count it in the total
+          // number of menu items
+        }
+        <Dropdown.Item
+          as="button"
+          type="submit"
+          form={logOutFormId}
+          role="menuitem"
+        >
+          {t('log_out')}
+        </Dropdown.Item>
+        <form id={logOutFormId} method="POST" action="/logout">
+          <input type="hidden" name="_csrf" value={getMeta('ol-csrfToken')} />
+        </form>
+      </DropdownListItem>
+    </>
+  )
+}

+ 5 - 39
services/web/frontend/js/features/ui/components/bootstrap-5/navbar/logged-in-items.tsx

@@ -1,12 +1,8 @@
 import { useTranslation } from 'react-i18next'
-import { Dropdown } from 'react-bootstrap-5'
-import NavDropdownDivider from '@/features/ui/components/bootstrap-5/navbar/nav-dropdown-divider'
-import getMeta from '@/utils/meta'
 import NavDropdownMenu from '@/features/ui/components/bootstrap-5/navbar/nav-dropdown-menu'
-import NavDropdownLinkItem from '@/features/ui/components/bootstrap-5/navbar/nav-dropdown-link-item'
-import DropdownListItem from '@/features/ui/components/bootstrap-5/dropdown-list-item'
 import type { NavbarSessionUser } from '@/features/ui/components/types/navbar'
 import NavLinkItem from '@/features/ui/components/bootstrap-5/navbar/nav-link-item'
+import { AccountMenuItems } from './account-menu-items'
 
 export default function LoggedInItems({
   sessionUser,
@@ -16,44 +12,14 @@ export default function LoggedInItems({
   showSubscriptionLink: boolean
 }) {
   const { t } = useTranslation()
-  const logOutFormId = 'logOutForm'
-
   return (
     <>
       <NavLinkItem href="/project">{t('projects')}</NavLinkItem>
       <NavDropdownMenu title={t('Account')}>
-        <Dropdown.Item as="li" disabled role="menuitem">
-          {sessionUser.email}
-        </Dropdown.Item>
-        <NavDropdownDivider />
-        <NavDropdownLinkItem href="/user/settings">
-          {t('Account Settings')}
-        </NavDropdownLinkItem>
-        {showSubscriptionLink ? (
-          <NavDropdownLinkItem href="/user/subscription">
-            {t('subscription')}
-          </NavDropdownLinkItem>
-        ) : null}
-        <NavDropdownDivider />
-        <DropdownListItem>
-          {
-            // The button is outside the form but still belongs to it via the
-            // form attribute. The reason to do this is that if the button is
-            // inside the form, screen readers will not count it in the total
-            // number of menu items
-          }
-          <Dropdown.Item
-            as="button"
-            type="submit"
-            form={logOutFormId}
-            role="menuitem"
-          >
-            {t('log_out')}
-          </Dropdown.Item>
-          <form id={logOutFormId} method="POST" action="/logout">
-            <input type="hidden" name="_csrf" value={getMeta('ol-csrfToken')} />
-          </form>
-        </DropdownListItem>
+        <AccountMenuItems
+          sessionUser={sessionUser}
+          showSubscriptionLink={showSubscriptionLink}
+        />
       </NavDropdownMenu>
     </>
   )

+ 23 - 3
services/web/frontend/js/features/ui/components/bootstrap-5/navbar/nav-dropdown-from-data.tsx

@@ -1,4 +1,7 @@
-import type { NavbarDropdownItemData } from '@/features/ui/components/types/navbar'
+import type {
+  NavbarDropdownItemData,
+  NavbarItemDropdownData,
+} from '@/features/ui/components/types/navbar'
 import NavDropdownDivider from '@/features/ui/components/bootstrap-5/navbar/nav-dropdown-divider'
 import { sendMB } from '@/infrastructure/event-tracking'
 import { isDropdownLinkItem } from '@/features/ui/components/bootstrap-5/navbar/util'
@@ -16,7 +19,24 @@ export default function NavDropdownFromData({
 }) {
   return (
     <NavDropdownMenu title={item.translatedText} className={item.class}>
-      {item.dropdown.map((child, index) => {
+      <NavDropdownMenuItems
+        dropdown={item.dropdown}
+        showContactUsModal={showContactUsModal}
+      />
+    </NavDropdownMenu>
+  )
+}
+
+export function NavDropdownMenuItems({
+  dropdown,
+  showContactUsModal,
+}: {
+  dropdown: NavbarItemDropdownData
+  showContactUsModal: (event?: Event) => void
+}) {
+  return (
+    <>
+      {dropdown.map((child, index) => {
         if ('divider' in child) {
           return <NavDropdownDivider key={index} />
         } else if ('isContactUs' in child) {
@@ -41,6 +61,6 @@ export default function NavDropdownFromData({
           )
         }
       })}
-    </NavDropdownMenu>
+    </>
   )
 }

+ 54 - 9
services/web/frontend/stylesheets/bootstrap-5/pages/project-list-ds-nav.scss

@@ -25,10 +25,6 @@
     display: flex;
     overflow-y: hidden;
 
-    .new-project-button {
-      margin-bottom: var(--spacing-08);
-    }
-
     .project-list-sidebar-wrapper-react {
       position: relative;
       display: flex;
@@ -41,8 +37,8 @@
       .project-list-sidebar-scroll {
         flex: 1 1 auto;
         overflow: auto;
-        margin: 0 calc(var(--spacing-07) * -1);
-        padding: 0 var(--spacing-07);
+        margin: var(--spacing-05) calc(var(--spacing-07) * -1);
+        padding: var(--spacing-05) var(--spacing-07);
       }
 
       .project-list-sidebar-survey-link {
@@ -59,9 +55,15 @@
           padding: 0;
         }
       }
+
+      .project-list-sidebar-survey-wrapper .user-notifications {
+        margin-bottom: var(--spacing-05);
+      }
     }
 
     ul.project-list-filters {
+      margin-bottom: 0;
+
       hr {
         margin: var(--spacing-05) 0;
       }
@@ -143,8 +145,51 @@
       width: 100% !important;
     }
   }
-}
 
-.project-list-sidebar-survey-wrapper {
-  margin-top: var(--spacing-05);
+  .ds-nav-icon-dropdown {
+    .dropdown-toggle {
+      color: var(--content-secondary);
+      background: none;
+      height: 44px;
+      width: 44px;
+      padding: 0;
+      overflow: hidden;
+
+      &:hover {
+        background-color: var(--bg-light-secondary);
+      }
+
+      &.show {
+        background-color: var(--bg-accent-03);
+      }
+
+      &::after {
+        display: none;
+      }
+
+      > div {
+        height: 100%;
+        display: flex;
+        align-items: center;
+        justify-content: center;
+      }
+
+      .material-symbols {
+        vertical-align: middle;
+      }
+    }
+  }
+
+  .ds-nav-ds-link {
+    margin-bottom: var(--spacing-05);
+
+    a {
+      color: var(--content-secondary) !important;
+      text-transform: uppercase;
+      font-weight: bold;
+      text-decoration: none;
+
+      @include body-xs;
+    }
+  }
 }