From e12c93c537109bab6d5afac0958a643d08b290cc Mon Sep 17 00:00:00 2001
From: ilkin-overleaf <100852799+ilkin-overleaf@users.noreply.github.com>
Date: Fri, 23 Sep 2022 11:37:02 +0300
Subject: [PATCH] Merge pull request #9700 from
overleaf/ii-dashboard-mobile-view
[web] Projects dashboard mobile view
GitOrigin-RevId: 84894e19c814a2cc1ce751181952c0ade6b62044
---
.../web/app/views/layout/navbar-marketing.pug | 21 +-
.../web/frontend/extracted-translations.json | 14 +-
.../current-plan-widget/free-plan.tsx | 6 +-
.../components/dropdown/actions-dropdown.tsx | 211 ++++++++++++++++
.../components/dropdown/menu-item-button.tsx | 24 ++
.../components/dropdown/projects-dropdown.tsx | 108 ++++++++
.../components/dropdown/sort-by-dropdown.tsx | 83 +++++++
.../modals/archive-project-modal.tsx | 42 ++++
.../{sidebar => modals}/create-tag-modal.tsx | 10 +-
.../modals/delete-project-modal.tsx | 37 +++
.../{sidebar => modals}/delete-tag-modal.tsx | 18 +-
.../components/modals/edit-tag-modal.tsx | 128 ++++++++++
.../components/modals/leave-project-modal.tsx | 37 +++
.../projects-action-modal.tsx | 58 +----
.../{sidebar => modals}/rename-tag-modal.tsx | 24 +-
.../components/modals/trash-project-modal.tsx | 42 ++++
.../components/new-project-button.tsx | 15 +-
.../components/project-list-root.tsx | 63 +++--
.../components/projects-filter-menu.ts | 15 ++
.../project-list/components/search-form.tsx | 53 ++--
.../components/sidebar/sidebar-filters.tsx | 27 +-
.../components/sidebar/tags-list.tsx | 110 ++------
.../components/sort/with-content.tsx | 45 ++++
.../action-buttons/archive-project-button.tsx | 55 ++--
.../action-buttons/copy-project-button.tsx | 49 ++--
.../action-buttons/delete-project-button.tsx | 54 ++--
.../download-project-button.tsx | 51 ++--
...t-buttton.tsx => leave-project-button.tsx} | 56 +++--
.../action-buttons/trash-project-button.tsx | 52 ++--
.../unarchive-project-button.tsx | 54 ++--
.../action-buttons/untrash-project-button.tsx | 52 ++--
.../components/table/cells/actions-cell.tsx | 33 +--
.../components/table/cells/inline-tags.tsx | 32 ++-
.../components/table/cells/owner-cell.tsx | 15 +-
.../table/project-list-table-row.tsx | 34 ++-
.../components/table/project-list-table.tsx | 233 ++++++++---------
.../buttons/archive-projects-button.tsx | 7 +-
.../buttons/trash-projects-button.tsx | 7 +-
.../project-list/components/tags-list.tsx | 111 +++++++++
.../components/welcome-message.tsx | 5 +-
.../context/project-list-context.tsx | 3 +
.../features/project-list/hooks/use-sort.ts | 22 ++
.../features/project-list/hooks/use-tag.tsx | 161 ++++++++++++
.../shared/components/controlled-dropdown.js | 1 +
.../new-project-button.stories.tsx | 4 +-
.../stylesheets/app/project-list-react.less | 235 ++++++++++++++++++
.../stylesheets/app/project-list.less | 1 +
.../stylesheets/components/navbar.less | 7 +-
.../frontend/stylesheets/core/spacing.less | 16 ++
.../web/frontend/stylesheets/core/type.less | 5 +-
.../frontend/stylesheets/core/utilities.less | 14 ++
services/web/locales/en.json | 5 +
.../components/new-project-button.test.tsx | 2 +-
.../components/project-list-root.test.tsx | 16 +-
.../components/project-search.test.tsx | 27 +-
.../components/sidebar/tags-list.test.tsx | 4 +-
.../archive-project-button.test.tsx | 12 +-
.../copy-project-button.test.tsx | 12 +-
.../delete-project-button.test.tsx | 12 +-
.../download-project-button.test.tsx | 4 +-
.../leave-project-button.test.tsx | 14 +-
.../trash-project-button.test.tsx | 10 +-
.../unarchive-project-button.test.tsx | 12 +-
.../untrash-project-button.test.tsx | 10 +-
.../table/project-list-table.test.tsx | 10 +-
.../table/projects-action-modal.test.tsx | 2 +-
...ith-context.js => render-with-context.tsx} | 16 +-
67 files changed, 2089 insertions(+), 639 deletions(-)
create mode 100644 services/web/frontend/js/features/project-list/components/dropdown/actions-dropdown.tsx
create mode 100644 services/web/frontend/js/features/project-list/components/dropdown/menu-item-button.tsx
create mode 100644 services/web/frontend/js/features/project-list/components/dropdown/projects-dropdown.tsx
create mode 100644 services/web/frontend/js/features/project-list/components/dropdown/sort-by-dropdown.tsx
create mode 100644 services/web/frontend/js/features/project-list/components/modals/archive-project-modal.tsx
rename services/web/frontend/js/features/project-list/components/{sidebar => modals}/create-tag-modal.tsx (95%)
create mode 100644 services/web/frontend/js/features/project-list/components/modals/delete-project-modal.tsx
rename services/web/frontend/js/features/project-list/components/{sidebar => modals}/delete-tag-modal.tsx (81%)
create mode 100644 services/web/frontend/js/features/project-list/components/modals/edit-tag-modal.tsx
create mode 100644 services/web/frontend/js/features/project-list/components/modals/leave-project-modal.tsx
rename services/web/frontend/js/features/project-list/components/{table => modals}/projects-action-modal.tsx (65%)
rename services/web/frontend/js/features/project-list/components/{sidebar => modals}/rename-tag-modal.tsx (83%)
create mode 100644 services/web/frontend/js/features/project-list/components/modals/trash-project-modal.tsx
create mode 100644 services/web/frontend/js/features/project-list/components/projects-filter-menu.ts
create mode 100644 services/web/frontend/js/features/project-list/components/sort/with-content.tsx
rename services/web/frontend/js/features/project-list/components/table/cells/action-buttons/{leave-project-buttton.tsx => leave-project-button.tsx} (60%)
create mode 100644 services/web/frontend/js/features/project-list/components/tags-list.tsx
create mode 100644 services/web/frontend/js/features/project-list/hooks/use-sort.ts
create mode 100644 services/web/frontend/js/features/project-list/hooks/use-tag.tsx
rename services/web/test/frontend/features/project-list/helpers/{render-with-context.js => render-with-context.tsx} (66%)
diff --git a/services/web/app/views/layout/navbar-marketing.pug b/services/web/app/views/layout/navbar-marketing.pug
index d679b847ab..2aee392da9 100644
--- a/services/web/app/views/layout/navbar-marketing.pug
+++ b/services/web/app/views/layout/navbar-marketing.pug
@@ -9,6 +9,17 @@ nav.navbar.navbar-default.navbar-main
aria-label="Toggle " + translate('navigation')
)
i.fa.fa-bars(aria-hidden="true")
+ if (usersBestSubscription && usersBestSubscription.type === 'free')
+ a.btn.btn-primary.pull-right.me-2.visible-xs(
+ href="/user/subscription/plans"
+ event-tracking="upgrade-button-click"
+ event-tracking-mb="true"
+ event-tracking-ga="subscription-funnel"
+ event-tracking-action="dashboard-top"
+ event-tracking-label="upgrade"
+ event-tracking-trigger="click"
+ event-segmentation='{"source": "dashboard-top"}'
+ ) #{translate("upgrade")}
if settings.nav.custom_logo
a(href='/', aria-label=settings.appName, style='background-image:url("'+settings.nav.custom_logo+'")').navbar-brand
else if (nav.title)
@@ -57,9 +68,9 @@ nav.navbar.navbar-default.navbar-main
each item in ((splitTestVariants && (splitTestVariants['unified-navigation'] === 'show-unified-navigation')) ? nav.header_extras_unified : nav.header_extras)
-
if ((item.only_when_logged_in && getSessionUser())
- || (item.only_when_logged_out && (!getSessionUser()))
- || (!item.only_when_logged_out && !item.only_when_logged_in && !item.only_content_pages)
- || (item.only_content_pages && (typeof(suppressNavContentLinks) == "undefined" || !suppressNavContentLinks))
+ || (item.only_when_logged_out && (!getSessionUser()))
+ || (!item.only_when_logged_out && !item.only_when_logged_in && !item.only_content_pages)
+ || (item.only_content_pages && (typeof(suppressNavContentLinks) == "undefined" || !suppressNavContentLinks))
){
var showNavItem = true
} else {
@@ -82,7 +93,7 @@ nav.navbar.navbar-default.navbar-main
each child in item.dropdown
if child.divider
li.divider
- if child.splitTest
+ if child.splitTest
if (splitTestVariants && (splitTestVariants[child.splitTest.name] === child.splitTest.variant))
li
if child.url
@@ -99,7 +110,7 @@ nav.navbar.navbar-default.navbar-main
li
if child.url
a(
- href=child.url,
+ href=child.url,
class=child.class,
event-tracking=child.event
event-tracking-mb="true"
diff --git a/services/web/frontend/extracted-translations.json b/services/web/frontend/extracted-translations.json
index 59bbc42c89..80cd045a94 100644
--- a/services/web/frontend/extracted-translations.json
+++ b/services/web/frontend/extracted-translations.json
@@ -153,6 +153,7 @@
"edit_dictionary": "",
"edit_dictionary_empty": "",
"edit_dictionary_remove": "",
+ "edit_folder": "",
"editing": "",
"editor_and_pdf": "",
"editor_only_hide_pdf": "",
@@ -209,6 +210,7 @@
"galileo_insert_instruction_button": "",
"galileo_insert_math_button": "",
"galileo_is": "",
+ "galileo_only_available_in_cm6": "",
"galileo_promo_autocomplete_content": "",
"galileo_promo_autocomplete_title": "",
"galileo_promo_shadow_text_content": "",
@@ -218,7 +220,6 @@
"galileo_suggestion_feedback_button": "",
"galileo_suggestions_loading_error": "",
"galileo_toggle_description": "",
- "galileo_only_available_in_cm6": "",
"generic_linked_file_compile_error": "",
"generic_something_went_wrong": "",
"get_collaborative_benefits": "",
@@ -303,6 +304,9 @@
"is_email_affiliated": "",
"join_project": "",
"joining": "",
+ "labs_program_already_participating": "",
+ "labs_program_benefits": "<0>0>",
+ "labs_program_not_participating": "",
"last_modified": "",
"last_name": "",
"last_updated_date_by_x": "",
@@ -353,6 +357,7 @@
"make_private": "",
"manage_beta_program_membership": "",
"manage_files_from_your_dropbox_folder": "",
+ "manage_labs_program_membership": "",
"manage_newsletter": "",
"manage_sessions": "",
"math_display": "",
@@ -410,10 +415,7 @@
"other_logs_and_files": "",
"other_output_files": "",
"overleaf_labs": "",
- "labs_program_benefits": "",
- "labs_program_already_participating": "",
- "labs_program_not_participating": "",
- "manage_labs_program_membership": "",
+ "owned_by": "",
"owner": "",
"page_current": "",
"pagination_navigation": "",
@@ -511,6 +513,7 @@
"revoke": "",
"revoke_invite": "",
"role": "",
+ "save_changes": "",
"save_or_cancel-cancel": "",
"save_or_cancel-or": "",
"save_or_cancel-save": "",
@@ -569,6 +572,7 @@
"something_went_wrong_rendering_pdf": "",
"something_went_wrong_server": "",
"somthing_went_wrong_compiling": "",
+ "sort_by": "",
"sort_by_x": "",
"sso_link_error": "",
"start_by_adding_your_email": "",
diff --git a/services/web/frontend/js/features/project-list/components/current-plan-widget/free-plan.tsx b/services/web/frontend/js/features/project-list/components/current-plan-widget/free-plan.tsx
index 161ea38528..0f0122ecb2 100644
--- a/services/web/frontend/js/features/project-list/components/current-plan-widget/free-plan.tsx
+++ b/services/web/frontend/js/features/project-list/components/current-plan-widget/free-plan.tsx
@@ -13,6 +13,9 @@ function FreePlan() {
return (
<>
+
+ }} />
+
}} />{' '}
@@ -28,6 +31,7 @@ function FreePlan() {
{' '}
}
+ bodyBottom={
+
+ {t('archiving_projects_wont_affect_collaborators')}{' '}
+
+ {t('find_out_more_nt')}
+
+
+ }
+ showModal={showModal}
+ handleCloseModal={handleCloseModal}
+ projects={projects}
+ />
+ )
+}
+
+export default ArchiveProjectModal
diff --git a/services/web/frontend/js/features/project-list/components/sidebar/create-tag-modal.tsx b/services/web/frontend/js/features/project-list/components/modals/create-tag-modal.tsx
similarity index 95%
rename from services/web/frontend/js/features/project-list/components/sidebar/create-tag-modal.tsx
rename to services/web/frontend/js/features/project-list/components/modals/create-tag-modal.tsx
index 569d62a264..3b71974366 100644
--- a/services/web/frontend/js/features/project-list/components/sidebar/create-tag-modal.tsx
+++ b/services/web/frontend/js/features/project-list/components/modals/create-tag-modal.tsx
@@ -8,12 +8,14 @@ import { createTag } from '../../util/api'
import { MAX_TAG_LENGTH } from '../../util/tag'
type CreateTagModalProps = {
+ id: string
show: boolean
onCreate: (tag: Tag) => void
onClose: () => void
}
export default function CreateTagModal({
+ id,
show,
onCreate,
onClose,
@@ -55,13 +57,7 @@ export default function CreateTagModal({
}
return (
-
+
{t('create_new_folder')}
diff --git a/services/web/frontend/js/features/project-list/components/modals/delete-project-modal.tsx b/services/web/frontend/js/features/project-list/components/modals/delete-project-modal.tsx
new file mode 100644
index 0000000000..4fb6260f4c
--- /dev/null
+++ b/services/web/frontend/js/features/project-list/components/modals/delete-project-modal.tsx
@@ -0,0 +1,37 @@
+import { useTranslation } from 'react-i18next'
+import ProjectsActionModal from './projects-action-modal'
+import Icon from '../../../../shared/components/icon'
+
+type DeleteProjectModalProps = Pick<
+ React.ComponentProps,
+ 'projects' | 'actionHandler' | 'showModal' | 'handleCloseModal'
+>
+
+function DeleteProjectModal({
+ projects,
+ actionHandler,
+ showModal,
+ handleCloseModal,
+}: DeleteProjectModalProps) {
+ const { t } = useTranslation()
+
+ return (
+ {t('about_to_delete_projects')}}
+ bodyBottom={
+
+ {' '}
+ {t('this_action_cannot_be_undone')}
+
+ }
+ showModal={showModal}
+ handleCloseModal={handleCloseModal}
+ projects={projects}
+ />
+ )
+}
+
+export default DeleteProjectModal
diff --git a/services/web/frontend/js/features/project-list/components/sidebar/delete-tag-modal.tsx b/services/web/frontend/js/features/project-list/components/modals/delete-tag-modal.tsx
similarity index 81%
rename from services/web/frontend/js/features/project-list/components/sidebar/delete-tag-modal.tsx
rename to services/web/frontend/js/features/project-list/components/modals/delete-tag-modal.tsx
index f362f91ab2..1d04dfb391 100644
--- a/services/web/frontend/js/features/project-list/components/sidebar/delete-tag-modal.tsx
+++ b/services/web/frontend/js/features/project-list/components/modals/delete-tag-modal.tsx
@@ -7,18 +7,20 @@ import useAsync from '../../../../shared/hooks/use-async'
import { deleteTag } from '../../util/api'
type DeleteTagModalProps = {
+ id: string
tag?: Tag
onDelete: (tagId: string) => void
onClose: () => void
}
export default function DeleteTagModal({
+ id,
tag,
onDelete,
onClose,
}: DeleteTagModalProps) {
const { t } = useTranslation()
- const { isError, runAsync, status } = useAsync()
+ const { isLoading, isError, runAsync } = useAsync()
const runDeleteTag = useCallback(
(tagId: string) => {
@@ -36,13 +38,7 @@ export default function DeleteTagModal({
}
return (
-
+
{t('delete_folder')}
@@ -62,15 +58,15 @@ export default function DeleteTagModal({
)}
-
+
{t('cancel')}
runDeleteTag(tag._id)}
bsStyle="primary"
- disabled={status === 'pending'}
+ disabled={isLoading}
>
- {status === 'pending' ? t('deleting') + '…' : t('delete')}
+ {isLoading ? <>{t('deleting')} …> : t('delete')}
diff --git a/services/web/frontend/js/features/project-list/components/modals/edit-tag-modal.tsx b/services/web/frontend/js/features/project-list/components/modals/edit-tag-modal.tsx
new file mode 100644
index 0000000000..f575057da5
--- /dev/null
+++ b/services/web/frontend/js/features/project-list/components/modals/edit-tag-modal.tsx
@@ -0,0 +1,128 @@
+import { useCallback, useState } from 'react'
+import { useTranslation } from 'react-i18next'
+import { Button, Form, Modal } from 'react-bootstrap'
+import AccessibleModal from '../../../../shared/components/accessible-modal'
+import useAsync from '../../../../shared/hooks/use-async'
+import { deleteTag, renameTag } from '../../util/api'
+import { Tag } from '../../../../../../app/src/Features/Tags/types'
+
+type EditTagModalProps = {
+ id: string
+ tag?: Tag
+ onRename: (tagId: string, newTagName: string) => void
+ onDelete: (tagId: string) => void
+ onClose: () => void
+}
+
+export default function EditTagModal({
+ id,
+ tag,
+ onRename,
+ onDelete,
+ onClose,
+}: EditTagModalProps) {
+ const { t } = useTranslation()
+ const {
+ isLoading: isDeleteLoading,
+ isError: isDeleteError,
+ runAsync: runDeleteAsync,
+ } = useAsync()
+ const {
+ isLoading: isRenameLoading,
+ isError: isRenameError,
+ runAsync: runRenameAsync,
+ } = useAsync()
+ const [newTagName, setNewTagName] = useState()
+
+ const runDeleteTag = useCallback(
+ (tagId: string) => {
+ runDeleteAsync(deleteTag(tagId))
+ .then(() => {
+ onDelete(tagId)
+ })
+ .catch(console.error)
+ },
+ [runDeleteAsync, onDelete]
+ )
+
+ const runRenameTag = useCallback(
+ (tagId: string) => {
+ if (newTagName) {
+ runRenameAsync(renameTag(tagId, newTagName))
+ .then(() => onRename(tagId, newTagName))
+ .catch(console.error)
+ }
+ },
+ [runRenameAsync, newTagName, onRename]
+ )
+
+ const handleSubmit = useCallback(
+ e => {
+ e.preventDefault()
+ if (tag) {
+ runRenameTag(tag._id)
+ }
+ },
+ [tag, runRenameTag]
+ )
+
+ if (!tag) {
+ return null
+ }
+
+ return (
+
+
+ {t('edit_folder')}
+
+
+
+
+
+
+
+
+
+ runDeleteTag(tag._id)}
+ bsStyle="primary"
+ disabled={isDeleteLoading || isRenameLoading}
+ >
+ {isDeleteLoading ? <>{t('deleting')} …> : t('delete')}
+
+
+
+ {t('cancel')}
+
+
runRenameTag(tag._id)}
+ bsStyle="primary"
+ disabled={isRenameLoading || isDeleteLoading || !newTagName?.length}
+ >
+ {isRenameLoading ? <>{t('saving')} …> : t('save_changes')}
+
+
+ {(isDeleteError || isRenameError) && (
+
+
+ {t('generic_something_went_wrong')}
+
+
+ )}
+
+
+ )
+}
diff --git a/services/web/frontend/js/features/project-list/components/modals/leave-project-modal.tsx b/services/web/frontend/js/features/project-list/components/modals/leave-project-modal.tsx
new file mode 100644
index 0000000000..f708caa01d
--- /dev/null
+++ b/services/web/frontend/js/features/project-list/components/modals/leave-project-modal.tsx
@@ -0,0 +1,37 @@
+import { useTranslation } from 'react-i18next'
+import ProjectsActionModal from './projects-action-modal'
+import Icon from '../../../../shared/components/icon'
+
+type LeaveProjectModalProps = Pick<
+ React.ComponentProps,
+ 'projects' | 'actionHandler' | 'showModal' | 'handleCloseModal'
+>
+
+function LeaveProjectModal({
+ projects,
+ actionHandler,
+ showModal,
+ handleCloseModal,
+}: LeaveProjectModalProps) {
+ const { t } = useTranslation()
+
+ return (
+ {t('about_to_leave_projects')}}
+ bodyBottom={
+
+ {' '}
+ {t('this_action_cannot_be_undone')}
+
+ }
+ showModal={showModal}
+ handleCloseModal={handleCloseModal}
+ projects={projects}
+ />
+ )
+}
+
+export default LeaveProjectModal
diff --git a/services/web/frontend/js/features/project-list/components/table/projects-action-modal.tsx b/services/web/frontend/js/features/project-list/components/modals/projects-action-modal.tsx
similarity index 65%
rename from services/web/frontend/js/features/project-list/components/table/projects-action-modal.tsx
rename to services/web/frontend/js/features/project-list/components/modals/projects-action-modal.tsx
index 87acb89e02..7c9df72b77 100644
--- a/services/web/frontend/js/features/project-list/components/table/projects-action-modal.tsx
+++ b/services/web/frontend/js/features/project-list/components/modals/projects-action-modal.tsx
@@ -6,24 +6,28 @@ import AccessibleModal from '../../../../shared/components/accessible-modal'
import { getUserFacingMessage } from '../../../../infrastructure/fetch-json'
import useIsMounted from '../../../../shared/hooks/use-is-mounted'
import * as eventTracking from '../../../../infrastructure/event-tracking'
-import Icon from '../../../../shared/components/icon'
type ProjectsActionModalProps = {
+ title?: string
action: 'archive' | 'trash' | 'delete' | 'leave'
actionHandler: (project: Project) => Promise
handleCloseModal: () => void
+ bodyTop?: React.ReactNode
+ bodyBottom?: React.ReactNode
projects: Array
showModal: boolean
}
function ProjectsActionModal({
+ title,
action,
actionHandler,
handleCloseModal,
+ bodyTop,
+ bodyBottom,
showModal,
projects,
}: ProjectsActionModalProps) {
- let bodyTop, bodyBottom, title
const { t } = useTranslation()
const [errors, setErrors] = useState>([])
const [isProcessing, setIsProcessing] = useState(false)
@@ -63,56 +67,6 @@ function ProjectsActionModal({
}
}, [action, showModal])
- if (action === 'archive') {
- title = t('archive_projects')
- bodyTop = {t('about_to_archive_projects')}
- bodyBottom = (
-
- {t('archiving_projects_wont_affect_collaborators')}{' '}
-
- {t('find_out_more_nt')}
-
-
- )
- } else if (action === 'leave') {
- title = t('leave_projects')
- bodyTop = {t('about_to_leave_projects')}
- bodyBottom = (
-
- {' '}
- {t('this_action_cannot_be_undone')}
-
- )
- } else if (action === 'trash') {
- title = t('trash_projects')
- bodyTop = {t('about_to_trash_projects')}
- bodyBottom = (
-
- {t('trashing_projects_wont_affect_collaborators')}{' '}
-
- {t('find_out_more_nt')}
-
-
- )
- } else if (action === 'delete') {
- title = t('delete_projects')
- bodyTop = {t('about_to_delete_projects')}
- bodyBottom = (
-
- {' '}
- {t('this_action_cannot_be_undone')}
-
- )
- }
-
return (
void
onClose: () => void
}
export default function RenameTagModal({
+ id,
tag,
onRename,
onClose,
}: RenameTagModalProps) {
const { t } = useTranslation()
- const { isError, runAsync, status } = useAsync()
+ const { isLoading, isError, runAsync } = useAsync()
- const [newTagName, setNewTageName] = useState()
+ const [newTagName, setNewTagName] = useState()
const [validationError, setValidationError] = useState()
const runRenameTag = useCallback(
@@ -60,13 +62,7 @@ export default function RenameTagModal({
}
return (
-
+
{t('rename_folder')}
@@ -80,7 +76,7 @@ export default function RenameTagModal({
name="new-tag-name"
value={newTagName === undefined ? tag.name : newTagName}
required
- onChange={e => setNewTageName(e.target.value)}
+ onChange={e => setNewTagName(e.target.value)}
/>
@@ -98,17 +94,15 @@ export default function RenameTagModal({
)}
-
+
{t('cancel')}
runRenameTag(tag._id)}
bsStyle="primary"
- disabled={
- status === 'pending' || !newTagName?.length || !!validationError
- }
+ disabled={isLoading || !newTagName?.length || !!validationError}
>
- {status === 'pending' ? t('renaming') + '…' : t('rename')}
+ {isLoading ? <>{t('renaming')} …> : t('rename')}
diff --git a/services/web/frontend/js/features/project-list/components/modals/trash-project-modal.tsx b/services/web/frontend/js/features/project-list/components/modals/trash-project-modal.tsx
new file mode 100644
index 0000000000..7a3962e937
--- /dev/null
+++ b/services/web/frontend/js/features/project-list/components/modals/trash-project-modal.tsx
@@ -0,0 +1,42 @@
+import { useTranslation } from 'react-i18next'
+import ProjectsActionModal from './projects-action-modal'
+
+type TrashProjectPropsModalProps = Pick<
+ React.ComponentProps,
+ 'projects' | 'actionHandler' | 'showModal' | 'handleCloseModal'
+>
+
+function TrashProjectModal({
+ projects,
+ actionHandler,
+ showModal,
+ handleCloseModal,
+}: TrashProjectPropsModalProps) {
+ const { t } = useTranslation()
+
+ return (
+ {t('about_to_trash_projects')}}
+ bodyBottom={
+
+ {t('trashing_projects_wont_affect_collaborators')}{' '}
+
+ {t('find_out_more_nt')}
+
+
+ }
+ showModal={showModal}
+ handleCloseModal={handleCloseModal}
+ projects={projects}
+ />
+ )
+}
+
+export default TrashProjectModal
diff --git a/services/web/frontend/js/features/project-list/components/new-project-button.tsx b/services/web/frontend/js/features/project-list/components/new-project-button.tsx
index 7d848d2604..2fbdc3551b 100644
--- a/services/web/frontend/js/features/project-list/components/new-project-button.tsx
+++ b/services/web/frontend/js/features/project-list/components/new-project-button.tsx
@@ -7,8 +7,19 @@ import getMeta from '../../../utils/meta'
import NewProjectButtonModal, {
NewProjectButtonModalVariant,
} from './new-project-button/new-project-button-modal'
+import { Nullable } from '../../../../../types/utils'
-function NewProjectButton({ buttonText }: { buttonText?: string }) {
+type NewProjectButtonProps = {
+ id: string
+ buttonText?: string
+ className?: string
+}
+
+function NewProjectButton({
+ id,
+ buttonText,
+ className,
+}: NewProjectButtonProps) {
const { t } = useTranslation()
const { templateLinks } = getMeta('ol-ExposedSettings') as ExposedSettings
const [modal, setModal] =
@@ -16,7 +27,7 @@ function NewProjectButton({ buttonText }: { buttonText?: string }) {
return (
<>
-
+
) : (
-
-
+
+
{error ?
: ''}
{totalProjectsCount > 0 ? (
<>
-
+
+
-
-
+
+
-
+
- {selectedProjects.length === 0 ? (
+
+ {selectedProjects.length === 0 ? (
+
+ ) : (
+
+ )}
+
+
+
-
+
-
+
>
) : (
diff --git a/services/web/frontend/js/features/project-list/components/projects-filter-menu.ts b/services/web/frontend/js/features/project-list/components/projects-filter-menu.ts
new file mode 100644
index 0000000000..7c45debd95
--- /dev/null
+++ b/services/web/frontend/js/features/project-list/components/projects-filter-menu.ts
@@ -0,0 +1,15 @@
+import { Filter, useProjectListContext } from '../context/project-list-context'
+
+type ProjectsMenuFilterType = {
+ children: (isActive: boolean) => React.ReactElement
+ filter: Filter
+}
+
+function ProjectsFilterMenu({ children, filter }: ProjectsMenuFilterType) {
+ const { filter: activeFilter, selectedTagId } = useProjectListContext()
+ const isActive = selectedTagId === undefined && filter === activeFilter
+
+ return children(isActive)
+}
+
+export default ProjectsFilterMenu
diff --git a/services/web/frontend/js/features/project-list/components/search-form.tsx b/services/web/frontend/js/features/project-list/components/search-form.tsx
index fb3c70c463..c15a92316f 100644
--- a/services/web/frontend/js/features/project-list/components/search-form.tsx
+++ b/services/web/frontend/js/features/project-list/components/search-form.tsx
@@ -1,21 +1,35 @@
-import { useState, useEffect } from 'react'
import { useTranslation } from 'react-i18next'
-import { Form, FormGroup, Col, FormControl } from 'react-bootstrap'
+import {
+ Form,
+ FormGroup,
+ FormGroupProps,
+ Col,
+ FormControl,
+} from 'react-bootstrap'
import Icon from '../../../shared/components/icon'
import * as eventTracking from '../../../infrastructure/event-tracking'
+import classnames from 'classnames'
-type SearchFormProps = {
- onChange: (input: string) => void
+type SearchFormOwnProps = {
+ inputValue: string
+ setInputValue: (input: string) => void
+ formGroupProps?: FormGroupProps &
+ Omit, keyof FormGroupProps>
}
-function SearchForm({ onChange }: SearchFormProps) {
- const { t } = useTranslation()
- const [input, setInput] = useState('')
- const placeholder = `${t('search_projects')}…`
+type SearchFormProps = SearchFormOwnProps &
+ Omit, keyof SearchFormOwnProps>
- useEffect(() => {
- onChange(input)
- }, [input, onChange])
+function SearchForm({
+ inputValue,
+ setInputValue,
+ formGroupProps,
+ ...props
+}: SearchFormProps) {
+ const { t } = useTranslation()
+ const placeholder = `${t('search_projects')}…`
+ const { className: formGroupClassName, ...restFormGroupProps } =
+ formGroupProps || {}
const handleChange = (
e: React.ChangeEvent<
@@ -27,10 +41,10 @@ function SearchForm({ onChange }: SearchFormProps) {
'project-search',
'keydown'
)
- setInput(e.target.value)
+ setInputValue(e.target.value)
}
- const handleClear = () => setInput('')
+ const handleClear = () => setInputValue('')
return (