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:
Aaryan Khandelwal 2023-09-28 15:16:24 +05:30 committed by GitHub
parent b2d17e6ec9
commit 3bf590b67e
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
34 changed files with 1042 additions and 464 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1 +0,0 @@
export * from "./root";

View File

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

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

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

View File

@ -0,0 +1,2 @@
export * from "./months-dropdown";
export * from "./options-dropdown";

View File

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

View File

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

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

View 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";

View File

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

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

View 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;
}

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

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

View File

@ -1,2 +1,2 @@
export * from "./calendar";
export * from "./kanban";
export * from "./calandar";

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -125,7 +125,6 @@ export const handleIssueQueryParamsByLayout = (_layout: TIssueLayouts | undefine
"start_date",
"target_date",
"type",
"calendar_date_range",
];
if (_layout === "spreadsheet")
return [

View File

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

View File

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

View File

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

View File

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

View File

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