diff --git a/web/components/cycles/active-cycle-details.tsx b/web/components/cycles/active-cycle-details.tsx index df12f835d6c..d42f9f54328 100644 --- a/web/components/cycles/active-cycle-details.tsx +++ b/web/components/cycles/active-cycle-details.tsx @@ -3,7 +3,7 @@ import Link from "next/link"; import { observer } from "mobx-react-lite"; import useSWR from "swr"; // hooks -import { useApplication, useCycle, useIssues, useProject } from "hooks/store"; +import { useCycle, useIssues, useProject, useUser } from "hooks/store"; import useToast from "hooks/use-toast"; // ui import { SingleProgressStats } from "components/core"; @@ -22,6 +22,7 @@ import { import ProgressChart from "components/core/sidebar/progress-chart"; import { ActiveCycleProgressStats } from "components/cycles"; import { StateDropdown } from "components/dropdowns"; +import { EmptyState, getEmptyStateImagePath } from "components/empty-state"; // icons import { ArrowRight, CalendarCheck, CalendarDays, Star, Target } from "lucide-react"; // helpers @@ -32,7 +33,7 @@ import { ICycle, TCycleGroups } from "@plane/types"; // constants import { EIssuesStoreType } from "constants/issue"; import { CYCLE_ISSUES_WITH_PARAMS } from "constants/fetch-keys"; -import { CYCLE_STATE_GROUPS_DETAILS } from "constants/cycle"; +import { CYCLE_EMPTY_STATE_DETAILS, CYCLE_STATE_GROUPS_DETAILS } from "constants/cycle"; interface IActiveCycleDetails { workspaceSlug: string; @@ -43,12 +44,10 @@ export const ActiveCycleDetails: React.FC = observer((props // props const { workspaceSlug, projectId } = props; // store hooks + const { currentUser } = useUser(); const { issues: { fetchActiveCycleIssues }, } = useIssues(EIssuesStoreType.CYCLE); - const { - commandPalette: { toggleCreateCycleModal }, - } = useApplication(); const { fetchActiveCycle, currentProjectActiveCycleId, @@ -76,6 +75,9 @@ export const ActiveCycleDetails: React.FC = observer((props : null ); + const emptyStateDetail = CYCLE_EMPTY_STATE_DETAILS["active"]; + const emptyStateImage = getEmptyStateImagePath("cycle", "active", currentUser?.theme.theme === "light"); + if (!activeCycle && isLoading) return ( @@ -85,27 +87,12 @@ export const ActiveCycleDetails: React.FC = observer((props if (!activeCycle) return ( -
-
-
- - - - -
-

No active cycle

- -
-
+ ); const endDate = new Date(activeCycle.end_date ?? ""); diff --git a/web/components/cycles/cycles-board.tsx b/web/components/cycles/cycles-board.tsx index 967e8a39527..1365e4aa48a 100644 --- a/web/components/cycles/cycles-board.tsx +++ b/web/components/cycles/cycles-board.tsx @@ -1,9 +1,12 @@ import { FC } from "react"; import { observer } from "mobx-react-lite"; // hooks -import { useApplication } from "hooks/store"; +import { useUser } from "hooks/store"; // components import { CyclePeekOverview, CyclesBoardCard } from "components/cycles"; +import { EmptyState, getEmptyStateImagePath } from "components/empty-state"; +// constants +import { CYCLE_EMPTY_STATE_DETAILS } from "constants/cycle"; export interface ICyclesBoard { cycleIds: string[]; @@ -16,7 +19,10 @@ export interface ICyclesBoard { export const CyclesBoard: FC = observer((props) => { const { cycleIds, filter, workspaceSlug, projectId, peekCycle } = props; // store hooks - const { commandPalette: commandPaletteStore } = useApplication(); + const { currentUser } = useUser(); + + const emptyStateDetail = CYCLE_EMPTY_STATE_DETAILS[filter as keyof typeof CYCLE_EMPTY_STATE_DETAILS]; + const emptyStateImage = getEmptyStateImagePath("cycle", filter, currentUser?.theme.theme === "light"); return ( <> @@ -41,27 +47,12 @@ export const CyclesBoard: FC = observer((props) => { ) : ( -
-
-
- - - - -
-

{filter === "all" ? "No cycles" : `No ${filter} cycles`}

- -
-
+ )} ); diff --git a/web/components/cycles/cycles-list.tsx b/web/components/cycles/cycles-list.tsx index 87796340e0a..3dfa2130b2f 100644 --- a/web/components/cycles/cycles-list.tsx +++ b/web/components/cycles/cycles-list.tsx @@ -1,11 +1,14 @@ import { FC } from "react"; import { observer } from "mobx-react-lite"; // hooks -import { useApplication } from "hooks/store"; +import { useUser } from "hooks/store"; // components import { CyclePeekOverview, CyclesListItem } from "components/cycles"; +import { EmptyState, getEmptyStateImagePath } from "components/empty-state"; // ui import { Loader } from "@plane/ui"; +// constants +import { CYCLE_EMPTY_STATE_DETAILS } from "constants/cycle"; export interface ICyclesList { cycleIds: string[]; @@ -17,10 +20,10 @@ export interface ICyclesList { export const CyclesList: FC = observer((props) => { const { cycleIds, filter, workspaceSlug, projectId } = props; // store hooks - const { - commandPalette: commandPaletteStore, - eventTracker: { setTrackElement }, - } = useApplication(); + const { currentUser } = useUser(); + + const emptyStateDetail = CYCLE_EMPTY_STATE_DETAILS[filter as keyof typeof CYCLE_EMPTY_STATE_DETAILS]; + const emptyStateImage = getEmptyStateImagePath("cycle", filter, currentUser?.theme.theme === "light"); return ( <> @@ -46,32 +49,12 @@ export const CyclesList: FC = observer((props) => { ) : ( -
-
-
- - - - -
-

- {filter === "all" ? "No cycles" : `No ${filter} cycles`} -

- -
-
+ )} ) : ( diff --git a/web/components/empty-state/comic-box-button.tsx b/web/components/empty-state/comic-box-button.tsx new file mode 100644 index 00000000000..607d74a9171 --- /dev/null +++ b/web/components/empty-state/comic-box-button.tsx @@ -0,0 +1,75 @@ +import { useState } from "react"; +import { Popover } from "@headlessui/react"; +// popper +import { usePopper } from "react-popper"; +// helper +import { getButtonStyling } from "@plane/ui"; + +type Props = { + label: string; + icon?: any; + title: string | undefined; + description: string | undefined; + onClick?: () => void; + disabled?: boolean; +}; + +export const ComicBoxButton: React.FC = (props) => { + const { label, icon, title, description, onClick, disabled = false } = props; + const [isHovered, setIsHovered] = useState(false); + + const handleMouseEnter = () => { + setIsHovered(true); + }; + + const handleMouseLeave = () => { + setIsHovered(false); + }; + + const [referenceElement, setReferenceElement] = useState(null); + const [popperElement, setPopperElement] = useState(); + const { styles, attributes } = usePopper(referenceElement, popperElement, { + placement: "right-end", + modifiers: [ + { + name: "offset", + options: { + offset: [0, 10], + }, + }, + ], + }); + + return ( + + +
+ {icon} + {label} + +
+
+ +
+ + {isHovered && ( + +
+

{title}

+

{description}

+ + )} + + ); +}; diff --git a/web/components/empty-state/empty-state.tsx b/web/components/empty-state/empty-state.tsx new file mode 100644 index 00000000000..5416eef431d --- /dev/null +++ b/web/components/empty-state/empty-state.tsx @@ -0,0 +1,113 @@ +import React from "react"; +// components +import { ComicBoxButton } from "./comic-box-button"; +// ui +import { Button, getButtonStyling } from "@plane/ui"; +// helper +import { cn } from "helpers/common.helper"; + +type Props = { + title: string; + description?: string; + image: any; + primaryButton?: { + icon?: any; + text: string; + onClick: () => void; + }; + secondaryButton?: { + icon?: any; + text: string; + onClick: () => void; + }; + comicBox?: { + title: string; + description: string; + }; + size?: "sm" | "lg"; + disabled?: boolean; +}; + +export const EmptyState: React.FC = ({ + title, + description, + image, + primaryButton, + secondaryButton, + comicBox, + size = "sm", + disabled = false, +}) => { + const emptyStateHeader = ( + <> + {description ? ( + <> +

{title}

+

{description}

+ + ) : ( +

{title}

+ )} + + ); + + const imageElement = {primaryButton?.text; + + const secondaryButtonElement = secondaryButton && ( + + ); + + return ( +
+
+
{emptyStateHeader}
+ + {imageElement} + +
+ {primaryButton && ( + <> +
+ {comicBox ? ( + primaryButton.onClick()} + disabled={disabled} + /> + ) : ( +
primaryButton.onClick()} + > + {primaryButton.icon} + {primaryButton.text} +
+ )} +
+ + )} + + {secondaryButton && secondaryButtonElement} +
+
+
+ ); +}; diff --git a/web/components/empty-state/helper.tsx b/web/components/empty-state/helper.tsx new file mode 100644 index 00000000000..4f5cfc12744 --- /dev/null +++ b/web/components/empty-state/helper.tsx @@ -0,0 +1,2 @@ +export const getEmptyStateImagePath = (category: string, type: string, isLightMode: boolean) => + `/empty-state/${category}/${type}-${isLightMode ? "light" : "dark"}.webp`; diff --git a/web/components/empty-state/index.ts b/web/components/empty-state/index.ts new file mode 100644 index 00000000000..57e63c1bb95 --- /dev/null +++ b/web/components/empty-state/index.ts @@ -0,0 +1,3 @@ +export * from "./empty-state"; +export * from "./helper"; +export * from "./comic-box-button"; diff --git a/web/components/issues/issue-layouts/empty-states/archived-issues.tsx b/web/components/issues/issue-layouts/empty-states/archived-issues.tsx new file mode 100644 index 00000000000..33f46ba24e0 --- /dev/null +++ b/web/components/issues/issue-layouts/empty-states/archived-issues.tsx @@ -0,0 +1,92 @@ +import { observer } from "mobx-react-lite"; +import { useRouter } from "next/router"; +import size from "lodash/size"; +// hooks +import { useIssues, useUser } from "hooks/store"; +// components +import { EmptyState, getEmptyStateImagePath } from "components/empty-state"; +// constants +import { EUserProjectRoles } from "constants/project"; +import { EIssueFilterType, EIssuesStoreType } from "constants/issue"; +// types +import { IIssueFilterOptions } from "@plane/types"; + +interface EmptyStateProps { + title: string; + image: string; + description?: string; + comicBox?: { title: string; description: string }; + primaryButton?: { text: string; icon?: React.ReactNode; onClick: () => void }; + secondaryButton?: { text: string; onClick: () => void }; + size?: "lg" | "sm" | undefined; + disabled?: boolean | undefined; +} + +export const ProjectArchivedEmptyState: React.FC = observer(() => { + // router + const router = useRouter(); + const { workspaceSlug, projectId } = router.query; + const { + membership: { currentProjectRole }, + currentUser, + } = useUser(); + const { issuesFilter } = useIssues(EIssuesStoreType.ARCHIVED); + + const userFilters = issuesFilter?.issueFilters?.filters; + const activeLayout = issuesFilter?.issueFilters?.displayFilters?.layout; + + const currentLayoutEmptyStateImagePath = getEmptyStateImagePath( + "empty-filters", + activeLayout ?? "list", + currentUser?.theme.theme === "light" + ); + const EmptyStateImagePath = getEmptyStateImagePath("archived", "empty-issues", currentUser?.theme.theme === "light"); + + const issueFilterCount = size( + Object.fromEntries( + Object.entries(userFilters ?? {}).filter(([key, value]) => value && Array.isArray(value) && value.length > 0) + ) + ); + + const handleClearAllFilters = () => { + if (!workspaceSlug || !projectId) return; + const newFilters: IIssueFilterOptions = {}; + Object.keys(userFilters ?? {}).forEach((key) => { + newFilters[key as keyof IIssueFilterOptions] = null; + }); + issuesFilter.updateFilters(workspaceSlug.toString(), projectId.toString(), EIssueFilterType.FILTERS, { + ...newFilters, + }); + }; + + const isEditingAllowed = !!currentProjectRole && currentProjectRole >= EUserProjectRoles.MEMBER; + + const emptyStateProps: EmptyStateProps = + issueFilterCount > 0 + ? { + title: "No issues found matching the filters applied", + image: currentLayoutEmptyStateImagePath, + secondaryButton: { + text: "Clear all filters", + onClick: handleClearAllFilters, + }, + } + : { + title: "No archived issues yet", + description: + "Archived issues help you remove issues you completed or cancelled from focus. You can set automation to auto archive issues and find them here.", + image: EmptyStateImagePath, + primaryButton: { + text: "Set Automation", + onClick: () => router.push(`/${workspaceSlug}/projects/${projectId}/settings/automations`), + }, + size: "sm", + disabled: !isEditingAllowed, + }; + + return ( +
+ +
+ ); +}); diff --git a/web/components/issues/issue-layouts/empty-states/draft-issues.tsx b/web/components/issues/issue-layouts/empty-states/draft-issues.tsx new file mode 100644 index 00000000000..258d0d5d37f --- /dev/null +++ b/web/components/issues/issue-layouts/empty-states/draft-issues.tsx @@ -0,0 +1,88 @@ +import { observer } from "mobx-react-lite"; +import { useRouter } from "next/router"; +import size from "lodash/size"; +// hooks +import { useIssues, useUser } from "hooks/store"; +// components +import { EmptyState, getEmptyStateImagePath } from "components/empty-state"; +// constants +import { EUserProjectRoles } from "constants/project"; +import { EIssueFilterType, EIssuesStoreType } from "constants/issue"; +// types +import { IIssueFilterOptions } from "@plane/types"; + +interface EmptyStateProps { + title: string; + image: string; + description?: string; + comicBox?: { title: string; description: string }; + primaryButton?: { text: string; icon?: React.ReactNode; onClick: () => void }; + secondaryButton?: { text: string; onClick: () => void }; + size?: "lg" | "sm" | undefined; + disabled?: boolean | undefined; +} + +export const ProjectDraftEmptyState: React.FC = observer(() => { + // router + const router = useRouter(); + const { workspaceSlug, projectId } = router.query; + const { + membership: { currentProjectRole }, + currentUser, + } = useUser(); + const { issuesFilter } = useIssues(EIssuesStoreType.DRAFT); + + const userFilters = issuesFilter?.issueFilters?.filters; + const activeLayout = issuesFilter?.issueFilters?.displayFilters?.layout; + + const currentLayoutEmptyStateImagePath = getEmptyStateImagePath( + "empty-filters", + activeLayout ?? "list", + currentUser?.theme.theme === "light" + ); + const EmptyStateImagePath = getEmptyStateImagePath("draft", "empty-issues", currentUser?.theme.theme === "light"); + + const issueFilterCount = size( + Object.fromEntries( + Object.entries(userFilters ?? {}).filter(([key, value]) => value && Array.isArray(value) && value.length > 0) + ) + ); + + const handleClearAllFilters = () => { + if (!workspaceSlug || !projectId) return; + const newFilters: IIssueFilterOptions = {}; + Object.keys(userFilters ?? {}).forEach((key) => { + newFilters[key as keyof IIssueFilterOptions] = null; + }); + issuesFilter.updateFilters(workspaceSlug.toString(), projectId.toString(), EIssueFilterType.FILTERS, { + ...newFilters, + }); + }; + + const isEditingAllowed = !!currentProjectRole && currentProjectRole >= EUserProjectRoles.MEMBER; + + const emptyStateProps: EmptyStateProps = + issueFilterCount > 0 + ? { + title: "No issues found matching the filters applied", + image: currentLayoutEmptyStateImagePath, + secondaryButton: { + text: "Clear all filters", + onClick: handleClearAllFilters, + }, + } + : { + title: "No draft issues yet", + description: + "Quickly stepping away but want to keep your place? No worries – save a draft now. Your issues will be right here waiting for you.", + image: EmptyStateImagePath, + size: "sm", + disabled: !isEditingAllowed, + }; + + return ( +
+ +
+ ); +}); diff --git a/web/components/issues/issue-layouts/empty-states/index.ts b/web/components/issues/issue-layouts/empty-states/index.ts index 0373709d282..1320076e775 100644 --- a/web/components/issues/issue-layouts/empty-states/index.ts +++ b/web/components/issues/issue-layouts/empty-states/index.ts @@ -2,4 +2,6 @@ export * from "./cycle"; export * from "./global-view"; export * from "./module"; export * from "./project-view"; -export * from "./project"; +export * from "./project-issues"; +export * from "./draft-issues"; +export * from "./archived-issues"; diff --git a/web/components/issues/issue-layouts/empty-states/project-issues.tsx b/web/components/issues/issue-layouts/empty-states/project-issues.tsx new file mode 100644 index 00000000000..49de72acae3 --- /dev/null +++ b/web/components/issues/issue-layouts/empty-states/project-issues.tsx @@ -0,0 +1,106 @@ +import { observer } from "mobx-react-lite"; +import { useRouter } from "next/router"; +import size from "lodash/size"; +// hooks +import { useApplication, useIssues, useUser } from "hooks/store"; +// components +import { EmptyState, getEmptyStateImagePath } from "components/empty-state"; +// constants +import { EUserProjectRoles } from "constants/project"; +import { EIssueFilterType, EIssuesStoreType } from "constants/issue"; +// types +import { IIssueFilterOptions } from "@plane/types"; + +interface EmptyStateProps { + title: string; + image: string; + description?: string; + comicBox?: { title: string; description: string }; + primaryButton?: { text: string; icon?: React.ReactNode; onClick: () => void }; + secondaryButton?: { text: string; onClick: () => void }; + size?: "lg" | "sm" | undefined; + disabled?: boolean | undefined; +} + +export const ProjectEmptyState: React.FC = observer(() => { + // router + const router = useRouter(); + const { workspaceSlug, projectId } = router.query; + // store hooks + const { + commandPalette: commandPaletteStore, + eventTracker: { setTrackElement }, + } = useApplication(); + const { + membership: { currentProjectRole }, + currentUser, + } = useUser(); + const { issuesFilter } = useIssues(EIssuesStoreType.PROJECT); + + const userFilters = issuesFilter?.issueFilters?.filters; + const activeLayout = issuesFilter?.issueFilters?.displayFilters?.layout; + + const currentLayoutEmptyStateImagePath = getEmptyStateImagePath( + "empty-filters", + activeLayout ?? "list", + currentUser?.theme.theme === "light" + ); + const EmptyStateImagePath = getEmptyStateImagePath("onboarding", "issues", currentUser?.theme.theme === "light"); + + const issueFilterCount = size( + Object.fromEntries( + Object.entries(userFilters ?? {}).filter(([key, value]) => value && Array.isArray(value) && value.length > 0) + ) + ); + + const handleClearAllFilters = () => { + if (!workspaceSlug || !projectId) return; + const newFilters: IIssueFilterOptions = {}; + Object.keys(userFilters ?? {}).forEach((key) => { + newFilters[key as keyof IIssueFilterOptions] = null; + }); + issuesFilter.updateFilters(workspaceSlug.toString(), projectId.toString(), EIssueFilterType.FILTERS, { + ...newFilters, + }); + }; + + const isEditingAllowed = !!currentProjectRole && currentProjectRole >= EUserProjectRoles.MEMBER; + + const emptyStateProps: EmptyStateProps = + issueFilterCount > 0 + ? { + title: "No issues found matching the filters applied", + image: currentLayoutEmptyStateImagePath, + secondaryButton: { + text: "Clear all filters", + onClick: handleClearAllFilters, + }, + } + : { + title: "Create an issue and assign it to someone, even yourself", + description: + "Think of issues as jobs, tasks, work, or JTBD. Which we like. An issue and its sub-issues are usually time-based actionables assigned to members of your team. Your team creates, assigns, and completes issues to move your project towards its goal.", + image: EmptyStateImagePath, + comicBox: { + title: "Issues are building blocks in Plane.", + description: + "Redesign the Plane UI, Rebrand the company, or Launch the new fuel injection system are examples of issues that likely have sub-issues.", + }, + primaryButton: { + text: "Create your first issue", + + onClick: () => { + setTrackElement("PROJECT_EMPTY_STATE"); + commandPaletteStore.toggleCreateIssueModal(true, EIssuesStoreType.PROJECT); + }, + }, + size: "lg", + disabled: !isEditingAllowed, + }; + + return ( +
+ +
+ ); +}); diff --git a/web/components/issues/issue-layouts/empty-states/project.tsx b/web/components/issues/issue-layouts/empty-states/project.tsx deleted file mode 100644 index 592264d82da..00000000000 --- a/web/components/issues/issue-layouts/empty-states/project.tsx +++ /dev/null @@ -1,49 +0,0 @@ -import { observer } from "mobx-react-lite"; -import { PlusIcon } from "lucide-react"; -// hooks -import { useApplication, useUser } from "hooks/store"; -// components -import { NewEmptyState } from "components/common/new-empty-state"; -// constants -import { EUserProjectRoles } from "constants/project"; -// assets -import emptyIssue from "public/empty-state/empty_issues.webp"; -import { EIssuesStoreType } from "constants/issue"; - -export const ProjectEmptyState: React.FC = observer(() => { - // store hooks - const { - commandPalette: commandPaletteStore, - eventTracker: { setTrackElement }, - } = useApplication(); - const { - membership: { currentProjectRole }, - } = useUser(); - - const isEditingAllowed = !!currentProjectRole && currentProjectRole >= EUserProjectRoles.MEMBER; - - return ( -
- , - onClick: () => { - setTrackElement("PROJECT_EMPTY_STATE"); - commandPaletteStore.toggleCreateIssueModal(true, EIssuesStoreType.PROJECT); - }, - }} - disabled={!isEditingAllowed} - /> -
- ); -}); diff --git a/web/components/issues/issue-layouts/roots/all-issue-layout-root.tsx b/web/components/issues/issue-layouts/roots/all-issue-layout-root.tsx index 7939404cebb..394e8eafb4f 100644 --- a/web/components/issues/issue-layouts/roots/all-issue-layout-root.tsx +++ b/web/components/issues/issue-layouts/roots/all-issue-layout-root.tsx @@ -3,19 +3,22 @@ import { useRouter } from "next/router"; import { observer } from "mobx-react-lite"; import useSWR from "swr"; // hooks -import { useGlobalView, useIssues, useUser } from "hooks/store"; +import { useApplication, useGlobalView, useIssues, useProject, useUser } from "hooks/store"; import { useWorskspaceIssueProperties } from "hooks/use-worskspace-issue-properties"; // components import { GlobalViewsAppliedFiltersRoot, IssuePeekOverview } from "components/issues"; import { SpreadsheetView } from "components/issues/issue-layouts"; import { AllIssueQuickActions } from "components/issues/issue-layouts/quick-action-dropdowns"; +import { EmptyState, getEmptyStateImagePath } from "components/empty-state"; // ui import { Spinner } from "@plane/ui"; // types import { TIssue, IIssueDisplayFilterOptions } from "@plane/types"; import { EIssueActions } from "../types"; +// constants import { EUserProjectRoles } from "constants/project"; import { EIssueFilterType, EIssuesStoreType } from "constants/issue"; +import { ALL_ISSUES_EMPTY_STATE_DETAILS, EUserWorkspaceRoles } from "constants/workspace"; export const AllIssueLayoutRoot: React.FC = observer(() => { // router @@ -24,6 +27,7 @@ export const AllIssueLayoutRoot: React.FC = observer(() => { //swr hook for fetching issue properties useWorskspaceIssueProperties(workspaceSlug); // store + const { commandPalette: commandPaletteStore } = useApplication(); const { issuesFilter: { filters, fetchFilters, updateFilters }, issues: { loader, groupedIssueIds, fetchIssues, updateIssue, removeIssue }, @@ -31,10 +35,17 @@ export const AllIssueLayoutRoot: React.FC = observer(() => { const { dataViewId, issueIds } = groupedIssueIds; const { - membership: { currentWorkspaceAllProjectsRole }, + membership: { currentWorkspaceAllProjectsRole, currentWorkspaceRole }, + currentUser, } = useUser(); const { fetchAllGlobalViews } = useGlobalView(); - // derived values + const { workspaceProjectIds } = useProject(); + + const isDefaultView = ["all-issues", "assigned", "created", "subscribed"].includes(groupedIssueIds.dataViewId); + const currentView = isDefaultView ? groupedIssueIds.dataViewId : "custom-view"; + const currentViewDetails = ALL_ISSUES_EMPTY_STATE_DETAILS[currentView as keyof typeof ALL_ISSUES_EMPTY_STATE_DETAILS]; + + const emptyStateImage = getEmptyStateImagePath("all-issues", currentView, currentUser?.theme.theme === "light"); useSWR(workspaceSlug ? `WORKSPACE_GLOBAL_VIEWS${workspaceSlug}` : null, async () => { if (workspaceSlug) { @@ -116,6 +127,8 @@ export const AllIssueLayoutRoot: React.FC = observer(() => { [handleIssues] ); + const isEditingAllowed = !!currentWorkspaceRole && currentWorkspaceRole >= EUserWorkspaceRoles.MEMBER; + return (
{!globalViewId || globalViewId !== dataViewId || loader === "init-loader" || !issueIds ? ( @@ -127,7 +140,28 @@ export const AllIssueLayoutRoot: React.FC = observer(() => { {(issueIds ?? {}).length == 0 ? ( - <>{/* */} + 0 ? currentViewDetails.title : "No project"} + description={ + (workspaceProjectIds ?? []).length > 0 + ? currentViewDetails.description + : "To create issues or manage your work, you need to create a project or be a part of one." + } + size="sm" + primaryButton={ + (workspaceProjectIds ?? []).length > 0 + ? { + text: "Create new issue", + onClick: () => commandPaletteStore.toggleCreateIssueModal(true, EIssuesStoreType.PROJECT), + } + : { + text: "Start your first project", + onClick: () => commandPaletteStore.toggleCreateProjectModal(true), + } + } + disabled={!isEditingAllowed} + /> ) : (
{ ) : ( <> {!issues?.groupedIssueIds ? ( - // TODO: Replace this with project view empty state - + ) : ( <>
diff --git a/web/components/issues/issue-layouts/roots/draft-issue-layout-root.tsx b/web/components/issues/issue-layouts/roots/draft-issue-layout-root.tsx index 19a4da55302..075a16aa277 100644 --- a/web/components/issues/issue-layouts/roots/draft-issue-layout-root.tsx +++ b/web/components/issues/issue-layouts/roots/draft-issue-layout-root.tsx @@ -7,7 +7,7 @@ import { useIssues } from "hooks/store"; // components import { DraftIssueAppliedFiltersRoot } from "../filters/applied-filters/roots/draft-issue"; import { DraftIssueListLayout } from "../list/roots/draft-issue-root"; -import { ProjectEmptyState } from "../empty-states"; +import { ProjectDraftEmptyState } from "../empty-states"; // ui import { Spinner } from "@plane/ui"; import { DraftKanBanLayout } from "../kanban/roots/draft-issue-root"; @@ -49,8 +49,7 @@ export const DraftIssueLayoutRoot: React.FC = observer(() => { ) : ( <> {!issues?.groupedIssueIds ? ( - // TODO: Replace this with project view empty state - + ) : (
{activeLayout === "list" ? ( diff --git a/web/components/modules/modules-list-view.tsx b/web/components/modules/modules-list-view.tsx index 4d97f5b0df3..0708187c148 100644 --- a/web/components/modules/modules-list-view.tsx +++ b/web/components/modules/modules-list-view.tsx @@ -1,18 +1,15 @@ import { useRouter } from "next/router"; import { observer } from "mobx-react-lite"; -import { Plus } from "lucide-react"; // hooks import { useApplication, useModule, useUser } from "hooks/store"; import useLocalStorage from "hooks/use-local-storage"; // components import { ModuleCardItem, ModuleListItem, ModulePeekOverview, ModulesListGanttChartView } from "components/modules"; +import { EmptyState, getEmptyStateImagePath } from "components/empty-state"; // ui -import { Loader } from "@plane/ui"; +import { Loader, Spinner } from "@plane/ui"; // constants import { EUserProjectRoles } from "constants/project"; -// assets -import emptyModule from "public/empty-state/empty_modules.webp"; -import { NewEmptyState } from "components/common/new-empty-state"; export const ModulesListView: React.FC = observer(() => { // router @@ -22,13 +19,23 @@ export const ModulesListView: React.FC = observer(() => { const { commandPalette: commandPaletteStore } = useApplication(); const { membership: { currentProjectRole }, + currentUser, } = useUser(); - const { projectModuleIds } = useModule(); + const { projectModuleIds, loader } = useModule(); const { storedValue: modulesView } = useLocalStorage("modules_view", "grid"); + const EmptyStateImagePath = getEmptyStateImagePath("onboarding", "modules", currentUser?.theme.theme === "light"); + const isEditingAllowed = !!currentProjectRole && currentProjectRole >= EUserProjectRoles.MEMBER; + if (loader) + return ( +
+ +
+ ); + if (!projectModuleIds) return ( @@ -84,21 +91,20 @@ export const ModulesListView: React.FC = observer(() => { {modulesView === "gantt_chart" && } ) : ( - , text: "Build your first module", onClick: () => commandPaletteStore.toggleCreateModuleModal(true), }} + size="lg" disabled={!isEditingAllowed} /> )} diff --git a/web/components/page-views/workspace-dashboard.tsx b/web/components/page-views/workspace-dashboard.tsx index 949dbf75907..6e185e2d066 100644 --- a/web/components/page-views/workspace-dashboard.tsx +++ b/web/components/page-views/workspace-dashboard.tsx @@ -6,20 +6,30 @@ import { useApplication, useDashboard, useProject, useUser } from "hooks/store"; import { TourRoot } from "components/onboarding"; import { UserGreetingsView } from "components/user"; import { IssuePeekOverview } from "components/issues"; -import { DashboardProjectEmptyState, DashboardWidgets } from "components/dashboard"; +import { DashboardWidgets } from "components/dashboard"; +import { EmptyState, getEmptyStateImagePath } from "components/empty-state"; // ui import { Spinner } from "@plane/ui"; +// constants +import { EUserWorkspaceRoles } from "constants/workspace"; export const WorkspaceDashboardView = observer(() => { // store hooks const { + commandPalette: { toggleCreateProjectModal }, eventTracker: { postHogEventTracker }, router: { workspaceSlug }, } = useApplication(); - const { currentUser, updateTourCompleted } = useUser(); + const { + currentUser, + updateTourCompleted, + membership: { currentWorkspaceRole }, + } = useUser(); const { homeDashboardId, fetchHomeDashboardWidgets } = useDashboard(); const { joinedProjectIds } = useProject(); + const emptyStateImage = getEmptyStateImagePath("onboarding", "dashboard", currentUser?.theme.theme === "light"); + const handleTourCompleted = () => { updateTourCompleted() .then(() => { @@ -41,6 +51,8 @@ export const WorkspaceDashboardView = observer(() => { fetchHomeDashboardWidgets(workspaceSlug); }, [fetchHomeDashboardWidgets, workspaceSlug]); + const isEditingAllowed = currentWorkspaceRole === EUserWorkspaceRoles.ADMIN; + return ( <> @@ -52,7 +64,27 @@ export const WorkspaceDashboardView = observer(() => { {homeDashboardId && joinedProjectIds ? (
{currentUser && } - {joinedProjectIds.length > 0 ? : } + {joinedProjectIds.length > 0 ? ( + + ) : ( + toggleCreateProjectModal(true), + }} + comicBox={{ + title: "Everything starts with a project in Plane", + description: "A project could be a product’s roadmap, a marketing campaign, or launching a new car.", + }} + size="lg" + disabled={!isEditingAllowed} + /> + )}
) : (
diff --git a/web/components/pages/pages-list/list-view.tsx b/web/components/pages/pages-list/list-view.tsx index d00a641f448..d1bde308d2b 100644 --- a/web/components/pages/pages-list/list-view.tsx +++ b/web/components/pages/pages-list/list-view.tsx @@ -1,17 +1,16 @@ import { FC } from "react"; import { useRouter } from "next/router"; -import { Plus } from "lucide-react"; // hooks import { useApplication, useUser } from "hooks/store"; +import useLocalStorage from "hooks/use-local-storage"; // components -import { NewEmptyState } from "components/common/new-empty-state"; +import { EmptyState, getEmptyStateImagePath } from "components/empty-state"; +import { PagesListItem } from "./list-item"; // ui import { Loader } from "@plane/ui"; -// images -import emptyPage from "public/empty-state/empty_page.png"; // constants import { EUserProjectRoles } from "constants/project"; -import { PagesListItem } from "./list-item"; +import { PAGE_EMPTY_STATE_DETAILS } from "constants/page"; type IPagesListView = { pageIds: string[]; @@ -19,19 +18,32 @@ type IPagesListView = { export const PagesListView: FC = (props) => { const { pageIds: projectPageIds } = props; - // store hooks - // trace(true); const { commandPalette: { toggleCreatePageModal }, } = useApplication(); const { membership: { currentProjectRole }, + currentUser, } = useUser(); + // local storage + const { storedValue: pageTab } = useLocalStorage("pageTab", "Recent"); // router const router = useRouter(); const { workspaceSlug, projectId } = router.query; + const currentPageTabDetails = pageTab + ? PAGE_EMPTY_STATE_DETAILS[pageTab as keyof typeof PAGE_EMPTY_STATE_DETAILS] + : PAGE_EMPTY_STATE_DETAILS["All"]; + + const emptyStateImage = getEmptyStateImagePath( + "pages", + currentPageTabDetails.key, + currentUser?.theme.theme === "light" + ); + + const isButtonVisible = currentPageTabDetails.key !== "archived" && currentPageTabDetails.key !== "favorites"; + // here we are only observing the projectPageStore, so that we can re-render the component when the projectPageStore changes const isEditingAllowed = !!currentProjectRole && currentProjectRole >= EUserProjectRoles.MEMBER; @@ -47,21 +59,18 @@ export const PagesListView: FC = (props) => { ))} ) : ( - , - text: "Create your first page", - onClick: () => toggleCreatePageModal(true), - }} + toggleCreatePageModal(true), + } + : undefined + } disabled={!isEditingAllowed} /> )} diff --git a/web/components/pages/pages-list/recent-pages-list.tsx b/web/components/pages/pages-list/recent-pages-list.tsx index 77d3136121a..24916debc09 100644 --- a/web/components/pages/pages-list/recent-pages-list.tsx +++ b/web/components/pages/pages-list/recent-pages-list.tsx @@ -1,29 +1,29 @@ import React, { FC } from "react"; import { observer } from "mobx-react-lite"; -import { Plus } from "lucide-react"; // hooks import { useApplication, useUser } from "hooks/store"; +import { useProjectPages } from "hooks/store/use-project-specific-pages"; // components import { PagesListView } from "components/pages/pages-list"; -import { NewEmptyState } from "components/common/new-empty-state"; +import { EmptyState, getEmptyStateImagePath } from "components/empty-state"; // ui import { Loader } from "@plane/ui"; -// assets -import emptyPage from "public/empty-state/empty_page.png"; // helpers import { replaceUnderscoreIfSnakeCase } from "helpers/string.helper"; // constants import { EUserProjectRoles } from "constants/project"; -import { useProjectPages } from "hooks/store/use-project-specific-pages"; export const RecentPagesList: FC = observer(() => { // store hooks const { commandPalette: commandPaletteStore } = useApplication(); const { membership: { currentProjectRole }, + currentUser, } = useUser(); const { recentProjectPages } = useProjectPages(); + const EmptyStateImagePath = getEmptyStateImagePath("pages", "recent", currentUser?.theme.theme === "light"); + // FIXME: replace any with proper type const isEmpty = recentProjectPages && Object.values(recentProjectPages).every((value: any) => value.length === 0); @@ -58,21 +58,15 @@ export const RecentPagesList: FC = observer(() => { ) : ( <> - , - text: "Create your first page", + text: "Create new page", onClick: () => commandPaletteStore.toggleCreatePageModal(true), }} + size="sm" disabled={!isEditingAllowed} /> diff --git a/web/components/project/card-list.tsx b/web/components/project/card-list.tsx index 488fe5673bd..ebb166f4942 100644 --- a/web/components/project/card-list.tsx +++ b/web/components/project/card-list.tsx @@ -4,10 +4,9 @@ import { useApplication, useProject, useUser } from "hooks/store"; // components import { ProjectCard } from "components/project"; import { Loader } from "@plane/ui"; -// images -import emptyProject from "public/empty-state/empty_project.webp"; +import { EmptyState, getEmptyStateImagePath } from "components/empty-state"; // icons -import { NewEmptyState } from "components/common/new-empty-state"; +import { Plus } from "lucide-react"; // constants import { EUserWorkspaceRoles } from "constants/workspace"; @@ -19,9 +18,12 @@ export const ProjectCardList = observer(() => { } = useApplication(); const { membership: { currentWorkspaceRole }, + currentUser, } = useUser(); const { workspaceProjectIds, searchedProjects, getProjectById } = useProject(); + const emptyStateImage = getEmptyStateImagePath("onboarding", "projects", currentUser?.theme.theme === "light"); + const isEditingAllowed = !!currentWorkspaceRole && currentWorkspaceRole >= EUserWorkspaceRoles.MEMBER; if (!workspaceProjectIds) @@ -55,15 +57,10 @@ export const ProjectCardList = observer(() => { )}
) : ( - { @@ -71,6 +68,11 @@ export const ProjectCardList = observer(() => { commandPaletteStore.toggleCreateProjectModal(true); }, }} + comicBox={{ + title: "Everything starts with a project in Plane", + description: "A project could be a product’s roadmap, a marketing campaign, or launching a new car.", + }} + size="lg" disabled={!isEditingAllowed} /> )} diff --git a/web/components/views/views-list.tsx b/web/components/views/views-list.tsx index 1084fb704ad..c944c8e71a7 100644 --- a/web/components/views/views-list.tsx +++ b/web/components/views/views-list.tsx @@ -1,15 +1,13 @@ import { useState } from "react"; import { observer } from "mobx-react-lite"; -import { Plus, Search } from "lucide-react"; +import { Search } from "lucide-react"; // hooks import { useApplication, useProjectView, useUser } from "hooks/store"; // components import { ProjectViewListItem } from "components/views"; -import { NewEmptyState } from "components/common/new-empty-state"; +import { EmptyState, getEmptyStateImagePath } from "components/empty-state"; // ui -import { Input, Loader } from "@plane/ui"; -// assets -import emptyView from "public/empty-state/empty_view.webp"; +import { Input, Loader, Spinner } from "@plane/ui"; // constants import { EUserProjectRoles } from "constants/project"; @@ -22,10 +20,16 @@ export const ProjectViewsList = observer(() => { } = useApplication(); const { membership: { currentProjectRole }, + currentUser, } = useUser(); - const { projectViewIds, getViewById } = useProjectView(); + const { projectViewIds, getViewById, loader } = useProjectView(); - const isEditingAllowed = !!currentProjectRole && currentProjectRole >= EUserProjectRoles.MEMBER; + if (loader) + return ( +
+ +
+ ); if (!projectViewIds) return ( @@ -39,8 +43,12 @@ export const ProjectViewsList = observer(() => { const viewsList = projectViewIds.map((viewId) => getViewById(viewId)); + const EmptyStateImagePath = getEmptyStateImagePath("onboarding", "cycles", currentUser?.theme.theme === "light"); + const filteredViewsList = viewsList.filter((v) => v?.name.toLowerCase().includes(query.toLowerCase())); + const isEditingAllowed = !!currentProjectRole && currentProjectRole >= EUserProjectRoles.MEMBER; + return ( <> {viewsList.length > 0 ? ( @@ -64,20 +72,19 @@ export const ProjectViewsList = observer(() => { )}
) : ( - , - text: "Build your first view", + text: "Create your first view", onClick: () => toggleCreateViewModal(true), }} + size="lg" disabled={!isEditingAllowed} /> )} diff --git a/web/constants/cycle.ts b/web/constants/cycle.ts index 406fc73032c..f93bfdf99b5 100644 --- a/web/constants/cycle.ts +++ b/web/constants/cycle.ts @@ -114,3 +114,27 @@ export const CYCLE_STATE_GROUPS_DETAILS = [ color: "#ef4444", }, ]; + +export const CYCLE_EMPTY_STATE_DETAILS = { + active: { + key: "active", + title: "No active cycles", + description: + "An active cycle includes any period that encompasses today's date within its range. Find the progress and details of the active cycle here.", + }, + upcoming: { + key: "upcoming", + title: "No upcoming cycles", + description: "Upcoming cycles on deck! Just add dates to cycles in draft, and they'll show up right here.", + }, + completed: { + key: "completed", + title: "No completed cycles", + description: "Any cycle with a past due date is considered completed. Explore all completed cycles here.", + }, + draft: { + key: "draft", + title: "No draft cycles", + description: "No dates added in cycles? Find them here as drafts.", + }, +}; diff --git a/web/constants/page.ts b/web/constants/page.ts index 4b303ae73b6..4183d46d18b 100644 --- a/web/constants/page.ts +++ b/web/constants/page.ts @@ -52,3 +52,38 @@ export const PAGE_ACCESS_SPECIFIERS: { key: number; label: string; icon: any }[] icon: Lock, }, ]; + +export const PAGE_EMPTY_STATE_DETAILS = { + All: { + key: "all", + title: "Write a note, a doc, or a full knowledge base", + description: + "Pages help you organise your thoughts to create wikis, discussions or even document heated takes for your project. Use it wisely!", + }, + Favorites: { + key: "favorites", + title: "No favorite pages yet", + description: "Favorites for quick access? mark them and find them right here.", + }, + Private: { + key: "private", + title: "No private pages yet", + description: "Keep your private thoughts here. When you're ready to share, the team's just a click away.", + }, + Shared: { + key: "shared", + title: "No shared pages yet", + description: "See pages shared with everyone in your project right here.", + }, + Archived: { + key: "archived", + title: "No archived pages yet", + description: "Archive pages not on your radar. Access them here when needed.", + }, + Recent: { + key: "recent", + title: "Write a note, a doc, or a full knowledge base", + description: + "Pages help you organise your thoughts to create wikis, discussions or even document heated takes for your project. Use it wisely! Pages will be sorted and grouped by last updated", + }, +}; diff --git a/web/constants/workspace.ts b/web/constants/workspace.ts index 1471de3958c..4e7a0d6ae03 100644 --- a/web/constants/workspace.ts +++ b/web/constants/workspace.ts @@ -190,3 +190,31 @@ export const WORKSPACE_SETTINGS_LINKS: { Icon: SettingIcon, }, ]; + +export const ALL_ISSUES_EMPTY_STATE_DETAILS = { + "all-issues": { + key: "all-issues", + title: "No issues in the project", + description: "First project done! Now, slice your work into trackable pieces with issues. Let's go!", + }, + assigned: { + key: "assigned", + title: "No issues yet", + description: "Issues assigned to you can be tracked from here.", + }, + created: { + key: "created", + title: "No issues yet", + description: "All issues created by you come here, track them here directly.", + }, + subscribed: { + key: "subscribed", + title: "No issues yet", + description: "Subscribe to issues you are interested in, track all of them here.", + }, + "custom-view": { + key: "custom-view", + title: "No issues yet", + description: "Issues that applies to the filters, track all of them here.", + }, +}; diff --git a/web/pages/[workspaceSlug]/analytics.tsx b/web/pages/[workspaceSlug]/analytics.tsx index 4bd7b83d0c6..fcaef9f70bf 100644 --- a/web/pages/[workspaceSlug]/analytics.tsx +++ b/web/pages/[workspaceSlug]/analytics.tsx @@ -8,11 +8,7 @@ import { AppLayout } from "layouts/app-layout"; // components import { CustomAnalytics, ScopeAndDemand } from "components/analytics"; import { WorkspaceAnalyticsHeader } from "components/headers"; -import { NewEmptyState } from "components/common/new-empty-state"; -// icons -import { Plus } from "lucide-react"; -// assets -import emptyAnalytics from "public/empty-state/empty_analytics.webp"; +import { EmptyState, getEmptyStateImagePath } from "components/empty-state"; // constants import { ANALYTICS_TABS } from "constants/analytics"; import { EUserWorkspaceRoles } from "constants/workspace"; @@ -26,11 +22,13 @@ const AnalyticsPage: NextPageWithLayout = observer(() => { eventTracker: { setTrackElement }, } = useApplication(); const { - membership: { currentProjectRole }, + membership: { currentWorkspaceRole }, + currentUser, } = useUser(); const { workspaceProjectIds } = useProject(); - const isEditingAllowed = !!currentProjectRole && currentProjectRole >= EUserWorkspaceRoles.MEMBER; + const EmptyStateImagePath = getEmptyStateImagePath("onboarding", "analytics", currentUser?.theme.theme === "light"); + const isEditingAllowed = !!currentWorkspaceRole && currentWorkspaceRole >= EUserWorkspaceRoles.MEMBER; return ( <> @@ -63,29 +61,25 @@ const AnalyticsPage: NextPageWithLayout = observer(() => {
) : ( - <> - , - text: "Create Cycles and Modules first", - onClick: () => { - setTrackElement("ANALYTICS_EMPTY_STATE"); - toggleCreateProjectModal(true); - }, - }} - disabled={!isEditingAllowed} - /> - + { + setTrackElement("ANALYTICS_EMPTY_STATE"); + toggleCreateProjectModal(true); + }, + }} + comicBox={{ + title: "Analytics works best with Cycles + Modules", + description: + "First, timebox your issues into Cycles and, if you can, group issues that span more than a cycle into Modules. Check out both on the left nav.", + }} + size="lg" + disabled={!isEditingAllowed} + /> )} ); diff --git a/web/pages/[workspaceSlug]/projects/[projectId]/cycles/index.tsx b/web/pages/[workspaceSlug]/projects/[projectId]/cycles/index.tsx index e35f0c34128..c0077731926 100644 --- a/web/pages/[workspaceSlug]/projects/[projectId]/cycles/index.tsx +++ b/web/pages/[workspaceSlug]/projects/[projectId]/cycles/index.tsx @@ -2,7 +2,6 @@ import { Fragment, useCallback, useState, ReactElement } from "react"; import { useRouter } from "next/router"; import { observer } from "mobx-react-lite"; import { Tab } from "@headlessui/react"; -import { Plus } from "lucide-react"; // hooks import { useCycle, useUser } from "hooks/store"; import useLocalStorage from "hooks/use-local-storage"; @@ -11,11 +10,9 @@ import { AppLayout } from "layouts/app-layout"; // components import { CyclesHeader } from "components/headers"; import { CyclesView, ActiveCycleDetails, CycleCreateUpdateModal } from "components/cycles"; -import { NewEmptyState } from "components/common/new-empty-state"; +import { EmptyState, getEmptyStateImagePath } from "components/empty-state"; // ui -import { Tooltip } from "@plane/ui"; -// images -import emptyCycle from "public/empty-state/empty_cycles.webp"; +import { Spinner, Tooltip } from "@plane/ui"; // types import { TCycleView, TCycleLayout } from "@plane/types"; import { NextPageWithLayout } from "lib/types"; @@ -28,8 +25,9 @@ const ProjectCyclesPage: NextPageWithLayout = observer(() => { // store hooks const { membership: { currentProjectRole }, + currentUser, } = useUser(); - const { currentProjectCycleIds } = useCycle(); + const { currentProjectCycleIds, loader } = useCycle(); // router const router = useRouter(); const { workspaceSlug, projectId, peekCycle } = router.query; @@ -51,6 +49,7 @@ const ProjectCyclesPage: NextPageWithLayout = observer(() => { }, [handleCurrentLayout, setCycleTab] ); + const EmptyStateImagePath = getEmptyStateImagePath("onboarding", "cycles", currentUser?.theme.theme === "light"); const totalCycles = currentProjectCycleIds?.length ?? 0; @@ -58,6 +57,13 @@ const ProjectCyclesPage: NextPageWithLayout = observer(() => { if (!workspaceSlug || !projectId) return null; + if (loader) + return ( +
+ +
+ ); + return (
{ /> {totalCycles === 0 ? (
- , text: "Set your first cycle", onClick: () => { setCreateModal(true); }, }} + size="lg" disabled={!isEditingAllowed} />
diff --git a/web/pages/[workspaceSlug]/projects/[projectId]/pages/index.tsx b/web/pages/[workspaceSlug]/projects/[projectId]/pages/index.tsx index cd9699b34eb..2aaefc517b3 100644 --- a/web/pages/[workspaceSlug]/projects/[projectId]/pages/index.tsx +++ b/web/pages/[workspaceSlug]/projects/[projectId]/pages/index.tsx @@ -13,6 +13,7 @@ import { AppLayout } from "layouts/app-layout"; // components import { RecentPagesList, CreateUpdatePageModal } from "components/pages"; import { PagesHeader } from "components/headers"; +import { Spinner } from "@plane/ui"; // types import { NextPageWithLayout } from "lib/types"; // constants @@ -48,7 +49,7 @@ const ProjectPagesPage: NextPageWithLayout = observer(() => { // store const { currentUser, currentUserLoader } = useUser(); - const { fetchProjectPages, fetchArchivedProjectPages } = useProjectPages(); + const { fetchProjectPages, fetchArchivedProjectPages, loader } = useProjectPages(); // hooks const {} = useUserAuth({ user: currentUser, isLoading: currentUserLoader }); // local storage @@ -83,6 +84,13 @@ const ProjectPagesPage: NextPageWithLayout = observer(() => { } }; + if (loader) + return ( +
+ +
+ ); + return ( <> {workspaceSlug && projectId && ( diff --git a/web/public/empty-state/all-issues/all-issues-dark.webp b/web/public/empty-state/all-issues/all-issues-dark.webp new file mode 100644 index 00000000000..2e7da76b332 Binary files /dev/null and b/web/public/empty-state/all-issues/all-issues-dark.webp differ diff --git a/web/public/empty-state/all-issues/all-issues-light.webp b/web/public/empty-state/all-issues/all-issues-light.webp new file mode 100644 index 00000000000..6b5897bf97e Binary files /dev/null and b/web/public/empty-state/all-issues/all-issues-light.webp differ diff --git a/web/public/empty-state/all-issues/assigned-dark.webp b/web/public/empty-state/all-issues/assigned-dark.webp new file mode 100644 index 00000000000..5e8e3916f51 Binary files /dev/null and b/web/public/empty-state/all-issues/assigned-dark.webp differ diff --git a/web/public/empty-state/all-issues/assigned-light.webp b/web/public/empty-state/all-issues/assigned-light.webp new file mode 100644 index 00000000000..6b04f3b30b1 Binary files /dev/null and b/web/public/empty-state/all-issues/assigned-light.webp differ diff --git a/web/public/empty-state/all-issues/created-dark.webp b/web/public/empty-state/all-issues/created-dark.webp new file mode 100644 index 00000000000..6394e63f706 Binary files /dev/null and b/web/public/empty-state/all-issues/created-dark.webp differ diff --git a/web/public/empty-state/all-issues/created-light.webp b/web/public/empty-state/all-issues/created-light.webp new file mode 100644 index 00000000000..cf2b55dbb2d Binary files /dev/null and b/web/public/empty-state/all-issues/created-light.webp differ diff --git a/web/public/empty-state/all-issues/custom-view-dark.webp b/web/public/empty-state/all-issues/custom-view-dark.webp new file mode 100644 index 00000000000..aba847d79cb Binary files /dev/null and b/web/public/empty-state/all-issues/custom-view-dark.webp differ diff --git a/web/public/empty-state/all-issues/custom-view-light.webp b/web/public/empty-state/all-issues/custom-view-light.webp new file mode 100644 index 00000000000..a531babb32c Binary files /dev/null and b/web/public/empty-state/all-issues/custom-view-light.webp differ diff --git a/web/public/empty-state/all-issues/no-project-dark.webp b/web/public/empty-state/all-issues/no-project-dark.webp new file mode 100644 index 00000000000..50c1ccf2177 Binary files /dev/null and b/web/public/empty-state/all-issues/no-project-dark.webp differ diff --git a/web/public/empty-state/all-issues/no-project-light.webp b/web/public/empty-state/all-issues/no-project-light.webp new file mode 100644 index 00000000000..564c74ee5f6 Binary files /dev/null and b/web/public/empty-state/all-issues/no-project-light.webp differ diff --git a/web/public/empty-state/all-issues/subscribed-dark.webp b/web/public/empty-state/all-issues/subscribed-dark.webp new file mode 100644 index 00000000000..6923b65e107 Binary files /dev/null and b/web/public/empty-state/all-issues/subscribed-dark.webp differ diff --git a/web/public/empty-state/all-issues/subscribed-light.webp b/web/public/empty-state/all-issues/subscribed-light.webp new file mode 100644 index 00000000000..d0411895b20 Binary files /dev/null and b/web/public/empty-state/all-issues/subscribed-light.webp differ diff --git a/web/public/empty-state/archived/empty-issues-dark.webp b/web/public/empty-state/archived/empty-issues-dark.webp new file mode 100644 index 00000000000..09d522d286e Binary files /dev/null and b/web/public/empty-state/archived/empty-issues-dark.webp differ diff --git a/web/public/empty-state/archived/empty-issues-light.webp b/web/public/empty-state/archived/empty-issues-light.webp new file mode 100644 index 00000000000..7aa422a4f80 Binary files /dev/null and b/web/public/empty-state/archived/empty-issues-light.webp differ diff --git a/web/public/empty-state/cycle/active-dark.webp b/web/public/empty-state/cycle/active-dark.webp new file mode 100644 index 00000000000..76de471941f Binary files /dev/null and b/web/public/empty-state/cycle/active-dark.webp differ diff --git a/web/public/empty-state/cycle/active-light.webp b/web/public/empty-state/cycle/active-light.webp new file mode 100644 index 00000000000..d5508c069ca Binary files /dev/null and b/web/public/empty-state/cycle/active-light.webp differ diff --git a/web/public/empty-state/cycle/completed-dark.webp b/web/public/empty-state/cycle/completed-dark.webp new file mode 100644 index 00000000000..9121d1f4d2f Binary files /dev/null and b/web/public/empty-state/cycle/completed-dark.webp differ diff --git a/web/public/empty-state/cycle/completed-light.webp b/web/public/empty-state/cycle/completed-light.webp new file mode 100644 index 00000000000..c1799c34a3d Binary files /dev/null and b/web/public/empty-state/cycle/completed-light.webp differ diff --git a/web/public/empty-state/cycle/draft-dark.webp b/web/public/empty-state/cycle/draft-dark.webp new file mode 100644 index 00000000000..251016532a8 Binary files /dev/null and b/web/public/empty-state/cycle/draft-dark.webp differ diff --git a/web/public/empty-state/cycle/draft-light.webp b/web/public/empty-state/cycle/draft-light.webp new file mode 100644 index 00000000000..7e809f36256 Binary files /dev/null and b/web/public/empty-state/cycle/draft-light.webp differ diff --git a/web/public/empty-state/cycle/upcoming-dark.webp b/web/public/empty-state/cycle/upcoming-dark.webp new file mode 100644 index 00000000000..d412702c0ac Binary files /dev/null and b/web/public/empty-state/cycle/upcoming-dark.webp differ diff --git a/web/public/empty-state/cycle/upcoming-light.webp b/web/public/empty-state/cycle/upcoming-light.webp new file mode 100644 index 00000000000..febf6ec897f Binary files /dev/null and b/web/public/empty-state/cycle/upcoming-light.webp differ diff --git a/web/public/empty-state/draft/empty-issues-dark.webp b/web/public/empty-state/draft/empty-issues-dark.webp new file mode 100644 index 00000000000..6f3f7b54ec3 Binary files /dev/null and b/web/public/empty-state/draft/empty-issues-dark.webp differ diff --git a/web/public/empty-state/draft/empty-issues-light.webp b/web/public/empty-state/draft/empty-issues-light.webp new file mode 100644 index 00000000000..427c5cdb1e9 Binary files /dev/null and b/web/public/empty-state/draft/empty-issues-light.webp differ diff --git a/web/public/empty-state/empty-filters/calendar-dark.webp b/web/public/empty-state/empty-filters/calendar-dark.webp new file mode 100644 index 00000000000..160e1d533ec Binary files /dev/null and b/web/public/empty-state/empty-filters/calendar-dark.webp differ diff --git a/web/public/empty-state/empty-filters/calendar-light.webp b/web/public/empty-state/empty-filters/calendar-light.webp new file mode 100644 index 00000000000..0ab8f52c618 Binary files /dev/null and b/web/public/empty-state/empty-filters/calendar-light.webp differ diff --git a/web/public/empty-state/empty-filters/gantt_chart-dark.webp b/web/public/empty-state/empty-filters/gantt_chart-dark.webp new file mode 100644 index 00000000000..861439ba4e6 Binary files /dev/null and b/web/public/empty-state/empty-filters/gantt_chart-dark.webp differ diff --git a/web/public/empty-state/empty-filters/gantt_chart-light.webp b/web/public/empty-state/empty-filters/gantt_chart-light.webp new file mode 100644 index 00000000000..22537f8c576 Binary files /dev/null and b/web/public/empty-state/empty-filters/gantt_chart-light.webp differ diff --git a/web/public/empty-state/empty-filters/kanban-dark.webp b/web/public/empty-state/empty-filters/kanban-dark.webp new file mode 100644 index 00000000000..7845a126fb3 Binary files /dev/null and b/web/public/empty-state/empty-filters/kanban-dark.webp differ diff --git a/web/public/empty-state/empty-filters/kanban-light.webp b/web/public/empty-state/empty-filters/kanban-light.webp new file mode 100644 index 00000000000..e071a09a484 Binary files /dev/null and b/web/public/empty-state/empty-filters/kanban-light.webp differ diff --git a/web/public/empty-state/empty-filters/list-dark.webp b/web/public/empty-state/empty-filters/list-dark.webp new file mode 100644 index 00000000000..8d9f160b8b7 Binary files /dev/null and b/web/public/empty-state/empty-filters/list-dark.webp differ diff --git a/web/public/empty-state/empty-filters/list-light.webp b/web/public/empty-state/empty-filters/list-light.webp new file mode 100644 index 00000000000..00bacf12e83 Binary files /dev/null and b/web/public/empty-state/empty-filters/list-light.webp differ diff --git a/web/public/empty-state/empty-filters/spreadsheet-dark.webp b/web/public/empty-state/empty-filters/spreadsheet-dark.webp new file mode 100644 index 00000000000..3e86982e212 Binary files /dev/null and b/web/public/empty-state/empty-filters/spreadsheet-dark.webp differ diff --git a/web/public/empty-state/empty-filters/spreadsheet-light.webp b/web/public/empty-state/empty-filters/spreadsheet-light.webp new file mode 100644 index 00000000000..e9c6e3a02c2 Binary files /dev/null and b/web/public/empty-state/empty-filters/spreadsheet-light.webp differ diff --git a/web/public/empty-state/onboarding/analytics-dark.webp b/web/public/empty-state/onboarding/analytics-dark.webp new file mode 100644 index 00000000000..f9ed54ae638 Binary files /dev/null and b/web/public/empty-state/onboarding/analytics-dark.webp differ diff --git a/web/public/empty-state/onboarding/analytics-light.webp b/web/public/empty-state/onboarding/analytics-light.webp new file mode 100644 index 00000000000..ca0f5f5516a Binary files /dev/null and b/web/public/empty-state/onboarding/analytics-light.webp differ diff --git a/web/public/empty-state/onboarding/cycles-dark.webp b/web/public/empty-state/onboarding/cycles-dark.webp new file mode 100644 index 00000000000..d655b5226ac Binary files /dev/null and b/web/public/empty-state/onboarding/cycles-dark.webp differ diff --git a/web/public/empty-state/onboarding/cycles-light.webp b/web/public/empty-state/onboarding/cycles-light.webp new file mode 100644 index 00000000000..ca069f50a6b Binary files /dev/null and b/web/public/empty-state/onboarding/cycles-light.webp differ diff --git a/web/public/empty-state/onboarding/dashboard-dark.webp b/web/public/empty-state/onboarding/dashboard-dark.webp new file mode 100644 index 00000000000..486060c09e4 Binary files /dev/null and b/web/public/empty-state/onboarding/dashboard-dark.webp differ diff --git a/web/public/empty-state/onboarding/dashboard-light.webp b/web/public/empty-state/onboarding/dashboard-light.webp new file mode 100644 index 00000000000..89d97bf0851 Binary files /dev/null and b/web/public/empty-state/onboarding/dashboard-light.webp differ diff --git a/web/public/empty-state/onboarding/issues-dark.webp b/web/public/empty-state/onboarding/issues-dark.webp new file mode 100644 index 00000000000..5444ac3832b Binary files /dev/null and b/web/public/empty-state/onboarding/issues-dark.webp differ diff --git a/web/public/empty-state/onboarding/issues-light.webp b/web/public/empty-state/onboarding/issues-light.webp new file mode 100644 index 00000000000..b875a5eb0fa Binary files /dev/null and b/web/public/empty-state/onboarding/issues-light.webp differ diff --git a/web/public/empty-state/onboarding/modules-dark.webp b/web/public/empty-state/onboarding/modules-dark.webp new file mode 100644 index 00000000000..ee86e78806e Binary files /dev/null and b/web/public/empty-state/onboarding/modules-dark.webp differ diff --git a/web/public/empty-state/onboarding/modules-light.webp b/web/public/empty-state/onboarding/modules-light.webp new file mode 100644 index 00000000000..1eedadb6ef8 Binary files /dev/null and b/web/public/empty-state/onboarding/modules-light.webp differ diff --git a/web/public/empty-state/onboarding/pages-dark.webp b/web/public/empty-state/onboarding/pages-dark.webp new file mode 100644 index 00000000000..278d228df93 Binary files /dev/null and b/web/public/empty-state/onboarding/pages-dark.webp differ diff --git a/web/public/empty-state/onboarding/pages-light.webp b/web/public/empty-state/onboarding/pages-light.webp new file mode 100644 index 00000000000..b7826e24477 Binary files /dev/null and b/web/public/empty-state/onboarding/pages-light.webp differ diff --git a/web/public/empty-state/onboarding/projects-dark.webp b/web/public/empty-state/onboarding/projects-dark.webp new file mode 100644 index 00000000000..2f34a02dee9 Binary files /dev/null and b/web/public/empty-state/onboarding/projects-dark.webp differ diff --git a/web/public/empty-state/onboarding/projects-light.webp b/web/public/empty-state/onboarding/projects-light.webp new file mode 100644 index 00000000000..54894cbde26 Binary files /dev/null and b/web/public/empty-state/onboarding/projects-light.webp differ diff --git a/web/public/empty-state/onboarding/views-dark.webp b/web/public/empty-state/onboarding/views-dark.webp new file mode 100644 index 00000000000..bca0f383ce7 Binary files /dev/null and b/web/public/empty-state/onboarding/views-dark.webp differ diff --git a/web/public/empty-state/onboarding/views-light.webp b/web/public/empty-state/onboarding/views-light.webp new file mode 100644 index 00000000000..cd6900834af Binary files /dev/null and b/web/public/empty-state/onboarding/views-light.webp differ diff --git a/web/public/empty-state/pages/all-dark.webp b/web/public/empty-state/pages/all-dark.webp new file mode 100644 index 00000000000..3c9ea167aff Binary files /dev/null and b/web/public/empty-state/pages/all-dark.webp differ diff --git a/web/public/empty-state/pages/all-light.webp b/web/public/empty-state/pages/all-light.webp new file mode 100644 index 00000000000..54b1b47d997 Binary files /dev/null and b/web/public/empty-state/pages/all-light.webp differ diff --git a/web/public/empty-state/pages/archived-dark.webp b/web/public/empty-state/pages/archived-dark.webp new file mode 100644 index 00000000000..3a9543b54fa Binary files /dev/null and b/web/public/empty-state/pages/archived-dark.webp differ diff --git a/web/public/empty-state/pages/archived-light.webp b/web/public/empty-state/pages/archived-light.webp new file mode 100644 index 00000000000..54cc928c99d Binary files /dev/null and b/web/public/empty-state/pages/archived-light.webp differ diff --git a/web/public/empty-state/pages/favorites-dark.webp b/web/public/empty-state/pages/favorites-dark.webp new file mode 100644 index 00000000000..9ae67887085 Binary files /dev/null and b/web/public/empty-state/pages/favorites-dark.webp differ diff --git a/web/public/empty-state/pages/favorites-light.webp b/web/public/empty-state/pages/favorites-light.webp new file mode 100644 index 00000000000..88b62af4d71 Binary files /dev/null and b/web/public/empty-state/pages/favorites-light.webp differ diff --git a/web/public/empty-state/pages/private-dark.webp b/web/public/empty-state/pages/private-dark.webp new file mode 100644 index 00000000000..ac3e836b4d3 Binary files /dev/null and b/web/public/empty-state/pages/private-dark.webp differ diff --git a/web/public/empty-state/pages/private-light.webp b/web/public/empty-state/pages/private-light.webp new file mode 100644 index 00000000000..760e1365726 Binary files /dev/null and b/web/public/empty-state/pages/private-light.webp differ diff --git a/web/public/empty-state/pages/recent-dark.webp b/web/public/empty-state/pages/recent-dark.webp new file mode 100644 index 00000000000..4a103354e71 Binary files /dev/null and b/web/public/empty-state/pages/recent-dark.webp differ diff --git a/web/public/empty-state/pages/recent-light.webp b/web/public/empty-state/pages/recent-light.webp new file mode 100644 index 00000000000..4b908d39819 Binary files /dev/null and b/web/public/empty-state/pages/recent-light.webp differ diff --git a/web/public/empty-state/pages/shared-dark.webp b/web/public/empty-state/pages/shared-dark.webp new file mode 100644 index 00000000000..941960280d0 Binary files /dev/null and b/web/public/empty-state/pages/shared-dark.webp differ diff --git a/web/public/empty-state/pages/shared-light.webp b/web/public/empty-state/pages/shared-light.webp new file mode 100644 index 00000000000..a3ead55f305 Binary files /dev/null and b/web/public/empty-state/pages/shared-light.webp differ diff --git a/web/public/empty-state/profile/activities-dark.webp b/web/public/empty-state/profile/activities-dark.webp new file mode 100644 index 00000000000..1b693947a0f Binary files /dev/null and b/web/public/empty-state/profile/activities-dark.webp differ diff --git a/web/public/empty-state/profile/activities-light.webp b/web/public/empty-state/profile/activities-light.webp new file mode 100644 index 00000000000..c287e0cd5e1 Binary files /dev/null and b/web/public/empty-state/profile/activities-light.webp differ diff --git a/web/public/empty-state/profile/assigned-dark.webp b/web/public/empty-state/profile/assigned-dark.webp new file mode 100644 index 00000000000..eaec74dde25 Binary files /dev/null and b/web/public/empty-state/profile/assigned-dark.webp differ diff --git a/web/public/empty-state/profile/assigned-light.webp b/web/public/empty-state/profile/assigned-light.webp new file mode 100644 index 00000000000..59a7b06e36e Binary files /dev/null and b/web/public/empty-state/profile/assigned-light.webp differ diff --git a/web/public/empty-state/profile/created-dark.webp b/web/public/empty-state/profile/created-dark.webp new file mode 100644 index 00000000000..12f153519ab Binary files /dev/null and b/web/public/empty-state/profile/created-dark.webp differ diff --git a/web/public/empty-state/profile/created-light.webp b/web/public/empty-state/profile/created-light.webp new file mode 100644 index 00000000000..f95679f112c Binary files /dev/null and b/web/public/empty-state/profile/created-light.webp differ diff --git a/web/public/empty-state/profile/issues-by-priority-dark.webp b/web/public/empty-state/profile/issues-by-priority-dark.webp new file mode 100644 index 00000000000..e1a71802e2d Binary files /dev/null and b/web/public/empty-state/profile/issues-by-priority-dark.webp differ diff --git a/web/public/empty-state/profile/issues-by-priority-light.webp b/web/public/empty-state/profile/issues-by-priority-light.webp new file mode 100644 index 00000000000..16abada6170 Binary files /dev/null and b/web/public/empty-state/profile/issues-by-priority-light.webp differ diff --git a/web/public/empty-state/profile/issues-by-state-dark.webp b/web/public/empty-state/profile/issues-by-state-dark.webp new file mode 100644 index 00000000000..82210aa864c Binary files /dev/null and b/web/public/empty-state/profile/issues-by-state-dark.webp differ diff --git a/web/public/empty-state/profile/issues-by-state-light.webp b/web/public/empty-state/profile/issues-by-state-light.webp new file mode 100644 index 00000000000..73788bb8694 Binary files /dev/null and b/web/public/empty-state/profile/issues-by-state-light.webp differ diff --git a/web/public/empty-state/profile/subscribed-dark.webp b/web/public/empty-state/profile/subscribed-dark.webp new file mode 100644 index 00000000000..ae30d3d5db5 Binary files /dev/null and b/web/public/empty-state/profile/subscribed-dark.webp differ diff --git a/web/public/empty-state/profile/subscribed-light.webp b/web/public/empty-state/profile/subscribed-light.webp new file mode 100644 index 00000000000..d24f58f245b Binary files /dev/null and b/web/public/empty-state/profile/subscribed-light.webp differ diff --git a/web/store/cycle.store.ts b/web/store/cycle.store.ts index cb0e76f33eb..ed5077385c5 100644 --- a/web/store/cycle.store.ts +++ b/web/store/cycle.store.ts @@ -14,8 +14,9 @@ import { CycleService } from "services/cycle.service"; export interface ICycleStore { //Loaders - fetchedMap: Record; + loader: boolean; // observables + fetchedMap: Record; cycleMap: Record; activeCycleIdMap: Record; // computed @@ -32,8 +33,8 @@ export interface ICycleStore { // actions validateDate: (workspaceSlug: string, projectId: string, payload: CycleDateCheckData) => Promise; // fetch - fetchAllCycles: (workspaceSlug: string, projectId: string) => Promise; - fetchActiveCycle: (workspaceSlug: string, projectId: string) => Promise; + fetchAllCycles: (workspaceSlug: string, projectId: string) => Promise; + fetchActiveCycle: (workspaceSlug: string, projectId: string) => Promise; fetchCycleDetails: (workspaceSlug: string, projectId: string, cycleId: string) => Promise; // crud createCycle: (workspaceSlug: string, projectId: string, data: Partial) => Promise; @@ -51,6 +52,7 @@ export interface ICycleStore { export class CycleStore implements ICycleStore { // observables + loader: boolean = false; cycleMap: Record = {}; activeCycleIdMap: Record = {}; //loaders @@ -65,6 +67,7 @@ export class CycleStore implements ICycleStore { constructor(_rootStore: RootStore) { makeObservable(this, { // observables + loader: observable.ref, cycleMap: observable, activeCycleIdMap: observable, fetchedMap: observable, @@ -221,16 +224,24 @@ export class CycleStore implements ICycleStore { * @param projectId * @returns */ - fetchAllCycles = async (workspaceSlug: string, projectId: string) => - await this.cycleService.getCyclesWithParams(workspaceSlug, projectId).then((response) => { - runInAction(() => { - response.forEach((cycle) => { - set(this.cycleMap, [cycle.id], cycle); + fetchAllCycles = async (workspaceSlug: string, projectId: string) => { + try { + this.loader = true; + await this.cycleService.getCyclesWithParams(workspaceSlug, projectId).then((response) => { + runInAction(() => { + response.forEach((cycle) => { + set(this.cycleMap, [cycle.id], cycle); + }); + set(this.fetchedMap, projectId, true); + this.loader = false; }); - set(this.fetchedMap, projectId, true); + return response; }); - return response; - }); + } catch (error) { + this.loader = false; + return undefined; + } + }; /** * @description fetches active cycle for a project diff --git a/web/store/module.store.ts b/web/store/module.store.ts index ff864310a25..f0f576cbcf0 100644 --- a/web/store/module.store.ts +++ b/web/store/module.store.ts @@ -11,6 +11,7 @@ import { RootStore } from "store/root.store"; export interface IModuleStore { //Loaders + loader: boolean; fetchedMap: Record; // observables moduleMap: Record; @@ -21,7 +22,7 @@ export interface IModuleStore { getProjectModuleIds: (projectId: string) => string[] | null; // actions // fetch - fetchModules: (workspaceSlug: string, projectId: string) => Promise; + fetchModules: (workspaceSlug: string, projectId: string) => Promise; fetchModuleDetails: (workspaceSlug: string, projectId: string, moduleId: string) => Promise; // crud createModule: (workspaceSlug: string, projectId: string, data: Partial) => Promise; @@ -53,6 +54,7 @@ export interface IModuleStore { export class ModulesStore implements IModuleStore { // observables + loader: boolean = false; moduleMap: Record = {}; //loaders fetchedMap: Record = {}; @@ -65,6 +67,7 @@ export class ModulesStore implements IModuleStore { constructor(_rootStore: RootStore) { makeObservable(this, { // observables + loader: observable.ref, moduleMap: observable, fetchedMap: observable, // computed @@ -128,16 +131,24 @@ export class ModulesStore implements IModuleStore { * @param projectId * @returns IModule[] */ - fetchModules = async (workspaceSlug: string, projectId: string) => - await this.moduleService.getModules(workspaceSlug, projectId).then((response) => { - runInAction(() => { - response.forEach((module) => { - set(this.moduleMap, [module.id], { ...this.moduleMap[module.id], ...module }); + fetchModules = async (workspaceSlug: string, projectId: string) => { + try { + this.loader = true; + await this.moduleService.getModules(workspaceSlug, projectId).then((response) => { + runInAction(() => { + response.forEach((module) => { + set(this.moduleMap, [module.id], { ...this.moduleMap[module.id], ...module }); + }); + set(this.fetchedMap, projectId, true); + this.loader = false; }); - set(this.fetchedMap, projectId, true); + return response; }); - return response; - }); + } catch (error) { + this.loader = false; + return undefined; + } + }; /** * @description fetch module details diff --git a/web/store/project-page.store.ts b/web/store/project-page.store.ts index f2e3f92279d..925d25e5103 100644 --- a/web/store/project-page.store.ts +++ b/web/store/project-page.store.ts @@ -10,6 +10,7 @@ import { RootStore } from "./root.store"; import { isThisWeek, isToday, isYesterday } from "date-fns"; export interface IProjectPageStore { + loader: boolean; projectPageMap: Record>; projectArchivedPageMap: Record>; @@ -30,6 +31,7 @@ export interface IProjectPageStore { } export class ProjectPageStore implements IProjectPageStore { + loader: boolean = false; projectPageMap: Record> = {}; // { projectId: [page1, page2] } projectArchivedPageMap: Record> = {}; // { projectId: [page1, page2] } @@ -39,6 +41,7 @@ export class ProjectPageStore implements IProjectPageStore { pageService; constructor(_rootStore: RootStore) { makeObservable(this, { + loader: observable.ref, projectPageMap: observable, projectArchivedPageMap: observable, @@ -152,15 +155,19 @@ export class ProjectPageStore implements IProjectPageStore { */ fetchProjectPages = async (workspaceSlug: string, projectId: string) => { try { + this.loader = true; await this.pageService.getProjectPages(workspaceSlug, projectId).then((response) => { runInAction(() => { for (const page of response) { set(this.projectPageMap, [projectId, page.id], new PageStore(page, this.rootStore)); } + this.loader = false; }); return response; }); } catch (e) { + this.loader = false; + throw e; } }; @@ -173,15 +180,18 @@ export class ProjectPageStore implements IProjectPageStore { */ fetchArchivedProjectPages = async (workspaceSlug: string, projectId: string) => { try { + this.loader = true; await this.pageService.getArchivedPages(workspaceSlug, projectId).then((response) => { runInAction(() => { for (const page of response) { set(this.projectArchivedPageMap, [projectId, page.id], new PageStore(page, this.rootStore)); } + this.loader = false; }); return response; }); } catch (e) { + this.loader = false; throw e; } }; diff --git a/web/store/project-view.store.ts b/web/store/project-view.store.ts index 39778f9b781..dd5c4a034ac 100644 --- a/web/store/project-view.store.ts +++ b/web/store/project-view.store.ts @@ -9,6 +9,7 @@ import { IProjectView } from "@plane/types"; export interface IProjectViewStore { //Loaders + loader: boolean; fetchedMap: Record; // observables viewMap: Record; @@ -17,7 +18,7 @@ export interface IProjectViewStore { // computed actions getViewById: (viewId: string) => IProjectView; // fetch actions - fetchViews: (workspaceSlug: string, projectId: string) => Promise; + fetchViews: (workspaceSlug: string, projectId: string) => Promise; fetchViewDetails: (workspaceSlug: string, projectId: string, viewId: string) => Promise; // CRUD actions createView: (workspaceSlug: string, projectId: string, data: Partial) => Promise; @@ -35,6 +36,7 @@ export interface IProjectViewStore { export class ProjectViewStore implements IProjectViewStore { // observables + loader: boolean = false; viewMap: Record = {}; //loaders fetchedMap: Record = {}; @@ -46,6 +48,7 @@ export class ProjectViewStore implements IProjectViewStore { constructor(_rootStore: RootStore) { makeObservable(this, { // observables + loader: observable.ref, viewMap: observable, fetchedMap: observable, // computed @@ -88,16 +91,24 @@ export class ProjectViewStore implements IProjectViewStore { * @param projectId * @returns Promise */ - fetchViews = async (workspaceSlug: string, projectId: string) => - await this.viewService.getViews(workspaceSlug, projectId).then((response) => { - runInAction(() => { - response.forEach((view) => { - set(this.viewMap, [view.id], view); + fetchViews = async (workspaceSlug: string, projectId: string) => { + try { + this.loader = true; + await this.viewService.getViews(workspaceSlug, projectId).then((response) => { + runInAction(() => { + response.forEach((view) => { + set(this.viewMap, [view.id], view); + }); + set(this.fetchedMap, projectId, true); + this.loader = false; }); - set(this.fetchedMap, projectId, true); + return response; }); - return response; - }); + } catch (error) { + this.loader = false; + return undefined; + } + }; /** * Fetches view details for a specific view