From 40457f254c966238e76f7915903ff0689e4bac99 Mon Sep 17 00:00:00 2001 From: Lynnette Ong <103313573+lynnetteeee@users.noreply.github.com> Date: Fri, 29 Mar 2024 21:42:22 +0800 Subject: [PATCH] Improve UX of bookings page (#35) * Freeze action column and remove heading * Change dropdown arrow to view booking details and add tooltip * Allow expand details when clicking on row * Fix double loading bug when clicking expand icon * Update booking table columns * Change to pointer cursor for booking table hover * Update booking-details-view.tsx Add row in booking details tab to show Booking ID * Adjust admin and user booking table columns * Remove comment --------- Co-authored-by: Jonathan Loh --- .../booking-admin-table.tsx | 55 +++++++++++------- .../booking-base-table.module.scss | 5 ++ .../booking-base-table/booking-base-table.tsx | 46 ++++++++++----- .../booking-details-view.tsx | 8 ++- .../booking-user-table.module.scss | 4 ++ .../booking-user-table/booking-user-table.tsx | 57 +++++++++++-------- .../src/components/table/table.module.scss | 2 +- frontend/src/components/table/table.tsx | 21 ++++--- .../user-email-renderer.tsx | 1 + .../user-name-renderer/user-name-renderer.tsx | 1 + frontend/src/constants/index.ts | 4 +- frontend/src/utils/transform-utils.ts | 20 +++++++ 12 files changed, 152 insertions(+), 72 deletions(-) create mode 100644 frontend/src/components/booking-user-table/booking-user-table.module.scss diff --git a/frontend/src/components/booking-admin-table/booking-admin-table.tsx b/frontend/src/components/booking-admin-table/booking-admin-table.tsx index 1150a92..b5bce4c 100644 --- a/frontend/src/components/booking-admin-table/booking-admin-table.tsx +++ b/frontend/src/components/booking-admin-table/booking-admin-table.tsx @@ -6,14 +6,14 @@ import { BOOKER, CREATED_AT, CREATED_AT_STRING, + DATE_FORMAT, EMAIL, - END_DATE_TIME, - END_DATE_TIME_STRING, + EVENT_DATE, + EVENT_TIME_RANGE, ID, NAME, - START_DATE_TIME, - START_DATE_TIME_STRING, STATUS, + TITLE, VENUE, } from "../../constants"; import useTableState, { @@ -24,7 +24,7 @@ import { selectAllBookings, selectBookingsLoadingState, } from "../../redux/slices/bookings-slice"; -import { displayDateTime } from "../../utils/transform-utils"; +import { displayDateTime, displayTimeRange } from "../../utils/transform-utils"; import BookingBaseTable, { BookingViewProps } from "../booking-base-table"; import PlaceholderWrapper from "../placeholder-wrapper"; import SearchBar from "../search-bar"; @@ -40,9 +40,9 @@ const BOOKING_ADMIN_TABLE_STATE_OPTIONS: TableStateOptions = { ID, BOOKER_NAME, BOOKER_EMAIL, + EVENT_DATE, + EVENT_TIME_RANGE, VENUE_NAME, - START_DATE_TIME_STRING, - END_DATE_TIME_STRING, CREATED_AT_STRING, STATUS, ], @@ -56,8 +56,11 @@ function BookingAdminTable() { () => allBookings.map((booking) => ({ ...booking, - [START_DATE_TIME_STRING]: displayDateTime(booking.startDateTime), - [END_DATE_TIME_STRING]: displayDateTime(booking.endDateTime), + [EVENT_DATE]: displayDateTime(booking.startDateTime, DATE_FORMAT), + [EVENT_TIME_RANGE]: displayTimeRange( + booking.startDateTime, + booking.endDateTime, + ), [CREATED_AT_STRING]: displayDateTime(booking.createdAt), children: [{ [ID]: `${booking.id}-details`, booking }], })), @@ -100,16 +103,24 @@ function BookingAdminTable() { key={ID} dataKey={ID} title="ID" - width={60} + width={70} resizable sortable align="center" /> + + key={TITLE} + dataKey={TITLE} + title="Booking Title" + width={210} + resizable + sortable + /> key={BOOKER_NAME} dataKey={BOOKER} title="Name" - width={150} + width={90} resizable sortable cellRenderer={UserNameRenderer} @@ -118,7 +129,7 @@ function BookingAdminTable() { key={BOOKER_EMAIL} dataKey={BOOKER} title="Email" - width={180} + width={130} resizable sortable cellRenderer={UserEmailRenderer} @@ -127,23 +138,23 @@ function BookingAdminTable() { key={VENUE_NAME} dataKey={VENUE_NAME} title="Venue" - width={180} + width={190} resizable sortable /> - key={START_DATE_TIME} - dataKey={START_DATE_TIME_STRING} - title="Start" - width={160} + key={EVENT_DATE} + dataKey={EVENT_DATE} + title="Date" + width={100} resizable sortable /> - key={END_DATE_TIME} - dataKey={END_DATE_TIME_STRING} - title="End" - width={160} + key={EVENT_TIME_RANGE} + dataKey={EVENT_TIME_RANGE} + title="Time" + width={150} resizable sortable /> @@ -151,7 +162,7 @@ function BookingAdminTable() { key={CREATED_AT} dataKey={CREATED_AT_STRING} title="Created at" - width={160} + width={165} resizable sortable /> diff --git a/frontend/src/components/booking-base-table/booking-base-table.module.scss b/frontend/src/components/booking-base-table/booking-base-table.module.scss index 4a3f77f..3f39663 100644 --- a/frontend/src/components/booking-base-table/booking-base-table.module.scss +++ b/frontend/src/components/booking-base-table/booking-base-table.module.scss @@ -2,6 +2,11 @@ .bookingBaseTable { @include base.table; + cursor: pointer; + + div { + box-shadow: none; + } .extraContentContainer { width: 100%; diff --git a/frontend/src/components/booking-base-table/booking-base-table.tsx b/frontend/src/components/booking-base-table/booking-base-table.tsx index 3ee521c..a3ab959 100644 --- a/frontend/src/components/booking-base-table/booking-base-table.tsx +++ b/frontend/src/components/booking-base-table/booking-base-table.tsx @@ -1,13 +1,13 @@ -import { useCallback } from "react"; -import { AutoResizer, Column, ColumnShape } from "react-base-table"; +import { useCallback, useState } from "react"; +import { AutoResizer, Column, ColumnShape, RowKey } from "react-base-table"; import { Segment } from "semantic-ui-react"; import { ACTION, CREATED_AT_STRING, - END_DATE_TIME_STRING, + EVENT_DATE, + EVENT_TIME_RANGE, ID, - START_DATE_TIME_STRING, STATUS, } from "../../constants"; import { useGetSingleBooking } from "../../custom-hooks/api/bookings-api"; @@ -20,11 +20,11 @@ import Table, { TableProps } from "../table"; import styles from "./booking-base-table.module.scss"; export type BookingViewProps = BookingData & { - [START_DATE_TIME_STRING]: string; - [END_DATE_TIME_STRING]: string; + [EVENT_DATE]: string; + [EVENT_TIME_RANGE]: string; [CREATED_AT_STRING]: string; booking?: BookingData; - children: [{ [ID]: string; booking: BookingData }]; + children: { [ID]: string; booking: BookingData }[]; }; type Props = Partial> & { @@ -34,12 +34,17 @@ type Props = Partial> & { }; const RowRenderer: TableProps["rowRenderer"] = ({ - // eslint-disable-next-line react/prop-types rowData: { booking }, - // eslint-disable-next-line react/prop-types cells, + columns, +}: { + rowData: BookingViewProps; + cells: React.ReactNode[]; + columns: ColumnShape; }) => - booking ? ( + // Only render details if there are booking details + // and the column is not the frozen column + booking && columns.length > 1 ? ( ([]); const onRowExpand: TableProps["onRowExpand"] = async ({ expanded, rowData: { id, formResponseData }, @@ -82,9 +88,7 @@ function BookingBaseTable({ if (!expanded || formResponseData) { return; } - const booking = await getSingleBooking(id); - if (booking) { dispatch(updateBookingsAction({ bookings: [booking] })); } @@ -102,6 +106,20 @@ function BookingBaseTable({ fixed expandColumnKey={ACTION} onRowExpand={onRowExpand} + expandedRowKeys={expandedRowKeys} + rowEventHandlers={{ + onClick: async ({ rowData, rowKey, rowIndex }) => { + if (!rowData.children || rowData.children.length === 0) return; + if (expandedRowKeys.includes(rowKey)) + setExpandedRowKeys( + expandedRowKeys.filter((x) => x !== rowKey), + ); + else { + setExpandedRowKeys([...expandedRowKeys, rowKey]); + onRowExpand({ expanded: true, rowData, rowIndex, rowKey }); + } + }, + }} {...props} > {children} @@ -116,10 +134,10 @@ function BookingBaseTable({ /> key={ACTION} - title="Action" + title="" width={defaultActionColumnWidth} align="center" - resizable + frozen="right" /> )} diff --git a/frontend/src/components/booking-details-view/booking-details-view.tsx b/frontend/src/components/booking-details-view/booking-details-view.tsx index 46f7264..df2cdc0 100644 --- a/frontend/src/components/booking-details-view/booking-details-view.tsx +++ b/frontend/src/components/booking-details-view/booking-details-view.tsx @@ -12,11 +12,17 @@ type Props = { function BookingDetailsView({ className, - booking: { title, formResponseData }, + booking: { id, title, formResponseData }, }: Props) { return ( + + + Booking ID: + + {id} + Booking title: diff --git a/frontend/src/components/booking-user-table/booking-user-table.module.scss b/frontend/src/components/booking-user-table/booking-user-table.module.scss new file mode 100644 index 0000000..ff98478 --- /dev/null +++ b/frontend/src/components/booking-user-table/booking-user-table.module.scss @@ -0,0 +1,4 @@ +.userTableCell, +.userTableHeader { + padding-left: 24px; +} diff --git a/frontend/src/components/booking-user-table/booking-user-table.tsx b/frontend/src/components/booking-user-table/booking-user-table.tsx index e634797..9685e3f 100644 --- a/frontend/src/components/booking-user-table/booking-user-table.tsx +++ b/frontend/src/components/booking-user-table/booking-user-table.tsx @@ -1,17 +1,18 @@ import { useMemo } from "react"; import { Column } from "react-base-table"; import { Segment } from "semantic-ui-react"; +import styles from "./booking-user-table.module.scss"; import { CREATED_AT, CREATED_AT_STRING, - END_DATE_TIME, - END_DATE_TIME_STRING, + DATE_FORMAT, + EVENT_DATE, + EVENT_TIME_RANGE, ID, NAME, - START_DATE_TIME, - START_DATE_TIME_STRING, STATUS, + TITLE, VENUE, } from "../../constants"; import useTableState, { @@ -23,7 +24,7 @@ import { selectBookingsLoadingState, } from "../../redux/slices/bookings-slice"; import { selectCurrentUserDisplayInfo } from "../../redux/slices/current-user-slice"; -import { displayDateTime } from "../../utils/transform-utils"; +import { displayDateTime, displayTimeRange } from "../../utils/transform-utils"; import BookingBaseTable, { BookingViewProps } from "../booking-base-table"; import PlaceholderWrapper from "../placeholder-wrapper"; import SearchBar from "../search-bar"; @@ -34,8 +35,8 @@ const BOOKING_ADMIN_TABLE_STATE_OPTIONS: TableStateOptions = { searchKeys: [ ID, VENUE_NAME, - START_DATE_TIME_STRING, - END_DATE_TIME_STRING, + EVENT_DATE, + EVENT_TIME_RANGE, CREATED_AT_STRING, STATUS, ], @@ -53,8 +54,11 @@ function BookingUserTable() { () => userBookings.map((booking) => ({ ...booking, - [START_DATE_TIME_STRING]: displayDateTime(booking.startDateTime), - [END_DATE_TIME_STRING]: displayDateTime(booking.endDateTime), + [EVENT_DATE]: displayDateTime(booking.startDateTime, DATE_FORMAT), + [EVENT_TIME_RANGE]: displayTimeRange( + booking.startDateTime, + booking.endDateTime, + ), [CREATED_AT_STRING]: displayDateTime(booking.createdAt), children: [{ [ID]: `${booking.id}-details`, booking }], })), @@ -91,16 +95,21 @@ function BookingUserTable() { sortBy={sortBy} setSortBy={setSortBy} defaultStatusColumnWidth={110} - defaultActionColumnWidth={150} + defaultActionColumnWidth={50} > - key={ID} - dataKey={ID} - title="ID" - width={80} + key={TITLE} + dataKey={TITLE} + title="Booking Title" + width={330} resizable sortable - align="center" + cellRenderer={({ cellData }) => ( +
{cellData}
+ )} + headerRenderer={({ column }) => ( +
{column.title}
+ )} /> key={VENUE_NAME} @@ -111,18 +120,18 @@ function BookingUserTable() { sortable /> - key={START_DATE_TIME} - dataKey={START_DATE_TIME_STRING} - title="Start" - width={230} + key={EVENT_DATE} + dataKey={EVENT_DATE} + title="Date" + width={150} resizable sortable /> - key={END_DATE_TIME} - dataKey={END_DATE_TIME_STRING} - title="End" - width={230} + key={EVENT_TIME_RANGE} + dataKey={EVENT_TIME_RANGE} + title="Time" + width={190} resizable sortable /> @@ -130,7 +139,7 @@ function BookingUserTable() { key={CREATED_AT} dataKey={CREATED_AT_STRING} title="Created at" - width={230} + width={210} resizable sortable /> diff --git a/frontend/src/components/table/table.module.scss b/frontend/src/components/table/table.module.scss index 4a92ee5..94dbb13 100644 --- a/frontend/src/components/table/table.module.scss +++ b/frontend/src/components/table/table.module.scss @@ -18,7 +18,7 @@ transform: rotate(0deg); &.expanded { - transform: rotate(-135deg); + transform: rotate(-180deg); } } } diff --git a/frontend/src/components/table/table.tsx b/frontend/src/components/table/table.tsx index 2a13919..c5c8f03 100644 --- a/frontend/src/components/table/table.tsx +++ b/frontend/src/components/table/table.tsx @@ -7,7 +7,7 @@ import BaseTable, { SortOrder, TableComponents, } from "react-base-table"; -import { Icon } from "semantic-ui-react"; +import { Icon, Popup } from "semantic-ui-react"; import { SortBy } from "../../custom-hooks/use-table-state"; import styles from "./table.module.scss"; @@ -24,18 +24,23 @@ const TABLE_COMPONENTS: TableComponents = { ), // eslint-disable-next-line react/prop-types - ExpandIcon: ({ expandable, expanded, onExpand }) => { + ExpandIcon: ({ expandable, expanded }) => { if (!expandable) { return null; } return ( - onExpand(!expanded)} - fitted + + } /> ); }, diff --git a/frontend/src/components/user-email-renderer/user-email-renderer.tsx b/frontend/src/components/user-email-renderer/user-email-renderer.tsx index 33a68fa..bc5d6a9 100644 --- a/frontend/src/components/user-email-renderer/user-email-renderer.tsx +++ b/frontend/src/components/user-email-renderer/user-email-renderer.tsx @@ -17,6 +17,7 @@ function UserEmailRenderer({ className={styles.userEmailRenderer} rel="noopener noreferrer" href={`mailto:${email}`} + onClick={(e) => e.stopPropagation()} > {email} diff --git a/frontend/src/components/user-name-renderer/user-name-renderer.tsx b/frontend/src/components/user-name-renderer/user-name-renderer.tsx index 67ccd4f..81bf84b 100644 --- a/frontend/src/components/user-name-renderer/user-name-renderer.tsx +++ b/frontend/src/components/user-name-renderer/user-name-renderer.tsx @@ -17,6 +17,7 @@ function UserNameRenderer({ cellData, rowData }: Props) { href={PROFILE_PATH.replace(`:${USER_ID}`, `${id}`)} target="_blank" rel="noopener noreferrer" + onClick={(e) => e.stopPropagation()} > {name} diff --git a/frontend/src/constants/index.ts b/frontend/src/constants/index.ts index 4af8bd3..03d5976 100644 --- a/frontend/src/constants/index.ts +++ b/frontend/src/constants/index.ts @@ -23,7 +23,9 @@ export const EMAIL_REGEX = export const EMAILS = "emails"; export const END_DATE_TIME = "endDateTime"; export const EVENT = "event"; +export const EVENT_DATE = "eventDate"; // added export const EVENT_ID = "eventId"; +export const EVENT_TIME_RANGE = "eventTimeRange"; // added export const LABEL = "label"; export const RESPONSE = "response"; export const TYPE = "type"; @@ -65,8 +67,6 @@ export const VENUE_ID = "venueId"; export const VENUE_NAME = "venueName"; export const START = "start"; export const END = "end"; -export const START_DATE_TIME_STRING = "startDateTimeString"; -export const END_DATE_TIME_STRING = "endDateTimeString"; export const PROFILE_IMAGE = "profileImage"; export const DEFAULT_ARRAY = []; export const FULL_DETAILS = "fullDetails"; diff --git a/frontend/src/utils/transform-utils.ts b/frontend/src/utils/transform-utils.ts index 10219c5..d124d6a 100644 --- a/frontend/src/utils/transform-utils.ts +++ b/frontend/src/utils/transform-utils.ts @@ -54,6 +54,26 @@ export function displayDateTime( } } +// displays time range given 2 date times +export function displayTimeRange( + inputStartDateTime: string | number | Date, + inputEndDateTime: string | number | Date, +) { + const startDateTime = + typeof inputStartDateTime === "string" + ? parseInt(inputStartDateTime, 10) + : inputStartDateTime; + const endDateTime = + typeof inputEndDateTime === "string" + ? parseInt(inputEndDateTime, 10) + : inputEndDateTime; + + return `${displayDateTime(startDateTime, TIME_FORMAT)} - ${displayDateTime( + endDateTime, + TIME_FORMAT, + )}`; +} + export function displayDateTimeRange( inputStartDateTime: string | number | Date, inputEndDateTime: string | number | Date,