[WEB-3523] feat: start of week preference (#7033)
* chore: startOfWeek constant and types updated * chore: startOfWeek updated in profile store * chore: StartOfWeekPreference added to profile appearance settings * chore: calendar layout startOfWeek implementation * chore: date picker startOfWeek implementation * chore: gantt layout startOfWeek implementation * chore: code refactor * chore: code refactor * chore: code refactor
This commit is contained in:
parent
dc16f2862e
commit
8613a80b16
17 changed files with 251 additions and 36 deletions
|
|
@ -71,3 +71,53 @@ export const PROFILE_ADMINS_TAB = [
|
||||||
selected: "/activity/",
|
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
packages/types/src/users.d.ts
vendored
11
packages/types/src/users.d.ts
vendored
|
|
@ -1,3 +1,4 @@
|
||||||
|
import { EStartOfTheWeek } from "@plane/constants";
|
||||||
import { IIssueActivity, TIssuePriorities, TStateGroups } from ".";
|
import { IIssueActivity, TIssuePriorities, TStateGroups } from ".";
|
||||||
import { TUserPermissions } from "./enums";
|
import { TUserPermissions } from "./enums";
|
||||||
|
|
||||||
|
|
@ -64,6 +65,7 @@ export type TUserProfile = {
|
||||||
language: string;
|
language: string;
|
||||||
created_at: Date | string;
|
created_at: Date | string;
|
||||||
updated_at: Date | string;
|
updated_at: Date | string;
|
||||||
|
start_of_the_week: EStartOfTheWeek;
|
||||||
};
|
};
|
||||||
|
|
||||||
export interface IInstanceAdminStatus {
|
export interface IInstanceAdminStatus {
|
||||||
|
|
@ -155,14 +157,7 @@ export interface IUserProfileProjectSegregation {
|
||||||
id: string;
|
id: string;
|
||||||
pending_issues: number;
|
pending_issues: number;
|
||||||
}[];
|
}[];
|
||||||
user_data: Pick<
|
user_data: Pick<IUser, "avatar_url" | "cover_image_url" | "display_name" | "first_name" | "last_name"> & {
|
||||||
IUser,
|
|
||||||
| "avatar_url"
|
|
||||||
| "cover_image_url"
|
|
||||||
| "display_name"
|
|
||||||
| "first_name"
|
|
||||||
| "last_name"
|
|
||||||
> & {
|
|
||||||
date_joined: Date;
|
date_joined: Date;
|
||||||
user_timezone: string;
|
user_timezone: string;
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -17,6 +17,7 @@ export const Calendar = ({ className, classNames, showOutsideDays = true, ...pro
|
||||||
<DayPicker
|
<DayPicker
|
||||||
showOutsideDays={showOutsideDays}
|
showOutsideDays={showOutsideDays}
|
||||||
className={cn("p-3", className)}
|
className={cn("p-3", className)}
|
||||||
|
weekStartsOn={props.weekStartsOn}
|
||||||
// classNames={{
|
// classNames={{
|
||||||
// months: "flex flex-col sm:flex-row space-y-4 sm:space-x-4 sm:space-y-0",
|
// months: "flex flex-col sm:flex-row space-y-4 sm:space-x-4 sm:space-y-0",
|
||||||
// month: "space-y-4",
|
// month: "space-y-4",
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,7 @@
|
||||||
import set from "lodash/set";
|
import set from "lodash/set";
|
||||||
import { action, makeObservable, observable, runInAction } from "mobx";
|
import { action, makeObservable, observable, runInAction } from "mobx";
|
||||||
// plane imports
|
// plane imports
|
||||||
|
import { EStartOfTheWeek } from "@plane/constants";
|
||||||
import { UserService } from "@plane/services";
|
import { UserService } from "@plane/services";
|
||||||
import { TUserProfile } from "@plane/types";
|
import { TUserProfile } from "@plane/types";
|
||||||
// store
|
// store
|
||||||
|
|
@ -54,6 +55,7 @@ export class ProfileStore implements IProfileStore {
|
||||||
created_at: "",
|
created_at: "",
|
||||||
updated_at: "",
|
updated_at: "",
|
||||||
language: "",
|
language: "",
|
||||||
|
start_of_the_week: EStartOfTheWeek.SUNDAY,
|
||||||
};
|
};
|
||||||
|
|
||||||
// services
|
// services
|
||||||
|
|
|
||||||
|
|
@ -10,13 +10,13 @@ import { IUserTheme } from "@plane/types";
|
||||||
import { setPromiseToast } from "@plane/ui";
|
import { setPromiseToast } from "@plane/ui";
|
||||||
// components
|
// components
|
||||||
import { LogoSpinner } from "@/components/common";
|
import { LogoSpinner } from "@/components/common";
|
||||||
import { CustomThemeSelector, ThemeSwitch, PageHead } from "@/components/core";
|
import { ThemeSwitch, PageHead, CustomThemeSelector } from "@/components/core";
|
||||||
import { ProfileSettingContentHeader, ProfileSettingContentWrapper } from "@/components/profile";
|
import { ProfileSettingContentHeader, ProfileSettingContentWrapper, StartOfWeekPreference } from "@/components/profile";
|
||||||
// constants
|
|
||||||
// helpers
|
// helpers
|
||||||
import { applyTheme, unsetCustomCssVariables } from "@/helpers/theme.helper";
|
import { applyTheme, unsetCustomCssVariables } from "@/helpers/theme.helper";
|
||||||
// hooks
|
// hooks
|
||||||
import { useUserProfile } from "@/hooks/store";
|
import { useUserProfile } from "@/hooks/store";
|
||||||
|
|
||||||
const ProfileAppearancePage = observer(() => {
|
const ProfileAppearancePage = observer(() => {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const { setTheme } = useTheme();
|
const { setTheme } = useTheme();
|
||||||
|
|
@ -75,6 +75,7 @@ const ProfileAppearancePage = observer(() => {
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{userProfile?.theme?.theme === "custom" && <CustomThemeSelector applyThemeChange={applyThemeChange} />}
|
{userProfile?.theme?.theme === "custom" && <CustomThemeSelector applyThemeChange={applyThemeChange} />}
|
||||||
|
<StartOfWeekPreference />
|
||||||
</ProfileSettingContentWrapper>
|
</ProfileSettingContentWrapper>
|
||||||
) : (
|
) : (
|
||||||
<div className="grid h-full w-full place-items-center px-4 sm:px-0">
|
<div className="grid h-full w-full place-items-center px-4 sm:px-0">
|
||||||
|
|
|
||||||
|
|
@ -1,15 +1,18 @@
|
||||||
import React, { useRef, useState } from "react";
|
import React, { useRef, useState } from "react";
|
||||||
|
import { observer } from "mobx-react";
|
||||||
import { Matcher } from "react-day-picker";
|
import { Matcher } from "react-day-picker";
|
||||||
import { createPortal } from "react-dom";
|
import { createPortal } from "react-dom";
|
||||||
import { usePopper } from "react-popper";
|
import { usePopper } from "react-popper";
|
||||||
import { CalendarDays, X } from "lucide-react";
|
import { CalendarDays, X } from "lucide-react";
|
||||||
import { Combobox } from "@headlessui/react";
|
import { Combobox } from "@headlessui/react";
|
||||||
// ui
|
// ui
|
||||||
|
import { EStartOfTheWeek } from "@plane/constants";
|
||||||
import { ComboDropDown, Calendar } from "@plane/ui";
|
import { ComboDropDown, Calendar } from "@plane/ui";
|
||||||
// helpers
|
// helpers
|
||||||
import { cn } from "@/helpers/common.helper";
|
import { cn } from "@/helpers/common.helper";
|
||||||
import { renderFormattedDate, getDate } from "@/helpers/date-time.helper";
|
import { renderFormattedDate, getDate } from "@/helpers/date-time.helper";
|
||||||
// hooks
|
// hooks
|
||||||
|
import { useUserProfile } from "@/hooks/store";
|
||||||
import { useDropdown } from "@/hooks/use-dropdown";
|
import { useDropdown } from "@/hooks/use-dropdown";
|
||||||
// components
|
// components
|
||||||
import { DropdownButton } from "./buttons";
|
import { DropdownButton } from "./buttons";
|
||||||
|
|
@ -33,7 +36,7 @@ type Props = TDropdownProps & {
|
||||||
renderByDefault?: boolean;
|
renderByDefault?: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const DateDropdown: React.FC<Props> = (props) => {
|
export const DateDropdown: React.FC<Props> = observer((props) => {
|
||||||
const {
|
const {
|
||||||
buttonClassName = "",
|
buttonClassName = "",
|
||||||
buttonContainerClassName,
|
buttonContainerClassName,
|
||||||
|
|
@ -62,6 +65,9 @@ export const DateDropdown: React.FC<Props> = (props) => {
|
||||||
const [isOpen, setIsOpen] = useState(false);
|
const [isOpen, setIsOpen] = useState(false);
|
||||||
// refs
|
// refs
|
||||||
const dropdownRef = useRef<HTMLDivElement | null>(null);
|
const dropdownRef = useRef<HTMLDivElement | null>(null);
|
||||||
|
// hooks
|
||||||
|
const { data } = useUserProfile();
|
||||||
|
const startOfWeek = data?.start_of_the_week;
|
||||||
// popper-js refs
|
// popper-js refs
|
||||||
const [referenceElement, setReferenceElement] = useState<HTMLButtonElement | null>(null);
|
const [referenceElement, setReferenceElement] = useState<HTMLButtonElement | null>(null);
|
||||||
const [popperElement, setPopperElement] = useState<HTMLDivElement | null>(null);
|
const [popperElement, setPopperElement] = useState<HTMLDivElement | null>(null);
|
||||||
|
|
@ -186,6 +192,7 @@ export const DateDropdown: React.FC<Props> = (props) => {
|
||||||
disabled={disabledDays}
|
disabled={disabledDays}
|
||||||
mode="single"
|
mode="single"
|
||||||
fixedWeeks
|
fixedWeeks
|
||||||
|
weekStartsOn={startOfWeek}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</Combobox.Options>,
|
</Combobox.Options>,
|
||||||
|
|
@ -193,4 +200,4 @@ export const DateDropdown: React.FC<Props> = (props) => {
|
||||||
)}
|
)}
|
||||||
</ComboDropDown>
|
</ComboDropDown>
|
||||||
);
|
);
|
||||||
};
|
});
|
||||||
|
|
|
||||||
|
|
@ -1,10 +1,13 @@
|
||||||
import { FC, useEffect, useState } from "react";
|
import { FC, useEffect, useState } from "react";
|
||||||
import { observer } from "mobx-react";
|
import { observer } from "mobx-react";
|
||||||
|
// plane imports
|
||||||
|
import { EStartOfTheWeek } from "@plane/constants";
|
||||||
// components
|
// components
|
||||||
import { GanttChartHeader, GanttChartMainContent } from "@/components/gantt-chart";
|
import { GanttChartHeader, GanttChartMainContent } from "@/components/gantt-chart";
|
||||||
// helpers
|
// helpers
|
||||||
import { cn } from "@/helpers/common.helper";
|
import { cn } from "@/helpers/common.helper";
|
||||||
// hooks
|
// hooks
|
||||||
|
import { useUserProfile } from "@/hooks/store";
|
||||||
import { useTimeLineChartStore } from "@/hooks/use-timeline-chart";
|
import { useTimeLineChartStore } from "@/hooks/use-timeline-chart";
|
||||||
//
|
//
|
||||||
import { SIDEBAR_WIDTH } from "../constants";
|
import { SIDEBAR_WIDTH } from "../constants";
|
||||||
|
|
@ -87,6 +90,8 @@ export const ChartViewRoot: FC<ChartViewRootProps> = observer((props) => {
|
||||||
updateRenderView,
|
updateRenderView,
|
||||||
updateAllBlocksOnChartChangeWhileDragging,
|
updateAllBlocksOnChartChangeWhileDragging,
|
||||||
} = useTimeLineChartStore();
|
} = useTimeLineChartStore();
|
||||||
|
const { data } = useUserProfile();
|
||||||
|
const startOfWeek = data?.start_of_the_week;
|
||||||
|
|
||||||
const updateCurrentViewRenderPayload = (side: null | "left" | "right", view: TGanttViews, targetDate?: Date) => {
|
const updateCurrentViewRenderPayload = (side: null | "left" | "right", view: TGanttViews, targetDate?: Date) => {
|
||||||
const selectedCurrentView: TGanttViews = view;
|
const selectedCurrentView: TGanttViews = view;
|
||||||
|
|
@ -98,7 +103,7 @@ export const ChartViewRoot: FC<ChartViewRootProps> = observer((props) => {
|
||||||
if (selectedCurrentViewData === undefined) return;
|
if (selectedCurrentViewData === undefined) return;
|
||||||
|
|
||||||
const currentViewHelpers = timelineViewHelpers[selectedCurrentView];
|
const currentViewHelpers = timelineViewHelpers[selectedCurrentView];
|
||||||
const currentRender = currentViewHelpers.generateChart(selectedCurrentViewData, side, targetDate);
|
const currentRender = currentViewHelpers.generateChart(selectedCurrentViewData, side, targetDate, startOfWeek);
|
||||||
const mergeRenderPayloads = currentViewHelpers.mergeRenderPayloads as (
|
const mergeRenderPayloads = currentViewHelpers.mergeRenderPayloads as (
|
||||||
a: IWeekBlock[] | IMonthView | IMonthBlock[],
|
a: IWeekBlock[] | IMonthView | IMonthBlock[],
|
||||||
b: IWeekBlock[] | IMonthView | IMonthBlock[]
|
b: IWeekBlock[] | IMonthView | IMonthBlock[]
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,13 @@
|
||||||
// types
|
// types
|
||||||
|
import { EStartOfTheWeek } from "@plane/constants";
|
||||||
import { WeekMonthDataType, ChartDataType, TGanttViews } from "../types";
|
import { WeekMonthDataType, ChartDataType, TGanttViews } from "../types";
|
||||||
|
|
||||||
// constants
|
// constants
|
||||||
|
export const generateWeeks = (startOfWeek: EStartOfTheWeek = EStartOfTheWeek.SUNDAY): WeekMonthDataType[] => [
|
||||||
|
...weeks.slice(startOfWeek),
|
||||||
|
...weeks.slice(0, startOfWeek),
|
||||||
|
];
|
||||||
|
|
||||||
export const weeks: WeekMonthDataType[] = [
|
export const weeks: WeekMonthDataType[] = [
|
||||||
{ key: 0, shortTitle: "sun", title: "sunday", abbreviation: "Su" },
|
{ key: 0, shortTitle: "sun", title: "sunday", abbreviation: "Su" },
|
||||||
{ key: 1, shortTitle: "mon", title: "monday", abbreviation: "M" },
|
{ key: 1, shortTitle: "mon", title: "monday", abbreviation: "M" },
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,6 @@
|
||||||
//
|
//
|
||||||
import { weeks, months } from "../data";
|
import { EStartOfTheWeek } from "@plane/constants";
|
||||||
|
import { months, generateWeeks } from "../data";
|
||||||
import { ChartDataType } from "../types";
|
import { ChartDataType } from "../types";
|
||||||
import { getNumberOfDaysBetweenTwoDates, getWeekNumberByDate } from "./helpers";
|
import { getNumberOfDaysBetweenTwoDates, getWeekNumberByDate } from "./helpers";
|
||||||
export interface IDayBlock {
|
export interface IDayBlock {
|
||||||
|
|
@ -38,7 +39,12 @@ export interface IWeekBlock {
|
||||||
* @param side
|
* @param side
|
||||||
* @returns
|
* @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;
|
let renderState = weekPayload;
|
||||||
|
|
||||||
const range: number = renderState.data.approxFilterRange || 6;
|
const range: number = renderState.data.approxFilterRange || 6;
|
||||||
|
|
@ -56,7 +62,7 @@ const generateWeekChart = (weekPayload: ChartDataType, side: null | "left" | "ri
|
||||||
minusDate = new Date(currentDate.getFullYear(), currentDate.getMonth() - range, currentDate.getDate());
|
minusDate = new Date(currentDate.getFullYear(), currentDate.getMonth() - range, currentDate.getDate());
|
||||||
plusDate = 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;
|
startDate = filteredDates[0].startDate;
|
||||||
endDate = filteredDates[filteredDates.length - 1].endDate;
|
endDate = filteredDates[filteredDates.length - 1].endDate;
|
||||||
|
|
@ -77,7 +83,7 @@ const generateWeekChart = (weekPayload: ChartDataType, side: null | "left" | "ri
|
||||||
minusDate = new Date(currentDate.getFullYear(), currentDate.getMonth() - range, 1);
|
minusDate = new Date(currentDate.getFullYear(), currentDate.getMonth() - range, 1);
|
||||||
plusDate = new Date(chartStartDate.getFullYear(), chartStartDate.getMonth(), chartStartDate.getDate() - 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;
|
startDate = filteredDates[0].startDate;
|
||||||
endDate = new Date(chartStartDate.getFullYear(), chartStartDate.getMonth(), chartStartDate.getDate() - 1);
|
endDate = new Date(chartStartDate.getFullYear(), chartStartDate.getMonth(), chartStartDate.getDate() - 1);
|
||||||
|
|
@ -94,7 +100,7 @@ const generateWeekChart = (weekPayload: ChartDataType, side: null | "left" | "ri
|
||||||
minusDate = new Date(chartEndDate.getFullYear(), chartEndDate.getMonth(), chartEndDate.getDate() + 1);
|
minusDate = new Date(chartEndDate.getFullYear(), chartEndDate.getMonth(), chartEndDate.getDate() + 1);
|
||||||
plusDate = new Date(currentDate.getFullYear(), currentDate.getMonth() + range, 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);
|
startDate = new Date(chartEndDate.getFullYear(), chartEndDate.getMonth(), chartEndDate.getDate() + 1);
|
||||||
endDate = filteredDates[filteredDates.length - 1].endDate;
|
endDate = filteredDates[filteredDates.length - 1].endDate;
|
||||||
|
|
@ -120,14 +126,18 @@ const generateWeekChart = (weekPayload: ChartDataType, side: null | "left" | "ri
|
||||||
export const getWeeksBetweenTwoDates = (
|
export const getWeeksBetweenTwoDates = (
|
||||||
startDate: Date,
|
startDate: Date,
|
||||||
endDate: Date,
|
endDate: Date,
|
||||||
shouldPopulateDaysForWeek: boolean = true
|
shouldPopulateDaysForWeek: boolean = true,
|
||||||
|
startOfWeek: EStartOfTheWeek = EStartOfTheWeek.SUNDAY
|
||||||
): IWeekBlock[] => {
|
): IWeekBlock[] => {
|
||||||
const weeks: IWeekBlock[] = [];
|
const weeks: IWeekBlock[] = [];
|
||||||
|
|
||||||
const currentDate = new Date(startDate.getTime());
|
const currentDate = new Date(startDate.getTime());
|
||||||
const today = new Date();
|
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) {
|
while (currentDate <= endDate) {
|
||||||
const weekStartDate = new Date(currentDate.getTime());
|
const weekStartDate = new Date(currentDate.getTime());
|
||||||
|
|
@ -141,7 +151,7 @@ export const getWeeksBetweenTwoDates = (
|
||||||
const weekNumber = getWeekNumberByDate(currentDate);
|
const weekNumber = getWeekNumberByDate(currentDate);
|
||||||
|
|
||||||
weeks.push({
|
weeks.push({
|
||||||
children: shouldPopulateDaysForWeek ? populateDaysForWeek(weekStartDate) : undefined,
|
children: shouldPopulateDaysForWeek ? populateDaysForWeek(weekStartDate, startOfWeek) : undefined,
|
||||||
weekNumber,
|
weekNumber,
|
||||||
weekData: {
|
weekData: {
|
||||||
shortTitle: `w${weekNumber}`,
|
shortTitle: `w${weekNumber}`,
|
||||||
|
|
@ -171,17 +181,18 @@ export const getWeeksBetweenTwoDates = (
|
||||||
* @param startDate
|
* @param startDate
|
||||||
* @returns
|
* @returns
|
||||||
*/
|
*/
|
||||||
const populateDaysForWeek = (startDate: Date): IDayBlock[] => {
|
const populateDaysForWeek = (startDate: Date, startOfWeek: EStartOfTheWeek = EStartOfTheWeek.SUNDAY): IDayBlock[] => {
|
||||||
const currentDate = new Date(startDate);
|
const currentDate = new Date(startDate);
|
||||||
const days: IDayBlock[] = [];
|
const days: IDayBlock[] = [];
|
||||||
const today = new Date();
|
const today = new Date();
|
||||||
|
const weekDays = generateWeeks(startOfWeek);
|
||||||
|
|
||||||
for (let i = 0; i < 7; i++) {
|
for (let i = 0; i < 7; i++) {
|
||||||
days.push({
|
days.push({
|
||||||
date: new Date(currentDate),
|
date: new Date(currentDate),
|
||||||
day: currentDate.getDay(),
|
day: currentDate.getDay(),
|
||||||
dayData: weeks[currentDate.getDay()],
|
dayData: weekDays[i],
|
||||||
title: `${weeks[currentDate.getDay()].abbreviation} ${currentDate.getDate()}`,
|
title: `${weekDays[i].abbreviation} ${currentDate.getDate()}`,
|
||||||
today: today.setHours(0, 0, 0, 0) == currentDate.setHours(0, 0, 0, 0),
|
today: today.setHours(0, 0, 0, 0) == currentDate.setHours(0, 0, 0, 0),
|
||||||
});
|
});
|
||||||
currentDate.setDate(currentDate.getDate() + 1);
|
currentDate.setDate(currentDate.getDate() + 1);
|
||||||
|
|
|
||||||
|
|
@ -1,9 +1,14 @@
|
||||||
import { observer } from "mobx-react";
|
import { observer } from "mobx-react";
|
||||||
|
import { EStartOfTheWeek } from "@plane/constants";
|
||||||
import { TGroupedIssues, TIssue, TIssueMap, TPaginationData } from "@plane/types";
|
import { TGroupedIssues, TIssue, TIssueMap, TPaginationData } from "@plane/types";
|
||||||
|
import { cn } from "@plane/utils";
|
||||||
// components
|
// components
|
||||||
import { CalendarDayTile } from "@/components/issues";
|
import { CalendarDayTile } from "@/components/issues";
|
||||||
// helpers
|
// helpers
|
||||||
|
import { getOrderedDays } from "@/helpers/calendar.helper";
|
||||||
import { renderFormattedPayloadDate } from "@/helpers/date-time.helper";
|
import { renderFormattedPayloadDate } from "@/helpers/date-time.helper";
|
||||||
|
// hooks
|
||||||
|
import { useUserProfile } from "@/hooks/store";
|
||||||
// types
|
// types
|
||||||
import { IProjectEpicsFilter } from "@/plane-web/store/issue/epic";
|
import { IProjectEpicsFilter } from "@/plane-web/store/issue/epic";
|
||||||
import { ICycleIssuesFilter } from "@/store/issue/cycle";
|
import { ICycleIssuesFilter } from "@/store/issue/cycle";
|
||||||
|
|
@ -65,20 +70,33 @@ export const CalendarWeekDays: React.FC<Props> = observer((props) => {
|
||||||
canEditProperties,
|
canEditProperties,
|
||||||
isEpic = false,
|
isEpic = false,
|
||||||
} = props;
|
} = props;
|
||||||
|
// hooks
|
||||||
|
const { data } = useUserProfile();
|
||||||
|
const startOfWeek = data?.start_of_the_week;
|
||||||
|
|
||||||
const calendarLayout = issuesFilterStore?.issueFilters?.displayFilters?.calendar?.layout ?? "month";
|
const calendarLayout = issuesFilterStore?.issueFilters?.displayFilters?.calendar?.layout ?? "month";
|
||||||
const showWeekends = issuesFilterStore?.issueFilters?.displayFilters?.calendar?.show_weekends ?? false;
|
const showWeekends = issuesFilterStore?.issueFilters?.displayFilters?.calendar?.show_weekends ?? false;
|
||||||
|
|
||||||
if (!week) return null;
|
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 (
|
return (
|
||||||
<div
|
<div
|
||||||
className={`grid divide-custom-border-200 md:divide-x-[0.5px] ${showWeekends ? "grid-cols-7" : "grid-cols-5"} ${
|
className={cn("grid divide-custom-border-200 md:divide-x-[0.5px]", {
|
||||||
calendarLayout === "month" ? "" : "h-full"
|
"grid-cols-7": showWeekends,
|
||||||
}`}
|
"grid-cols-5": !showWeekends,
|
||||||
|
"h-full": calendarLayout !== "month",
|
||||||
|
})}
|
||||||
>
|
>
|
||||||
{Object.values(week).map((date: ICalendarDate) => {
|
{sortedWeekDays.map((date: ICalendarDate) => {
|
||||||
if (!showWeekends && (date.date.getDay() === 0 || date.date.getDay() === 6)) return null;
|
if (!shouldShowDay(date.date)) return null;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<CalendarDayTile
|
<CalendarDayTile
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,10 @@
|
||||||
import { observer } from "mobx-react";
|
import { observer } from "mobx-react";
|
||||||
|
import { EStartOfTheWeek } from "@plane/constants";
|
||||||
// constants
|
|
||||||
import { DAYS_LIST } from "@/constants/calendar";
|
import { DAYS_LIST } from "@/constants/calendar";
|
||||||
|
// helpers
|
||||||
|
import { getOrderedDays } from "@/helpers/calendar.helper";
|
||||||
|
// hooks
|
||||||
|
import { useUserProfile } from "@/hooks/store";
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
isLoading: boolean;
|
isLoading: boolean;
|
||||||
|
|
@ -10,6 +13,12 @@ type Props = {
|
||||||
|
|
||||||
export const CalendarWeekHeader: React.FC<Props> = observer((props) => {
|
export const CalendarWeekHeader: React.FC<Props> = observer((props) => {
|
||||||
const { isLoading, showWeekends } = props;
|
const { isLoading, showWeekends } = props;
|
||||||
|
// hooks
|
||||||
|
const { data } = useUserProfile();
|
||||||
|
const startOfWeek = data?.start_of_the_week;
|
||||||
|
|
||||||
|
// derived
|
||||||
|
const orderedDays = getOrderedDays(Object.values(DAYS_LIST), (item) => item.value, startOfWeek);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
|
|
@ -20,8 +29,9 @@ export const CalendarWeekHeader: React.FC<Props> = observer((props) => {
|
||||||
{isLoading && (
|
{isLoading && (
|
||||||
<div className="absolute h-[1.5px] w-3/4 animate-[bar-loader_2s_linear_infinite] bg-custom-primary-100" />
|
<div className="absolute h-[1.5px] w-3/4 animate-[bar-loader_2s_linear_infinite] bg-custom-primary-100" />
|
||||||
)}
|
)}
|
||||||
{Object.values(DAYS_LIST).map((day) => {
|
{orderedDays.map((day) => {
|
||||||
if (!showWeekends && (day.shortTitle === "Sat" || day.shortTitle === "Sun")) return null;
|
if (!showWeekends && (day.value === EStartOfTheWeek.SUNDAY || day.value === EStartOfTheWeek.SATURDAY))
|
||||||
|
return null;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
|
|
|
||||||
|
|
@ -6,3 +6,4 @@ export * from "./time";
|
||||||
export * from "./profile-setting-content-wrapper";
|
export * from "./profile-setting-content-wrapper";
|
||||||
export * from "./profile-setting-content-header";
|
export * from "./profile-setting-content-header";
|
||||||
export * from "./form";
|
export * from "./form";
|
||||||
|
export * from "./start-of-week-preference";
|
||||||
|
|
|
||||||
55
web/core/components/profile/start-of-week-preference.tsx
Normal file
55
web/core/components/profile/start-of-week-preference.tsx
Normal file
|
|
@ -0,0 +1,55 @@
|
||||||
|
"use client";
|
||||||
|
|
||||||
|
import React from "react";
|
||||||
|
import { observer } from "mobx-react";
|
||||||
|
// plane imports
|
||||||
|
import { EStartOfTheWeek, START_OF_THE_WEEK_OPTIONS } from "@plane/constants";
|
||||||
|
import { CustomSelect, setToast, TOAST_TYPE } from "@plane/ui";
|
||||||
|
// hooks
|
||||||
|
import { useUserProfile } from "@/hooks/store";
|
||||||
|
|
||||||
|
const getStartOfWeekLabel = (startOfWeek: EStartOfTheWeek) =>
|
||||||
|
START_OF_THE_WEEK_OPTIONS.find((option) => option.value === startOfWeek)?.label;
|
||||||
|
|
||||||
|
export const StartOfWeekPreference = observer(() => {
|
||||||
|
// hooks
|
||||||
|
const { data: userProfile, updateUserProfile } = useUserProfile();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="grid grid-cols-12 gap-4 py-6 sm:gap-16">
|
||||||
|
<div className="col-span-12 sm:col-span-6">
|
||||||
|
<h4 className="text-lg font-semibold text-custom-text-100">First day of the week</h4>
|
||||||
|
<p className="text-sm text-custom-text-200">This will change how all calendars in your app look.</p>
|
||||||
|
</div>
|
||||||
|
<div className="col-span-12 sm:col-span-6">
|
||||||
|
<CustomSelect
|
||||||
|
value={userProfile.start_of_the_week}
|
||||||
|
label={getStartOfWeekLabel(userProfile.start_of_the_week)}
|
||||||
|
onChange={(val: number) => {
|
||||||
|
updateUserProfile({ start_of_the_week: val })
|
||||||
|
.then(() => {
|
||||||
|
setToast({
|
||||||
|
type: TOAST_TYPE.SUCCESS,
|
||||||
|
title: "Success",
|
||||||
|
message: "First day of the week updated successfully",
|
||||||
|
});
|
||||||
|
})
|
||||||
|
.catch(() => {
|
||||||
|
setToast({ type: TOAST_TYPE.ERROR, title: "Update failed", message: "Please try again later." });
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
input
|
||||||
|
maxHeight="lg"
|
||||||
|
>
|
||||||
|
<>
|
||||||
|
{START_OF_THE_WEEK_OPTIONS.map((day) => (
|
||||||
|
<CustomSelect.Option key={day.value} value={day.value}>
|
||||||
|
{day.label}
|
||||||
|
</CustomSelect.Option>
|
||||||
|
))}
|
||||||
|
</>
|
||||||
|
</CustomSelect>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
@ -1,3 +1,4 @@
|
||||||
|
import { EStartOfTheWeek } from "@plane/constants";
|
||||||
import { TCalendarLayouts } from "@plane/types";
|
import { TCalendarLayouts } from "@plane/types";
|
||||||
|
|
||||||
export const MONTHS_LIST: {
|
export const MONTHS_LIST: {
|
||||||
|
|
@ -60,35 +61,43 @@ export const DAYS_LIST: {
|
||||||
[dayIndex: number]: {
|
[dayIndex: number]: {
|
||||||
shortTitle: string;
|
shortTitle: string;
|
||||||
title: string;
|
title: string;
|
||||||
|
value: EStartOfTheWeek;
|
||||||
};
|
};
|
||||||
} = {
|
} = {
|
||||||
1: {
|
1: {
|
||||||
shortTitle: "Sun",
|
shortTitle: "Sun",
|
||||||
title: "Sunday",
|
title: "Sunday",
|
||||||
|
value: EStartOfTheWeek.SUNDAY,
|
||||||
},
|
},
|
||||||
2: {
|
2: {
|
||||||
shortTitle: "Mon",
|
shortTitle: "Mon",
|
||||||
title: "Monday",
|
title: "Monday",
|
||||||
|
value: EStartOfTheWeek.MONDAY,
|
||||||
},
|
},
|
||||||
3: {
|
3: {
|
||||||
shortTitle: "Tue",
|
shortTitle: "Tue",
|
||||||
title: "Tuesday",
|
title: "Tuesday",
|
||||||
|
value: EStartOfTheWeek.TUESDAY,
|
||||||
},
|
},
|
||||||
4: {
|
4: {
|
||||||
shortTitle: "Wed",
|
shortTitle: "Wed",
|
||||||
title: "Wednesday",
|
title: "Wednesday",
|
||||||
|
value: EStartOfTheWeek.WEDNESDAY,
|
||||||
},
|
},
|
||||||
5: {
|
5: {
|
||||||
shortTitle: "Thu",
|
shortTitle: "Thu",
|
||||||
title: "Thursday",
|
title: "Thursday",
|
||||||
|
value: EStartOfTheWeek.THURSDAY,
|
||||||
},
|
},
|
||||||
6: {
|
6: {
|
||||||
shortTitle: "Fri",
|
shortTitle: "Fri",
|
||||||
title: "Friday",
|
title: "Friday",
|
||||||
|
value: EStartOfTheWeek.FRIDAY,
|
||||||
},
|
},
|
||||||
7: {
|
7: {
|
||||||
shortTitle: "Sat",
|
shortTitle: "Sat",
|
||||||
title: "Saturday",
|
title: "Saturday",
|
||||||
|
value: EStartOfTheWeek.SATURDAY,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -68,7 +68,29 @@ export class CalendarStore implements ICalendarStore {
|
||||||
|
|
||||||
const { activeMonthDate } = this.calendarFilters;
|
const { activeMonthDate } = this.calendarFilters;
|
||||||
|
|
||||||
return this.calendarPayload[`y-${activeMonthDate.getFullYear()}`][`m-${activeMonthDate.getMonth()}`];
|
const year = activeMonthDate.getFullYear();
|
||||||
|
const month = activeMonthDate.getMonth();
|
||||||
|
|
||||||
|
// Get the weeks for the current month
|
||||||
|
const weeks = this.calendarPayload[`y-${year}`][`m-${month}`];
|
||||||
|
|
||||||
|
// If no weeks exist, return undefined
|
||||||
|
if (!weeks) return undefined;
|
||||||
|
|
||||||
|
// Create a new object to store the reordered weeks
|
||||||
|
const reorderedWeeks: { [weekNumber: string]: ICalendarWeek } = {};
|
||||||
|
|
||||||
|
// Get all week numbers and sort them
|
||||||
|
const weekNumbers = Object.keys(weeks).map((key) => parseInt(key.replace("w-", "")));
|
||||||
|
weekNumbers.sort((a, b) => a - b);
|
||||||
|
|
||||||
|
// Reorder weeks based on start_of_week
|
||||||
|
weekNumbers.forEach((weekNumber) => {
|
||||||
|
const weekKey = `w-${weekNumber}`;
|
||||||
|
reorderedWeeks[weekKey] = weeks[weekKey];
|
||||||
|
});
|
||||||
|
|
||||||
|
return reorderedWeeks;
|
||||||
}
|
}
|
||||||
|
|
||||||
get activeWeekNumber() {
|
get activeWeekNumber() {
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,7 @@ import cloneDeep from "lodash/cloneDeep";
|
||||||
import set from "lodash/set";
|
import set from "lodash/set";
|
||||||
import { action, makeObservable, observable, runInAction } from "mobx";
|
import { action, makeObservable, observable, runInAction } from "mobx";
|
||||||
// types
|
// types
|
||||||
|
import { EStartOfTheWeek } from "@plane/constants";
|
||||||
import { IUserTheme, TUserProfile } from "@plane/types";
|
import { IUserTheme, TUserProfile } from "@plane/types";
|
||||||
// services
|
// services
|
||||||
import { UserService } from "@/services/user.service";
|
import { UserService } from "@/services/user.service";
|
||||||
|
|
@ -58,7 +59,8 @@ export class ProfileStore implements IUserProfileStore {
|
||||||
has_billing_address: false,
|
has_billing_address: false,
|
||||||
created_at: "",
|
created_at: "",
|
||||||
updated_at: "",
|
updated_at: "",
|
||||||
language: ""
|
language: "",
|
||||||
|
start_of_the_week: EStartOfTheWeek.SUNDAY,
|
||||||
};
|
};
|
||||||
|
|
||||||
// services
|
// services
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,7 @@
|
||||||
|
import { EStartOfTheWeek } from "@plane/constants";
|
||||||
// helpers
|
// helpers
|
||||||
import { ICalendarDate, ICalendarPayload } from "@/components/issues";
|
import { ICalendarDate, ICalendarPayload } from "@/components/issues";
|
||||||
|
import { DAYS_LIST } from "@/constants/calendar";
|
||||||
import { getWeekNumberOfDate, renderFormattedPayloadDate } from "@/helpers/date-time.helper";
|
import { getWeekNumberOfDate, renderFormattedPayloadDate } from "@/helpers/date-time.helper";
|
||||||
// types
|
// types
|
||||||
|
|
||||||
|
|
@ -92,3 +94,21 @@ export const generateCalendarData = (currentStructure: ICalendarPayload | null,
|
||||||
|
|
||||||
return calendarData;
|
return calendarData;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns a new array sorted by the startOfWeek.
|
||||||
|
* @param items Array of items to sort.
|
||||||
|
* @param getDayIndex Function to get the day index (0-6) from an item.
|
||||||
|
* @param startOfWeek The day to start the week on.
|
||||||
|
*/
|
||||||
|
export function getOrderedDays<T>(
|
||||||
|
items: T[],
|
||||||
|
getDayIndex: (item: T) => number,
|
||||||
|
startOfWeek: EStartOfTheWeek = EStartOfTheWeek.SUNDAY
|
||||||
|
): T[] {
|
||||||
|
return [...items].sort((a, b) => {
|
||||||
|
const dayA = (7 + getDayIndex(a) - startOfWeek) % 7;
|
||||||
|
const dayB = (7 + getDayIndex(b) - startOfWeek) % 7;
|
||||||
|
return dayA - dayB;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue