Merge branch 'develop' of github.com:makeplane/plane into develop
@ -235,6 +235,7 @@ class IssueSearchEndpoint(BaseAPIView):
|
||||
cycle = request.query_params.get("cycle", "false")
|
||||
module = request.query_params.get("module", False)
|
||||
sub_issue = request.query_params.get("sub_issue", "false")
|
||||
target_date = request.query_params.get("target_date", True)
|
||||
|
||||
issue_id = request.query_params.get("issue_id", False)
|
||||
|
||||
@ -273,6 +274,9 @@ class IssueSearchEndpoint(BaseAPIView):
|
||||
if module:
|
||||
issues = issues.exclude(issue_module__module=module)
|
||||
|
||||
if target_date == "none":
|
||||
issues = issues.filter(target_date__isnull=True)
|
||||
|
||||
return Response(
|
||||
issues.values(
|
||||
"name",
|
||||
|
1
packages/types/src/projects.d.ts
vendored
@ -130,6 +130,7 @@ export type TProjectIssuesSearchParams = {
|
||||
sub_issue?: boolean;
|
||||
issue_id?: string;
|
||||
workspace_search: boolean;
|
||||
target_date?: string;
|
||||
};
|
||||
|
||||
export interface ISearchIssueResponse {
|
||||
|
@ -1,10 +1,9 @@
|
||||
import { MouseEvent } from "react";
|
||||
import { observer } from "mobx-react-lite";
|
||||
import Link from "next/link";
|
||||
import { useTheme } from "next-themes";
|
||||
import useSWR from "swr";
|
||||
// hooks
|
||||
import { useCycle, useIssues, useMember, useProject, useUser } from "hooks/store";
|
||||
import { useCycle, useIssues, useMember, useProject } from "hooks/store";
|
||||
// ui
|
||||
import { SingleProgressStats } from "components/core";
|
||||
import {
|
||||
@ -23,7 +22,7 @@ import {
|
||||
import ProgressChart from "components/core/sidebar/progress-chart";
|
||||
import { ActiveCycleProgressStats } from "components/cycles";
|
||||
import { StateDropdown } from "components/dropdowns";
|
||||
import { EmptyState, getEmptyStateImagePath } from "components/empty-state";
|
||||
import { EmptyState } from "components/empty-state";
|
||||
// icons
|
||||
import { ArrowRight, CalendarCheck, CalendarDays, Star, Target } from "lucide-react";
|
||||
// helpers
|
||||
@ -35,7 +34,7 @@ import { ICycle, TCycleGroups } from "@plane/types";
|
||||
import { EIssuesStoreType } from "constants/issue";
|
||||
import { CYCLE_ISSUES_WITH_PARAMS } from "constants/fetch-keys";
|
||||
import { CYCLE_STATE_GROUPS_DETAILS } from "constants/cycle";
|
||||
import { CYCLE_EMPTY_STATE_DETAILS } from "constants/empty-state";
|
||||
import { EmptyStateType } from "constants/empty-state";
|
||||
|
||||
interface IActiveCycleDetails {
|
||||
workspaceSlug: string;
|
||||
@ -45,9 +44,6 @@ interface IActiveCycleDetails {
|
||||
export const ActiveCycleDetails: React.FC<IActiveCycleDetails> = observer((props) => {
|
||||
// props
|
||||
const { workspaceSlug, projectId } = props;
|
||||
const { resolvedTheme } = useTheme();
|
||||
// store hooks
|
||||
const { currentUser } = useUser();
|
||||
const {
|
||||
issues: { fetchActiveCycleIssues },
|
||||
} = useIssues(EIssuesStoreType.CYCLE);
|
||||
@ -78,11 +74,6 @@ export const ActiveCycleDetails: React.FC<IActiveCycleDetails> = observer((props
|
||||
: null
|
||||
);
|
||||
|
||||
const emptyStateDetail = CYCLE_EMPTY_STATE_DETAILS["active"];
|
||||
|
||||
const isLightMode = resolvedTheme ? resolvedTheme === "light" : currentUser?.theme.theme === "light";
|
||||
const emptyStateImage = getEmptyStateImagePath("cycle", "active", isLightMode);
|
||||
|
||||
if (!activeCycle && isLoading)
|
||||
return (
|
||||
<Loader>
|
||||
@ -90,15 +81,7 @@ export const ActiveCycleDetails: React.FC<IActiveCycleDetails> = observer((props
|
||||
</Loader>
|
||||
);
|
||||
|
||||
if (!activeCycle)
|
||||
return (
|
||||
<EmptyState
|
||||
title={emptyStateDetail.title}
|
||||
description={emptyStateDetail.description}
|
||||
image={emptyStateImage}
|
||||
size="sm"
|
||||
/>
|
||||
);
|
||||
if (!activeCycle) return <EmptyState type={EmptyStateType.PROJECT_CYCLE_ACTIVE} size="sm" />;
|
||||
|
||||
const endDate = new Date(activeCycle.end_date ?? "");
|
||||
const startDate = new Date(activeCycle.start_date ?? "");
|
||||
|
@ -1,13 +1,10 @@
|
||||
import { FC } from "react";
|
||||
import { observer } from "mobx-react-lite";
|
||||
import { useTheme } from "next-themes";
|
||||
// hooks
|
||||
// components
|
||||
import { CyclePeekOverview, CyclesBoardCard } from "components/cycles";
|
||||
import { EmptyState, getEmptyStateImagePath } from "components/empty-state";
|
||||
import { EmptyState } from "components/empty-state";
|
||||
// constants
|
||||
import { CYCLE_EMPTY_STATE_DETAILS } from "constants/empty-state";
|
||||
import { useUser } from "hooks/store";
|
||||
import { EMPTY_STATE_DETAILS } from "constants/empty-state";
|
||||
|
||||
export interface ICyclesBoard {
|
||||
cycleIds: string[];
|
||||
@ -19,15 +16,6 @@ export interface ICyclesBoard {
|
||||
|
||||
export const CyclesBoard: FC<ICyclesBoard> = observer((props) => {
|
||||
const { cycleIds, filter, workspaceSlug, projectId, peekCycle } = props;
|
||||
// theme
|
||||
const { resolvedTheme } = useTheme();
|
||||
// store hooks
|
||||
const { currentUser } = useUser();
|
||||
|
||||
const emptyStateDetail = CYCLE_EMPTY_STATE_DETAILS[filter as keyof typeof CYCLE_EMPTY_STATE_DETAILS];
|
||||
|
||||
const isLightMode = resolvedTheme ? resolvedTheme === "light" : currentUser?.theme.theme === "light";
|
||||
const emptyStateImage = getEmptyStateImagePath("cycle", filter, isLightMode);
|
||||
|
||||
return (
|
||||
<>
|
||||
@ -52,12 +40,7 @@ export const CyclesBoard: FC<ICyclesBoard> = observer((props) => {
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<EmptyState
|
||||
title={emptyStateDetail.title}
|
||||
description={emptyStateDetail.description}
|
||||
image={emptyStateImage}
|
||||
size="sm"
|
||||
/>
|
||||
<EmptyState type={`project-cycle-${filter}` as keyof typeof EMPTY_STATE_DETAILS} size="sm" />
|
||||
)}
|
||||
</>
|
||||
);
|
||||
|
@ -1,15 +1,12 @@
|
||||
import { FC } from "react";
|
||||
import { observer } from "mobx-react-lite";
|
||||
import { useTheme } from "next-themes";
|
||||
// hooks
|
||||
// components
|
||||
import { Loader } from "@plane/ui";
|
||||
import { CyclePeekOverview, CyclesListItem } from "components/cycles";
|
||||
import { EmptyState, getEmptyStateImagePath } from "components/empty-state";
|
||||
import { EmptyState } from "components/empty-state";
|
||||
// ui
|
||||
import { Loader } from "@plane/ui";
|
||||
// constants
|
||||
import { CYCLE_EMPTY_STATE_DETAILS } from "constants/empty-state";
|
||||
import { useUser } from "hooks/store";
|
||||
import { EMPTY_STATE_DETAILS } from "constants/empty-state";
|
||||
|
||||
export interface ICyclesList {
|
||||
cycleIds: string[];
|
||||
@ -20,15 +17,6 @@ export interface ICyclesList {
|
||||
|
||||
export const CyclesList: FC<ICyclesList> = observer((props) => {
|
||||
const { cycleIds, filter, workspaceSlug, projectId } = props;
|
||||
// theme
|
||||
const { resolvedTheme } = useTheme();
|
||||
// store hooks
|
||||
const { currentUser } = useUser();
|
||||
|
||||
const emptyStateDetail = CYCLE_EMPTY_STATE_DETAILS[filter as keyof typeof CYCLE_EMPTY_STATE_DETAILS];
|
||||
|
||||
const isLightMode = resolvedTheme ? resolvedTheme === "light" : currentUser?.theme.theme === "light";
|
||||
const emptyStateImage = getEmptyStateImagePath("cycle", filter, isLightMode);
|
||||
|
||||
return (
|
||||
<>
|
||||
@ -54,12 +42,7 @@ export const CyclesList: FC<ICyclesList> = observer((props) => {
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<EmptyState
|
||||
title={emptyStateDetail.title}
|
||||
description={emptyStateDetail.description}
|
||||
image={emptyStateImage}
|
||||
size="sm"
|
||||
/>
|
||||
<EmptyState type={`project-cycle-${filter}` as keyof typeof EMPTY_STATE_DETAILS} size="sm" />
|
||||
)}
|
||||
</>
|
||||
) : (
|
||||
|
@ -1,119 +1,150 @@
|
||||
import React from "react";
|
||||
import Link from "next/link";
|
||||
import Image from "next/image";
|
||||
// components
|
||||
// ui
|
||||
import { Button, getButtonStyling } from "@plane/ui";
|
||||
// helper
|
||||
import { cn } from "helpers/common.helper";
|
||||
import { ComicBoxButton } from "./comic-box-button";
|
||||
|
||||
type Props = {
|
||||
title: string;
|
||||
description?: string;
|
||||
image: any;
|
||||
primaryButton?: {
|
||||
icon?: any;
|
||||
text: string;
|
||||
onClick: () => void;
|
||||
};
|
||||
secondaryButton?: {
|
||||
icon?: any;
|
||||
text: string;
|
||||
onClick: () => void;
|
||||
};
|
||||
comicBox?: {
|
||||
title: string;
|
||||
description: string;
|
||||
};
|
||||
size?: "sm" | "lg";
|
||||
disabled?: boolean;
|
||||
import { useTheme } from "next-themes";
|
||||
// hooks
|
||||
import { useUser } from "hooks/store";
|
||||
// components
|
||||
import { Button, TButtonVariant } from "@plane/ui";
|
||||
import { ComicBoxButton } from "./comic-box-button";
|
||||
// constant
|
||||
import { EMPTY_STATE_DETAILS, EmptyStateKeys } from "constants/empty-state";
|
||||
// helpers
|
||||
import { cn } from "helpers/common.helper";
|
||||
|
||||
export type EmptyStateProps = {
|
||||
type: EmptyStateKeys;
|
||||
size?: "sm" | "md" | "lg";
|
||||
layout?: "widget-simple" | "screen-detailed" | "screen-simple";
|
||||
additionalPath?: string;
|
||||
primaryButtonOnClick?: () => void;
|
||||
primaryButtonLink?: string;
|
||||
secondaryButtonOnClick?: () => void;
|
||||
};
|
||||
|
||||
export const EmptyState: React.FC<Props> = ({
|
||||
title,
|
||||
description,
|
||||
image,
|
||||
primaryButton,
|
||||
secondaryButton,
|
||||
comicBox,
|
||||
size = "sm",
|
||||
disabled = false,
|
||||
}) => {
|
||||
const emptyStateHeader = (
|
||||
export const EmptyState: React.FC<EmptyStateProps> = (props) => {
|
||||
const {
|
||||
type,
|
||||
size = "lg",
|
||||
layout = "screen-detailed",
|
||||
additionalPath = "",
|
||||
primaryButtonOnClick,
|
||||
primaryButtonLink,
|
||||
secondaryButtonOnClick,
|
||||
} = props;
|
||||
// store
|
||||
const {
|
||||
membership: { currentWorkspaceRole, currentProjectRole },
|
||||
} = useUser();
|
||||
// theme
|
||||
const { resolvedTheme } = useTheme();
|
||||
// current empty state details
|
||||
const { key, title, description, path, primaryButton, secondaryButton, accessType, access } =
|
||||
EMPTY_STATE_DETAILS[type];
|
||||
// resolved empty state path
|
||||
const resolvedEmptyStatePath = `${additionalPath && additionalPath !== "" ? `${path}${additionalPath}` : path}-${
|
||||
resolvedTheme === "light" ? "light" : "dark"
|
||||
}.webp`;
|
||||
// current access type
|
||||
const currentAccessType = accessType === "workspace" ? currentWorkspaceRole : currentProjectRole;
|
||||
// permission
|
||||
const isEditingAllowed = currentAccessType && access && currentAccessType >= access;
|
||||
const anyButton = primaryButton || secondaryButton;
|
||||
|
||||
// primary button
|
||||
const renderPrimaryButton = () => {
|
||||
if (!primaryButton) return null;
|
||||
|
||||
const commonProps = {
|
||||
size: size,
|
||||
variant: "primary" as TButtonVariant,
|
||||
prependIcon: primaryButton.icon,
|
||||
onClick: primaryButtonOnClick ? primaryButtonOnClick : undefined,
|
||||
disabled: !isEditingAllowed,
|
||||
};
|
||||
|
||||
if (primaryButton.comicBox) {
|
||||
return (
|
||||
<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 ? (
|
||||
<>
|
||||
<h3 className="text-xl font-semibold">{title}</h3>
|
||||
<p className="text-sm">{description}</p>
|
||||
</>
|
||||
) : (
|
||||
<h3 className="text-xl font-medium">{title}</h3>
|
||||
{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">
|
||||
<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">
|
||||
{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 { observer } from "mobx-react-lite";
|
||||
import { useRouter } from "next/router";
|
||||
import { useTheme } from "next-themes";
|
||||
// store hooks
|
||||
import { Button, Loader, TOAST_TYPE, setToast } from "@plane/ui";
|
||||
import { EmptyState, getEmptyStateImagePath } from "components/empty-state";
|
||||
import { CreateUpdateEstimateModal, DeleteEstimateModal, EstimateListItem } from "components/estimates";
|
||||
import { PROJECT_SETTINGS_EMPTY_STATE_DETAILS } from "constants/empty-state";
|
||||
import { orderArrayBy } from "helpers/array.helper";
|
||||
import { useEstimate, useProject, useUser } from "hooks/store";
|
||||
import { useEstimate, useProject } from "hooks/store";
|
||||
// components
|
||||
import { CreateUpdateEstimateModal, DeleteEstimateModal, EstimateListItem } from "components/estimates";
|
||||
import { EmptyState } from "components/empty-state";
|
||||
// ui
|
||||
import { Button, Loader, TOAST_TYPE, setToast } from "@plane/ui";
|
||||
// types
|
||||
import { IEstimate } from "@plane/types";
|
||||
// helpers
|
||||
import { orderArrayBy } from "helpers/array.helper";
|
||||
// constants
|
||||
import { EmptyStateType } from "constants/empty-state";
|
||||
|
||||
export const EstimatesList: React.FC = observer(() => {
|
||||
// states
|
||||
@ -24,12 +23,9 @@ export const EstimatesList: React.FC = observer(() => {
|
||||
// router
|
||||
const router = useRouter();
|
||||
const { workspaceSlug, projectId } = router.query;
|
||||
// theme
|
||||
const { resolvedTheme } = useTheme();
|
||||
// store hooks
|
||||
const { updateProject, currentProjectDetails } = useProject();
|
||||
const { projectEstimates, getProjectEstimateById } = useEstimate();
|
||||
const { currentUser } = useUser();
|
||||
|
||||
const editEstimate = (estimate: IEstimate) => {
|
||||
setEstimateFormOpen(true);
|
||||
@ -55,10 +51,6 @@ export const EstimatesList: React.FC = observer(() => {
|
||||
});
|
||||
};
|
||||
|
||||
const emptyStateDetail = PROJECT_SETTINGS_EMPTY_STATE_DETAILS["estimate"];
|
||||
const isLightMode = resolvedTheme ? resolvedTheme === "light" : currentUser?.theme.theme === "light";
|
||||
const emptyStateImage = getEmptyStateImagePath("project-settings", "estimates", isLightMode);
|
||||
|
||||
return (
|
||||
<>
|
||||
<CreateUpdateEstimateModal
|
||||
@ -113,12 +105,7 @@ export const EstimatesList: React.FC = observer(() => {
|
||||
</section>
|
||||
) : (
|
||||
<div className="h-full w-full py-8">
|
||||
<EmptyState
|
||||
title={emptyStateDetail.title}
|
||||
description={emptyStateDetail.description}
|
||||
image={emptyStateImage}
|
||||
size="lg"
|
||||
/>
|
||||
<EmptyState type={EmptyStateType.PROJECT_SETTINGS_ESTIMATE} />
|
||||
</div>
|
||||
)
|
||||
) : (
|
||||
|
@ -4,26 +4,24 @@ import { observer } from "mobx-react-lite";
|
||||
import Image from "next/image";
|
||||
import Link from "next/link";
|
||||
import { useRouter } from "next/router";
|
||||
import { useTheme } from "next-themes";
|
||||
import useSWR, { mutate } from "swr";
|
||||
// hooks
|
||||
import { MoveLeft, MoveRight, RefreshCw } from "lucide-react";
|
||||
import { Button } from "@plane/ui";
|
||||
import { EmptyState, getEmptyStateImagePath } from "components/empty-state";
|
||||
import { Exporter, SingleExport } from "components/exporter";
|
||||
import { ImportExportSettingsLoader } from "components/ui";
|
||||
import { WORKSPACE_SETTINGS_EMPTY_STATE_DETAILS } from "constants/empty-state";
|
||||
import { EXPORT_SERVICES_LIST } from "constants/fetch-keys";
|
||||
import { EXPORTERS_LIST } from "constants/workspace";
|
||||
import { useUser } from "hooks/store";
|
||||
import useUserAuth from "hooks/use-user-auth";
|
||||
// services
|
||||
import { IntegrationService } from "services/integrations";
|
||||
// components
|
||||
import { Exporter, SingleExport } from "components/exporter";
|
||||
import { ImportExportSettingsLoader } from "components/ui";
|
||||
import { EmptyState } from "components/empty-state";
|
||||
// ui
|
||||
import { Button } from "@plane/ui";
|
||||
// icons
|
||||
// fetch-keys
|
||||
import { MoveLeft, MoveRight, RefreshCw } from "lucide-react";
|
||||
// constants
|
||||
import { EXPORT_SERVICES_LIST } from "constants/fetch-keys";
|
||||
import { EXPORTERS_LIST } from "constants/workspace";
|
||||
import { EmptyStateType } from "constants/empty-state";
|
||||
|
||||
// services
|
||||
const integrationService = new IntegrationService();
|
||||
@ -36,8 +34,6 @@ const IntegrationGuide = observer(() => {
|
||||
// router
|
||||
const router = useRouter();
|
||||
const { workspaceSlug, provider } = router.query;
|
||||
// theme
|
||||
const { resolvedTheme } = useTheme();
|
||||
// store hooks
|
||||
const { currentUser, currentUserLoader } = useUser();
|
||||
// custom hooks
|
||||
@ -50,10 +46,6 @@ const IntegrationGuide = observer(() => {
|
||||
: null
|
||||
);
|
||||
|
||||
const emptyStateDetail = WORKSPACE_SETTINGS_EMPTY_STATE_DETAILS["export"];
|
||||
const isLightMode = resolvedTheme ? resolvedTheme === "light" : currentUser?.theme.theme === "light";
|
||||
const emptyStateImage = getEmptyStateImagePath("workspace-settings", "exports", isLightMode);
|
||||
|
||||
const handleCsvClose = () => {
|
||||
router.replace(`/${workspaceSlug?.toString()}/settings/exports`);
|
||||
};
|
||||
@ -149,12 +141,7 @@ const IntegrationGuide = observer(() => {
|
||||
</div>
|
||||
) : (
|
||||
<div className="h-full w-full flex items-center justify-center">
|
||||
<EmptyState
|
||||
title={emptyStateDetail.title}
|
||||
description={emptyStateDetail.description}
|
||||
image={emptyStateImage}
|
||||
size="sm"
|
||||
/>
|
||||
<EmptyState type={EmptyStateType.WORKSPACE_SETTINGS_EXPORT} size="sm" />
|
||||
</div>
|
||||
)
|
||||
) : (
|
||||
|
@ -3,28 +3,26 @@ import { observer } from "mobx-react-lite";
|
||||
import Image from "next/image";
|
||||
import Link from "next/link";
|
||||
import { useRouter } from "next/router";
|
||||
import { useTheme } from "next-themes";
|
||||
import useSWR, { mutate } from "swr";
|
||||
// hooks
|
||||
import { RefreshCw } from "lucide-react";
|
||||
import { Button } from "@plane/ui";
|
||||
import { EmptyState, getEmptyStateImagePath } from "components/empty-state";
|
||||
import { DeleteImportModal, GithubImporterRoot, JiraImporterRoot, SingleImport } from "components/integration";
|
||||
import { ImportExportSettingsLoader } from "components/ui";
|
||||
import { WORKSPACE_SETTINGS_EMPTY_STATE_DETAILS } from "constants/empty-state";
|
||||
import { IMPORTER_SERVICES_LIST } from "constants/fetch-keys";
|
||||
import { IMPORTERS_LIST } from "constants/workspace";
|
||||
import { useUser } from "hooks/store";
|
||||
import useUserAuth from "hooks/use-user-auth";
|
||||
// services
|
||||
import { IntegrationService } from "services/integrations";
|
||||
// components
|
||||
import { ImportExportSettingsLoader } from "components/ui";
|
||||
import { DeleteImportModal, GithubImporterRoot, JiraImporterRoot, SingleImport } from "components/integration";
|
||||
import { EmptyState } from "components/empty-state";
|
||||
// ui
|
||||
import { Button } from "@plane/ui";
|
||||
// icons
|
||||
import { RefreshCw } from "lucide-react";
|
||||
// types
|
||||
import { IImporterService } from "@plane/types";
|
||||
// fetch-keys
|
||||
// constants
|
||||
import { IMPORTER_SERVICES_LIST } from "constants/fetch-keys";
|
||||
import { IMPORTERS_LIST } from "constants/workspace";
|
||||
import { EmptyStateType } from "constants/empty-state";
|
||||
|
||||
// services
|
||||
const integrationService = new IntegrationService();
|
||||
@ -37,8 +35,6 @@ const IntegrationGuide = observer(() => {
|
||||
// router
|
||||
const router = useRouter();
|
||||
const { workspaceSlug, provider } = router.query;
|
||||
// theme
|
||||
const { resolvedTheme } = useTheme();
|
||||
// store hooks
|
||||
const { currentUser, currentUserLoader } = useUser();
|
||||
// custom hooks
|
||||
@ -49,10 +45,6 @@ const IntegrationGuide = observer(() => {
|
||||
workspaceSlug ? () => integrationService.getImporterServicesList(workspaceSlug as string) : null
|
||||
);
|
||||
|
||||
const emptyStateDetail = WORKSPACE_SETTINGS_EMPTY_STATE_DETAILS["import"];
|
||||
const isLightMode = resolvedTheme ? resolvedTheme === "light" : currentUser?.theme.theme === "light";
|
||||
const emptyStateImage = getEmptyStateImagePath("workspace-settings", "imports", isLightMode);
|
||||
|
||||
const handleDeleteImport = (importService: IImporterService) => {
|
||||
setImportToDelete(importService);
|
||||
setDeleteImportModal(true);
|
||||
@ -145,12 +137,7 @@ const IntegrationGuide = observer(() => {
|
||||
</div>
|
||||
) : (
|
||||
<div className="h-full w-full flex items-center justify-center">
|
||||
<EmptyState
|
||||
title={emptyStateDetail.title}
|
||||
description={emptyStateDetail.description}
|
||||
image={emptyStateImage}
|
||||
size="sm"
|
||||
/>
|
||||
<EmptyState type={EmptyStateType.WORKSPACE_SETTINGS_IMPORT} size="sm" />
|
||||
</div>
|
||||
)
|
||||
) : (
|
||||
|
@ -29,12 +29,21 @@ interface IBaseCalendarRoot {
|
||||
[EIssueActions.ARCHIVE]?: (issue: TIssue) => Promise<void>;
|
||||
[EIssueActions.RESTORE]?: (issue: TIssue) => Promise<void>;
|
||||
};
|
||||
addIssuesToView?: (issueIds: string[]) => Promise<any>;
|
||||
viewId?: string;
|
||||
isCompletedCycle?: boolean;
|
||||
}
|
||||
|
||||
export const BaseCalendarRoot = observer((props: IBaseCalendarRoot) => {
|
||||
const { issueStore, issuesFilterStore, QuickActions, issueActions, viewId, isCompletedCycle = false } = props;
|
||||
const {
|
||||
issueStore,
|
||||
issuesFilterStore,
|
||||
QuickActions,
|
||||
issueActions,
|
||||
addIssuesToView,
|
||||
viewId,
|
||||
isCompletedCycle = false,
|
||||
} = props;
|
||||
|
||||
// router
|
||||
const router = useRouter();
|
||||
@ -128,6 +137,7 @@ export const BaseCalendarRoot = observer((props: IBaseCalendarRoot) => {
|
||||
readOnly={!isEditingAllowed || isCompletedCycle}
|
||||
/>
|
||||
)}
|
||||
addIssuesToView={addIssuesToView}
|
||||
quickAddCallback={issueStore.quickAddIssue}
|
||||
viewId={viewId}
|
||||
readOnly={!isEditingAllowed || isCompletedCycle}
|
||||
|
@ -30,6 +30,7 @@ type Props = {
|
||||
data: TIssue,
|
||||
viewId?: string
|
||||
) => Promise<TIssue | undefined>;
|
||||
addIssuesToView?: (issueIds: string[]) => Promise<any>;
|
||||
viewId?: string;
|
||||
readOnly?: boolean;
|
||||
};
|
||||
@ -43,6 +44,7 @@ export const CalendarChart: React.FC<Props> = observer((props) => {
|
||||
showWeekends,
|
||||
quickActions,
|
||||
quickAddCallback,
|
||||
addIssuesToView,
|
||||
viewId,
|
||||
readOnly = false,
|
||||
} = props;
|
||||
@ -90,6 +92,7 @@ export const CalendarChart: React.FC<Props> = observer((props) => {
|
||||
disableIssueCreation={!enableIssueCreation || !isEditingAllowed}
|
||||
quickActions={quickActions}
|
||||
quickAddCallback={quickAddCallback}
|
||||
addIssuesToView={addIssuesToView}
|
||||
viewId={viewId}
|
||||
readOnly={readOnly}
|
||||
/>
|
||||
@ -106,6 +109,7 @@ export const CalendarChart: React.FC<Props> = observer((props) => {
|
||||
disableIssueCreation={!enableIssueCreation || !isEditingAllowed}
|
||||
quickActions={quickActions}
|
||||
quickAddCallback={quickAddCallback}
|
||||
addIssuesToView={addIssuesToView}
|
||||
viewId={viewId}
|
||||
readOnly={readOnly}
|
||||
/>
|
||||
|
@ -4,9 +4,10 @@ import { observer } from "mobx-react-lite";
|
||||
// components
|
||||
import { CalendarIssueBlocks, ICalendarDate, CalendarQuickAddIssueForm } from "components/issues";
|
||||
// helpers
|
||||
import { renderFormattedPayloadDate } from "helpers/date-time.helper";
|
||||
// constants
|
||||
import { MONTHS_LIST } from "constants/calendar";
|
||||
import { renderFormattedPayloadDate } from "helpers/date-time.helper";
|
||||
// types
|
||||
import { ICycleIssuesFilter } from "store/issue/cycle";
|
||||
import { IModuleIssuesFilter } from "store/issue/module";
|
||||
import { IProjectIssuesFilter } from "store/issue/project";
|
||||
@ -27,6 +28,7 @@ type Props = {
|
||||
data: TIssue,
|
||||
viewId?: string
|
||||
) => Promise<TIssue | undefined>;
|
||||
addIssuesToView?: (issueIds: string[]) => Promise<any>;
|
||||
viewId?: string;
|
||||
readOnly?: boolean;
|
||||
};
|
||||
@ -41,6 +43,7 @@ export const CalendarDayTile: React.FC<Props> = observer((props) => {
|
||||
enableQuickIssueCreate,
|
||||
disableIssueCreation,
|
||||
quickAddCallback,
|
||||
addIssuesToView,
|
||||
viewId,
|
||||
readOnly = false,
|
||||
} = props;
|
||||
@ -112,6 +115,7 @@ export const CalendarDayTile: React.FC<Props> = observer((props) => {
|
||||
target_date: renderFormattedPayloadDate(date.date) ?? undefined,
|
||||
}}
|
||||
quickAddCallback={quickAddCallback}
|
||||
addIssuesToView={addIssuesToView}
|
||||
viewId={viewId}
|
||||
onOpen={() => setShowAllIssues(true)}
|
||||
/>
|
||||
|
@ -2,20 +2,24 @@ import { useEffect, useRef, useState } from "react";
|
||||
import { observer } from "mobx-react-lite";
|
||||
import { useRouter } from "next/router";
|
||||
import { useForm } from "react-hook-form";
|
||||
// components
|
||||
import { ExistingIssuesListModal } from "components/core";
|
||||
// hooks
|
||||
import { PlusIcon } from "lucide-react";
|
||||
import { TOAST_TYPE, setPromiseToast, setToast } from "@plane/ui";
|
||||
import { ISSUE_CREATED } from "constants/event-tracker";
|
||||
import { createIssuePayload } from "helpers/issue.helper";
|
||||
import { useEventTracker, useProject } from "hooks/store";
|
||||
import { useEventTracker, useIssueDetail, useProject } from "hooks/store";
|
||||
import useKeypress from "hooks/use-keypress";
|
||||
import useOutsideClickDetector from "hooks/use-outside-click-detector";
|
||||
// helpers
|
||||
import { createIssuePayload } from "helpers/issue.helper";
|
||||
// icons
|
||||
import { PlusIcon } from "lucide-react";
|
||||
// ui
|
||||
import { TOAST_TYPE, setPromiseToast, setToast, CustomMenu } from "@plane/ui";
|
||||
// types
|
||||
import { TIssue } from "@plane/types";
|
||||
import { ISearchIssueResponse, TIssue } from "@plane/types";
|
||||
// constants
|
||||
import { ISSUE_CREATED } from "constants/event-tracker";
|
||||
// helper
|
||||
import { cn } from "helpers/common.helper";
|
||||
|
||||
type Props = {
|
||||
formKey: keyof TIssue;
|
||||
@ -28,6 +32,7 @@ type Props = {
|
||||
data: TIssue,
|
||||
viewId?: string
|
||||
) => Promise<TIssue | undefined>;
|
||||
addIssuesToView?: (issueIds: string[]) => Promise<any>;
|
||||
viewId?: string;
|
||||
onOpen?: () => void;
|
||||
};
|
||||
@ -60,21 +65,26 @@ const Inputs = (props: any) => {
|
||||
};
|
||||
|
||||
export const CalendarQuickAddIssueForm: React.FC<Props> = observer((props) => {
|
||||
const { formKey, prePopulatedData, quickAddCallback, viewId, onOpen } = props;
|
||||
const { formKey, prePopulatedData, quickAddCallback, addIssuesToView, viewId, onOpen } = props;
|
||||
|
||||
// router
|
||||
const router = useRouter();
|
||||
const { workspaceSlug, projectId } = router.query;
|
||||
const { workspaceSlug, projectId, moduleId } = router.query;
|
||||
// store hooks
|
||||
const { getProjectById } = useProject();
|
||||
const { captureIssueEvent } = useEventTracker();
|
||||
const { updateIssue } = useIssueDetail();
|
||||
// refs
|
||||
const ref = useRef<HTMLDivElement>(null);
|
||||
// states
|
||||
const [isOpen, setIsOpen] = useState(false);
|
||||
|
||||
const [isMenuOpen, setIsMenuOpen] = useState(false);
|
||||
const [isExistingIssueModalOpen, setIsExistingIssueModalOpen] = useState(false);
|
||||
// derived values
|
||||
const projectDetail = projectId ? getProjectById(projectId.toString()) : null;
|
||||
const ExistingIssuesListModalPayload = moduleId
|
||||
? { module: moduleId.toString(), target_date: "none" }
|
||||
: { cycle: true, target_date: "none" };
|
||||
|
||||
const {
|
||||
reset,
|
||||
@ -158,13 +168,50 @@ export const CalendarQuickAddIssueForm: React.FC<Props> = observer((props) => {
|
||||
}
|
||||
};
|
||||
|
||||
const handleOpen = () => {
|
||||
const handleAddIssuesToView = async (data: ISearchIssueResponse[]) => {
|
||||
if (!workspaceSlug || !projectId) return;
|
||||
|
||||
const issueIds = data.map((i) => i.id);
|
||||
|
||||
try {
|
||||
// To handle all updates in parallel
|
||||
await Promise.all(
|
||||
data.map((issue) =>
|
||||
updateIssue(workspaceSlug.toString(), projectId.toString(), issue.id, prePopulatedData ?? {})
|
||||
)
|
||||
);
|
||||
if (addIssuesToView) {
|
||||
await addIssuesToView(issueIds);
|
||||
}
|
||||
} catch (error) {
|
||||
setToast({
|
||||
type: TOAST_TYPE.ERROR,
|
||||
title: "Error!",
|
||||
message: "Something went wrong. Please try again.",
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const handleNewIssue = () => {
|
||||
setIsOpen(true);
|
||||
if (onOpen) onOpen();
|
||||
};
|
||||
const handleExistingIssue = () => {
|
||||
setIsExistingIssueModalOpen(true);
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
{workspaceSlug && projectId && (
|
||||
<ExistingIssuesListModal
|
||||
workspaceSlug={workspaceSlug.toString()}
|
||||
projectId={projectId.toString()}
|
||||
isOpen={isExistingIssueModalOpen}
|
||||
handleClose={() => setIsExistingIssueModalOpen(false)}
|
||||
searchParams={ExistingIssuesListModalPayload}
|
||||
handleOnSubmit={handleAddIssuesToView}
|
||||
/>
|
||||
)}
|
||||
{isOpen && (
|
||||
<div
|
||||
ref={ref}
|
||||
@ -182,15 +229,38 @@ export const CalendarQuickAddIssueForm: React.FC<Props> = observer((props) => {
|
||||
)}
|
||||
|
||||
{!isOpen && (
|
||||
<div className="hidden rounded border-[0.5px] border-custom-border-200 group-hover:block">
|
||||
<button
|
||||
type="button"
|
||||
className="flex w-full items-center gap-x-[6px] rounded-md px-2 py-1.5 text-custom-primary-100"
|
||||
onClick={handleOpen}
|
||||
>
|
||||
<PlusIcon className="h-3.5 w-3.5 stroke-2" />
|
||||
<span className="text-sm font-medium text-custom-primary-100">New Issue</span>
|
||||
</button>
|
||||
<div
|
||||
className={cn("hidden rounded border-[0.5px] border-custom-border-200 group-hover:block", {
|
||||
block: isMenuOpen,
|
||||
})}
|
||||
>
|
||||
{addIssuesToView ? (
|
||||
<CustomMenu
|
||||
placement="bottom-start"
|
||||
menuButtonOnClick={() => setIsMenuOpen(true)}
|
||||
onMenuClose={() => setIsMenuOpen(false)}
|
||||
className="w-full"
|
||||
customButtonClassName="w-full"
|
||||
customButton={
|
||||
<div className="flex w-full items-center gap-x-[6px] rounded-md px-2 py-1.5 text-custom-primary-100">
|
||||
<PlusIcon className="h-3.5 w-3.5 stroke-2 flex-shrink-0" />
|
||||
<span className="text-sm font-medium flex-shrink-0 text-custom-primary-100">New Issue</span>
|
||||
</div>
|
||||
}
|
||||
>
|
||||
<CustomMenu.MenuItem onClick={handleNewIssue}>New Issue</CustomMenu.MenuItem>
|
||||
<CustomMenu.MenuItem onClick={handleExistingIssue}>Add existing issue</CustomMenu.MenuItem>
|
||||
</CustomMenu>
|
||||
) : (
|
||||
<button
|
||||
type="button"
|
||||
className="flex w-full items-center gap-x-[6px] rounded-md px-2 py-1.5 text-custom-primary-100"
|
||||
onClick={handleNewIssue}
|
||||
>
|
||||
<PlusIcon className="h-3.5 w-3.5 stroke-2" />
|
||||
<span className="text-sm font-medium text-custom-primary-100">New Issue</span>
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
|
@ -1,15 +1,16 @@
|
||||
import { useMemo } from "react";
|
||||
import { useCallback, useMemo } from "react";
|
||||
import { observer } from "mobx-react-lite";
|
||||
import { useRouter } from "next/router";
|
||||
//hooks
|
||||
import { CycleIssueQuickActions } from "components/issues";
|
||||
import { EIssuesStoreType } from "constants/issue";
|
||||
import { useCycle, useIssues } from "hooks/store";
|
||||
// components
|
||||
import { CycleIssueQuickActions } from "components/issues";
|
||||
import { BaseCalendarRoot } from "../base-calendar-root";
|
||||
// types
|
||||
import { TIssue } from "@plane/types";
|
||||
import { EIssueActions } from "../../types";
|
||||
import { BaseCalendarRoot } from "../base-calendar-root";
|
||||
// constants
|
||||
import { EIssuesStoreType } from "constants/issue";
|
||||
|
||||
export const CycleCalendarLayout: React.FC = observer(() => {
|
||||
const { issues, issuesFilter } = useIssues(EIssuesStoreType.CYCLE);
|
||||
@ -46,11 +47,20 @@ export const CycleCalendarLayout: React.FC = observer(() => {
|
||||
const isCompletedCycle =
|
||||
cycleId && currentProjectCompletedCycleIds ? currentProjectCompletedCycleIds.includes(cycleId.toString()) : false;
|
||||
|
||||
const addIssuesToView = useCallback(
|
||||
(issueIds: string[]) => {
|
||||
if (!workspaceSlug || !projectId || !cycleId) throw new Error();
|
||||
return issues.addIssueToCycle(workspaceSlug.toString(), projectId.toString(), cycleId.toString(), issueIds);
|
||||
},
|
||||
[issues?.addIssueToCycle, workspaceSlug, projectId, cycleId]
|
||||
);
|
||||
|
||||
return (
|
||||
<BaseCalendarRoot
|
||||
issueStore={issues}
|
||||
issuesFilterStore={issuesFilter}
|
||||
QuickActions={CycleIssueQuickActions}
|
||||
addIssuesToView={addIssuesToView}
|
||||
issueActions={issueActions}
|
||||
viewId={cycleId.toString()}
|
||||
isCompletedCycle={isCompletedCycle}
|
||||
|
@ -1,20 +1,21 @@
|
||||
import { useMemo } from "react";
|
||||
import { useCallback, useMemo } from "react";
|
||||
import { observer } from "mobx-react-lite";
|
||||
import { useRouter } from "next/router";
|
||||
// hoks
|
||||
import { ModuleIssueQuickActions } from "components/issues";
|
||||
import { EIssuesStoreType } from "constants/issue";
|
||||
// hooks
|
||||
import { useIssues } from "hooks/store";
|
||||
// components
|
||||
import { ModuleIssueQuickActions } from "components/issues";
|
||||
import { BaseCalendarRoot } from "../base-calendar-root";
|
||||
// types
|
||||
import { TIssue } from "@plane/types";
|
||||
import { EIssueActions } from "../../types";
|
||||
import { BaseCalendarRoot } from "../base-calendar-root";
|
||||
// constants
|
||||
import { EIssuesStoreType } from "constants/issue";
|
||||
|
||||
export const ModuleCalendarLayout: React.FC = observer(() => {
|
||||
const { issues, issuesFilter } = useIssues(EIssuesStoreType.MODULE);
|
||||
const router = useRouter();
|
||||
const { workspaceSlug, moduleId } = router.query as {
|
||||
const { workspaceSlug, projectId, moduleId } = router.query as {
|
||||
workspaceSlug: string;
|
||||
projectId: string;
|
||||
moduleId: string;
|
||||
@ -42,12 +43,21 @@ export const ModuleCalendarLayout: React.FC = observer(() => {
|
||||
[issues, workspaceSlug, moduleId]
|
||||
);
|
||||
|
||||
const addIssuesToView = useCallback(
|
||||
(issueIds: string[]) => {
|
||||
if (!workspaceSlug || !projectId || !moduleId) throw new Error();
|
||||
return issues.addIssuesToModule(workspaceSlug.toString(), projectId.toString(), moduleId.toString(), issueIds);
|
||||
},
|
||||
[issues?.addIssuesToModule, workspaceSlug, projectId, moduleId]
|
||||
);
|
||||
|
||||
return (
|
||||
<BaseCalendarRoot
|
||||
issueStore={issues}
|
||||
issuesFilterStore={issuesFilter}
|
||||
QuickActions={ModuleIssueQuickActions}
|
||||
issueActions={issueActions}
|
||||
addIssuesToView={addIssuesToView}
|
||||
viewId={moduleId}
|
||||
/>
|
||||
);
|
||||
|
@ -25,6 +25,7 @@ type Props = {
|
||||
data: TIssue,
|
||||
viewId?: string
|
||||
) => Promise<TIssue | undefined>;
|
||||
addIssuesToView?: (issueIds: string[]) => Promise<any>;
|
||||
viewId?: string;
|
||||
readOnly?: boolean;
|
||||
};
|
||||
@ -39,6 +40,7 @@ export const CalendarWeekDays: React.FC<Props> = observer((props) => {
|
||||
enableQuickIssueCreate,
|
||||
disableIssueCreation,
|
||||
quickAddCallback,
|
||||
addIssuesToView,
|
||||
viewId,
|
||||
readOnly = false,
|
||||
} = props;
|
||||
@ -68,6 +70,7 @@ export const CalendarWeekDays: React.FC<Props> = observer((props) => {
|
||||
enableQuickIssueCreate={enableQuickIssueCreate}
|
||||
disableIssueCreation={disableIssueCreation}
|
||||
quickAddCallback={quickAddCallback}
|
||||
addIssuesToView={addIssuesToView}
|
||||
viewId={viewId}
|
||||
readOnly={readOnly}
|
||||
/>
|
||||
|
@ -1,49 +1,27 @@
|
||||
import size from "lodash/size";
|
||||
import { observer } from "mobx-react-lite";
|
||||
import { useRouter } from "next/router";
|
||||
import { useTheme } from "next-themes";
|
||||
// hooks
|
||||
import { EmptyState, getEmptyStateImagePath } from "components/empty-state";
|
||||
import { EMPTY_FILTER_STATE_DETAILS, EMPTY_ISSUE_STATE_DETAILS } from "constants/empty-state";
|
||||
import { EIssueFilterType, EIssuesStoreType } from "constants/issue";
|
||||
import { EUserProjectRoles } from "constants/project";
|
||||
import { useIssues, useUser } from "hooks/store";
|
||||
import { useIssues } from "hooks/store";
|
||||
// components
|
||||
import { EmptyState } from "components/empty-state";
|
||||
// constants
|
||||
import { EIssueFilterType, EIssuesStoreType } from "constants/issue";
|
||||
import { EmptyStateType } from "constants/empty-state";
|
||||
// types
|
||||
import { IIssueFilterOptions } from "@plane/types";
|
||||
|
||||
interface EmptyStateProps {
|
||||
title: string;
|
||||
image: string;
|
||||
description?: string;
|
||||
comicBox?: { title: string; description: string };
|
||||
primaryButton?: { text: string; icon?: React.ReactNode; onClick: () => void };
|
||||
secondaryButton?: { text: string; onClick: () => void };
|
||||
size?: "lg" | "sm" | undefined;
|
||||
disabled?: boolean | undefined;
|
||||
}
|
||||
|
||||
export const ProjectArchivedEmptyState: React.FC = observer(() => {
|
||||
// router
|
||||
const router = useRouter();
|
||||
const { workspaceSlug, projectId } = router.query;
|
||||
// theme
|
||||
const { resolvedTheme } = useTheme();
|
||||
// store hooks
|
||||
const {
|
||||
membership: { currentProjectRole },
|
||||
currentUser,
|
||||
} = useUser();
|
||||
const { issuesFilter } = useIssues(EIssuesStoreType.ARCHIVED);
|
||||
|
||||
const userFilters = issuesFilter?.issueFilters?.filters;
|
||||
const activeLayout = issuesFilter?.issueFilters?.displayFilters?.layout;
|
||||
|
||||
const isLightMode = resolvedTheme ? resolvedTheme === "light" : currentUser?.theme.theme === "light";
|
||||
const currentLayoutEmptyStateImagePath = getEmptyStateImagePath("empty-filters", activeLayout ?? "list", isLightMode);
|
||||
const EmptyStateImagePath = getEmptyStateImagePath("archived", "empty-issues", isLightMode);
|
||||
|
||||
const issueFilterCount = size(
|
||||
Object.fromEntries(
|
||||
Object.entries(userFilters ?? {}).filter(([, value]) => value && Array.isArray(value) && value.length > 0)
|
||||
@ -61,33 +39,20 @@ export const ProjectArchivedEmptyState: React.FC = observer(() => {
|
||||
});
|
||||
};
|
||||
|
||||
const isEditingAllowed = !!currentProjectRole && currentProjectRole >= EUserProjectRoles.MEMBER;
|
||||
|
||||
const emptyStateProps: EmptyStateProps =
|
||||
issueFilterCount > 0
|
||||
? {
|
||||
title: EMPTY_FILTER_STATE_DETAILS["archived"].title,
|
||||
image: currentLayoutEmptyStateImagePath,
|
||||
secondaryButton: {
|
||||
text: EMPTY_FILTER_STATE_DETAILS["archived"].secondaryButton?.text,
|
||||
onClick: handleClearAllFilters,
|
||||
},
|
||||
}
|
||||
: {
|
||||
title: EMPTY_ISSUE_STATE_DETAILS["archived"].title,
|
||||
description: EMPTY_ISSUE_STATE_DETAILS["archived"].description,
|
||||
image: EmptyStateImagePath,
|
||||
primaryButton: {
|
||||
text: EMPTY_ISSUE_STATE_DETAILS["archived"].primaryButton.text,
|
||||
onClick: () => router.push(`/${workspaceSlug}/projects/${projectId}/settings/automations`),
|
||||
},
|
||||
size: "sm",
|
||||
disabled: !isEditingAllowed,
|
||||
};
|
||||
const emptyStateType =
|
||||
issueFilterCount > 0 ? EmptyStateType.PROJECT_ARCHIVED_EMPTY_FILTER : EmptyStateType.PROJECT_ARCHIVED_NO_ISSUES;
|
||||
const additionalPath = issueFilterCount > 0 ? activeLayout ?? "list" : undefined;
|
||||
|
||||
return (
|
||||
<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>
|
||||
);
|
||||
});
|
||||
|
@ -1,21 +1,17 @@
|
||||
import { useState } from "react";
|
||||
import { observer } from "mobx-react-lite";
|
||||
import { useTheme } from "next-themes";
|
||||
import { PlusIcon } from "lucide-react";
|
||||
// hooks
|
||||
import { useApplication, useEventTracker, useIssues } from "hooks/store";
|
||||
// ui
|
||||
import { TOAST_TYPE, setToast } from "@plane/ui";
|
||||
import { ExistingIssuesListModal } from "components/core";
|
||||
// ui
|
||||
// components
|
||||
import { EmptyState, getEmptyStateImagePath } from "components/empty-state";
|
||||
import { CYCLE_EMPTY_STATE_DETAILS, EMPTY_FILTER_STATE_DETAILS } from "constants/empty-state";
|
||||
import { EIssuesStoreType } from "constants/issue";
|
||||
import { EUserProjectRoles } from "constants/project";
|
||||
import { useApplication, useEventTracker, useIssues, useUser } from "hooks/store";
|
||||
// components
|
||||
import { EmptyState } from "components/empty-state";
|
||||
// types
|
||||
import { ISearchIssueResponse, TIssueLayouts } from "@plane/types";
|
||||
// constants
|
||||
import { EIssuesStoreType } from "constants/issue";
|
||||
import { EmptyStateType } from "constants/empty-state";
|
||||
|
||||
type Props = {
|
||||
workspaceSlug: string | undefined;
|
||||
@ -26,33 +22,16 @@ type Props = {
|
||||
isEmptyFilters?: boolean;
|
||||
};
|
||||
|
||||
interface EmptyStateProps {
|
||||
title: string;
|
||||
image: string;
|
||||
description?: string;
|
||||
comicBox?: { title: string; description: string };
|
||||
primaryButton?: { text: string; icon?: React.ReactNode; onClick: () => void };
|
||||
secondaryButton?: { text: string; icon?: React.ReactNode; onClick: () => void };
|
||||
size?: "lg" | "sm" | undefined;
|
||||
disabled?: boolean | undefined;
|
||||
}
|
||||
|
||||
export const CycleEmptyState: React.FC<Props> = observer((props) => {
|
||||
const { workspaceSlug, projectId, cycleId, activeLayout, handleClearAllFilters, isEmptyFilters = false } = props;
|
||||
// states
|
||||
const [cycleIssuesListModal, setCycleIssuesListModal] = useState(false);
|
||||
// theme
|
||||
const { resolvedTheme } = useTheme();
|
||||
// store hooks
|
||||
const { issues } = useIssues(EIssuesStoreType.CYCLE);
|
||||
const {
|
||||
commandPalette: { toggleCreateIssueModal },
|
||||
} = useApplication();
|
||||
const { setTrackElement } = useEventTracker();
|
||||
const {
|
||||
membership: { currentProjectRole: userRole },
|
||||
currentUser,
|
||||
} = useUser();
|
||||
|
||||
const handleAddIssuesToCycle = async (data: ISearchIssueResponse[]) => {
|
||||
if (!workspaceSlug || !projectId || !cycleId) return;
|
||||
@ -77,43 +56,9 @@ export const CycleEmptyState: React.FC<Props> = observer((props) => {
|
||||
);
|
||||
};
|
||||
|
||||
const emptyStateDetail = CYCLE_EMPTY_STATE_DETAILS["no-issues"];
|
||||
|
||||
const isLightMode = resolvedTheme ? resolvedTheme === "light" : currentUser?.theme.theme === "light";
|
||||
const currentLayoutEmptyStateImagePath = getEmptyStateImagePath("empty-filters", activeLayout ?? "list", isLightMode);
|
||||
const emptyStateImage = getEmptyStateImagePath("cycle-issues", activeLayout ?? "list", isLightMode);
|
||||
|
||||
const isEditingAllowed = !!userRole && userRole >= EUserProjectRoles.MEMBER;
|
||||
|
||||
const emptyStateProps: EmptyStateProps = isEmptyFilters
|
||||
? {
|
||||
title: EMPTY_FILTER_STATE_DETAILS["project"].title,
|
||||
image: currentLayoutEmptyStateImagePath,
|
||||
secondaryButton: {
|
||||
text: EMPTY_FILTER_STATE_DETAILS["project"].secondaryButton.text,
|
||||
onClick: handleClearAllFilters,
|
||||
},
|
||||
}
|
||||
: {
|
||||
title: emptyStateDetail.title,
|
||||
description: emptyStateDetail.description,
|
||||
image: emptyStateImage,
|
||||
primaryButton: {
|
||||
text: emptyStateDetail.primaryButton.text,
|
||||
icon: <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,
|
||||
};
|
||||
const emptyStateType = isEmptyFilters ? EmptyStateType.PROJECT_EMPTY_FILTER : EmptyStateType.PROJECT_CYCLE_NO_ISSUES;
|
||||
const additionalPath = activeLayout ?? "list";
|
||||
const emptyStateSize = isEmptyFilters ? "lg" : "sm";
|
||||
|
||||
return (
|
||||
<>
|
||||
@ -126,7 +71,20 @@ export const CycleEmptyState: React.FC<Props> = observer((props) => {
|
||||
handleOnSubmit={handleAddIssuesToCycle}
|
||||
/>
|
||||
<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>
|
||||
</>
|
||||
);
|
||||
|
@ -1,49 +1,26 @@
|
||||
import size from "lodash/size";
|
||||
import { observer } from "mobx-react-lite";
|
||||
import { useRouter } from "next/router";
|
||||
import { useTheme } from "next-themes";
|
||||
// hooks
|
||||
import { EmptyState, getEmptyStateImagePath } from "components/empty-state";
|
||||
import { EMPTY_FILTER_STATE_DETAILS, EMPTY_ISSUE_STATE_DETAILS } from "constants/empty-state";
|
||||
import { EIssueFilterType, EIssuesStoreType } from "constants/issue";
|
||||
import { EUserProjectRoles } from "constants/project";
|
||||
import { useIssues, useUser } from "hooks/store";
|
||||
import { useIssues } from "hooks/store";
|
||||
// components
|
||||
import { EmptyState } from "components/empty-state";
|
||||
// constants
|
||||
import { EIssueFilterType, EIssuesStoreType } from "constants/issue";
|
||||
import { EmptyStateType } from "constants/empty-state";
|
||||
// types
|
||||
import { IIssueFilterOptions } from "@plane/types";
|
||||
|
||||
interface EmptyStateProps {
|
||||
title: string;
|
||||
image: string;
|
||||
description?: string;
|
||||
comicBox?: { title: string; description: string };
|
||||
primaryButton?: { text: string; icon?: React.ReactNode; onClick: () => void };
|
||||
secondaryButton?: { text: string; onClick: () => void };
|
||||
size?: "lg" | "sm" | undefined;
|
||||
disabled?: boolean | undefined;
|
||||
}
|
||||
|
||||
export const ProjectDraftEmptyState: React.FC = observer(() => {
|
||||
// router
|
||||
const router = useRouter();
|
||||
const { workspaceSlug, projectId } = router.query;
|
||||
// theme
|
||||
const { resolvedTheme } = useTheme();
|
||||
// store hooks
|
||||
const {
|
||||
membership: { currentProjectRole },
|
||||
currentUser,
|
||||
} = useUser();
|
||||
const { issuesFilter } = useIssues(EIssuesStoreType.DRAFT);
|
||||
|
||||
const userFilters = issuesFilter?.issueFilters?.filters;
|
||||
const activeLayout = issuesFilter?.issueFilters?.displayFilters?.layout;
|
||||
|
||||
const isLightMode = resolvedTheme ? resolvedTheme === "light" : currentUser?.theme.theme === "light";
|
||||
const currentLayoutEmptyStateImagePath = getEmptyStateImagePath("empty-filters", activeLayout ?? "list", isLightMode);
|
||||
const EmptyStateImagePath = getEmptyStateImagePath("draft", "draft-issues-empty", isLightMode);
|
||||
|
||||
const issueFilterCount = size(
|
||||
Object.fromEntries(
|
||||
Object.entries(userFilters ?? {}).filter(([, value]) => value && Array.isArray(value) && value.length > 0)
|
||||
@ -61,29 +38,19 @@ export const ProjectDraftEmptyState: React.FC = observer(() => {
|
||||
});
|
||||
};
|
||||
|
||||
const isEditingAllowed = !!currentProjectRole && currentProjectRole >= EUserProjectRoles.MEMBER;
|
||||
|
||||
const emptyStateProps: EmptyStateProps =
|
||||
issueFilterCount > 0
|
||||
? {
|
||||
title: EMPTY_FILTER_STATE_DETAILS["draft"].title,
|
||||
image: currentLayoutEmptyStateImagePath,
|
||||
secondaryButton: {
|
||||
text: EMPTY_FILTER_STATE_DETAILS["draft"].secondaryButton.text,
|
||||
onClick: handleClearAllFilters,
|
||||
},
|
||||
}
|
||||
: {
|
||||
title: EMPTY_ISSUE_STATE_DETAILS["draft"].title,
|
||||
description: EMPTY_ISSUE_STATE_DETAILS["draft"].description,
|
||||
image: EmptyStateImagePath,
|
||||
size: "sm",
|
||||
disabled: !isEditingAllowed,
|
||||
};
|
||||
const emptyStateType =
|
||||
issueFilterCount > 0 ? EmptyStateType.PROJECT_DRAFT_EMPTY_FILTER : EmptyStateType.PROJECT_DRAFT_NO_ISSUES;
|
||||
const additionalPath = issueFilterCount > 0 ? activeLayout ?? "list" : undefined;
|
||||
const emptyStateSize = issueFilterCount > 0 ? "lg" : "sm";
|
||||
|
||||
return (
|
||||
<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>
|
||||
);
|
||||
});
|
||||
|
@ -1,20 +1,18 @@
|
||||
import { useState } from "react";
|
||||
import { observer } from "mobx-react-lite";
|
||||
import { useTheme } from "next-themes";
|
||||
import { PlusIcon } from "lucide-react";
|
||||
// hooks
|
||||
import { useApplication, useEventTracker, useIssues } from "hooks/store";
|
||||
// ui
|
||||
import { TOAST_TYPE, setToast } from "@plane/ui";
|
||||
import { ExistingIssuesListModal } from "components/core";
|
||||
import { EmptyState, getEmptyStateImagePath } from "components/empty-state";
|
||||
import { EMPTY_FILTER_STATE_DETAILS, MODULE_EMPTY_STATE_DETAILS } from "constants/empty-state";
|
||||
import { EIssuesStoreType } from "constants/issue";
|
||||
import { EUserProjectRoles } from "constants/project";
|
||||
import { useApplication, useEventTracker, useIssues, useUser } from "hooks/store";
|
||||
// ui
|
||||
// components
|
||||
import { ExistingIssuesListModal } from "components/core";
|
||||
import { EmptyState } from "components/empty-state";
|
||||
// types
|
||||
import { ISearchIssueResponse, TIssueLayouts } from "@plane/types";
|
||||
// constants
|
||||
import { EIssuesStoreType } from "constants/issue";
|
||||
import { EmptyStateType } from "constants/empty-state";
|
||||
|
||||
type Props = {
|
||||
workspaceSlug: string | undefined;
|
||||
@ -25,33 +23,16 @@ type Props = {
|
||||
isEmptyFilters?: boolean;
|
||||
};
|
||||
|
||||
interface EmptyStateProps {
|
||||
title: string;
|
||||
image: string;
|
||||
description?: string;
|
||||
comicBox?: { title: string; description: string };
|
||||
primaryButton?: { text: string; icon?: React.ReactNode; onClick: () => void };
|
||||
secondaryButton?: { text: string; icon?: React.ReactNode; onClick: () => void };
|
||||
size?: "lg" | "sm" | undefined;
|
||||
disabled?: boolean | undefined;
|
||||
}
|
||||
|
||||
export const ModuleEmptyState: React.FC<Props> = observer((props) => {
|
||||
const { workspaceSlug, projectId, moduleId, activeLayout, handleClearAllFilters, isEmptyFilters = false } = props;
|
||||
// states
|
||||
const [moduleIssuesListModal, setModuleIssuesListModal] = useState(false);
|
||||
// theme
|
||||
const { resolvedTheme } = useTheme();
|
||||
// store hooks
|
||||
const { issues } = useIssues(EIssuesStoreType.MODULE);
|
||||
const {
|
||||
commandPalette: { toggleCreateIssueModal },
|
||||
} = useApplication();
|
||||
const { setTrackElement } = useEventTracker();
|
||||
const {
|
||||
membership: { currentProjectRole: userRole },
|
||||
currentUser,
|
||||
} = useUser();
|
||||
|
||||
const handleAddIssuesToModule = async (data: ISearchIssueResponse[]) => {
|
||||
if (!workspaceSlug || !projectId || !moduleId) return;
|
||||
@ -75,42 +56,8 @@ export const ModuleEmptyState: React.FC<Props> = observer((props) => {
|
||||
);
|
||||
};
|
||||
|
||||
const emptyStateDetail = MODULE_EMPTY_STATE_DETAILS["no-issues"];
|
||||
|
||||
const isLightMode = resolvedTheme ? resolvedTheme === "light" : currentUser?.theme.theme === "light";
|
||||
const currentLayoutEmptyStateImagePath = getEmptyStateImagePath("empty-filters", activeLayout ?? "list", isLightMode);
|
||||
const emptyStateImage = getEmptyStateImagePath("module-issues", activeLayout ?? "list", isLightMode);
|
||||
|
||||
const isEditingAllowed = !!userRole && userRole >= EUserProjectRoles.MEMBER;
|
||||
|
||||
const emptyStateProps: EmptyStateProps = isEmptyFilters
|
||||
? {
|
||||
title: EMPTY_FILTER_STATE_DETAILS["project"].title,
|
||||
image: currentLayoutEmptyStateImagePath,
|
||||
secondaryButton: {
|
||||
text: EMPTY_FILTER_STATE_DETAILS["project"].secondaryButton.text,
|
||||
onClick: handleClearAllFilters,
|
||||
},
|
||||
}
|
||||
: {
|
||||
title: emptyStateDetail.title,
|
||||
description: emptyStateDetail.description,
|
||||
image: emptyStateImage,
|
||||
primaryButton: {
|
||||
text: emptyStateDetail.primaryButton.text,
|
||||
icon: <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,
|
||||
};
|
||||
const emptyStateType = isEmptyFilters ? EmptyStateType.PROJECT_EMPTY_FILTER : EmptyStateType.PROJECT_MODULE_ISSUES;
|
||||
const additionalPath = activeLayout ?? "list";
|
||||
|
||||
return (
|
||||
<>
|
||||
@ -123,7 +70,19 @@ export const ModuleEmptyState: React.FC<Props> = observer((props) => {
|
||||
handleOnSubmit={handleAddIssuesToModule}
|
||||
/>
|
||||
<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>
|
||||
</>
|
||||
);
|
||||
|
@ -1,51 +1,29 @@
|
||||
import size from "lodash/size";
|
||||
import { observer } from "mobx-react-lite";
|
||||
import { useRouter } from "next/router";
|
||||
import { useTheme } from "next-themes";
|
||||
// hooks
|
||||
import { EmptyState, getEmptyStateImagePath } from "components/empty-state";
|
||||
import { EMPTY_FILTER_STATE_DETAILS, EMPTY_ISSUE_STATE_DETAILS } from "constants/empty-state";
|
||||
import { EIssueFilterType, EIssuesStoreType } from "constants/issue";
|
||||
import { EUserProjectRoles } from "constants/project";
|
||||
import { useApplication, useEventTracker, useIssues, useUser } from "hooks/store";
|
||||
import { useApplication, useEventTracker, useIssues } from "hooks/store";
|
||||
// components
|
||||
import { EmptyState } from "components/empty-state";
|
||||
// constants
|
||||
import { EIssueFilterType, EIssuesStoreType } from "constants/issue";
|
||||
import { EmptyStateType } from "constants/empty-state";
|
||||
// types
|
||||
import { IIssueFilterOptions } from "@plane/types";
|
||||
|
||||
interface EmptyStateProps {
|
||||
title: string;
|
||||
image: string;
|
||||
description?: string;
|
||||
comicBox?: { title: string; description: string };
|
||||
primaryButton?: { text: string; icon?: React.ReactNode; onClick: () => void };
|
||||
secondaryButton?: { text: string; onClick: () => void };
|
||||
size?: "lg" | "sm" | undefined;
|
||||
disabled?: boolean | undefined;
|
||||
}
|
||||
|
||||
export const ProjectEmptyState: React.FC = observer(() => {
|
||||
// router
|
||||
const router = useRouter();
|
||||
const { workspaceSlug, projectId } = router.query;
|
||||
// theme
|
||||
const { resolvedTheme } = useTheme();
|
||||
// store hooks
|
||||
const { commandPalette: commandPaletteStore } = useApplication();
|
||||
const { setTrackElement } = useEventTracker();
|
||||
const {
|
||||
membership: { currentProjectRole },
|
||||
currentUser,
|
||||
} = useUser();
|
||||
|
||||
const { issuesFilter } = useIssues(EIssuesStoreType.PROJECT);
|
||||
|
||||
const userFilters = issuesFilter?.issueFilters?.filters;
|
||||
const activeLayout = issuesFilter?.issueFilters?.displayFilters?.layout;
|
||||
|
||||
const isLightMode = resolvedTheme ? resolvedTheme === "light" : currentUser?.theme.theme === "light";
|
||||
const currentLayoutEmptyStateImagePath = getEmptyStateImagePath("empty-filters", activeLayout ?? "list", isLightMode);
|
||||
const EmptyStateImagePath = getEmptyStateImagePath("onboarding", "issues", isLightMode);
|
||||
|
||||
const issueFilterCount = size(
|
||||
Object.fromEntries(
|
||||
Object.entries(userFilters ?? {}).filter(([, value]) => value && Array.isArray(value) && value.length > 0)
|
||||
@ -63,40 +41,26 @@ export const ProjectEmptyState: React.FC = observer(() => {
|
||||
});
|
||||
};
|
||||
|
||||
const isEditingAllowed = !!currentProjectRole && currentProjectRole >= EUserProjectRoles.MEMBER;
|
||||
|
||||
const emptyStateProps: EmptyStateProps =
|
||||
issueFilterCount > 0
|
||||
? {
|
||||
title: EMPTY_FILTER_STATE_DETAILS["project"].title,
|
||||
image: currentLayoutEmptyStateImagePath,
|
||||
secondaryButton: {
|
||||
text: EMPTY_FILTER_STATE_DETAILS["project"].secondaryButton.text,
|
||||
onClick: handleClearAllFilters,
|
||||
},
|
||||
}
|
||||
: {
|
||||
title: EMPTY_ISSUE_STATE_DETAILS["project"].title,
|
||||
description: EMPTY_ISSUE_STATE_DETAILS["project"].description,
|
||||
image: EmptyStateImagePath,
|
||||
comicBox: {
|
||||
title: EMPTY_ISSUE_STATE_DETAILS["project"].comicBox.title,
|
||||
description: EMPTY_ISSUE_STATE_DETAILS["project"].comicBox.description,
|
||||
},
|
||||
primaryButton: {
|
||||
text: EMPTY_ISSUE_STATE_DETAILS["project"].primaryButton.text,
|
||||
onClick: () => {
|
||||
setTrackElement("Project issue empty state");
|
||||
commandPaletteStore.toggleCreateIssueModal(true, EIssuesStoreType.PROJECT);
|
||||
},
|
||||
},
|
||||
size: "lg",
|
||||
disabled: !isEditingAllowed,
|
||||
};
|
||||
const emptyStateType = issueFilterCount > 0 ? EmptyStateType.PROJECT_EMPTY_FILTER : EmptyStateType.PROJECT_NO_ISSUES;
|
||||
const additionalPath = issueFilterCount > 0 ? activeLayout ?? "list" : undefined;
|
||||
const emptyStateSize = issueFilterCount > 0 ? "lg" : "sm";
|
||||
|
||||
return (
|
||||
<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>
|
||||
);
|
||||
});
|
||||
|
@ -2,32 +2,28 @@ import React, { Fragment, useCallback, useMemo } from "react";
|
||||
import isEmpty from "lodash/isEmpty";
|
||||
import { observer } from "mobx-react-lite";
|
||||
import { useRouter } from "next/router";
|
||||
import { useTheme } from "next-themes";
|
||||
import useSWR from "swr";
|
||||
// hooks
|
||||
import { EmptyState, getEmptyStateImagePath } from "components/empty-state";
|
||||
import { useWorkspaceIssueProperties } from "hooks/use-workspace-issue-properties";
|
||||
import { useApplication, useEventTracker, useGlobalView, useIssues, useProject, useUser } from "hooks/store";
|
||||
// components
|
||||
import { GlobalViewsAppliedFiltersRoot, IssuePeekOverview } from "components/issues";
|
||||
import { SpreadsheetView } from "components/issues/issue-layouts";
|
||||
import { AllIssueQuickActions } from "components/issues/issue-layouts/quick-action-dropdowns";
|
||||
import { EmptyState } from "components/empty-state";
|
||||
import { SpreadsheetLayoutLoader } from "components/ui";
|
||||
import { ALL_ISSUES_EMPTY_STATE_DETAILS } from "constants/empty-state";
|
||||
import { EIssueFilterType, EIssuesStoreType, ISSUE_DISPLAY_FILTERS_BY_LAYOUT } from "constants/issue";
|
||||
import { EUserProjectRoles } from "constants/project";
|
||||
import { EUserWorkspaceRoles } from "constants/workspace";
|
||||
import { useApplication, useEventTracker, useGlobalView, useIssues, useProject, useUser } from "hooks/store";
|
||||
import { useWorkspaceIssueProperties } from "hooks/use-workspace-issue-properties";
|
||||
// components
|
||||
// types
|
||||
import { TIssue, IIssueDisplayFilterOptions } from "@plane/types";
|
||||
import { EIssueActions } from "../types";
|
||||
// constants
|
||||
import { EUserProjectRoles } from "constants/project";
|
||||
import { EIssueFilterType, EIssuesStoreType, ISSUE_DISPLAY_FILTERS_BY_LAYOUT } from "constants/issue";
|
||||
import { EMPTY_STATE_DETAILS, EmptyStateType } from "constants/empty-state";
|
||||
|
||||
export const AllIssueLayoutRoot: React.FC = observer(() => {
|
||||
// router
|
||||
const router = useRouter();
|
||||
const { workspaceSlug, globalViewId, ...routeFilters } = router.query;
|
||||
// theme
|
||||
const { resolvedTheme } = useTheme();
|
||||
//swr hook for fetching issue properties
|
||||
useWorkspaceIssueProperties(workspaceSlug);
|
||||
// store
|
||||
@ -39,8 +35,7 @@ export const AllIssueLayoutRoot: React.FC = observer(() => {
|
||||
|
||||
const { dataViewId, issueIds } = groupedIssueIds;
|
||||
const {
|
||||
membership: { currentWorkspaceAllProjectsRole, currentWorkspaceRole },
|
||||
currentUser,
|
||||
membership: { currentWorkspaceAllProjectsRole },
|
||||
} = useUser();
|
||||
const { fetchAllGlobalViews } = useGlobalView();
|
||||
const { workspaceProjectIds } = useProject();
|
||||
@ -48,10 +43,6 @@ export const AllIssueLayoutRoot: React.FC = observer(() => {
|
||||
|
||||
const isDefaultView = ["all-issues", "assigned", "created", "subscribed"].includes(groupedIssueIds.dataViewId);
|
||||
const currentView = isDefaultView ? groupedIssueIds.dataViewId : "custom-view";
|
||||
const currentViewDetails = ALL_ISSUES_EMPTY_STATE_DETAILS[currentView as keyof typeof ALL_ISSUES_EMPTY_STATE_DETAILS];
|
||||
|
||||
const isLightMode = resolvedTheme ? resolvedTheme === "light" : currentUser?.theme.theme === "light";
|
||||
const emptyStateImage = getEmptyStateImagePath("all-issues", currentView, isLightMode);
|
||||
|
||||
// filter init from the query params
|
||||
|
||||
@ -185,46 +176,34 @@ export const AllIssueLayoutRoot: React.FC = observer(() => {
|
||||
[canEditProperties, handleIssues]
|
||||
);
|
||||
|
||||
const isEditingAllowed = !!currentWorkspaceRole && currentWorkspaceRole >= EUserWorkspaceRoles.MEMBER;
|
||||
|
||||
if (loader === "init-loader" || !globalViewId || globalViewId !== dataViewId || !issueIds) {
|
||||
return <SpreadsheetLayoutLoader />;
|
||||
}
|
||||
|
||||
const emptyStateType =
|
||||
(workspaceProjectIds ?? []).length > 0 ? `workspace-${currentView}` : EmptyStateType.WORKSPACE_NO_PROJECTS;
|
||||
|
||||
return (
|
||||
<div className="relative flex h-full w-full flex-col overflow-hidden">
|
||||
<div className="relative h-full w-full flex flex-col">
|
||||
<GlobalViewsAppliedFiltersRoot globalViewId={globalViewId} />
|
||||
{issueIds.length === 0 ? (
|
||||
<EmptyState
|
||||
image={emptyStateImage}
|
||||
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."
|
||||
}
|
||||
type={emptyStateType as keyof typeof EMPTY_STATE_DETAILS}
|
||||
size="sm"
|
||||
primaryButton={
|
||||
primaryButtonOnClick={
|
||||
(workspaceProjectIds ?? []).length > 0
|
||||
? currentView !== "custom-view" && currentView !== "subscribed"
|
||||
? {
|
||||
text: "Create new issue",
|
||||
onClick: () => {
|
||||
setTrackElement("All issues empty state");
|
||||
commandPaletteStore.toggleCreateIssueModal(true, EIssuesStoreType.PROJECT);
|
||||
},
|
||||
? () => {
|
||||
setTrackElement("All issues empty state");
|
||||
commandPaletteStore.toggleCreateIssueModal(true, EIssuesStoreType.PROJECT);
|
||||
}
|
||||
: undefined
|
||||
: {
|
||||
text: "Start your first project",
|
||||
onClick: () => {
|
||||
setTrackElement("All issues empty state");
|
||||
commandPaletteStore.toggleCreateProjectModal(true);
|
||||
},
|
||||
: () => {
|
||||
setTrackElement("All issues empty state");
|
||||
commandPaletteStore.toggleCreateProjectModal(true);
|
||||
}
|
||||
}
|
||||
disabled={!isEditingAllowed}
|
||||
/>
|
||||
) : (
|
||||
<Fragment>
|
||||
|
@ -1,4 +1,6 @@
|
||||
import React, { useState, useRef } from "react";
|
||||
import { useRouter } from "next/router";
|
||||
import { observer } from "mobx-react";
|
||||
import {
|
||||
DragDropContext,
|
||||
Draggable,
|
||||
@ -7,26 +9,23 @@ import {
|
||||
DropResult,
|
||||
Droppable,
|
||||
} from "@hello-pangea/dnd";
|
||||
import { observer } from "mobx-react-lite";
|
||||
import { useRouter } from "next/router";
|
||||
import { useTheme } from "next-themes";
|
||||
// hooks
|
||||
import { Button, Loader } from "@plane/ui";
|
||||
import { EmptyState, getEmptyStateImagePath } from "components/empty-state";
|
||||
import { useLabel } from "hooks/store";
|
||||
import useDraggableInPortal from "hooks/use-draggable-portal";
|
||||
// components
|
||||
import {
|
||||
CreateUpdateLabelInline,
|
||||
DeleteLabelModal,
|
||||
ProjectSettingLabelGroup,
|
||||
ProjectSettingLabelItem,
|
||||
} from "components/labels";
|
||||
import { PROJECT_SETTINGS_EMPTY_STATE_DETAILS } from "constants/empty-state";
|
||||
import { useLabel, useUser } from "hooks/store";
|
||||
import useDraggableInPortal from "hooks/use-draggable-portal";
|
||||
// components
|
||||
import { EmptyState } from "components/empty-state";
|
||||
// ui
|
||||
import { Button, Loader } from "@plane/ui";
|
||||
// types
|
||||
import { IIssueLabel } from "@plane/types";
|
||||
// constants
|
||||
import { EmptyStateType } from "constants/empty-state";
|
||||
|
||||
const LABELS_ROOT = "labels.root";
|
||||
|
||||
@ -41,10 +40,7 @@ export const ProjectSettingsLabelList: React.FC = observer(() => {
|
||||
// router
|
||||
const router = useRouter();
|
||||
const { workspaceSlug, projectId } = router.query;
|
||||
// theme
|
||||
const { resolvedTheme } = useTheme();
|
||||
// store hooks
|
||||
const { currentUser } = useUser();
|
||||
const { projectLabels, updateLabelPosition, projectLabelsTree } = useLabel();
|
||||
// portal
|
||||
const renderDraggable = useDraggableInPortal();
|
||||
@ -54,10 +50,6 @@ export const ProjectSettingsLabelList: React.FC = observer(() => {
|
||||
setLabelForm(true);
|
||||
};
|
||||
|
||||
const emptyStateDetail = PROJECT_SETTINGS_EMPTY_STATE_DETAILS["labels"];
|
||||
const isLightMode = resolvedTheme ? resolvedTheme === "light" : currentUser?.theme.theme === "light";
|
||||
const emptyStateImage = getEmptyStateImagePath("project-settings", "labels", isLightMode);
|
||||
|
||||
const onDragEnd = (result: DropResult) => {
|
||||
const { combine, draggableId, destination, source } = result;
|
||||
|
||||
@ -121,13 +113,8 @@ export const ProjectSettingsLabelList: React.FC = observer(() => {
|
||||
)}
|
||||
{projectLabels ? (
|
||||
projectLabels.length === 0 && !showLabelForm ? (
|
||||
<div className="flex h-full w-full items-center justify-center">
|
||||
<EmptyState
|
||||
title={emptyStateDetail.title}
|
||||
description={emptyStateDetail.description}
|
||||
image={emptyStateImage}
|
||||
size="lg"
|
||||
/>
|
||||
<div className="flex items-center justify-center h-full w-full">
|
||||
<EmptyState type={EmptyStateType.PROJECT_SETTINGS_LABELS} />
|
||||
</div>
|
||||
) : (
|
||||
projectLabelsTree && (
|
||||
|
@ -1,40 +1,28 @@
|
||||
import { observer } from "mobx-react-lite";
|
||||
import { useRouter } from "next/router";
|
||||
import { useTheme } from "next-themes";
|
||||
// hooks
|
||||
import { useApplication, useEventTracker, useModule } from "hooks/store";
|
||||
import useLocalStorage from "hooks/use-local-storage";
|
||||
// components
|
||||
import { EmptyState, getEmptyStateImagePath } from "components/empty-state";
|
||||
import { ModuleCardItem, ModuleListItem, ModulePeekOverview, ModulesListGanttChartView } from "components/modules";
|
||||
import { EmptyState } from "components/empty-state";
|
||||
// ui
|
||||
import { CycleModuleBoardLayout, CycleModuleListLayout, GanttLayoutLoader } from "components/ui";
|
||||
// constants
|
||||
import { MODULE_EMPTY_STATE_DETAILS } from "constants/empty-state";
|
||||
import { EUserProjectRoles } from "constants/project";
|
||||
import { useApplication, useEventTracker, useModule, useUser } from "hooks/store";
|
||||
import useLocalStorage from "hooks/use-local-storage";
|
||||
import { EmptyStateType } from "constants/empty-state";
|
||||
|
||||
export const ModulesListView: React.FC = observer(() => {
|
||||
// router
|
||||
const router = useRouter();
|
||||
const { workspaceSlug, projectId, peekModule } = router.query;
|
||||
// theme
|
||||
const { resolvedTheme } = useTheme();
|
||||
// store hooks
|
||||
const { commandPalette: commandPaletteStore } = useApplication();
|
||||
const { setTrackElement } = useEventTracker();
|
||||
const {
|
||||
membership: { currentProjectRole },
|
||||
currentUser,
|
||||
} = useUser();
|
||||
|
||||
const { projectModuleIds, loader } = useModule();
|
||||
|
||||
const { storedValue: modulesView } = useLocalStorage("modules_view", "grid");
|
||||
|
||||
const isLightMode = resolvedTheme ? resolvedTheme === "light" : currentUser?.theme.theme === "light";
|
||||
const EmptyStateImagePath = getEmptyStateImagePath("onboarding", "modules", isLightMode);
|
||||
|
||||
const isEditingAllowed = !!currentProjectRole && currentProjectRole >= EUserProjectRoles.MEMBER;
|
||||
|
||||
if (loader || !projectModuleIds)
|
||||
return (
|
||||
<>
|
||||
@ -88,22 +76,11 @@ export const ModulesListView: React.FC = observer(() => {
|
||||
</>
|
||||
) : (
|
||||
<EmptyState
|
||||
title={MODULE_EMPTY_STATE_DETAILS["modules"].title}
|
||||
description={MODULE_EMPTY_STATE_DETAILS["modules"].description}
|
||||
image={EmptyStateImagePath}
|
||||
comicBox={{
|
||||
title: MODULE_EMPTY_STATE_DETAILS["modules"].comicBox.title,
|
||||
description: MODULE_EMPTY_STATE_DETAILS["modules"].comicBox.description,
|
||||
type={EmptyStateType.PROJECT_MODULE}
|
||||
primaryButtonOnClick={() => {
|
||||
setTrackElement("Module empty state");
|
||||
commandPaletteStore.toggleCreateModuleModal(true);
|
||||
}}
|
||||
primaryButton={{
|
||||
text: MODULE_EMPTY_STATE_DETAILS["modules"].primaryButton.text,
|
||||
onClick: () => {
|
||||
setTrackElement("Module empty state");
|
||||
commandPaletteStore.toggleCreateModuleModal(true);
|
||||
},
|
||||
}}
|
||||
size="lg"
|
||||
disabled={!isEditingAllowed}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
|
@ -1,41 +1,30 @@
|
||||
import { useEffect } from "react";
|
||||
import { observer } from "mobx-react-lite";
|
||||
import { useTheme } from "next-themes";
|
||||
// hooks
|
||||
import { useApplication, useEventTracker, useDashboard, useProject, useUser } from "hooks/store";
|
||||
// components
|
||||
import { Spinner } from "@plane/ui";
|
||||
import { DashboardWidgets } from "components/dashboard";
|
||||
import { EmptyState, getEmptyStateImagePath } from "components/empty-state";
|
||||
import { EmptyState } from "components/empty-state";
|
||||
import { IssuePeekOverview } from "components/issues";
|
||||
import { TourRoot } from "components/onboarding";
|
||||
import { UserGreetingsView } from "components/user";
|
||||
// ui
|
||||
import { Spinner } from "@plane/ui";
|
||||
// constants
|
||||
import { WORKSPACE_EMPTY_STATE_DETAILS } from "constants/empty-state";
|
||||
import { PRODUCT_TOUR_COMPLETED } from "constants/event-tracker";
|
||||
import { EUserWorkspaceRoles } from "constants/workspace";
|
||||
import { useApplication, useEventTracker, useDashboard, useProject, useUser } from "hooks/store";
|
||||
import { EmptyStateType } from "constants/empty-state";
|
||||
|
||||
export const WorkspaceDashboardView = observer(() => {
|
||||
// theme
|
||||
const { resolvedTheme } = useTheme();
|
||||
// store hooks
|
||||
const { captureEvent, setTrackElement } = useEventTracker();
|
||||
const {
|
||||
commandPalette: { toggleCreateProjectModal },
|
||||
router: { workspaceSlug },
|
||||
} = useApplication();
|
||||
const {
|
||||
currentUser,
|
||||
updateTourCompleted,
|
||||
membership: { currentWorkspaceRole },
|
||||
} = useUser();
|
||||
const { currentUser, updateTourCompleted } = useUser();
|
||||
const { homeDashboardId, fetchHomeDashboardWidgets } = useDashboard();
|
||||
const { joinedProjectIds } = useProject();
|
||||
|
||||
const isLightMode = resolvedTheme ? resolvedTheme === "light" : currentUser?.theme.theme === "light";
|
||||
const emptyStateImage = getEmptyStateImagePath("onboarding", "dashboard", isLightMode);
|
||||
|
||||
const handleTourCompleted = () => {
|
||||
updateTourCompleted()
|
||||
.then(() => {
|
||||
@ -56,8 +45,6 @@ export const WorkspaceDashboardView = observer(() => {
|
||||
fetchHomeDashboardWidgets(workspaceSlug);
|
||||
}, [fetchHomeDashboardWidgets, workspaceSlug]);
|
||||
|
||||
const isEditingAllowed = currentWorkspaceRole === EUserWorkspaceRoles.ADMIN;
|
||||
|
||||
return (
|
||||
<>
|
||||
{currentUser && !currentUser.is_tour_completed && (
|
||||
@ -78,22 +65,11 @@ export const WorkspaceDashboardView = observer(() => {
|
||||
</>
|
||||
) : (
|
||||
<EmptyState
|
||||
image={emptyStateImage}
|
||||
title={WORKSPACE_EMPTY_STATE_DETAILS["dashboard"].title}
|
||||
description={WORKSPACE_EMPTY_STATE_DETAILS["dashboard"].description}
|
||||
primaryButton={{
|
||||
text: WORKSPACE_EMPTY_STATE_DETAILS["dashboard"].primaryButton.text,
|
||||
onClick: () => {
|
||||
setTrackElement("Dashboard empty state");
|
||||
toggleCreateProjectModal(true);
|
||||
},
|
||||
type={EmptyStateType.WORKSPACE_DASHBOARD}
|
||||
primaryButtonOnClick={() => {
|
||||
setTrackElement("Dashboard empty state");
|
||||
toggleCreateProjectModal(true);
|
||||
}}
|
||||
comicBox={{
|
||||
title: WORKSPACE_EMPTY_STATE_DETAILS["dashboard"].comicBox.title,
|
||||
description: WORKSPACE_EMPTY_STATE_DETAILS["dashboard"].comicBox.description,
|
||||
}}
|
||||
size="lg"
|
||||
disabled={!isEditingAllowed}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
|
@ -1,17 +1,15 @@
|
||||
import { FC } from "react";
|
||||
import { useRouter } from "next/router";
|
||||
import { useTheme } from "next-themes";
|
||||
// hooks
|
||||
import { Loader } from "@plane/ui";
|
||||
import { EmptyState, getEmptyStateImagePath } from "components/empty-state";
|
||||
import { PAGE_EMPTY_STATE_DETAILS } from "constants/empty-state";
|
||||
import { EUserProjectRoles } from "constants/project";
|
||||
import { useApplication, useUser } from "hooks/store";
|
||||
import { useApplication } from "hooks/store";
|
||||
import useLocalStorage from "hooks/use-local-storage";
|
||||
// components
|
||||
import { EmptyState } from "components/empty-state";
|
||||
import { PagesListItem } from "./list-item";
|
||||
// ui
|
||||
import { Loader } from "@plane/ui";
|
||||
// constants
|
||||
import { EMPTY_STATE_DETAILS, EmptyStateType } from "constants/empty-state";
|
||||
|
||||
type IPagesListView = {
|
||||
pageIds: string[];
|
||||
@ -19,34 +17,20 @@ type IPagesListView = {
|
||||
|
||||
export const PagesListView: FC<IPagesListView> = (props) => {
|
||||
const { pageIds: projectPageIds } = props;
|
||||
// theme
|
||||
const { resolvedTheme } = useTheme();
|
||||
// store hooks
|
||||
const {
|
||||
commandPalette: { toggleCreatePageModal },
|
||||
} = useApplication();
|
||||
const {
|
||||
membership: { currentProjectRole },
|
||||
currentUser,
|
||||
} = useUser();
|
||||
// local storage
|
||||
const { storedValue: pageTab } = useLocalStorage("pageTab", "Recent");
|
||||
// router
|
||||
const router = useRouter();
|
||||
const { workspaceSlug, projectId } = router.query;
|
||||
|
||||
const currentPageTabDetails = pageTab
|
||||
? PAGE_EMPTY_STATE_DETAILS[pageTab as keyof typeof PAGE_EMPTY_STATE_DETAILS]
|
||||
: PAGE_EMPTY_STATE_DETAILS["All"];
|
||||
|
||||
const isLightMode = resolvedTheme ? resolvedTheme === "light" : currentUser?.theme.theme === "light";
|
||||
const emptyStateImage = getEmptyStateImagePath("pages", currentPageTabDetails.key, isLightMode);
|
||||
|
||||
const isButtonVisible = currentPageTabDetails.key !== "archived" && currentPageTabDetails.key !== "favorites";
|
||||
|
||||
// here we are only observing the projectPageStore, so that we can re-render the component when the projectPageStore changes
|
||||
|
||||
const isEditingAllowed = !!currentProjectRole && currentProjectRole >= EUserProjectRoles.MEMBER;
|
||||
const emptyStateType = pageTab ? `project-page-${pageTab}` : EmptyStateType.PROJECT_PAGE_ALL;
|
||||
const isButtonVisible = pageTab !== "archived" && pageTab !== "favorites";
|
||||
|
||||
return (
|
||||
<>
|
||||
@ -60,18 +44,8 @@ export const PagesListView: FC<IPagesListView> = (props) => {
|
||||
</ul>
|
||||
) : (
|
||||
<EmptyState
|
||||
title={currentPageTabDetails.title}
|
||||
description={currentPageTabDetails.description}
|
||||
image={emptyStateImage}
|
||||
primaryButton={
|
||||
isButtonVisible
|
||||
? {
|
||||
text: "Create new page",
|
||||
onClick: () => toggleCreatePageModal(true),
|
||||
}
|
||||
: undefined
|
||||
}
|
||||
disabled={!isEditingAllowed}
|
||||
type={emptyStateType as keyof typeof EMPTY_STATE_DETAILS}
|
||||
primaryButtonOnClick={isButtonVisible ? () => toggleCreatePageModal(true) : undefined}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
|
@ -1,39 +1,27 @@
|
||||
import React, { FC } from "react";
|
||||
import { observer } from "mobx-react-lite";
|
||||
import { useTheme } from "next-themes";
|
||||
// hooks
|
||||
import { Loader } from "@plane/ui";
|
||||
import { EmptyState, getEmptyStateImagePath } from "components/empty-state";
|
||||
import { PagesListView } from "components/pages/pages-list";
|
||||
import { PAGE_EMPTY_STATE_DETAILS } from "constants/empty-state";
|
||||
import { EUserProjectRoles } from "constants/project";
|
||||
import { replaceUnderscoreIfSnakeCase } from "helpers/string.helper";
|
||||
import { useApplication, useUser } from "hooks/store";
|
||||
import { useApplication } from "hooks/store";
|
||||
import { useProjectPages } from "hooks/store/use-project-specific-pages";
|
||||
// components
|
||||
import { PagesListView } from "components/pages/pages-list";
|
||||
import { EmptyState } from "components/empty-state";
|
||||
// ui
|
||||
import { Loader } from "@plane/ui";
|
||||
// helpers
|
||||
import { replaceUnderscoreIfSnakeCase } from "helpers/string.helper";
|
||||
// constants
|
||||
import { EmptyStateType } from "constants/empty-state";
|
||||
|
||||
export const RecentPagesList: FC = observer(() => {
|
||||
// theme
|
||||
const { resolvedTheme } = useTheme();
|
||||
// store hooks
|
||||
const { commandPalette: commandPaletteStore } = useApplication();
|
||||
const {
|
||||
membership: { currentProjectRole },
|
||||
currentUser,
|
||||
} = useUser();
|
||||
const { recentProjectPages } = useProjectPages();
|
||||
|
||||
const isLightMode = resolvedTheme ? resolvedTheme === "light" : currentUser?.theme.theme === "light";
|
||||
const EmptyStateImagePath = getEmptyStateImagePath("pages", "recent", isLightMode);
|
||||
const { recentProjectPages } = useProjectPages();
|
||||
|
||||
// FIXME: replace any with proper type
|
||||
const isEmpty = recentProjectPages && Object.values(recentProjectPages).every((value: any) => value.length === 0);
|
||||
|
||||
const isEditingAllowed = !!currentProjectRole && currentProjectRole >= EUserProjectRoles.MEMBER;
|
||||
|
||||
if (!recentProjectPages) {
|
||||
return (
|
||||
<Loader className="space-y-4">
|
||||
@ -64,15 +52,9 @@ export const RecentPagesList: FC = observer(() => {
|
||||
) : (
|
||||
<>
|
||||
<EmptyState
|
||||
title={PAGE_EMPTY_STATE_DETAILS["Recent"].title}
|
||||
description={PAGE_EMPTY_STATE_DETAILS["Recent"].description}
|
||||
image={EmptyStateImagePath}
|
||||
primaryButton={{
|
||||
text: PAGE_EMPTY_STATE_DETAILS["Recent"].primaryButton.text,
|
||||
onClick: () => commandPaletteStore.toggleCreatePageModal(true),
|
||||
}}
|
||||
type={EmptyStateType.PROJECT_PAGE_RECENT}
|
||||
primaryButtonOnClick={() => commandPaletteStore.toggleCreatePageModal(true)}
|
||||
size="sm"
|
||||
disabled={!isEditingAllowed}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
|
@ -1,20 +1,18 @@
|
||||
import React, { useEffect } from "react";
|
||||
import { observer } from "mobx-react-lite";
|
||||
import { useRouter } from "next/router";
|
||||
import { useTheme } from "next-themes";
|
||||
import useSWR from "swr";
|
||||
// components
|
||||
import { EmptyState, getEmptyStateImagePath } from "components/empty-state";
|
||||
import { IssuePeekOverview, ProfileIssuesAppliedFiltersRoot } from "components/issues";
|
||||
import { ProfileIssuesKanBanLayout } from "components/issues/issue-layouts/kanban/roots/profile-issues-root";
|
||||
import { ProfileIssuesListLayout } from "components/issues/issue-layouts/list/roots/profile-issues-root";
|
||||
import { KanbanLayoutLoader, ListLayoutLoader } from "components/ui";
|
||||
import { EmptyState } from "components/empty-state";
|
||||
// hooks
|
||||
import { PROFILE_EMPTY_STATE_DETAILS } from "constants/empty-state";
|
||||
import { EIssuesStoreType } from "constants/issue";
|
||||
import { EUserWorkspaceRoles } from "constants/workspace";
|
||||
import { useIssues, useUser } from "hooks/store";
|
||||
import { useIssues } from "hooks/store";
|
||||
// constants
|
||||
import { EIssuesStoreType } from "constants/issue";
|
||||
import { EMPTY_STATE_DETAILS } from "constants/empty-state";
|
||||
|
||||
interface IProfileIssuesPage {
|
||||
type: "assigned" | "subscribed" | "created";
|
||||
@ -28,13 +26,7 @@ export const ProfileIssuesPage = observer((props: IProfileIssuesPage) => {
|
||||
workspaceSlug: string;
|
||||
userId: string;
|
||||
};
|
||||
// theme
|
||||
const { resolvedTheme } = useTheme();
|
||||
// store hooks
|
||||
const {
|
||||
membership: { currentWorkspaceRole },
|
||||
currentUser,
|
||||
} = useUser();
|
||||
const {
|
||||
issues: { loader, groupedIssueIds, fetchIssues, setViewId },
|
||||
issuesFilter: { issueFilters, fetchFilters },
|
||||
@ -55,26 +47,15 @@ export const ProfileIssuesPage = observer((props: IProfileIssuesPage) => {
|
||||
{ revalidateIfStale: false, revalidateOnFocus: false }
|
||||
);
|
||||
|
||||
const isLightMode = resolvedTheme ? resolvedTheme === "light" : currentUser?.theme.theme === "light";
|
||||
const emptyStateImage = getEmptyStateImagePath("profile", type, isLightMode);
|
||||
|
||||
const activeLayout = issueFilters?.displayFilters?.layout || undefined;
|
||||
|
||||
const isEditingAllowed = !!currentWorkspaceRole && currentWorkspaceRole >= EUserWorkspaceRoles.MEMBER;
|
||||
const emptyStateType = `profile-${type}`;
|
||||
|
||||
if (!groupedIssueIds || loader === "init-loader")
|
||||
return <>{activeLayout === "list" ? <ListLayoutLoader /> : <KanbanLayoutLoader />}</>;
|
||||
|
||||
if (groupedIssueIds.length === 0) {
|
||||
return (
|
||||
<EmptyState
|
||||
image={emptyStateImage}
|
||||
title={PROFILE_EMPTY_STATE_DETAILS[type].title}
|
||||
description={PROFILE_EMPTY_STATE_DETAILS[type].description}
|
||||
size="sm"
|
||||
disabled={!isEditingAllowed}
|
||||
/>
|
||||
);
|
||||
return <EmptyState type={emptyStateType as keyof typeof EMPTY_STATE_DETAILS} size="sm" />;
|
||||
}
|
||||
|
||||
return (
|
||||
|
@ -1,32 +1,20 @@
|
||||
import { observer } from "mobx-react-lite";
|
||||
import { useTheme } from "next-themes";
|
||||
// hooks
|
||||
import { useApplication, useEventTracker, useProject } from "hooks/store";
|
||||
// components
|
||||
import { EmptyState, getEmptyStateImagePath } from "components/empty-state";
|
||||
import { EmptyState } from "components/empty-state";
|
||||
import { ProjectCard } from "components/project";
|
||||
import { ProjectsLoader } from "components/ui";
|
||||
// constants
|
||||
import { WORKSPACE_EMPTY_STATE_DETAILS } from "constants/empty-state";
|
||||
import { EUserWorkspaceRoles } from "constants/workspace";
|
||||
import { useApplication, useEventTracker, useProject, useUser } from "hooks/store";
|
||||
import { EmptyStateType } from "constants/empty-state";
|
||||
|
||||
export const ProjectCardList = observer(() => {
|
||||
// theme
|
||||
const { resolvedTheme } = useTheme();
|
||||
// store hooks
|
||||
const { commandPalette: commandPaletteStore } = useApplication();
|
||||
const { setTrackElement } = useEventTracker();
|
||||
const {
|
||||
membership: { currentWorkspaceRole },
|
||||
currentUser,
|
||||
} = useUser();
|
||||
|
||||
const { workspaceProjectIds, searchedProjects, getProjectById } = useProject();
|
||||
|
||||
const isLightMode = resolvedTheme ? resolvedTheme === "light" : currentUser?.theme.theme === "light";
|
||||
const emptyStateImage = getEmptyStateImagePath("onboarding", "projects", isLightMode);
|
||||
|
||||
const isEditingAllowed = !!currentWorkspaceRole && currentWorkspaceRole >= EUserWorkspaceRoles.MEMBER;
|
||||
|
||||
if (!workspaceProjectIds) return <ProjectsLoader />;
|
||||
|
||||
return (
|
||||
@ -49,22 +37,11 @@ export const ProjectCardList = observer(() => {
|
||||
</div>
|
||||
) : (
|
||||
<EmptyState
|
||||
image={emptyStateImage}
|
||||
title={WORKSPACE_EMPTY_STATE_DETAILS["projects"].title}
|
||||
description={WORKSPACE_EMPTY_STATE_DETAILS["projects"].description}
|
||||
primaryButton={{
|
||||
text: WORKSPACE_EMPTY_STATE_DETAILS["projects"].primaryButton.text,
|
||||
onClick: () => {
|
||||
setTrackElement("Project empty state");
|
||||
commandPaletteStore.toggleCreateProjectModal(true);
|
||||
},
|
||||
type={EmptyStateType.WORKSPACE_PROJECTS}
|
||||
primaryButtonOnClick={() => {
|
||||
setTrackElement("Project empty state");
|
||||
commandPaletteStore.toggleCreateProjectModal(true);
|
||||
}}
|
||||
comicBox={{
|
||||
title: WORKSPACE_EMPTY_STATE_DETAILS["projects"].comicBox.title,
|
||||
description: WORKSPACE_EMPTY_STATE_DETAILS["projects"].comicBox.description,
|
||||
}}
|
||||
size="lg"
|
||||
disabled={!isEditingAllowed}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
|
@ -1,45 +1,32 @@
|
||||
import { useState } from "react";
|
||||
import { observer } from "mobx-react-lite";
|
||||
import { useTheme } from "next-themes";
|
||||
import { Search } from "lucide-react";
|
||||
// hooks
|
||||
import { useApplication, useProjectView } from "hooks/store";
|
||||
// components
|
||||
import { Input } from "@plane/ui";
|
||||
import { EmptyState, getEmptyStateImagePath } from "components/empty-state";
|
||||
// ui
|
||||
import { EmptyState } from "components/empty-state";
|
||||
import { ViewListLoader } from "components/ui";
|
||||
import { ProjectViewListItem } from "components/views";
|
||||
// ui
|
||||
import { Input } from "@plane/ui";
|
||||
// constants
|
||||
import { VIEW_EMPTY_STATE_DETAILS } from "constants/empty-state";
|
||||
import { EUserProjectRoles } from "constants/project";
|
||||
import { useApplication, useProjectView, useUser } from "hooks/store";
|
||||
import { EmptyStateType } from "constants/empty-state";
|
||||
|
||||
export const ProjectViewsList = observer(() => {
|
||||
// states
|
||||
const [query, setQuery] = useState("");
|
||||
// theme
|
||||
const { resolvedTheme } = useTheme();
|
||||
// store hooks
|
||||
const {
|
||||
commandPalette: { toggleCreateViewModal },
|
||||
} = useApplication();
|
||||
const {
|
||||
membership: { currentProjectRole },
|
||||
currentUser,
|
||||
} = useUser();
|
||||
const { projectViewIds, getViewById, loader } = useProjectView();
|
||||
|
||||
if (loader || !projectViewIds) return <ViewListLoader />;
|
||||
|
||||
const viewsList = projectViewIds.map((viewId) => getViewById(viewId));
|
||||
|
||||
const isLightMode = resolvedTheme ? resolvedTheme === "light" : currentUser?.theme.theme === "light";
|
||||
const EmptyStateImagePath = getEmptyStateImagePath("onboarding", "views", isLightMode);
|
||||
|
||||
const filteredViewsList = viewsList.filter((v) => v?.name.toLowerCase().includes(query.toLowerCase()));
|
||||
|
||||
const isEditingAllowed = !!currentProjectRole && currentProjectRole >= EUserProjectRoles.MEMBER;
|
||||
|
||||
return (
|
||||
<>
|
||||
{viewsList.length > 0 ? (
|
||||
@ -65,21 +52,7 @@ export const ProjectViewsList = observer(() => {
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<EmptyState
|
||||
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}
|
||||
/>
|
||||
<EmptyState type={EmptyStateType.PROJECT_VIEW} primaryButtonOnClick={() => toggleCreateViewModal(true)} />
|
||||
)}
|
||||
</>
|
||||
);
|
||||
|
@ -11,7 +11,6 @@ import OverdueIssuesLight from "public/empty-state/dashboard/light/overdue-issue
|
||||
import UpcomingIssuesLight from "public/empty-state/dashboard/light/upcoming-issues.svg";
|
||||
// types
|
||||
import { TIssuesListTypes, TStateGroups } from "@plane/types";
|
||||
|
||||
// constants
|
||||
import { EUserWorkspaceRoles } from "./workspace";
|
||||
// icons
|
||||
|
@ -1,366 +1,516 @@
|
||||
// workspace empty state
|
||||
export const WORKSPACE_EMPTY_STATE_DETAILS = {
|
||||
dashboard: {
|
||||
import { EUserProjectRoles } from "./project";
|
||||
import { EUserWorkspaceRoles } from "./workspace";
|
||||
|
||||
export interface EmptyStateDetails {
|
||||
key: string;
|
||||
title?: string;
|
||||
description?: string;
|
||||
path?: string;
|
||||
primaryButton?: {
|
||||
icon?: any;
|
||||
text: string;
|
||||
comicBox?: {
|
||||
title?: string;
|
||||
description?: string;
|
||||
};
|
||||
};
|
||||
secondaryButton?: {
|
||||
icon?: any;
|
||||
text: string;
|
||||
comicBox?: {
|
||||
title?: string;
|
||||
description?: string;
|
||||
};
|
||||
};
|
||||
accessType?: "workspace" | "project";
|
||||
access?: EUserWorkspaceRoles | EUserProjectRoles;
|
||||
}
|
||||
|
||||
export type EmptyStateKeys = keyof typeof emptyStateDetails;
|
||||
|
||||
export enum EmptyStateType {
|
||||
WORKSPACE_DASHBOARD = "workspace-dashboard",
|
||||
WORKSPACE_ANALYTICS = "workspace-analytics",
|
||||
WORKSPACE_PROJECTS = "workspace-projects",
|
||||
WORKSPACE_ALL_ISSUES = "workspace-all-issues",
|
||||
WORKSPACE_ASSIGNED = "workspace-assigned",
|
||||
WORKSPACE_CREATED = "workspace-created",
|
||||
WORKSPACE_SUBSCRIBED = "workspace-subscribed",
|
||||
WORKSPACE_CUSTOM_VIEW = "workspace-custom-view",
|
||||
WORKSPACE_NO_PROJECTS = "workspace-no-projects",
|
||||
WORKSPACE_SETTINGS_API_TOKENS = "workspace-settings-api-tokens",
|
||||
WORKSPACE_SETTINGS_WEBHOOKS = "workspace-settings-webhooks",
|
||||
WORKSPACE_SETTINGS_EXPORT = "workspace-settings-export",
|
||||
WORKSPACE_SETTINGS_IMPORT = "workspace-settings-import",
|
||||
PROFILE_ASSIGNED = "profile-assigned",
|
||||
PROFILE_CREATED = "profile-created",
|
||||
PROFILE_SUBSCRIBED = "profile-subscribed",
|
||||
PROJECT_SETTINGS_LABELS = "project-settings-labels",
|
||||
PROJECT_SETTINGS_INTEGRATIONS = "project-settings-integrations",
|
||||
PROJECT_SETTINGS_ESTIMATE = "project-settings-estimate",
|
||||
PROJECT_CYCLES = "project-cycles",
|
||||
PROJECT_CYCLE_NO_ISSUES = "project-cycle-no-issues",
|
||||
PROJECT_CYCLE_ACTIVE = "project-cycle-active",
|
||||
PROJECT_CYCLE_UPCOMING = "project-cycle-upcoming",
|
||||
PROJECT_CYCLE_COMPLETED = "project-cycle-completed",
|
||||
PROJECT_CYCLE_DRAFT = "project-cycle-draft",
|
||||
PROJECT_EMPTY_FILTER = "project-empty-filter",
|
||||
PROJECT_ARCHIVED_EMPTY_FILTER = "project-archived-empty-filter",
|
||||
PROJECT_DRAFT_EMPTY_FILTER = "project-draft-empty-filter",
|
||||
PROJECT_NO_ISSUES = "project-no-issues",
|
||||
PROJECT_ARCHIVED_NO_ISSUES = "project-archived-no-issues",
|
||||
PROJECT_DRAFT_NO_ISSUES = "project-draft-no-issues",
|
||||
VIEWS_EMPTY_SEARCH = "views-empty-search",
|
||||
PROJECTS_EMPTY_SEARCH = "projects-empty-search",
|
||||
COMMANDK_EMPTY_SEARCH = "commandK-empty-search",
|
||||
MEMBERS_EMPTY_SEARCH = "members-empty-search",
|
||||
PROJECT_MODULE_ISSUES = "project-module-issues",
|
||||
PROJECT_MODULE = "project-module",
|
||||
PROJECT_VIEW = "project-view",
|
||||
PROJECT_PAGE = "project-page",
|
||||
PROJECT_PAGE_ALL = "project-page-all",
|
||||
PROJECT_PAGE_FAVORITE = "project-page-favorite",
|
||||
PROJECT_PAGE_PRIVATE = "project-page-private",
|
||||
PROJECT_PAGE_SHARED = "project-page-shared",
|
||||
PROJECT_PAGE_ARCHIVED = "project-page-archived",
|
||||
PROJECT_PAGE_RECENT = "project-page-recent",
|
||||
}
|
||||
|
||||
const emptyStateDetails = {
|
||||
// workspace
|
||||
"workspace-dashboard": {
|
||||
key: "workspace-dashboard",
|
||||
title: "Overview of your projects, activity, and metrics",
|
||||
description:
|
||||
" Welcome to Plane, we are excited to have you here. Create your first project and track your issues, and this page will transform into a space that helps you progress. Admins will also see items which help their team progress.",
|
||||
path: "/empty-state/onboarding/dashboard",
|
||||
// path: "/empty-state/onboarding/",
|
||||
primaryButton: {
|
||||
text: "Build your first project",
|
||||
comicBox: {
|
||||
title: "Everything starts with a project in Plane",
|
||||
description: "A project could be a product’s roadmap, a marketing campaign, or launching a new car.",
|
||||
},
|
||||
},
|
||||
comicBox: {
|
||||
title: "Everything starts with a project in Plane",
|
||||
description: "A project could be a product’s roadmap, a marketing campaign, or launching a new car.",
|
||||
},
|
||||
|
||||
accessType: "workspace",
|
||||
access: EUserWorkspaceRoles.MEMBER,
|
||||
},
|
||||
analytics: {
|
||||
"workspace-analytics": {
|
||||
key: "workspace-analytics",
|
||||
title: "Track progress, workloads, and allocations. Spot trends, remove blockers, and move work faster",
|
||||
description:
|
||||
"See scope versus demand, estimates, and scope creep. Get performance by team members and teams, and make sure your project runs on time.",
|
||||
path: "/empty-state/onboarding/analytics",
|
||||
primaryButton: {
|
||||
text: "Create Cycles and Modules first",
|
||||
comicBox: {
|
||||
title: "Analytics works best with Cycles + Modules",
|
||||
description:
|
||||
"First, timebox your issues into Cycles and, if you can, group issues that span more than a cycle into Modules. Check out both on the left nav.",
|
||||
},
|
||||
},
|
||||
comicBox: {
|
||||
title: "Analytics works best with Cycles + Modules",
|
||||
description:
|
||||
"First, timebox your issues into Cycles and, if you can, group issues that span more than a cycle into Modules. Check out both on the left nav.",
|
||||
},
|
||||
accessType: "workspace",
|
||||
access: EUserWorkspaceRoles.MEMBER,
|
||||
},
|
||||
projects: {
|
||||
"workspace-projects": {
|
||||
key: "workspace-projects",
|
||||
title: "Start a Project",
|
||||
description:
|
||||
"Think of each project as the parent for goal-oriented work. Projects are where Jobs, Cycles, and Modules live and, along with your colleagues, help you achieve that goal.",
|
||||
path: "/empty-state/onboarding/projects",
|
||||
primaryButton: {
|
||||
text: "Start your first project",
|
||||
comicBox: {
|
||||
title: "Everything starts with a project in Plane",
|
||||
description: "A project could be a product’s roadmap, a marketing campaign, or launching a new car.",
|
||||
},
|
||||
},
|
||||
comicBox: {
|
||||
title: "Everything starts with a project in Plane",
|
||||
description: "A project could be a product’s roadmap, a marketing campaign, or launching a new car.",
|
||||
},
|
||||
accessType: "workspace",
|
||||
access: EUserWorkspaceRoles.MEMBER,
|
||||
},
|
||||
"assigned-notification": {
|
||||
key: "assigned-notification",
|
||||
title: "No issues assigned",
|
||||
description: "Updates for issues assigned to you can be seen here",
|
||||
},
|
||||
"created-notification": {
|
||||
key: "created-notification",
|
||||
title: "No updates to issues",
|
||||
description: "Updates to issues created by you can be seen here",
|
||||
},
|
||||
"subscribed-notification": {
|
||||
key: "subscribed-notification",
|
||||
title: "No updates to issues",
|
||||
description: "Updates to any issue you are subscribed to can be seen here",
|
||||
},
|
||||
};
|
||||
|
||||
export const ALL_ISSUES_EMPTY_STATE_DETAILS = {
|
||||
"all-issues": {
|
||||
key: "all-issues",
|
||||
// all-issues
|
||||
"workspace-all-issues": {
|
||||
key: "workspace-all-issues",
|
||||
title: "No issues in the project",
|
||||
description: "First project done! Now, slice your work into trackable pieces with issues. Let's go!",
|
||||
path: "/empty-state/all-issues/all-issues",
|
||||
primaryButton: {
|
||||
text: "Create new issue",
|
||||
},
|
||||
accessType: "workspace",
|
||||
access: EUserWorkspaceRoles.MEMBER,
|
||||
},
|
||||
assigned: {
|
||||
key: "assigned",
|
||||
"workspace-assigned": {
|
||||
key: "workspace-assigned",
|
||||
title: "No issues yet",
|
||||
description: "Issues assigned to you can be tracked from here.",
|
||||
path: "/empty-state/all-issues/assigned",
|
||||
primaryButton: {
|
||||
text: "Create new issue",
|
||||
},
|
||||
accessType: "workspace",
|
||||
access: EUserWorkspaceRoles.MEMBER,
|
||||
},
|
||||
created: {
|
||||
key: "created",
|
||||
"workspace-created": {
|
||||
key: "workspace-created",
|
||||
title: "No issues yet",
|
||||
description: "All issues created by you come here, track them here directly.",
|
||||
path: "/empty-state/all-issues/created",
|
||||
primaryButton: {
|
||||
text: "Create new issue",
|
||||
},
|
||||
accessType: "workspace",
|
||||
access: EUserWorkspaceRoles.MEMBER,
|
||||
},
|
||||
subscribed: {
|
||||
key: "subscribed",
|
||||
"workspace-subscribed": {
|
||||
key: "workspace-subscribed",
|
||||
title: "No issues yet",
|
||||
description: "Subscribe to issues you are interested in, track all of them here.",
|
||||
path: "/empty-state/all-issues/subscribed",
|
||||
},
|
||||
"custom-view": {
|
||||
key: "custom-view",
|
||||
"workspace-custom-view": {
|
||||
key: "workspace-custom-view",
|
||||
title: "No issues yet",
|
||||
description: "Issues that applies to the filters, track all of them here.",
|
||||
path: "/empty-state/all-issues/custom-view",
|
||||
},
|
||||
};
|
||||
|
||||
export const SEARCH_EMPTY_STATE_DETAILS = {
|
||||
views: {
|
||||
key: "views",
|
||||
title: "No matching views",
|
||||
description: "No views match the search criteria. Create a new view instead.",
|
||||
"workspace-no-projects": {
|
||||
key: "workspace-no-projects",
|
||||
title: "No project",
|
||||
description: "To create issues or manage your work, you need to create a project or be a part of one.",
|
||||
path: "/empty-state/onboarding/projects",
|
||||
primaryButton: {
|
||||
text: "Start your first project",
|
||||
comicBox: {
|
||||
title: "Everything starts with a project in Plane",
|
||||
description: "A project could be a product’s roadmap, a marketing campaign, or launching a new car.",
|
||||
},
|
||||
},
|
||||
accessType: "workspace",
|
||||
access: EUserWorkspaceRoles.MEMBER,
|
||||
},
|
||||
projects: {
|
||||
key: "projects",
|
||||
title: "No matching projects",
|
||||
description: "No projects detected with the matching criteria. Create a new project instead.",
|
||||
},
|
||||
commandK: {
|
||||
key: "commandK",
|
||||
title: "No results found. ",
|
||||
},
|
||||
members: {
|
||||
key: "members",
|
||||
title: "No matching members",
|
||||
description: "Add them to the project if they are already a part of the workspace",
|
||||
},
|
||||
};
|
||||
|
||||
export const WORKSPACE_SETTINGS_EMPTY_STATE_DETAILS = {
|
||||
"api-tokens": {
|
||||
key: "api-tokens",
|
||||
// workspace settings
|
||||
"workspace-settings-api-tokens": {
|
||||
key: "workspace-settings-api-tokens",
|
||||
title: "No API tokens created",
|
||||
description:
|
||||
"Plane APIs can be used to integrate your data in Plane with any external system. Create a token to get started.",
|
||||
path: "/empty-state/workspace-settings/api-tokens",
|
||||
},
|
||||
webhooks: {
|
||||
key: "webhooks",
|
||||
"workspace-settings-webhooks": {
|
||||
key: "workspace-settings-webhooks",
|
||||
title: "No webhooks added",
|
||||
description: "Create webhooks to receive real-time updates and automate actions.",
|
||||
path: "/empty-state/workspace-settings/webhooks",
|
||||
},
|
||||
export: {
|
||||
key: "export",
|
||||
"workspace-settings-export": {
|
||||
key: "workspace-settings-export",
|
||||
title: "No previous exports yet",
|
||||
description: "Anytime you export, you will also have a copy here for reference.",
|
||||
path: "/empty-state/workspace-settings/exports",
|
||||
},
|
||||
import: {
|
||||
key: "export",
|
||||
"workspace-settings-import": {
|
||||
key: "workspace-settings-import",
|
||||
title: "No previous imports yet",
|
||||
description: "Find all your previous imports here and download them.",
|
||||
path: "/empty-state/workspace-settings/imports",
|
||||
},
|
||||
};
|
||||
|
||||
// profile empty state
|
||||
export const PROFILE_EMPTY_STATE_DETAILS = {
|
||||
assigned: {
|
||||
key: "assigned",
|
||||
// profile
|
||||
"profile-assigned": {
|
||||
key: "profile-assigned",
|
||||
title: "No issues are assigned to you",
|
||||
description: "Issues assigned to you can be tracked from here.",
|
||||
path: "/empty-state/profile/assigned",
|
||||
},
|
||||
subscribed: {
|
||||
key: "created",
|
||||
"profile-created": {
|
||||
key: "profile-created",
|
||||
title: "No issues yet",
|
||||
description: "All issues created by you come here, track them here directly.",
|
||||
path: "/empty-state/profile/created",
|
||||
},
|
||||
created: {
|
||||
key: "subscribed",
|
||||
"profile-subscribed": {
|
||||
key: "profile-subscribed",
|
||||
title: "No issues yet",
|
||||
description: "Subscribe to issues you are interested in, track all of them here.",
|
||||
path: "/empty-state/profile/subscribed",
|
||||
},
|
||||
};
|
||||
|
||||
// project empty state
|
||||
|
||||
export const PROJECT_SETTINGS_EMPTY_STATE_DETAILS = {
|
||||
labels: {
|
||||
key: "labels",
|
||||
// project settings
|
||||
"project-settings-labels": {
|
||||
key: "project-settings-labels",
|
||||
title: "No labels yet",
|
||||
description: "Create labels to help organize and filter issues in you project.",
|
||||
path: "/empty-state/project-settings/labels",
|
||||
},
|
||||
integrations: {
|
||||
key: "integrations",
|
||||
"project-settings-integrations": {
|
||||
key: "project-settings-integrations",
|
||||
title: "No integrations configured",
|
||||
description: "Configure GitHub and other integrations to sync your project issues.",
|
||||
path: "/empty-state/project-settings/integrations",
|
||||
},
|
||||
estimate: {
|
||||
key: "estimate",
|
||||
"project-settings-estimate": {
|
||||
key: "project-settings-estimate",
|
||||
title: "No estimates added",
|
||||
description: "Create a set of estimates to communicate the amount of work per issue.",
|
||||
path: "/empty-state/project-settings/estimates",
|
||||
},
|
||||
};
|
||||
|
||||
export const CYCLE_EMPTY_STATE_DETAILS = {
|
||||
cycles: {
|
||||
// project cycles
|
||||
"project-cycles": {
|
||||
key: "project-cycles",
|
||||
title: "Group and timebox your work in Cycles.",
|
||||
description:
|
||||
"Break work down by timeboxed chunks, work backwards from your project deadline to set dates, and make tangible progress as a team.",
|
||||
comicBox: {
|
||||
title: "Cycles are repetitive time-boxes.",
|
||||
description:
|
||||
"A sprint, an iteration, and or any other term you use for weekly or fortnightly tracking of work is a cycle.",
|
||||
},
|
||||
path: "/empty-state/onboarding/cycles",
|
||||
primaryButton: {
|
||||
text: "Set your first cycle",
|
||||
comicBox: {
|
||||
title: "Cycles are repetitive time-boxes.",
|
||||
description:
|
||||
"A sprint, an iteration, and or any other term you use for weekly or fortnightly tracking of work is a cycle.",
|
||||
},
|
||||
},
|
||||
accessType: "workspace",
|
||||
access: EUserWorkspaceRoles.MEMBER,
|
||||
},
|
||||
"no-issues": {
|
||||
key: "no-issues",
|
||||
"project-cycle-no-issues": {
|
||||
key: "project-cycle-no-issues",
|
||||
title: "No issues added to the cycle",
|
||||
description: "Add or create issues you wish to timebox and deliver within this cycle",
|
||||
path: "/empty-state/cycle-issues/",
|
||||
primaryButton: {
|
||||
text: "Create new issue ",
|
||||
},
|
||||
secondaryButton: {
|
||||
text: "Add an existing issue",
|
||||
},
|
||||
accessType: "project",
|
||||
access: EUserProjectRoles.MEMBER,
|
||||
},
|
||||
active: {
|
||||
key: "active",
|
||||
"project-cycle-active": {
|
||||
key: "project-cycle-active",
|
||||
title: "No active cycles",
|
||||
description:
|
||||
"An active cycle includes any period that encompasses today's date within its range. Find the progress and details of the active cycle here.",
|
||||
path: "/empty-state/cycle/active",
|
||||
},
|
||||
upcoming: {
|
||||
key: "upcoming",
|
||||
"project-cycle-upcoming": {
|
||||
key: "project-cycle-upcoming",
|
||||
title: "No upcoming cycles",
|
||||
description: "Upcoming cycles on deck! Just add dates to cycles in draft, and they'll show up right here.",
|
||||
path: "/empty-state/cycle/upcoming",
|
||||
},
|
||||
completed: {
|
||||
key: "completed",
|
||||
"project-cycle-completed": {
|
||||
key: "project-cycle-completed",
|
||||
title: "No completed cycles",
|
||||
description: "Any cycle with a past due date is considered completed. Explore all completed cycles here.",
|
||||
path: "/empty-state/cycle/completed",
|
||||
},
|
||||
draft: {
|
||||
key: "draft",
|
||||
"project-cycle-draft": {
|
||||
key: "project-cycle-draft",
|
||||
title: "No draft cycles",
|
||||
description: "No dates added in cycles? Find them here as drafts.",
|
||||
path: "/empty-state/cycle/draft",
|
||||
},
|
||||
};
|
||||
|
||||
export const EMPTY_FILTER_STATE_DETAILS = {
|
||||
archived: {
|
||||
key: "archived",
|
||||
// empty filters
|
||||
"project-empty-filter": {
|
||||
key: "project-empty-filter",
|
||||
title: "No issues found matching the filters applied",
|
||||
path: "/empty-state/empty-filters/",
|
||||
secondaryButton: {
|
||||
text: "Clear all filters",
|
||||
},
|
||||
accessType: "project",
|
||||
access: EUserProjectRoles.MEMBER,
|
||||
},
|
||||
draft: {
|
||||
key: "draft",
|
||||
"project-archived-empty-filter": {
|
||||
key: "project-archived-empty-filter",
|
||||
title: "No issues found matching the filters applied",
|
||||
path: "/empty-state/empty-filters/",
|
||||
secondaryButton: {
|
||||
text: "Clear all filters",
|
||||
},
|
||||
accessType: "project",
|
||||
access: EUserProjectRoles.MEMBER,
|
||||
},
|
||||
project: {
|
||||
key: "project",
|
||||
"project-draft-empty-filter": {
|
||||
key: "project-draft-empty-filter",
|
||||
title: "No issues found matching the filters applied",
|
||||
path: "/empty-state/empty-filters/",
|
||||
secondaryButton: {
|
||||
text: "Clear all filters",
|
||||
},
|
||||
accessType: "project",
|
||||
access: EUserProjectRoles.MEMBER,
|
||||
},
|
||||
};
|
||||
|
||||
export const EMPTY_ISSUE_STATE_DETAILS = {
|
||||
archived: {
|
||||
key: "archived",
|
||||
title: "No archived issues yet",
|
||||
description:
|
||||
"Archived issues help you remove issues you completed or canceled from focus. You can set automation to auto archive issues and find them here.",
|
||||
primaryButton: {
|
||||
text: "Set automation",
|
||||
},
|
||||
},
|
||||
draft: {
|
||||
key: "draft",
|
||||
title: "No draft issues yet",
|
||||
description:
|
||||
"Quickly stepping away but want to keep your place? No worries – save a draft now. Your issues will be right here waiting for you.",
|
||||
},
|
||||
project: {
|
||||
key: "project",
|
||||
// project issues
|
||||
"project-no-issues": {
|
||||
key: "project-no-issues",
|
||||
title: "Create an issue and assign it to someone, even yourself",
|
||||
description:
|
||||
"Think of issues as jobs, tasks, work, or JTBD. Which we like. An issue and its sub-issues are usually time-based actionables assigned to members of your team. Your team creates, assigns, and completes issues to move your project towards its goal.",
|
||||
comicBox: {
|
||||
title: "Issues are building blocks in Plane.",
|
||||
description:
|
||||
"Redesign the Plane UI, Rebrand the company, or Launch the new fuel injection system are examples of issues that likely have sub-issues.",
|
||||
},
|
||||
path: "/empty-state/onboarding/issues",
|
||||
primaryButton: {
|
||||
text: "Create your first issue",
|
||||
comicBox: {
|
||||
title: "Issues are building blocks in Plane.",
|
||||
description:
|
||||
"Redesign the Plane UI, Rebrand the company, or Launch the new fuel injection system are examples of issues that likely have sub-issues.",
|
||||
},
|
||||
},
|
||||
accessType: "project",
|
||||
access: EUserProjectRoles.MEMBER,
|
||||
},
|
||||
};
|
||||
|
||||
export const MODULE_EMPTY_STATE_DETAILS = {
|
||||
"no-issues": {
|
||||
key: "no-issues",
|
||||
"project-archived-no-issues": {
|
||||
key: "project-archived-no-issues",
|
||||
title: "No archived issues yet",
|
||||
description:
|
||||
"Archived issues help you remove issues you completed or cancelled from focus. You can set automation to auto archive issues and find them here.",
|
||||
path: "/empty-state/archived/empty-issues",
|
||||
primaryButton: {
|
||||
text: "Set automation",
|
||||
},
|
||||
accessType: "project",
|
||||
access: EUserProjectRoles.MEMBER,
|
||||
},
|
||||
"project-draft-no-issues": {
|
||||
key: "project-draft-no-issues",
|
||||
title: "No draft issues yet",
|
||||
description:
|
||||
"Quickly stepping away but want to keep your place? No worries – save a draft now. Your issues will be right here waiting for you.",
|
||||
path: "/empty-state/draft/draft-issues-empty",
|
||||
},
|
||||
"views-empty-search": {
|
||||
key: "views-empty-search",
|
||||
title: "No matching views",
|
||||
description: "No views match the search criteria. Create a new view instead.",
|
||||
path: "/empty-state/search/search",
|
||||
},
|
||||
"projects-empty-search": {
|
||||
key: "projects-empty-search",
|
||||
title: "No matching projects",
|
||||
description: "No projects detected with the matching criteria. Create a new project instead.",
|
||||
path: "/empty-state/search/project",
|
||||
},
|
||||
"commandK-empty-search": {
|
||||
key: "commandK-empty-search",
|
||||
title: "No results found. ",
|
||||
path: "/empty-state/search/search",
|
||||
},
|
||||
"members-empty-search": {
|
||||
key: "members-empty-search",
|
||||
title: "No matching members",
|
||||
description: "Add them to the project if they are already a part of the workspace",
|
||||
path: "/empty-state/search/member",
|
||||
},
|
||||
// project module
|
||||
"project-module-issues": {
|
||||
key: "project-modules-issues",
|
||||
title: "No issues in the module",
|
||||
description: "Create or add issues which you want to accomplish as part of this module",
|
||||
path: "/empty-state/module-issues/",
|
||||
primaryButton: {
|
||||
text: "Create new issue ",
|
||||
},
|
||||
secondaryButton: {
|
||||
text: "Add an existing issue",
|
||||
},
|
||||
accessType: "project",
|
||||
access: EUserProjectRoles.MEMBER,
|
||||
},
|
||||
modules: {
|
||||
"project-module": {
|
||||
key: "project-module",
|
||||
title: "Map your project milestones to Modules and track aggregated work easily.",
|
||||
description:
|
||||
"A group of issues that belong to a logical, hierarchical parent form a module. Think of them as a way to track work by project milestones. They have their own periods and deadlines as well as analytics to help you see how close or far you are from a milestone.",
|
||||
|
||||
comicBox: {
|
||||
title: "Modules help group work by hierarchy.",
|
||||
description: "A cart module, a chassis module, and a warehouse module are all good example of this grouping.",
|
||||
},
|
||||
path: "/empty-state/onboarding/modules",
|
||||
primaryButton: {
|
||||
text: "Build your first module",
|
||||
comicBox: {
|
||||
title: "Modules help group work by hierarchy.",
|
||||
description: "A cart module, a chassis module, and a warehouse module are all good example of this grouping.",
|
||||
},
|
||||
},
|
||||
accessType: "project",
|
||||
access: EUserProjectRoles.MEMBER,
|
||||
},
|
||||
};
|
||||
|
||||
export const VIEW_EMPTY_STATE_DETAILS = {
|
||||
"project-views": {
|
||||
// project views
|
||||
"project-view": {
|
||||
key: "project-view",
|
||||
title: "Save filtered views for your project. Create as many as you need",
|
||||
description:
|
||||
"Views are a set of saved filters that you use frequently or want easy access to. All your colleagues in a project can see everyone’s views and choose whichever suits their needs best.",
|
||||
comicBox: {
|
||||
title: "Views work atop Issue properties.",
|
||||
description: "You can create a view from here with as many properties as filters as you see fit.",
|
||||
},
|
||||
path: "/empty-state/onboarding/views",
|
||||
primaryButton: {
|
||||
text: "Create your first view",
|
||||
comicBox: {
|
||||
title: "Views work atop Issue properties.",
|
||||
description: "You can create a view from here with as many properties as filters as you see fit.",
|
||||
},
|
||||
},
|
||||
accessType: "project",
|
||||
access: EUserProjectRoles.MEMBER,
|
||||
},
|
||||
};
|
||||
|
||||
export const PAGE_EMPTY_STATE_DETAILS = {
|
||||
pages: {
|
||||
// project pages
|
||||
"project-page": {
|
||||
key: "pages",
|
||||
title: "Write a note, a doc, or a full knowledge base. Get Galileo, Plane’s AI assistant, to help you get started",
|
||||
description:
|
||||
"Pages are thoughts potting space in Plane. Take down meeting notes, format them easily, embed issues, lay them out using a library of components, and keep them all in your project’s context. To make short work of any doc, invoke Galileo, Plane’s AI, with a shortcut or the click of a button.",
|
||||
path: "/empty-state/onboarding/pages",
|
||||
primaryButton: {
|
||||
text: "Create your first page",
|
||||
comicBox: {
|
||||
title: "A page can be a doc or a doc of docs.",
|
||||
description:
|
||||
"We wrote Nikhil and Meera’s love story. You could write your project’s mission, goals, and eventual vision.",
|
||||
},
|
||||
},
|
||||
comicBox: {
|
||||
title: "A page can be a doc or a doc of docs.",
|
||||
description:
|
||||
"We wrote Nikhil and Meera’s love story. You could write your project’s mission, goals, and eventual vision.",
|
||||
},
|
||||
accessType: "project",
|
||||
access: EUserProjectRoles.MEMBER,
|
||||
},
|
||||
All: {
|
||||
key: "all",
|
||||
"project-page-all": {
|
||||
key: "project-page-all",
|
||||
title: "Write a note, a doc, or a full knowledge base",
|
||||
description:
|
||||
"Pages help you organise your thoughts to create wikis, discussions or even document heated takes for your project. Use it wisely!",
|
||||
path: "/empty-state/pages/all",
|
||||
},
|
||||
Favorites: {
|
||||
key: "favorites",
|
||||
"project-page-favorite": {
|
||||
key: "project-page-favorite",
|
||||
title: "No favorite pages yet",
|
||||
description: "Favorites for quick access? mark them and find them right here.",
|
||||
path: "/empty-state/pages/favorites",
|
||||
},
|
||||
Private: {
|
||||
key: "private",
|
||||
"project-page-private": {
|
||||
key: "project-page-private",
|
||||
title: "No private pages yet",
|
||||
description: "Keep your private thoughts here. When you're ready to share, the team's just a click away.",
|
||||
path: "/empty-state/pages/private",
|
||||
},
|
||||
Shared: {
|
||||
key: "shared",
|
||||
"project-page-shared": {
|
||||
key: "project-page-shared",
|
||||
title: "No shared pages yet",
|
||||
description: "See pages shared with everyone in your project right here.",
|
||||
path: "/empty-state/pages/shared",
|
||||
},
|
||||
Archived: {
|
||||
key: "archived",
|
||||
"project-page-archived": {
|
||||
key: "project-page-archived",
|
||||
title: "No archived pages yet",
|
||||
description: "Archive pages not on your radar. Access them here when needed.",
|
||||
path: "/empty-state/pages/archived",
|
||||
},
|
||||
Recent: {
|
||||
key: "recent",
|
||||
"project-page-recent": {
|
||||
key: "project-page-recent",
|
||||
title: "Write a note, a doc, or a full knowledge base",
|
||||
description:
|
||||
"Pages help you organise your thoughts to create wikis, discussions or even document heated takes for your project. Use it wisely! Pages will be sorted and grouped by last updated",
|
||||
path: "/empty-state/pages/recent",
|
||||
primaryButton: {
|
||||
text: "Create new page",
|
||||
},
|
||||
accessType: "project",
|
||||
access: EUserProjectRoles.MEMBER,
|
||||
},
|
||||
};
|
||||
} as const;
|
||||
|
||||
export const EMPTY_STATE_DETAILS: Record<EmptyStateKeys, EmptyStateDetails> = emptyStateDetails;
|
||||
|
@ -1,44 +1,33 @@
|
||||
import React, { Fragment, ReactElement } from "react";
|
||||
import { observer } from "mobx-react-lite";
|
||||
import { useRouter } from "next/router";
|
||||
import { useTheme } from "next-themes";
|
||||
import { Tab } from "@headlessui/react";
|
||||
// hooks
|
||||
import { useApplication, useEventTracker, useProject, useWorkspace } from "hooks/store";
|
||||
// layouts
|
||||
import { AppLayout } from "layouts/app-layout";
|
||||
// components
|
||||
import { CustomAnalytics, ScopeAndDemand } from "components/analytics";
|
||||
import { PageHead } from "components/core";
|
||||
import { EmptyState, getEmptyStateImagePath } from "components/empty-state";
|
||||
import { EmptyState } from "components/empty-state";
|
||||
import { WorkspaceAnalyticsHeader } from "components/headers";
|
||||
// constants
|
||||
import { ANALYTICS_TABS } from "constants/analytics";
|
||||
import { WORKSPACE_EMPTY_STATE_DETAILS } from "constants/empty-state";
|
||||
import { EUserWorkspaceRoles } from "constants/workspace";
|
||||
import { useApplication, useEventTracker, useProject, useUser, useWorkspace } from "hooks/store";
|
||||
import { AppLayout } from "layouts/app-layout";
|
||||
// type
|
||||
import { NextPageWithLayout } from "lib/types";
|
||||
// constants
|
||||
import { ANALYTICS_TABS } from "constants/analytics";
|
||||
import { EmptyStateType } from "constants/empty-state";
|
||||
|
||||
const AnalyticsPage: NextPageWithLayout = observer(() => {
|
||||
const router = useRouter();
|
||||
const { analytics_tab } = router.query;
|
||||
// theme
|
||||
const { resolvedTheme } = useTheme();
|
||||
// store hooks
|
||||
const {
|
||||
commandPalette: { toggleCreateProjectModal },
|
||||
} = useApplication();
|
||||
const { setTrackElement } = useEventTracker();
|
||||
const {
|
||||
membership: { currentWorkspaceRole },
|
||||
currentUser,
|
||||
} = useUser();
|
||||
const { workspaceProjectIds } = useProject();
|
||||
const { currentWorkspace } = useWorkspace();
|
||||
// derived values
|
||||
const isLightMode = resolvedTheme ? resolvedTheme === "light" : currentUser?.theme.theme === "light";
|
||||
const EmptyStateImagePath = getEmptyStateImagePath("onboarding", "analytics", isLightMode);
|
||||
const isEditingAllowed = !!currentWorkspaceRole && currentWorkspaceRole >= EUserWorkspaceRoles.MEMBER;
|
||||
const pageTitle = currentWorkspace?.name ? `${currentWorkspace?.name} - Analytics` : undefined;
|
||||
|
||||
return (
|
||||
@ -79,22 +68,11 @@ const AnalyticsPage: NextPageWithLayout = observer(() => {
|
||||
</div>
|
||||
) : (
|
||||
<EmptyState
|
||||
image={EmptyStateImagePath}
|
||||
title={WORKSPACE_EMPTY_STATE_DETAILS["analytics"].title}
|
||||
description={WORKSPACE_EMPTY_STATE_DETAILS["analytics"].description}
|
||||
primaryButton={{
|
||||
text: WORKSPACE_EMPTY_STATE_DETAILS["analytics"].primaryButton.text,
|
||||
onClick: () => {
|
||||
setTrackElement("Analytics empty state");
|
||||
toggleCreateProjectModal(true);
|
||||
},
|
||||
type={EmptyStateType.WORKSPACE_ANALYTICS}
|
||||
primaryButtonOnClick={() => {
|
||||
setTrackElement("Analytics empty state");
|
||||
toggleCreateProjectModal(true);
|
||||
}}
|
||||
comicBox={{
|
||||
title: WORKSPACE_EMPTY_STATE_DETAILS["analytics"].comicBox.title,
|
||||
description: WORKSPACE_EMPTY_STATE_DETAILS["analytics"].comicBox.description,
|
||||
}}
|
||||
size="lg"
|
||||
disabled={!isEditingAllowed}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
|
@ -1,39 +1,31 @@
|
||||
import { Fragment, useCallback, useState, ReactElement } from "react";
|
||||
import { observer } from "mobx-react-lite";
|
||||
import { useRouter } from "next/router";
|
||||
import { useTheme } from "next-themes";
|
||||
import { Tab } from "@headlessui/react";
|
||||
// hooks
|
||||
import { Tooltip } from "@plane/ui";
|
||||
import { PageHead } from "components/core";
|
||||
import { CyclesView, ActiveCycleDetails, CycleCreateUpdateModal } from "components/cycles";
|
||||
import { EmptyState, getEmptyStateImagePath } from "components/empty-state";
|
||||
import { CyclesHeader } from "components/headers";
|
||||
import { CycleModuleBoardLayout, CycleModuleListLayout, GanttLayoutLoader } from "components/ui";
|
||||
import { CYCLE_TAB_LIST, CYCLE_VIEW_LAYOUTS } from "constants/cycle";
|
||||
import { CYCLE_EMPTY_STATE_DETAILS } from "constants/empty-state";
|
||||
import { EUserWorkspaceRoles } from "constants/workspace";
|
||||
import { useEventTracker, useCycle, useUser, useProject } from "hooks/store";
|
||||
import { useEventTracker, useCycle, useProject } from "hooks/store";
|
||||
import useLocalStorage from "hooks/use-local-storage";
|
||||
// layouts
|
||||
import { AppLayout } from "layouts/app-layout";
|
||||
// components
|
||||
import { PageHead } from "components/core";
|
||||
import { CyclesHeader } from "components/headers";
|
||||
import { CyclesView, ActiveCycleDetails, CycleCreateUpdateModal } from "components/cycles";
|
||||
import { EmptyState } from "components/empty-state";
|
||||
import { CycleModuleBoardLayout, CycleModuleListLayout, GanttLayoutLoader } from "components/ui";
|
||||
// ui
|
||||
import { Tooltip } from "@plane/ui";
|
||||
// types
|
||||
import { NextPageWithLayout } from "lib/types";
|
||||
import { TCycleView, TCycleLayout } from "@plane/types";
|
||||
// constants
|
||||
import { CYCLE_TAB_LIST, CYCLE_VIEW_LAYOUTS } from "constants/cycle";
|
||||
import { EmptyStateType } from "constants/empty-state";
|
||||
|
||||
const ProjectCyclesPage: NextPageWithLayout = observer(() => {
|
||||
const [createModal, setCreateModal] = useState(false);
|
||||
// theme
|
||||
const { resolvedTheme } = useTheme();
|
||||
// store hooks
|
||||
const { setTrackElement } = useEventTracker();
|
||||
const {
|
||||
membership: { currentProjectRole },
|
||||
currentUser,
|
||||
} = useUser();
|
||||
const { currentProjectCycleIds, loader } = useCycle();
|
||||
const { getProjectById } = useProject();
|
||||
// router
|
||||
@ -43,10 +35,7 @@ const ProjectCyclesPage: NextPageWithLayout = observer(() => {
|
||||
const { storedValue: cycleTab, setValue: setCycleTab } = useLocalStorage<TCycleView>("cycle_tab", "active");
|
||||
const { storedValue: cycleLayout, setValue: setCycleLayout } = useLocalStorage<TCycleLayout>("cycle_layout", "list");
|
||||
// derived values
|
||||
const isLightMode = resolvedTheme ? resolvedTheme === "light" : currentUser?.theme.theme === "light";
|
||||
const EmptyStateImagePath = getEmptyStateImagePath("onboarding", "cycles", isLightMode);
|
||||
const totalCycles = currentProjectCycleIds?.length ?? 0;
|
||||
const isEditingAllowed = !!currentProjectRole && currentProjectRole >= EUserWorkspaceRoles.MEMBER;
|
||||
const project = projectId ? getProjectById(projectId?.toString()) : undefined;
|
||||
const pageTitle = project?.name ? `${project?.name} - Cycles` : undefined;
|
||||
|
||||
@ -89,22 +78,11 @@ const ProjectCyclesPage: NextPageWithLayout = observer(() => {
|
||||
{totalCycles === 0 ? (
|
||||
<div className="h-full place-items-center">
|
||||
<EmptyState
|
||||
title={CYCLE_EMPTY_STATE_DETAILS["cycles"].title}
|
||||
description={CYCLE_EMPTY_STATE_DETAILS["cycles"].description}
|
||||
image={EmptyStateImagePath}
|
||||
comicBox={{
|
||||
title: CYCLE_EMPTY_STATE_DETAILS["cycles"].comicBox.title,
|
||||
description: CYCLE_EMPTY_STATE_DETAILS["cycles"].comicBox.description,
|
||||
type={EmptyStateType.PROJECT_CYCLES}
|
||||
primaryButtonOnClick={() => {
|
||||
setTrackElement("Cycle empty state");
|
||||
setCreateModal(true);
|
||||
}}
|
||||
primaryButton={{
|
||||
text: CYCLE_EMPTY_STATE_DETAILS["cycles"].primaryButton.text,
|
||||
onClick: () => {
|
||||
setTrackElement("Cycle empty state");
|
||||
setCreateModal(true);
|
||||
},
|
||||
}}
|
||||
size="lg"
|
||||
disabled={!isEditingAllowed}
|
||||
/>
|
||||
</div>
|
||||
) : (
|
||||
|
@ -2,18 +2,9 @@ import { useState, Fragment, ReactElement } from "react";
|
||||
import { observer } from "mobx-react-lite";
|
||||
import dynamic from "next/dynamic";
|
||||
import { useRouter } from "next/router";
|
||||
import { useTheme } from "next-themes";
|
||||
import useSWR from "swr";
|
||||
import { Tab } from "@headlessui/react";
|
||||
// hooks
|
||||
import { PageHead } from "components/core";
|
||||
import { EmptyState, getEmptyStateImagePath } from "components/empty-state";
|
||||
import { PagesHeader } from "components/headers";
|
||||
import { RecentPagesList, CreateUpdatePageModal } from "components/pages";
|
||||
import { PagesLoader } from "components/ui";
|
||||
import { PAGE_EMPTY_STATE_DETAILS } from "constants/empty-state";
|
||||
import { PAGE_TABS_LIST } from "constants/page";
|
||||
import { EUserWorkspaceRoles } from "constants/workspace";
|
||||
import { useApplication, useEventTracker, useUser, useProject } from "hooks/store";
|
||||
import { useProjectPages } from "hooks/store/use-project-page";
|
||||
import useLocalStorage from "hooks/use-local-storage";
|
||||
@ -22,9 +13,16 @@ import useSize from "hooks/use-window-size";
|
||||
// layouts
|
||||
import { AppLayout } from "layouts/app-layout";
|
||||
// components
|
||||
import { RecentPagesList, CreateUpdatePageModal } from "components/pages";
|
||||
import { EmptyState } from "components/empty-state";
|
||||
import { PagesHeader } from "components/headers";
|
||||
import { PagesLoader } from "components/ui";
|
||||
import { PageHead } from "components/core";
|
||||
// types
|
||||
import { NextPageWithLayout } from "lib/types";
|
||||
// constants
|
||||
import { PAGE_TABS_LIST } from "constants/page";
|
||||
import { EmptyStateType } from "constants/empty-state";
|
||||
|
||||
const AllPagesList = dynamic<any>(() => import("components/pages").then((a) => a.AllPagesList), {
|
||||
ssr: false,
|
||||
@ -52,14 +50,8 @@ const ProjectPagesPage: NextPageWithLayout = observer(() => {
|
||||
const { workspaceSlug, projectId } = router.query;
|
||||
// states
|
||||
const [createUpdatePageModal, setCreateUpdatePageModal] = useState(false);
|
||||
// theme
|
||||
const { resolvedTheme } = useTheme();
|
||||
// store hooks
|
||||
const {
|
||||
currentUser,
|
||||
currentUserLoader,
|
||||
membership: { currentProjectRole },
|
||||
} = useUser();
|
||||
const { currentUser, currentUserLoader } = useUser();
|
||||
const {
|
||||
commandPalette: { toggleCreatePageModal },
|
||||
} = useApplication();
|
||||
@ -103,9 +95,6 @@ const ProjectPagesPage: NextPageWithLayout = observer(() => {
|
||||
};
|
||||
|
||||
// derived values
|
||||
const isLightMode = resolvedTheme ? resolvedTheme === "light" : currentUser?.theme.theme === "light";
|
||||
const EmptyStateImagePath = getEmptyStateImagePath("onboarding", "pages", isLightMode);
|
||||
const isEditingAllowed = !!currentProjectRole && currentProjectRole >= EUserWorkspaceRoles.MEMBER;
|
||||
const project = projectId ? getProjectById(projectId.toString()) : undefined;
|
||||
const pageTitle = project?.name ? `${project?.name} - Pages` : undefined;
|
||||
|
||||
@ -216,22 +205,11 @@ const ProjectPagesPage: NextPageWithLayout = observer(() => {
|
||||
</>
|
||||
) : (
|
||||
<EmptyState
|
||||
image={EmptyStateImagePath}
|
||||
title={PAGE_EMPTY_STATE_DETAILS["pages"].title}
|
||||
description={PAGE_EMPTY_STATE_DETAILS["pages"].description}
|
||||
primaryButton={{
|
||||
text: PAGE_EMPTY_STATE_DETAILS["pages"].primaryButton.text,
|
||||
onClick: () => {
|
||||
setTrackElement("Pages empty state");
|
||||
toggleCreatePageModal(true);
|
||||
},
|
||||
type={EmptyStateType.PROJECT_PAGE}
|
||||
primaryButtonOnClick={() => {
|
||||
setTrackElement("Pages empty state");
|
||||
toggleCreatePageModal(true);
|
||||
}}
|
||||
comicBox={{
|
||||
title: PAGE_EMPTY_STATE_DETAILS["pages"].comicBox.title,
|
||||
description: PAGE_EMPTY_STATE_DETAILS["pages"].comicBox.description,
|
||||
}}
|
||||
size="lg"
|
||||
disabled={!isEditingAllowed}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
|
@ -1,17 +1,9 @@
|
||||
import { ReactElement } from "react";
|
||||
import { observer } from "mobx-react";
|
||||
import { useRouter } from "next/router";
|
||||
import { useTheme } from "next-themes";
|
||||
import useSWR from "swr";
|
||||
// hooks
|
||||
import { PageHead } from "components/core";
|
||||
import { EmptyState, getEmptyStateImagePath } from "components/empty-state";
|
||||
import { ProjectSettingHeader } from "components/headers";
|
||||
import { IntegrationCard } from "components/project";
|
||||
import { IntegrationsSettingsLoader } from "components/ui";
|
||||
import { PROJECT_SETTINGS_EMPTY_STATE_DETAILS } from "constants/empty-state";
|
||||
import { PROJECT_DETAILS, WORKSPACE_INTEGRATIONS } from "constants/fetch-keys";
|
||||
import { useUser } from "hooks/store";
|
||||
// layouts
|
||||
import { AppLayout } from "layouts/app-layout";
|
||||
import { ProjectSettingLayout } from "layouts/settings-layout";
|
||||
@ -20,10 +12,17 @@ import { NextPageWithLayout } from "lib/types";
|
||||
import { IntegrationService } from "services/integrations";
|
||||
import { ProjectService } from "services/project";
|
||||
// components
|
||||
import { PageHead } from "components/core";
|
||||
import { IntegrationCard } from "components/project";
|
||||
import { ProjectSettingHeader } from "components/headers";
|
||||
import { EmptyState } from "components/empty-state";
|
||||
// ui
|
||||
// types
|
||||
import { IProject } from "@plane/types";
|
||||
// fetch-keys
|
||||
import { PROJECT_DETAILS, WORKSPACE_INTEGRATIONS } from "constants/fetch-keys";
|
||||
// constants
|
||||
import { EmptyStateType } from "constants/empty-state";
|
||||
|
||||
// services
|
||||
const integrationService = new IntegrationService();
|
||||
@ -32,10 +31,6 @@ const projectService = new ProjectService();
|
||||
const ProjectIntegrationsPage: NextPageWithLayout = observer(() => {
|
||||
const router = useRouter();
|
||||
const { workspaceSlug, projectId } = router.query;
|
||||
// theme
|
||||
const { resolvedTheme } = useTheme();
|
||||
// store hooks
|
||||
const { currentUser } = useUser();
|
||||
// fetch project details
|
||||
const { data: projectDetails } = useSWR<IProject>(
|
||||
workspaceSlug && projectId ? PROJECT_DETAILS(projectId as string) : null,
|
||||
@ -47,9 +42,6 @@ const ProjectIntegrationsPage: NextPageWithLayout = observer(() => {
|
||||
() => (workspaceSlug ? integrationService.getWorkspaceIntegrationsList(workspaceSlug as string) : null)
|
||||
);
|
||||
// derived values
|
||||
const emptyStateDetail = PROJECT_SETTINGS_EMPTY_STATE_DETAILS["integrations"];
|
||||
const isLightMode = resolvedTheme ? resolvedTheme === "light" : currentUser?.theme.theme === "light";
|
||||
const emptyStateImage = getEmptyStateImagePath("project-settings", "integrations", isLightMode);
|
||||
const isAdmin = projectDetails?.member_role === 20;
|
||||
const pageTitle = projectDetails?.name ? `${projectDetails?.name} - Integrations` : undefined;
|
||||
|
||||
@ -70,15 +62,8 @@ const ProjectIntegrationsPage: NextPageWithLayout = observer(() => {
|
||||
) : (
|
||||
<div className="h-full w-full py-8">
|
||||
<EmptyState
|
||||
title={emptyStateDetail.title}
|
||||
description={emptyStateDetail.description}
|
||||
image={emptyStateImage}
|
||||
primaryButton={{
|
||||
text: "Configure now",
|
||||
onClick: () => router.push(`/${workspaceSlug}/settings/integrations`),
|
||||
}}
|
||||
size="lg"
|
||||
disabled={!isAdmin}
|
||||
type={EmptyStateType.PROJECT_SETTINGS_INTEGRATIONS}
|
||||
primaryButtonLink={`/${workspaceSlug}/settings/integrations`}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
|
@ -1,29 +1,28 @@
|
||||
import React, { useState } from "react";
|
||||
import { observer } from "mobx-react-lite";
|
||||
import { useRouter } from "next/router";
|
||||
import { useTheme } from "next-themes";
|
||||
import useSWR from "swr";
|
||||
// store hooks
|
||||
import { Button } from "@plane/ui";
|
||||
import { ApiTokenListItem, CreateApiTokenModal } from "components/api-token";
|
||||
import { PageHead } from "components/core";
|
||||
import { EmptyState, getEmptyStateImagePath } from "components/empty-state";
|
||||
import { WorkspaceSettingHeader } from "components/headers";
|
||||
import { APITokenSettingsLoader } from "components/ui";
|
||||
import { WORKSPACE_SETTINGS_EMPTY_STATE_DETAILS } from "constants/empty-state";
|
||||
import { API_TOKENS_LIST } from "constants/fetch-keys";
|
||||
import { EUserWorkspaceRoles } from "constants/workspace";
|
||||
import { useUser, useWorkspace } from "hooks/store";
|
||||
// layouts
|
||||
import { AppLayout } from "layouts/app-layout";
|
||||
import { WorkspaceSettingLayout } from "layouts/settings-layout";
|
||||
// component
|
||||
import { APITokenSettingsLoader } from "components/ui";
|
||||
import { WorkspaceSettingHeader } from "components/headers";
|
||||
import { ApiTokenListItem, CreateApiTokenModal } from "components/api-token";
|
||||
import { EmptyState } from "components/empty-state";
|
||||
import { PageHead } from "components/core";
|
||||
// ui
|
||||
import { Button } from "@plane/ui";
|
||||
// services
|
||||
import { NextPageWithLayout } from "lib/types";
|
||||
import { APITokenService } from "services/api_token.service";
|
||||
// types
|
||||
// constants
|
||||
import { API_TOKENS_LIST } from "constants/fetch-keys";
|
||||
import { EUserWorkspaceRoles } from "constants/workspace";
|
||||
import { EmptyStateType } from "constants/empty-state";
|
||||
|
||||
const apiTokenService = new APITokenService();
|
||||
|
||||
@ -33,12 +32,9 @@ const ApiTokensPage: NextPageWithLayout = observer(() => {
|
||||
// router
|
||||
const router = useRouter();
|
||||
const { workspaceSlug } = router.query;
|
||||
// theme
|
||||
const { resolvedTheme } = useTheme();
|
||||
// store hooks
|
||||
const {
|
||||
membership: { currentWorkspaceRole },
|
||||
currentUser,
|
||||
} = useUser();
|
||||
const { currentWorkspace } = useWorkspace();
|
||||
|
||||
@ -48,9 +44,6 @@ const ApiTokensPage: NextPageWithLayout = observer(() => {
|
||||
workspaceSlug && isAdmin ? apiTokenService.getApiTokens(workspaceSlug.toString()) : null
|
||||
);
|
||||
|
||||
const emptyStateDetail = WORKSPACE_SETTINGS_EMPTY_STATE_DETAILS["api-tokens"];
|
||||
const isLightMode = resolvedTheme ? resolvedTheme === "light" : currentUser?.theme.theme === "light";
|
||||
const emptyStateImage = getEmptyStateImagePath("workspace-settings", "api-tokens", isLightMode);
|
||||
const pageTitle = currentWorkspace?.name ? `${currentWorkspace.name} - API Tokens` : undefined;
|
||||
|
||||
if (!isAdmin)
|
||||
@ -95,12 +88,7 @@ const ApiTokensPage: NextPageWithLayout = observer(() => {
|
||||
</Button>
|
||||
</div>
|
||||
<div className="h-full w-full flex items-center justify-center">
|
||||
<EmptyState
|
||||
title={emptyStateDetail.title}
|
||||
description={emptyStateDetail.description}
|
||||
image={emptyStateImage}
|
||||
size="lg"
|
||||
/>
|
||||
<EmptyState type={EmptyStateType.WORKSPACE_SETTINGS_API_TOKENS} />
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
@ -1,25 +1,24 @@
|
||||
import React, { useEffect, useState } from "react";
|
||||
import { observer } from "mobx-react-lite";
|
||||
import { useRouter } from "next/router";
|
||||
import { useTheme } from "next-themes";
|
||||
import useSWR from "swr";
|
||||
// hooks
|
||||
import { Button } from "@plane/ui";
|
||||
import { PageHead } from "components/core";
|
||||
import { EmptyState, getEmptyStateImagePath } from "components/empty-state";
|
||||
import { WorkspaceSettingHeader } from "components/headers";
|
||||
import { WebhookSettingsLoader } from "components/ui";
|
||||
import { WebhooksList, CreateWebhookModal } from "components/web-hooks";
|
||||
import { WORKSPACE_SETTINGS_EMPTY_STATE_DETAILS } from "constants/empty-state";
|
||||
import { useUser, useWebhook, useWorkspace } from "hooks/store";
|
||||
// layouts
|
||||
import { AppLayout } from "layouts/app-layout";
|
||||
import { WorkspaceSettingLayout } from "layouts/settings-layout";
|
||||
// components
|
||||
import { PageHead } from "components/core";
|
||||
import { WorkspaceSettingHeader } from "components/headers";
|
||||
import { WebhookSettingsLoader } from "components/ui";
|
||||
import { WebhooksList, CreateWebhookModal } from "components/web-hooks";
|
||||
import { EmptyState } from "components/empty-state";
|
||||
// ui
|
||||
import { Button } from "@plane/ui";
|
||||
// types
|
||||
import { NextPageWithLayout } from "lib/types";
|
||||
// constants
|
||||
import { EmptyStateType } from "constants/empty-state";
|
||||
|
||||
const WebhooksListPage: NextPageWithLayout = observer(() => {
|
||||
// states
|
||||
@ -27,12 +26,9 @@ const WebhooksListPage: NextPageWithLayout = observer(() => {
|
||||
// router
|
||||
const router = useRouter();
|
||||
const { workspaceSlug } = router.query;
|
||||
// theme
|
||||
const { resolvedTheme } = useTheme();
|
||||
// mobx store
|
||||
const {
|
||||
membership: { currentWorkspaceRole },
|
||||
currentUser,
|
||||
} = useUser();
|
||||
const { fetchWebhooks, webhooks, clearSecretKey, webhookSecretKey, createWebhook } = useWebhook();
|
||||
const { currentWorkspace } = useWorkspace();
|
||||
@ -44,10 +40,6 @@ const WebhooksListPage: NextPageWithLayout = observer(() => {
|
||||
workspaceSlug && isAdmin ? () => fetchWebhooks(workspaceSlug.toString()) : null
|
||||
);
|
||||
|
||||
const emptyStateDetail = WORKSPACE_SETTINGS_EMPTY_STATE_DETAILS["webhooks"];
|
||||
|
||||
const isLightMode = resolvedTheme ? resolvedTheme === "light" : currentUser?.theme.theme === "light";
|
||||
const emptyStateImage = getEmptyStateImagePath("workspace-settings", "webhooks", isLightMode);
|
||||
const pageTitle = currentWorkspace?.name ? `${currentWorkspace.name} - Webhooks` : undefined;
|
||||
|
||||
// clear secret key when modal is closed.
|
||||
@ -99,12 +91,7 @@ const WebhooksListPage: NextPageWithLayout = observer(() => {
|
||||
</Button>
|
||||
</div>
|
||||
<div className="h-full w-full flex items-center justify-center">
|
||||
<EmptyState
|
||||
title={emptyStateDetail.title}
|
||||
description={emptyStateDetail.description}
|
||||
image={emptyStateImage}
|
||||
size="lg"
|
||||
/>
|
||||
<EmptyState type={EmptyStateType.WORKSPACE_SETTINGS_WEBHOOKS} />
|
||||
</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 |