587 option hide show non working days (#613)
Co-authored-by: Camille Moussu <cmoussu@linagora.com>
This commit is contained in:
@@ -0,0 +1,467 @@
|
|||||||
|
import SettingsPage from "@/features/Settings/SettingsPage";
|
||||||
|
import settingsReducer from "@/features/Settings/SettingsSlice";
|
||||||
|
import userReducer, {
|
||||||
|
getOpenPaasUserDataAsync,
|
||||||
|
} from "@/features/User/userSlice";
|
||||||
|
import { api } from "@/utils/apiUtils";
|
||||||
|
import { configureStore } from "@reduxjs/toolkit";
|
||||||
|
import "@testing-library/jest-dom";
|
||||||
|
import { fireEvent, screen, waitFor } from "@testing-library/react";
|
||||||
|
import { renderWithProviders } from "../../utils/Renderwithproviders";
|
||||||
|
|
||||||
|
jest.mock("@/utils/apiUtils");
|
||||||
|
|
||||||
|
const basePreloadedState = {
|
||||||
|
user: {
|
||||||
|
userData: {
|
||||||
|
sub: "test",
|
||||||
|
email: "test@test.com",
|
||||||
|
family_name: "Doe",
|
||||||
|
name: "John",
|
||||||
|
sid: "mockSid",
|
||||||
|
openpaasId: "667037022b752d0026472254",
|
||||||
|
},
|
||||||
|
organiserData: null,
|
||||||
|
tokens: null,
|
||||||
|
coreConfig: {
|
||||||
|
language: "en",
|
||||||
|
datetime: { timeZone: "UTC" },
|
||||||
|
},
|
||||||
|
alarmEmailsEnabled: false,
|
||||||
|
loading: false,
|
||||||
|
error: null,
|
||||||
|
},
|
||||||
|
settings: {
|
||||||
|
language: "en",
|
||||||
|
timeZone: "UTC",
|
||||||
|
isBrowserDefaultTimeZone: false,
|
||||||
|
view: "settings",
|
||||||
|
businessHours: {
|
||||||
|
start: "8:0",
|
||||||
|
end: "19:0",
|
||||||
|
daysOfWeek: [1, 2, 3, 4, 5],
|
||||||
|
},
|
||||||
|
workingDays: false,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
describe("Working Days and Business Hours Settings", () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
jest.clearAllMocks();
|
||||||
|
(api.patch as jest.Mock).mockResolvedValue({ status: 204 });
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("Initial rendering", () => {
|
||||||
|
it("renders the working days section with day selector", () => {
|
||||||
|
renderWithProviders(<SettingsPage />, basePreloadedState);
|
||||||
|
|
||||||
|
expect(
|
||||||
|
screen.getByText("settings.chooseWorkingDays")
|
||||||
|
).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("renders the show only working days toggle", () => {
|
||||||
|
renderWithProviders(<SettingsPage />, basePreloadedState);
|
||||||
|
|
||||||
|
expect(
|
||||||
|
screen.getByRole("switch", { name: /settings.showOnlyWorkingDays/i })
|
||||||
|
).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("shows working days toggle as unchecked when workingDays is false", () => {
|
||||||
|
renderWithProviders(<SettingsPage />, basePreloadedState);
|
||||||
|
|
||||||
|
const toggle = screen.getByRole("switch", {
|
||||||
|
name: /settings.showOnlyWorkingDays/i,
|
||||||
|
}) as HTMLInputElement;
|
||||||
|
expect(toggle.checked).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("shows working days toggle as checked when workingDays is true", () => {
|
||||||
|
renderWithProviders(<SettingsPage />, {
|
||||||
|
...basePreloadedState,
|
||||||
|
settings: { ...basePreloadedState.settings, workingDays: true },
|
||||||
|
});
|
||||||
|
|
||||||
|
const toggle = screen.getByRole("switch", {
|
||||||
|
name: /settings.showOnlyWorkingDays/i,
|
||||||
|
}) as HTMLInputElement;
|
||||||
|
expect(toggle.checked).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("shows working days toggle as unchecked when workingDays is null", () => {
|
||||||
|
renderWithProviders(<SettingsPage />, {
|
||||||
|
...basePreloadedState,
|
||||||
|
settings: { ...basePreloadedState.settings, workingDays: null },
|
||||||
|
});
|
||||||
|
|
||||||
|
const toggle = screen.getByRole("switch", {
|
||||||
|
name: /settings.showOnlyWorkingDays/i,
|
||||||
|
}) as HTMLInputElement;
|
||||||
|
expect(toggle.checked).toBe(false);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("workingDays toggle", () => {
|
||||||
|
it("updates workingDays immediately (optimistic update) and calls API", async () => {
|
||||||
|
const { store } = renderWithProviders(
|
||||||
|
<SettingsPage />,
|
||||||
|
basePreloadedState
|
||||||
|
);
|
||||||
|
|
||||||
|
const toggle = screen.getByRole("switch", {
|
||||||
|
name: /settings.showOnlyWorkingDays/i,
|
||||||
|
});
|
||||||
|
fireEvent.click(toggle);
|
||||||
|
|
||||||
|
await waitFor(() => {
|
||||||
|
expect(store.getState().settings.workingDays).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
await waitFor(() => {
|
||||||
|
expect(api.patch).toHaveBeenCalledWith(
|
||||||
|
"api/configurations?scope=user",
|
||||||
|
expect.objectContaining({
|
||||||
|
json: expect.arrayContaining([
|
||||||
|
expect.objectContaining({
|
||||||
|
name: "linagora.esn.calendar",
|
||||||
|
configurations: expect.arrayContaining([
|
||||||
|
expect.objectContaining({
|
||||||
|
name: "workingDays",
|
||||||
|
value: true,
|
||||||
|
}),
|
||||||
|
]),
|
||||||
|
}),
|
||||||
|
]),
|
||||||
|
})
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("rolls back workingDays if API call fails", async () => {
|
||||||
|
(api.patch as jest.Mock).mockRejectedValue(new Error("API Error"));
|
||||||
|
|
||||||
|
const { store } = renderWithProviders(
|
||||||
|
<SettingsPage />,
|
||||||
|
basePreloadedState
|
||||||
|
);
|
||||||
|
|
||||||
|
const toggle = screen.getByRole("switch", {
|
||||||
|
name: /settings.showOnlyWorkingDays/i,
|
||||||
|
});
|
||||||
|
fireEvent.click(toggle);
|
||||||
|
|
||||||
|
await waitFor(
|
||||||
|
() => {
|
||||||
|
expect(store.getState().settings.workingDays).toBe(false);
|
||||||
|
},
|
||||||
|
{ timeout: 3000 }
|
||||||
|
);
|
||||||
|
|
||||||
|
await waitFor(() => {
|
||||||
|
expect(
|
||||||
|
screen.getByText("settings.workingDaysUpdateError")
|
||||||
|
).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("toggles workingDays from true to false", async () => {
|
||||||
|
const { store } = renderWithProviders(<SettingsPage />, {
|
||||||
|
...basePreloadedState,
|
||||||
|
settings: { ...basePreloadedState.settings, workingDays: true },
|
||||||
|
});
|
||||||
|
|
||||||
|
const toggle = screen.getByRole("switch", {
|
||||||
|
name: /settings.showOnlyWorkingDays/i,
|
||||||
|
});
|
||||||
|
fireEvent.click(toggle);
|
||||||
|
|
||||||
|
await waitFor(() => {
|
||||||
|
expect(store.getState().settings.workingDays).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
await waitFor(() => {
|
||||||
|
expect(api.patch).toHaveBeenCalledWith(
|
||||||
|
"api/configurations?scope=user",
|
||||||
|
expect.objectContaining({
|
||||||
|
json: expect.arrayContaining([
|
||||||
|
expect.objectContaining({
|
||||||
|
name: "linagora.esn.calendar",
|
||||||
|
configurations: expect.arrayContaining([
|
||||||
|
expect.objectContaining({
|
||||||
|
name: "workingDays",
|
||||||
|
value: false,
|
||||||
|
}),
|
||||||
|
]),
|
||||||
|
}),
|
||||||
|
]),
|
||||||
|
})
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("businessHours day selector", () => {
|
||||||
|
it("updates businessHours in Redux immediately when a day is clicked", async () => {
|
||||||
|
const { store } = renderWithProviders(
|
||||||
|
<SettingsPage />,
|
||||||
|
basePreloadedState
|
||||||
|
);
|
||||||
|
|
||||||
|
// Sunday (0) is not in initial daysOfWeek [1,2,3,4,5], click it to add
|
||||||
|
const sundayButton = screen.getByRole("button", {
|
||||||
|
name: /sunday/i,
|
||||||
|
});
|
||||||
|
fireEvent.click(sundayButton);
|
||||||
|
|
||||||
|
await waitFor(() => {
|
||||||
|
expect(store.getState().settings.businessHours?.daysOfWeek).toContain(
|
||||||
|
0
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("removes a day from businessHours when an already selected day is clicked", async () => {
|
||||||
|
const { store } = renderWithProviders(
|
||||||
|
<SettingsPage />,
|
||||||
|
basePreloadedState
|
||||||
|
);
|
||||||
|
|
||||||
|
// Monday (1) is in initial daysOfWeek, click to remove
|
||||||
|
const mondayButton = screen.getByRole("button", { name: /monday/i });
|
||||||
|
fireEvent.click(mondayButton);
|
||||||
|
|
||||||
|
await waitFor(() => {
|
||||||
|
expect(
|
||||||
|
store.getState().settings.businessHours?.daysOfWeek
|
||||||
|
).not.toContain(1);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("debounces API call when multiple days are clicked rapidly", async () => {
|
||||||
|
jest.useFakeTimers();
|
||||||
|
|
||||||
|
renderWithProviders(<SettingsPage />, basePreloadedState);
|
||||||
|
|
||||||
|
const sundayButton = screen.getByRole("button", { name: /sunday/i });
|
||||||
|
const saturdayButton = screen.getByRole("button", { name: /saturday/i });
|
||||||
|
|
||||||
|
fireEvent.click(sundayButton);
|
||||||
|
fireEvent.click(saturdayButton);
|
||||||
|
fireEvent.click(sundayButton);
|
||||||
|
|
||||||
|
// API should not have been called yet
|
||||||
|
expect(api.patch).not.toHaveBeenCalled();
|
||||||
|
|
||||||
|
// Advance timers past debounce threshold
|
||||||
|
jest.runAllTimers();
|
||||||
|
|
||||||
|
await waitFor(() => {
|
||||||
|
expect(api.patch).toHaveBeenCalledTimes(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
jest.useRealTimers();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("sends businessHours as array to API", async () => {
|
||||||
|
jest.useFakeTimers();
|
||||||
|
|
||||||
|
renderWithProviders(<SettingsPage />, basePreloadedState);
|
||||||
|
|
||||||
|
const sundayButton = screen.getByRole("button", { name: /sunday/i });
|
||||||
|
fireEvent.click(sundayButton);
|
||||||
|
|
||||||
|
jest.runAllTimers();
|
||||||
|
|
||||||
|
await waitFor(() => {
|
||||||
|
expect(api.patch).toHaveBeenCalledWith(
|
||||||
|
"api/configurations?scope=user",
|
||||||
|
expect.objectContaining({
|
||||||
|
json: expect.arrayContaining([
|
||||||
|
expect.objectContaining({
|
||||||
|
name: "core",
|
||||||
|
configurations: expect.arrayContaining([
|
||||||
|
expect.objectContaining({
|
||||||
|
name: "businessHours",
|
||||||
|
value: expect.arrayContaining([
|
||||||
|
expect.objectContaining({
|
||||||
|
start: "8:0",
|
||||||
|
end: "19:0",
|
||||||
|
daysOfWeek: expect.arrayContaining([0]),
|
||||||
|
}),
|
||||||
|
]),
|
||||||
|
}),
|
||||||
|
]),
|
||||||
|
}),
|
||||||
|
]),
|
||||||
|
})
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
jest.useRealTimers();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("rolls back businessHours if API call fails", async () => {
|
||||||
|
jest.useFakeTimers();
|
||||||
|
(api.patch as jest.Mock).mockRejectedValue(new Error("API Error"));
|
||||||
|
|
||||||
|
const { store } = renderWithProviders(
|
||||||
|
<SettingsPage />,
|
||||||
|
basePreloadedState
|
||||||
|
);
|
||||||
|
|
||||||
|
const sundayButton = screen.getByRole("button", { name: /sunday/i });
|
||||||
|
fireEvent.click(sundayButton);
|
||||||
|
|
||||||
|
jest.runAllTimers();
|
||||||
|
|
||||||
|
await waitFor(
|
||||||
|
() => {
|
||||||
|
expect(
|
||||||
|
store.getState().settings.businessHours?.daysOfWeek
|
||||||
|
).not.toContain(0);
|
||||||
|
},
|
||||||
|
{ timeout: 3000 }
|
||||||
|
);
|
||||||
|
|
||||||
|
await waitFor(() => {
|
||||||
|
expect(
|
||||||
|
screen.getByText("settings.workingDaysUpdateError")
|
||||||
|
).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
|
jest.useRealTimers();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("getOpenPaasUserDataAsync integration", () => {
|
||||||
|
it("populates businessHours from core module configurations", async () => {
|
||||||
|
const store = configureStore({
|
||||||
|
reducer: { user: userReducer, settings: settingsReducer },
|
||||||
|
preloadedState: {
|
||||||
|
user: {
|
||||||
|
userData: {
|
||||||
|
sub: "test",
|
||||||
|
email: "test@test.com",
|
||||||
|
family_name: "Doe",
|
||||||
|
name: "John",
|
||||||
|
sid: "mockSid",
|
||||||
|
openpaasId: "667037022b752d0026472254",
|
||||||
|
},
|
||||||
|
organiserData: null,
|
||||||
|
tokens: null,
|
||||||
|
coreConfig: { language: "en", datetime: { timeZone: "UTC" } },
|
||||||
|
alarmEmailsEnabled: null,
|
||||||
|
loading: false,
|
||||||
|
error: null,
|
||||||
|
},
|
||||||
|
settings: {
|
||||||
|
language: "en",
|
||||||
|
timeZone: "UTC",
|
||||||
|
isBrowserDefaultTimeZone: false,
|
||||||
|
view: "calendar",
|
||||||
|
businessHours: null,
|
||||||
|
workingDays: null,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const mockResponse = {
|
||||||
|
id: "667037022b752d0026472254",
|
||||||
|
firstname: "John",
|
||||||
|
lastname: "Doe",
|
||||||
|
preferredEmail: "test@test.com",
|
||||||
|
configurations: {
|
||||||
|
modules: [
|
||||||
|
{
|
||||||
|
name: "core",
|
||||||
|
configurations: [
|
||||||
|
{
|
||||||
|
name: "businessHours",
|
||||||
|
value: [
|
||||||
|
{ start: "8:0", end: "19:0", daysOfWeek: [1, 2, 3, 4, 5] },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "linagora.esn.calendar",
|
||||||
|
configurations: [{ name: "workingDays", value: true }],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
await store.dispatch(
|
||||||
|
getOpenPaasUserDataAsync.fulfilled(mockResponse, "", undefined)
|
||||||
|
);
|
||||||
|
|
||||||
|
const state = store.getState();
|
||||||
|
expect(state.settings.businessHours).toEqual({
|
||||||
|
start: "8:0",
|
||||||
|
end: "19:0",
|
||||||
|
daysOfWeek: [1, 2, 3, 4, 5],
|
||||||
|
});
|
||||||
|
expect(state.settings.workingDays).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("sets businessHours to null when not present in API response", async () => {
|
||||||
|
const store = configureStore({
|
||||||
|
reducer: { user: userReducer, settings: settingsReducer },
|
||||||
|
preloadedState: {
|
||||||
|
user: {
|
||||||
|
userData: {
|
||||||
|
sub: "test",
|
||||||
|
email: "test@test.com",
|
||||||
|
family_name: "Doe",
|
||||||
|
name: "John",
|
||||||
|
sid: "mockSid",
|
||||||
|
openpaasId: "667037022b752d0026472254",
|
||||||
|
},
|
||||||
|
organiserData: null,
|
||||||
|
tokens: null,
|
||||||
|
coreConfig: { language: "en", datetime: { timeZone: "UTC" } },
|
||||||
|
alarmEmailsEnabled: null,
|
||||||
|
loading: false,
|
||||||
|
error: null,
|
||||||
|
},
|
||||||
|
settings: {
|
||||||
|
language: "en",
|
||||||
|
timeZone: "UTC",
|
||||||
|
isBrowserDefaultTimeZone: false,
|
||||||
|
view: "calendar",
|
||||||
|
businessHours: null,
|
||||||
|
workingDays: null,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const mockResponse = {
|
||||||
|
id: "667037022b752d0026472254",
|
||||||
|
firstname: "John",
|
||||||
|
lastname: "Doe",
|
||||||
|
preferredEmail: "test@test.com",
|
||||||
|
configurations: {
|
||||||
|
modules: [
|
||||||
|
{
|
||||||
|
name: "core",
|
||||||
|
configurations: [],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "linagora.esn.calendar",
|
||||||
|
configurations: [{ name: "workingDays", value: null }],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
await store.dispatch(
|
||||||
|
getOpenPaasUserDataAsync.fulfilled(mockResponse, "", undefined)
|
||||||
|
);
|
||||||
|
|
||||||
|
const state = store.getState();
|
||||||
|
expect(state.settings.businessHours).toBeNull();
|
||||||
|
expect(state.settings.workingDays).toBeNull();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -78,9 +78,21 @@ export default function CalendarApp({
|
|||||||
const theme = useTheme();
|
const theme = useTheme();
|
||||||
const view = useAppSelector((state) => state.settings.view);
|
const view = useAppSelector((state) => state.settings.view);
|
||||||
const userData = useAppSelector((state) => state.user.userData);
|
const userData = useAppSelector((state) => state.user.userData);
|
||||||
|
const workingDays = useAppSelector(
|
||||||
|
(state) => state.settings.businessHours?.daysOfWeek
|
||||||
|
);
|
||||||
|
const hideWorkingDays = useAppSelector((state) => state.settings.workingDays);
|
||||||
|
|
||||||
const hideDeclinedEvents = useAppSelector(
|
const hideDeclinedEvents = useAppSelector(
|
||||||
(state) => state.settings.hideDeclinedEvents
|
(state) => state.settings.hideDeclinedEvents
|
||||||
);
|
);
|
||||||
|
const hiddenDays = useMemo(() => {
|
||||||
|
if (!hideWorkingDays || !workingDays || workingDays.length === 0) return [];
|
||||||
|
const validWorkingDays = workingDays.filter((d) => d >= 0 && d <= 6);
|
||||||
|
if (validWorkingDays.length === 0) return [];
|
||||||
|
return [0, 1, 2, 3, 4, 5, 6].filter((d) => !validWorkingDays.includes(d));
|
||||||
|
}, [hideWorkingDays, workingDays]);
|
||||||
|
|
||||||
const calendars = useAppSelector((state) => state.calendars.list);
|
const calendars = useAppSelector((state) => state.calendars.list);
|
||||||
const isPending = useAppSelector((state) => state.calendars.pending);
|
const isPending = useAppSelector((state) => state.calendars.pending);
|
||||||
const displayWeekNumbers = useAppSelector(
|
const displayWeekNumbers = useAppSelector(
|
||||||
@@ -413,6 +425,7 @@ export default function CalendarApp({
|
|||||||
{menubarProps?.isIframe && <Menubar {...menubarProps} />}
|
{menubarProps?.isIframe && <Menubar {...menubarProps} />}
|
||||||
{view === "calendar" && (
|
{view === "calendar" && (
|
||||||
<FullCalendar
|
<FullCalendar
|
||||||
|
key={hiddenDays.join(",")}
|
||||||
ref={(ref) => {
|
ref={(ref) => {
|
||||||
if (ref) {
|
if (ref) {
|
||||||
calendarRef.current = ref.getApi();
|
calendarRef.current = ref.getApi();
|
||||||
@@ -428,6 +441,7 @@ export default function CalendarApp({
|
|||||||
firstDay={1}
|
firstDay={1}
|
||||||
editable={true}
|
editable={true}
|
||||||
locale={localeMap[lang]}
|
locale={localeMap[lang]}
|
||||||
|
hiddenDays={hiddenDays}
|
||||||
selectable={true}
|
selectable={true}
|
||||||
timeZone={timezone}
|
timeZone={timezone}
|
||||||
height={"100%"}
|
height={"100%"}
|
||||||
|
|||||||
@@ -23,6 +23,7 @@ import "dayjs/locale/vi";
|
|||||||
import { useI18n } from "twake-i18n";
|
import { useI18n } from "twake-i18n";
|
||||||
import { ReadOnlyDateField } from "./components/ReadOnlyPickerField";
|
import { ReadOnlyDateField } from "./components/ReadOnlyPickerField";
|
||||||
import { LONG_DATE_FORMAT } from "./utils/dateTimeFormatters";
|
import { LONG_DATE_FORMAT } from "./utils/dateTimeFormatters";
|
||||||
|
import { FC_DAYS, WeekDaySelector } from "./WeekDaySelector";
|
||||||
|
|
||||||
export default function RepeatEvent({
|
export default function RepeatEvent({
|
||||||
repetition,
|
repetition,
|
||||||
@@ -57,31 +58,6 @@ export default function RepeatEvent({
|
|||||||
|
|
||||||
const defaultEndDate = dayjs(eventStart).add(1, "day").format("YYYY-MM-DD");
|
const defaultEndDate = dayjs(eventStart).add(1, "day").format("YYYY-MM-DD");
|
||||||
|
|
||||||
const handleDayChange = (dayCode: string) => {
|
|
||||||
const currentDays = repetition.byday || [];
|
|
||||||
const updatedDays = currentDays.includes(dayCode)
|
|
||||||
? currentDays.filter((d) => d !== dayCode)
|
|
||||||
: [...currentDays, dayCode];
|
|
||||||
|
|
||||||
setRepetition({
|
|
||||||
...repetition,
|
|
||||||
byday: updatedDays.length > 0 ? updatedDays : null,
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
const getDayLabel = (day: string) => {
|
|
||||||
const dayMap: { [key: string]: string } = {
|
|
||||||
MO: t("event.repeat.days.monday"),
|
|
||||||
TU: t("event.repeat.days.tuesday"),
|
|
||||||
WE: t("event.repeat.days.wednesday"),
|
|
||||||
TH: t("event.repeat.days.thursday"),
|
|
||||||
FR: t("event.repeat.days.friday"),
|
|
||||||
SA: t("event.repeat.days.saturday"),
|
|
||||||
SU: t("event.repeat.days.sunday"),
|
|
||||||
};
|
|
||||||
return dayMap[day] || day;
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Box>
|
<Box>
|
||||||
<Stack>
|
<Stack>
|
||||||
@@ -150,51 +126,21 @@ export default function RepeatEvent({
|
|||||||
{/* Weekly selection */}
|
{/* Weekly selection */}
|
||||||
{repetition.freq === "weekly" && (
|
{repetition.freq === "weekly" && (
|
||||||
<Box mb={2}>
|
<Box mb={2}>
|
||||||
<Box display="flex" gap={1}>
|
<WeekDaySelector
|
||||||
{days.map((dayCode) => {
|
selectedDays={(repetition.byday ?? [])
|
||||||
const isSelected = repetition.byday?.includes(dayCode) ?? false;
|
.map((ics) => FC_DAYS.find((d) => d.ics === ics)?.fc ?? -1)
|
||||||
const fullLabel = getDayLabel(dayCode);
|
.filter((d) => d !== -1)}
|
||||||
const label = fullLabel.charAt(0);
|
onChange={(fcDays) => {
|
||||||
|
const icsDays = fcDays
|
||||||
return (
|
.map((fc) => FC_DAYS.find((d) => d.fc === fc)?.ics ?? "")
|
||||||
<Box
|
.filter(Boolean);
|
||||||
key={dayCode}
|
setRepetition({
|
||||||
role="button"
|
...repetition,
|
||||||
aria-label={fullLabel}
|
byday: icsDays.length > 0 ? icsDays : null,
|
||||||
onClick={() => {
|
});
|
||||||
if (!isOwn) return;
|
}}
|
||||||
handleDayChange(dayCode);
|
disabled={!isOwn}
|
||||||
}}
|
/>
|
||||||
sx={{
|
|
||||||
width: 40,
|
|
||||||
height: 40,
|
|
||||||
borderRadius: "4px",
|
|
||||||
border: "1px solid",
|
|
||||||
borderColor: isSelected ? "primary.main" : "#AEAEC0",
|
|
||||||
color: isSelected ? "#fff" : "#8C9CAF",
|
|
||||||
fontSize: 16,
|
|
||||||
fontWeight: 400,
|
|
||||||
lineHeight: "24px",
|
|
||||||
display: "flex",
|
|
||||||
alignItems: "center",
|
|
||||||
justifyContent: "center",
|
|
||||||
textAlign: "center",
|
|
||||||
bgcolor: isSelected ? "primary.main" : "transparent",
|
|
||||||
cursor: isOwn ? "pointer" : "default",
|
|
||||||
"&:hover": isOwn
|
|
||||||
? {
|
|
||||||
borderColor: "primary.main",
|
|
||||||
bgcolor: "primary.main",
|
|
||||||
color: "#fff",
|
|
||||||
}
|
|
||||||
: undefined,
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{label}
|
|
||||||
</Box>
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
</Box>
|
|
||||||
</Box>
|
</Box>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,92 @@
|
|||||||
|
import { Box } from "@linagora/twake-mui";
|
||||||
|
import { useI18n } from "twake-i18n";
|
||||||
|
|
||||||
|
interface WeekDaySelectorProps {
|
||||||
|
selectedDays: number[]; // FullCalendar format: 0=Sun, 1=Mon...
|
||||||
|
onChange: (days: number[]) => void;
|
||||||
|
disabled?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const FC_DAYS = [
|
||||||
|
{ fc: 1, ics: "MO" },
|
||||||
|
{ fc: 2, ics: "TU" },
|
||||||
|
{ fc: 3, ics: "WE" },
|
||||||
|
{ fc: 4, ics: "TH" },
|
||||||
|
{ fc: 5, ics: "FR" },
|
||||||
|
{ fc: 6, ics: "SA" },
|
||||||
|
{ fc: 0, ics: "SU" },
|
||||||
|
];
|
||||||
|
|
||||||
|
export function WeekDaySelector({
|
||||||
|
selectedDays,
|
||||||
|
onChange,
|
||||||
|
disabled,
|
||||||
|
}: WeekDaySelectorProps) {
|
||||||
|
const { t } = useI18n();
|
||||||
|
|
||||||
|
const getDayLabel = (ics: string) => {
|
||||||
|
const dayMap: Record<string, string> = {
|
||||||
|
MO: t("event.repeat.days.monday"),
|
||||||
|
TU: t("event.repeat.days.tuesday"),
|
||||||
|
WE: t("event.repeat.days.wednesday"),
|
||||||
|
TH: t("event.repeat.days.thursday"),
|
||||||
|
FR: t("event.repeat.days.friday"),
|
||||||
|
SA: t("event.repeat.days.saturday"),
|
||||||
|
SU: t("event.repeat.days.sunday"),
|
||||||
|
};
|
||||||
|
return dayMap[ics] || ics;
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleToggle = (fcDay: number) => {
|
||||||
|
if (disabled) return;
|
||||||
|
const updated = selectedDays.includes(fcDay)
|
||||||
|
? selectedDays.filter((d) => d !== fcDay)
|
||||||
|
: [...selectedDays, fcDay];
|
||||||
|
onChange(updated);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Box display="flex" gap={1}>
|
||||||
|
{FC_DAYS.map(({ fc, ics }) => {
|
||||||
|
const isSelected = selectedDays.includes(fc);
|
||||||
|
const fullLabel = getDayLabel(ics);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Box
|
||||||
|
component="button"
|
||||||
|
type="button"
|
||||||
|
key={ics}
|
||||||
|
aria-label={fullLabel}
|
||||||
|
aria-pressed={isSelected}
|
||||||
|
onClick={() => handleToggle(fc)}
|
||||||
|
disabled={disabled}
|
||||||
|
sx={{
|
||||||
|
width: 40,
|
||||||
|
height: 40,
|
||||||
|
borderRadius: "4px",
|
||||||
|
border: "1px solid",
|
||||||
|
borderColor: isSelected ? "primary.main" : "#AEAEC0",
|
||||||
|
color: isSelected ? "#fff" : "#8C9CAF",
|
||||||
|
fontSize: 16,
|
||||||
|
fontWeight: 400,
|
||||||
|
display: "flex",
|
||||||
|
alignItems: "center",
|
||||||
|
justifyContent: "center",
|
||||||
|
bgcolor: isSelected ? "primary.main" : "transparent",
|
||||||
|
cursor: disabled ? "default" : "pointer",
|
||||||
|
"&:hover": !disabled
|
||||||
|
? {
|
||||||
|
borderColor: "primary.main",
|
||||||
|
bgcolor: "primary.main",
|
||||||
|
color: "#fff",
|
||||||
|
}
|
||||||
|
: undefined,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{fullLabel.charAt(0)}
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,339 @@
|
|||||||
|
import { useAppDispatch, useAppSelector } from "@/app/hooks";
|
||||||
|
import { useTimeZoneList } from "@/components/Calendar/TimezoneSelector";
|
||||||
|
import { WeekDaySelector } from "@/components/Event/WeekDaySelector";
|
||||||
|
import { TimezoneAutocomplete } from "@/components/Timezone/TimezoneAutocomplete";
|
||||||
|
import { browserDefaultTimeZone, getTimezoneOffset } from "@/utils/timezone";
|
||||||
|
import {
|
||||||
|
Box,
|
||||||
|
FormControl,
|
||||||
|
FormControlLabel,
|
||||||
|
MenuItem,
|
||||||
|
Select,
|
||||||
|
SelectChangeEvent,
|
||||||
|
Switch,
|
||||||
|
Typography,
|
||||||
|
} from "@linagora/twake-mui";
|
||||||
|
import { useRef, useCallback, useEffect } from "react";
|
||||||
|
import { useI18n } from "twake-i18n";
|
||||||
|
import {
|
||||||
|
setLanguage as setUserLanguage,
|
||||||
|
setTimezone as setUserTimeZone,
|
||||||
|
updateUserConfigurationsAsync,
|
||||||
|
} from "../User/userSlice";
|
||||||
|
import { AVAILABLE_LANGUAGES } from "./constants";
|
||||||
|
import {
|
||||||
|
BusinessHour,
|
||||||
|
setBusinessHours,
|
||||||
|
setDisplayWeekNumbers,
|
||||||
|
setHideDeclinedEvents,
|
||||||
|
setIsBrowserDefaultTimeZone,
|
||||||
|
setLanguage as setSettingsLanguage,
|
||||||
|
setTimeZone as setSettingsTimeZone,
|
||||||
|
setWorkingDays,
|
||||||
|
} from "./SettingsSlice";
|
||||||
|
|
||||||
|
interface GeneralSettingsProps {
|
||||||
|
onLanguageError: () => void;
|
||||||
|
onTimeZoneError: () => void;
|
||||||
|
onHideDeclinedEventsError: () => void;
|
||||||
|
onDisplayWeekNumbersError: () => void;
|
||||||
|
onWorkingDaysError: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function GeneralSettings({
|
||||||
|
onLanguageError,
|
||||||
|
onTimeZoneError,
|
||||||
|
onHideDeclinedEventsError,
|
||||||
|
onDisplayWeekNumbersError,
|
||||||
|
onWorkingDaysError,
|
||||||
|
}: GeneralSettingsProps) {
|
||||||
|
const dispatch = useAppDispatch();
|
||||||
|
const { t } = useI18n();
|
||||||
|
|
||||||
|
const previousConfig = useAppSelector((state) => state.user.coreConfig);
|
||||||
|
const userLanguage = useAppSelector(
|
||||||
|
(state) => state.user?.coreConfig.language
|
||||||
|
);
|
||||||
|
const settingsLanguage = useAppSelector((state) => state.settings?.language);
|
||||||
|
const currentLanguage = userLanguage || settingsLanguage || "en";
|
||||||
|
|
||||||
|
const timezoneList = useTimeZoneList();
|
||||||
|
const userTimeZone = useAppSelector(
|
||||||
|
(state) => state.user?.coreConfig?.datetime?.timeZone
|
||||||
|
);
|
||||||
|
const settingTimeZone = useAppSelector((state) => state.settings?.timeZone);
|
||||||
|
const currentTimeZone =
|
||||||
|
userTimeZone ?? settingTimeZone ?? browserDefaultTimeZone;
|
||||||
|
const isBrowserDefault = useAppSelector(
|
||||||
|
(state) => state.settings.isBrowserDefaultTimeZone
|
||||||
|
);
|
||||||
|
|
||||||
|
const hideDeclinedEvents = useAppSelector(
|
||||||
|
(state) => state.settings?.hideDeclinedEvents
|
||||||
|
);
|
||||||
|
const displayWeekNumbers = useAppSelector(
|
||||||
|
(state) => state.settings?.displayWeekNumbers
|
||||||
|
);
|
||||||
|
const workingDays = useAppSelector((state) => state.settings.workingDays);
|
||||||
|
const businessHours = useAppSelector((state) => state.settings.businessHours);
|
||||||
|
const pendingBusinessHoursRef = useRef<BusinessHour | null>(null);
|
||||||
|
const businessHoursTimeoutRef = useRef<ReturnType<typeof setTimeout> | null>(
|
||||||
|
null
|
||||||
|
);
|
||||||
|
|
||||||
|
const handleLanguageChange = (event: SelectChangeEvent<string>) => {
|
||||||
|
const newLanguage = event.target.value;
|
||||||
|
const previousLanguage = currentLanguage;
|
||||||
|
dispatch(setUserLanguage(newLanguage));
|
||||||
|
dispatch(setSettingsLanguage(newLanguage));
|
||||||
|
dispatch(updateUserConfigurationsAsync({ language: newLanguage }))
|
||||||
|
.unwrap()
|
||||||
|
.catch(() => {
|
||||||
|
dispatch(setUserLanguage(previousLanguage));
|
||||||
|
dispatch(setSettingsLanguage(previousLanguage));
|
||||||
|
onLanguageError();
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleTimeZoneChange = (newTimeZone: string) => {
|
||||||
|
const previousTimeZone = currentTimeZone;
|
||||||
|
dispatch(setUserTimeZone(newTimeZone));
|
||||||
|
dispatch(setSettingsTimeZone(newTimeZone));
|
||||||
|
dispatch(
|
||||||
|
updateUserConfigurationsAsync({ timezone: newTimeZone, previousConfig })
|
||||||
|
)
|
||||||
|
.unwrap()
|
||||||
|
.catch(() => {
|
||||||
|
dispatch(setUserTimeZone(previousTimeZone));
|
||||||
|
dispatch(setSettingsTimeZone(previousTimeZone));
|
||||||
|
onTimeZoneError();
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleTimeZoneDefaultChange = (isDefault: boolean) => {
|
||||||
|
const previousTimeZone = currentTimeZone;
|
||||||
|
dispatch(setIsBrowserDefaultTimeZone(isDefault));
|
||||||
|
if (isDefault) {
|
||||||
|
dispatch(setUserTimeZone(null));
|
||||||
|
dispatch(setSettingsTimeZone(browserDefaultTimeZone));
|
||||||
|
dispatch(
|
||||||
|
updateUserConfigurationsAsync({ timezone: null, previousConfig })
|
||||||
|
)
|
||||||
|
.unwrap()
|
||||||
|
.catch(() => {
|
||||||
|
dispatch(setUserTimeZone(previousTimeZone));
|
||||||
|
dispatch(setSettingsTimeZone(previousTimeZone));
|
||||||
|
dispatch(setIsBrowserDefaultTimeZone(!isDefault));
|
||||||
|
onTimeZoneError();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleHideDeclinedEvents = (value: boolean) => {
|
||||||
|
dispatch(setHideDeclinedEvents(value));
|
||||||
|
dispatch(updateUserConfigurationsAsync({ hideDeclinedEvents: value }))
|
||||||
|
.unwrap()
|
||||||
|
.catch(() => {
|
||||||
|
dispatch(setHideDeclinedEvents(!value));
|
||||||
|
onHideDeclinedEventsError();
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleDisplayWeekNumbers = (value: boolean) => {
|
||||||
|
dispatch(setDisplayWeekNumbers(value));
|
||||||
|
dispatch(updateUserConfigurationsAsync({ displayWeekNumbers: value }))
|
||||||
|
.unwrap()
|
||||||
|
.catch(() => {
|
||||||
|
dispatch(setDisplayWeekNumbers(!value));
|
||||||
|
onDisplayWeekNumbersError();
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleBusinessHour = useCallback(
|
||||||
|
({ days }: { days: number[] }) => {
|
||||||
|
const previousHours = businessHours;
|
||||||
|
const value: BusinessHour | null = businessHours
|
||||||
|
? { ...businessHours, daysOfWeek: days }
|
||||||
|
: null;
|
||||||
|
|
||||||
|
dispatch(setBusinessHours(value));
|
||||||
|
pendingBusinessHoursRef.current = value;
|
||||||
|
|
||||||
|
if (businessHoursTimeoutRef.current) {
|
||||||
|
clearTimeout(businessHoursTimeoutRef.current);
|
||||||
|
}
|
||||||
|
|
||||||
|
businessHoursTimeoutRef.current = setTimeout(() => {
|
||||||
|
dispatch(
|
||||||
|
updateUserConfigurationsAsync({
|
||||||
|
businessHours: pendingBusinessHoursRef.current,
|
||||||
|
})
|
||||||
|
)
|
||||||
|
.unwrap()
|
||||||
|
.catch(() => {
|
||||||
|
dispatch(setBusinessHours(previousHours));
|
||||||
|
onWorkingDaysError();
|
||||||
|
});
|
||||||
|
}, 500);
|
||||||
|
},
|
||||||
|
[businessHours, dispatch, onWorkingDaysError]
|
||||||
|
);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
return () => {
|
||||||
|
if (businessHoursTimeoutRef.current) {
|
||||||
|
clearTimeout(businessHoursTimeoutRef.current);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const handleWorkingDays = (value: boolean) => {
|
||||||
|
dispatch(setWorkingDays(value));
|
||||||
|
dispatch(updateUserConfigurationsAsync({ workingDays: value }))
|
||||||
|
.unwrap()
|
||||||
|
.catch(() => {
|
||||||
|
dispatch(setWorkingDays(!value));
|
||||||
|
onWorkingDaysError();
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Box className="settings-tab-content">
|
||||||
|
{/* Language */}
|
||||||
|
<Box sx={{ mb: 4 }}>
|
||||||
|
<Typography variant="h6" sx={{ mb: 1 }}>
|
||||||
|
{t("settings.language") || "Language"}
|
||||||
|
</Typography>
|
||||||
|
<Typography variant="body2" color="text.secondary" sx={{ mb: 3 }}>
|
||||||
|
{t("settings.languageDescription") ||
|
||||||
|
"This will be the language used in your Twake Calendar"}
|
||||||
|
</Typography>
|
||||||
|
<FormControl size="small" sx={{ minWidth: 500 }}>
|
||||||
|
<Select
|
||||||
|
value={currentLanguage}
|
||||||
|
onChange={handleLanguageChange}
|
||||||
|
variant="outlined"
|
||||||
|
aria-label={t("settings.languageSelector") || "Language selector"}
|
||||||
|
>
|
||||||
|
{AVAILABLE_LANGUAGES.map(({ code, label }) => (
|
||||||
|
<MenuItem key={code} value={code}>
|
||||||
|
{label}
|
||||||
|
</MenuItem>
|
||||||
|
))}
|
||||||
|
</Select>
|
||||||
|
</FormControl>
|
||||||
|
</Box>
|
||||||
|
|
||||||
|
{/* Timezone */}
|
||||||
|
<Box
|
||||||
|
sx={{
|
||||||
|
mb: 4,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Typography variant="h6" sx={{ mb: 1 }}>
|
||||||
|
{t("settings.timeZone")}
|
||||||
|
</Typography>
|
||||||
|
<Box>
|
||||||
|
<FormControl size="small" sx={{ minWidth: 500 }}>
|
||||||
|
<FormControlLabel
|
||||||
|
control={
|
||||||
|
<Switch
|
||||||
|
checked={isBrowserDefault}
|
||||||
|
onChange={() =>
|
||||||
|
handleTimeZoneDefaultChange(!isBrowserDefault)
|
||||||
|
}
|
||||||
|
aria-label={t("settings.timeZoneBrowserDefault")}
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
label={t("settings.timeZoneBrowserDefault")}
|
||||||
|
labelPlacement="start"
|
||||||
|
sx={{
|
||||||
|
minWidth: 400,
|
||||||
|
justifyContent: "space-between",
|
||||||
|
marginLeft: 0,
|
||||||
|
mb: 2,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
{!isBrowserDefault && (
|
||||||
|
<TimezoneAutocomplete
|
||||||
|
value={currentTimeZone}
|
||||||
|
zones={timezoneList.zones}
|
||||||
|
getTimezoneOffset={getTimezoneOffset}
|
||||||
|
onChange={handleTimeZoneChange}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</FormControl>
|
||||||
|
</Box>
|
||||||
|
</Box>
|
||||||
|
|
||||||
|
{/* Working */}
|
||||||
|
<Box sx={{ mb: 4 }}>
|
||||||
|
<Typography variant="h6" sx={{ mb: 1 }}>
|
||||||
|
{t("settings.chooseWorkingDays")}
|
||||||
|
</Typography>
|
||||||
|
<WeekDaySelector
|
||||||
|
selectedDays={businessHours?.daysOfWeek ?? []}
|
||||||
|
onChange={(days) => handleBusinessHour({ days })}
|
||||||
|
/>
|
||||||
|
<FormControl size="small" sx={{ minWidth: 500, mt: 2 }}>
|
||||||
|
<FormControlLabel
|
||||||
|
control={
|
||||||
|
<Switch
|
||||||
|
checked={Boolean(workingDays)}
|
||||||
|
onChange={() => handleWorkingDays(!workingDays)}
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
label={t("settings.showOnlyWorkingDays")}
|
||||||
|
labelPlacement="start"
|
||||||
|
sx={{
|
||||||
|
minWidth: 400,
|
||||||
|
justifyContent: "space-between",
|
||||||
|
marginLeft: 0,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</FormControl>
|
||||||
|
</Box>
|
||||||
|
|
||||||
|
{/* Calendar & Events */}
|
||||||
|
<Box sx={{ mb: 4 }}>
|
||||||
|
<Typography variant="h6" sx={{ mb: 1 }}>
|
||||||
|
{t("settings.calAndEvent")}
|
||||||
|
</Typography>
|
||||||
|
<FormControl size="small" sx={{ minWidth: 500 }}>
|
||||||
|
<FormControlLabel
|
||||||
|
control={
|
||||||
|
<Switch
|
||||||
|
checked={Boolean(!hideDeclinedEvents)}
|
||||||
|
onChange={() => handleHideDeclinedEvents(!hideDeclinedEvents)}
|
||||||
|
aria-label={t("settings.showDeclinedEvent")}
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
label={t("settings.showDeclinedEvent")}
|
||||||
|
labelPlacement="start"
|
||||||
|
sx={{
|
||||||
|
minWidth: 400,
|
||||||
|
justifyContent: "space-between",
|
||||||
|
marginLeft: 0,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<FormControlLabel
|
||||||
|
control={
|
||||||
|
<Switch
|
||||||
|
checked={Boolean(displayWeekNumbers)}
|
||||||
|
onChange={() => handleDisplayWeekNumbers(!displayWeekNumbers)}
|
||||||
|
aria-label={t("settings.displayWeekNumbers")}
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
label={t("settings.displayWeekNumbers")}
|
||||||
|
labelPlacement="start"
|
||||||
|
sx={{
|
||||||
|
minWidth: 400,
|
||||||
|
justifyContent: "space-between",
|
||||||
|
marginLeft: 0,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</FormControl>
|
||||||
|
</Box>
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,55 @@
|
|||||||
|
import { useAppDispatch, useAppSelector } from "@/app/hooks";
|
||||||
|
import { FormControlLabel, Switch, Typography, Box } from "@linagora/twake-mui";
|
||||||
|
import React from "react";
|
||||||
|
import { useI18n } from "twake-i18n";
|
||||||
|
import {
|
||||||
|
setAlarmEmails,
|
||||||
|
updateUserConfigurationsAsync,
|
||||||
|
} from "../User/userSlice";
|
||||||
|
|
||||||
|
interface NotificationsSettingsProps {
|
||||||
|
onAlarmEmailsError: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function NotificationsSettings({
|
||||||
|
onAlarmEmailsError,
|
||||||
|
}: NotificationsSettingsProps) {
|
||||||
|
const dispatch = useAppDispatch();
|
||||||
|
const { t } = useI18n();
|
||||||
|
const alarmEmailsEnabled = useAppSelector(
|
||||||
|
(state) => state.user?.alarmEmailsEnabled ?? true
|
||||||
|
);
|
||||||
|
|
||||||
|
const handleAlarmEmailsToggle = (
|
||||||
|
event: React.ChangeEvent<HTMLInputElement>
|
||||||
|
) => {
|
||||||
|
const newValue = event.target.checked;
|
||||||
|
const previousValue = alarmEmailsEnabled;
|
||||||
|
dispatch(setAlarmEmails(newValue));
|
||||||
|
dispatch(updateUserConfigurationsAsync({ alarmEmails: newValue }))
|
||||||
|
.unwrap()
|
||||||
|
.catch(() => {
|
||||||
|
dispatch(setAlarmEmails(previousValue));
|
||||||
|
onAlarmEmailsError();
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Box className="settings-tab-content">
|
||||||
|
<Typography variant="h6" sx={{ mb: 3 }}>
|
||||||
|
{t("settings.notifications.deliveryMethod")}
|
||||||
|
</Typography>
|
||||||
|
<FormControlLabel
|
||||||
|
control={
|
||||||
|
<Switch
|
||||||
|
checked={alarmEmailsEnabled}
|
||||||
|
onChange={handleAlarmEmailsToggle}
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
label={t("settings.notifications.email")}
|
||||||
|
labelPlacement="start"
|
||||||
|
sx={{ minWidth: 400, justifyContent: "space-between", marginLeft: 0 }}
|
||||||
|
/>
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -1,45 +1,24 @@
|
|||||||
import { useAppDispatch, useAppSelector } from "@/app/hooks";
|
import { useAppDispatch } from "@/app/hooks";
|
||||||
import { useTimeZoneList } from "@/components/Calendar/TimezoneSelector";
|
|
||||||
import { TimezoneAutocomplete } from "@/components/Timezone/TimezoneAutocomplete";
|
|
||||||
import { browserDefaultTimeZone, getTimezoneOffset } from "@/utils/timezone";
|
|
||||||
import {
|
import {
|
||||||
Box,
|
Box,
|
||||||
FormControl,
|
|
||||||
FormControlLabel,
|
|
||||||
IconButton,
|
IconButton,
|
||||||
List,
|
List,
|
||||||
ListItem,
|
ListItem,
|
||||||
ListItemIcon,
|
ListItemIcon,
|
||||||
ListItemText,
|
ListItemText,
|
||||||
MenuItem,
|
|
||||||
Select,
|
|
||||||
SelectChangeEvent,
|
|
||||||
Snackbar,
|
Snackbar,
|
||||||
Switch,
|
|
||||||
Tab,
|
Tab,
|
||||||
Tabs,
|
Tabs,
|
||||||
Typography,
|
Typography,
|
||||||
} from "@linagora/twake-mui";
|
} from "@linagora/twake-mui";
|
||||||
import ArrowBackIcon from "@mui/icons-material/ArrowBack";
|
import ArrowBackIcon from "@mui/icons-material/ArrowBack";
|
||||||
import SettingsIcon from "@mui/icons-material/Settings";
|
import SettingsIcon from "@mui/icons-material/Settings";
|
||||||
import React, { useState } from "react";
|
import { useState } from "react";
|
||||||
import { useI18n } from "twake-i18n";
|
import { useI18n } from "twake-i18n";
|
||||||
import {
|
import { GeneralSettings } from "./GeneralSettings";
|
||||||
setAlarmEmails,
|
import { NotificationsSettings } from "./NotificationSettings";
|
||||||
setLanguage as setUserLanguage,
|
|
||||||
setTimezone as setUserTimeZone,
|
|
||||||
updateUserConfigurationsAsync,
|
|
||||||
} from "../User/userSlice";
|
|
||||||
import { AVAILABLE_LANGUAGES } from "./constants";
|
|
||||||
import "./SettingsPage.styl";
|
import "./SettingsPage.styl";
|
||||||
import {
|
import { setView } from "./SettingsSlice";
|
||||||
setDisplayWeekNumbers,
|
|
||||||
setHideDeclinedEvents,
|
|
||||||
setIsBrowserDefaultTimeZone,
|
|
||||||
setLanguage as setSettingsLanguage,
|
|
||||||
setTimeZone as setSettingsTimeZone,
|
|
||||||
setView,
|
|
||||||
} from "./SettingsSlice";
|
|
||||||
|
|
||||||
type SidebarNavItem = "settings" | "sync";
|
type SidebarNavItem = "settings" | "sync";
|
||||||
type SettingsSubTab = "settings" | "notifications";
|
type SettingsSubTab = "settings" | "notifications";
|
||||||
@@ -47,39 +26,12 @@ type SettingsSubTab = "settings" | "notifications";
|
|||||||
export default function SettingsPage({ isInIframe }: { isInIframe?: boolean }) {
|
export default function SettingsPage({ isInIframe }: { isInIframe?: boolean }) {
|
||||||
const dispatch = useAppDispatch();
|
const dispatch = useAppDispatch();
|
||||||
const { t } = useI18n();
|
const { t } = useI18n();
|
||||||
const previousConfig = useAppSelector((state) => state.user.coreConfig);
|
|
||||||
const userLanguage = useAppSelector(
|
|
||||||
(state) => state.user?.coreConfig.language
|
|
||||||
);
|
|
||||||
const settingsLanguage = useAppSelector((state) => state.settings?.language);
|
|
||||||
const currentLanguage = userLanguage || settingsLanguage || "en";
|
|
||||||
|
|
||||||
const timezoneList = useTimeZoneList();
|
|
||||||
const userTimeZone = useAppSelector(
|
|
||||||
(state) => state.user?.coreConfig?.datetime?.timeZone
|
|
||||||
);
|
|
||||||
const settingTimeZone = useAppSelector((state) => state.settings?.timeZone);
|
|
||||||
const currentTimeZone =
|
|
||||||
userTimeZone ?? settingTimeZone ?? browserDefaultTimeZone;
|
|
||||||
const isBrowserDefault = useAppSelector(
|
|
||||||
(state) => state.settings.isBrowserDefaultTimeZone
|
|
||||||
);
|
|
||||||
const alarmEmailsEnabled = useAppSelector(
|
|
||||||
(state) => state.user?.alarmEmailsEnabled ?? true
|
|
||||||
);
|
|
||||||
|
|
||||||
const hideDeclinedEvents = useAppSelector(
|
|
||||||
(state) => state.settings?.hideDeclinedEvents
|
|
||||||
);
|
|
||||||
|
|
||||||
const displayWeekNumbers = useAppSelector(
|
|
||||||
(state) => state.settings?.displayWeekNumbers
|
|
||||||
);
|
|
||||||
|
|
||||||
const [activeNavItem, setActiveNavItem] =
|
const [activeNavItem, setActiveNavItem] =
|
||||||
useState<SidebarNavItem>("settings");
|
useState<SidebarNavItem>("settings");
|
||||||
const [activeSettingsSubTab, setActiveSettingsSubTab] =
|
const [activeSettingsSubTab, setActiveSettingsSubTab] =
|
||||||
useState<SettingsSubTab>("settings");
|
useState<SettingsSubTab>("settings");
|
||||||
|
|
||||||
const [languageErrorOpen, setLanguageErrorOpen] = useState(false);
|
const [languageErrorOpen, setLanguageErrorOpen] = useState(false);
|
||||||
const [timeZoneErrorOpen, setTimeZoneErrorOpen] = useState(false);
|
const [timeZoneErrorOpen, setTimeZoneErrorOpen] = useState(false);
|
||||||
const [alarmEmailsErrorOpen, setAlarmEmailsErrorOpen] = useState(false);
|
const [alarmEmailsErrorOpen, setAlarmEmailsErrorOpen] = useState(false);
|
||||||
@@ -87,6 +39,8 @@ export default function SettingsPage({ isInIframe }: { isInIframe?: boolean }) {
|
|||||||
useState(false);
|
useState(false);
|
||||||
const [displayWeekNumbersErrorOpen, setDisplayWeekNumbersErrorOpen] =
|
const [displayWeekNumbersErrorOpen, setDisplayWeekNumbersErrorOpen] =
|
||||||
useState(false);
|
useState(false);
|
||||||
|
const [workingDaysErrorOpen, setWorkingDaysErrorOpen] = useState(false);
|
||||||
|
|
||||||
const handleBackClick = () => {
|
const handleBackClick = () => {
|
||||||
dispatch(setView("calendar"));
|
dispatch(setView("calendar"));
|
||||||
};
|
};
|
||||||
@@ -104,143 +58,18 @@ export default function SettingsPage({ isInIframe }: { isInIframe?: boolean }) {
|
|||||||
) => {
|
) => {
|
||||||
setActiveSettingsSubTab(newValue);
|
setActiveSettingsSubTab(newValue);
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleLanguageChange = (event: SelectChangeEvent<string>) => {
|
|
||||||
const newLanguage = event.target.value;
|
|
||||||
const previousLanguage = currentLanguage;
|
|
||||||
|
|
||||||
// Optimistic update - update UI immediately
|
|
||||||
dispatch(setUserLanguage(newLanguage));
|
|
||||||
dispatch(setSettingsLanguage(newLanguage));
|
|
||||||
|
|
||||||
// Call API in background, don't wait for it
|
|
||||||
dispatch(updateUserConfigurationsAsync({ language: newLanguage }))
|
|
||||||
.unwrap()
|
|
||||||
.catch((error) => {
|
|
||||||
console.error("Failed to update language:", error);
|
|
||||||
// Rollback on error
|
|
||||||
dispatch(setUserLanguage(previousLanguage));
|
|
||||||
dispatch(setSettingsLanguage(previousLanguage));
|
|
||||||
setLanguageErrorOpen(true);
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleLanguageErrorClose = () => {
|
const handleLanguageErrorClose = () => {
|
||||||
setLanguageErrorOpen(false);
|
setLanguageErrorOpen(false);
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleTimeZoneChange = (newTimeZone: string) => {
|
|
||||||
const previousTimeZone = currentTimeZone;
|
|
||||||
|
|
||||||
// Optimistic update - update UI immediately
|
|
||||||
dispatch(setUserTimeZone(newTimeZone));
|
|
||||||
dispatch(setSettingsTimeZone(newTimeZone));
|
|
||||||
|
|
||||||
// Call API in background, don't wait for it
|
|
||||||
dispatch(
|
|
||||||
updateUserConfigurationsAsync({ timezone: newTimeZone, previousConfig })
|
|
||||||
)
|
|
||||||
.unwrap()
|
|
||||||
.catch((error) => {
|
|
||||||
console.error("Failed to update TimeZone:", error);
|
|
||||||
// Rollback on error
|
|
||||||
dispatch(setUserTimeZone(previousTimeZone));
|
|
||||||
dispatch(setSettingsTimeZone(previousTimeZone));
|
|
||||||
setTimeZoneErrorOpen(true);
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleTimeZoneDefaultChange = (isDefault: boolean) => {
|
|
||||||
const previousTimeZone = currentTimeZone;
|
|
||||||
|
|
||||||
// Optimistic update - update UI immediately
|
|
||||||
dispatch(setIsBrowserDefaultTimeZone(isDefault));
|
|
||||||
if (isDefault) {
|
|
||||||
dispatch(setUserTimeZone(null));
|
|
||||||
dispatch(setSettingsTimeZone(browserDefaultTimeZone));
|
|
||||||
|
|
||||||
// Call API in background, don't wait for it
|
|
||||||
dispatch(
|
|
||||||
updateUserConfigurationsAsync({ timezone: null, previousConfig })
|
|
||||||
)
|
|
||||||
.unwrap()
|
|
||||||
.catch((error) => {
|
|
||||||
console.error("Failed to update TimeZone:", error);
|
|
||||||
// Rollback on error
|
|
||||||
dispatch(setUserTimeZone(previousTimeZone));
|
|
||||||
dispatch(setSettingsTimeZone(previousTimeZone));
|
|
||||||
dispatch(setIsBrowserDefaultTimeZone(!isDefault));
|
|
||||||
setTimeZoneErrorOpen(true);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleTimeZoneErrorClose = () => {
|
const handleTimeZoneErrorClose = () => {
|
||||||
setTimeZoneErrorOpen(false);
|
setTimeZoneErrorOpen(false);
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleHideDeclinedEvents = (doHideDeclinedEvents: boolean) => {
|
|
||||||
// Optimistic update - update UI immediately
|
|
||||||
dispatch(setHideDeclinedEvents(doHideDeclinedEvents));
|
|
||||||
|
|
||||||
// Call API in background, don't wait for it
|
|
||||||
dispatch(
|
|
||||||
updateUserConfigurationsAsync({
|
|
||||||
hideDeclinedEvents: doHideDeclinedEvents,
|
|
||||||
})
|
|
||||||
)
|
|
||||||
.unwrap()
|
|
||||||
.catch((error) => {
|
|
||||||
console.error("Failed to update hide declined event:", error);
|
|
||||||
dispatch(setHideDeclinedEvents(!doHideDeclinedEvents));
|
|
||||||
setHideDeclinedEventsErrorOpen(true);
|
|
||||||
});
|
|
||||||
};
|
|
||||||
const handleHideDeclinedEventsErrorClose = () => {
|
const handleHideDeclinedEventsErrorClose = () => {
|
||||||
setHideDeclinedEventsErrorOpen(false);
|
setHideDeclinedEventsErrorOpen(false);
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleAlarmEmailsToggle = (
|
|
||||||
event: React.ChangeEvent<HTMLInputElement>
|
|
||||||
) => {
|
|
||||||
const newValue = event.target.checked;
|
|
||||||
const previousValue = alarmEmailsEnabled;
|
|
||||||
|
|
||||||
// Optimistic update - update UI immediately
|
|
||||||
dispatch(setAlarmEmails(newValue));
|
|
||||||
|
|
||||||
// Call API in background, don't wait for it
|
|
||||||
dispatch(updateUserConfigurationsAsync({ alarmEmails: newValue }))
|
|
||||||
.unwrap()
|
|
||||||
.catch((error) => {
|
|
||||||
console.error("Failed to update alarm emails:", error);
|
|
||||||
// Rollback on error
|
|
||||||
dispatch(setAlarmEmails(previousValue));
|
|
||||||
setAlarmEmailsErrorOpen(true);
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleAlarmEmailsErrorClose = () => {
|
const handleAlarmEmailsErrorClose = () => {
|
||||||
setAlarmEmailsErrorOpen(false);
|
setAlarmEmailsErrorOpen(false);
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleDisplayWeekNumbers = (doDisplayWeekNumbers: boolean) => {
|
|
||||||
// Optimistic update - update UI immediately
|
|
||||||
dispatch(setDisplayWeekNumbers(doDisplayWeekNumbers));
|
|
||||||
|
|
||||||
// Call API in background, don't wait for it
|
|
||||||
dispatch(
|
|
||||||
updateUserConfigurationsAsync({
|
|
||||||
displayWeekNumbers: doDisplayWeekNumbers,
|
|
||||||
})
|
|
||||||
)
|
|
||||||
.unwrap()
|
|
||||||
.catch((error) => {
|
|
||||||
console.error("Failed to update the week number setting:", error);
|
|
||||||
dispatch(setDisplayWeekNumbers(!doDisplayWeekNumbers));
|
|
||||||
setDisplayWeekNumbersErrorOpen(true);
|
|
||||||
});
|
|
||||||
};
|
|
||||||
const handleDisplayWeekNumbersErrorClose = () => {
|
const handleDisplayWeekNumbersErrorClose = () => {
|
||||||
setDisplayWeekNumbersErrorOpen(false);
|
setDisplayWeekNumbersErrorOpen(false);
|
||||||
};
|
};
|
||||||
@@ -300,157 +129,33 @@ export default function SettingsPage({ isInIframe }: { isInIframe?: boolean }) {
|
|||||||
{activeNavItem === "settings" && (
|
{activeNavItem === "settings" && (
|
||||||
<>
|
<>
|
||||||
{activeSettingsSubTab === "settings" && (
|
{activeSettingsSubTab === "settings" && (
|
||||||
<Box className="settings-tab-content">
|
<GeneralSettings
|
||||||
<Box sx={{ mb: 6 }}>
|
onLanguageError={() => setLanguageErrorOpen(true)}
|
||||||
<Typography variant="h6" sx={{ mb: 1 }}>
|
onTimeZoneError={() => setTimeZoneErrorOpen(true)}
|
||||||
{t("settings.language") || "Language"}
|
onHideDeclinedEventsError={() =>
|
||||||
</Typography>
|
setHideDeclinedEventsErrorOpen(true)
|
||||||
<Typography
|
}
|
||||||
variant="body2"
|
onDisplayWeekNumbersError={() =>
|
||||||
color="text.secondary"
|
setDisplayWeekNumbersErrorOpen(true)
|
||||||
sx={{ mb: 3 }}
|
}
|
||||||
>
|
onWorkingDaysError={() => setWorkingDaysErrorOpen(true)}
|
||||||
{t("settings.languageDescription") ||
|
/>
|
||||||
"This will be the language used in your Twake Calendar"}
|
|
||||||
</Typography>
|
|
||||||
<FormControl size="small" sx={{ minWidth: 500 }}>
|
|
||||||
<Select
|
|
||||||
value={currentLanguage}
|
|
||||||
onChange={handleLanguageChange}
|
|
||||||
variant="outlined"
|
|
||||||
aria-label={
|
|
||||||
t("settings.languageSelector") || "Language selector"
|
|
||||||
}
|
|
||||||
>
|
|
||||||
{AVAILABLE_LANGUAGES.map(({ code, label }) => (
|
|
||||||
<MenuItem key={code} value={code}>
|
|
||||||
{label}
|
|
||||||
</MenuItem>
|
|
||||||
))}
|
|
||||||
</Select>
|
|
||||||
</FormControl>
|
|
||||||
</Box>
|
|
||||||
<Box>
|
|
||||||
<Typography variant="h6" sx={{ mb: 1 }}>
|
|
||||||
{t("settings.timeZone")}
|
|
||||||
</Typography>
|
|
||||||
<Box
|
|
||||||
sx={{
|
|
||||||
mb: 6,
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<FormControl size="small" sx={{ minWidth: 500 }}>
|
|
||||||
<FormControlLabel
|
|
||||||
control={
|
|
||||||
<Switch
|
|
||||||
checked={isBrowserDefault}
|
|
||||||
onChange={() =>
|
|
||||||
handleTimeZoneDefaultChange(!isBrowserDefault)
|
|
||||||
}
|
|
||||||
aria-label={t("settings.timeZoneBrowserDefault")}
|
|
||||||
/>
|
|
||||||
}
|
|
||||||
label={t("settings.timeZoneBrowserDefault")}
|
|
||||||
labelPlacement="start"
|
|
||||||
sx={{
|
|
||||||
minWidth: 400,
|
|
||||||
justifyContent: "space-between",
|
|
||||||
marginLeft: 0,
|
|
||||||
mb: 2,
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
{!isBrowserDefault && (
|
|
||||||
<TimezoneAutocomplete
|
|
||||||
value={currentTimeZone}
|
|
||||||
zones={timezoneList.zones}
|
|
||||||
getTimezoneOffset={getTimezoneOffset}
|
|
||||||
onChange={handleTimeZoneChange}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</FormControl>
|
|
||||||
</Box>
|
|
||||||
</Box>
|
|
||||||
<Box sx={{ mb: 6 }}>
|
|
||||||
<Typography variant="h6" sx={{ mb: 1 }}>
|
|
||||||
{t("settings.calAndEvent")}
|
|
||||||
</Typography>
|
|
||||||
<FormControl size="small" sx={{ minWidth: 500 }}>
|
|
||||||
<FormControlLabel
|
|
||||||
control={
|
|
||||||
<Switch
|
|
||||||
checked={Boolean(!hideDeclinedEvents)}
|
|
||||||
onChange={() =>
|
|
||||||
handleHideDeclinedEvents(!hideDeclinedEvents)
|
|
||||||
}
|
|
||||||
aria-label={t("settings.showDeclinedEvent")}
|
|
||||||
/>
|
|
||||||
}
|
|
||||||
label={t("settings.showDeclinedEvent")}
|
|
||||||
labelPlacement="start"
|
|
||||||
sx={{
|
|
||||||
minWidth: 400,
|
|
||||||
justifyContent: "space-between",
|
|
||||||
marginLeft: 0,
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
<FormControlLabel
|
|
||||||
control={
|
|
||||||
<Switch
|
|
||||||
checked={Boolean(displayWeekNumbers)}
|
|
||||||
onChange={() =>
|
|
||||||
handleDisplayWeekNumbers(!displayWeekNumbers)
|
|
||||||
}
|
|
||||||
aria-label={t("settings.displayWeekNumbers")}
|
|
||||||
/>
|
|
||||||
}
|
|
||||||
label={t("settings.displayWeekNumbers")}
|
|
||||||
labelPlacement="start"
|
|
||||||
sx={{
|
|
||||||
minWidth: 400,
|
|
||||||
justifyContent: "space-between",
|
|
||||||
marginLeft: 0,
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</FormControl>
|
|
||||||
</Box>
|
|
||||||
</Box>
|
|
||||||
)}
|
)}
|
||||||
{activeSettingsSubTab === "notifications" && (
|
{activeSettingsSubTab === "notifications" && (
|
||||||
<Box className="settings-tab-content">
|
<NotificationsSettings
|
||||||
<Typography variant="h6" sx={{ mb: 3 }}>
|
onAlarmEmailsError={() => setAlarmEmailsErrorOpen(true)}
|
||||||
{t("settings.notifications.deliveryMethod") ||
|
/>
|
||||||
"Delivery method"}
|
|
||||||
</Typography>
|
|
||||||
<FormControlLabel
|
|
||||||
control={
|
|
||||||
<Switch
|
|
||||||
checked={alarmEmailsEnabled}
|
|
||||||
onChange={handleAlarmEmailsToggle}
|
|
||||||
aria-label={
|
|
||||||
t("settings.notifications.email") || "Email"
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
}
|
|
||||||
label={t("settings.notifications.email") || "Email"}
|
|
||||||
labelPlacement="start"
|
|
||||||
sx={{
|
|
||||||
minWidth: 400,
|
|
||||||
justifyContent: "space-between",
|
|
||||||
marginLeft: 0,
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</Box>
|
|
||||||
)}
|
)}
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
{activeNavItem === "sync" && (
|
|
||||||
<Box className="settings-tab-content">
|
|
||||||
<Typography variant="body1" color="text.secondary">
|
|
||||||
{t("settings.sync.empty") || "Sync settings coming soon"}
|
|
||||||
</Typography>
|
|
||||||
</Box>
|
|
||||||
)}
|
|
||||||
</Box>
|
</Box>
|
||||||
|
{activeNavItem === "sync" && (
|
||||||
|
<Box className="settings-tab-content">
|
||||||
|
<Typography variant="body1" color="text.secondary">
|
||||||
|
{t("settings.sync.empty") || "Sync settings coming soon"}
|
||||||
|
</Typography>
|
||||||
|
</Box>
|
||||||
|
)}
|
||||||
</Box>
|
</Box>
|
||||||
<Snackbar
|
<Snackbar
|
||||||
open={languageErrorOpen}
|
open={languageErrorOpen}
|
||||||
@@ -475,7 +180,6 @@ export default function SettingsPage({ isInIframe }: { isInIframe?: boolean }) {
|
|||||||
"Failed to update email notifications setting"
|
"Failed to update email notifications setting"
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<Snackbar
|
<Snackbar
|
||||||
open={hideDeclinedEventsErrorOpen}
|
open={hideDeclinedEventsErrorOpen}
|
||||||
autoHideDuration={4000}
|
autoHideDuration={4000}
|
||||||
@@ -488,6 +192,12 @@ export default function SettingsPage({ isInIframe }: { isInIframe?: boolean }) {
|
|||||||
onClose={handleDisplayWeekNumbersErrorClose}
|
onClose={handleDisplayWeekNumbersErrorClose}
|
||||||
message={t("settings.displayWeekNumbersUpdateError")}
|
message={t("settings.displayWeekNumbersUpdateError")}
|
||||||
/>
|
/>
|
||||||
|
<Snackbar
|
||||||
|
open={workingDaysErrorOpen}
|
||||||
|
autoHideDuration={4000}
|
||||||
|
onClose={() => setWorkingDaysErrorOpen(false)}
|
||||||
|
message={t("settings.workingDaysUpdateError")}
|
||||||
|
/>
|
||||||
</main>
|
</main>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -10,8 +10,14 @@ export interface SettingsState {
|
|||||||
hideDeclinedEvents: boolean | null;
|
hideDeclinedEvents: boolean | null;
|
||||||
displayWeekNumbers: boolean;
|
displayWeekNumbers: boolean;
|
||||||
view: "calendar" | "settings" | "search";
|
view: "calendar" | "settings" | "search";
|
||||||
|
businessHours: BusinessHour | null;
|
||||||
|
workingDays: boolean | null;
|
||||||
|
}
|
||||||
|
export interface BusinessHour {
|
||||||
|
start: string;
|
||||||
|
end: string;
|
||||||
|
daysOfWeek: number[];
|
||||||
}
|
}
|
||||||
|
|
||||||
const savedLang = localStorage.getItem("lang");
|
const savedLang = localStorage.getItem("lang");
|
||||||
const defaultLang = savedLang ?? window.LANG ?? "en";
|
const defaultLang = savedLang ?? window.LANG ?? "en";
|
||||||
|
|
||||||
@@ -27,6 +33,8 @@ const initialState: SettingsState = {
|
|||||||
hideDeclinedEvents: null,
|
hideDeclinedEvents: null,
|
||||||
displayWeekNumbers: true,
|
displayWeekNumbers: true,
|
||||||
view: "calendar",
|
view: "calendar",
|
||||||
|
businessHours: null,
|
||||||
|
workingDays: null,
|
||||||
};
|
};
|
||||||
|
|
||||||
export const settingsSlice = createSlice({
|
export const settingsSlice = createSlice({
|
||||||
@@ -56,6 +64,12 @@ export const settingsSlice = createSlice({
|
|||||||
) => {
|
) => {
|
||||||
state.view = action.payload;
|
state.view = action.payload;
|
||||||
},
|
},
|
||||||
|
setBusinessHours: (state, action: PayloadAction<BusinessHour | null>) => {
|
||||||
|
state.businessHours = action.payload ?? null;
|
||||||
|
},
|
||||||
|
setWorkingDays: (state, action: PayloadAction<boolean | null>) => {
|
||||||
|
state.workingDays = action.payload;
|
||||||
|
},
|
||||||
},
|
},
|
||||||
extraReducers: (builder) => {
|
extraReducers: (builder) => {
|
||||||
builder.addCase(getOpenPaasUserDataAsync.fulfilled, (state, action) => {
|
builder.addCase(getOpenPaasUserDataAsync.fulfilled, (state, action) => {
|
||||||
@@ -90,6 +104,23 @@ export const settingsSlice = createSlice({
|
|||||||
? hideDeclinedEventsConfig.value
|
? hideDeclinedEventsConfig.value
|
||||||
: null;
|
: null;
|
||||||
|
|
||||||
|
const businessHoursConfig = coreModule?.configurations?.find(
|
||||||
|
(config: ConfigurationItem) => config.name === "businessHours"
|
||||||
|
);
|
||||||
|
state.businessHours =
|
||||||
|
Array.isArray(businessHoursConfig?.value) &&
|
||||||
|
businessHoursConfig?.value?.[0]
|
||||||
|
? businessHoursConfig.value[0]
|
||||||
|
: null;
|
||||||
|
|
||||||
|
// From esnCalendarModule (alongside hideDeclinedEvents)
|
||||||
|
const workingDaysConfig = esnCalendarModule?.configurations?.find(
|
||||||
|
(config: ConfigurationItem) => config.name === "workingDays"
|
||||||
|
);
|
||||||
|
state.workingDays =
|
||||||
|
typeof workingDaysConfig?.value === "boolean"
|
||||||
|
? workingDaysConfig.value
|
||||||
|
: null;
|
||||||
const calendarModule = action.payload.configurations?.modules?.find(
|
const calendarModule = action.payload.configurations?.modules?.find(
|
||||||
(module: ModuleConfiguration) => module.name === "calendar"
|
(module: ModuleConfiguration) => module.name === "calendar"
|
||||||
);
|
);
|
||||||
@@ -112,5 +143,7 @@ export const {
|
|||||||
setIsBrowserDefaultTimeZone,
|
setIsBrowserDefaultTimeZone,
|
||||||
setHideDeclinedEvents,
|
setHideDeclinedEvents,
|
||||||
setDisplayWeekNumbers,
|
setDisplayWeekNumbers,
|
||||||
|
setBusinessHours,
|
||||||
|
setWorkingDays,
|
||||||
} = settingsSlice.actions;
|
} = settingsSlice.actions;
|
||||||
export default settingsSlice.reducer;
|
export default settingsSlice.reducer;
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import { User } from "@/components/Attendees/PeopleSearch";
|
import { User } from "@/components/Attendees/PeopleSearch";
|
||||||
import { api } from "@/utils/apiUtils";
|
import { api } from "@/utils/apiUtils";
|
||||||
|
import { BusinessHour } from "../Settings/SettingsSlice";
|
||||||
import { OpenPaasUserData } from "./type/OpenPaasUserData";
|
import { OpenPaasUserData } from "./type/OpenPaasUserData";
|
||||||
import {
|
import {
|
||||||
ConfigurationItem,
|
ConfigurationItem,
|
||||||
@@ -49,6 +50,8 @@ export interface UserConfigurationUpdates {
|
|||||||
previousConfig?: Record<string, unknown>;
|
previousConfig?: Record<string, unknown>;
|
||||||
alarmEmails?: boolean;
|
alarmEmails?: boolean;
|
||||||
hideDeclinedEvents?: boolean;
|
hideDeclinedEvents?: boolean;
|
||||||
|
workingDays?: boolean;
|
||||||
|
businessHours?: BusinessHour | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function updateUserConfigurations(
|
export async function updateUserConfigurations(
|
||||||
@@ -76,22 +79,34 @@ export async function updateUserConfigurations(
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
if (updates.businessHours !== undefined) {
|
||||||
|
coreConfigs.push({
|
||||||
|
name: "businessHours",
|
||||||
|
value: updates.businessHours ? [updates.businessHours] : [],
|
||||||
|
});
|
||||||
|
}
|
||||||
if (updates.alarmEmails !== undefined) {
|
if (updates.alarmEmails !== undefined) {
|
||||||
calendarConfigs.push({
|
calendarConfigs.push({
|
||||||
name: "alarmEmails",
|
name: "alarmEmails",
|
||||||
value: updates.alarmEmails,
|
value: updates.alarmEmails,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
if (updates.displayWeekNumbers !== undefined) {
|
||||||
|
calendarConfigs.push({
|
||||||
|
name: "displayWeekNumbers",
|
||||||
|
value: updates.displayWeekNumbers,
|
||||||
|
});
|
||||||
|
}
|
||||||
if (updates.hideDeclinedEvents !== undefined) {
|
if (updates.hideDeclinedEvents !== undefined) {
|
||||||
esnCalendarConfigs.push({
|
esnCalendarConfigs.push({
|
||||||
name: "hideDeclinedEvents",
|
name: "hideDeclinedEvents",
|
||||||
value: updates.hideDeclinedEvents,
|
value: updates.hideDeclinedEvents,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
if (updates.displayWeekNumbers !== undefined) {
|
if (updates.workingDays !== undefined) {
|
||||||
calendarConfigs.push({
|
esnCalendarConfigs.push({
|
||||||
name: "displayWeekNumbers",
|
name: "workingDays",
|
||||||
value: updates.displayWeekNumbers,
|
value: updates.workingDays,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+4
-1
@@ -257,7 +257,10 @@
|
|||||||
"calAndEvent": "Calendar and events",
|
"calAndEvent": "Calendar and events",
|
||||||
"showDeclinedEvent": "Show declined events",
|
"showDeclinedEvent": "Show declined events",
|
||||||
"hideDeclinedEventsUpdateError": "Failed to update hide declined event.",
|
"hideDeclinedEventsUpdateError": "Failed to update hide declined event.",
|
||||||
"back": "Back to calendar"
|
"back": "Back to calendar",
|
||||||
|
"chooseWorkingDays": "Choose your working days",
|
||||||
|
"showOnlyWorkingDays": "Show only working days",
|
||||||
|
"workingDaysUpdateError": "Failed to update working days setting"
|
||||||
},
|
},
|
||||||
"eventPreview": {
|
"eventPreview": {
|
||||||
"emailAttendees": "Email attendees",
|
"emailAttendees": "Email attendees",
|
||||||
|
|||||||
+4
-1
@@ -258,7 +258,10 @@
|
|||||||
"calAndEvent": "Calendrier et événements",
|
"calAndEvent": "Calendrier et événements",
|
||||||
"showDeclinedEvent": "Afficher les événements déclinés",
|
"showDeclinedEvent": "Afficher les événements déclinés",
|
||||||
"hideDeclinedEventsUpdateError": "Échec de la mise à jour du masquage des événements déclinés.",
|
"hideDeclinedEventsUpdateError": "Échec de la mise à jour du masquage des événements déclinés.",
|
||||||
"back": "Retour au calendrier"
|
"back": "Retour au calendrier",
|
||||||
|
"chooseWorkingDays": "Choisissez vos jours ouvrés",
|
||||||
|
"showOnlyWorkingDays": "Afficher uniquement les jours sélectionnés",
|
||||||
|
"workingDaysUpdateError": "Échec de la mise à jour des jours ouvrés"
|
||||||
},
|
},
|
||||||
"eventPreview": {
|
"eventPreview": {
|
||||||
"emailAttendees": "Envoyer un e-mail aux participants",
|
"emailAttendees": "Envoyer un e-mail aux participants",
|
||||||
|
|||||||
+4
-1
@@ -258,7 +258,10 @@
|
|||||||
"calAndEvent": "Календарь и мероприятия",
|
"calAndEvent": "Календарь и мероприятия",
|
||||||
"showDeclinedEvent": "Показать отклонённые мероприятия",
|
"showDeclinedEvent": "Показать отклонённые мероприятия",
|
||||||
"hideDeclinedEventsUpdateError": "Не удалось обновить скрытие отклонённых событий.",
|
"hideDeclinedEventsUpdateError": "Не удалось обновить скрытие отклонённых событий.",
|
||||||
"back": "Вернуться к календарю"
|
"back": "Вернуться к календарю",
|
||||||
|
"chooseWorkingDays": "Выберите ваши рабочие дни",
|
||||||
|
"showOnlyWorkingDays": "Показывать только рабочие дни",
|
||||||
|
"workingDaysUpdateError": "Не удалось обновить настройку рабочих дней"
|
||||||
},
|
},
|
||||||
"eventPreview": {
|
"eventPreview": {
|
||||||
"emailAttendees": "Написать участникам",
|
"emailAttendees": "Написать участникам",
|
||||||
|
|||||||
+4
-1
@@ -256,7 +256,10 @@
|
|||||||
"calAndEvent": "Lịch và sự kiện",
|
"calAndEvent": "Lịch và sự kiện",
|
||||||
"showDeclinedEvent": "Hiển thị các sự kiện đã từ chối",
|
"showDeclinedEvent": "Hiển thị các sự kiện đã từ chối",
|
||||||
"hideDeclinedEventsUpdateError": "Không cập nhật được các sự kiện ẩn bị từ chối.",
|
"hideDeclinedEventsUpdateError": "Không cập nhật được các sự kiện ẩn bị từ chối.",
|
||||||
"back": "Quay lại lịch"
|
"back": "Quay lại lịch",
|
||||||
|
"chooseWorkingDays": "Chọn ngày làm việc của bạn",
|
||||||
|
"showOnlyWorkingDays": "Chỉ hiển thị ngày làm việc",
|
||||||
|
"workingDaysUpdateError": "Không thể cập nhật cài đặt ngày làm việc"
|
||||||
},
|
},
|
||||||
"eventPreview": {
|
"eventPreview": {
|
||||||
"emailAttendees": "Gửi email cho người tham gia",
|
"emailAttendees": "Gửi email cho người tham gia",
|
||||||
|
|||||||
Reference in New Issue
Block a user