[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
This commit is contained in:
Antoine Clausse
2024-12-11 14:55:36 +01:00
committed by Copybot
parent ff533ae06f
commit 1c33c32407
17 changed files with 488 additions and 75 deletions

View File

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

View File

@@ -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>

View File

@@ -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>
)
}

View File

@@ -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>

View File

@@ -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

View File

@@ -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>
)

View File

@@ -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>

View File

@@ -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>

View File

@@ -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'
}

View File

@@ -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>
</>

View File

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

View File

@@ -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';

View File

@@ -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;
}
}
}
}

View File

@@ -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);
}

View File

@@ -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",

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 12 KiB

View File

@@ -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>
)