Merge pull request #19557 from overleaf/rd-left-menu-dropdown

[web] Migrate the left menu on the project dashboard part #2

GitOrigin-RevId: 55b213352dd850650668bb4e507e11607aee0107
This commit is contained in:
Rebeka Dekany 2024-08-12 16:45:27 +02:00 committed by Copybot
parent c636089939
commit 92a2debc9a
10 changed files with 284 additions and 134 deletions

View file

@ -1383,6 +1383,7 @@
"tc_switch_guests_tip": "", "tc_switch_guests_tip": "",
"tc_switch_user_tip": "", "tc_switch_user_tip": "",
"tell_the_project_owner_and_ask_them_to_upgrade": "", "tell_the_project_owner_and_ask_them_to_upgrade": "",
"template": "",
"template_approved_by_publisher": "", "template_approved_by_publisher": "",
"template_description": "", "template_description": "",
"template_title_taken_from_project_title": "", "template_title_taken_from_project_title": "",

View file

@ -1,5 +1,8 @@
import { type JSXElementConstructor, useCallback, useState } from 'react' import { type JSXElementConstructor, useCallback, useState } from 'react'
import { Dropdown, MenuItem } from 'react-bootstrap' import {
Dropdown as BS3Dropdown,
MenuItem as BS3MenuItem,
} from 'react-bootstrap'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
import ControlledDropdown from '../../../shared/components/controlled-dropdown' import ControlledDropdown from '../../../shared/components/controlled-dropdown'
import getMeta from '../../../utils/meta' import getMeta from '../../../utils/meta'
@ -10,6 +13,15 @@ import AddAffiliation, { useAddAffiliation } from './add-affiliation'
import { Nullable } from '../../../../../types/utils' import { Nullable } from '../../../../../types/utils'
import { sendMB } from '../../../infrastructure/event-tracking' import { sendMB } from '../../../infrastructure/event-tracking'
import importOverleafModules from '../../../../macros/import-overleaf-module.macro' import importOverleafModules from '../../../../macros/import-overleaf-module.macro'
import BootstrapVersionSwitcher from '@/features/ui/components/bootstrap-5/bootstrap-version-switcher'
import {
Dropdown,
DropdownDivider,
DropdownHeader,
DropdownItem,
DropdownMenu,
DropdownToggle,
} from '@/features/ui/components/bootstrap-5/dropdown-menu'
type SendTrackingEvent = { type SendTrackingEvent = {
dropdownMenu: string dropdownMenu: string
@ -30,7 +42,6 @@ type NewProjectButtonProps = {
id: string id: string
buttonText?: string buttonText?: string
className?: string className?: string
menuClassName?: string
trackingKey?: string trackingKey?: string
showAddAffiliationWidget?: boolean showAddAffiliationWidget?: boolean
} }
@ -39,7 +50,6 @@ function NewProjectButton({
id, id,
buttonText, buttonText,
className, className,
menuClassName,
trackingKey, trackingKey,
showAddAffiliationWidget, showAddAffiliationWidget,
}: NewProjectButtonProps) { }: NewProjectButtonProps) {
@ -88,7 +98,7 @@ function NewProjectButton({
const handleModalMenuClick = useCallback( const handleModalMenuClick = useCallback(
( (
e: React.MouseEvent<Record<string, unknown>>, e: React.MouseEvent,
{ modalVariant, dropdownMenuEvent }: ModalMenuClickOptions { modalVariant, dropdownMenuEvent }: ModalMenuClickOptions
) => { ) => {
// avoid invoking the "onClick" callback on the main dropdown button // avoid invoking the "onClick" callback on the main dropdown button
@ -105,10 +115,7 @@ function NewProjectButton({
) )
const handlePortalTemplateClick = useCallback( const handlePortalTemplateClick = useCallback(
( (e: React.MouseEvent, institutionTemplateName: string) => {
e: React.MouseEvent<Record<string, unknown>>,
institutionTemplateName: string
) => {
// avoid invoking the "onClick" callback on the main dropdown button // avoid invoking the "onClick" callback on the main dropdown button
e.stopPropagation() e.stopPropagation()
@ -122,10 +129,7 @@ function NewProjectButton({
) )
const handleStaticTemplateClick = useCallback( const handleStaticTemplateClick = useCallback(
( (e: React.MouseEvent, templateTrackingKey: string) => {
e: React.MouseEvent<Record<string, unknown>>,
templateTrackingKey: string
) => {
// avoid invoking the "onClick" callback on the main dropdown button // avoid invoking the "onClick" callback on the main dropdown button
e.stopPropagation() e.stopPropagation()
@ -142,26 +146,24 @@ function NewProjectButton({
) )
const ImportProjectFromGithubMenu: JSXElementConstructor<{ const ImportProjectFromGithubMenu: JSXElementConstructor<{
onClick: (e: React.MouseEvent<Record<string, unknown>>) => void onClick: (e: React.MouseEvent) => void
}> = importProjectFromGithubMenu?.import.default }> = importProjectFromGithubMenu?.import.default
return ( return (
<> <>
<ControlledDropdown <BootstrapVersionSwitcher
id={id} bs3={
className={className} <ControlledDropdown id={id} className={className}>
onMainButtonClick={handleMainButtonClick} <BS3Dropdown.Toggle
>
<Dropdown.Toggle
noCaret noCaret
className="new-project-button" className="new-project-button"
bsStyle="primary" bsStyle="primary"
> >
{buttonText || t('new_project')} {buttonText || t('new_project')}
</Dropdown.Toggle> </BS3Dropdown.Toggle>
<Dropdown.Menu className={menuClassName}> <BS3Dropdown.Menu>
<MenuItem <BS3MenuItem
onClick={e => onClick={(e: React.MouseEvent) =>
handleModalMenuClick(e, { handleModalMenuClick(e, {
modalVariant: 'blank_project', modalVariant: 'blank_project',
dropdownMenuEvent: 'blank-project', dropdownMenuEvent: 'blank-project',
@ -169,9 +171,9 @@ function NewProjectButton({
} }
> >
{t('blank_project')} {t('blank_project')}
</MenuItem> </BS3MenuItem>
<MenuItem <BS3MenuItem
onClick={e => onClick={(e: React.MouseEvent) =>
handleModalMenuClick(e, { handleModalMenuClick(e, {
modalVariant: 'example_project', modalVariant: 'example_project',
dropdownMenuEvent: 'example-project', dropdownMenuEvent: 'example-project',
@ -179,9 +181,9 @@ function NewProjectButton({
} }
> >
{t('example_project')} {t('example_project')}
</MenuItem> </BS3MenuItem>
<MenuItem <BS3MenuItem
onClick={e => onClick={(e: React.MouseEvent) =>
handleModalMenuClick(e, { handleModalMenuClick(e, {
modalVariant: 'upload_project', modalVariant: 'upload_project',
dropdownMenuEvent: 'upload-project', dropdownMenuEvent: 'upload-project',
@ -189,7 +191,7 @@ function NewProjectButton({
} }
> >
{t('upload_project')} {t('upload_project')}
</MenuItem> </BS3MenuItem>
{ImportProjectFromGithubMenu && ( {ImportProjectFromGithubMenu && (
<ImportProjectFromGithubMenu <ImportProjectFromGithubMenu
onClick={e => onClick={e =>
@ -202,53 +204,172 @@ function NewProjectButton({
)} )}
{portalTemplates.length > 0 ? ( {portalTemplates.length > 0 ? (
<> <>
<MenuItem divider /> <BS3MenuItem divider />
<MenuItem header> <div aria-hidden="true" className="dropdown-header">
{`${t('institution')} ${t('templates')}`} {`${t('institution')} ${t('templates')}`}
</MenuItem> </div>
{portalTemplates.map((portalTemplate, index) => ( {portalTemplates.map((portalTemplate, index) => (
<MenuItem <BS3MenuItem
key={`portal-template-${index}`} key={`portal-template-${index}`}
href={`${portalTemplate.url}#templates`} href={`${portalTemplate.url}#templates`}
onClick={e => onClick={(e: React.MouseEvent) =>
handlePortalTemplateClick(e, portalTemplate.name) handlePortalTemplateClick(e, portalTemplate.name)
} }
> >
{portalTemplate.name} {portalTemplate.name}
</MenuItem> </BS3MenuItem>
))} ))}
</> </>
) : null} ) : null}
{templateLinks && templateLinks.length > 0 && ( {templateLinks && templateLinks.length > 0 && (
<> <>
<MenuItem divider /> <BS3MenuItem divider />
<MenuItem header>{t('templates')}</MenuItem> <div aria-hidden="true" className="dropdown-header">
{t('templates')}
</div>
</> </>
)} )}
{templateLinks?.map((templateLink, index) => ( {templateLinks?.map((templateLink, index) => (
<MenuItem <BS3MenuItem
key={`new-project-button-template-${index}`} key={`new-project-button-template-${index}`}
href={templateLink.url} href={templateLink.url}
onClick={e => onClick={(e: React.MouseEvent) =>
handleStaticTemplateClick(e, templateLink.trackingKey) handleStaticTemplateClick(e, templateLink.trackingKey)
} }
> >
{templateLink.name === 'view_all' {templateLink.name === 'view_all'
? t('view_all') ? t('view_all')
: templateLink.name} : templateLink.name}
</MenuItem> </BS3MenuItem>
))} ))}
{showAddAffiliationWidget && enableAddAffiliationWidget ? ( {showAddAffiliationWidget && enableAddAffiliationWidget ? (
<> <>
<MenuItem divider /> <BS3MenuItem divider />
<li className="add-affiliation-mobile-wrapper"> <li className="add-affiliation-mobile-wrapper">
<AddAffiliation className="is-mobile" /> <AddAffiliation className="is-mobile" />
</li> </li>
</> </>
) : null} ) : null}
</Dropdown.Menu> </BS3Dropdown.Menu>
</ControlledDropdown> </ControlledDropdown>
}
bs5={
<Dropdown className={className} onSelect={handleMainButtonClick}>
<DropdownToggle
id={id}
className="new-project-button"
variant="primary"
>
{buttonText || t('new_project')}
</DropdownToggle>
<DropdownMenu>
<li role="none">
<DropdownItem
onClick={e =>
handleModalMenuClick(e, {
modalVariant: 'blank_project',
dropdownMenuEvent: 'blank-project',
})
}
>
{t('blank_project')}
</DropdownItem>
</li>
<li role="none">
<DropdownItem
onClick={e =>
handleModalMenuClick(e, {
modalVariant: 'example_project',
dropdownMenuEvent: 'example-project',
})
}
>
{t('example_project')}
</DropdownItem>
</li>
<li role="none">
<DropdownItem
onClick={e =>
handleModalMenuClick(e, {
modalVariant: 'upload_project',
dropdownMenuEvent: 'upload-project',
})
}
>
{t('upload_project')}
</DropdownItem>
</li>
<li role="none">
{ImportProjectFromGithubMenu && (
<ImportProjectFromGithubMenu
onClick={e =>
handleModalMenuClick(e, {
modalVariant: 'import_from_github',
dropdownMenuEvent: 'import-from-github',
})
}
/>
)}
</li>
{portalTemplates.length > 0 ? (
<>
<DropdownDivider />
<DropdownHeader aria-hidden="true">
{`${t('institution')} ${t('templates')}`}
</DropdownHeader>
{portalTemplates.map((portalTemplate, index) => (
<li role="none" key={`portal-template-${index}`}>
<DropdownItem
key={`portal-template-${index}`}
href={`${portalTemplate.url}#templates`}
onClick={e =>
handlePortalTemplateClick(e, portalTemplate.name)
}
aria-label={`${portalTemplate.name} ${t('template')}`}
>
{portalTemplate.name}
</DropdownItem>
</li>
))}
</>
) : null}
{templateLinks && templateLinks.length > 0 && (
<>
<DropdownDivider />
<DropdownHeader aria-hidden="true">
{t('templates')}
</DropdownHeader>
</>
)}
{templateLinks?.map((templateLink, index) => (
<li role="none" key={`new-project-button-template-${index}`}>
<DropdownItem
href={templateLink.url}
onClick={e =>
handleStaticTemplateClick(e, templateLink.trackingKey)
}
aria-label={`${templateLink.name} ${t('template')}`}
>
{templateLink.name === 'view_all'
? t('view_all')
: templateLink.name}
</DropdownItem>
</li>
))}
{showAddAffiliationWidget && enableAddAffiliationWidget ? (
<>
<DropdownDivider />
<li className="add-affiliation-mobile-wrapper">
<AddAffiliation className="is-mobile" />
</li>
</>
) : null}
</DropdownMenu>
</Dropdown>
}
/>
<NewProjectButtonModal modal={modal} onHide={() => setModal(null)} /> <NewProjectButtonModal modal={modal} onHide={() => setModal(null)} />
</> </>
) )

View file

@ -101,8 +101,12 @@ function ModalContentNewProjectForm({ onCancel, template = 'none' }: Props) {
variant="primary" variant="primary"
onClick={createNewProject} onClick={createNewProject}
disabled={projectName === '' || isLoading} disabled={projectName === '' || isLoading}
isLoading={isLoading}
bs3Props={{
loading: isLoading ? `${t('creating')}` : t('create'),
}}
> >
{isLoading ? `${t('creating')}` : t('create')} {t('create')}
</OLButton> </OLButton>
</OLModalFooter> </OLModalFooter>
</> </>

View file

@ -36,8 +36,12 @@ export default function TagsList() {
return ( return (
<> <>
<li role="separator" className="separator"> <li
<h2>{t('organize_projects')}</h2> className="dropdown-header"
aria-hidden="true"
data-testid="organize-projects"
>
{t('organize_projects')}
</li> </li>
<li className="tag"> <li className="tag">
<button type="button" className="tag-name" onClick={openCreateTagModal}> <button type="button" className="tag-name" onClick={openCreateTagModal}>

View file

@ -267,7 +267,9 @@ function WelcomeMessageCreateNewProjectDropdown({
{(portalTemplates?.length ?? 0) > 0 ? ( {(portalTemplates?.length ?? 0) > 0 ? (
<> <>
<DropdownDivider /> <DropdownDivider />
<DropdownHeader>{t('institution_templates')}</DropdownHeader> <DropdownHeader aria-hidden="true">
{t('institution_templates')}
</DropdownHeader>
{portalTemplates?.map((portalTemplate, index) => ( {portalTemplates?.map((portalTemplate, index) => (
<DropdownItem <DropdownItem
key={`portal-template-${index}`} key={`portal-template-${index}`}

View file

@ -36,6 +36,7 @@ export type DropdownItemProps = PropsWithChildren<{
export type DropdownToggleProps = PropsWithChildren<{ export type DropdownToggleProps = PropsWithChildren<{
bsPrefix?: string bsPrefix?: string
className?: string
disabled?: boolean disabled?: boolean
split?: boolean split?: boolean
id?: string // necessary for assistive technologies id?: string // necessary for assistive technologies

View file

@ -212,6 +212,18 @@
ul.project-list-filters { ul.project-list-filters {
margin: @margin-sm @folders-menu-margin; margin: @margin-sm @folders-menu-margin;
.dropdown-header {
margin-top: @folders-title-margin-top;
margin-bottom: @folders-title-margin-bottom;
font-size: @folders-title-font-size;
color: @folders-title-color;
text-transform: @folders-title-text-transform;
padding: 12.5px 16px;
font-weight: @folders-title-font-weight;
font-family: @font-family-sans-serif;
text-transform: uppercase;
}
.subdued { .subdued {
color: @gray-light; color: @gray-light;
} }
@ -263,17 +275,6 @@
} }
} }
h2 {
margin-top: @folders-title-margin-top;
margin-bottom: @folders-title-margin-bottom;
font-size: @folders-title-font-size;
color: @folders-title-color;
text-transform: @folders-title-text-transform;
padding: @folders-title-padding;
font-weight: @folders-title-font-weight;
font-family: @font-family-sans-serif;
}
> li.tag { > li.tag {
&.active { &.active {
.tag-menu > button { .tag-menu > button {
@ -318,6 +319,11 @@
align-items: center; align-items: center;
word-wrap: anywhere; word-wrap: anywhere;
.tag-list-icon {
vertical-align: sub;
font-weight: bold;
}
span.name { span.name {
padding-left: 0.5em; padding-left: 0.5em;
line-height: @folders-tag-line-height; line-height: @folders-tag-line-height;
@ -725,12 +731,6 @@
// There is enough space for these on mobile devices (checked DE and EN translations). // There is enough space for these on mobile devices (checked DE and EN translations).
white-space: nowrap; white-space: nowrap;
} }
.dropdown-header {
padding: 14px 20px;
font-size: 13px;
text-transform: uppercase;
}
} }
.projects-types-menu-item { .projects-types-menu-item {

View file

@ -74,6 +74,10 @@
} }
} }
.new-project-button.dropdown-toggle::after {
display: none;
}
.project-list-welcome-wrapper { .project-list-welcome-wrapper {
width: 100%; width: 100%;
padding-bottom: var(--spacing-08); padding-bottom: var(--spacing-08);
@ -199,6 +203,13 @@
} }
} }
.dropdown-header {
@include body-sm;
padding: var(--spacing-05) var(--spacing-06);
text-transform: uppercase;
}
> li.active { > li.active {
border-radius: 0; border-radius: 0;
@ -598,6 +609,8 @@
@extend .btn; @extend .btn;
@extend .btn-lg; @extend .btn-lg;
@extend .btn-primary; @extend .btn-primary;
margin-bottom: var(--spacing-07);
} }
} }
} }

View file

@ -96,6 +96,10 @@ button.dropdown-toggle.dropdown-toggle-no-background {
outline: none; outline: none;
width: 100%; width: 100%;
} }
.dropdown-header {
color: @dropdown-header-color;
}
} }
// This removes positioning, display and z-index, which is used just to style the menu in situations where something // This removes positioning, display and z-index, which is used just to style the menu in situations where something
@ -199,7 +203,7 @@ button.dropdown-toggle.dropdown-toggle-no-background {
// Dropdown section headers // Dropdown section headers
.dropdown-header { .dropdown-header {
display: block; display: block;
padding: 3px 20px; padding: 12.5px 15px;
font-size: @font-size-small; font-size: @font-size-small;
line-height: @line-height-base; line-height: @line-height-base;
color: @dropdown-header-color; color: @dropdown-header-color;

View file

@ -41,9 +41,9 @@ describe('<TagsList />', function () {
}) })
it('displays the tags list', function () { it('displays the tags list', function () {
screen.getByRole('heading', { const header = screen.getByTestId('organize-projects')
name: 'Organize Projects', expect(header.textContent).to.equal('Organize Projects')
})
screen.getByRole('button', { screen.getByRole('button', {
name: 'New Tag', name: 'New Tag',
}) })