sidebar availability search (#128)

* [123] added people search in left side bar

* [#123] added toggle temp calendars

* [#123] added tests]

* fixup! [123] added people search in left side bar

* fixup! [#123] added toggle temp calendars

* [#123] fixed event creation on Enter

* fixup! [#123] fixed event creation on Enter

* fixup! [#123] fixed event creation on Enter

* fixup! [#123] fixed event creation on Enter

* fixup! [#123] fixed event creation on Enter

* fixup! [#123] fixed event creation on Enter

* fixup! [#123] fixed event creation on Enter

* fixup! [#123] fixed event creation on Enter

* [#123] added color diff for temp calendars

* fixup! [#123] fixed event creation on Enter

* fixup! [#123] added tests]

* fixup! [#123] added toggle temp calendars

---------

Co-authored-by: Camille Moussu <cmoussu@linagora.com>
This commit is contained in:
Camille Moussu
2025-09-24 17:05:45 +02:00
committed by GitHub
parent 1db93efed9
commit 00c0b58032
14 changed files with 707 additions and 114 deletions
+116 -34
View File
@@ -12,6 +12,7 @@ import { getOpenPaasUser, getUserDetails } from "../User/userAPI";
import { parseCalendarEvent } from "../Events/eventUtils";
import { deleteEvent, getEvent, moveEvent, putEvent } from "../Events/EventApi";
import { formatDateToYYYYMMDDTHHMMSS } from "../../utils/dateUtils";
import { User } from "../../components/Attendees/PeopleSearch";
export const getCalendarsListAsync = createAsyncThunk<
Record<string, Calendars> // Return type
@@ -55,10 +56,50 @@ export const getCalendarsListAsync = createAsyncThunk<
return importedCalendars;
});
export const getTempCalendarsListAsync = createAsyncThunk<
Record<string, Calendars>,
User
>("calendars/getTempCalendars", async (tempUser) => {
const importedCalendars: Record<string, Calendars> = {};
const calendars = (await getCalendars(
tempUser.openpaasId ?? "",
"sharedPublic=true&WithRights=true"
)) as Record<string, any>;
const rawCalendars = calendars._embedded["dav:calendar"];
for (const cal of rawCalendars) {
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;
const id = source.replace("/calendars/", "").replace(".json", "");
const ownerData: any = await getUserDetails(id.split("/")[0]);
importedCalendars[id] = {
id,
name,
link,
owner: `${ownerData.firstname ? `${ownerData.firstname} ` : ""}${ownerData.lastname}`,
ownerEmails: ownerData.emails,
description,
delegated,
color: tempUser.color ?? "#a8a8a8ff",
events: {},
};
}
return importedCalendars;
});
export const getCalendarDetailAsync = createAsyncThunk<
{ calId: string; events: CalendarEvent[] }, // Return type
{ calId: string; match: { start: string; end: string } } // Arg type
>("calendars/getCalendarDetails", async ({ calId, match }) => {
{ calId: string; events: CalendarEvent[]; calType?: string }, // Return type
{ calId: string; match: { start: string; end: string }; calType?: string } // Arg type
>("calendars/getCalendarDetails", async ({ calId, match, calType }) => {
const calendar = (await getCalendar(calId, match)) as Record<string, any>;
const color = calendar["apple:color"];
const events: CalendarEvent[] = calendar._embedded["dav:item"].flatMap(
@@ -72,13 +113,13 @@ export const getCalendarDetailAsync = createAsyncThunk<
}
);
return { calId, events };
return { calId, events, calType };
});
export const putEventAsync = createAsyncThunk<
{ calId: string; events: CalendarEvent[] }, // Return type
{ cal: Calendars; newEvent: CalendarEvent } // Arg type
>("calendars/putEvent", async ({ cal, newEvent }) => {
{ calId: string; events: CalendarEvent[]; calType?: "temp" }, // Return type
{ cal: Calendars; newEvent: CalendarEvent; calType?: "temp" } // Arg type
>("calendars/putEvent", async ({ cal, newEvent, calType }) => {
const response = await putEvent(
newEvent,
cal.ownerEmails ? cal.ownerEmails[0] : undefined
@@ -116,6 +157,7 @@ export const putEventAsync = createAsyncThunk<
return {
calId: cal.id,
events,
calType,
};
});
@@ -247,7 +289,11 @@ export const addSharedCalendarAsync = createAsyncThunk<
const CalendarSlice = createSlice({
name: "calendars",
initialState: { list: {} as Record<string, Calendars>, pending: false },
initialState: {
list: {} as Record<string, Calendars>,
templist: {} as Record<string, Calendars>,
pending: false,
},
reducers: {
createCalendar: (state, action: PayloadAction<Record<string, string>>) => {
const id = Date.now().toString(36);
@@ -280,6 +326,9 @@ const CalendarSlice = createSlice({
action.payload.eventUid
];
},
removeTempCal: (state, action: PayloadAction<string>) => {
delete state.templist[action.payload];
},
updateEventLocal: (
state,
action: PayloadAction<{ calId: string; event: CalendarEvent }>
@@ -297,56 +346,81 @@ const CalendarSlice = createSlice({
state.list = action.payload;
}
)
.addCase(
getTempCalendarsListAsync.fulfilled,
(state, action: PayloadAction<Record<string, Calendars>>) => {
state.pending = false;
Object.keys(action.payload).forEach(
(id) => (state.templist[id] = action.payload[id])
);
}
)
.addCase(
getCalendarDetailAsync.fulfilled,
(
state,
action: PayloadAction<{ calId: string; events: CalendarEvent[] }>
action: PayloadAction<{
calId: string;
events: CalendarEvent[];
calType?: string;
}>
) => {
state.pending = false;
if (!state.list[action.payload.calId]) {
state.list[action.payload.calId] = {
const type = action.payload.calType === "temp" ? "templist" : "list";
if (!state[type][action.payload.calId]) {
state[type][action.payload.calId] = {
id: action.payload.calId,
events: {},
} as Calendars;
}
action.payload.events.forEach((event) => {
state.list[action.payload.calId].events[event.uid] = event;
});
Object.keys(state.list[action.payload.calId].events).forEach((id) => {
state.list[action.payload.calId].events[id].color =
state.list[action.payload.calId].color;
state.list[action.payload.calId].events[id].calId =
action.payload.calId;
state.list[action.payload.calId].events[id].timezone =
Intl.DateTimeFormat().resolvedOptions().timeZone;
state[type][action.payload.calId].events[event.uid] = event;
});
Object.keys(state[type][action.payload.calId].events).forEach(
(id) => {
state[type][action.payload.calId].events[id].color =
state[type][action.payload.calId].color;
state[type][action.payload.calId].events[id].calId =
action.payload.calId;
state[type][action.payload.calId].events[id].timezone =
Intl.DateTimeFormat().resolvedOptions().timeZone;
}
);
}
)
.addCase(
putEventAsync.fulfilled,
(
state,
action: PayloadAction<{ calId: string; events: CalendarEvent[] }>
action: PayloadAction<{
calId: string;
events: CalendarEvent[];
calType?: "temp";
}>
) => {
state.pending = false;
if (!state.list[action.payload.calId]) {
state.list[action.payload.calId] = {
const type = action.payload.calType === "temp" ? "templist" : "list";
if (!state[type][action.payload.calId]) {
state[type][action.payload.calId] = {
id: action.payload.calId,
events: {},
} as Calendars;
}
action.payload.events.forEach((event) => {
state.list[action.payload.calId].events[event.uid] = event;
});
Object.keys(state.list[action.payload.calId].events).forEach((id) => {
state.list[action.payload.calId].events[id].color =
state.list[action.payload.calId].color;
state.list[action.payload.calId].events[id].calId =
action.payload.calId;
state.list[action.payload.calId].events[id].timezone =
Intl.DateTimeFormat().resolvedOptions().timeZone;
state[type][action.payload.calId].events[event.uid] = event;
});
Object.keys(state[type][action.payload.calId].events).forEach(
(id) => {
state[type][action.payload.calId].events[id].color =
state[type][action.payload.calId].color;
state[type][action.payload.calId].events[id].calId =
action.payload.calId;
state[type][action.payload.calId].events[id].timezone =
Intl.DateTimeFormat().resolvedOptions().timeZone;
}
);
}
)
.addCase(
@@ -484,12 +558,20 @@ const CalendarSlice = createSlice({
.addCase(createCalendarAsync.pending, (state) => {
state.pending = true;
})
.addCase(getTempCalendarsListAsync.pending, (state) => {
state.pending = true;
})
.addCase(addSharedCalendarAsync.pending, (state) => {
state.pending = true;
});
},
});
export const { addEvent, removeEvent, createCalendar, updateEventLocal } =
CalendarSlice.actions;
export const {
addEvent,
removeEvent,
createCalendar,
updateEventLocal,
removeTempCal,
} = CalendarSlice.actions;
export default CalendarSlice.reducer;
+6 -4
View File
@@ -45,22 +45,24 @@ import { getCalendar } from "../Calendars/CalendarApi";
export default function EventPreviewModal({
eventId,
calId,
tempEvent,
anchorPosition,
open,
onClose,
}: {
eventId: string;
calId: string;
tempEvent?: boolean;
anchorPosition: PopoverPosition | null;
open: boolean;
onClose: (event: {}, reason: "backdropClick" | "escapeKeyDown") => void;
}) {
const dispatch = useAppDispatch();
const calendars = useAppSelector((state) => state.calendars);
const calendar = calendars.list[calId];
const event = useAppSelector(
(state) => state.calendars.list[calId]?.events[eventId]
);
const calendar = tempEvent
? calendars.templist[calId]
: calendars.list[calId];
const event = calendar.events[eventId];
const user = useAppSelector((state) => state.user);
const [showAllAttendees, setShowAllAttendees] = useState(false);
const [openFullDisplay, setOpenFullDisplay] = useState(false);
+28 -9
View File
@@ -67,13 +67,16 @@ function EventPopover({
const [showMore, setShowMore] = useState(false);
const [title, setTitle] = useState(event?.title ?? "");
const [description, setDescription] = useState(event?.description ?? "");
const [location, setLocation] = useState(event?.location ?? "");
const [start, setStart] = useState(
event?.start ? new Date(event.start).toISOString() : ""
event?.start
? new Date(event.start).toISOString()
: new Date().toISOString()
);
const [end, setEnd] = useState(
event?.end ? new Date(event.end)?.toISOString() : ""
event?.end ? new Date(event.end)?.toISOString() : new Date().toISOString()
);
const [calendarid, setCalendarid] = useState(
event?.calId
@@ -104,6 +107,24 @@ function EventPopover({
}
}, [selectedRange]);
useEffect(() => {
setTitle(event?.title ?? "");
setAttendees(
event?.attendee
? event.attendee.filter((a) => a.cal_address !== organizer?.cal_address)
: []
);
}, [event, organizer?.cal_address]);
const handleClose = () => {
onClose({}, "backdropClick"); // Reset
setTitle("");
setDescription("");
setAttendees([]);
setLocation("");
setCalendarid(0);
};
const handleSave = async () => {
const newEventUID = crypto.randomUUID();
@@ -142,7 +163,7 @@ function EventPopover({
newEvent.attendee = newEvent.attendee.concat(attendees);
}
dispatch(
await dispatch(
putEventAsync({
cal: userPersonnalCalendars[calendarid],
newEvent,
@@ -153,6 +174,7 @@ function EventPopover({
// Reset
setTitle("");
setDescription("");
setAttendees([]);
setLocation("");
setCalendarid(0);
};
@@ -161,7 +183,7 @@ function EventPopover({
<Popover
open={open}
anchorEl={anchorEl}
onClose={onClose}
onClose={handleClose}
anchorOrigin={{
vertical: "center",
horizontal: "center",
@@ -172,7 +194,7 @@ function EventPopover({
}}
>
<Card>
<CardHeader title={event ? "Duplicate Event" : "Create Event"} />
<CardHeader title={event?.uid ? "Duplicate Event" : "Create Event"} />
<CardContent
sx={{ maxHeight: "85vh", maxWidth: "40vw", overflow: "auto" }}
>
@@ -359,10 +381,7 @@ function EventPopover({
<CardActions>
<Box mt={2} display="flex" justifyContent="flex-end" gap={1}>
<Button
variant="outlined"
onClick={() => onClose({}, "backdropClick")}
>
<Button variant="outlined" onClick={handleClose}>
Cancel
</Button>
<Button size="small" onClick={() => setShowMore(!showMore)}>
+2 -8
View File
@@ -1,3 +1,4 @@
import { User } from "../../components/Attendees/PeopleSearch";
import { api } from "../../utils/apiUtils";
export async function getOpenPaasUser() {
@@ -8,14 +9,7 @@ export async function getOpenPaasUser() {
export async function searchUsers(
query: string,
objectTypes: string[] = ["user", "contact"]
): Promise<
{
email: string;
displayName: string;
avatarUrl: string;
openpaasId: string;
}[]
> {
): Promise<User[]> {
const response: any[] = await api
.post(`api/people/search`, {
body: JSON.stringify({