Sfoglia il codice sorgente

Merge pull request #12851 from overleaf/jk-dashboard-filter-visibility

[web] Improve filter visibility on project dashboard

GitOrigin-RevId: de7a9f999d6d0164ab3c18c58e305c7c628f946c
June Kelly 3 anni fa
parent
commit
ff4ac0a803

+ 5 - 0
services/web/frontend/extracted-translations.json

@@ -742,6 +742,11 @@
   "search_bib_files": "",
   "search_bib_files": "",
   "search_command_find": "",
   "search_command_find": "",
   "search_command_replace": "",
   "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_match_case": "",
   "search_next": "",
   "search_next": "",
   "search_previous": "",
   "search_previous": "",

+ 31 - 16
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 ProjectsDropdown from './dropdown/projects-dropdown'
 import SortByDropdown from './dropdown/sort-by-dropdown'
 import SortByDropdown from './dropdown/sort-by-dropdown'
 import ProjectTools from './table/project-tools/project-tools'
 import ProjectTools from './table/project-tools/project-tools'
+import ProjectListTitle from './title/project-list-title'
 import Sidebar from './sidebar/sidebar'
 import Sidebar from './sidebar/sidebar'
 import LoadMore from './load-more'
 import LoadMore from './load-more'
 import { useEffect } from 'react'
 import { useEffect } from 'react'
@@ -44,8 +45,13 @@ function ProjectListPageContent() {
     searchText,
     searchText,
     setSearchText,
     setSearchText,
     selectedProjects,
     selectedProjects,
+    filter,
+    tags,
+    selectedTagId,
   } = useProjectListContext()
   } = useProjectListContext()
 
 
+  const selectedTag = tags.find(tag => tag._id === selectedTagId)
+
   useEffect(() => {
   useEffect(() => {
     eventTracking.sendMB('loads_v2_dash', {})
     eventTracking.sendMB('loads_v2_dash', {})
   }, [])
   }, [])
@@ -68,27 +74,34 @@ function ProjectListPageContent() {
                   <UserNotifications />
                   <UserNotifications />
                 </Col>
                 </Col>
               </Row>
               </Row>
-              <Row>
-                <Col md={7} className="hidden-xs">
+              <div className="project-list-header-row">
+                <ProjectListTitle
+                  filter={filter}
+                  selectedTag={selectedTag}
+                  className="hidden-xs text-truncate"
+                />
+                <div className="project-tools">
+                  <div className="hidden-xs">
+                    {selectedProjects.length === 0 ? (
+                      <CurrentPlanWidget />
+                    ) : (
+                      <ProjectTools />
+                    )}
+                  </div>
+                  <div className="visible-xs">
+                    <CurrentPlanWidget />
+                  </div>
+                </div>
+              </div>
+              <Row className="hidden-xs">
+                <Col md={7}>
                   <SearchForm
                   <SearchForm
                     inputValue={searchText}
                     inputValue={searchText}
                     setInputValue={setSearchText}
                     setInputValue={setSearchText}
+                    filter={filter}
+                    selectedTag={selectedTag}
                   />
                   />
                 </Col>
                 </Col>
-                <Col md={5}>
-                  <div className="project-tools">
-                    <div className="hidden-xs">
-                      {selectedProjects.length === 0 ? (
-                        <CurrentPlanWidget />
-                      ) : (
-                        <ProjectTools />
-                      )}
-                    </div>
-                    <div className="visible-xs">
-                      <CurrentPlanWidget />
-                    </div>
-                  </div>
-                </Col>
               </Row>
               </Row>
               <div className="project-list-sidebar-survey-wrapper visible-xs">
               <div className="project-list-sidebar-survey-wrapper visible-xs">
                 <SurveyWidget />
                 <SurveyWidget />
@@ -112,6 +125,8 @@ function ProjectListPageContent() {
                         <SearchForm
                         <SearchForm
                           inputValue={searchText}
                           inputValue={searchText}
                           setInputValue={setSearchText}
                           setInputValue={setSearchText}
+                          filter={filter}
+                          selectedTag={selectedTag}
                           className="overflow-hidden"
                           className="overflow-hidden"
                           formGroupProps={{ className: 'mb-0' }}
                           formGroupProps={{ className: 'mb-0' }}
                         />
                         />

+ 29 - 1
services/web/frontend/js/features/project-list/components/search-form.tsx

@@ -9,10 +9,14 @@ import {
 import Icon from '../../../shared/components/icon'
 import Icon from '../../../shared/components/icon'
 import * as eventTracking from '../../../infrastructure/event-tracking'
 import * as eventTracking from '../../../infrastructure/event-tracking'
 import classnames from 'classnames'
 import classnames from 'classnames'
+import { Tag } from '../../../../../app/src/Features/Tags/types'
+import { Filter } from '../context/project-list-context'
 
 
 type SearchFormOwnProps = {
 type SearchFormOwnProps = {
   inputValue: string
   inputValue: string
   setInputValue: (input: string) => void
   setInputValue: (input: string) => void
+  filter: Filter
+  selectedTag: Tag | undefined
   formGroupProps?: FormGroupProps &
   formGroupProps?: FormGroupProps &
     Omit<React.ComponentProps<'div'>, keyof FormGroupProps>
     Omit<React.ComponentProps<'div'>, keyof FormGroupProps>
 }
 }
@@ -23,11 +27,35 @@ type SearchFormProps = SearchFormOwnProps &
 function SearchForm({
 function SearchForm({
   inputValue,
   inputValue,
   setInputValue,
   setInputValue,
+  filter,
+  selectedTag,
   formGroupProps,
   formGroupProps,
   ...props
   ...props
 }: SearchFormProps) {
 }: SearchFormProps) {
   const { t } = useTranslation()
   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 } =
   const { className: formGroupClassName, ...restFormGroupProps } =
     formGroupProps || {}
     formGroupProps || {}
 
 

+ 45 - 0
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 (
+    <div className={classnames('project-list-title', className)}>
+      <span>{message}</span>
+    </div>
+  )
+}
+
+export default ProjectListTitle

+ 23 - 0
services/web/frontend/stylesheets/app/project-list-react.less

@@ -83,6 +83,23 @@
     padding: @content-margin-vertical @grid-gutter-width / 2;
     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 {
   ul.project-list-filters {
     margin: @margin-sm @folders-menu-margin;
     margin: @margin-sm @folders-menu-margin;
 
 
@@ -248,6 +265,11 @@
     }
     }
   }
   }
 
 
+  .project-tools {
+    flex-shrink: 0;
+    margin-left: auto;
+  }
+
   .project-dash-table {
   .project-dash-table {
     width: 100%;
     width: 100%;
     table-layout: fixed;
     table-layout: fixed;
@@ -533,6 +555,7 @@
   @media (max-width: @screen-xs-max) {
   @media (max-width: @screen-xs-max) {
     .project-tools {
     .project-tools {
       float: left;
       float: left;
+      margin-left: initial;
     }
     }
   }
   }
 
 

+ 2 - 2
services/web/frontend/stylesheets/core/variables.less

@@ -945,10 +945,10 @@
 @sidebar-color: @ol-blue-gray-2;
 @sidebar-color: @ol-blue-gray-2;
 @sidebar-link-color: #fff;
 @sidebar-link-color: #fff;
 @sidebar-active-border-radius: 0;
 @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-color: #fff;
 @sidebar-active-font-weight: 700;
 @sidebar-active-font-weight: 700;
-@sidebar-hover-bg: @ol-blue-gray-4;
+@sidebar-hover-bg: @ol-blue-gray-6;
 @sidebar-hover-text-decoration: none;
 @sidebar-hover-text-decoration: none;
 @v2-dash-pane-bg: @ol-blue-gray-4;
 @v2-dash-pane-bg: @ol-blue-gray-4;
 @v2-dash-pane-link-color: #fff;
 @v2-dash-pane-link-color: #fff;

+ 5 - 0
services/web/locales/en.json

@@ -1288,6 +1288,11 @@
   "search_bib_files": "Search by author, title, year",
   "search_bib_files": "Search by author, title, year",
   "search_command_find": "Find",
   "search_command_find": "Find",
   "search_command_replace": "Replace",
   "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_match_case": "Match case",
   "search_next": "next",
   "search_next": "next",
   "search_previous": "previous",
   "search_previous": "previous",

+ 1 - 1
services/web/test/frontend/features/project-list/components/project-list-root.test.tsx

@@ -1037,7 +1037,7 @@ describe('<ProjectListRoot />', function () {
     describe('search', function () {
     describe('search', function () {
       it('shows only projects based on the input', async function () {
       it('shows only projects based on the input', async function () {
         const input = screen.getAllByRole('textbox', {
         const input = screen.getAllByRole('textbox', {
-          name: /search projects/i,
+          name: /search in all projects/i,
         })[0]
         })[0]
         const value = currentList[0].name
         const value = currentList[0].name
 
 

+ 64 - 0
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('<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'))
+    })
+  }
+})

+ 101 - 6
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 SearchForm from '../../../../../frontend/js/features/project-list/components/search-form'
 import * as eventTracking from '../../../../../frontend/js/infrastructure/event-tracking'
 import * as eventTracking from '../../../../../frontend/js/infrastructure/event-tracking'
 import fetchMock from 'fetch-mock'
 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 () {
 describe('Project list search form', function () {
   let sendMBSpy: sinon.SinonSpy
   let sendMBSpy: sinon.SinonSpy
@@ -19,17 +21,35 @@ describe('Project list search form', function () {
   })
   })
 
 
   it('renders the 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('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 () {
   it('calls clear text when clear button is clicked', function () {
+    const filter: Filter = 'all'
+    const selectedTag = undefined
     const setInputValueMock = sinon.stub()
     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', {
     const input = screen.getByRole<HTMLInputElement>('textbox', {
-      name: /search projects/i,
+      name: /search in all projects/i,
     })
     })
 
 
     expect(input.value).to.equal('abc')
     expect(input.value).to.equal('abc')
@@ -43,8 +63,20 @@ describe('Project list search form', function () {
   it('changes text', function () {
   it('changes text', function () {
     const setInputValueMock = sinon.stub()
     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'
     const value = 'abc'
 
 
     fireEvent.change(input, { target: { value } })
     fireEvent.change(input, { target: { value } })
@@ -56,4 +88,67 @@ describe('Project list search form', function () {
     })
     })
     expect(setInputValueMock).to.be.calledWith(value)
     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'),
+      })
+    })
+  }
 })
 })