[WEB-630] refactor: empty state (#3858)

* refactor: empty state global config file added and empty state component refactor

* refactor: empty state component refactor

* chore: empty state refactor

* chore: empty state config file updated

* chore: empty state action button permission logic updated

* chore: empty state config file updated

* chore: cycle and module empty filter state updated

* chore: empty state asset updated

* chore: empty state config file updated

* chore: empty state config file updated

* chore: empty state component improvement

* chore: empty state action button improvement

* fix: merge conflict
This commit is contained in:
Anmol Singh Bhatia 2024-03-06 20:16:54 +05:30 committed by GitHub
parent 4861be2773
commit a08f401452
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
32 changed files with 759 additions and 1155 deletions

View File

@ -1,10 +1,9 @@
import { MouseEvent } from "react"; import { MouseEvent } from "react";
import { observer } from "mobx-react-lite"; import { observer } from "mobx-react-lite";
import Link from "next/link"; import Link from "next/link";
import { useTheme } from "next-themes";
import useSWR from "swr"; import useSWR from "swr";
// hooks // hooks
import { useCycle, useIssues, useMember, useProject, useUser } from "hooks/store"; import { useCycle, useIssues, useMember, useProject } from "hooks/store";
// ui // ui
import { SingleProgressStats } from "components/core"; import { SingleProgressStats } from "components/core";
import { import {
@ -23,7 +22,7 @@ import {
import ProgressChart from "components/core/sidebar/progress-chart"; import ProgressChart from "components/core/sidebar/progress-chart";
import { ActiveCycleProgressStats } from "components/cycles"; import { ActiveCycleProgressStats } from "components/cycles";
import { StateDropdown } from "components/dropdowns"; import { StateDropdown } from "components/dropdowns";
import { EmptyState, getEmptyStateImagePath } from "components/empty-state"; import { EmptyState } from "components/empty-state";
// icons // icons
import { ArrowRight, CalendarCheck, CalendarDays, Star, Target } from "lucide-react"; import { ArrowRight, CalendarCheck, CalendarDays, Star, Target } from "lucide-react";
// helpers // helpers
@ -35,7 +34,7 @@ import { ICycle, TCycleGroups } from "@plane/types";
import { EIssuesStoreType } from "constants/issue"; import { EIssuesStoreType } from "constants/issue";
import { CYCLE_ISSUES_WITH_PARAMS } from "constants/fetch-keys"; import { CYCLE_ISSUES_WITH_PARAMS } from "constants/fetch-keys";
import { CYCLE_STATE_GROUPS_DETAILS } from "constants/cycle"; 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 { interface IActiveCycleDetails {
workspaceSlug: string; workspaceSlug: string;
@ -45,9 +44,6 @@ interface IActiveCycleDetails {
export const ActiveCycleDetails: React.FC<IActiveCycleDetails> = observer((props) => { export const ActiveCycleDetails: React.FC<IActiveCycleDetails> = observer((props) => {
// props // props
const { workspaceSlug, projectId } = props; const { workspaceSlug, projectId } = props;
const { resolvedTheme } = useTheme();
// store hooks
const { currentUser } = useUser();
const { const {
issues: { fetchActiveCycleIssues }, issues: { fetchActiveCycleIssues },
} = useIssues(EIssuesStoreType.CYCLE); } = useIssues(EIssuesStoreType.CYCLE);
@ -78,11 +74,6 @@ export const ActiveCycleDetails: React.FC<IActiveCycleDetails> = observer((props
: null : 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) if (!activeCycle && isLoading)
return ( return (
<Loader> <Loader>
@ -90,15 +81,7 @@ export const ActiveCycleDetails: React.FC<IActiveCycleDetails> = observer((props
</Loader> </Loader>
); );
if (!activeCycle) if (!activeCycle) return <EmptyState type={EmptyStateType.PROJECT_CYCLE_ACTIVE} size="sm" />;
return (
<EmptyState
title={emptyStateDetail.title}
description={emptyStateDetail.description}
image={emptyStateImage}
size="sm"
/>
);
const endDate = new Date(activeCycle.end_date ?? ""); const endDate = new Date(activeCycle.end_date ?? "");
const startDate = new Date(activeCycle.start_date ?? ""); const startDate = new Date(activeCycle.start_date ?? "");

View File

@ -1,13 +1,10 @@
import { FC } from "react"; import { FC } from "react";
import { observer } from "mobx-react-lite"; import { observer } from "mobx-react-lite";
import { useTheme } from "next-themes";
// hooks
// components // components
import { CyclePeekOverview, CyclesBoardCard } from "components/cycles"; import { CyclePeekOverview, CyclesBoardCard } from "components/cycles";
import { EmptyState, getEmptyStateImagePath } from "components/empty-state"; import { EmptyState } from "components/empty-state";
// constants // constants
import { CYCLE_EMPTY_STATE_DETAILS } from "constants/empty-state"; import { EMPTY_STATE_DETAILS } from "constants/empty-state";
import { useUser } from "hooks/store";
export interface ICyclesBoard { export interface ICyclesBoard {
cycleIds: string[]; cycleIds: string[];
@ -19,15 +16,6 @@ export interface ICyclesBoard {
export const CyclesBoard: FC<ICyclesBoard> = observer((props) => { export const CyclesBoard: FC<ICyclesBoard> = observer((props) => {
const { cycleIds, filter, workspaceSlug, projectId, peekCycle } = 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 ( return (
<> <>
@ -52,12 +40,7 @@ export const CyclesBoard: FC<ICyclesBoard> = observer((props) => {
</div> </div>
</div> </div>
) : ( ) : (
<EmptyState <EmptyState type={`project-cycle-${filter}` as keyof typeof EMPTY_STATE_DETAILS} size="sm" />
title={emptyStateDetail.title}
description={emptyStateDetail.description}
image={emptyStateImage}
size="sm"
/>
)} )}
</> </>
); );

View File

@ -1,15 +1,12 @@
import { FC } from "react"; import { FC } from "react";
import { observer } from "mobx-react-lite"; import { observer } from "mobx-react-lite";
import { useTheme } from "next-themes";
// hooks
// components // components
import { Loader } from "@plane/ui";
import { CyclePeekOverview, CyclesListItem } from "components/cycles"; import { CyclePeekOverview, CyclesListItem } from "components/cycles";
import { EmptyState, getEmptyStateImagePath } from "components/empty-state"; import { EmptyState } from "components/empty-state";
// ui // ui
import { Loader } from "@plane/ui";
// constants // constants
import { CYCLE_EMPTY_STATE_DETAILS } from "constants/empty-state"; import { EMPTY_STATE_DETAILS } from "constants/empty-state";
import { useUser } from "hooks/store";
export interface ICyclesList { export interface ICyclesList {
cycleIds: string[]; cycleIds: string[];
@ -20,15 +17,6 @@ export interface ICyclesList {
export const CyclesList: FC<ICyclesList> = observer((props) => { export const CyclesList: FC<ICyclesList> = observer((props) => {
const { cycleIds, filter, workspaceSlug, projectId } = 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 ( return (
<> <>
@ -54,12 +42,7 @@ export const CyclesList: FC<ICyclesList> = observer((props) => {
</div> </div>
</div> </div>
) : ( ) : (
<EmptyState <EmptyState type={`project-cycle-${filter}` as keyof typeof EMPTY_STATE_DETAILS} size="sm" />
title={emptyStateDetail.title}
description={emptyStateDetail.description}
image={emptyStateImage}
size="sm"
/>
)} )}
</> </>
) : ( ) : (

View File

@ -1,119 +1,150 @@
import React from "react"; import React from "react";
import Link from "next/link";
import Image from "next/image"; 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 = { import { useTheme } from "next-themes";
title: string; // hooks
description?: string; import { useUser } from "hooks/store";
image: any; // components
primaryButton?: { import { Button, TButtonVariant } from "@plane/ui";
icon?: any; import { ComicBoxButton } from "./comic-box-button";
text: string; // constant
onClick: () => void; import { EMPTY_STATE_DETAILS, EmptyStateKeys } from "constants/empty-state";
}; // helpers
secondaryButton?: { import { cn } from "helpers/common.helper";
icon?: any;
text: string; export type EmptyStateProps = {
onClick: () => void; type: EmptyStateKeys;
}; size?: "sm" | "md" | "lg";
comicBox?: { layout?: "widget-simple" | "screen-detailed" | "screen-simple";
title: string; additionalPath?: string;
description: string; primaryButtonOnClick?: () => void;
}; primaryButtonLink?: string;
size?: "sm" | "lg"; secondaryButtonOnClick?: () => void;
disabled?: boolean;
}; };
export const EmptyState: React.FC<Props> = ({ export const EmptyState: React.FC<EmptyStateProps> = (props) => {
title, const {
description, type,
image, size = "lg",
primaryButton, layout = "screen-detailed",
secondaryButton, additionalPath = "",
comicBox, primaryButtonOnClick,
size = "sm", primaryButtonLink,
disabled = false, secondaryButtonOnClick,
}) => { } = props;
const emptyStateHeader = ( // 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 (
<ComicBoxButton
label={primaryButton.text}
icon={primaryButton.icon}
title={primaryButton.comicBox?.title}
description={primaryButton.comicBox?.description}
onClick={primaryButtonOnClick}
disabled={!isEditingAllowed}
/>
);
} else if (primaryButtonLink) {
return (
<Link href={primaryButtonLink}>
<Button {...commonProps}>{primaryButton.text}</Button>
</Link>
);
} else {
return <Button {...commonProps}>{primaryButton.text}</Button>;
}
};
// secondary button
const renderSecondaryButton = () => {
if (!secondaryButton) return null;
return (
<Button
size={size}
variant="neutral-primary"
prependIcon={secondaryButton.icon}
onClick={secondaryButtonOnClick}
disabled={!isEditingAllowed}
>
{secondaryButton.text}
</Button>
);
};
return (
<> <>
{description ? ( {layout === "screen-detailed" && (
<> <div className="flex items-center justify-center min-h-full min-w-full overflow-y-auto py-10 md:px-20 px-5">
<h3 className="text-xl font-semibold">{title}</h3> <div
<p className="text-sm">{description}</p> className={cn("flex flex-col gap-5", {
</> "md:min-w-[24rem] max-w-[45rem]": size === "sm",
) : ( "md:min-w-[30rem] max-w-[60rem]": size === "lg",
<h3 className="text-xl font-medium">{title}</h3> })}
>
<div className="flex flex-col gap-1.5 flex-shrink">
{description ? (
<>
<h3 className="text-xl font-semibold">{title}</h3>
<p className="text-sm">{description}</p>
</>
) : (
<h3 className="text-xl font-medium">{title}</h3>
)}
</div>
{path && (
<Image
src={resolvedEmptyStatePath}
alt={key || "button image"}
width={384}
height={250}
layout="responsive"
lazyBoundary="100%"
/>
)}
{anyButton && (
<>
<div className="relative flex items-center justify-center gap-2 flex-shrink-0 w-full">
{renderPrimaryButton()}
{renderSecondaryButton()}
</div>
</>
)}
</div>
</div>
)} )}
</> </>
); );
const secondaryButtonElement = secondaryButton && (
<Button
size={size === "sm" ? "md" : "lg"}
variant="neutral-primary"
prependIcon={secondaryButton.icon}
onClick={secondaryButton.onClick}
disabled={disabled}
>
{secondaryButton.text}
</Button>
);
return (
<div className="flex items-center justify-center min-h-full min-w-full overflow-y-auto py-10 md:px-20 px-5">
<div
className={cn("flex flex-col gap-5", {
"md:min-w-[24rem] max-w-[45rem]": size === "sm",
"md:min-w-[30rem] max-w-[60rem]": size === "lg",
})}
>
<div className="flex flex-col gap-1.5 flex-shrink">{emptyStateHeader}</div>
<Image
src={image}
alt={primaryButton?.text || "button image"}
width={384}
height={250}
layout="responsive"
lazyBoundary="100%"
/>
<div className="relative flex items-center justify-center gap-2 flex-shrink-0 w-full">
{primaryButton && (
<>
<div className="relative flex items-start justify-center">
{comicBox ? (
<ComicBoxButton
label={primaryButton.text}
icon={primaryButton.icon}
title={comicBox?.title}
description={comicBox?.description}
onClick={() => primaryButton.onClick()}
disabled={disabled}
/>
) : (
<div
className={`flex items-center gap-2.5 ${
disabled ? "cursor-not-allowed" : "cursor-pointer"
} ${getButtonStyling("primary", "lg", disabled)}`}
onClick={() => primaryButton.onClick()}
>
{primaryButton.icon}
<span className="leading-4">{primaryButton.text}</span>
</div>
)}
</div>
</>
)}
{secondaryButton && secondaryButtonElement}
</div>
</div>
</div>
);
}; };

View File

@ -1,20 +1,19 @@
import React, { useState } from "react"; import React, { useState } from "react";
import { observer } from "mobx-react-lite"; import { observer } from "mobx-react-lite";
import { useRouter } from "next/router"; import { useRouter } from "next/router";
import { useTheme } from "next-themes";
// store hooks // store hooks
import { Button, Loader, TOAST_TYPE, setToast } from "@plane/ui"; import { useEstimate, useProject } from "hooks/store";
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";
// components // components
import { CreateUpdateEstimateModal, DeleteEstimateModal, EstimateListItem } from "components/estimates";
import { EmptyState } from "components/empty-state";
// ui // ui
import { Button, Loader, TOAST_TYPE, setToast } from "@plane/ui";
// types // types
import { IEstimate } from "@plane/types"; import { IEstimate } from "@plane/types";
// helpers // helpers
import { orderArrayBy } from "helpers/array.helper";
// constants // constants
import { EmptyStateType } from "constants/empty-state";
export const EstimatesList: React.FC = observer(() => { export const EstimatesList: React.FC = observer(() => {
// states // states
@ -24,12 +23,9 @@ export const EstimatesList: React.FC = observer(() => {
// router // router
const router = useRouter(); const router = useRouter();
const { workspaceSlug, projectId } = router.query; const { workspaceSlug, projectId } = router.query;
// theme
const { resolvedTheme } = useTheme();
// store hooks // store hooks
const { updateProject, currentProjectDetails } = useProject(); const { updateProject, currentProjectDetails } = useProject();
const { projectEstimates, getProjectEstimateById } = useEstimate(); const { projectEstimates, getProjectEstimateById } = useEstimate();
const { currentUser } = useUser();
const editEstimate = (estimate: IEstimate) => { const editEstimate = (estimate: IEstimate) => {
setEstimateFormOpen(true); 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 ( return (
<> <>
<CreateUpdateEstimateModal <CreateUpdateEstimateModal
@ -113,12 +105,7 @@ export const EstimatesList: React.FC = observer(() => {
</section> </section>
) : ( ) : (
<div className="h-full w-full py-8"> <div className="h-full w-full py-8">
<EmptyState <EmptyState type={EmptyStateType.PROJECT_SETTINGS_ESTIMATE} />
title={emptyStateDetail.title}
description={emptyStateDetail.description}
image={emptyStateImage}
size="lg"
/>
</div> </div>
) )
) : ( ) : (

View File

@ -4,26 +4,24 @@ import { observer } from "mobx-react-lite";
import Image from "next/image"; import Image from "next/image";
import Link from "next/link"; import Link from "next/link";
import { useRouter } from "next/router"; import { useRouter } from "next/router";
import { useTheme } from "next-themes";
import useSWR, { mutate } from "swr"; import useSWR, { mutate } from "swr";
// hooks // 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 { useUser } from "hooks/store";
import useUserAuth from "hooks/use-user-auth"; import useUserAuth from "hooks/use-user-auth";
// services // services
import { IntegrationService } from "services/integrations"; import { IntegrationService } from "services/integrations";
// components // components
import { Exporter, SingleExport } from "components/exporter";
import { ImportExportSettingsLoader } from "components/ui";
import { EmptyState } from "components/empty-state";
// ui // ui
import { Button } from "@plane/ui";
// icons // icons
// fetch-keys import { MoveLeft, MoveRight, RefreshCw } from "lucide-react";
// constants // constants
import { EXPORT_SERVICES_LIST } from "constants/fetch-keys";
import { EXPORTERS_LIST } from "constants/workspace";
import { EmptyStateType } from "constants/empty-state";
// services // services
const integrationService = new IntegrationService(); const integrationService = new IntegrationService();
@ -36,8 +34,6 @@ const IntegrationGuide = observer(() => {
// router // router
const router = useRouter(); const router = useRouter();
const { workspaceSlug, provider } = router.query; const { workspaceSlug, provider } = router.query;
// theme
const { resolvedTheme } = useTheme();
// store hooks // store hooks
const { currentUser, currentUserLoader } = useUser(); const { currentUser, currentUserLoader } = useUser();
// custom hooks // custom hooks
@ -50,10 +46,6 @@ const IntegrationGuide = observer(() => {
: null : 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 = () => { const handleCsvClose = () => {
router.replace(`/${workspaceSlug?.toString()}/settings/exports`); router.replace(`/${workspaceSlug?.toString()}/settings/exports`);
}; };
@ -149,12 +141,7 @@ const IntegrationGuide = observer(() => {
</div> </div>
) : ( ) : (
<div className="h-full w-full flex items-center justify-center"> <div className="h-full w-full flex items-center justify-center">
<EmptyState <EmptyState type={EmptyStateType.WORKSPACE_SETTINGS_EXPORT} size="sm" />
title={emptyStateDetail.title}
description={emptyStateDetail.description}
image={emptyStateImage}
size="sm"
/>
</div> </div>
) )
) : ( ) : (

View File

@ -3,28 +3,26 @@ import { observer } from "mobx-react-lite";
import Image from "next/image"; import Image from "next/image";
import Link from "next/link"; import Link from "next/link";
import { useRouter } from "next/router"; import { useRouter } from "next/router";
import { useTheme } from "next-themes";
import useSWR, { mutate } from "swr"; import useSWR, { mutate } from "swr";
// hooks // 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 { useUser } from "hooks/store";
import useUserAuth from "hooks/use-user-auth"; import useUserAuth from "hooks/use-user-auth";
// services // services
import { IntegrationService } from "services/integrations"; import { IntegrationService } from "services/integrations";
// components // components
import { ImportExportSettingsLoader } from "components/ui";
import { DeleteImportModal, GithubImporterRoot, JiraImporterRoot, SingleImport } from "components/integration";
import { EmptyState } from "components/empty-state";
// ui // ui
import { Button } from "@plane/ui";
// icons // icons
import { RefreshCw } from "lucide-react";
// types // types
import { IImporterService } from "@plane/types"; import { IImporterService } from "@plane/types";
// fetch-keys
// constants // constants
import { IMPORTER_SERVICES_LIST } from "constants/fetch-keys";
import { IMPORTERS_LIST } from "constants/workspace";
import { EmptyStateType } from "constants/empty-state";
// services // services
const integrationService = new IntegrationService(); const integrationService = new IntegrationService();
@ -37,8 +35,6 @@ const IntegrationGuide = observer(() => {
// router // router
const router = useRouter(); const router = useRouter();
const { workspaceSlug, provider } = router.query; const { workspaceSlug, provider } = router.query;
// theme
const { resolvedTheme } = useTheme();
// store hooks // store hooks
const { currentUser, currentUserLoader } = useUser(); const { currentUser, currentUserLoader } = useUser();
// custom hooks // custom hooks
@ -49,10 +45,6 @@ const IntegrationGuide = observer(() => {
workspaceSlug ? () => integrationService.getImporterServicesList(workspaceSlug as string) : null 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) => { const handleDeleteImport = (importService: IImporterService) => {
setImportToDelete(importService); setImportToDelete(importService);
setDeleteImportModal(true); setDeleteImportModal(true);
@ -145,12 +137,7 @@ const IntegrationGuide = observer(() => {
</div> </div>
) : ( ) : (
<div className="h-full w-full flex items-center justify-center"> <div className="h-full w-full flex items-center justify-center">
<EmptyState <EmptyState type={EmptyStateType.WORKSPACE_SETTINGS_IMPORT} size="sm" />
title={emptyStateDetail.title}
description={emptyStateDetail.description}
image={emptyStateImage}
size="sm"
/>
</div> </div>
) )
) : ( ) : (

View File

@ -1,49 +1,27 @@
import size from "lodash/size"; import size from "lodash/size";
import { observer } from "mobx-react-lite"; import { observer } from "mobx-react-lite";
import { useRouter } from "next/router"; import { useRouter } from "next/router";
import { useTheme } from "next-themes";
// hooks // hooks
import { EmptyState, getEmptyStateImagePath } from "components/empty-state"; import { useIssues } from "hooks/store";
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";
// components // components
import { EmptyState } from "components/empty-state";
// constants // constants
import { EIssueFilterType, EIssuesStoreType } from "constants/issue";
import { EmptyStateType } from "constants/empty-state";
// types // types
import { IIssueFilterOptions } from "@plane/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(() => { export const ProjectArchivedEmptyState: React.FC = observer(() => {
// router // router
const router = useRouter(); const router = useRouter();
const { workspaceSlug, projectId } = router.query; const { workspaceSlug, projectId } = router.query;
// theme // theme
const { resolvedTheme } = useTheme();
// store hooks // store hooks
const {
membership: { currentProjectRole },
currentUser,
} = useUser();
const { issuesFilter } = useIssues(EIssuesStoreType.ARCHIVED); const { issuesFilter } = useIssues(EIssuesStoreType.ARCHIVED);
const userFilters = issuesFilter?.issueFilters?.filters; const userFilters = issuesFilter?.issueFilters?.filters;
const activeLayout = issuesFilter?.issueFilters?.displayFilters?.layout; 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( const issueFilterCount = size(
Object.fromEntries( Object.fromEntries(
Object.entries(userFilters ?? {}).filter(([, value]) => value && Array.isArray(value) && value.length > 0) 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 emptyStateType =
issueFilterCount > 0 ? EmptyStateType.PROJECT_ARCHIVED_EMPTY_FILTER : EmptyStateType.PROJECT_ARCHIVED_NO_ISSUES;
const emptyStateProps: EmptyStateProps = const additionalPath = issueFilterCount > 0 ? activeLayout ?? "list" : undefined;
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,
};
return ( return (
<div className="relative h-full w-full overflow-y-auto"> <div className="relative h-full w-full overflow-y-auto">
<EmptyState {...emptyStateProps} /> <EmptyState
type={emptyStateType}
additionalPath={additionalPath}
primaryButtonLink={
issueFilterCount > 0 ? undefined : `/${workspaceSlug}/projects/${projectId}/settings/automations`
}
secondaryButtonOnClick={issueFilterCount > 0 ? handleClearAllFilters : undefined}
/>
</div> </div>
); );
}); });

View File

@ -1,21 +1,17 @@
import { useState } from "react"; import { useState } from "react";
import { observer } from "mobx-react-lite"; import { observer } from "mobx-react-lite";
import { useTheme } from "next-themes";
import { PlusIcon } from "lucide-react";
// hooks // hooks
import { useApplication, useEventTracker, useIssues } from "hooks/store";
// ui
import { TOAST_TYPE, setToast } from "@plane/ui"; import { TOAST_TYPE, setToast } from "@plane/ui";
import { ExistingIssuesListModal } from "components/core"; 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 // components
import { EmptyState } from "components/empty-state";
// types // types
import { ISearchIssueResponse, TIssueLayouts } from "@plane/types"; import { ISearchIssueResponse, TIssueLayouts } from "@plane/types";
// constants // constants
import { EIssuesStoreType } from "constants/issue";
import { EmptyStateType } from "constants/empty-state";
type Props = { type Props = {
workspaceSlug: string | undefined; workspaceSlug: string | undefined;
@ -26,33 +22,16 @@ type Props = {
isEmptyFilters?: boolean; 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<Props> = observer((props) => { export const CycleEmptyState: React.FC<Props> = observer((props) => {
const { workspaceSlug, projectId, cycleId, activeLayout, handleClearAllFilters, isEmptyFilters = false } = props; const { workspaceSlug, projectId, cycleId, activeLayout, handleClearAllFilters, isEmptyFilters = false } = props;
// states // states
const [cycleIssuesListModal, setCycleIssuesListModal] = useState(false); const [cycleIssuesListModal, setCycleIssuesListModal] = useState(false);
// theme
const { resolvedTheme } = useTheme();
// store hooks // store hooks
const { issues } = useIssues(EIssuesStoreType.CYCLE); const { issues } = useIssues(EIssuesStoreType.CYCLE);
const { const {
commandPalette: { toggleCreateIssueModal }, commandPalette: { toggleCreateIssueModal },
} = useApplication(); } = useApplication();
const { setTrackElement } = useEventTracker(); const { setTrackElement } = useEventTracker();
const {
membership: { currentProjectRole: userRole },
currentUser,
} = useUser();
const handleAddIssuesToCycle = async (data: ISearchIssueResponse[]) => { const handleAddIssuesToCycle = async (data: ISearchIssueResponse[]) => {
if (!workspaceSlug || !projectId || !cycleId) return; if (!workspaceSlug || !projectId || !cycleId) return;
@ -77,43 +56,9 @@ export const CycleEmptyState: React.FC<Props> = observer((props) => {
); );
}; };
const emptyStateDetail = CYCLE_EMPTY_STATE_DETAILS["no-issues"]; const emptyStateType = isEmptyFilters ? EmptyStateType.PROJECT_EMPTY_FILTER : EmptyStateType.PROJECT_CYCLE_NO_ISSUES;
const additionalPath = activeLayout ?? "list";
const isLightMode = resolvedTheme ? resolvedTheme === "light" : currentUser?.theme.theme === "light"; const emptyStateSize = isEmptyFilters ? "lg" : "sm";
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: <PlusIcon className="h-3 w-3" strokeWidth={2} />,
onClick: () => {
setTrackElement("Cycle issue empty state");
toggleCreateIssueModal(true, EIssuesStoreType.CYCLE);
},
},
secondaryButton: {
text: emptyStateDetail.secondaryButton.text,
icon: <PlusIcon className="h-3 w-3" strokeWidth={2} />,
onClick: () => setCycleIssuesListModal(true),
},
size: "sm",
disabled: !isEditingAllowed,
};
return ( return (
<> <>
@ -126,7 +71,20 @@ export const CycleEmptyState: React.FC<Props> = observer((props) => {
handleOnSubmit={handleAddIssuesToCycle} handleOnSubmit={handleAddIssuesToCycle}
/> />
<div className="grid h-full w-full place-items-center"> <div className="grid h-full w-full place-items-center">
<EmptyState {...emptyStateProps} /> <EmptyState
type={emptyStateType}
additionalPath={additionalPath}
size={emptyStateSize}
primaryButtonOnClick={
isEmptyFilters
? undefined
: () => {
setTrackElement("Cycle issue empty state");
toggleCreateIssueModal(true, EIssuesStoreType.CYCLE);
}
}
secondaryButtonOnClick={isEmptyFilters ? handleClearAllFilters : () => setCycleIssuesListModal(true)}
/>
</div> </div>
</> </>
); );

View File

@ -1,49 +1,26 @@
import size from "lodash/size"; import size from "lodash/size";
import { observer } from "mobx-react-lite"; import { observer } from "mobx-react-lite";
import { useRouter } from "next/router"; import { useRouter } from "next/router";
import { useTheme } from "next-themes";
// hooks // hooks
import { EmptyState, getEmptyStateImagePath } from "components/empty-state"; import { useIssues } from "hooks/store";
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";
// components // components
import { EmptyState } from "components/empty-state";
// constants // constants
import { EIssueFilterType, EIssuesStoreType } from "constants/issue";
import { EmptyStateType } from "constants/empty-state";
// types // types
import { IIssueFilterOptions } from "@plane/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(() => { export const ProjectDraftEmptyState: React.FC = observer(() => {
// router // router
const router = useRouter(); const router = useRouter();
const { workspaceSlug, projectId } = router.query; const { workspaceSlug, projectId } = router.query;
// theme
const { resolvedTheme } = useTheme();
// store hooks // store hooks
const {
membership: { currentProjectRole },
currentUser,
} = useUser();
const { issuesFilter } = useIssues(EIssuesStoreType.DRAFT); const { issuesFilter } = useIssues(EIssuesStoreType.DRAFT);
const userFilters = issuesFilter?.issueFilters?.filters; const userFilters = issuesFilter?.issueFilters?.filters;
const activeLayout = issuesFilter?.issueFilters?.displayFilters?.layout; 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( const issueFilterCount = size(
Object.fromEntries( Object.fromEntries(
Object.entries(userFilters ?? {}).filter(([, value]) => value && Array.isArray(value) && value.length > 0) 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 emptyStateType =
issueFilterCount > 0 ? EmptyStateType.PROJECT_DRAFT_EMPTY_FILTER : EmptyStateType.PROJECT_DRAFT_NO_ISSUES;
const emptyStateProps: EmptyStateProps = const additionalPath = issueFilterCount > 0 ? activeLayout ?? "list" : undefined;
issueFilterCount > 0 const emptyStateSize = issueFilterCount > 0 ? "lg" : "sm";
? {
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,
};
return ( return (
<div className="relative h-full w-full overflow-y-auto"> <div className="relative h-full w-full overflow-y-auto">
<EmptyState {...emptyStateProps} /> <EmptyState
type={emptyStateType}
additionalPath={additionalPath}
size={emptyStateSize}
secondaryButtonOnClick={issueFilterCount > 0 ? handleClearAllFilters : undefined}
/>
</div> </div>
); );
}); });

View File

@ -1,20 +1,18 @@
import { useState } from "react"; import { useState } from "react";
import { observer } from "mobx-react-lite"; import { observer } from "mobx-react-lite";
import { useTheme } from "next-themes";
import { PlusIcon } from "lucide-react";
// hooks // hooks
import { useApplication, useEventTracker, useIssues } from "hooks/store";
// ui
import { TOAST_TYPE, setToast } from "@plane/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 // ui
// components // components
import { ExistingIssuesListModal } from "components/core";
import { EmptyState } from "components/empty-state";
// types // types
import { ISearchIssueResponse, TIssueLayouts } from "@plane/types"; import { ISearchIssueResponse, TIssueLayouts } from "@plane/types";
// constants // constants
import { EIssuesStoreType } from "constants/issue";
import { EmptyStateType } from "constants/empty-state";
type Props = { type Props = {
workspaceSlug: string | undefined; workspaceSlug: string | undefined;
@ -25,33 +23,16 @@ type Props = {
isEmptyFilters?: boolean; 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<Props> = observer((props) => { export const ModuleEmptyState: React.FC<Props> = observer((props) => {
const { workspaceSlug, projectId, moduleId, activeLayout, handleClearAllFilters, isEmptyFilters = false } = props; const { workspaceSlug, projectId, moduleId, activeLayout, handleClearAllFilters, isEmptyFilters = false } = props;
// states // states
const [moduleIssuesListModal, setModuleIssuesListModal] = useState(false); const [moduleIssuesListModal, setModuleIssuesListModal] = useState(false);
// theme
const { resolvedTheme } = useTheme();
// store hooks // store hooks
const { issues } = useIssues(EIssuesStoreType.MODULE); const { issues } = useIssues(EIssuesStoreType.MODULE);
const { const {
commandPalette: { toggleCreateIssueModal }, commandPalette: { toggleCreateIssueModal },
} = useApplication(); } = useApplication();
const { setTrackElement } = useEventTracker(); const { setTrackElement } = useEventTracker();
const {
membership: { currentProjectRole: userRole },
currentUser,
} = useUser();
const handleAddIssuesToModule = async (data: ISearchIssueResponse[]) => { const handleAddIssuesToModule = async (data: ISearchIssueResponse[]) => {
if (!workspaceSlug || !projectId || !moduleId) return; if (!workspaceSlug || !projectId || !moduleId) return;
@ -75,42 +56,8 @@ export const ModuleEmptyState: React.FC<Props> = observer((props) => {
); );
}; };
const emptyStateDetail = MODULE_EMPTY_STATE_DETAILS["no-issues"]; const emptyStateType = isEmptyFilters ? EmptyStateType.PROJECT_EMPTY_FILTER : EmptyStateType.PROJECT_MODULE_ISSUES;
const additionalPath = activeLayout ?? "list";
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: <PlusIcon className="h-3 w-3" strokeWidth={2} />,
onClick: () => {
setTrackElement("Module issue empty state");
toggleCreateIssueModal(true, EIssuesStoreType.MODULE);
},
},
secondaryButton: {
text: emptyStateDetail.secondaryButton.text,
icon: <PlusIcon className="h-3 w-3" strokeWidth={2} />,
onClick: () => setModuleIssuesListModal(true),
},
disabled: !isEditingAllowed,
};
return ( return (
<> <>
@ -123,7 +70,19 @@ export const ModuleEmptyState: React.FC<Props> = observer((props) => {
handleOnSubmit={handleAddIssuesToModule} handleOnSubmit={handleAddIssuesToModule}
/> />
<div className="grid h-full w-full place-items-center"> <div className="grid h-full w-full place-items-center">
<EmptyState {...emptyStateProps} /> <EmptyState
type={emptyStateType}
additionalPath={additionalPath}
primaryButtonOnClick={
isEmptyFilters
? undefined
: () => {
setTrackElement("Cycle issue empty state");
toggleCreateIssueModal(true, EIssuesStoreType.CYCLE);
}
}
secondaryButtonOnClick={isEmptyFilters ? handleClearAllFilters : () => setModuleIssuesListModal(true)}
/>
</div> </div>
</> </>
); );

View File

@ -1,51 +1,29 @@
import size from "lodash/size"; import size from "lodash/size";
import { observer } from "mobx-react-lite"; import { observer } from "mobx-react-lite";
import { useRouter } from "next/router"; import { useRouter } from "next/router";
import { useTheme } from "next-themes";
// hooks // hooks
import { EmptyState, getEmptyStateImagePath } from "components/empty-state"; import { useApplication, useEventTracker, useIssues } from "hooks/store";
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";
// components // components
import { EmptyState } from "components/empty-state";
// constants // constants
import { EIssueFilterType, EIssuesStoreType } from "constants/issue";
import { EmptyStateType } from "constants/empty-state";
// types // types
import { IIssueFilterOptions } from "@plane/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(() => { export const ProjectEmptyState: React.FC = observer(() => {
// router // router
const router = useRouter(); const router = useRouter();
const { workspaceSlug, projectId } = router.query; const { workspaceSlug, projectId } = router.query;
// theme
const { resolvedTheme } = useTheme();
// store hooks // store hooks
const { commandPalette: commandPaletteStore } = useApplication(); const { commandPalette: commandPaletteStore } = useApplication();
const { setTrackElement } = useEventTracker(); const { setTrackElement } = useEventTracker();
const {
membership: { currentProjectRole },
currentUser,
} = useUser();
const { issuesFilter } = useIssues(EIssuesStoreType.PROJECT); const { issuesFilter } = useIssues(EIssuesStoreType.PROJECT);
const userFilters = issuesFilter?.issueFilters?.filters; const userFilters = issuesFilter?.issueFilters?.filters;
const activeLayout = issuesFilter?.issueFilters?.displayFilters?.layout; 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( const issueFilterCount = size(
Object.fromEntries( Object.fromEntries(
Object.entries(userFilters ?? {}).filter(([, value]) => value && Array.isArray(value) && value.length > 0) 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 emptyStateType = issueFilterCount > 0 ? EmptyStateType.PROJECT_EMPTY_FILTER : EmptyStateType.PROJECT_NO_ISSUES;
const additionalPath = issueFilterCount > 0 ? activeLayout ?? "list" : undefined;
const emptyStateProps: EmptyStateProps = const emptyStateSize = issueFilterCount > 0 ? "lg" : "sm";
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,
};
return ( return (
<div className="relative h-full w-full overflow-y-auto"> <div className="relative h-full w-full overflow-y-auto">
<EmptyState {...emptyStateProps} /> <EmptyState
type={emptyStateType}
additionalPath={additionalPath}
size={emptyStateSize}
primaryButtonOnClick={
issueFilterCount > 0
? undefined
: () => {
setTrackElement("Project issue empty state");
commandPaletteStore.toggleCreateIssueModal(true, EIssuesStoreType.PROJECT);
}
}
secondaryButtonOnClick={issueFilterCount > 0 ? handleClearAllFilters : undefined}
/>
</div> </div>
); );
}); });

View File

@ -2,32 +2,28 @@ import React, { Fragment, useCallback, useMemo } from "react";
import isEmpty from "lodash/isEmpty"; import isEmpty from "lodash/isEmpty";
import { observer } from "mobx-react-lite"; import { observer } from "mobx-react-lite";
import { useRouter } from "next/router"; import { useRouter } from "next/router";
import { useTheme } from "next-themes";
import useSWR from "swr"; import useSWR from "swr";
// hooks // 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 { GlobalViewsAppliedFiltersRoot, IssuePeekOverview } from "components/issues";
import { SpreadsheetView } from "components/issues/issue-layouts"; import { SpreadsheetView } from "components/issues/issue-layouts";
import { AllIssueQuickActions } from "components/issues/issue-layouts/quick-action-dropdowns"; import { AllIssueQuickActions } from "components/issues/issue-layouts/quick-action-dropdowns";
import { EmptyState } from "components/empty-state";
import { SpreadsheetLayoutLoader } from "components/ui"; 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 // types
import { TIssue, IIssueDisplayFilterOptions } from "@plane/types"; import { TIssue, IIssueDisplayFilterOptions } from "@plane/types";
import { EIssueActions } from "../types"; import { EIssueActions } from "../types";
// constants // 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(() => { export const AllIssueLayoutRoot: React.FC = observer(() => {
// router // router
const router = useRouter(); const router = useRouter();
const { workspaceSlug, globalViewId, ...routeFilters } = router.query; const { workspaceSlug, globalViewId, ...routeFilters } = router.query;
// theme
const { resolvedTheme } = useTheme();
//swr hook for fetching issue properties //swr hook for fetching issue properties
useWorkspaceIssueProperties(workspaceSlug); useWorkspaceIssueProperties(workspaceSlug);
// store // store
@ -39,8 +35,7 @@ export const AllIssueLayoutRoot: React.FC = observer(() => {
const { dataViewId, issueIds } = groupedIssueIds; const { dataViewId, issueIds } = groupedIssueIds;
const { const {
membership: { currentWorkspaceAllProjectsRole, currentWorkspaceRole }, membership: { currentWorkspaceAllProjectsRole },
currentUser,
} = useUser(); } = useUser();
const { fetchAllGlobalViews } = useGlobalView(); const { fetchAllGlobalViews } = useGlobalView();
const { workspaceProjectIds } = useProject(); const { workspaceProjectIds } = useProject();
@ -48,10 +43,6 @@ export const AllIssueLayoutRoot: React.FC = observer(() => {
const isDefaultView = ["all-issues", "assigned", "created", "subscribed"].includes(groupedIssueIds.dataViewId); const isDefaultView = ["all-issues", "assigned", "created", "subscribed"].includes(groupedIssueIds.dataViewId);
const currentView = isDefaultView ? groupedIssueIds.dataViewId : "custom-view"; 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 // filter init from the query params
@ -185,46 +176,34 @@ export const AllIssueLayoutRoot: React.FC = observer(() => {
[canEditProperties, handleIssues] [canEditProperties, handleIssues]
); );
const isEditingAllowed = !!currentWorkspaceRole && currentWorkspaceRole >= EUserWorkspaceRoles.MEMBER;
if (loader === "init-loader" || !globalViewId || globalViewId !== dataViewId || !issueIds) { if (loader === "init-loader" || !globalViewId || globalViewId !== dataViewId || !issueIds) {
return <SpreadsheetLayoutLoader />; return <SpreadsheetLayoutLoader />;
} }
const emptyStateType =
(workspaceProjectIds ?? []).length > 0 ? `workspace-${currentView}` : EmptyStateType.WORKSPACE_NO_PROJECTS;
return ( return (
<div className="relative flex h-full w-full flex-col overflow-hidden"> <div className="relative flex h-full w-full flex-col overflow-hidden">
<div className="relative h-full w-full flex flex-col"> <div className="relative h-full w-full flex flex-col">
<GlobalViewsAppliedFiltersRoot globalViewId={globalViewId} /> <GlobalViewsAppliedFiltersRoot globalViewId={globalViewId} />
{issueIds.length === 0 ? ( {issueIds.length === 0 ? (
<EmptyState <EmptyState
image={emptyStateImage} type={emptyStateType as keyof typeof EMPTY_STATE_DETAILS}
title={(workspaceProjectIds ?? []).length > 0 ? currentViewDetails.title : "No project"}
description={
(workspaceProjectIds ?? []).length > 0
? currentViewDetails.description
: "To create issues or manage your work, you need to create a project or be a part of one."
}
size="sm" size="sm"
primaryButton={ primaryButtonOnClick={
(workspaceProjectIds ?? []).length > 0 (workspaceProjectIds ?? []).length > 0
? currentView !== "custom-view" && currentView !== "subscribed" ? currentView !== "custom-view" && currentView !== "subscribed"
? { ? () => {
text: "Create new issue", setTrackElement("All issues empty state");
onClick: () => { commandPaletteStore.toggleCreateIssueModal(true, EIssuesStoreType.PROJECT);
setTrackElement("All issues empty state");
commandPaletteStore.toggleCreateIssueModal(true, EIssuesStoreType.PROJECT);
},
} }
: undefined : undefined
: { : () => {
text: "Start your first project", setTrackElement("All issues empty state");
onClick: () => { commandPaletteStore.toggleCreateProjectModal(true);
setTrackElement("All issues empty state");
commandPaletteStore.toggleCreateProjectModal(true);
},
} }
} }
disabled={!isEditingAllowed}
/> />
) : ( ) : (
<Fragment> <Fragment>

View File

@ -1,4 +1,6 @@
import React, { useState, useRef } from "react"; import React, { useState, useRef } from "react";
import { useRouter } from "next/router";
import { observer } from "mobx-react";
import { import {
DragDropContext, DragDropContext,
Draggable, Draggable,
@ -7,26 +9,23 @@ import {
DropResult, DropResult,
Droppable, Droppable,
} from "@hello-pangea/dnd"; } from "@hello-pangea/dnd";
import { observer } from "mobx-react-lite";
import { useRouter } from "next/router";
import { useTheme } from "next-themes";
// hooks // hooks
import { Button, Loader } from "@plane/ui"; import { useLabel } from "hooks/store";
import { EmptyState, getEmptyStateImagePath } from "components/empty-state"; import useDraggableInPortal from "hooks/use-draggable-portal";
// components
import { import {
CreateUpdateLabelInline, CreateUpdateLabelInline,
DeleteLabelModal, DeleteLabelModal,
ProjectSettingLabelGroup, ProjectSettingLabelGroup,
ProjectSettingLabelItem, ProjectSettingLabelItem,
} from "components/labels"; } from "components/labels";
import { PROJECT_SETTINGS_EMPTY_STATE_DETAILS } from "constants/empty-state"; import { EmptyState } from "components/empty-state";
import { useLabel, useUser } from "hooks/store";
import useDraggableInPortal from "hooks/use-draggable-portal";
// components
// ui // ui
import { Button, Loader } from "@plane/ui";
// types // types
import { IIssueLabel } from "@plane/types"; import { IIssueLabel } from "@plane/types";
// constants // constants
import { EmptyStateType } from "constants/empty-state";
const LABELS_ROOT = "labels.root"; const LABELS_ROOT = "labels.root";
@ -41,10 +40,7 @@ export const ProjectSettingsLabelList: React.FC = observer(() => {
// router // router
const router = useRouter(); const router = useRouter();
const { workspaceSlug, projectId } = router.query; const { workspaceSlug, projectId } = router.query;
// theme
const { resolvedTheme } = useTheme();
// store hooks // store hooks
const { currentUser } = useUser();
const { projectLabels, updateLabelPosition, projectLabelsTree } = useLabel(); const { projectLabels, updateLabelPosition, projectLabelsTree } = useLabel();
// portal // portal
const renderDraggable = useDraggableInPortal(); const renderDraggable = useDraggableInPortal();
@ -54,10 +50,6 @@ export const ProjectSettingsLabelList: React.FC = observer(() => {
setLabelForm(true); 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 onDragEnd = (result: DropResult) => {
const { combine, draggableId, destination, source } = result; const { combine, draggableId, destination, source } = result;
@ -121,13 +113,8 @@ export const ProjectSettingsLabelList: React.FC = observer(() => {
)} )}
{projectLabels ? ( {projectLabels ? (
projectLabels.length === 0 && !showLabelForm ? ( projectLabels.length === 0 && !showLabelForm ? (
<div className="flex h-full w-full items-center justify-center"> <div className="flex items-center justify-center h-full w-full">
<EmptyState <EmptyState type={EmptyStateType.PROJECT_SETTINGS_LABELS} />
title={emptyStateDetail.title}
description={emptyStateDetail.description}
image={emptyStateImage}
size="lg"
/>
</div> </div>
) : ( ) : (
projectLabelsTree && ( projectLabelsTree && (

View File

@ -1,40 +1,28 @@
import { observer } from "mobx-react-lite"; import { observer } from "mobx-react-lite";
import { useRouter } from "next/router"; import { useRouter } from "next/router";
import { useTheme } from "next-themes";
// hooks // hooks
import { useApplication, useEventTracker, useModule } from "hooks/store";
import useLocalStorage from "hooks/use-local-storage";
// components // components
import { EmptyState, getEmptyStateImagePath } from "components/empty-state";
import { ModuleCardItem, ModuleListItem, ModulePeekOverview, ModulesListGanttChartView } from "components/modules"; import { ModuleCardItem, ModuleListItem, ModulePeekOverview, ModulesListGanttChartView } from "components/modules";
import { EmptyState } from "components/empty-state";
// ui // ui
import { CycleModuleBoardLayout, CycleModuleListLayout, GanttLayoutLoader } from "components/ui"; import { CycleModuleBoardLayout, CycleModuleListLayout, GanttLayoutLoader } from "components/ui";
// constants // constants
import { MODULE_EMPTY_STATE_DETAILS } from "constants/empty-state"; import { EmptyStateType } from "constants/empty-state";
import { EUserProjectRoles } from "constants/project";
import { useApplication, useEventTracker, useModule, useUser } from "hooks/store";
import useLocalStorage from "hooks/use-local-storage";
export const ModulesListView: React.FC = observer(() => { export const ModulesListView: React.FC = observer(() => {
// router // router
const router = useRouter(); const router = useRouter();
const { workspaceSlug, projectId, peekModule } = router.query; const { workspaceSlug, projectId, peekModule } = router.query;
// theme
const { resolvedTheme } = useTheme();
// store hooks // store hooks
const { commandPalette: commandPaletteStore } = useApplication(); const { commandPalette: commandPaletteStore } = useApplication();
const { setTrackElement } = useEventTracker(); const { setTrackElement } = useEventTracker();
const {
membership: { currentProjectRole },
currentUser,
} = useUser();
const { projectModuleIds, loader } = useModule(); const { projectModuleIds, loader } = useModule();
const { storedValue: modulesView } = useLocalStorage("modules_view", "grid"); 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) if (loader || !projectModuleIds)
return ( return (
<> <>
@ -88,22 +76,11 @@ export const ModulesListView: React.FC = observer(() => {
</> </>
) : ( ) : (
<EmptyState <EmptyState
title={MODULE_EMPTY_STATE_DETAILS["modules"].title} type={EmptyStateType.PROJECT_MODULE}
description={MODULE_EMPTY_STATE_DETAILS["modules"].description} primaryButtonOnClick={() => {
image={EmptyStateImagePath} setTrackElement("Module empty state");
comicBox={{ commandPaletteStore.toggleCreateModuleModal(true);
title: MODULE_EMPTY_STATE_DETAILS["modules"].comicBox.title,
description: MODULE_EMPTY_STATE_DETAILS["modules"].comicBox.description,
}} }}
primaryButton={{
text: MODULE_EMPTY_STATE_DETAILS["modules"].primaryButton.text,
onClick: () => {
setTrackElement("Module empty state");
commandPaletteStore.toggleCreateModuleModal(true);
},
}}
size="lg"
disabled={!isEditingAllowed}
/> />
)} )}
</> </>

View File

@ -1,41 +1,30 @@
import { useEffect } from "react"; import { useEffect } from "react";
import { observer } from "mobx-react-lite"; import { observer } from "mobx-react-lite";
import { useTheme } from "next-themes";
// hooks // hooks
import { useApplication, useEventTracker, useDashboard, useProject, useUser } from "hooks/store";
// components // components
import { Spinner } from "@plane/ui";
import { DashboardWidgets } from "components/dashboard"; import { DashboardWidgets } from "components/dashboard";
import { EmptyState, getEmptyStateImagePath } from "components/empty-state"; import { EmptyState } from "components/empty-state";
import { IssuePeekOverview } from "components/issues"; import { IssuePeekOverview } from "components/issues";
import { TourRoot } from "components/onboarding"; import { TourRoot } from "components/onboarding";
import { UserGreetingsView } from "components/user"; import { UserGreetingsView } from "components/user";
// ui // ui
import { Spinner } from "@plane/ui";
// constants // constants
import { WORKSPACE_EMPTY_STATE_DETAILS } from "constants/empty-state";
import { PRODUCT_TOUR_COMPLETED } from "constants/event-tracker"; import { PRODUCT_TOUR_COMPLETED } from "constants/event-tracker";
import { EUserWorkspaceRoles } from "constants/workspace"; import { EmptyStateType } from "constants/empty-state";
import { useApplication, useEventTracker, useDashboard, useProject, useUser } from "hooks/store";
export const WorkspaceDashboardView = observer(() => { export const WorkspaceDashboardView = observer(() => {
// theme
const { resolvedTheme } = useTheme();
// store hooks // store hooks
const { captureEvent, setTrackElement } = useEventTracker(); const { captureEvent, setTrackElement } = useEventTracker();
const { const {
commandPalette: { toggleCreateProjectModal }, commandPalette: { toggleCreateProjectModal },
router: { workspaceSlug }, router: { workspaceSlug },
} = useApplication(); } = useApplication();
const { const { currentUser, updateTourCompleted } = useUser();
currentUser,
updateTourCompleted,
membership: { currentWorkspaceRole },
} = useUser();
const { homeDashboardId, fetchHomeDashboardWidgets } = useDashboard(); const { homeDashboardId, fetchHomeDashboardWidgets } = useDashboard();
const { joinedProjectIds } = useProject(); const { joinedProjectIds } = useProject();
const isLightMode = resolvedTheme ? resolvedTheme === "light" : currentUser?.theme.theme === "light";
const emptyStateImage = getEmptyStateImagePath("onboarding", "dashboard", isLightMode);
const handleTourCompleted = () => { const handleTourCompleted = () => {
updateTourCompleted() updateTourCompleted()
.then(() => { .then(() => {
@ -56,8 +45,6 @@ export const WorkspaceDashboardView = observer(() => {
fetchHomeDashboardWidgets(workspaceSlug); fetchHomeDashboardWidgets(workspaceSlug);
}, [fetchHomeDashboardWidgets, workspaceSlug]); }, [fetchHomeDashboardWidgets, workspaceSlug]);
const isEditingAllowed = currentWorkspaceRole === EUserWorkspaceRoles.ADMIN;
return ( return (
<> <>
{currentUser && !currentUser.is_tour_completed && ( {currentUser && !currentUser.is_tour_completed && (
@ -78,22 +65,11 @@ export const WorkspaceDashboardView = observer(() => {
</> </>
) : ( ) : (
<EmptyState <EmptyState
image={emptyStateImage} type={EmptyStateType.WORKSPACE_DASHBOARD}
title={WORKSPACE_EMPTY_STATE_DETAILS["dashboard"].title} primaryButtonOnClick={() => {
description={WORKSPACE_EMPTY_STATE_DETAILS["dashboard"].description} setTrackElement("Dashboard empty state");
primaryButton={{ toggleCreateProjectModal(true);
text: WORKSPACE_EMPTY_STATE_DETAILS["dashboard"].primaryButton.text,
onClick: () => {
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}
/> />
)} )}
</> </>

View File

@ -1,17 +1,15 @@
import { FC } from "react"; import { FC } from "react";
import { useRouter } from "next/router"; import { useRouter } from "next/router";
import { useTheme } from "next-themes";
// hooks // hooks
import { Loader } from "@plane/ui"; import { useApplication } from "hooks/store";
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 useLocalStorage from "hooks/use-local-storage"; import useLocalStorage from "hooks/use-local-storage";
// components // components
import { EmptyState } from "components/empty-state";
import { PagesListItem } from "./list-item"; import { PagesListItem } from "./list-item";
// ui // ui
import { Loader } from "@plane/ui";
// constants // constants
import { EMPTY_STATE_DETAILS, EmptyStateType } from "constants/empty-state";
type IPagesListView = { type IPagesListView = {
pageIds: string[]; pageIds: string[];
@ -19,34 +17,20 @@ type IPagesListView = {
export const PagesListView: FC<IPagesListView> = (props) => { export const PagesListView: FC<IPagesListView> = (props) => {
const { pageIds: projectPageIds } = props; const { pageIds: projectPageIds } = props;
// theme
const { resolvedTheme } = useTheme();
// store hooks // store hooks
const { const {
commandPalette: { toggleCreatePageModal }, commandPalette: { toggleCreatePageModal },
} = useApplication(); } = useApplication();
const {
membership: { currentProjectRole },
currentUser,
} = useUser();
// local storage // local storage
const { storedValue: pageTab } = useLocalStorage("pageTab", "Recent"); const { storedValue: pageTab } = useLocalStorage("pageTab", "Recent");
// router // router
const router = useRouter(); const router = useRouter();
const { workspaceSlug, projectId } = router.query; 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 // 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 ( return (
<> <>
@ -60,18 +44,8 @@ export const PagesListView: FC<IPagesListView> = (props) => {
</ul> </ul>
) : ( ) : (
<EmptyState <EmptyState
title={currentPageTabDetails.title} type={emptyStateType as keyof typeof EMPTY_STATE_DETAILS}
description={currentPageTabDetails.description} primaryButtonOnClick={isButtonVisible ? () => toggleCreatePageModal(true) : undefined}
image={emptyStateImage}
primaryButton={
isButtonVisible
? {
text: "Create new page",
onClick: () => toggleCreatePageModal(true),
}
: undefined
}
disabled={!isEditingAllowed}
/> />
)} )}
</div> </div>

View File

@ -1,39 +1,27 @@
import React, { FC } from "react"; import React, { FC } from "react";
import { observer } from "mobx-react-lite"; import { observer } from "mobx-react-lite";
import { useTheme } from "next-themes";
// hooks // hooks
import { Loader } from "@plane/ui"; import { useApplication } from "hooks/store";
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 { useProjectPages } from "hooks/store/use-project-specific-pages"; import { useProjectPages } from "hooks/store/use-project-specific-pages";
// components // components
import { PagesListView } from "components/pages/pages-list";
import { EmptyState } from "components/empty-state";
// ui // ui
import { Loader } from "@plane/ui";
// helpers // helpers
import { replaceUnderscoreIfSnakeCase } from "helpers/string.helper";
// constants // constants
import { EmptyStateType } from "constants/empty-state";
export const RecentPagesList: FC = observer(() => { export const RecentPagesList: FC = observer(() => {
// theme
const { resolvedTheme } = useTheme();
// store hooks // store hooks
const { commandPalette: commandPaletteStore } = useApplication(); const { commandPalette: commandPaletteStore } = useApplication();
const {
membership: { currentProjectRole },
currentUser,
} = useUser();
const { recentProjectPages } = useProjectPages();
const isLightMode = resolvedTheme ? resolvedTheme === "light" : currentUser?.theme.theme === "light"; const { recentProjectPages } = useProjectPages();
const EmptyStateImagePath = getEmptyStateImagePath("pages", "recent", isLightMode);
// FIXME: replace any with proper type // FIXME: replace any with proper type
const isEmpty = recentProjectPages && Object.values(recentProjectPages).every((value: any) => value.length === 0); const isEmpty = recentProjectPages && Object.values(recentProjectPages).every((value: any) => value.length === 0);
const isEditingAllowed = !!currentProjectRole && currentProjectRole >= EUserProjectRoles.MEMBER;
if (!recentProjectPages) { if (!recentProjectPages) {
return ( return (
<Loader className="space-y-4"> <Loader className="space-y-4">
@ -64,15 +52,9 @@ export const RecentPagesList: FC = observer(() => {
) : ( ) : (
<> <>
<EmptyState <EmptyState
title={PAGE_EMPTY_STATE_DETAILS["Recent"].title} type={EmptyStateType.PROJECT_PAGE_RECENT}
description={PAGE_EMPTY_STATE_DETAILS["Recent"].description} primaryButtonOnClick={() => commandPaletteStore.toggleCreatePageModal(true)}
image={EmptyStateImagePath}
primaryButton={{
text: PAGE_EMPTY_STATE_DETAILS["Recent"].primaryButton.text,
onClick: () => commandPaletteStore.toggleCreatePageModal(true),
}}
size="sm" size="sm"
disabled={!isEditingAllowed}
/> />
</> </>
)} )}

View File

@ -1,20 +1,18 @@
import React, { useEffect } from "react"; import React, { useEffect } from "react";
import { observer } from "mobx-react-lite"; import { observer } from "mobx-react-lite";
import { useRouter } from "next/router"; import { useRouter } from "next/router";
import { useTheme } from "next-themes";
import useSWR from "swr"; import useSWR from "swr";
// components // components
import { EmptyState, getEmptyStateImagePath } from "components/empty-state";
import { IssuePeekOverview, ProfileIssuesAppliedFiltersRoot } from "components/issues"; import { IssuePeekOverview, ProfileIssuesAppliedFiltersRoot } from "components/issues";
import { ProfileIssuesKanBanLayout } from "components/issues/issue-layouts/kanban/roots/profile-issues-root"; 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 { ProfileIssuesListLayout } from "components/issues/issue-layouts/list/roots/profile-issues-root";
import { KanbanLayoutLoader, ListLayoutLoader } from "components/ui"; import { KanbanLayoutLoader, ListLayoutLoader } from "components/ui";
import { EmptyState } from "components/empty-state";
// hooks // hooks
import { PROFILE_EMPTY_STATE_DETAILS } from "constants/empty-state"; import { useIssues } from "hooks/store";
import { EIssuesStoreType } from "constants/issue";
import { EUserWorkspaceRoles } from "constants/workspace";
import { useIssues, useUser } from "hooks/store";
// constants // constants
import { EIssuesStoreType } from "constants/issue";
import { EMPTY_STATE_DETAILS } from "constants/empty-state";
interface IProfileIssuesPage { interface IProfileIssuesPage {
type: "assigned" | "subscribed" | "created"; type: "assigned" | "subscribed" | "created";
@ -28,13 +26,7 @@ export const ProfileIssuesPage = observer((props: IProfileIssuesPage) => {
workspaceSlug: string; workspaceSlug: string;
userId: string; userId: string;
}; };
// theme
const { resolvedTheme } = useTheme();
// store hooks // store hooks
const {
membership: { currentWorkspaceRole },
currentUser,
} = useUser();
const { const {
issues: { loader, groupedIssueIds, fetchIssues, setViewId }, issues: { loader, groupedIssueIds, fetchIssues, setViewId },
issuesFilter: { issueFilters, fetchFilters }, issuesFilter: { issueFilters, fetchFilters },
@ -55,26 +47,15 @@ export const ProfileIssuesPage = observer((props: IProfileIssuesPage) => {
{ revalidateIfStale: false, revalidateOnFocus: false } { 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 activeLayout = issueFilters?.displayFilters?.layout || undefined;
const isEditingAllowed = !!currentWorkspaceRole && currentWorkspaceRole >= EUserWorkspaceRoles.MEMBER; const emptyStateType = `profile-${type}`;
if (!groupedIssueIds || loader === "init-loader") if (!groupedIssueIds || loader === "init-loader")
return <>{activeLayout === "list" ? <ListLayoutLoader /> : <KanbanLayoutLoader />}</>; return <>{activeLayout === "list" ? <ListLayoutLoader /> : <KanbanLayoutLoader />}</>;
if (groupedIssueIds.length === 0) { if (groupedIssueIds.length === 0) {
return ( return <EmptyState type={emptyStateType as keyof typeof EMPTY_STATE_DETAILS} size="sm" />;
<EmptyState
image={emptyStateImage}
title={PROFILE_EMPTY_STATE_DETAILS[type].title}
description={PROFILE_EMPTY_STATE_DETAILS[type].description}
size="sm"
disabled={!isEditingAllowed}
/>
);
} }
return ( return (

View File

@ -1,32 +1,20 @@
import { observer } from "mobx-react-lite"; import { observer } from "mobx-react-lite";
import { useTheme } from "next-themes";
// hooks // hooks
import { useApplication, useEventTracker, useProject } from "hooks/store";
// components // components
import { EmptyState, getEmptyStateImagePath } from "components/empty-state"; import { EmptyState } from "components/empty-state";
import { ProjectCard } from "components/project"; import { ProjectCard } from "components/project";
import { ProjectsLoader } from "components/ui"; import { ProjectsLoader } from "components/ui";
// constants // constants
import { WORKSPACE_EMPTY_STATE_DETAILS } from "constants/empty-state"; import { EmptyStateType } from "constants/empty-state";
import { EUserWorkspaceRoles } from "constants/workspace";
import { useApplication, useEventTracker, useProject, useUser } from "hooks/store";
export const ProjectCardList = observer(() => { export const ProjectCardList = observer(() => {
// theme
const { resolvedTheme } = useTheme();
// store hooks // store hooks
const { commandPalette: commandPaletteStore } = useApplication(); const { commandPalette: commandPaletteStore } = useApplication();
const { setTrackElement } = useEventTracker(); const { setTrackElement } = useEventTracker();
const {
membership: { currentWorkspaceRole },
currentUser,
} = useUser();
const { workspaceProjectIds, searchedProjects, getProjectById } = useProject(); 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 <ProjectsLoader />; if (!workspaceProjectIds) return <ProjectsLoader />;
return ( return (
@ -49,22 +37,11 @@ export const ProjectCardList = observer(() => {
</div> </div>
) : ( ) : (
<EmptyState <EmptyState
image={emptyStateImage} type={EmptyStateType.WORKSPACE_PROJECTS}
title={WORKSPACE_EMPTY_STATE_DETAILS["projects"].title} primaryButtonOnClick={() => {
description={WORKSPACE_EMPTY_STATE_DETAILS["projects"].description} setTrackElement("Project empty state");
primaryButton={{ commandPaletteStore.toggleCreateProjectModal(true);
text: WORKSPACE_EMPTY_STATE_DETAILS["projects"].primaryButton.text,
onClick: () => {
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}
/> />
)} )}
</> </>

View File

@ -1,45 +1,32 @@
import { useState } from "react"; import { useState } from "react";
import { observer } from "mobx-react-lite"; import { observer } from "mobx-react-lite";
import { useTheme } from "next-themes";
import { Search } from "lucide-react"; import { Search } from "lucide-react";
// hooks // hooks
import { useApplication, useProjectView } from "hooks/store";
// components // components
import { Input } from "@plane/ui"; import { EmptyState } from "components/empty-state";
import { EmptyState, getEmptyStateImagePath } from "components/empty-state";
// ui
import { ViewListLoader } from "components/ui"; import { ViewListLoader } from "components/ui";
import { ProjectViewListItem } from "components/views"; import { ProjectViewListItem } from "components/views";
// ui
import { Input } from "@plane/ui";
// constants // constants
import { VIEW_EMPTY_STATE_DETAILS } from "constants/empty-state"; import { EmptyStateType } from "constants/empty-state";
import { EUserProjectRoles } from "constants/project";
import { useApplication, useProjectView, useUser } from "hooks/store";
export const ProjectViewsList = observer(() => { export const ProjectViewsList = observer(() => {
// states // states
const [query, setQuery] = useState(""); const [query, setQuery] = useState("");
// theme
const { resolvedTheme } = useTheme();
// store hooks // store hooks
const { const {
commandPalette: { toggleCreateViewModal }, commandPalette: { toggleCreateViewModal },
} = useApplication(); } = useApplication();
const {
membership: { currentProjectRole },
currentUser,
} = useUser();
const { projectViewIds, getViewById, loader } = useProjectView(); const { projectViewIds, getViewById, loader } = useProjectView();
if (loader || !projectViewIds) return <ViewListLoader />; if (loader || !projectViewIds) return <ViewListLoader />;
const viewsList = projectViewIds.map((viewId) => getViewById(viewId)); 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 filteredViewsList = viewsList.filter((v) => v?.name.toLowerCase().includes(query.toLowerCase()));
const isEditingAllowed = !!currentProjectRole && currentProjectRole >= EUserProjectRoles.MEMBER;
return ( return (
<> <>
{viewsList.length > 0 ? ( {viewsList.length > 0 ? (
@ -65,21 +52,7 @@ export const ProjectViewsList = observer(() => {
</div> </div>
</div> </div>
) : ( ) : (
<EmptyState <EmptyState type={EmptyStateType.PROJECT_VIEW} primaryButtonOnClick={() => toggleCreateViewModal(true)} />
title={VIEW_EMPTY_STATE_DETAILS["project-views"].title}
description={VIEW_EMPTY_STATE_DETAILS["project-views"].description}
image={EmptyStateImagePath}
comicBox={{
title: VIEW_EMPTY_STATE_DETAILS["project-views"].comicBox.title,
description: VIEW_EMPTY_STATE_DETAILS["project-views"].comicBox.description,
}}
primaryButton={{
text: VIEW_EMPTY_STATE_DETAILS["project-views"].primaryButton.text,
onClick: () => toggleCreateViewModal(true),
}}
size="lg"
disabled={!isEditingAllowed}
/>
)} )}
</> </>
); );

View File

@ -1,366 +1,516 @@
// workspace empty state import { EUserProjectRoles } from "./project";
export const WORKSPACE_EMPTY_STATE_DETAILS = { import { EUserWorkspaceRoles } from "./workspace";
dashboard: {
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", title: "Overview of your projects, activity, and metrics",
description: 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.", " 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: { primaryButton: {
text: "Build your first project", text: "Build your first project",
comicBox: {
title: "Everything starts with a project in Plane",
description: "A project could be a products roadmap, a marketing campaign, or launching a new car.",
},
}, },
comicBox: {
title: "Everything starts with a project in Plane", accessType: "workspace",
description: "A project could be a products roadmap, a marketing campaign, or launching a new car.", access: EUserWorkspaceRoles.MEMBER,
},
}, },
analytics: { "workspace-analytics": {
key: "workspace-analytics",
title: "Track progress, workloads, and allocations. Spot trends, remove blockers, and move work faster", title: "Track progress, workloads, and allocations. Spot trends, remove blockers, and move work faster",
description: description:
"See scope versus demand, estimates, and scope creep. Get performance by team members and teams, and make sure your project runs on time.", "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: { primaryButton: {
text: "Create Cycles and Modules first", 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: { accessType: "workspace",
title: "Analytics works best with Cycles + Modules", access: EUserWorkspaceRoles.MEMBER,
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.",
},
}, },
projects: { "workspace-projects": {
key: "workspace-projects",
title: "Start a Project", title: "Start a Project",
description: 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.", "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: { primaryButton: {
text: "Start your first project", text: "Start your first project",
comicBox: {
title: "Everything starts with a project in Plane",
description: "A project could be a products roadmap, a marketing campaign, or launching a new car.",
},
}, },
comicBox: { accessType: "workspace",
title: "Everything starts with a project in Plane", access: EUserWorkspaceRoles.MEMBER,
description: "A project could be a products roadmap, a marketing campaign, or launching a new car.",
},
}, },
"assigned-notification": { // all-issues
key: "assigned-notification", "workspace-all-issues": {
title: "No issues assigned", key: "workspace-all-issues",
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",
title: "No issues in the project", title: "No issues in the project",
description: "First project done! Now, slice your work into trackable pieces with issues. Let's go!", 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: { "workspace-assigned": {
key: "assigned", key: "workspace-assigned",
title: "No issues yet", title: "No issues yet",
description: "Issues assigned to you can be tracked from here.", 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: { "workspace-created": {
key: "created", key: "workspace-created",
title: "No issues yet", title: "No issues yet",
description: "All issues created by you come here, track them here directly.", 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: { "workspace-subscribed": {
key: "subscribed", key: "workspace-subscribed",
title: "No issues yet", title: "No issues yet",
description: "Subscribe to issues you are interested in, track all of them here.", description: "Subscribe to issues you are interested in, track all of them here.",
path: "/empty-state/all-issues/subscribed",
}, },
"custom-view": { "workspace-custom-view": {
key: "custom-view", key: "workspace-custom-view",
title: "No issues yet", title: "No issues yet",
description: "Issues that applies to the filters, track all of them here.", description: "Issues that applies to the filters, track all of them here.",
path: "/empty-state/all-issues/custom-view",
}, },
}; "workspace-no-projects": {
key: "workspace-no-projects",
export const SEARCH_EMPTY_STATE_DETAILS = { title: "No project",
views: { description: "To create issues or manage your work, you need to create a project or be a part of one.",
key: "views", path: "/empty-state/onboarding/projects",
title: "No matching views", primaryButton: {
description: "No views match the search criteria. Create a new view instead.", text: "Start your first project",
comicBox: {
title: "Everything starts with a project in Plane",
description: "A project could be a products roadmap, a marketing campaign, or launching a new car.",
},
},
accessType: "workspace",
access: EUserWorkspaceRoles.MEMBER,
}, },
projects: { // workspace settings
key: "projects", "workspace-settings-api-tokens": {
title: "No matching projects", key: "workspace-settings-api-tokens",
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",
title: "No API tokens created", title: "No API tokens created",
description: description:
"Plane APIs can be used to integrate your data in Plane with any external system. Create a token to get started.", "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: { "workspace-settings-webhooks": {
key: "webhooks", key: "workspace-settings-webhooks",
title: "No webhooks added", title: "No webhooks added",
description: "Create webhooks to receive real-time updates and automate actions.", description: "Create webhooks to receive real-time updates and automate actions.",
path: "/empty-state/workspace-settings/webhooks",
}, },
export: { "workspace-settings-export": {
key: "export", key: "workspace-settings-export",
title: "No previous exports yet", title: "No previous exports yet",
description: "Anytime you export, you will also have a copy here for reference.", description: "Anytime you export, you will also have a copy here for reference.",
path: "/empty-state/workspace-settings/exports",
}, },
import: { "workspace-settings-import": {
key: "export", key: "workspace-settings-import",
title: "No previous imports yet", title: "No previous imports yet",
description: "Find all your previous imports here and download them.", description: "Find all your previous imports here and download them.",
path: "/empty-state/workspace-settings/imports",
}, },
}; // profile
"profile-assigned": {
// profile empty state key: "profile-assigned",
export const PROFILE_EMPTY_STATE_DETAILS = {
assigned: {
key: "assigned",
title: "No issues are assigned to you", title: "No issues are assigned to you",
description: "Issues assigned to you can be tracked from here.", description: "Issues assigned to you can be tracked from here.",
path: "/empty-state/profile/assigned",
}, },
subscribed: { "profile-created": {
key: "created", key: "profile-created",
title: "No issues yet", title: "No issues yet",
description: "All issues created by you come here, track them here directly.", description: "All issues created by you come here, track them here directly.",
path: "/empty-state/profile/created",
}, },
created: { "profile-subscribed": {
key: "subscribed", key: "profile-subscribed",
title: "No issues yet", title: "No issues yet",
description: "Subscribe to issues you are interested in, track all of them here.", description: "Subscribe to issues you are interested in, track all of them here.",
path: "/empty-state/profile/subscribed",
}, },
}; // project settings
"project-settings-labels": {
// project empty state key: "project-settings-labels",
export const PROJECT_SETTINGS_EMPTY_STATE_DETAILS = {
labels: {
key: "labels",
title: "No labels yet", title: "No labels yet",
description: "Create labels to help organize and filter issues in you project.", description: "Create labels to help organize and filter issues in you project.",
path: "/empty-state/project-settings/labels",
}, },
integrations: { "project-settings-integrations": {
key: "integrations", key: "project-settings-integrations",
title: "No integrations configured", title: "No integrations configured",
description: "Configure GitHub and other integrations to sync your project issues.", description: "Configure GitHub and other integrations to sync your project issues.",
path: "/empty-state/project-settings/integrations",
}, },
estimate: { "project-settings-estimate": {
key: "estimate", key: "project-settings-estimate",
title: "No estimates added", title: "No estimates added",
description: "Create a set of estimates to communicate the amount of work per issue.", description: "Create a set of estimates to communicate the amount of work per issue.",
path: "/empty-state/project-settings/estimates",
}, },
}; // project cycles
"project-cycles": {
export const CYCLE_EMPTY_STATE_DETAILS = { key: "project-cycles",
cycles: {
title: "Group and timebox your work in Cycles.", title: "Group and timebox your work in Cycles.",
description: description:
"Break work down by timeboxed chunks, work backwards from your project deadline to set dates, and make tangible progress as a team.", "Break work down by timeboxed chunks, work backwards from your project deadline to set dates, and make tangible progress as a team.",
comicBox: { path: "/empty-state/onboarding/cycles",
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.",
},
primaryButton: { primaryButton: {
text: "Set your first cycle", 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": { "project-cycle-no-issues": {
key: "no-issues", key: "project-cycle-no-issues",
title: "No issues added to the cycle", title: "No issues added to the cycle",
description: "Add or create issues you wish to timebox and deliver within this cycle", description: "Add or create issues you wish to timebox and deliver within this cycle",
path: "/empty-state/cycle-issues/",
primaryButton: { primaryButton: {
text: "Create new issue ", text: "Create new issue ",
}, },
secondaryButton: { secondaryButton: {
text: "Add an existing issue", text: "Add an existing issue",
}, },
accessType: "project",
access: EUserProjectRoles.MEMBER,
}, },
active: { "project-cycle-active": {
key: "active", key: "project-cycle-active",
title: "No active cycles", title: "No active cycles",
description: 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.", "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: { "project-cycle-upcoming": {
key: "upcoming", key: "project-cycle-upcoming",
title: "No upcoming cycles", title: "No upcoming cycles",
description: "Upcoming cycles on deck! Just add dates to cycles in draft, and they'll show up right here.", 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: { "project-cycle-completed": {
key: "completed", key: "project-cycle-completed",
title: "No completed cycles", title: "No completed cycles",
description: "Any cycle with a past due date is considered completed. Explore all completed cycles here.", description: "Any cycle with a past due date is considered completed. Explore all completed cycles here.",
path: "/empty-state/cycle/completed",
}, },
draft: { "project-cycle-draft": {
key: "draft", key: "project-cycle-draft",
title: "No draft cycles", title: "No draft cycles",
description: "No dates added in cycles? Find them here as drafts.", description: "No dates added in cycles? Find them here as drafts.",
path: "/empty-state/cycle/draft",
}, },
}; // empty filters
"project-empty-filter": {
export const EMPTY_FILTER_STATE_DETAILS = { key: "project-empty-filter",
archived: {
key: "archived",
title: "No issues found matching the filters applied", title: "No issues found matching the filters applied",
path: "/empty-state/empty-filters/",
secondaryButton: { secondaryButton: {
text: "Clear all filters", text: "Clear all filters",
}, },
accessType: "project",
access: EUserProjectRoles.MEMBER,
}, },
draft: { "project-archived-empty-filter": {
key: "draft", key: "project-archived-empty-filter",
title: "No issues found matching the filters applied", title: "No issues found matching the filters applied",
path: "/empty-state/empty-filters/",
secondaryButton: { secondaryButton: {
text: "Clear all filters", text: "Clear all filters",
}, },
accessType: "project",
access: EUserProjectRoles.MEMBER,
}, },
project: { "project-draft-empty-filter": {
key: "project", key: "project-draft-empty-filter",
title: "No issues found matching the filters applied", title: "No issues found matching the filters applied",
path: "/empty-state/empty-filters/",
secondaryButton: { secondaryButton: {
text: "Clear all filters", text: "Clear all filters",
}, },
accessType: "project",
access: EUserProjectRoles.MEMBER,
}, },
}; // project issues
"project-no-issues": {
export const EMPTY_ISSUE_STATE_DETAILS = { key: "project-no-issues",
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",
title: "Create an issue and assign it to someone, even yourself", title: "Create an issue and assign it to someone, even yourself",
description: 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.", "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: { path: "/empty-state/onboarding/issues",
title: "Issues are building blocks in Plane.",
description:
"Redesign the Plane UI, Rebrand the company, or Launch the new fuel injection system are examples of issues that likely have sub-issues.",
},
primaryButton: { primaryButton: {
text: "Create your first issue", 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,
}, },
}; "project-archived-no-issues": {
key: "project-archived-no-issues",
export const MODULE_EMPTY_STATE_DETAILS = { title: "No archived issues yet",
"no-issues": { description:
key: "no-issues", "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", title: "No issues in the module",
description: "Create or add issues which you want to accomplish as part of this module", description: "Create or add issues which you want to accomplish as part of this module",
path: "/empty-state/module-issues/",
primaryButton: { primaryButton: {
text: "Create new issue ", text: "Create new issue ",
}, },
secondaryButton: { secondaryButton: {
text: "Add an existing issue", 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.", title: "Map your project milestones to Modules and track aggregated work easily.",
description: 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.", "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.",
path: "/empty-state/onboarding/modules",
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.",
},
primaryButton: { primaryButton: {
text: "Build your first module", 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,
}, },
}; // project views
"project-view": {
export const VIEW_EMPTY_STATE_DETAILS = { key: "project-view",
"project-views": {
title: "Save filtered views for your project. Create as many as you need", title: "Save filtered views for your project. Create as many as you need",
description: 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 everyones views and choose whichever suits their needs best.", "Views are a set of saved filters that you use frequently or want easy access to. All your colleagues in a project can see everyones views and choose whichever suits their needs best.",
comicBox: { path: "/empty-state/onboarding/views",
title: "Views work atop Issue properties.",
description: "You can create a view from here with as many properties as filters as you see fit.",
},
primaryButton: { primaryButton: {
text: "Create your first view", 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,
}, },
}; // project pages
"project-page": {
export const PAGE_EMPTY_STATE_DETAILS = {
pages: {
key: "pages", key: "pages",
title: "Write a note, a doc, or a full knowledge base. Get Galileo, Planes AI assistant, to help you get started", title: "Write a note, a doc, or a full knowledge base. Get Galileo, Planes AI assistant, to help you get started",
description: 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 projects context. To make short work of any doc, invoke Galileo, Planes AI, with a shortcut or the click of a button.", "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 projects context. To make short work of any doc, invoke Galileo, Planes AI, with a shortcut or the click of a button.",
path: "/empty-state/onboarding/pages",
primaryButton: { primaryButton: {
text: "Create your first page", text: "Create your first page",
comicBox: {
title: "A page can be a doc or a doc of docs.",
description:
"We wrote Nikhil and Meeras love story. You could write your projects mission, goals, and eventual vision.",
},
}, },
comicBox: { accessType: "project",
title: "A page can be a doc or a doc of docs.", access: EUserProjectRoles.MEMBER,
description:
"We wrote Nikhil and Meeras love story. You could write your projects mission, goals, and eventual vision.",
},
}, },
All: { "project-page-all": {
key: "all", key: "project-page-all",
title: "Write a note, a doc, or a full knowledge base", title: "Write a note, a doc, or a full knowledge base",
description: description:
"Pages help you organise your thoughts to create wikis, discussions or even document heated takes for your project. Use it wisely!", "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: { "project-page-favorite": {
key: "favorites", key: "project-page-favorite",
title: "No favorite pages yet", title: "No favorite pages yet",
description: "Favorites for quick access? mark them and find them right here.", description: "Favorites for quick access? mark them and find them right here.",
path: "/empty-state/pages/favorites",
}, },
Private: { "project-page-private": {
key: "private", key: "project-page-private",
title: "No private pages yet", title: "No private pages yet",
description: "Keep your private thoughts here. When you're ready to share, the team's just a click away.", 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: { "project-page-shared": {
key: "shared", key: "project-page-shared",
title: "No shared pages yet", title: "No shared pages yet",
description: "See pages shared with everyone in your project right here.", description: "See pages shared with everyone in your project right here.",
path: "/empty-state/pages/shared",
}, },
Archived: { "project-page-archived": {
key: "archived", key: "project-page-archived",
title: "No archived pages yet", title: "No archived pages yet",
description: "Archive pages not on your radar. Access them here when needed.", description: "Archive pages not on your radar. Access them here when needed.",
path: "/empty-state/pages/archived",
}, },
Recent: { "project-page-recent": {
key: "recent", key: "project-page-recent",
title: "Write a note, a doc, or a full knowledge base", title: "Write a note, a doc, or a full knowledge base",
description: 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", "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: { primaryButton: {
text: "Create new page", text: "Create new page",
}, },
accessType: "project",
access: EUserProjectRoles.MEMBER,
}, },
}; } as const;
export const EMPTY_STATE_DETAILS: Record<EmptyStateKeys, EmptyStateDetails> = emptyStateDetails;

View File

@ -1,44 +1,33 @@
import React, { Fragment, ReactElement } from "react"; import React, { Fragment, ReactElement } from "react";
import { observer } from "mobx-react-lite"; import { observer } from "mobx-react-lite";
import { useRouter } from "next/router"; import { useRouter } from "next/router";
import { useTheme } from "next-themes";
import { Tab } from "@headlessui/react"; import { Tab } from "@headlessui/react";
// hooks // hooks
import { useApplication, useEventTracker, useProject, useWorkspace } from "hooks/store";
// layouts // layouts
import { AppLayout } from "layouts/app-layout";
// components // components
import { CustomAnalytics, ScopeAndDemand } from "components/analytics"; import { CustomAnalytics, ScopeAndDemand } from "components/analytics";
import { PageHead } from "components/core"; import { PageHead } from "components/core";
import { EmptyState, getEmptyStateImagePath } from "components/empty-state"; import { EmptyState } from "components/empty-state";
import { WorkspaceAnalyticsHeader } from "components/headers"; 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 // type
import { NextPageWithLayout } from "lib/types"; import { NextPageWithLayout } from "lib/types";
// constants
import { ANALYTICS_TABS } from "constants/analytics";
import { EmptyStateType } from "constants/empty-state";
const AnalyticsPage: NextPageWithLayout = observer(() => { const AnalyticsPage: NextPageWithLayout = observer(() => {
const router = useRouter(); const router = useRouter();
const { analytics_tab } = router.query; const { analytics_tab } = router.query;
// theme
const { resolvedTheme } = useTheme();
// store hooks // store hooks
const { const {
commandPalette: { toggleCreateProjectModal }, commandPalette: { toggleCreateProjectModal },
} = useApplication(); } = useApplication();
const { setTrackElement } = useEventTracker(); const { setTrackElement } = useEventTracker();
const {
membership: { currentWorkspaceRole },
currentUser,
} = useUser();
const { workspaceProjectIds } = useProject(); const { workspaceProjectIds } = useProject();
const { currentWorkspace } = useWorkspace(); const { currentWorkspace } = useWorkspace();
// derived values // 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; const pageTitle = currentWorkspace?.name ? `${currentWorkspace?.name} - Analytics` : undefined;
return ( return (
@ -79,22 +68,11 @@ const AnalyticsPage: NextPageWithLayout = observer(() => {
</div> </div>
) : ( ) : (
<EmptyState <EmptyState
image={EmptyStateImagePath} type={EmptyStateType.WORKSPACE_ANALYTICS}
title={WORKSPACE_EMPTY_STATE_DETAILS["analytics"].title} primaryButtonOnClick={() => {
description={WORKSPACE_EMPTY_STATE_DETAILS["analytics"].description} setTrackElement("Analytics empty state");
primaryButton={{ toggleCreateProjectModal(true);
text: WORKSPACE_EMPTY_STATE_DETAILS["analytics"].primaryButton.text,
onClick: () => {
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}
/> />
)} )}
</> </>

View File

@ -1,39 +1,31 @@
import { Fragment, useCallback, useState, ReactElement } from "react"; import { Fragment, useCallback, useState, ReactElement } from "react";
import { observer } from "mobx-react-lite"; import { observer } from "mobx-react-lite";
import { useRouter } from "next/router"; import { useRouter } from "next/router";
import { useTheme } from "next-themes";
import { Tab } from "@headlessui/react"; import { Tab } from "@headlessui/react";
// hooks // hooks
import { Tooltip } from "@plane/ui"; import { useEventTracker, useCycle, useProject } from "hooks/store";
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 useLocalStorage from "hooks/use-local-storage"; import useLocalStorage from "hooks/use-local-storage";
// layouts // layouts
import { AppLayout } from "layouts/app-layout"; import { AppLayout } from "layouts/app-layout";
// components // 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 // ui
import { Tooltip } from "@plane/ui";
// types // types
import { NextPageWithLayout } from "lib/types"; import { NextPageWithLayout } from "lib/types";
import { TCycleView, TCycleLayout } from "@plane/types"; import { TCycleView, TCycleLayout } from "@plane/types";
// constants // constants
import { CYCLE_TAB_LIST, CYCLE_VIEW_LAYOUTS } from "constants/cycle";
import { EmptyStateType } from "constants/empty-state";
const ProjectCyclesPage: NextPageWithLayout = observer(() => { const ProjectCyclesPage: NextPageWithLayout = observer(() => {
const [createModal, setCreateModal] = useState(false); const [createModal, setCreateModal] = useState(false);
// theme
const { resolvedTheme } = useTheme();
// store hooks // store hooks
const { setTrackElement } = useEventTracker(); const { setTrackElement } = useEventTracker();
const {
membership: { currentProjectRole },
currentUser,
} = useUser();
const { currentProjectCycleIds, loader } = useCycle(); const { currentProjectCycleIds, loader } = useCycle();
const { getProjectById } = useProject(); const { getProjectById } = useProject();
// router // router
@ -43,10 +35,7 @@ const ProjectCyclesPage: NextPageWithLayout = observer(() => {
const { storedValue: cycleTab, setValue: setCycleTab } = useLocalStorage<TCycleView>("cycle_tab", "active"); const { storedValue: cycleTab, setValue: setCycleTab } = useLocalStorage<TCycleView>("cycle_tab", "active");
const { storedValue: cycleLayout, setValue: setCycleLayout } = useLocalStorage<TCycleLayout>("cycle_layout", "list"); const { storedValue: cycleLayout, setValue: setCycleLayout } = useLocalStorage<TCycleLayout>("cycle_layout", "list");
// derived values // derived values
const isLightMode = resolvedTheme ? resolvedTheme === "light" : currentUser?.theme.theme === "light";
const EmptyStateImagePath = getEmptyStateImagePath("onboarding", "cycles", isLightMode);
const totalCycles = currentProjectCycleIds?.length ?? 0; const totalCycles = currentProjectCycleIds?.length ?? 0;
const isEditingAllowed = !!currentProjectRole && currentProjectRole >= EUserWorkspaceRoles.MEMBER;
const project = projectId ? getProjectById(projectId?.toString()) : undefined; const project = projectId ? getProjectById(projectId?.toString()) : undefined;
const pageTitle = project?.name ? `${project?.name} - Cycles` : undefined; const pageTitle = project?.name ? `${project?.name} - Cycles` : undefined;
@ -89,22 +78,11 @@ const ProjectCyclesPage: NextPageWithLayout = observer(() => {
{totalCycles === 0 ? ( {totalCycles === 0 ? (
<div className="h-full place-items-center"> <div className="h-full place-items-center">
<EmptyState <EmptyState
title={CYCLE_EMPTY_STATE_DETAILS["cycles"].title} type={EmptyStateType.PROJECT_CYCLES}
description={CYCLE_EMPTY_STATE_DETAILS["cycles"].description} primaryButtonOnClick={() => {
image={EmptyStateImagePath} setTrackElement("Cycle empty state");
comicBox={{ setCreateModal(true);
title: CYCLE_EMPTY_STATE_DETAILS["cycles"].comicBox.title,
description: CYCLE_EMPTY_STATE_DETAILS["cycles"].comicBox.description,
}} }}
primaryButton={{
text: CYCLE_EMPTY_STATE_DETAILS["cycles"].primaryButton.text,
onClick: () => {
setTrackElement("Cycle empty state");
setCreateModal(true);
},
}}
size="lg"
disabled={!isEditingAllowed}
/> />
</div> </div>
) : ( ) : (

View File

@ -2,18 +2,9 @@ import { useState, Fragment, ReactElement } from "react";
import { observer } from "mobx-react-lite"; import { observer } from "mobx-react-lite";
import dynamic from "next/dynamic"; import dynamic from "next/dynamic";
import { useRouter } from "next/router"; import { useRouter } from "next/router";
import { useTheme } from "next-themes";
import useSWR from "swr"; import useSWR from "swr";
import { Tab } from "@headlessui/react"; import { Tab } from "@headlessui/react";
// hooks // 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 { useApplication, useEventTracker, useUser, useProject } from "hooks/store";
import { useProjectPages } from "hooks/store/use-project-page"; import { useProjectPages } from "hooks/store/use-project-page";
import useLocalStorage from "hooks/use-local-storage"; import useLocalStorage from "hooks/use-local-storage";
@ -22,9 +13,16 @@ import useSize from "hooks/use-window-size";
// layouts // layouts
import { AppLayout } from "layouts/app-layout"; import { AppLayout } from "layouts/app-layout";
// components // 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 // types
import { NextPageWithLayout } from "lib/types"; import { NextPageWithLayout } from "lib/types";
// constants // constants
import { PAGE_TABS_LIST } from "constants/page";
import { EmptyStateType } from "constants/empty-state";
const AllPagesList = dynamic<any>(() => import("components/pages").then((a) => a.AllPagesList), { const AllPagesList = dynamic<any>(() => import("components/pages").then((a) => a.AllPagesList), {
ssr: false, ssr: false,
@ -52,14 +50,8 @@ const ProjectPagesPage: NextPageWithLayout = observer(() => {
const { workspaceSlug, projectId } = router.query; const { workspaceSlug, projectId } = router.query;
// states // states
const [createUpdatePageModal, setCreateUpdatePageModal] = useState(false); const [createUpdatePageModal, setCreateUpdatePageModal] = useState(false);
// theme
const { resolvedTheme } = useTheme();
// store hooks // store hooks
const { const { currentUser, currentUserLoader } = useUser();
currentUser,
currentUserLoader,
membership: { currentProjectRole },
} = useUser();
const { const {
commandPalette: { toggleCreatePageModal }, commandPalette: { toggleCreatePageModal },
} = useApplication(); } = useApplication();
@ -103,9 +95,6 @@ const ProjectPagesPage: NextPageWithLayout = observer(() => {
}; };
// derived values // 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 project = projectId ? getProjectById(projectId.toString()) : undefined;
const pageTitle = project?.name ? `${project?.name} - Pages` : undefined; const pageTitle = project?.name ? `${project?.name} - Pages` : undefined;
@ -216,22 +205,11 @@ const ProjectPagesPage: NextPageWithLayout = observer(() => {
</> </>
) : ( ) : (
<EmptyState <EmptyState
image={EmptyStateImagePath} type={EmptyStateType.PROJECT_PAGE}
title={PAGE_EMPTY_STATE_DETAILS["pages"].title} primaryButtonOnClick={() => {
description={PAGE_EMPTY_STATE_DETAILS["pages"].description} setTrackElement("Pages empty state");
primaryButton={{ toggleCreatePageModal(true);
text: PAGE_EMPTY_STATE_DETAILS["pages"].primaryButton.text,
onClick: () => {
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}
/> />
)} )}
</> </>

View File

@ -1,17 +1,9 @@
import { ReactElement } from "react"; import { ReactElement } from "react";
import { observer } from "mobx-react"; import { observer } from "mobx-react";
import { useRouter } from "next/router"; import { useRouter } from "next/router";
import { useTheme } from "next-themes";
import useSWR from "swr"; import useSWR from "swr";
// hooks // 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 { 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 // layouts
import { AppLayout } from "layouts/app-layout"; import { AppLayout } from "layouts/app-layout";
import { ProjectSettingLayout } from "layouts/settings-layout"; import { ProjectSettingLayout } from "layouts/settings-layout";
@ -20,10 +12,17 @@ import { NextPageWithLayout } from "lib/types";
import { IntegrationService } from "services/integrations"; import { IntegrationService } from "services/integrations";
import { ProjectService } from "services/project"; import { ProjectService } from "services/project";
// components // components
import { PageHead } from "components/core";
import { IntegrationCard } from "components/project";
import { ProjectSettingHeader } from "components/headers";
import { EmptyState } from "components/empty-state";
// ui // ui
// types // types
import { IProject } from "@plane/types"; import { IProject } from "@plane/types";
// fetch-keys // fetch-keys
import { PROJECT_DETAILS, WORKSPACE_INTEGRATIONS } from "constants/fetch-keys";
// constants
import { EmptyStateType } from "constants/empty-state";
// services // services
const integrationService = new IntegrationService(); const integrationService = new IntegrationService();
@ -32,10 +31,6 @@ const projectService = new ProjectService();
const ProjectIntegrationsPage: NextPageWithLayout = observer(() => { const ProjectIntegrationsPage: NextPageWithLayout = observer(() => {
const router = useRouter(); const router = useRouter();
const { workspaceSlug, projectId } = router.query; const { workspaceSlug, projectId } = router.query;
// theme
const { resolvedTheme } = useTheme();
// store hooks
const { currentUser } = useUser();
// fetch project details // fetch project details
const { data: projectDetails } = useSWR<IProject>( const { data: projectDetails } = useSWR<IProject>(
workspaceSlug && projectId ? PROJECT_DETAILS(projectId as string) : null, workspaceSlug && projectId ? PROJECT_DETAILS(projectId as string) : null,
@ -47,9 +42,6 @@ const ProjectIntegrationsPage: NextPageWithLayout = observer(() => {
() => (workspaceSlug ? integrationService.getWorkspaceIntegrationsList(workspaceSlug as string) : null) () => (workspaceSlug ? integrationService.getWorkspaceIntegrationsList(workspaceSlug as string) : null)
); );
// derived values // 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 isAdmin = projectDetails?.member_role === 20;
const pageTitle = projectDetails?.name ? `${projectDetails?.name} - Integrations` : undefined; const pageTitle = projectDetails?.name ? `${projectDetails?.name} - Integrations` : undefined;
@ -70,15 +62,8 @@ const ProjectIntegrationsPage: NextPageWithLayout = observer(() => {
) : ( ) : (
<div className="h-full w-full py-8"> <div className="h-full w-full py-8">
<EmptyState <EmptyState
title={emptyStateDetail.title} type={EmptyStateType.PROJECT_SETTINGS_INTEGRATIONS}
description={emptyStateDetail.description} primaryButtonLink={`/${workspaceSlug}/settings/integrations`}
image={emptyStateImage}
primaryButton={{
text: "Configure now",
onClick: () => router.push(`/${workspaceSlug}/settings/integrations`),
}}
size="lg"
disabled={!isAdmin}
/> />
</div> </div>
) )

View File

@ -1,29 +1,28 @@
import React, { useState } from "react"; import React, { useState } from "react";
import { observer } from "mobx-react-lite"; import { observer } from "mobx-react-lite";
import { useRouter } from "next/router"; import { useRouter } from "next/router";
import { useTheme } from "next-themes";
import useSWR from "swr"; import useSWR from "swr";
// store hooks // 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"; import { useUser, useWorkspace } from "hooks/store";
// layouts // layouts
import { AppLayout } from "layouts/app-layout"; import { AppLayout } from "layouts/app-layout";
import { WorkspaceSettingLayout } from "layouts/settings-layout"; import { WorkspaceSettingLayout } from "layouts/settings-layout";
// component // 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 // ui
import { Button } from "@plane/ui";
// services // services
import { NextPageWithLayout } from "lib/types"; import { NextPageWithLayout } from "lib/types";
import { APITokenService } from "services/api_token.service"; import { APITokenService } from "services/api_token.service";
// types // types
// constants // constants
import { API_TOKENS_LIST } from "constants/fetch-keys";
import { EUserWorkspaceRoles } from "constants/workspace";
import { EmptyStateType } from "constants/empty-state";
const apiTokenService = new APITokenService(); const apiTokenService = new APITokenService();
@ -33,12 +32,9 @@ const ApiTokensPage: NextPageWithLayout = observer(() => {
// router // router
const router = useRouter(); const router = useRouter();
const { workspaceSlug } = router.query; const { workspaceSlug } = router.query;
// theme
const { resolvedTheme } = useTheme();
// store hooks // store hooks
const { const {
membership: { currentWorkspaceRole }, membership: { currentWorkspaceRole },
currentUser,
} = useUser(); } = useUser();
const { currentWorkspace } = useWorkspace(); const { currentWorkspace } = useWorkspace();
@ -48,9 +44,6 @@ const ApiTokensPage: NextPageWithLayout = observer(() => {
workspaceSlug && isAdmin ? apiTokenService.getApiTokens(workspaceSlug.toString()) : null 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; const pageTitle = currentWorkspace?.name ? `${currentWorkspace.name} - API Tokens` : undefined;
if (!isAdmin) if (!isAdmin)
@ -95,12 +88,7 @@ const ApiTokensPage: NextPageWithLayout = observer(() => {
</Button> </Button>
</div> </div>
<div className="h-full w-full flex items-center justify-center"> <div className="h-full w-full flex items-center justify-center">
<EmptyState <EmptyState type={EmptyStateType.WORKSPACE_SETTINGS_API_TOKENS} />
title={emptyStateDetail.title}
description={emptyStateDetail.description}
image={emptyStateImage}
size="lg"
/>
</div> </div>
</div> </div>
)} )}

View File

@ -1,25 +1,24 @@
import React, { useEffect, useState } from "react"; import React, { useEffect, useState } from "react";
import { observer } from "mobx-react-lite"; import { observer } from "mobx-react-lite";
import { useRouter } from "next/router"; import { useRouter } from "next/router";
import { useTheme } from "next-themes";
import useSWR from "swr"; import useSWR from "swr";
// hooks // 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"; import { useUser, useWebhook, useWorkspace } from "hooks/store";
// layouts // layouts
import { AppLayout } from "layouts/app-layout"; import { AppLayout } from "layouts/app-layout";
import { WorkspaceSettingLayout } from "layouts/settings-layout"; import { WorkspaceSettingLayout } from "layouts/settings-layout";
// components // 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 // ui
import { Button } from "@plane/ui";
// types // types
import { NextPageWithLayout } from "lib/types"; import { NextPageWithLayout } from "lib/types";
// constants // constants
import { EmptyStateType } from "constants/empty-state";
const WebhooksListPage: NextPageWithLayout = observer(() => { const WebhooksListPage: NextPageWithLayout = observer(() => {
// states // states
@ -27,12 +26,9 @@ const WebhooksListPage: NextPageWithLayout = observer(() => {
// router // router
const router = useRouter(); const router = useRouter();
const { workspaceSlug } = router.query; const { workspaceSlug } = router.query;
// theme
const { resolvedTheme } = useTheme();
// mobx store // mobx store
const { const {
membership: { currentWorkspaceRole }, membership: { currentWorkspaceRole },
currentUser,
} = useUser(); } = useUser();
const { fetchWebhooks, webhooks, clearSecretKey, webhookSecretKey, createWebhook } = useWebhook(); const { fetchWebhooks, webhooks, clearSecretKey, webhookSecretKey, createWebhook } = useWebhook();
const { currentWorkspace } = useWorkspace(); const { currentWorkspace } = useWorkspace();
@ -44,10 +40,6 @@ const WebhooksListPage: NextPageWithLayout = observer(() => {
workspaceSlug && isAdmin ? () => fetchWebhooks(workspaceSlug.toString()) : null 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; const pageTitle = currentWorkspace?.name ? `${currentWorkspace.name} - Webhooks` : undefined;
// clear secret key when modal is closed. // clear secret key when modal is closed.
@ -99,12 +91,7 @@ const WebhooksListPage: NextPageWithLayout = observer(() => {
</Button> </Button>
</div> </div>
<div className="h-full w-full flex items-center justify-center"> <div className="h-full w-full flex items-center justify-center">
<EmptyState <EmptyState type={EmptyStateType.WORKSPACE_SETTINGS_WEBHOOKS} />
title={emptyStateDetail.title}
description={emptyStateDetail.description}
image={emptyStateImage}
size="lg"
/>
</div> </div>
</div> </div>
)} )}

View File

Before

Width:  |  Height:  |  Size: 43 KiB

After

Width:  |  Height:  |  Size: 43 KiB

View File

Before

Width:  |  Height:  |  Size: 67 KiB

After

Width:  |  Height:  |  Size: 67 KiB

View File

Before

Width:  |  Height:  |  Size: 43 KiB

After

Width:  |  Height:  |  Size: 43 KiB

View File

Before

Width:  |  Height:  |  Size: 68 KiB

After

Width:  |  Height:  |  Size: 68 KiB