Skip to content

Commit 43a2368

Browse files
committed
Added partial support for attendees when creating event
1 parent d0def05 commit 43a2368

File tree

2 files changed

+74
-34
lines changed

2 files changed

+74
-34
lines changed

calendar/routes/calendar/+page.svelte

Lines changed: 47 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,14 @@
11
<script lang="ts">
22
import type { EventInitFormData } from '@axium/calendar/client';
3-
import { getCalPermissionsInfo, weekDaysFor, type EventInit } from '@axium/calendar/common';
3+
import { getCalPermissionsInfo, weekDaysFor, type Event, type EventInit } from '@axium/calendar/common';
44
import * as Calendar from '@axium/calendar/components';
55
import { contextMenu, dynamicRows } from '@axium/client/attachments';
6-
import { AccessControlDialog, FormDialog, Icon, Popover, ZodInput, ZodForm } from '@axium/client/components';
6+
import { AccessControlDialog, FormDialog, Icon, Popover } from '@axium/client/components';
7+
import UserDiscovery from '@axium/client/components/UserDiscovery';
78
import { fetchAPI } from '@axium/client/requests';
89
import { SvelteDate } from 'svelte/reactivity';
9-
import { _throw } from 'utilium';
10-
import * as z from 'zod';
10+
import { _throw, type WithRequired } from 'utilium';
11+
import z from 'zod';
1112
const { data } = $props();
1213
1314
const { user } = data.session;
@@ -27,7 +28,7 @@
2728
2829
let dialogs = $state<Record<string, HTMLDialogElement>>({});
2930
30-
let eventInit = $state<EventInit>({} as EventInit);
31+
let eventInit = $state<WithRequired<EventInit, 'attendees'>>({ attendees: [] } as any);
3132
</script>
3233

3334
<svelte:head>
@@ -146,26 +147,21 @@
146147
</div>
147148
</div>
148149

149-
<!--
150-
export const EventInit = z.object({
151-
calId: z.uuid(),
152-
summary: z.string(),
153-
location: z.string().nullish(),
154-
start: z.coerce.date(),
155-
end: z.coerce.date(),
156-
description: z.string().nullish(),
157-
attendees: AttendeeInit.array().optional().default([]),
158-
// note: recurrences are not support yet
159-
recurrence: z.string().nullish(),
160-
recurrenceExcludes: z.array(z.string()).nullish(),
161-
recurrenceId: z.uuid().nullish(),
162-
});
163-
-->
164-
165150
<FormDialog
166151
id="new-event"
167152
submitText="Create"
168-
submit={(data: EventInitFormData) => fetchAPI('PUT', 'calendars/:id/events', { ...data, ...eventInit, attendees: [] }, data.calId)}
153+
submit={async (data: EventInitFormData) => {
154+
const calendar = calendars.find(cal => cal.id == data.calId);
155+
if (!calendar) throw 'Invalid calendar';
156+
const event: Event = await fetchAPI(
157+
'PUT',
158+
'calendars/:id/events',
159+
{ ...data, ...eventInit, recurrenceExcludes: [], recurrenceId: null },
160+
data.calId
161+
);
162+
event.calendar = calendar;
163+
calendar.events?.push(event);
164+
}}
169165
>
170166
<input name="summary" type="text" required placeholder="Add title" />
171167
<div class="event-times-container">
@@ -174,13 +170,15 @@ export const EventInit = z.object({
174170
<input type="datetime-local" name="start" id="eventInit.start" required />
175171
<input type="datetime-local" name="end" id="eventInit.end" required />
176172
<div class="event-time-options">
177-
<input bind:checked={eventInit.isAllDay} id="eventInit.isAllDay:checkbox" type="checkbox" required />
173+
<input bind:checked={eventInit.isAllDay} id="eventInit.isAllDay:checkbox" type="checkbox" />
178174
<label for="eventInit.isAllDay:checkbox" class="checkbox">
179175
{#if eventInit.isAllDay}<Icon i="check" --size="1.3em" />{/if}
180176
</label>
181177
<label for="eventInit.isAllDay:checkbox">All day</label>
182178
<div class="spacing"></div>
183-
<select class="recurrence"></select>
179+
<select class="recurrence">
180+
<!-- @todo -->
181+
</select>
184182
</div>
185183
</div>
186184
</div>
@@ -194,6 +192,27 @@ export const EventInit = z.object({
194192
</select>
195193
</div>
196194

195+
<div class="attendees-container">
196+
<label for="eventInit.attendee"><Icon i="user-group" /></label>
197+
<div class="attendees">
198+
<UserDiscovery
199+
noRoles
200+
allowExact
201+
onSelect={target => {
202+
const { data: userId } = z.uuid().safeParse(target);
203+
const { data: email } = z.email().safeParse(target);
204+
if (!userId && !email) throw 'Can not determine attendee: ' + target;
205+
if (!email) throw 'Specifying attendees without an email is not supported yet';
206+
// @todo supports roles and also contacts
207+
eventInit.attendees.push({ userId, email });
208+
}}
209+
/>
210+
{#each eventInit.attendees as attendee (attendee.email)}
211+
<div class="attendee">{attendee.email}</div>
212+
{/each}
213+
</div>
214+
</div>
215+
197216
<div>
198217
<label for="eventInit.location"><Icon i="location-dot" /></label>
199218
<input name="location" id="eventInit.location" placeholder="Add location" />
@@ -244,13 +263,15 @@ export const EventInit = z.object({
244263
div:has(label ~ input),
245264
div:has(label ~ textarea),
246265
div:has(label ~ select),
247-
.event-times-container {
266+
.event-times-container,
267+
.attendees-container {
248268
display: flex;
249269
flex-direction: row;
250270
gap: 0.5em;
251271
}
252272

253-
div.event-times {
273+
div.event-times,
274+
div.attendees {
254275
display: flex;
255276
flex-direction: column;
256277
gap: 0.5em;

calendar/src/server.ts

Lines changed: 27 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -101,6 +101,7 @@ addRoute({
101101
.select(withAttendees)
102102
.where('calId', '=', id)
103103
.where(sql<boolean>`(${sql.ref('start')}, ${sql.ref('end')}) OVERLAPS (${sql.val(filter.start)}, ${sql.val(filter.end)})`)
104+
.limit(1000)
104105
.execute()
105106
.catch(withError('Could not get events'));
106107

@@ -113,15 +114,33 @@ addRoute({
113114

114115
const { item: calendar } = await authRequestForItem(request, 'calendars', id, { edit: true });
115116

116-
const event = await database
117-
.insertInto('events')
118-
.values({ ...init, calId: id })
119-
.returningAll()
120-
.returning(withAttendees)
121-
.executeTakeFirstOrThrow()
122-
.catch(withError('Could not create event'));
117+
const { attendees: attendeesInit = [] } = init;
118+
delete init.attendees;
123119

124-
return Object.assign(event, { calendar });
120+
if (attendeesInit.length > 100) throw error(400, 'Too many attendees');
121+
122+
const tx = await database.startTransaction().execute();
123+
try {
124+
const event = await tx
125+
.insertInto('events')
126+
.values({ ...init, calId: id })
127+
.returningAll()
128+
.returning(sql<Attendee[]>`'[]'::jsonb`.as('attendees'))
129+
.executeTakeFirstOrThrow()
130+
.catch(withError('Could not create event'));
131+
132+
const attendees = await tx
133+
.insertInto('attendees')
134+
.values(attendeesInit.map(a => ({ ...a, eventId: event.id })))
135+
.returningAll()
136+
.execute();
137+
138+
await tx.commit().execute();
139+
return Object.assign(event, { attendees, calendar });
140+
} catch (e) {
141+
await tx.rollback().execute();
142+
throw e;
143+
}
125144
},
126145
});
127146

0 commit comments

Comments
 (0)