mirror of
https://github.com/yu-i-i/overleaf-cep.git
synced 2026-11-12 13:13:45 +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:
@@ -577,6 +577,7 @@ async function projectListPage(req, res, next) {
|
|||||||
'overleaf-library',
|
'overleaf-library',
|
||||||
'import-markdown',
|
'import-markdown',
|
||||||
'themed-modals',
|
'themed-modals',
|
||||||
|
'shared-workspace',
|
||||||
].filter(Boolean)
|
].filter(Boolean)
|
||||||
|
|
||||||
await Promise.all(
|
await Promise.all(
|
||||||
|
|||||||
@@ -1344,6 +1344,7 @@
|
|||||||
"no_actions": "",
|
"no_actions": "",
|
||||||
"no_add_on_purchase_while_paused": "",
|
"no_add_on_purchase_while_paused": "",
|
||||||
"no_ai_assistant": "",
|
"no_ai_assistant": "",
|
||||||
|
"no_archived_projects": "",
|
||||||
"no_audit_logs_found": "",
|
"no_audit_logs_found": "",
|
||||||
"no_borders": "",
|
"no_borders": "",
|
||||||
"no_caption": "",
|
"no_caption": "",
|
||||||
@@ -1370,6 +1371,8 @@
|
|||||||
"no_preview_available": "",
|
"no_preview_available": "",
|
||||||
"no_project_notifications_description": "",
|
"no_project_notifications_description": "",
|
||||||
"no_projects": "",
|
"no_projects": "",
|
||||||
|
"no_projects_match_query": "",
|
||||||
|
"no_projects_yet": "",
|
||||||
"no_references_found": "",
|
"no_references_found": "",
|
||||||
"no_references_found_in_this_file": "",
|
"no_references_found_in_this_file": "",
|
||||||
"no_resolved_comments": "",
|
"no_resolved_comments": "",
|
||||||
@@ -1379,6 +1382,7 @@
|
|||||||
"no_selection_select_file": "",
|
"no_selection_select_file": "",
|
||||||
"no_symbols_found": "",
|
"no_symbols_found": "",
|
||||||
"no_thanks_cancel_now": "",
|
"no_thanks_cancel_now": "",
|
||||||
|
"no_trashed_projects": "",
|
||||||
"no_trashed_references": "",
|
"no_trashed_references": "",
|
||||||
"non_blinking_cursor": "",
|
"non_blinking_cursor": "",
|
||||||
"normal": "",
|
"normal": "",
|
||||||
@@ -1386,6 +1390,7 @@
|
|||||||
"normally_x_price_per_year": "",
|
"normally_x_price_per_year": "",
|
||||||
"not_managed": "",
|
"not_managed": "",
|
||||||
"not_now": "",
|
"not_now": "",
|
||||||
|
"nothing_shared_with_you_yet": "",
|
||||||
"notification": "",
|
"notification": "",
|
||||||
"notification_personal_and_group_subscriptions": "",
|
"notification_personal_and_group_subscriptions": "",
|
||||||
"notification_project_invite_accepted_message": "",
|
"notification_project_invite_accepted_message": "",
|
||||||
@@ -1608,6 +1613,13 @@
|
|||||||
"projects": "",
|
"projects": "",
|
||||||
"projects_count": "",
|
"projects_count": "",
|
||||||
"projects_list": "",
|
"projects_list": "",
|
||||||
|
"projects_shared_with_you_directly_or_in_shared_workspaces_will_appear_here": "",
|
||||||
|
"projects_shared_with_you_directly_will_appear_here": "",
|
||||||
|
"projects_you_archive_will_appear_here": "",
|
||||||
|
"projects_you_create_across_all_your_workspaces_will_appear_here": "",
|
||||||
|
"projects_you_create_will_appear_here": "",
|
||||||
|
"projects_you_own_or_have_access_to_will_appear_here": "",
|
||||||
|
"projects_you_trash_will_appear_here": "",
|
||||||
"provide_details_of_your_sso_configuration": "",
|
"provide_details_of_your_sso_configuration": "",
|
||||||
"provider_rate_limited": "",
|
"provider_rate_limited": "",
|
||||||
"public": "",
|
"public": "",
|
||||||
@@ -2573,6 +2585,7 @@
|
|||||||
"you_can_now_log_in_sso": "",
|
"you_can_now_log_in_sso": "",
|
||||||
"you_can_now_search_and_add_references_from_your_rm_library_without_needing_to_import_files": "",
|
"you_can_now_search_and_add_references_from_your_rm_library_without_needing_to_import_files": "",
|
||||||
"you_can_only_be_affiliated_with_one_group": "",
|
"you_can_only_be_affiliated_with_one_group": "",
|
||||||
|
"you_can_restore_or_permanently_delete_them_at_any_time": "",
|
||||||
"you_can_select_or_invite_collaborator": "",
|
"you_can_select_or_invite_collaborator": "",
|
||||||
"you_can_select_or_invite_collaborator_plural": "",
|
"you_can_select_or_invite_collaborator_plural": "",
|
||||||
"you_can_still_use_your_premium_features": "",
|
"you_can_still_use_your_premium_features": "",
|
||||||
@@ -2639,6 +2652,7 @@
|
|||||||
"your_subscription_will_pause_on": "",
|
"your_subscription_will_pause_on": "",
|
||||||
"your_subscription_will_pause_on_short": "",
|
"your_subscription_will_pause_on_short": "",
|
||||||
"your_subscriptions": "",
|
"your_subscriptions": "",
|
||||||
|
"your_tags": "",
|
||||||
"youre_a_member_of_overleaf_labs": "",
|
"youre_a_member_of_overleaf_labs": "",
|
||||||
"youre_about_to_enable_single_sign_on": "",
|
"youre_about_to_enable_single_sign_on": "",
|
||||||
"youre_about_to_enable_single_sign_on_sso_only": "",
|
"youre_about_to_enable_single_sign_on_sso_only": "",
|
||||||
|
|||||||
BIN
Binary file not shown.
@@ -5,6 +5,7 @@
|
|||||||
|
|
||||||
export default /** @type {const} */ ([
|
export default /** @type {const} */ ([
|
||||||
'account_balance',
|
'account_balance',
|
||||||
|
'archive',
|
||||||
'arrow_back_ios_new',
|
'arrow_back_ios_new',
|
||||||
'arrow_circle_up',
|
'arrow_circle_up',
|
||||||
'auto_delete',
|
'auto_delete',
|
||||||
@@ -30,6 +31,7 @@ export default /** @type {const} */ ([
|
|||||||
'edit_square',
|
'edit_square',
|
||||||
'error',
|
'error',
|
||||||
'experiment',
|
'experiment',
|
||||||
|
'find_in_page',
|
||||||
'find_replace',
|
'find_replace',
|
||||||
'format_strikethrough',
|
'format_strikethrough',
|
||||||
'forum',
|
'forum',
|
||||||
|
|||||||
@@ -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 overleafLogo from '@/shared/svgs/overleaf-a-ds-solution-mallard.svg'
|
||||||
import overleafLogoDark from '@/shared/svgs/overleaf-a-ds-solution-mallard-dark.svg'
|
import overleafLogoDark from '@/shared/svgs/overleaf-a-ds-solution-mallard-dark.svg'
|
||||||
import CookieBanner from '@/shared/components/cookie-banner'
|
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 { useActiveOverallTheme } from '@/shared/hooks/use-active-overall-theme'
|
||||||
import { isSplitTestEnabled } from '@/utils/splitTestUtils'
|
import { isSplitTestEnabled } from '@/utils/splitTestUtils'
|
||||||
|
import { useFeatureFlag } from '@/shared/context/split-test-context'
|
||||||
|
|
||||||
export function ProjectListDsNav() {
|
export function ProjectListDsNav() {
|
||||||
const navbarProps = getMeta('ol-navbar')
|
const navbarProps = getMeta('ol-navbar')
|
||||||
@@ -39,9 +41,11 @@ export function ProjectListDsNav() {
|
|||||||
filter,
|
filter,
|
||||||
tags,
|
tags,
|
||||||
selectedTagId,
|
selectedTagId,
|
||||||
|
currentFilterProjectsCount,
|
||||||
} = useProjectListContext()
|
} = useProjectListContext()
|
||||||
const activeOverallTheme = useActiveOverallTheme()
|
const activeOverallTheme = useActiveOverallTheme()
|
||||||
const isLibraryEnabled = isSplitTestEnabled('overleaf-library')
|
const isLibraryEnabled = isSplitTestEnabled('overleaf-library')
|
||||||
|
const isSharedWorkspaceEnabled = useFeatureFlag('shared-workspace')
|
||||||
|
|
||||||
const selectedTag = tags.find(tag => tag._id === selectedTagId)
|
const selectedTag = tags.find(tag => tag._id === selectedTagId)
|
||||||
const showTrashHeader = isLibraryEnabled && filter === 'trashed'
|
const showTrashHeader = isLibraryEnabled && filter === 'trashed'
|
||||||
@@ -142,49 +146,52 @@ export function ProjectListDsNav() {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{showTrashHeader && <TrashPageTabs activeTab="projects" />}
|
{showTrashHeader && <TrashPageTabs activeTab="projects" />}
|
||||||
<div className="project-ds-nav-project-list">
|
{isSharedWorkspaceEnabled &&
|
||||||
<OLRow className="d-none d-md-flex align-items-center">
|
selectedTagId === undefined &&
|
||||||
<OLCol md={isLibraryEnabled ? 8 : undefined} lg={7}>
|
currentFilterProjectsCount === 0 ? (
|
||||||
<SearchForm
|
<>
|
||||||
inputValue={searchText}
|
<ProjectsToolbarMobile />
|
||||||
setInputValue={setSearchText}
|
<NoProjects
|
||||||
filter={filter}
|
activeSection={filter}
|
||||||
selectedTag={selectedTag}
|
showNewProjectButton={showNewProjectButton}
|
||||||
/>
|
/>
|
||||||
</OLCol>
|
</>
|
||||||
{isLibraryEnabled && showNewProjectButton && (
|
) : (
|
||||||
<OLCol className="ms-auto" xs="auto">
|
<div className="project-ds-nav-project-list">
|
||||||
<NewProjectButton
|
<OLRow className="d-none d-md-flex align-items-center">
|
||||||
id="new-project-button-projects-table"
|
<OLCol md={isLibraryEnabled ? 8 : undefined} lg={7}>
|
||||||
showAddAffiliationWidget
|
<SearchForm
|
||||||
align="end"
|
inputValue={searchText}
|
||||||
|
setInputValue={setSearchText}
|
||||||
|
filter={filter}
|
||||||
|
selectedTag={selectedTag}
|
||||||
/>
|
/>
|
||||||
</OLCol>
|
</OLCol>
|
||||||
)}
|
{isLibraryEnabled && showNewProjectButton && (
|
||||||
</OLRow>
|
<OLCol className="ms-auto" xs="auto">
|
||||||
<div className="project-list-sidebar-survey-wrapper d-md-none">
|
<NewProjectButton
|
||||||
{/* Omit the survey card in mobile view for now */}
|
id="new-project-button-projects-table"
|
||||||
</div>
|
showAddAffiliationWidget
|
||||||
<div className="mt-1 d-md-none">
|
align="end"
|
||||||
<div
|
/>
|
||||||
role="toolbar"
|
</OLCol>
|
||||||
className="projects-toolbar"
|
)}
|
||||||
aria-label={t('projects')}
|
</OLRow>
|
||||||
>
|
<div className="project-list-sidebar-survey-wrapper d-md-none">
|
||||||
<NavigationDropdown activePage="projects" />
|
{/* Omit the survey card in mobile view for now */}
|
||||||
<SortByDropdown />
|
</div>
|
||||||
|
<ProjectsToolbarMobile />
|
||||||
|
<div className="mt-3">
|
||||||
|
<TableContainer bordered>
|
||||||
|
{tableTopArea}
|
||||||
|
<ProjectListTable />
|
||||||
|
</TableContainer>
|
||||||
|
</div>
|
||||||
|
<div className="mt-3">
|
||||||
|
<LoadMore />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="mt-3">
|
)}
|
||||||
<TableContainer bordered>
|
|
||||||
{tableTopArea}
|
|
||||||
<ProjectListTable />
|
|
||||||
</TableContainer>
|
|
||||||
</div>
|
|
||||||
<div className="mt-3">
|
|
||||||
<LoadMore />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</main>
|
</main>
|
||||||
</div>
|
</div>
|
||||||
<Footer {...footerProps} />
|
<Footer {...footerProps} />
|
||||||
@@ -195,3 +202,20 @@ export function ProjectListDsNav() {
|
|||||||
</div>
|
</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,
|
OLDropdownMenu,
|
||||||
OLDropdownToggle,
|
OLDropdownToggle,
|
||||||
} from '@/shared/components/ol/ol-dropdown-menu'
|
} from '@/shared/components/ol/ol-dropdown-menu'
|
||||||
|
import { useFeatureFlag } from '@/shared/context/split-test-context'
|
||||||
|
|
||||||
export default function TagsList() {
|
export default function TagsList() {
|
||||||
const { t } = useTranslation()
|
const { t } = useTranslation()
|
||||||
|
const isSharedWorkspaceEnabled = useFeatureFlag('shared-workspace')
|
||||||
const {
|
const {
|
||||||
tags,
|
tags,
|
||||||
projectsPerTag,
|
projectsPerTag,
|
||||||
@@ -40,7 +42,7 @@ export default function TagsList() {
|
|||||||
aria-hidden="true"
|
aria-hidden="true"
|
||||||
data-testid="organize-projects"
|
data-testid="organize-projects"
|
||||||
>
|
>
|
||||||
{t('organize_tags')}
|
{isSharedWorkspaceEnabled ? t('your_tags') : t('organize_tags')}
|
||||||
</li>
|
</li>
|
||||||
<li className="tag">
|
<li className="tag">
|
||||||
<button type="button" className="tag-name" onClick={openCreateTagModal}>
|
<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 OLTable from '@/shared/components/ol/ol-table'
|
||||||
import OLFormCheckbox from '@/shared/components/ol/ol-form-checkbox'
|
import OLFormCheckbox from '@/shared/components/ol/ol-form-checkbox'
|
||||||
import MaterialIcon from '@/shared/components/material-icon'
|
import MaterialIcon from '@/shared/components/material-icon'
|
||||||
|
import { useFeatureFlag } from '@/shared/context/split-test-context'
|
||||||
|
|
||||||
function SortBtn({ onClick, text, iconType, screenReaderText }: SortBtnProps) {
|
function SortBtn({ onClick, text, iconType, screenReaderText }: SortBtnProps) {
|
||||||
return (
|
return (
|
||||||
@@ -30,7 +31,9 @@ function ProjectListTable() {
|
|||||||
sort,
|
sort,
|
||||||
selectedProjects,
|
selectedProjects,
|
||||||
selectOrUnselectAllProjects,
|
selectOrUnselectAllProjects,
|
||||||
|
searchText,
|
||||||
} = useProjectListContext()
|
} = useProjectListContext()
|
||||||
|
const isSharedWorkspaceEnabled = useFeatureFlag('shared-workspace')
|
||||||
const { handleSort } = useSort()
|
const { handleSort } = useSort()
|
||||||
const checkAllRef = useRef<HTMLInputElement>(null)
|
const checkAllRef = useRef<HTMLInputElement>(null)
|
||||||
|
|
||||||
@@ -150,7 +153,9 @@ function ProjectListTable() {
|
|||||||
) : (
|
) : (
|
||||||
<tr className="no-projects">
|
<tr className="no-projects">
|
||||||
<td className="text-center" colSpan={5}>
|
<td className="text-center" colSpan={5}>
|
||||||
{t('no_projects')}
|
{isSharedWorkspaceEnabled && searchText
|
||||||
|
? t('no_projects_match_query', { query: searchText })
|
||||||
|
: t('no_projects')}
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -89,6 +89,7 @@ export type ProjectListContextValue = {
|
|||||||
tags: Tag[]
|
tags: Tag[]
|
||||||
untaggedProjectsCount: number
|
untaggedProjectsCount: number
|
||||||
projectsPerTag: Record<Tag['_id'], Project[]>
|
projectsPerTag: Record<Tag['_id'], Project[]>
|
||||||
|
currentFilterProjectsCount: number
|
||||||
filter: Filter
|
filter: Filter
|
||||||
selectFilter: (filter: Filter) => void
|
selectFilter: (filter: Filter) => void
|
||||||
selectedTagId?: string | undefined
|
selectedTagId?: string | undefined
|
||||||
@@ -378,6 +379,16 @@ export function ProjectListProvider({
|
|||||||
}, {})
|
}, {})
|
||||||
}, [tags, loadedProjects])
|
}, [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(
|
const selectFilter = useCallback(
|
||||||
(filter: Filter) => {
|
(filter: Filter) => {
|
||||||
const url = getNavigationUrl({ type: 'filter', filter })
|
const url = getNavigationUrl({ type: 'filter', filter })
|
||||||
@@ -561,6 +572,7 @@ export function ProjectListProvider({
|
|||||||
updateProjectViewData,
|
updateProjectViewData,
|
||||||
updateTag,
|
updateTag,
|
||||||
projectsPerTag,
|
projectsPerTag,
|
||||||
|
currentFilterProjectsCount,
|
||||||
visibleProjects,
|
visibleProjects,
|
||||||
}),
|
}),
|
||||||
[
|
[
|
||||||
@@ -600,6 +612,7 @@ export function ProjectListProvider({
|
|||||||
updateProjectViewData,
|
updateProjectViewData,
|
||||||
updateTag,
|
updateTag,
|
||||||
projectsPerTag,
|
projectsPerTag,
|
||||||
|
currentFilterProjectsCount,
|
||||||
visibleProjects,
|
visibleProjects,
|
||||||
]
|
]
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -2,6 +2,7 @@
|
|||||||
--ds-nav-active-bg: var(--bg-accent-03);
|
--ds-nav-active-bg: var(--bg-accent-03);
|
||||||
--ds-nav-active-color: var(--green-60);
|
--ds-nav-active-color: var(--green-60);
|
||||||
--theme-toggle-selected-background: var(--green-20);
|
--theme-toggle-selected-background: var(--green-20);
|
||||||
|
--ds-nav-content-bg-primary: var(--bg-light-primary);
|
||||||
--ds-nav-content-bg-secondary: var(--bg-light-secondary);
|
--ds-nav-content-bg-secondary: var(--bg-light-secondary);
|
||||||
--table-icon-bg-hover: 27 34 44;
|
--table-icon-bg-hover: 27 34 44;
|
||||||
--themed-dashboard-popover-bg: var(--bg-dark-primary);
|
--themed-dashboard-popover-bg: var(--bg-dark-primary);
|
||||||
@@ -15,6 +16,7 @@
|
|||||||
--ds-nav-active-bg: var(--green-70);
|
--ds-nav-active-bg: var(--green-70);
|
||||||
--ds-nav-active-color: var(--green-10);
|
--ds-nav-active-color: var(--green-10);
|
||||||
--theme-toggle-selected-background: var(--green-70);
|
--theme-toggle-selected-background: var(--green-70);
|
||||||
|
--ds-nav-content-bg-primary: var(--bg-dark-primary);
|
||||||
--ds-nav-content-bg-secondary: var(--bg-dark-secondary);
|
--ds-nav-content-bg-secondary: var(--bg-dark-secondary);
|
||||||
--table-icon-bg-hover: 255 255 255;
|
--table-icon-bg-hover: 255 255 255;
|
||||||
--themed-dashboard-popover-bg: var(--bg-light-primary);
|
--themed-dashboard-popover-bg: var(--bg-light-primary);
|
||||||
@@ -404,12 +406,70 @@ body {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.project-ds-nav-main {
|
.project-ds-nav-main {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
min-height: 100%;
|
min-height: 100%;
|
||||||
padding: var(--spacing-08) var(--spacing-06);
|
padding: var(--spacing-08) var(--spacing-06);
|
||||||
|
|
||||||
@include media-breakpoint-up(md) {
|
@include media-breakpoint-up(md) {
|
||||||
padding: var(--spacing-08);
|
padding: var(--spacing-08);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
main {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.project-ds-empty-project-list {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
flex: 1;
|
||||||
|
padding: var(--spacing-09);
|
||||||
|
background: var(--ds-nav-content-bg-primary);
|
||||||
|
border-radius: var(--border-radius-base);
|
||||||
|
|
||||||
|
@include media-breakpoint-down(md) {
|
||||||
|
margin-top: var(--spacing-06);
|
||||||
|
}
|
||||||
|
|
||||||
|
.project-ds-empty-project-list-inner {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: var(--spacing-06);
|
||||||
|
max-width: 560px;
|
||||||
|
|
||||||
|
.project-ds-empty-project-list-icon {
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
margin: 0 auto;
|
||||||
|
height: 80px;
|
||||||
|
width: 80px;
|
||||||
|
padding: var(--spacing-08);
|
||||||
|
border-radius: 50%;
|
||||||
|
background-color: var(--bg-secondary-themed);
|
||||||
|
color: var(--content-secondary-themed);
|
||||||
|
|
||||||
|
.material-symbols {
|
||||||
|
font-size: var(--font-size-08);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.project-ds-empty-project-list-title {
|
||||||
|
@include heading-sm;
|
||||||
|
|
||||||
|
text-align: center;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
.project-ds-empty-project-list-description {
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.project-dash-table {
|
.project-dash-table {
|
||||||
|
|||||||
@@ -1735,6 +1735,7 @@
|
|||||||
"no_actions": "No actions",
|
"no_actions": "No actions",
|
||||||
"no_add_on_purchase_while_paused": "You need to unpause or cancel your existing subscription to buy an add-on.",
|
"no_add_on_purchase_while_paused": "You need to unpause or cancel your existing subscription to buy an add-on.",
|
||||||
"no_ai_assistant": "No AI Assistant",
|
"no_ai_assistant": "No AI Assistant",
|
||||||
|
"no_archived_projects": "No archived projects",
|
||||||
"no_articles_matching_your_tags": "There are no articles matching your tags",
|
"no_articles_matching_your_tags": "There are no articles matching your tags",
|
||||||
"no_audit_logs_found": "No audit logs found",
|
"no_audit_logs_found": "No audit logs found",
|
||||||
"no_borders": "No borders",
|
"no_borders": "No borders",
|
||||||
@@ -1765,6 +1766,8 @@
|
|||||||
"no_preview_available": "Sorry, no preview is available.",
|
"no_preview_available": "Sorry, no preview is available.",
|
||||||
"no_project_notifications_description": "You won’t be notified about this project.",
|
"no_project_notifications_description": "You won’t be notified about this project.",
|
||||||
"no_projects": "No projects",
|
"no_projects": "No projects",
|
||||||
|
"no_projects_match_query": "No projects match “__query__”",
|
||||||
|
"no_projects_yet": "No projects yet",
|
||||||
"no_references_found": "No references found",
|
"no_references_found": "No references found",
|
||||||
"no_references_found_in_this_file": "No references found in this file",
|
"no_references_found_in_this_file": "No references found in this file",
|
||||||
"no_resolved_comments": "No resolved comments",
|
"no_resolved_comments": "No resolved comments",
|
||||||
@@ -1774,6 +1777,7 @@
|
|||||||
"no_selection_select_file": "Currently, no file is selected. Please select a file from the file tree.",
|
"no_selection_select_file": "Currently, no file is selected. Please select a file from the file tree.",
|
||||||
"no_symbols_found": "No symbols found",
|
"no_symbols_found": "No symbols found",
|
||||||
"no_thanks_cancel_now": "No thanks, I still want to cancel",
|
"no_thanks_cancel_now": "No thanks, I still want to cancel",
|
||||||
|
"no_trashed_projects": "No trashed projects",
|
||||||
"no_trashed_references": "No references in Trash",
|
"no_trashed_references": "No references in Trash",
|
||||||
"no_update_email": "No, update email",
|
"no_update_email": "No, update email",
|
||||||
"non_blinking_cursor": "Non-blinking cursor",
|
"non_blinking_cursor": "Non-blinking cursor",
|
||||||
@@ -1787,6 +1791,7 @@
|
|||||||
"not_now": "Not now",
|
"not_now": "Not now",
|
||||||
"not_registered": "Not registered",
|
"not_registered": "Not registered",
|
||||||
"note_features_under_development": "<0>Please note</0> that features in this program are still being tested and actively developed. This means that they might <0>change</0>, be <0>removed</0> or <0>become part of a premium plan</0>",
|
"note_features_under_development": "<0>Please note</0> that features in this program are still being tested and actively developed. This means that they might <0>change</0>, be <0>removed</0> or <0>become part of a premium plan</0>",
|
||||||
|
"nothing_shared_with_you_yet": "Nothing shared with you yet",
|
||||||
"notification": "Notification",
|
"notification": "Notification",
|
||||||
"notification_features_upgraded_by_affiliation": "Good news! Your affiliated organization __institutionName__ has an Overleaf subscription, and you now have access to all of Overleaf premium features.",
|
"notification_features_upgraded_by_affiliation": "Good news! Your affiliated organization __institutionName__ has an Overleaf subscription, and you now have access to all of Overleaf premium features.",
|
||||||
"notification_old_debug_projects": "You have some old debug projects that you should delete, if you no longer need them.",
|
"notification_old_debug_projects": "You have some old debug projects that you should delete, if you no longer need them.",
|
||||||
@@ -2092,6 +2097,13 @@
|
|||||||
"projects": "Projects",
|
"projects": "Projects",
|
||||||
"projects_count": "Projects count",
|
"projects_count": "Projects count",
|
||||||
"projects_list": "Projects list",
|
"projects_list": "Projects list",
|
||||||
|
"projects_shared_with_you_directly_or_in_shared_workspaces_will_appear_here": "Projects shared with you directly, or ones you’ve joined in a shared workspace, will appear here.",
|
||||||
|
"projects_shared_with_you_directly_will_appear_here": "Projects shared with you directly will appear here.",
|
||||||
|
"projects_you_archive_will_appear_here": "Projects you archive will appear here. Archiving won’t affect anyone else working on the project.",
|
||||||
|
"projects_you_create_across_all_your_workspaces_will_appear_here": "Projects you create across all your workspaces will appear here.",
|
||||||
|
"projects_you_create_will_appear_here": "Projects you create will appear here.",
|
||||||
|
"projects_you_own_or_have_access_to_will_appear_here": "Projects you own or have access to will appear here.",
|
||||||
|
"projects_you_trash_will_appear_here": "Projects you trash will appear here.",
|
||||||
"provide_details_of_your_sso_configuration": "Add, edit, or delete your Identity Provider’s SAML metadata.",
|
"provide_details_of_your_sso_configuration": "Add, edit, or delete your Identity Provider’s SAML metadata.",
|
||||||
"provider_rate_limited": "Your reference provider is temporarily rate limited. Please try again later.",
|
"provider_rate_limited": "Your reference provider is temporarily rate limited. Please try again later.",
|
||||||
"pt": "Portuguese",
|
"pt": "Portuguese",
|
||||||
@@ -3233,6 +3245,7 @@
|
|||||||
"you_can_now_sign_in_with_new_password": "You can now sign in with your new password.",
|
"you_can_now_sign_in_with_new_password": "You can now sign in with your new password.",
|
||||||
"you_can_only_be_affiliated_with_one_group": "You can only be affiliated with one group.",
|
"you_can_only_be_affiliated_with_one_group": "You can only be affiliated with one group.",
|
||||||
"you_can_opt_in_and_out_of_the_program_at_any_time_on_this_page": "You can <0>opt in and out</0> of the program at any time on this page",
|
"you_can_opt_in_and_out_of_the_program_at_any_time_on_this_page": "You can <0>opt in and out</0> of the program at any time on this page",
|
||||||
|
"you_can_restore_or_permanently_delete_them_at_any_time": "You can restore or permanently delete them at any time.",
|
||||||
"you_can_select_or_invite_collaborator": "You can select or invite __count__ collaborator on your current plan. Upgrade to add more editors or reviewers.",
|
"you_can_select_or_invite_collaborator": "You can select or invite __count__ collaborator on your current plan. Upgrade to add more editors or reviewers.",
|
||||||
"you_can_select_or_invite_collaborator_plural": "You can select or invite __count__ collaborators on your current plan. Upgrade to add more editors or reviewers.",
|
"you_can_select_or_invite_collaborator_plural": "You can select or invite __count__ collaborators on your current plan. Upgrade to add more editors or reviewers.",
|
||||||
"you_can_still_use_your_premium_features": "You can still use your premium features until the pause becomes active.",
|
"you_can_still_use_your_premium_features": "You can still use your premium features until the pause becomes active.",
|
||||||
@@ -3307,6 +3320,7 @@
|
|||||||
"your_subscription_will_pause_on": "Your <0>__planName__</0> subscription will pause on <0>__pauseDate__</0>. It’ll automatically unpause on <0>__reactivationDate__</0>. Or you can unpause it yourself at any time.",
|
"your_subscription_will_pause_on": "Your <0>__planName__</0> subscription will pause on <0>__pauseDate__</0>. It’ll automatically unpause on <0>__reactivationDate__</0>. Or you can unpause it yourself at any time.",
|
||||||
"your_subscription_will_pause_on_short": "Your subscription will pause on <0>__pauseDate__</0>.",
|
"your_subscription_will_pause_on_short": "Your subscription will pause on <0>__pauseDate__</0>.",
|
||||||
"your_subscriptions": "Your subscriptions",
|
"your_subscriptions": "Your subscriptions",
|
||||||
|
"your_tags": "Your tags",
|
||||||
"youre_a_member_of_overleaf_labs": "You’re a member of Overleaf Labs. Don’t forget to check in regularly to see what experiments you can sign up to.",
|
"youre_a_member_of_overleaf_labs": "You’re a member of Overleaf Labs. Don’t forget to check in regularly to see what experiments you can sign up to.",
|
||||||
"youre_about_to_enable_single_sign_on": "You’re about to enable single sign-on (SSO). Before you do this, you should ensure you’re confident the SSO configuration is correct and all your group members have managed user accounts.",
|
"youre_about_to_enable_single_sign_on": "You’re about to enable single sign-on (SSO). Before you do this, you should ensure you’re confident the SSO configuration is correct and all your group members have managed user accounts.",
|
||||||
"youre_about_to_enable_single_sign_on_sso_only": "You’re about to enable single sign-on (SSO). Before you do this, you should ensure you’re confident the SSO configuration is correct.",
|
"youre_about_to_enable_single_sign_on_sso_only": "You’re about to enable single sign-on (SSO). Before you do this, you should ensure you’re confident the SSO configuration is correct.",
|
||||||
|
|||||||
@@ -0,0 +1,153 @@
|
|||||||
|
import NoProjects from '@/features/project-list/components/no-projects'
|
||||||
|
import {
|
||||||
|
Filter,
|
||||||
|
ProjectListProvider,
|
||||||
|
} from '@/features/project-list/context/project-list-context'
|
||||||
|
import { SplitTestProvider } from '@/shared/context/split-test-context'
|
||||||
|
|
||||||
|
describe('<NoProjects />', function () {
|
||||||
|
beforeEach(function () {
|
||||||
|
cy.intercept('post', '/api/project', {
|
||||||
|
body: { projects: [], totalSize: 0 },
|
||||||
|
})
|
||||||
|
|
||||||
|
cy.window().then(win => {
|
||||||
|
win.metaAttributesCache.set('ol-ExposedSettings', {
|
||||||
|
isOverleaf: true,
|
||||||
|
templateLinks: [],
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
const setGroupPlan = () =>
|
||||||
|
cy.window().then(win => {
|
||||||
|
win.metaAttributesCache.set('ol-usersBestSubscription', {
|
||||||
|
type: 'group',
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
const setIndividualPlan = () =>
|
||||||
|
cy.window().then(win => {
|
||||||
|
win.metaAttributesCache.set('ol-usersBestSubscription', {
|
||||||
|
type: 'individual',
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
const mountNoProjects = (
|
||||||
|
activeSection: Filter,
|
||||||
|
showNewProjectButton = false
|
||||||
|
) =>
|
||||||
|
cy.mount(
|
||||||
|
<SplitTestProvider>
|
||||||
|
<ProjectListProvider>
|
||||||
|
<NoProjects
|
||||||
|
activeSection={activeSection}
|
||||||
|
showNewProjectButton={showNewProjectButton}
|
||||||
|
/>
|
||||||
|
</ProjectListProvider>
|
||||||
|
</SplitTestProvider>
|
||||||
|
)
|
||||||
|
|
||||||
|
describe('all projects', function () {
|
||||||
|
it('shows the generic empty state', function () {
|
||||||
|
mountNoProjects('all')
|
||||||
|
|
||||||
|
cy.findByText('No projects yet').should('be.visible')
|
||||||
|
cy.findByText(
|
||||||
|
'Projects you own or have access to will appear here.'
|
||||||
|
).should('be.visible')
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('your projects', function () {
|
||||||
|
it('shows the non-group copy when the user is not on a group plan', function () {
|
||||||
|
setIndividualPlan()
|
||||||
|
mountNoProjects('owned')
|
||||||
|
|
||||||
|
cy.findByText('No projects yet').should('be.visible')
|
||||||
|
cy.findByText('Projects you create will appear here.').should(
|
||||||
|
'be.visible'
|
||||||
|
)
|
||||||
|
cy.findByText(
|
||||||
|
'Projects you create across all your workspaces will appear here.'
|
||||||
|
).should('not.exist')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('shows the workspaces copy when the user is on a group plan', function () {
|
||||||
|
setGroupPlan()
|
||||||
|
mountNoProjects('owned')
|
||||||
|
|
||||||
|
cy.findByText('No projects yet').should('be.visible')
|
||||||
|
cy.findByText(
|
||||||
|
'Projects you create across all your workspaces will appear here.'
|
||||||
|
).should('be.visible')
|
||||||
|
cy.findByText('Projects you create will appear here.').should('not.exist')
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('shared with you', function () {
|
||||||
|
it('shows the non-group copy when the user is not on a group plan', function () {
|
||||||
|
setIndividualPlan()
|
||||||
|
mountNoProjects('shared')
|
||||||
|
|
||||||
|
cy.findByText('Nothing shared with you yet').should('be.visible')
|
||||||
|
cy.findByText(
|
||||||
|
'Projects shared with you directly will appear here.'
|
||||||
|
).should('be.visible')
|
||||||
|
cy.findByText(
|
||||||
|
'Projects shared with you directly, or ones you’ve joined in a shared workspace, will appear here.'
|
||||||
|
).should('not.exist')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('shows the workspaces copy when the user is on a group plan', function () {
|
||||||
|
setGroupPlan()
|
||||||
|
mountNoProjects('shared')
|
||||||
|
|
||||||
|
cy.findByText('Nothing shared with you yet').should('be.visible')
|
||||||
|
cy.findByText(
|
||||||
|
'Projects shared with you directly, or ones you’ve joined in a shared workspace, will appear here.'
|
||||||
|
).should('be.visible')
|
||||||
|
cy.findByText(
|
||||||
|
'Projects shared with you directly will appear here.'
|
||||||
|
).should('not.exist')
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('archived projects', function () {
|
||||||
|
it('shows the archived empty state', function () {
|
||||||
|
mountNoProjects('archived')
|
||||||
|
|
||||||
|
cy.findByText('No archived projects').should('be.visible')
|
||||||
|
cy.findByText(
|
||||||
|
'Projects you archive will appear here. Archiving won’t affect anyone else working on the project.'
|
||||||
|
).should('be.visible')
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('trashed projects', function () {
|
||||||
|
it('shows the trashed empty state with the restore hint', function () {
|
||||||
|
mountNoProjects('trashed')
|
||||||
|
|
||||||
|
cy.findByText('No trashed projects').should('be.visible')
|
||||||
|
cy.findByText(/projects you trash will appear here/i).should('be.visible')
|
||||||
|
cy.findByText(
|
||||||
|
/you can restore or permanently delete them at any time/i
|
||||||
|
).should('be.visible')
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('new project button', function () {
|
||||||
|
it('shows the button when showNewProjectButton is true', function () {
|
||||||
|
mountNoProjects('all', true)
|
||||||
|
|
||||||
|
cy.findByRole('button', { name: 'New project' }).should('be.visible')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('hides the button when showNewProjectButton is false', function () {
|
||||||
|
mountNoProjects('all', false)
|
||||||
|
|
||||||
|
cy.findByText('No projects yet').should('be.visible')
|
||||||
|
cy.findByRole('button', { name: 'New project' }).should('not.exist')
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
+54
@@ -1,6 +1,8 @@
|
|||||||
import { screen, within, fireEvent, waitFor } from '@testing-library/react'
|
import { screen, within, fireEvent, waitFor } from '@testing-library/react'
|
||||||
import { expect } from 'chai'
|
import { expect } from 'chai'
|
||||||
|
import { useEffect } from 'react'
|
||||||
import ProjectListTable from '../../../../../../frontend/js/features/project-list/components/table/project-list-table'
|
import ProjectListTable from '../../../../../../frontend/js/features/project-list/components/table/project-list-table'
|
||||||
|
import { useProjectListContext } from '@/features/project-list/context/project-list-context'
|
||||||
import { currentProjects } from '../../fixtures/projects-data'
|
import { currentProjects } from '../../fixtures/projects-data'
|
||||||
import fetchMock from 'fetch-mock'
|
import fetchMock from 'fetch-mock'
|
||||||
import { renderWithProjectListContext } from '../../helpers/render-with-context'
|
import { renderWithProjectListContext } from '../../helpers/render-with-context'
|
||||||
@@ -236,4 +238,56 @@ describe('<ProjectListTable />', function () {
|
|||||||
const allCheckboxesChecked = allCheckboxes.filter(c => c.checked)
|
const allCheckboxesChecked = allCheckboxes.filter(c => c.checked)
|
||||||
expect(allCheckboxesChecked.length).to.equal(1)
|
expect(allCheckboxesChecked.length).to.equal(1)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
describe('when no projects are visible', function () {
|
||||||
|
function SetSearchText({ value }: { value: string }) {
|
||||||
|
const { setSearchText } = useProjectListContext()
|
||||||
|
useEffect(() => {
|
||||||
|
setSearchText(value)
|
||||||
|
}, [setSearchText, value])
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
const renderWithSearchText = (
|
||||||
|
value: string,
|
||||||
|
options?: { projects?: typeof currentProjects }
|
||||||
|
) =>
|
||||||
|
renderWithProjectListContext(
|
||||||
|
<>
|
||||||
|
<SetSearchText value={value} />
|
||||||
|
<ProjectListTable />
|
||||||
|
</>,
|
||||||
|
options
|
||||||
|
)
|
||||||
|
|
||||||
|
it('names the search query when the shared-workspace split test is enabled', async function () {
|
||||||
|
window.metaAttributesCache.set('ol-splitTestVariants', {
|
||||||
|
'shared-workspace': 'enabled',
|
||||||
|
})
|
||||||
|
renderWithSearchText('Literature review')
|
||||||
|
await fetchMock.callHistory.flush(true)
|
||||||
|
|
||||||
|
await screen.findByText('No projects match “Literature review”')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('does not name the search query when the split test is disabled', async function () {
|
||||||
|
window.metaAttributesCache.set('ol-splitTestVariants', {})
|
||||||
|
renderWithSearchText('Literature review')
|
||||||
|
await fetchMock.callHistory.flush(true)
|
||||||
|
|
||||||
|
await screen.findByText('No projects')
|
||||||
|
expect(screen.queryByText('No projects match “Literature review”')).to.be
|
||||||
|
.null
|
||||||
|
})
|
||||||
|
|
||||||
|
it('shows the generic message when there is no search query', async function () {
|
||||||
|
window.metaAttributesCache.set('ol-splitTestVariants', {
|
||||||
|
'shared-workspace': 'enabled',
|
||||||
|
})
|
||||||
|
renderWithSearchText('', { projects: [] })
|
||||||
|
await fetchMock.callHistory.flush(true)
|
||||||
|
|
||||||
|
await screen.findByText('No projects')
|
||||||
|
})
|
||||||
|
})
|
||||||
})
|
})
|
||||||
|
|||||||
Reference in New Issue
Block a user