diff --git a/services/web/frontend/extracted-translations.json b/services/web/frontend/extracted-translations.json
index 85d472ed56..54047c26ea 100644
--- a/services/web/frontend/extracted-translations.json
+++ b/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": "",
diff --git a/services/web/frontend/js/features/project-list/components/add-affiliation.tsx b/services/web/frontend/js/features/project-list/components/add-affiliation.tsx
index de4a130bfc..0a60301b1a 100644
--- a/services/web/frontend/js/features/project-list/components/add-affiliation.tsx
+++ b/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 (
-
{t('are_you_affiliated_with_an_institution')}
+
+ {t('are_you_affiliated_with_an_institution')}
+
{t('add_affiliation')}
diff --git a/services/web/frontend/js/features/project-list/components/project-list-ds-nav.tsx b/services/web/frontend/js/features/project-list/components/project-list-ds-nav.tsx
new file mode 100644
index 0000000000..ccc7bf1c69
--- /dev/null
+++ b/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 = (
+
+
+
+
+ )
+
+ return (
+
+
item.text !== 'help')}
+ customLogo="/img/ol-brand/overleaf-a-ds-solution-mallard.svg"
+ showAccountButtons={false}
+ />
+
+
+
+
Notifications and search and stuff
+
+ {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 197662e03f..90b4191674 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
@@ -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() {
)
} else if (hasDsNav) {
- return (
- <>
-
Header with cut-down nav
-
Project list with DS nav and footer
- >
- )
+ return
} else {
return (
diff --git a/services/web/frontend/js/features/project-list/components/sidebar/sidebar-ds-nav.tsx b/services/web/frontend/js/features/project-list/components/sidebar/sidebar-ds-nav.tsx
new file mode 100644
index 0000000000..5c0e7f65ab
--- /dev/null
+++ b/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 (
+
+
+
+
+ {showAddAffiliationWidget &&
}
+
+
+
+
+
+
+ Help / Profile
+
+ DS Nav
+
+
+
+ )
+}
+
+export default SidebarDsNav
diff --git a/services/web/frontend/js/features/project-list/components/sidebar/sidebar-filters.tsx b/services/web/frontend/js/features/project-list/components/sidebar/sidebar-filters.tsx
index 325456f6cb..ec0184396b 100644
--- a/services/web/frontend/js/features/project-list/components/sidebar/sidebar-filters.tsx
+++ b/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() {
+ {withHr && (
+
+
+
+ )}
)
diff --git a/services/web/frontend/js/features/project-list/components/sidebar/tags-list.tsx b/services/web/frontend/js/features/project-list/components/sidebar/tags-list.tsx
index a6cc8e94c9..bb626b87c9 100644
--- a/services/web/frontend/js/features/project-list/components/sidebar/tags-list.tsx
+++ b/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 (
<>
- {t('organize_projects')}
+ {isDsNav ? t('organize_tags') : t('organize_projects')}
- dismissSurvey()} />
+ dismissSurvey()} />
diff --git a/services/web/frontend/js/features/project-list/components/use-is-ds-nav.ts b/services/web/frontend/js/features/project-list/components/use-is-ds-nav.ts
new file mode 100644
index 0000000000..0d44c46a08
--- /dev/null
+++ b/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'
+}
diff --git a/services/web/frontend/js/features/ui/components/bootstrap-5/navbar/default-navbar.tsx b/services/web/frontend/js/features/ui/components/bootstrap-5/navbar/default-navbar.tsx
index 45625be250..d0f2ee8b9c 100644
--- a/services/web/frontend/js/features/ui/components/bootstrap-5/navbar/default-navbar.tsx
+++ b/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 ? (
-
- ) : (
-
- )}
+ {showAccountButtons &&
+ (sessionUser ? (
+
+ ) : (
+
+ ))}
>
diff --git a/services/web/frontend/js/features/ui/components/types/default-navbar-metadata.ts b/services/web/frontend/js/features/ui/components/types/default-navbar-metadata.ts
index 4bba1baf1c..b1d05cc771 100644
--- a/services/web/frontend/js/features/ui/components/types/default-navbar-metadata.ts
+++ b/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
diff --git a/services/web/frontend/stylesheets/bootstrap-5/pages/all.scss b/services/web/frontend/stylesheets/bootstrap-5/pages/all.scss
index 3c87c6f165..4337d95d14 100644
--- a/services/web/frontend/stylesheets/bootstrap-5/pages/all.scss
+++ b/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';
diff --git a/services/web/frontend/stylesheets/bootstrap-5/pages/project-list-ds-nav.scss b/services/web/frontend/stylesheets/bootstrap-5/pages/project-list-ds-nav.scss
new file mode 100644
index 0000000000..defbf29098
--- /dev/null
+++ b/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;
+ }
+ }
+ }
+}
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 909bd5c687..f8fc4a2deb 100644
--- a/services/web/frontend/stylesheets/bootstrap-5/pages/project-list.scss
+++ b/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);
}
diff --git a/services/web/locales/en.json b/services/web/locales/en.json
index bc2ed19165..c808a05dc4 100644
--- a/services/web/locales/en.json
+++ b/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",
diff --git a/services/web/public/img/ol-brand/overleaf-a-ds-solution-mallard.svg b/services/web/public/img/ol-brand/overleaf-a-ds-solution-mallard.svg
new file mode 100644
index 0000000000..a0e156545f
--- /dev/null
+++ b/services/web/public/img/ol-brand/overleaf-a-ds-solution-mallard.svg
@@ -0,0 +1 @@
+
diff --git a/services/web/test/frontend/features/project-list/helpers/render-with-context.tsx b/services/web/test/frontend/features/project-list/helpers/render-with-context.tsx
index ec8f879a63..deda37a2fa 100644
--- a/services/web/test/frontend/features/project-list/helpers/render-with-context.tsx
+++ b/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
}) => (
- {children}
+
+ {children}
+
)