diff --git a/.eslintrc.json b/.eslintrc.json deleted file mode 100644 index f298005..0000000 --- a/.eslintrc.json +++ /dev/null @@ -1,3 +0,0 @@ -{ - "extends": ["react-app", "react-app/jest"] -} diff --git a/Jenkinsfile b/Jenkinsfile index c09614e..621a28b 100644 --- a/Jenkinsfile +++ b/Jenkinsfile @@ -33,6 +33,11 @@ pipeline { sh 'npm run test' } } + stage('Check Lint') { + steps { + sh 'npm run lint' + } + } stage('Check Formatting') { steps { sh 'npx prettier --check .' diff --git a/__test__/components/Calendar.test.tsx b/__test__/components/Calendar.test.tsx index f699d55..0411b42 100644 --- a/__test__/components/Calendar.test.tsx +++ b/__test__/components/Calendar.test.tsx @@ -532,7 +532,7 @@ describe("calendar Availability search", () => { }) ); - const calendarRef = (window as any).__calendarRef; + const calendarRef = window.__calendarRef; await waitFor(() => { expect(calendarRef.current).not.toBeNull(); @@ -573,7 +573,7 @@ describe("calendar Availability search", () => { expect(spy).toHaveBeenCalled(); }); - const calendarRef = (window as any).__calendarRef; + const calendarRef = window.__calendarRef; const calendarApi = calendarRef.current; const view = calendarApi?.view; await act(async () => { diff --git a/__test__/features/Calendars/CalendarModal.test.tsx b/__test__/features/Calendars/CalendarModal.test.tsx index 330f4ec..13fc534 100644 --- a/__test__/features/Calendars/CalendarModal.test.tsx +++ b/__test__/features/Calendars/CalendarModal.test.tsx @@ -309,7 +309,7 @@ describe("CalendarPopover - Tabs Scenarios", () => { }); it("copies CalDAV link from Access tab", async () => { - (window as any).CALENDAR_BASE_URL = "https://cal.example.org"; + window.CALENDAR_BASE_URL = "https://cal.example.org"; Object.assign(navigator, { clipboard: { writeText: jest.fn() }, }); @@ -443,7 +443,7 @@ describe("CalendarPopover - Tabs Scenarios", () => { }); it("fetches and resets the secret link", async () => { - (window as any).CALENDAR_BASE_URL = "https://cal.example.org"; + window.CALENDAR_BASE_URL = "https://cal.example.org"; (getSecretLink as jest.Mock) .mockResolvedValueOnce({ diff --git a/__test__/features/Calendars/CalendarSlice.test.tsx b/__test__/features/Calendars/CalendarSlice.test.tsx index 6ae6171..96de7e1 100644 --- a/__test__/features/Calendars/CalendarSlice.test.tsx +++ b/__test__/features/Calendars/CalendarSlice.test.tsx @@ -10,15 +10,12 @@ import { Calendar } from "@/features/Calendars/CalendarTypes"; import { addSharedCalendarAsync, createCalendarAsync, - deleteEventAsync, getCalendarDetailAsync, getCalendarsListAsync, getEventAsync, getTempCalendarsListAsync, - moveEventAsync, patchACLCalendarAsync, patchCalendarAsync, - putEventAsync, removeCalendarAsync, } from "@/features/Calendars/services"; import { CalendarEvent } from "@/features/Events/EventsTypes"; diff --git a/__test__/features/Events/EventApi.test.tsx b/__test__/features/Events/EventApi.test.tsx index 3eb11a8..2a21831 100644 --- a/__test__/features/Events/EventApi.test.tsx +++ b/__test__/features/Events/EventApi.test.tsx @@ -68,18 +68,18 @@ describe("eventApi", () => { }); it("putEvent logs when status is 201", async () => { - const consoleLogSpy = jest.spyOn(console, "log").mockImplementation(); + const consoleInfoSpy = jest.spyOn(console, "info").mockImplementation(); const mockResponse = { status: 201, url: "/dav/cals/test.ics" }; (api as unknown as jest.Mock).mockReturnValue(mockResponse); await putEvent(mockEvent); - expect(consoleLogSpy).toHaveBeenCalledWith( + expect(consoleInfoSpy).toHaveBeenCalledWith( "Event created successfully:", "/dav/cals/test.ics" ); - consoleLogSpy.mockRestore(); + consoleInfoSpy.mockRestore(); }); test("moveEvent sends MOVE request with destination header", async () => { diff --git a/__test__/features/Events/EventDisplay.test.tsx b/__test__/features/Events/EventDisplay.test.tsx index 8ae1e48..0fb2a27 100644 --- a/__test__/features/Events/EventDisplay.test.tsx +++ b/__test__/features/Events/EventDisplay.test.tsx @@ -27,7 +27,7 @@ describe("Event Preview Display", () => { beforeEach(() => { jest.clearAllMocks(); sessionStorage.clear(); - (window as any).MAIL_SPA_URL = null; + window.MAIL_SPA_URL = null; }); const preloadedState = { @@ -598,7 +598,7 @@ describe("Event Preview Display", () => { }); }); it("properly render message button when MAIL_SPA_URL is not null and event has attendees", () => { - (window as any).MAIL_SPA_URL = "test"; + window.MAIL_SPA_URL = "test"; renderWithProviders( { expect(screen.getByText("eventPreview.emailAttendees")).toBeInTheDocument(); }); it("doesnt render message button when MAIL_SPA_URL is not null and event has no attendees", () => { - (window as any).MAIL_SPA_URL = "test"; + window.MAIL_SPA_URL = "test"; renderWithProviders( { expect(screen.queryByTestId("EmailIcon")).not.toBeInTheDocument(); }); it("message button opens url with attendees as uri and title as subject", () => { - (window as any).MAIL_SPA_URL = "test"; + window.MAIL_SPA_URL = "test"; const mockOpen = jest.fn(); window.open = mockOpen; @@ -669,7 +669,7 @@ describe("Event Preview Display", () => { }); it("message button encodes special characters in event title correctly", () => { - (window as any).MAIL_SPA_URL = "test"; + window.MAIL_SPA_URL = "test"; const mockOpen = jest.fn(); window.open = mockOpen; @@ -952,7 +952,7 @@ describe("Event Preview Display", () => { beforeEach(() => { jest.clearAllMocks(); - (window as any).MAIL_SPA_URL = null; + window.MAIL_SPA_URL = null; }); const createStateWithAttendees = (attendees: any[]) => ({ diff --git a/__test__/features/websocket/WebSocketGate.test.tsx b/__test__/features/websocket/WebSocketGate.test.tsx index 2784885..ed591d0 100644 --- a/__test__/features/websocket/WebSocketGate.test.tsx +++ b/__test__/features/websocket/WebSocketGate.test.tsx @@ -322,7 +322,7 @@ describe("WebSocketGate", () => { it("should reset reconnection attempts counter on successful connection", async () => { jest.useFakeTimers(); - const consoleLog = jest.spyOn(console, "log").mockImplementation(); + const consoleLog = jest.spyOn(console, "info").mockImplementation(); let onCloseCallback: Function | undefined; (createWebSocketConnection as jest.Mock).mockImplementation( @@ -576,7 +576,7 @@ describe("WebSocketGate", () => { it("should reset attempt counter when online event fires", async () => { jest.useFakeTimers(); - const consoleLog = jest.spyOn(console, "log").mockImplementation(); + const consoleLog = jest.spyOn(console, "info").mockImplementation(); let onCloseCallback: Function | undefined; (createWebSocketConnection as jest.Mock).mockImplementation( diff --git a/__test__/features/websocket/createWebSocketConnection.test.tsx b/__test__/features/websocket/createWebSocketConnection.test.tsx index 5c3154c..e7da29e 100644 --- a/__test__/features/websocket/createWebSocketConnection.test.tsx +++ b/__test__/features/websocket/createWebSocketConnection.test.tsx @@ -49,22 +49,22 @@ describe("createWebSocketConnection", () => { beforeEach(() => { ({ webSocketInstances, mockWebSocket, cleanup } = setupWebsocket()); - (window as any).WEBSOCKET_URL = "wss://calendar.example.com"; + window.WEBSOCKET_URL = "wss://calendar.example.com"; (fetchWebSocketTicket as jest.Mock).mockResolvedValue(mockTicket); }); afterEach(() => { jest.clearAllMocks(); - delete (window as any).WEBSOCKET_URL; - delete (window as any).CALENDAR_BASE_URL; + delete window.WEBSOCKET_URL; + delete window.CALENDAR_BASE_URL; cleanup(); }); /** ---------- Tests ---------- */ it("throws when WEBSOCKET_URL is not defined", async () => { - delete (window as any).WEBSOCKET_URL; + delete window.WEBSOCKET_URL; const mockCallbacks = { onMessage: jest.fn(), }; @@ -88,8 +88,8 @@ describe("createWebSocketConnection", () => { }); it("creates WebSocket with correct URL and ticket without the WEBSOCKET_URL", async () => { - delete (window as any).WEBSOCKET_URL; - (window as any).CALENDAR_BASE_URL = "https://calendar.example.com"; + delete window.WEBSOCKET_URL; + window.CALENDAR_BASE_URL = "https://calendar.example.com"; await createAndOpenConnection(); expect(mockWebSocket).toHaveBeenCalledWith( diff --git a/__test__/features/websocket/utils/notificationStorm.test.tsx b/__test__/features/websocket/utils/notificationStorm.test.tsx index be2160d..483b9b1 100644 --- a/__test__/features/websocket/utils/notificationStorm.test.tsx +++ b/__test__/features/websocket/utils/notificationStorm.test.tsx @@ -57,7 +57,7 @@ describe("websocket messages storm", () => { jest.resetModules(); (getDisplayedCalendarRange as jest.Mock).mockReturnValue(mockRange); (store.getState as jest.Mock).mockReturnValue(mockState); - (window as any).WS_DEBOUNCE_PERIOD_MS = 500; + window.WS_DEBOUNCE_PERIOD_MS = 500; mockAccumulators.calendarsToRefresh = new Map(); mockAccumulators.calendarsToHide = new Set(); mockAccumulators.currentDebouncePeriod = 0; @@ -118,7 +118,7 @@ describe("websocket messages storm", () => { }); it("executes immediately when debounce is disabled", () => { - (window as any).WS_DEBOUNCE_PERIOD_MS = 0; + window.WS_DEBOUNCE_PERIOD_MS = 0; updateCalendars( { "/calendars/cal1/entry1": { syncToken: "abc" } }, diff --git a/__test__/features/websocket/websocketAPI/registerToCalendars.test.tsx b/__test__/features/websocket/websocketAPI/registerToCalendars.test.tsx index 2518bc4..cf20800 100644 --- a/__test__/features/websocket/websocketAPI/registerToCalendars.test.tsx +++ b/__test__/features/websocket/websocketAPI/registerToCalendars.test.tsx @@ -42,16 +42,16 @@ describe("registerToCalendars", () => { }); it("should log registration", () => { - const consoleLogSpy = jest.spyOn(console, "log").mockImplementation(); + const consoleInfoSpy = jest.spyOn(console, "info").mockImplementation(); const calendarURIs = ["/calendars/cal1", "/calendars/cal2"]; registerToCalendars(mockSocket, calendarURIs); - expect(consoleLogSpy).toHaveBeenCalledWith( + expect(consoleInfoSpy).toHaveBeenCalledWith( "Registered to calendars", calendarURIs ); - consoleLogSpy.mockRestore(); + consoleInfoSpy.mockRestore(); }); }); diff --git a/__test__/features/websocket/websocketAPI/unregisterToCalendars.test.tsx b/__test__/features/websocket/websocketAPI/unregisterToCalendars.test.tsx index 923dd5b..bfa77c7 100644 --- a/__test__/features/websocket/websocketAPI/unregisterToCalendars.test.tsx +++ b/__test__/features/websocket/websocketAPI/unregisterToCalendars.test.tsx @@ -43,16 +43,16 @@ describe("unregisterToCalendars", () => { }); it("should log unregistration", () => { - const consoleLogSpy = jest.spyOn(console, "log").mockImplementation(); + const consoleInfoSpy = jest.spyOn(console, "info").mockImplementation(); const calendarURIs = ["/calendars/cal1", "/calendars/cal2"]; unregisterToCalendars(mockSocket, calendarURIs); - expect(consoleLogSpy).toHaveBeenCalledWith( + expect(consoleInfoSpy).toHaveBeenCalledWith( "Unregistered to calendars", calendarURIs ); - consoleLogSpy.mockRestore(); + consoleInfoSpy.mockRestore(); }); }); diff --git a/__test__/utils/calendarUtils.test.tsx b/__test__/utils/calendarUtils.test.tsx index a170f85..1bb7b08 100644 --- a/__test__/utils/calendarUtils.test.tsx +++ b/__test__/utils/calendarUtils.test.tsx @@ -1,10 +1,5 @@ import { getCalendarVisibility } from "@/components/Calendar/utils/calendarUtils"; - -interface AclEntry { - privilege: string; - principal: string; - protected: boolean; -} +import { AclEntry } from "@/features/Calendars/types/CalendarData"; describe("getCalendarVisibility", () => { it("returns 'public' when {DAV:}authenticated has {DAV:}read", () => { diff --git a/eslint.config.js b/eslint.config.js new file mode 100644 index 0000000..cafa9c6 --- /dev/null +++ b/eslint.config.js @@ -0,0 +1,74 @@ +import js from "@eslint/js"; +import tseslint from "typescript-eslint"; +import react from "eslint-plugin-react"; +import reactHooks from "eslint-plugin-react-hooks"; +import importPlugin from "eslint-plugin-import"; +import jest from "eslint-plugin-jest"; +import prettier from "eslint-config-prettier"; + +export default [ + { + ignores: ["dist", "build", "node_modules"], + }, + + js.configs.recommended, + + ...tseslint.configs.recommended, + + { + files: ["**/*.{js,jsx,ts,tsx}"], + + languageOptions: { + parser: tseslint.parser, + parserOptions: { + ecmaVersion: "latest", + sourceType: "module", + ecmaFeatures: { jsx: true }, + project: true, + }, + }, + + plugins: { + react, + "react-hooks": reactHooks, + import: importPlugin, + jest, + }, + + settings: { + react: { + version: "detect", + }, + }, + + rules: { + /* React */ + "react/react-in-jsx-scope": "off", // React 18+ + "react/prop-types": "off", + + /* Hooks */ + "react-hooks/rules-of-hooks": "error", + "react-hooks/exhaustive-deps": "warn", + + /* Imports */ + "import/order": "off", + + /* TS */ + "@typescript-eslint/no-unused-vars": [ + "error", + { argsIgnorePattern: "^_" }, + ], + + /* Jest */ + "jest/no-disabled-tests": "warn", + "jest/no-focused-tests": "error", + + /* Prettier compatibility */ + ...prettier.rules, + + /* No noises */ + "no-debugger": "error", + "no-console": ["warn", { allow: ["info", "warn", "error"] }], + }, + }, +]; diff --git a/package-lock.json b/package-lock.json index 7583aac..f3ad127 100644 --- a/package-lock.json +++ b/package-lock.json @@ -65,7 +65,7 @@ "eslint": "^9.39.1", "eslint-config-prettier": "^10.1.8", "eslint-plugin-import": "^2.32.0", - "eslint-plugin-jest": "^29.2.1", + "eslint-plugin-jest": "^29.13.0", "eslint-plugin-promise": "^7.2.1", "eslint-plugin-react": "^7.37.5", "eslint-plugin-react-hooks": "^7.0.1", @@ -77,7 +77,8 @@ "prettier": "^3.6.2", "ts-jest": "^29.4.6", "ts-node": "^10.9.2", - "typescript": "^4.9.5" + "typescript": "^4.9.5", + "typescript-eslint": "^8.54.0" }, "engines": { "node": "24.x" @@ -141,6 +142,7 @@ "integrity": "sha512-e7jT4DxYvIDLk1ZHmU/m/mB19rex9sv0c2ftBtjSBv+kVM/902eh0fINUzD7UwLLNR+jU585GxUJ8/EBfAM5fw==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@babel/code-frame": "^7.27.1", "@babel/generator": "^7.28.5", @@ -2073,6 +2075,7 @@ } ], "license": "MIT", + "peer": true, "engines": { "node": ">=18" }, @@ -2096,6 +2099,7 @@ } ], "license": "MIT", + "peer": true, "engines": { "node": ">=18" } @@ -2198,6 +2202,7 @@ "resolved": "https://registry.npmjs.org/@emotion/react/-/react-11.14.0.tgz", "integrity": "sha512-O000MLDBDdk/EohJPFUqvnp4qnHeYkVP5B0xEG0D/L7cOKP9kefu2DXn8dj74cQfsEzUqh+sr1RzFqiL1o+PpA==", "license": "MIT", + "peer": true, "dependencies": { "@babel/runtime": "^7.18.3", "@emotion/babel-plugin": "^11.13.5", @@ -2241,6 +2246,7 @@ "resolved": "https://registry.npmjs.org/@emotion/styled/-/styled-11.14.1.tgz", "integrity": "sha512-qEEJt42DuToa3gurlH4Qqc1kVpNq8wO8cJtDzU46TjlzWjDlsVyevtYCRijVq3SrHsROS+gVQ8Fnea108GnKzw==", "license": "MIT", + "peer": true, "dependencies": { "@babel/runtime": "^7.18.3", "@emotion/babel-plugin": "^11.13.5", @@ -2287,9 +2293,9 @@ "license": "MIT" }, "node_modules/@eslint-community/eslint-utils": { - "version": "4.9.0", - "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.9.0.tgz", - "integrity": "sha512-ayVFHdtZ+hsq1t2Dy24wCmGXGe4q9Gu3smhLYALJrr473ZH27MsnSL+LKUlimp4BWJqMDMLmPpx/Q9R3OAlL4g==", + "version": "4.9.1", + "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.9.1.tgz", + "integrity": "sha512-phrYmNiYppR7znFEdqgfWHXR6NCkZEK7hwWDHZUjit/2/U0r6XvkDl0SYnoM51Hq7FhCGdLDT6zxCCOY1hexsQ==", "dev": true, "license": "MIT", "dependencies": { @@ -2513,6 +2519,7 @@ "resolved": "https://registry.npmjs.org/@fullcalendar/core/-/core-6.1.20.tgz", "integrity": "sha512-1cukXLlePFiJ8YKXn/4tMKsy0etxYLCkXk8nUCFi11nRONF2Ba2CD5b21/ovtOO2tL6afTJfwmc1ed3HG7eB1g==", "license": "MIT", + "peer": true, "dependencies": { "preact": "~10.12.1" } @@ -3271,6 +3278,7 @@ "resolved": "https://registry.npmjs.org/@mui/material/-/material-7.3.6.tgz", "integrity": "sha512-R4DaYF3dgCQCUAkr4wW1w26GHXcf5rCmBRHVBuuvJvaGLmZdD8EjatP80Nz5JCw0KxORAzwftnHzXVnjR8HnFw==", "license": "MIT", + "peer": true, "dependencies": { "@babel/runtime": "^7.28.4", "@mui/core-downloads-tracker": "^7.3.6", @@ -3381,6 +3389,7 @@ "resolved": "https://registry.npmjs.org/@mui/system/-/system-7.3.6.tgz", "integrity": "sha512-8fehAazkHNP1imMrdD2m2hbA9sl7Ur6jfuNweh5o4l9YPty4iaZzRXqYvBCWQNwFaSHmMEj2KPbyXGp7Bt73Rg==", "license": "MIT", + "peer": true, "dependencies": { "@babel/runtime": "^7.28.4", "@mui/private-theming": "^7.3.6", @@ -3739,6 +3748,7 @@ "integrity": "sha512-LvoOF53PL6zXgdzEhgnnP51S4FseDFH1bHrobK4EK6zZX/tN8qgf5tdlmN7h4OkMv/Qs1oUfvj0QcLWSstnnvA==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@rspack/core": "1.6.8", "@rspack/lite-tapable": "~1.1.0", @@ -3966,6 +3976,7 @@ "integrity": "sha512-FolcIAH5FW4J2FET+qwjd1kNeFbCkd0VLuIHO0thyolEjaPSxw5qxG67DA7BZGm6PVcoiSgPLks1DL6eZ8c+fA==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@module-federation/runtime-tools": "0.21.6", "@rspack/binding": "1.6.8", @@ -4225,6 +4236,7 @@ "integrity": "sha512-8QqtOQT5ACVlmsvKOJNEaWmRPmcojMOzCz4Hs2BGG/toAp/K38LcsMRyLp349glq5AzJbCEeimEoxaX6v/fLrA==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@babel/core": "^7.21.3", "@svgr/babel-preset": "8.1.0", @@ -4309,6 +4321,7 @@ "integrity": "sha512-TXTnIcNJQEKwThMMqBXsZ4VGAza6bvN4pa41Rkqoio6QBKMvo+5lexeTMScGCIxtzgQJzElcvIltani+adC5PQ==", "dev": true, "license": "Apache-2.0", + "peer": true, "dependencies": { "tslib": "^2.8.0" } @@ -4319,6 +4332,7 @@ "integrity": "sha512-o4PXJQidqJl82ckFaXUeoAW+XysPLauYI43Abki5hABd853iMhitooc6znOnczgbTYmEP6U6/y1ZyKAIsvMKGg==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@babel/code-frame": "^7.10.4", "@babel/runtime": "^7.12.5", @@ -4621,6 +4635,7 @@ "integrity": "sha512-N2clP5pJhB2YnZJ3PIHFk5RkygRX5WO/5f0WC08tp0wd+sv0rsJk3MqWn3CbNmT2J505a5336jaQj4ph1AdMug==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "undici-types": "~6.21.0" } @@ -4642,6 +4657,7 @@ "resolved": "https://registry.npmjs.org/@types/react/-/react-18.3.27.tgz", "integrity": "sha512-cisd7gxkzjBKU2GgdYrTdtQx1SORymWyaAFhaxQPK9bYO9ot3Y5OikQRvY0VYQtvwjeQnizCINJAenh/V7MK2w==", "license": "MIT", + "peer": true, "dependencies": { "@types/prop-types": "*", "csstype": "^3.2.2" @@ -4653,6 +4669,7 @@ "integrity": "sha512-MEe3UeoENYVFXzoXEWsvcpg6ZvlrFNlOQ7EOsvhI3CfAXwzPfO8Qwuxd40nepsYKqyyVQnTdEfv68q91yLcKrQ==", "dev": true, "license": "MIT", + "peer": true, "peerDependencies": { "@types/react": "^18.0.0" } @@ -4704,20 +4721,21 @@ "license": "MIT" }, "node_modules/@typescript-eslint/eslint-plugin": { - "version": "8.51.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.51.0.tgz", - "integrity": "sha512-XtssGWJvypyM2ytBnSnKtHYOGT+4ZwTnBVl36TA4nRO2f4PRNGz5/1OszHzcZCvcBMh+qb7I06uoCmLTRdR9og==", + "version": "8.54.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.54.0.tgz", + "integrity": "sha512-hAAP5io/7csFStuOmR782YmTthKBJ9ND3WVL60hcOjvtGFb+HJxH4O5huAcmcZ9v9G8P+JETiZ/G1B8MALnWZQ==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { - "@eslint-community/regexpp": "^4.10.0", - "@typescript-eslint/scope-manager": "8.51.0", - "@typescript-eslint/type-utils": "8.51.0", - "@typescript-eslint/utils": "8.51.0", - "@typescript-eslint/visitor-keys": "8.51.0", - "ignore": "^7.0.0", + "@eslint-community/regexpp": "^4.12.2", + "@typescript-eslint/scope-manager": "8.54.0", + "@typescript-eslint/type-utils": "8.54.0", + "@typescript-eslint/utils": "8.54.0", + "@typescript-eslint/visitor-keys": "8.54.0", + "ignore": "^7.0.5", "natural-compare": "^1.4.0", - "ts-api-utils": "^2.2.0" + "ts-api-utils": "^2.4.0" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -4727,23 +4745,24 @@ "url": "https://opencollective.com/typescript-eslint" }, "peerDependencies": { - "@typescript-eslint/parser": "^8.51.0", + "@typescript-eslint/parser": "^8.54.0", "eslint": "^8.57.0 || ^9.0.0", "typescript": ">=4.8.4 <6.0.0" } }, "node_modules/@typescript-eslint/parser": { - "version": "8.51.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.51.0.tgz", - "integrity": "sha512-3xP4XzzDNQOIqBMWogftkwxhg5oMKApqY0BAflmLZiFYHqyhSOxv/cd/zPQLTcCXr4AkaKb25joocY0BD1WC6A==", + "version": "8.54.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.54.0.tgz", + "integrity": "sha512-BtE0k6cjwjLZoZixN0t5AKP0kSzlGu7FctRXYuPAm//aaiZhmfq1JwdYpYr1brzEspYyFeF+8XF5j2VK6oalrA==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { - "@typescript-eslint/scope-manager": "8.51.0", - "@typescript-eslint/types": "8.51.0", - "@typescript-eslint/typescript-estree": "8.51.0", - "@typescript-eslint/visitor-keys": "8.51.0", - "debug": "^4.3.4" + "@typescript-eslint/scope-manager": "8.54.0", + "@typescript-eslint/types": "8.54.0", + "@typescript-eslint/typescript-estree": "8.54.0", + "@typescript-eslint/visitor-keys": "8.54.0", + "debug": "^4.4.3" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -4758,15 +4777,15 @@ } }, "node_modules/@typescript-eslint/project-service": { - "version": "8.51.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.51.0.tgz", - "integrity": "sha512-Luv/GafO07Z7HpiI7qeEW5NW8HUtZI/fo/kE0YbtQEFpJRUuR0ajcWfCE5bnMvL7QQFrmT/odMe8QZww8X2nfQ==", + "version": "8.54.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.54.0.tgz", + "integrity": "sha512-YPf+rvJ1s7MyiWM4uTRhE4DvBXrEV+d8oC3P9Y2eT7S+HBS0clybdMIPnhiATi9vZOYDc7OQ1L/i6ga6NFYK/g==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/tsconfig-utils": "^8.51.0", - "@typescript-eslint/types": "^8.51.0", - "debug": "^4.3.4" + "@typescript-eslint/tsconfig-utils": "^8.54.0", + "@typescript-eslint/types": "^8.54.0", + "debug": "^4.4.3" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -4780,14 +4799,14 @@ } }, "node_modules/@typescript-eslint/scope-manager": { - "version": "8.51.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.51.0.tgz", - "integrity": "sha512-JhhJDVwsSx4hiOEQPeajGhCWgBMBwVkxC/Pet53EpBVs7zHHtayKefw1jtPaNRXpI9RA2uocdmpdfE7T+NrizA==", + "version": "8.54.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.54.0.tgz", + "integrity": "sha512-27rYVQku26j/PbHYcVfRPonmOlVI6gihHtXFbTdB5sb6qA0wdAQAbyXFVarQ5t4HRojIz64IV90YtsjQSSGlQg==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/types": "8.51.0", - "@typescript-eslint/visitor-keys": "8.51.0" + "@typescript-eslint/types": "8.54.0", + "@typescript-eslint/visitor-keys": "8.54.0" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -4798,9 +4817,9 @@ } }, "node_modules/@typescript-eslint/tsconfig-utils": { - "version": "8.51.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.51.0.tgz", - "integrity": "sha512-Qi5bSy/vuHeWyir2C8u/uqGMIlIDu8fuiYWv48ZGlZ/k+PRPHtaAu7erpc7p5bzw2WNNSniuxoMSO4Ar6V9OXw==", + "version": "8.54.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.54.0.tgz", + "integrity": "sha512-dRgOyT2hPk/JwxNMZDsIXDgyl9axdJI3ogZ2XWhBPsnZUv+hPesa5iuhdYt2gzwA9t8RE5ytOJ6xB0moV0Ujvw==", "dev": true, "license": "MIT", "engines": { @@ -4815,17 +4834,17 @@ } }, "node_modules/@typescript-eslint/type-utils": { - "version": "8.51.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.51.0.tgz", - "integrity": "sha512-0XVtYzxnobc9K0VU7wRWg1yiUrw4oQzexCG2V2IDxxCxhqBMSMbjB+6o91A+Uc0GWtgjCa3Y8bi7hwI0Tu4n5Q==", + "version": "8.54.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.54.0.tgz", + "integrity": "sha512-hiLguxJWHjjwL6xMBwD903ciAwd7DmK30Y9Axs/etOkftC3ZNN9K44IuRD/EB08amu+Zw6W37x9RecLkOo3pMA==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/types": "8.51.0", - "@typescript-eslint/typescript-estree": "8.51.0", - "@typescript-eslint/utils": "8.51.0", - "debug": "^4.3.4", - "ts-api-utils": "^2.2.0" + "@typescript-eslint/types": "8.54.0", + "@typescript-eslint/typescript-estree": "8.54.0", + "@typescript-eslint/utils": "8.54.0", + "debug": "^4.4.3", + "ts-api-utils": "^2.4.0" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -4840,9 +4859,9 @@ } }, "node_modules/@typescript-eslint/types": { - "version": "8.51.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.51.0.tgz", - "integrity": "sha512-TizAvWYFM6sSscmEakjY3sPqGwxZRSywSsPEiuZF6d5GmGD9Gvlsv0f6N8FvAAA0CD06l3rIcWNbsN1e5F/9Ag==", + "version": "8.54.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.54.0.tgz", + "integrity": "sha512-PDUI9R1BVjqu7AUDsRBbKMtwmjWcn4J3le+5LpcFgWULN3LvHC5rkc9gCVxbrsrGmO1jfPybN5s6h4Jy+OnkAA==", "dev": true, "license": "MIT", "engines": { @@ -4854,21 +4873,21 @@ } }, "node_modules/@typescript-eslint/typescript-estree": { - "version": "8.51.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.51.0.tgz", - "integrity": "sha512-1qNjGqFRmlq0VW5iVlcyHBbCjPB7y6SxpBkrbhNWMy/65ZoncXCEPJxkRZL8McrseNH6lFhaxCIaX+vBuFnRng==", + "version": "8.54.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.54.0.tgz", + "integrity": "sha512-BUwcskRaPvTk6fzVWgDPdUndLjB87KYDrN5EYGetnktoeAvPtO4ONHlAZDnj5VFnUANg0Sjm7j4usBlnoVMHwA==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/project-service": "8.51.0", - "@typescript-eslint/tsconfig-utils": "8.51.0", - "@typescript-eslint/types": "8.51.0", - "@typescript-eslint/visitor-keys": "8.51.0", - "debug": "^4.3.4", - "minimatch": "^9.0.4", - "semver": "^7.6.0", + "@typescript-eslint/project-service": "8.54.0", + "@typescript-eslint/tsconfig-utils": "8.54.0", + "@typescript-eslint/types": "8.54.0", + "@typescript-eslint/visitor-keys": "8.54.0", + "debug": "^4.4.3", + "minimatch": "^9.0.5", + "semver": "^7.7.3", "tinyglobby": "^0.2.15", - "ts-api-utils": "^2.2.0" + "ts-api-utils": "^2.4.0" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -4882,9 +4901,9 @@ } }, "node_modules/@typescript-eslint/typescript-estree/node_modules/semver": { - "version": "7.7.3", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.3.tgz", - "integrity": "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q==", + "version": "7.7.4", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz", + "integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==", "dev": true, "license": "ISC", "bin": { @@ -4895,16 +4914,16 @@ } }, "node_modules/@typescript-eslint/utils": { - "version": "8.51.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.51.0.tgz", - "integrity": "sha512-11rZYxSe0zabiKaCP2QAwRf/dnmgFgvTmeDTtZvUvXG3UuAdg/GU02NExmmIXzz3vLGgMdtrIosI84jITQOxUA==", + "version": "8.54.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.54.0.tgz", + "integrity": "sha512-9Cnda8GS57AQakvRyG0PTejJNlA2xhvyNtEVIMlDWOOeEyBkYWhGPnfrIAnqxLMTSTo6q8g12XVjjev5l1NvMA==", "dev": true, "license": "MIT", "dependencies": { - "@eslint-community/eslint-utils": "^4.7.0", - "@typescript-eslint/scope-manager": "8.51.0", - "@typescript-eslint/types": "8.51.0", - "@typescript-eslint/typescript-estree": "8.51.0" + "@eslint-community/eslint-utils": "^4.9.1", + "@typescript-eslint/scope-manager": "8.54.0", + "@typescript-eslint/types": "8.54.0", + "@typescript-eslint/typescript-estree": "8.54.0" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -4919,13 +4938,13 @@ } }, "node_modules/@typescript-eslint/visitor-keys": { - "version": "8.51.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.51.0.tgz", - "integrity": "sha512-mM/JRQOzhVN1ykejrvwnBRV3+7yTKK8tVANVN3o1O0t0v7o+jqdVu9crPy5Y9dov15TJk/FTIgoUGHrTOVL3Zg==", + "version": "8.54.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.54.0.tgz", + "integrity": "sha512-VFlhGSl4opC0bprJiItPQ1RfUhGDIBokcPwaFH4yiBCaNPeld/9VeXbiPO1cLyorQi1G1vL+ecBk1x8o1axORA==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/types": "8.51.0", + "@typescript-eslint/types": "8.54.0", "eslint-visitor-keys": "^4.2.1" }, "engines": { @@ -5244,6 +5263,7 @@ "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==", "dev": true, "license": "MIT", + "peer": true, "bin": { "acorn": "bin/acorn" }, @@ -6003,6 +6023,7 @@ } ], "license": "MIT", + "peer": true, "dependencies": { "baseline-browser-mapping": "^2.9.0", "caniuse-lite": "^1.0.30001759", @@ -6714,6 +6735,7 @@ "resolved": "https://registry.npmjs.org/date-fns/-/date-fns-2.30.0.tgz", "integrity": "sha512-fnULvOpxnC5/Vg3NCiWelDsLiUc9bRwAPs/+LfTLNvetFCtCTN+yQz15C/fs4AwX1R9K5GLtLfn8QW+dWisaAw==", "license": "MIT", + "peer": true, "dependencies": { "@babel/runtime": "^7.21.0" }, @@ -6729,7 +6751,8 @@ "version": "1.11.19", "resolved": "https://registry.npmjs.org/dayjs/-/dayjs-1.11.19.tgz", "integrity": "sha512-t5EcLVS6QPBNqM2z8fakk/NKel+Xzshgt8FFKAn+qwlD1pzZWxh0nVCrvFK7ZDb6XucZeF9z8C7CBWTRIVApAw==", - "license": "MIT" + "license": "MIT", + "peer": true }, "node_modules/debug": { "version": "4.4.3", @@ -7329,6 +7352,7 @@ "integrity": "sha512-LEyamqS7W5HB3ujJyvi0HQK/dtVINZvd5mAAp9eT5S/ujByGjiZLCzPcHVzuXbpJDJF/cxwHlfceVUDZ2lnSTw==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@eslint-community/eslint-utils": "^4.8.0", "@eslint-community/regexpp": "^4.12.1", @@ -7518,9 +7542,9 @@ } }, "node_modules/eslint-plugin-jest": { - "version": "29.11.2", - "resolved": "https://registry.npmjs.org/eslint-plugin-jest/-/eslint-plugin-jest-29.11.2.tgz", - "integrity": "sha512-ISlgFckxC8Pon9/3gD2Cosbt9qA9VmrNobI6aGHfimklCOuPR5uftI413YBwa2rqLWy+VHDlx5je/6elPUMLlg==", + "version": "29.13.0", + "resolved": "https://registry.npmjs.org/eslint-plugin-jest/-/eslint-plugin-jest-29.13.0.tgz", + "integrity": "sha512-VoONe0NsaQLb7ijvg4k35rzchqfyCaBsXammNMCkTyLvKLTpzQOVdXiPC54q7Vp/W7shMcqPBLwAc3yRSiGjSQ==", "dev": true, "license": "MIT", "dependencies": { @@ -7531,8 +7555,9 @@ }, "peerDependencies": { "@typescript-eslint/eslint-plugin": "^8.0.0", - "eslint": "^8.57.0 || ^9.0.0", - "jest": "*" + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", + "jest": "*", + "typescript": ">=4.8.4 <6.0.0" }, "peerDependenciesMeta": { "@typescript-eslint/eslint-plugin": { @@ -7540,6 +7565,9 @@ }, "jest": { "optional": true + }, + "typescript": { + "optional": true } } }, @@ -8683,6 +8711,7 @@ "resolved": "https://registry.npmjs.org/history/-/history-5.3.0.tgz", "integrity": "sha512-ZqaKwjjrAYUYfLG+htGaIIZ4nioX2L70ZUMIFysS3xvBsSG4x/n1V6TXV3N8ZYNuFGlDirFg32T7B6WOUPDYcQ==", "license": "MIT", + "peer": true, "dependencies": { "@babel/runtime": "^7.7.6" } @@ -9657,6 +9686,7 @@ "integrity": "sha512-F26gjC0yWN8uAA5m5Ss8ZQf5nDHWGlN/xWZIh8S5SRbsEKBovwZhxGd6LJlbZYxBgCYOtreSUyb8hpXyGC5O4A==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@jest/core": "30.2.0", "@jest/types": "30.2.0", @@ -10420,6 +10450,7 @@ "integrity": "sha512-/xdLSWxK5QkqG524ONSjvg3V/FkNyCv538OIBdQqPNaAta3AsXj/Bd2FbvR87yMbXO2hFSWiAe/Q6IkVPDw+mw==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@babel/core": "^7.19.6", "@svgr/babel-preset": "^6.5.1", @@ -10878,6 +10909,7 @@ "integrity": "sha512-Cvc9WUhxSMEo4McES3P7oK3QaXldCfNWp7pl2NNeiIFlCoLr3kfq9kb1fxftiwk1FLV7CvpvDfonxtzUDeSOPg==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "cssstyle": "^4.2.1", "data-urls": "^5.0.0", @@ -11314,6 +11346,7 @@ "resolved": "https://registry.npmjs.org/moment/-/moment-2.30.1.tgz", "integrity": "sha512-uEmtNhbDOrWPFS+hdjFCBfy9f2YoyzRpwcl+DqpC6taX21FzsTLQVbMV/W7PzNSX6x/bhC1zA3c2UQ5NzH6how==", "license": "MIT", + "peer": true, "engines": { "node": "*" } @@ -11323,6 +11356,7 @@ "resolved": "https://registry.npmjs.org/moment-timezone/-/moment-timezone-0.5.48.tgz", "integrity": "sha512-f22b8LV1gbTO2ms2j2z13MuPogNoh5UzxL3nzNAYKGraILnbGc9NEE6dyiiiLv46DGRb8A4kg8UKWLjPthxBHw==", "license": "MIT", + "peer": true, "dependencies": { "moment": "^2.29.4" }, @@ -11358,7 +11392,6 @@ } ], "license": "MIT", - "peer": true, "bin": { "nanoid": "bin/nanoid.cjs" }, @@ -12307,6 +12340,7 @@ "resolved": "https://registry.npmjs.org/react/-/react-18.3.1.tgz", "integrity": "sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ==", "license": "MIT", + "peer": true, "dependencies": { "loose-envify": "^1.1.0" }, @@ -12329,6 +12363,7 @@ "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.3.1.tgz", "integrity": "sha512-5m4nQKp+rZRb09LNH59GM4BxTh9251/ylbKIbpe7TpGxfJ+9kv6BLkLBXIjjspbgbnIBNqlI23tRnTWT0snUIw==", "license": "MIT", + "peer": true, "dependencies": { "loose-envify": "^1.1.0", "scheduler": "^0.23.2" @@ -12348,6 +12383,7 @@ "resolved": "https://registry.npmjs.org/react-redux/-/react-redux-9.2.0.tgz", "integrity": "sha512-ROY9fvHhwOD9ySfrF0wmvu//bKCQ6AeZZq1nJNtbDC+kk5DuSuNX/n6YWYF/SYy7bSba4D4FSz8DJeKY/S/r+g==", "license": "MIT", + "peer": true, "dependencies": { "@types/use-sync-external-store": "^0.0.6", "use-sync-external-store": "^1.4.0" @@ -12372,6 +12408,7 @@ "integrity": "sha512-QgT5//D3jfjJb6Gsjxv0Slpj23ip+HtOpnNgnb2S5zU3CB26G/IDPGoy4RJB42wzFE46DRsstbW6tKHoKbhAxw==", "dev": true, "license": "MIT", + "peer": true, "engines": { "node": ">=0.10.0" } @@ -12472,7 +12509,8 @@ "version": "5.0.1", "resolved": "https://registry.npmjs.org/redux/-/redux-5.0.1.tgz", "integrity": "sha512-M9/ELqF6fy8FwmkpnF0S3YKOqMyoWJ4+CS5Efg2ct3oY9daQvd/Pc71FpGZsVsbl3Cpb+IIcjBDUnnyBdQbq4w==", - "license": "MIT" + "license": "MIT", + "peer": true }, "node_modules/redux-first-history": { "version": "5.2.0", @@ -13699,6 +13737,7 @@ "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", "dev": true, "license": "MIT", + "peer": true, "engines": { "node": ">=12" }, @@ -13783,9 +13822,9 @@ } }, "node_modules/ts-api-utils": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-2.3.0.tgz", - "integrity": "sha512-6eg3Y9SF7SsAvGzRHQvvc1skDAhwI4YQ32ui1scxD1Ccr0G5qIIbUBT3pFTKX8kmWIQClHobtUdNuaBgwdfdWg==", + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-2.4.0.tgz", + "integrity": "sha512-3TaVTaAv2gTiMB35i3FiGJaRfwb3Pyn/j3m/bfAvGe8FB7CF6u+LMYqYlDh7reQf7UNvoTvdfAqHGmPGOSsPmA==", "dev": true, "license": "MIT", "engines": { @@ -13879,6 +13918,7 @@ "resolved": "https://registry.npmjs.org/ts-node/-/ts-node-10.9.2.tgz", "integrity": "sha512-f0FFpIdcHgn8zcPSbf1dRevwt047YMnaiJM3u2w2RewrB+fob/zePZcrOyQoLMMO7aBIddLcQIEK5dYjkLnGrQ==", "dev": true, + "peer": true, "dependencies": { "@cspotcode/source-map-support": "^0.8.0", "@tsconfig/node10": "^1.0.7", @@ -14095,6 +14135,7 @@ "integrity": "sha512-1FXk9E2Hm+QzZQ7z+McJiHL4NW1F2EzMu9Nq9i3zAaGqibafqYwCVU6WyWAuyQRRzOlxou8xZSyXLEN8oKj24g==", "dev": true, "license": "Apache-2.0", + "peer": true, "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" @@ -14103,6 +14144,30 @@ "node": ">=4.2.0" } }, + "node_modules/typescript-eslint": { + "version": "8.54.0", + "resolved": "https://registry.npmjs.org/typescript-eslint/-/typescript-eslint-8.54.0.tgz", + "integrity": "sha512-CKsJ+g53QpsNPqbzUsfKVgd3Lny4yKZ1pP4qN3jdMOg/sisIDLGyDMezycquXLE5JsEU0wp3dGNdzig0/fmSVQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/eslint-plugin": "8.54.0", + "@typescript-eslint/parser": "8.54.0", + "@typescript-eslint/typescript-estree": "8.54.0", + "@typescript-eslint/utils": "8.54.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0", + "typescript": ">=4.8.4 <6.0.0" + } + }, "node_modules/uglify-js": { "version": "3.19.3", "resolved": "https://registry.npmjs.org/uglify-js/-/uglify-js-3.19.3.tgz", @@ -14903,6 +14968,7 @@ "integrity": "sha512-0wZ1IRqGGhMP76gLqz8EyfBXKk0J2qo2+H3fi4mcUP/KtTocoX08nmIAHl1Z2kJIZbZee8KOpBCSNPRgauucjw==", "dev": true, "license": "MIT", + "peer": true, "funding": { "url": "https://github.com/sponsors/colinhacks" } diff --git a/package.json b/package.json index 416d7d8..8541a63 100644 --- a/package.json +++ b/package.json @@ -41,7 +41,8 @@ "build": "rsbuild build", "preview": "rsbuild preview", "test": "jest", - "lint": "eslint src --ext .js,.jsx,.ts,.tsx", + "lint": "eslint src", + "lint:fix": "eslint src --fix", "jest-preview": "jest-preview" }, "browserslist": { @@ -83,7 +84,7 @@ "eslint": "^9.39.1", "eslint-config-prettier": "^10.1.8", "eslint-plugin-import": "^2.32.0", - "eslint-plugin-jest": "^29.2.1", + "eslint-plugin-jest": "^29.13.0", "eslint-plugin-promise": "^7.2.1", "eslint-plugin-react": "^7.37.5", "eslint-plugin-react-hooks": "^7.0.1", @@ -95,6 +96,7 @@ "prettier": "^3.6.2", "ts-jest": "^29.4.6", "ts-node": "^10.9.2", - "typescript": "^4.9.5" + "typescript": "^4.9.5", + "typescript-eslint": "^8.54.0" } } diff --git a/src/App.tsx b/src/App.tsx index adb25b7..ed8637e 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -48,7 +48,7 @@ function App() { ); const settingsLanguage = useAppSelector((state) => state.settings.language); const savedLang = localStorage.getItem("lang"); - const defaultLang = (window as any).LANG; + const defaultLang = window.LANG; const lang = [userLanguage, settingsLanguage, savedLang, defaultLang].find( diff --git a/src/components/Attendees/AttendeeSearch.tsx b/src/components/Attendees/AttendeeSearch.tsx index 5324bea..99cd962 100644 --- a/src/components/Attendees/AttendeeSearch.tsx +++ b/src/components/Attendees/AttendeeSearch.tsx @@ -15,7 +15,7 @@ export default function UserSearch({ placeholder, }: { attendees: userAttendee[]; - setAttendees: Function; + setAttendees: (attendees: userAttendee[]) => void; disabled?: boolean; inputSlot?: ( params: ExtendedAutocompleteRenderInputParams @@ -47,7 +47,7 @@ export default function UserSearch({ disabled={disabled} inputSlot={inputSlot} placeholder={placeholder} - onChange={(event: any, value: User[]) => { + onChange={(_event, value: User[]) => { setAttendees( value.map((attendee: User) => createAttendee({ diff --git a/src/components/Attendees/PeopleSearch.tsx b/src/components/Attendees/PeopleSearch.tsx index 2350535..e0f59aa 100644 --- a/src/components/Attendees/PeopleSearch.tsx +++ b/src/components/Attendees/PeopleSearch.tsx @@ -23,6 +23,7 @@ import { useEffect, useState, type ReactNode, + type SyntheticEvent, } from "react"; import { useI18n } from "twake-i18n"; @@ -55,7 +56,7 @@ export function PeopleSearch({ customSlotProps, }: { selectedUsers: User[]; - onChange: (event: any, users: User[]) => void; + onChange: (event: SyntheticEvent, users: User[]) => void; objectTypes: string[]; disabled?: boolean; freeSolo?: boolean; @@ -155,7 +156,7 @@ export function PeopleSearch({ ), }; - const enhancedParamsWithInputProps = { + const enhancedParams = { ...params, InputProps: inputProps, inputProps: { @@ -164,8 +165,6 @@ export function PeopleSearch({ }, }; - const { InputProps, ...enhancedParams } = enhancedParamsWithInputProps; - const handleEnterKey = (e: React.KeyboardEvent) => { if (e.key === "Enter" && onToggleEventPreview) { e.preventDefault(); @@ -193,7 +192,7 @@ export function PeopleSearch({ {t("peopleSearch.label")} {inputSlot({ - ...enhancedParamsWithInputProps, + ...enhancedParams, error: !!inputError, helperText: inputError, placeholder: searchPlaceholder, @@ -218,6 +217,7 @@ export function PeopleSearch({ ); }, + // eslint-disable-next-line react-hooks/exhaustive-deps [inputError, t, onToggleEventPreview, loading, searchPlaceholder] ); @@ -296,7 +296,7 @@ export function PeopleSearch({ } renderOption={(props, option) => { if (selectedUsers.find((u) => u.email === option.email)) return null; - const { key, ...otherProps } = props as any; + const { key, ...otherProps } = props; return ( diff --git a/src/components/Calendar/AccessTab.tsx b/src/components/Calendar/AccessTab.tsx index da4bf54..529aa3f 100644 --- a/src/components/Calendar/AccessTab.tsx +++ b/src/components/Calendar/AccessTab.tsx @@ -22,7 +22,7 @@ import { SnackbarAlert } from "../Loading/SnackBarAlert"; export function AccessTab({ calendar }: { calendar: Calendar }) { const { t } = useI18n(); - const calDAVLink = `${(window as any).CALENDAR_BASE_URL}${calendar.link.replace(".json", "")}`; + const calDAVLink = `${window.CALENDAR_BASE_URL}${calendar.link.replace(".json", "")}`; const [secretLink, setSecretLink] = useState(""); const [open, setOpen] = useState(false); diff --git a/src/components/Calendar/Calendar.tsx b/src/components/Calendar/Calendar.tsx index 5e2a3cf..7c3e4b9 100644 --- a/src/components/Calendar/Calendar.tsx +++ b/src/components/Calendar/Calendar.tsx @@ -15,6 +15,7 @@ import { extractEventBaseUuid } from "@/utils/extractEventBaseUuid"; import { setSelectedCalendars as setSelectedCalendarsToStorage } from "@/utils/storage/setSelectedCalendars"; import { useSelectedCalendars } from "@/utils/storage/useSelectedCalendars"; import { browserDefaultTimeZone } from "@/utils/timezone"; +import type { EventApi, LocaleInput } from "@fullcalendar/core"; import { CalendarApi, DateSelectArg } from "@fullcalendar/core"; import frLocale from "@fullcalendar/core/locales/fr"; import ruLocale from "@fullcalendar/core/locales/ru"; @@ -47,7 +48,7 @@ import { updateSlotLabelVisibility, } from "./utils/calendarUtils"; -const localeMap: Record = { +const localeMap: Record = { fr: frLocale, ru: ruLocale, vi: viLocale, @@ -221,7 +222,7 @@ export default function CalendarApp({ [rangeStart, rangeEnd] ); - let filteredEvents: CalendarEvent[] = extractEvents( + const filteredEvents: CalendarEvent[] = extractEvents( selectedCalendars, calendars || {}, userData?.email, @@ -233,7 +234,7 @@ export default function CalendarApp({ [tempcalendars] ); - let filteredTempEvents: CalendarEvent[] = extractEvents( + const filteredTempEvents: CalendarEvent[] = extractEvents( tempCalendarIds, tempcalendars || {}, userData?.email, @@ -303,7 +304,7 @@ export default function CalendarApp({ }, }) ).unwrap(); - } catch (error) { + } catch { fetchedRangesRef.current[id] = ""; } }); @@ -323,6 +324,7 @@ export default function CalendarApp({ return () => { cancelled = true; }; + // eslint-disable-next-line react-hooks/exhaustive-deps }, [dispatch, rangeKey, sortedSelectedCalendars, rangeStart, rangeEnd]); useEffect(() => { @@ -352,6 +354,7 @@ export default function CalendarApp({ prefetchedCalendarsRef.current[id] = ""; }); }); + // eslint-disable-next-line react-hooks/exhaustive-deps }, [ calendarIdsString, selectedCalendars, @@ -458,7 +461,7 @@ export default function CalendarApp({ calType: "temp", }) ).unwrap(); - } catch (error) { + } catch { tempFetchedRangesRef.current[id] = ""; } }); @@ -521,7 +524,7 @@ export default function CalendarApp({ }) ); } - } catch (err) { + } catch { // Ignore sessionStorage errors } } @@ -543,10 +546,10 @@ export default function CalendarApp({ null ); const [, setTypeOfAction] = useState<"solo" | "all" | undefined>(undefined); - const [afterChoiceFunc, setAfterChoiceFunc] = useState(); - const [selectedEvent, setSelectedEvent] = useState( - {} as CalendarEvent - ); + const [afterChoiceFunc, setAfterChoiceFunc] = useState< + ((type: "solo" | "all" | undefined) => void) | undefined + >(); + const [, setSelectedEvent] = useState({} as CalendarEvent); const [selectedRange, setSelectedRange] = useState( null ); @@ -587,7 +590,7 @@ export default function CalendarApp({ }); if (process.env.NODE_ENV === "test") { - (window as any).__calendarRef = calendarRef; + window.__calendarRef = calendarRef; } const { t, lang } = useI18n(); @@ -635,7 +638,6 @@ export default function CalendarApp({ @@ -697,7 +699,7 @@ export default function CalendarApp({ userData?.email, isPending )} - eventOrder={(a: any, b: any) => + eventOrder={(a: EventApi, b: EventApi) => a.extendedProps.priority - b.extendedProps.priority } weekNumbers={ @@ -825,10 +827,11 @@ export default function CalendarApp({ { setTypeOfAction(type); - afterChoiceFunc && afterChoiceFunc(type); + if (afterChoiceFunc) { + afterChoiceFunc(type); + } }} /> {openEventDisplay && eventDisplayedId && eventDisplayedCalId && ( diff --git a/src/components/Calendar/CalendarColorPicker.tsx b/src/components/Calendar/CalendarColorPicker.tsx index 2337b22..c856fd2 100644 --- a/src/components/Calendar/CalendarColorPicker.tsx +++ b/src/components/Calendar/CalendarColorPicker.tsx @@ -123,11 +123,11 @@ function ColorPickerBox({ selectedColor ?? { light: "#ffffff", dark: "#808080" } ); const [color, setColor] = useState(oldColor); - const [anchorEl, setAnchorEl] = useState(null); + const [anchorEl, setAnchorEl] = useState(null); const open = Boolean(anchorEl); const theme = useTheme(); - const handleClick = (event: any) => { + const handleClick = (event: React.MouseEvent) => { setAnchorEl(event.currentTarget); }; diff --git a/src/components/Calendar/CalendarLayout.tsx b/src/components/Calendar/CalendarLayout.tsx index 5a729fb..3dc7e8e 100644 --- a/src/components/Calendar/CalendarLayout.tsx +++ b/src/components/Calendar/CalendarLayout.tsx @@ -1,6 +1,7 @@ import { useAppDispatch, useAppSelector } from "@/app/hooks"; import SettingsPage from "@/features/Settings/SettingsPage"; import { getCalendarRange } from "@/utils/dateUtils"; +import type { CalendarApi } from "@fullcalendar/core"; import { useEffect, useRef, useState } from "react"; import { ErrorSnackbar } from "../Error/ErrorSnackbar"; import { refreshCalendars } from "../Event/utils/eventUtils"; @@ -8,7 +9,7 @@ import { Menubar, MenubarProps } from "../Menubar/Menubar"; import CalendarApp from "./Calendar"; export default function CalendarLayout() { - const calendarRef = useRef(null); + const calendarRef = useRef(null); const dispatch = useAppDispatch(); const error = useAppSelector((state) => state.calendars.error); const selectedCalendars = useAppSelector((state) => state.calendars.list); diff --git a/src/components/Calendar/CalendarModal.tsx b/src/components/Calendar/CalendarModal.tsx index 5bc2d49..195cd8b 100644 --- a/src/components/Calendar/CalendarModal.tsx +++ b/src/components/Calendar/CalendarModal.tsx @@ -138,28 +138,30 @@ function CalendarPopover({ newCalColor, newCalVisibility ); - importedContent && + if (importedContent) { dispatch( importEventFromFileAsync({ calLink: `/calendar/${userData.openpaasId}/${calId}.json`, file: importedContent, }) ); + } } } else { - importedContent && + if (importedContent) { dispatch( importEventFromFileAsync({ calLink: calendars[importTarget].link, file: importedContent, }) ); + } } handleClose({}, "backdropClick"); }; const handleClose = ( - e: {}, + e: object | null, reason: "backdropClick" | "escapeKeyDown" ): void => { onClose(e, reason); @@ -182,7 +184,7 @@ function CalendarPopover({ open={open} onClose={() => handleClose({}, "backdropClick")} title={ - setTab(v)}> + setTab(v)}> diff --git a/src/components/Calendar/CalendarSearch.tsx b/src/components/Calendar/CalendarSearch.tsx index 4e6cc6b..d9a5bd3 100644 --- a/src/components/Calendar/CalendarSearch.tsx +++ b/src/components/Calendar/CalendarSearch.tsx @@ -21,7 +21,7 @@ import { ColorPicker } from "./CalendarColorPicker"; import { defaultColors, getAccessiblePair } from "./utils/calendarColorsUtils"; interface CalendarWithOwner { - cal: Record; + cal: Record; owner: User; } @@ -117,7 +117,7 @@ function SelectedCalendarsList({ > >((acc, cal) => { const exists = Object.values(calendars).some( - (existing: any) => + (existing: Calendar) => existing.id === cal.cal?._links?.self?.href .replace("/calendars/", "") @@ -190,13 +190,14 @@ function SelectedCalendarsList({ } export default function CalendarSearch({ - anchorEl, open, onClose, }: { - anchorEl: HTMLElement | null; open: boolean; - onClose: Function; + onClose: ( + result?: string[] | Record, + reason?: "backdropClick" | "escapeKeyDown" + ) => void; }) { const dispatch = useAppDispatch(); const theme = useTheme(); @@ -214,7 +215,7 @@ export default function CalendarSearch({ selectedCal.map(async (cal) => { const calId = crypto.randomUUID(); const exists = Object.values(calendars).some( - (existing: any) => + (existing: Calendar) => existing.id === cal.cal?._links?.self?.href .replace("/calendars/", "") @@ -279,7 +280,7 @@ export default function CalendarSearch({ objectTypes={["user"]} selectedUsers={selectedUsers} inputSlot={(params) => } - onChange={async (event: any, value: User[]) => { + onChange={async (_event: React.SyntheticEvent, value: User[]) => { setSelectedUsers(value); const cals = await Promise.all( @@ -288,10 +289,10 @@ export default function CalendarSearch({ const cals = (await getCalendars( user.openpaasId, "sharedPublic=true&" - )) as Record; + )) as Record; return cals._embedded?.["dav:calendar"] ? cals._embedded["dav:calendar"].map( - (cal: Record) => ({ cal, owner: user }) + (cal: Record) => ({ cal, owner: user }) ) : { cal: undefined, owner: user }; } diff --git a/src/components/Calendar/CalendarSelection.tsx b/src/components/Calendar/CalendarSelection.tsx index 69b00a0..7036c89 100644 --- a/src/components/Calendar/CalendarSelection.tsx +++ b/src/components/Calendar/CalendarSelection.tsx @@ -13,12 +13,11 @@ import { ListItem, Menu, MenuItem, - Typography, } from "@linagora/twake-mui"; import AddIcon from "@mui/icons-material/Add"; import ExpandMoreIcon from "@mui/icons-material/ExpandMore"; import MoreHorizIcon from "@mui/icons-material/MoreHoriz"; -import { useEffect, useMemo, useState } from "react"; +import { SetStateAction, useEffect, useMemo, useState } from "react"; import { useI18n } from "twake-i18n"; import CalendarPopover from "./CalendarModal"; import CalendarSearch from "./CalendarSearch"; @@ -39,9 +38,9 @@ function CalendarAccordion({ selectedCalendars: string[]; handleToggle: (id: string) => void; showAddButton?: boolean; - onAddClick?: Function; + onAddClick?: () => void; defaultExpanded?: boolean; - setOpen: Function; + setOpen: (id: string) => void; }) { const allCalendars = useAppSelector((state) => state.calendars.list); const { t } = useI18n(); @@ -86,8 +85,12 @@ function CalendarAccordion({ { - expended && e.stopPropagation(); - onAddClick && onAddClick(); + if (expended) { + e.stopPropagation(); + } + if (onAddClick) { + onAddClick(); + } }} > @@ -116,7 +119,7 @@ export default function CalendarSelection({ setSelectedCalendars, }: { selectedCalendars: string[]; - setSelectedCalendars: Function; + setSelectedCalendars: (value: SetStateAction) => void; }) { const { t } = useI18n(); const userId = @@ -202,7 +205,6 @@ export default function CalendarSelection({ }} /> { setAnchorElCalOthers(null); @@ -228,7 +230,7 @@ function CalendarSelector({ isPersonal: boolean; selectedCalendars: string[]; handleCalendarToggle: (name: string) => void; - setOpen: Function; + setOpen: () => void; }) { const { t } = useI18n(); const dispatch = useAppDispatch(); diff --git a/src/components/Calendar/ImportTab.tsx b/src/components/Calendar/ImportTab.tsx index d8fa630..8204e1f 100644 --- a/src/components/Calendar/ImportTab.tsx +++ b/src/components/Calendar/ImportTab.tsx @@ -24,21 +24,21 @@ export function ImportTab({ }: { userId: string; importTarget: string; - setImportTarget: Function; - setImportedContent: Function; + setImportTarget: (target: string) => void; + setImportedContent: (content: File | null) => void; newCalParams: { name: string; - setName: Function; + setName: (name: string) => void; description: string; setDescription: (d: string) => void; color: Record; - setColor: Function; + setColor: (color: Record) => void; visibility: "public" | "private"; - setVisibility: Function; + setVisibility: (visibility: "public" | "private") => void; }; }) { const { t } = useI18n(); - const [importMode, setImportMode] = useState<"file" | "url">("file"); + const [importMode] = useState<"file" | "url">("file"); const [importFile, setImportFile] = useState(null); const [importUrl, setImportUrl] = useState(""); const calendars = useAppSelector((state) => state.calendars.list); @@ -48,7 +48,7 @@ export function ImportTab({ useEffect(() => { setImportedContent(importMode === "file" ? importFile : null); - }, [importFile, importUrl, importMode]); + }, [importFile, importUrl, importMode, setImportedContent]); return ( <> diff --git a/src/components/Calendar/MiniCalendar.tsx b/src/components/Calendar/MiniCalendar.tsx index 2b5abed..f8aa7db 100644 --- a/src/components/Calendar/MiniCalendar.tsx +++ b/src/components/Calendar/MiniCalendar.tsx @@ -13,19 +13,20 @@ import moment from "moment"; import KeyboardArrowDownIcon from "@mui/icons-material/KeyboardArrowDown"; import { useEffect, useState } from "react"; import { useI18n } from "twake-i18n"; +import { CalendarEvent } from "@/features/Events/EventsTypes"; +import type { CalendarApi } from "@fullcalendar/core"; export function MiniCalendar({ calendarRef, selectedDate, - setSelectedDate, + setSelectedMiniDate, dottedEvents, }: { - calendarRef: any; + calendarRef: React.MutableRefObject; selectedDate: Date; - setSelectedDate: (d: Date) => void; setSelectedMiniDate: (d: Date) => void; - dottedEvents: any[]; + dottedEvents: CalendarEvent[]; }) { const dispatch = useAppDispatch(); const calendars = useAppSelector((state) => state.calendars); diff --git a/src/components/Calendar/SettingsTab.tsx b/src/components/Calendar/SettingsTab.tsx index c445093..6051ee7 100644 --- a/src/components/Calendar/SettingsTab.tsx +++ b/src/components/Calendar/SettingsTab.tsx @@ -27,13 +27,13 @@ export function SettingsTab({ calendar, }: { name: string; - setName: Function; + setName: (name: string) => void; description: string; setDescription: (d: string) => void; color: Record; - setColor: Function; + setColor: (color: Record) => void; visibility: "public" | "private"; - setVisibility: Function; + setVisibility: (visibility: "public" | "private") => void; calendar?: Calendar; }) { const { t } = useI18n(); diff --git a/src/components/Calendar/TempCalendarsInput.tsx b/src/components/Calendar/TempCalendarsInput.tsx index 32f3f92..2aaf55b 100644 --- a/src/components/Calendar/TempCalendarsInput.tsx +++ b/src/components/Calendar/TempCalendarsInput.tsx @@ -22,7 +22,7 @@ export function TempCalendarsInput({ setTempUsers: (users: User[]) => void; handleToggleEventPreview: () => void; selectedCalendars: string[]; - setSelectedCalendars: Function; + setSelectedCalendars: React.Dispatch>; }) { const dispatch = useAppDispatch(); const tempcalendars = @@ -34,7 +34,7 @@ export function TempCalendarsInput({ const prevUsersRef = useRef([]); const userColorsRef = useRef(new Map()); - const handleUserChange = async (_: any, users: User[]) => { + const handleUserChange = async (_: React.SyntheticEvent, users: User[]) => { setTempUsers(users); const prevUsers = prevUsersRef.current; diff --git a/src/components/Calendar/handlers/eventHandlers.ts b/src/components/Calendar/handlers/eventHandlers.ts index 2e1642f..6ef6e5d 100644 --- a/src/components/Calendar/handlers/eventHandlers.ts +++ b/src/components/Calendar/handlers/eventHandlers.ts @@ -13,7 +13,12 @@ import { CalendarEvent } from "@/features/Events/EventsTypes"; import { userAttendee } from "@/features/User/models/attendee"; import { createAttendee } from "@/features/User/models/attendee.mapper"; import { getDeltaInMilliseconds } from "@/utils/dateUtils"; -import { CalendarApi, DateSelectArg, EventDropArg } from "@fullcalendar/core"; +import { + CalendarApi, + DateSelectArg, + EventClickArg, + EventDropArg, +} from "@fullcalendar/core"; import { EventResizeDoneArg } from "@fullcalendar/interaction"; export interface EventHandlersProps { @@ -27,7 +32,9 @@ export interface EventHandlersProps { setEventDisplayedTemp: (temp: boolean) => void; calendars: Record; setSelectedEvent: (event: CalendarEvent) => void; - setAfterChoiceFunc: (func: Function) => void; + setAfterChoiceFunc: ( + func: ((type: "solo" | "all" | undefined) => void) | undefined + ) => void; setOpenEditModePopup: (open: string) => void; tempUsers: User[]; setTempEvent: (event: CalendarEvent) => void; @@ -87,7 +94,7 @@ export const createEventHandlers = (props: EventHandlersProps) => { setOpenEventDisplay(false); }; - const handleEventClick = (info: any) => { + const handleEventClick = (info: EventClickArg) => { info.jsEvent.preventDefault(); if (info.event.url) { diff --git a/src/components/Calendar/handlers/mouseHandlers.ts b/src/components/Calendar/handlers/mouseHandlers.ts index ad4bcf9..5a2bfab 100644 --- a/src/components/Calendar/handlers/mouseHandlers.ts +++ b/src/components/Calendar/handlers/mouseHandlers.ts @@ -1,5 +1,9 @@ +interface CalendarHTMLElement extends HTMLElement { + __calendarMouseMoveHandler?: (e: MouseEvent) => void; + __calendarMouseLeaveHandler?: () => void; +} export interface MouseHandlersProps { - calendarEl: HTMLElement; + calendarEl: CalendarHTMLElement; } export const createMouseHandlers = (props: MouseHandlersProps) => { @@ -75,24 +79,24 @@ export const createMouseHandlers = (props: MouseHandlersProps) => { calendarEl.addEventListener("mousemove", handleMouseMove); calendarEl.addEventListener("mouseleave", handleMouseLeave); - (calendarEl as any).__calendarMouseMoveHandler = handleMouseMove; - (calendarEl as any).__calendarMouseLeaveHandler = handleMouseLeave; + calendarEl.__calendarMouseMoveHandler = handleMouseMove; + calendarEl.__calendarMouseLeaveHandler = handleMouseLeave; }; const removeMouseEventListeners = () => { - if ((calendarEl as any).__calendarMouseMoveHandler) { + if (calendarEl.__calendarMouseMoveHandler) { calendarEl.removeEventListener( "mousemove", - (calendarEl as any).__calendarMouseMoveHandler + calendarEl.__calendarMouseMoveHandler ); - delete (calendarEl as any).__calendarMouseMoveHandler; + delete calendarEl.__calendarMouseMoveHandler; } - if ((calendarEl as any).__calendarMouseLeaveHandler) { + if (calendarEl.__calendarMouseLeaveHandler) { calendarEl.removeEventListener( "mouseleave", - (calendarEl as any).__calendarMouseLeaveHandler + calendarEl.__calendarMouseLeaveHandler ); - delete (calendarEl as any).__calendarMouseLeaveHandler; + delete calendarEl.__calendarMouseLeaveHandler; } }; diff --git a/src/components/Calendar/handlers/viewHandlers.ts b/src/components/Calendar/handlers/viewHandlers.ts index de60a05..9e61fc0 100644 --- a/src/components/Calendar/handlers/viewHandlers.ts +++ b/src/components/Calendar/handlers/viewHandlers.ts @@ -2,7 +2,14 @@ import { EventErrorHandler } from "@/components/Error/EventErrorHandler"; import { EventChip } from "@/components/Event/EventChip/EventChip"; import { Calendar } from "@/features/Calendars/CalendarTypes"; import { userAttendee } from "@/features/User/models/attendee"; -import { CalendarApi, NowIndicatorContentArg } from "@fullcalendar/core"; +import { + CalendarApi, + DayHeaderMountArg, + EventContentArg, + EventMountArg, + NowIndicatorContentArg, + ViewMountArg, +} from "@fullcalendar/core"; import React from "react"; import { createMouseHandlers } from "./mouseHandlers"; @@ -46,7 +53,7 @@ export const createViewHandlers = (props: ViewHandlersProps) => { } }; - const handleDayHeaderDidMount = (arg: any) => { + const handleDayHeaderDidMount = (arg: DayHeaderMountArg) => { if (arg.view.type === "timeGridWeek") { const headerEl = arg.el; @@ -61,22 +68,19 @@ export const createViewHandlers = (props: ViewHandlersProps) => { }; headerEl.addEventListener("click", handleDayHeaderClick); - (headerEl as any).__dayHeaderClickHandler = handleDayHeaderClick; + headerEl.__dayHeaderClickHandler = handleDayHeaderClick; } }; - const handleDayHeaderWillUnmount = (arg: any) => { + const handleDayHeaderWillUnmount = (arg: DayHeaderMountArg) => { const headerEl = arg.el; - if ((headerEl as any).__dayHeaderClickHandler) { - headerEl.removeEventListener( - "click", - (headerEl as any).__dayHeaderClickHandler - ); - delete (headerEl as any).__dayHeaderClickHandler; + if (headerEl.__dayHeaderClickHandler) { + headerEl.removeEventListener("click", headerEl.__dayHeaderClickHandler); + delete headerEl.__dayHeaderClickHandler; } }; - const handleViewDidMount = (arg: any) => { + const handleViewDidMount = (arg: ViewMountArg) => { if (arg.view.type === "timeGridWeek" || arg.view.type === "timeGridDay") { const calendarEl = document.querySelector(".fc") as HTMLElement; if (calendarEl) { @@ -86,15 +90,15 @@ export const createViewHandlers = (props: ViewHandlersProps) => { } }; - const handleViewWillUnmount = (arg: any) => { - if ((arg.el as any).__timeInterval) { - clearInterval((arg.el as any).__timeInterval); - delete (arg.el as any).__timeInterval; + const handleViewWillUnmount = (arg: ViewMountArg) => { + if (arg.el.__timeInterval) { + clearInterval(arg.el.__timeInterval); + delete arg.el.__timeInterval; } - if ((arg.el as any).__timeObserver) { - (arg.el as any).__timeObserver.disconnect(); - delete (arg.el as any).__timeObserver; + if (arg.el.__timeObserver) { + arg.el.__timeObserver.disconnect(); + delete arg.el.__timeObserver; } const calendarEl = document.querySelector(".fc") as HTMLElement; @@ -104,7 +108,7 @@ export const createViewHandlers = (props: ViewHandlersProps) => { } }; - const handleEventContent = (arg: any) => { + const handleEventContent = (arg: EventContentArg) => { return React.createElement(EventChip, { arg, calendars, @@ -113,7 +117,7 @@ export const createViewHandlers = (props: ViewHandlersProps) => { }); }; - const handleEventDidMount = (arg: any) => { + const handleEventDidMount = (arg: EventMountArg) => { const attendees = arg.event._def.extendedProps.attendee || []; if (!calendars[arg.event._def.extendedProps.calId]) return; const ownerEmails = new Set( diff --git a/src/components/Calendar/hooks/useCalendarEventHandlers.ts b/src/components/Calendar/hooks/useCalendarEventHandlers.ts index 10a860c..b8f2eaa 100644 --- a/src/components/Calendar/hooks/useCalendarEventHandlers.ts +++ b/src/components/Calendar/hooks/useCalendarEventHandlers.ts @@ -1,3 +1,4 @@ +/* eslint-disable react-hooks/exhaustive-deps */ import { useCallback } from "react"; import { createEventHandlers, diff --git a/src/components/Calendar/hooks/useCalendarViewHandlers.ts b/src/components/Calendar/hooks/useCalendarViewHandlers.ts index 25d63c3..83d76e1 100644 --- a/src/components/Calendar/hooks/useCalendarViewHandlers.ts +++ b/src/components/Calendar/hooks/useCalendarViewHandlers.ts @@ -1,3 +1,4 @@ +/* eslint-disable react-hooks/exhaustive-deps */ import { useCallback } from "react"; import { createViewHandlers, diff --git a/src/components/Calendar/utils/calendarUtils.ts b/src/components/Calendar/utils/calendarUtils.ts index ee7e472..dfe0912 100644 --- a/src/components/Calendar/utils/calendarUtils.ts +++ b/src/components/Calendar/utils/calendarUtils.ts @@ -1,12 +1,12 @@ -import { refreshSingularCalendar } from "@/components/Event/utils/eventUtils"; +import { AppDispatch } from "@/app/store"; import { Calendar } from "@/features/Calendars/CalendarTypes"; import { getCalendarDetailAsync } from "@/features/Calendars/services"; +import { AclEntry } from "@/features/Calendars/types/CalendarData"; import { CalendarEvent } from "@/features/Events/EventsTypes"; import { formatDateToYYYYMMDDTHHMMSS } from "@/utils/dateUtils"; import { extractEventBaseUuid } from "@/utils/extractEventBaseUuid"; import { convertEventDateTimeToISO } from "@/utils/timezone"; import { SlotLabelContentArg } from "@fullcalendar/core"; -import { ThunkDispatch } from "@reduxjs/toolkit"; import moment from "moment-timezone"; import { useI18n } from "twake-i18n"; @@ -56,7 +56,10 @@ export const checkIfCurrentWeekOrDay = (): boolean => { return !!nowIndicator; }; -export function formatEventChipTitle(e: CalendarEvent, t: Function) { +export function formatEventChipTitle( + e: CalendarEvent, + t: (key: string) => string +) { if (!e.title) { return t("event.untitled"); } @@ -72,6 +75,7 @@ export const eventToFullCalendarFormat = ( userAddress: string | undefined, pending: boolean ) => { + // eslint-disable-next-line react-hooks/rules-of-hooks const { t } = useI18n(); return filteredEvents .concat(filteredTempEvents.map((e) => ({ ...e, temp: true }))) @@ -83,7 +87,11 @@ export const eventToFullCalendarFormat = ( : true; // if there are no organizer in the event we assume it was organized by the owner const isPersonnalEvent = extractEventBaseUuid(e.calId) === userId && isOrganiser; - const convertedEvent: any = { + const convertedEvent: CalendarEvent & { + colors: Record | undefined; + editable: boolean; + priority: number; + } = { ...e, title: formatEventChipTitle(e, t), colors: e.color, @@ -147,7 +155,7 @@ export const updateCalsDetails = ( pending: boolean, rangeKey: string, previousRangeKey: string, - dispatch: Function, + dispatch: AppDispatch, calendarRange: { start: Date; end: Date }, calType?: "temp", controllers?: Map @@ -223,15 +231,9 @@ export const updateCalsDetails = ( } }; -interface AclEntry { - privilege: string; - principal: string; - protected: boolean; -} - export function getCalendarVisibility(acl: AclEntry[]): "private" | "public" { let hasRead = false; - let hasFreeBusy = false; + // const hasFreeBusy = false; if (acl) { for (const entry of acl) { if (entry.principal !== "{DAV:}authenticated") continue; diff --git a/src/components/Dialog/ResponsiveDialog.tsx b/src/components/Dialog/ResponsiveDialog.tsx index bf9f9bb..198dc2d 100644 --- a/src/components/Dialog/ResponsiveDialog.tsx +++ b/src/components/Dialog/ResponsiveDialog.tsx @@ -158,7 +158,7 @@ function ResponsiveDialog({ }, [isExpanded]); const handleClose = ( - event: {}, + event: unknown, reason: "backdropClick" | "escapeKeyDown" ) => { if (isExpanded && reason === "backdropClick") { diff --git a/src/components/Event/EditModeDialog.tsx b/src/components/Event/EditModeDialog.tsx index 74c4b7d..08aebad 100644 --- a/src/components/Event/EditModeDialog.tsx +++ b/src/components/Event/EditModeDialog.tsx @@ -1,4 +1,3 @@ -import { CalendarEvent } from "@/features/Events/EventsTypes"; import { Button, ButtonGroup, @@ -16,12 +15,10 @@ import { useI18n } from "twake-i18n"; export function EditModeDialog({ type, setOpen, - event, eventAction, }: { type: string | null; setOpen: (e: string | null) => void; - event: CalendarEvent; eventAction: (type: "solo" | "all" | undefined) => void; }) { const { t } = useI18n(); diff --git a/src/components/Event/EventChip/ErrorEventChip.tsx b/src/components/Event/EventChip/ErrorEventChip.tsx index 5945309..4ff2d6f 100644 --- a/src/components/Event/EventChip/ErrorEventChip.tsx +++ b/src/components/Event/EventChip/ErrorEventChip.tsx @@ -1,4 +1,5 @@ import { EventErrorHandler } from "@/components/Error/EventErrorHandler"; +import { EventContentArg } from "@fullcalendar/core"; import { Card, Typography } from "@linagora/twake-mui"; import { useEffect, useRef } from "react"; @@ -7,18 +8,30 @@ export function ErrorEventChip({ errorHandler, error, }: { - event: any; + event: EventContentArg["event"]; errorHandler: EventErrorHandler; - error: any; + error: unknown; }) { const hasReported = useRef(false); useEffect(() => { if (!hasReported.current) { - const message = - error instanceof Error - ? error.message - : `${error.class} error during rendering ${event._def.extendedProps.uid || event.id}`; + let message: string; + + if (error instanceof Error) { + message = error.message; + } else if ( + typeof error === "object" && + error !== null && + "class" in error + ) { + message = `${String( + (error as { class: string }).class + )} error during rendering ${event._def.extendedProps.uid || event.id}`; + } else { + message = `Unknown error during rendering ${event._def.extendedProps.uid || event.id}`; + } + errorHandler.reportError( event._def.extendedProps.uid || event.id, message diff --git a/src/components/Event/EventChip/EventChip.tsx b/src/components/Event/EventChip/EventChip.tsx index 7b55c8c..fc73da7 100644 --- a/src/components/Event/EventChip/EventChip.tsx +++ b/src/components/Event/EventChip/EventChip.tsx @@ -59,7 +59,7 @@ export function EventChip({ const ownerEmails = new Set( calendar.ownerEmails?.map((e) => e.toLowerCase()) ); - const delegated = calendar.delegated; + // const delegated = calendar.delegated; // Determine owner attendee const ownerAttendee = getOwnerAttendee(attendees, ownerEmails); @@ -258,7 +258,7 @@ export function EventChip({ !event._def.extendedProps.allday && event._def.extendedProps.organizer && !showCompact && - (window as any).displayOrgAvatar && ( + window.displayOrgAvatar && ( ; tempcalendars: Record; errorHandler: EventErrorHandler; @@ -23,9 +24,9 @@ export interface IconDisplayConfig { needAction: boolean; private: boolean; } -export function getEventDuration(event: any): number { - return moment(event._instance.range.end).diff( - moment(event._instance.range.start), +export function getEventDuration(event: EventContentArg["event"]): number { + return moment(event._instance?.range.end).diff( + moment(event._instance?.range.start), "minutes" ); } @@ -34,7 +35,10 @@ export function getBestColor(colors: { light: string; dark: string }): string { const contrastToLight = getContrastRatio(colors?.light, "#fff"); return contrastToDark > contrastToLight ? colors?.dark : colors?.light; } -export function getEventTimes(event: any, timeZone: string) { +export function getEventTimes( + event: EventContentArg["event"], + timeZone: string +) { return { startTime: moment.tz(event.start, timeZone).format("HH:mm"), endTime: moment.tz(event.end, timeZone).format("HH:mm"), @@ -52,7 +56,7 @@ export function getOwnerAttendee( export function getTitleStyle( bestColor: string, partstat?: string, - calendar?: any + calendar?: Calendar ): React.CSSProperties { const baseStyle: React.CSSProperties = { fontFamily: "Roboto", @@ -122,7 +126,7 @@ export function getCardStyle( bestColor: string, eventLength: number, partstat?: string, - calendar?: any + calendar?: Calendar ): React.CSSProperties { const baseStyle: React.CSSProperties = getCardVariantStyle( getEventVariant(eventLength), diff --git a/src/components/Event/EventDuplicate.tsx b/src/components/Event/EventDuplicate.tsx index 41f2c55..44fd9ae 100644 --- a/src/components/Event/EventDuplicate.tsx +++ b/src/components/Event/EventDuplicate.tsx @@ -1,14 +1,9 @@ -import { CalendarEvent } from "@/features/Events/EventsTypes"; import { MenuItem } from "@linagora/twake-mui"; import { useI18n } from "twake-i18n"; export default function EventDuplication({ - onClose, - event, onOpenDuplicate, }: { - onClose: Function; - event: CalendarEvent; onOpenDuplicate?: () => void; }) { const { t } = useI18n(); diff --git a/src/components/Event/EventFormFields.tsx b/src/components/Event/EventFormFields.tsx index 592bd92..60d5bd0 100644 --- a/src/components/Event/EventFormFields.tsx +++ b/src/components/Event/EventFormFields.tsx @@ -21,17 +21,15 @@ import { ToggleButton, ToggleButtonGroup, Typography, - useTheme, } from "@linagora/twake-mui"; -import { alpha } from "@mui/material/styles"; import { Close as DeleteIcon, ContentCopy as CopyIcon, - Public as PublicIcon, LocationOn as LocationIcon, + Public as PublicIcon, } from "@mui/icons-material"; -import SquareRoundedIcon from "@mui/icons-material/SquareRounded"; import LockOutlineIcon from "@mui/icons-material/LockOutline"; +import SquareRoundedIcon from "@mui/icons-material/SquareRounded"; import React from "react"; import { useI18n } from "twake-i18n"; import AttendeeSelector from "../Attendees/AttendeeSearch"; @@ -163,7 +161,6 @@ export default function EventFormFields({ onHasEndDateChangedChange, }: EventFormFieldsProps) { const { t } = useI18n(); - const theme = useTheme(); // Internal state for 4 separate fields const [startDate, setStartDate] = React.useState(""); @@ -377,7 +374,6 @@ export default function EventFormFields({ showMore, }); }, [ - title, startDate, startTime, endDate, diff --git a/src/components/Event/EventRepeat.tsx b/src/components/Event/EventRepeat.tsx index ab0a40f..2528a6b 100644 --- a/src/components/Event/EventRepeat.tsx +++ b/src/components/Event/EventRepeat.tsx @@ -25,7 +25,7 @@ export default function RepeatEvent({ }: { repetition: RepetitionObject; eventStart: Date; - setRepetition: Function; + setRepetition: (repetition: RepetitionObject) => void; isOwn?: boolean; }) { const { t } = useI18n(); diff --git a/src/components/Event/components/DateTimeFields.tsx b/src/components/Event/components/DateTimeFields.tsx index b380784..de43fb2 100644 --- a/src/components/Event/components/DateTimeFields.tsx +++ b/src/components/Event/components/DateTimeFields.tsx @@ -83,7 +83,6 @@ export const DateTimeFields: React.FC = ({ showMore, hasEndDateChanged, showEndDate, - onToggleEndDate, validation, onStartDateChange, onStartTimeChange, diff --git a/src/components/Event/components/DateTimeSummary.tsx b/src/components/Event/components/DateTimeSummary.tsx index 91c1d11..36223ea 100644 --- a/src/components/Event/components/DateTimeSummary.tsx +++ b/src/components/Event/components/DateTimeSummary.tsx @@ -88,7 +88,7 @@ export const DateTimeSummary: React.FC = ({ const offset = getTimezoneOffset(tz, dateForOffset); const tzName = tz.replace(/_/g, " "); return `(${offset}) ${tzName}`; - } catch (error) { + } catch { return tz.replace(/_/g, " "); } }; diff --git a/src/components/Event/components/EditableTimeField.tsx b/src/components/Event/components/EditableTimeField.tsx index 3335dc5..92339d1 100644 --- a/src/components/Event/components/EditableTimeField.tsx +++ b/src/components/Event/components/EditableTimeField.tsx @@ -19,12 +19,12 @@ import { parseTimeInput } from "../utils/dateTimeHelpers"; type FieldType = "date" | "time" | "date-time"; -type GenericPickerFieldProps = PickerFieldProps & { +type GenericPickerFieldProps = PickerFieldProps & { fieldType: FieldType; validator: ( - value: any, + value: Dayjs | null, context: PickerValidationScope, - adapter: PickerFieldAdapter + adapter: PickerFieldAdapter ) => string | null; }; @@ -223,7 +223,7 @@ function EditableTimePickerField(props: GenericPickerFieldProps) { dispatchCloseOtherPickers(); }; - const handleBlur = (e: React.FocusEvent) => { + const handleBlur = (_e: React.FocusEvent) => { // If dropdown is open, don't parse input // MUI will handle selection and sync value via useEffect if (pickerContext.open) { diff --git a/src/components/Event/components/ReadOnlyPickerField.tsx b/src/components/Event/components/ReadOnlyPickerField.tsx index 55a6b40..bfdb5de 100644 --- a/src/components/Event/components/ReadOnlyPickerField.tsx +++ b/src/components/Event/components/ReadOnlyPickerField.tsx @@ -12,15 +12,16 @@ import { useValidation, validateDate, } from "@mui/x-date-pickers/validation"; +import { Dayjs } from "dayjs"; type FieldType = "date" | "time" | "date-time"; -type GenericPickerFieldProps = PickerFieldProps & { +type GenericPickerFieldProps = PickerFieldProps & { fieldType: FieldType; validator: ( - value: any, + value: Dayjs | null, context: PickerValidationScope, - adapter: PickerFieldAdapter + adapter: PickerFieldAdapter ) => string | null; }; diff --git a/src/components/Event/eventHandlers/eventHandlers.ts b/src/components/Event/eventHandlers/eventHandlers.ts index 1e0a0c0..2eb3051 100644 --- a/src/components/Event/eventHandlers/eventHandlers.ts +++ b/src/components/Event/eventHandlers/eventHandlers.ts @@ -119,28 +119,18 @@ export async function handleRSVP( export function handleDelete( isRecurring: boolean, typeOfAction: "solo" | "all" | undefined, - onClose: (event: {}, reason: "backdropClick" | "escapeKeyDown") => void, - dispatch: Function, + onClose: (event: unknown, reason: "backdropClick" | "escapeKeyDown") => void, + dispatch: AppDispatch, calendar: Calendar, event: CalendarEvent, calId: string, eventId: string ) { - if (isRecurring) { - onClose({}, "backdropClick"); - if (typeOfAction === "solo") { - dispatch(deleteEventInstanceAsync({ cal: calendar, event })); - } else { - dispatch( - deleteEventAsync({ - calId, - eventId, - eventURL: event.URL, - }) - ); - } + onClose({}, "backdropClick"); + + if (isRecurring && typeOfAction === "solo") { + dispatch(deleteEventInstanceAsync({ cal: calendar, event })); } else { - onClose({}, "backdropClick"); dispatch( deleteEventAsync({ calId, diff --git a/src/components/Event/utils/eventUtils.tsx b/src/components/Event/utils/eventUtils.tsx index a5a6207..ab51f05 100644 --- a/src/components/Event/utils/eventUtils.tsx +++ b/src/components/Event/utils/eventUtils.tsx @@ -16,7 +16,7 @@ import CheckCircleIcon from "@mui/icons-material/CheckCircle"; export function renderAttendeeBadge( a: userAttendee, key: string, - t: Function, + t: (key: string) => string, isFull?: boolean, isOrganizer?: boolean ) { @@ -110,7 +110,9 @@ export async function refreshCalendars( ) { if (process.env.NODE_ENV === "test") return; - !calType && (await dispatch(getCalendarsListAsync())); + if (!calType) { + await dispatch(getCalendarsListAsync()); + } const results = await Promise.allSettled( calendars.map((calendar) => diff --git a/src/components/Loading/SnackBarAlert.tsx b/src/components/Loading/SnackBarAlert.tsx index a1b4393..4681dcb 100644 --- a/src/components/Loading/SnackBarAlert.tsx +++ b/src/components/Loading/SnackBarAlert.tsx @@ -8,7 +8,7 @@ export function SnackbarAlert({ severity = "success", }: { open: boolean; - setOpen: Function; + setOpen: (o: boolean) => void; message: string; severity?: AlertColor; }) { diff --git a/src/components/Menubar/EventSearchBar.tsx b/src/components/Menubar/EventSearchBar.tsx index 228fb4c..07ea2ed 100644 --- a/src/components/Menubar/EventSearchBar.tsx +++ b/src/components/Menubar/EventSearchBar.tsx @@ -142,7 +142,7 @@ export default function SearchBar() { setFilterError(false); }; - const handleContactSelect = (_event: any, contacts: User[]) => { + const handleContactSelect = (contacts: User[]) => { setSelectedContacts(contacts); setSearch(""); if (contacts.length > 0) { @@ -173,7 +173,9 @@ export default function SearchBar() { dispatch(setView("search")); setAnchorEl(null); } else { - filterOpen && setFilterError(true); + if (filterOpen) { + setFilterError(true); + } } }; @@ -222,8 +224,8 @@ export default function SearchBar() { {extended && ( { - handleContactSelect(event, users); + onChange={(_event, users) => { + handleContactSelect(users); }} objectTypes={["user", "contact"]} onToggleEventPreview={() => {}} diff --git a/src/components/Menubar/Menubar.tsx b/src/components/Menubar/Menubar.tsx index 895100e..e9935c2 100644 --- a/src/components/Menubar/Menubar.tsx +++ b/src/components/Menubar/Menubar.tsx @@ -56,9 +56,9 @@ export function Menubar({ currentView, onViewChange, }: MenubarProps) { - const { t, lang } = useI18n(); // deliberately NOT using f() + const { t } = useI18n(); // deliberately NOT using f() const user = useAppSelector((state) => state.user.userData); - const applist: AppIconProps[] = (window as any).appList ?? []; + const applist: AppIconProps[] = window.appList ?? []; const [anchorEl, setAnchorEl] = useState(null); const [userMenuAnchorEl, setUserMenuAnchorEl] = useState( null diff --git a/src/features/Calendars/CalendarApi.ts b/src/features/Calendars/CalendarApi.ts index 48788e2..898e472 100644 --- a/src/features/Calendars/CalendarApi.ts +++ b/src/features/Calendars/CalendarApi.ts @@ -1,10 +1,11 @@ import { api } from "@/utils/apiUtils"; +import { CalendarInput, CalendarList } from "./types/CalendarData"; export async function getCalendars( userId: string, scope: string = "personal=true&sharedDelegationStatus=accepted&sharedPublicSubscription=true&", signal?: AbortSignal -) { +): Promise { const calendars = await api .get(`dav/calendars/${userId}.json?${scope}`, { headers: { @@ -13,7 +14,7 @@ export async function getCalendars( signal, }) .json(); - return calendars; + return calendars as CalendarList; } export async function getCalendar( @@ -59,7 +60,7 @@ export async function postCalendar( export async function addSharedCalendar( userId: string, calId: string, - cal: Record + cal: CalendarInput ) { const response = await api.post(`dav/calendars/${userId}.json`, { headers: { @@ -70,7 +71,7 @@ export async function addSharedCalendar( ...cal.cal, "dav:name": cal.cal["dav:name"] === "#default" - ? cal.owner.displayName + "'s calendar" + ? (cal.owner?.displayName ?? "Unknown") + "'s calendar" : cal.cal["dav:name"], "calendarserver:source": { acl: cal.cal.acl, diff --git a/src/features/Calendars/services/addSharedCalendarAsync.ts b/src/features/Calendars/services/addSharedCalendarAsync.ts index c1a6e26..fafbd75 100644 --- a/src/features/Calendars/services/addSharedCalendarAsync.ts +++ b/src/features/Calendars/services/addSharedCalendarAsync.ts @@ -1,7 +1,8 @@ -import { formatReduxError } from "@/utils/errorUtils"; -import { createAsyncThunk } from "@reduxjs/toolkit"; import { getUserDetails } from "@/features/User/userAPI"; +import { toRejectedError } from "@/utils/errorUtils"; +import { createAsyncThunk } from "@reduxjs/toolkit"; import { addSharedCalendar } from "../CalendarApi"; +import { CalendarInput } from "../types/CalendarData"; import { RejectedError } from "../types/RejectedError"; export const addSharedCalendarAsync = createAsyncThunk< @@ -14,14 +15,14 @@ export const addSharedCalendarAsync = createAsyncThunk< owner: string; ownerEmails: string[]; }, - { userId: string; calId: string; cal: Record }, + { userId: string; calId: string; cal: CalendarInput }, { rejectValue: RejectedError } >( "calendars/addSharedCalendar", async ({ userId, calId, cal }, { rejectWithValue }) => { try { await addSharedCalendar(userId, calId, cal); - const ownerData: any = await getUserDetails( + const ownerData = await getUserDetails( cal.cal._links.self.href .replace("/calendars/", "") .replace(".json", "") @@ -29,28 +30,25 @@ export const addSharedCalendarAsync = createAsyncThunk< ); return { - calId: cal.cal._links.self.href - .replace("/calendars/", "") + calId: cal.cal._links.self?.href + ?.replace("/calendars/", "") .replace(".json", ""), color: cal.color, link: `/calendars/${userId}/${calId}.json`, - desc: cal.cal["caldav:description"], + desc: cal.cal["caldav:description"] ?? "", name: ownerData.id !== userId && cal.cal["dav:name"] === "#default" ? `${ownerData.firstname ? `${ownerData.firstname} ` : ""}${ - ownerData.lastname + ownerData.lastname ?? "" }` + "'s calendar" - : cal.cal["dav:name"], + : (cal.cal["dav:name"] ?? ""), owner: `${ownerData.firstname ? `${ownerData.firstname} ` : ""}${ - ownerData.lastname + ownerData.lastname ?? "" }`, ownerEmails: ownerData.emails, }; - } catch (err: any) { - return rejectWithValue({ - message: formatReduxError(err), - status: err.response?.status, - }); + } catch (err) { + return rejectWithValue(toRejectedError(err)); } } ); diff --git a/src/features/Calendars/services/createCalendarAsync.ts b/src/features/Calendars/services/createCalendarAsync.ts index 9a3e11d..e116899 100644 --- a/src/features/Calendars/services/createCalendarAsync.ts +++ b/src/features/Calendars/services/createCalendarAsync.ts @@ -1,6 +1,5 @@ -import { getUserDetails } from "@/features/User/userAPI"; import { userData } from "@/features/User/userDataTypes"; -import { formatReduxError } from "@/utils/errorUtils"; +import { toRejectedError } from "@/utils/errorUtils"; import { createAsyncThunk } from "@reduxjs/toolkit"; import { postCalendar } from "../CalendarApi"; import { RejectedError } from "../types/RejectedError"; @@ -45,11 +44,8 @@ export const createCalendarAsync = createAsyncThunk< owner, ownerEmails: userData.email ? [userData.email] : [], }; - } catch (err: any) { - return rejectWithValue({ - message: formatReduxError(err), - status: err.response?.status, - }); + } catch (err) { + return rejectWithValue(toRejectedError(err)); } } ); diff --git a/src/features/Calendars/services/deleteEventAsync.ts b/src/features/Calendars/services/deleteEventAsync.ts index 2b59750..0169338 100644 --- a/src/features/Calendars/services/deleteEventAsync.ts +++ b/src/features/Calendars/services/deleteEventAsync.ts @@ -1,5 +1,5 @@ import { deleteEvent } from "@/features/Events/EventApi"; -import { formatReduxError } from "@/utils/errorUtils"; +import { toRejectedError } from "@/utils/errorUtils"; import { createAsyncThunk } from "@reduxjs/toolkit"; import { RejectedError } from "../types/RejectedError"; @@ -13,11 +13,8 @@ export const deleteEventAsync = createAsyncThunk< try { await deleteEvent(eventURL); return { calId, eventId }; - } catch (err: any) { - return rejectWithValue({ - message: formatReduxError(err), - status: err.response?.status, - }); + } catch (err) { + return rejectWithValue(toRejectedError(err)); } } ); diff --git a/src/features/Calendars/services/deleteEventInstanceAsync.ts b/src/features/Calendars/services/deleteEventInstanceAsync.ts index 4a8b8a0..f188b4b 100644 --- a/src/features/Calendars/services/deleteEventInstanceAsync.ts +++ b/src/features/Calendars/services/deleteEventInstanceAsync.ts @@ -11,12 +11,13 @@ export const deleteEventInstanceAsync = createAsyncThunk< { rejectValue: RejectedError } >("calendars/delEventInstance", async ({ cal, event }, { rejectWithValue }) => { try { - await deleteEventInstance(event, cal.ownerEmails?.[0]); + await deleteEventInstance(event); return { calId: cal.id, eventId: event.uid }; - } catch (err: any) { + } catch (err) { + const error = err as { response?: { status?: number } }; return rejectWithValue({ message: formatReduxError(err), - status: err.response?.status, + status: error.response?.status, }); } }); diff --git a/src/features/Calendars/services/getCalendarDetailAsync.ts b/src/features/Calendars/services/getCalendarDetailAsync.ts index d25d264..0765524 100644 --- a/src/features/Calendars/services/getCalendarDetailAsync.ts +++ b/src/features/Calendars/services/getCalendarDetailAsync.ts @@ -1,9 +1,14 @@ import { CalendarEvent } from "@/features/Events/EventsTypes"; -import { formatReduxError } from "@/utils/errorUtils"; +import { + CalendarData, + CalendarItem, +} from "@/features/Calendars/types/CalendarData"; +import { toRejectedError } from "@/utils/errorUtils"; import { createAsyncThunk } from "@reduxjs/toolkit"; import { getCalendar } from "../CalendarApi"; import { RejectedError } from "../types/RejectedError"; import { extractCalendarEvents } from "../utils/extractCalendarEvents"; +import { defaultColors } from "@/components/Calendar/utils/calendarColorsUtils"; export const getCalendarDetailAsync = createAsyncThunk< { @@ -23,24 +28,27 @@ export const getCalendarDetailAsync = createAsyncThunk< "calendars/getCalendarDetails", async ({ calId, match, calType, signal }, { rejectWithValue }) => { try { - const calendar = (await getCalendar(calId, match, signal)) as any; + const calendar = (await getCalendar( + calId, + match, + signal + )) as CalendarData; - const color = calendar["apple:color"]; + const color = calendar["apple:color"] + ? { light: calendar["apple:color"], dark: calendar["apple:color"] } + : defaultColors[0]; const syncToken = calendar._embedded?.["sync-token"]; const items = calendar._embedded?.["dav:item"]; const events: CalendarEvent[] = Array.isArray(items) - ? items.flatMap((item: any) => + ? items.flatMap((item: CalendarItem) => extractCalendarEvents(item, { calId, color }) ) : []; return { calId, events, calType, syncToken }; - } catch (err: any) { - return rejectWithValue({ - message: formatReduxError(err), - status: err.response?.status, - }); + } catch (err) { + return rejectWithValue(toRejectedError(err)); } } ); diff --git a/src/features/Calendars/services/getCalendarsListAsync.ts b/src/features/Calendars/services/getCalendarsListAsync.ts index 8d50805..0667843 100644 --- a/src/features/Calendars/services/getCalendarsListAsync.ts +++ b/src/features/Calendars/services/getCalendarsListAsync.ts @@ -1,32 +1,33 @@ +import { RootState } from "@/app/store"; +import { OpenPaasUserData } from "@/features/User/type/OpenPaasUserData"; import { getOpenPaasUser, getUserDetails } from "@/features/User/userAPI"; import { formatReduxError } from "@/utils/errorUtils"; import { createAsyncThunk } from "@reduxjs/toolkit"; import { getCalendars } from "../CalendarApi"; -import { RejectedError } from "../types/RejectedError"; import { Calendar } from "../CalendarTypes"; +import { CalendarData } from "../types/CalendarData"; +import { RejectedError } from "../types/RejectedError"; import { normalizeCalendar } from "../utils/normalizeCalendar"; export const getCalendarsListAsync = createAsyncThunk< { importedCalendars: Record; errors: string }, void, - { rejectValue: RejectedError; state: any } + { rejectValue: RejectedError; state: RootState } >("calendars/getCalendars", async (_, { rejectWithValue, getState }) => { - const state = getState() as any; + const state = getState(); const existingCalendars = state.calendars.list || {}; const existingUser = { id: state.user?.userData?.openpaasId || undefined }; try { const fetchedCalendars: Record = {}; const user = existingUser.id ? existingUser - : ((await getOpenPaasUser()) as Record); - const calendars = (await getCalendars(user.id)) as Record; - const rawCalendars = calendars._embedded["dav:calendar"] as Record< - string, - any - >[]; + : ((await getOpenPaasUser()) as OpenPaasUserData); + const calendars = await getCalendars(user.id); + const rawCalendars = calendars._embedded["dav:calendar"]; + const errors: string[] = []; - const normalizedCalendars = rawCalendars.map((cal) => + const normalizedCalendars = rawCalendars.map((cal: CalendarData) => normalizeCalendar(cal) ); @@ -34,14 +35,14 @@ export const getCalendarsListAsync = createAsyncThunk< new Set(normalizedCalendars.map(({ ownerId }) => ownerId).filter(Boolean)) ); - const ownerDataMap = new Map(); + const ownerDataMap = new Map(); const OWNER_BATCH_SIZE = 20; const fetchOwnerData = async (ownerId: string) => { try { const data = await getUserDetails(ownerId); ownerDataMap.set(ownerId, data); - } catch (error: any) { + } catch (error) { console.error(`Failed to fetch user details for ${ownerId}:`, error); ownerDataMap.set(ownerId, { firstname: "", @@ -72,7 +73,7 @@ export const getCalendarsListAsync = createAsyncThunk< const color = { light: cal["apple:color"] ?? "#006BD8", - dark: cal["X-TWAKE-Dark-theme-color"] ?? "#FFF", + dark: "#FFF", }; fetchedCalendars[id] = { id, @@ -120,10 +121,11 @@ export const getCalendarsListAsync = createAsyncThunk< importedCalendars, errors: errors.join("\n"), }; - } catch (err: any) { + } catch (err) { + const error = err as { response?: { status?: number } }; return rejectWithValue({ message: formatReduxError(err), - status: err.response?.status, + status: error.response?.status, }); } }); diff --git a/src/features/Calendars/services/getEventAsync.ts b/src/features/Calendars/services/getEventAsync.ts index b0f6e37..a115684 100644 --- a/src/features/Calendars/services/getEventAsync.ts +++ b/src/features/Calendars/services/getEventAsync.ts @@ -15,10 +15,11 @@ export const getEventAsync = createAsyncThunk< calId: event.calId, event: response, }; - } catch (err: any) { + } catch (err) { + const error = err as { response?: { status?: number } }; return rejectWithValue({ message: formatReduxError(err), - status: err.response?.status, + status: error.response?.status, }); } }); diff --git a/src/features/Calendars/services/getTempCalendarsListAsync.ts b/src/features/Calendars/services/getTempCalendarsListAsync.ts index 2ef35db..8b9dc3b 100644 --- a/src/features/Calendars/services/getTempCalendarsListAsync.ts +++ b/src/features/Calendars/services/getTempCalendarsListAsync.ts @@ -4,8 +4,8 @@ import { getUserDetails } from "@/features/User/userAPI"; import { formatReduxError } from "@/utils/errorUtils"; import { createAsyncThunk } from "@reduxjs/toolkit"; import { getCalendars } from "../CalendarApi"; -import { RejectedError } from "../types/RejectedError"; import { Calendar } from "../CalendarTypes"; +import { RejectedError } from "../types/RejectedError"; export const getTempCalendarsListAsync = createAsyncThunk< Record, @@ -20,10 +20,10 @@ export const getTempCalendarsListAsync = createAsyncThunk< `TRANSLATION:calendar.userDoesNotHaveValidId|name=${encodeURIComponent(username)}` ); } - const calendars = (await getCalendars( + const calendars = await getCalendars( tempUser.openpaasId, "sharedPublic=true&" - )) as Record; + ); const rawCalendars = calendars._embedded?.["dav:calendar"]; if (!rawCalendars || rawCalendars.length === 0) { @@ -36,17 +36,20 @@ export const getTempCalendarsListAsync = createAsyncThunk< } for (const cal of rawCalendars) { - const name = cal["dav:name"]; - const description = cal["caldav:description"]; + const name = cal["dav:name"] ?? ""; + const description = cal["caldav:description"] ?? ""; const delegated = cal["calendarserver:delegatedsource"] ? true : false; const source = cal["calendarserver:source"] - ? cal["calendarserver:source"]._links.self.href - : cal._links.self.href; - const link = cal._links.self.href; + ? cal["calendarserver:source"]._links.self?.href + : cal._links.self?.href; + if (!source) { + throw new Error("No source for calendar"); + } + const link = cal._links.self?.href ?? ""; const id = source.replace("/calendars/", "").replace(".json", ""); - const visibility = getCalendarVisibility(cal["acl"]); - const ownerData: any = await getUserDetails(id.split("/")[0]); + const visibility = getCalendarVisibility(cal["acl"] ?? []); + const ownerData = await getUserDetails(id.split("/")[0]); importedCalendars[id] = { id, @@ -66,10 +69,11 @@ export const getTempCalendarsListAsync = createAsyncThunk< } return importedCalendars; - } catch (err: any) { + } catch (err) { + const error = err as { response?: { status?: number } }; return rejectWithValue({ message: formatReduxError(err), - status: err.response?.status, + status: error.response?.status, }); } }); diff --git a/src/features/Calendars/services/importEventFromFileAsync.ts b/src/features/Calendars/services/importEventFromFileAsync.ts index 47f3843..a5d1289 100644 --- a/src/features/Calendars/services/importEventFromFileAsync.ts +++ b/src/features/Calendars/services/importEventFromFileAsync.ts @@ -22,10 +22,11 @@ export const importEventFromFileAsync = createAsyncThunk< }); } await importEventFromFile(id, calLink); - } catch (err: any) { + } catch (err) { + const error = err as { response?: { status?: number } }; return rejectWithValue({ message: formatReduxError(err), - status: err.response?.status, + status: error.response?.status, }); } }); diff --git a/src/features/Calendars/services/moveEventAsync.ts b/src/features/Calendars/services/moveEventAsync.ts index 1268196..ac0ae13 100644 --- a/src/features/Calendars/services/moveEventAsync.ts +++ b/src/features/Calendars/services/moveEventAsync.ts @@ -1,15 +1,9 @@ import { moveEvent } from "@/features/Events/EventApi"; import { CalendarEvent } from "@/features/Events/EventsTypes"; -import { parseCalendarEvent } from "@/features/Events/eventUtils"; -import { - computeWeekRange, - formatDateToYYYYMMDDTHHMMSS, -} from "@/utils/dateUtils"; -import { formatReduxError } from "@/utils/errorUtils"; +import { toRejectedError } from "@/utils/errorUtils"; import { createAsyncThunk } from "@reduxjs/toolkit"; -import { getCalendar } from "../CalendarApi"; -import { RejectedError } from "../types/RejectedError"; import { Calendar } from "../CalendarTypes"; +import { RejectedError } from "../types/RejectedError"; export const moveEventAsync = createAsyncThunk< { calId: string }, @@ -24,11 +18,8 @@ export const moveEventAsync = createAsyncThunk< return { calId: cal.id, }; - } catch (err: any) { - return rejectWithValue({ - message: formatReduxError(err), - status: err.response?.status, - }); + } catch (err) { + return rejectWithValue(toRejectedError(err)); } } ); diff --git a/src/features/Calendars/services/patchACLCalendarAsync.ts b/src/features/Calendars/services/patchACLCalendarAsync.ts index 1c939e2..f94aca5 100644 --- a/src/features/Calendars/services/patchACLCalendarAsync.ts +++ b/src/features/Calendars/services/patchACLCalendarAsync.ts @@ -1,4 +1,4 @@ -import { formatReduxError } from "@/utils/errorUtils"; +import { toRejectedError } from "@/utils/errorUtils"; import { createAsyncThunk } from "@reduxjs/toolkit"; import { updateAclCalendar } from "../CalendarApi"; import { RejectedError } from "../types/RejectedError"; @@ -25,11 +25,8 @@ export const patchACLCalendarAsync = createAsyncThunk< calLink, request, }; - } catch (err: any) { - return rejectWithValue({ - message: formatReduxError(err), - status: err.response?.status, - }); + } catch (err) { + return rejectWithValue(toRejectedError(err)); } } ); diff --git a/src/features/Calendars/services/patchCalendarAsync.ts b/src/features/Calendars/services/patchCalendarAsync.ts index 4c94f1f..ce7cb59 100644 --- a/src/features/Calendars/services/patchCalendarAsync.ts +++ b/src/features/Calendars/services/patchCalendarAsync.ts @@ -1,4 +1,4 @@ -import { formatReduxError } from "@/utils/errorUtils"; +import { toRejectedError } from "@/utils/errorUtils"; import { createAsyncThunk } from "@reduxjs/toolkit"; import { proppatchCalendar } from "../CalendarApi"; import { RejectedError } from "../types/RejectedError"; @@ -25,11 +25,8 @@ export const patchCalendarAsync = createAsyncThunk< calLink, patch, }; - } catch (err: any) { - return rejectWithValue({ - message: formatReduxError(err), - status: err.response?.status, - }); + } catch (err) { + return rejectWithValue(toRejectedError(err)); } } ); diff --git a/src/features/Calendars/services/putEventAsync.ts b/src/features/Calendars/services/putEventAsync.ts index 8ba1c03..3bdd7e3 100644 --- a/src/features/Calendars/services/putEventAsync.ts +++ b/src/features/Calendars/services/putEventAsync.ts @@ -1,9 +1,9 @@ import { putEvent } from "@/features/Events/EventApi"; import { CalendarEvent } from "@/features/Events/EventsTypes"; -import { formatReduxError } from "@/utils/errorUtils"; +import { toRejectedError } from "@/utils/errorUtils"; import { createAsyncThunk } from "@reduxjs/toolkit"; -import { RejectedError } from "../types/RejectedError"; import { Calendar } from "../CalendarTypes"; +import { RejectedError } from "../types/RejectedError"; export const putEventAsync = createAsyncThunk< { calId: string; calType?: "temp" }, @@ -22,11 +22,8 @@ export const putEventAsync = createAsyncThunk< calId: cal.id, calType, }; - } catch (err: any) { - return rejectWithValue({ - message: formatReduxError(err), - status: err.response?.status, - }); + } catch (err) { + return rejectWithValue(toRejectedError(err)); } } ); diff --git a/src/features/Calendars/services/refreshCalendar.ts b/src/features/Calendars/services/refreshCalendar.ts index 7b8aa72..f406d09 100644 --- a/src/features/Calendars/services/refreshCalendar.ts +++ b/src/features/Calendars/services/refreshCalendar.ts @@ -1,10 +1,10 @@ import { CalendarEvent } from "@/features/Events/EventsTypes"; -import { formatReduxError } from "@/utils/errorUtils"; +import { toRejectedError } from "@/utils/errorUtils"; import { createAsyncThunk } from "@reduxjs/toolkit"; import pMap from "p-map"; import { fetchSyncTokenChanges } from "../api/fetchSyncTokenChanges"; -import { RejectedError } from "../types/RejectedError"; import { Calendar } from "../CalendarTypes"; +import { RejectedError } from "../types/RejectedError"; import { expandEventFunction } from "../utils/expandEventFunction"; import { processSyncUpdates } from "../utils/processSyncTokenUpdates"; @@ -70,11 +70,8 @@ export const refreshCalendarWithSyncToken = createAsyncThunk< syncToken: newSyncToken, syncStatus: newSyncToken ? "SUCCESS" : "NO_NEW_SYNC_TOKEN", }; - } catch (err: any) { - return rejectWithValue({ - message: formatReduxError(err), - status: err.response?.status, - }); + } catch (err) { + return rejectWithValue(toRejectedError(err)); } } ); diff --git a/src/features/Calendars/services/removeCalendarAsync.ts b/src/features/Calendars/services/removeCalendarAsync.ts index cb51a96..077e074 100644 --- a/src/features/Calendars/services/removeCalendarAsync.ts +++ b/src/features/Calendars/services/removeCalendarAsync.ts @@ -1,4 +1,4 @@ -import { formatReduxError } from "@/utils/errorUtils"; +import { toRejectedError } from "@/utils/errorUtils"; import { createAsyncThunk } from "@reduxjs/toolkit"; import { removeCalendar } from "../CalendarApi"; import { RejectedError } from "../types/RejectedError"; @@ -20,11 +20,8 @@ export const removeCalendarAsync = createAsyncThunk< return { calId, }; - } catch (err: any) { - return rejectWithValue({ - message: formatReduxError(err), - status: err.response?.status, - }); + } catch (err) { + return rejectWithValue(toRejectedError(err)); } } ); diff --git a/src/features/Calendars/services/updateEventInstanceAsync.ts b/src/features/Calendars/services/updateEventInstanceAsync.ts index 6f8a5ee..40176b5 100644 --- a/src/features/Calendars/services/updateEventInstanceAsync.ts +++ b/src/features/Calendars/services/updateEventInstanceAsync.ts @@ -1,6 +1,6 @@ import { putEventWithOverrides } from "@/features/Events/EventApi"; import { CalendarEvent } from "@/features/Events/EventsTypes"; -import { formatReduxError } from "@/utils/errorUtils"; +import { toRejectedError } from "@/utils/errorUtils"; import { createAsyncThunk } from "@reduxjs/toolkit"; import { RejectedError } from "../types/RejectedError"; import { Calendar } from "../CalendarTypes"; @@ -15,11 +15,8 @@ export const updateEventInstanceAsync = createAsyncThunk< try { await putEventWithOverrides(event, cal.ownerEmails?.[0]); return { calId: cal.id, event }; - } catch (err: any) { - return rejectWithValue({ - message: formatReduxError(err), - status: err.response?.status, - }); + } catch (err) { + return rejectWithValue(toRejectedError(err)); } } ); diff --git a/src/features/Calendars/services/updateSeriesAsync.ts b/src/features/Calendars/services/updateSeriesAsync.ts index 39ac5a7..8eed05a 100644 --- a/src/features/Calendars/services/updateSeriesAsync.ts +++ b/src/features/Calendars/services/updateSeriesAsync.ts @@ -1,9 +1,9 @@ import { updateSeries } from "@/features/Events/EventApi"; import { CalendarEvent } from "@/features/Events/EventsTypes"; -import { formatReduxError } from "@/utils/errorUtils"; +import { toRejectedError } from "@/utils/errorUtils"; import { createAsyncThunk } from "@reduxjs/toolkit"; -import { RejectedError } from "../types/RejectedError"; import { Calendar } from "../CalendarTypes"; +import { RejectedError } from "../types/RejectedError"; export const updateSeriesAsync = createAsyncThunk< void, @@ -14,11 +14,8 @@ export const updateSeriesAsync = createAsyncThunk< async ({ cal, event, removeOverrides = true }, { rejectWithValue }) => { try { await updateSeries(event, cal.ownerEmails?.[0] ?? "", removeOverrides); - } catch (err: any) { - return rejectWithValue({ - message: formatReduxError(err), - status: err.response?.status, - }); + } catch (err) { + return rejectWithValue(toRejectedError(err)); } } ); diff --git a/src/features/Calendars/types/AsyncThunkResult.ts b/src/features/Calendars/types/AsyncThunkResult.ts new file mode 100644 index 0000000..3768c26 --- /dev/null +++ b/src/features/Calendars/types/AsyncThunkResult.ts @@ -0,0 +1,7 @@ +// Type for async thunk results +export interface AsyncThunkResult { + type: string; + error?: { message?: string }; + payload?: { message?: string }; + unwrap?: () => Promise; +} diff --git a/src/features/Calendars/types/CalendarData.ts b/src/features/Calendars/types/CalendarData.ts new file mode 100644 index 0000000..705d262 --- /dev/null +++ b/src/features/Calendars/types/CalendarData.ts @@ -0,0 +1,90 @@ +import { User } from "@/components/Attendees/PeopleSearch"; +import { CalDavLink } from "../api/types"; + +// Access control entry +export interface AclEntry { + privilege: string; + principal: string; + protected: boolean; +} + +// VObject property value can be various types +export type VObjectValue = + | string + | number + | boolean + | Date + | Record + | null + | RepetitionRule + | undefined; + +// VObject property tuple +export type VObjectProperty = [ + string, + Record, + string | Array, + VObjectValue, +]; + +export type VCalComponent = [ + string, + VObjectProperty[], + VCalComponent[], + ...unknown[], +]; + +export interface Organizer { + cn?: string; + email: string; +} + +// The `dav:item` object in _embedded +export interface CalendarItem { + _links: CalDavLink; + etag: string; + status: number; + data: [ + "vcalendar", + Array< + VObjectProperty | [string, VObjectProperty[], unknown[]] // vevent array + >, + ]; +} + +// Main calendar data +export interface CalendarData { + _links: CalDavLink; + "caldav:description"?: string; + "dav:name"?: string; + "apple:color"?: string; + id?: string; + acl?: AclEntry[]; + invite?: unknown; + _embedded: { + "sync-token": string; + "dav:item": CalendarItem[]; + }; + "calendarserver:source"?: { _links: CalDavLink }; + "calendarserver:delegatedsource"?: string; +} + +export interface CalendarList { + _embedded: { "dav:calendar": CalendarData[] }; +} + +// Calendar input for forms or UI +export interface CalendarInput { + cal: CalendarData; + color: Record; + owner?: User; +} + +// Vevent repetition rule +export interface RepetitionRule { + freq: string; + interval?: number; + count?: number; + until?: string; + byday?: string | string[]; +} diff --git a/src/features/Calendars/utils/expandEventFunction.ts b/src/features/Calendars/utils/expandEventFunction.ts index eac2893..84da10a 100644 --- a/src/features/Calendars/utils/expandEventFunction.ts +++ b/src/features/Calendars/utils/expandEventFunction.ts @@ -20,7 +20,7 @@ export function expandEventFunction( }); return events; } catch (err) { - console.error("Failed to fetch event", eventUrl); + console.error("Failed to fetch event", eventUrl, err); return undefined; } }; diff --git a/src/features/Calendars/utils/extractCalendarEvents.ts b/src/features/Calendars/utils/extractCalendarEvents.ts index 9acbb20..a301756 100644 --- a/src/features/Calendars/utils/extractCalendarEvents.ts +++ b/src/features/Calendars/utils/extractCalendarEvents.ts @@ -2,6 +2,7 @@ import { defaultColors } from "@/components/Calendar/utils/calendarColorsUtils"; import { CalendarEvent } from "@/features/Events/EventsTypes"; import { parseCalendarEvent } from "@/features/Events/eventUtils"; import { CalDavItem } from "../api/types"; +import { VCalComponent } from "../types/CalendarData"; export function extractCalendarEvents( item: CalDavItem, @@ -50,7 +51,7 @@ export function extractCalendarEvents( .filter(Boolean) as CalendarEvent[]; } -function extractValarm(vevent: any[]) { +function extractValarm(vevent: VCalComponent[]) { const subComponents = vevent[2]; if (!Array.isArray(subComponents)) { return undefined; diff --git a/src/features/Calendars/utils/normalizeCalendar.ts b/src/features/Calendars/utils/normalizeCalendar.ts index 37fc541..3cd6ef0 100644 --- a/src/features/Calendars/utils/normalizeCalendar.ts +++ b/src/features/Calendars/utils/normalizeCalendar.ts @@ -1,19 +1,23 @@ import { getCalendarVisibility } from "@/components/Calendar/utils/calendarUtils"; +import { CalendarData } from "../types/CalendarData"; -export function normalizeCalendar(rawCalendar: Record) { +export function normalizeCalendar(rawCalendar: CalendarData) { const description = rawCalendar["caldav:description"]; let delegated = false; let source = rawCalendar["calendarserver:source"] - ? rawCalendar["calendarserver:source"]._links.self.href - : rawCalendar._links.self.href; - const link = rawCalendar._links.self.href; + ? rawCalendar["calendarserver:source"]._links.self?.href + : rawCalendar._links.self?.href; + const link = rawCalendar._links.self?.href; if (rawCalendar["calendarserver:delegatedsource"]) { source = rawCalendar["calendarserver:delegatedsource"]; delegated = true; } + if (!source) { + throw new Error("No source for calendar"); + } const id = source.replace("/calendars/", "").replace(".json", ""); const ownerId = id.split("/")[0]; - const visibility = getCalendarVisibility(rawCalendar["acl"]); + const visibility = getCalendarVisibility(rawCalendar["acl"] ?? []); return { cal: rawCalendar, description, diff --git a/src/features/Calendars/utils/processSyncTokenUpdates.ts b/src/features/Calendars/utils/processSyncTokenUpdates.ts index eb21373..c249cf3 100644 --- a/src/features/Calendars/utils/processSyncTokenUpdates.ts +++ b/src/features/Calendars/utils/processSyncTokenUpdates.ts @@ -30,6 +30,6 @@ export function processSyncUpdates( } function extractFileNameFromHref(href: string): string { - const fileNameMatch = href.match(/\/([^\/]+)\.ics$/); // CalDAV href are like /calendars/userID/CalendarID/EventId.ics + const fileNameMatch = href.match(/\/([^/]+)\.ics$/); // CalDAV href are like /calendars/userID/CalendarID/EventId.ics return fileNameMatch ? fileNameMatch[1] : href; } diff --git a/src/features/Events/AttendanceValidation/AttendanceValidation.tsx b/src/features/Events/AttendanceValidation/AttendanceValidation.tsx index 3f0d7b0..37691f3 100644 --- a/src/features/Events/AttendanceValidation/AttendanceValidation.tsx +++ b/src/features/Events/AttendanceValidation/AttendanceValidation.tsx @@ -9,7 +9,9 @@ import { PartStat } from "@/features/User/models/attendee"; interface AttendanceValidationProps { contextualizedEvent: ContextualizedEvent; user: userData | undefined; - setAfterChoiceFunc: Dispatch>; + setAfterChoiceFunc: ( + func: ((type: "solo" | "all" | undefined) => void) | undefined + ) => void; setOpenEditModePopup: Dispatch>; } diff --git a/src/features/Events/AttendanceValidation/RSVPButton.tsx b/src/features/Events/AttendanceValidation/RSVPButton.tsx index ef21582..10a5b31 100644 --- a/src/features/Events/AttendanceValidation/RSVPButton.tsx +++ b/src/features/Events/AttendanceValidation/RSVPButton.tsx @@ -1,8 +1,8 @@ import { useAppDispatch } from "@/app/hooks"; import { PartStat } from "@/features/User/models/attendee"; import { userData } from "@/features/User/userDataTypes"; -import { Button, CircularProgress, Box, Theme } from "@linagora/twake-mui"; -import { Dispatch, SetStateAction, useEffect, useRef, useState } from "react"; +import { Box, Button, CircularProgress, Theme } from "@linagora/twake-mui"; +import { Dispatch, SetStateAction, useEffect, useRef } from "react"; import { useI18n } from "twake-i18n"; import { ContextualizedEvent } from "../EventsTypes"; import { handleRSVPClick } from "./handleRSVPClick"; @@ -19,7 +19,9 @@ interface RSVPButtonProps { rsvpValue: PartStat; contextualizedEvent: ContextualizedEvent; user: userData | undefined; - setAfterChoiceFunc: Dispatch>; + setAfterChoiceFunc: ( + func: ((type: "solo" | "all" | undefined) => void) | undefined + ) => void; setOpenEditModePopup: Dispatch>; isLoading: boolean; onLoadingChange: (loading: boolean, value?: PartStat) => void; diff --git a/src/features/Events/AttendanceValidation/handleRSVPClick.tsx b/src/features/Events/AttendanceValidation/handleRSVPClick.tsx index 2a9a9c9..0709280 100644 --- a/src/features/Events/AttendanceValidation/handleRSVPClick.tsx +++ b/src/features/Events/AttendanceValidation/handleRSVPClick.tsx @@ -9,7 +9,9 @@ export async function handleRSVPClick( rsvp: PartStat, contextualizedEvent: ContextualizedEvent, user: userData | undefined, - setAfterChoiceFunc: Dispatch>, + setAfterChoiceFunc: ( + func: ((type: "solo" | "all" | undefined) => void) | undefined + ) => void, setOpenEditModePopup: Dispatch>, dispatch: AppDispatch, onLoadingChange?: (loading: boolean, value?: PartStat) => void diff --git a/src/features/Events/EventApi.ts b/src/features/Events/EventApi.ts index c74eb09..238f399 100644 --- a/src/features/Events/EventApi.ts +++ b/src/features/Events/EventApi.ts @@ -3,6 +3,7 @@ import { resolveTimezoneId, convertEventDateTimeToISO } from "@/utils/timezone"; import { TIMEZONES } from "@/utils/timezone-data"; import ICAL from "ical.js"; import { CalDavItem } from "../Calendars/api/types"; +import { SearchEventsResponse } from "../Search/types/SearchEventsResponse"; import { CalendarEvent } from "./EventsTypes"; import { calendarEventToJCal, @@ -11,6 +12,23 @@ import { parseCalendarEvent, } from "./eventUtils"; +type JCalValue = string | number | boolean | null; + +type JCalParams = Record; + +type JCalProperty = [ + name: string, + params: JCalParams, + type: string, + value: JCalValue, +]; + +type JCalComponent = [ + name: string, + properties: JCalProperty[], + components?: JCalComponent[], +]; + export async function reportEvent( event: CalendarEvent, match: { start: string; end: string } @@ -43,8 +61,8 @@ export async function getEvent(event: CalendarEvent, isMaster?: boolean) { let targetVevent; if (isMaster) { targetVevent = vevents.find( - ([, props]: [string, any[]]) => - !props.find(([k]: string[]) => k.toLowerCase() === "recurrence-id") + ([, props]: JCalComponent) => + !props.find(([k]) => k.toLowerCase() === "recurrence-id") ); if (!targetVevent) { targetVevent = vevents[0]; @@ -152,7 +170,7 @@ export async function putEvent(event: CalendarEvent, calOwnerEmail?: string) { }); if (response.status === 201) { - console.log("Event created successfully:", response.url || event.URL); + console.info("Event created successfully:", response.url || event.URL); } return response; @@ -198,17 +216,14 @@ export async function putEventWithOverrides( }); } -export const deleteEventInstance = async ( - event: CalendarEvent, - calOwnerEmail?: string -) => { +export const deleteEventInstance = async (event: CalendarEvent) => { // Get all VEVENTs (master + overrides) from the series const vevents = await getAllRecurrentEvent(event); // Find the master VEVENT const masterIndex = vevents.findIndex( - ([, props]: [string, any[]]) => - !props.find(([k]: string[]) => k.toLowerCase() === "recurrence-id") + ([, props]: JCalComponent) => + !props.find(([k]) => k.toLowerCase() === "recurrence-id") ); if (masterIndex === -1) { @@ -220,8 +235,9 @@ export const deleteEventInstance = async ( const masterProps = vevents[masterIndex][1]; // Check if this date is already in EXDATE (avoid duplicates) - const normalizeRecurrenceId = (id: string) => (id ?? "").replace(/Z$/, ""); - const isDuplicate = masterProps.some((prop: any[]) => { + const normalizeRecurrenceId = (id: JCalValue) => + String(id ?? "").replace(/Z$/, ""); + const isDuplicate = masterProps.some((prop: JCalProperty) => { if (prop[0].toLowerCase() === "exdate" && prop[3]) { return ( normalizeRecurrenceId(prop[3]) === normalizeRecurrenceId(exdateValue) @@ -240,9 +256,9 @@ export const deleteEventInstance = async ( vevents[masterIndex][1] = masterProps; // Remove the override instance if it exists (in case it was an override being deleted) - const filteredVevents = vevents.filter(([, props]: [string, any[]]) => { + const filteredVevents = vevents.filter(([, props]: JCalComponent) => { const recurrenceIdProp = props.find( - ([k]: string[]) => k.toLowerCase() === "recurrence-id" + ([k]) => k.toLowerCase() === "recurrence-id" ); if (!recurrenceIdProp) return true; // Keep master return ( @@ -278,9 +294,9 @@ export const updateSeriesPartstat = async ( const vevents = await getAllRecurrentEvent(event); // Update PARTSTAT in ALL VEVENTs (master + exceptions) - const updatedVevents = vevents.map((vevent: any[]) => { + const updatedVevents = vevents.map((vevent: JCalComponent) => { const properties = vevent[1]; - const updatedProperties = properties.map((prop: any[]) => { + const updatedProperties = properties.map((prop: JCalProperty) => { // Find ATTENDEE properties if (prop[0] === "attendee") { const calAddress = prop[3]; @@ -321,7 +337,7 @@ export const updateSeries = async ( ) => { const vevents = await getAllRecurrentEvent(event); const masterIndex = vevents.findIndex( - ([, props]: [string, string[]]) => + ([, props]: JCalComponent) => !props.find(([k]) => k.toLowerCase() === "recurrence-id") ); if (masterIndex === -1) { @@ -404,7 +420,7 @@ export async function searchEvent( organizers: string[]; attendees: string[]; } -) { +): Promise { const { keywords, searchIn, organizers, attendees } = filters; const reqParam: { @@ -413,7 +429,7 @@ export async function searchEvent( organizers?: string[]; attendees?: string[]; } = { - query: !!keywords ? keywords : query, + query: keywords ? keywords : query, calendars: searchIn.map((calId) => { const [userId, calendarId] = calId.split("/"); return { calendarId, userId }; @@ -431,5 +447,5 @@ export async function searchEvent( }) .json(); - return response; + return response as SearchEventsResponse; } diff --git a/src/features/Events/EventDisplayPreview.tsx b/src/features/Events/EventDisplayPreview.tsx index 70f058a..9b7d3ae 100644 --- a/src/features/Events/EventDisplayPreview.tsx +++ b/src/features/Events/EventDisplayPreview.tsx @@ -1,3 +1,4 @@ +/* eslint-disable react-hooks/rules-of-hooks */ import { useAppDispatch, useAppSelector } from "@/app/hooks"; import { CalendarName } from "@/components/Calendar/CalendarName"; import { getTimezoneOffset } from "@/utils/timezone"; @@ -59,7 +60,7 @@ export default function EventPreviewModal({ calId: string; tempEvent?: boolean; open: boolean; - onClose: (event: {}, reason: "backdropClick" | "escapeKeyDown") => void; + onClose: (event: unknown, reason: "backdropClick" | "escapeKeyDown") => void; }) { const { t } = useI18n(); const dispatch = useAppDispatch(); @@ -67,9 +68,7 @@ export default function EventPreviewModal({ const timezone = useAppSelector((state) => state.settings.timeZone) ?? browserDefaultTimeZone; - const calendarList = Object.values( - useAppSelector((state) => state.calendars.list) - ); + const calendar = tempEvent ? calendars.templist[calId] : calendars.list[calId]; @@ -95,14 +94,16 @@ export default function EventPreviewModal({ const [typeOfAction, setTypeOfAction] = useState<"solo" | "all" | undefined>( undefined ); - const [afterChoiceFunc, setAfterChoiceFunc] = useState(); + const [afterChoiceFunc, setAfterChoiceFunc] = useState< + ((type: "solo" | "all" | undefined) => void) | undefined + >(); const attendeePreview = makeAttendeePreview(event.attendee, t); const hasCheckedSessionStorageRef = useRef(false); const [toggleActionMenu, setToggleActionMenu] = useState( null ); - const mailSpaUrl = (window as any).MAIL_SPA_URL ?? null; + const mailSpaUrl = window.MAIL_SPA_URL ?? null; useEffect(() => { if (!event || !calendar) { @@ -160,7 +161,7 @@ export default function EventPreviewModal({ } } } - } catch (err) { + } catch { // Ignore sessionStorage errors } }; @@ -201,7 +202,7 @@ export default function EventPreviewModal({ // Clear sessionStorage after reopening try { sessionStorage.removeItem("eventUpdateModalReopen"); - } catch (err) { + } catch { // Ignore sessionStorage errors } } @@ -241,7 +242,7 @@ export default function EventPreviewModal({ } } } - } catch (err) { + } catch { // Ignore sessionStorage errors } }; @@ -296,7 +297,7 @@ export default function EventPreviewModal({ gap={0.5} width="100%" > - {(window as any).DEBUG && ( + {window.DEBUG && ( { @@ -373,8 +374,6 @@ export default function EventPreviewModal({ )} setToggleActionMenu(null)} onOpenDuplicate={() => { setToggleActionMenu(null); setHidePreview(true); @@ -723,10 +722,11 @@ export default function EventPreviewModal({ { setTypeOfAction(type); - afterChoiceFunc && afterChoiceFunc(type); + if (afterChoiceFunc) { + afterChoiceFunc(type); + } }} /> string ): string | undefined { if (!event.repetition) return; @@ -851,7 +851,7 @@ function makeRecurrenceString( function formatDate( date: Date | string, - t: Function, + t: (k: string, p?: string | object) => string, timeZone: string, allday?: boolean ) { @@ -880,7 +880,7 @@ function formatDate( function formatEnd( start: Date | string, end: Date | string, - t: Function, + t: (k: string, p?: string | object) => string, timeZone: string, allday?: boolean ) { @@ -924,7 +924,10 @@ function formatEnd( } } -export function makeAttendeePreview(attendees: userAttendee[], t: Function) { +export function makeAttendeePreview( + attendees: userAttendee[], + t: (k: string, p?: string | object) => string +) { const attendeePreview = []; const yesCount = attendees?.filter((a) => a.partstat === "ACCEPTED").length; const noCount = attendees?.filter((a) => a.partstat === "DECLINED").length; diff --git a/src/features/Events/EventModal.tsx b/src/features/Events/EventModal.tsx index 6098874..48def8a 100644 --- a/src/features/Events/EventModal.tsx +++ b/src/features/Events/EventModal.tsx @@ -1,6 +1,5 @@ import { useAppDispatch, useAppSelector } from "@/app/hooks"; -import { getTimezoneOffset, resolveTimezone } from "@/utils/timezone"; -import { updateTempCalendar } from "@/components/Calendar/utils/calendarUtils"; +import { RootState } from "@/app/store"; import { ResponsiveDialog } from "@/components/Dialog"; import EventFormFields from "@/components/Event/EventFormFields"; import { addDays } from "@/components/Event/utils/dateRules"; @@ -9,7 +8,6 @@ import { formatLocalDateTime, } from "@/components/Event/utils/dateTimeFormatters"; import { convertFormDateTimeToISO } from "@/components/Event/utils/dateTimeHelpers"; -import { getCalendarRange } from "@/utils/dateUtils"; import { buildEventFormTempData, clearEventFormTempData, @@ -19,7 +17,11 @@ import { saveEventFormDataToTemp, showErrorNotification, } from "@/utils/eventFormTempStorage"; -import { browserDefaultTimeZone } from "@/utils/timezone"; +import { + browserDefaultTimeZone, + getTimezoneOffset, + resolveTimezone, +} from "@/utils/timezone"; import { TIMEZONES } from "@/utils/timezone-data"; import { addVideoConferenceToDescription } from "@/utils/videoConferenceUtils"; import { CalendarApi, DateSelectArg } from "@fullcalendar/core"; @@ -37,6 +39,7 @@ import React, { import { useI18n } from "twake-i18n"; import { Calendar } from "../Calendars/CalendarTypes"; import { putEventAsync } from "../Calendars/services"; +import { AsyncThunkResult } from "../Calendars/types/AsyncThunkResult"; import { userAttendee } from "../User/models/attendee"; import { CalendarEvent, RepetitionObject } from "./EventsTypes"; @@ -52,7 +55,7 @@ function EventPopover({ open: boolean; onClose: (refresh?: boolean) => void; selectedRange: DateSelectArg | null; - setSelectedRange: Function; + setSelectedRange: React.Dispatch>; calendarRef: React.RefObject; event?: CalendarEvent; }) { @@ -64,8 +67,8 @@ function EventPopover({ useAppSelector((state) => state.user.userData?.openpaasId) ?? ""; const calList = useAppSelector((state) => state.calendars.list); const selectPersonalCalendars = createSelector( - (state: any) => state.calendars, - (calendars: any) => + (state: RootState) => state.calendars, + (calendars: RootState["calendars"]) => Object.keys(calendars.list || {}) .map((id) => { if (id.split("/")[0] === userId) { @@ -704,7 +707,6 @@ function EventPopover({ }, 0); } } - // eslint-disable-next-line react-hooks/exhaustive-deps }, [open, event?.uid]); const handleSave = async () => { @@ -818,20 +820,20 @@ function EventPopover({ ); // Handle result of putEventAsync - check if rejected first + const typedResult = result as AsyncThunkResult; + // Check if result is a rejected action - if (result.type && result.type.endsWith("/rejected")) { + if (typedResult.type && typedResult.type.endsWith("/rejected")) { throw new Error( - result.error?.message || result.payload?.message || "API call failed" + typedResult.error?.message || + typedResult.payload?.message || + "API call failed" ); } // If result has unwrap, call it (it will throw if rejected) - if (result && typeof result.unwrap === "function") { - try { - await result.unwrap(); - } catch (unwrapError: any) { - throw unwrapError; - } + if (typedResult && typeof typedResult.unwrap === "function") { + await typedResult.unwrap(); } // Clear temp data on successful save @@ -839,7 +841,9 @@ function EventPopover({ // Reset all state to default values only on successful save resetAllStateToDefault(); - } catch (error: any) { + } catch (error) { + const errorObj = error as { message?: string }; + // API failed - restore form data and mark as error const errorFormData = { ...formDataToSave, @@ -849,7 +853,7 @@ function EventPopover({ // Show error notification showErrorNotification( - error?.message || "Failed to create event. Please try again." + errorObj.message || "Failed to create event. Please try again." ); // Try to reopen modal by dispatching custom event diff --git a/src/features/Events/EventUpdateModal.tsx b/src/features/Events/EventUpdateModal.tsx index c89344c..60ed972 100644 --- a/src/features/Events/EventUpdateModal.tsx +++ b/src/features/Events/EventUpdateModal.tsx @@ -11,6 +11,7 @@ import { updateEventInstanceAsync, updateSeriesAsync, } from "@/features/Calendars/services"; +import { assertThunkSuccess } from "@/utils/assertThunkSuccess"; import { buildEventFormTempData, clearEventFormTempData, @@ -24,8 +25,8 @@ import { import { extractEventBaseUuid } from "@/utils/extractEventBaseUuid"; import { browserDefaultTimeZone, - resolveTimezone, getTimezoneOffset, + resolveTimezone, } from "@/utils/timezone"; import { TIMEZONES } from "@/utils/timezone-data"; import { addVideoConferenceToDescription } from "@/utils/videoConferenceUtils"; @@ -39,6 +40,7 @@ import { updateEventLocal, } from "../Calendars/CalendarSlice"; import { Calendar } from "../Calendars/CalendarTypes"; +import { AsyncThunkResult } from "../Calendars/types/AsyncThunkResult"; import { userAttendee } from "../User/models/attendee"; import { deleteEvent, getEvent, putEvent } from "./EventApi"; import { CalendarEvent, RepetitionObject } from "./EventsTypes"; @@ -56,7 +58,7 @@ function EventUpdateModal({ eventId: string; calId: string; open: boolean; - onClose: (event: {}, reason: "backdropClick" | "escapeKeyDown") => void; + onClose: (event: unknown, reason: "backdropClick" | "escapeKeyDown") => void; onCloseAll?: () => void; eventData?: CalendarEvent | null; typeOfAction?: "solo" | "all"; @@ -192,7 +194,7 @@ function EventUpdateModal({ }; const fetchedMasterEvent = await getEvent(masterEventToFetch, true); setMasterEvent(fetchedMasterEvent); - } catch (err: any) { + } catch (err) { console.error("Failed to fetch master event:", err); // Fallback to using the clicked instance setMasterEvent(event); @@ -335,6 +337,7 @@ function EventUpdateModal({ } } } + // eslint-disable-next-line react-hooks/exhaustive-deps }, [ open, event, @@ -675,16 +678,22 @@ function EventUpdateModal({ const deletePromises = Array.from(uniqueURLs).map(async (url) => { try { await deleteEvent(url); - } catch (deleteError: any) { + } catch (deleteError) { + // Check if error is an object with response or message properties + const errorObj = deleteError as { + response?: { status?: number }; + message?: string; + }; + // Silently ignore 404 - file might already be deleted const is404 = - deleteError.response?.status === 404 || - deleteError.message?.includes("404") || - deleteError.message?.includes("Not Found"); + errorObj.response?.status === 404 || + errorObj.message?.includes("404") || + errorObj.message?.includes("Not Found"); if (!is404) { console.error( - `Failed to delete event file: ${deleteError.message}` + `Failed to delete event file: ${errorObj.message || "Unknown error"}` ); } } @@ -724,7 +733,10 @@ function EventUpdateModal({ // Reset all state to default values only on successful save resetAllStateToDefault(); initializedKeyRef.current = null; - } catch (err: any) { + } catch (err) { + // Check if error is an object with a message property + const errorObj = err as { message?: string }; + // API failed - restore form data and mark as error const errorFormData = { ...formDataToSave, @@ -734,7 +746,8 @@ function EventUpdateModal({ // Show error notification showErrorNotification( - err?.message || "Failed to convert recurring event. Please try again." + errorObj.message || + "Failed to convert recurring event. Please try again." ); if (createdSingleEventUid) { @@ -769,7 +782,6 @@ function EventUpdateModal({ if (recurrenceId) { if (typeOfAction === "solo") { // Update single instance with optimistic update + rollback - const oldEvent = { ...event }; dispatch( updateEventLocal({ @@ -778,43 +790,30 @@ function EventUpdateModal({ }) ); - try { - const result = await dispatch( - updateEventInstanceAsync({ - cal: targetCalendar, - event: { ...newEvent, recurrenceId }, - }) - ); + const result = await dispatch( + updateEventInstanceAsync({ + cal: targetCalendar, + event: { ...newEvent, recurrenceId }, + }) + ); - // Handle result of updateEventInstanceAsync - if (result && typeof (result as any).unwrap === "function") { - try { - await (result as any).unwrap(); - } catch (unwrapError: any) { - throw unwrapError; - } - } else { - // Check if result is rejected - if ( - result.type && - (result.type as string).endsWith("/rejected") - ) { - const rejectedResult = result as any; - throw new Error( - rejectedResult.error?.message || - rejectedResult.payload?.message || - "API call failed" - ); - } + // Handle result of updateEventInstanceAsync + const typedResult = result as AsyncThunkResult; + if (typedResult && typeof typedResult.unwrap === "function") { + await typedResult.unwrap(); + } else { + // Check if result is rejected + if (typedResult.type && typedResult.type.endsWith("/rejected")) { + throw new Error( + typedResult.error?.message || + typedResult.payload?.message || + "API call failed" + ); } - - // Clear temp data on successful save - clearEventFormTempData("update"); - } catch (error: any) { - // Rollback optimistic update - dispatch(updateEventLocal({ calId, event: oldEvent })); - throw error; // Re-throw to be caught by outer catch } + + // Clear temp data on successful save + clearEventFormTempData("update"); } else if (typeOfAction === "all") { // Update all instances - check if repetition rules changed const baseUID = extractEventBaseUuid(event.uid); @@ -872,22 +871,18 @@ function EventUpdateModal({ ); // Handle result of updateSeriesAsync - if (result && typeof (result as any).unwrap === "function") { - try { - await (result as any).unwrap(); - } catch (unwrapError: any) { - throw unwrapError; - } + const typedResult = result as AsyncThunkResult; + if (typedResult && typeof typedResult.unwrap === "function") { + await typedResult.unwrap(); } else { // Check if result is rejected if ( - result.type && - (result.type as string).endsWith("/rejected") + typedResult.type && + typedResult.type.endsWith("/rejected") ) { - const rejectedResult = result as any; throw new Error( - rejectedResult.error?.message || - rejectedResult.payload?.message || + typedResult.error?.message || + typedResult.payload?.message || "API call failed" ); } @@ -949,26 +944,7 @@ function EventUpdateModal({ ); // Handle result of updateSeriesAsync - if (result && typeof (result as any).unwrap === "function") { - try { - await (result as any).unwrap(); - } catch (unwrapError: any) { - throw unwrapError; - } - } else { - // Check if result is rejected - if ( - result.type && - (result.type as string).endsWith("/rejected") - ) { - const rejectedResult = result as any; - throw new Error( - rejectedResult.error?.message || - rejectedResult.payload?.message || - "API call failed" - ); - } - } + assertThunkSuccess(result); // Clear cache to ensure navigation shows updated data dispatch(clearFetchCache(calId)); @@ -990,20 +966,7 @@ function EventUpdateModal({ ); // Handle result of putEventAsync - check if rejected first - if (result.type && result.type.endsWith("/rejected")) { - throw new Error( - result.error?.message || - result.payload?.message || - "API call failed" - ); - } - if (result && typeof result.unwrap === "function") { - try { - await result.unwrap(); - } catch (unwrapError: any) { - throw unwrapError; - } - } + assertThunkSuccess(result); // Remove old single event AFTER new recurring instances are added to store // This prevents empty grid during the transition @@ -1024,19 +987,16 @@ function EventUpdateModal({ ); // Handle result of putEventAsync - check if rejected first - if (result.type && result.type.endsWith("/rejected")) { + const typedResult = result as AsyncThunkResult; + if (typedResult.type && typedResult.type.endsWith("/rejected")) { throw new Error( - result.error?.message || - result.payload?.message || + typedResult.error?.message || + typedResult.payload?.message || "API call failed" ); } - if (result && typeof result.unwrap === "function") { - try { - await result.unwrap(); - } catch (unwrapError: any) { - throw unwrapError; - } + if (typedResult && typeof typedResult.unwrap === "function") { + await typedResult.unwrap(); } // Clear temp data on successful save @@ -1060,8 +1020,9 @@ function EventUpdateModal({ ); // Handle result of putEventAsync - if (putResult && typeof putResult.unwrap === "function") { - await putResult.unwrap(); + const typedPutResult = putResult as AsyncThunkResult; + if (typedPutResult && typeof typedPutResult.unwrap === "function") { + await typedPutResult.unwrap(); } // Then move it to the new calendar @@ -1074,8 +1035,9 @@ function EventUpdateModal({ ); // Handle result of moveEventAsync - if (moveResult && typeof moveResult.unwrap === "function") { - await moveResult.unwrap(); + const typedMoveResult = moveResult as AsyncThunkResult; + if (typedMoveResult && typeof typedMoveResult.unwrap === "function") { + await typedMoveResult.unwrap(); } // Clear temp data on successful move @@ -1086,7 +1048,10 @@ function EventUpdateModal({ clearEventFormTempData("update"); resetAllStateToDefault(); initializedKeyRef.current = null; - } catch (error: any) { + } catch (error) { + // Check if error is an object with a message property + const errorObj = error as { message?: string }; + // Handle errors for all branches // Rollback optimistic updates if any // API failed - restore form data and mark as error @@ -1098,7 +1063,7 @@ function EventUpdateModal({ // Show error notification showErrorNotification( - error?.message || "Failed to update event. Please try again." + errorObj.message || "Failed to update event. Please try again." ); // Try to reopen modal diff --git a/src/features/Events/EventsTypes.ts b/src/features/Events/EventsTypes.ts index de0e449..3e1ad60 100644 --- a/src/features/Events/EventsTypes.ts +++ b/src/features/Events/EventsTypes.ts @@ -31,10 +31,10 @@ export interface CalendarEvent { export interface RepetitionObject { freq: string; - interval?: number; + interval?: number | null; byday?: string[] | null; - occurrences?: number; - endDate?: string; + occurrences?: number | null; + endDate?: string | null; } export interface AlarmObject { diff --git a/src/features/Events/eventUtils.ts b/src/features/Events/eventUtils.ts index f4ed7c8..6dfecf8 100644 --- a/src/features/Events/eventUtils.ts +++ b/src/features/Events/eventUtils.ts @@ -1,17 +1,19 @@ import { convertFormDateTimeToISO } from "@/components/Event/utils/dateTimeHelpers"; import { extractEventBaseUuid } from "@/utils/extractEventBaseUuid"; -import { resolveTimezoneId, convertEventDateTimeToISO } from "@/utils/timezone"; +import { convertEventDateTimeToISO, resolveTimezoneId } from "@/utils/timezone"; import { TIMEZONES } from "@/utils/timezone-data"; import ICAL from "ical.js"; import moment from "moment-timezone"; +import { + RepetitionRule, + VObjectProperty, +} from "../Calendars/types/CalendarData"; import { userAttendee } from "../User/models/attendee"; import { createAttendee } from "../User/models/attendee.mapper"; import { AlarmObject, CalendarEvent, RepetitionObject } from "./EventsTypes"; -type RawEntry = [string, Record, string, any]; function inferTimezoneFromValue( - params: Record | undefined, - value: string + params: Record | undefined ): string | undefined { if (!params) { return undefined; @@ -30,11 +32,11 @@ function inferTimezoneFromValue( } export function parseCalendarEvent( - data: RawEntry[], + data: VObjectProperty[], color: Record, calendarid: string, eventURL: string, - valarm?: RawEntry[] + valarm?: VObjectProperty[] ): CalendarEvent { const event: Partial = { color, attendee: [] }; let recurrenceId; @@ -44,18 +46,20 @@ export function parseCalendarEvent( for (const [key, params, , value] of data) { switch (key.toLowerCase()) { case "uid": - event.uid = value; + event.uid = String(value); break; case "transp": - event.transp = value; + event.transp = String(value); break; case "dtstart": { - event.start = value; - const detectedTz = inferTimezoneFromValue(params, value); + event.start = String(value); + const detectedTz = inferTimezoneFromValue( + params as Record + ); if (detectedTz) { event.timezone = detectedTz; } - if (dateRegex.test(value)) { + if (dateRegex.test(String(value))) { event.allday = true; } else { event.allday = false; @@ -63,14 +67,16 @@ export function parseCalendarEvent( break; } case "dtend": { - event.end = value; + event.end = String(value); if (!event.timezone) { - const detectedTz = inferTimezoneFromValue(params, value); + const detectedTz = inferTimezoneFromValue( + params as Record + ); if (detectedTz) { event.timezone = detectedTz; } } - if (dateRegex.test(value)) { + if (dateRegex.test(String(value))) { event.allday = true; } else { event.allday = false; @@ -78,50 +84,54 @@ export function parseCalendarEvent( break; } case "class": - event.class = value; + event.class = String(value); break; case "x-openpaas-videoconference": - event.x_openpass_videoconference = value; + event.x_openpass_videoconference = String(value); break; case "summary": - event.title = value; + event.title = String(value); break; case "description": - event.description = value; + event.description = String(value); break; case "location": - event.location = value; + event.location = String(value); break; - case "organizer": + case "organizer": { + const paramsObj = params as Record; event.organizer = { - cn: params?.cn ?? "", - cal_address: value?.replace(/^mailto:/i, ""), + cn: paramsObj?.cn ?? "", + cal_address: String(value).replace(/^mailto:/i, ""), }; break; - case "attendee": + } + case "attendee": { + const paramsObj = params as Record; (event.attendee as userAttendee[]).push( createAttendee({ - cn: params?.cn, - cal_address: value.replace(/^mailto:/i, ""), - partstat: params?.partstat as userAttendee["partstat"], - rsvp: params?.rsvp as userAttendee["rsvp"], - role: params?.role as userAttendee["role"], - cutype: params?.cutype as userAttendee["cutype"], + cn: paramsObj?.cn, + cal_address: String(value).replace(/^mailto:/i, ""), + partstat: paramsObj?.partstat as userAttendee["partstat"], + rsvp: paramsObj?.rsvp as userAttendee["rsvp"], + role: paramsObj?.role as userAttendee["role"], + cutype: paramsObj?.cutype as userAttendee["cutype"], }) ); break; + } case "dtstamp": - event.stamp = value; + event.stamp = String(value); break; case "sequence": event.sequence = Number(value); break; case "recurrence-id": - recurrenceId = value; + recurrenceId = String(value); break; case "exdate": if (!event.exdates) event.exdates = []; - event.exdates.push(value); + event.exdates.push(String(value)); break; case "status": event.status = String(value); @@ -129,25 +139,27 @@ export function parseCalendarEvent( case "duration": duration = String(value); break; - case "rrule": - event.repetition = { freq: value.freq.toLowerCase() }; - if (value.byday) { - if (typeof value.byday === "string") { - event.repetition.byday = [value.byday]; + case "rrule": { + const ruleValue = value as RepetitionRule; + event.repetition = { freq: ruleValue.freq.toLowerCase() }; + if (ruleValue.byday) { + if (typeof ruleValue.byday === "string") { + event.repetition.byday = [ruleValue.byday]; } else { - event.repetition.byday = value.byday; + event.repetition.byday = ruleValue.byday; } } - if (value.until) { - event.repetition.endDate = value.until; + if (ruleValue.until) { + event.repetition.endDate = ruleValue.until; } - if (value.count) { - event.repetition.occurrences = value.count; + if (ruleValue.count) { + event.repetition.occurrences = ruleValue.count; } - if (value.interval) { - event.repetition.interval = value.interval; + if (ruleValue.interval) { + event.repetition.interval = ruleValue.interval; } break; + } } } if (recurrenceId && event.uid) { @@ -160,10 +172,10 @@ export function parseCalendarEvent( for (const [key, , , value] of valarm[1]) { switch (key.toLowerCase()) { case "action": - event.alarm.action = value; + event.alarm.action = String(value); break; case "trigger": - event.alarm.trigger = value; + event.alarm.trigger = String(value); break; } } @@ -209,10 +221,10 @@ export function parseCalendarEvent( export function calendarEventToJCal( event: CalendarEvent, calOwnerEmail?: string -): any[] { +) { const tzid = event.timezone; // Fallback to UTC if no timezone provided - const vevent: any[] = makeVevent(event, tzid, calOwnerEmail); + const vevent = makeVevent(event, tzid, calOwnerEmail); const timezoneData = TIMEZONES.zones[event.timezone]; const vtimezone = makeTimezone(timezoneData, event); @@ -242,7 +254,7 @@ export function makeVevent( calOwnerEmail: string | undefined, isMasterEvent?: boolean ) { - const vevent: any[] = [ + const vevent: [string, unknown[]] = [ "vevent", [ ["uid", {}, "text", extractEventBaseUuid(event.uid)], @@ -279,18 +291,13 @@ export function makeVevent( ]; vevent.push([["valarm", valarm]]); } - vevent.push([]); if (event.end) { - const startDate = new Date(event.start); - const endDate = new Date(event.end); - let finalEndDate = endDate; - vevent[1].push([ "dtend", { tzid }, event.allday ? "date" : "date-time", - formatDateToICal(finalEndDate, event.allday ?? false, tzid), + formatDateToICal(new Date(event.end), event.allday ?? false, tzid), ]); } if (event.organizer) { @@ -311,21 +318,21 @@ export function makeVevent( vevent[1].push(["description", {}, "text", event.description]); } if (event.repetition?.freq) { - const repetitionRule: Record = { freq: event.repetition.freq }; + const repetitionRule: RepetitionRule = { freq: event.repetition.freq }; if (event.repetition.interval) { - repetitionRule["interval"] = event.repetition.interval; + repetitionRule.interval = event.repetition.interval; } if (event.repetition.occurrences) { - repetitionRule["count"] = event.repetition.occurrences; + repetitionRule.count = event.repetition.occurrences; } if (event.repetition.endDate) { - repetitionRule["until"] = event.repetition.endDate; + repetitionRule.until = event.repetition.endDate; } if ( event.repetition.byday !== null && event.repetition.byday !== undefined ) { - repetitionRule["byday"] = event.repetition.byday; + repetitionRule.byday = event.repetition.byday; } vevent[1].push(["rrule", {}, "recur", repetitionRule]); } @@ -362,7 +369,7 @@ export function makeVevent( return vevent; } -function formatDateToICal(date: Date, allday: Boolean, timezone?: string) { +function formatDateToICal(date: Date, allday: boolean, timezone?: string) { const pad = (n: number) => n.toString().padStart(2, "0"); if (allday) { diff --git a/src/features/Events/formHelpers.ts b/src/features/Events/formHelpers.ts index a9a640e..cf3634f 100644 --- a/src/features/Events/formHelpers.ts +++ b/src/features/Events/formHelpers.ts @@ -6,6 +6,7 @@ import { TIMEZONES } from "@/utils/timezone-data"; import { addVideoConferenceToDescription } from "@/utils/videoConferenceUtils"; import { userAttendee } from "../User/models/attendee"; import { CalendarEvent, RepetitionObject } from "./EventsTypes"; +import { Calendar } from "../Calendars/CalendarTypes"; export interface TimezoneListResult { zones: string[]; @@ -58,7 +59,7 @@ export interface PopulateFormFromEventParams { setHasVideoConference: (value: boolean) => void; setMeetingLink: (value: string | null) => void; setCalendarid?: (value: string) => void; - calendarsList?: Record; + calendarsList?: Record; calId?: string; } diff --git a/src/features/Search/SearchResultsPage.tsx b/src/features/Search/SearchResultsPage.tsx index 634ca8d..318911a 100644 --- a/src/features/Search/SearchResultsPage.tsx +++ b/src/features/Search/SearchResultsPage.tsx @@ -21,6 +21,7 @@ import EventPreviewModal from "../Events/EventDisplayPreview"; import { CalendarEvent } from "../Events/EventsTypes"; import { setView } from "../Settings/SettingsSlice"; import "./searchResult.styl"; +import { SearchEventResult } from "./types/SearchEventResult"; const styles = { M3BodyLarge: { @@ -110,7 +111,7 @@ export default function SearchResultsPage() { layout = ( - {results?.map((r: any, idx: number) => ( + {results.map((r: SearchEventResult, idx: number) => ( ; + eventData: SearchEventResult; dispatch: AppDispatch; }) { const { t } = useI18n(); @@ -163,7 +164,7 @@ function ResultItem({ const [openPreview, setOpenPreview] = useState(false); - const handleOpenResult = async (eventData: Record) => { + const handleOpenResult = async (eventData: SearchEventResult) => { if (calendar) { const event = { URL: eventData._links.self.href, diff --git a/src/features/Search/SearchSlice.ts b/src/features/Search/SearchSlice.ts index 3bd6d8d..22e1da7 100644 --- a/src/features/Search/SearchSlice.ts +++ b/src/features/Search/SearchSlice.ts @@ -4,7 +4,7 @@ import { searchEvent } from "../Events/EventApi"; export interface SearchResultsState { hits: number; - results: Record[]; + results: Record[]; error: string | null; loading: boolean; } @@ -17,24 +17,30 @@ const initialState: SearchResultsState = { }; export const searchEventsAsync = createAsyncThunk< - { hits: number; events: Record[] }, - { search: string; filters: any }, + { hits: number; events: Record[] }, + { + search: string; + filters: { + searchIn: string[]; + keywords: string; + organizers: string[]; + attendees: string[]; + }; + }, { rejectValue: { message: string; status?: number } } >("events/searchEvents", async ({ search, filters }, { rejectWithValue }) => { try { - const response = (await searchEvent(search, filters)) as Record< - string, - any - >; + const response = await searchEvent(search, filters); return { hits: Number(response._total_hits), events: response._embedded?.events ?? [], }; - } catch (err: any) { + } catch (err) { + const error = err as { response?: { status?: number } }; return rejectWithValue({ message: formatReduxError(err), - status: err.response?.status, + status: error.response?.status, }); } }); diff --git a/src/features/Search/types/SearchEventData.ts b/src/features/Search/types/SearchEventData.ts new file mode 100644 index 0000000..36f4c11 --- /dev/null +++ b/src/features/Search/types/SearchEventData.ts @@ -0,0 +1,20 @@ +export type SearchEventData = { + uid: string; + userId: string; + calendarId: string; + start: string; + end?: string; + allDay?: boolean; + summary?: string; + description?: string; + location?: string; + class?: string; + dtstamp?: string; + isRecurrentMaster?: boolean; + attendees?: unknown[]; + organizer?: { + cn?: string; + email?: string; + }; + ["x-openpaas-videoconference"]?: string; +}; diff --git a/src/features/Search/types/SearchEventResult.ts b/src/features/Search/types/SearchEventResult.ts new file mode 100644 index 0000000..8cb32d6 --- /dev/null +++ b/src/features/Search/types/SearchEventResult.ts @@ -0,0 +1,10 @@ +import { SearchEventData } from "./SearchEventData"; + +export type SearchEventResult = { + data: SearchEventData; + _links: { + self: { + href: string; + }; + }; +}; diff --git a/src/features/Search/types/SearchEventsResponse.ts b/src/features/Search/types/SearchEventsResponse.ts new file mode 100644 index 0000000..abdef01 --- /dev/null +++ b/src/features/Search/types/SearchEventsResponse.ts @@ -0,0 +1,6 @@ +export type SearchEventsResponse = { + _total_hits?: number | string; + _embedded?: { + events?: Record[]; + }; +}; diff --git a/src/features/Settings/SettingsPage.tsx b/src/features/Settings/SettingsPage.tsx index d144452..a5dc527 100644 --- a/src/features/Settings/SettingsPage.tsx +++ b/src/features/Settings/SettingsPage.tsx @@ -1,8 +1,7 @@ import { useAppDispatch, useAppSelector } from "@/app/hooks"; import { useTimeZoneList } from "@/components/Calendar/TimezoneSelector"; -import { getTimezoneOffset } from "@/utils/timezone"; import { TimezoneAutocomplete } from "@/components/Timezone/TimezoneAutocomplete"; -import { browserDefaultTimeZone } from "@/utils/timezone"; +import { browserDefaultTimeZone, getTimezoneOffset } from "@/utils/timezone"; import { Box, FormControl, @@ -14,6 +13,7 @@ import { ListItemText, MenuItem, Select, + SelectChangeEvent, Snackbar, Switch, Tab, @@ -105,7 +105,7 @@ export default function SettingsPage() { setActiveSettingsSubTab(newValue); }; - const handleLanguageChange = (event: any) => { + const handleLanguageChange = (event: SelectChangeEvent) => { const newLanguage = event.target.value; const previousLanguage = currentLanguage; diff --git a/src/features/Settings/SettingsSlice.ts b/src/features/Settings/SettingsSlice.ts index c956c38..6341ea9 100644 --- a/src/features/Settings/SettingsSlice.ts +++ b/src/features/Settings/SettingsSlice.ts @@ -1,5 +1,6 @@ import { browserDefaultTimeZone } from "@/utils/timezone"; import { createSlice, PayloadAction } from "@reduxjs/toolkit"; +import { ConfigurationItem, ModuleConfiguration } from "../User/userDataTypes"; import { getOpenPaasUserDataAsync } from "../User/userSlice"; export interface SettingsState { @@ -12,7 +13,7 @@ export interface SettingsState { } const savedLang = localStorage.getItem("lang"); -const defaultLang = savedLang ?? (window as any).LANG ?? "en"; +const defaultLang = savedLang ?? window.LANG ?? "en"; const savedTimeZone = localStorage.getItem("timeZone"); // If savedTimeZone is the string "null" or doesn't exist, use null @@ -59,12 +60,15 @@ export const settingsSlice = createSlice({ extraReducers: (builder) => { builder.addCase(getOpenPaasUserDataAsync.fulfilled, (state, action) => { const coreModule = action.payload.configurations?.modules?.find( - (module: any) => module.name === "core" + (module: ModuleConfiguration) => module.name === "core" ); const datetimeConfig = coreModule?.configurations?.find( - (config: any) => config.name === "datetime" + (config: ConfigurationItem) => config.name === "datetime" ); - const timeZone = datetimeConfig?.value?.timeZone; + const datetimeValue = datetimeConfig?.value as + | { timeZone?: string } + | undefined; + const timeZone = datetimeValue?.timeZone; if (timeZone) { state.timeZone = timeZone; @@ -76,25 +80,25 @@ export const settingsSlice = createSlice({ localStorage.setItem("timeZone", browserDefaultTimeZone); } const esnCalendarModule = action.payload.configurations?.modules?.find( - (module: any) => module.name === "linagora.esn.calendar" + (module: ModuleConfiguration) => module.name === "linagora.esn.calendar" ); const hideDeclinedEventsConfig = esnCalendarModule?.configurations?.find( - (config: any) => config.name === "hideDeclinedEvents" + (config: ConfigurationItem) => config.name === "hideDeclinedEvents" ); state.hideDeclinedEvents = typeof hideDeclinedEventsConfig?.value === "boolean" ? hideDeclinedEventsConfig.value : null; - const calendarModule = action.payload.configurations.modules.find( - (module: any) => module.name === "calendar" + const calendarModule = action.payload.configurations?.modules?.find( + (module: ModuleConfiguration) => module.name === "calendar" ); if (calendarModule?.configurations) { - const alarmEmailsConfig = calendarModule.configurations.find( - (config: any) => config.name === "displayWeekNumbers" + const displayWeekNumbersConfig = calendarModule.configurations.find( + (config: ConfigurationItem) => config.name === "displayWeekNumbers" ); - if (alarmEmailsConfig) { - state.displayWeekNumbers = alarmEmailsConfig.value === true; + if (displayWeekNumbersConfig) { + state.displayWeekNumbers = displayWeekNumbersConfig.value === true; } } }); diff --git a/src/features/User/HandleLogin.tsx b/src/features/User/HandleLogin.tsx index 41308ec..e24160e 100644 --- a/src/features/User/HandleLogin.tsx +++ b/src/features/User/HandleLogin.tsx @@ -52,6 +52,7 @@ export function HandleLogin() { }; initiateLogin(); + // eslint-disable-next-line react-hooks/exhaustive-deps }, [userData.userData, calendars.list, dispatch]); // Navigate to /calendar only when all data is ready diff --git a/src/features/User/oidcAuth.ts b/src/features/User/oidcAuth.ts index eb204bf..3b1af84 100644 --- a/src/features/User/oidcAuth.ts +++ b/src/features/User/oidcAuth.ts @@ -1,14 +1,14 @@ -import * as client from "openid-client"; import { getLocation } from "@/utils/apiUtils"; +import * as client from "openid-client"; export const clientConfig = { - url: (window as any).SSO_BASE_URL ?? "", - client_id: (window as any).SSO_CLIENT_ID ?? "", - scope: (window as any).SSO_SCOPE ?? "", - redirect_uri: (window as any).SSO_REDIRECT_URI ?? "", - response_type: (window as any).SSO_RESPONSE_TYPE ?? "", - code_challenge_method: (window as any).SSO_CODE_CHALLENGE_METHOD ?? "", - post_logout_redirect_uri: (window as any).SSO_POST_LOGOUT_REDIRECT ?? "", + url: window.SSO_BASE_URL ?? "", + client_id: window.SSO_CLIENT_ID ?? "", + scope: window.SSO_SCOPE ?? "", + redirect_uri: window.SSO_REDIRECT_URI ?? "", + response_type: window.SSO_RESPONSE_TYPE ?? "", + code_challenge_method: window.SSO_CODE_CHALLENGE_METHOD ?? "", + post_logout_redirect_uri: window.SSO_POST_LOGOUT_REDIRECT ?? "", }; export async function getClientConfig() { @@ -19,18 +19,21 @@ export async function getClientConfig() { } export async function Auth() { - let code_verifier = client.randomPKCECodeVerifier(); - let code_challenge = await client.calculatePKCECodeChallenge(code_verifier); + const code_verifier = client.randomPKCECodeVerifier(); + const code_challenge = await client.calculatePKCECodeChallenge(code_verifier); const openIdClientConfig = await getClientConfig(); - let state = client.randomState(); - let parameters: Record = { + const state = client.randomState(); + const parameters: Record = { redirect_uri: clientConfig.redirect_uri, scope: clientConfig.scope!, code_challenge, code_challenge_method: clientConfig.code_challenge_method, state, }; - let redirectTo = client.buildAuthorizationUrl(openIdClientConfig, parameters); + const redirectTo = client.buildAuthorizationUrl( + openIdClientConfig, + parameters + ); return { redirectTo, code_verifier, state }; } @@ -44,13 +47,15 @@ export async function Logout() { return endSessionUrl; } -export async function Callback(code_verifier: string, state: any) { +export async function Callback( + code_verifier: string, + state: string | undefined +) { try { const openIdClientConfig = await getClientConfig(); const currentLocation = getLocation(); - console.log("Callback URL:", currentLocation); - console.log("Code verifier:", code_verifier); + console.info("Callback URL:", currentLocation); const tokenSet = await client.authorizationCodeGrant( openIdClientConfig, diff --git a/src/features/User/type/OpenPaasUserData.ts b/src/features/User/type/OpenPaasUserData.ts new file mode 100644 index 0000000..b0ac704 --- /dev/null +++ b/src/features/User/type/OpenPaasUserData.ts @@ -0,0 +1,11 @@ +import { ModuleConfiguration } from "../userDataTypes"; + +export interface OpenPaasUserData { + firstname?: string; + lastname?: string; + id?: string; + preferredEmail?: string; + configurations?: { + modules?: ModuleConfiguration[]; + }; +} diff --git a/src/features/User/userAPI.ts b/src/features/User/userAPI.ts index 19b19d7..7716564 100644 --- a/src/features/User/userAPI.ts +++ b/src/features/User/userAPI.ts @@ -1,5 +1,11 @@ import { User } from "@/components/Attendees/PeopleSearch"; import { api } from "@/utils/apiUtils"; +import { OpenPaasUserData } from "./type/OpenPaasUserData"; +import { + ConfigurationItem, + ModuleConfiguration, + SearchResponseItem, +} from "./userDataTypes"; export async function getOpenPaasUser() { const user = await api.get(`api/user`); @@ -10,7 +16,7 @@ export async function searchUsers( query: string, objectTypes: string[] = ["user", "contact"] ): Promise { - const response: any[] = await api + const response: SearchResponseItem[] = await api .post(`api/people/search`, { body: JSON.stringify({ limit: 10, @@ -29,9 +35,9 @@ export async function searchUsers( })); } -export async function getUserDetails(id: string) { +export async function getUserDetails(id: string): Promise { const user = await api.get(`api/users/${id}`).json(); - return user; + return user as OpenPaasUserData; } export interface UserConfigurationUpdates { @@ -39,7 +45,7 @@ export interface UserConfigurationUpdates { notifications?: Record; timezone?: string | null; displayWeekNumbers?: boolean; - previousConfig?: Record; + previousConfig?: Record; alarmEmails?: boolean; hideDeclinedEvents?: boolean; } @@ -47,9 +53,9 @@ export interface UserConfigurationUpdates { export async function updateUserConfigurations( updates: UserConfigurationUpdates ): Promise { - const coreConfigs: Array<{ name: string; value: any }> = []; - const calendarConfigs: Array<{ name: string; value: any }> = []; - const esnCalendarConfigs: Array<{ name: string; value: any }> = []; + const coreConfigs: ConfigurationItem[] = []; + const calendarConfigs: ConfigurationItem[] = []; + const esnCalendarConfigs: ConfigurationItem[] = []; if (updates.language !== undefined) { coreConfigs.push({ name: "language", value: updates.language }); @@ -58,10 +64,13 @@ export async function updateUserConfigurations( coreConfigs.push({ name: "notifications", value: updates.notifications }); } if (updates.timezone !== undefined) { + const previousDatetime = updates.previousConfig?.datetime as + | { timeZone?: string } + | undefined; coreConfigs.push({ name: "datetime", value: { - ...updates.previousConfig?.datetime, + ...previousDatetime, timeZone: updates.timezone, }, }); @@ -85,10 +94,7 @@ export async function updateUserConfigurations( }); } - const modules: Array<{ - name: string; - configurations: Array<{ name: string; value: any }>; - }> = []; + const modules: ModuleConfiguration[] = []; if (coreConfigs.length > 0) { modules.push({ diff --git a/src/features/User/userDataTypes.ts b/src/features/User/userDataTypes.ts index 44d7f14..555663b 100644 --- a/src/features/User/userDataTypes.ts +++ b/src/features/User/userDataTypes.ts @@ -11,13 +11,7 @@ export interface userData { } export interface UserConfigurations { - modules?: Array<{ - name: string; - configurations?: Array<{ - name: string; - value: any; - }>; - }>; + modules?: ModuleConfiguration[]; } export interface NotificationSettings { @@ -33,3 +27,22 @@ export interface userOrganiser { cn: string; cal_address: string; } +// Type for search response from the API +export interface SearchResponseItem { + id?: string; + emailAddresses?: Array<{ value?: string }>; + names?: Array<{ displayName?: string }>; + photos?: Array<{ url?: string }>; +} + +// Type for configuration item +export interface ConfigurationItem { + name: string; + value: unknown; +} + +// Type for module configuration +export interface ModuleConfiguration { + name: string; + configurations: ConfigurationItem[]; +} diff --git a/src/features/User/userSlice.ts b/src/features/User/userSlice.ts index 6d72ca2..5c959d7 100644 --- a/src/features/User/userSlice.ts +++ b/src/features/User/userSlice.ts @@ -1,24 +1,44 @@ import { formatReduxError } from "@/utils/errorUtils"; import { createAsyncThunk, createSlice } from "@reduxjs/toolkit"; +import { OpenPaasUserData } from "./type/OpenPaasUserData"; import { getOpenPaasUser, updateUserConfigurations, UserConfigurationUpdates, } from "./userAPI"; -import { userData, userOrganiser } from "./userDataTypes"; +import { + ConfigurationItem, + ModuleConfiguration, + userData, + userOrganiser, +} from "./userDataTypes"; + +// Type for core config datetime +interface DatetimeConfig { + timeZone: string | null; + [key: string]: unknown; +} + +// Type for core config +interface CoreConfig { + language: string | null; + datetime: DatetimeConfig; + [key: string]: unknown; +} export const getOpenPaasUserDataAsync = createAsyncThunk< - Record, + OpenPaasUserData, void, { rejectValue: { message: string; status?: number } } >("user/getOpenPaasUserData", async (_, { rejectWithValue }) => { try { - const user = (await getOpenPaasUser()) as Record; - return user; - } catch (err: any) { + const user = await getOpenPaasUser(); + return user as OpenPaasUserData; + } catch (err) { + const error = err as { response?: { status?: number } }; return rejectWithValue({ message: formatReduxError(err), - status: err.response?.status, + status: error.response?.status, }); } }); @@ -31,10 +51,11 @@ export const updateUserConfigurationsAsync = createAsyncThunk< try { await updateUserConfigurations(updates); return updates; - } catch (err: any) { + } catch (err) { + const error = err as { response?: { status?: number } }; return rejectWithValue({ message: formatReduxError(err), - status: err.response?.status, + status: error.response?.status, }); } }); @@ -46,11 +67,11 @@ export const userSlice = createSlice({ organiserData: null as unknown as userOrganiser, tokens: null as unknown as Record, coreConfig: { - language: null as string | null, + language: null, datetime: { - timeZone: null as string | null, + timeZone: null, }, - } as Record, + } as CoreConfig, alarmEmailsEnabled: null as boolean | null, loading: true, error: null as unknown as string | null, @@ -76,7 +97,7 @@ export const userSlice = createSlice({ }, setTimezone: (state, action) => { if (!state.coreConfig.datetime) { - state.coreConfig.datetime = {}; + state.coreConfig.datetime = { timeZone: null }; } state.coreConfig.datetime.timeZone = action.payload; if (state.userData) { @@ -110,40 +131,44 @@ export const userSlice = createSlice({ state.organiserData.cal_address = action.payload.preferredEmail; state.userData.email = action.payload.preferredEmail; } - // Extract data from configurations.modules if (action.payload.configurations?.modules) { const coreModule = action.payload.configurations.modules.find( - (module: any) => module.name === "core" + (module: ModuleConfiguration) => module.name === "core" ); if (coreModule?.configurations) { const newCoreConfig = Object.fromEntries( - coreModule.configurations.map( - (e: { name: string; value: any }) => [e.name, e.value] - ) + coreModule.configurations.map((e: ConfigurationItem) => [ + e.name, + e.value, + ]) ); - state.coreConfig = { ...state.coreConfig, ...newCoreConfig, - }; + } as CoreConfig; const languageConfig = coreModule.configurations.find( - (config: any) => config.name === "language" + (config: ConfigurationItem) => config.name === "language" ); if (languageConfig?.value) { - state.coreConfig.language = languageConfig.value; + state.coreConfig.language = languageConfig.value as string; if (state.userData) - state.userData.language = languageConfig.value; + state.userData.language = languageConfig.value as string; } - const datetimeConfig = coreModule.configurations.find( - (config: any) => config.name === "datetime" + (config: ConfigurationItem) => config.name === "datetime" ); if (datetimeConfig?.value) { - const serverTimeZone = datetimeConfig.value.timeZone; + const datetimeValue = datetimeConfig.value as { + timeZone?: string; + }; + const serverTimeZone = datetimeValue.timeZone; state.coreConfig.datetime = { ...state.coreConfig.datetime, - ...datetimeConfig.value, + ...(typeof datetimeConfig.value === "object" && + datetimeConfig.value !== null + ? datetimeConfig.value + : {}), timeZone: serverTimeZone !== undefined ? serverTimeZone : null, }; if (state.userData) { @@ -160,14 +185,13 @@ export const userSlice = createSlice({ } } } - // Extract alarmEmails from configurations.modules const calendarModule = action.payload.configurations.modules.find( - (module: any) => module.name === "calendar" + (module: ModuleConfiguration) => module.name === "calendar" ); if (calendarModule?.configurations) { const alarmEmailsConfig = calendarModule.configurations.find( - (config: any) => config.name === "alarmEmails" + (config: ConfigurationItem) => config.name === "alarmEmails" ); if (alarmEmailsConfig) { state.alarmEmailsEnabled = alarmEmailsConfig.value === true; @@ -194,7 +218,7 @@ export const userSlice = createSlice({ } if (action.payload.timezone !== undefined) { if (!state.coreConfig.datetime) { - state.coreConfig.datetime = {}; + state.coreConfig.datetime = { timeZone: null }; } state.coreConfig.datetime.timeZone = action.payload.timezone; if (state.userData) { diff --git a/src/setupTests.ts b/src/setupTests.ts index ecfb6f8..61de887 100644 --- a/src/setupTests.ts +++ b/src/setupTests.ts @@ -1,3 +1,4 @@ +/* eslint-disable @typescript-eslint/no-explicit-any */ import "@testing-library/jest-dom"; import { TextEncoder } from "util"; @@ -49,7 +50,7 @@ class ResizeObserverMock { disconnect() {} } -(global as any).ResizeObserver = ResizeObserverMock; +global.ResizeObserver = ResizeObserverMock; class IntersectionObserverMock { observe() {} @@ -59,8 +60,8 @@ class IntersectionObserverMock { (global as any).IntersectionObserver = IntersectionObserverMock; if (typeof window !== "undefined") { - (window as any).WS_PING_PERIOD_MS = 5000; - (window as any).WS_PING_TIMEOUT_PERIOD_MS = 5000; + window.WS_PING_PERIOD_MS = 5000; + window.WS_PING_TIMEOUT_PERIOD_MS = 5000; } // Suppress jsdom CSS selector parsing errors for Emotion/MUI if (typeof window !== "undefined" && window.getComputedStyle) { diff --git a/src/utils/__test__/videoConferenceUtils.test.ts b/src/utils/__test__/videoConferenceUtils.test.ts index 12e1ff8..84a9d9e 100644 --- a/src/utils/__test__/videoConferenceUtils.test.ts +++ b/src/utils/__test__/videoConferenceUtils.test.ts @@ -11,7 +11,7 @@ const mockWindow = { VIDEO_CONFERENCE_BASE_URL: "https://meet.linagora.com", }; -// @ts-ignore +// @ts-expect-error : Mock window object for Node.js environment global.window = mockWindow; describe("videoConferenceUtils", () => { diff --git a/src/utils/apiUtils.ts b/src/utils/apiUtils.ts index aef517c..1f8497b 100644 --- a/src/utils/apiUtils.ts +++ b/src/utils/apiUtils.ts @@ -8,7 +8,7 @@ const RETRY_CONFIG = { maxDelay: 120000, }; export const api = ky.extend({ - prefixUrl: (window as any).CALENDAR_BASE_URL, + prefixUrl: window.CALENDAR_BASE_URL, retry: { limit: RETRY_CONFIG.maxRetries, backoffLimit: RETRY_CONFIG.maxDelay, @@ -90,7 +90,7 @@ export function isValidUrl(string?: string) { try { url = new URL(string ?? ""); - } catch (_) { + } catch { return false; } return url; diff --git a/src/utils/assertThunkSuccess.tsx b/src/utils/assertThunkSuccess.tsx new file mode 100644 index 0000000..1525738 --- /dev/null +++ b/src/utils/assertThunkSuccess.tsx @@ -0,0 +1,13 @@ +import { AsyncThunkResult } from "@/features/Calendars/types/AsyncThunkResult"; + +export async function assertThunkSuccess(result: unknown): Promise { + const typed = result as AsyncThunkResult; + if (typed?.type?.endsWith("/rejected")) { + throw new Error( + typed.error?.message || typed.payload?.message || "API call failed" + ); + } + if (typeof typed.unwrap === "function") { + await typed.unwrap(); + } +} diff --git a/src/utils/errorUtils.ts b/src/utils/errorUtils.ts index d24fbcd..cfefa3b 100644 --- a/src/utils/errorUtils.ts +++ b/src/utils/errorUtils.ts @@ -1,9 +1,26 @@ +import { RejectedError } from "@/features/Calendars/types/RejectedError"; + export function formatReduxError(error: unknown): string { if (!error) return "Unknown error"; + if (typeof error === "string") return error; - if (typeof error === "object") { - const err = error as any; - if (err?.message) return err.message; + + if (typeof error === "object" && error !== null) { + if ( + "message" in error && + typeof (error as { message: unknown }).message === "string" + ) { + return (error as { message: string }).message; + } } + return "Unexpected error occurred"; } + +export function toRejectedError(err: unknown): RejectedError { + const status = + typeof err === "object" && err !== null && "response" in err + ? (err as { response?: { status?: number } }).response?.status + : undefined; + return { message: formatReduxError(err), status }; +} diff --git a/src/utils/timezone.ts b/src/utils/timezone.ts index 231534c..36a521b 100644 --- a/src/utils/timezone.ts +++ b/src/utils/timezone.ts @@ -22,14 +22,14 @@ export function registerTimezones() { } } -function buildTimezone(tzid: string, ics: string): any { +function buildTimezone(tzid: string, ics: string): ICAL.Timezone { return ( ICAL.TimezoneService.get(tzid) || new ICAL.Timezone(new ICAL.Component(ICAL.parse(ics))) ); } -function findTimezone(tzid: string): any { +function findTimezone(tzid: string): ICAL.Timezone { if (TIMEZONES.zones[tzid]) { return buildTimezone(tzid, TIMEZONES.zones[tzid].ics); } diff --git a/src/utils/videoConferenceUtils.ts b/src/utils/videoConferenceUtils.ts index e2b6325..b61c779 100644 --- a/src/utils/videoConferenceUtils.ts +++ b/src/utils/videoConferenceUtils.ts @@ -25,9 +25,7 @@ export function generateMeetingId(): string { */ export function generateMeetingLink(baseUrl?: string): string { const base = - baseUrl || - (window as any).VIDEO_CONFERENCE_BASE_URL || - "https://meet.linagora.com"; + baseUrl || window.VIDEO_CONFERENCE_BASE_URL || "https://meet.linagora.com"; const meetingId = generateMeetingId(); return `${base}/${meetingId}`; } diff --git a/src/websocket/WebSocketGate.tsx b/src/websocket/WebSocketGate.tsx index 6202d3a..024c5a8 100644 --- a/src/websocket/WebSocketGate.tsx +++ b/src/websocket/WebSocketGate.tsx @@ -1,5 +1,6 @@ import { useAppDispatch, useAppSelector } from "@/app/hooks"; import { AppDispatch } from "@/app/store"; +import { Calendar } from "@/features/Calendars/CalendarTypes"; import { useSelectedCalendars } from "@/utils/storage/useSelectedCalendars"; import { useCallback, useEffect, useMemo, useRef, useState } from "react"; import { useI18n } from "twake-i18n"; @@ -51,7 +52,9 @@ export function WebSocketGate() { useAppSelector((state) => state?.calendars?.templist) ?? {} ); - const calendarsToRefreshRef = useRef>(new Map()); + const calendarsToRefreshRef = useRef< + Map + >(new Map()); const calendarsToHideRef = useRef>(new Set()); const debouncedUpdateFnRef = useRef< ((dispatch: AppDispatch) => void) | undefined @@ -102,6 +105,7 @@ export function WebSocketGate() { clearReconnectTimeout(); } }, + // eslint-disable-next-line react-hooks/exhaustive-deps [scheduleReconnect, clearReconnectTimeout] ); @@ -111,6 +115,7 @@ export function WebSocketGate() { (error as ErrorEvent)?.message ?? error.type ?? "unknown"; setWebSocketStatus(t("websocket.error", { error: errorMessage })); setWebSocketStatusSerity("error"); + // eslint-disable-next-line react-hooks/exhaustive-deps }, []); const callBacks = useMemo( @@ -145,6 +150,7 @@ export function WebSocketGate() { clearReconnectTimeout(); } + // eslint-disable-next-line react-hooks/exhaustive-deps }, [isSocketOpen, clearReconnectTimeout]); // Manage WebSocket connection @@ -206,6 +212,7 @@ export function WebSocketGate() { abortController.abort(); cleanup(); }; + // eslint-disable-next-line react-hooks/exhaustive-deps }, [ isAuthenticated, callBacks, @@ -220,7 +227,7 @@ export function WebSocketGate() { // If we just reconnected, force a re-sync if (justReconnectedRef.current && isSocketOpen) { - console.log("Re-syncing calendars after reconnection"); + console.info("Re-syncing calendars after reconnection"); previousCalendarListRef.current = []; previousTempCalendarListRef.current = []; justReconnectedRef.current = false; @@ -277,6 +284,7 @@ export function WebSocketGate() { window.removeEventListener("online", handleOnline); window.removeEventListener("offline", handleOffline); }; + // eslint-disable-next-line react-hooks/exhaustive-deps }, [isSocketOpen, isAuthenticated, clearReconnectTimeout]); useEffect(() => { @@ -315,6 +323,7 @@ export function WebSocketGate() { pingCleanupRef.current = null; } }; + // eslint-disable-next-line react-hooks/exhaustive-deps }, [isSocketOpen]); return websocketStatus ? ( diff --git a/src/websocket/connection/createConnection.ts b/src/websocket/connection/createConnection.ts index 18b92de..21adade 100644 --- a/src/websocket/connection/createConnection.ts +++ b/src/websocket/connection/createConnection.ts @@ -6,8 +6,8 @@ export async function createWebSocketConnection( callbacks: WebSocketCallbacks ): Promise { const wsBaseUrl = - (window as any).WEBSOCKET_URL ?? - (window as any).CALENDAR_BASE_URL?.replace( + window.WEBSOCKET_URL ?? + window.CALENDAR_BASE_URL?.replace( /^http(s)?:/, (_: string, s: string | undefined) => (s ? "wss:" : "ws:") ) ?? @@ -37,7 +37,7 @@ export async function createWebSocketConnection( }, CONNECTION_TIMEOUT_MS); const openHandler = () => { - console.log("WebSocket connection opened"); + console.info("WebSocket connection opened"); clearTimeout(timeoutId); socket.removeEventListener( WS_INBOUND_EVENTS.CONNECTION_OPENED, @@ -73,7 +73,7 @@ export async function createWebSocketConnection( }; const closeHandler = (event: CloseEvent) => { - console.log("WebSocket closed:", event.code, event.reason); + console.info("WebSocket closed:", event.code, event.reason); cleanup(); callbacks.onClose?.(event); }; diff --git a/src/websocket/connection/lifecycle/pingWebSocket.ts b/src/websocket/connection/lifecycle/pingWebSocket.ts index 78b39a7..c848ce5 100644 --- a/src/websocket/connection/lifecycle/pingWebSocket.ts +++ b/src/websocket/connection/lifecycle/pingWebSocket.ts @@ -19,8 +19,8 @@ export interface PingCleanup { sendPing: () => void; } -const DEFAULT_PING_INTERVAL = (window as any).WS_PING_PERIOD_MS ?? 30000; -const DEFAULT_PONG_TIMEOUT = (window as any).WS_PING_TIMEOUT_PERIOD_MS ?? 35000; +const DEFAULT_PING_INTERVAL = window.WS_PING_PERIOD_MS ?? 30000; +const DEFAULT_PONG_TIMEOUT = window.WS_PING_TIMEOUT_PERIOD_MS ?? 35000; /** * Sets up a ping/pong mechanism to monitor WebSocket connection health diff --git a/src/websocket/connection/lifecycle/useWebSocketReconnect.ts b/src/websocket/connection/lifecycle/useWebSocketReconnect.ts index 95aa4cd..f0e8db6 100644 --- a/src/websocket/connection/lifecycle/useWebSocketReconnect.ts +++ b/src/websocket/connection/lifecycle/useWebSocketReconnect.ts @@ -18,6 +18,7 @@ export function useWebSocketReconnect( clearTimeout(reconnectTimeoutRef.current); reconnectTimeoutRef.current = null; } + // eslint-disable-next-line react-hooks/exhaustive-deps }, []); const scheduleReconnect = useCallback(() => { @@ -35,7 +36,7 @@ export function useWebSocketReconnect( const delay = getRetryDelay(reconnectAttemptsRef.current, RECONNECT_CONFIG); reconnectAttemptsRef.current += 1; - console.log( + console.info( `Scheduling WebSocket reconnection in ${Math.round(delay)}ms ` + `(attempt ${reconnectAttemptsRef.current}/${MAX_RECONNECT_ATTEMPTS})` ); @@ -45,12 +46,13 @@ export function useWebSocketReconnect( reconnectTimeoutRef.current = null; return; } - console.log( + console.info( `Attempting WebSocket reconnection (attempt ${reconnectAttemptsRef.current}/${MAX_RECONNECT_ATTEMPTS})` ); setShouldConnect((prev) => !prev); clearReconnectTimeout(); }, delay); + // eslint-disable-next-line react-hooks/exhaustive-deps }, [clearReconnectTimeout]); return { scheduleReconnect, clearReconnectTimeout }; } diff --git a/src/websocket/messaging/type/UpdateCalendarsAccumulators.ts b/src/websocket/messaging/type/UpdateCalendarsAccumulators.ts index d6734d8..575f1c9 100644 --- a/src/websocket/messaging/type/UpdateCalendarsAccumulators.ts +++ b/src/websocket/messaging/type/UpdateCalendarsAccumulators.ts @@ -1,7 +1,8 @@ import { AppDispatch } from "@/app/store"; +import { Calendar } from "@/features/Calendars/CalendarTypes"; export interface UpdateCalendarsAccumulators { - calendarsToRefresh: Map; + calendarsToRefresh: Map; calendarsToHide: Set; debouncedUpdateFn?: (dispatch: AppDispatch) => void; currentDebouncePeriod?: number; diff --git a/src/websocket/messaging/updateCalendars.ts b/src/websocket/messaging/updateCalendars.ts index 7927633..68912df 100644 --- a/src/websocket/messaging/updateCalendars.ts +++ b/src/websocket/messaging/updateCalendars.ts @@ -1,5 +1,6 @@ import type { AppDispatch } from "@/app/store"; import { store } from "@/app/store"; +import { Calendar } from "@/features/Calendars/CalendarTypes"; import { refreshCalendarWithSyncToken } from "@/features/Calendars/services"; import { findCalendarById, getDisplayedCalendarRange } from "@/utils"; import { setSelectedCalendars } from "@/utils/storage/setSelectedCalendars"; @@ -12,7 +13,10 @@ const DEFAULT_DEBOUNCE_MS = 0; function createDebouncedUpdate( debouncePeriodMs: number, - getCalendarsToRefresh: () => Map, + getCalendarsToRefresh: () => Map< + string, + { calendar: Calendar; type?: "temp" } + >, getCalendarsToHide: () => Set ) { return debounce( @@ -55,8 +59,7 @@ export function updateCalendars( ); accumulateCalendarsToHide(calendarsToHide, accumulators.calendarsToHide); - const debouncePeriod = - (window as any).WS_DEBOUNCE_PERIOD_MS ?? DEFAULT_DEBOUNCE_MS; + const debouncePeriod = window.WS_DEBOUNCE_PERIOD_MS ?? DEFAULT_DEBOUNCE_MS; if (debouncePeriod > 0) { if ( @@ -94,7 +97,7 @@ export function updateCalendars( function accumulateCalendarsToRefresh( state: ReturnType, calendarPaths: Set, - calendarsToRefreshMap: Map + calendarsToRefreshMap: Map ) { calendarPaths.forEach((calendarPath) => { const calendarId = parseCalendarPath(calendarPath); @@ -126,7 +129,7 @@ function accumulateCalendarsToHide( function processCalendarsToRefresh( dispatch: AppDispatch, currentRange: { start: Date; end: Date }, - calendarsMap: Map + calendarsMap: Map ) { calendarsMap.forEach((calendar) => { dispatch( diff --git a/src/websocket/operations/registerToCalendars.ts b/src/websocket/operations/registerToCalendars.ts index ebdde12..740fd1b 100644 --- a/src/websocket/operations/registerToCalendars.ts +++ b/src/websocket/operations/registerToCalendars.ts @@ -12,5 +12,5 @@ export function registerToCalendars( }) ); - console.log("Registered to calendars", calendarURIList); + console.info("Registered to calendars", calendarURIList); } diff --git a/src/websocket/operations/unregisterToCalendars.ts b/src/websocket/operations/unregisterToCalendars.ts index 03a9be1..688eaa2 100644 --- a/src/websocket/operations/unregisterToCalendars.ts +++ b/src/websocket/operations/unregisterToCalendars.ts @@ -12,5 +12,5 @@ export function unregisterToCalendars( }) ); - console.log("Unregistered to calendars", calendarURIList); + console.info("Unregistered to calendars", calendarURIList); } diff --git a/src/window.d.ts b/src/window.d.ts new file mode 100644 index 0000000..5844bb2 --- /dev/null +++ b/src/window.d.ts @@ -0,0 +1,33 @@ +import type { AppIconProps } from "@/components/Menubar/Menubar"; +import type { MutableRefObject } from "react"; +import type { CalendarApi } from "@fullcalendar/core"; + +export {}; + +declare global { + interface Window { + SSO_BASE_URL: string; + SSO_CLIENT_ID: string; + SSO_SCOPE: string; + SSO_REDIRECT_URI: string; + SSO_RESPONSE_TYPE: "code"; + SSO_CODE_CHALLENGE_METHOD: "S256"; + SSO_POST_LOGOUT_REDIRECT: string; + + CALENDAR_BASE_URL: string; + MAIL_SPA_URL: string; + VIDEO_CONFERENCE_BASE_URL: string; + + DEBUG: boolean; + LANG: string; + + WEBSOCKET_URL: string; + WS_DEBOUNCE_PERIOD_MS: number; + WS_PING_PERIOD_MS: number; + WS_PING_TIMEOUT_PERIOD_MS: number; + + appList: AppIconProps[]; + + __calendarRef: MutableRefObject; + } +}