Add guest management functionality to existing bookings#10
Conversation
* feat: ability to add guests via app.cal.com/bookings * fix: some update * fix: minor issue * fix: final update * update * update * add requested changes * fix type error * small update * final update * fix type error * fix location * update calender event --------- Co-authored-by: Somay Chauhan <somaychauhan98@gmail.com>
WalkthroughThis change introduces an "Add Guests" feature for bookings, enabling users to add multiple guest emails to an existing booking via a new dialog interface. The implementation covers UI components, email validation, backend API handler, schema validation, and notification email templates for both organizers and attendees. Localization strings and exports are updated accordingly. Changes
Sequence Diagram(s)sequenceDiagram
participant User
participant BookingListItem
participant AddGuestsDialog
participant TRPC_API
participant addGuestsHandler
participant EmailManager
User->>BookingListItem: Click "Add Guests"
BookingListItem->>AddGuestsDialog: Open dialog
User->>AddGuestsDialog: Enter emails, click "Add"
AddGuestsDialog->>TRPC_API: Call addGuests mutation
TRPC_API->>addGuestsHandler: Validate and process request
addGuestsHandler->>addGuestsHandler: Check permissions, filter emails, update booking
addGuestsHandler->>EmailManager: Send notification emails
EmailManager-->>addGuestsHandler: Emails sent (async)
addGuestsHandler->>TRPC_API: Return success
TRPC_API->>AddGuestsDialog: Mutation result
AddGuestsDialog->>User: Show success or error
Estimated code review effort🎯 4 (Complex) | ⏱️ ~40 minutes Poem
✨ Finishing Touches
🧪 Generate unit tests
🪧 TipsChatThere are 3 ways to chat with CodeRabbit:
SupportNeed help? Create a ticket on our support page for assistance with any issues or questions. Note: Be mindful of the bot's finite context window. It's strongly recommended to break down tasks such as reading entire modules into smaller chunks. For a focused discussion, use review comments to chat about specific files and their changes, instead of using the PR comments. CodeRabbit Commands (Invoked using PR comments)
Other keywords and placeholders
CodeRabbit Configuration File (
|
✅ Actions performedReview triggered.
|
|
@coderabbitai review |
✅ Actions performedReview triggered.
|
|
Hey there and thank you for opening this pull request! 👋🏼 We require pull request titles to follow the Conventional Commits specification and it looks like your proposed title needs to be adjusted. Details: |
There was a problem hiding this comment.
Actionable comments posted: 6
🧹 Nitpick comments (10)
packages/ui/index.tsx (1)
150-154: Confirm path & keep constants immutable
- Verify that
./form/MultiEmailLazyactually default-exports the component (case-sensitive path & barrel exports sometimes get out of sync).- If the intention is to make all three lazy-loaded inputs (Address/Phone/MultiEmail) appear as a fixed trio, consider freezing their props rather than leaving the TODO comment indefinitely.
packages/emails/src/templates/index.ts (1)
38-39: Minor: keep exports alphabetised to avoid merge churn
AttendeeAddGuestsEmailshould be placed beforeOrganizerAddGuestsEmailif alphabetical ordering is desired (the rest of the file appears sorted).apps/web/public/static/locales/en/common.json (4)
1122-1124: Consider tightening message phrasing & capitalisation for consistencyNeighbouring status-type strings use the pattern “Updated – …”.
For similar clarity and parallel wording you might prefer:-"new_guests_added": "Added - New guests added to your event", -"guests_added_event_type_subject": "Guests Added: {{eventType}} with {{name}} at {{date}}", +"new_guests_added": "Updated – New guests have been added to your event", +"guests_added_event_type_subject": "Updated – Guests added: {{eventType}} with {{name}} at {{date}}",Purely a copy-deck nitpick – feel free to ignore if this deviates from your style guide.
1136-1137: Minor wording alignmentThe UI already has “Add guests” (line 580). To mirror that action/label style, you could drop the past-tense verb here:
-"guests_added": "Guests added", +"guests_added": "Added guests",Not critical, just keeps verbs first like other status keys (“Updated”, “Canceled”, etc.).
1139-1139: Tiny punctuation touch-upAdd a trailing period so it reads like other validation sentences:
-"emails_must_be_unique_valid": "Emails must be unique and valid" +"emails_must_be_unique_valid": "Emails must be unique and valid."
2434-2434: Label capitalisationMost action labels in this file use sentence-case (“Add guests”, “Add location”). For consistency:
-"add_emails": "Add Emails", +"add_emails": "Add emails",packages/trpc/server/routers/viewer/bookings/addGuests.schema.ts (1)
3-6: Consider adding business rule validations.The schema correctly validates email format but lacks business constraints that might be important:
Consider enhancing the schema with additional validations:
export const ZAddGuestsInputSchema = z.object({ - bookingId: z.number(), - guests: z.array(z.string().email()), + bookingId: z.number().int().positive(), + guests: z.array(z.string().email()).min(1).max(50), // Prevent empty arrays and limit abuse });This would prevent negative/invalid booking IDs and ensure reasonable limits on guest additions.
apps/web/components/dialog/AddGuestsDialog.tsx (2)
26-29: Consider enhancing email uniqueness validation.The current validation only checks for uniqueness but doesn't filter out empty strings, which could cause issues with the initial state.
Consider refining the validation schema:
const ZAddGuestsInputSchema = z.array(z.string().email()).refine((emails) => { + const validEmails = emails.filter(email => email.trim() !== ''); - const uniqueEmails = new Set(emails); + const uniqueEmails = new Set(validEmails); - return uniqueEmails.size === emails.length; + return uniqueEmails.size === validEmails.length && validEmails.length > 0; });This would handle empty strings more gracefully and ensure at least one valid email is provided.
32-32: Consider initializing with empty array instead of array with empty string.The initial state
[""]might cause validation issues and user confusion.-const [multiEmailValue, setMultiEmailValue] = useState<string[]>([""]); +const [multiEmailValue, setMultiEmailValue] = useState<string[]>([]);This would be more consistent with the expectation of an empty state and avoid potential validation edge cases.
packages/ui/form/MultiEmail.tsx (1)
73-75: Remove unnecessary empty fragment.The empty fragment serves no purpose and can be removed.
</div> - ) : ( - <></> - )} + ) : null}
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (15)
apps/web/components/booking/BookingListItem.tsx(4 hunks)apps/web/components/dialog/AddGuestsDialog.tsx(1 hunks)apps/web/public/static/locales/en/common.json(3 hunks)packages/emails/email-manager.ts(3 hunks)packages/emails/src/templates/AttendeeAddGuestsEmail.tsx(1 hunks)packages/emails/src/templates/OrganizerAddGuestsEmail.tsx(1 hunks)packages/emails/src/templates/index.ts(1 hunks)packages/emails/templates/attendee-add-guests-email.ts(1 hunks)packages/emails/templates/organizer-add-guests-email.ts(1 hunks)packages/trpc/server/routers/viewer/bookings/_router.tsx(3 hunks)packages/trpc/server/routers/viewer/bookings/addGuests.handler.ts(1 hunks)packages/trpc/server/routers/viewer/bookings/addGuests.schema.ts(1 hunks)packages/ui/form/MultiEmail.tsx(1 hunks)packages/ui/form/MultiEmailLazy.tsx(1 hunks)packages/ui/index.tsx(1 hunks)
🧰 Additional context used
🧬 Code Graph Analysis (7)
packages/trpc/server/routers/viewer/bookings/_router.tsx (1)
packages/trpc/server/routers/viewer/bookings/addGuests.schema.ts (1)
ZAddGuestsInputSchema(3-6)
packages/emails/email-manager.ts (6)
packages/types/Calendar.d.ts (1)
CalendarEvent(157-211)packages/lib/formatCalendarEvent.ts (1)
formatCalEvent(19-27)packages/emails/src/templates/OrganizerAddGuestsEmail.tsx (1)
OrganizerAddGuestsEmail(3-11)packages/emails/templates/organizer-add-guests-email.ts (1)
OrganizerAddGuestsEmail(7-38)packages/emails/src/templates/AttendeeAddGuestsEmail.tsx (1)
AttendeeAddGuestsEmail(3-10)packages/emails/templates/attendee-add-guests-email.ts (1)
AttendeeAddGuestsEmail(5-34)
apps/web/components/dialog/AddGuestsDialog.tsx (3)
packages/lib/hooks/useLocale.ts (1)
useLocale(5-22)packages/trpc/server/routers/viewer/bookings/addGuests.schema.ts (1)
ZAddGuestsInputSchema(3-6)packages/ui/index.tsx (6)
showToast(146-146)Dialog(133-133)DialogContent(135-135)DialogHeader(137-137)DialogFooter(136-136)Button(14-14)
packages/emails/src/templates/AttendeeAddGuestsEmail.tsx (2)
packages/emails/src/templates/index.ts (2)
AttendeeAddGuestsEmail(39-39)AttendeeScheduledEmail(9-9)packages/emails/templates/attendee-add-guests-email.ts (1)
AttendeeAddGuestsEmail(5-34)
packages/emails/templates/attendee-add-guests-email.ts (2)
packages/emails/src/templates/AttendeeAddGuestsEmail.tsx (1)
AttendeeAddGuestsEmail(3-10)packages/emails/src/templates/index.ts (2)
AttendeeAddGuestsEmail(39-39)AttendeeScheduledEmail(9-9)
packages/emails/templates/organizer-add-guests-email.ts (3)
packages/emails/src/templates/OrganizerAddGuestsEmail.tsx (1)
OrganizerAddGuestsEmail(3-11)packages/emails/src/templates/index.ts (2)
OrganizerAddGuestsEmail(38-38)OrganizerScheduledEmail(21-21)packages/lib/constants.ts (1)
APP_NAME(26-26)
apps/web/components/booking/BookingListItem.tsx (1)
apps/web/components/dialog/AddGuestsDialog.tsx (1)
AddGuestsDialog(24-107)
🔇 Additional comments (16)
packages/ui/form/MultiEmailLazy.tsx (1)
1-6: Tighten thedynamictyping and add a loading fallbackThe double
as unknown ascast hides type mismatches and loses IntelliSense.
You can keep full typing and supply a tiny fallback component with:-import dynamic from "next/dynamic"; - -/** These are like 40kb that not every user needs */ -const MultiEmail = dynamic(() => import("./MultiEmail")) as unknown as typeof import("./MultiEmail").default; +import dynamic from "next/dynamic"; + +// ~40 kb UI widget – lazy-loaded to drop initial bundle +const MultiEmail = dynamic<typeof import("./MultiEmail").default>( + () => import(/* webpackChunkName: "multi-email" */ "./MultiEmail"), + { ssr: false, loading: () => null } // or a small spinner +);[ suggest_essential_refactor ]
packages/emails/src/templates/AttendeeAddGuestsEmail.tsx (1)
3-9: Same prop-override caveat as organizer templateRe-order the spread so callers cannot accidentally change locked fields:
- <AttendeeScheduledEmail - title="new_guests_added" - headerType="calendarCircle" - subject="guests_added_event_type_subject" - {...props} - /> + <AttendeeScheduledEmail + {...props} + title="new_guests_added" + headerType="calendarCircle" + subject="guests_added_event_type_subject" + />[ suggest_essential_refactor ]
packages/trpc/server/routers/viewer/bookings/_router.tsx (3)
4-4: LGTM! Proper import addition.The import follows the established pattern used by other schema imports in the file.
18-18: LGTM! Consistent cache type definition.The handler cache type definition follows the same pattern as other handlers in the file.
79-95: LGTM! Procedure implementation follows established patterns.The
addGuestsprocedure implementation is consistent with other procedures in this router:
- Uses
authedProcedurefor proper authentication- Implements the same lazy loading pattern with
UNSTABLE_HANDLER_CACHE- Includes the standard error handling for failed handler loading
- Follows the exact same structure as
editLocationand other procedurespackages/emails/email-manager.ts (3)
21-21: LGTM! Proper import addition.The import follows the established pattern for email template imports.
52-52: LGTM! Consistent import pattern.The import follows the same pattern as other organizer email template imports.
525-550: LGTM! Well-implemented email notification logic.The
sendAddGuestsEmailsfunction follows the established patterns in this file:
- Properly formats the calendar event using
formatCalEvent- Sends notifications to organizer and team members using the new
OrganizerAddGuestsEmailtemplate- Correctly differentiates between new guests (who get
AttendeeScheduledEmail) and existing attendees (who getAttendeeAddGuestsEmail)- Uses
Promise.allfor efficient concurrent email sending, consistent with other functions in the fileThe logic for distinguishing new guests vs. existing attendees is sound and the implementation is robust.
apps/web/components/dialog/AddGuestsDialog.tsx (2)
88-96: LGTM! Proper state cleanup on cancel.The cancel button correctly resets all form state and closes the dialog.
76-85: LGTM! Good error message display.The error message display with icon and proper styling provides clear feedback to users when email validation fails.
apps/web/components/booking/BookingListItem.tsx (4)
46-46: LGTM! Proper import addition.The import follows the established pattern for dialog component imports.
193-200: LGTM! Well-integrated action definition.The "Add Guests" action follows the exact same pattern as other actions in
editBookingActions:
- Consistent ID naming convention
- Proper translation key usage
- Appropriate icon choice (
user-plus)- Follows the same onClick pattern as other dialog triggers
268-268: LGTM! Consistent state management.The state variable follows the same naming pattern as other dialog states in the component.
357-361: LGTM! Proper dialog integration.The AddGuestsDialog rendering follows the established pattern used by other dialogs in this component:
- Consistent prop naming and structure
- Proper state binding
- Correct bookingId passing
packages/emails/templates/attendee-add-guests-email.ts (1)
5-34: LGTM! Well-structured email template implementation.The email template correctly extends
AttendeeScheduledEmailand properly implements the notification payload with appropriate iCalendar attachment, localized content, and consistent formatting.packages/trpc/server/routers/viewer/bookings/addGuests.handler.ts (1)
74-78: Make email comparison consistently case-insensitive.The email comparison is case-sensitive for existing attendees but case-insensitive for blacklisted emails. This inconsistency could allow duplicate emails with different casing.
const uniqueGuests = guests.filter( (guest) => - !booking.attendees.some((attendee) => guest === attendee.email) && - !blacklistedGuestEmails.includes(guest) + !booking.attendees.some((attendee) => guest.toLowerCase() === attendee.email.toLowerCase()) && + !blacklistedGuestEmails.includes(guest.toLowerCase()) );Likely an incorrect or invalid review comment.
| const handleAdd = () => { | ||
| if (multiEmailValue.length === 0) { | ||
| return; | ||
| } | ||
| const validationResult = ZAddGuestsInputSchema.safeParse(multiEmailValue); | ||
| if (validationResult.success) { | ||
| addGuestsMutation.mutate({ bookingId, guests: multiEmailValue }); | ||
| } else { | ||
| setIsInvalidEmail(true); | ||
| } | ||
| }; |
There was a problem hiding this comment.
🛠️ Refactor suggestion
Improve empty state validation logic.
The current check multiEmailValue.length === 0 may not work correctly with the initial state of [""].
const handleAdd = () => {
- if (multiEmailValue.length === 0) {
+ const validEmails = multiEmailValue.filter(email => email.trim() !== '');
+ if (validEmails.length === 0) {
return;
}
- const validationResult = ZAddGuestsInputSchema.safeParse(multiEmailValue);
+ const validationResult = ZAddGuestsInputSchema.safeParse(validEmails);
if (validationResult.success) {
- addGuestsMutation.mutate({ bookingId, guests: multiEmailValue });
+ addGuestsMutation.mutate({ bookingId, guests: validEmails });
} else {
setIsInvalidEmail(true);
}
};This ensures only non-empty email strings are processed and sent to the server.
📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| const handleAdd = () => { | |
| if (multiEmailValue.length === 0) { | |
| return; | |
| } | |
| const validationResult = ZAddGuestsInputSchema.safeParse(multiEmailValue); | |
| if (validationResult.success) { | |
| addGuestsMutation.mutate({ bookingId, guests: multiEmailValue }); | |
| } else { | |
| setIsInvalidEmail(true); | |
| } | |
| }; | |
| const handleAdd = () => { | |
| const validEmails = multiEmailValue.filter(email => email.trim() !== ''); | |
| if (validEmails.length === 0) { | |
| return; | |
| } | |
| const validationResult = ZAddGuestsInputSchema.safeParse(validEmails); | |
| if (validationResult.success) { | |
| addGuestsMutation.mutate({ bookingId, guests: validEmails }); | |
| } else { | |
| setIsInvalidEmail(true); | |
| } | |
| }; |
🤖 Prompt for AI Agents
In apps/web/components/dialog/AddGuestsDialog.tsx around lines 48 to 58, the
current empty state check using multiEmailValue.length === 0 does not handle
cases where the array contains empty strings like [""] properly. Update the
validation to filter out empty strings from multiEmailValue before checking if
there are any valid emails to process. Only proceed with mutation if there is at
least one non-empty email string, ensuring that empty or blank emails are not
sent to the server.
| export const OrganizerAddGuestsEmail = (props: React.ComponentProps<typeof OrganizerScheduledEmail>) => ( | ||
| <OrganizerScheduledEmail | ||
| title="new_guests_added" | ||
| headerType="calendarCircle" | ||
| subject="guests_added_event_type_subject" | ||
| callToAction={null} | ||
| {...props} | ||
| /> |
There was a problem hiding this comment.
🛠️ Refactor suggestion
Order of spread makes props override your “constants”
{...props} is applied after the hard-coded props, so a caller can change title, subject, etc., defeating the locked-in template.
If these values must remain fixed, move the spread before the explicit props:
- <OrganizerScheduledEmail
- title="new_guests_added"
- headerType="calendarCircle"
- subject="guests_added_event_type_subject"
- callToAction={null}
- {...props}
- />
+ <OrganizerScheduledEmail
+ {...props}
+ title="new_guests_added"
+ headerType="calendarCircle"
+ subject="guests_added_event_type_subject"
+ callToAction={null}
+ />📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| export const OrganizerAddGuestsEmail = (props: React.ComponentProps<typeof OrganizerScheduledEmail>) => ( | |
| <OrganizerScheduledEmail | |
| title="new_guests_added" | |
| headerType="calendarCircle" | |
| subject="guests_added_event_type_subject" | |
| callToAction={null} | |
| {...props} | |
| /> | |
| export const OrganizerAddGuestsEmail = ( | |
| props: React.ComponentProps<typeof OrganizerScheduledEmail>, | |
| ) => ( | |
| <OrganizerScheduledEmail | |
| {...props} | |
| title="new_guests_added" | |
| headerType="calendarCircle" | |
| subject="guests_added_event_type_subject" | |
| callToAction={null} | |
| /> | |
| ); |
🤖 Prompt for AI Agents
In packages/emails/src/templates/OrganizerAddGuestsEmail.tsx around lines 3 to
10, the spread operator {...props} is placed after the fixed props, allowing
callers to override constants like title and subject. To fix this, move
{...props} before the explicit props so that the hard-coded values cannot be
overridden by incoming props, ensuring the template's fixed values remain locked
in.
| subject: `${this.t("guests_added_event_type_subject", { | ||
| eventType: this.calEvent.type, | ||
| name: this.calEvent.attendees[0].name, | ||
| date: this.getFormattedDate(), | ||
| })}`, |
There was a problem hiding this comment.
Add safety check for empty attendees array.
Accessing this.calEvent.attendees[0].name without checking if the array is empty could cause a runtime error.
subject: `${this.t("guests_added_event_type_subject", {
eventType: this.calEvent.type,
- name: this.calEvent.attendees[0].name,
+ name: this.calEvent.attendees[0]?.name || "Guest",
date: this.getFormattedDate(),
})}`,📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| subject: `${this.t("guests_added_event_type_subject", { | |
| eventType: this.calEvent.type, | |
| name: this.calEvent.attendees[0].name, | |
| date: this.getFormattedDate(), | |
| })}`, | |
| subject: `${this.t("guests_added_event_type_subject", { | |
| eventType: this.calEvent.type, | |
| name: this.calEvent.attendees[0]?.name || "Guest", | |
| date: this.getFormattedDate(), | |
| })}`, |
🤖 Prompt for AI Agents
In packages/emails/templates/organizer-add-guests-email.ts around lines 26 to
30, the code accesses this.calEvent.attendees[0].name without verifying if the
attendees array is non-empty, which can cause a runtime error. Add a safety
check to ensure this.calEvent.attendees has at least one element before
accessing the first attendee's name, and provide a fallback value if the array
is empty to prevent errors.
| const isTeamAdminOrOwner = | ||
| (await isTeamAdmin(user.id, booking.eventType?.teamId ?? 0)) && | ||
| (await isTeamOwner(user.id, booking.eventType?.teamId ?? 0)); |
There was a problem hiding this comment.
Fix incorrect boolean logic for team admin/owner check.
The current logic requires a user to be both admin AND owner, which is incorrect. It should check if the user is either admin OR owner.
const isTeamAdminOrOwner =
- (await isTeamAdmin(user.id, booking.eventType?.teamId ?? 0)) &&
- (await isTeamOwner(user.id, booking.eventType?.teamId ?? 0));
+ (await isTeamAdmin(user.id, booking.eventType?.teamId ?? 0)) ||
+ (await isTeamOwner(user.id, booking.eventType?.teamId ?? 0));🤖 Prompt for AI Agents
In packages/trpc/server/routers/viewer/bookings/addGuests.handler.ts around
lines 46 to 48, the boolean logic incorrectly requires the user to be both a
team admin and owner by using the AND operator. Change the operator to OR so
that the check passes if the user is either a team admin or a team owner.
| try { | ||
| await sendAddGuestsEmails(evt, guests); | ||
| } catch (err) { | ||
| console.log("Error sending AddGuestsEmails"); | ||
| } |
There was a problem hiding this comment.
🛠️ Refactor suggestion
Improve error handling for email sending.
The current error handling only logs a generic message without error details. Consider logging the actual error and potentially notifying the user if email sending fails.
try {
await sendAddGuestsEmails(evt, guests);
} catch (err) {
- console.log("Error sending AddGuestsEmails");
+ console.error("Error sending AddGuestsEmails:", err);
+ // Consider whether to throw the error or return a warning to the user
}📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| try { | |
| await sendAddGuestsEmails(evt, guests); | |
| } catch (err) { | |
| console.log("Error sending AddGuestsEmails"); | |
| } | |
| try { | |
| await sendAddGuestsEmails(evt, guests); | |
| } catch (err) { | |
| console.error("Error sending AddGuestsEmails:", err); | |
| // Consider whether to throw the error or return a warning to the user | |
| } |
🤖 Prompt for AI Agents
In packages/trpc/server/routers/viewer/bookings/addGuests.handler.ts around
lines 167 to 171, the catch block for sendAddGuestsEmails only logs a generic
message without the error details. Update the catch block to log the actual
error object to provide more context. Additionally, consider adding user
notification logic to inform them if the email sending fails.
|
|
||
| function MultiEmail({ value, readOnly, label, setValue, placeholder }: MultiEmailProps) { | ||
| const { t } = useLocale(); | ||
| value = value || []; |
There was a problem hiding this comment.
Avoid reassigning function parameters.
Reassigning the value parameter can lead to confusion and is considered an anti-pattern. Use a default parameter or create a new variable instead.
-function MultiEmail({ value, readOnly, label, setValue, placeholder }: MultiEmailProps) {
+function MultiEmail({ value = [], readOnly, label, setValue, placeholder }: MultiEmailProps) {
const { t } = useLocale();
- value = value || [];🤖 Prompt for AI Agents
In packages/ui/form/MultiEmail.tsx at line 14, avoid reassigning the function
parameter `value`. Instead, set a default value for the parameter in the
function signature or assign `value` to a new variable initialized to an empty
array if it is falsy. This prevents confusion and adheres to best practices.
|
This PR is being marked as stale due to inactivity. |
Test 10
Summary by CodeRabbit
New Features
Bug Fixes
Documentation