diff --git a/services/web/app/src/Features/Project/ProjectListController.mjs b/services/web/app/src/Features/Project/ProjectListController.mjs index e3e7f6cfe6..8c173e0f6d 100644 --- a/services/web/app/src/Features/Project/ProjectListController.mjs +++ b/services/web/app/src/Features/Project/ProjectListController.mjs @@ -443,6 +443,15 @@ async function projectListPage(req, res, next) { ) } + // Get the user's assignment for the DS unified nav split test, which + // populates splitTestVariants with a value for the split test name and allows + // Pug to send it to the browser + await SplitTestHandler.promises.getAssignment( + req, + res, + 'sidebar-navigation-ui-update' + ) + res.render('project/list-react', { title: 'your_projects', usersBestSubscription, diff --git a/services/web/app/views/project/list-react.pug b/services/web/app/views/project/list-react.pug index d7c9295e14..5b452118f1 100644 --- a/services/web/app/views/project/list-react.pug +++ b/services/web/app/views/project/list-react.pug @@ -4,7 +4,9 @@ block entrypointVar - entrypoint = 'pages/project-list' block vars - - var suppressNavContentLinks = true + - const suppressNavContentLinks = true + - const suppressNavbar = true + - const suppressFooter = true - bootstrap5PageStatus = 'enabled' // One of 'disabled', 'enabled', and 'queryStringOnly' block append meta @@ -40,5 +42,4 @@ block append meta meta(name="ol-usGovBannerVariant" data-type="string" content=usGovBannerVariant) block content - main.content.content-alt.project-list-react#main-content - #project-list-root + #project-list-root diff --git a/services/web/frontend/js/features/project-list/components/dash-api-error.tsx b/services/web/frontend/js/features/project-list/components/dash-api-error.tsx new file mode 100644 index 0000000000..da37b00baa --- /dev/null +++ b/services/web/frontend/js/features/project-list/components/dash-api-error.tsx @@ -0,0 +1,20 @@ +import { useTranslation } from 'react-i18next' +import OLRow from '@/features/ui/components/ol/ol-row' +import OLCol from '@/features/ui/components/ol/ol-col' +import Notification from '@/shared/components/notification' + +export default function DashApiError() { + const { t } = useTranslation() + return ( + + +
+ +
+
+
+ ) +} diff --git a/services/web/frontend/js/features/project-list/components/project-list-default.tsx b/services/web/frontend/js/features/project-list/components/project-list-default.tsx new file mode 100644 index 0000000000..307e11e490 --- /dev/null +++ b/services/web/frontend/js/features/project-list/components/project-list-default.tsx @@ -0,0 +1,119 @@ +import { useProjectListContext } from '../context/project-list-context' +import { useTranslation } from 'react-i18next' +import CurrentPlanWidget from './current-plan-widget/current-plan-widget' +import NewProjectButton from './new-project-button' +import ProjectListTable from './table/project-list-table' +import SurveyWidget from './survey-widget' +import UserNotifications from './notifications/user-notifications' +import SearchForm from './search-form' +import ProjectsDropdown from './dropdown/projects-dropdown' +import SortByDropdown from './dropdown/sort-by-dropdown' +import ProjectTools from './table/project-tools/project-tools' +import ProjectListTitle from './title/project-list-title' +import Sidebar from './sidebar/sidebar' +import LoadMore from './load-more' +import OLCol from '@/features/ui/components/ol/ol-col' +import OLRow from '@/features/ui/components/ol/ol-row' +import { TableContainer } from '@/features/ui/components/bootstrap-5/table' +import DashApiError from '@/features/project-list/components/dash-api-error' + +export default function ProjectListDefault() { + const { t } = useTranslation() + const { + error, + searchText, + setSearchText, + selectedProjects, + filter, + tags, + selectedTagId, + } = useProjectListContext() + + const selectedTag = tags.find(tag => tag._id === selectedTagId) + + const tableTopArea = ( +
+ + +
+ ) + + return ( + <> + +
+ {error ? : ''} + + + + + +
+ +
+
+ {selectedProjects.length === 0 ? ( + + ) : ( + + )} +
+
+ +
+
+
+ + + + + +
+ +
+
+
+ + +
+
+ + + + {tableTopArea} + + + + + + + + + +
+ + ) +} diff --git a/services/web/frontend/js/features/project-list/components/project-list-root.tsx b/services/web/frontend/js/features/project-list/components/project-list-root.tsx index 7eb287c362..197662e03f 100644 --- a/services/web/frontend/js/features/project-list/components/project-list-root.tsx +++ b/services/web/frontend/js/features/project-list/components/project-list-root.tsx @@ -1,34 +1,25 @@ +import { ReactNode, useEffect } from 'react' import { ProjectListProvider, useProjectListContext, } from '../context/project-list-context' +import { + SplitTestProvider, + useSplitTestContext, +} from '@/shared/context/split-test-context' import { ColorPickerProvider } from '../context/color-picker-context' import * as eventTracking from '../../../infrastructure/event-tracking' import { useTranslation } from 'react-i18next' import useWaitForI18n from '../../../shared/hooks/use-wait-for-i18n' -import CurrentPlanWidget from './current-plan-widget/current-plan-widget' -import NewProjectButton from './new-project-button' -import ProjectListTable from './table/project-list-table' -import SurveyWidget from './survey-widget' -import WelcomeMessage from './welcome-message' import LoadingBranded from '../../../shared/components/loading-branded' import SystemMessages from '../../../shared/components/system-messages' -import UserNotifications from './notifications/user-notifications' -import SearchForm from './search-form' -import ProjectsDropdown from './dropdown/projects-dropdown' -import SortByDropdown from './dropdown/sort-by-dropdown' -import ProjectTools from './table/project-tools/project-tools' -import ProjectListTitle from './title/project-list-title' -import Sidebar from './sidebar/sidebar' -import LoadMore from './load-more' -import { useEffect } from 'react' import withErrorBoundary from '../../../infrastructure/error-boundary' -import { GenericErrorBoundaryFallback } from '../../../shared/components/generic-error-boundary-fallback' -import { SplitTestProvider } from '@/shared/context/split-test-context' -import OLCol from '@/features/ui/components/ol/ol-col' -import Notification from '@/shared/components/notification' -import OLRow from '@/features/ui/components/ol/ol-row' -import { TableContainer } from '@/features/ui/components/bootstrap-5/table' +import { GenericErrorBoundaryFallback } from '@/shared/components/generic-error-boundary-fallback' +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 WelcomePageContent from '@/features/project-list/components/welcome-page-content' +import ProjectListDefault from '@/features/project-list/components/project-list-default' function ProjectListRoot() { const { isReady } = useWaitForI18n() @@ -52,21 +43,38 @@ export function ProjectListRootInner() { ) } -function ProjectListPageContent() { - const { - totalProjectsCount, - error, - isLoading, - loadProgress, - searchText, - setSearchText, - selectedProjects, - filter, - tags, - selectedTagId, - } = useProjectListContext() +function DefaultNavbarAndFooter({ children }: { children: ReactNode }) { + const navbarProps = getMeta('ol-navbar') + const footerProps = getMeta('ol-footer') - const selectedTag = tags.find(tag => tag._id === selectedTagId) + return ( + <> + +
+ {children} +
+ + + ) +} + +function DefaultPageContentWrapper({ children }: { children: ReactNode }) { + return ( + + +
{children}
+
+ ) +} + +function ProjectListPageContent() { + const { totalProjectsCount, isLoading, loadProgress } = + useProjectListContext() + + const { splitTestVariants } = useSplitTestContext() useEffect(() => { eventTracking.sendMB('loads_v2_dash', {}) @@ -74,140 +82,45 @@ function ProjectListPageContent() { const { t } = useTranslation() - const tableTopArea = ( -
-
- - -
-
- ) + const hasDsNav = + splitTestVariants['sidebar-navigation-ui-update'] === 'active' - return isLoading ? ( -
+ if (isLoading) { + const loadingComponent = ( -
- ) : ( - <> - + ) -
- {totalProjectsCount > 0 ? ( - <> - -
- {error ? : ''} - - - - - -
- -
-
- {selectedProjects.length === 0 ? ( - - ) : ( - - )} -
-
- -
-
-
- - - - - -
- -
-
-
- - -
-
- - - - {tableTopArea} - - - - - - - - - -
- - ) : ( -
- {error ? : ''} - - - - - - - - - - -
- )} -
- - ) -} + if (hasDsNav) { + return loadingComponent + } else { + return ( + +
{loadingComponent}
+
+ ) + } + } -function DashApiError() { - const { t } = useTranslation() - return ( - - -
- -
-
-
- ) + if (totalProjectsCount === 0) { + return ( + + + + ) + } else if (hasDsNav) { + return ( + <> +
Header with cut-down nav
+
Project list with DS nav and footer
+ + ) + } else { + return ( + + + + ) + } } export default withErrorBoundary(ProjectListRoot, GenericErrorBoundaryFallback) diff --git a/services/web/frontend/js/features/project-list/components/welcome-page-content.tsx b/services/web/frontend/js/features/project-list/components/welcome-page-content.tsx new file mode 100644 index 0000000000..083598c213 --- /dev/null +++ b/services/web/frontend/js/features/project-list/components/welcome-page-content.tsx @@ -0,0 +1,30 @@ +import { useProjectListContext } from '@/features/project-list/context/project-list-context' +import DashApiError from '@/features/project-list/components/dash-api-error' +import OLRow from '@/features/ui/components/ol/ol-row' +import OLCol from '@/features/ui/components/ol/ol-col' +import UserNotifications from '@/features/project-list/components/notifications/user-notifications' +import WelcomeMessage from '@/features/project-list/components/welcome-message' + +export default function WelcomePageContent() { + const { error } = useProjectListContext() + + return ( +
+ {error ? : ''} + + + + + + + + + + +
+ ) +} diff --git a/services/web/frontend/js/pages/project-list.jsx b/services/web/frontend/js/pages/project-list.tsx similarity index 92% rename from services/web/frontend/js/pages/project-list.jsx rename to services/web/frontend/js/pages/project-list.tsx index 68f4564c46..3886db545e 100644 --- a/services/web/frontend/js/pages/project-list.jsx +++ b/services/web/frontend/js/pages/project-list.tsx @@ -5,7 +5,6 @@ import './../i18n' import '../features/event-tracking' import '../features/cookie-banner' import '../features/link-helpers/slow-link' -import '../features/header-footer-react' import ReactDOM from 'react-dom' import ProjectListRoot from '../features/project-list/components/project-list-root' diff --git a/services/web/frontend/stylesheets/bootstrap-5/pages/project-list.scss b/services/web/frontend/stylesheets/bootstrap-5/pages/project-list.scss index ca949ef486..fcc372d0b5 100644 --- a/services/web/frontend/stylesheets/bootstrap-5/pages/project-list.scss +++ b/services/web/frontend/stylesheets/bootstrap-5/pages/project-list.scss @@ -18,7 +18,7 @@ } .project-list-react { - body > &.content { + #project-list-root > &.content { padding-top: $header-height; padding-bottom: 0; min-height: calc(100vh - #{$header-height}); diff --git a/services/web/test/frontend/features/project-list/components/new-project-button.test.tsx b/services/web/test/frontend/features/project-list/components/new-project-button.test.tsx index 93c7a68e3a..d3c465434e 100644 --- a/services/web/test/frontend/features/project-list/components/new-project-button.test.tsx +++ b/services/web/test/frontend/features/project-list/components/new-project-button.test.tsx @@ -4,8 +4,20 @@ import fetchMock from 'fetch-mock' import NewProjectButton from '../../../../../frontend/js/features/project-list/components/new-project-button' import { renderWithProjectListContext } from '../helpers/render-with-context' import getMeta from '@/utils/meta' +import * as bootstrapUtils from '@/features/utils/bootstrap-5' +import sinon, { type SinonStub } from 'sinon' describe('', function () { + let isBootstrap5Stub: SinonStub + + before(function () { + isBootstrap5Stub = sinon.stub(bootstrapUtils, 'isBootstrap5').returns(true) + }) + + after(function () { + isBootstrap5Stub.restore() + }) + beforeEach(function () { fetchMock.reset() }) diff --git a/services/web/test/frontend/features/project-list/components/project-list-root.test.tsx b/services/web/test/frontend/features/project-list/components/project-list-root.test.tsx index 7139aa61c3..b01e86b426 100644 --- a/services/web/test/frontend/features/project-list/components/project-list-root.test.tsx +++ b/services/web/test/frontend/features/project-list/components/project-list-root.test.tsx @@ -54,6 +54,13 @@ describe('', function () { // we need a blank user here since its used in checking if we should display certain ads window.metaAttributesCache.set('ol-user', {}) window.metaAttributesCache.set('ol-user_id', userId) + window.metaAttributesCache.set('ol-footer', { + translatedLanguages: { en: 'English' }, + subdomainLang: { en: { lngCode: 'en', url: 'overleaf.com' } }, + }) + window.metaAttributesCache.set('ol-navbar', { + items: [], + }) assignStub = sinon.stub() this.locationStub = sinon.stub(useLocationModule, 'useLocation').returns({ assign: assignStub, diff --git a/services/web/test/frontend/features/project-list/components/table/project-tools/project-tools-rename.test.tsx b/services/web/test/frontend/features/project-list/components/table/project-tools/project-tools-rename.test.tsx index 276c98fdee..44bc7e0fed 100644 --- a/services/web/test/frontend/features/project-list/components/table/project-tools/project-tools-rename.test.tsx +++ b/services/web/test/frontend/features/project-list/components/table/project-tools/project-tools-rename.test.tsx @@ -4,6 +4,8 @@ import moment from 'moment/moment' import fetchMock from 'fetch-mock' import { Project } from '../../../../../../../types/project/dashboard/api' import { ProjectListRootInner } from '@/features/project-list/components/project-list-root' +import * as bootstrapUtils from '@/features/utils/bootstrap-5' +import sinon, { type SinonStub } from 'sinon' const users = { picard: { @@ -46,12 +48,26 @@ const projects: Project[] = [ ] describe('', function () { + let isBootstrap5Stub: SinonStub + + before(function () { + isBootstrap5Stub = sinon.stub(bootstrapUtils, 'isBootstrap5').returns(true) + }) + + after(function () { + isBootstrap5Stub.restore() + }) beforeEach(function () { window.metaAttributesCache.set('ol-user', {}) window.metaAttributesCache.set('ol-prefetchedProjectsBlob', { projects, totalSize: 100, }) + + window.metaAttributesCache.set('ol-footer', { + translatedLanguages: { en: 'English' }, + subdomainLang: { en: { lngCode: 'en', url: 'overleaf.com' } }, + }) fetchMock.get('/system/messages', []) })