Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[WEB-404] chore: calendar layout add existing issue workflow improvement #3877

4 changes: 4 additions & 0 deletions apiserver/plane/app/views/search.py
Original file line number Diff line number Diff line change
Expand Up @@ -235,6 +235,7 @@ def get(self, request, slug, project_id):
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)

Expand Down Expand Up @@ -273,6 +274,9 @@ def get(self, request, slug, project_id):
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",
Expand Down
1 change: 1 addition & 0 deletions packages/types/src/projects.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -130,6 +130,7 @@ export type TProjectIssuesSearchParams = {
sub_issue?: boolean;
issue_id?: string;
workspace_search: boolean;
target_date?: string;
};

export interface ISearchIssueResponse {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand Down Expand Up @@ -128,6 +137,7 @@ export const BaseCalendarRoot = observer((props: IBaseCalendarRoot) => {
readOnly={!isEditingAllowed || isCompletedCycle}
/>
)}
addIssuesToView={addIssuesToView}
quickAddCallback={issueStore.quickAddIssue}
viewId={viewId}
readOnly={!isEditingAllowed || isCompletedCycle}
Expand Down
4 changes: 4 additions & 0 deletions web/components/issues/issue-layouts/calendar/calendar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ type Props = {
data: TIssue,
viewId?: string
) => Promise<TIssue | undefined>;
addIssuesToView?: (issueIds: string[]) => Promise<any>;
viewId?: string;
readOnly?: boolean;
};
Expand All @@ -43,6 +44,7 @@ export const CalendarChart: React.FC<Props> = observer((props) => {
showWeekends,
quickActions,
quickAddCallback,
addIssuesToView,
viewId,
readOnly = false,
} = props;
Expand Down Expand Up @@ -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}
/>
Expand All @@ -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}
/>
Expand Down
6 changes: 5 additions & 1 deletion web/components/issues/issue-layouts/calendar/day-tile.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand All @@ -27,6 +28,7 @@ type Props = {
data: TIssue,
viewId?: string
) => Promise<TIssue | undefined>;
addIssuesToView?: (issueIds: string[]) => Promise<any>;
viewId?: string;
readOnly?: boolean;
};
Expand All @@ -41,6 +43,7 @@ export const CalendarDayTile: React.FC<Props> = observer((props) => {
enableQuickIssueCreate,
disableIssueCreation,
quickAddCallback,
addIssuesToView,
viewId,
readOnly = false,
} = props;
Expand Down Expand Up @@ -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)}
/>
Expand Down
108 changes: 89 additions & 19 deletions web/components/issues/issue-layouts/calendar/quick-add-issue-form.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -28,6 +32,7 @@ type Props = {
data: TIssue,
viewId?: string
) => Promise<TIssue | undefined>;
addIssuesToView?: (issueIds: string[]) => Promise<any>;
viewId?: string;
onOpen?: () => void;
};
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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}
Expand All @@ -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>
)}
</>
Expand Down
18 changes: 14 additions & 4 deletions web/components/issues/issue-layouts/calendar/roots/cycle-root.tsx
Original file line number Diff line number Diff line change
@@ -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);
Expand Down Expand Up @@ -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}
Expand Down
22 changes: 16 additions & 6 deletions web/components/issues/issue-layouts/calendar/roots/module-root.tsx
Original file line number Diff line number Diff line change
@@ -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;
Expand Down Expand Up @@ -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}
/>
);
Expand Down
Loading
Loading