@@ -2,9 +2,9 @@ import {
|
|||||||
hasFreeBusyConflict,
|
hasFreeBusyConflict,
|
||||||
useAttendeesFreeBusy
|
useAttendeesFreeBusy
|
||||||
} from '@/components/Attendees/useFreeBusy'
|
} from '@/components/Attendees/useFreeBusy'
|
||||||
import * as getFreeBusyREPORT from '@/features/Events/api/getFreeBusyForAddedAttendeesREPORT'
|
import * as CalendarApi from '@/features/Calendars/CalendarApi'
|
||||||
import * as getFreeBusyPOST from '@/features/Events/api/getFreeBusyForEventAttendeesPOST'
|
import * as FreeBusyDao from '@/features/Events/FreeBusyDao'
|
||||||
import * as getUserData from '@/features/Events/api/getUserDataFromEmail'
|
import * as UserDao from '@/features/User/UserDao'
|
||||||
import { renderHook, waitFor } from '@testing-library/react'
|
import { renderHook, waitFor } from '@testing-library/react'
|
||||||
|
|
||||||
jest.mock('moment-timezone', () => {
|
jest.mock('moment-timezone', () => {
|
||||||
@@ -12,26 +12,130 @@ jest.mock('moment-timezone', () => {
|
|||||||
return actual
|
return actual
|
||||||
})
|
})
|
||||||
|
|
||||||
jest.mock('@/features/Events/api/getUserDataFromEmail')
|
jest.mock('@/features/Events/FreeBusyDao')
|
||||||
jest.mock('@/features/Events/api/getFreeBusyForAddedAttendeesREPORT')
|
jest.mock('@/features/User/UserDao')
|
||||||
jest.mock('@/features/Events/api/getFreeBusyForEventAttendeesPOST')
|
jest.mock('@/features/Calendars/CalendarApi')
|
||||||
|
|
||||||
const mockGetUserData = getUserData.getUserDataFromEmail as jest.MockedFunction<
|
const mockGetUserData = UserDao.fetchUserByEmail as jest.MockedFunction<
|
||||||
typeof getUserData.getUserDataFromEmail
|
typeof UserDao.fetchUserByEmail
|
||||||
>
|
>
|
||||||
const mockREPORT =
|
const mockREPORT = FreeBusyDao.fetchFreeBusyReports as jest.MockedFunction<
|
||||||
getFreeBusyREPORT.getFreeBusyForAddedAttendeesREPORT as jest.MockedFunction<
|
typeof FreeBusyDao.fetchFreeBusyReports
|
||||||
typeof getFreeBusyREPORT.getFreeBusyForAddedAttendeesREPORT
|
>
|
||||||
>
|
const mockPOST = FreeBusyDao.fetchFreeBusyPost as jest.MockedFunction<
|
||||||
const mockPOST =
|
typeof FreeBusyDao.fetchFreeBusyPost
|
||||||
getFreeBusyPOST.getFreeBusyForEventAttendeesPOST as jest.MockedFunction<
|
>
|
||||||
typeof getFreeBusyPOST.getFreeBusyForEventAttendeesPOST
|
const mockGetCalendars = CalendarApi.getCalendars as jest.MockedFunction<
|
||||||
>
|
typeof CalendarApi.getCalendars
|
||||||
|
>
|
||||||
|
|
||||||
|
const mockCalendarsResponse = {
|
||||||
|
_embedded: {
|
||||||
|
'dav:calendar': [
|
||||||
|
{
|
||||||
|
_links: {
|
||||||
|
self: {
|
||||||
|
href: '/calendars/user-carol/user-carol'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const freeIcal = [
|
||||||
|
{
|
||||||
|
_links: {
|
||||||
|
self: {
|
||||||
|
href: '\/calendars\/user-carol\/e38d1ea7-6d2c-46c6-8933-cdb325d4458e'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
data: [
|
||||||
|
'vcalendar',
|
||||||
|
[],
|
||||||
|
[
|
||||||
|
[
|
||||||
|
'vfreebusy',
|
||||||
|
[
|
||||||
|
['dtstart', {}, 'date-time', '2026-03-14T14:00:00Z'],
|
||||||
|
['dtend', {}, 'date-time', '2026-03-14T15:00:00Z'],
|
||||||
|
['dtstamp', {}, 'date-time', '2026-03-14T13:00:00Z']
|
||||||
|
],
|
||||||
|
[]
|
||||||
|
]
|
||||||
|
]
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
|
||||||
|
const busyIcal = [
|
||||||
|
{
|
||||||
|
_links: {
|
||||||
|
self: {
|
||||||
|
href: '\/calendars\/user-carol\/e38d1ea7-6d2c-46c6-8933-cdb325d4458e'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
data: [
|
||||||
|
'vcalendar',
|
||||||
|
[],
|
||||||
|
[
|
||||||
|
[
|
||||||
|
'vfreebusy',
|
||||||
|
[
|
||||||
|
['dtstart', {}, 'date-time', '2026-03-14T14:00:00Z'],
|
||||||
|
['dtend', {}, 'date-time', '2026-03-14T15:00:00Z'],
|
||||||
|
['dtstamp', {}, 'date-time', '2026-03-14T13:00:00Z'],
|
||||||
|
[
|
||||||
|
'freebusy',
|
||||||
|
{},
|
||||||
|
'period',
|
||||||
|
['2026-03-14T14:00:00Z', '2026-03-14T15:00:00Z']
|
||||||
|
]
|
||||||
|
],
|
||||||
|
[]
|
||||||
|
]
|
||||||
|
]
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
|
||||||
const START = '2026-03-14T14:00:00'
|
const START = '2026-03-14T14:00:00'
|
||||||
const END = '2026-03-14T15:00:00'
|
const END = '2026-03-14T15:00:00'
|
||||||
const TZ = 'Europe/Paris'
|
const TZ = 'Europe/Paris'
|
||||||
|
|
||||||
|
const freePostResponse = {
|
||||||
|
start: '20260314T130000',
|
||||||
|
end: '20260314T140000',
|
||||||
|
users: [
|
||||||
|
{
|
||||||
|
id: 'user-carol',
|
||||||
|
calendars: [{ id: 'user-carol', busy: [] }]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
const busyPostResponse = {
|
||||||
|
start: '20260314T130000',
|
||||||
|
end: '20260314T140000',
|
||||||
|
users: [
|
||||||
|
{
|
||||||
|
id: 'user-carol',
|
||||||
|
calendars: [
|
||||||
|
{
|
||||||
|
id: 'user-carol',
|
||||||
|
busy: [
|
||||||
|
{
|
||||||
|
uid: 'fa507111-07d5-44b4-a611-c27845a66ccf',
|
||||||
|
start: START,
|
||||||
|
end: END
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
describe('hasFreeBusyConflict', () => {
|
describe('hasFreeBusyConflict', () => {
|
||||||
it('returns false for non-vcalendar data', () => {
|
it('returns false for non-vcalendar data', () => {
|
||||||
expect(hasFreeBusyConflict({ data: ['not-vcalendar', [], []] })).toBe(false)
|
expect(hasFreeBusyConflict({ data: ['not-vcalendar', [], []] })).toBe(false)
|
||||||
@@ -82,13 +186,16 @@ describe('hasFreeBusyConflict', () => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
describe('useAttendeesFreeBusy — Flow B (new attendees)', () => {
|
describe('useAttendeesFreeBusy — Flow B (new attendees)', () => {
|
||||||
beforeEach(() => jest.clearAllMocks())
|
beforeEach(() => {
|
||||||
|
jest.clearAllMocks()
|
||||||
|
mockGetCalendars.mockResolvedValue(mockCalendarsResponse as never)
|
||||||
|
})
|
||||||
|
|
||||||
const newAttendee = { email: 'alice@example.com', userId: 'user-alice' }
|
const newAttendee = { email: 'alice@example.com', userId: 'user-alice' }
|
||||||
|
|
||||||
it('shows loading then free for a new attendee', async () => {
|
it('shows loading then free for a new attendee', async () => {
|
||||||
mockREPORT.mockResolvedValue(false)
|
mockGetCalendars.mockResolvedValue(mockCalendarsResponse as never)
|
||||||
|
mockREPORT.mockResolvedValue(freeIcal)
|
||||||
const { result } = renderHook(() =>
|
const { result } = renderHook(() =>
|
||||||
useAttendeesFreeBusy({
|
useAttendeesFreeBusy({
|
||||||
existingAttendees: [],
|
existingAttendees: [],
|
||||||
@@ -105,8 +212,8 @@ describe('useAttendeesFreeBusy — Flow B (new attendees)', () => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
it('shows busy when REPORT returns true', async () => {
|
it('shows busy when REPORT returns true', async () => {
|
||||||
mockREPORT.mockResolvedValue(true)
|
mockGetCalendars.mockResolvedValue(mockCalendarsResponse as never)
|
||||||
|
mockREPORT.mockResolvedValue(busyIcal)
|
||||||
const { result } = renderHook(() =>
|
const { result } = renderHook(() =>
|
||||||
useAttendeesFreeBusy({
|
useAttendeesFreeBusy({
|
||||||
existingAttendees: [],
|
existingAttendees: [],
|
||||||
@@ -140,8 +247,20 @@ describe('useAttendeesFreeBusy — Flow B (new attendees)', () => {
|
|||||||
|
|
||||||
it('resolves userId via getUserDataFromEmail when not provided', async () => {
|
it('resolves userId via getUserDataFromEmail when not provided', async () => {
|
||||||
mockGetUserData.mockResolvedValue([{ _id: 'resolved-id' }] as never)
|
mockGetUserData.mockResolvedValue([{ _id: 'resolved-id' }] as never)
|
||||||
mockREPORT.mockResolvedValue(false)
|
mockGetCalendars.mockResolvedValue({
|
||||||
|
_embedded: {
|
||||||
|
'dav:calendar': [
|
||||||
|
{
|
||||||
|
_links: {
|
||||||
|
self: {
|
||||||
|
href: '/calendars/resolved-id/resolved-id'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
} as never)
|
||||||
|
mockREPORT.mockResolvedValue(freeIcal)
|
||||||
const attendeeWithoutId = { email: 'bob@example.com' }
|
const attendeeWithoutId = { email: 'bob@example.com' }
|
||||||
|
|
||||||
const { result } = renderHook(() =>
|
const { result } = renderHook(() =>
|
||||||
@@ -159,9 +278,11 @@ describe('useAttendeesFreeBusy — Flow B (new attendees)', () => {
|
|||||||
)
|
)
|
||||||
expect(mockGetUserData).toHaveBeenCalledWith('bob@example.com')
|
expect(mockGetUserData).toHaveBeenCalledWith('bob@example.com')
|
||||||
expect(mockREPORT).toHaveBeenCalledWith(
|
expect(mockREPORT).toHaveBeenCalledWith(
|
||||||
'resolved-id',
|
expect.objectContaining({
|
||||||
expect.any(String),
|
hrefs: ['/calendars/resolved-id/resolved-id'],
|
||||||
expect.any(String)
|
start: expect.any(String),
|
||||||
|
end: expect.any(String)
|
||||||
|
})
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -185,8 +306,8 @@ describe('useAttendeesFreeBusy — Flow B (new attendees)', () => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
it('does not re-fetch an attendee already fetched', async () => {
|
it('does not re-fetch an attendee already fetched', async () => {
|
||||||
mockREPORT.mockResolvedValue(false)
|
mockGetCalendars.mockResolvedValue(mockCalendarsResponse as never)
|
||||||
|
mockREPORT.mockResolvedValue(freeIcal)
|
||||||
const { result, rerender } = renderHook(
|
const { result, rerender } = renderHook(
|
||||||
({ attendees }) =>
|
({ attendees }) =>
|
||||||
useAttendeesFreeBusy({
|
useAttendeesFreeBusy({
|
||||||
@@ -206,8 +327,8 @@ describe('useAttendeesFreeBusy — Flow B (new attendees)', () => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
it('removes departed attendee from the map', async () => {
|
it('removes departed attendee from the map', async () => {
|
||||||
mockREPORT.mockResolvedValue(false)
|
mockGetCalendars.mockResolvedValue(mockCalendarsResponse as never)
|
||||||
|
mockREPORT.mockResolvedValue(freeIcal)
|
||||||
const { result, rerender } = renderHook(
|
const { result, rerender } = renderHook(
|
||||||
({ attendees }) =>
|
({ attendees }) =>
|
||||||
useAttendeesFreeBusy({
|
useAttendeesFreeBusy({
|
||||||
@@ -230,8 +351,8 @@ describe('useAttendeesFreeBusy — Flow B (new attendees)', () => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
it('invalidates cache and re-fetches when time window changes', async () => {
|
it('invalidates cache and re-fetches when time window changes', async () => {
|
||||||
mockREPORT.mockResolvedValue(false)
|
mockGetCalendars.mockResolvedValue(mockCalendarsResponse as never)
|
||||||
|
mockREPORT.mockResolvedValue(freeIcal)
|
||||||
const { result, rerender } = renderHook(
|
const { result, rerender } = renderHook(
|
||||||
({ start, end }) =>
|
({ start, end }) =>
|
||||||
useAttendeesFreeBusy({
|
useAttendeesFreeBusy({
|
||||||
@@ -254,8 +375,8 @@ describe('useAttendeesFreeBusy — Flow B (new attendees)', () => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
it('passes UTC-converted iCal times to the API', async () => {
|
it('passes UTC-converted iCal times to the API', async () => {
|
||||||
mockREPORT.mockResolvedValue(false)
|
mockGetCalendars.mockResolvedValue(mockCalendarsResponse as never)
|
||||||
|
mockREPORT.mockResolvedValue(freeIcal)
|
||||||
renderHook(() =>
|
renderHook(() =>
|
||||||
useAttendeesFreeBusy({
|
useAttendeesFreeBusy({
|
||||||
existingAttendees: [],
|
existingAttendees: [],
|
||||||
@@ -270,9 +391,10 @@ describe('useAttendeesFreeBusy — Flow B (new attendees)', () => {
|
|||||||
|
|
||||||
// Paris is UTC+1 in March (before DST), so 16:00 → 15:00 UTC
|
// Paris is UTC+1 in March (before DST), so 16:00 → 15:00 UTC
|
||||||
expect(mockREPORT).toHaveBeenCalledWith(
|
expect(mockREPORT).toHaveBeenCalledWith(
|
||||||
'user-alice',
|
expect.objectContaining({
|
||||||
'20260314T150000',
|
start: '20260314T150000',
|
||||||
'20260314T160000'
|
end: '20260314T160000'
|
||||||
|
})
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
@@ -297,7 +419,7 @@ describe('useAttendeesFreeBusy — Flow A (existing attendees)', () => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
it('shows loading then free for existing attendees', async () => {
|
it('shows loading then free for existing attendees', async () => {
|
||||||
mockPOST.mockResolvedValue({ 'user-carol': false })
|
mockPOST.mockResolvedValue(freePostResponse)
|
||||||
|
|
||||||
const { result } = renderHook(() =>
|
const { result } = renderHook(() =>
|
||||||
useAttendeesFreeBusy({
|
useAttendeesFreeBusy({
|
||||||
@@ -318,7 +440,7 @@ describe('useAttendeesFreeBusy — Flow A (existing attendees)', () => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
it('shows busy when POST returns busy for userId', async () => {
|
it('shows busy when POST returns busy for userId', async () => {
|
||||||
mockPOST.mockResolvedValue({ 'user-carol': true })
|
mockPOST.mockResolvedValue(busyPostResponse)
|
||||||
|
|
||||||
const { result } = renderHook(() =>
|
const { result } = renderHook(() =>
|
||||||
useAttendeesFreeBusy({
|
useAttendeesFreeBusy({
|
||||||
@@ -331,9 +453,9 @@ describe('useAttendeesFreeBusy — Flow A (existing attendees)', () => {
|
|||||||
})
|
})
|
||||||
)
|
)
|
||||||
|
|
||||||
await waitFor(() =>
|
await waitFor(() => {
|
||||||
expect(result.current[existingAttendee.email]).toBe('busy')
|
expect(result.current[existingAttendee.email]).toBe('busy')
|
||||||
)
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
it('shows unknown when POST throws', async () => {
|
it('shows unknown when POST throws', async () => {
|
||||||
@@ -356,7 +478,7 @@ describe('useAttendeesFreeBusy — Flow A (existing attendees)', () => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
it('passes the eventUid to the POST API', async () => {
|
it('passes the eventUid to the POST API', async () => {
|
||||||
mockPOST.mockResolvedValue({ 'user-carol': false })
|
mockPOST.mockResolvedValue(freePostResponse)
|
||||||
|
|
||||||
renderHook(() =>
|
renderHook(() =>
|
||||||
useAttendeesFreeBusy({
|
useAttendeesFreeBusy({
|
||||||
@@ -370,12 +492,12 @@ describe('useAttendeesFreeBusy — Flow A (existing attendees)', () => {
|
|||||||
)
|
)
|
||||||
|
|
||||||
await waitFor(() => expect(mockPOST).toHaveBeenCalled())
|
await waitFor(() => expect(mockPOST).toHaveBeenCalled())
|
||||||
expect(mockPOST).toHaveBeenCalledWith(
|
expect(mockPOST).toHaveBeenCalledWith({
|
||||||
['user-carol'],
|
end: '20260314T140000',
|
||||||
expect.any(String),
|
eventUid: 'event-abc',
|
||||||
expect.any(String),
|
start: '20260314T130000',
|
||||||
'event-abc'
|
userIds: ['user-carol']
|
||||||
)
|
})
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,8 @@
|
|||||||
import { getFreeBusyForAddedAttendeesREPORT } from '@/features/Events/api/getFreeBusyForAddedAttendeesREPORT'
|
import {
|
||||||
import { getFreeBusyForEventAttendeesPOST } from '@/features/Events/api/getFreeBusyForEventAttendeesPOST'
|
getFreeBusyForAddedAttendee,
|
||||||
import { getUserDataFromEmail } from '@/features/Events/api/getUserDataFromEmail'
|
getFreeBusyForEventAttendees
|
||||||
|
} from '@/features/Events/FreeBusyApi'
|
||||||
|
import { getUserDataFromEmail } from '@/features/User/userAPI'
|
||||||
import moment from 'moment-timezone'
|
import moment from 'moment-timezone'
|
||||||
import { useEffect, useRef, useState } from 'react'
|
import { useEffect, useRef, useState } from 'react'
|
||||||
|
|
||||||
@@ -150,7 +152,7 @@ export function useAttendeesFreeBusy({
|
|||||||
setStatusMap(prev => ({ ...prev, ...toLoadingMap(existingAttendees) }))
|
setStatusMap(prev => ({ ...prev, ...toLoadingMap(existingAttendees) }))
|
||||||
|
|
||||||
fetchFreeBusyMap(existingAttendees, resolved =>
|
fetchFreeBusyMap(existingAttendees, resolved =>
|
||||||
getFreeBusyForEventAttendeesPOST(
|
getFreeBusyForEventAttendees(
|
||||||
resolved.map(r => r.userId),
|
resolved.map(r => r.userId),
|
||||||
toUtcIcal(start, timezone),
|
toUtcIcal(start, timezone),
|
||||||
toUtcIcal(end, timezone),
|
toUtcIcal(end, timezone),
|
||||||
@@ -208,7 +210,7 @@ export function useAttendeesFreeBusy({
|
|||||||
Promise.all(
|
Promise.all(
|
||||||
resolved.map(async ({ email, userId }) => {
|
resolved.map(async ({ email, userId }) => {
|
||||||
try {
|
try {
|
||||||
const busy = await getFreeBusyForAddedAttendeesREPORT(
|
const busy = await getFreeBusyForAddedAttendee(
|
||||||
userId,
|
userId,
|
||||||
moment.tz(start, timezone).utc().format('YYYYMMDDTHHmmss'),
|
moment.tz(start, timezone).utc().format('YYYYMMDDTHHmmss'),
|
||||||
moment.tz(end, timezone).utc().format('YYYYMMDDTHHmmss')
|
moment.tz(end, timezone).utc().format('YYYYMMDDTHHmmss')
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ import { SnackbarAlert } from '@/components/Loading/SnackBarAlert'
|
|||||||
import moment from 'moment-timezone'
|
import moment from 'moment-timezone'
|
||||||
import { useEffect, useState } from 'react'
|
import { useEffect, useState } from 'react'
|
||||||
import { useI18n } from 'twake-i18n'
|
import { useI18n } from 'twake-i18n'
|
||||||
import { postCounterProposal } from '../api/sendCounterProposal'
|
import { postCounterProposal } from '../EventApi'
|
||||||
import { EventTimeSubtitle } from '../EventPreview/EventTimeSubtitle'
|
import { EventTimeSubtitle } from '../EventPreview/EventTimeSubtitle'
|
||||||
import { ContextualizedEvent } from '../EventsTypes'
|
import { ContextualizedEvent } from '../EventsTypes'
|
||||||
|
|
||||||
|
|||||||
+190
-161
@@ -1,4 +1,4 @@
|
|||||||
import { api } from '@/utils/apiUtils'
|
import { extractEventBaseUuid } from '@/utils/extractEventBaseUuid'
|
||||||
import { convertEventDateTimeToISO, resolveTimezoneId } from '@/utils/timezone'
|
import { convertEventDateTimeToISO, resolveTimezoneId } from '@/utils/timezone'
|
||||||
import { TIMEZONES } from '@/utils/timezone-data'
|
import { TIMEZONES } from '@/utils/timezone-data'
|
||||||
import ICAL from 'ical.js'
|
import ICAL from 'ical.js'
|
||||||
@@ -10,6 +10,19 @@ import {
|
|||||||
VObjectValue
|
VObjectValue
|
||||||
} from '../Calendars/types/CalendarData'
|
} from '../Calendars/types/CalendarData'
|
||||||
import { SearchEventsResponse } from '../Search/types/SearchEventsResponse'
|
import { SearchEventsResponse } from '../Search/types/SearchEventsResponse'
|
||||||
|
import {
|
||||||
|
CounterProposalPayload,
|
||||||
|
deleteEventRaw,
|
||||||
|
fetchAllRecurrentVevents,
|
||||||
|
fetchEventIcs,
|
||||||
|
fetchEventRaw,
|
||||||
|
importEventRaw,
|
||||||
|
moveEventRaw,
|
||||||
|
postCounterProposalRaw,
|
||||||
|
putEventRaw,
|
||||||
|
reportEventRaw,
|
||||||
|
searchEventRaw
|
||||||
|
} from './EventDao'
|
||||||
import { CalendarEvent } from './EventsTypes'
|
import { CalendarEvent } from './EventsTypes'
|
||||||
import {
|
import {
|
||||||
calendarEventToJCal,
|
calendarEventToJCal,
|
||||||
@@ -22,36 +35,30 @@ export async function reportEvent(
|
|||||||
event: CalendarEvent,
|
event: CalendarEvent,
|
||||||
match: { start: string; end: string }
|
match: { start: string; end: string }
|
||||||
): Promise<CalDavItem> {
|
): Promise<CalDavItem> {
|
||||||
const response = await api(`dav${event.URL}`, {
|
return reportEventRaw(event, match)
|
||||||
method: 'REPORT',
|
|
||||||
body: JSON.stringify({ match }),
|
|
||||||
headers: { Accept: 'application/json' }
|
|
||||||
})
|
|
||||||
if (!response.ok) {
|
|
||||||
throw new Error(`REPORT request failed with status ${response.status}`)
|
|
||||||
}
|
|
||||||
const eventData: CalDavItem = await response.json()
|
|
||||||
return eventData
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function getEvent(event: CalendarEvent, isMaster?: boolean) {
|
export async function getEvent(
|
||||||
const response = await api.get(`dav${event.URL}`)
|
event: CalendarEvent,
|
||||||
const eventData = await response.text()
|
isMaster?: boolean
|
||||||
|
): Promise<CalendarEvent> {
|
||||||
|
const eventData = await fetchEventRaw(event)
|
||||||
|
|
||||||
const eventical = ICAL.parse(eventData)
|
const eventical = ICAL.parse(eventData) as VCalComponent
|
||||||
const vevents = (eventical[2] || []).filter(
|
const vevents = (eventical[2] ?? []).filter(
|
||||||
([name]: [string]) => name.toLowerCase() === 'vevent'
|
([name]) => name.toLowerCase() === 'vevent'
|
||||||
|
)
|
||||||
|
const vtimezones = (eventical[2] ?? []).filter(
|
||||||
|
([name]) => name.toLowerCase() === 'vtimezone'
|
||||||
)
|
)
|
||||||
|
|
||||||
const vtimezones = (eventical[2] || []).filter(
|
let targetVevent: VCalComponent | undefined
|
||||||
([name]: [string]) => name.toLowerCase() === 'vtimezone'
|
|
||||||
)
|
|
||||||
|
|
||||||
let targetVevent
|
|
||||||
if (isMaster) {
|
if (isMaster) {
|
||||||
targetVevent = vevents.find(
|
targetVevent = vevents.find(
|
||||||
([, props]: VCalComponent) =>
|
([, props]) =>
|
||||||
!props.find(([k]) => k.toLowerCase() === 'recurrence-id')
|
!(props as VObjectProperty[]).find(
|
||||||
|
([k]) => k.toLowerCase() === 'recurrence-id'
|
||||||
|
)
|
||||||
)
|
)
|
||||||
if (!targetVevent) {
|
if (!targetVevent) {
|
||||||
targetVevent = vevents[0]
|
targetVevent = vevents[0]
|
||||||
@@ -63,11 +70,11 @@ export async function getEvent(event: CalendarEvent, isMaster?: boolean) {
|
|||||||
let timezoneFromVTimezone: string | undefined
|
let timezoneFromVTimezone: string | undefined
|
||||||
if (vtimezones.length > 0) {
|
if (vtimezones.length > 0) {
|
||||||
const vtimezone = vtimezones[0]
|
const vtimezone = vtimezones[0]
|
||||||
const tzidProp = vtimezone[1]?.find(
|
const tzidProp = (vtimezone[1] as VObjectProperty[]).find(
|
||||||
([k]: string[]) => k.toLowerCase() === 'tzid'
|
([k]) => k.toLowerCase() === 'tzid'
|
||||||
)
|
)
|
||||||
if (tzidProp && tzidProp[3]) {
|
if (tzidProp?.[3]) {
|
||||||
const resolvedTz = resolveTimezoneId(tzidProp[3])
|
const resolvedTz = resolveTimezoneId(tzidProp[3] as string)
|
||||||
if (resolvedTz) {
|
if (resolvedTz) {
|
||||||
timezoneFromVTimezone = resolvedTz
|
timezoneFromVTimezone = resolvedTz
|
||||||
}
|
}
|
||||||
@@ -75,47 +82,41 @@ export async function getEvent(event: CalendarEvent, isMaster?: boolean) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
let timezoneFromDTSTART: string | undefined
|
let timezoneFromDTSTART: string | undefined
|
||||||
const dtstartProp = targetVevent[1]?.find(
|
const dtstartProp = (targetVevent[1] as VObjectProperty[]).find(
|
||||||
([k]: string[]) => k.toLowerCase() === 'dtstart'
|
([k]) => k.toLowerCase() === 'dtstart'
|
||||||
)
|
)
|
||||||
if (dtstartProp) {
|
const dtstartParams = dtstartProp?.[1] as Record<string, string>
|
||||||
const dtstartParams = dtstartProp[1]
|
const dtstartValue = dtstartProp?.[3]
|
||||||
const dtstartValue = dtstartProp[3]
|
const tzParam =
|
||||||
if (dtstartParams) {
|
dtstartParams?.['tzid'] ??
|
||||||
const tzParam =
|
dtstartParams?.['TZID'] ??
|
||||||
dtstartParams.tzid ||
|
dtstartParams?.['Tzid'] ??
|
||||||
dtstartParams.TZID ||
|
dtstartParams?.['tZid'] ??
|
||||||
dtstartParams.Tzid ||
|
dtstartParams?.['tzId']
|
||||||
dtstartParams.tZid ||
|
|
||||||
dtstartParams.tzId
|
timezoneFromDTSTART = resolveTimezoneId(tzParam)
|
||||||
if (tzParam) {
|
|
||||||
const resolvedTz = resolveTimezoneId(tzParam)
|
if (
|
||||||
if (resolvedTz) {
|
!timezoneFromDTSTART &&
|
||||||
timezoneFromDTSTART = resolvedTz
|
typeof dtstartValue === 'string' &&
|
||||||
}
|
dtstartValue.endsWith('Z')
|
||||||
}
|
) {
|
||||||
}
|
timezoneFromDTSTART = 'Etc/UTC'
|
||||||
if (
|
|
||||||
!timezoneFromDTSTART &&
|
|
||||||
typeof dtstartValue === 'string' &&
|
|
||||||
dtstartValue.endsWith('Z')
|
|
||||||
) {
|
|
||||||
timezoneFromDTSTART = 'Etc/UTC'
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const eventjson = parseCalendarEvent(
|
const eventjson = parseCalendarEvent(
|
||||||
targetVevent[1],
|
targetVevent[1] as VObjectProperty[],
|
||||||
event.color ?? {},
|
event.color ?? {},
|
||||||
{ id: event?.calId } as Calendar,
|
{ id: event?.calId } as Calendar,
|
||||||
event.URL
|
event.URL
|
||||||
)
|
)
|
||||||
|
|
||||||
const finalTimezone =
|
const finalTimezone =
|
||||||
timezoneFromVTimezone ||
|
timezoneFromVTimezone ??
|
||||||
timezoneFromDTSTART ||
|
timezoneFromDTSTART ??
|
||||||
eventjson.timezone ||
|
eventjson.timezone ??
|
||||||
'Etc/UTC'
|
'Etc/UTC'
|
||||||
|
|
||||||
eventjson.timezone = finalTimezone
|
eventjson.timezone = finalTimezone
|
||||||
|
|
||||||
if (!eventjson.allday && eventjson.start && finalTimezone) {
|
if (!eventjson.allday && eventjson.start && finalTimezone) {
|
||||||
@@ -132,31 +133,22 @@ export async function getEvent(event: CalendarEvent, isMaster?: boolean) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (isMaster) {
|
|
||||||
const merged = { ...event, ...eventjson }
|
|
||||||
merged.timezone = finalTimezone
|
|
||||||
return merged
|
|
||||||
}
|
|
||||||
const merged = { ...event, ...eventjson }
|
const merged = { ...event, ...eventjson }
|
||||||
merged.timezone = finalTimezone
|
merged.timezone = finalTimezone
|
||||||
return merged
|
return merged
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function dlEvent(event: CalendarEvent) {
|
export async function dlEvent(event: CalendarEvent): Promise<string> {
|
||||||
const response = await api.get(`dav${event.URL}?export=`)
|
const response = await fetchEventIcs(event)
|
||||||
const eventData = await response.text()
|
return response
|
||||||
|
|
||||||
return eventData
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function putEvent(event: CalendarEvent, calOwnerEmail?: string) {
|
export async function putEvent(
|
||||||
const response = await api(`dav${event.URL}`, {
|
event: CalendarEvent,
|
||||||
method: 'PUT',
|
calOwnerEmail?: string
|
||||||
body: JSON.stringify(calendarEventToJCal(event, calOwnerEmail)),
|
): Promise<Response> {
|
||||||
headers: {
|
const jCal = calendarEventToJCal(event, calOwnerEmail)
|
||||||
'content-type': 'text/calendar; charset=utf-8'
|
const response = await putEventRaw(event, jCal)
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
if (response.status === 201) {
|
if (response.status === 201) {
|
||||||
console.info('Event created successfully:', response.url || event.URL)
|
console.info('Event created successfully:', response.url || event.URL)
|
||||||
@@ -168,8 +160,8 @@ export async function putEvent(event: CalendarEvent, calOwnerEmail?: string) {
|
|||||||
export async function putEventWithOverrides(
|
export async function putEventWithOverrides(
|
||||||
updatedEvent: CalendarEvent,
|
updatedEvent: CalendarEvent,
|
||||||
calOwnerEmail?: string
|
calOwnerEmail?: string
|
||||||
) {
|
): Promise<Response> {
|
||||||
const vevents = await getAllRecurrentEvent(updatedEvent)
|
const vevents = await fetchAllRecurrentVevents(updatedEvent)
|
||||||
|
|
||||||
const updatedVevent = makeVevent(
|
const updatedVevent = makeVevent(
|
||||||
updatedEvent,
|
updatedEvent,
|
||||||
@@ -180,61 +172,64 @@ export async function putEventWithOverrides(
|
|||||||
let replaced = false
|
let replaced = false
|
||||||
for (let i = 0; i < vevents.length; i++) {
|
for (let i = 0; i < vevents.length; i++) {
|
||||||
const ve = vevents[i]
|
const ve = vevents[i]
|
||||||
const recurrenceId = ve[1].find(([k]: string[]) => k === 'recurrence-id')
|
const recurrenceId = (ve[1] as VObjectProperty[]).find(
|
||||||
|
([k]) => k === 'recurrence-id'
|
||||||
|
)
|
||||||
if (recurrenceId && recurrenceId[3] === updatedEvent.recurrenceId) {
|
if (recurrenceId && recurrenceId[3] === updatedEvent.recurrenceId) {
|
||||||
vevents[i] = updatedVevent // replace
|
vevents[i] = updatedVevent as VCalComponent // replace
|
||||||
replaced = true
|
replaced = true
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (!replaced && updatedEvent.recurrenceId) {
|
if (!replaced && updatedEvent.recurrenceId) {
|
||||||
vevents.push(updatedVevent) // add new override
|
vevents.push(updatedVevent as VCalComponent) // add new override
|
||||||
}
|
}
|
||||||
|
|
||||||
const timezoneData = TIMEZONES.zones[updatedEvent.timezone]
|
const timezoneData = TIMEZONES.zones[updatedEvent.timezone]
|
||||||
const vtimezone = makeTimezone(timezoneData, updatedEvent)
|
const vtimezone = makeTimezone(timezoneData, updatedEvent)
|
||||||
|
|
||||||
const newJCal = ['vcalendar', [], [...vevents, vtimezone.component.jCal]]
|
const newJCal = ['vcalendar', [], [...vevents, vtimezone.component.jCal]]
|
||||||
|
return putEventRaw(updatedEvent, newJCal)
|
||||||
return api(`dav${updatedEvent.URL}`, {
|
|
||||||
method: 'PUT',
|
|
||||||
body: JSON.stringify(newJCal),
|
|
||||||
headers: {
|
|
||||||
'content-type': 'text/calendar; charset=utf-8'
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export const deleteEventInstance = async (event: CalendarEvent) => {
|
export const deleteEventInstance = async (
|
||||||
|
event: CalendarEvent
|
||||||
|
): Promise<Response> => {
|
||||||
// Get all VEVENTs (master + overrides) from the series
|
// Get all VEVENTs (master + overrides) from the series
|
||||||
const vevents = await getAllRecurrentEvent(event)
|
const vevents = await fetchAllRecurrentVevents(event)
|
||||||
|
|
||||||
// Find the master VEVENT
|
// Find the master VEVENT
|
||||||
const masterIndex = vevents.findIndex(
|
const masterIndex = vevents.findIndex(
|
||||||
([, props]: VCalComponent) =>
|
([, props]) =>
|
||||||
!props.find(([k]) => k.toLowerCase() === 'recurrence-id')
|
!(props as VObjectProperty[]).find(
|
||||||
|
([k]) => k.toLowerCase() === 'recurrence-id'
|
||||||
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
if (masterIndex === -1) {
|
if (masterIndex === -1) {
|
||||||
throw new Error('No master VEVENT found for this series')
|
throw new Error('No master VEVENT found for this series')
|
||||||
}
|
}
|
||||||
|
|
||||||
const exdateValue = event.recurrenceId || event.start
|
const exdateValue = event.recurrenceId ?? event.start
|
||||||
const seriesEvent = parseCalendarEvent(
|
const seriesEvent = parseCalendarEvent(
|
||||||
vevents[masterIndex][1],
|
vevents[masterIndex][1] as VObjectProperty[],
|
||||||
{},
|
{},
|
||||||
{ id: event.calId } as Calendar,
|
{ id: event.calId } as Calendar,
|
||||||
''
|
''
|
||||||
)
|
)
|
||||||
const masterProps = vevents[masterIndex][1]
|
const masterProps = vevents[masterIndex][1] as VObjectProperty[]
|
||||||
|
|
||||||
|
const normalizeRecurrenceId = (id: VObjectValue): string =>
|
||||||
|
(typeof id === 'string' || typeof id === 'number'
|
||||||
|
? String(id)
|
||||||
|
: ''
|
||||||
|
).replace(/Z$/, '')
|
||||||
|
|
||||||
// Check if this date is already in EXDATE (avoid duplicates)
|
|
||||||
const normalizeRecurrenceId = (id: VObjectValue) =>
|
|
||||||
String(id ?? '').replace(/Z$/, '')
|
|
||||||
const isDuplicate = masterProps.some((prop: VObjectProperty) => {
|
const isDuplicate = masterProps.some((prop: VObjectProperty) => {
|
||||||
if (prop[0].toLowerCase() === 'exdate' && prop[3]) {
|
if (prop[0].toLowerCase() === 'exdate' && prop[3]) {
|
||||||
return (
|
return (
|
||||||
normalizeRecurrenceId(prop[3]) === normalizeRecurrenceId(exdateValue)
|
normalizeRecurrenceId(prop[3]) ===
|
||||||
|
normalizeRecurrenceId(exdateValue as VObjectValue)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
return false
|
return false
|
||||||
@@ -250,14 +245,14 @@ export const deleteEventInstance = async (event: CalendarEvent) => {
|
|||||||
vevents[masterIndex][1] = masterProps
|
vevents[masterIndex][1] = masterProps
|
||||||
|
|
||||||
// Remove the override instance if it exists (in case it was an override being deleted)
|
// Remove the override instance if it exists (in case it was an override being deleted)
|
||||||
const filteredVevents = vevents.filter(([, props]: VCalComponent) => {
|
const filteredVevents = vevents.filter(([, props]) => {
|
||||||
const recurrenceIdProp = props.find(
|
const recurrenceIdProp = (props as VObjectProperty[]).find(
|
||||||
([k]) => k.toLowerCase() === 'recurrence-id'
|
([k]) => k.toLowerCase() === 'recurrence-id'
|
||||||
)
|
)
|
||||||
if (!recurrenceIdProp) return true // Keep master
|
if (!recurrenceIdProp) return true // Keep master
|
||||||
return (
|
return (
|
||||||
normalizeRecurrenceId(recurrenceIdProp[3]) !==
|
normalizeRecurrenceId(recurrenceIdProp[3]) !==
|
||||||
normalizeRecurrenceId(event.recurrenceId ?? '')
|
normalizeRecurrenceId((event.recurrenceId ?? '') as VObjectValue)
|
||||||
) // Remove matching override
|
) // Remove matching override
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -271,39 +266,33 @@ export const deleteEventInstance = async (event: CalendarEvent) => {
|
|||||||
[...filteredVevents, vtimezone.component.jCal]
|
[...filteredVevents, vtimezone.component.jCal]
|
||||||
]
|
]
|
||||||
|
|
||||||
return api(`dav${event.URL}`, {
|
return putEventRaw(event, newJCal)
|
||||||
method: 'PUT',
|
|
||||||
body: JSON.stringify(newJCal),
|
|
||||||
headers: {
|
|
||||||
'content-type': 'text/calendar; charset=utf-8'
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export const updateSeriesPartstat = async (
|
export const updateSeriesPartstat = async (
|
||||||
event: CalendarEvent,
|
event: CalendarEvent,
|
||||||
attendeeEmail: string,
|
attendeeEmail: string,
|
||||||
partstat: string
|
partstat: string
|
||||||
) => {
|
): Promise<Response> => {
|
||||||
const vevents = await getAllRecurrentEvent(event)
|
const vevents = await fetchAllRecurrentVevents(event)
|
||||||
|
|
||||||
// Update PARTSTAT in ALL VEVENTs (master + exceptions)
|
// Update PARTSTAT in ALL VEVENTs (master + exceptions)
|
||||||
const updatedVevents = vevents.map((vevent: VCalComponent) => {
|
const updatedVevents = vevents.map((vevent: VCalComponent) => {
|
||||||
const properties = vevent[1]
|
const properties = vevent[1] as VObjectProperty[]
|
||||||
const updatedProperties = properties.map((prop: VObjectProperty) => {
|
const updatedProperties = properties.map((prop: VObjectProperty) => {
|
||||||
// Find ATTENDEE properties
|
const calAddress = prop[3] as string
|
||||||
if (prop[0] === 'attendee') {
|
// Find ATTENDEE properties & Check if this is the target attendee
|
||||||
const calAddress = prop[3] as string
|
if (
|
||||||
// Check if this is the target attendee
|
prop[0] === 'attendee' &&
|
||||||
if (calAddress.toLowerCase().includes(attendeeEmail.toLowerCase())) {
|
calAddress.toLowerCase().includes(attendeeEmail.toLowerCase())
|
||||||
// Update PARTSTAT parameter
|
) {
|
||||||
const params = { ...prop[1], partstat: partstat }
|
// Update PARTSTAT parameter
|
||||||
return [prop[0], params, prop[2], prop[3]]
|
const params = { ...(prop[1] as Record<string, string>), partstat }
|
||||||
}
|
return [prop[0], params, prop[2], prop[3]] as VObjectProperty
|
||||||
}
|
}
|
||||||
return prop
|
return prop
|
||||||
})
|
})
|
||||||
return [vevent[0], updatedProperties, vevent[2]]
|
return [vevent[0], updatedProperties, vevent[2]] as VCalComponent
|
||||||
})
|
})
|
||||||
|
|
||||||
const timezoneData = TIMEZONES.zones[event.timezone]
|
const timezoneData = TIMEZONES.zones[event.timezone]
|
||||||
@@ -315,45 +304,31 @@ export const updateSeriesPartstat = async (
|
|||||||
[...updatedVevents, vtimezone.component.jCal]
|
[...updatedVevents, vtimezone.component.jCal]
|
||||||
]
|
]
|
||||||
|
|
||||||
return api(`dav${event.URL}`, {
|
return putEventRaw(event, newJCal)
|
||||||
method: 'PUT',
|
|
||||||
body: JSON.stringify(newJCal),
|
|
||||||
headers: {
|
|
||||||
'content-type': 'text/calendar; charset=utf-8'
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function getAllRecurrentEvent(event: CalendarEvent) {
|
export async function getAllRecurrentEvent(
|
||||||
const response = await api.get(`dav${event.URL}`)
|
event: CalendarEvent
|
||||||
const eventData = await response.text()
|
): Promise<VCalComponent[]> {
|
||||||
const jcal = ICAL.parse(eventData)
|
return fetchAllRecurrentVevents(event)
|
||||||
const vevents = jcal[2].filter(([name]: string[]) => name === 'vevent')
|
|
||||||
return vevents
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function moveEvent(event: CalendarEvent, newUrl: string) {
|
export async function moveEvent(
|
||||||
const response = await api(`dav${event.URL}`, {
|
event: CalendarEvent,
|
||||||
method: 'MOVE',
|
newUrl: string
|
||||||
headers: {
|
): Promise<Response> {
|
||||||
destination: newUrl
|
return moveEventRaw(event, newUrl)
|
||||||
}
|
|
||||||
})
|
|
||||||
return response
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function deleteEvent(eventURL: string) {
|
export async function deleteEvent(eventURL: string): Promise<Response> {
|
||||||
const response = await api(`dav${eventURL}`, {
|
return deleteEventRaw({ URL: eventURL } as CalendarEvent)
|
||||||
method: 'DELETE'
|
|
||||||
})
|
|
||||||
return response
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function importEventFromFile(id: string, calLink: string) {
|
export async function importEventFromFile(
|
||||||
const response = await api.post(`api/import`, {
|
id: string,
|
||||||
body: JSON.stringify({ fileId: id, target: calLink })
|
calLink: string
|
||||||
})
|
): Promise<Response> {
|
||||||
return response
|
return importEventRaw(id, calLink)
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function searchEvent(
|
export async function searchEvent(
|
||||||
@@ -385,11 +360,65 @@ export async function searchEvent(
|
|||||||
if (attendees.length) {
|
if (attendees.length) {
|
||||||
reqParam.attendees = attendees
|
reqParam.attendees = attendees
|
||||||
}
|
}
|
||||||
const response = await api
|
return searchEventRaw(reqParam)
|
||||||
.post('calendar/api/events/search?limit=30&offset=0', {
|
}
|
||||||
body: JSON.stringify(reqParam)
|
|
||||||
})
|
export async function postCounterProposal({
|
||||||
.json()
|
event,
|
||||||
|
senderEmail,
|
||||||
return response as SearchEventsResponse
|
recipientEmail,
|
||||||
|
proposedStart,
|
||||||
|
proposedEnd,
|
||||||
|
message
|
||||||
|
}: {
|
||||||
|
event: CalendarEvent
|
||||||
|
senderEmail: string
|
||||||
|
recipientEmail: string
|
||||||
|
proposedStart: string
|
||||||
|
proposedEnd: string
|
||||||
|
message?: string
|
||||||
|
}): Promise<Response> {
|
||||||
|
// Build the counter event with proposed dates
|
||||||
|
const counterEvent: CalendarEvent = {
|
||||||
|
...event,
|
||||||
|
start: proposedStart,
|
||||||
|
end: proposedEnd,
|
||||||
|
sequence: event.sequence ?? 0
|
||||||
|
}
|
||||||
|
// Build vevent jCal
|
||||||
|
const vevent = makeVevent(
|
||||||
|
counterEvent,
|
||||||
|
counterEvent.timezone,
|
||||||
|
senderEmail,
|
||||||
|
!event.recurrenceId
|
||||||
|
)
|
||||||
|
if (message) {
|
||||||
|
vevent[1].push(['comment', {}, 'text', message])
|
||||||
|
}
|
||||||
|
// Build vtimezone
|
||||||
|
const timezoneData = TIMEZONES.zones[counterEvent.timezone]
|
||||||
|
const vtimezone = makeTimezone(timezoneData, counterEvent)
|
||||||
|
|
||||||
|
// Assemble full vcalendar with METHOD:COUNTER
|
||||||
|
const jcal = [
|
||||||
|
'vcalendar',
|
||||||
|
[
|
||||||
|
['version', {}, 'text', '2.0'],
|
||||||
|
['prodid', {}, 'text', '-//OpenPaaS//OpenPaaS//EN'],
|
||||||
|
['method', {}, 'text', 'COUNTER']
|
||||||
|
],
|
||||||
|
[vevent, vtimezone.component.jCal]
|
||||||
|
]
|
||||||
|
|
||||||
|
// Serialize to raw ICS
|
||||||
|
const counterICS = new ICAL.Component(jcal).toString()
|
||||||
|
const payload: CounterProposalPayload = {
|
||||||
|
ical: counterICS,
|
||||||
|
sender: senderEmail,
|
||||||
|
recipient: recipientEmail,
|
||||||
|
uid: extractEventBaseUuid(event.uid),
|
||||||
|
sequence: counterEvent.sequence ?? 0,
|
||||||
|
method: 'COUNTER'
|
||||||
|
}
|
||||||
|
return postCounterProposalRaw(event, payload)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,123 @@
|
|||||||
|
import { api } from '@/utils/apiUtils'
|
||||||
|
import ICAL from 'ical.js'
|
||||||
|
import { CalDavItem } from '../Calendars/api/types'
|
||||||
|
import { VCalComponent } from '../Calendars/types/CalendarData'
|
||||||
|
import { SearchEventsResponse } from '../Search/types/SearchEventsResponse'
|
||||||
|
import { CalendarEvent } from './EventsTypes'
|
||||||
|
|
||||||
|
export async function reportEventRaw(
|
||||||
|
event: CalendarEvent,
|
||||||
|
match: { start: string; end: string }
|
||||||
|
): Promise<CalDavItem> {
|
||||||
|
const response = await api(`dav${event.URL}`, {
|
||||||
|
method: 'REPORT',
|
||||||
|
body: JSON.stringify({ match }),
|
||||||
|
headers: { Accept: 'application/json' }
|
||||||
|
})
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error(`REPORT request failed with status ${response.status}`)
|
||||||
|
}
|
||||||
|
return response.json()
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function fetchEventRaw(event: CalendarEvent): Promise<string> {
|
||||||
|
const response = await api.get(`dav${event.URL}`)
|
||||||
|
return response.text()
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function fetchEventIcs(event: CalendarEvent): Promise<string> {
|
||||||
|
const response = await api.get(`dav${event.URL}?export=`)
|
||||||
|
return response.text()
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function putEventRaw(
|
||||||
|
event: CalendarEvent,
|
||||||
|
jCal: unknown[]
|
||||||
|
): Promise<Response> {
|
||||||
|
return api(`dav${event.URL}`, {
|
||||||
|
method: 'PUT',
|
||||||
|
body: JSON.stringify(jCal),
|
||||||
|
headers: { 'content-type': 'text/calendar; charset=utf-8' }
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function deleteEventRaw(event: CalendarEvent): Promise<Response> {
|
||||||
|
return api(`dav${event.URL}`, { method: 'DELETE' })
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function moveEventRaw(
|
||||||
|
event: CalendarEvent,
|
||||||
|
toURL: string
|
||||||
|
): Promise<Response> {
|
||||||
|
return api(`dav${event.URL}`, {
|
||||||
|
method: 'MOVE',
|
||||||
|
headers: { destination: toURL }
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function importEventRaw(
|
||||||
|
id: string,
|
||||||
|
calLink: string
|
||||||
|
): Promise<Response> {
|
||||||
|
return api.post('api/import', {
|
||||||
|
body: JSON.stringify({ fileId: id, target: calLink })
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function searchEventRaw(reqParam: {
|
||||||
|
query: string
|
||||||
|
calendars: { calendarId: string; userId: string }[]
|
||||||
|
organizers?: string[]
|
||||||
|
attendees?: string[]
|
||||||
|
}): Promise<SearchEventsResponse> {
|
||||||
|
return api
|
||||||
|
.post('calendar/api/events/search?limit=30&offset=0', {
|
||||||
|
body: JSON.stringify(reqParam)
|
||||||
|
})
|
||||||
|
.json()
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fetches all VEVENTs (master + overrides) for a recurring series.
|
||||||
|
* Returns the raw jCal VEVENT components — no transformation applied.
|
||||||
|
*/
|
||||||
|
export async function fetchAllRecurrentVevents(
|
||||||
|
event: CalendarEvent
|
||||||
|
): Promise<VCalComponent[]> {
|
||||||
|
const response = await api.get(`dav${event.URL}`)
|
||||||
|
const eventData = await response.text()
|
||||||
|
const jcal = ICAL.parse(eventData) as VCalComponent
|
||||||
|
return (jcal[2] ?? []).filter(([name]) => name === 'vevent')
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* POSTs an iTIP COUNTER proposal for a calendar event.
|
||||||
|
* Accepts a pre-serialized ICS string and the envelope metadata.
|
||||||
|
*/
|
||||||
|
export interface CounterProposalPayload {
|
||||||
|
ical: string
|
||||||
|
sender: string
|
||||||
|
recipient: string
|
||||||
|
uid: string
|
||||||
|
sequence: number
|
||||||
|
method: 'COUNTER'
|
||||||
|
}
|
||||||
|
export async function postCounterProposalRaw(
|
||||||
|
event: CalendarEvent,
|
||||||
|
payload: CounterProposalPayload
|
||||||
|
): Promise<Response> {
|
||||||
|
const response = await api(`dav${event.URL}`, {
|
||||||
|
method: 'POST',
|
||||||
|
body: JSON.stringify(payload),
|
||||||
|
headers: {
|
||||||
|
accept: 'application/json,text/plain,*/*',
|
||||||
|
'content-type': 'application/calendar+json',
|
||||||
|
Prefer: 'return=representation',
|
||||||
|
'X-Http-Method-Override': 'ITIP'
|
||||||
|
}
|
||||||
|
})
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error(`postCounterProposal failed with status ${response.status}`)
|
||||||
|
}
|
||||||
|
return response
|
||||||
|
}
|
||||||
@@ -0,0 +1,66 @@
|
|||||||
|
import { hasFreeBusyConflict } from '@/components/Attendees/useFreeBusy'
|
||||||
|
import { extractCalendarHrefs } from '@/utils/extractCalendarHrefs'
|
||||||
|
import { extractEventBaseUuid } from '@/utils/extractEventBaseUuid'
|
||||||
|
import { getCalendars } from '../Calendars/CalendarApi'
|
||||||
|
import {
|
||||||
|
fetchFreeBusyPost,
|
||||||
|
fetchFreeBusyReports,
|
||||||
|
FreeBusyPostQuery
|
||||||
|
} from './FreeBusyDao'
|
||||||
|
|
||||||
|
export async function getFreeBusyForAddedAttendee(
|
||||||
|
userId: string,
|
||||||
|
start: string,
|
||||||
|
end: string
|
||||||
|
): Promise<boolean> {
|
||||||
|
const calendars = await getCalendars(
|
||||||
|
userId,
|
||||||
|
'withFreeBusy=true&withRights=true'
|
||||||
|
)
|
||||||
|
const hrefs = extractCalendarHrefs(calendars)
|
||||||
|
if (hrefs.length === 0) return false
|
||||||
|
|
||||||
|
const results = await fetchFreeBusyReports({ hrefs, start, end })
|
||||||
|
return results.some(data => (data ? hasFreeBusyConflict(data) : false))
|
||||||
|
}
|
||||||
|
|
||||||
|
interface BusySlot {
|
||||||
|
uid: string
|
||||||
|
start: string
|
||||||
|
end: string
|
||||||
|
}
|
||||||
|
|
||||||
|
interface CalendarFreeBusy {
|
||||||
|
id: string
|
||||||
|
busy: BusySlot[]
|
||||||
|
}
|
||||||
|
|
||||||
|
interface UserFreeBusy {
|
||||||
|
id: string
|
||||||
|
calendars: CalendarFreeBusy[]
|
||||||
|
}
|
||||||
|
interface FreeBusyPayload {
|
||||||
|
users?: UserFreeBusy[]
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function getFreeBusyForEventAttendees(
|
||||||
|
userIds: string[],
|
||||||
|
start: string,
|
||||||
|
end: string,
|
||||||
|
eventUid: string
|
||||||
|
): Promise<Record<string, boolean>> {
|
||||||
|
const query: FreeBusyPostQuery = { userIds, start, end, eventUid }
|
||||||
|
const payload = (await fetchFreeBusyPost(query)) as FreeBusyPayload
|
||||||
|
const users = Array.isArray(payload.users) ? payload.users : []
|
||||||
|
const eventUidBase = extractEventBaseUuid(eventUid)
|
||||||
|
return Object.fromEntries(
|
||||||
|
users.map(u => {
|
||||||
|
const isBusy = (u.calendars ?? []).some(cal =>
|
||||||
|
(cal.busy ?? []).some(
|
||||||
|
slot => extractEventBaseUuid(slot.uid) !== eventUidBase
|
||||||
|
)
|
||||||
|
)
|
||||||
|
return [u.id, isBusy]
|
||||||
|
})
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -0,0 +1,59 @@
|
|||||||
|
import { api } from '@/utils/apiUtils'
|
||||||
|
|
||||||
|
export interface FreeBusyQuery {
|
||||||
|
hrefs: string[]
|
||||||
|
start: string
|
||||||
|
end: string
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* Fires REPORT requests against each calendar href to collect raw free/busy
|
||||||
|
* data. Returns an array of parsed JSON payloads (null on failure).
|
||||||
|
* It's for getting the free busy for added attendees in an event (new or while modifying)
|
||||||
|
*/
|
||||||
|
export async function fetchFreeBusyReports(
|
||||||
|
query: FreeBusyQuery
|
||||||
|
): Promise<unknown[]> {
|
||||||
|
const body = JSON.stringify({
|
||||||
|
type: 'free-busy-query',
|
||||||
|
match: { start: query.start, end: query.end }
|
||||||
|
})
|
||||||
|
return Promise.all(
|
||||||
|
query.hrefs.map(href =>
|
||||||
|
api(`dav${href}`, {
|
||||||
|
method: 'REPORT',
|
||||||
|
headers: { Accept: 'application/json, text/plain, */*' },
|
||||||
|
body
|
||||||
|
})
|
||||||
|
.then(r => (r.ok ? r.json() : null))
|
||||||
|
.catch(() => null)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface FreeBusyPostQuery {
|
||||||
|
userIds: string[]
|
||||||
|
start: string
|
||||||
|
end: string
|
||||||
|
eventUid: string
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* POSTs to the freebusy endpoint and returns the raw response payload.
|
||||||
|
* Throws on non-OK status.
|
||||||
|
* It's for getting the free busy for present attendees while editing event
|
||||||
|
*/
|
||||||
|
export async function fetchFreeBusyPost(
|
||||||
|
query: FreeBusyPostQuery
|
||||||
|
): Promise<unknown> {
|
||||||
|
const r = await api('dav/calendars/freebusy', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: { Accept: 'application/json, text/plain, */*' },
|
||||||
|
body: JSON.stringify({
|
||||||
|
start: query.start,
|
||||||
|
end: query.end,
|
||||||
|
users: query.userIds,
|
||||||
|
uids: [query.eventUid]
|
||||||
|
})
|
||||||
|
})
|
||||||
|
if (!r.ok) throw new Error(`HTTP ${r.status}`)
|
||||||
|
return r.json()
|
||||||
|
}
|
||||||
@@ -1,37 +0,0 @@
|
|||||||
import { getCalendars } from '@/features/Calendars/CalendarApi'
|
|
||||||
|
|
||||||
import { api } from '@/utils/apiUtils'
|
|
||||||
import { extractCalendarHrefs } from '@/utils/extractCalendarHrefs'
|
|
||||||
import { hasFreeBusyConflict } from '../../../components/Attendees/useFreeBusy'
|
|
||||||
|
|
||||||
export async function getFreeBusyForAddedAttendeesREPORT(
|
|
||||||
userId: string,
|
|
||||||
start: string,
|
|
||||||
end: string
|
|
||||||
): Promise<boolean> {
|
|
||||||
const calendars = await getCalendars(
|
|
||||||
userId,
|
|
||||||
'withFreeBusy=true&withRights=true'
|
|
||||||
)
|
|
||||||
const hrefs = extractCalendarHrefs(calendars)
|
|
||||||
if (hrefs.length === 0) return false
|
|
||||||
|
|
||||||
const freeBusyBody = JSON.stringify({
|
|
||||||
type: 'free-busy-query',
|
|
||||||
match: { start, end }
|
|
||||||
})
|
|
||||||
|
|
||||||
const results = await Promise.all(
|
|
||||||
hrefs.map(href =>
|
|
||||||
api(`dav${href}`, {
|
|
||||||
method: 'REPORT',
|
|
||||||
headers: { Accept: 'application/json, text/plain, */*' },
|
|
||||||
body: freeBusyBody
|
|
||||||
})
|
|
||||||
.then(r => (r.ok ? r.json() : null))
|
|
||||||
.then(data => (data ? hasFreeBusyConflict(data) : false))
|
|
||||||
.catch(() => false)
|
|
||||||
)
|
|
||||||
)
|
|
||||||
return results.some(Boolean)
|
|
||||||
}
|
|
||||||
@@ -1,48 +0,0 @@
|
|||||||
import { api } from '@/utils/apiUtils'
|
|
||||||
import { extractEventBaseUuid } from '@/utils/extractEventBaseUuid'
|
|
||||||
|
|
||||||
interface BusySlot {
|
|
||||||
uid: string
|
|
||||||
start: string
|
|
||||||
end: string
|
|
||||||
}
|
|
||||||
|
|
||||||
interface CalendarFreeBusy {
|
|
||||||
id: string
|
|
||||||
busy: BusySlot[]
|
|
||||||
}
|
|
||||||
|
|
||||||
interface UserFreeBusy {
|
|
||||||
id: string
|
|
||||||
calendars: CalendarFreeBusy[]
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function getFreeBusyForEventAttendeesPOST(
|
|
||||||
userIds: string[],
|
|
||||||
start: string,
|
|
||||||
end: string,
|
|
||||||
eventUid: string
|
|
||||||
): Promise<Record<string, boolean>> {
|
|
||||||
const r = await api('dav/calendars/freebusy', {
|
|
||||||
method: 'POST',
|
|
||||||
headers: { Accept: 'application/json, text/plain, */*' },
|
|
||||||
body: JSON.stringify({ start, end, users: userIds, uids: [eventUid] })
|
|
||||||
})
|
|
||||||
if (!r.ok) throw new Error(`HTTP ${r.status}`)
|
|
||||||
|
|
||||||
const payload: unknown = await r.json()
|
|
||||||
const users = Array.isArray((payload as { users?: unknown })?.users)
|
|
||||||
? ((payload as { users: UserFreeBusy[] }).users ?? [])
|
|
||||||
: []
|
|
||||||
|
|
||||||
const eventUidBase = extractEventBaseUuid(eventUid)
|
|
||||||
|
|
||||||
return Object.fromEntries(
|
|
||||||
users.map(u => {
|
|
||||||
const isBusy = u.calendars.some(cal =>
|
|
||||||
cal.busy.some(slot => extractEventBaseUuid(slot.uid) !== eventUidBase)
|
|
||||||
)
|
|
||||||
return [u.id, isBusy]
|
|
||||||
})
|
|
||||||
)
|
|
||||||
}
|
|
||||||
@@ -1,11 +0,0 @@
|
|||||||
import { api } from '@/utils/apiUtils'
|
|
||||||
import { isValidEmail } from '@/utils/isValidEmail'
|
|
||||||
|
|
||||||
export async function getUserDataFromEmail(email: string) {
|
|
||||||
if (!isValidEmail(email)) {
|
|
||||||
return []
|
|
||||||
}
|
|
||||||
const r = await api(`api/users?email=${encodeURIComponent(email)}`)
|
|
||||||
const result: Array<Record<string, string>> = await r.json()
|
|
||||||
return result
|
|
||||||
}
|
|
||||||
@@ -1,84 +0,0 @@
|
|||||||
import { api } from '@/utils/apiUtils'
|
|
||||||
import { extractEventBaseUuid } from '@/utils/extractEventBaseUuid'
|
|
||||||
import { TIMEZONES } from '@/utils/timezone-data'
|
|
||||||
import ICAL from 'ical.js'
|
|
||||||
import { CalendarEvent } from '../EventsTypes'
|
|
||||||
import { makeTimezone, makeVevent } from '../utils'
|
|
||||||
|
|
||||||
export async function postCounterProposal({
|
|
||||||
event,
|
|
||||||
senderEmail,
|
|
||||||
recipientEmail,
|
|
||||||
proposedStart,
|
|
||||||
proposedEnd,
|
|
||||||
message
|
|
||||||
}: {
|
|
||||||
event: CalendarEvent
|
|
||||||
senderEmail: string
|
|
||||||
recipientEmail: string
|
|
||||||
proposedStart: string
|
|
||||||
proposedEnd: string
|
|
||||||
message?: string
|
|
||||||
}): Promise<Response> {
|
|
||||||
// Build the counter event with proposed dates
|
|
||||||
const counterEvent: CalendarEvent = {
|
|
||||||
...event,
|
|
||||||
start: proposedStart,
|
|
||||||
end: proposedEnd,
|
|
||||||
sequence: event.sequence ?? 0
|
|
||||||
}
|
|
||||||
|
|
||||||
// Build vevent jCal
|
|
||||||
const vevent = makeVevent(
|
|
||||||
counterEvent,
|
|
||||||
counterEvent.timezone,
|
|
||||||
senderEmail,
|
|
||||||
!event.recurrenceId
|
|
||||||
)
|
|
||||||
if (message) {
|
|
||||||
vevent?.[1]?.push(['comment', {}, 'text', message])
|
|
||||||
}
|
|
||||||
// Build vtimezone
|
|
||||||
const timezoneData = TIMEZONES.zones[counterEvent.timezone]
|
|
||||||
const vtimezone = makeTimezone(timezoneData, counterEvent)
|
|
||||||
|
|
||||||
// Assemble full vcalendar with METHOD:COUNTER
|
|
||||||
const jcal = [
|
|
||||||
'vcalendar',
|
|
||||||
[
|
|
||||||
['version', {}, 'text', '2.0'],
|
|
||||||
['prodid', {}, 'text', '-//OpenPaaS//OpenPaaS//EN'],
|
|
||||||
['method', {}, 'text', 'COUNTER']
|
|
||||||
],
|
|
||||||
[vevent, vtimezone.component.jCal]
|
|
||||||
]
|
|
||||||
|
|
||||||
// Serialize to raw ICS
|
|
||||||
const counterICS = new ICAL.Component(jcal).toString()
|
|
||||||
|
|
||||||
const postResponse = await api(`dav${event.URL}`, {
|
|
||||||
method: 'POST',
|
|
||||||
body: JSON.stringify({
|
|
||||||
ical: counterICS,
|
|
||||||
sender: senderEmail,
|
|
||||||
recipient: recipientEmail,
|
|
||||||
uid: extractEventBaseUuid(event.uid),
|
|
||||||
sequence: counterEvent.sequence,
|
|
||||||
method: 'COUNTER'
|
|
||||||
}),
|
|
||||||
headers: {
|
|
||||||
accept: 'application/json,text/plain,*/*',
|
|
||||||
'content-type': 'application/calendar+json',
|
|
||||||
Prefer: 'return=representation',
|
|
||||||
'X-Http-Method-Override': 'ITIP'
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
if (!postResponse.ok) {
|
|
||||||
throw new Error(
|
|
||||||
`postCounterProposal failed with status ${postResponse.status}`
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
return postResponse
|
|
||||||
}
|
|
||||||
@@ -1,10 +1,9 @@
|
|||||||
import { api } from '@/utils/apiUtils'
|
|
||||||
import { TIMEZONES } from '@/utils/timezone-data'
|
import { TIMEZONES } from '@/utils/timezone-data'
|
||||||
import {
|
import {
|
||||||
VCalComponent,
|
VCalComponent,
|
||||||
VObjectProperty
|
VObjectProperty
|
||||||
} from '../../Calendars/types/CalendarData'
|
} from '../../Calendars/types/CalendarData'
|
||||||
import { getAllRecurrentEvent } from '../EventApi'
|
import { fetchAllRecurrentVevents, putEventRaw } from '../EventDao'
|
||||||
import { CalendarEvent } from '../EventsTypes'
|
import { CalendarEvent } from '../EventsTypes'
|
||||||
import { makeTimezone, makeVevent } from '../utils'
|
import { makeTimezone, makeVevent } from '../utils'
|
||||||
|
|
||||||
@@ -20,26 +19,22 @@ const METADATA_FIELDS = [
|
|||||||
] as const
|
] as const
|
||||||
|
|
||||||
// Helper function to get field values from props
|
// Helper function to get field values from props
|
||||||
const getFieldValues = (props: VObjectProperty[], fieldName: string) => {
|
const getFieldValues = (props: VObjectProperty[], fieldName: string) =>
|
||||||
return props.filter(([k]) => k.toLowerCase() === fieldName.toLowerCase())
|
props.filter(([k]) => k.toLowerCase() === fieldName.toLowerCase())
|
||||||
}
|
|
||||||
|
|
||||||
// Helper function to find a single field value from props
|
// Helper function to find a single field value from props
|
||||||
const findFieldValue = (props: VObjectProperty[], fieldName: string) => {
|
const findFieldValue = (props: VObjectProperty[], fieldName: string) =>
|
||||||
return props.find(([k]) => k.toLowerCase() === fieldName.toLowerCase())
|
props.find(([k]) => k.toLowerCase() === fieldName.toLowerCase())
|
||||||
}
|
|
||||||
|
|
||||||
// Helper function to serialize for comparison
|
// Helper function to serialize for comparison
|
||||||
const serialize = (values: VObjectProperty[] | VCalComponent[]) => {
|
const serialize = (values: VObjectProperty[] | VCalComponent[]) =>
|
||||||
return JSON.stringify(values)
|
JSON.stringify(values)
|
||||||
}
|
|
||||||
|
|
||||||
// Helper function to filter components by name
|
// Helper function to filter components by name
|
||||||
const filterComponentsByName = (components: VCalComponent[], name: string) => {
|
const filterComponentsByName = (components: VCalComponent[], name: string) =>
|
||||||
return components.filter(
|
components.filter(
|
||||||
([componentName]) => componentName.toLowerCase() === name.toLowerCase()
|
([componentName]) => componentName.toLowerCase() === name.toLowerCase()
|
||||||
)
|
)
|
||||||
}
|
|
||||||
|
|
||||||
// Detect which metadata fields changed between old and new master
|
// Detect which metadata fields changed between old and new master
|
||||||
const detectChangedMetadataFields = (
|
const detectChangedMetadataFields = (
|
||||||
@@ -125,11 +120,10 @@ const incrementSequenceNumber = (props: VObjectProperty[]) => {
|
|||||||
const updateValarmComponents = (
|
const updateValarmComponents = (
|
||||||
components: VCalComponent[],
|
components: VCalComponent[],
|
||||||
newValarm: VCalComponent[]
|
newValarm: VCalComponent[]
|
||||||
) => {
|
) =>
|
||||||
return components
|
components
|
||||||
.filter(([name]) => name.toLowerCase() !== 'valarm')
|
.filter(([name]) => name.toLowerCase() !== 'valarm')
|
||||||
.concat(newValarm)
|
.concat(newValarm)
|
||||||
}
|
|
||||||
|
|
||||||
// Update a single vevent with metadata changes
|
// Update a single vevent with metadata changes
|
||||||
const updateVeventWithMetadataChanges = (
|
const updateVeventWithMetadataChanges = (
|
||||||
@@ -156,10 +150,9 @@ const updateVeventWithMetadataChanges = (
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Handle VALARM component updates
|
// Handle VALARM component updates
|
||||||
let updatedComponents = components
|
const updatedComponents = valarmChanged
|
||||||
if (valarmChanged) {
|
? updateValarmComponents(components, newValarm)
|
||||||
updatedComponents = updateValarmComponents(components, newValarm)
|
: components
|
||||||
}
|
|
||||||
|
|
||||||
return [veventType, newProps, updatedComponents]
|
return [veventType, newProps, updatedComponents]
|
||||||
}
|
}
|
||||||
@@ -205,7 +198,8 @@ export const updateSeries = async (
|
|||||||
calOwnerEmail?: string,
|
calOwnerEmail?: string,
|
||||||
removeOverrides: boolean = true
|
removeOverrides: boolean = true
|
||||||
) => {
|
) => {
|
||||||
const vevents = (await getAllRecurrentEvent(event)) as VCalComponent[]
|
const vevents = await fetchAllRecurrentVevents(event)
|
||||||
|
|
||||||
const masterIndex = vevents.findIndex(
|
const masterIndex = vevents.findIndex(
|
||||||
([, props]) => !findFieldValue(props, 'recurrence-id')
|
([, props]) => !findFieldValue(props, 'recurrence-id')
|
||||||
)
|
)
|
||||||
@@ -249,11 +243,5 @@ export const updateSeries = async (
|
|||||||
|
|
||||||
const newJCal = ['vcalendar', [], [...finalVevents, vtimezone.component.jCal]]
|
const newJCal = ['vcalendar', [], [...finalVevents, vtimezone.component.jCal]]
|
||||||
|
|
||||||
return api(`dav${event.URL}`, {
|
return putEventRaw(event, newJCal)
|
||||||
method: 'PUT',
|
|
||||||
body: JSON.stringify(newJCal),
|
|
||||||
headers: {
|
|
||||||
'content-type': 'text/calendar; charset=utf-8'
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,14 @@
|
|||||||
|
import { api } from '@/utils/apiUtils'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fetches user data by email. Returns raw JSON array.
|
||||||
|
*/
|
||||||
|
export async function fetchUserByEmail(
|
||||||
|
email: string
|
||||||
|
): Promise<Array<Record<string, string>>> {
|
||||||
|
const r = await api(`api/users?email=${encodeURIComponent(email)}`)
|
||||||
|
if (!r.ok) {
|
||||||
|
return []
|
||||||
|
}
|
||||||
|
return r.json()
|
||||||
|
}
|
||||||
@@ -1,13 +1,15 @@
|
|||||||
import { User } from '@/components/Attendees/types'
|
import { User } from '@/components/Attendees/types'
|
||||||
import { api } from '@/utils/apiUtils'
|
import { api } from '@/utils/apiUtils'
|
||||||
|
import { isValidEmail } from '@/utils/isValidEmail'
|
||||||
import { BusinessHour } from '../Settings/SettingsSlice'
|
import { BusinessHour } from '../Settings/SettingsSlice'
|
||||||
import { OpenPaasUserData } from './type/OpenPaasUserData'
|
import { OpenPaasUserData } from './type/OpenPaasUserData'
|
||||||
|
import { ResourceData } from './type/ResourceData'
|
||||||
|
import { fetchUserByEmail } from './UserDao'
|
||||||
import {
|
import {
|
||||||
ConfigurationItem,
|
ConfigurationItem,
|
||||||
ModuleConfiguration,
|
ModuleConfiguration,
|
||||||
SearchResponseItem
|
SearchResponseItem
|
||||||
} from './userDataTypes'
|
} from './userDataTypes'
|
||||||
import { ResourceData } from './type/ResourceData'
|
|
||||||
|
|
||||||
export async function getOpenPaasUser() {
|
export async function getOpenPaasUser() {
|
||||||
const user = await api.get(`api/user`)
|
const user = await api.get(`api/user`)
|
||||||
@@ -146,3 +148,10 @@ export async function updateUserConfigurations(
|
|||||||
json: modules
|
json: modules
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export async function getUserDataFromEmail(
|
||||||
|
email: string
|
||||||
|
): Promise<Array<Record<string, string>>> {
|
||||||
|
if (!isValidEmail(email)) return []
|
||||||
|
return fetchUserByEmail(email)
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user