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_user_tip": "",
"tell_the_project_owner_and_ask_them_to_upgrade": "",
"template": "",
"template_approved_by_publisher": "",
"template_description": "",
"template_title_taken_from_project_title": "",

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -212,6 +212,18 @@
ul.project-list-filters {
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 {
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 {
&.active {
.tag-menu > button {
@ -318,6 +319,11 @@
align-items: center;
word-wrap: anywhere;
.tag-list-icon {
vertical-align: sub;
font-weight: bold;
}
span.name {
padding-left: 0.5em;
line-height: @folders-tag-line-height;
@ -725,12 +731,6 @@
// There is enough space for these on mobile devices (checked DE and EN translations).
white-space: nowrap;
}
.dropdown-header {
padding: 14px 20px;
font-size: 13px;
text-transform: uppercase;
}
}
.projects-types-menu-item {

View file

@ -74,6 +74,10 @@
}
}
.new-project-button.dropdown-toggle::after {
display: none;
}
.project-list-welcome-wrapper {
width: 100%;
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 {
border-radius: 0;
@ -598,6 +609,8 @@
@extend .btn;
@extend .btn-lg;
@extend .btn-primary;
margin-bottom: var(--spacing-07);
}
}
}

View file

@ -96,6 +96,10 @@ button.dropdown-toggle.dropdown-toggle-no-background {
outline: none;
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
@ -199,7 +203,7 @@ button.dropdown-toggle.dropdown-toggle-no-background {
// Dropdown section headers
.dropdown-header {
display: block;
padding: 3px 20px;
padding: 12.5px 15px;
font-size: @font-size-small;
line-height: @line-height-base;
color: @dropdown-header-color;

View file

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