Explorar o código

[web] Update project list page layout and sidebar to new design (#22186)

* PoC DS nav project page shell

* Rename files: use `ds-nav` as a suffix

(really just moving code without changes)

* Update NavBar to the new design

* Small updates to project-list-ds-nav.tsx so it gets its basic shape

* Nest `.survey-notification` and `.project-list-sidebar-survey-wrapper` in the default classes to avoid interferences

* Create `SidebarDsNav`

* Add props to sidebar components so they work in both versions

* Update the SCSS code for the sidebar redesign

* Update subheader to "Organize Tags"

* Mute add affiliation font

* Remove `<aside className="project-list-sidebar-react">` and add sidebar max/min widths

* Fixup buttons padding

* Fix tests: add SplitTestProvider

* Fixup sidebar scroll div: add `flex: 1 1 auto`

* Make "Uncategorized" italic

* Update logo to SVG

* Optimize the svg
https://jakearchibald.github.io/svgomg/
9.12k → 4.92k 53.91%

---------

Co-authored-by: Tim Down <158919+timdown@users.noreply.github.com>
GitOrigin-RevId: 3d08b4b80291d9465fae87ffdf0c6a9f6deda554
Antoine Clausse hai 1 ano
pai
achega
629579b840

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

@@ -1040,6 +1040,7 @@
   "or": "",
   "organization_name": "",
   "organize_projects": "",
+  "organize_tags": "",
   "other": "",
   "other_logs_and_files": "",
   "other_output_files": "",

+ 5 - 1
services/web/frontend/js/features/project-list/components/add-affiliation.tsx

@@ -3,6 +3,7 @@ import { useProjectListContext } from '../context/project-list-context'
 import getMeta from '../../../utils/meta'
 import classNames from 'classnames'
 import OLButton from '@/features/ui/components/ol/ol-button'
+import { useIsDsNav } from '@/features/project-list/components/use-is-ds-nav'
 
 export function useAddAffiliation() {
   const { totalProjectsCount } = useProjectListContext()
@@ -21,6 +22,7 @@ type AddAffiliationProps = {
 function AddAffiliation({ className }: AddAffiliationProps) {
   const { t } = useTranslation()
   const { show } = useAddAffiliation()
+  const isDsNav = useIsDsNav()
 
   if (!show) {
     return null
@@ -30,7 +32,9 @@ function AddAffiliation({ className }: AddAffiliationProps) {
 
   return (
     <div className={classes}>
-      <p>{t('are_you_affiliated_with_an_institution')}</p>
+      <p className={isDsNav ? 'text-muted' : undefined}>
+        {t('are_you_affiliated_with_an_institution')}
+      </p>
       <OLButton variant="secondary" href="/user/settings">
         {t('add_affiliation')}
       </OLButton>

+ 80 - 0
services/web/frontend/js/features/project-list/components/project-list-ds-nav.tsx

@@ -0,0 +1,80 @@
+import getMeta from '@/utils/meta'
+import DefaultNavbar from '@/features/ui/components/bootstrap-5/navbar/default-navbar'
+import FatFooter from '@/features/ui/components/bootstrap-5/footer/fat-footer'
+import NewProjectButton from '@/features/project-list/components/new-project-button'
+import CurrentPlanWidget from '@/features/project-list/components/current-plan-widget/current-plan-widget'
+import ProjectTools from '@/features/project-list/components/table/project-tools/project-tools'
+import { useProjectListContext } from '@/features/project-list/context/project-list-context'
+import SearchForm from '@/features/project-list/components/search-form'
+import { TableContainer } from '@/features/ui/components/bootstrap-5/table'
+import ProjectListTable from '@/features/project-list/components/table/project-list-table'
+import SidebarDsNav from '@/features/project-list/components/sidebar/sidebar-ds-nav'
+
+export function ProjectListDsNav() {
+  const navbarProps = getMeta('ol-navbar')
+  const footerProps = getMeta('ol-footer')
+  const {
+    searchText,
+    setSearchText,
+    selectedProjects,
+    filter,
+    tags,
+    selectedTagId,
+  } = useProjectListContext()
+
+  const selectedTag = tags.find(tag => tag._id === selectedTagId)
+
+  const tableTopArea = (
+    <div className="pt-2 pb-3 d-md-none d-flex gap-2">
+      <NewProjectButton
+        id="new-project-button-projects-table"
+        showAddAffiliationWidget
+      />
+      <SearchForm
+        inputValue={searchText}
+        setInputValue={setSearchText}
+        filter={filter}
+        selectedTag={selectedTag}
+        className="overflow-hidden flex-grow-1"
+      />
+    </div>
+  )
+
+  return (
+    <div className="project-ds-nav-page website-redesign">
+      <DefaultNavbar
+        {...navbarProps}
+        items={navbarProps.items.filter(item => item.text !== 'help')}
+        customLogo="/img/ol-brand/overleaf-a-ds-solution-mallard.svg"
+        showAccountButtons={false}
+      />
+      <main className="project-ds-nav-sidebar-and-content">
+        <SidebarDsNav />
+        <div className="project-ds-nav-content">
+          <div>Notifications and search and stuff</div>
+          <div className="project-ds-nav-project-list">
+            {selectedProjects.length === 0 ? (
+              <CurrentPlanWidget />
+            ) : (
+              <ProjectTools />
+            )}
+
+            <SearchForm
+              inputValue={searchText}
+              setInputValue={setSearchText}
+              filter={filter}
+              selectedTag={selectedTag}
+              className="overflow-hidden flex-grow-1"
+            />
+
+            <TableContainer bordered>
+              {tableTopArea}
+              <ProjectListTable />
+            </TableContainer>
+          </div>
+          <FatFooter {...footerProps} />
+        </div>
+      </main>
+    </div>
+  )
+}

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

@@ -20,6 +20,7 @@ import DefaultNavbar from '@/features/ui/components/bootstrap-5/navbar/default-n
 import FatFooter from '@/features/ui/components/bootstrap-5/footer/fat-footer'
 import WelcomePageContent from '@/features/project-list/components/welcome-page-content'
 import ProjectListDefault from '@/features/project-list/components/project-list-default'
+import { ProjectListDsNav } from '@/features/project-list/components/project-list-ds-nav'
 
 function ProjectListRoot() {
   const { isReady } = useWaitForI18n()
@@ -108,12 +109,7 @@ function ProjectListPageContent() {
       </DefaultPageContentWrapper>
     )
   } else if (hasDsNav) {
-    return (
-      <>
-        <div>Header with cut-down nav</div>
-        <div>Project list with DS nav and footer</div>
-      </>
-    )
+    return <ProjectListDsNav />
   } else {
     return (
       <DefaultPageContentWrapper>

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

@@ -0,0 +1,52 @@
+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'
+
+function SidebarDsNav() {
+  const { show: showAddAffiliationWidget } = useAddAffiliation()
+  const { mousePos, getHandleProps, getTargetProps } = usePersistedResize({
+    name: 'project-sidebar',
+  })
+
+  return (
+    <div
+      className="project-ds-nav-sidebar d-none d-md-flex"
+      {...getTargetProps({
+        style: {
+          ...(mousePos?.x && { flexBasis: `${mousePos.x}px` }),
+        },
+      })}
+    >
+      <NewProjectButton id="new-project-button-sidebar" />
+      <div className="project-list-sidebar-scroll">
+        <SidebarFilters withHr />
+        {showAddAffiliationWidget && <hr />}
+        <AddAffiliation />
+      </div>
+      <div className="project-list-sidebar-survey-wrapper">
+        <SurveyWidget variant="light" />
+      </div>
+      <div className="bg-warning">
+        Help / Profile
+        <br />
+        DS Nav
+      </div>
+      <div
+        {...getHandleProps({
+          style: {
+            position: 'absolute',
+            zIndex: 1,
+            top: 0,
+            right: '-2px',
+            height: '100%',
+            width: '4px',
+          },
+        })}
+      />
+    </div>
+  )
+}
+
+export default SidebarDsNav

+ 6 - 1
services/web/frontend/js/features/project-list/components/sidebar/sidebar-filters.tsx

@@ -27,7 +27,7 @@ export function SidebarFilter({ filter, text }: SidebarFilterProps) {
   )
 }
 
-export default function SidebarFilters() {
+export default function SidebarFilters({ withHr }: { withHr?: boolean }) {
   const { t } = useTranslation()
 
   return (
@@ -37,6 +37,11 @@ export default function SidebarFilters() {
       <SidebarFilter filter="shared" text={t('shared_with_you')} />
       <SidebarFilter filter="archived" text={t('archived_projects')} />
       <SidebarFilter filter="trashed" text={t('trashed_projects')} />
+      {withHr && (
+        <li role="none">
+          <hr />
+        </li>
+      )}
       <TagsList />
     </ul>
   )

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

@@ -13,6 +13,7 @@ import {
   DropdownMenu,
   DropdownToggle,
 } from '@/features/ui/components/bootstrap-5/dropdown-menu'
+import { useIsDsNav } from '@/features/project-list/components/use-is-ds-nav'
 
 export default function TagsList() {
   const { t } = useTranslation()
@@ -33,6 +34,7 @@ export default function TagsList() {
     DeleteTagModal,
   } = useTag()
 
+  const isDsNav = useIsDsNav()
   return (
     <>
       <li
@@ -40,7 +42,7 @@ export default function TagsList() {
         aria-hidden="true"
         data-testid="organize-projects"
       >
-        {t('organize_projects')}
+        {isDsNav ? t('organize_tags') : t('organize_projects')}
       </li>
       <li className="tag">
         <button type="button" className="tag-name" onClick={openCreateTagModal}>
@@ -113,7 +115,7 @@ export default function TagsList() {
             className="tag-name"
             onClick={() => selectTag(UNCATEGORIZED_KEY)}
           >
-            <span className="name">
+            <span className="name fst-italic">
               {t('uncategorized')}{' '}
               <span className="subdued">({untaggedProjectsCount})</span>
             </span>

+ 6 - 2
services/web/frontend/js/features/project-list/components/survey-widget.tsx

@@ -3,7 +3,11 @@ import getMeta from '../../../utils/meta'
 import { useCallback } from 'react'
 import Close from '@/shared/components/close'
 
-export default function SurveyWidget() {
+export default function SurveyWidget({
+  variant = 'dark',
+}: {
+  variant?: 'light' | 'dark'
+}) {
   const survey = getMeta('ol-survey')
   const [dismissedSurvey, setDismissedSurvey] = usePersistedState(
     `dismissed-${survey?.name}`,
@@ -34,7 +38,7 @@ export default function SurveyWidget() {
             </a>
           </div>
           <div className="notification-close notification-close-button-style">
-            <Close variant="dark" onDismiss={() => dismissSurvey()} />
+            <Close variant={variant} onDismiss={() => dismissSurvey()} />
           </div>
         </div>
       </div>

+ 6 - 0
services/web/frontend/js/features/project-list/components/use-is-ds-nav.ts

@@ -0,0 +1,6 @@
+import { useSplitTestContext } from '@/shared/context/split-test-context'
+
+export const useIsDsNav = () => {
+  const { splitTestVariants } = useSplitTestContext()
+  return splitTestVariants['sidebar-navigation-ui-update'] === 'active'
+}

+ 13 - 11
services/web/frontend/js/features/ui/components/bootstrap-5/navbar/default-navbar.tsx

@@ -23,6 +23,7 @@ function DefaultNavbar(props: DefaultNavbarMetadata) {
     enableUpgradeButton,
     suppressNavbarRight,
     suppressNavContentLinks,
+    showAccountButtons = true,
     showSubscriptionLink,
     showSignUpLink,
     currentUrl,
@@ -110,17 +111,18 @@ function DefaultNavbar(props: DefaultNavbarMetadata) {
                       />
                     ) : null
                   })}
-                  {sessionUser ? (
-                    <LoggedInItems
-                      sessionUser={sessionUser}
-                      showSubscriptionLink={showSubscriptionLink}
-                    />
-                  ) : (
-                    <LoggedOutItems
-                      showSignUpLink={showSignUpLink}
-                      currentUrl={currentUrl}
-                    />
-                  )}
+                  {showAccountButtons &&
+                    (sessionUser ? (
+                      <LoggedInItems
+                        sessionUser={sessionUser}
+                        showSubscriptionLink={showSubscriptionLink}
+                      />
+                    ) : (
+                      <LoggedOutItems
+                        showSignUpLink={showSignUpLink}
+                        currentUrl={currentUrl}
+                      />
+                    ))}
                 </Nav>
               </Navbar.Collapse>
             </>

+ 1 - 0
services/web/frontend/js/features/ui/components/types/default-navbar-metadata.ts

@@ -13,6 +13,7 @@ export type DefaultNavbarMetadata = {
   enableUpgradeButton: boolean
   suppressNavbarRight: boolean
   suppressNavContentLinks: boolean
+  showAccountButtons?: boolean
   showSubscriptionLink: boolean
   showSignUpLink: boolean
   currentUrl: string

+ 1 - 0
services/web/frontend/stylesheets/bootstrap-5/pages/all.scss

@@ -2,6 +2,7 @@
 @import 'cms';
 @import 'content';
 @import 'project-list';
+@import 'project-list-ds-nav';
 @import 'sidebar-v2-dash-pane';
 @import 'editor/ide';
 @import 'editor/toolbar';

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

@@ -0,0 +1,254 @@
+.project-ds-nav-page {
+  display: flex;
+  flex-direction: column;
+  height: 100vh;
+  height: 100dvh;
+  color: var(--content-secondary);
+
+  .navbar-default {
+    position: relative;
+  }
+
+  .project-ds-nav-sidebar-and-content {
+    flex-grow: 1;
+    display: flex;
+    overflow-y: hidden;
+
+    .project-ds-nav-sidebar {
+      position: relative;
+      display: flex;
+      flex-direction: column;
+      flex: 0 0 15%;
+      max-width: 320px;
+      min-width: 200px;
+      padding: var(--spacing-08) var(--spacing-08) 0 var(--spacing-05);
+
+      .project-list-sidebar-scroll {
+        flex: 1 1 auto;
+        overflow: auto;
+        margin: 0 calc(var(--spacing-07) * -1);
+        padding: 0 var(--spacing-07);
+      }
+
+      .new-project-button {
+        margin-bottom: var(--spacing-08);
+
+        &::after {
+          display: none;
+        }
+      }
+
+      .dropdown {
+        width: 100%;
+
+        .new-project-button {
+          width: 100%;
+        }
+      }
+    }
+
+    ul.project-list-filters {
+      .subdued {
+        color: var(--content-disabled);
+      }
+
+      hr {
+        margin: var(--spacing-05) 0;
+      }
+
+      > li {
+        position: relative;
+
+        > button {
+          width: 100%;
+          text-align: left;
+          color: var(--content-secondary);
+          background: none;
+          border-radius: var(--border-radius-medium);
+          border: none;
+          padding: var(--spacing-04) var(--spacing-05);
+
+          &:hover {
+            background-color: var(--bg-light-secondary);
+          }
+        }
+
+        &.active {
+          button {
+            background-color: var(--bg-accent-03);
+            color: var(--green-60);
+            font-weight: bold;
+          }
+        }
+      }
+
+      .dropdown-header {
+        @include body-sm;
+
+        padding: var(--spacing-05) var(--spacing-06);
+        text-transform: uppercase;
+        font-weight: bold;
+      }
+
+      > li.tag {
+        &:hover {
+          .tag-menu {
+            display: block;
+          }
+        }
+
+        button.tag-name {
+          position: relative;
+          padding-right: var(--spacing-08);
+          display: flex;
+          align-items: center;
+          word-wrap: anywhere;
+
+          .tag-list-icon {
+            vertical-align: sub;
+            font-weight: bold;
+          }
+
+          span.name {
+            padding-left: 0.5em;
+            line-height: 1.4;
+          }
+        }
+      }
+
+      .tag-menu {
+        &.show {
+          display: block;
+        }
+
+        button.dropdown-toggle {
+          border: 1px solid var(--content-secondary);
+          border-radius: var(--border-radius-base);
+          background-color: transparent;
+          color: var(--content-secondary);
+          display: flex;
+          align-items: center;
+          justify-content: center;
+          width: 16px;
+          height: 16px;
+          position: relative;
+          padding: var(--spacing-01) var(--spacing-03);
+
+          &::after {
+            margin: 0;
+          }
+        }
+
+        display: none;
+        width: auto;
+        position: absolute;
+        top: 50%;
+        margin-top: -8px; // Half the element height.
+        right: 4px;
+
+        &.open {
+          display: block;
+        }
+
+        button.tag-action {
+          border-radius: unset;
+          width: 100%;
+          background-color: transparent;
+          border-color: transparent;
+          color: var(--neutral-70);
+          text-align: left;
+          font-weight: normal;
+
+          &:active {
+            outline: none;
+          }
+        }
+      }
+    }
+
+    .project-ds-nav-content {
+      flex-grow: 1;
+      overflow-y: auto;
+      position: relative;
+      background-color: var(--bg-light-secondary);
+      padding: var(--spacing-08);
+
+      @media (width >= 768px) {
+        border-top-left-radius: var(--border-radius-large);
+      }
+    }
+  }
+
+  .fat-footer-container {
+    width: 100% !important;
+  }
+}
+
+.add-affiliation {
+  .progress {
+    height: var(--spacing-05);
+    margin-bottom: var(--spacing-03);
+  }
+
+  p {
+    margin-bottom: var(--spacing-03);
+  }
+
+  &.is-mobile p {
+    @include body-xs;
+
+    white-space: normal;
+  }
+}
+
+.survey-notification {
+  display: flex;
+  flex-wrap: wrap;
+  padding: var(--spacing-06);
+  background-color: var(--bg-light-secondary);
+  border-color: transparent;
+  color: var(--content-secondary);
+  border-radius: var(--border-radius-base);
+
+  @include media-breakpoint-up(md) {
+    flex-wrap: nowrap;
+  }
+
+  button.close {
+    padding: 0;
+  }
+}
+
+.project-list-sidebar-survey-wrapper {
+  margin-top: var(--spacing-05);
+
+  .survey-notification {
+    font-size: var(--font-size-02);
+
+    a {
+      text-decoration: none;
+    }
+  }
+
+  .project-list-sidebar-survey-link {
+    color: var(--content-secondary) !important;
+  }
+
+  @include media-breakpoint-down(md) {
+    position: static;
+    margin-top: var(--spacing-05);
+
+    .survey-notification {
+      font-size: unset;
+
+      .project-list-sidebar-survey-link {
+        display: block;
+        align-items: center;
+        min-width: 48px;
+        min-height: 48px;
+        padding-top: var(--spacing-07);
+        color: var(--content-secondary) !important;
+      }
+    }
+  }
+}

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

@@ -682,6 +682,57 @@
       margin: 0 auto;
     }
   }
+
+  .survey-notification {
+    display: flex;
+    flex-wrap: wrap;
+    padding: var(--spacing-06);
+    background-color: var(--bg-dark-tertiary);
+    border-color: transparent;
+    color: var(--neutral-20);
+    box-shadow: 2px 4px 6px rgb(0 0 0 / 25%);
+    border-radius: var(--border-radius-base);
+
+    @include media-breakpoint-up(md) {
+      flex-wrap: nowrap;
+    }
+
+    button.close {
+      @extend .text-white;
+
+      padding: 0;
+    }
+  }
+
+  .project-list-sidebar-survey-wrapper {
+    position: sticky;
+    bottom: 0;
+
+    .survey-notification {
+      font-size: var(--font-size-02);
+
+      a {
+        text-decoration: none;
+      }
+    }
+
+    @include media-breakpoint-down(md) {
+      position: static;
+      margin-top: var(--spacing-05);
+
+      .survey-notification {
+        font-size: unset;
+
+        .project-list-sidebar-survey-link {
+          display: block;
+          align-items: center;
+          min-width: 48px;
+          min-height: 48px;
+          padding-top: var(--spacing-07);
+        }
+      }
+    }
+  }
 }
 
 .current-plan {
@@ -714,57 +765,6 @@
   }
 }
 
-.survey-notification {
-  display: flex;
-  flex-wrap: wrap;
-  padding: var(--spacing-06);
-  background-color: var(--bg-dark-tertiary);
-  border-color: transparent;
-  color: var(--neutral-20);
-  box-shadow: 2px 4px 6px rgb(0 0 0 / 25%);
-  border-radius: var(--border-radius-base);
-
-  @include media-breakpoint-up(md) {
-    flex-wrap: nowrap;
-  }
-
-  button.close {
-    @extend .text-white;
-
-    padding: 0;
-  }
-}
-
-.project-list-sidebar-survey-wrapper {
-  position: sticky;
-  bottom: 0;
-
-  .survey-notification {
-    font-size: var(--font-size-02);
-
-    a {
-      text-decoration: none;
-    }
-  }
-
-  @include media-breakpoint-down(md) {
-    position: static;
-    margin-top: var(--spacing-05);
-
-    .survey-notification {
-      font-size: unset;
-
-      .project-list-sidebar-survey-link {
-        display: block;
-        align-items: center;
-        min-width: 48px;
-        min-height: 48px;
-        padding-top: var(--spacing-07);
-      }
-    }
-  }
-}
-
 .project-list-load-more-button {
   margin-bottom: var(--spacing-05);
 }

+ 1 - 0
services/web/locales/en.json

@@ -1474,6 +1474,7 @@
   "organization_or_company_name": "Organization or company name",
   "organization_or_company_type": "Organization or company type",
   "organize_projects": "Organize Projects",
+  "organize_tags": "Organize Tags",
   "original_price": "Original price",
   "other": "Other",
   "other_actions": "Other Actions",

A diferenza do arquivo foi suprimida porque é demasiado grande
+ 0 - 0
services/web/public/img/ol-brand/overleaf-a-ds-solution-mallard.svg


+ 4 - 1
services/web/test/frontend/features/project-list/helpers/render-with-context.tsx

@@ -5,6 +5,7 @@ import { ColorPickerProvider } from '../../../../../frontend/js/features/project
 import { ProjectListProvider } from '../../../../../frontend/js/features/project-list/context/project-list-context'
 import { Project } from '../../../../../types/project/dashboard/api'
 import { projectsData } from '../fixtures/projects-data'
+import { SplitTestProvider } from '@/shared/context/split-test-context'
 
 type Options = {
   projects?: Project[]
@@ -36,7 +37,9 @@ export function renderWithProjectListContext(
     children: React.ReactNode
   }) => (
     <ProjectListProvider>
-      <ColorPickerProvider>{children}</ColorPickerProvider>
+      <SplitTestProvider>
+        <ColorPickerProvider>{children}</ColorPickerProvider>
+      </SplitTestProvider>
     </ProjectListProvider>
   )
 

Algúns arquivos non se mostraron porque demasiados arquivos cambiaron neste cambio