chore: implemented CRUD operations in all the layouts (#2505)

* chore: basic crud operations added to the list view

* refactor: cycle details page

* refactor: module details page

* chore: added quick actions to kanban issue block

* chore: implement quick actions in calendar layout

* fix: custom menu component

* chore: separate quick action dropdowns implemented

* style: loader for calendar

* fix: build errors
This commit is contained in:
Aaryan Khandelwal 2023-10-20 17:07:46 +05:30 committed by GitHub
parent 9bddd2eb67
commit d78b4dccf3
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
76 changed files with 2336 additions and 1153 deletions

View file

@ -9,14 +9,17 @@ import { Spinner } from "@plane/ui";
// types
import { ICalendarWeek } from "./types";
import { IIssueGroupedStructure } from "store/issue";
import { IIssue } from "types";
type Props = {
issues: IIssueGroupedStructure | null;
layout: "month" | "week" | undefined;
showWeekends: boolean;
quickActions: (issue: IIssue) => React.ReactNode;
};
export const CalendarChart: React.FC<Props> = observer((props) => {
const { issues, layout } = props;
const { issues, layout, showWeekends, quickActions } = props;
const { calendar: calendarStore } = useMobxStore();
@ -35,17 +38,17 @@ export const CalendarChart: React.FC<Props> = observer((props) => {
<>
<div className="h-full w-full flex flex-col overflow-hidden">
<CalendarHeader />
<CalendarWeekHeader />
<CalendarWeekHeader isLoading={!issues} showWeekends={showWeekends} />
<div className="h-full w-full overflow-y-auto">
{layout === "month" ? (
<div className="h-full w-full grid grid-cols-1 divide-y-[0.5px] divide-custom-border-200">
{allWeeksOfActiveMonth &&
Object.values(allWeeksOfActiveMonth).map((week: ICalendarWeek, weekIndex) => (
<CalendarWeekDays key={weekIndex} week={week} issues={issues} />
<CalendarWeekDays key={weekIndex} week={week} issues={issues} quickActions={quickActions} />
))}
</div>
) : (
<CalendarWeekDays week={calendarStore.allDaysOfActiveWeek} issues={issues} />
<CalendarWeekDays week={calendarStore.allDaysOfActiveWeek} issues={issues} quickActions={quickActions} />
)}
</div>
</div>

View file

@ -11,11 +11,16 @@ import { renderDateFormat } from "helpers/date-time.helper";
import { IIssueGroupedStructure } from "store/issue";
// constants
import { MONTHS_LIST } from "constants/calendar";
import { IIssue } from "types";
type Props = { date: ICalendarDate; issues: IIssueGroupedStructure | null };
type Props = {
date: ICalendarDate;
issues: IIssueGroupedStructure | null;
quickActions: (issue: IIssue) => React.ReactNode;
};
export const CalendarDayTile: React.FC<Props> = observer((props) => {
const { date, issues } = props;
const { date, issues, quickActions } = props;
const { issueFilter: issueFilterStore } = useMobxStore();
@ -48,7 +53,7 @@ export const CalendarDayTile: React.FC<Props> = observer((props) => {
{date.date.getDate() === 1 && MONTHS_LIST[date.date.getMonth() + 1].shortTitle + " "}
{date.date.getDate()}
</div>
<CalendarIssueBlocks issues={issuesList} />
<CalendarIssueBlocks issues={issuesList} quickActions={quickActions} />
{provided.placeholder}
</>
</div>

View file

@ -1,7 +1,7 @@
import React from "react";
import React, { useState } from "react";
import { Popover, Transition } from "@headlessui/react";
import { observer } from "mobx-react-lite";
import { usePopper } from "react-popper";
// mobx store
import { useMobxStore } from "lib/mobx/store-provider";
// icons
@ -14,6 +14,21 @@ export const CalendarMonthsDropdown: React.FC = observer(() => {
const calendarLayout = issueFilterStore.userDisplayFilters.calendar?.layout ?? "month";
const [referenceElement, setReferenceElement] = useState<HTMLButtonElement | null>(null);
const [popperElement, setPopperElement] = useState<HTMLDivElement | null>(null);
const { styles, attributes } = usePopper(referenceElement, popperElement, {
placement: "auto",
modifiers: [
{
name: "preventOverflow",
options: {
padding: 12,
},
},
],
});
const { activeMonthDate } = calendarStore.calendarFilters;
const getWeekLayoutHeader = (): string => {
@ -47,10 +62,17 @@ export const CalendarMonthsDropdown: React.FC = observer(() => {
return (
<Popover className="relative">
<Popover.Button className="outline-none text-xl font-semibold" disabled={calendarLayout === "week"}>
{calendarLayout === "month"
? `${MONTHS_LIST[activeMonthDate.getMonth() + 1].title} ${activeMonthDate.getFullYear()}`
: getWeekLayoutHeader()}
<Popover.Button as={React.Fragment}>
<button
type="button"
ref={setReferenceElement}
className="outline-none text-xl font-semibold"
disabled={calendarLayout === "week"}
>
{calendarLayout === "month"
? `${MONTHS_LIST[activeMonthDate.getMonth() + 1].title} ${activeMonthDate.getFullYear()}`
: getWeekLayoutHeader()}
</button>
</Popover.Button>
<Transition
as={React.Fragment}
@ -61,8 +83,13 @@ export const CalendarMonthsDropdown: React.FC = observer(() => {
leaveFrom="opacity-100 translate-y-0"
leaveTo="opacity-0 translate-y-1"
>
<Popover.Panel>
<div className="absolute left-0 z-10 mt-1 bg-custom-background-100 border border-custom-border-200 shadow-custom-shadow-rg rounded w-56 p-3 divide-y divide-custom-border-200">
<Popover.Panel className="fixed z-50">
<div
ref={setPopperElement}
style={styles.popper}
{...attributes.popper}
className="bg-custom-background-100 border border-custom-border-200 shadow-custom-shadow-rg rounded w-56 p-3 divide-y divide-custom-border-200"
>
<div className="flex items-center justify-between gap-2 pb-3">
<button
type="button"

View file

@ -1,8 +1,8 @@
import React from "react";
import React, { useState } from "react";
import { useRouter } from "next/router";
import { Popover, Transition } from "@headlessui/react";
import { observer } from "mobx-react-lite";
import { usePopper } from "react-popper";
// mobx store
import { useMobxStore } from "lib/mobx/store-provider";
// ui
@ -20,6 +20,21 @@ export const CalendarOptionsDropdown: React.FC = observer(() => {
const { issueFilter: issueFilterStore, calendar: calendarStore } = useMobxStore();
const [referenceElement, setReferenceElement] = useState<HTMLButtonElement | null>(null);
const [popperElement, setPopperElement] = useState<HTMLDivElement | null>(null);
const { styles, attributes } = usePopper(referenceElement, popperElement, {
placement: "auto",
modifiers: [
{
name: "preventOverflow",
options: {
padding: 12,
},
},
],
});
const calendarLayout = issueFilterStore.userDisplayFilters.calendar?.layout ?? "month";
const showWeekends = issueFilterStore.userDisplayFilters.calendar?.show_weekends ?? false;
@ -57,12 +72,12 @@ export const CalendarOptionsDropdown: React.FC = observer(() => {
return (
<Popover className="relative">
{({ open }) => {
if (open) {
}
return (
<>
<Popover.Button
{({ open }) => (
<>
<Popover.Button as={React.Fragment}>
<button
type="button"
ref={setReferenceElement}
className={`outline-none bg-custom-background-80 text-xs rounded flex items-center gap-1.5 px-2.5 py-1 hover:bg-custom-background-80 ${
open ? "text-custom-text-100" : "text-custom-text-200"
}`}
@ -73,45 +88,50 @@ export const CalendarOptionsDropdown: React.FC = observer(() => {
>
<ChevronUp width={12} strokeWidth={2} />
</div>
</Popover.Button>
<Transition
as={React.Fragment}
enter="transition ease-out duration-200"
enterFrom="opacity-0 translate-y-1"
enterTo="opacity-100 translate-y-0"
leave="transition ease-in duration-150"
leaveFrom="opacity-100 translate-y-0"
leaveTo="opacity-0 translate-y-1"
>
<Popover.Panel>
<div className="absolute right-0 z-10 mt-1 bg-custom-background-100 border border-custom-border-200 shadow-custom-shadow-rg rounded min-w-[12rem] p-1 overflow-hidden">
<div>
{Object.entries(CALENDAR_LAYOUTS).map(([layout, layoutDetails]) => (
<button
key={layout}
type="button"
className="text-xs hover:bg-custom-background-80 w-full text-left px-1 py-1.5 rounded flex items-center justify-between gap-2"
onClick={() => handleLayoutChange(layoutDetails.key)}
>
{layoutDetails.title}
{calendarLayout === layout && <Check size={12} strokeWidth={2} />}
</button>
))}
</button>
</Popover.Button>
<Transition
as={React.Fragment}
enter="transition ease-out duration-200"
enterFrom="opacity-0 translate-y-1"
enterTo="opacity-100 translate-y-0"
leave="transition ease-in duration-150"
leaveFrom="opacity-100 translate-y-0"
leaveTo="opacity-0 translate-y-1"
>
<Popover.Panel className="fixed z-50">
<div
ref={setPopperElement}
style={styles.popper}
{...attributes.popper}
className="absolute right-0 z-10 mt-1 bg-custom-background-100 border border-custom-border-200 shadow-custom-shadow-sm rounded min-w-[12rem] p-1 overflow-hidden"
>
<div>
{Object.entries(CALENDAR_LAYOUTS).map(([layout, layoutDetails]) => (
<button
key={layout}
type="button"
className="text-xs hover:bg-custom-background-80 w-full text-left px-1 py-1.5 rounded flex items-center justify-between gap-2"
onClick={handleToggleWeekends}
onClick={() => handleLayoutChange(layoutDetails.key)}
>
Show weekends
<ToggleSwitch value={showWeekends} onChange={() => {}} />
{layoutDetails.title}
{calendarLayout === layout && <Check size={12} strokeWidth={2} />}
</button>
</div>
))}
<button
type="button"
className="text-xs hover:bg-custom-background-80 w-full text-left px-1 py-1.5 rounded flex items-center justify-between gap-2"
onClick={handleToggleWeekends}
>
Show weekends
<ToggleSwitch value={showWeekends} onChange={() => {}} />
</button>
</div>
</Popover.Panel>
</Transition>
</>
);
}}
</div>
</Popover.Panel>
</Transition>
</>
)}
</Popover>
);
});

View file

@ -1,15 +1,17 @@
import Link from "next/link";
import { useRouter } from "next/router";
import { Draggable } from "@hello-pangea/dnd";
import { observer } from "mobx-react-lite";
import { Draggable } from "@hello-pangea/dnd";
// types
import { IIssue } from "types";
type Props = { issues: IIssue[] | null };
type Props = {
issues: IIssue[] | null;
quickActions: (issue: IIssue) => React.ReactNode;
};
export const CalendarIssueBlocks: React.FC<Props> = observer((props) => {
const { issues } = props;
const { issues, quickActions } = props;
const router = useRouter();
const { workspaceSlug } = router.query;
@ -21,7 +23,7 @@ export const CalendarIssueBlocks: React.FC<Props> = observer((props) => {
{(provided, snapshot) => (
<Link href={`/${workspaceSlug?.toString()}/projects/${issue.project}/issues/${issue.id}`}>
<a
className={`h-8 w-full shadow-custom-shadow-2xs rounded py-1.5 px-1 flex items-center gap-1.5 border-[0.5px] border-custom-border-100 ${
className={`group/calendar-block h-8 w-full shadow-custom-shadow-2xs rounded py-1.5 px-1 flex items-center gap-1.5 border-[0.5px] border-custom-border-100 ${
snapshot.isDragging
? "shadow-custom-shadow-rg bg-custom-background-90"
: "bg-custom-background-100 hover:bg-custom-background-90"
@ -40,6 +42,12 @@ export const CalendarIssueBlocks: React.FC<Props> = observer((props) => {
{issue.project_detail.identifier}-{issue.sequence_id}
</div>
<h6 className="text-xs flex-grow truncate">{issue.name}</h6>
<div className="hidden group-hover/calendar-block:block">{quickActions(issue)}</div>
{/* <IssueQuickActions
issue={issue}
handleDelete={async () => handleIssues(issue.target_date ?? "", issue, "delete")}
handleUpdate={async (data) => handleIssues(issue.target_date ?? "", data, "update")}
/> */}
</a>
</Link>
)}

View file

@ -1,15 +1,20 @@
import { useCallback } from "react";
import { useRouter } from "next/router";
import { observer } from "mobx-react-lite";
import { DragDropContext, DropResult } from "@hello-pangea/dnd";
// mobx store
import { useMobxStore } from "lib/mobx/store-provider";
// components
import { CalendarChart } from "components/issues";
import { CalendarChart, CycleIssueQuickActions } from "components/issues";
// types
import { IIssueGroupedStructure } from "store/issue";
import { IIssue } from "types";
export const CycleCalendarLayout: React.FC = observer(() => {
const { cycleIssue: cycleIssueStore, issueFilter: issueFilterStore } = useMobxStore();
const { cycleIssue: cycleIssueStore, issueFilter: issueFilterStore, issueDetail: issueDetailStore } = useMobxStore();
const router = useRouter();
const { workspaceSlug, cycleId } = router.query;
// TODO: add drag and drop functionality
const onDragEnd = (result: DropResult) => {
@ -26,12 +31,43 @@ export const CycleCalendarLayout: React.FC = observer(() => {
const issues = cycleIssueStore.getIssues;
const handleIssues = useCallback(
(date: string, issue: IIssue, action: "update" | "delete" | "remove") => {
if (!workspaceSlug || !cycleId) return;
if (action === "update") {
cycleIssueStore.updateIssueStructure(date, null, issue);
issueDetailStore.updateIssue(workspaceSlug.toString(), issue.project, issue.id, issue);
}
if (action === "delete") cycleIssueStore.deleteIssue(date, null, issue);
if (action === "remove" && issue.bridge_id) {
cycleIssueStore.deleteIssue(date, null, issue);
cycleIssueStore.removeIssueFromCycle(
workspaceSlug.toString(),
issue.project,
cycleId.toString(),
issue.bridge_id
);
}
},
[cycleIssueStore, issueDetailStore, cycleId, workspaceSlug]
);
return (
<div className="h-full w-full pt-4 bg-custom-background-100 overflow-hidden">
<DragDropContext onDragEnd={onDragEnd}>
<CalendarChart
issues={issues as IIssueGroupedStructure | null}
layout={issueFilterStore.userDisplayFilters.calendar?.layout}
showWeekends={issueFilterStore.userDisplayFilters.calendar?.show_weekends ?? false}
quickActions={(issue) => (
<CycleIssueQuickActions
issue={issue}
handleDelete={async () => handleIssues(issue.target_date ?? "", issue, "delete")}
handleUpdate={async (data) => handleIssues(issue.target_date ?? "", data, "update")}
handleRemoveFromCycle={async () => handleIssues(issue.target_date ?? "", issue, "remove")}
/>
)}
/>
</DragDropContext>
</div>

View file

@ -1,15 +1,24 @@
import { useCallback } from "react";
import { useRouter } from "next/router";
import { observer } from "mobx-react-lite";
import { DragDropContext, DropResult } from "@hello-pangea/dnd";
// mobx store
import { useMobxStore } from "lib/mobx/store-provider";
// components
import { CalendarChart } from "components/issues";
import { CalendarChart, ModuleIssueQuickActions } from "components/issues";
// types
import { IIssueGroupedStructure } from "store/issue";
import { IIssue } from "types";
export const ModuleCalendarLayout: React.FC = observer(() => {
const { moduleIssue: moduleIssueStore, issueFilter: issueFilterStore } = useMobxStore();
const {
moduleIssue: moduleIssueStore,
issueFilter: issueFilterStore,
issueDetail: issueDetailStore,
} = useMobxStore();
const router = useRouter();
const { workspaceSlug, moduleId } = router.query;
// TODO: add drag and drop functionality
const onDragEnd = (result: DropResult) => {
@ -26,12 +35,45 @@ export const ModuleCalendarLayout: React.FC = observer(() => {
const issues = moduleIssueStore.getIssues;
const handleIssues = useCallback(
(date: string, issue: IIssue, action: "update" | "delete" | "remove") => {
if (!workspaceSlug || !moduleId) return;
if (action === "update") {
moduleIssueStore.updateIssueStructure(date, null, issue);
issueDetailStore.updateIssue(workspaceSlug.toString(), issue.project, issue.id, issue);
} else {
moduleIssueStore.deleteIssue(date, null, issue);
issueDetailStore.deleteIssue(workspaceSlug.toString(), issue.project, issue.id);
}
if (action === "remove" && issue.bridge_id) {
moduleIssueStore.deleteIssue(date, null, issue);
moduleIssueStore.removeIssueFromModule(
workspaceSlug.toString(),
issue.project,
moduleId.toString(),
issue.bridge_id
);
}
},
[moduleIssueStore, issueDetailStore, moduleId, workspaceSlug]
);
return (
<div className="h-full w-full pt-4 bg-custom-background-100 overflow-hidden">
<DragDropContext onDragEnd={onDragEnd}>
<CalendarChart
issues={issues as IIssueGroupedStructure | null}
layout={issueFilterStore.userDisplayFilters.calendar?.layout}
showWeekends={issueFilterStore.userDisplayFilters.calendar?.show_weekends ?? false}
quickActions={(issue) => (
<ModuleIssueQuickActions
issue={issue}
handleDelete={async () => handleIssues(issue.target_date ?? "", issue, "delete")}
handleUpdate={async (data) => handleIssues(issue.target_date ?? "", data, "update")}
handleRemoveFromModule={async () => handleIssues(issue.target_date ?? "", issue, "remove")}
/>
)}
/>
</DragDropContext>
</div>

View file

@ -1,15 +1,20 @@
import { useCallback } from "react";
import { useRouter } from "next/router";
import { observer } from "mobx-react-lite";
import { DragDropContext, DropResult } from "@hello-pangea/dnd";
// mobx store
import { useMobxStore } from "lib/mobx/store-provider";
// components
import { CalendarChart } from "components/issues";
import { CalendarChart, ProjectIssueQuickActions } from "components/issues";
// types
import { IIssueGroupedStructure } from "store/issue";
import { IIssue } from "types";
export const CalendarLayout: React.FC = observer(() => {
const { issue: issueStore, issueFilter: issueFilterStore } = useMobxStore();
const { issue: issueStore, issueFilter: issueFilterStore, issueDetail: issueDetailStore } = useMobxStore();
const router = useRouter();
const { workspaceSlug } = router.query;
// TODO: add drag and drop functionality
const onDragEnd = (result: DropResult) => {
@ -26,12 +31,35 @@ export const CalendarLayout: React.FC = observer(() => {
const issues = issueStore.getIssues;
const handleIssues = useCallback(
(date: string, issue: IIssue, action: "update" | "delete") => {
if (!workspaceSlug) return;
if (action === "update") {
issueStore.updateIssueStructure(date, null, issue);
issueDetailStore.updateIssue(workspaceSlug.toString(), issue.project, issue.id, issue);
} else {
issueStore.deleteIssue(date, null, issue);
issueDetailStore.deleteIssue(workspaceSlug.toString(), issue.project, issue.id);
}
},
[issueStore, issueDetailStore, workspaceSlug]
);
return (
<div className="h-full w-full pt-4 bg-custom-background-100 overflow-hidden">
<DragDropContext onDragEnd={onDragEnd}>
<CalendarChart
issues={issues as IIssueGroupedStructure | null}
layout={issueFilterStore.userDisplayFilters.calendar?.layout}
showWeekends={issueFilterStore.userDisplayFilters.calendar?.show_weekends ?? false}
quickActions={(issue) => (
<ProjectIssueQuickActions
issue={issue}
handleDelete={async () => handleIssues(issue.target_date ?? "", issue, "delete")}
handleUpdate={async (data) => handleIssues(issue.target_date ?? "", data, "update")}
/>
)}
/>
</DragDropContext>
</div>

View file

@ -1,15 +1,24 @@
import { useCallback } from "react";
import { useRouter } from "next/router";
import { observer } from "mobx-react-lite";
import { DragDropContext, DropResult } from "@hello-pangea/dnd";
// mobx store
import { useMobxStore } from "lib/mobx/store-provider";
// components
import { CalendarChart } from "components/issues";
import { CalendarChart, ProjectIssueQuickActions } from "components/issues";
// types
import { IIssueGroupedStructure } from "store/issue";
import { IIssue } from "types";
export const ProjectViewCalendarLayout: React.FC = observer(() => {
const { projectViewIssues: projectViewIssuesStore, issueFilter: issueFilterStore } = useMobxStore();
const {
projectViewIssues: projectViewIssuesStore,
issueFilter: issueFilterStore,
issueDetail: issueDetailStore,
} = useMobxStore();
const router = useRouter();
const { workspaceSlug } = router.query;
// TODO: add drag and drop functionality
const onDragEnd = (result: DropResult) => {
@ -26,12 +35,35 @@ export const ProjectViewCalendarLayout: React.FC = observer(() => {
const issues = projectViewIssuesStore.getIssues;
const handleIssues = useCallback(
(date: string, issue: IIssue, action: "update" | "delete") => {
if (!workspaceSlug) return;
if (action === "update") {
projectViewIssuesStore.updateIssueStructure(date, null, issue);
issueDetailStore.updateIssue(workspaceSlug.toString(), issue.project, issue.id, issue);
} else {
projectViewIssuesStore.deleteIssue(date, null, issue);
issueDetailStore.deleteIssue(workspaceSlug.toString(), issue.project, issue.id);
}
},
[projectViewIssuesStore, issueDetailStore, workspaceSlug]
);
return (
<div className="h-full w-full pt-4 bg-custom-background-100 overflow-hidden">
<DragDropContext onDragEnd={onDragEnd}>
<CalendarChart
issues={issues as IIssueGroupedStructure | null}
layout={issueFilterStore.userDisplayFilters.calendar?.layout}
showWeekends={issueFilterStore.userDisplayFilters.calendar?.show_weekends ?? false}
quickActions={(issue) => (
<ProjectIssueQuickActions
issue={issue}
handleDelete={async () => handleIssues(issue.target_date ?? "", issue, "delete")}
handleUpdate={async (data) => handleIssues(issue.target_date ?? "", data, "update")}
/>
)}
/>
</DragDropContext>
</div>

View file

@ -9,14 +9,16 @@ import { renderDateFormat } from "helpers/date-time.helper";
// types
import { ICalendarDate, ICalendarWeek } from "./types";
import { IIssueGroupedStructure } from "store/issue";
import { IIssue } from "types";
type Props = {
issues: IIssueGroupedStructure | null;
week: ICalendarWeek | undefined;
quickActions: (issue: IIssue) => React.ReactNode;
};
export const CalendarWeekDays: React.FC<Props> = observer((props) => {
const { issues, week } = props;
const { issues, week, quickActions } = props;
const { issueFilter: issueFilterStore } = useMobxStore();
@ -34,7 +36,9 @@ export const CalendarWeekDays: React.FC<Props> = observer((props) => {
{Object.values(week).map((date: ICalendarDate) => {
if (!showWeekends && (date.date.getDay() === 0 || date.date.getDay() === 6)) return null;
return <CalendarDayTile key={renderDateFormat(date.date)} date={date} issues={issues} />;
return (
<CalendarDayTile key={renderDateFormat(date.date)} date={date} issues={issues} quickActions={quickActions} />
);
})}
</div>
);

View file

@ -1,21 +1,25 @@
import { observer } from "mobx-react-lite";
// mobx store
import { useMobxStore } from "lib/mobx/store-provider";
// constants
import { DAYS_LIST } from "constants/calendar";
export const CalendarWeekHeader: React.FC = observer(() => {
const { issueFilter: issueFilterStore } = useMobxStore();
type Props = {
isLoading: boolean;
showWeekends: boolean;
};
const showWeekends = issueFilterStore.userDisplayFilters.calendar?.show_weekends ?? false;
export const CalendarWeekHeader: React.FC<Props> = observer((props) => {
const { isLoading, showWeekends } = props;
return (
<div
className={`grid text-sm font-medium divide-x-[0.5px] divide-custom-border-200 ${
className={`relative grid text-sm font-medium divide-x-[0.5px] divide-custom-border-200 ${
showWeekends ? "grid-cols-7" : "grid-cols-5"
}`}
>
{isLoading && (
<div className="absolute h-[1.5px] w-3/4 bg-custom-primary-100 animate-[bar-loader_2s_linear_infinite]" />
)}
{Object.values(DAYS_LIST).map((day) => {
if (!showWeekends && (day.shortTitle === "Sat" || day.shortTitle === "Sun")) return null;