[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
@ -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 ?? "");
|
||||||
|
@ -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"
|
|
||||||
/>
|
|
||||||
)}
|
)}
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
|
@ -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"
|
|
||||||
/>
|
|
||||||
)}
|
)}
|
||||||
</>
|
</>
|
||||||
) : (
|
) : (
|
||||||
|
@ -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>
|
|
||||||
);
|
|
||||||
};
|
};
|
||||||
|
@ -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>
|
||||||
)
|
)
|
||||||
) : (
|
) : (
|
||||||
|
@ -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>
|
||||||
)
|
)
|
||||||
) : (
|
) : (
|
||||||
|
@ -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>
|
||||||
)
|
)
|
||||||
) : (
|
) : (
|
||||||
|
@ -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>
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
@ -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>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
|
@ -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>
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
@ -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>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
|
@ -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>
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
@ -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>
|
||||||
|
@ -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 && (
|
||||||
|
@ -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}
|
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</>
|
</>
|
||||||
|
@ -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}
|
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</>
|
</>
|
||||||
|
@ -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>
|
||||||
|
@ -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}
|
|
||||||
/>
|
/>
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
|
@ -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 (
|
||||||
|
@ -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}
|
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</>
|
</>
|
||||||
|
@ -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}
|
|
||||||
/>
|
|
||||||
)}
|
)}
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
|
@ -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 product’s 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 product’s 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 product’s 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 product’s 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 product’s 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 everyone’s 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 everyone’s 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, Plane’s AI assistant, to help you get started",
|
title: "Write a note, a doc, or a full knowledge base. Get Galileo, Plane’s 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 project’s context. To make short work of any doc, invoke Galileo, Plane’s 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 project’s context. To make short work of any doc, invoke Galileo, Plane’s AI, with a shortcut or the click of a button.",
|
||||||
|
path: "/empty-state/onboarding/pages",
|
||||||
primaryButton: {
|
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 Meera’s love story. You could write your project’s 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 Meera’s love story. You could write your project’s 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;
|
||||||
|
@ -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}
|
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</>
|
</>
|
||||||
|
@ -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>
|
||||||
) : (
|
) : (
|
||||||
|
@ -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}
|
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</>
|
</>
|
||||||
|
@ -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>
|
||||||
)
|
)
|
||||||
|
@ -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>
|
||||||
)}
|
)}
|
||||||
|
@ -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>
|
||||||
)}
|
)}
|
||||||
|
Before Width: | Height: | Size: 43 KiB After Width: | Height: | Size: 43 KiB |
Before Width: | Height: | Size: 67 KiB After Width: | Height: | Size: 67 KiB |
Before Width: | Height: | Size: 43 KiB After Width: | Height: | Size: 43 KiB |
Before Width: | Height: | Size: 68 KiB After Width: | Height: | Size: 68 KiB |