Skip to content

Commit 787b15f

Browse files
feat: enhance release notes UI and layout
1 parent eb3406a commit 787b15f

File tree

11 files changed

+72
-57
lines changed

11 files changed

+72
-57
lines changed

.env

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -49,4 +49,4 @@ LIBRARY_UNSUPPORTED_BLOCKS="conditional,step-builder,problem-builder"
4949
# Fallback in local style files
5050
PARAGON_THEME_URLS={}
5151
COURSE_TEAM_SUPPORT_EMAIL=''
52-
ENABLE_RELEASE_NOTES=false
52+
ENABLE_RELEASE_NOTES=false

.env.development

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -52,4 +52,4 @@ LIBRARY_UNSUPPORTED_BLOCKS="conditional,step-builder,problem-builder"
5252
# Fallback in local style files
5353
PARAGON_THEME_URLS={}
5454
COURSE_TEAM_SUPPORT_EMAIL=''
55-
ENABLE_RELEASE_NOTES=true
55+
ENABLE_RELEASE_NOTES=true

.env.test

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -43,4 +43,4 @@ ENABLE_GRADING_METHOD_IN_PROBLEMS=false
4343
LIBRARY_UNSUPPORTED_BLOCKS="conditional,step-builder,problem-builder"
4444
PARAGON_THEME_URLS=
4545
COURSE_TEAM_SUPPORT_EMAIL='[email protected]'
46-
ENABLE_RELEASE_NOTES=false
46+
ENABLE_RELEASE_NOTES=false

src/release-notes/ReleaseNotes.jsx

Lines changed: 19 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ import DeleteModal from './delete-modal/DeleteModal';
2525
import ReleaseNoteForm from './update-form/ReleaseNoteForm';
2626
import ReleaseNotesSidebar from './sidebar/ReleaseNotesSidebar';
2727
import { REQUEST_TYPES } from '../course-updates/constants';
28+
import { groupNotesByDate } from './utils/groupNotes';
2829

2930
const ReleaseNotes = () => {
3031
const intl = useIntl();
@@ -77,24 +78,7 @@ const ReleaseNotes = () => {
7778
};
7879
}, [isFormOpen]);
7980

80-
const groups = useMemo(() => {
81-
const map = new Map();
82-
(notes || []).forEach((n) => {
83-
const key = n.published_at ? moment(n.published_at).format('YYYY-MM-DD') : 'unscheduled';
84-
if (!map.has(key)) { map.set(key, []); }
85-
map.get(key).push(n);
86-
});
87-
const keys = Array.from(map.keys()).sort((a, b) => {
88-
if (a === 'unscheduled') { return 1; }
89-
if (b === 'unscheduled') { return -1; }
90-
return moment(b).valueOf() - moment(a).valueOf();
91-
});
92-
return keys.map((k) => ({
93-
key: k,
94-
label: k === 'unscheduled' ? intl.formatMessage({ id: 'release-notes.unscheduled.label', defaultMessage: 'Unscheduled' }) : moment(k).format('MMMM D, YYYY'),
95-
items: map.get(k),
96-
}));
97-
}, [notes, intl]);
81+
const groups = useMemo(() => groupNotesByDate(notes, intl), [notes, intl]);
9882

9983
return (
10084
<>
@@ -111,7 +95,7 @@ const ReleaseNotes = () => {
11195
title={intl.formatMessage(messages.headingTitle)}
11296
subtitle=""
11397
instruction=""
114-
headerActions={administrator ? (
98+
headerActions={administrator && !errors.loadingNotes ? (
11599
<Button
116100
variant="primary"
117101
iconBefore={AddIcon}
@@ -140,7 +124,11 @@ const ReleaseNotes = () => {
140124
<div id={`note-${post.id}`} key={post.id} className="release-note-item mb-4 pb-4">
141125
<div className="d-flex justify-content-between align-items-start">
142126
<div>
143-
<h2 className="mb-4 pb-4">{moment(post.published_at).format('MMMM D, YYYY')}</h2>
127+
<h2 className="mb-4 pb-4">
128+
{post.published_at
129+
? moment(post.published_at).format('MMMM D, YYYY')
130+
: intl.formatMessage({ id: 'release-notes.unscheduled.label', defaultMessage: 'Unscheduled' })}
131+
</h2>
144132
{post.published_at && moment(post.published_at).isAfter(moment()) && (
145133
<OverlayTrigger
146134
placement="right"
@@ -152,7 +140,13 @@ const ReleaseNotes = () => {
152140
</Tooltip>
153141
)}
154142
>
155-
<div className="d-inline-flex align-items-center text-muted small mr-2" role="button" tabIndex={0}>
143+
<button
144+
type="button"
145+
className="btn-link d-inline-flex align-items-center text-muted small mr-2 p-0 border-0 text-decoration-none"
146+
aria-label={intl.formatMessage(messages.scheduledTooltip, {
147+
date: `${moment(post.published_at).format('MMMM D, YYYY h:mm A')} ${getTzName(new Date(post.published_at))}`,
148+
})}
149+
>
156150
<Icon
157151
className="mr-1 p-0 justify-content-start scheduled-icon"
158152
src={ClockIcon}
@@ -161,7 +155,7 @@ const ReleaseNotes = () => {
161155
})}
162156
/>
163157
<span className="post-scheduled">{intl.formatMessage(messages.scheduledLabel)}</span>
164-
</div>
158+
</button>
165159
</OverlayTrigger>
166160
)}
167161
<div className="d-flex align-items-center mb-1 justify-content-between">
@@ -194,7 +188,7 @@ const ReleaseNotes = () => {
194188
{post.created_by && (
195189
<div className="mt-3">
196190
<small>
197-
{intl.formatMessage({ id: 'release-notes.questions.contact', defaultMessage: 'Questions? Contact {email}' }, {
191+
{intl.formatMessage(messages.questionsContact, {
198192
email: post.created_by,
199193
})}
200194
</small>
@@ -236,7 +230,7 @@ const ReleaseNotes = () => {
236230
</ModalDialog.Title>
237231
</ModalDialog.Header>
238232
<ModalDialog.Body>
239-
{(errors.savingNotes || errors.creatingNote) && (
233+
{(errors.savingNote || errors.creatingNote) && (
240234
<Alert variant="danger" icon={Info} className="mb-3">
241235
{intl.formatMessage(messages.errorSavingPost)}
242236
</Alert>
@@ -253,7 +247,7 @@ const ReleaseNotes = () => {
253247
isOpen={isDeleteModalOpen}
254248
close={closeDeleteModal}
255249
onDeleteSubmit={handleDeleteUpdateSubmit}
256-
errorDeleting={errors.deletingNotes}
250+
errorDeleting={errors.deletingNote}
257251
/>
258252
<StudioFooterSlot />
259253
</>

src/release-notes/data/slice.js

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,9 +14,9 @@ const initialState = {
1414
},
1515
errors: {
1616
creatingNote: false,
17-
deletingNotes: false,
17+
deletingNote: false,
1818
loadingNotes: false,
19-
savingNotes: false,
19+
savingNote: false,
2020
},
2121
};
2222

src/release-notes/data/thunk.js

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -105,14 +105,14 @@ export function editReleaseNoteQuery(data) {
105105
dispatch(hideProcessingNotification());
106106
dispatch(updateSavingStatuses({
107107
status: { editReleaseNoteQuery: RequestStatus.SUCCESSFUL },
108-
error: { savingNotes: false },
108+
error: { savingNote: false },
109109
}));
110110
return { success: true };
111111
} catch (error) {
112112
dispatch(hideProcessingNotification());
113113
dispatch(updateSavingStatuses({
114114
status: { editReleaseNoteQuery: RequestStatus.FAILED },
115-
error: { savingNotes: true },
115+
error: { savingNote: true },
116116
}));
117117
return { success: false };
118118
}
@@ -129,14 +129,14 @@ export function deleteReleaseNoteQuery(noteId) {
129129
dispatch(hideProcessingNotification());
130130
dispatch(updateSavingStatuses({
131131
status: { deleteReleaseNoteQuery: RequestStatus.SUCCESSFUL },
132-
error: { deletingNotes: false },
132+
error: { deletingNote: false },
133133
}));
134134
return { success: true };
135135
} catch (error) {
136136
dispatch(hideProcessingNotification());
137137
dispatch(updateSavingStatuses({
138138
status: { deleteReleaseNoteQuery: RequestStatus.FAILED },
139-
error: { deletingNotes: true },
139+
error: { deletingNote: true },
140140
}));
141141
return { success: false };
142142
}

src/release-notes/data/thunk.test.js

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -108,7 +108,7 @@ describe('release-notes thunks', () => {
108108
const last = actions[actions.length - 1];
109109
expect(last.type).toBe(updateSavingStatuses.type);
110110
expect(last.payload.status.editReleaseNoteQuery).toBe(RequestStatus.FAILED);
111-
expect(last.payload.error.savingNotes).toBe(true);
111+
expect(last.payload.error.savingNote).toBe(true);
112112
});
113113

114114
test('deleteReleaseNoteQuery toggles processing notifications', async () => {
@@ -138,7 +138,7 @@ describe('release-notes thunks', () => {
138138
const last = actions[actions.length - 1];
139139
expect(last.type).toBe(updateSavingStatuses.type);
140140
expect(last.payload.status.deleteReleaseNoteQuery).toBe(RequestStatus.FAILED);
141-
expect(last.payload.error.deletingNotes).toBe(true);
141+
expect(last.payload.error.deletingNote).toBe(true);
142142
});
143143

144144
test('createReleaseNoteQuery transforms payload (raw_html_content) before API', async () => {

src/release-notes/hooks.jsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ import {
2020
const useReleaseNotes = () => {
2121
const dispatch = useDispatch();
2222
const initialNote = {
23-
id: 0, published_at: moment().utc().toDate(), title: '', description: '',
23+
id: undefined, published_at: moment().utc().toDate(), title: '', description: '',
2424
};
2525

2626
const [requestType, setRequestType] = useState('');

src/release-notes/messages.js

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -94,6 +94,11 @@ const messages = defineMessages({
9494
id: 'release-notes.form.error.description.required',
9595
defaultMessage: 'Enter post content',
9696
},
97+
questionsContact: {
98+
id: 'release-notes.questions.contact',
99+
defaultMessage: 'Questions? Contact {email}',
100+
description: 'Text shown at the bottom of each release note to provide contact information',
101+
},
97102
});
98103

99104
export default messages;

src/release-notes/sidebar/ReleaseNotesSidebar.jsx

Lines changed: 5 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -1,33 +1,18 @@
11
import React, { useMemo } from 'react';
22
import PropTypes from 'prop-types';
3-
import moment from 'moment';
4-
5-
const dayKey = (iso) => (iso ? moment(iso).format('YYYY-MM-DD') : 'unscheduled');
6-
const dayLabel = (key) => (key === 'unscheduled' ? 'Unscheduled' : moment(key).format('MMMM D, YYYY'));
3+
import { useIntl } from '@edx/frontend-platform/i18n';
4+
import { groupNotesByDate } from '../utils/groupNotes';
75

86
const ReleaseNotesSidebar = ({ notes }) => {
9-
const groups = useMemo(() => {
10-
const map = new Map();
11-
(notes || []).forEach((n) => {
12-
const key = dayKey(n.published_at);
13-
if (!map.has(key)) { map.set(key, []); }
14-
map.get(key).push(n);
15-
});
16-
// Sort groups by date desc, unscheduled last
17-
const keys = Array.from(map.keys()).sort((a, b) => {
18-
if (a === 'unscheduled') { return 1; }
19-
if (b === 'unscheduled') { return -1; }
20-
return moment(b).valueOf() - moment(a).valueOf();
21-
});
22-
return keys.map((k) => ({ key: k, label: dayLabel(k), items: map.get(k) }));
23-
}, [notes]);
7+
const intl = useIntl();
8+
const groups = useMemo(() => groupNotesByDate(notes, intl), [notes, intl]);
249

2510
return (
2611
<aside className="release-notes-sidebar">
2712
<div className="pt-5">
2813
{groups.map((g) => (
2914
<div key={g.key} className="mb-3">
30-
<p className="mb-1">{g.label}</p>
15+
<h6 className="mb-1">{g.label}</h6>
3116
<ul className="list-unstyled m-0 p-0 ml-3">
3217
{g.items.map((n) => (
3318
<li key={n.id} className="mb-2">

0 commit comments

Comments
 (0)