From ff4ac0a803b248228f970dd9967e0e1f2da8d3c4 Mon Sep 17 00:00:00 2001 From: June Kelly Date: Tue, 16 May 2023 09:17:36 +0100 Subject: [PATCH] Merge pull request #12851 from overleaf/jk-dashboard-filter-visibility [web] Improve filter visibility on project dashboard GitOrigin-RevId: de7a9f999d6d0164ab3c18c58e305c7c628f946c --- .../web/frontend/extracted-translations.json | 5 + .../components/project-list-root.tsx | 47 +++++--- .../project-list/components/search-form.tsx | 30 ++++- .../components/title/project-list-title.tsx | 45 ++++++++ .../stylesheets/app/project-list-react.less | 23 ++++ .../frontend/stylesheets/core/variables.less | 4 +- services/web/locales/en.json | 5 + .../components/project-list-root.test.tsx | 2 +- .../components/project-list-title.tsx | 64 +++++++++++ .../components/project-search.test.tsx | 107 +++++++++++++++++- 10 files changed, 306 insertions(+), 26 deletions(-) create mode 100644 services/web/frontend/js/features/project-list/components/title/project-list-title.tsx create mode 100644 services/web/test/frontend/features/project-list/components/project-list-title.tsx diff --git a/services/web/frontend/extracted-translations.json b/services/web/frontend/extracted-translations.json index efefdacd91..ecc519ad6f 100644 --- a/services/web/frontend/extracted-translations.json +++ b/services/web/frontend/extracted-translations.json @@ -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": "", diff --git a/services/web/frontend/js/features/project-list/components/project-list-root.tsx b/services/web/frontend/js/features/project-list/components/project-list-root.tsx index 2640df6646..4d6166e59c 100644 --- a/services/web/frontend/js/features/project-list/components/project-list-root.tsx +++ b/services/web/frontend/js/features/project-list/components/project-list-root.tsx @@ -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,27 +74,34 @@ function ProjectListPageContent() { - - +
+ +
+
+ {selectedProjects.length === 0 ? ( + + ) : ( + + )} +
+
+ +
+
+
+ + - -
-
- {selectedProjects.length === 0 ? ( - - ) : ( - - )} -
-
- -
-
-
@@ -112,6 +125,8 @@ function ProjectListPageContent() { diff --git a/services/web/frontend/js/features/project-list/components/search-form.tsx b/services/web/frontend/js/features/project-list/components/search-form.tsx index f3ebc7f6cd..9e9f1cd249 100644 --- a/services/web/frontend/js/features/project-list/components/search-form.tsx +++ b/services/web/frontend/js/features/project-list/components/search-form.tsx @@ -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, 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 || {} diff --git a/services/web/frontend/js/features/project-list/components/title/project-list-title.tsx b/services/web/frontend/js/features/project-list/components/title/project-list-title.tsx new file mode 100644 index 0000000000..5facf67ce0 --- /dev/null +++ b/services/web/frontend/js/features/project-list/components/title/project-list-title.tsx @@ -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 ( +
+ {message} +
+ ) +} + +export default ProjectListTitle diff --git a/services/web/frontend/stylesheets/app/project-list-react.less b/services/web/frontend/stylesheets/app/project-list-react.less index 6447537eab..ac959bb56f 100644 --- a/services/web/frontend/stylesheets/app/project-list-react.less +++ b/services/web/frontend/stylesheets/app/project-list-react.less @@ -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; } } diff --git a/services/web/frontend/stylesheets/core/variables.less b/services/web/frontend/stylesheets/core/variables.less index 5d67ef583e..60ff67f37b 100644 --- a/services/web/frontend/stylesheets/core/variables.less +++ b/services/web/frontend/stylesheets/core/variables.less @@ -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; diff --git a/services/web/locales/en.json b/services/web/locales/en.json index 02caf50768..670b6ff03f 100644 --- a/services/web/locales/en.json +++ b/services/web/locales/en.json @@ -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", diff --git a/services/web/test/frontend/features/project-list/components/project-list-root.test.tsx b/services/web/test/frontend/features/project-list/components/project-list-root.test.tsx index da469774c8..2ed424d3c6 100644 --- a/services/web/test/frontend/features/project-list/components/project-list-root.test.tsx +++ b/services/web/test/frontend/features/project-list/components/project-list-root.test.tsx @@ -1037,7 +1037,7 @@ describe('', 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 diff --git a/services/web/test/frontend/features/project-list/components/project-list-title.tsx b/services/web/test/frontend/features/project-list/components/project-list-title.tsx new file mode 100644 index 0000000000..18d845174a --- /dev/null +++ b/services/web/test/frontend/features/project-list/components/project-list-title.tsx @@ -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('', function () { + type TestCase = { + filter: Filter + selectedTag: Tag | undefined + expectedText: string + } + + const testCases: Array = [ + // 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( + + ) + screen.getByText(new RegExp(testCase.expectedText, 'i')) + }) + } +}) diff --git a/services/web/test/frontend/features/project-list/components/project-search.test.tsx b/services/web/test/frontend/features/project-list/components/project-search.test.tsx index ac2b1c8af1..c09d8431ed 100644 --- a/services/web/test/frontend/features/project-list/components/project-search.test.tsx +++ b/services/web/test/frontend/features/project-list/components/project-search.test.tsx @@ -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( {}} />) + const filter: Filter = 'all' + const selectedTag = undefined + render( + {}} + 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() + render( + + ) const input = screen.getByRole('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() - const input = screen.getByRole('textbox', { name: /search projects/i }) + const filter: Filter = 'all' + const selectedTag = undefined + + render( + + ) + 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 = [ + // 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( + {}} + filter={testCase.filter} + selectedTag={testCase.selectedTag} + /> + ) + screen.getByRole('search') + screen.getByRole('textbox', { + name: new RegExp(testCase.expectedText, 'i'), + }) + }) + } })