mirror of
https://github.com/overleaf/overleaf.git
synced 2024-11-21 20:47:08 -05:00
Merge pull request #12851 from overleaf/jk-dashboard-filter-visibility
[web] Improve filter visibility on project dashboard GitOrigin-RevId: de7a9f999d6d0164ab3c18c58e305c7c628f946c
This commit is contained in:
parent
d1bbbc1bf1
commit
ff4ac0a803
10 changed files with 306 additions and 26 deletions
|
@ -742,6 +742,11 @@
|
|||
"search_bib_files": "",
|
||||
"search_command_find": "",
|
||||
"search_command_replace": "",
|
||||
"search_in_all_projects": "",
|
||||
"search_in_archived_projects": "",
|
||||
"search_in_shared_projects": "",
|
||||
"search_in_trashed_projects": "",
|
||||
"search_in_your_projects": "",
|
||||
"search_match_case": "",
|
||||
"search_next": "",
|
||||
"search_previous": "",
|
||||
|
|
|
@ -19,6 +19,7 @@ import SearchForm from './search-form'
|
|||
import ProjectsDropdown from './dropdown/projects-dropdown'
|
||||
import SortByDropdown from './dropdown/sort-by-dropdown'
|
||||
import ProjectTools from './table/project-tools/project-tools'
|
||||
import ProjectListTitle from './title/project-list-title'
|
||||
import Sidebar from './sidebar/sidebar'
|
||||
import LoadMore from './load-more'
|
||||
import { useEffect } from 'react'
|
||||
|
@ -44,8 +45,13 @@ function ProjectListPageContent() {
|
|||
searchText,
|
||||
setSearchText,
|
||||
selectedProjects,
|
||||
filter,
|
||||
tags,
|
||||
selectedTagId,
|
||||
} = useProjectListContext()
|
||||
|
||||
const selectedTag = tags.find(tag => tag._id === selectedTagId)
|
||||
|
||||
useEffect(() => {
|
||||
eventTracking.sendMB('loads_v2_dash', {})
|
||||
}, [])
|
||||
|
@ -68,14 +74,12 @@ function ProjectListPageContent() {
|
|||
<UserNotifications />
|
||||
</Col>
|
||||
</Row>
|
||||
<Row>
|
||||
<Col md={7} className="hidden-xs">
|
||||
<SearchForm
|
||||
inputValue={searchText}
|
||||
setInputValue={setSearchText}
|
||||
<div className="project-list-header-row">
|
||||
<ProjectListTitle
|
||||
filter={filter}
|
||||
selectedTag={selectedTag}
|
||||
className="hidden-xs text-truncate"
|
||||
/>
|
||||
</Col>
|
||||
<Col md={5}>
|
||||
<div className="project-tools">
|
||||
<div className="hidden-xs">
|
||||
{selectedProjects.length === 0 ? (
|
||||
|
@ -88,6 +92,15 @@ function ProjectListPageContent() {
|
|||
<CurrentPlanWidget />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<Row className="hidden-xs">
|
||||
<Col md={7}>
|
||||
<SearchForm
|
||||
inputValue={searchText}
|
||||
setInputValue={setSearchText}
|
||||
filter={filter}
|
||||
selectedTag={selectedTag}
|
||||
/>
|
||||
</Col>
|
||||
</Row>
|
||||
<div className="project-list-sidebar-survey-wrapper visible-xs">
|
||||
|
@ -112,6 +125,8 @@ function ProjectListPageContent() {
|
|||
<SearchForm
|
||||
inputValue={searchText}
|
||||
setInputValue={setSearchText}
|
||||
filter={filter}
|
||||
selectedTag={selectedTag}
|
||||
className="overflow-hidden"
|
||||
formGroupProps={{ className: 'mb-0' }}
|
||||
/>
|
||||
|
|
|
@ -9,10 +9,14 @@ import {
|
|||
import Icon from '../../../shared/components/icon'
|
||||
import * as eventTracking from '../../../infrastructure/event-tracking'
|
||||
import classnames from 'classnames'
|
||||
import { Tag } from '../../../../../app/src/Features/Tags/types'
|
||||
import { Filter } from '../context/project-list-context'
|
||||
|
||||
type SearchFormOwnProps = {
|
||||
inputValue: string
|
||||
setInputValue: (input: string) => void
|
||||
filter: Filter
|
||||
selectedTag: Tag | undefined
|
||||
formGroupProps?: FormGroupProps &
|
||||
Omit<React.ComponentProps<'div'>, keyof FormGroupProps>
|
||||
}
|
||||
|
@ -23,11 +27,35 @@ type SearchFormProps = SearchFormOwnProps &
|
|||
function SearchForm({
|
||||
inputValue,
|
||||
setInputValue,
|
||||
filter,
|
||||
selectedTag,
|
||||
formGroupProps,
|
||||
...props
|
||||
}: SearchFormProps) {
|
||||
const { t } = useTranslation()
|
||||
const placeholder = `${t('search_projects')}…`
|
||||
let placeholderMessage = t('search_projects')
|
||||
if (selectedTag) {
|
||||
placeholderMessage = `${t('search')} ${selectedTag.name}`
|
||||
} else {
|
||||
switch (filter) {
|
||||
case 'all':
|
||||
placeholderMessage = t('search_in_all_projects')
|
||||
break
|
||||
case 'owned':
|
||||
placeholderMessage = t('search_in_your_projects')
|
||||
break
|
||||
case 'shared':
|
||||
placeholderMessage = t('search_in_shared_projects')
|
||||
break
|
||||
case 'archived':
|
||||
placeholderMessage = t('search_in_archived_projects')
|
||||
break
|
||||
case 'trashed':
|
||||
placeholderMessage = t('search_in_trashed_projects')
|
||||
break
|
||||
}
|
||||
}
|
||||
const placeholder = `${placeholderMessage}…`
|
||||
const { className: formGroupClassName, ...restFormGroupProps } =
|
||||
formGroupProps || {}
|
||||
|
||||
|
|
|
@ -0,0 +1,45 @@
|
|||
import { useTranslation } from 'react-i18next'
|
||||
import classnames from 'classnames'
|
||||
import { Tag } from '../../../../../../app/src/Features/Tags/types'
|
||||
import { Filter } from '../../context/project-list-context'
|
||||
|
||||
function ProjectListTitle({
|
||||
filter,
|
||||
selectedTag,
|
||||
className,
|
||||
}: {
|
||||
filter: Filter
|
||||
selectedTag: Tag | undefined
|
||||
className?: string
|
||||
}) {
|
||||
const { t } = useTranslation()
|
||||
let message = t('projects')
|
||||
if (selectedTag) {
|
||||
message = `${selectedTag.name}`
|
||||
} else {
|
||||
switch (filter) {
|
||||
case 'all':
|
||||
message = t('all_projects')
|
||||
break
|
||||
case 'owned':
|
||||
message = t('your_projects')
|
||||
break
|
||||
case 'shared':
|
||||
message = t('shared_with_you')
|
||||
break
|
||||
case 'archived':
|
||||
message = t('archived_projects')
|
||||
break
|
||||
case 'trashed':
|
||||
message = t('trashed_projects')
|
||||
break
|
||||
}
|
||||
}
|
||||
return (
|
||||
<div className={classnames('project-list-title', className)}>
|
||||
<span>{message}</span>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default ProjectListTitle
|
|
@ -83,6 +83,23 @@
|
|||
padding: @content-margin-vertical @grid-gutter-width / 2;
|
||||
}
|
||||
|
||||
.project-list-header-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin-bottom: @margin-sm;
|
||||
}
|
||||
|
||||
.project-list-title {
|
||||
min-width: 0;
|
||||
color: @content-secondary;
|
||||
font-family: Lato, sans-serif;
|
||||
font-size: @font-size-large;
|
||||
line-height: 28px;
|
||||
font-weight: bold;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
ul.project-list-filters {
|
||||
margin: @margin-sm @folders-menu-margin;
|
||||
|
||||
|
@ -248,6 +265,11 @@
|
|||
}
|
||||
}
|
||||
|
||||
.project-tools {
|
||||
flex-shrink: 0;
|
||||
margin-left: auto;
|
||||
}
|
||||
|
||||
.project-dash-table {
|
||||
width: 100%;
|
||||
table-layout: fixed;
|
||||
|
@ -533,6 +555,7 @@
|
|||
@media (max-width: @screen-xs-max) {
|
||||
.project-tools {
|
||||
float: left;
|
||||
margin-left: initial;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -945,10 +945,10 @@
|
|||
@sidebar-color: @ol-blue-gray-2;
|
||||
@sidebar-link-color: #fff;
|
||||
@sidebar-active-border-radius: 0;
|
||||
@sidebar-active-bg: @ol-blue-gray-6;
|
||||
@sidebar-active-bg: @ol-blue-gray-4;
|
||||
@sidebar-active-color: #fff;
|
||||
@sidebar-active-font-weight: 700;
|
||||
@sidebar-hover-bg: @ol-blue-gray-4;
|
||||
@sidebar-hover-bg: @ol-blue-gray-6;
|
||||
@sidebar-hover-text-decoration: none;
|
||||
@v2-dash-pane-bg: @ol-blue-gray-4;
|
||||
@v2-dash-pane-link-color: #fff;
|
||||
|
|
|
@ -1288,6 +1288,11 @@
|
|||
"search_bib_files": "Search by author, title, year",
|
||||
"search_command_find": "Find",
|
||||
"search_command_replace": "Replace",
|
||||
"search_in_all_projects": "Search in all projects",
|
||||
"search_in_archived_projects": "Search in archived projects",
|
||||
"search_in_shared_projects": "Search in projects shared with you",
|
||||
"search_in_trashed_projects": "Search in trashed projects",
|
||||
"search_in_your_projects": "Search in your projects",
|
||||
"search_match_case": "Match case",
|
||||
"search_next": "next",
|
||||
"search_previous": "previous",
|
||||
|
|
|
@ -1037,7 +1037,7 @@ describe('<ProjectListRoot />', function () {
|
|||
describe('search', function () {
|
||||
it('shows only projects based on the input', async function () {
|
||||
const input = screen.getAllByRole('textbox', {
|
||||
name: /search projects/i,
|
||||
name: /search in all projects/i,
|
||||
})[0]
|
||||
const value = currentList[0].name
|
||||
|
||||
|
|
|
@ -0,0 +1,64 @@
|
|||
import { render, screen } from '@testing-library/react'
|
||||
import { Filter } from '../../../../../frontend/js/features/project-list/context/project-list-context'
|
||||
import { Tag } from '../../../../../app/src/Features/Tags/types'
|
||||
import ProjectListTitle from '../../../../../frontend/js/features/project-list/components/title/project-list-title'
|
||||
|
||||
describe('<ProjectListTitle />', function () {
|
||||
type TestCase = {
|
||||
filter: Filter
|
||||
selectedTag: Tag | undefined
|
||||
expectedText: string
|
||||
}
|
||||
|
||||
const testCases: Array<TestCase> = [
|
||||
// Filter, without tag
|
||||
{
|
||||
filter: 'all',
|
||||
selectedTag: undefined,
|
||||
expectedText: 'all projects',
|
||||
},
|
||||
{
|
||||
filter: 'owned',
|
||||
selectedTag: undefined,
|
||||
expectedText: 'your projects',
|
||||
},
|
||||
{
|
||||
filter: 'shared',
|
||||
selectedTag: undefined,
|
||||
expectedText: 'shared with you',
|
||||
},
|
||||
{
|
||||
filter: 'archived',
|
||||
selectedTag: undefined,
|
||||
expectedText: 'archived projects',
|
||||
},
|
||||
{
|
||||
filter: 'trashed',
|
||||
selectedTag: undefined,
|
||||
expectedText: 'trashed projects',
|
||||
},
|
||||
// Tags
|
||||
{
|
||||
filter: 'all',
|
||||
selectedTag: { _id: '', user_id: '', name: 'sometag' },
|
||||
expectedText: 'sometag',
|
||||
},
|
||||
{
|
||||
filter: 'shared',
|
||||
selectedTag: { _id: '', user_id: '', name: 'othertag' },
|
||||
expectedText: 'othertag',
|
||||
},
|
||||
]
|
||||
|
||||
for (const testCase of testCases) {
|
||||
it(`renders the title text for filter: ${testCase.filter}, tag: ${testCase?.selectedTag?.name}`, function () {
|
||||
render(
|
||||
<ProjectListTitle
|
||||
filter={testCase.filter}
|
||||
selectedTag={testCase.selectedTag}
|
||||
/>
|
||||
)
|
||||
screen.getByText(new RegExp(testCase.expectedText, 'i'))
|
||||
})
|
||||
}
|
||||
})
|
|
@ -4,6 +4,8 @@ import { expect } from 'chai'
|
|||
import SearchForm from '../../../../../frontend/js/features/project-list/components/search-form'
|
||||
import * as eventTracking from '../../../../../frontend/js/infrastructure/event-tracking'
|
||||
import fetchMock from 'fetch-mock'
|
||||
import { Filter } from '../../../../../frontend/js/features/project-list/context/project-list-context'
|
||||
import { Tag } from '../../../../../app/src/Features/Tags/types'
|
||||
|
||||
describe('Project list search form', function () {
|
||||
let sendMBSpy: sinon.SinonSpy
|
||||
|
@ -19,17 +21,35 @@ describe('Project list search form', function () {
|
|||
})
|
||||
|
||||
it('renders the search form', function () {
|
||||
render(<SearchForm inputValue="" setInputValue={() => {}} />)
|
||||
const filter: Filter = 'all'
|
||||
const selectedTag = undefined
|
||||
render(
|
||||
<SearchForm
|
||||
inputValue=""
|
||||
setInputValue={() => {}}
|
||||
filter={filter}
|
||||
selectedTag={selectedTag}
|
||||
/>
|
||||
)
|
||||
screen.getByRole('search')
|
||||
screen.getByRole('textbox', { name: /search projects/i })
|
||||
screen.getByRole('textbox', { name: /search in all projects/i })
|
||||
})
|
||||
|
||||
it('calls clear text when clear button is clicked', function () {
|
||||
const filter: Filter = 'all'
|
||||
const selectedTag = undefined
|
||||
const setInputValueMock = sinon.stub()
|
||||
render(<SearchForm inputValue="abc" setInputValue={setInputValueMock} />)
|
||||
render(
|
||||
<SearchForm
|
||||
inputValue="abc"
|
||||
setInputValue={setInputValueMock}
|
||||
filter={filter}
|
||||
selectedTag={selectedTag}
|
||||
/>
|
||||
)
|
||||
|
||||
const input = screen.getByRole<HTMLInputElement>('textbox', {
|
||||
name: /search projects/i,
|
||||
name: /search in all projects/i,
|
||||
})
|
||||
|
||||
expect(input.value).to.equal('abc')
|
||||
|
@ -43,8 +63,20 @@ describe('Project list search form', function () {
|
|||
it('changes text', function () {
|
||||
const setInputValueMock = sinon.stub()
|
||||
|
||||
render(<SearchForm inputValue="" setInputValue={setInputValueMock} />)
|
||||
const input = screen.getByRole('textbox', { name: /search projects/i })
|
||||
const filter: Filter = 'all'
|
||||
const selectedTag = undefined
|
||||
|
||||
render(
|
||||
<SearchForm
|
||||
inputValue=""
|
||||
setInputValue={setInputValueMock}
|
||||
filter={filter}
|
||||
selectedTag={selectedTag}
|
||||
/>
|
||||
)
|
||||
const input = screen.getByRole('textbox', {
|
||||
name: /search in all projects/i,
|
||||
})
|
||||
const value = 'abc'
|
||||
|
||||
fireEvent.change(input, { target: { value } })
|
||||
|
@ -56,4 +88,67 @@ describe('Project list search form', function () {
|
|||
})
|
||||
expect(setInputValueMock).to.be.calledWith(value)
|
||||
})
|
||||
|
||||
type TestCase = {
|
||||
filter: Filter
|
||||
selectedTag: Tag | undefined
|
||||
expectedText: string
|
||||
}
|
||||
|
||||
const placeholderTestCases: Array<TestCase> = [
|
||||
// Filter, without tag
|
||||
{
|
||||
filter: 'all',
|
||||
selectedTag: undefined,
|
||||
expectedText: 'search in all projects',
|
||||
},
|
||||
{
|
||||
filter: 'owned',
|
||||
selectedTag: undefined,
|
||||
expectedText: 'search in your projects',
|
||||
},
|
||||
{
|
||||
filter: 'shared',
|
||||
selectedTag: undefined,
|
||||
expectedText: 'search in projects shared with you',
|
||||
},
|
||||
{
|
||||
filter: 'archived',
|
||||
selectedTag: undefined,
|
||||
expectedText: 'search in archived projects',
|
||||
},
|
||||
{
|
||||
filter: 'trashed',
|
||||
selectedTag: undefined,
|
||||
expectedText: 'search in trashed projects',
|
||||
},
|
||||
// Tags
|
||||
{
|
||||
filter: 'all',
|
||||
selectedTag: { _id: '', user_id: '', name: 'sometag' },
|
||||
expectedText: 'search sometag',
|
||||
},
|
||||
{
|
||||
filter: 'shared',
|
||||
selectedTag: { _id: '', user_id: '', name: 'othertag' },
|
||||
expectedText: 'search othertag',
|
||||
},
|
||||
]
|
||||
|
||||
for (const testCase of placeholderTestCases) {
|
||||
it(`renders placeholder text for filter:${testCase.filter}, tag:${testCase?.selectedTag?.name}`, function () {
|
||||
render(
|
||||
<SearchForm
|
||||
inputValue=""
|
||||
setInputValue={() => {}}
|
||||
filter={testCase.filter}
|
||||
selectedTag={testCase.selectedTag}
|
||||
/>
|
||||
)
|
||||
screen.getByRole('search')
|
||||
screen.getByRole('textbox', {
|
||||
name: new RegExp(testCase.expectedText, 'i'),
|
||||
})
|
||||
})
|
||||
}
|
||||
})
|
||||
|
|
Loading…
Reference in a new issue