From 9b4feeed591f9f9ffeb23c8ba06276a6be2ef1ee Mon Sep 17 00:00:00 2001 From: Alejandro Celaya Date: Fri, 21 Jun 2024 16:39:43 +0200 Subject: [PATCH] Mitigate word wrapping in date columns for dashboard activity tables (#6386) --- .../components/dashboard/AssignmentActivity.tsx | 10 ++++++---- .../components/dashboard/CourseActivity.tsx | 4 ++-- .../components/dashboard/FormattedDate.tsx | 17 +++++++++++++++++ .../dashboard/OrderableActivityTable.tsx | 2 +- .../dashboard/OrganizationActivity.tsx | 10 ++++++---- .../dashboard/test/AssignmentActivity-test.js | 5 +++++ .../dashboard/test/CourseActivity-test.js | 8 ++++---- .../dashboard/test/OrganizationActivity-test.js | 10 ++++++++-- 8 files changed, 49 insertions(+), 17 deletions(-) create mode 100644 lms/static/scripts/frontend_apps/components/dashboard/FormattedDate.tsx diff --git a/lms/static/scripts/frontend_apps/components/dashboard/AssignmentActivity.tsx b/lms/static/scripts/frontend_apps/components/dashboard/AssignmentActivity.tsx index 1b9e710b44..52f6d3b361 100644 --- a/lms/static/scripts/frontend_apps/components/dashboard/AssignmentActivity.tsx +++ b/lms/static/scripts/frontend_apps/components/dashboard/AssignmentActivity.tsx @@ -4,9 +4,9 @@ import { useParams } from 'wouter-preact'; import type { Assignment, StudentsResponse } from '../../api-types'; import { useConfig } from '../../config'; import { urlPath, useAPIFetch } from '../../utils/api'; -import { formatDateTime } from '../../utils/date'; import { replaceURLParams } from '../../utils/url'; import DashboardBreadcrumbs from './DashboardBreadcrumbs'; +import FormattedDate from './FormattedDate'; import OrderableActivityTable from './OrderableActivityTable'; type StudentsTableRow = { @@ -100,9 +100,11 @@ export default function AssignmentActivity() { case 'replies': return
{stats[field]}
; case 'last_activity': - return stats.last_activity - ? formatDateTime(new Date(stats.last_activity)) - : ''; + return stats.last_activity ? ( + + ) : ( + '' + ); case 'display_name': return ( stats.display_name ?? ( diff --git a/lms/static/scripts/frontend_apps/components/dashboard/CourseActivity.tsx b/lms/static/scripts/frontend_apps/components/dashboard/CourseActivity.tsx index 44b3fd59fa..42e149cb8d 100644 --- a/lms/static/scripts/frontend_apps/components/dashboard/CourseActivity.tsx +++ b/lms/static/scripts/frontend_apps/components/dashboard/CourseActivity.tsx @@ -5,9 +5,9 @@ import { Link as RouterLink, useParams } from 'wouter-preact'; import type { AssignmentsResponse, Course } from '../../api-types'; import { useConfig } from '../../config'; import { urlPath, useAPIFetch } from '../../utils/api'; -import { formatDateTime } from '../../utils/date'; import { replaceURLParams } from '../../utils/url'; import DashboardBreadcrumbs from './DashboardBreadcrumbs'; +import FormattedDate from './FormattedDate'; import OrderableActivityTable from './OrderableActivityTable'; type AssignmentsTableRow = { @@ -105,7 +105,7 @@ export default function CourseActivity() { } return ( - stats.last_activity && formatDateTime(new Date(stats.last_activity)) + stats.last_activity && ); }} navigateOnConfirmRow={stats => assignmentURL(stats.id)} diff --git a/lms/static/scripts/frontend_apps/components/dashboard/FormattedDate.tsx b/lms/static/scripts/frontend_apps/components/dashboard/FormattedDate.tsx new file mode 100644 index 0000000000..76957c58c2 --- /dev/null +++ b/lms/static/scripts/frontend_apps/components/dashboard/FormattedDate.tsx @@ -0,0 +1,17 @@ +import { useMemo } from 'preact/hooks'; + +import { formatDateTime } from '../../utils/date'; + +export type FormattedDateProps = { + /** Date in ISO format */ + date: string; +}; + +/** + * Formats a date for current user's locale, and shows it in a non-wrapping + * container + */ +export default function FormattedDate({ date }: FormattedDateProps) { + const formattedDate = useMemo(() => formatDateTime(new Date(date)), [date]); + return
{formattedDate}
; +} diff --git a/lms/static/scripts/frontend_apps/components/dashboard/OrderableActivityTable.tsx b/lms/static/scripts/frontend_apps/components/dashboard/OrderableActivityTable.tsx index d47ef149eb..f03e444620 100644 --- a/lms/static/scripts/frontend_apps/components/dashboard/OrderableActivityTable.tsx +++ b/lms/static/scripts/frontend_apps/components/dashboard/OrderableActivityTable.tsx @@ -46,7 +46,7 @@ export default function OrderableActivityTable({ columns.map(({ field, label }, index) => ({ field, label, - classes: index === 0 ? 'w-[60%]' : undefined, + classes: index === 0 ? 'lg:w-[60%] md:w-[45%]' : undefined, })), [columns], ); diff --git a/lms/static/scripts/frontend_apps/components/dashboard/OrganizationActivity.tsx b/lms/static/scripts/frontend_apps/components/dashboard/OrganizationActivity.tsx index d353c90551..7d188260cf 100644 --- a/lms/static/scripts/frontend_apps/components/dashboard/OrganizationActivity.tsx +++ b/lms/static/scripts/frontend_apps/components/dashboard/OrganizationActivity.tsx @@ -5,8 +5,8 @@ import { Link as RouterLink } from 'wouter-preact'; import type { CoursesResponse } from '../../api-types'; import { useConfig } from '../../config'; import { urlPath, useAPIFetch } from '../../utils/api'; -import { formatDateTime } from '../../utils/date'; import { replaceURLParams } from '../../utils/url'; +import FormattedDate from './FormattedDate'; import OrderableActivityTable from './OrderableActivityTable'; export type OrganizationActivityProps = { @@ -76,9 +76,11 @@ export default function OrganizationActivity({ if (field === 'assignments') { return
{stats[field]}
; } else if (field === 'last_launched') { - return stats.last_launched - ? formatDateTime(new Date(stats.last_launched)) - : ''; + return stats.last_launched ? ( + + ) : ( + '' + ); } return ( diff --git a/lms/static/scripts/frontend_apps/components/dashboard/test/AssignmentActivity-test.js b/lms/static/scripts/frontend_apps/components/dashboard/test/AssignmentActivity-test.js index df132ed526..838a0be612 100644 --- a/lms/static/scripts/frontend_apps/components/dashboard/test/AssignmentActivity-test.js +++ b/lms/static/scripts/frontend_apps/components/dashboard/test/AssignmentActivity-test.js @@ -62,6 +62,11 @@ describe('AssignmentActivity', () => { }; $imports.$mock(mockImportedComponents()); + $imports.$restore({ + // Do not mock FormattedDate, for consistency when checking + // rendered values in different columns + './FormattedDate': true, + }); $imports.$mock({ '../../utils/api': { useAPIFetch: fakeUseAPIFetch, diff --git a/lms/static/scripts/frontend_apps/components/dashboard/test/CourseActivity-test.js b/lms/static/scripts/frontend_apps/components/dashboard/test/CourseActivity-test.js index 7cd484c77b..7ee3fa4d57 100644 --- a/lms/static/scripts/frontend_apps/components/dashboard/test/CourseActivity-test.js +++ b/lms/static/scripts/frontend_apps/components/dashboard/test/CourseActivity-test.js @@ -6,7 +6,6 @@ import { mount } from 'enzyme'; import sinon from 'sinon'; import { Config } from '../../../config'; -import { formatDateTime } from '../../../utils/date'; import CourseActivity, { $imports } from '../CourseActivity'; describe('CourseActivity', () => { @@ -154,7 +153,7 @@ describe('CourseActivity', () => { { fieldName: 'replies', expectedValue: '25' }, { fieldName: 'last_activity', - expectedValue: formatDateTime(new Date('2024-01-01T10:35:18')), + expectedValue: '2024-01-01T10:35:18', }, ].forEach(({ fieldName, expectedValue }) => { it('renders every field as expected', () => { @@ -172,12 +171,13 @@ describe('CourseActivity', () => { .props() .renderItem(assignmentStats, fieldName); + const itemWrapper = mount(item); + if (fieldName === 'last_activity') { - assert.equal(item, expectedValue); + assert.equal(itemWrapper.prop('date'), expectedValue); return; } - const itemWrapper = mount(item); assert.equal(itemWrapper.text(), expectedValue); if (fieldName === 'title') { diff --git a/lms/static/scripts/frontend_apps/components/dashboard/test/OrganizationActivity-test.js b/lms/static/scripts/frontend_apps/components/dashboard/test/OrganizationActivity-test.js index 5bb2bffb8f..a8c1c364c3 100644 --- a/lms/static/scripts/frontend_apps/components/dashboard/test/OrganizationActivity-test.js +++ b/lms/static/scripts/frontend_apps/components/dashboard/test/OrganizationActivity-test.js @@ -47,6 +47,11 @@ describe('OrganizationActivity', () => { }; $imports.$mock(mockImportedComponents()); + $imports.$restore({ + // Do not mock FormattedDate, for consistency when checking + // rendered values in different columns + './FormattedDate': true, + }); $imports.$mock({ '../../utils/api': { useAPIFetch: fakeUseAPIFetch, @@ -114,11 +119,12 @@ describe('OrganizationActivity', () => { it('renders last launched date', () => { const wrapper = createComponent(); - const item = renderItem(wrapper, 'last_launched'); const { last_launched } = courseRow; + const item = renderItem(wrapper, 'last_launched'); + const itemText = last_launched ? mount(item).text() : ''; assert.equal( - item, + itemText, last_launched ? formatDateTime(new Date(last_launched)) : '', ); });