Skip to content
50 changes: 50 additions & 0 deletions packages/constants/src/profile.ts
Original file line number Diff line number Diff line change
Expand Up @@ -71,3 +71,53 @@ export const PROFILE_ADMINS_TAB = [
selected: "/activity/",
},
];

/**
* @description The start of the week for the user
* @enum {number}
*/
export enum EStartOfTheWeek {
SUNDAY = 0,
MONDAY = 1,
TUESDAY = 2,
WEDNESDAY = 3,
THURSDAY = 4,
FRIDAY = 5,
SATURDAY = 6,
}

/**
* @description The options for the start of the week
* @type {Array<{value: EStartOfTheWeek, label: string}>}
* @constant
*/
export const START_OF_THE_WEEK_OPTIONS = [
{
value: EStartOfTheWeek.SUNDAY,
label: "Sunday",
},
{
value: EStartOfTheWeek.MONDAY,
label: "Monday",
},
{
value: EStartOfTheWeek.TUESDAY,
label: "Tuesday",
},
{
value: EStartOfTheWeek.WEDNESDAY,
label: "Wednesday",
},
{
value: EStartOfTheWeek.THURSDAY,
label: "Thursday",
},
{
value: EStartOfTheWeek.FRIDAY,
label: "Friday",
},
{
value: EStartOfTheWeek.SATURDAY,
label: "Saturday",
},
];
11 changes: 3 additions & 8 deletions packages/types/src/users.d.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { EStartOfTheWeek } from "@plane/constants";
import { IIssueActivity, TIssuePriorities, TStateGroups } from ".";
import { TUserPermissions } from "./enums";

Expand Down Expand Up @@ -64,6 +65,7 @@ export type TUserProfile = {
language: string;
created_at: Date | string;
updated_at: Date | string;
start_of_the_week: EStartOfTheWeek;
};

export interface IInstanceAdminStatus {
Expand Down Expand Up @@ -155,14 +157,7 @@ export interface IUserProfileProjectSegregation {
id: string;
pending_issues: number;
}[];
user_data: Pick<
IUser,
| "avatar_url"
| "cover_image_url"
| "display_name"
| "first_name"
| "last_name"
> & {
user_data: Pick<IUser, "avatar_url" | "cover_image_url" | "display_name" | "first_name" | "last_name"> & {
date_joined: Date;
user_timezone: string;
};
Expand Down
1 change: 1 addition & 0 deletions packages/ui/src/calendar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ export const Calendar = ({ className, classNames, showOutsideDays = true, ...pro
<DayPicker
showOutsideDays={showOutsideDays}
className={cn("p-3", className)}
weekStartsOn={props.weekStartsOn}
// classNames={{
// months: "flex flex-col sm:flex-row space-y-4 sm:space-x-4 sm:space-y-0",
// month: "space-y-4",
Expand Down
2 changes: 2 additions & 0 deletions space/core/store/profile.store.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import set from "lodash/set";
import { action, makeObservable, observable, runInAction } from "mobx";
// plane imports
import { EStartOfTheWeek } from "@plane/constants";
import { UserService } from "@plane/services";
import { TUserProfile } from "@plane/types";
// store
Expand Down Expand Up @@ -54,6 +55,7 @@ export class ProfileStore implements IProfileStore {
created_at: "",
updated_at: "",
language: "",
start_of_the_week: EStartOfTheWeek.SUNDAY,
};

// services
Expand Down
7 changes: 4 additions & 3 deletions web/app/profile/appearance/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,13 +10,13 @@ import { IUserTheme } from "@plane/types";
import { setPromiseToast } from "@plane/ui";
// components
import { LogoSpinner } from "@/components/common";
import { CustomThemeSelector, ThemeSwitch, PageHead } from "@/components/core";
import { ProfileSettingContentHeader, ProfileSettingContentWrapper } from "@/components/profile";
// constants
import { ThemeSwitch, PageHead, CustomThemeSelector } from "@/components/core";
import { ProfileSettingContentHeader, ProfileSettingContentWrapper, StartOfWeekPreference } from "@/components/profile";
// helpers
import { applyTheme, unsetCustomCssVariables } from "@/helpers/theme.helper";
// hooks
import { useUserProfile } from "@/hooks/store";

const ProfileAppearancePage = observer(() => {
const { t } = useTranslation();
const { setTheme } = useTheme();
Expand Down Expand Up @@ -75,6 +75,7 @@ const ProfileAppearancePage = observer(() => {
</div>
</div>
{userProfile?.theme?.theme === "custom" && <CustomThemeSelector applyThemeChange={applyThemeChange} />}
<StartOfWeekPreference />
</ProfileSettingContentWrapper>
) : (
<div className="grid h-full w-full place-items-center px-4 sm:px-0">
Expand Down
11 changes: 9 additions & 2 deletions web/core/components/dropdowns/date.tsx
Original file line number Diff line number Diff line change
@@ -1,15 +1,18 @@
import React, { useRef, useState } from "react";
import { observer } from "mobx-react";
import { Matcher } from "react-day-picker";
import { createPortal } from "react-dom";
import { usePopper } from "react-popper";
import { CalendarDays, X } from "lucide-react";
import { Combobox } from "@headlessui/react";
// ui
import { EStartOfTheWeek } from "@plane/constants";
import { ComboDropDown, Calendar } from "@plane/ui";
// helpers
import { cn } from "@/helpers/common.helper";
import { renderFormattedDate, getDate } from "@/helpers/date-time.helper";
// hooks
import { useUserProfile } from "@/hooks/store";
import { useDropdown } from "@/hooks/use-dropdown";
// components
import { DropdownButton } from "./buttons";
Expand All @@ -33,7 +36,7 @@ type Props = TDropdownProps & {
renderByDefault?: boolean;
};

export const DateDropdown: React.FC<Props> = (props) => {
export const DateDropdown: React.FC<Props> = observer((props) => {
const {
buttonClassName = "",
buttonContainerClassName,
Expand Down Expand Up @@ -62,6 +65,9 @@ export const DateDropdown: React.FC<Props> = (props) => {
const [isOpen, setIsOpen] = useState(false);
// refs
const dropdownRef = useRef<HTMLDivElement | null>(null);
// hooks
const { data } = useUserProfile();
const startOfWeek = data?.start_of_the_week;
// popper-js refs
const [referenceElement, setReferenceElement] = useState<HTMLButtonElement | null>(null);
const [popperElement, setPopperElement] = useState<HTMLDivElement | null>(null);
Expand Down Expand Up @@ -186,11 +192,12 @@ export const DateDropdown: React.FC<Props> = (props) => {
disabled={disabledDays}
mode="single"
fixedWeeks
weekStartsOn={startOfWeek}
/>
</div>
</Combobox.Options>,
document.body
)}
</ComboDropDown>
);
};
});
7 changes: 6 additions & 1 deletion web/core/components/gantt-chart/chart/root.tsx
Original file line number Diff line number Diff line change
@@ -1,10 +1,13 @@
import { FC, useEffect, useState } from "react";
import { observer } from "mobx-react";
// plane imports
import { EStartOfTheWeek } from "@plane/constants";
// components
import { GanttChartHeader, GanttChartMainContent } from "@/components/gantt-chart";
// helpers
import { cn } from "@/helpers/common.helper";
// hooks
import { useUserProfile } from "@/hooks/store";
import { useTimeLineChartStore } from "@/hooks/use-timeline-chart";
//
import { SIDEBAR_WIDTH } from "../constants";
Expand Down Expand Up @@ -87,6 +90,8 @@ export const ChartViewRoot: FC<ChartViewRootProps> = observer((props) => {
updateRenderView,
updateAllBlocksOnChartChangeWhileDragging,
} = useTimeLineChartStore();
const { data } = useUserProfile();
const startOfWeek = data?.start_of_the_week;

const updateCurrentViewRenderPayload = (side: null | "left" | "right", view: TGanttViews, targetDate?: Date) => {
const selectedCurrentView: TGanttViews = view;
Expand All @@ -98,7 +103,7 @@ export const ChartViewRoot: FC<ChartViewRootProps> = observer((props) => {
if (selectedCurrentViewData === undefined) return;

const currentViewHelpers = timelineViewHelpers[selectedCurrentView];
const currentRender = currentViewHelpers.generateChart(selectedCurrentViewData, side, targetDate);
const currentRender = currentViewHelpers.generateChart(selectedCurrentViewData, side, targetDate, startOfWeek);
const mergeRenderPayloads = currentViewHelpers.mergeRenderPayloads as (
a: IWeekBlock[] | IMonthView | IMonthBlock[],
b: IWeekBlock[] | IMonthView | IMonthBlock[]
Expand Down
6 changes: 6 additions & 0 deletions web/core/components/gantt-chart/data/index.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,13 @@
// types
import { EStartOfTheWeek } from "@plane/constants";
import { WeekMonthDataType, ChartDataType, TGanttViews } from "../types";

// constants
export const generateWeeks = (startOfWeek: EStartOfTheWeek = EStartOfTheWeek.SUNDAY): WeekMonthDataType[] => [
...weeks.slice(startOfWeek),
...weeks.slice(0, startOfWeek),
];

export const weeks: WeekMonthDataType[] = [
{ key: 0, shortTitle: "sun", title: "sunday", abbreviation: "Su" },
{ key: 1, shortTitle: "mon", title: "monday", abbreviation: "M" },
Expand Down
33 changes: 22 additions & 11 deletions web/core/components/gantt-chart/views/week-view.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
//
import { weeks, months } from "../data";
import { EStartOfTheWeek } from "@plane/constants";
import { months, generateWeeks } from "../data";
import { ChartDataType } from "../types";
import { getNumberOfDaysBetweenTwoDates, getWeekNumberByDate } from "./helpers";
export interface IDayBlock {
Expand Down Expand Up @@ -38,7 +39,12 @@ export interface IWeekBlock {
* @param side
* @returns
*/
const generateWeekChart = (weekPayload: ChartDataType, side: null | "left" | "right", targetDate?: Date) => {
const generateWeekChart = (
weekPayload: ChartDataType,
side: null | "left" | "right",
targetDate?: Date,
startOfWeek: EStartOfTheWeek = EStartOfTheWeek.SUNDAY
) => {
let renderState = weekPayload;

const range: number = renderState.data.approxFilterRange || 6;
Expand All @@ -56,7 +62,7 @@ const generateWeekChart = (weekPayload: ChartDataType, side: null | "left" | "ri
minusDate = new Date(currentDate.getFullYear(), currentDate.getMonth() - range, currentDate.getDate());
plusDate = new Date(currentDate.getFullYear(), currentDate.getMonth() + range, currentDate.getDate());

if (minusDate && plusDate) filteredDates = getWeeksBetweenTwoDates(minusDate, plusDate);
if (minusDate && plusDate) filteredDates = getWeeksBetweenTwoDates(minusDate, plusDate, true, startOfWeek);

startDate = filteredDates[0].startDate;
endDate = filteredDates[filteredDates.length - 1].endDate;
Expand All @@ -77,7 +83,7 @@ const generateWeekChart = (weekPayload: ChartDataType, side: null | "left" | "ri
minusDate = new Date(currentDate.getFullYear(), currentDate.getMonth() - range, 1);
plusDate = new Date(chartStartDate.getFullYear(), chartStartDate.getMonth(), chartStartDate.getDate() - 1);

if (minusDate && plusDate) filteredDates = getWeeksBetweenTwoDates(minusDate, plusDate);
if (minusDate && plusDate) filteredDates = getWeeksBetweenTwoDates(minusDate, plusDate, true, startOfWeek);

startDate = filteredDates[0].startDate;
endDate = new Date(chartStartDate.getFullYear(), chartStartDate.getMonth(), chartStartDate.getDate() - 1);
Expand All @@ -94,7 +100,7 @@ const generateWeekChart = (weekPayload: ChartDataType, side: null | "left" | "ri
minusDate = new Date(chartEndDate.getFullYear(), chartEndDate.getMonth(), chartEndDate.getDate() + 1);
plusDate = new Date(currentDate.getFullYear(), currentDate.getMonth() + range, 1);

if (minusDate && plusDate) filteredDates = getWeeksBetweenTwoDates(minusDate, plusDate);
if (minusDate && plusDate) filteredDates = getWeeksBetweenTwoDates(minusDate, plusDate, true, startOfWeek);

startDate = new Date(chartEndDate.getFullYear(), chartEndDate.getMonth(), chartEndDate.getDate() + 1);
endDate = filteredDates[filteredDates.length - 1].endDate;
Expand All @@ -120,14 +126,18 @@ const generateWeekChart = (weekPayload: ChartDataType, side: null | "left" | "ri
export const getWeeksBetweenTwoDates = (
startDate: Date,
endDate: Date,
shouldPopulateDaysForWeek: boolean = true
shouldPopulateDaysForWeek: boolean = true,
startOfWeek: EStartOfTheWeek = EStartOfTheWeek.SUNDAY
): IWeekBlock[] => {
const weeks: IWeekBlock[] = [];

const currentDate = new Date(startDate.getTime());
const today = new Date();

currentDate.setDate(currentDate.getDate() - currentDate.getDay());
// Adjust the current date to the start of the week
const day = currentDate.getDay();
const diff = (day + 7 - startOfWeek) % 7; // Calculate days to subtract to get to startOfWeek
currentDate.setDate(currentDate.getDate() - diff);

while (currentDate <= endDate) {
const weekStartDate = new Date(currentDate.getTime());
Expand All @@ -141,7 +151,7 @@ export const getWeeksBetweenTwoDates = (
const weekNumber = getWeekNumberByDate(currentDate);

weeks.push({
children: shouldPopulateDaysForWeek ? populateDaysForWeek(weekStartDate) : undefined,
children: shouldPopulateDaysForWeek ? populateDaysForWeek(weekStartDate, startOfWeek) : undefined,
weekNumber,
weekData: {
shortTitle: `w${weekNumber}`,
Expand Down Expand Up @@ -171,17 +181,18 @@ export const getWeeksBetweenTwoDates = (
* @param startDate
* @returns
*/
const populateDaysForWeek = (startDate: Date): IDayBlock[] => {
const populateDaysForWeek = (startDate: Date, startOfWeek: EStartOfTheWeek = EStartOfTheWeek.SUNDAY): IDayBlock[] => {
const currentDate = new Date(startDate);
const days: IDayBlock[] = [];
const today = new Date();
const weekDays = generateWeeks(startOfWeek);

for (let i = 0; i < 7; i++) {
days.push({
date: new Date(currentDate),
day: currentDate.getDay(),
dayData: weeks[currentDate.getDay()],
title: `${weeks[currentDate.getDay()].abbreviation} ${currentDate.getDate()}`,
dayData: weekDays[i],
title: `${weekDays[i].abbreviation} ${currentDate.getDate()}`,
today: today.setHours(0, 0, 0, 0) == currentDate.setHours(0, 0, 0, 0),
});
currentDate.setDate(currentDate.getDate() + 1);
Expand Down
28 changes: 23 additions & 5 deletions web/core/components/issues/issue-layouts/calendar/week-days.tsx
Original file line number Diff line number Diff line change
@@ -1,9 +1,14 @@
import { observer } from "mobx-react";
import { EStartOfTheWeek } from "@plane/constants";
import { TGroupedIssues, TIssue, TIssueMap, TPaginationData } from "@plane/types";
import { cn } from "@plane/utils";
// components
import { CalendarDayTile } from "@/components/issues";
// helpers
import { getOrderedDays } from "@/helpers/calendar.helper";
import { renderFormattedPayloadDate } from "@/helpers/date-time.helper";
// hooks
import { useUserProfile } from "@/hooks/store";
// types
import { IProjectEpicsFilter } from "@/plane-web/store/issue/epic";
import { ICycleIssuesFilter } from "@/store/issue/cycle";
Expand Down Expand Up @@ -65,20 +70,33 @@ export const CalendarWeekDays: React.FC<Props> = observer((props) => {
canEditProperties,
isEpic = false,
} = props;
// hooks
const { data } = useUserProfile();
const startOfWeek = data?.start_of_the_week;

const calendarLayout = issuesFilterStore?.issueFilters?.displayFilters?.calendar?.layout ?? "month";
const showWeekends = issuesFilterStore?.issueFilters?.displayFilters?.calendar?.show_weekends ?? false;

if (!week) return null;

const shouldShowDay = (dayDate: Date) => {
if (showWeekends) return true;
const day = dayDate.getDay();
return !(day === 0 || day === 6);
};

const sortedWeekDays = getOrderedDays(Object.values(week), (item) => item.date.getDay(), startOfWeek);

return (
<div
className={`grid divide-custom-border-200 md:divide-x-[0.5px] ${showWeekends ? "grid-cols-7" : "grid-cols-5"} ${
calendarLayout === "month" ? "" : "h-full"
}`}
className={cn("grid divide-custom-border-200 md:divide-x-[0.5px]", {
"grid-cols-7": showWeekends,
"grid-cols-5": !showWeekends,
"h-full": calendarLayout !== "month",
})}
>
{Object.values(week).map((date: ICalendarDate) => {
if (!showWeekends && (date.date.getDay() === 0 || date.date.getDay() === 6)) return null;
{sortedWeekDays.map((date: ICalendarDate) => {
if (!shouldShowDay(date.date)) return null;

return (
<CalendarDayTile
Expand Down
Loading