Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Update a single meeting occurrence #271

Merged
merged 17 commits into from
Sep 20, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/dry-coins-fetch.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@nordeck/matrix-meetings-widget': minor
---

Support editing a single occurrence of a recurring meeting.
1 change: 1 addition & 0 deletions e2e/src/meetingReaper.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -144,6 +144,7 @@ test.describe('Meeting Reaper', () => {
);

const aliceEditMeetingWidgetPage = await meeting.editMeeting();
await aliceEditMeetingWidgetPage.toggleRecurringEdit();
await aliceEditMeetingWidgetPage.setStart([2040, 10, 4], '11:00 AM');
await aliceEditMeetingWidgetPage.submit();
await aliceElementWebPage.approveWidgetIdentity();
Expand Down
16 changes: 12 additions & 4 deletions e2e/src/pages/scheduleMeetingWidgetPage.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ export class ScheduleMeetingWidgetPage {
public readonly afterMeetingCountRadio: Locator;
public readonly afterMeetingCountSpinbutton: Locator;
public readonly customRecurrenceRuleGroup: Locator;
public readonly switchRecurringEdit: Locator;

constructor(
private readonly page: Page,
Expand All @@ -37,6 +38,9 @@ export class ScheduleMeetingWidgetPage {
const dialog = this.page.getByRole('dialog', {
name: /Schedule Meeting|Edit Meeting/,
});
this.switchRecurringEdit = widget.getByRole('checkbox', {
name: 'Edit the recurring meeting series',
});
this.submitMeetingButton = dialog.getByRole('button', {
name: /Create Meeting|Save/,
});
Expand Down Expand Up @@ -87,17 +91,21 @@ export class ScheduleMeetingWidgetPage {
);
}

async toggleRecurringEdit() {
await this.switchRecurringEdit.click();
}

async toggleChatPermission() {
await this.allowMessagingCheckbox.click();
}

async addParticipant(name: string) {
await this.participantsCombobox.type(name);
await this.widget.getByRole('option', { name }).waitFor();
await this.participantsCombobox.press('ArrowDown');
await this.participantsCombobox.press('Enter');
}

async toggleChatPermission() {
await this.allowMessagingCheckbox.click();
}

async addWidget(widgetName: string) {
await this.widgetsCombobox.click();
await this.widget.getByRole('option', { name: widgetName }).click();
Expand Down
46 changes: 44 additions & 2 deletions e2e/src/recurringMeetings.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -149,6 +149,7 @@ test.describe('Recurring Meetings', () => {
);

const aliceEditMeetingWidgetPage = await meeting.editMeeting();
await aliceEditMeetingWidgetPage.toggleRecurringEdit();
await aliceEditMeetingWidgetPage.setStart([2040, 10, 4], '11:00 AM');
await aliceEditMeetingWidgetPage.selectRecurrence('custom');
await aliceEditMeetingWidgetPage.selectRecurrenceFrequency('weeks');
Expand Down Expand Up @@ -187,6 +188,48 @@ test.describe('Recurring Meetings', () => {
).toBeVisible();
});

test('should edit one instance of the recurring meeting', async ({
aliceMeetingsWidgetPage,
aliceElementWebPage,
}) => {
await aliceMeetingsWidgetPage.setDateFilter([2040, 10, 1], [2040, 10, 9]);

const aliceScheduleMeetingWidgetPage =
await aliceMeetingsWidgetPage.scheduleMeeting();

await aliceScheduleMeetingWidgetPage.titleTextbox.fill('My Meeting');
await aliceScheduleMeetingWidgetPage.descriptionTextbox.fill(
'My Description',
);
await aliceScheduleMeetingWidgetPage.setStart([2040, 10, 3], '10:30 AM');
await aliceScheduleMeetingWidgetPage.selectRecurrence('daily');
await aliceScheduleMeetingWidgetPage.setEndAfterMeetingCount(5);
await aliceScheduleMeetingWidgetPage.submit();

const meeting = aliceMeetingsWidgetPage.getMeeting(
'My Meeting',
'10/03/2040',
);
await expect(meeting.meetingTimeRangeText).toHaveText(
'10:30 AM – 11:30 AM. Recurrence: Every day for 5 times',
);

const aliceEditMeetingWidgetPage = await meeting.editMeeting();
await aliceEditMeetingWidgetPage.setStart([2040, 10, 9], '10:40 AM');
await aliceEditMeetingWidgetPage.submit();
await aliceElementWebPage.approveWidgetIdentity();

await expect(
aliceMeetingsWidgetPage.getMeeting('My Meeting', '10/09/2040')
.meetingTimeRangeText,
).toHaveText('10:40 AM – 11:40 AM. Recurrence: Every day for 5 times');

await expect(
aliceMeetingsWidgetPage.getMeeting('My Meeting', '10/04/2040')
.meetingTimeRangeText,
).toHaveText('10:30 AM – 11:30 AM. Recurrence: Every day for 5 times');
});

test('should covert a recurring meeting into a single meeting', async ({
aliceMeetingsWidgetPage,
aliceElementWebPage,
Expand Down Expand Up @@ -214,6 +257,7 @@ test.describe('Recurring Meetings', () => {
);

const aliceEditMeetingWidgetPage = await meeting.editMeeting();
await aliceEditMeetingWidgetPage.toggleRecurringEdit();
await aliceEditMeetingWidgetPage.selectRecurrence('no repetition');
await aliceEditMeetingWidgetPage.submit();
await aliceElementWebPage.approveWidgetIdentity();
Expand Down Expand Up @@ -294,8 +338,6 @@ test.describe('Recurring Meetings', () => {
).toBeVisible();
});

// TODO: Edit single meeting

// TODO: Edit starting from

test('should delete recurring meeting', async ({
Expand Down
8 changes: 4 additions & 4 deletions matrix-meetings-widget/public/locales/de/translation.json
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,10 @@
"save": "Speichern",
"title": "Bearbeiten"
},
"editRecurringMessage": {
"message": "Alle Instanzen der Besprechungsserie können bearbeitet werden",
"titleOne": "Wiederkehrende Besprechungsserie bearbeiten"
},
"invitedMeetingList": {
"detail": {
"invitedBy": "Einladung von: {{name}}"
Expand Down Expand Up @@ -364,10 +368,6 @@
"hasPowerToKickUser": "Du hast nicht die Berechtigung um diesen Nutzer aus der Besprechung zu entfernen.",
"meetingAlreadyStarted": "Die Besprechung ist bereits gestartet.",
"participants": "Teilnehmer",
"recurringMeetingMessage": {
"message": "Alle Wiederholungen des Termins werden bearbeitet.",
"title": "Du bearbeitest einen Serientermin"
},
"startAt": "Startzeitpunkt",
"title": "Titel (erforderlich)",
"titleHelperText": "Ein Titel ist erforderlich",
Expand Down
8 changes: 4 additions & 4 deletions matrix-meetings-widget/public/locales/en/translation.json
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,10 @@
"save": "Save",
"title": "Edit Meeting"
},
"editRecurringMessage": {
"message": "All instances of the recurring meeting are editable",
"titleOne": "Edit the recurring meeting series"
},
"invitedMeetingList": {
"detail": {
"invitedBy": "Invited By: {{name}}"
Expand Down Expand Up @@ -367,10 +371,6 @@
"hasPowerToKickUser": "You don't have the permission to remove this participant.",
"meetingAlreadyStarted": "The meeting already started.",
"participants": "Participants",
"recurringMeetingMessage": {
"message": "All instances of the recurring meeting are edited",
"title": "You are editing a recurring meeting"
},
"startAt": "Start at",
"title": "Title (required)",
"titleHelperText": "A title is required",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -36,9 +36,9 @@ describe('<MeetingNotEndedGuard/>', () => {
<MeetingNotEndedGuard
meeting={mockMeeting({
content: {
startTime: '2000-01-02T01:00:00Z',
endTime: '2000-01-02T02:00:00Z',
recurrenceId: '2000-01-02T01:00:00Z',
startTime: '2000-01-01T01:00:00Z',
endTime: '2000-01-01T02:00:00Z',
recurrenceId: undefined,
calendarEntries: [
mockCalendarEntry({
dtstart: '20000101T010000',
Expand All @@ -64,7 +64,7 @@ describe('<MeetingNotEndedGuard/>', () => {
content: {
startTime: '2000-01-02T01:00:00Z',
endTime: '2000-01-02T02:00:00Z',
recurrenceId: '2000-01-02T01:00:00Z',
recurrenceId: undefined,
calendarEntries: [
mockCalendarEntry({
dtstart: '20000101T010000',
Expand Down Expand Up @@ -115,7 +115,7 @@ describe('<MeetingNotEndedGuard/>', () => {
content: {
startTime: '2000-01-02T01:00:00Z',
endTime: '2000-01-02T02:00:00Z',
recurrenceId: '2000-01-02T01:00:00Z',
recurrenceId: undefined,
calendarEntries: [
mockCalendarEntry({
dtstart: '20000101T010000',
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ import React, {
ReactNode,
} from 'react';
import { useTranslation } from 'react-i18next';
import { getCalendarEnd } from '../../../lib/utils';
import { getCalendarEvent } from '../../../lib/utils';
import { isMeetingBreakOutRoom, Meeting } from '../../../reducer/meetingsApi';
import { useUpdateOnDate } from '../hooks';

Expand Down Expand Up @@ -56,13 +56,19 @@ export function MeetingNotEndedGuard({
}: MeetingNotEndedGuardProps): ReactElement {
const { t } = useTranslation();

const endTime = meeting ? getCalendarEnd(meeting.calendarEntries) : undefined;
const endTime = meeting
? getCalendarEvent(
meeting.calendarEntries,
meeting.calendarUid,
meeting?.recurrenceId,
)?.endTime
: undefined;

// Make sure this component is rerendered (and the now below re-evaluated)
// after the meeting ended.
useUpdateOnDate(endTime?.toJSDate());
useUpdateOnDate(endTime);

if (meeting && endTime && DateTime.now() >= endTime) {
if (meeting && endTime && DateTime.now() >= DateTime.fromISO(endTime)) {
if (!withMessage) {
return <React.Fragment />;
}
Expand All @@ -81,7 +87,12 @@ export function MeetingNotEndedGuard({

const isBreakOutRoom = isMeetingBreakOutRoom(meeting.type);
return (
<Alert icon={<AccessTimeIcon />} role="status" severity="info">
<Alert
icon={<AccessTimeIcon />}
role="status"
severity="info"
sx={{ mx: 1 }}
>
{isBreakOutRoom
? t(
'meetingViewEditMeeting.breakoutSessionIsOver',
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ describe('useUpdateOnDate', () => {
});

it('should rerender at date', () => {
function Component({ date }: { date: Date }) {
function Component({ date }: { date: string }) {
useUpdateOnDate(date);

return <div>{new Date(Date.now()).toISOString()}</div>;
Expand All @@ -33,7 +33,7 @@ describe('useUpdateOnDate', () => {
jest.useFakeTimers();
jest.setSystemTime(new Date('2022-12-07T20:30:00.000Z'));

render(<Component date={new Date('2022-12-07T20:31:00.000Z')}></Component>);
render(<Component date={'2022-12-07T20:31:00.000Z'}></Component>);

expect(screen.getByText('2022-12-07T20:30:00.000Z')).toBeInTheDocument();

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,14 +14,15 @@
* limitations under the License.
*/

import { DateTime } from 'luxon';
import { useEffect } from 'react';
import { useUpdate } from 'react-use';

/**
* Triggers to update a component after date.
* @param date
*/
export function useUpdateOnDate(date: Date | undefined): void {
export function useUpdateOnDate(date: string | undefined): void {
const update = useUpdate();

// trigger an update when the meeting ends
Expand Down Expand Up @@ -50,7 +51,7 @@ export function useUpdateOnDate(date: Date | undefined): void {
return () => {};
}

schedule(date.getTime());
schedule(+DateTime.fromISO(date));

return () => {
clearTimeout(timeoutRef);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -55,9 +55,9 @@ export function withCurrentRoomMeeting<T>(
const NewComponent = useMemo(() => withRoomIdMeeting(WrappedComponent), []);

// As the meeting is the current instance of a recurring meeting, we have
// to reevaluate the meeting everytime the end of the meeting instance is
// to reevaluate the meeting every time the end of the meeting instance is
// reached!
useUpdateOnDate(entry ? new Date(entry.endTime) : undefined);
useUpdateOnDate(entry?.endTime);

return (
<NewComponent
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -35,10 +35,9 @@ import {
Meeting,
makeSelectRoomPermissions,
selectNordeckMeetingMetadataEventByRoomId,
selectRoomPowerLevelsEventByRoomId,
useCloseMeetingMutation,
} from '../../../reducer/meetingsApi';
import { useAppSelector } from '../../../store';
import { useAppDispatch, useAppSelector } from '../../../store';
import { ConfirmDeleteDialog } from '../../common/ConfirmDeleteDialog';
import { withoutYearDateFormat } from '../../common/DateTimePickers';
import {
Expand All @@ -47,7 +46,7 @@ import {
OpenXchangeMenuButtonItem,
getOpenXChangeExternalReference,
} from '../../common/MenuButton';
import { useEditMeeting } from '../ScheduleMeetingModal';
import { editMeetingThunk } from '../ScheduleMeetingModal';
import { ScheduledDeletionWarning } from './ScheduledDeletionWarning';

type MeetingCardMenuProps = {
Expand Down Expand Up @@ -143,20 +142,14 @@ export function MeetingCardMenu({
widgetApi,
]);

const { editMeeting } = useEditMeeting();

const isMessagingEnabled = useAppSelector((state) => {
const event = selectRoomPowerLevelsEventByRoomId(state, meeting.meetingId);
return event?.content.events_default === 0;
});

const dispatch = useAppDispatch();
const handleClickEditMeeting = useCallback(async () => {
try {
await editMeeting(meeting, isMessagingEnabled);
await dispatch(editMeetingThunk(meeting)).unwrap();
} catch {
setShowErrorDialog(true);
}
}, [editMeeting, meeting, isMessagingEnabled]);
}, [dispatch, meeting]);

if (!canUpdateMeeting || !canCloseMeeting) {
// If the menu would be empty, skip it
Expand Down
Loading
Loading