Co-authored-by: Camille Moussu <cmoussu@linagora.com>
This commit is contained in:
Vendored
+5
@@ -26,6 +26,11 @@ pipeline {
|
|||||||
sh 'npm run test'
|
sh 'npm run test'
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
stage('Check Formatting') {
|
||||||
|
steps {
|
||||||
|
sh 'npx prettier --check .'
|
||||||
|
}
|
||||||
|
}
|
||||||
stage('Deploy docker images') {
|
stage('Deploy docker images') {
|
||||||
when {
|
when {
|
||||||
anyOf {
|
anyOf {
|
||||||
|
|||||||
@@ -8,11 +8,20 @@ This project aims at service a Single Page Application allowing a user to intera
|
|||||||
|
|
||||||
This frontend-only application will interact with:
|
This frontend-only application will interact with:
|
||||||
|
|
||||||
- [esn-sabre](https://github.com/linagora/esn-sabre/) CalDAV + CardDAV server, tailor made for LINAGORA needs.
|
- [esn-sabre](https://github.com/linagora/esn-sabre/) CalDAV + CardDAV server, tailor made for LINAGORA needs.
|
||||||
- [Twake Calendar side service](https://github.com/linagora/twake-calendar-side-service) that delivers additional backend features for Sabre.
|
- [Twake Calendar side service](https://github.com/linagora/twake-calendar-side-service) that delivers additional backend features for Sabre.
|
||||||
|
|
||||||
It is meant as a drop in replacement of [esn-frontend-calendar](https://github.com/linagora/esn-frontend-calendar).
|
It is meant as a drop in replacement of [esn-frontend-calendar](https://github.com/linagora/esn-frontend-calendar).
|
||||||
|
|
||||||
|
## Contributing
|
||||||
|
|
||||||
|
### Formating
|
||||||
|
|
||||||
|
We use [Prettier](https://prettier.io/) to keep code style consistent.
|
||||||
|
A `.prettierrc` file is already included in the repo, so formatting rules are predefined.
|
||||||
|
|
||||||
|
Before committing, make sure you format your files either using your IDE Prettier extension or Prettier CLI.
|
||||||
|
|
||||||
## Running it
|
## Running it
|
||||||
|
|
||||||
Requirement: node 24+
|
Requirement: node 24+
|
||||||
@@ -89,7 +98,11 @@ Example:
|
|||||||
|
|
||||||
```js
|
```js
|
||||||
var appList = [
|
var appList = [
|
||||||
{ name: "My App", icon: "https://myapp.com/myapp.png", link: "https://myapp.com" },
|
{
|
||||||
|
name: "My App",
|
||||||
|
icon: "https://myapp.com/myapp.png",
|
||||||
|
link: "https://myapp.com",
|
||||||
|
},
|
||||||
];
|
];
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|||||||
@@ -111,7 +111,7 @@ describe("CalendarApp integration", () => {
|
|||||||
openpaasId: "667037022b752d0026472254",
|
openpaasId: "667037022b752d0026472254",
|
||||||
},
|
},
|
||||||
tokens: {
|
tokens: {
|
||||||
accessToken: "token"
|
accessToken: "token",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
calendars: {
|
calendars: {
|
||||||
@@ -133,14 +133,16 @@ describe("CalendarApp integration", () => {
|
|||||||
cn: "Alice",
|
cn: "Alice",
|
||||||
cal_address: "alice@example.com",
|
cal_address: "alice@example.com",
|
||||||
},
|
},
|
||||||
attendee: [{
|
attendee: [
|
||||||
cn: "Alice",
|
{
|
||||||
partstat: "ACCEPTED",
|
cn: "Alice",
|
||||||
rsvp: "TRUE",
|
partstat: "ACCEPTED",
|
||||||
role: "REQ-PARTICIPANT",
|
rsvp: "TRUE",
|
||||||
cutype: "INDIVIDUAL",
|
role: "REQ-PARTICIPANT",
|
||||||
cal_address: "alice@example.com",
|
cutype: "INDIVIDUAL",
|
||||||
}, ],
|
cal_address: "alice@example.com",
|
||||||
|
},
|
||||||
|
],
|
||||||
...eventProps,
|
...eventProps,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -57,33 +57,34 @@ describe("MiniCalendar", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it.each([
|
it.each([
|
||||||
{ today: new Date(2025, 8, 1), label: "Monday 1 Sept 2025" },
|
{ today: new Date(2025, 8, 1), label: "Monday 1 Sept 2025" },
|
||||||
{ today: new Date(2025, 8, 3), label: "Wednesday 3 Sept 2025" },
|
{ today: new Date(2025, 8, 3), label: "Wednesday 3 Sept 2025" },
|
||||||
{ today: new Date(2025, 8, 7), label: "Sunday 7 Sept 2025" },
|
{ today: new Date(2025, 8, 7), label: "Sunday 7 Sept 2025" },
|
||||||
])(
|
])(
|
||||||
"renders mini calendar with the week in gray (except for today) when today is $label",
|
"renders mini calendar with the week in gray (except for today) when today is $label",
|
||||||
({ today }) => {
|
({ today }) => {
|
||||||
jest.useFakeTimers();
|
jest.useFakeTimers();
|
||||||
jest.setSystemTime(today);
|
jest.setSystemTime(today);
|
||||||
|
|
||||||
renderCalendar();
|
renderCalendar();
|
||||||
|
|
||||||
const monday = new Date(2025, 8, 1); // Monday Sept 1
|
const monday = new Date(2025, 8, 1); // Monday Sept 1
|
||||||
|
|
||||||
for (let i = 0; i < 7; i++) {
|
for (let i = 0; i < 7; i++) {
|
||||||
const date = new Date(monday);
|
const date = new Date(monday);
|
||||||
date.setDate(monday.getDate() + i);
|
date.setDate(monday.getDate() + i);
|
||||||
const dateTestId = `date-${date.getFullYear()}-${date.getMonth()}-${date.getDate()}`;
|
const dateTestId = `date-${date.getFullYear()}-${date.getMonth()}-${date.getDate()}`;
|
||||||
|
|
||||||
const tile = screen.getByTestId(dateTestId);
|
const tile = screen.getByTestId(dateTestId);
|
||||||
|
|
||||||
if (date.getTime() !== today.setHours(0, 0, 0, 0)) {
|
if (date.getTime() !== today.setHours(0, 0, 0, 0)) {
|
||||||
expect(tile?.parentElement).toHaveClass("selectedWeek");
|
expect(tile?.parentElement).toHaveClass("selectedWeek");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
jest.useRealTimers();
|
jest.useRealTimers();
|
||||||
});
|
}
|
||||||
|
);
|
||||||
|
|
||||||
it("renders mini calendar with the day in gray (except for today) when full calendar in day view", async () => {
|
it("renders mini calendar with the day in gray (except for today) when full calendar in day view", async () => {
|
||||||
renderCalendar();
|
renderCalendar();
|
||||||
|
|||||||
@@ -95,15 +95,13 @@ describe("Event Preview Display", () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
it("renders correctly event data", () => {
|
it("renders correctly event data", () => {
|
||||||
jest
|
jest.spyOn(Date.prototype, "toLocaleString").mockImplementation(function (
|
||||||
.spyOn(Date.prototype, "toLocaleString")
|
this: Date,
|
||||||
.mockImplementation(function (
|
locales?: Intl.LocalesArgument,
|
||||||
this: Date,
|
options?: Intl.DateTimeFormatOptions | undefined
|
||||||
locales?: Intl.LocalesArgument,
|
): string {
|
||||||
options?: Intl.DateTimeFormatOptions | undefined
|
return RealDateToLocaleString.call(this, "en-UK", options);
|
||||||
): string {
|
});
|
||||||
return RealDateToLocaleString.call(this, "en-UK", options);
|
|
||||||
});
|
|
||||||
renderWithProviders(
|
renderWithProviders(
|
||||||
<EventPreviewModal
|
<EventPreviewModal
|
||||||
anchorPosition={{ top: 0, left: 0 }}
|
anchorPosition={{ top: 0, left: 0 }}
|
||||||
|
|||||||
+2
-2
@@ -1,6 +1,6 @@
|
|||||||
module.exports = {
|
module.exports = {
|
||||||
presets: [
|
presets: [
|
||||||
['@babel/preset-env', { targets: { node: 'current' } }],
|
["@babel/preset-env", { targets: { node: "current" } }],
|
||||||
['@babel/preset-react', { runtime: 'automatic', importSource: 'preact' }],
|
["@babel/preset-react", { runtime: "automatic", importSource: "preact" }],
|
||||||
],
|
],
|
||||||
};
|
};
|
||||||
@@ -6,4 +6,4 @@ var SSO_RESPONSE_TYPE = "code";
|
|||||||
var SSO_CODE_CHALLENGE_METHOD = "S256";
|
var SSO_CODE_CHALLENGE_METHOD = "S256";
|
||||||
var SSO_POST_LOGOUT_REDIRECT = "http://example.com?logout=1";
|
var SSO_POST_LOGOUT_REDIRECT = "http://example.com?logout=1";
|
||||||
var CALENDAR_BASE_URL = "https://calendar.example.com";
|
var CALENDAR_BASE_URL = "https://calendar.example.com";
|
||||||
var MAIL_SPA_URL = "https://mail.example.com"
|
var MAIL_SPA_URL = "https://mail.example.com";
|
||||||
|
|||||||
+1
-1
@@ -1,4 +1,4 @@
|
|||||||
<!DOCTYPE html>
|
<!doctype html>
|
||||||
<html lang="en">
|
<html lang="en">
|
||||||
<head>
|
<head>
|
||||||
<meta charset="utf-8" />
|
<meta charset="utf-8" />
|
||||||
|
|||||||
Vendored
+7
-1
@@ -1,5 +1,11 @@
|
|||||||
declare namespace Intl {
|
declare namespace Intl {
|
||||||
type Key = 'calendar' | 'collation' | 'currency' | 'numberingSystem' | 'timeZone' | 'unit';
|
type Key =
|
||||||
|
| "calendar"
|
||||||
|
| "collation"
|
||||||
|
| "currency"
|
||||||
|
| "numberingSystem"
|
||||||
|
| "timeZone"
|
||||||
|
| "unit";
|
||||||
|
|
||||||
function supportedValuesOf(input: Key): string[];
|
function supportedValuesOf(input: Key): string[];
|
||||||
}
|
}
|
||||||
+4
-4
@@ -1,5 +1,5 @@
|
|||||||
import { useDispatch, useSelector } from 'react-redux'
|
import { useDispatch, useSelector } from "react-redux";
|
||||||
import type { RootState, AppDispatch } from './store'
|
import type { RootState, AppDispatch } from "./store";
|
||||||
|
|
||||||
export const useAppDispatch = useDispatch.withTypes<AppDispatch>()
|
export const useAppDispatch = useDispatch.withTypes<AppDispatch>();
|
||||||
export const useAppSelector = useSelector.withTypes<RootState>()
|
export const useAppSelector = useSelector.withTypes<RootState>();
|
||||||
|
|||||||
@@ -133,7 +133,9 @@ main {
|
|||||||
|
|
||||||
/* selected day hover */
|
/* selected day hover */
|
||||||
.react-calendar__tile {
|
.react-calendar__tile {
|
||||||
transition: background-color 0.3s ease, color 0.3s ease;
|
transition:
|
||||||
|
background-color 0.3s ease,
|
||||||
|
color 0.3s ease;
|
||||||
border: none;
|
border: none;
|
||||||
background-color: white;
|
background-color: white;
|
||||||
width: 12px;
|
width: 12px;
|
||||||
|
|||||||
@@ -31,7 +31,7 @@ import { createSelector } from "@reduxjs/toolkit";
|
|||||||
import HelpOutlineIcon from "@mui/icons-material/HelpOutline";
|
import HelpOutlineIcon from "@mui/icons-material/HelpOutline";
|
||||||
import ClearIcon from "@mui/icons-material/Clear";
|
import ClearIcon from "@mui/icons-material/Clear";
|
||||||
import AccessTimeIcon from "@mui/icons-material/AccessTime";
|
import AccessTimeIcon from "@mui/icons-material/AccessTime";
|
||||||
import LockIcon from '@mui/icons-material/Lock';
|
import LockIcon from "@mui/icons-material/Lock";
|
||||||
import { userAttendee } from "../../features/User/userDataTypes";
|
import { userAttendee } from "../../features/User/userDataTypes";
|
||||||
|
|
||||||
const computeStartOfTheWeek = (date: Date): Date => {
|
const computeStartOfTheWeek = (date: Date): Date => {
|
||||||
@@ -204,7 +204,6 @@ export default function CalendarApp() {
|
|||||||
calendarRef.current?.view.type === "timeGridWeek" ||
|
calendarRef.current?.view.type === "timeGridWeek" ||
|
||||||
calendarRef.current?.view.type === undefined
|
calendarRef.current?.view.type === undefined
|
||||||
) {
|
) {
|
||||||
|
|
||||||
const startOfWeek = computeStartOfTheWeek(selected);
|
const startOfWeek = computeStartOfTheWeek(selected);
|
||||||
|
|
||||||
const endOfWeek = new Date(startOfWeek);
|
const endOfWeek = new Date(startOfWeek);
|
||||||
@@ -430,7 +429,9 @@ export default function CalendarApp() {
|
|||||||
if (!calendars[arg.event._def.extendedProps.calId]) return;
|
if (!calendars[arg.event._def.extendedProps.calId]) return;
|
||||||
|
|
||||||
const attendees = event._def.extendedProps.attendee || [];
|
const attendees = event._def.extendedProps.attendee || [];
|
||||||
const isPrivate = event._def.extendedProps.class === "PRIVATE" || event._def.extendedProps.class === "CONFIDENTIAL";
|
const isPrivate =
|
||||||
|
event._def.extendedProps.class === "PRIVATE" ||
|
||||||
|
event._def.extendedProps.class === "CONFIDENTIAL";
|
||||||
let Icon = null;
|
let Icon = null;
|
||||||
let titleStyle: React.CSSProperties = {};
|
let titleStyle: React.CSSProperties = {};
|
||||||
const ownerEmails = new Set(
|
const ownerEmails = new Set(
|
||||||
@@ -461,9 +462,17 @@ export default function CalendarApp() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div style={{ display: 'flex', alignItems: 'center' }}>
|
<div style={{ display: "flex", alignItems: "center" }}>
|
||||||
{isPrivate && <LockIcon data-testid="lock-icon" fontSize="small" style={{ marginRight: '4px' }} />}
|
{isPrivate && (
|
||||||
{Icon && <Icon fontSize="small" style={{ marginRight: '4px' }}/>}
|
<LockIcon
|
||||||
|
data-testid="lock-icon"
|
||||||
|
fontSize="small"
|
||||||
|
style={{ marginRight: "4px" }}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
{Icon && (
|
||||||
|
<Icon fontSize="small" style={{ marginRight: "4px" }} />
|
||||||
|
)}
|
||||||
<span style={titleStyle}>{event.title}</span>
|
<span style={titleStyle}>{event.title}</span>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -28,7 +28,6 @@ function CalendarPopover({
|
|||||||
const [timeZone, setTimeZone] = useState("");
|
const [timeZone, setTimeZone] = useState("");
|
||||||
const timezones = Intl.supportedValuesOf?.("timeZone") ?? [];
|
const timezones = Intl.supportedValuesOf?.("timeZone") ?? [];
|
||||||
const handleSave = () => {
|
const handleSave = () => {
|
||||||
|
|
||||||
dispatch(createCalendar({ name, description, color }));
|
dispatch(createCalendar({ name, description, color }));
|
||||||
onClose({}, "backdropClick");
|
onClose({}, "backdropClick");
|
||||||
|
|
||||||
@@ -64,7 +63,11 @@ function CalendarPopover({
|
|||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Box p={2}>
|
<Box p={2}>
|
||||||
<Typography variant="h6" gutterBottom style={{backgroundColor:color}}>
|
<Typography
|
||||||
|
variant="h6"
|
||||||
|
gutterBottom
|
||||||
|
style={{ backgroundColor: color }}
|
||||||
|
>
|
||||||
Create a Calendar
|
Create a Calendar
|
||||||
</Typography>
|
</Typography>
|
||||||
<TextField
|
<TextField
|
||||||
|
|||||||
@@ -58,7 +58,7 @@ export function parseCalendarEvent(
|
|||||||
case "organizer":
|
case "organizer":
|
||||||
event.organizer = {
|
event.organizer = {
|
||||||
cn: params?.cn ?? "",
|
cn: params?.cn ?? "",
|
||||||
cal_address: value.replace(/^mailto:/, ""),
|
cal_address: value?.replace(/^mailto:/, ""),
|
||||||
};
|
};
|
||||||
break;
|
break;
|
||||||
case "attendee":
|
case "attendee":
|
||||||
|
|||||||
+5
-5
@@ -1,13 +1,13 @@
|
|||||||
body {
|
body {
|
||||||
margin: 0;
|
margin: 0;
|
||||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen',
|
font-family:
|
||||||
'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue',
|
-apple-system, BlinkMacSystemFont, "Segoe UI", "Roboto", "Oxygen", "Ubuntu",
|
||||||
sans-serif;
|
"Cantarell", "Fira Sans", "Droid Sans", "Helvetica Neue", sans-serif;
|
||||||
-webkit-font-smoothing: antialiased;
|
-webkit-font-smoothing: antialiased;
|
||||||
-moz-osx-font-smoothing: grayscale;
|
-moz-osx-font-smoothing: grayscale;
|
||||||
}
|
}
|
||||||
|
|
||||||
code {
|
code {
|
||||||
font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New',
|
font-family:
|
||||||
monospace;
|
source-code-pro, Menlo, Monaco, Consolas, "Courier New", monospace;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,15 +0,0 @@
|
|||||||
import { ReportHandler } from 'web-vitals';
|
|
||||||
|
|
||||||
const reportWebVitals = (onPerfEntry?: ReportHandler) => {
|
|
||||||
if (onPerfEntry && onPerfEntry instanceof Function) {
|
|
||||||
import('web-vitals').then(({ getCLS, getFID, getFCP, getLCP, getTTFB }) => {
|
|
||||||
getCLS(onPerfEntry);
|
|
||||||
getFID(onPerfEntry);
|
|
||||||
getFCP(onPerfEntry);
|
|
||||||
getLCP(onPerfEntry);
|
|
||||||
getTTFB(onPerfEntry);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
export default reportWebVitals;
|
|
||||||
Reference in New Issue
Block a user