From a2cdbd52dcc734ddf9336d355f79052b293ba50a Mon Sep 17 00:00:00 2001 From: Prateek Shourya Date: Fri, 31 May 2024 18:30:38 +0530 Subject: [PATCH] [WEB-1436] chore: pages improvement. (#4657) * add empty state if no pages are available. * set access to private in create page modal when the modal is open form private tab. --- .../actions/project-actions.tsx | 2 +- .../command-palette/command-palette.tsx | 9 +++--- web/components/headers/pages.tsx | 13 ++++---- .../pages/dropdowns/quick-actions.tsx | 7 +---- web/components/pages/header/root.tsx | 4 +-- web/components/pages/list/root.tsx | 2 +- web/components/pages/list/search-input.tsx | 7 ++--- .../pages/modals/create-page-modal.tsx | 15 ++++++---- .../pages/modals/delete-page-modal.tsx | 5 ++-- .../pages/pages-list-main-content.tsx | 23 ++++++++++---- web/components/pages/pages-list-view.tsx | 10 +++---- web/constants/empty-state.ts | 5 ---- web/constants/page.ts | 10 +++++++ web/hooks/store/pages/use-project-page.ts | 5 +--- .../projects/[projectId]/pages/[pageId].tsx | 2 +- web/store/command-palette.store.ts | 30 ++++++++++++------- web/store/pages/project-page.store.ts | 10 +++++++ 17 files changed, 95 insertions(+), 64 deletions(-) diff --git a/web/components/command-palette/actions/project-actions.tsx b/web/components/command-palette/actions/project-actions.tsx index 32e7ed045..ed4bdcadc 100644 --- a/web/components/command-palette/actions/project-actions.tsx +++ b/web/components/command-palette/actions/project-actions.tsx @@ -71,7 +71,7 @@ export const CommandPaletteProjectActions: React.FC = (props) => { onSelect={() => { closePalette(); setTrackElement("Command palette"); - toggleCreatePageModal(true); + toggleCreatePageModal({ isOpen: true }); }} className="focus:outline-none" > diff --git a/web/components/command-palette/command-palette.tsx b/web/components/command-palette/command-palette.tsx index 9143d44c7..41afbb5e2 100644 --- a/web/components/command-palette/command-palette.tsx +++ b/web/components/command-palette/command-palette.tsx @@ -50,7 +50,7 @@ export const CommandPalette: FC = observer(() => { toggleCreateIssueModal, isCreateCycleModalOpen, toggleCreateCycleModal, - isCreatePageModalOpen, + createPageModal, toggleCreatePageModal, isCreateProjectModalOpen, toggleCreateProjectModal, @@ -150,7 +150,7 @@ export const CommandPalette: FC = observer(() => { d: { title: "Create a new page", description: "Create a new page in the current project", - action: () => toggleCreatePageModal(true), + action: () => toggleCreatePageModal({ isOpen: true }), }, m: { title: "Create a new module", @@ -297,8 +297,9 @@ export const CommandPalette: FC = observer(() => { toggleCreatePageModal(false)} + isModalOpen={createPageModal.isOpen} + pageAccess={createPageModal.pageAccess} + handleModalClose={() => toggleCreatePageModal({ isOpen: false })} redirectionEnabled /> diff --git a/web/components/headers/pages.tsx b/web/components/headers/pages.tsx index b2914688c..893c4409d 100644 --- a/web/components/headers/pages.tsx +++ b/web/components/headers/pages.tsx @@ -1,20 +1,20 @@ import { observer } from "mobx-react"; import { useRouter } from "next/router"; import { FileText } from "lucide-react"; -// hooks // ui import { Breadcrumbs, Button } from "@plane/ui"; // helpers import { BreadcrumbLink, Logo } from "@/components/common"; -import { EUserProjectRoles } from "@/constants/project"; // constants -// components +import { EPageAccess } from "@/constants/page"; +import { EUserProjectRoles } from "@/constants/project"; +// hooks import { useCommandPalette, useEventTracker, useProject, useUser } from "@/hooks/store"; export const PagesHeader = observer(() => { // router const router = useRouter(); - const { workspaceSlug } = router.query; + const { workspaceSlug, type: pageType } = router.query; // store hooks const { toggleCreatePageModal } = useCommandPalette(); const { @@ -61,7 +61,10 @@ export const PagesHeader = observer(() => { size="sm" onClick={() => { setTrackElement("Project pages page"); - toggleCreatePageModal(true); + toggleCreatePageModal({ + isOpen: true, + pageAccess: pageType === "private" ? EPageAccess.PRIVATE : EPageAccess.PUBLIC, + }); }} > Add Page diff --git a/web/components/pages/dropdowns/quick-actions.tsx b/web/components/pages/dropdowns/quick-actions.tsx index e218c1389..ab0438f84 100644 --- a/web/components/pages/dropdowns/quick-actions.tsx +++ b/web/components/pages/dropdowns/quick-actions.tsx @@ -85,12 +85,7 @@ export const PageQuickActions: React.FC = observer((props) => { return ( <> - setDeletePageModal(false)} - pageId={pageId} - projectId={projectId} - /> + setDeletePageModal(false)} pageId={pageId} /> {MENU_ITEMS.map((item) => { diff --git a/web/components/pages/header/root.tsx b/web/components/pages/header/root.tsx index b55fd7917..ec7aaddf1 100644 --- a/web/components/pages/header/root.tsx +++ b/web/components/pages/header/root.tsx @@ -25,7 +25,7 @@ type Props = { export const PagesListHeaderRoot: React.FC = observer((props) => { const { pageType, projectId, workspaceSlug } = props; // store hooks - const { filters, updateFilters, clearAllFilters } = useProjectPages(projectId); + const { filters, updateFilters, clearAllFilters } = useProjectPages(); const { workspace: { workspaceMemberIds }, } = useMember(); @@ -52,7 +52,7 @@ export const PagesListHeaderRoot: React.FC = observer((props) => {
- + = observer((props) => { const { pageType, projectId, workspaceSlug } = props; // store hooks - const { getCurrentProjectFilteredPageIds } = useProjectPages(projectId); + const { getCurrentProjectFilteredPageIds } = useProjectPages(); // derived values const filteredPageIds = getCurrentProjectFilteredPageIds(pageType); diff --git a/web/components/pages/list/search-input.tsx b/web/components/pages/list/search-input.tsx index b6aa05790..957c08183 100644 --- a/web/components/pages/list/search-input.tsx +++ b/web/components/pages/list/search-input.tsx @@ -5,15 +5,12 @@ import { cn } from "@/helpers/common.helper"; import { useProjectPages } from "@/hooks/store"; import useOutsideClickDetector from "@/hooks/use-outside-click-detector"; -export type TPageSearchInput = { projectId: string }; - -export const PageSearchInput: FC = observer((props) => { - const { projectId } = props; +export const PageSearchInput: FC = observer(() => { // hooks const { filters: { searchQuery }, updateFilters, - } = useProjectPages(projectId); + } = useProjectPages(); // states const [isSearchOpen, setIsSearchOpen] = useState(false); // refs diff --git a/web/components/pages/modals/create-page-modal.tsx b/web/components/pages/modals/create-page-modal.tsx index ea3f44737..747cba275 100644 --- a/web/components/pages/modals/create-page-modal.tsx +++ b/web/components/pages/modals/create-page-modal.tsx @@ -1,4 +1,4 @@ -import { FC, useState } from "react"; +import { FC, useEffect, useState } from "react"; import { useRouter } from "next/router"; // types import { TPage } from "@plane/types"; @@ -15,29 +15,34 @@ type Props = { workspaceSlug: string; projectId: string; isModalOpen: boolean; + pageAccess?: EPageAccess; handleModalClose: () => void; redirectionEnabled?: boolean; }; export const CreatePageModal: FC = (props) => { - const { workspaceSlug, projectId, isModalOpen, handleModalClose, redirectionEnabled = false } = props; + const { workspaceSlug, projectId, isModalOpen, pageAccess, handleModalClose, redirectionEnabled = false } = props; // states const [pageFormData, setPageFormData] = useState>({ id: undefined, name: "", - access: EPageAccess.PUBLIC, logo_props: undefined, }); // router const router = useRouter(); // store hooks - const { createPage } = useProjectPages(projectId); + const { createPage } = useProjectPages(); const { capturePageEvent } = useEventTracker(); const handlePageFormData = (key: T, value: TPage[T]) => setPageFormData((prev) => ({ ...prev, [key]: value })); + // update page access in form data when page access from the store changes + useEffect(() => { + setPageFormData((prev) => ({ ...prev, access: pageAccess })); + }, [pageAccess]); + const handleStateClear = () => { - setPageFormData({ id: undefined, name: "", access: EPageAccess.PUBLIC }); + setPageFormData({ id: undefined, name: "", access: pageAccess }); handleModalClose(); }; diff --git a/web/components/pages/modals/delete-page-modal.tsx b/web/components/pages/modals/delete-page-modal.tsx index 61a2e2ad9..f2e749a4a 100644 --- a/web/components/pages/modals/delete-page-modal.tsx +++ b/web/components/pages/modals/delete-page-modal.tsx @@ -13,15 +13,14 @@ type TConfirmPageDeletionProps = { isOpen: boolean; onClose: () => void; pageId: string; - projectId: string; }; export const DeletePageModal: React.FC = observer((props) => { - const { pageId, projectId, isOpen, onClose } = props; + const { pageId, isOpen, onClose } = props; // states const [isDeleting, setIsDeleting] = useState(false); // store hooks - const { removePage } = useProjectPages(projectId); + const { removePage } = useProjectPages(); const { capturePageEvent } = useEventTracker(); const page = usePage(pageId); diff --git a/web/components/pages/pages-list-main-content.tsx b/web/components/pages/pages-list-main-content.tsx index 3e8fc9fdd..fbb2bb1f5 100644 --- a/web/components/pages/pages-list-main-content.tsx +++ b/web/components/pages/pages-list-main-content.tsx @@ -8,6 +8,7 @@ import { PageLoader } from "@/components/pages"; // constants import { EmptyStateType } from "@/constants/empty-state"; // hooks +import { EPageAccess } from "@/constants/page"; import { useCommandPalette, useProjectPages } from "@/hooks/store"; // assets import AllFiltersImage from "public/empty-state/pages/all-filters.svg"; @@ -16,13 +17,13 @@ import NameFilterImage from "public/empty-state/pages/name-filter.svg"; type Props = { children: React.ReactNode; pageType: TPageNavigationTabs; - projectId: string; }; export const PagesListMainContent: React.FC = observer((props) => { - const { children, pageType, projectId } = props; + const { children, pageType } = props; // store hooks - const { loader, getCurrentProjectFilteredPageIds, getCurrentProjectPageIds, filters } = useProjectPages(projectId); + const { loader, isAnyPageAvailable, getCurrentProjectFilteredPageIds, getCurrentProjectPageIds, filters } = + useProjectPages(); const { toggleCreatePageModal } = useCommandPalette(); // derived values const pageIds = getCurrentProjectPageIds(pageType); @@ -30,13 +31,23 @@ export const PagesListMainContent: React.FC = observer((props) => { if (loader === "init-loader") return ; // if no pages exist in the active page type - if (pageIds?.length === 0) { + if (!isAnyPageAvailable || pageIds?.length === 0) { + if (!isAnyPageAvailable) { + return ( + { + toggleCreatePageModal({ isOpen: true }); + }} + /> + ); + } if (pageType === "public") return ( { - toggleCreatePageModal(true); + toggleCreatePageModal({ isOpen: true, pageAccess: EPageAccess.PUBLIC }); }} /> ); @@ -45,7 +56,7 @@ export const PagesListMainContent: React.FC = observer((props) => { { - toggleCreatePageModal(true); + toggleCreatePageModal({ isOpen: true, pageAccess: EPageAccess.PRIVATE }); }} /> ); diff --git a/web/components/pages/pages-list-view.tsx b/web/components/pages/pages-list-view.tsx index 7f12cfc92..bfaf0fe72 100644 --- a/web/components/pages/pages-list-view.tsx +++ b/web/components/pages/pages-list-view.tsx @@ -16,7 +16,7 @@ type TPageView = { export const PagesListView: React.FC = observer((props) => { const { workspaceSlug, projectId, pageType, children } = props; // store hooks - const { getAllPages } = useProjectPages(projectId); + const { isAnyPageAvailable, getAllPages } = useProjectPages(); // fetching pages list useSWR(projectId ? `PROJECT_PAGES_${projectId}` : null, projectId ? () => getAllPages(pageType) : null); @@ -24,10 +24,10 @@ export const PagesListView: React.FC = observer((props) => { return (
{/* tab header */} - - - {children} - + {isAnyPageAvailable && ( + + )} + {children}
); }); diff --git a/web/constants/empty-state.ts b/web/constants/empty-state.ts index 9ca6f4edb..fcf975f1b 100644 --- a/web/constants/empty-state.ts +++ b/web/constants/empty-state.ts @@ -486,11 +486,6 @@ const emptyStateDetails = { path: "/empty-state/onboarding/pages", primaryButton: { text: "Create your first page", - comicBox: { - title: "A page can be a doc or a doc of docs.", - description: - "We wrote Nikhil and Meera’s love story. You could write your project’s mission, goals, and eventual vision.", - }, }, accessType: "project", access: EUserProjectRoles.MEMBER, diff --git a/web/constants/page.ts b/web/constants/page.ts index f196b6152..20314b263 100644 --- a/web/constants/page.ts +++ b/web/constants/page.ts @@ -32,3 +32,13 @@ export const PAGE_SORT_BY_OPTIONS: { { key: "asc", label: "Ascending" }, { key: "desc", label: "Descending" }, ]; + +export type TCreatePageModal = { + isOpen: boolean; + pageAccess?: EPageAccess; +} + +export const DEFAULT_CREATE_PAGE_MODAL_DATA: TCreatePageModal = { + isOpen: false, + pageAccess: EPageAccess.PUBLIC, +}; diff --git a/web/hooks/store/pages/use-project-page.ts b/web/hooks/store/pages/use-project-page.ts index 2749ab87d..c6fb088d1 100644 --- a/web/hooks/store/pages/use-project-page.ts +++ b/web/hooks/store/pages/use-project-page.ts @@ -4,11 +4,8 @@ import { StoreContext } from "@/lib/store-context"; // mobx store import { IProjectPageStore } from "@/store/pages/project-page.store"; -export const useProjectPages = (projectId: string | undefined): IProjectPageStore => { +export const useProjectPages = (): IProjectPageStore => { const context = useContext(StoreContext); if (context === undefined) throw new Error("useProjectPage must be used within StoreProvider"); - - if (!projectId) throw new Error("projectId must be passed as a property"); - return context.projectPages; }; diff --git a/web/pages/[workspaceSlug]/projects/[projectId]/pages/[pageId].tsx b/web/pages/[workspaceSlug]/projects/[projectId]/pages/[pageId].tsx index e456f7d8a..4a1f523b9 100644 --- a/web/pages/[workspaceSlug]/projects/[projectId]/pages/[pageId].tsx +++ b/web/pages/[workspaceSlug]/projects/[projectId]/pages/[pageId].tsx @@ -36,7 +36,7 @@ const PageDetailsPage: NextPageWithLayout = observer(() => { const router = useRouter(); const { workspaceSlug, projectId, pageId } = router.query; // store hooks - const { createPage, getPageById } = useProjectPages(projectId?.toString() ?? ""); + const { createPage, getPageById } = useProjectPages(); const page = usePage(pageId?.toString() ?? ""); const { access, description_html, id, name } = page; // editor markings hook diff --git a/web/store/command-palette.store.ts b/web/store/command-palette.store.ts index d45896eb1..4cede3434 100644 --- a/web/store/command-palette.store.ts +++ b/web/store/command-palette.store.ts @@ -1,6 +1,8 @@ import { observable, action, makeObservable, computed } from "mobx"; // services import { EIssuesStoreType, TCreateModalStoreTypes } from "@/constants/issue"; +// types / constants +import { DEFAULT_CREATE_PAGE_MODAL_DATA, EPageAccess, TCreatePageModal } from "@/constants/page"; export interface ModalData { store: EIssuesStoreType; @@ -16,7 +18,7 @@ export interface ICommandPaletteStore { isCreateCycleModalOpen: boolean; isCreateModuleModalOpen: boolean; isCreateViewModalOpen: boolean; - isCreatePageModalOpen: boolean; + createPageModal: TCreatePageModal; isCreateIssueModalOpen: boolean; isDeleteIssueModalOpen: boolean; isBulkDeleteIssueModalOpen: boolean; @@ -28,7 +30,7 @@ export interface ICommandPaletteStore { toggleCreateProjectModal: (value?: boolean) => void; toggleCreateCycleModal: (value?: boolean) => void; toggleCreateViewModal: (value?: boolean) => void; - toggleCreatePageModal: (value?: boolean) => void; + toggleCreatePageModal: (value?: TCreatePageModal) => void; toggleCreateIssueModal: (value?: boolean, storeType?: TCreateModalStoreTypes) => void; toggleCreateModuleModal: (value?: boolean) => void; toggleDeleteIssueModal: (value?: boolean) => void; @@ -45,10 +47,10 @@ export class CommandPaletteStore implements ICommandPaletteStore { isCreateCycleModalOpen: boolean = false; isCreateModuleModalOpen: boolean = false; isCreateViewModalOpen: boolean = false; - isCreatePageModalOpen: boolean = false; isCreateIssueModalOpen: boolean = false; isDeleteIssueModalOpen: boolean = false; isBulkDeleteIssueModalOpen: boolean = false; + createPageModal: TCreatePageModal = DEFAULT_CREATE_PAGE_MODAL_DATA; createIssueStoreType: TCreateModalStoreTypes = EIssuesStoreType.PROJECT; @@ -61,10 +63,10 @@ export class CommandPaletteStore implements ICommandPaletteStore { isCreateCycleModalOpen: observable.ref, isCreateModuleModalOpen: observable.ref, isCreateViewModalOpen: observable.ref, - isCreatePageModalOpen: observable.ref, isCreateIssueModalOpen: observable.ref, isDeleteIssueModalOpen: observable.ref, isBulkDeleteIssueModalOpen: observable.ref, + createPageModal: observable, // computed isAnyModalOpen: computed, // projectPages: computed, @@ -90,13 +92,13 @@ export class CommandPaletteStore implements ICommandPaletteStore { return Boolean( this.isCreateIssueModalOpen || this.isCreateCycleModalOpen || - this.isCreatePageModalOpen || this.isCreateProjectModalOpen || this.isCreateModuleModalOpen || this.isCreateViewModalOpen || this.isShortcutModalOpen || this.isBulkDeleteIssueModalOpen || - this.isDeleteIssueModalOpen + this.isDeleteIssueModalOpen || + this.createPageModal.isOpen ); } @@ -166,15 +168,21 @@ export class CommandPaletteStore implements ICommandPaletteStore { }; /** - * Toggles the create page modal + * Toggles the create page modal along with the page access * @param value * @returns */ - toggleCreatePageModal = (value?: boolean) => { - if (value !== undefined) { - this.isCreatePageModalOpen = value; + toggleCreatePageModal = (value?: TCreatePageModal) => { + if (value) { + this.createPageModal = { + isOpen: value.isOpen, + pageAccess: value.pageAccess || EPageAccess.PUBLIC, + }; } else { - this.isCreatePageModalOpen = !this.isCreatePageModalOpen; + this.createPageModal = { + isOpen: !this.createPageModal.isOpen, + pageAccess: EPageAccess.PUBLIC, + }; } }; diff --git a/web/store/pages/project-page.store.ts b/web/store/pages/project-page.store.ts index 1ae5454a6..a304e57c3 100644 --- a/web/store/pages/project-page.store.ts +++ b/web/store/pages/project-page.store.ts @@ -22,6 +22,8 @@ export interface IProjectPageStore { data: Record; // pageId => PageStore error: TError | undefined; filters: TPageFilters; + // computed + isAnyPageAvailable: boolean; // helper actions getCurrentProjectPageIds: (pageType: TPageNavigationTabs) => string[] | undefined; getCurrentProjectFilteredPageIds: (pageType: TPageNavigationTabs) => string[] | undefined; @@ -76,6 +78,14 @@ export class ProjectPageStore implements IProjectPageStore { ); } + /** + * @description check if any page is available + */ + get isAnyPageAvailable() { + if (this.loader) return true; + return Object.keys(this.data).length > 0; + } + /** * @description get the current project page ids based on the pageType * @param {TPageNavigationTabs} pageType