Răsfoiți Sursa

Add DS nav page switcher behind overleaf-library flag (#33112)

* Add DS nav page switcher behind overleaf-library flag

- Add shared DsNavPageSwitcher component (Library/Projects nav links + logo)
- Show page switcher in projects sidebar when overleaf-library flag enabled
- Hide 'All projects' filter and sidebar New Project button behind flag
- Move New Project button to content area header when flag enabled
- Prevent full page reload when clicking active nav item
- Change Upgrade button to premium variant when flag enabled
- Add overleaf-library split test to ProjectListController
- Add library-page class to remove rounded corner on /library
- Add Cypress component tests for DsNavPageSwitcher

Closes #33092

GitOrigin-RevId: 2e348da8307bf944d481b54b3a2bcc2eb319e18e
Tom Wells 3 luni în urmă
părinte
comite
73cc1b571b

+ 1 - 0
services/web/app/src/Features/Project/ProjectListController.mjs

@@ -530,6 +530,7 @@ async function projectListPage(req, res, next) {
   const splitTests = [
     // Split tests that will be made available to the frontend
     'import-docx',
+    'overleaf-library',
   ].filter(Boolean)
 
   await Promise.all(

+ 4 - 1
services/web/frontend/js/features/project-list/components/current-plan-widget/free-plan.tsx

@@ -4,10 +4,12 @@ import OLTooltip from '@/shared/components/ol/ol-tooltip'
 import MaterialIcon from '@/shared/components/material-icon'
 import { FreePlanSubscription } from '../../../../../../types/project/dashboard/subscription'
 import * as eventTracking from '../../../../infrastructure/event-tracking'
+import { isSplitTestEnabled } from '@/utils/splitTestUtils'
 
 type FreePlanProps = Pick<FreePlanSubscription, 'featuresPageURL'>
 
 function FreePlan({ featuresPageURL }: FreePlanProps) {
+  const isLibraryEnabled = isSplitTestEnabled('overleaf-library')
   const { t } = useTranslation()
   const currentPlanLabel = (
     <Trans
@@ -43,7 +45,8 @@ function FreePlan({ featuresPageURL }: FreePlanProps) {
       </OLTooltip>{' '}
       <span className="d-none d-md-inline-block">
         <OLButton
-          variant="primary"
+          variant={isLibraryEnabled ? 'premium' : 'primary'}
+          size={isLibraryEnabled ? 'sm' : undefined}
           href="/user/subscription/plans"
           onClick={handleClick}
         >

+ 44 - 14
services/web/frontend/js/features/project-list/components/project-list-ds-nav.tsx

@@ -23,6 +23,7 @@ import overleafLogo from '@/shared/svgs/overleaf-a-ds-solution-mallard.svg'
 import overleafLogoDark from '@/shared/svgs/overleaf-a-ds-solution-mallard-dark.svg'
 import CookieBanner from '@/shared/components/cookie-banner'
 import { useActiveOverallTheme } from '@/shared/hooks/use-active-overall-theme'
+import { isSplitTestEnabled } from '@/utils/splitTestUtils'
 
 export function ProjectListDsNav() {
   const navbarProps = getMeta('ol-navbar')
@@ -38,27 +39,48 @@ export function ProjectListDsNav() {
     selectedTagId,
   } = useProjectListContext()
   const activeOverallTheme = useActiveOverallTheme()
+  const isLibraryEnabled = isSplitTestEnabled('overleaf-library')
 
   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"
-      />
+      {isLibraryEnabled ? (
+        <>
+          <SearchForm
+            inputValue={searchText}
+            setInputValue={setSearchText}
+            filter={filter}
+            selectedTag={selectedTag}
+            className="overflow-hidden flex-grow-1"
+          />
+          <NewProjectButton
+            id="new-project-button-projects-table"
+            showAddAffiliationWidget
+          />
+        </>
+      ) : (
+        <>
+          <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">
+    <div
+      className={`project-ds-nav-page website-redesign${isLibraryEnabled ? ' library-enabled' : ''}`}
+    >
       <SystemMessages />
       <DefaultNavbar
         {...navbarProps}
@@ -96,8 +118,8 @@ export function ProjectListDsNav() {
                   </div>
                 </div>
                 <div className="project-ds-nav-project-list">
-                  <OLRow className="d-none d-md-block">
-                    <OLCol lg={7}>
+                  <OLRow className="d-none d-md-flex align-items-center">
+                    <OLCol md={isLibraryEnabled ? 8 : undefined} lg={7}>
                       <SearchForm
                         inputValue={searchText}
                         setInputValue={setSearchText}
@@ -105,6 +127,14 @@ export function ProjectListDsNav() {
                         selectedTag={selectedTag}
                       />
                     </OLCol>
+                    {isLibraryEnabled && (
+                      <OLCol className="ms-auto" xs="auto">
+                        <NewProjectButton
+                          id="new-project-button-projects-table"
+                          showAddAffiliationWidget
+                        />
+                      </OLCol>
+                    )}
                   </OLRow>
                   <div className="project-list-sidebar-survey-wrapper d-md-none">
                     {/* Omit the survey card in mobile view for now */}

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

@@ -7,10 +7,15 @@ import { usePersistedResize } from '@/shared/hooks/use-resize'
 import { useScrolled } from '@/features/project-list/components/sidebar/use-scroll'
 import { SurveyWidgetDsNav } from '@/features/project-list/components/survey-widget-ds-nav'
 import { SidebarLowerSection } from '@/shared/components/sidebar/sidebar-lower-section'
+import { isSplitTestEnabled } from '@/utils/splitTestUtils'
+import { DsNavPageSwitcher } from '@/shared/components/sidebar/ds-nav-page-switcher'
+import { useProjectListContext } from '@/features/project-list/context/project-list-context'
 
 function SidebarDsNav() {
   const { t } = useTranslation()
   const { show: showAddAffiliationWidget } = useAddAffiliation()
+  const isLibraryEnabled = isSplitTestEnabled('overleaf-library')
+  const { selectFilter } = useProjectListContext()
   const { mousePos, getHandleProps, getTargetProps } = usePersistedResize({
     name: 'project-sidebar',
   })
@@ -25,14 +30,26 @@ function SidebarDsNav() {
         },
       })}
     >
+      {isLibraryEnabled && (
+        <>
+          <DsNavPageSwitcher
+            activePage="projects"
+            showLogo={false}
+            onProjectsClick={() => selectFilter('all')}
+          />
+          <hr className="ds-nav-page-switcher-divider" />
+        </>
+      )}
       <nav
         className="flex-grow flex-shrink"
         aria-label={t('project_categories_tags')}
       >
-        <NewProjectButton
-          id="new-project-button-sidebar"
-          className={scrolledDown ? 'show-shadow' : undefined}
-        />
+        {!isLibraryEnabled && (
+          <NewProjectButton
+            id="new-project-button-sidebar"
+            className={scrolledDown ? 'show-shadow' : undefined}
+          />
+        )}
         <div
           className="project-list-sidebar-scroll"
           ref={containerRef}

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

@@ -5,6 +5,7 @@ import {
 } from '../../context/project-list-context'
 import TagsList from './tags-list'
 import ProjectsFilterMenu from '../projects-filter-menu'
+import { isSplitTestEnabled } from '@/utils/splitTestUtils'
 
 type SidebarFilterProps = {
   filter: Filter
@@ -29,10 +30,13 @@ export function SidebarFilter({ filter, text }: SidebarFilterProps) {
 
 export default function SidebarFilters() {
   const { t } = useTranslation()
+  const isLibraryEnabled = isSplitTestEnabled('overleaf-library')
 
   return (
     <ul className="list-unstyled project-list-filters">
-      <SidebarFilter filter="all" text={t('all_projects')} />
+      {!isLibraryEnabled && (
+        <SidebarFilter filter="all" text={t('all_projects')} />
+      )}
       <SidebarFilter filter="owned" text={t('your_projects')} />
       <SidebarFilter filter="shared" text={t('shared_with_you')} />
       <SidebarFilter filter="archived" text={t('archived_projects')} />

+ 79 - 0
services/web/frontend/js/shared/components/sidebar/ds-nav-page-switcher.tsx

@@ -0,0 +1,79 @@
+import { useTranslation } from 'react-i18next'
+import { BookBookmark, Folder } from '@phosphor-icons/react'
+import { useActiveOverallTheme } from '@/shared/hooks/use-active-overall-theme'
+import overleafLogo from '@/shared/svgs/overleaf-a-ds-solution-mallard.svg'
+import overleafLogoDark from '@/shared/svgs/overleaf-a-ds-solution-mallard-dark.svg'
+
+type ActivePage = 'library' | 'projects'
+
+export function DsNavPageSwitcher({
+  activePage,
+  showLogo = true,
+  onLibraryClick,
+  onProjectsClick,
+}: {
+  activePage: ActivePage
+  showLogo?: boolean
+  onLibraryClick?: React.MouseEventHandler
+  onProjectsClick?: React.MouseEventHandler
+}) {
+  const { t } = useTranslation()
+  const activeOverallTheme = useActiveOverallTheme()
+
+  return (
+    <>
+      {showLogo && (
+        <div className="ds-nav-page-switcher-logo">
+          <img
+            src={
+              activeOverallTheme === 'dark' ? overleafLogoDark : overleafLogo
+            }
+            alt="Overleaf, A Digital Science Solution"
+            height="59"
+            width="130"
+          />
+        </div>
+      )}
+      <ul
+        className={`list-unstyled ds-nav-page-switcher-items${!showLogo ? ' ds-nav-page-switcher-items--no-logo' : ''}`}
+      >
+        <li>
+          <a
+            href="/library"
+            className={`ds-nav-page-switcher-item${activePage === 'library' ? ' active' : ''}`}
+            aria-current={activePage === 'library' ? 'page' : undefined}
+            onClick={
+              onLibraryClick
+                ? e => {
+                    e.preventDefault()
+                    onLibraryClick(e)
+                  }
+                : undefined
+            }
+          >
+            <BookBookmark size={24} />
+            <span>{t('library')}</span>
+          </a>
+        </li>
+        <li>
+          <a
+            href="/project"
+            className={`ds-nav-page-switcher-item${activePage === 'projects' ? ' active' : ''}`}
+            aria-current={activePage === 'projects' ? 'page' : undefined}
+            onClick={
+              onProjectsClick
+                ? e => {
+                    e.preventDefault()
+                    onProjectsClick(e)
+                  }
+                : undefined
+            }
+          >
+            <Folder size={24} />
+            <span>{t('projects')}</span>
+          </a>
+        </li>
+      </ul>
+    </>
+  )
+}

+ 74 - 0
services/web/frontend/stylesheets/pages/project-list-ds-nav.scss

@@ -103,6 +103,36 @@ body {
     }
   }
 
+  &.library-enabled {
+    .navbar-default .navbar-header .navbar-logo {
+      @include media-breakpoint-up(md) {
+        top: calc(var(--spacing-05) * 2);
+        left: var(--spacing-05);
+      }
+    }
+
+    .project-list-wrapper .project-list-sidebar-wrapper-react {
+      .ds-nav-page-switcher-divider {
+        margin: var(--spacing-05) 0;
+      }
+
+      .project-list-sidebar-scroll {
+        padding-top: 0;
+      }
+    }
+  }
+
+  &.library-page {
+    .project-list-wrapper
+      .project-ds-nav-content-and-messages
+      .project-ds-nav-content {
+      @include media-breakpoint-up(md) {
+        border-top-left-radius: 0;
+        border-top: none;
+      }
+    }
+  }
+
   .navbar-nav > li > .dropdown-toggle::after {
     display: none;
   }
@@ -156,6 +186,50 @@ body {
           var(--spacing-05);
       }
 
+      .ds-nav-page-switcher-logo {
+        padding: 0 var(--spacing-08) var(--spacing-07);
+
+        img {
+          width: 130px;
+          height: auto;
+        }
+      }
+
+      .ds-nav-page-switcher-items {
+        display: flex;
+        flex-direction: column;
+        gap: var(--spacing-02);
+        padding: 0 var(--spacing-05);
+        margin: 0;
+
+        &--no-logo {
+          padding-top: var(--spacing-05);
+        }
+      }
+
+      .ds-nav-page-switcher-item {
+        display: flex;
+        align-items: center;
+        gap: var(--spacing-05);
+        padding: var(--spacing-05);
+        border-radius: var(--border-radius-medium);
+        color: var(--ds-nav-color);
+        font-size: var(--font-size-03);
+        text-decoration: none;
+
+        &:hover {
+          background-color: var(--ds-nav-hover-bg);
+          color: var(--ds-nav-color);
+          text-decoration: none;
+        }
+
+        &.active {
+          background-color: var(--ds-nav-active-bg);
+          color: var(--ds-nav-active-color);
+          font-weight: bold;
+        }
+      }
+
       .ds-nav-sidebar-lower {
         padding: var(--spacing-05) var(--spacing-08) 0 var(--spacing-05);
         border-top: solid 1px transparent;

+ 114 - 0
services/web/test/frontend/shared/components/sidebar/ds-nav-page-switcher.spec.tsx

@@ -0,0 +1,114 @@
+import { DsNavPageSwitcher } from '../../../../../frontend/js/shared/components/sidebar/ds-nav-page-switcher'
+import { SplitTestProvider } from '@/shared/context/split-test-context'
+import { UserSettingsProvider } from '@/shared/context/user-settings-context'
+
+function mountDsNavPageSwitcher(
+  props: React.ComponentProps<typeof DsNavPageSwitcher>
+) {
+  cy.mount(
+    <SplitTestProvider>
+      <UserSettingsProvider>
+        <DsNavPageSwitcher {...props} />
+      </UserSettingsProvider>
+    </SplitTestProvider>
+  )
+}
+
+describe('<DsNavPageSwitcher />', function () {
+  describe('activePage="library"', function () {
+    beforeEach(function () {
+      mountDsNavPageSwitcher({ activePage: 'library' })
+    })
+
+    it('marks the Library link as active', function () {
+      cy.findByRole('link', { name: /library/i }).should('have.class', 'active')
+    })
+
+    it('does not mark the Projects link as active', function () {
+      cy.findByRole('link', { name: /projects/i }).should(
+        'not.have.class',
+        'active'
+      )
+    })
+
+    it('sets aria-current on the Library link', function () {
+      cy.findByRole('link', { name: /library/i }).should(
+        'have.attr',
+        'aria-current',
+        'page'
+      )
+    })
+
+    it('does not set aria-current on the Projects link', function () {
+      cy.findByRole('link', { name: /projects/i }).should(
+        'not.have.attr',
+        'aria-current'
+      )
+    })
+
+    it('renders the logo', function () {
+      cy.get('.ds-nav-page-switcher-logo img').should('exist')
+    })
+  })
+
+  describe('activePage="projects"', function () {
+    beforeEach(function () {
+      mountDsNavPageSwitcher({ activePage: 'projects' })
+    })
+
+    it('marks the Projects link as active', function () {
+      cy.findByRole('link', { name: /projects/i }).should(
+        'have.class',
+        'active'
+      )
+    })
+
+    it('does not mark the Library link as active', function () {
+      cy.findByRole('link', { name: /library/i }).should(
+        'not.have.class',
+        'active'
+      )
+    })
+
+    it('renders the logo', function () {
+      cy.get('.ds-nav-page-switcher-logo img').should('exist')
+    })
+  })
+
+  describe('link hrefs', function () {
+    beforeEach(function () {
+      mountDsNavPageSwitcher({ activePage: 'library' })
+    })
+
+    it('Library link points to /library', function () {
+      cy.findByRole('link', { name: /library/i }).should(
+        'have.attr',
+        'href',
+        '/library'
+      )
+    })
+
+    it('Projects link points to /project', function () {
+      cy.findByRole('link', { name: /projects/i }).should(
+        'have.attr',
+        'href',
+        '/project'
+      )
+    })
+  })
+
+  describe('showLogo={false}', function () {
+    beforeEach(function () {
+      mountDsNavPageSwitcher({ activePage: 'projects', showLogo: false })
+    })
+
+    it('does not render the logo', function () {
+      cy.get('.ds-nav-page-switcher-logo').should('not.exist')
+    })
+
+    it('still renders both nav links', function () {
+      cy.findByRole('link', { name: /library/i }).should('exist')
+      cy.findByRole('link', { name: /projects/i }).should('exist')
+    })
+  })
+})