diff --git a/frontend/src/component/personalDashboard/LatestProjectEvents.tsx b/frontend/src/component/personalDashboard/LatestProjectEvents.tsx index 146a28b1b6bb..773dbed99ef9 100644 --- a/frontend/src/component/personalDashboard/LatestProjectEvents.tsx +++ b/frontend/src/component/personalDashboard/LatestProjectEvents.tsx @@ -2,7 +2,9 @@ import type { FC } from 'react'; import { Markdown } from '../common/Markdown/Markdown'; import type { PersonalDashboardProjectDetailsSchema } from '../../openapi'; import { UserAvatar } from '../common/UserAvatar/UserAvatar'; -import { styled } from '@mui/material'; +import { Typography, styled } from '@mui/material'; +import { formatDateYMDHM } from 'utils/formatDate'; +import { useLocationSettings } from 'hooks/useLocationSettings'; const Events = styled('ul')(({ theme }) => ({ padding: 0, @@ -16,27 +18,72 @@ const Event = styled('li')(({ theme }) => ({ gap: theme.spacing(2), alignItems: 'center', marginBottom: theme.spacing(4), + + '*': { + fontWeight: 'normal', + }, +})); + +const TitleContainer = styled('div')(({ theme }) => ({ + display: 'flex', + flexDirection: 'row', + gap: theme.spacing(2), + alignItems: 'center', +})); + +const ActionBox = styled('article')(({ theme }) => ({ + padding: theme.spacing(0, 2), + display: 'flex', + gap: theme.spacing(3), + flexDirection: 'column', +})); + +const Timestamp = styled('time')(({ theme }) => ({ + color: theme.palette.text.secondary, + fontSize: theme.typography.fontSize, + marginBottom: theme.spacing(1), })); export const LatestProjectEvents: FC<{ latestEvents: PersonalDashboardProjectDetailsSchema['latestEvents']; }> = ({ latestEvents }) => { + const { locationSettings } = useLocationSettings(); return ( - - {latestEvents.map((event) => { - return ( - - - - {event.summary || - 'No preview available for this event'} - - - ); - })} - + + + + Latest Events + + + + {latestEvents.map((event) => { + return ( + + +
+ + {formatDateYMDHM( + event.createdAt, + locationSettings.locale, + )} + + + {event.summary || + 'No preview available for this event'} + +
+
+ ); + })} +
+
); }; diff --git a/frontend/src/openapi/models/personalDashboardProjectDetailsSchemaLatestEventsItem.ts b/frontend/src/openapi/models/personalDashboardProjectDetailsSchemaLatestEventsItem.ts index 5de24466500d..2341bdaf3b00 100644 --- a/frontend/src/openapi/models/personalDashboardProjectDetailsSchemaLatestEventsItem.ts +++ b/frontend/src/openapi/models/personalDashboardProjectDetailsSchemaLatestEventsItem.ts @@ -22,4 +22,5 @@ export type PersonalDashboardProjectDetailsSchemaLatestEventsItem = { * @nullable */ summary: string | null; + createdAt: string; }; diff --git a/src/lib/features/personal-dashboard/personal-dashboard-controller.e2e.test.ts b/src/lib/features/personal-dashboard/personal-dashboard-controller.e2e.test.ts index 2e5ee74758b4..b797fad0f858 100644 --- a/src/lib/features/personal-dashboard/personal-dashboard-controller.e2e.test.ts +++ b/src/lib/features/personal-dashboard/personal-dashboard-controller.e2e.test.ts @@ -237,6 +237,8 @@ test('should return personal dashboard project details', async () => { `/api/admin/personal-dashboard/${project.id}`, ); + const timestampPattern = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d{3}Z$/; + expect(body).toMatchObject({ owners: [ { @@ -268,24 +270,28 @@ test('should return personal dashboard project details', async () => { }, latestEvents: [ { + createdAt: expect.stringMatching(timestampPattern), createdBy: 'new_user@test.com', summary: expect.stringContaining( '**new_user@test.com** created **[log_feature_c]', ), }, { + createdAt: expect.stringMatching(timestampPattern), createdBy: 'new_user@test.com', summary: expect.stringContaining( '**new_user@test.com** created **[log_feature_b]', ), }, { + createdAt: expect.stringMatching(timestampPattern), createdBy: 'new_user@test.com', summary: expect.stringContaining( '**new_user@test.com** created **[log_feature_a]', ), }, { + createdAt: expect.stringMatching(timestampPattern), createdBy: 'unknown', summary: expect.stringContaining( 'triggered **project-access-added**', diff --git a/src/lib/features/personal-dashboard/personal-dashboard-controller.ts b/src/lib/features/personal-dashboard/personal-dashboard-controller.ts index 0eed07137968..770236b3bca3 100644 --- a/src/lib/features/personal-dashboard/personal-dashboard-controller.ts +++ b/src/lib/features/personal-dashboard/personal-dashboard-controller.ts @@ -1,4 +1,9 @@ -import { type IUnleashConfig, type IUnleashServices, NONE } from '../../types'; +import { + type IUnleashConfig, + type IUnleashServices, + NONE, + serializeDates, +} from '../../types'; import type { OpenApiService } from '../../services'; import { createResponseSchema, @@ -114,9 +119,9 @@ export default class PersonalDashboardController extends Controller { 200, res, personalDashboardProjectDetailsSchema.$id, - { + serializeDates({ ...projectDetails, - }, + }), ); } } diff --git a/src/lib/features/personal-dashboard/personal-dashboard-service.ts b/src/lib/features/personal-dashboard/personal-dashboard-service.ts index 17a341db7b95..6a360bea6f0d 100644 --- a/src/lib/features/personal-dashboard/personal-dashboard-service.ts +++ b/src/lib/features/personal-dashboard/personal-dashboard-service.ts @@ -23,6 +23,19 @@ import type { PersonalDashboardProjectDetailsSchema } from '../../openapi'; import type { IRoleWithProject } from '../../types/stores/access-store'; import { NotFoundError } from '../../error'; +type PersonalDashboardProjectDetailsUnserialized = Omit< + PersonalDashboardProjectDetailsSchema, + 'latestEvents' +> & { + latestEvents: { + createdBy: string; + summary: string; + createdByImageUrl: string; + id: number; + createdAt: Date; + }[]; +}; + export class PersonalDashboardService { private personalDashboardReadModel: IPersonalDashboardReadModel; @@ -105,7 +118,7 @@ export class PersonalDashboardService { async getPersonalProjectDetails( userId: number, projectId: string, - ): Promise { + ): Promise { const onboardingStatus = await this.onboardingReadModel.getOnboardingStatusForProject( projectId, @@ -119,6 +132,7 @@ export class PersonalDashboardService { const formatEvents = (recentEvents: IEvent[]) => recentEvents.map((event) => ({ + createdAt: event.createdAt, summary: this.featureEventFormatter.format(event).text, createdBy: event.createdBy, id: event.id, diff --git a/src/lib/openapi/spec/personal-dashboard-project-details-schema.ts b/src/lib/openapi/spec/personal-dashboard-project-details-schema.ts index d42718f0f4d8..eb1e290df368 100644 --- a/src/lib/openapi/spec/personal-dashboard-project-details-schema.ts +++ b/src/lib/openapi/spec/personal-dashboard-project-details-schema.ts @@ -89,7 +89,13 @@ export const personalDashboardProjectDetailsSchema = { type: 'object', description: 'An event summary', additionalProperties: false, - required: ['summary', 'createdBy', 'createdByImageUrl', 'id'], + required: [ + 'summary', + 'createdBy', + 'createdByImageUrl', + 'id', + 'createdAt', + ], properties: { id: { type: 'integer', @@ -112,6 +118,12 @@ export const personalDashboardProjectDetailsSchema = { description: `URL used for the user profile image of the event author`, example: 'https://example.com/242x200.png', }, + createdAt: { + type: 'string', + format: 'date-time', + description: 'When the event was recorded', + example: '2021-09-01T12:00:00Z', + }, }, }, },