Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
105 changes: 76 additions & 29 deletions web/components/issues/issue-layouts/empty-states/cycle.tsx
Original file line number Diff line number Diff line change
@@ -1,38 +1,85 @@
import { useState } from "react";
import { observer } from "mobx-react-lite";
import { PlusIcon } from "lucide-react";
// mobx store
import { useMobxStore } from "lib/mobx/store-provider";
// hooks
import useToast from "hooks/use-toast";
// components
import { EmptyState } from "components/common";
import { ExistingIssuesListModal } from "components/core";
// ui
import { Button } from "@plane/ui";
// assets
import emptyIssue from "public/empty-state/issue.svg";
import { Button } from "@plane/ui";
// types
import { ISearchIssueResponse } from "types";

type Props = {
openIssuesListModal: () => void;
workspaceSlug: string | undefined;
projectId: string | undefined;
cycleId: string | undefined;
};

export const CycleEmptyState: React.FC<Props> = ({ openIssuesListModal }) => (
<div className="h-full w-full grid place-items-center">
<EmptyState
title="Cycle issues will appear here"
description="Issues help you track individual pieces of work. With Issues, keep track of what's going on, who is working on it, and what's done."
image={emptyIssue}
primaryButton={{
text: "New issue",
icon: <PlusIcon className="h-3 w-3" strokeWidth={2} />,
onClick: () => {
const e = new KeyboardEvent("keydown", {
key: "c",
});
document.dispatchEvent(e);
},
}}
secondaryButton={
<Button
variant="neutral-primary"
prependIcon={<PlusIcon className="h-3 w-3" strokeWidth={2} onClick={openIssuesListModal} />}
>
Add an existing issue
</Button>
}
/>
</div>
);
export const CycleEmptyState: React.FC<Props> = observer((props) => {
const { workspaceSlug, projectId, cycleId } = props;
// states
const [cycleIssuesListModal, setCycleIssuesListModal] = useState(false);

const { cycleIssue: cycleIssueStore } = useMobxStore();

const { setToastAlert } = useToast();

const handleAddIssuesToCycle = async (data: ISearchIssueResponse[]) => {
if (!workspaceSlug || !projectId || !cycleId) return;

const issueIds = data.map((i) => i.id);

await cycleIssueStore
.addIssueToCycle(workspaceSlug.toString(), projectId.toString(), cycleId.toString(), issueIds)
.catch(() => {
setToastAlert({
type: "error",
title: "Error!",
message: "Selected issues could not be added to the cycle. Please try again.",
});
});
};

return (
<>
<ExistingIssuesListModal
isOpen={cycleIssuesListModal}
handleClose={() => setCycleIssuesListModal(false)}
searchParams={{ cycle: true }}
handleOnSubmit={handleAddIssuesToCycle}
/>
<div className="h-full w-full grid place-items-center">
<EmptyState
title="Cycle issues will appear here"
description="Issues help you track individual pieces of work. With Issues, keep track of what's going on, who is working on it, and what's done."
image={emptyIssue}
primaryButton={{
text: "New issue",
icon: <PlusIcon className="h-3 w-3" strokeWidth={2} />,
onClick: () => {
const e = new KeyboardEvent("keydown", {
key: "c",
});
document.dispatchEvent(e);
},
}}
secondaryButton={
<Button
variant="neutral-primary"
prependIcon={<PlusIcon className="h-3 w-3" strokeWidth={2} />}
onClick={() => setCycleIssuesListModal(true)}
>
Add an existing issue
</Button>
}
/>
</div>
</>
);
});
100 changes: 71 additions & 29 deletions web/components/issues/issue-layouts/empty-states/module.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,36 +4,78 @@ import { EmptyState } from "components/common";
import { Button } from "@plane/ui";
// assets
import emptyIssue from "public/empty-state/issue.svg";
import { ExistingIssuesListModal } from "components/core";
import { observer } from "mobx-react-lite";
import { useMobxStore } from "lib/mobx/store-provider";
import { ISearchIssueResponse } from "types";
import useToast from "hooks/use-toast";
import { useState } from "react";

type Props = {
openIssuesListModal: () => void;
workspaceSlug: string | undefined;
projectId: string | undefined;
moduleId: string | undefined;
};

export const ModuleEmptyState: React.FC<Props> = ({ openIssuesListModal }) => (
<div className="h-full w-full grid place-items-center">
<EmptyState
title="Module issues will appear here"
description="Issues help you track individual pieces of work. With Issues, keep track of what's going on, who is working on it, and what's done."
image={emptyIssue}
primaryButton={{
text: "New issue",
icon: <PlusIcon className="h-3 w-3" strokeWidth={2} />,
onClick: () => {
const e = new KeyboardEvent("keydown", {
key: "c",
});
document.dispatchEvent(e);
},
}}
secondaryButton={
<Button
variant="neutral-primary"
prependIcon={<PlusIcon className="h-3 w-3" strokeWidth={2} />}
onClick={openIssuesListModal}
>
Add an existing issue
</Button>
}
/>
</div>
);
export const ModuleEmptyState: React.FC<Props> = observer((props) => {
const { workspaceSlug, projectId, moduleId } = props;
// states
const [moduleIssuesListModal, setModuleIssuesListModal] = useState(false);

const { moduleIssue: moduleIssueStore } = useMobxStore();

const { setToastAlert } = useToast();

const handleAddIssuesToModule = async (data: ISearchIssueResponse[]) => {
if (!workspaceSlug || !projectId || !moduleId) return;

const issueIds = data.map((i) => i.id);

await moduleIssueStore
.addIssueToModule(workspaceSlug.toString(), projectId.toString(), moduleId.toString(), issueIds)
.catch(() =>
setToastAlert({
type: "error",
title: "Error!",
message: "Selected issues could not be added to the module. Please try again.",
})
);
};

return (
<>
<ExistingIssuesListModal
isOpen={moduleIssuesListModal}
handleClose={() => setModuleIssuesListModal(false)}
searchParams={{ module: true }}
handleOnSubmit={handleAddIssuesToModule}
/>
<div className="h-full w-full grid place-items-center">
<EmptyState
title="Module issues will appear here"
description="Issues help you track individual pieces of work. With Issues, keep track of what's going on, who is working on it, and what's done."
image={emptyIssue}
primaryButton={{
text: "New issue",
icon: <PlusIcon className="h-3 w-3" strokeWidth={2} />,
onClick: () => {
const e = new KeyboardEvent("keydown", {
key: "c",
});
document.dispatchEvent(e);
},
}}
secondaryButton={
<Button
variant="neutral-primary"
prependIcon={<PlusIcon className="h-3 w-3" strokeWidth={2} />}
onClick={() => setModuleIssuesListModal(true)}
>
Add an existing issue
</Button>
}
/>
</div>
</>
);
});
12 changes: 6 additions & 6 deletions web/components/issues/issue-layouts/roots/cycle-layout-root.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -20,11 +20,7 @@ import { Spinner } from "@plane/ui";
// helpers
import { getDateRangeStatus } from "helpers/date-time.helper";

type Props = {
openIssuesListModal: () => void;
};

export const CycleLayoutRoot: React.FC<Props> = observer(({ openIssuesListModal }) => {
export const CycleLayoutRoot: React.FC = observer(() => {
const [transferIssuesModal, setTransferIssuesModal] = useState(false);

const router = useRouter();
Expand Down Expand Up @@ -73,7 +69,11 @@ export const CycleLayoutRoot: React.FC<Props> = observer(({ openIssuesListModal
{cycleStatus === "completed" && <TransferIssues handleClick={() => setTransferIssuesModal(true)} />}
<CycleAppliedFiltersRoot />
{(activeLayout === "list" || activeLayout === "spreadsheet") && issueCount === 0 ? (
<CycleEmptyState openIssuesListModal={openIssuesListModal} />
<CycleEmptyState
workspaceSlug={workspaceSlug?.toString()}
projectId={projectId?.toString()}
cycleId={cycleId?.toString()}
/>
) : (
<div className="w-full h-full overflow-auto">
{activeLayout === "list" ? (
Expand Down
12 changes: 6 additions & 6 deletions web/components/issues/issue-layouts/roots/module-layout-root.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -18,11 +18,7 @@ import {
// ui
import { Spinner } from "@plane/ui";

type Props = {
openIssuesListModal: () => void;
};

export const ModuleLayoutRoot: React.FC<Props> = observer(({ openIssuesListModal }) => {
export const ModuleLayoutRoot: React.FC = observer(() => {
const router = useRouter();
const { workspaceSlug, projectId, moduleId } = router.query as {
workspaceSlug: string;
Expand Down Expand Up @@ -66,7 +62,11 @@ export const ModuleLayoutRoot: React.FC<Props> = observer(({ openIssuesListModal
<div className="relative w-full h-full flex flex-col overflow-hidden">
<ModuleAppliedFiltersRoot />
{(activeLayout === "list" || activeLayout === "spreadsheet") && issueCount === 0 ? (
<ModuleEmptyState openIssuesListModal={openIssuesListModal} />
<ModuleEmptyState
workspaceSlug={workspaceSlug?.toString()}
projectId={projectId?.toString()}
moduleId={moduleId?.toString()}
/>
) : (
<div className="h-full w-full overflow-auto">
{activeLayout === "list" ? (
Expand Down
4 changes: 2 additions & 2 deletions web/components/issues/issue-peek-overview/properties.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -74,13 +74,13 @@ export const PeekOverviewProperties: FC<IPeekOverviewProperties> = observer((pro
};
const addIssueToCycle = async (cycleId: string) => {
if (!workspaceSlug || !issue || !cycleId) return;
cycleIssueStore.addIssueToCycle(workspaceSlug.toString(), issue.project_detail.id, cycleId, issue.id);
cycleIssueStore.addIssueToCycle(workspaceSlug.toString(), issue.project_detail.id, cycleId, [issue.id]);
};

const addIssueToModule = async (moduleId: string) => {
if (!workspaceSlug || !issue || !moduleId) return;

moduleIssueStore.addIssueToModule(workspaceSlug.toString(), issue.project_detail.id, moduleId, issue.id);
moduleIssueStore.addIssueToModule(workspaceSlug.toString(), issue.project_detail.id, moduleId, [issue.id]);
};
const handleLabels = (formData: Partial<IIssue>) => {
issueUpdate({ ...issue, ...formData });
Expand Down
4 changes: 2 additions & 2 deletions web/components/issues/modal.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -171,13 +171,13 @@ export const CreateUpdateIssueModal: React.FC<IssuesModalProps> = observer((prop
const addIssueToCycle = async (issueId: string, cycleId: string) => {
if (!workspaceSlug || !activeProject) return;

cycleIssueStore.addIssueToCycle(workspaceSlug.toString(), activeProject, cycleId, issueId);
cycleIssueStore.addIssueToCycle(workspaceSlug.toString(), activeProject, cycleId, [issueId]);
};

const addIssueToModule = async (issueId: string, moduleId: string) => {
if (!workspaceSlug || !activeProject) return;

moduleIssueStore.addIssueToModule(workspaceSlug.toString(), activeProject, moduleId, issueId);
moduleIssueStore.addIssueToModule(workspaceSlug.toString(), activeProject, moduleId, [issueId]);
};

const createIssue = async (payload: Partial<IIssue>) => {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,43 +1,29 @@
import { useState, ReactElement } from "react";
import { ReactElement } from "react";
import { useRouter } from "next/router";
import useSWR from "swr";
// mobx store
import { useMobxStore } from "lib/mobx/store-provider";
// services
import { IssueService } from "services/issue";
// hooks
import useLocalStorage from "hooks/use-local-storage";
import useUser from "hooks/use-user";
import useToast from "hooks/use-toast";
// layouts
import { AppLayout } from "layouts/app-layout";
// components
import { CycleIssuesHeader } from "components/headers";
import { ExistingIssuesListModal } from "components/core";
import { CycleDetailsSidebar } from "components/cycles";
import { CycleLayoutRoot } from "components/issues/issue-layouts";
// ui
import { EmptyState } from "components/common";
// assets
import emptyCycle from "public/empty-state/cycle.svg";
// types
import { ISearchIssueResponse } from "types";
import { NextPageWithLayout } from "types/app";

const issueService = new IssueService();

const CycleDetailPage: NextPageWithLayout = () => {
const [cycleIssuesListModal, setCycleIssuesListModal] = useState(false);

const router = useRouter();
const { workspaceSlug, projectId, cycleId } = router.query;

const { cycle: cycleStore } = useMobxStore();

const { user } = useUser();

const { setToastAlert } = useToast();

const { setValue, storedValue } = useLocalStorage("cycle_sidebar_collapsed", "false");
const isSidebarCollapsed = storedValue ? (storedValue === "true" ? true : false) : false;

Expand All @@ -52,38 +38,8 @@ const CycleDetailPage: NextPageWithLayout = () => {
setValue(`${!isSidebarCollapsed}`);
};

// TODO: add this function to bulk add issues to cycle
const handleAddIssuesToCycle = async (data: ISearchIssueResponse[]) => {
if (!workspaceSlug || !projectId) return;

const payload = {
issues: data.map((i) => i.id),
};

await issueService
.addIssueToCycle(workspaceSlug as string, projectId as string, cycleId as string, payload, user)
.catch(() => {
setToastAlert({
type: "error",
title: "Error!",
message: "Selected issues could not be added to the cycle. Please try again.",
});
});
};

const openIssuesListModal = () => {
setCycleIssuesListModal(true);
};

return (
<>
{/* TODO: Update logic to bulk add issues to a cycle */}
<ExistingIssuesListModal
isOpen={cycleIssuesListModal}
handleClose={() => setCycleIssuesListModal(false)}
searchParams={{ cycle: true }}
handleOnSubmit={handleAddIssuesToCycle}
/>
{error ? (
<EmptyState
image={emptyCycle}
Expand All @@ -98,7 +54,7 @@ const CycleDetailPage: NextPageWithLayout = () => {
<>
<div className="flex h-full w-full">
<div className="h-full w-full overflow-hidden">
<CycleLayoutRoot openIssuesListModal={openIssuesListModal} />
<CycleLayoutRoot />
</div>
{cycleId && !isSidebarCollapsed && (
<div
Expand Down
Loading