mirror of
https://github.com/yu-i-i/overleaf-cep.git
synced 2026-11-15 14:43:46 +01:00
Merge pull request #36230 from overleaf/ii-workspaces-projects-page
Empty states for the project list views GitOrigin-RevId: 3ce8e327bb675f3c63763e7599396020a4252025
This commit is contained in:
@@ -0,0 +1,83 @@
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import MaterialIcon from '@/shared/components/material-icon'
|
||||
import { Filter } from '@/features/project-list/context/project-list-context'
|
||||
import NewProjectButton from '@/features/project-list/components/new-project-button'
|
||||
import getMeta from '@/utils/meta'
|
||||
|
||||
type NoProjectsCommonProps = {
|
||||
showNewProjectButton?: boolean
|
||||
}
|
||||
|
||||
type NoProjectsProps = NoProjectsCommonProps & {
|
||||
activeSection: Filter
|
||||
}
|
||||
|
||||
function NoProjects({ activeSection, showNewProjectButton }: NoProjectsProps) {
|
||||
const { t } = useTranslation()
|
||||
const usersBestSubscription = getMeta('ol-usersBestSubscription')
|
||||
const isGroupPlan = usersBestSubscription?.type === 'group'
|
||||
|
||||
return (
|
||||
<div className="project-ds-empty-project-list">
|
||||
<div className="project-ds-empty-project-list-inner">
|
||||
<div className="project-ds-empty-project-list-icon">
|
||||
{activeSection === 'all' && (
|
||||
<MaterialIcon type="find_in_page" unfilled />
|
||||
)}
|
||||
{activeSection === 'owned' && <MaterialIcon type="person" unfilled />}
|
||||
{activeSection === 'shared' && <MaterialIcon type="group" unfilled />}
|
||||
{activeSection === 'archived' && (
|
||||
<MaterialIcon type="archive" unfilled />
|
||||
)}
|
||||
{activeSection === 'trashed' && (
|
||||
<MaterialIcon type="delete" unfilled />
|
||||
)}
|
||||
</div>
|
||||
<div className="d-flex flex-column gap-1">
|
||||
<div className="project-ds-empty-project-list-title">
|
||||
{(activeSection === 'all' || activeSection === 'owned') &&
|
||||
t('no_projects_yet')}
|
||||
{activeSection === 'shared' && t('nothing_shared_with_you_yet')}
|
||||
{activeSection === 'archived' && t('no_archived_projects')}
|
||||
{activeSection === 'trashed' && t('no_trashed_projects')}
|
||||
</div>
|
||||
<div className="project-ds-empty-project-list-description">
|
||||
{activeSection === 'all' &&
|
||||
t('projects_you_own_or_have_access_to_will_appear_here')}
|
||||
{activeSection === 'owned' &&
|
||||
(isGroupPlan
|
||||
? t(
|
||||
'projects_you_create_across_all_your_workspaces_will_appear_here'
|
||||
)
|
||||
: t('projects_you_create_will_appear_here'))}
|
||||
{activeSection === 'shared' &&
|
||||
(isGroupPlan
|
||||
? t(
|
||||
'projects_shared_with_you_directly_or_in_shared_workspaces_will_appear_here'
|
||||
)
|
||||
: t('projects_shared_with_you_directly_will_appear_here'))}
|
||||
{activeSection === 'archived' &&
|
||||
t('projects_you_archive_will_appear_here')}
|
||||
{activeSection === 'trashed' && (
|
||||
<>
|
||||
{t('projects_you_trash_will_appear_here')}
|
||||
<br />
|
||||
{t('you_can_restore_or_permanently_delete_them_at_any_time')}
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
{showNewProjectButton && (
|
||||
<div className="text-center">
|
||||
<NewProjectButton
|
||||
id="new-project-button-no-projects"
|
||||
showAddAffiliationWidget
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default NoProjects
|
||||
@@ -24,8 +24,10 @@ import SystemMessages from '@/shared/components/system-messages'
|
||||
import overleafLogo from '@/shared/svgs/overleaf-a-ds-solution-mallard.svg'
|
||||
import overleafLogoDark from '@/shared/svgs/overleaf-a-ds-solution-mallard-dark.svg'
|
||||
import CookieBanner from '@/shared/components/cookie-banner'
|
||||
import NoProjects from '@/features/project-list/components/no-projects'
|
||||
import { useActiveOverallTheme } from '@/shared/hooks/use-active-overall-theme'
|
||||
import { isSplitTestEnabled } from '@/utils/splitTestUtils'
|
||||
import { useFeatureFlag } from '@/shared/context/split-test-context'
|
||||
|
||||
export function ProjectListDsNav() {
|
||||
const navbarProps = getMeta('ol-navbar')
|
||||
@@ -39,9 +41,11 @@ export function ProjectListDsNav() {
|
||||
filter,
|
||||
tags,
|
||||
selectedTagId,
|
||||
currentFilterProjectsCount,
|
||||
} = useProjectListContext()
|
||||
const activeOverallTheme = useActiveOverallTheme()
|
||||
const isLibraryEnabled = isSplitTestEnabled('overleaf-library')
|
||||
const isSharedWorkspaceEnabled = useFeatureFlag('shared-workspace')
|
||||
|
||||
const selectedTag = tags.find(tag => tag._id === selectedTagId)
|
||||
const showTrashHeader = isLibraryEnabled && filter === 'trashed'
|
||||
@@ -142,49 +146,52 @@ export function ProjectListDsNav() {
|
||||
</div>
|
||||
</div>
|
||||
{showTrashHeader && <TrashPageTabs activeTab="projects" />}
|
||||
<div className="project-ds-nav-project-list">
|
||||
<OLRow className="d-none d-md-flex align-items-center">
|
||||
<OLCol md={isLibraryEnabled ? 8 : undefined} lg={7}>
|
||||
<SearchForm
|
||||
inputValue={searchText}
|
||||
setInputValue={setSearchText}
|
||||
filter={filter}
|
||||
selectedTag={selectedTag}
|
||||
/>
|
||||
</OLCol>
|
||||
{isLibraryEnabled && showNewProjectButton && (
|
||||
<OLCol className="ms-auto" xs="auto">
|
||||
<NewProjectButton
|
||||
id="new-project-button-projects-table"
|
||||
showAddAffiliationWidget
|
||||
align="end"
|
||||
{isSharedWorkspaceEnabled &&
|
||||
selectedTagId === undefined &&
|
||||
currentFilterProjectsCount === 0 ? (
|
||||
<>
|
||||
<ProjectsToolbarMobile />
|
||||
<NoProjects
|
||||
activeSection={filter}
|
||||
showNewProjectButton={showNewProjectButton}
|
||||
/>
|
||||
</>
|
||||
) : (
|
||||
<div className="project-ds-nav-project-list">
|
||||
<OLRow className="d-none d-md-flex align-items-center">
|
||||
<OLCol md={isLibraryEnabled ? 8 : undefined} lg={7}>
|
||||
<SearchForm
|
||||
inputValue={searchText}
|
||||
setInputValue={setSearchText}
|
||||
filter={filter}
|
||||
selectedTag={selectedTag}
|
||||
/>
|
||||
</OLCol>
|
||||
)}
|
||||
</OLRow>
|
||||
<div className="project-list-sidebar-survey-wrapper d-md-none">
|
||||
{/* Omit the survey card in mobile view for now */}
|
||||
</div>
|
||||
<div className="mt-1 d-md-none">
|
||||
<div
|
||||
role="toolbar"
|
||||
className="projects-toolbar"
|
||||
aria-label={t('projects')}
|
||||
>
|
||||
<NavigationDropdown activePage="projects" />
|
||||
<SortByDropdown />
|
||||
{isLibraryEnabled && showNewProjectButton && (
|
||||
<OLCol className="ms-auto" xs="auto">
|
||||
<NewProjectButton
|
||||
id="new-project-button-projects-table"
|
||||
showAddAffiliationWidget
|
||||
align="end"
|
||||
/>
|
||||
</OLCol>
|
||||
)}
|
||||
</OLRow>
|
||||
<div className="project-list-sidebar-survey-wrapper d-md-none">
|
||||
{/* Omit the survey card in mobile view for now */}
|
||||
</div>
|
||||
<ProjectsToolbarMobile />
|
||||
<div className="mt-3">
|
||||
<TableContainer bordered>
|
||||
{tableTopArea}
|
||||
<ProjectListTable />
|
||||
</TableContainer>
|
||||
</div>
|
||||
<div className="mt-3">
|
||||
<LoadMore />
|
||||
</div>
|
||||
</div>
|
||||
<div className="mt-3">
|
||||
<TableContainer bordered>
|
||||
{tableTopArea}
|
||||
<ProjectListTable />
|
||||
</TableContainer>
|
||||
</div>
|
||||
<div className="mt-3">
|
||||
<LoadMore />
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</main>
|
||||
</div>
|
||||
<Footer {...footerProps} />
|
||||
@@ -195,3 +202,20 @@ export function ProjectListDsNav() {
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
function ProjectsToolbarMobile() {
|
||||
const { t } = useTranslation()
|
||||
|
||||
return (
|
||||
<div className="mt-1 d-md-none">
|
||||
<div
|
||||
role="toolbar"
|
||||
className="projects-toolbar"
|
||||
aria-label={t('projects')}
|
||||
>
|
||||
<NavigationDropdown activePage="projects" />
|
||||
<SortByDropdown />
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -13,9 +13,11 @@ import {
|
||||
OLDropdownMenu,
|
||||
OLDropdownToggle,
|
||||
} from '@/shared/components/ol/ol-dropdown-menu'
|
||||
import { useFeatureFlag } from '@/shared/context/split-test-context'
|
||||
|
||||
export default function TagsList() {
|
||||
const { t } = useTranslation()
|
||||
const isSharedWorkspaceEnabled = useFeatureFlag('shared-workspace')
|
||||
const {
|
||||
tags,
|
||||
projectsPerTag,
|
||||
@@ -40,7 +42,7 @@ export default function TagsList() {
|
||||
aria-hidden="true"
|
||||
data-testid="organize-projects"
|
||||
>
|
||||
{t('organize_tags')}
|
||||
{isSharedWorkspaceEnabled ? t('your_tags') : t('organize_tags')}
|
||||
</li>
|
||||
<li className="tag">
|
||||
<button type="button" className="tag-name" onClick={openCreateTagModal}>
|
||||
|
||||
+6
-1
@@ -7,6 +7,7 @@ import withContent, { SortBtnProps } from '../sort/with-content'
|
||||
import OLTable from '@/shared/components/ol/ol-table'
|
||||
import OLFormCheckbox from '@/shared/components/ol/ol-form-checkbox'
|
||||
import MaterialIcon from '@/shared/components/material-icon'
|
||||
import { useFeatureFlag } from '@/shared/context/split-test-context'
|
||||
|
||||
function SortBtn({ onClick, text, iconType, screenReaderText }: SortBtnProps) {
|
||||
return (
|
||||
@@ -30,7 +31,9 @@ function ProjectListTable() {
|
||||
sort,
|
||||
selectedProjects,
|
||||
selectOrUnselectAllProjects,
|
||||
searchText,
|
||||
} = useProjectListContext()
|
||||
const isSharedWorkspaceEnabled = useFeatureFlag('shared-workspace')
|
||||
const { handleSort } = useSort()
|
||||
const checkAllRef = useRef<HTMLInputElement>(null)
|
||||
|
||||
@@ -150,7 +153,9 @@ function ProjectListTable() {
|
||||
) : (
|
||||
<tr className="no-projects">
|
||||
<td className="text-center" colSpan={5}>
|
||||
{t('no_projects')}
|
||||
{isSharedWorkspaceEnabled && searchText
|
||||
? t('no_projects_match_query', { query: searchText })
|
||||
: t('no_projects')}
|
||||
</td>
|
||||
</tr>
|
||||
)}
|
||||
|
||||
@@ -89,6 +89,7 @@ export type ProjectListContextValue = {
|
||||
tags: Tag[]
|
||||
untaggedProjectsCount: number
|
||||
projectsPerTag: Record<Tag['_id'], Project[]>
|
||||
currentFilterProjectsCount: number
|
||||
filter: Filter
|
||||
selectFilter: (filter: Filter) => void
|
||||
selectedTagId?: string | undefined
|
||||
@@ -378,6 +379,16 @@ export function ProjectListProvider({
|
||||
}, {})
|
||||
}, [tags, loadedProjects])
|
||||
|
||||
// How many projects the current filter matches, ignoring the search text.
|
||||
// `visibleProjects` can't answer "is this view empty?" on its own, because it
|
||||
// is also narrowed by the search text (which persists across filter changes)
|
||||
// and by pagination. Callers need the distinction to tell an empty view apart
|
||||
// from a search that matched nothing.
|
||||
const currentFilterProjectsCount = useMemo(
|
||||
() => arrayFilter(loadedProjects, filters[filter]).length,
|
||||
[loadedProjects, filter]
|
||||
)
|
||||
|
||||
const selectFilter = useCallback(
|
||||
(filter: Filter) => {
|
||||
const url = getNavigationUrl({ type: 'filter', filter })
|
||||
@@ -561,6 +572,7 @@ export function ProjectListProvider({
|
||||
updateProjectViewData,
|
||||
updateTag,
|
||||
projectsPerTag,
|
||||
currentFilterProjectsCount,
|
||||
visibleProjects,
|
||||
}),
|
||||
[
|
||||
@@ -600,6 +612,7 @@ export function ProjectListProvider({
|
||||
updateProjectViewData,
|
||||
updateTag,
|
||||
projectsPerTag,
|
||||
currentFilterProjectsCount,
|
||||
visibleProjects,
|
||||
]
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user