diff --git a/web/components/cycles/active-cycle-details.tsx b/web/components/cycles/active-cycle-details.tsx index d9309d4b5..a6457ab3c 100644 --- a/web/components/cycles/active-cycle-details.tsx +++ b/web/components/cycles/active-cycle-details.tsx @@ -1,10 +1,9 @@ import { MouseEvent } from "react"; import { observer } from "mobx-react-lite"; import Link from "next/link"; -import { useTheme } from "next-themes"; import useSWR from "swr"; // hooks -import { useCycle, useIssues, useMember, useProject, useUser } from "hooks/store"; +import { useCycle, useIssues, useMember, useProject } from "hooks/store"; // ui import { SingleProgressStats } from "components/core"; import { @@ -23,7 +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"; +import { EmptyState } from "components/empty-state"; // icons import { ArrowRight, CalendarCheck, CalendarDays, Star, Target } from "lucide-react"; // helpers @@ -35,7 +34,7 @@ import { ICycle, TCycleGroups } from "@plane/types"; 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 } from "constants/empty-state"; +import { EmptyStateType } from "constants/empty-state"; interface IActiveCycleDetails { workspaceSlug: string; @@ -45,9 +44,6 @@ interface IActiveCycleDetails { export const ActiveCycleDetails: React.FC = observer((props) => { // props const { workspaceSlug, projectId } = props; - const { resolvedTheme } = useTheme(); - // store hooks - const { currentUser } = useUser(); const { issues: { fetchActiveCycleIssues }, } = useIssues(EIssuesStoreType.CYCLE); @@ -78,11 +74,6 @@ export const ActiveCycleDetails: React.FC = observer((props : null ); - const emptyStateDetail = CYCLE_EMPTY_STATE_DETAILS["active"]; - - const isLightMode = resolvedTheme ? resolvedTheme === "light" : currentUser?.theme.theme === "light"; - const emptyStateImage = getEmptyStateImagePath("cycle", "active", isLightMode); - if (!activeCycle && isLoading) return ( @@ -90,15 +81,7 @@ export const ActiveCycleDetails: React.FC = observer((props ); - if (!activeCycle) - return ( - - ); + if (!activeCycle) return ; const endDate = new Date(activeCycle.end_date ?? ""); const startDate = new Date(activeCycle.start_date ?? ""); diff --git a/web/components/cycles/cycles-board.tsx b/web/components/cycles/cycles-board.tsx index 00c98e57c..278d55071 100644 --- a/web/components/cycles/cycles-board.tsx +++ b/web/components/cycles/cycles-board.tsx @@ -1,13 +1,10 @@ import { FC } from "react"; import { observer } from "mobx-react-lite"; -import { useTheme } from "next-themes"; -// hooks // components import { CyclePeekOverview, CyclesBoardCard } from "components/cycles"; -import { EmptyState, getEmptyStateImagePath } from "components/empty-state"; +import { EmptyState } from "components/empty-state"; // constants -import { CYCLE_EMPTY_STATE_DETAILS } from "constants/empty-state"; -import { useUser } from "hooks/store"; +import { EMPTY_STATE_DETAILS } from "constants/empty-state"; export interface ICyclesBoard { cycleIds: string[]; @@ -19,15 +16,6 @@ export interface ICyclesBoard { export const CyclesBoard: FC = observer((props) => { const { cycleIds, filter, workspaceSlug, projectId, peekCycle } = props; - // theme - const { resolvedTheme } = useTheme(); - // store hooks - const { currentUser } = useUser(); - - const emptyStateDetail = CYCLE_EMPTY_STATE_DETAILS[filter as keyof typeof CYCLE_EMPTY_STATE_DETAILS]; - - const isLightMode = resolvedTheme ? resolvedTheme === "light" : currentUser?.theme.theme === "light"; - const emptyStateImage = getEmptyStateImagePath("cycle", filter, isLightMode); return ( <> @@ -52,12 +40,7 @@ export const CyclesBoard: FC = observer((props) => { ) : ( - + )} ); diff --git a/web/components/cycles/cycles-list.tsx b/web/components/cycles/cycles-list.tsx index 99cf1f2b1..f6ad64f99 100644 --- a/web/components/cycles/cycles-list.tsx +++ b/web/components/cycles/cycles-list.tsx @@ -1,15 +1,12 @@ import { FC } from "react"; import { observer } from "mobx-react-lite"; -import { useTheme } from "next-themes"; -// hooks // components -import { Loader } from "@plane/ui"; import { CyclePeekOverview, CyclesListItem } from "components/cycles"; -import { EmptyState, getEmptyStateImagePath } from "components/empty-state"; +import { EmptyState } from "components/empty-state"; // ui +import { Loader } from "@plane/ui"; // constants -import { CYCLE_EMPTY_STATE_DETAILS } from "constants/empty-state"; -import { useUser } from "hooks/store"; +import { EMPTY_STATE_DETAILS } from "constants/empty-state"; export interface ICyclesList { cycleIds: string[]; @@ -20,15 +17,6 @@ export interface ICyclesList { export const CyclesList: FC = observer((props) => { const { cycleIds, filter, workspaceSlug, projectId } = props; - // theme - const { resolvedTheme } = useTheme(); - // store hooks - const { currentUser } = useUser(); - - const emptyStateDetail = CYCLE_EMPTY_STATE_DETAILS[filter as keyof typeof CYCLE_EMPTY_STATE_DETAILS]; - - const isLightMode = resolvedTheme ? resolvedTheme === "light" : currentUser?.theme.theme === "light"; - const emptyStateImage = getEmptyStateImagePath("cycle", filter, isLightMode); return ( <> @@ -54,12 +42,7 @@ export const CyclesList: FC = observer((props) => { ) : ( - + )} ) : ( diff --git a/web/components/empty-state/empty-state.tsx b/web/components/empty-state/empty-state.tsx index 9d77a81d0..9ef216068 100644 --- a/web/components/empty-state/empty-state.tsx +++ b/web/components/empty-state/empty-state.tsx @@ -1,119 +1,150 @@ import React from "react"; +import Link from "next/link"; import Image from "next/image"; -// components -// ui -import { Button, getButtonStyling } from "@plane/ui"; -// helper -import { cn } from "helpers/common.helper"; -import { ComicBoxButton } from "./comic-box-button"; -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; +import { useTheme } from "next-themes"; +// hooks +import { useUser } from "hooks/store"; +// components +import { Button, TButtonVariant } from "@plane/ui"; +import { ComicBoxButton } from "./comic-box-button"; +// constant +import { EMPTY_STATE_DETAILS, EmptyStateKeys } from "constants/empty-state"; +// helpers +import { cn } from "helpers/common.helper"; + +export type EmptyStateProps = { + type: EmptyStateKeys; + size?: "sm" | "md" | "lg"; + layout?: "widget-simple" | "screen-detailed" | "screen-simple"; + additionalPath?: string; + primaryButtonOnClick?: () => void; + primaryButtonLink?: string; + secondaryButtonOnClick?: () => void; }; -export const EmptyState: React.FC = ({ - title, - description, - image, - primaryButton, - secondaryButton, - comicBox, - size = "sm", - disabled = false, -}) => { - const emptyStateHeader = ( +export const EmptyState: React.FC = (props) => { + const { + type, + size = "lg", + layout = "screen-detailed", + additionalPath = "", + primaryButtonOnClick, + primaryButtonLink, + secondaryButtonOnClick, + } = props; + // store + const { + membership: { currentWorkspaceRole, currentProjectRole }, + } = useUser(); + // theme + const { resolvedTheme } = useTheme(); + // current empty state details + const { key, title, description, path, primaryButton, secondaryButton, accessType, access } = + EMPTY_STATE_DETAILS[type]; + // resolved empty state path + const resolvedEmptyStatePath = `${additionalPath && additionalPath !== "" ? `${path}${additionalPath}` : path}-${ + resolvedTheme === "light" ? "light" : "dark" + }.webp`; + // current access type + const currentAccessType = accessType === "workspace" ? currentWorkspaceRole : currentProjectRole; + // permission + const isEditingAllowed = currentAccessType && access && currentAccessType >= access; + const anyButton = primaryButton || secondaryButton; + + // primary button + const renderPrimaryButton = () => { + if (!primaryButton) return null; + + const commonProps = { + size: size, + variant: "primary" as TButtonVariant, + prependIcon: primaryButton.icon, + onClick: primaryButtonOnClick ? primaryButtonOnClick : undefined, + disabled: !isEditingAllowed, + }; + + if (primaryButton.comicBox) { + return ( + + ); + } else if (primaryButtonLink) { + return ( + + + + ); + } else { + return ; + } + }; + // secondary button + const renderSecondaryButton = () => { + if (!secondaryButton) return null; + + return ( + + ); + }; + + return ( <> - {description ? ( - <> -

{title}

-

{description}

- - ) : ( -

{title}

+ {layout === "screen-detailed" && ( +
+
+
+ {description ? ( + <> +

{title}

+

{description}

+ + ) : ( +

{title}

+ )} +
+ + {path && ( + {key + )} + + {anyButton && ( + <> +
+ {renderPrimaryButton()} + {renderSecondaryButton()} +
+ + )} +
+
)} ); - - const secondaryButtonElement = secondaryButton && ( - - ); - - return ( -
-
-
{emptyStateHeader}
- - {primaryButton?.text - -
- {primaryButton && ( - <> -
- {comicBox ? ( - primaryButton.onClick()} - disabled={disabled} - /> - ) : ( -
primaryButton.onClick()} - > - {primaryButton.icon} - {primaryButton.text} -
- )} -
- - )} - - {secondaryButton && secondaryButtonElement} -
-
-
- ); }; diff --git a/web/components/estimates/estimates-list.tsx b/web/components/estimates/estimates-list.tsx index 8e447d6ac..1769ba016 100644 --- a/web/components/estimates/estimates-list.tsx +++ b/web/components/estimates/estimates-list.tsx @@ -1,20 +1,19 @@ import React, { useState } from "react"; import { observer } from "mobx-react-lite"; import { useRouter } from "next/router"; -import { useTheme } from "next-themes"; // store hooks -import { Button, Loader, TOAST_TYPE, setToast } from "@plane/ui"; -import { EmptyState, getEmptyStateImagePath } from "components/empty-state"; -import { CreateUpdateEstimateModal, DeleteEstimateModal, EstimateListItem } from "components/estimates"; -import { PROJECT_SETTINGS_EMPTY_STATE_DETAILS } from "constants/empty-state"; -import { orderArrayBy } from "helpers/array.helper"; -import { useEstimate, useProject, useUser } from "hooks/store"; +import { useEstimate, useProject } from "hooks/store"; // components +import { CreateUpdateEstimateModal, DeleteEstimateModal, EstimateListItem } from "components/estimates"; +import { EmptyState } from "components/empty-state"; // ui +import { Button, Loader, TOAST_TYPE, setToast } from "@plane/ui"; // types import { IEstimate } from "@plane/types"; // helpers +import { orderArrayBy } from "helpers/array.helper"; // constants +import { EmptyStateType } from "constants/empty-state"; export const EstimatesList: React.FC = observer(() => { // states @@ -24,12 +23,9 @@ export const EstimatesList: React.FC = observer(() => { // router const router = useRouter(); const { workspaceSlug, projectId } = router.query; - // theme - const { resolvedTheme } = useTheme(); // store hooks const { updateProject, currentProjectDetails } = useProject(); const { projectEstimates, getProjectEstimateById } = useEstimate(); - const { currentUser } = useUser(); const editEstimate = (estimate: IEstimate) => { setEstimateFormOpen(true); @@ -55,10 +51,6 @@ export const EstimatesList: React.FC = observer(() => { }); }; - const emptyStateDetail = PROJECT_SETTINGS_EMPTY_STATE_DETAILS["estimate"]; - const isLightMode = resolvedTheme ? resolvedTheme === "light" : currentUser?.theme.theme === "light"; - const emptyStateImage = getEmptyStateImagePath("project-settings", "estimates", isLightMode); - return ( <> { ) : (
- +
) ) : ( diff --git a/web/components/exporter/guide.tsx b/web/components/exporter/guide.tsx index 381b168bd..03d925b62 100644 --- a/web/components/exporter/guide.tsx +++ b/web/components/exporter/guide.tsx @@ -4,26 +4,24 @@ import { observer } from "mobx-react-lite"; import Image from "next/image"; import Link from "next/link"; import { useRouter } from "next/router"; -import { useTheme } from "next-themes"; import useSWR, { mutate } from "swr"; // hooks -import { MoveLeft, MoveRight, RefreshCw } from "lucide-react"; -import { Button } from "@plane/ui"; -import { EmptyState, getEmptyStateImagePath } from "components/empty-state"; -import { Exporter, SingleExport } from "components/exporter"; -import { ImportExportSettingsLoader } from "components/ui"; -import { WORKSPACE_SETTINGS_EMPTY_STATE_DETAILS } from "constants/empty-state"; -import { EXPORT_SERVICES_LIST } from "constants/fetch-keys"; -import { EXPORTERS_LIST } from "constants/workspace"; import { useUser } from "hooks/store"; import useUserAuth from "hooks/use-user-auth"; // services import { IntegrationService } from "services/integrations"; // components +import { Exporter, SingleExport } from "components/exporter"; +import { ImportExportSettingsLoader } from "components/ui"; +import { EmptyState } from "components/empty-state"; // ui +import { Button } from "@plane/ui"; // icons -// fetch-keys +import { MoveLeft, MoveRight, RefreshCw } from "lucide-react"; // constants +import { EXPORT_SERVICES_LIST } from "constants/fetch-keys"; +import { EXPORTERS_LIST } from "constants/workspace"; +import { EmptyStateType } from "constants/empty-state"; // services const integrationService = new IntegrationService(); @@ -36,8 +34,6 @@ const IntegrationGuide = observer(() => { // router const router = useRouter(); const { workspaceSlug, provider } = router.query; - // theme - const { resolvedTheme } = useTheme(); // store hooks const { currentUser, currentUserLoader } = useUser(); // custom hooks @@ -50,10 +46,6 @@ const IntegrationGuide = observer(() => { : null ); - const emptyStateDetail = WORKSPACE_SETTINGS_EMPTY_STATE_DETAILS["export"]; - const isLightMode = resolvedTheme ? resolvedTheme === "light" : currentUser?.theme.theme === "light"; - const emptyStateImage = getEmptyStateImagePath("workspace-settings", "exports", isLightMode); - const handleCsvClose = () => { router.replace(`/${workspaceSlug?.toString()}/settings/exports`); }; @@ -149,12 +141,7 @@ const IntegrationGuide = observer(() => { ) : (
- +
) ) : ( diff --git a/web/components/integration/guide.tsx b/web/components/integration/guide.tsx index a75c71f1f..84d422d12 100644 --- a/web/components/integration/guide.tsx +++ b/web/components/integration/guide.tsx @@ -3,28 +3,26 @@ import { observer } from "mobx-react-lite"; import Image from "next/image"; import Link from "next/link"; import { useRouter } from "next/router"; -import { useTheme } from "next-themes"; import useSWR, { mutate } from "swr"; // hooks -import { RefreshCw } from "lucide-react"; -import { Button } from "@plane/ui"; -import { EmptyState, getEmptyStateImagePath } from "components/empty-state"; -import { DeleteImportModal, GithubImporterRoot, JiraImporterRoot, SingleImport } from "components/integration"; -import { ImportExportSettingsLoader } from "components/ui"; -import { WORKSPACE_SETTINGS_EMPTY_STATE_DETAILS } from "constants/empty-state"; -import { IMPORTER_SERVICES_LIST } from "constants/fetch-keys"; -import { IMPORTERS_LIST } from "constants/workspace"; import { useUser } from "hooks/store"; import useUserAuth from "hooks/use-user-auth"; // services import { IntegrationService } from "services/integrations"; // components +import { ImportExportSettingsLoader } from "components/ui"; +import { DeleteImportModal, GithubImporterRoot, JiraImporterRoot, SingleImport } from "components/integration"; +import { EmptyState } from "components/empty-state"; // ui +import { Button } from "@plane/ui"; // icons +import { RefreshCw } from "lucide-react"; // types import { IImporterService } from "@plane/types"; -// fetch-keys // constants +import { IMPORTER_SERVICES_LIST } from "constants/fetch-keys"; +import { IMPORTERS_LIST } from "constants/workspace"; +import { EmptyStateType } from "constants/empty-state"; // services const integrationService = new IntegrationService(); @@ -37,8 +35,6 @@ const IntegrationGuide = observer(() => { // router const router = useRouter(); const { workspaceSlug, provider } = router.query; - // theme - const { resolvedTheme } = useTheme(); // store hooks const { currentUser, currentUserLoader } = useUser(); // custom hooks @@ -49,10 +45,6 @@ const IntegrationGuide = observer(() => { workspaceSlug ? () => integrationService.getImporterServicesList(workspaceSlug as string) : null ); - const emptyStateDetail = WORKSPACE_SETTINGS_EMPTY_STATE_DETAILS["import"]; - const isLightMode = resolvedTheme ? resolvedTheme === "light" : currentUser?.theme.theme === "light"; - const emptyStateImage = getEmptyStateImagePath("workspace-settings", "imports", isLightMode); - const handleDeleteImport = (importService: IImporterService) => { setImportToDelete(importService); setDeleteImportModal(true); @@ -145,12 +137,7 @@ const IntegrationGuide = observer(() => { ) : (
- +
) ) : ( diff --git a/web/components/issues/issue-layouts/empty-states/archived-issues.tsx b/web/components/issues/issue-layouts/empty-states/archived-issues.tsx index 96887ed60..c9de2279c 100644 --- a/web/components/issues/issue-layouts/empty-states/archived-issues.tsx +++ b/web/components/issues/issue-layouts/empty-states/archived-issues.tsx @@ -1,49 +1,27 @@ import size from "lodash/size"; import { observer } from "mobx-react-lite"; import { useRouter } from "next/router"; -import { useTheme } from "next-themes"; // hooks -import { EmptyState, getEmptyStateImagePath } from "components/empty-state"; -import { EMPTY_FILTER_STATE_DETAILS, EMPTY_ISSUE_STATE_DETAILS } from "constants/empty-state"; -import { EIssueFilterType, EIssuesStoreType } from "constants/issue"; -import { EUserProjectRoles } from "constants/project"; -import { useIssues, useUser } from "hooks/store"; +import { useIssues } from "hooks/store"; // components +import { EmptyState } from "components/empty-state"; // constants +import { EIssueFilterType, EIssuesStoreType } from "constants/issue"; +import { EmptyStateType } from "constants/empty-state"; // 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; // theme - const { resolvedTheme } = useTheme(); // store hooks - const { - membership: { currentProjectRole }, - currentUser, - } = useUser(); const { issuesFilter } = useIssues(EIssuesStoreType.ARCHIVED); const userFilters = issuesFilter?.issueFilters?.filters; const activeLayout = issuesFilter?.issueFilters?.displayFilters?.layout; - const isLightMode = resolvedTheme ? resolvedTheme === "light" : currentUser?.theme.theme === "light"; - const currentLayoutEmptyStateImagePath = getEmptyStateImagePath("empty-filters", activeLayout ?? "list", isLightMode); - const EmptyStateImagePath = getEmptyStateImagePath("archived", "empty-issues", isLightMode); - const issueFilterCount = size( Object.fromEntries( Object.entries(userFilters ?? {}).filter(([, value]) => value && Array.isArray(value) && value.length > 0) @@ -61,33 +39,20 @@ export const ProjectArchivedEmptyState: React.FC = observer(() => { }); }; - const isEditingAllowed = !!currentProjectRole && currentProjectRole >= EUserProjectRoles.MEMBER; - - const emptyStateProps: EmptyStateProps = - issueFilterCount > 0 - ? { - title: EMPTY_FILTER_STATE_DETAILS["archived"].title, - image: currentLayoutEmptyStateImagePath, - secondaryButton: { - text: EMPTY_FILTER_STATE_DETAILS["archived"].secondaryButton?.text, - onClick: handleClearAllFilters, - }, - } - : { - title: EMPTY_ISSUE_STATE_DETAILS["archived"].title, - description: EMPTY_ISSUE_STATE_DETAILS["archived"].description, - image: EmptyStateImagePath, - primaryButton: { - text: EMPTY_ISSUE_STATE_DETAILS["archived"].primaryButton.text, - onClick: () => router.push(`/${workspaceSlug}/projects/${projectId}/settings/automations`), - }, - size: "sm", - disabled: !isEditingAllowed, - }; + const emptyStateType = + issueFilterCount > 0 ? EmptyStateType.PROJECT_ARCHIVED_EMPTY_FILTER : EmptyStateType.PROJECT_ARCHIVED_NO_ISSUES; + const additionalPath = issueFilterCount > 0 ? activeLayout ?? "list" : undefined; return (
- + 0 ? undefined : `/${workspaceSlug}/projects/${projectId}/settings/automations` + } + secondaryButtonOnClick={issueFilterCount > 0 ? handleClearAllFilters : undefined} + />
); }); diff --git a/web/components/issues/issue-layouts/empty-states/cycle.tsx b/web/components/issues/issue-layouts/empty-states/cycle.tsx index 7f8c318c7..350e4dbb4 100644 --- a/web/components/issues/issue-layouts/empty-states/cycle.tsx +++ b/web/components/issues/issue-layouts/empty-states/cycle.tsx @@ -1,21 +1,17 @@ import { useState } from "react"; import { observer } from "mobx-react-lite"; -import { useTheme } from "next-themes"; -import { PlusIcon } from "lucide-react"; // hooks +import { useApplication, useEventTracker, useIssues } from "hooks/store"; +// ui import { TOAST_TYPE, setToast } from "@plane/ui"; import { ExistingIssuesListModal } from "components/core"; -// ui -// components -import { EmptyState, getEmptyStateImagePath } from "components/empty-state"; -import { CYCLE_EMPTY_STATE_DETAILS, EMPTY_FILTER_STATE_DETAILS } from "constants/empty-state"; -import { EIssuesStoreType } from "constants/issue"; -import { EUserProjectRoles } from "constants/project"; -import { useApplication, useEventTracker, useIssues, useUser } from "hooks/store"; // components +import { EmptyState } from "components/empty-state"; // types import { ISearchIssueResponse, TIssueLayouts } from "@plane/types"; // constants +import { EIssuesStoreType } from "constants/issue"; +import { EmptyStateType } from "constants/empty-state"; type Props = { workspaceSlug: string | undefined; @@ -26,33 +22,16 @@ type Props = { isEmptyFilters?: boolean; }; -interface EmptyStateProps { - title: string; - image: string; - description?: string; - comicBox?: { title: string; description: string }; - primaryButton?: { text: string; icon?: React.ReactNode; onClick: () => void }; - secondaryButton?: { text: string; icon?: React.ReactNode; onClick: () => void }; - size?: "lg" | "sm" | undefined; - disabled?: boolean | undefined; -} - export const CycleEmptyState: React.FC = observer((props) => { const { workspaceSlug, projectId, cycleId, activeLayout, handleClearAllFilters, isEmptyFilters = false } = props; // states const [cycleIssuesListModal, setCycleIssuesListModal] = useState(false); - // theme - const { resolvedTheme } = useTheme(); // store hooks const { issues } = useIssues(EIssuesStoreType.CYCLE); const { commandPalette: { toggleCreateIssueModal }, } = useApplication(); const { setTrackElement } = useEventTracker(); - const { - membership: { currentProjectRole: userRole }, - currentUser, - } = useUser(); const handleAddIssuesToCycle = async (data: ISearchIssueResponse[]) => { if (!workspaceSlug || !projectId || !cycleId) return; @@ -77,43 +56,9 @@ export const CycleEmptyState: React.FC = observer((props) => { ); }; - const emptyStateDetail = CYCLE_EMPTY_STATE_DETAILS["no-issues"]; - - const isLightMode = resolvedTheme ? resolvedTheme === "light" : currentUser?.theme.theme === "light"; - const currentLayoutEmptyStateImagePath = getEmptyStateImagePath("empty-filters", activeLayout ?? "list", isLightMode); - const emptyStateImage = getEmptyStateImagePath("cycle-issues", activeLayout ?? "list", isLightMode); - - const isEditingAllowed = !!userRole && userRole >= EUserProjectRoles.MEMBER; - - const emptyStateProps: EmptyStateProps = isEmptyFilters - ? { - title: EMPTY_FILTER_STATE_DETAILS["project"].title, - image: currentLayoutEmptyStateImagePath, - secondaryButton: { - text: EMPTY_FILTER_STATE_DETAILS["project"].secondaryButton.text, - onClick: handleClearAllFilters, - }, - } - : { - title: emptyStateDetail.title, - description: emptyStateDetail.description, - image: emptyStateImage, - primaryButton: { - text: emptyStateDetail.primaryButton.text, - icon: , - onClick: () => { - setTrackElement("Cycle issue empty state"); - toggleCreateIssueModal(true, EIssuesStoreType.CYCLE); - }, - }, - secondaryButton: { - text: emptyStateDetail.secondaryButton.text, - icon: , - onClick: () => setCycleIssuesListModal(true), - }, - size: "sm", - disabled: !isEditingAllowed, - }; + const emptyStateType = isEmptyFilters ? EmptyStateType.PROJECT_EMPTY_FILTER : EmptyStateType.PROJECT_CYCLE_NO_ISSUES; + const additionalPath = activeLayout ?? "list"; + const emptyStateSize = isEmptyFilters ? "lg" : "sm"; return ( <> @@ -126,7 +71,20 @@ export const CycleEmptyState: React.FC = observer((props) => { handleOnSubmit={handleAddIssuesToCycle} />
- + { + setTrackElement("Cycle issue empty state"); + toggleCreateIssueModal(true, EIssuesStoreType.CYCLE); + } + } + secondaryButtonOnClick={isEmptyFilters ? handleClearAllFilters : () => setCycleIssuesListModal(true)} + />
); diff --git a/web/components/issues/issue-layouts/empty-states/draft-issues.tsx b/web/components/issues/issue-layouts/empty-states/draft-issues.tsx index 77b1123b6..0968ed07a 100644 --- a/web/components/issues/issue-layouts/empty-states/draft-issues.tsx +++ b/web/components/issues/issue-layouts/empty-states/draft-issues.tsx @@ -1,49 +1,26 @@ import size from "lodash/size"; import { observer } from "mobx-react-lite"; import { useRouter } from "next/router"; -import { useTheme } from "next-themes"; // hooks -import { EmptyState, getEmptyStateImagePath } from "components/empty-state"; -import { EMPTY_FILTER_STATE_DETAILS, EMPTY_ISSUE_STATE_DETAILS } from "constants/empty-state"; -import { EIssueFilterType, EIssuesStoreType } from "constants/issue"; -import { EUserProjectRoles } from "constants/project"; -import { useIssues, useUser } from "hooks/store"; +import { useIssues } from "hooks/store"; // components +import { EmptyState } from "components/empty-state"; // constants +import { EIssueFilterType, EIssuesStoreType } from "constants/issue"; +import { EmptyStateType } from "constants/empty-state"; // 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; - // theme - const { resolvedTheme } = useTheme(); // store hooks - const { - membership: { currentProjectRole }, - currentUser, - } = useUser(); const { issuesFilter } = useIssues(EIssuesStoreType.DRAFT); const userFilters = issuesFilter?.issueFilters?.filters; const activeLayout = issuesFilter?.issueFilters?.displayFilters?.layout; - const isLightMode = resolvedTheme ? resolvedTheme === "light" : currentUser?.theme.theme === "light"; - const currentLayoutEmptyStateImagePath = getEmptyStateImagePath("empty-filters", activeLayout ?? "list", isLightMode); - const EmptyStateImagePath = getEmptyStateImagePath("draft", "draft-issues-empty", isLightMode); - const issueFilterCount = size( Object.fromEntries( Object.entries(userFilters ?? {}).filter(([, value]) => value && Array.isArray(value) && value.length > 0) @@ -61,29 +38,19 @@ export const ProjectDraftEmptyState: React.FC = observer(() => { }); }; - const isEditingAllowed = !!currentProjectRole && currentProjectRole >= EUserProjectRoles.MEMBER; - - const emptyStateProps: EmptyStateProps = - issueFilterCount > 0 - ? { - title: EMPTY_FILTER_STATE_DETAILS["draft"].title, - image: currentLayoutEmptyStateImagePath, - secondaryButton: { - text: EMPTY_FILTER_STATE_DETAILS["draft"].secondaryButton.text, - onClick: handleClearAllFilters, - }, - } - : { - title: EMPTY_ISSUE_STATE_DETAILS["draft"].title, - description: EMPTY_ISSUE_STATE_DETAILS["draft"].description, - image: EmptyStateImagePath, - size: "sm", - disabled: !isEditingAllowed, - }; + const emptyStateType = + issueFilterCount > 0 ? EmptyStateType.PROJECT_DRAFT_EMPTY_FILTER : EmptyStateType.PROJECT_DRAFT_NO_ISSUES; + const additionalPath = issueFilterCount > 0 ? activeLayout ?? "list" : undefined; + const emptyStateSize = issueFilterCount > 0 ? "lg" : "sm"; return (
- + 0 ? handleClearAllFilters : undefined} + />
); }); diff --git a/web/components/issues/issue-layouts/empty-states/module.tsx b/web/components/issues/issue-layouts/empty-states/module.tsx index c17099335..6c0cd0cd6 100644 --- a/web/components/issues/issue-layouts/empty-states/module.tsx +++ b/web/components/issues/issue-layouts/empty-states/module.tsx @@ -1,20 +1,18 @@ import { useState } from "react"; import { observer } from "mobx-react-lite"; -import { useTheme } from "next-themes"; -import { PlusIcon } from "lucide-react"; // hooks +import { useApplication, useEventTracker, useIssues } from "hooks/store"; +// ui import { TOAST_TYPE, setToast } from "@plane/ui"; -import { ExistingIssuesListModal } from "components/core"; -import { EmptyState, getEmptyStateImagePath } from "components/empty-state"; -import { EMPTY_FILTER_STATE_DETAILS, MODULE_EMPTY_STATE_DETAILS } from "constants/empty-state"; -import { EIssuesStoreType } from "constants/issue"; -import { EUserProjectRoles } from "constants/project"; -import { useApplication, useEventTracker, useIssues, useUser } from "hooks/store"; // ui // components +import { ExistingIssuesListModal } from "components/core"; +import { EmptyState } from "components/empty-state"; // types import { ISearchIssueResponse, TIssueLayouts } from "@plane/types"; // constants +import { EIssuesStoreType } from "constants/issue"; +import { EmptyStateType } from "constants/empty-state"; type Props = { workspaceSlug: string | undefined; @@ -25,33 +23,16 @@ type Props = { isEmptyFilters?: boolean; }; -interface EmptyStateProps { - title: string; - image: string; - description?: string; - comicBox?: { title: string; description: string }; - primaryButton?: { text: string; icon?: React.ReactNode; onClick: () => void }; - secondaryButton?: { text: string; icon?: React.ReactNode; onClick: () => void }; - size?: "lg" | "sm" | undefined; - disabled?: boolean | undefined; -} - export const ModuleEmptyState: React.FC = observer((props) => { const { workspaceSlug, projectId, moduleId, activeLayout, handleClearAllFilters, isEmptyFilters = false } = props; // states const [moduleIssuesListModal, setModuleIssuesListModal] = useState(false); - // theme - const { resolvedTheme } = useTheme(); // store hooks const { issues } = useIssues(EIssuesStoreType.MODULE); const { commandPalette: { toggleCreateIssueModal }, } = useApplication(); const { setTrackElement } = useEventTracker(); - const { - membership: { currentProjectRole: userRole }, - currentUser, - } = useUser(); const handleAddIssuesToModule = async (data: ISearchIssueResponse[]) => { if (!workspaceSlug || !projectId || !moduleId) return; @@ -75,42 +56,8 @@ export const ModuleEmptyState: React.FC = observer((props) => { ); }; - const emptyStateDetail = MODULE_EMPTY_STATE_DETAILS["no-issues"]; - - const isLightMode = resolvedTheme ? resolvedTheme === "light" : currentUser?.theme.theme === "light"; - const currentLayoutEmptyStateImagePath = getEmptyStateImagePath("empty-filters", activeLayout ?? "list", isLightMode); - const emptyStateImage = getEmptyStateImagePath("module-issues", activeLayout ?? "list", isLightMode); - - const isEditingAllowed = !!userRole && userRole >= EUserProjectRoles.MEMBER; - - const emptyStateProps: EmptyStateProps = isEmptyFilters - ? { - title: EMPTY_FILTER_STATE_DETAILS["project"].title, - image: currentLayoutEmptyStateImagePath, - secondaryButton: { - text: EMPTY_FILTER_STATE_DETAILS["project"].secondaryButton.text, - onClick: handleClearAllFilters, - }, - } - : { - title: emptyStateDetail.title, - description: emptyStateDetail.description, - image: emptyStateImage, - primaryButton: { - text: emptyStateDetail.primaryButton.text, - icon: , - onClick: () => { - setTrackElement("Module issue empty state"); - toggleCreateIssueModal(true, EIssuesStoreType.MODULE); - }, - }, - secondaryButton: { - text: emptyStateDetail.secondaryButton.text, - icon: , - onClick: () => setModuleIssuesListModal(true), - }, - disabled: !isEditingAllowed, - }; + const emptyStateType = isEmptyFilters ? EmptyStateType.PROJECT_EMPTY_FILTER : EmptyStateType.PROJECT_MODULE_ISSUES; + const additionalPath = activeLayout ?? "list"; return ( <> @@ -123,7 +70,19 @@ export const ModuleEmptyState: React.FC = observer((props) => { handleOnSubmit={handleAddIssuesToModule} />
- + { + setTrackElement("Cycle issue empty state"); + toggleCreateIssueModal(true, EIssuesStoreType.CYCLE); + } + } + secondaryButtonOnClick={isEmptyFilters ? handleClearAllFilters : () => setModuleIssuesListModal(true)} + />
); diff --git a/web/components/issues/issue-layouts/empty-states/project-issues.tsx b/web/components/issues/issue-layouts/empty-states/project-issues.tsx index e44dd5626..12642d364 100644 --- a/web/components/issues/issue-layouts/empty-states/project-issues.tsx +++ b/web/components/issues/issue-layouts/empty-states/project-issues.tsx @@ -1,51 +1,29 @@ import size from "lodash/size"; import { observer } from "mobx-react-lite"; import { useRouter } from "next/router"; -import { useTheme } from "next-themes"; // hooks -import { EmptyState, getEmptyStateImagePath } from "components/empty-state"; -import { EMPTY_FILTER_STATE_DETAILS, EMPTY_ISSUE_STATE_DETAILS } from "constants/empty-state"; -import { EIssueFilterType, EIssuesStoreType } from "constants/issue"; -import { EUserProjectRoles } from "constants/project"; -import { useApplication, useEventTracker, useIssues, useUser } from "hooks/store"; +import { useApplication, useEventTracker, useIssues } from "hooks/store"; // components +import { EmptyState } from "components/empty-state"; // constants +import { EIssueFilterType, EIssuesStoreType } from "constants/issue"; +import { EmptyStateType } from "constants/empty-state"; // 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; - // theme - const { resolvedTheme } = useTheme(); // store hooks const { commandPalette: commandPaletteStore } = useApplication(); const { setTrackElement } = useEventTracker(); - const { - membership: { currentProjectRole }, - currentUser, - } = useUser(); + const { issuesFilter } = useIssues(EIssuesStoreType.PROJECT); const userFilters = issuesFilter?.issueFilters?.filters; const activeLayout = issuesFilter?.issueFilters?.displayFilters?.layout; - const isLightMode = resolvedTheme ? resolvedTheme === "light" : currentUser?.theme.theme === "light"; - const currentLayoutEmptyStateImagePath = getEmptyStateImagePath("empty-filters", activeLayout ?? "list", isLightMode); - const EmptyStateImagePath = getEmptyStateImagePath("onboarding", "issues", isLightMode); - const issueFilterCount = size( Object.fromEntries( Object.entries(userFilters ?? {}).filter(([, value]) => value && Array.isArray(value) && value.length > 0) @@ -63,40 +41,26 @@ export const ProjectEmptyState: React.FC = observer(() => { }); }; - const isEditingAllowed = !!currentProjectRole && currentProjectRole >= EUserProjectRoles.MEMBER; - - const emptyStateProps: EmptyStateProps = - issueFilterCount > 0 - ? { - title: EMPTY_FILTER_STATE_DETAILS["project"].title, - image: currentLayoutEmptyStateImagePath, - secondaryButton: { - text: EMPTY_FILTER_STATE_DETAILS["project"].secondaryButton.text, - onClick: handleClearAllFilters, - }, - } - : { - title: EMPTY_ISSUE_STATE_DETAILS["project"].title, - description: EMPTY_ISSUE_STATE_DETAILS["project"].description, - image: EmptyStateImagePath, - comicBox: { - title: EMPTY_ISSUE_STATE_DETAILS["project"].comicBox.title, - description: EMPTY_ISSUE_STATE_DETAILS["project"].comicBox.description, - }, - primaryButton: { - text: EMPTY_ISSUE_STATE_DETAILS["project"].primaryButton.text, - onClick: () => { - setTrackElement("Project issue empty state"); - commandPaletteStore.toggleCreateIssueModal(true, EIssuesStoreType.PROJECT); - }, - }, - size: "lg", - disabled: !isEditingAllowed, - }; + const emptyStateType = issueFilterCount > 0 ? EmptyStateType.PROJECT_EMPTY_FILTER : EmptyStateType.PROJECT_NO_ISSUES; + const additionalPath = issueFilterCount > 0 ? activeLayout ?? "list" : undefined; + const emptyStateSize = issueFilterCount > 0 ? "lg" : "sm"; return (
- + 0 + ? undefined + : () => { + setTrackElement("Project issue empty state"); + commandPaletteStore.toggleCreateIssueModal(true, EIssuesStoreType.PROJECT); + } + } + secondaryButtonOnClick={issueFilterCount > 0 ? handleClearAllFilters : undefined} + />
); }); 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 84101542f..1367eccc4 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 @@ -2,32 +2,28 @@ import React, { Fragment, useCallback, useMemo } from "react"; import isEmpty from "lodash/isEmpty"; import { observer } from "mobx-react-lite"; import { useRouter } from "next/router"; -import { useTheme } from "next-themes"; import useSWR from "swr"; // hooks -import { EmptyState, getEmptyStateImagePath } from "components/empty-state"; +import { useWorkspaceIssueProperties } from "hooks/use-workspace-issue-properties"; +import { useApplication, useEventTracker, useGlobalView, useIssues, useProject, useUser } from "hooks/store"; +// 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 } from "components/empty-state"; import { SpreadsheetLayoutLoader } from "components/ui"; -import { ALL_ISSUES_EMPTY_STATE_DETAILS } from "constants/empty-state"; -import { EIssueFilterType, EIssuesStoreType, ISSUE_DISPLAY_FILTERS_BY_LAYOUT } from "constants/issue"; -import { EUserProjectRoles } from "constants/project"; -import { EUserWorkspaceRoles } from "constants/workspace"; -import { useApplication, useEventTracker, useGlobalView, useIssues, useProject, useUser } from "hooks/store"; -import { useWorkspaceIssueProperties } from "hooks/use-workspace-issue-properties"; -// components // types import { TIssue, IIssueDisplayFilterOptions } from "@plane/types"; import { EIssueActions } from "../types"; // constants +import { EUserProjectRoles } from "constants/project"; +import { EIssueFilterType, EIssuesStoreType, ISSUE_DISPLAY_FILTERS_BY_LAYOUT } from "constants/issue"; +import { EMPTY_STATE_DETAILS, EmptyStateType } from "constants/empty-state"; export const AllIssueLayoutRoot: React.FC = observer(() => { // router const router = useRouter(); const { workspaceSlug, globalViewId, ...routeFilters } = router.query; - // theme - const { resolvedTheme } = useTheme(); //swr hook for fetching issue properties useWorkspaceIssueProperties(workspaceSlug); // store @@ -39,8 +35,7 @@ export const AllIssueLayoutRoot: React.FC = observer(() => { const { dataViewId, issueIds } = groupedIssueIds; const { - membership: { currentWorkspaceAllProjectsRole, currentWorkspaceRole }, - currentUser, + membership: { currentWorkspaceAllProjectsRole }, } = useUser(); const { fetchAllGlobalViews } = useGlobalView(); const { workspaceProjectIds } = useProject(); @@ -48,10 +43,6 @@ export const AllIssueLayoutRoot: React.FC = observer(() => { 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 isLightMode = resolvedTheme ? resolvedTheme === "light" : currentUser?.theme.theme === "light"; - const emptyStateImage = getEmptyStateImagePath("all-issues", currentView, isLightMode); // filter init from the query params @@ -185,46 +176,34 @@ export const AllIssueLayoutRoot: React.FC = observer(() => { [canEditProperties, handleIssues] ); - const isEditingAllowed = !!currentWorkspaceRole && currentWorkspaceRole >= EUserWorkspaceRoles.MEMBER; - if (loader === "init-loader" || !globalViewId || globalViewId !== dataViewId || !issueIds) { return ; } + const emptyStateType = + (workspaceProjectIds ?? []).length > 0 ? `workspace-${currentView}` : EmptyStateType.WORKSPACE_NO_PROJECTS; + return (
{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." - } + type={emptyStateType as keyof typeof EMPTY_STATE_DETAILS} size="sm" - primaryButton={ + primaryButtonOnClick={ (workspaceProjectIds ?? []).length > 0 ? currentView !== "custom-view" && currentView !== "subscribed" - ? { - text: "Create new issue", - onClick: () => { - setTrackElement("All issues empty state"); - commandPaletteStore.toggleCreateIssueModal(true, EIssuesStoreType.PROJECT); - }, + ? () => { + setTrackElement("All issues empty state"); + commandPaletteStore.toggleCreateIssueModal(true, EIssuesStoreType.PROJECT); } : undefined - : { - text: "Start your first project", - onClick: () => { - setTrackElement("All issues empty state"); - commandPaletteStore.toggleCreateProjectModal(true); - }, + : () => { + setTrackElement("All issues empty state"); + commandPaletteStore.toggleCreateProjectModal(true); } } - disabled={!isEditingAllowed} /> ) : ( diff --git a/web/components/labels/project-setting-label-list.tsx b/web/components/labels/project-setting-label-list.tsx index ba6b43b0b..1e83167ae 100644 --- a/web/components/labels/project-setting-label-list.tsx +++ b/web/components/labels/project-setting-label-list.tsx @@ -1,4 +1,6 @@ import React, { useState, useRef } from "react"; +import { useRouter } from "next/router"; +import { observer } from "mobx-react"; import { DragDropContext, Draggable, @@ -7,26 +9,23 @@ import { DropResult, Droppable, } from "@hello-pangea/dnd"; -import { observer } from "mobx-react-lite"; -import { useRouter } from "next/router"; -import { useTheme } from "next-themes"; // hooks -import { Button, Loader } from "@plane/ui"; -import { EmptyState, getEmptyStateImagePath } from "components/empty-state"; +import { useLabel } from "hooks/store"; +import useDraggableInPortal from "hooks/use-draggable-portal"; +// components import { CreateUpdateLabelInline, DeleteLabelModal, ProjectSettingLabelGroup, ProjectSettingLabelItem, } from "components/labels"; -import { PROJECT_SETTINGS_EMPTY_STATE_DETAILS } from "constants/empty-state"; -import { useLabel, useUser } from "hooks/store"; -import useDraggableInPortal from "hooks/use-draggable-portal"; -// components +import { EmptyState } from "components/empty-state"; // ui +import { Button, Loader } from "@plane/ui"; // types import { IIssueLabel } from "@plane/types"; // constants +import { EmptyStateType } from "constants/empty-state"; const LABELS_ROOT = "labels.root"; @@ -41,10 +40,7 @@ export const ProjectSettingsLabelList: React.FC = observer(() => { // router const router = useRouter(); const { workspaceSlug, projectId } = router.query; - // theme - const { resolvedTheme } = useTheme(); // store hooks - const { currentUser } = useUser(); const { projectLabels, updateLabelPosition, projectLabelsTree } = useLabel(); // portal const renderDraggable = useDraggableInPortal(); @@ -54,10 +50,6 @@ export const ProjectSettingsLabelList: React.FC = observer(() => { setLabelForm(true); }; - const emptyStateDetail = PROJECT_SETTINGS_EMPTY_STATE_DETAILS["labels"]; - const isLightMode = resolvedTheme ? resolvedTheme === "light" : currentUser?.theme.theme === "light"; - const emptyStateImage = getEmptyStateImagePath("project-settings", "labels", isLightMode); - const onDragEnd = (result: DropResult) => { const { combine, draggableId, destination, source } = result; @@ -121,13 +113,8 @@ export const ProjectSettingsLabelList: React.FC = observer(() => { )} {projectLabels ? ( projectLabels.length === 0 && !showLabelForm ? ( -
- +
+
) : ( projectLabelsTree && ( diff --git a/web/components/modules/modules-list-view.tsx b/web/components/modules/modules-list-view.tsx index 33c11cbd8..78b4a6571 100644 --- a/web/components/modules/modules-list-view.tsx +++ b/web/components/modules/modules-list-view.tsx @@ -1,40 +1,28 @@ import { observer } from "mobx-react-lite"; import { useRouter } from "next/router"; -import { useTheme } from "next-themes"; // hooks +import { useApplication, useEventTracker, useModule } from "hooks/store"; +import useLocalStorage from "hooks/use-local-storage"; // components -import { EmptyState, getEmptyStateImagePath } from "components/empty-state"; import { ModuleCardItem, ModuleListItem, ModulePeekOverview, ModulesListGanttChartView } from "components/modules"; +import { EmptyState } from "components/empty-state"; // ui import { CycleModuleBoardLayout, CycleModuleListLayout, GanttLayoutLoader } from "components/ui"; // constants -import { MODULE_EMPTY_STATE_DETAILS } from "constants/empty-state"; -import { EUserProjectRoles } from "constants/project"; -import { useApplication, useEventTracker, useModule, useUser } from "hooks/store"; -import useLocalStorage from "hooks/use-local-storage"; +import { EmptyStateType } from "constants/empty-state"; export const ModulesListView: React.FC = observer(() => { // router const router = useRouter(); const { workspaceSlug, projectId, peekModule } = router.query; - // theme - const { resolvedTheme } = useTheme(); // store hooks const { commandPalette: commandPaletteStore } = useApplication(); const { setTrackElement } = useEventTracker(); - const { - membership: { currentProjectRole }, - currentUser, - } = useUser(); + const { projectModuleIds, loader } = useModule(); const { storedValue: modulesView } = useLocalStorage("modules_view", "grid"); - const isLightMode = resolvedTheme ? resolvedTheme === "light" : currentUser?.theme.theme === "light"; - const EmptyStateImagePath = getEmptyStateImagePath("onboarding", "modules", isLightMode); - - const isEditingAllowed = !!currentProjectRole && currentProjectRole >= EUserProjectRoles.MEMBER; - if (loader || !projectModuleIds) return ( <> @@ -88,22 +76,11 @@ export const ModulesListView: React.FC = observer(() => { ) : ( { + setTrackElement("Module empty state"); + commandPaletteStore.toggleCreateModuleModal(true); }} - primaryButton={{ - text: MODULE_EMPTY_STATE_DETAILS["modules"].primaryButton.text, - onClick: () => { - setTrackElement("Module empty state"); - 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 2f8392bc2..f910625ca 100644 --- a/web/components/page-views/workspace-dashboard.tsx +++ b/web/components/page-views/workspace-dashboard.tsx @@ -1,41 +1,30 @@ import { useEffect } from "react"; import { observer } from "mobx-react-lite"; -import { useTheme } from "next-themes"; // hooks +import { useApplication, useEventTracker, useDashboard, useProject, useUser } from "hooks/store"; // components -import { Spinner } from "@plane/ui"; import { DashboardWidgets } from "components/dashboard"; -import { EmptyState, getEmptyStateImagePath } from "components/empty-state"; +import { EmptyState } from "components/empty-state"; import { IssuePeekOverview } from "components/issues"; import { TourRoot } from "components/onboarding"; import { UserGreetingsView } from "components/user"; // ui +import { Spinner } from "@plane/ui"; // constants -import { WORKSPACE_EMPTY_STATE_DETAILS } from "constants/empty-state"; import { PRODUCT_TOUR_COMPLETED } from "constants/event-tracker"; -import { EUserWorkspaceRoles } from "constants/workspace"; -import { useApplication, useEventTracker, useDashboard, useProject, useUser } from "hooks/store"; +import { EmptyStateType } from "constants/empty-state"; export const WorkspaceDashboardView = observer(() => { - // theme - const { resolvedTheme } = useTheme(); // store hooks const { captureEvent, setTrackElement } = useEventTracker(); const { commandPalette: { toggleCreateProjectModal }, router: { workspaceSlug }, } = useApplication(); - const { - currentUser, - updateTourCompleted, - membership: { currentWorkspaceRole }, - } = useUser(); + const { currentUser, updateTourCompleted } = useUser(); const { homeDashboardId, fetchHomeDashboardWidgets } = useDashboard(); const { joinedProjectIds } = useProject(); - const isLightMode = resolvedTheme ? resolvedTheme === "light" : currentUser?.theme.theme === "light"; - const emptyStateImage = getEmptyStateImagePath("onboarding", "dashboard", isLightMode); - const handleTourCompleted = () => { updateTourCompleted() .then(() => { @@ -56,8 +45,6 @@ export const WorkspaceDashboardView = observer(() => { fetchHomeDashboardWidgets(workspaceSlug); }, [fetchHomeDashboardWidgets, workspaceSlug]); - const isEditingAllowed = currentWorkspaceRole === EUserWorkspaceRoles.ADMIN; - return ( <> {currentUser && !currentUser.is_tour_completed && ( @@ -78,22 +65,11 @@ export const WorkspaceDashboardView = observer(() => { ) : ( { - setTrackElement("Dashboard empty state"); - toggleCreateProjectModal(true); - }, + type={EmptyStateType.WORKSPACE_DASHBOARD} + primaryButtonOnClick={() => { + setTrackElement("Dashboard empty state"); + toggleCreateProjectModal(true); }} - comicBox={{ - title: WORKSPACE_EMPTY_STATE_DETAILS["dashboard"].comicBox.title, - description: WORKSPACE_EMPTY_STATE_DETAILS["dashboard"].comicBox.description, - }} - 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 0d468ef3c..8c1a09e73 100644 --- a/web/components/pages/pages-list/list-view.tsx +++ b/web/components/pages/pages-list/list-view.tsx @@ -1,17 +1,15 @@ import { FC } from "react"; import { useRouter } from "next/router"; -import { useTheme } from "next-themes"; // hooks -import { Loader } from "@plane/ui"; -import { EmptyState, getEmptyStateImagePath } from "components/empty-state"; -import { PAGE_EMPTY_STATE_DETAILS } from "constants/empty-state"; -import { EUserProjectRoles } from "constants/project"; -import { useApplication, useUser } from "hooks/store"; +import { useApplication } from "hooks/store"; import useLocalStorage from "hooks/use-local-storage"; // components +import { EmptyState } from "components/empty-state"; import { PagesListItem } from "./list-item"; // ui +import { Loader } from "@plane/ui"; // constants +import { EMPTY_STATE_DETAILS, EmptyStateType } from "constants/empty-state"; type IPagesListView = { pageIds: string[]; @@ -19,34 +17,20 @@ type IPagesListView = { export const PagesListView: FC = (props) => { const { pageIds: projectPageIds } = props; - // theme - const { resolvedTheme } = useTheme(); // store hooks 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 isLightMode = resolvedTheme ? resolvedTheme === "light" : currentUser?.theme.theme === "light"; - const emptyStateImage = getEmptyStateImagePath("pages", currentPageTabDetails.key, isLightMode); - - 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; + const emptyStateType = pageTab ? `project-page-${pageTab}` : EmptyStateType.PROJECT_PAGE_ALL; + const isButtonVisible = pageTab !== "archived" && pageTab !== "favorites"; return ( <> @@ -60,18 +44,8 @@ export const PagesListView: FC = (props) => { ) : ( toggleCreatePageModal(true), - } - : undefined - } - disabled={!isEditingAllowed} + type={emptyStateType as keyof typeof EMPTY_STATE_DETAILS} + primaryButtonOnClick={isButtonVisible ? () => toggleCreatePageModal(true) : undefined} /> )}
diff --git a/web/components/pages/pages-list/recent-pages-list.tsx b/web/components/pages/pages-list/recent-pages-list.tsx index 28a430031..45de8db0d 100644 --- a/web/components/pages/pages-list/recent-pages-list.tsx +++ b/web/components/pages/pages-list/recent-pages-list.tsx @@ -1,39 +1,27 @@ import React, { FC } from "react"; import { observer } from "mobx-react-lite"; -import { useTheme } from "next-themes"; // hooks -import { Loader } from "@plane/ui"; -import { EmptyState, getEmptyStateImagePath } from "components/empty-state"; -import { PagesListView } from "components/pages/pages-list"; -import { PAGE_EMPTY_STATE_DETAILS } from "constants/empty-state"; -import { EUserProjectRoles } from "constants/project"; -import { replaceUnderscoreIfSnakeCase } from "helpers/string.helper"; -import { useApplication, useUser } from "hooks/store"; +import { useApplication } from "hooks/store"; import { useProjectPages } from "hooks/store/use-project-specific-pages"; // components +import { PagesListView } from "components/pages/pages-list"; +import { EmptyState } from "components/empty-state"; // ui +import { Loader } from "@plane/ui"; // helpers +import { replaceUnderscoreIfSnakeCase } from "helpers/string.helper"; // constants +import { EmptyStateType } from "constants/empty-state"; export const RecentPagesList: FC = observer(() => { - // theme - const { resolvedTheme } = useTheme(); // store hooks const { commandPalette: commandPaletteStore } = useApplication(); - const { - membership: { currentProjectRole }, - currentUser, - } = useUser(); - const { recentProjectPages } = useProjectPages(); - const isLightMode = resolvedTheme ? resolvedTheme === "light" : currentUser?.theme.theme === "light"; - const EmptyStateImagePath = getEmptyStateImagePath("pages", "recent", isLightMode); + const { recentProjectPages } = useProjectPages(); // FIXME: replace any with proper type const isEmpty = recentProjectPages && Object.values(recentProjectPages).every((value: any) => value.length === 0); - const isEditingAllowed = !!currentProjectRole && currentProjectRole >= EUserProjectRoles.MEMBER; - if (!recentProjectPages) { return ( @@ -64,15 +52,9 @@ export const RecentPagesList: FC = observer(() => { ) : ( <> commandPaletteStore.toggleCreatePageModal(true), - }} + type={EmptyStateType.PROJECT_PAGE_RECENT} + primaryButtonOnClick={() => commandPaletteStore.toggleCreatePageModal(true)} size="sm" - disabled={!isEditingAllowed} /> )} diff --git a/web/components/profile/profile-issues.tsx b/web/components/profile/profile-issues.tsx index b6a99baf9..f94c1d91f 100644 --- a/web/components/profile/profile-issues.tsx +++ b/web/components/profile/profile-issues.tsx @@ -1,20 +1,18 @@ import React, { useEffect } from "react"; import { observer } from "mobx-react-lite"; import { useRouter } from "next/router"; -import { useTheme } from "next-themes"; import useSWR from "swr"; // components -import { EmptyState, getEmptyStateImagePath } from "components/empty-state"; import { IssuePeekOverview, ProfileIssuesAppliedFiltersRoot } from "components/issues"; import { ProfileIssuesKanBanLayout } from "components/issues/issue-layouts/kanban/roots/profile-issues-root"; import { ProfileIssuesListLayout } from "components/issues/issue-layouts/list/roots/profile-issues-root"; import { KanbanLayoutLoader, ListLayoutLoader } from "components/ui"; +import { EmptyState } from "components/empty-state"; // hooks -import { PROFILE_EMPTY_STATE_DETAILS } from "constants/empty-state"; -import { EIssuesStoreType } from "constants/issue"; -import { EUserWorkspaceRoles } from "constants/workspace"; -import { useIssues, useUser } from "hooks/store"; +import { useIssues } from "hooks/store"; // constants +import { EIssuesStoreType } from "constants/issue"; +import { EMPTY_STATE_DETAILS } from "constants/empty-state"; interface IProfileIssuesPage { type: "assigned" | "subscribed" | "created"; @@ -28,13 +26,7 @@ export const ProfileIssuesPage = observer((props: IProfileIssuesPage) => { workspaceSlug: string; userId: string; }; - // theme - const { resolvedTheme } = useTheme(); // store hooks - const { - membership: { currentWorkspaceRole }, - currentUser, - } = useUser(); const { issues: { loader, groupedIssueIds, fetchIssues, setViewId }, issuesFilter: { issueFilters, fetchFilters }, @@ -55,26 +47,15 @@ export const ProfileIssuesPage = observer((props: IProfileIssuesPage) => { { revalidateIfStale: false, revalidateOnFocus: false } ); - const isLightMode = resolvedTheme ? resolvedTheme === "light" : currentUser?.theme.theme === "light"; - const emptyStateImage = getEmptyStateImagePath("profile", type, isLightMode); - const activeLayout = issueFilters?.displayFilters?.layout || undefined; - const isEditingAllowed = !!currentWorkspaceRole && currentWorkspaceRole >= EUserWorkspaceRoles.MEMBER; + const emptyStateType = `profile-${type}`; if (!groupedIssueIds || loader === "init-loader") return <>{activeLayout === "list" ? : }; if (groupedIssueIds.length === 0) { - return ( - - ); + return ; } return ( diff --git a/web/components/project/card-list.tsx b/web/components/project/card-list.tsx index a19b53fbb..df63dfb73 100644 --- a/web/components/project/card-list.tsx +++ b/web/components/project/card-list.tsx @@ -1,32 +1,20 @@ import { observer } from "mobx-react-lite"; -import { useTheme } from "next-themes"; // hooks +import { useApplication, useEventTracker, useProject } from "hooks/store"; // components -import { EmptyState, getEmptyStateImagePath } from "components/empty-state"; +import { EmptyState } from "components/empty-state"; import { ProjectCard } from "components/project"; import { ProjectsLoader } from "components/ui"; // constants -import { WORKSPACE_EMPTY_STATE_DETAILS } from "constants/empty-state"; -import { EUserWorkspaceRoles } from "constants/workspace"; -import { useApplication, useEventTracker, useProject, useUser } from "hooks/store"; +import { EmptyStateType } from "constants/empty-state"; export const ProjectCardList = observer(() => { - // theme - const { resolvedTheme } = useTheme(); // store hooks const { commandPalette: commandPaletteStore } = useApplication(); const { setTrackElement } = useEventTracker(); - const { - membership: { currentWorkspaceRole }, - currentUser, - } = useUser(); + const { workspaceProjectIds, searchedProjects, getProjectById } = useProject(); - const isLightMode = resolvedTheme ? resolvedTheme === "light" : currentUser?.theme.theme === "light"; - const emptyStateImage = getEmptyStateImagePath("onboarding", "projects", isLightMode); - - const isEditingAllowed = !!currentWorkspaceRole && currentWorkspaceRole >= EUserWorkspaceRoles.MEMBER; - if (!workspaceProjectIds) return ; return ( @@ -49,22 +37,11 @@ export const ProjectCardList = observer(() => {
) : ( { - setTrackElement("Project empty state"); - commandPaletteStore.toggleCreateProjectModal(true); - }, + type={EmptyStateType.WORKSPACE_PROJECTS} + primaryButtonOnClick={() => { + setTrackElement("Project empty state"); + commandPaletteStore.toggleCreateProjectModal(true); }} - comicBox={{ - title: WORKSPACE_EMPTY_STATE_DETAILS["projects"].comicBox.title, - description: WORKSPACE_EMPTY_STATE_DETAILS["projects"].comicBox.description, - }} - size="lg" - disabled={!isEditingAllowed} /> )} diff --git a/web/components/views/views-list.tsx b/web/components/views/views-list.tsx index 9d8bf85e6..ba4bef2b8 100644 --- a/web/components/views/views-list.tsx +++ b/web/components/views/views-list.tsx @@ -1,45 +1,32 @@ import { useState } from "react"; import { observer } from "mobx-react-lite"; -import { useTheme } from "next-themes"; import { Search } from "lucide-react"; // hooks +import { useApplication, useProjectView } from "hooks/store"; // components -import { Input } from "@plane/ui"; -import { EmptyState, getEmptyStateImagePath } from "components/empty-state"; -// ui +import { EmptyState } from "components/empty-state"; import { ViewListLoader } from "components/ui"; import { ProjectViewListItem } from "components/views"; +// ui +import { Input } from "@plane/ui"; // constants -import { VIEW_EMPTY_STATE_DETAILS } from "constants/empty-state"; -import { EUserProjectRoles } from "constants/project"; -import { useApplication, useProjectView, useUser } from "hooks/store"; +import { EmptyStateType } from "constants/empty-state"; export const ProjectViewsList = observer(() => { // states const [query, setQuery] = useState(""); - // theme - const { resolvedTheme } = useTheme(); // store hooks const { commandPalette: { toggleCreateViewModal }, } = useApplication(); - const { - membership: { currentProjectRole }, - currentUser, - } = useUser(); const { projectViewIds, getViewById, loader } = useProjectView(); if (loader || !projectViewIds) return ; const viewsList = projectViewIds.map((viewId) => getViewById(viewId)); - const isLightMode = resolvedTheme ? resolvedTheme === "light" : currentUser?.theme.theme === "light"; - const EmptyStateImagePath = getEmptyStateImagePath("onboarding", "views", isLightMode); - const filteredViewsList = viewsList.filter((v) => v?.name.toLowerCase().includes(query.toLowerCase())); - const isEditingAllowed = !!currentProjectRole && currentProjectRole >= EUserProjectRoles.MEMBER; - return ( <> {viewsList.length > 0 ? ( @@ -65,21 +52,7 @@ export const ProjectViewsList = observer(() => {
) : ( - toggleCreateViewModal(true), - }} - size="lg" - disabled={!isEditingAllowed} - /> + toggleCreateViewModal(true)} /> )} ); diff --git a/web/constants/empty-state.ts b/web/constants/empty-state.ts index a1b2b06f3..38f334b20 100644 --- a/web/constants/empty-state.ts +++ b/web/constants/empty-state.ts @@ -1,366 +1,516 @@ -// workspace empty state -export const WORKSPACE_EMPTY_STATE_DETAILS = { - dashboard: { +import { EUserProjectRoles } from "./project"; +import { EUserWorkspaceRoles } from "./workspace"; + +export interface EmptyStateDetails { + key: string; + title?: string; + description?: string; + path?: string; + primaryButton?: { + icon?: any; + text: string; + comicBox?: { + title?: string; + description?: string; + }; + }; + secondaryButton?: { + icon?: any; + text: string; + comicBox?: { + title?: string; + description?: string; + }; + }; + accessType?: "workspace" | "project"; + access?: EUserWorkspaceRoles | EUserProjectRoles; +} + +export type EmptyStateKeys = keyof typeof emptyStateDetails; + +export enum EmptyStateType { + WORKSPACE_DASHBOARD = "workspace-dashboard", + WORKSPACE_ANALYTICS = "workspace-analytics", + WORKSPACE_PROJECTS = "workspace-projects", + WORKSPACE_ALL_ISSUES = "workspace-all-issues", + WORKSPACE_ASSIGNED = "workspace-assigned", + WORKSPACE_CREATED = "workspace-created", + WORKSPACE_SUBSCRIBED = "workspace-subscribed", + WORKSPACE_CUSTOM_VIEW = "workspace-custom-view", + WORKSPACE_NO_PROJECTS = "workspace-no-projects", + WORKSPACE_SETTINGS_API_TOKENS = "workspace-settings-api-tokens", + WORKSPACE_SETTINGS_WEBHOOKS = "workspace-settings-webhooks", + WORKSPACE_SETTINGS_EXPORT = "workspace-settings-export", + WORKSPACE_SETTINGS_IMPORT = "workspace-settings-import", + PROFILE_ASSIGNED = "profile-assigned", + PROFILE_CREATED = "profile-created", + PROFILE_SUBSCRIBED = "profile-subscribed", + PROJECT_SETTINGS_LABELS = "project-settings-labels", + PROJECT_SETTINGS_INTEGRATIONS = "project-settings-integrations", + PROJECT_SETTINGS_ESTIMATE = "project-settings-estimate", + PROJECT_CYCLES = "project-cycles", + PROJECT_CYCLE_NO_ISSUES = "project-cycle-no-issues", + PROJECT_CYCLE_ACTIVE = "project-cycle-active", + PROJECT_CYCLE_UPCOMING = "project-cycle-upcoming", + PROJECT_CYCLE_COMPLETED = "project-cycle-completed", + PROJECT_CYCLE_DRAFT = "project-cycle-draft", + PROJECT_EMPTY_FILTER = "project-empty-filter", + PROJECT_ARCHIVED_EMPTY_FILTER = "project-archived-empty-filter", + PROJECT_DRAFT_EMPTY_FILTER = "project-draft-empty-filter", + PROJECT_NO_ISSUES = "project-no-issues", + PROJECT_ARCHIVED_NO_ISSUES = "project-archived-no-issues", + PROJECT_DRAFT_NO_ISSUES = "project-draft-no-issues", + VIEWS_EMPTY_SEARCH = "views-empty-search", + PROJECTS_EMPTY_SEARCH = "projects-empty-search", + COMMANDK_EMPTY_SEARCH = "commandK-empty-search", + MEMBERS_EMPTY_SEARCH = "members-empty-search", + PROJECT_MODULE_ISSUES = "project-module-issues", + PROJECT_MODULE = "project-module", + PROJECT_VIEW = "project-view", + PROJECT_PAGE = "project-page", + PROJECT_PAGE_ALL = "project-page-all", + PROJECT_PAGE_FAVORITE = "project-page-favorite", + PROJECT_PAGE_PRIVATE = "project-page-private", + PROJECT_PAGE_SHARED = "project-page-shared", + PROJECT_PAGE_ARCHIVED = "project-page-archived", + PROJECT_PAGE_RECENT = "project-page-recent", +} + +const emptyStateDetails = { + // workspace + "workspace-dashboard": { + key: "workspace-dashboard", title: "Overview of your projects, activity, and metrics", description: " Welcome to Plane, we are excited to have you here. Create your first project and track your issues, and this page will transform into a space that helps you progress. Admins will also see items which help their team progress.", + path: "/empty-state/onboarding/dashboard", + // path: "/empty-state/onboarding/", primaryButton: { text: "Build your first project", + 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.", + }, }, - 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.", - }, + + accessType: "workspace", + access: EUserWorkspaceRoles.MEMBER, }, - analytics: { + "workspace-analytics": { + key: "workspace-analytics", title: "Track progress, workloads, and allocations. Spot trends, remove blockers, and move work faster", description: "See scope versus demand, estimates, and scope creep. Get performance by team members and teams, and make sure your project runs on time.", + path: "/empty-state/onboarding/analytics", primaryButton: { text: "Create Cycles and Modules first", + 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.", + }, }, - 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.", - }, + accessType: "workspace", + access: EUserWorkspaceRoles.MEMBER, }, - projects: { + "workspace-projects": { + key: "workspace-projects", title: "Start a Project", description: "Think of each project as the parent for goal-oriented work. Projects are where Jobs, Cycles, and Modules live and, along with your colleagues, help you achieve that goal.", + path: "/empty-state/onboarding/projects", primaryButton: { text: "Start your first project", + 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.", + }, }, - 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.", - }, + accessType: "workspace", + access: EUserWorkspaceRoles.MEMBER, }, - "assigned-notification": { - key: "assigned-notification", - title: "No issues assigned", - description: "Updates for issues assigned to you can be seen here", - }, - "created-notification": { - key: "created-notification", - title: "No updates to issues", - description: "Updates to issues created by you can be seen here", - }, - "subscribed-notification": { - key: "subscribed-notification", - title: "No updates to issues", - description: "Updates to any issue you are subscribed to can be seen here", - }, -}; - -export const ALL_ISSUES_EMPTY_STATE_DETAILS = { - "all-issues": { - key: "all-issues", + // all-issues + "workspace-all-issues": { + key: "workspace-all-issues", title: "No issues in the project", description: "First project done! Now, slice your work into trackable pieces with issues. Let's go!", + path: "/empty-state/all-issues/all-issues", + primaryButton: { + text: "Create new issue", + }, + accessType: "workspace", + access: EUserWorkspaceRoles.MEMBER, }, - assigned: { - key: "assigned", + "workspace-assigned": { + key: "workspace-assigned", title: "No issues yet", description: "Issues assigned to you can be tracked from here.", + path: "/empty-state/all-issues/assigned", + primaryButton: { + text: "Create new issue", + }, + accessType: "workspace", + access: EUserWorkspaceRoles.MEMBER, }, - created: { - key: "created", + "workspace-created": { + key: "workspace-created", title: "No issues yet", description: "All issues created by you come here, track them here directly.", + path: "/empty-state/all-issues/created", + primaryButton: { + text: "Create new issue", + }, + accessType: "workspace", + access: EUserWorkspaceRoles.MEMBER, }, - subscribed: { - key: "subscribed", + "workspace-subscribed": { + key: "workspace-subscribed", title: "No issues yet", description: "Subscribe to issues you are interested in, track all of them here.", + path: "/empty-state/all-issues/subscribed", }, - "custom-view": { - key: "custom-view", + "workspace-custom-view": { + key: "workspace-custom-view", title: "No issues yet", description: "Issues that applies to the filters, track all of them here.", + path: "/empty-state/all-issues/custom-view", }, -}; - -export const SEARCH_EMPTY_STATE_DETAILS = { - views: { - key: "views", - title: "No matching views", - description: "No views match the search criteria. Create a new view instead.", + "workspace-no-projects": { + key: "workspace-no-projects", + title: "No project", + description: "To create issues or manage your work, you need to create a project or be a part of one.", + path: "/empty-state/onboarding/projects", + primaryButton: { + text: "Start your first project", + 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.", + }, + }, + accessType: "workspace", + access: EUserWorkspaceRoles.MEMBER, }, - projects: { - key: "projects", - title: "No matching projects", - description: "No projects detected with the matching criteria. Create a new project instead.", - }, - commandK: { - key: "commandK", - title: "No results found. ", - }, - members: { - key: "members", - title: "No matching members", - description: "Add them to the project if they are already a part of the workspace", - }, -}; - -export const WORKSPACE_SETTINGS_EMPTY_STATE_DETAILS = { - "api-tokens": { - key: "api-tokens", + // workspace settings + "workspace-settings-api-tokens": { + key: "workspace-settings-api-tokens", title: "No API tokens created", description: "Plane APIs can be used to integrate your data in Plane with any external system. Create a token to get started.", + path: "/empty-state/workspace-settings/api-tokens", }, - webhooks: { - key: "webhooks", + "workspace-settings-webhooks": { + key: "workspace-settings-webhooks", title: "No webhooks added", description: "Create webhooks to receive real-time updates and automate actions.", + path: "/empty-state/workspace-settings/webhooks", }, - export: { - key: "export", + "workspace-settings-export": { + key: "workspace-settings-export", title: "No previous exports yet", description: "Anytime you export, you will also have a copy here for reference.", + path: "/empty-state/workspace-settings/exports", }, - import: { - key: "export", + "workspace-settings-import": { + key: "workspace-settings-import", title: "No previous imports yet", description: "Find all your previous imports here and download them.", + path: "/empty-state/workspace-settings/imports", }, -}; - -// profile empty state -export const PROFILE_EMPTY_STATE_DETAILS = { - assigned: { - key: "assigned", + // profile + "profile-assigned": { + key: "profile-assigned", title: "No issues are assigned to you", description: "Issues assigned to you can be tracked from here.", + path: "/empty-state/profile/assigned", }, - subscribed: { - key: "created", + "profile-created": { + key: "profile-created", title: "No issues yet", description: "All issues created by you come here, track them here directly.", + path: "/empty-state/profile/created", }, - created: { - key: "subscribed", + "profile-subscribed": { + key: "profile-subscribed", title: "No issues yet", description: "Subscribe to issues you are interested in, track all of them here.", + path: "/empty-state/profile/subscribed", }, -}; - -// project empty state - -export const PROJECT_SETTINGS_EMPTY_STATE_DETAILS = { - labels: { - key: "labels", + // project settings + "project-settings-labels": { + key: "project-settings-labels", title: "No labels yet", description: "Create labels to help organize and filter issues in you project.", + path: "/empty-state/project-settings/labels", }, - integrations: { - key: "integrations", + "project-settings-integrations": { + key: "project-settings-integrations", title: "No integrations configured", description: "Configure GitHub and other integrations to sync your project issues.", + path: "/empty-state/project-settings/integrations", }, - estimate: { - key: "estimate", + "project-settings-estimate": { + key: "project-settings-estimate", title: "No estimates added", description: "Create a set of estimates to communicate the amount of work per issue.", + path: "/empty-state/project-settings/estimates", }, -}; - -export const CYCLE_EMPTY_STATE_DETAILS = { - cycles: { + // project cycles + "project-cycles": { + key: "project-cycles", title: "Group and timebox your work in Cycles.", description: "Break work down by timeboxed chunks, work backwards from your project deadline to set dates, and make tangible progress as a team.", - comicBox: { - title: "Cycles are repetitive time-boxes.", - description: - "A sprint, an iteration, and or any other term you use for weekly or fortnightly tracking of work is a cycle.", - }, + path: "/empty-state/onboarding/cycles", primaryButton: { text: "Set your first cycle", + comicBox: { + title: "Cycles are repetitive time-boxes.", + description: + "A sprint, an iteration, and or any other term you use for weekly or fortnightly tracking of work is a cycle.", + }, }, + accessType: "workspace", + access: EUserWorkspaceRoles.MEMBER, }, - "no-issues": { - key: "no-issues", + "project-cycle-no-issues": { + key: "project-cycle-no-issues", title: "No issues added to the cycle", description: "Add or create issues you wish to timebox and deliver within this cycle", + path: "/empty-state/cycle-issues/", primaryButton: { text: "Create new issue ", }, secondaryButton: { text: "Add an existing issue", }, + accessType: "project", + access: EUserProjectRoles.MEMBER, }, - active: { - key: "active", + "project-cycle-active": { + key: "project-cycle-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.", + path: "/empty-state/cycle/active", }, - upcoming: { - key: "upcoming", + "project-cycle-upcoming": { + key: "project-cycle-upcoming", title: "No upcoming cycles", description: "Upcoming cycles on deck! Just add dates to cycles in draft, and they'll show up right here.", + path: "/empty-state/cycle/upcoming", }, - completed: { - key: "completed", + "project-cycle-completed": { + key: "project-cycle-completed", title: "No completed cycles", description: "Any cycle with a past due date is considered completed. Explore all completed cycles here.", + path: "/empty-state/cycle/completed", }, - draft: { - key: "draft", + "project-cycle-draft": { + key: "project-cycle-draft", title: "No draft cycles", description: "No dates added in cycles? Find them here as drafts.", + path: "/empty-state/cycle/draft", }, -}; - -export const EMPTY_FILTER_STATE_DETAILS = { - archived: { - key: "archived", + // empty filters + "project-empty-filter": { + key: "project-empty-filter", title: "No issues found matching the filters applied", + path: "/empty-state/empty-filters/", secondaryButton: { text: "Clear all filters", }, + accessType: "project", + access: EUserProjectRoles.MEMBER, }, - draft: { - key: "draft", + "project-archived-empty-filter": { + key: "project-archived-empty-filter", title: "No issues found matching the filters applied", + path: "/empty-state/empty-filters/", secondaryButton: { text: "Clear all filters", }, + accessType: "project", + access: EUserProjectRoles.MEMBER, }, - project: { - key: "project", + "project-draft-empty-filter": { + key: "project-draft-empty-filter", title: "No issues found matching the filters applied", + path: "/empty-state/empty-filters/", secondaryButton: { text: "Clear all filters", }, + accessType: "project", + access: EUserProjectRoles.MEMBER, }, -}; - -export const EMPTY_ISSUE_STATE_DETAILS = { - archived: { - key: "archived", - title: "No archived issues yet", - description: - "Archived issues help you remove issues you completed or canceled from focus. You can set automation to auto archive issues and find them here.", - primaryButton: { - text: "Set automation", - }, - }, - draft: { - key: "draft", - 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.", - }, - project: { - key: "project", + // project issues + "project-no-issues": { + key: "project-no-issues", 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.", - 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.", - }, + path: "/empty-state/onboarding/issues", primaryButton: { text: "Create your first issue", + 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.", + }, }, + accessType: "project", + access: EUserProjectRoles.MEMBER, }, -}; - -export const MODULE_EMPTY_STATE_DETAILS = { - "no-issues": { - key: "no-issues", + "project-archived-no-issues": { + key: "project-archived-no-issues", + 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.", + path: "/empty-state/archived/empty-issues", + primaryButton: { + text: "Set automation", + }, + accessType: "project", + access: EUserProjectRoles.MEMBER, + }, + "project-draft-no-issues": { + key: "project-draft-no-issues", + 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.", + path: "/empty-state/draft/draft-issues-empty", + }, + "views-empty-search": { + key: "views-empty-search", + title: "No matching views", + description: "No views match the search criteria. Create a new view instead.", + path: "/empty-state/search/search", + }, + "projects-empty-search": { + key: "projects-empty-search", + title: "No matching projects", + description: "No projects detected with the matching criteria. Create a new project instead.", + path: "/empty-state/search/project", + }, + "commandK-empty-search": { + key: "commandK-empty-search", + title: "No results found. ", + path: "/empty-state/search/search", + }, + "members-empty-search": { + key: "members-empty-search", + title: "No matching members", + description: "Add them to the project if they are already a part of the workspace", + path: "/empty-state/search/member", + }, + // project module + "project-module-issues": { + key: "project-modules-issues", title: "No issues in the module", description: "Create or add issues which you want to accomplish as part of this module", + path: "/empty-state/module-issues/", primaryButton: { text: "Create new issue ", }, secondaryButton: { text: "Add an existing issue", }, + accessType: "project", + access: EUserProjectRoles.MEMBER, }, - modules: { + "project-module": { + key: "project-module", title: "Map your project milestones to Modules and track aggregated work easily.", description: "A group of issues that belong to a logical, hierarchical parent form a module. Think of them as a way to track work by project milestones. They have their own periods and deadlines as well as analytics to help you see how close or far you are from a milestone.", - - comicBox: { - title: "Modules help group work by hierarchy.", - description: "A cart module, a chassis module, and a warehouse module are all good example of this grouping.", - }, + path: "/empty-state/onboarding/modules", primaryButton: { text: "Build your first module", + comicBox: { + title: "Modules help group work by hierarchy.", + description: "A cart module, a chassis module, and a warehouse module are all good example of this grouping.", + }, }, + accessType: "project", + access: EUserProjectRoles.MEMBER, }, -}; - -export const VIEW_EMPTY_STATE_DETAILS = { - "project-views": { + // project views + "project-view": { + key: "project-view", title: "Save filtered views for your project. Create as many as you need", description: "Views are a set of saved filters that you use frequently or want easy access to. All your colleagues in a project can see everyone’s views and choose whichever suits their needs best.", - comicBox: { - title: "Views work atop Issue properties.", - description: "You can create a view from here with as many properties as filters as you see fit.", - }, + path: "/empty-state/onboarding/views", primaryButton: { text: "Create your first view", + comicBox: { + title: "Views work atop Issue properties.", + description: "You can create a view from here with as many properties as filters as you see fit.", + }, }, + accessType: "project", + access: EUserProjectRoles.MEMBER, }, -}; - -export const PAGE_EMPTY_STATE_DETAILS = { - pages: { + // project pages + "project-page": { key: "pages", title: "Write a note, a doc, or a full knowledge base. Get Galileo, Plane’s AI assistant, to help you get started", description: "Pages are thoughts potting space in Plane. Take down meeting notes, format them easily, embed issues, lay them out using a library of components, and keep them all in your project’s context. To make short work of any doc, invoke Galileo, Plane’s AI, with a shortcut or the click of a button.", + 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.", + }, }, - 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, }, - All: { - key: "all", + "project-page-all": { + key: "project-page-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!", + path: "/empty-state/pages/all", }, - Favorites: { - key: "favorites", + "project-page-favorite": { + key: "project-page-favorite", title: "No favorite pages yet", description: "Favorites for quick access? mark them and find them right here.", + path: "/empty-state/pages/favorites", }, - Private: { - key: "private", + "project-page-private": { + key: "project-page-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.", + path: "/empty-state/pages/private", }, - Shared: { - key: "shared", + "project-page-shared": { + key: "project-page-shared", title: "No shared pages yet", description: "See pages shared with everyone in your project right here.", + path: "/empty-state/pages/shared", }, - Archived: { - key: "archived", + "project-page-archived": { + key: "project-page-archived", title: "No archived pages yet", description: "Archive pages not on your radar. Access them here when needed.", + path: "/empty-state/pages/archived", }, - Recent: { - key: "recent", + "project-page-recent": { + key: "project-page-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", + path: "/empty-state/pages/recent", primaryButton: { text: "Create new page", }, + accessType: "project", + access: EUserProjectRoles.MEMBER, }, -}; +} as const; + +export const EMPTY_STATE_DETAILS: Record = emptyStateDetails; diff --git a/web/pages/[workspaceSlug]/analytics.tsx b/web/pages/[workspaceSlug]/analytics.tsx index 658f3e34c..c7ee67cab 100644 --- a/web/pages/[workspaceSlug]/analytics.tsx +++ b/web/pages/[workspaceSlug]/analytics.tsx @@ -1,44 +1,33 @@ import React, { Fragment, ReactElement } from "react"; import { observer } from "mobx-react-lite"; import { useRouter } from "next/router"; -import { useTheme } from "next-themes"; import { Tab } from "@headlessui/react"; // hooks +import { useApplication, useEventTracker, useProject, useWorkspace } from "hooks/store"; // layouts +import { AppLayout } from "layouts/app-layout"; // components import { CustomAnalytics, ScopeAndDemand } from "components/analytics"; import { PageHead } from "components/core"; -import { EmptyState, getEmptyStateImagePath } from "components/empty-state"; +import { EmptyState } from "components/empty-state"; import { WorkspaceAnalyticsHeader } from "components/headers"; -// constants -import { ANALYTICS_TABS } from "constants/analytics"; -import { WORKSPACE_EMPTY_STATE_DETAILS } from "constants/empty-state"; -import { EUserWorkspaceRoles } from "constants/workspace"; -import { useApplication, useEventTracker, useProject, useUser, useWorkspace } from "hooks/store"; -import { AppLayout } from "layouts/app-layout"; // type import { NextPageWithLayout } from "lib/types"; +// constants +import { ANALYTICS_TABS } from "constants/analytics"; +import { EmptyStateType } from "constants/empty-state"; const AnalyticsPage: NextPageWithLayout = observer(() => { const router = useRouter(); const { analytics_tab } = router.query; - // theme - const { resolvedTheme } = useTheme(); // store hooks const { commandPalette: { toggleCreateProjectModal }, } = useApplication(); const { setTrackElement } = useEventTracker(); - const { - membership: { currentWorkspaceRole }, - currentUser, - } = useUser(); const { workspaceProjectIds } = useProject(); const { currentWorkspace } = useWorkspace(); // derived values - const isLightMode = resolvedTheme ? resolvedTheme === "light" : currentUser?.theme.theme === "light"; - const EmptyStateImagePath = getEmptyStateImagePath("onboarding", "analytics", isLightMode); - const isEditingAllowed = !!currentWorkspaceRole && currentWorkspaceRole >= EUserWorkspaceRoles.MEMBER; const pageTitle = currentWorkspace?.name ? `${currentWorkspace?.name} - Analytics` : undefined; return ( @@ -79,22 +68,11 @@ const AnalyticsPage: NextPageWithLayout = observer(() => { ) : ( { - setTrackElement("Analytics empty state"); - toggleCreateProjectModal(true); - }, + type={EmptyStateType.WORKSPACE_ANALYTICS} + primaryButtonOnClick={() => { + setTrackElement("Analytics empty state"); + toggleCreateProjectModal(true); }} - comicBox={{ - title: WORKSPACE_EMPTY_STATE_DETAILS["analytics"].comicBox.title, - description: WORKSPACE_EMPTY_STATE_DETAILS["analytics"].comicBox.description, - }} - 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 ac2b760ef..a22e252f2 100644 --- a/web/pages/[workspaceSlug]/projects/[projectId]/cycles/index.tsx +++ b/web/pages/[workspaceSlug]/projects/[projectId]/cycles/index.tsx @@ -1,39 +1,31 @@ import { Fragment, useCallback, useState, ReactElement } from "react"; import { observer } from "mobx-react-lite"; import { useRouter } from "next/router"; -import { useTheme } from "next-themes"; import { Tab } from "@headlessui/react"; // hooks -import { Tooltip } from "@plane/ui"; -import { PageHead } from "components/core"; -import { CyclesView, ActiveCycleDetails, CycleCreateUpdateModal } from "components/cycles"; -import { EmptyState, getEmptyStateImagePath } from "components/empty-state"; -import { CyclesHeader } from "components/headers"; -import { CycleModuleBoardLayout, CycleModuleListLayout, GanttLayoutLoader } from "components/ui"; -import { CYCLE_TAB_LIST, CYCLE_VIEW_LAYOUTS } from "constants/cycle"; -import { CYCLE_EMPTY_STATE_DETAILS } from "constants/empty-state"; -import { EUserWorkspaceRoles } from "constants/workspace"; -import { useEventTracker, useCycle, useUser, useProject } from "hooks/store"; +import { useEventTracker, useCycle, useProject } from "hooks/store"; import useLocalStorage from "hooks/use-local-storage"; // layouts import { AppLayout } from "layouts/app-layout"; // components +import { PageHead } from "components/core"; +import { CyclesHeader } from "components/headers"; +import { CyclesView, ActiveCycleDetails, CycleCreateUpdateModal } from "components/cycles"; +import { EmptyState } from "components/empty-state"; +import { CycleModuleBoardLayout, CycleModuleListLayout, GanttLayoutLoader } from "components/ui"; // ui +import { Tooltip } from "@plane/ui"; // types import { NextPageWithLayout } from "lib/types"; import { TCycleView, TCycleLayout } from "@plane/types"; // constants +import { CYCLE_TAB_LIST, CYCLE_VIEW_LAYOUTS } from "constants/cycle"; +import { EmptyStateType } from "constants/empty-state"; const ProjectCyclesPage: NextPageWithLayout = observer(() => { const [createModal, setCreateModal] = useState(false); - // theme - const { resolvedTheme } = useTheme(); // store hooks const { setTrackElement } = useEventTracker(); - const { - membership: { currentProjectRole }, - currentUser, - } = useUser(); const { currentProjectCycleIds, loader } = useCycle(); const { getProjectById } = useProject(); // router @@ -43,10 +35,7 @@ const ProjectCyclesPage: NextPageWithLayout = observer(() => { const { storedValue: cycleTab, setValue: setCycleTab } = useLocalStorage("cycle_tab", "active"); const { storedValue: cycleLayout, setValue: setCycleLayout } = useLocalStorage("cycle_layout", "list"); // derived values - const isLightMode = resolvedTheme ? resolvedTheme === "light" : currentUser?.theme.theme === "light"; - const EmptyStateImagePath = getEmptyStateImagePath("onboarding", "cycles", isLightMode); const totalCycles = currentProjectCycleIds?.length ?? 0; - const isEditingAllowed = !!currentProjectRole && currentProjectRole >= EUserWorkspaceRoles.MEMBER; const project = projectId ? getProjectById(projectId?.toString()) : undefined; const pageTitle = project?.name ? `${project?.name} - Cycles` : undefined; @@ -89,22 +78,11 @@ const ProjectCyclesPage: NextPageWithLayout = observer(() => { {totalCycles === 0 ? (
{ + setTrackElement("Cycle empty state"); + setCreateModal(true); }} - primaryButton={{ - text: CYCLE_EMPTY_STATE_DETAILS["cycles"].primaryButton.text, - onClick: () => { - setTrackElement("Cycle empty state"); - 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 45204541b..d299c2182 100644 --- a/web/pages/[workspaceSlug]/projects/[projectId]/pages/index.tsx +++ b/web/pages/[workspaceSlug]/projects/[projectId]/pages/index.tsx @@ -2,18 +2,9 @@ import { useState, Fragment, ReactElement } from "react"; import { observer } from "mobx-react-lite"; import dynamic from "next/dynamic"; import { useRouter } from "next/router"; -import { useTheme } from "next-themes"; import useSWR from "swr"; import { Tab } from "@headlessui/react"; // hooks -import { PageHead } from "components/core"; -import { EmptyState, getEmptyStateImagePath } from "components/empty-state"; -import { PagesHeader } from "components/headers"; -import { RecentPagesList, CreateUpdatePageModal } from "components/pages"; -import { PagesLoader } from "components/ui"; -import { PAGE_EMPTY_STATE_DETAILS } from "constants/empty-state"; -import { PAGE_TABS_LIST } from "constants/page"; -import { EUserWorkspaceRoles } from "constants/workspace"; import { useApplication, useEventTracker, useUser, useProject } from "hooks/store"; import { useProjectPages } from "hooks/store/use-project-page"; import useLocalStorage from "hooks/use-local-storage"; @@ -22,9 +13,16 @@ import useSize from "hooks/use-window-size"; // layouts import { AppLayout } from "layouts/app-layout"; // components +import { RecentPagesList, CreateUpdatePageModal } from "components/pages"; +import { EmptyState } from "components/empty-state"; +import { PagesHeader } from "components/headers"; +import { PagesLoader } from "components/ui"; +import { PageHead } from "components/core"; // types import { NextPageWithLayout } from "lib/types"; // constants +import { PAGE_TABS_LIST } from "constants/page"; +import { EmptyStateType } from "constants/empty-state"; const AllPagesList = dynamic(() => import("components/pages").then((a) => a.AllPagesList), { ssr: false, @@ -52,14 +50,8 @@ const ProjectPagesPage: NextPageWithLayout = observer(() => { const { workspaceSlug, projectId } = router.query; // states const [createUpdatePageModal, setCreateUpdatePageModal] = useState(false); - // theme - const { resolvedTheme } = useTheme(); // store hooks - const { - currentUser, - currentUserLoader, - membership: { currentProjectRole }, - } = useUser(); + const { currentUser, currentUserLoader } = useUser(); const { commandPalette: { toggleCreatePageModal }, } = useApplication(); @@ -103,9 +95,6 @@ const ProjectPagesPage: NextPageWithLayout = observer(() => { }; // derived values - const isLightMode = resolvedTheme ? resolvedTheme === "light" : currentUser?.theme.theme === "light"; - const EmptyStateImagePath = getEmptyStateImagePath("onboarding", "pages", isLightMode); - const isEditingAllowed = !!currentProjectRole && currentProjectRole >= EUserWorkspaceRoles.MEMBER; const project = projectId ? getProjectById(projectId.toString()) : undefined; const pageTitle = project?.name ? `${project?.name} - Pages` : undefined; @@ -216,22 +205,11 @@ const ProjectPagesPage: NextPageWithLayout = observer(() => { ) : ( { - setTrackElement("Pages empty state"); - toggleCreatePageModal(true); - }, + type={EmptyStateType.PROJECT_PAGE} + primaryButtonOnClick={() => { + setTrackElement("Pages empty state"); + toggleCreatePageModal(true); }} - comicBox={{ - title: PAGE_EMPTY_STATE_DETAILS["pages"].comicBox.title, - description: PAGE_EMPTY_STATE_DETAILS["pages"].comicBox.description, - }} - size="lg" - disabled={!isEditingAllowed} /> )} diff --git a/web/pages/[workspaceSlug]/projects/[projectId]/settings/integrations.tsx b/web/pages/[workspaceSlug]/projects/[projectId]/settings/integrations.tsx index b227becf9..60e9ca61a 100644 --- a/web/pages/[workspaceSlug]/projects/[projectId]/settings/integrations.tsx +++ b/web/pages/[workspaceSlug]/projects/[projectId]/settings/integrations.tsx @@ -1,17 +1,9 @@ import { ReactElement } from "react"; import { observer } from "mobx-react"; import { useRouter } from "next/router"; -import { useTheme } from "next-themes"; import useSWR from "swr"; // hooks -import { PageHead } from "components/core"; -import { EmptyState, getEmptyStateImagePath } from "components/empty-state"; -import { ProjectSettingHeader } from "components/headers"; -import { IntegrationCard } from "components/project"; import { IntegrationsSettingsLoader } from "components/ui"; -import { PROJECT_SETTINGS_EMPTY_STATE_DETAILS } from "constants/empty-state"; -import { PROJECT_DETAILS, WORKSPACE_INTEGRATIONS } from "constants/fetch-keys"; -import { useUser } from "hooks/store"; // layouts import { AppLayout } from "layouts/app-layout"; import { ProjectSettingLayout } from "layouts/settings-layout"; @@ -20,10 +12,17 @@ import { NextPageWithLayout } from "lib/types"; import { IntegrationService } from "services/integrations"; import { ProjectService } from "services/project"; // components +import { PageHead } from "components/core"; +import { IntegrationCard } from "components/project"; +import { ProjectSettingHeader } from "components/headers"; +import { EmptyState } from "components/empty-state"; // ui // types import { IProject } from "@plane/types"; // fetch-keys +import { PROJECT_DETAILS, WORKSPACE_INTEGRATIONS } from "constants/fetch-keys"; +// constants +import { EmptyStateType } from "constants/empty-state"; // services const integrationService = new IntegrationService(); @@ -32,10 +31,6 @@ const projectService = new ProjectService(); const ProjectIntegrationsPage: NextPageWithLayout = observer(() => { const router = useRouter(); const { workspaceSlug, projectId } = router.query; - // theme - const { resolvedTheme } = useTheme(); - // store hooks - const { currentUser } = useUser(); // fetch project details const { data: projectDetails } = useSWR( workspaceSlug && projectId ? PROJECT_DETAILS(projectId as string) : null, @@ -47,9 +42,6 @@ const ProjectIntegrationsPage: NextPageWithLayout = observer(() => { () => (workspaceSlug ? integrationService.getWorkspaceIntegrationsList(workspaceSlug as string) : null) ); // derived values - const emptyStateDetail = PROJECT_SETTINGS_EMPTY_STATE_DETAILS["integrations"]; - const isLightMode = resolvedTheme ? resolvedTheme === "light" : currentUser?.theme.theme === "light"; - const emptyStateImage = getEmptyStateImagePath("project-settings", "integrations", isLightMode); const isAdmin = projectDetails?.member_role === 20; const pageTitle = projectDetails?.name ? `${projectDetails?.name} - Integrations` : undefined; @@ -70,15 +62,8 @@ const ProjectIntegrationsPage: NextPageWithLayout = observer(() => { ) : (
router.push(`/${workspaceSlug}/settings/integrations`), - }} - size="lg" - disabled={!isAdmin} + type={EmptyStateType.PROJECT_SETTINGS_INTEGRATIONS} + primaryButtonLink={`/${workspaceSlug}/settings/integrations`} />
) diff --git a/web/pages/[workspaceSlug]/settings/api-tokens.tsx b/web/pages/[workspaceSlug]/settings/api-tokens.tsx index 75d46b63d..59c205968 100644 --- a/web/pages/[workspaceSlug]/settings/api-tokens.tsx +++ b/web/pages/[workspaceSlug]/settings/api-tokens.tsx @@ -1,29 +1,28 @@ import React, { useState } from "react"; import { observer } from "mobx-react-lite"; import { useRouter } from "next/router"; -import { useTheme } from "next-themes"; import useSWR from "swr"; // store hooks -import { Button } from "@plane/ui"; -import { ApiTokenListItem, CreateApiTokenModal } from "components/api-token"; -import { PageHead } from "components/core"; -import { EmptyState, getEmptyStateImagePath } from "components/empty-state"; -import { WorkspaceSettingHeader } from "components/headers"; -import { APITokenSettingsLoader } from "components/ui"; -import { WORKSPACE_SETTINGS_EMPTY_STATE_DETAILS } from "constants/empty-state"; -import { API_TOKENS_LIST } from "constants/fetch-keys"; -import { EUserWorkspaceRoles } from "constants/workspace"; import { useUser, useWorkspace } from "hooks/store"; // layouts import { AppLayout } from "layouts/app-layout"; import { WorkspaceSettingLayout } from "layouts/settings-layout"; // component +import { APITokenSettingsLoader } from "components/ui"; +import { WorkspaceSettingHeader } from "components/headers"; +import { ApiTokenListItem, CreateApiTokenModal } from "components/api-token"; +import { EmptyState } from "components/empty-state"; +import { PageHead } from "components/core"; // ui +import { Button } from "@plane/ui"; // services import { NextPageWithLayout } from "lib/types"; import { APITokenService } from "services/api_token.service"; // types // constants +import { API_TOKENS_LIST } from "constants/fetch-keys"; +import { EUserWorkspaceRoles } from "constants/workspace"; +import { EmptyStateType } from "constants/empty-state"; const apiTokenService = new APITokenService(); @@ -33,12 +32,9 @@ const ApiTokensPage: NextPageWithLayout = observer(() => { // router const router = useRouter(); const { workspaceSlug } = router.query; - // theme - const { resolvedTheme } = useTheme(); // store hooks const { membership: { currentWorkspaceRole }, - currentUser, } = useUser(); const { currentWorkspace } = useWorkspace(); @@ -48,9 +44,6 @@ const ApiTokensPage: NextPageWithLayout = observer(() => { workspaceSlug && isAdmin ? apiTokenService.getApiTokens(workspaceSlug.toString()) : null ); - const emptyStateDetail = WORKSPACE_SETTINGS_EMPTY_STATE_DETAILS["api-tokens"]; - const isLightMode = resolvedTheme ? resolvedTheme === "light" : currentUser?.theme.theme === "light"; - const emptyStateImage = getEmptyStateImagePath("workspace-settings", "api-tokens", isLightMode); const pageTitle = currentWorkspace?.name ? `${currentWorkspace.name} - API Tokens` : undefined; if (!isAdmin) @@ -95,12 +88,7 @@ const ApiTokensPage: NextPageWithLayout = observer(() => {
- +
)} diff --git a/web/pages/[workspaceSlug]/settings/webhooks/index.tsx b/web/pages/[workspaceSlug]/settings/webhooks/index.tsx index d5058e29f..24dca325c 100644 --- a/web/pages/[workspaceSlug]/settings/webhooks/index.tsx +++ b/web/pages/[workspaceSlug]/settings/webhooks/index.tsx @@ -1,25 +1,24 @@ import React, { useEffect, useState } from "react"; import { observer } from "mobx-react-lite"; import { useRouter } from "next/router"; -import { useTheme } from "next-themes"; import useSWR from "swr"; // hooks -import { Button } from "@plane/ui"; -import { PageHead } from "components/core"; -import { EmptyState, getEmptyStateImagePath } from "components/empty-state"; -import { WorkspaceSettingHeader } from "components/headers"; -import { WebhookSettingsLoader } from "components/ui"; -import { WebhooksList, CreateWebhookModal } from "components/web-hooks"; -import { WORKSPACE_SETTINGS_EMPTY_STATE_DETAILS } from "constants/empty-state"; import { useUser, useWebhook, useWorkspace } from "hooks/store"; // layouts import { AppLayout } from "layouts/app-layout"; import { WorkspaceSettingLayout } from "layouts/settings-layout"; // components +import { PageHead } from "components/core"; +import { WorkspaceSettingHeader } from "components/headers"; +import { WebhookSettingsLoader } from "components/ui"; +import { WebhooksList, CreateWebhookModal } from "components/web-hooks"; +import { EmptyState } from "components/empty-state"; // ui +import { Button } from "@plane/ui"; // types import { NextPageWithLayout } from "lib/types"; // constants +import { EmptyStateType } from "constants/empty-state"; const WebhooksListPage: NextPageWithLayout = observer(() => { // states @@ -27,12 +26,9 @@ const WebhooksListPage: NextPageWithLayout = observer(() => { // router const router = useRouter(); const { workspaceSlug } = router.query; - // theme - const { resolvedTheme } = useTheme(); // mobx store const { membership: { currentWorkspaceRole }, - currentUser, } = useUser(); const { fetchWebhooks, webhooks, clearSecretKey, webhookSecretKey, createWebhook } = useWebhook(); const { currentWorkspace } = useWorkspace(); @@ -44,10 +40,6 @@ const WebhooksListPage: NextPageWithLayout = observer(() => { workspaceSlug && isAdmin ? () => fetchWebhooks(workspaceSlug.toString()) : null ); - const emptyStateDetail = WORKSPACE_SETTINGS_EMPTY_STATE_DETAILS["webhooks"]; - - const isLightMode = resolvedTheme ? resolvedTheme === "light" : currentUser?.theme.theme === "light"; - const emptyStateImage = getEmptyStateImagePath("workspace-settings", "webhooks", isLightMode); const pageTitle = currentWorkspace?.name ? `${currentWorkspace.name} - Webhooks` : undefined; // clear secret key when modal is closed. @@ -99,12 +91,7 @@ const WebhooksListPage: NextPageWithLayout = observer(() => {
- +
)} diff --git a/web/public/empty-state/module-issues/gantt-dark-resp.webp b/web/public/empty-state/module-issues/gantt_chart-dark-resp.webp similarity index 100% rename from web/public/empty-state/module-issues/gantt-dark-resp.webp rename to web/public/empty-state/module-issues/gantt_chart-dark-resp.webp diff --git a/web/public/empty-state/module-issues/gantt-dark.webp b/web/public/empty-state/module-issues/gantt_chart-dark.webp similarity index 100% rename from web/public/empty-state/module-issues/gantt-dark.webp rename to web/public/empty-state/module-issues/gantt_chart-dark.webp diff --git a/web/public/empty-state/module-issues/gantt-light-resp.webp b/web/public/empty-state/module-issues/gantt_chart-light-resp.webp similarity index 100% rename from web/public/empty-state/module-issues/gantt-light-resp.webp rename to web/public/empty-state/module-issues/gantt_chart-light-resp.webp diff --git a/web/public/empty-state/module-issues/gantt-light.webp b/web/public/empty-state/module-issues/gantt_chart-light.webp similarity index 100% rename from web/public/empty-state/module-issues/gantt-light.webp rename to web/public/empty-state/module-issues/gantt_chart-light.webp