mirror of
https://github.com/overleaf/overleaf.git
synced 2024-11-29 03:43:39 -05:00
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:
parent
c636089939
commit
92a2debc9a
10 changed files with 284 additions and 134 deletions
|
@ -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": "",
|
||||
|
|
|
@ -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)} />
|
||||
</>
|
||||
)
|
||||
|
|
|
@ -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>
|
||||
</>
|
||||
|
|
|
@ -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}>
|
||||
|
|
|
@ -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}`}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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',
|
||||
})
|
||||
|
|
Loading…
Reference in a new issue