Merge branch 'develop' of github.com:makeplane/plane into develop

This commit is contained in:
sriram veeraghanta 2024-03-06 20:19:01 +05:30
commit b4adb82d40
42 changed files with 906 additions and 1187 deletions

View File

@ -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",

View File

@ -130,6 +130,7 @@ export type TProjectIssuesSearchParams = {
sub_issue?: boolean;
issue_id?: string;
workspace_search: boolean;
target_date?: string;
};
export interface ISearchIssueResponse {

View File

@ -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 ?? "");

View File

@ -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" />
)}
</>
);

View File

@ -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" />
)}
</>
) : (

View File

@ -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>
);
};

View File

@ -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>
)
) : (

View File

@ -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>
)
) : (

View File

@ -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>
)
) : (

View File

@ -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}

View File

@ -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}
/>

View File

@ -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)}
/>

View File

@ -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>
)}
</>

View File

@ -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}

View File

@ -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}
/>
);

View File

@ -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}
/>

View File

@ -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>
);
});

View File

@ -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>
</>
);

View File

@ -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>
);
});

View File

@ -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>
</>
);

View File

@ -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>
);
});

View File

@ -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>

View File

@ -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 && (

View File

@ -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}
/>
)}
</>

View File

@ -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}
/>
)}
</>

View File

@ -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>

View File

@ -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}
/>
</>
)}

View File

@ -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 (

View File

@ -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}
/>
)}
</>

View File

@ -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)} />
)}
</>
);

View File

@ -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

View File

@ -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 products roadmap, a marketing campaign, or launching a new car.",
},
},
comicBox: {
title: "Everything starts with a project in Plane",
description: "A project could be a products roadmap, a marketing campaign, or launching a new car.",
},
accessType: "workspace",
access: EUserWorkspaceRoles.MEMBER,
},
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 products roadmap, a marketing campaign, or launching a new car.",
},
},
comicBox: {
title: "Everything starts with a project in Plane",
description: "A project could be a products roadmap, a marketing campaign, or launching a new car.",
},
accessType: "workspace",
access: EUserWorkspaceRoles.MEMBER,
},
"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 products 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 everyones 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, Planes 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 projects context. To make short work of any doc, invoke Galileo, Planes AI, with a shortcut or the click of a button.",
path: "/empty-state/onboarding/pages",
primaryButton: {
text: "Create your first page",
comicBox: {
title: "A page can be a doc or a doc of docs.",
description:
"We wrote Nikhil and Meeras love story. You could write your projects mission, goals, and eventual vision.",
},
},
comicBox: {
title: "A page can be a doc or a doc of docs.",
description:
"We wrote Nikhil and Meeras love story. You could write your projects mission, goals, and eventual vision.",
},
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;

View File

@ -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}
/>
)}
</>

View File

@ -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>
) : (

View File

@ -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}
/>
)}
</>

View File

@ -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>
)

View File

@ -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>
)}

View File

@ -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>
)}

View File

Before

Width:  |  Height:  |  Size: 43 KiB

After

Width:  |  Height:  |  Size: 43 KiB

View File

Before

Width:  |  Height:  |  Size: 67 KiB

After

Width:  |  Height:  |  Size: 67 KiB

View File

Before

Width:  |  Height:  |  Size: 43 KiB

After

Width:  |  Height:  |  Size: 43 KiB

View File

Before

Width:  |  Height:  |  Size: 68 KiB

After

Width:  |  Height:  |  Size: 68 KiB