diff --git a/client/src/features/adminDashboard/EventCard.js b/client/src/features/adminDashboard/EventCard.js
new file mode 100644
index 0000000..1a1c975
--- /dev/null
+++ b/client/src/features/adminDashboard/EventCard.js
@@ -0,0 +1,21 @@
+import React from "react";
+
+function EventCard({ event }) {
+ return (
+
+
+ -
+ {event.title}
+
+ - {event.description}
+
+ - Scheduled by: {event.user?.displayName || "UNKNOWN"}
+
+
+ );
+}
+
+export default EventCard;
diff --git a/client/src/features/adminDashboard/GroupEventCard.js b/client/src/features/adminDashboard/GroupEventCard.js
new file mode 100644
index 0000000..8bf4282
--- /dev/null
+++ b/client/src/features/adminDashboard/GroupEventCard.js
@@ -0,0 +1,12 @@
+import React from "react";
+import EventCard from "features/adminDashboard/EventCard";
+
+function GroupEventCard({ events }) {
+ return (
+
+ );
+}
+
+export default GroupEventCard;
diff --git a/client/src/pages/AdminDashboard.js b/client/src/pages/AdminDashboard.js
index c1ab614..60760de 100644
--- a/client/src/pages/AdminDashboard.js
+++ b/client/src/pages/AdminDashboard.js
@@ -2,15 +2,39 @@ import React from "react";
import { useEffect, useState } from "react";
import "index.css";
import DataService from "services/dataService";
+import EventCard from "features/adminDashboard/EventCard";
+import GroupEventCard from "features/adminDashboard/GroupEventCard";
export const AdminDashboard = () => {
- const [allEvents, setAllEvents] = useState({});
+ const [singleEvents, setSingleEvents] = useState({});
+ const [groupEvents, setGroupEvents] = useState({});
const [loading, setLoading] = useState(false);
const handleGetAllEvents = async () => {
setLoading(true);
const response = await DataService.getAll();
- setAllEvents(response.data);
+
+ // Filter only single events
+ setSingleEvents(response.data.filter(e => e.groupId === null));
+
+ // Group remaining events by groupId
+ let gEvents = response.data.reduce((map, event) => {
+ // Ignore events without a groupId
+ if (event.groupId === null) {
+ return map;
+ }
+
+ // Collect events with a groupId
+ if (event.groupId in map) {
+ map[event.groupId].push(event);
+ } else {
+ map[event.groupId] = [event];
+ }
+
+ return map;
+ }, {});
+
+ setGroupEvents(gEvents);
setLoading(false);
};
@@ -20,7 +44,15 @@ export const AdminDashboard = () => {
fetch();
}, []);
- let eventStatus = loading ? "LOADING..." : Object.keys(allEvents).length;
+ const singleEventCount = loading
+ ? "LOADING..."
+ : Object.keys(singleEvents).length;
+ const groupEventCount = loading
+ ? "LOADING..."
+ : Object.keys(groupEvents).length;
+ const eventStatus = loading
+ ? "LOADING..."
+ : singleEventCount + groupEventCount;
return (
@@ -35,29 +67,41 @@ export const AdminDashboard = () => {
🗘 Refresh Events
-
- {/* Display all events in an unordered list when the data is finished loading */}
- {loading ||
- Object.keys(allEvents).map(key => {
- return (
-
-
- -
- {allEvents[key].title}
-
- -
- {allEvents[key].description}
-
-
- - Scheduled by: {allEvents[key].user.displayName}
-
-
- );
- })}
-
+
+
+ Singular Events (
+ {singleEventCount})
+
+
+
+ {/* Display all events in an unordered list when the data is finished loading */}
+ {loading ||
+ Object.keys(singleEvents).map(key => {
+ return (
+
+ );
+ })}
+
+
+
+ Recurring Events (
+ {groupEventCount})
+
+
+ {loading ||
+ Object.keys(groupEvents).map(key => {
+ return (
+
+ );
+ })}
+
+
);
};
diff --git a/cypress/e2e/admin-dashboard.cy.js b/cypress/e2e/admin-dashboard.cy.js
index cb7262a..d8b8539 100644
--- a/cypress/e2e/admin-dashboard.cy.js
+++ b/cypress/e2e/admin-dashboard.cy.js
@@ -2,33 +2,64 @@
import tgt from "../support/tgt";
-// Generate test event
const TEST_EVENT_AUTHOR = "100_DEVER";
const TEST_EVENT_GROUP_ID = "admin_text_event_id";
-const TEST_EVENTS = [
+
+// Generate singular test events
+const SINGULAR_TEST_EVENTS = [
{
- title: "1st EVENT",
- description: "1st Description",
+ title: "1st SINGULAR EVENT",
+ description: "1st Singular Description",
location: "1st Location",
- groupId: TEST_EVENT_GROUP_ID,
+ groupId: null,
startAt: Date.now(),
- endAt: Date.now() + 3600,
+ endAt: Date.now() + 60 * 60,
},
{
- title: "2nd EVENT",
- description: "2nd Description",
+ title: "2nd SINGULAR EVENT",
+ description: "2nd Singular Description",
location: "2nd Location",
+ groupId: null,
+ startAt: Date.now() + 60 * 60,
+ endAt: Date.now() + 60 * 60 * 2,
+ },
+];
+
+// Generate group (recurring) test events
+const GROUP_TEST_EVENTS = [
+ {
+ title: "1st GROUP EVENT",
+ description: "1st Group Description",
+ location: "1st Location",
groupId: TEST_EVENT_GROUP_ID,
startAt: Date.now(),
- endAt: Date.now() + 3600,
+ endAt: Date.now() + 60 * 60,
+ },
+ {
+ title: "1st GROUP EVENT",
+ description: "1st Group Description",
+ location: "1st Location",
+ groupId: TEST_EVENT_GROUP_ID,
+ startAt: Date.now() + 60 * 60 * 24,
+ endAt: Date.now() + 60 * 60 * 24 + 3600,
},
];
-const addTestEvents = (test_events = TEST_EVENTS) => {
- cy.url().then(return_url => {
- cy.visit("/");
+// Calculate group event counts (how many times a group event recurs)
+// indexed by its groupId property
+const GROUP_EVENT_COUNTS_BY_GROUP_ID = GROUP_TEST_EVENTS.reduce(
+ (acc, event) => {
+ acc[event.groupId] = acc[event.groupId] || 0;
+ return { [event.groupId]: acc[event.groupId] + 1, ...acc };
+ },
+ {}
+);
+// Utility function to add test events consistently before each test
+const addTestEvents = test_events => {
+ cy.url().then(return_url => {
// Generate test events
+ cy.visit("/");
cy.createOwnEvents(TEST_EVENT_AUTHOR, ...test_events);
// Close event creation modal
@@ -53,68 +84,81 @@ describe("Admin Dashboard", () => {
cy.get("h1").contains("Admin Dashboard");
});
- describe("Add Events", () => {
- before(() => {
- // Navigate to admin dashboard
- cy.visit("/admindashboard");
- cy.get("#events").should("exist");
+ describe("Page updates when events change", () => {
+ describe("Add events", () => {
+ before(() => {
+ // Navigate to admin dashboard
+ cy.visit("/admindashboard");
+ cy.get("#events").should("exist");
- // Create alias for initial event count
- aliasHeaderEventCount("initialEventCount");
- });
- beforeEach(() => {
- // Add test events
- addTestEvents(TEST_EVENTS);
+ // Create alias for initial event count
+ aliasHeaderEventCount("initialEventCount");
+ });
+ beforeEach(() => {
+ // Add test events
+ addTestEvents([...SINGULAR_TEST_EVENTS, ...GROUP_TEST_EVENTS]);
- // Navigate to admin dashboard
- cy.visit("/admindashboard");
- });
+ // Navigate to admin dashboard
+ cy.visit("/admindashboard");
+ });
- it("Event titles and descriptions", () => {
- TEST_EVENTS.forEach((test_event, i) => {
- // Ensure test events rendered with correct title
- cy.get("#events")
- .find(`.event:nth-of-type(${i + 1}) li:nth-of-type(1)`)
- .invoke("text")
- .should("contain", test_event.title);
-
- // Ensure test event rendered with correct description
- cy.get("#events")
- .find(`.event:nth-of-type(${i + 1}) li:nth-of-type(2)`)
- .invoke("text")
- .should("contain", test_event.description);
+ it("Single event titles and descriptions", () => {
+ SINGULAR_TEST_EVENTS.forEach((test_event, i) => {
+ // Ensure test events rendered with correct title
+ cy.get("#events")
+ .find(`.single-event:nth-of-type(${i + 1}) li:nth-of-type(1)`)
+ .invoke("text")
+ .should("contain", test_event.title);
+
+ // Ensure test event rendered with correct description
+ cy.get("#events")
+ .find(`.single-event:nth-of-type(${i + 1}) li:nth-of-type(2)`)
+ .invoke("text")
+ .should("contain", test_event.description);
+ });
});
- });
- it("Event Counts", () => {
- // TODO: there should be a better way to wait for a React re-render to update the
- // event count, but a manual wait works currently
- cy.wait(100);
-
- // Create alias for updated event count
- aliasHeaderEventCount("newEventCount");
-
- cy.get("h1")
- .should("be.visible")
- .should("not.contain", "LOADING")
- .then(function () {
- // Ensure updated event count is 1 greater then initial event count
- expect(this.newEventCount).to.eq(
- this.initialEventCount + TEST_EVENTS.length
- );
+ it("Group event titles and descriptions", () => {
+ GROUP_TEST_EVENTS.forEach((test_event, i) => {
+ // Ensure test events rendered with correct title and description
+ cy.get("#events")
+ .find(`.group-event`)
+ .invoke("text")
+ .should("contain", test_event.title)
+ .should("contain", test_event.description);
});
+ });
+
+ it("Total event counts", () => {
+ // TODO: there should be a better way to wait for a React re-render to update the
+ // event counts, but a manual wait works currently. This applies to other similar
+ // manual waits in other admin dashboard tests.
+ cy.wait(100);
+
+ // Create alias for updated event count
+ aliasHeaderEventCount("newEventCount");
+
+ cy.get("h1")
+ .should("be.visible")
+ .should("not.contain", "LOADING")
+ .then(function () {
+ // Ensure updated event count is 1 greater then initial event count
+ expect(this.newEventCount).to.eq(
+ this.initialEventCount +
+ SINGULAR_TEST_EVENTS.length +
+ Object.entries(GROUP_EVENT_COUNTS_BY_GROUP_ID).length
+ );
+ });
+ });
});
- });
- describe("Page updates when events change", () => {
describe("Delete Events", () => {
before(() => {});
beforeEach(() => {
// Add test events
- addTestEvents(TEST_EVENTS);
+ addTestEvents([...SINGULAR_TEST_EVENTS, ...GROUP_TEST_EVENTS]);
// Reload page and navigate to admin dashboard
- // cy.reload();
cy.visit("/admindashboard");
cy.wait(100);
@@ -122,32 +166,75 @@ describe("Admin Dashboard", () => {
aliasHeaderEventCount("initialEventCount");
});
- it("Event Counts", () => {
- // TODO: there should be a better way to wait for a React re-render to update the
- // event count, but a manual wait works currently
+ it("Delete singular events", () => {
+ // Delete all singular events by id
+ cy.getAllEvents().then(({ body }) => {
+ body.forEach(event => {
+ if (!event.groupId) {
+ cy.deleteOwnEvent(event._id);
+ }
+ });
+ });
+
+ cy.visit("/admindashboard");
cy.wait(100);
+ // Create alias for updated event count
+ aliasHeaderEventCount("newEventCount");
+
+ cy.get("h1")
+ .should("be.visible")
+ .should("not.contain", "LOADING")
+ .then(function () {
+ // Ensure updated event count matches initial count minus deleted events
+ expect(this.newEventCount).to.eq(
+ this.initialEventCount - SINGULAR_TEST_EVENTS.length
+ );
+ });
+ });
+
+ it("Delete group events", () => {
+ // Delete all group events
cy.deleteOwnGroupEvents(TEST_EVENT_AUTHOR, TEST_EVENT_GROUP_ID);
+ cy.visit("/admindashboard");
+ cy.wait(100);
+
// Create alias for updated event count
aliasHeaderEventCount("newEventCount");
- cy.visit("/admindashboard");
cy.get("h1")
.should("be.visible")
.should("not.contain", "LOADING")
.then(function () {
// Ensure updated event count matches initial count minus deleted events
expect(this.newEventCount).to.eq(
- this.initialEventCount - TEST_EVENTS.length
+ this.initialEventCount -
+ Object.entries(GROUP_EVENT_COUNTS_BY_GROUP_ID).length
);
});
});
it("Event titles and descriptions no longer exist", () => {
+ // Delete all singular events by id
+ cy.getAllEvents().then(({ body }) => {
+ body.forEach(event => {
+ if (!event.groupId) {
+ cy.deleteOwnEvent(event._id);
+ }
+ });
+ });
+
+ // Delete all group events
cy.deleteOwnGroupEvents(TEST_EVENT_AUTHOR, TEST_EVENT_GROUP_ID);
- // Ensure all events were deleted
- cy.get("#events").not("be.visible");
+
+ cy.wait(100);
+
+ // Ensure single events are no longer displayed
+ cy.get("#single-events .single-event").should("not.exist");
+
+ // Ensure group events are no longer displayed
+ cy.get("#group-events .group-event").should("not.exist");
});
});
});
diff --git a/cypress/support/commands.js b/cypress/support/commands.js
index 72128af..da6f3c3 100644
--- a/cypress/support/commands.js
+++ b/cypress/support/commands.js
@@ -42,3 +42,14 @@ Cypress.Commands.add("deleteOwnGroupEvents", (userCode, groupId) => {
cy.reload();
});
+
+Cypress.Commands.add("deleteOwnEvent", id => {
+ if (id) {
+ cy.request("DELETE", `/events/${id}`);
+ }
+});
+
+Cypress.Commands.add("getAllEvents", userCode => {
+ // cy.login(userCode)
+ return cy.request("GET", "/events");
+});