mirror of
https://github.com/makeplane/plane
synced 2024-06-14 14:31:34 +00:00
dev: calendar view layout revamp (#2293)
* dev: calendar view init * chore: new render logic * chore: implement calendar view * chore: calendar view * refactor: calendar payload * chore: remove active month logic from backend * chore: setup new store for calendar * refactor: issues fetching structure * chore: months dropdown * chore: modify request query params for calendar layout * refactor: remove console logs and add comments
This commit is contained in:
parent
b2d17e6ec9
commit
3bf590b67e
@ -20,18 +20,15 @@ export const AnalyticsYearWiseIssues: React.FC<Props> = ({ defaultAnalytics }) =
|
||||
{
|
||||
id: "issues_closed",
|
||||
color: "rgb(var(--color-primary-100))",
|
||||
data: MONTHS_LIST.map((month) => ({
|
||||
x: month.label.substring(0, 3),
|
||||
data: Object.entries(MONTHS_LIST).map(([index, month]) => ({
|
||||
x: month.shortTitle,
|
||||
y:
|
||||
defaultAnalytics.issue_completed_month_wise.find(
|
||||
(data) => data.month === month.value
|
||||
)?.count || 0,
|
||||
defaultAnalytics.issue_completed_month_wise.find((data) => data.month === parseInt(index, 10))?.count ||
|
||||
0,
|
||||
})),
|
||||
},
|
||||
]}
|
||||
customYAxisTickValues={defaultAnalytics.issue_completed_month_wise.map(
|
||||
(data) => data.count
|
||||
)}
|
||||
customYAxisTickValues={defaultAnalytics.issue_completed_month_wise.map((data) => data.count)}
|
||||
height="300px"
|
||||
colors={(datum) => datum.color}
|
||||
curve="monotoneX"
|
||||
|
@ -14,7 +14,7 @@ import useUser from "hooks/use-user";
|
||||
import { useProjectMyMembership } from "contexts/project-member.context";
|
||||
// components
|
||||
import { AllLists, AllBoards, CalendarView, SpreadsheetView, GanttChartView } from "components/core";
|
||||
import { KanBanLayout } from "components/issues/issue-layouts";
|
||||
import { CalendarLayout, KanBanLayout } from "components/issues";
|
||||
// ui
|
||||
import { EmptyState, Spinner } from "components/ui";
|
||||
// icons
|
||||
@ -31,6 +31,7 @@ import { STATES_LIST } from "constants/fetch-keys";
|
||||
// store
|
||||
import { useMobxStore } from "lib/mobx/store-provider";
|
||||
import { RootStore } from "store/root";
|
||||
import { observer } from "mobx-react-lite";
|
||||
|
||||
type Props = {
|
||||
addIssueToDate: (date: string) => void;
|
||||
@ -58,22 +59,7 @@ type Props = {
|
||||
viewProps: IIssueViewProps;
|
||||
};
|
||||
|
||||
export const AllViews: React.FC<Props> = ({
|
||||
addIssueToDate,
|
||||
addIssueToGroup,
|
||||
disableUserActions,
|
||||
dragDisabled = false,
|
||||
emptyState,
|
||||
handleIssueAction,
|
||||
handleDraftIssueAction,
|
||||
handleOnDragEnd,
|
||||
openIssuesListModal,
|
||||
removeIssue,
|
||||
disableAddIssueOption = false,
|
||||
trashBox,
|
||||
setTrashBox,
|
||||
viewProps,
|
||||
}) => {
|
||||
export const AllViews: React.FC<Props> = observer(({ trashBox, setTrashBox }) => {
|
||||
const router = useRouter();
|
||||
const { workspaceSlug, projectId, cycleId, moduleId } = router.query as {
|
||||
workspaceSlug: string;
|
||||
@ -84,10 +70,7 @@ export const AllViews: React.FC<Props> = ({
|
||||
|
||||
const [myIssueProjectId, setMyIssueProjectId] = useState<string | null>(null);
|
||||
|
||||
const { user } = useUser();
|
||||
const { memberRole } = useProjectMyMembership();
|
||||
|
||||
const { groupedIssues, isEmpty, displayFilters } = viewProps;
|
||||
const { issue: issueStore, project: projectStore, issueFilter: issueFilterStore } = useMobxStore();
|
||||
|
||||
const { data: stateGroups } = useSWR(
|
||||
workspaceSlug && projectId ? STATES_LIST(projectId as string) : null,
|
||||
@ -106,8 +89,6 @@ export const AllViews: React.FC<Props> = ({
|
||||
[trashBox, setTrashBox]
|
||||
);
|
||||
|
||||
const { issue: issueStore, project: projectStore, issueFilter: issueFilterStore }: RootStore = useMobxStore();
|
||||
|
||||
useSWR(workspaceSlug && projectId ? `PROJECT_ISSUES` : null, async () => {
|
||||
if (workspaceSlug && projectId) {
|
||||
await issueFilterStore.fetchUserProjectFilters(workspaceSlug, projectId);
|
||||
@ -120,121 +101,11 @@ export const AllViews: React.FC<Props> = ({
|
||||
}
|
||||
});
|
||||
|
||||
const activeLayout = issueFilterStore.userDisplayFilters.layout;
|
||||
|
||||
return (
|
||||
// <DragDropContext onDragEnd={handleOnDragEnd}>
|
||||
// <StrictModeDroppable droppableId="trashBox">
|
||||
// {(provided, snapshot) => (
|
||||
// <div
|
||||
// className={`${
|
||||
// trashBox ? "pointer-events-auto opacity-100" : "pointer-events-none opacity-0"
|
||||
// } fixed top-4 left-1/2 -translate-x-1/2 z-40 w-72 flex items-center justify-center gap-2 rounded border-2 border-red-500/20 bg-custom-background-100 px-3 py-5 text-xs font-medium italic text-red-500 ${
|
||||
// snapshot.isDraggingOver ? "bg-red-500 blur-2xl opacity-70" : ""
|
||||
// } transition duration-300`}
|
||||
// ref={provided.innerRef}
|
||||
// {...provided.droppableProps}
|
||||
// >
|
||||
// <TrashIcon className="h-4 w-4" />
|
||||
// Drop here to delete the issue.
|
||||
// </div>
|
||||
// )}
|
||||
// </StrictModeDroppable>
|
||||
// {groupedIssues ? (
|
||||
// !isEmpty ||
|
||||
// displayFilters?.layout === "kanban" ||
|
||||
// displayFilters?.layout === "calendar" ||
|
||||
// displayFilters?.layout === "gantt_chart" ? (
|
||||
// <>
|
||||
// {displayFilters?.layout === "list" ? (
|
||||
// <AllLists
|
||||
// states={states}
|
||||
// addIssueToGroup={addIssueToGroup}
|
||||
// handleIssueAction={handleIssueAction}
|
||||
// handleDraftIssueAction={handleDraftIssueAction}
|
||||
// openIssuesListModal={cycleId || moduleId ? openIssuesListModal : null}
|
||||
// removeIssue={removeIssue}
|
||||
// myIssueProjectId={myIssueProjectId}
|
||||
// handleMyIssueOpen={handleMyIssueOpen}
|
||||
// disableUserActions={disableUserActions}
|
||||
// disableAddIssueOption={disableAddIssueOption}
|
||||
// user={user}
|
||||
// userAuth={memberRole}
|
||||
// viewProps={viewProps}
|
||||
// />
|
||||
// ) : displayFilters?.layout === "kanban" ? (
|
||||
// <AllBoards
|
||||
// addIssueToGroup={addIssueToGroup}
|
||||
// disableUserActions={disableUserActions}
|
||||
// disableAddIssueOption={disableAddIssueOption}
|
||||
// dragDisabled={dragDisabled}
|
||||
// handleIssueAction={handleIssueAction}
|
||||
// handleDraftIssueAction={handleDraftIssueAction}
|
||||
// handleTrashBox={handleTrashBox}
|
||||
// openIssuesListModal={cycleId || moduleId ? openIssuesListModal : null}
|
||||
// myIssueProjectId={myIssueProjectId}
|
||||
// handleMyIssueOpen={handleMyIssueOpen}
|
||||
// removeIssue={removeIssue}
|
||||
// states={states}
|
||||
// user={user}
|
||||
// userAuth={memberRole}
|
||||
// viewProps={viewProps}
|
||||
// />
|
||||
// ) : displayFilters?.layout === "calendar" ? (
|
||||
// <CalendarView
|
||||
// handleIssueAction={handleIssueAction}
|
||||
// addIssueToDate={addIssueToDate}
|
||||
// disableUserActions={disableUserActions}
|
||||
// user={user}
|
||||
// userAuth={memberRole}
|
||||
// />
|
||||
// ) : displayFilters?.layout === "spreadsheet" ? (
|
||||
// <SpreadsheetView
|
||||
// handleIssueAction={handleIssueAction}
|
||||
// openIssuesListModal={cycleId || moduleId ? openIssuesListModal : null}
|
||||
// disableUserActions={disableUserActions}
|
||||
// user={user}
|
||||
// userAuth={memberRole}
|
||||
// />
|
||||
// ) : (
|
||||
// displayFilters?.layout === "gantt_chart" && <GanttChartView disableUserActions={disableUserActions} />
|
||||
// )}
|
||||
// </>
|
||||
// ) : router.pathname.includes("archived-issues") ? (
|
||||
// <EmptyState
|
||||
// title="Archived Issues will be shown here"
|
||||
// description="All the issues that have been in the completed or canceled groups for the configured period of time can be viewed here."
|
||||
// image={emptyIssueArchive}
|
||||
// primaryButton={{
|
||||
// text: "Go to Automation Settings",
|
||||
// onClick: () => {
|
||||
// router.push(`/${workspaceSlug}/projects/${projectId}/settings/automations`);
|
||||
// },
|
||||
// }}
|
||||
// />
|
||||
// ) : (
|
||||
// <EmptyState
|
||||
// title={emptyState.title}
|
||||
// description={emptyState.description}
|
||||
// image={emptyIssue}
|
||||
// primaryButton={
|
||||
// emptyState.primaryButton
|
||||
// ? {
|
||||
// icon: emptyState.primaryButton.icon,
|
||||
// text: emptyState.primaryButton.text,
|
||||
// onClick: emptyState.primaryButton.onClick,
|
||||
// }
|
||||
// : undefined
|
||||
// }
|
||||
// secondaryButton={emptyState.secondaryButton}
|
||||
// />
|
||||
// )
|
||||
// ) : (
|
||||
// <div className="flex h-full w-full items-center justify-center">
|
||||
// <Spinner />
|
||||
// </div>
|
||||
// )}
|
||||
// </DragDropContext>
|
||||
<div className="relative w-full h-full overflow-auto">
|
||||
<KanBanLayout />
|
||||
{activeLayout === "kanban" ? <KanBanLayout /> : activeLayout === "calendar" ? <CalendarLayout /> : null}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
});
|
||||
|
@ -1,71 +0,0 @@
|
||||
import React from "react";
|
||||
|
||||
// components
|
||||
import {
|
||||
FilterAssignees,
|
||||
FilterCreatedBy,
|
||||
FilterLabels,
|
||||
FilterPriority,
|
||||
FilterState,
|
||||
FilterStateGroup,
|
||||
} from "components/issue-layouts";
|
||||
|
||||
type Props = {
|
||||
workspaceSlug: string;
|
||||
projectId: string;
|
||||
};
|
||||
|
||||
export const FilterSelection: React.FC<Props> = (props) => {
|
||||
const { workspaceSlug, projectId } = props;
|
||||
|
||||
return (
|
||||
<div className="w-full h-full overflow-hidden select-none relative flex flex-col divide-y divide-custom-border-200 px-0.5">
|
||||
{/* <div className="flex-shrink-0 p-2 text-sm">Search container</div> */}
|
||||
<div className="w-full h-full overflow-hidden overflow-y-auto relative pb-2 divide-y divide-custom-border-200">
|
||||
{/* priority */}
|
||||
<div className="pb-1 px-2">
|
||||
<FilterPriority workspaceSlug={workspaceSlug} projectId={projectId} />
|
||||
</div>
|
||||
|
||||
{/* state group */}
|
||||
<div className="py-1 px-2">
|
||||
<FilterStateGroup workspaceSlug={workspaceSlug} projectId={projectId} />
|
||||
</div>
|
||||
|
||||
{/* state */}
|
||||
<div className="py-1 px-2">
|
||||
<FilterState workspaceSlug={workspaceSlug} projectId={projectId} />
|
||||
</div>
|
||||
|
||||
{/* assignees */}
|
||||
<div className="py-1 px-2">
|
||||
<FilterAssignees workspaceSlug={workspaceSlug} projectId={projectId} />
|
||||
</div>
|
||||
|
||||
{/* created_by */}
|
||||
<div className="py-1 px-2">
|
||||
<FilterCreatedBy workspaceSlug={workspaceSlug} projectId={projectId} />
|
||||
</div>
|
||||
|
||||
{/* labels */}
|
||||
<div className="py-1 px-2">
|
||||
<FilterLabels workspaceSlug={workspaceSlug} projectId={projectId} />
|
||||
</div>
|
||||
|
||||
{/* start_date */}
|
||||
{/* {handleFilterSectionVisibility("start_date") && (
|
||||
<div className="py-1 px-2">
|
||||
<FilterStartDate />
|
||||
</div>
|
||||
)} */}
|
||||
|
||||
{/* due_date */}
|
||||
{/* {handleFilterSectionVisibility("due_date") && (
|
||||
<div className="pt-1 px-2">
|
||||
<FilterTargetDate />
|
||||
</div>
|
||||
)} */}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
@ -90,7 +90,7 @@ export const FilterSelection: React.FC<Props> = observer((props) => {
|
||||
|
||||
return (
|
||||
<div className="w-full h-full flex flex-col overflow-hidden">
|
||||
<div className="p-2.5 bg-custom-background-100 sticky top-0 z-[1]">
|
||||
<div className="p-2.5 bg-custom-background-100">
|
||||
<div className="bg-custom-background-90 border-[0.5px] border-custom-border-200 text-xs rounded flex items-center gap-1.5 px-1.5 py-1">
|
||||
<Search className="text-custom-text-400" size={12} strokeWidth={2} />
|
||||
<input
|
||||
|
@ -19,9 +19,8 @@ export const LayoutSelection: React.FC<Props> = (props) => {
|
||||
return (
|
||||
<div className="flex items-center gap-1 p-1 rounded bg-custom-background-80">
|
||||
{ISSUE_LAYOUTS.filter((l) => layouts.includes(l.key)).map((layout) => (
|
||||
<Tooltip tooltipContent={layout.title}>
|
||||
<Tooltip key={layout.key} tooltipContent={layout.title}>
|
||||
<button
|
||||
key={layout.key}
|
||||
type="button"
|
||||
className={`w-7 h-[22px] rounded grid place-items-center transition-all hover:bg-custom-background-100 overflow-hidden group ${
|
||||
selectedLayout == layout.key ? "bg-custom-background-100 shadow-custom-shadow-2xs" : ""
|
||||
|
@ -8,6 +8,7 @@ export * from "./delete-issue-modal";
|
||||
export * from "./description-form";
|
||||
export * from "./form";
|
||||
export * from "./gantt-chart";
|
||||
export * from "./issue-layouts";
|
||||
export * from "./main-content";
|
||||
export * from "./modal";
|
||||
export * from "./parent-issues-list-modal";
|
||||
|
@ -1 +0,0 @@
|
||||
export * from "./root";
|
@ -1,17 +0,0 @@
|
||||
import React from "react";
|
||||
|
||||
export interface ICalendarLayout {
|
||||
issues: any;
|
||||
handleDragDrop: () => void;
|
||||
}
|
||||
|
||||
export const CalendarLayout: React.FC<ICalendarLayout> = ({}) => {
|
||||
console.log("kanaban layout");
|
||||
return (
|
||||
<div>
|
||||
<div>header</div>
|
||||
<div>content</div>
|
||||
<div>footer</div>
|
||||
</div>
|
||||
);
|
||||
};
|
54
web/components/issues/issue-layouts/calendar/calendar.tsx
Normal file
54
web/components/issues/issue-layouts/calendar/calendar.tsx
Normal file
@ -0,0 +1,54 @@
|
||||
import { observer } from "mobx-react-lite";
|
||||
|
||||
// mobx store
|
||||
import { useMobxStore } from "lib/mobx/store-provider";
|
||||
// components
|
||||
import { CalendarHeader, CalendarWeekDays, CalendarWeekHeader } from "components/issues";
|
||||
// ui
|
||||
import { Spinner } from "components/ui";
|
||||
// types
|
||||
import { ICalendarWeek } from "./types";
|
||||
import { IIssueGroupedStructure } from "store/issue";
|
||||
|
||||
type Props = {
|
||||
issues: IIssueGroupedStructure | null;
|
||||
};
|
||||
|
||||
export const CalendarChart: React.FC<Props> = observer((props) => {
|
||||
const { issues } = props;
|
||||
|
||||
const { calendar: calendarStore, issueFilter: issueFilterStore } = useMobxStore();
|
||||
|
||||
const calendarLayout = issueFilterStore.userDisplayFilters.calendar?.layout ?? "month";
|
||||
const calendarPayload = calendarStore.calendarPayload;
|
||||
|
||||
const allWeeksOfActiveMonth = calendarStore.allWeeksOfActiveMonth;
|
||||
|
||||
if (!calendarPayload)
|
||||
return (
|
||||
<div className="h-full w-full grid place-items-center">
|
||||
<Spinner />
|
||||
</div>
|
||||
);
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="h-full w-full flex flex-col overflow-hidden">
|
||||
<CalendarHeader />
|
||||
<CalendarWeekHeader />
|
||||
<div className="h-full w-full overflow-y-auto">
|
||||
{calendarLayout === "month" ? (
|
||||
<div className="h-full w-full grid grid-cols-1">
|
||||
{allWeeksOfActiveMonth &&
|
||||
Object.values(allWeeksOfActiveMonth).map((week: ICalendarWeek, weekIndex) => (
|
||||
<CalendarWeekDays key={weekIndex} week={week} issues={issues} />
|
||||
))}
|
||||
</div>
|
||||
) : (
|
||||
<CalendarWeekDays week={calendarStore.allDaysOfActiveWeek} issues={issues} />
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
});
|
58
web/components/issues/issue-layouts/calendar/day-tile.tsx
Normal file
58
web/components/issues/issue-layouts/calendar/day-tile.tsx
Normal file
@ -0,0 +1,58 @@
|
||||
import { observer } from "mobx-react-lite";
|
||||
import { Droppable } from "@hello-pangea/dnd";
|
||||
|
||||
// mobx store
|
||||
import { useMobxStore } from "lib/mobx/store-provider";
|
||||
// components
|
||||
import { CalendarIssueBlocks, ICalendarDate } from "components/issues";
|
||||
// helpers
|
||||
import { renderDateFormat } from "helpers/date-time.helper";
|
||||
// types
|
||||
import { IIssueGroupedStructure } from "store/issue";
|
||||
// constants
|
||||
import { MONTHS_LIST } from "constants/calendar";
|
||||
|
||||
type Props = { date: ICalendarDate; issues: IIssueGroupedStructure | null };
|
||||
|
||||
export const CalendarDayTile: React.FC<Props> = observer((props) => {
|
||||
const { date, issues } = props;
|
||||
|
||||
const { issueFilter: issueFilterStore } = useMobxStore();
|
||||
|
||||
const calendarLayout = issueFilterStore.userDisplayFilters.calendar?.layout ?? "month";
|
||||
|
||||
const issuesList = issues ? (issues as IIssueGroupedStructure)[renderDateFormat(date.date)] : null;
|
||||
|
||||
return (
|
||||
<Droppable droppableId={renderDateFormat(date.date)}>
|
||||
{(provided, snapshot) => (
|
||||
<div
|
||||
className={`flex-grow p-2 space-y-1 w-full flex flex-col overflow-hidden ${
|
||||
snapshot.isDraggingOver || date.date.getDay() === 0 || date.date.getDay() === 6
|
||||
? "bg-custom-background-90"
|
||||
: "bg-custom-background-100"
|
||||
} ${calendarLayout === "month" ? "min-h-[9rem]" : ""}`}
|
||||
{...provided.droppableProps}
|
||||
ref={provided.innerRef}
|
||||
>
|
||||
<>
|
||||
<div
|
||||
className={`text-xs text-right ${
|
||||
calendarLayout === "month" // if month layout, highlight current month days
|
||||
? date.is_current_month
|
||||
? "font-medium"
|
||||
: "text-custom-text-300"
|
||||
: "font-medium" // if week layout, highlight all days
|
||||
}`}
|
||||
>
|
||||
{date.date.getDate() === 1 && MONTHS_LIST[date.date.getMonth() + 1].shortTitle + " "}
|
||||
{date.date.getDate()}
|
||||
</div>
|
||||
<CalendarIssueBlocks issues={issuesList} />
|
||||
{provided.placeholder}
|
||||
</>
|
||||
</div>
|
||||
)}
|
||||
</Droppable>
|
||||
);
|
||||
});
|
@ -0,0 +1,2 @@
|
||||
export * from "./months-dropdown";
|
||||
export * from "./options-dropdown";
|
@ -0,0 +1,120 @@
|
||||
import React from "react";
|
||||
import { Popover, Transition } from "@headlessui/react";
|
||||
import { observer } from "mobx-react-lite";
|
||||
|
||||
// mobx store
|
||||
import { useMobxStore } from "lib/mobx/store-provider";
|
||||
// constants
|
||||
import { MONTHS_LIST } from "constants/calendar";
|
||||
import { ChevronLeft, ChevronRight } from "lucide-react";
|
||||
|
||||
export const CalendarMonthsDropdown: React.FC = observer(() => {
|
||||
const { calendar: calendarStore, issueFilter: issueFilterStore } = useMobxStore();
|
||||
|
||||
const calendarLayout = issueFilterStore.userDisplayFilters.calendar?.layout ?? "month";
|
||||
|
||||
const { activeMonthDate } = calendarStore.calendarFilters;
|
||||
|
||||
const getWeekLayoutHeader = (): string => {
|
||||
const allDaysOfActiveWeek = calendarStore.allDaysOfActiveWeek;
|
||||
|
||||
if (!allDaysOfActiveWeek) return "Week view";
|
||||
|
||||
const daysList = Object.keys(allDaysOfActiveWeek);
|
||||
|
||||
const firstDay = new Date(daysList[0]);
|
||||
const lastDay = new Date(daysList[daysList.length - 1]);
|
||||
|
||||
if (firstDay.getMonth() === lastDay.getMonth() && firstDay.getFullYear() === lastDay.getFullYear())
|
||||
return `${MONTHS_LIST[firstDay.getMonth() + 1].shortTitle} ${firstDay.getFullYear()}`;
|
||||
|
||||
if (firstDay.getFullYear() !== lastDay.getFullYear()) {
|
||||
return `${MONTHS_LIST[firstDay.getMonth() + 1].shortTitle} ${firstDay.getFullYear()} - ${
|
||||
MONTHS_LIST[lastDay.getMonth() + 1].shortTitle
|
||||
} ${lastDay.getFullYear()}`;
|
||||
} else
|
||||
return `${MONTHS_LIST[firstDay.getMonth() + 1].shortTitle} - ${
|
||||
MONTHS_LIST[lastDay.getMonth() + 1].shortTitle
|
||||
} ${lastDay.getFullYear()}`;
|
||||
};
|
||||
|
||||
const handleDateChange = (date: Date) => {
|
||||
calendarStore.updateCalendarFilters({
|
||||
activeMonthDate: date,
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<Popover className="relative">
|
||||
{({ close }) => (
|
||||
<>
|
||||
<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>
|
||||
<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 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">
|
||||
<div className="flex items-center justify-between gap-2 pb-3">
|
||||
<button
|
||||
type="button"
|
||||
className="grid place-items-center"
|
||||
onClick={() => {
|
||||
const previousYear = new Date(activeMonthDate.getFullYear() - 1, activeMonthDate.getMonth(), 1);
|
||||
handleDateChange(previousYear);
|
||||
|
||||
close();
|
||||
}}
|
||||
>
|
||||
<ChevronLeft size={14} />
|
||||
</button>
|
||||
<span className="text-xs">{activeMonthDate.getFullYear()}</span>
|
||||
<button
|
||||
type="button"
|
||||
className="grid place-items-center"
|
||||
onClick={() => {
|
||||
const nextYear = new Date(activeMonthDate.getFullYear() + 1, activeMonthDate.getMonth(), 1);
|
||||
handleDateChange(nextYear);
|
||||
|
||||
close();
|
||||
}}
|
||||
>
|
||||
<ChevronRight size={14} />
|
||||
</button>
|
||||
</div>
|
||||
<div className="grid grid-cols-4 gap-4 items-stretch justify-items-stretch pt-3">
|
||||
{Object.values(MONTHS_LIST).map((month, index) => (
|
||||
<button
|
||||
key={month.shortTitle}
|
||||
type="button"
|
||||
className={`text-xs hover:bg-custom-background-80 rounded py-0.5 ${
|
||||
activeMonthDate.getMonth() === index ? "bg-custom-background-80" : ""
|
||||
}`}
|
||||
onClick={() => {
|
||||
const newDate = new Date(activeMonthDate.getFullYear(), index, 1);
|
||||
handleDateChange(newDate);
|
||||
|
||||
close();
|
||||
}}
|
||||
>
|
||||
{month.shortTitle}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</Popover.Panel>
|
||||
</Transition>
|
||||
</>
|
||||
)}
|
||||
</Popover>
|
||||
);
|
||||
});
|
@ -0,0 +1,117 @@
|
||||
import React from "react";
|
||||
import { useRouter } from "next/router";
|
||||
import { Popover, Transition } from "@headlessui/react";
|
||||
import { observer } from "mobx-react-lite";
|
||||
|
||||
// mobx store
|
||||
import { useMobxStore } from "lib/mobx/store-provider";
|
||||
// ui
|
||||
import { ToggleSwitch } from "components/ui";
|
||||
// icons
|
||||
import { Check, ChevronUp } from "lucide-react";
|
||||
// types
|
||||
import { TCalendarLayouts } from "types";
|
||||
// constants
|
||||
import { CALENDAR_LAYOUTS } from "constants/calendar";
|
||||
|
||||
export const CalendarOptionsDropdown: React.FC = observer(() => {
|
||||
const router = useRouter();
|
||||
const { workspaceSlug, projectId } = router.query;
|
||||
|
||||
const { issueFilter: issueFilterStore, calendar: calendarStore } = useMobxStore();
|
||||
|
||||
const calendarLayout = issueFilterStore.userDisplayFilters.calendar?.layout ?? "month";
|
||||
const showWeekends = issueFilterStore.userDisplayFilters.calendar?.show_weekends ?? false;
|
||||
|
||||
const handleLayoutChange = (layout: TCalendarLayouts) => {
|
||||
if (!workspaceSlug || !projectId) return;
|
||||
|
||||
issueFilterStore.updateUserFilters(workspaceSlug.toString(), projectId.toString(), {
|
||||
display_filters: {
|
||||
calendar: {
|
||||
...issueFilterStore.userDisplayFilters.calendar,
|
||||
layout,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
calendarStore.updateCalendarPayload(
|
||||
layout === "month" ? calendarStore.calendarFilters.activeMonthDate : calendarStore.calendarFilters.activeWeekDate
|
||||
);
|
||||
};
|
||||
|
||||
const handleToggleWeekends = () => {
|
||||
const showWeekends = issueFilterStore.userDisplayFilters.calendar?.show_weekends ?? false;
|
||||
|
||||
if (!workspaceSlug || !projectId) return;
|
||||
|
||||
issueFilterStore.updateUserFilters(workspaceSlug.toString(), projectId.toString(), {
|
||||
display_filters: {
|
||||
calendar: {
|
||||
...issueFilterStore.userDisplayFilters.calendar,
|
||||
show_weekends: !showWeekends,
|
||||
},
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<Popover className="relative">
|
||||
{({ open }) => {
|
||||
if (open) {
|
||||
}
|
||||
return (
|
||||
<>
|
||||
<Popover.Button
|
||||
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"
|
||||
}`}
|
||||
>
|
||||
<div className="font-medium">Options</div>
|
||||
<div
|
||||
className={`w-3.5 h-3.5 flex items-center justify-center transition-all ${open ? "" : "rotate-180"}`}
|
||||
>
|
||||
<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
|
||||
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>
|
||||
</div>
|
||||
</Popover.Panel>
|
||||
</Transition>
|
||||
</>
|
||||
);
|
||||
}}
|
||||
</Popover>
|
||||
);
|
||||
});
|
98
web/components/issues/issue-layouts/calendar/header.tsx
Normal file
98
web/components/issues/issue-layouts/calendar/header.tsx
Normal file
@ -0,0 +1,98 @@
|
||||
import { observer } from "mobx-react-lite";
|
||||
|
||||
// mobx store
|
||||
import { useMobxStore } from "lib/mobx/store-provider";
|
||||
// components
|
||||
import { CalendarMonthsDropdown, CalendarOptionsDropdown } from "components/issues";
|
||||
// icons
|
||||
import { ChevronLeft, ChevronRight } from "lucide-react";
|
||||
|
||||
export const CalendarHeader: React.FC = observer(() => {
|
||||
const { issueFilter: issueFilterStore, calendar: calendarStore } = useMobxStore();
|
||||
|
||||
const calendarLayout = issueFilterStore.userDisplayFilters.calendar?.layout ?? "month";
|
||||
|
||||
const { activeMonthDate, activeWeekDate } = calendarStore.calendarFilters;
|
||||
|
||||
const handlePrevious = () => {
|
||||
if (calendarLayout === "month") {
|
||||
const previousMonthYear =
|
||||
activeMonthDate.getMonth() === 0 ? activeMonthDate.getFullYear() - 1 : activeMonthDate.getFullYear();
|
||||
const previousMonthMonth = activeMonthDate.getMonth() === 0 ? 11 : activeMonthDate.getMonth() - 1;
|
||||
|
||||
const previousMonthFirstDate = new Date(previousMonthYear, previousMonthMonth, 1);
|
||||
|
||||
calendarStore.updateCalendarFilters({
|
||||
activeMonthDate: previousMonthFirstDate,
|
||||
});
|
||||
} else {
|
||||
const previousWeekDate = new Date(
|
||||
activeWeekDate.getFullYear(),
|
||||
activeWeekDate.getMonth(),
|
||||
activeWeekDate.getDate() - 7
|
||||
);
|
||||
|
||||
calendarStore.updateCalendarFilters({
|
||||
activeWeekDate: previousWeekDate,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const handleNext = () => {
|
||||
if (calendarLayout === "month") {
|
||||
const nextMonthYear =
|
||||
activeMonthDate.getMonth() === 11 ? activeMonthDate.getFullYear() + 1 : activeMonthDate.getFullYear();
|
||||
const nextMonthMonth = (activeMonthDate.getMonth() + 1) % 12;
|
||||
|
||||
const nextMonthFirstDate = new Date(nextMonthYear, nextMonthMonth, 1);
|
||||
|
||||
calendarStore.updateCalendarFilters({
|
||||
activeMonthDate: nextMonthFirstDate,
|
||||
});
|
||||
} else {
|
||||
const nextWeekDate = new Date(
|
||||
activeWeekDate.getFullYear(),
|
||||
activeWeekDate.getMonth(),
|
||||
activeWeekDate.getDate() + 7
|
||||
);
|
||||
|
||||
calendarStore.updateCalendarFilters({
|
||||
activeWeekDate: nextWeekDate,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const handleToday = () => {
|
||||
const today = new Date();
|
||||
const firstDayOfCurrentMonth = new Date(today.getFullYear(), today.getMonth(), 1);
|
||||
|
||||
calendarStore.updateCalendarFilters({
|
||||
activeMonthDate: firstDayOfCurrentMonth,
|
||||
activeWeekDate: today,
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="flex items-center justify-between gap-2 px-3 mb-4">
|
||||
<div className="flex items-center gap-1.5">
|
||||
<button type="button" className="grid place-items-center" onClick={handlePrevious}>
|
||||
<ChevronLeft size={16} strokeWidth={2} />
|
||||
</button>
|
||||
<button type="button" className="grid place-items-center" onClick={handleNext}>
|
||||
<ChevronRight size={16} strokeWidth={2} />
|
||||
</button>
|
||||
<CalendarMonthsDropdown />
|
||||
</div>
|
||||
<div className="flex items-center gap-1.5">
|
||||
<button
|
||||
type="button"
|
||||
className="px-2.5 py-1 text-xs bg-custom-background-80 rounded font-medium"
|
||||
onClick={handleToday}
|
||||
>
|
||||
Today
|
||||
</button>
|
||||
<CalendarOptionsDropdown />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
});
|
9
web/components/issues/issue-layouts/calendar/index.ts
Normal file
9
web/components/issues/issue-layouts/calendar/index.ts
Normal file
@ -0,0 +1,9 @@
|
||||
export * from "./dropdowns";
|
||||
export * from "./calendar";
|
||||
export * from "./types.d";
|
||||
export * from "./day-tile";
|
||||
export * from "./header";
|
||||
export * from "./issue-blocks";
|
||||
export * from "./root";
|
||||
export * from "./week-days";
|
||||
export * from "./week-header";
|
@ -0,0 +1,48 @@
|
||||
import Link from "next/link";
|
||||
import { useRouter } from "next/router";
|
||||
import { Draggable } from "@hello-pangea/dnd";
|
||||
import { observer } from "mobx-react-lite";
|
||||
|
||||
// types
|
||||
import { IIssue } from "types";
|
||||
|
||||
type Props = { issues: IIssue[] | null };
|
||||
|
||||
export const CalendarIssueBlocks: React.FC<Props> = observer((props) => {
|
||||
const { issues } = props;
|
||||
|
||||
const router = useRouter();
|
||||
const { workspaceSlug } = router.query;
|
||||
|
||||
return (
|
||||
<div className="space-y-2 h-full w-full overflow-y-auto p-0.5">
|
||||
{issues?.map((issue, index) => (
|
||||
<Draggable key={issue.id} draggableId={issue.id} index={index}>
|
||||
{(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-200 ${
|
||||
snapshot.isDragging ? "shadow-custom-shadow-rg bg-custom-background-90" : "bg-custom-background-100"
|
||||
}`}
|
||||
{...provided.draggableProps}
|
||||
{...provided.dragHandleProps}
|
||||
ref={provided.innerRef}
|
||||
>
|
||||
<span
|
||||
className="h-full w-0.5 rounded flex-shrink-0"
|
||||
style={{
|
||||
backgroundColor: issue.state_detail.color,
|
||||
}}
|
||||
/>
|
||||
<div className="text-xs text-custom-text-300 flex-shrink-0">
|
||||
{issue.project_detail.identifier}-{issue.sequence_id}
|
||||
</div>
|
||||
<h6 className="text-xs flex-grow truncate">{issue.name}</h6>
|
||||
</a>
|
||||
</Link>
|
||||
)}
|
||||
</Draggable>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
});
|
36
web/components/issues/issue-layouts/calendar/root.tsx
Normal file
36
web/components/issues/issue-layouts/calendar/root.tsx
Normal file
@ -0,0 +1,36 @@
|
||||
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";
|
||||
// types
|
||||
import { IIssueGroupedStructure } from "store/issue";
|
||||
|
||||
export const CalendarLayout: React.FC = observer(() => {
|
||||
const { issue: issueStore } = useMobxStore();
|
||||
|
||||
// TODO: add drag and drop functionality
|
||||
const onDragEnd = (result: DropResult) => {
|
||||
if (!result) return;
|
||||
|
||||
// return if not dropped on the correct place
|
||||
if (!result.destination) return;
|
||||
|
||||
// return if dropped on the same date
|
||||
if (result.destination.droppableId === result.source.droppableId) return;
|
||||
|
||||
// issueKanBanViewStore?.handleDragDrop(result.source, result.destination);
|
||||
};
|
||||
|
||||
const issues = issueStore.getIssues;
|
||||
|
||||
return (
|
||||
<div className="h-full w-full pt-4 bg-custom-background-100 overflow-hidden">
|
||||
<DragDropContext onDragEnd={onDragEnd}>
|
||||
<CalendarChart issues={issues as IIssueGroupedStructure | null} />
|
||||
</DragDropContext>
|
||||
</div>
|
||||
);
|
||||
});
|
24
web/components/issues/issue-layouts/calendar/types.d.ts
vendored
Normal file
24
web/components/issues/issue-layouts/calendar/types.d.ts
vendored
Normal file
@ -0,0 +1,24 @@
|
||||
export interface ICalendarDate {
|
||||
date: Date;
|
||||
year: number;
|
||||
month: number;
|
||||
day: number;
|
||||
week: number; // week number wrt year, eg- 51, 52
|
||||
is_current_month: boolean;
|
||||
is_current_week: boolean;
|
||||
is_today: boolean;
|
||||
}
|
||||
|
||||
export interface ICalendarWeek {
|
||||
[date: string]: ICalendarDate;
|
||||
}
|
||||
|
||||
export interface ICalendarMonth {
|
||||
[monthIndex: string]: {
|
||||
[weekNumber: string]: ICalendarWeek;
|
||||
};
|
||||
}
|
||||
|
||||
export interface ICalendarPayload {
|
||||
[year: string]: ICalendarMonth;
|
||||
}
|
41
web/components/issues/issue-layouts/calendar/week-days.tsx
Normal file
41
web/components/issues/issue-layouts/calendar/week-days.tsx
Normal file
@ -0,0 +1,41 @@
|
||||
import { observer } from "mobx-react-lite";
|
||||
|
||||
// mobx store
|
||||
import { useMobxStore } from "lib/mobx/store-provider";
|
||||
// components
|
||||
import { CalendarDayTile } from "components/issues";
|
||||
// helpers
|
||||
import { renderDateFormat } from "helpers/date-time.helper";
|
||||
// types
|
||||
import { ICalendarDate, ICalendarWeek } from "./types";
|
||||
import { IIssueGroupedStructure } from "store/issue";
|
||||
|
||||
type Props = {
|
||||
issues: IIssueGroupedStructure | null;
|
||||
week: ICalendarWeek | undefined;
|
||||
};
|
||||
|
||||
export const CalendarWeekDays: React.FC<Props> = observer((props) => {
|
||||
const { issues, week } = props;
|
||||
|
||||
const { issueFilter: issueFilterStore } = useMobxStore();
|
||||
|
||||
const calendarLayout = issueFilterStore.userDisplayFilters.calendar?.layout ?? "month";
|
||||
const showWeekends = issueFilterStore.userDisplayFilters.calendar?.show_weekends ?? false;
|
||||
|
||||
if (!week) return null;
|
||||
|
||||
return (
|
||||
<div
|
||||
className={`grid divide-x-[0.5px] divide-y-[0.5px] divide-custom-border-200 ${
|
||||
showWeekends ? "grid-cols-7" : "grid-cols-5"
|
||||
} ${calendarLayout === "month" ? "" : "h-full"}`}
|
||||
>
|
||||
{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} />;
|
||||
})}
|
||||
</div>
|
||||
);
|
||||
});
|
30
web/components/issues/issue-layouts/calendar/week-header.tsx
Normal file
30
web/components/issues/issue-layouts/calendar/week-header.tsx
Normal file
@ -0,0 +1,30 @@
|
||||
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();
|
||||
|
||||
const showWeekends = issueFilterStore.userDisplayFilters.calendar?.show_weekends ?? false;
|
||||
|
||||
return (
|
||||
<div
|
||||
className={`grid text-sm font-medium divide-x-[0.5px] divide-custom-border-200 ${
|
||||
showWeekends ? "grid-cols-7" : "grid-cols-5"
|
||||
}`}
|
||||
>
|
||||
{Object.values(DAYS_LIST).map((day) => {
|
||||
if (!showWeekends && (day.shortTitle === "Sat" || day.shortTitle === "Sun")) return null;
|
||||
|
||||
return (
|
||||
<div key={day.shortTitle} className="h-11 bg-custom-background-90 flex items-center px-4">
|
||||
{day.shortTitle}
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
);
|
||||
});
|
@ -1,2 +1,2 @@
|
||||
export * from "./calendar";
|
||||
export * from "./kanban";
|
||||
export * from "./calandar";
|
||||
|
@ -1,22 +1,109 @@
|
||||
export const MONTHS_LIST = [
|
||||
{ value: 1, label: "January" },
|
||||
{ value: 2, label: "February" },
|
||||
{ value: 3, label: "March" },
|
||||
{ value: 4, label: "April" },
|
||||
{ value: 5, label: "May" },
|
||||
{ value: 6, label: "June" },
|
||||
{ value: 7, label: "July" },
|
||||
{ value: 8, label: "August" },
|
||||
{ value: 9, label: "September" },
|
||||
{ value: 10, label: "October" },
|
||||
{ value: 11, label: "November" },
|
||||
{ value: 12, label: "December" },
|
||||
];
|
||||
import { TCalendarLayouts } from "types";
|
||||
|
||||
export const YEARS_LIST = [
|
||||
{ value: "2021", label: "2021" },
|
||||
{ value: "2022", label: "2022" },
|
||||
{ value: "2023", label: "2023" },
|
||||
{ value: "2024", label: "2024" },
|
||||
{ value: "2025", label: "2025" },
|
||||
];
|
||||
export const MONTHS_LIST: {
|
||||
[monthNumber: number]: {
|
||||
shortTitle: string;
|
||||
title: string;
|
||||
};
|
||||
} = {
|
||||
1: {
|
||||
shortTitle: "Jan",
|
||||
title: "January",
|
||||
},
|
||||
2: {
|
||||
shortTitle: "Feb",
|
||||
title: "February",
|
||||
},
|
||||
3: {
|
||||
shortTitle: "Mar",
|
||||
title: "March",
|
||||
},
|
||||
4: {
|
||||
shortTitle: "Apr",
|
||||
title: "April",
|
||||
},
|
||||
5: {
|
||||
shortTitle: "May",
|
||||
title: "May",
|
||||
},
|
||||
6: {
|
||||
shortTitle: "Jun",
|
||||
title: "June",
|
||||
},
|
||||
7: {
|
||||
shortTitle: "Jul",
|
||||
title: "July",
|
||||
},
|
||||
8: {
|
||||
shortTitle: "Aug",
|
||||
title: "August",
|
||||
},
|
||||
9: {
|
||||
shortTitle: "Sep",
|
||||
title: "September",
|
||||
},
|
||||
10: {
|
||||
shortTitle: "Oct",
|
||||
title: "October",
|
||||
},
|
||||
11: {
|
||||
shortTitle: "Nov",
|
||||
title: "November",
|
||||
},
|
||||
12: {
|
||||
shortTitle: "Dec",
|
||||
title: "December",
|
||||
},
|
||||
};
|
||||
|
||||
export const DAYS_LIST: {
|
||||
[dayIndex: number]: {
|
||||
shortTitle: string;
|
||||
title: string;
|
||||
};
|
||||
} = {
|
||||
1: {
|
||||
shortTitle: "Sun",
|
||||
title: "Sunday",
|
||||
},
|
||||
2: {
|
||||
shortTitle: "Mon",
|
||||
title: "Monday",
|
||||
},
|
||||
3: {
|
||||
shortTitle: "Tue",
|
||||
title: "Tuesday",
|
||||
},
|
||||
4: {
|
||||
shortTitle: "Wed",
|
||||
title: "Wednesday",
|
||||
},
|
||||
5: {
|
||||
shortTitle: "Thu",
|
||||
title: "Thursday",
|
||||
},
|
||||
6: {
|
||||
shortTitle: "Fri",
|
||||
title: "Friday",
|
||||
},
|
||||
7: {
|
||||
shortTitle: "Sat",
|
||||
title: "Saturday",
|
||||
},
|
||||
};
|
||||
|
||||
export const CALENDAR_LAYOUTS: {
|
||||
[layout in TCalendarLayouts]: {
|
||||
key: TCalendarLayouts;
|
||||
title: string;
|
||||
};
|
||||
} = {
|
||||
month: {
|
||||
key: "month",
|
||||
title: "Month layout",
|
||||
},
|
||||
week: {
|
||||
key: "week",
|
||||
title: "Week layout",
|
||||
},
|
||||
};
|
||||
|
@ -112,7 +112,6 @@ export const ISSUE_EXTRA_OPTIONS: {
|
||||
}[] = [
|
||||
{ key: "sub_issue", title: "Show sub-issues" }, // in spreadsheet its always false
|
||||
{ key: "show_empty_groups", title: "Show empty states" }, // filter on front-end
|
||||
{ key: "calendar_date_range", title: "Calendar Date Range" }, // calendar date range yyyy-mm-dd;before range yyyy-mm-dd;after
|
||||
{ key: "start_target_date", title: "Start target Date" }, // gantt always be true
|
||||
];
|
||||
|
||||
|
@ -22,12 +22,7 @@ import {
|
||||
IProjectViewProps,
|
||||
} from "types";
|
||||
// fetch-keys
|
||||
import {
|
||||
CYCLE_DETAILS,
|
||||
MODULE_DETAILS,
|
||||
USER_PROJECT_VIEW,
|
||||
VIEW_DETAILS,
|
||||
} from "constants/fetch-keys";
|
||||
import { CYCLE_DETAILS, MODULE_DETAILS, USER_PROJECT_VIEW, VIEW_DETAILS } from "constants/fetch-keys";
|
||||
|
||||
export const issueViewContext = createContext<ContextType>({} as ContextType);
|
||||
|
||||
@ -48,7 +43,6 @@ type ReducerFunctionType = (state: StateType, action: ReducerActionType) => Stat
|
||||
|
||||
export const initialState: StateType = {
|
||||
display_filters: {
|
||||
calendar_date_range: "",
|
||||
group_by: null,
|
||||
layout: "list",
|
||||
order_by: "-created_at",
|
||||
@ -123,11 +117,7 @@ export const reducer: ReducerFunctionType = (state, action) => {
|
||||
}
|
||||
};
|
||||
|
||||
const saveDataToServer = async (
|
||||
workspaceSlug: string,
|
||||
projectId: string,
|
||||
state: IProjectViewProps
|
||||
) => {
|
||||
const saveDataToServer = async (workspaceSlug: string, projectId: string, state: IProjectViewProps) => {
|
||||
mutate<IProjectMember>(
|
||||
workspaceSlug && projectId ? USER_PROJECT_VIEW(projectId as string) : null,
|
||||
(prevData) => {
|
||||
@ -238,36 +228,21 @@ export const IssueViewContextProvider: React.FC<{ children: React.ReactNode }> =
|
||||
const { data: viewDetails, mutate: mutateViewDetails } = useSWR(
|
||||
workspaceSlug && projectId && viewId ? VIEW_DETAILS(viewId as string) : null,
|
||||
workspaceSlug && projectId && viewId
|
||||
? () =>
|
||||
viewsService.getViewDetails(
|
||||
workspaceSlug as string,
|
||||
projectId as string,
|
||||
viewId as string
|
||||
)
|
||||
? () => viewsService.getViewDetails(workspaceSlug as string, projectId as string, viewId as string)
|
||||
: null
|
||||
);
|
||||
|
||||
const { data: cycleDetails, mutate: mutateCycleDetails } = useSWR(
|
||||
workspaceSlug && projectId && cycleId ? CYCLE_DETAILS(cycleId as string) : null,
|
||||
workspaceSlug && projectId && cycleId
|
||||
? () =>
|
||||
cyclesService.getCycleDetails(
|
||||
workspaceSlug.toString(),
|
||||
projectId.toString(),
|
||||
cycleId.toString()
|
||||
)
|
||||
? () => cyclesService.getCycleDetails(workspaceSlug.toString(), projectId.toString(), cycleId.toString())
|
||||
: null
|
||||
);
|
||||
|
||||
const { data: moduleDetails, mutate: mutateModuleDetails } = useSWR(
|
||||
workspaceSlug && projectId && moduleId ? MODULE_DETAILS(moduleId.toString()) : null,
|
||||
workspaceSlug && projectId && moduleId
|
||||
? () =>
|
||||
modulesService.getModuleDetails(
|
||||
workspaceSlug.toString(),
|
||||
projectId.toString(),
|
||||
moduleId.toString()
|
||||
)
|
||||
? () => modulesService.getModuleDetails(workspaceSlug.toString(), projectId.toString(), moduleId.toString())
|
||||
: null
|
||||
);
|
||||
|
||||
@ -288,11 +263,7 @@ export const IssueViewContextProvider: React.FC<{ children: React.ReactNode }> =
|
||||
order_by: displayFilter.order_by ?? state.display_filters?.order_by,
|
||||
};
|
||||
|
||||
if (
|
||||
displayFilter.layout &&
|
||||
displayFilter.layout === "kanban" &&
|
||||
state.display_filters?.group_by === null
|
||||
) {
|
||||
if (displayFilter.layout && displayFilter.layout === "kanban" && state.display_filters?.group_by === null) {
|
||||
additionalProperties.group_by = "state";
|
||||
dispatch({
|
||||
type: "SET_DISPLAY_FILTERS",
|
||||
@ -343,8 +314,7 @@ export const IssueViewContextProvider: React.FC<{ children: React.ReactNode }> =
|
||||
const setFilters = useCallback(
|
||||
(property: Partial<IIssueFilterOptions>, saveToServer = true) => {
|
||||
Object.keys(property).forEach((key) => {
|
||||
if (property[key as keyof typeof property]?.length === 0)
|
||||
property[key as keyof typeof property] = null;
|
||||
if (property[key as keyof typeof property]?.length === 0) property[key as keyof typeof property] = null;
|
||||
});
|
||||
|
||||
dispatch({
|
||||
|
@ -1,79 +1,7 @@
|
||||
export const startOfWeek = (date: Date) => {
|
||||
const startOfMonthDate = new Date(date.getFullYear(), date.getMonth(), 1);
|
||||
const dayOfWeek = startOfMonthDate.getDay() % 7;
|
||||
const startOfWeekDate = new Date(
|
||||
startOfMonthDate.getFullYear(),
|
||||
startOfMonthDate.getMonth(),
|
||||
startOfMonthDate.getDate() - dayOfWeek
|
||||
);
|
||||
const timezoneOffset = startOfMonthDate.getTimezoneOffset();
|
||||
const timezoneOffsetMilliseconds = timezoneOffset * 60 * 1000;
|
||||
const startOfWeekAdjusted = new Date(startOfWeekDate.getTime() - timezoneOffsetMilliseconds);
|
||||
return startOfWeekAdjusted;
|
||||
};
|
||||
|
||||
export const lastDayOfWeek = (date: Date) => {
|
||||
const lastDayOfPreviousMonth = new Date(date.getFullYear(), date.getMonth() + 1, 0);
|
||||
const dayOfWeek = lastDayOfPreviousMonth.getDay() % 7;
|
||||
const daysUntilEndOfWeek = 6 - dayOfWeek;
|
||||
const lastDayOfWeekDate = new Date(
|
||||
lastDayOfPreviousMonth.getFullYear(),
|
||||
lastDayOfPreviousMonth.getMonth(),
|
||||
lastDayOfPreviousMonth.getDate() + daysUntilEndOfWeek
|
||||
);
|
||||
const timezoneOffset = lastDayOfPreviousMonth.getTimezoneOffset();
|
||||
const timezoneOffsetMilliseconds = timezoneOffset * 60 * 1000;
|
||||
const lastDayOfWeekAdjusted = new Date(lastDayOfWeekDate.getTime() - timezoneOffsetMilliseconds);
|
||||
return lastDayOfWeekAdjusted;
|
||||
};
|
||||
|
||||
export const getCurrentWeekStartDate = (date: Date) => {
|
||||
const today = new Date(date);
|
||||
const dayOfWeek = today.getDay();
|
||||
const startOfWeek = new Date(today.getFullYear(), today.getMonth(), today.getDate() - dayOfWeek);
|
||||
const timezoneOffset = startOfWeek.getTimezoneOffset();
|
||||
const timezoneOffsetMilliseconds = timezoneOffset * 60 * 1000;
|
||||
const startOfWeekAdjusted = new Date(startOfWeek.getTime() - timezoneOffsetMilliseconds);
|
||||
return startOfWeekAdjusted;
|
||||
};
|
||||
|
||||
export const getCurrentWeekEndDate = (date: Date) => {
|
||||
const today = new Date(date);
|
||||
const dayOfWeek = today.getDay();
|
||||
const endOfWeek = new Date(
|
||||
today.getFullYear(),
|
||||
today.getMonth(),
|
||||
today.getDate() + (6 - dayOfWeek)
|
||||
);
|
||||
const timezoneOffset = endOfWeek.getTimezoneOffset();
|
||||
const timezoneOffsetMilliseconds = timezoneOffset * 60 * 1000;
|
||||
const endOfWeekAdjusted = new Date(endOfWeek.getTime() - timezoneOffsetMilliseconds);
|
||||
return endOfWeekAdjusted;
|
||||
};
|
||||
|
||||
export const eachDayOfInterval = ({ start, end }: { start: Date; end: Date }) => {
|
||||
const days = [];
|
||||
const current = new Date(start);
|
||||
while (current <= end) {
|
||||
days.push(new Date(current));
|
||||
current.setDate(current.getDate() + 1);
|
||||
}
|
||||
return days;
|
||||
};
|
||||
|
||||
export const weekDayInterval = ({ start, end }: { start: Date; end: Date }) => {
|
||||
const dates = [];
|
||||
const currentDate = new Date(start);
|
||||
const endDate = new Date(end);
|
||||
while (currentDate <= endDate) {
|
||||
const dayOfWeek = currentDate.getDay();
|
||||
if (dayOfWeek !== 0 && dayOfWeek !== 6) {
|
||||
dates.push(new Date(currentDate));
|
||||
}
|
||||
currentDate.setDate(currentDate.getDate() + 1);
|
||||
}
|
||||
return dates;
|
||||
};
|
||||
// helpers
|
||||
import { getWeekNumberOfDate, renderDateFormat } from "helpers/date-time.helper";
|
||||
// types
|
||||
import { ICalendarDate, ICalendarPayload } from "components/issues";
|
||||
|
||||
export const formatDate = (date: Date, format: string): string => {
|
||||
const day = date.getDate();
|
||||
@ -112,50 +40,53 @@ export const formatDate = (date: Date, format: string): string => {
|
||||
return formattedDate;
|
||||
};
|
||||
|
||||
export const subtractMonths = (date: Date, numMonths: number) => {
|
||||
const result = new Date(date);
|
||||
result.setMonth(result.getMonth() - numMonths);
|
||||
return result;
|
||||
};
|
||||
/**
|
||||
* @returns {ICalendarPayload} calendar payload to render the calendar
|
||||
* @param {ICalendarPayload | null} currentStructure current calendar payload
|
||||
* @param {Date} startDate date of the month to render
|
||||
* @description Returns calendar payload to render the calendar, if currentStructure is null, it will generate the payload for the month of startDate, else it will construct the payload for the month of startDate and append it to the currentStructure
|
||||
*/
|
||||
export const generateCalendarData = (currentStructure: ICalendarPayload | null, startDate: Date): ICalendarPayload => {
|
||||
const calendarData: ICalendarPayload = currentStructure ?? {};
|
||||
|
||||
export const addMonths = (date: Date, numMonths: number) => {
|
||||
const result = new Date(date);
|
||||
result.setMonth(result.getMonth() + numMonths);
|
||||
return result;
|
||||
};
|
||||
const startMonth = startDate.getMonth();
|
||||
const startYear = startDate.getFullYear();
|
||||
|
||||
export const updateDateWithYear = (yearString: string, date: Date) => {
|
||||
const year = parseInt(yearString);
|
||||
const month = date.getMonth();
|
||||
const day = date.getDate();
|
||||
return new Date(year, month, day);
|
||||
};
|
||||
const currentDate = new Date(startYear, startMonth, 1);
|
||||
const year = currentDate.getFullYear();
|
||||
const month = currentDate.getMonth();
|
||||
const totalDaysInMonth = new Date(year, month + 1, 0).getDate();
|
||||
const firstDayOfMonth = new Date(year, month, 1).getDay(); // Sunday is 0, Monday is 1, ..., Saturday is 6
|
||||
|
||||
export const updateDateWithMonth = (monthString: string, date: Date) => {
|
||||
const month = parseInt(monthString) - 1;
|
||||
const year = date.getFullYear();
|
||||
const day = date.getDate();
|
||||
return new Date(year, month, day);
|
||||
};
|
||||
calendarData[`y-${year}`] ||= {};
|
||||
calendarData[`y-${year}`][`m-${month}`] ||= {};
|
||||
|
||||
export const isSameMonth = (monthString: string, date: Date) => {
|
||||
const month = parseInt(monthString) - 1;
|
||||
return month === date.getMonth();
|
||||
};
|
||||
const numWeeks = Math.ceil((totalDaysInMonth + firstDayOfMonth) / 7);
|
||||
|
||||
export const isSameYear = (yearString: string, date: Date) => {
|
||||
const year = parseInt(yearString);
|
||||
return year === date.getFullYear();
|
||||
};
|
||||
for (let week = 0; week < numWeeks; week++) {
|
||||
const currentWeekObject: { [date: string]: ICalendarDate } = {};
|
||||
|
||||
export const addSevenDaysToDate = (date: Date) => {
|
||||
const currentDate = new Date(date);
|
||||
const newDate = new Date(currentDate.setDate(currentDate.getDate() + 7));
|
||||
return newDate;
|
||||
};
|
||||
const weekNumber = getWeekNumberOfDate(new Date(year, month, week * 7 - firstDayOfMonth + 1));
|
||||
|
||||
export const subtract7DaysToDate = (date: Date) => {
|
||||
const currentDate = new Date(date);
|
||||
const newDate = new Date(currentDate.getTime() - 7 * 24 * 60 * 60 * 1000);
|
||||
return newDate;
|
||||
for (let i = 0; i < 7; i++) {
|
||||
const dayNumber = week * 7 + i - firstDayOfMonth;
|
||||
|
||||
const date = new Date(year, month, dayNumber + 1);
|
||||
|
||||
currentWeekObject[renderDateFormat(date)] = {
|
||||
date,
|
||||
year,
|
||||
month,
|
||||
day: dayNumber + 1,
|
||||
week: weekNumber,
|
||||
is_current_month: date.getMonth() === month,
|
||||
is_current_week: getWeekNumberOfDate(date) === getWeekNumberOfDate(new Date()),
|
||||
is_today: date.toDateString() === new Date().toDateString(),
|
||||
};
|
||||
}
|
||||
|
||||
calendarData[`y-${year}`][`m-${month}`][`w-${weekNumber}`] = currentWeekObject;
|
||||
}
|
||||
|
||||
return calendarData;
|
||||
};
|
||||
|
@ -130,10 +130,7 @@ export const formatDateDistance = (date: string | Date) => {
|
||||
}
|
||||
};
|
||||
|
||||
export const getDateRangeStatus = (
|
||||
startDate: string | null | undefined,
|
||||
endDate: string | null | undefined
|
||||
) => {
|
||||
export const getDateRangeStatus = (startDate: string | null | undefined, endDate: string | null | undefined) => {
|
||||
if (!startDate || !endDate) return "draft";
|
||||
|
||||
const today = renderDateFormat(new Date());
|
||||
@ -155,20 +152,7 @@ export const renderShortDateWithYearFormat = (date: string | Date, placeholder?:
|
||||
|
||||
date = new Date(date);
|
||||
|
||||
const months = [
|
||||
"Jan",
|
||||
"Feb",
|
||||
"Mar",
|
||||
"Apr",
|
||||
"May",
|
||||
"Jun",
|
||||
"Jul",
|
||||
"Aug",
|
||||
"Sep",
|
||||
"Oct",
|
||||
"Nov",
|
||||
"Dec",
|
||||
];
|
||||
const months = ["Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"];
|
||||
const day = date.getDate();
|
||||
const month = months[date.getMonth()];
|
||||
const year = date.getFullYear();
|
||||
@ -181,20 +165,7 @@ export const renderShortDate = (date: string | Date, placeholder?: string) => {
|
||||
|
||||
date = new Date(date);
|
||||
|
||||
const months = [
|
||||
"Jan",
|
||||
"Feb",
|
||||
"Mar",
|
||||
"Apr",
|
||||
"May",
|
||||
"Jun",
|
||||
"Jul",
|
||||
"Aug",
|
||||
"Sep",
|
||||
"Oct",
|
||||
"Nov",
|
||||
"Dec",
|
||||
];
|
||||
const months = ["Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"];
|
||||
const day = date.getDate();
|
||||
const month = months[date.getMonth()];
|
||||
|
||||
@ -234,8 +205,7 @@ export const render24HourFormatTime = (date: string | Date): string => {
|
||||
return hours + ":" + minutes;
|
||||
};
|
||||
|
||||
export const isDateRangeValid = (startDate: string, endDate: string) =>
|
||||
new Date(startDate) < new Date(endDate);
|
||||
export const isDateRangeValid = (startDate: string, endDate: string) => new Date(startDate) < new Date(endDate);
|
||||
|
||||
export const isDateGreaterThanToday = (dateStr: string) => {
|
||||
const date = new Date(dateStr);
|
||||
@ -331,8 +301,7 @@ export const getDatesAfterCurrentDate = (): Array<{
|
||||
* @example checkIfStringIsDate("2021-01-32") // false
|
||||
*/
|
||||
|
||||
export const checkIfStringIsDate = (date: string): boolean =>
|
||||
new Date(date).toString() !== "Invalid Date";
|
||||
export const checkIfStringIsDate = (date: string): boolean => new Date(date).toString() !== "Invalid Date";
|
||||
|
||||
// return an array of dates starting from 12:00 to 23:30 with 30 minutes interval as dates
|
||||
export const getDatesWith30MinutesInterval = (): Array<Date> => {
|
||||
@ -384,11 +353,7 @@ export const getAllTimeIn30MinutesInterval = (): Array<{
|
||||
* @example checkIfStringIsDate("2021-01-01", "2021-01-08") // 8
|
||||
*/
|
||||
|
||||
export const findTotalDaysInRange = (
|
||||
startDate: Date | string,
|
||||
endDate: Date | string,
|
||||
inclusive: boolean
|
||||
): number => {
|
||||
export const findTotalDaysInRange = (startDate: Date | string, endDate: Date | string, inclusive: boolean): number => {
|
||||
if (!startDate || !endDate) return 0;
|
||||
|
||||
startDate = new Date(startDate);
|
||||
@ -405,3 +370,46 @@ export const findTotalDaysInRange = (
|
||||
};
|
||||
|
||||
export const getUserTimeZoneFromWindow = () => Intl.DateTimeFormat().resolvedOptions().timeZone;
|
||||
|
||||
/**
|
||||
* @returns {number} week number of date
|
||||
* @description Returns week number of date
|
||||
* @param {Date} date
|
||||
* @example getWeekNumber(new Date("2023-09-01")) // 35
|
||||
*/
|
||||
export const getWeekNumberOfDate = (date: Date): number => {
|
||||
const currentDate = new Date(date);
|
||||
|
||||
// Adjust the starting day to Sunday (0) instead of Monday (1)
|
||||
const startDate = new Date(currentDate.getFullYear(), 0, 1);
|
||||
|
||||
// Calculate the number of days between currentDate and startDate
|
||||
const days = Math.floor((currentDate.getTime() - startDate.getTime()) / (24 * 60 * 60 * 1000));
|
||||
|
||||
// Adjust the calculation for weekNumber
|
||||
const weekNumber = Math.ceil((days + 1) / 7);
|
||||
|
||||
return weekNumber;
|
||||
};
|
||||
|
||||
/**
|
||||
* @returns {Date} first date of week
|
||||
* @description Returns week number of date
|
||||
* @param {Date} date
|
||||
* @example getFirstDateOfWeek(35, 2023) // 2023-08-27T00:00:00.000Z
|
||||
*/
|
||||
export const getFirstDateOfWeek = (date: Date): Date => {
|
||||
const year = date.getFullYear();
|
||||
const weekNumber = getWeekNumberOfDate(date);
|
||||
|
||||
const januaryFirst: Date = new Date(year, 0, 1); // January is month 0
|
||||
const daysToAdd: number = (weekNumber - 1) * 7; // Subtract 1 from the week number since weeks are 0-indexed
|
||||
const firstDateOfWeek: Date = new Date(januaryFirst);
|
||||
firstDateOfWeek.setDate(januaryFirst.getDate() + daysToAdd);
|
||||
|
||||
// Adjust the date to Sunday (week start)
|
||||
const dayOfWeek: number = firstDateOfWeek.getDay();
|
||||
firstDateOfWeek.setDate(firstDateOfWeek.getDate() - dayOfWeek); // Move back to Sunday
|
||||
|
||||
return firstDateOfWeek;
|
||||
};
|
||||
|
@ -125,7 +125,6 @@ export const handleIssueQueryParamsByLayout = (_layout: TIssueLayouts | undefine
|
||||
"start_date",
|
||||
"target_date",
|
||||
"type",
|
||||
"calendar_date_range",
|
||||
];
|
||||
if (_layout === "spreadsheet")
|
||||
return [
|
||||
|
@ -41,7 +41,6 @@ const useCalendarIssuesView = () => {
|
||||
labels: filters?.labels ? filters?.labels.join(",") : undefined,
|
||||
created_by: filters?.created_by ? filters?.created_by.join(",") : undefined,
|
||||
start_date: filters?.start_date ? filters?.start_date.join(",") : undefined,
|
||||
target_date: displayFilters?.calendar_date_range,
|
||||
};
|
||||
|
||||
const { data: projectCalendarIssues, mutate: mutateProjectCalendarIssues } = useSWR(
|
||||
|
19
web/pages/calendar.tsx
Normal file
19
web/pages/calendar.tsx
Normal file
@ -0,0 +1,19 @@
|
||||
import React from "react";
|
||||
|
||||
// layouts
|
||||
import DefaultLayout from "layouts/default-layout";
|
||||
import { UserAuthorizationLayout } from "layouts/auth-layout/user-authorization-wrapper";
|
||||
// components
|
||||
import { CalendarView } from "components/issues";
|
||||
// types
|
||||
import type { NextPage } from "next";
|
||||
|
||||
const OnBoard: NextPage = () => (
|
||||
<UserAuthorizationLayout>
|
||||
<DefaultLayout>
|
||||
<CalendarView />
|
||||
</DefaultLayout>
|
||||
</UserAuthorizationLayout>
|
||||
);
|
||||
|
||||
export default OnBoard;
|
121
web/store/calendar.ts
Normal file
121
web/store/calendar.ts
Normal file
@ -0,0 +1,121 @@
|
||||
import { observable, action, makeObservable, runInAction, computed } from "mobx";
|
||||
|
||||
// helpers
|
||||
import { generateCalendarData } from "helpers/calendar.helper";
|
||||
// types
|
||||
import { RootStore } from "./root";
|
||||
import { ICalendarPayload, ICalendarWeek } from "components/issues";
|
||||
import { getWeekNumberOfDate } from "helpers/date-time.helper";
|
||||
|
||||
export interface ICalendarStore {
|
||||
calendarFilters: {
|
||||
activeMonthDate: Date;
|
||||
activeWeekDate: Date;
|
||||
};
|
||||
calendarPayload: ICalendarPayload | null;
|
||||
|
||||
// action
|
||||
updateCalendarFilters: (filters: Partial<{ activeMonthDate: Date; activeWeekDate: Date }>) => void;
|
||||
updateCalendarPayload: (date: Date) => void;
|
||||
|
||||
// computed
|
||||
allWeeksOfActiveMonth:
|
||||
| {
|
||||
[weekNumber: string]: ICalendarWeek;
|
||||
}
|
||||
| undefined;
|
||||
activeWeekNumber: number;
|
||||
allDaysOfActiveWeek: ICalendarWeek | undefined;
|
||||
}
|
||||
|
||||
class CalendarStore implements ICalendarStore {
|
||||
loader: boolean = false;
|
||||
error: any | null = null;
|
||||
|
||||
// observables
|
||||
calendarFilters: { activeMonthDate: Date; activeWeekDate: Date } = {
|
||||
activeMonthDate: new Date(),
|
||||
activeWeekDate: new Date(),
|
||||
};
|
||||
calendarPayload: ICalendarPayload | null = null;
|
||||
|
||||
// root store
|
||||
rootStore;
|
||||
|
||||
constructor(_rootStore: RootStore) {
|
||||
makeObservable(this, {
|
||||
loader: observable.ref,
|
||||
error: observable.ref,
|
||||
|
||||
// observables
|
||||
calendarFilters: observable.ref,
|
||||
calendarPayload: observable.ref,
|
||||
|
||||
// actions
|
||||
updateCalendarFilters: action,
|
||||
updateCalendarPayload: action,
|
||||
|
||||
//computed
|
||||
allWeeksOfActiveMonth: computed,
|
||||
activeWeekNumber: computed,
|
||||
allDaysOfActiveWeek: computed,
|
||||
});
|
||||
|
||||
this.rootStore = _rootStore;
|
||||
|
||||
this.initCalendar();
|
||||
}
|
||||
|
||||
get allWeeksOfActiveMonth() {
|
||||
if (!this.calendarPayload) return undefined;
|
||||
|
||||
const { activeMonthDate } = this.calendarFilters;
|
||||
|
||||
return this.calendarPayload[`y-${activeMonthDate.getFullYear()}`][`m-${activeMonthDate.getMonth()}`];
|
||||
}
|
||||
|
||||
get activeWeekNumber() {
|
||||
return getWeekNumberOfDate(this.calendarFilters.activeWeekDate);
|
||||
}
|
||||
|
||||
get allDaysOfActiveWeek() {
|
||||
if (!this.calendarPayload) return undefined;
|
||||
|
||||
const { activeWeekDate } = this.calendarFilters;
|
||||
|
||||
return this.calendarPayload[`y-${activeWeekDate.getFullYear()}`][`m-${activeWeekDate.getMonth()}`][
|
||||
`w-${this.activeWeekNumber}`
|
||||
];
|
||||
}
|
||||
|
||||
updateCalendarFilters = (filters: Partial<{ activeMonthDate: Date; activeWeekDate: Date }>) => {
|
||||
this.updateCalendarPayload(filters.activeMonthDate || filters.activeWeekDate || new Date());
|
||||
|
||||
runInAction(() => {
|
||||
this.calendarFilters = {
|
||||
...this.calendarFilters,
|
||||
...filters,
|
||||
};
|
||||
});
|
||||
};
|
||||
|
||||
updateCalendarPayload = (date: Date) => {
|
||||
if (!this.calendarPayload) return null;
|
||||
|
||||
const nextDate = new Date(date);
|
||||
|
||||
runInAction(() => {
|
||||
this.calendarPayload = generateCalendarData(this.calendarPayload, nextDate);
|
||||
});
|
||||
};
|
||||
|
||||
initCalendar = () => {
|
||||
const newCalendarPayload = generateCalendarData(null, new Date());
|
||||
|
||||
runInAction(() => {
|
||||
this.calendarPayload = newCalendarPayload;
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
export default CalendarStore;
|
@ -72,8 +72,8 @@ class IssueStore implements IIssueStore {
|
||||
}
|
||||
|
||||
get getIssueType() {
|
||||
const groupedLayouts = ["kanban", "list"];
|
||||
const ungroupedLayouts = ["calendar", "spreadsheet", "gantt_chart"];
|
||||
const groupedLayouts = ["kanban", "list", "calendar"];
|
||||
const ungroupedLayouts = ["spreadsheet", "gantt_chart"];
|
||||
|
||||
const issueLayout = this.rootStore?.issueFilter?.userDisplayFilters?.layout || null;
|
||||
const issueSubGroup = this.rootStore?.issueFilter?.userDisplayFilters?.sub_group_by || null;
|
||||
@ -143,7 +143,11 @@ class IssueStore implements IIssueStore {
|
||||
this.rootStore.project.setProjectId(projectId);
|
||||
|
||||
// TODO: replace this once the issue filter is completed
|
||||
const params = { group_by: "state", order_by: "-created_at" };
|
||||
const params = {
|
||||
group_by: "target_date",
|
||||
order_by: "-created_at",
|
||||
target_date: "2023-09-01;after,2023-09-30;before",
|
||||
};
|
||||
const issueResponse = await this.issueService.getIssuesWithParams(workspaceSlug, projectId, params);
|
||||
|
||||
const issueType = this.getIssueType;
|
||||
|
@ -4,6 +4,7 @@ import { ProjectService } from "services/project.service";
|
||||
import { IssueService } from "services/issue.service";
|
||||
// helpers
|
||||
import { handleIssueQueryParamsByLayout } from "helpers/issue.helper";
|
||||
import { renderDateFormat } from "helpers/date-time.helper";
|
||||
// types
|
||||
import { RootStore } from "./root";
|
||||
import {
|
||||
@ -25,15 +26,15 @@ export interface IIssueFilterStore {
|
||||
filtersSearchQuery: string;
|
||||
|
||||
// action
|
||||
fetchUserProjectFilters: (workspaceSlug: string, projectSlug: string) => Promise<void>;
|
||||
fetchUserProjectFilters: (workspaceSlug: string, projectId: string) => Promise<void>;
|
||||
updateUserFilters: (
|
||||
workspaceSlug: string,
|
||||
projectSlug: string,
|
||||
projectId: string,
|
||||
filterToUpdate: Partial<IProjectViewProps>
|
||||
) => Promise<void>;
|
||||
updateDisplayProperties: (
|
||||
workspaceSlug: string,
|
||||
projectSlug: string,
|
||||
projectId: string,
|
||||
properties: Partial<IIssueDisplayProperties>
|
||||
) => Promise<void>;
|
||||
updateFiltersSearchQuery: (query: string) => void;
|
||||
@ -116,6 +117,22 @@ class IssueFilterStore implements IIssueFilterStore {
|
||||
return computedFilters;
|
||||
};
|
||||
|
||||
calendarLayoutDateRange = () => {
|
||||
const { activeMonthDate, activeWeekDate } = this.rootStore.calendar.calendarFilters;
|
||||
|
||||
const calendarLayout = this.userDisplayFilters.calendar?.layout ?? "month";
|
||||
|
||||
let filterDate = new Date();
|
||||
|
||||
if (calendarLayout === "month") filterDate = activeMonthDate;
|
||||
else filterDate = activeWeekDate;
|
||||
|
||||
const startOfMonth = renderDateFormat(new Date(filterDate.getFullYear(), filterDate.getMonth(), 1));
|
||||
const endOfMonth = renderDateFormat(new Date(filterDate.getFullYear(), filterDate.getMonth() + 1, 0));
|
||||
|
||||
return [`${startOfMonth};after`, `${endOfMonth};before`];
|
||||
};
|
||||
|
||||
get appliedFilters(): TIssueParams[] | null {
|
||||
if (
|
||||
!this.userFilters ||
|
||||
@ -140,10 +157,11 @@ class IssueFilterStore implements IIssueFilterStore {
|
||||
type: this.userDisplayFilters?.type || undefined,
|
||||
sub_issue: this.userDisplayFilters?.sub_issue || true,
|
||||
show_empty_groups: this.userDisplayFilters?.show_empty_groups || true,
|
||||
calendar_date_range: this.userDisplayFilters?.calendar_date_range || undefined,
|
||||
start_target_date: this.userDisplayFilters?.start_target_date || true,
|
||||
};
|
||||
|
||||
if (this.userDisplayFilters.layout === "calendar") filteredRouteParams.target_date = this.calendarLayoutDateRange();
|
||||
|
||||
const filteredParams = handleIssueQueryParamsByLayout(this.userDisplayFilters.layout);
|
||||
if (filteredParams) filteredRouteParams = this.computedFilter(filteredRouteParams, filteredParams);
|
||||
|
||||
|
@ -14,6 +14,7 @@ import ViewStore, { IViewStore } from "./views";
|
||||
import IssueFilterStore, { IIssueFilterStore } from "./issue_filters";
|
||||
import IssueViewDetailStore from "./issue_detail";
|
||||
import IssueKanBanViewStore from "./kanban_view";
|
||||
import CalendarStore, { ICalendarStore } from "./calendar";
|
||||
|
||||
enableStaticRendering(typeof window === "undefined");
|
||||
|
||||
@ -31,6 +32,7 @@ export class RootStore {
|
||||
issueFilter: IIssueFilterStore;
|
||||
issueDetail: IssueViewDetailStore;
|
||||
issueKanBanView: IssueKanBanViewStore;
|
||||
calendar: ICalendarStore;
|
||||
|
||||
constructor() {
|
||||
this.user = new UserStore(this);
|
||||
@ -46,5 +48,6 @@ export class RootStore {
|
||||
this.issueDetail = new IssueViewDetailStore(this);
|
||||
this.issueKanBanView = new IssueKanBanViewStore(this);
|
||||
this.draftIssuesStore = new DraftIssuesStore(this);
|
||||
this.calendar = new CalendarStore(this);
|
||||
}
|
||||
}
|
||||
|
8
web/types/view-props.d.ts
vendored
8
web/types/view-props.d.ts
vendored
@ -45,9 +45,10 @@ export type TIssueParams =
|
||||
| "type"
|
||||
| "sub_issue"
|
||||
| "show_empty_groups"
|
||||
| "calendar_date_range"
|
||||
| "start_target_date";
|
||||
|
||||
export type TCalendarLayouts = "month" | "week";
|
||||
|
||||
export interface IIssueFilterOptions {
|
||||
assignees?: string[] | null;
|
||||
created_by?: string[] | null;
|
||||
@ -61,7 +62,10 @@ export interface IIssueFilterOptions {
|
||||
}
|
||||
|
||||
export interface IIssueDisplayFilterOptions {
|
||||
calendar_date_range?: string;
|
||||
calendar?: {
|
||||
show_weekends?: boolean;
|
||||
layout?: TCalendarLayouts;
|
||||
};
|
||||
group_by?: TIssueGroupByOptions;
|
||||
sub_group_by?: TIssueGroupByOptions;
|
||||
layout?: TIssueLayouts;
|
||||
|
Loading…
Reference in New Issue
Block a user