@@ -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'),
+ })
+ })
+ }
})