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',
|
||||
'import-markdown',
|
||||
'themed-modals',
|
||||
'shared-workspace',
|
||||
].filter(Boolean)
|
||||
|
||||
await Promise.all(
|
||||
|
||||
@@ -1344,6 +1344,7 @@
|
||||
"no_actions": "",
|
||||
"no_add_on_purchase_while_paused": "",
|
||||
"no_ai_assistant": "",
|
||||
"no_archived_projects": "",
|
||||
"no_audit_logs_found": "",
|
||||
"no_borders": "",
|
||||
"no_caption": "",
|
||||
@@ -1370,6 +1371,8 @@
|
||||
"no_preview_available": "",
|
||||
"no_project_notifications_description": "",
|
||||
"no_projects": "",
|
||||
"no_projects_match_query": "",
|
||||
"no_projects_yet": "",
|
||||
"no_references_found": "",
|
||||
"no_references_found_in_this_file": "",
|
||||
"no_resolved_comments": "",
|
||||
@@ -1379,6 +1382,7 @@
|
||||
"no_selection_select_file": "",
|
||||
"no_symbols_found": "",
|
||||
"no_thanks_cancel_now": "",
|
||||
"no_trashed_projects": "",
|
||||
"no_trashed_references": "",
|
||||
"non_blinking_cursor": "",
|
||||
"normal": "",
|
||||
@@ -1386,6 +1390,7 @@
|
||||
"normally_x_price_per_year": "",
|
||||
"not_managed": "",
|
||||
"not_now": "",
|
||||
"nothing_shared_with_you_yet": "",
|
||||
"notification": "",
|
||||
"notification_personal_and_group_subscriptions": "",
|
||||
"notification_project_invite_accepted_message": "",
|
||||
@@ -1608,6 +1613,13 @@
|
||||
"projects": "",
|
||||
"projects_count": "",
|
||||
"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": "",
|
||||
"provider_rate_limited": "",
|
||||
"public": "",
|
||||
@@ -2573,6 +2585,7 @@
|
||||
"you_can_now_log_in_sso": "",
|
||||
"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_restore_or_permanently_delete_them_at_any_time": "",
|
||||
"you_can_select_or_invite_collaborator": "",
|
||||
"you_can_select_or_invite_collaborator_plural": "",
|
||||
"you_can_still_use_your_premium_features": "",
|
||||
@@ -2639,6 +2652,7 @@
|
||||
"your_subscription_will_pause_on": "",
|
||||
"your_subscription_will_pause_on_short": "",
|
||||
"your_subscriptions": "",
|
||||
"your_tags": "",
|
||||
"youre_a_member_of_overleaf_labs": "",
|
||||
"youre_about_to_enable_single_sign_on": "",
|
||||
"youre_about_to_enable_single_sign_on_sso_only": "",
|
||||
|
||||
BIN
Binary file not shown.
@@ -5,6 +5,7 @@
|
||||
|
||||
export default /** @type {const} */ ([
|
||||
'account_balance',
|
||||
'archive',
|
||||
'arrow_back_ios_new',
|
||||
'arrow_circle_up',
|
||||
'auto_delete',
|
||||
@@ -30,6 +31,7 @@ export default /** @type {const} */ ([
|
||||
'edit_square',
|
||||
'error',
|
||||
'experiment',
|
||||
'find_in_page',
|
||||
'find_replace',
|
||||
'format_strikethrough',
|
||||
'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 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,
|
||||
]
|
||||
)
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
--ds-nav-active-bg: var(--bg-accent-03);
|
||||
--ds-nav-active-color: var(--green-60);
|
||||
--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);
|
||||
--table-icon-bg-hover: 27 34 44;
|
||||
--themed-dashboard-popover-bg: var(--bg-dark-primary);
|
||||
@@ -15,6 +16,7 @@
|
||||
--ds-nav-active-bg: var(--green-70);
|
||||
--ds-nav-active-color: var(--green-10);
|
||||
--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);
|
||||
--table-icon-bg-hover: 255 255 255;
|
||||
--themed-dashboard-popover-bg: var(--bg-light-primary);
|
||||
@@ -404,12 +406,70 @@ body {
|
||||
}
|
||||
|
||||
.project-ds-nav-main {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
min-height: 100%;
|
||||
padding: var(--spacing-08) var(--spacing-06);
|
||||
|
||||
@include media-breakpoint-up(md) {
|
||||
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 {
|
||||
|
||||
@@ -1735,6 +1735,7 @@
|
||||
"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_ai_assistant": "No AI Assistant",
|
||||
"no_archived_projects": "No archived projects",
|
||||
"no_articles_matching_your_tags": "There are no articles matching your tags",
|
||||
"no_audit_logs_found": "No audit logs found",
|
||||
"no_borders": "No borders",
|
||||
@@ -1765,6 +1766,8 @@
|
||||
"no_preview_available": "Sorry, no preview is available.",
|
||||
"no_project_notifications_description": "You won’t be notified about this project.",
|
||||
"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_in_this_file": "No references found in this file",
|
||||
"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_symbols_found": "No symbols found",
|
||||
"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_update_email": "No, update email",
|
||||
"non_blinking_cursor": "Non-blinking cursor",
|
||||
@@ -1787,6 +1791,7 @@
|
||||
"not_now": "Not now",
|
||||
"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>",
|
||||
"nothing_shared_with_you_yet": "Nothing shared with you yet",
|
||||
"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_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_count": "Projects count",
|
||||
"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.",
|
||||
"provider_rate_limited": "Your reference provider is temporarily rate limited. Please try again later.",
|
||||
"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_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_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_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.",
|
||||
@@ -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_short": "Your subscription will pause on <0>__pauseDate__</0>.",
|
||||
"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_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.",
|
||||
|
||||
@@ -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 { expect } from 'chai'
|
||||
import { useEffect } from 'react'
|
||||
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 fetchMock from 'fetch-mock'
|
||||
import { renderWithProjectListContext } from '../../helpers/render-with-context'
|
||||
@@ -236,4 +238,56 @@ describe('<ProjectListTable />', function () {
|
||||
const allCheckboxesChecked = allCheckboxes.filter(c => c.checked)
|
||||
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