Skip to content

Commit 8a4352b

Browse files
feat: handle API errors in release notes component
1 parent d76490c commit 8a4352b

File tree

6 files changed

+176
-99
lines changed

6 files changed

+176
-99
lines changed

src/release-notes/ReleaseNotes.jsx

Lines changed: 124 additions & 88 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
1-
import React, { useMemo } from 'react';
1+
import React, { useMemo, useEffect } from 'react';
22
import { StudioFooterSlot } from '@edx/frontend-component-footer';
33
import {
4-
Add as AddIcon, EditOutline, DeleteOutline, AccessTime as ClockIcon,
4+
Add as AddIcon, EditOutline, DeleteOutline, AccessTime as ClockIcon, Info,
55
} from '@openedx/paragon/icons';
66
import {
77
Button,
@@ -12,6 +12,7 @@ import {
1212
OverlayTrigger,
1313
Tooltip,
1414
ModalDialog,
15+
Alert,
1516
} from '@openedx/paragon';
1617
import { useIntl } from '@edx/frontend-platform/i18n';
1718
import moment from 'moment';
@@ -40,8 +41,24 @@ const ReleaseNotes = () => {
4041
handleOpenUpdateForm,
4142
handleDeleteUpdateSubmit,
4243
handleOpenDeleteForm,
44+
errors,
4345
} = useReleaseNotes();
4446

47+
useEffect(() => {
48+
const handleBeforeUnload = (e) => {
49+
if (isFormOpen) {
50+
e.preventDefault();
51+
e.returnValue = '';
52+
}
53+
};
54+
55+
window.addEventListener('beforeunload', handleBeforeUnload);
56+
57+
return () => {
58+
window.removeEventListener('beforeunload', handleBeforeUnload);
59+
};
60+
}, [isFormOpen]);
61+
4562
const groups = useMemo(() => {
4663
const map = new Map();
4764
(notes || []).forEach((n) => {
@@ -64,6 +81,13 @@ const ReleaseNotes = () => {
6481
return (
6582
<>
6683
<Header isHiddenMainMenu />
84+
{errors.loadingNotes && (
85+
<Container size="xl" className="px-4 pt-4">
86+
<Alert variant="danger" icon={Info}>
87+
{intl.formatMessage(messages.errorLoadingPage)}
88+
</Alert>
89+
</Container>
90+
)}
6791
<Container size="xl" className="release-notes-page px-4 pt-4">
6892
<SubHeader
6993
title={intl.formatMessage(messages.headingTitle)}
@@ -81,98 +105,100 @@ const ReleaseNotes = () => {
81105
) : null}
82106
/>
83107

84-
<Layout
85-
lg={[{ span: 9 }, { span: 3 }]}
86-
md={[{ span: 9 }, { span: 3 }]}
87-
xs={[{ span: 12 }, { span: 12 }]}
88-
>
89-
<Layout.Element>
90-
<article>
91-
<section className="release-notes-list p-4.5">
92-
{groups.length > 0 ? (
93-
groups.map((g) => (
94-
<div key={g.key} className="mb-4">
95-
{g.items.map((post) => (
96-
<div id={`note-${post.id}`} key={post.id} className="release-note-item mb-4 pb-4">
97-
<div className="d-flex justify-content-between align-items-start">
98-
<div>
99-
<h3 className="mb-4 pb-4">{moment(post.published_at).format('MMMM D, YYYY')}</h3>
100-
{post.published_at && moment(post.published_at).isAfter(moment()) && (
101-
<OverlayTrigger
102-
placement="top"
103-
overlay={(
104-
<Tooltip id={`scheduled-tooltip-${post.id}`}>
105-
{intl.formatMessage(messages.scheduledTooltip, {
106-
date: moment(post.published_at).format('MMMM D, YYYY h:mm A z'),
107-
})}
108-
</Tooltip>
108+
{!errors.loadingNotes && (
109+
groups.length > 0 ? (
110+
<Layout
111+
lg={[{ span: 9 }, { span: 3 }]}
112+
md={[{ span: 9 }, { span: 3 }]}
113+
xs={[{ span: 12 }, { span: 12 }]}
114+
>
115+
<Layout.Element>
116+
<article>
117+
<section className="release-notes-list p-4.5">
118+
{groups.map((g) => (
119+
<div key={g.key} className="mb-4">
120+
{g.items.map((post) => (
121+
<div id={`note-${post.id}`} key={post.id} className="release-note-item mb-4 pb-4">
122+
<div className="d-flex justify-content-between align-items-start">
123+
<div>
124+
<h3 className="mb-4 pb-4">{moment(post.published_at).format('MMMM D, YYYY')}</h3>
125+
{post.published_at && moment(post.published_at).isAfter(moment()) && (
126+
<OverlayTrigger
127+
placement="top"
128+
overlay={(
129+
<Tooltip id={`scheduled-tooltip-${post.id}`}>
130+
{intl.formatMessage(messages.scheduledTooltip, {
131+
date: moment(post.published_at).format('MMMM D, YYYY h:mm A z'),
132+
})}
133+
</Tooltip>
109134
)}
110-
>
111-
<div className="d-inline-flex align-items-center text-muted small mr-2" role="button" tabIndex={0}>
112-
<Icon
113-
className="mr-1 p-0 justify-content-start scheduled-icon"
114-
src={ClockIcon}
115-
alt={intl.formatMessage(messages.scheduledTooltip, {
116-
date: moment(post.published_at).format('MMMM D, YYYY h:mm A z'),
117-
})}
118-
/>
119-
<span>{intl.formatMessage(messages.scheduledLabel)}</span>
135+
>
136+
<div className="d-inline-flex align-items-center text-muted small mr-2" role="button" tabIndex={0}>
137+
<Icon
138+
className="mr-1 p-0 justify-content-start scheduled-icon"
139+
src={ClockIcon}
140+
alt={intl.formatMessage(messages.scheduledTooltip, {
141+
date: moment(post.published_at).format('MMMM D, YYYY h:mm A z'),
142+
})}
143+
/>
144+
<span>{intl.formatMessage(messages.scheduledLabel)}</span>
145+
</div>
146+
</OverlayTrigger>
147+
)}
148+
<div className="d-flex align-items-center mb-1 justify-content-between">
149+
<h6 className="m-0">{post.title}</h6>
150+
{administrator && (
151+
<div className="ml-3 d-flex">
152+
<IconButtonWithTooltip
153+
tooltipContent={intl.formatMessage(messages.editButton)}
154+
src={EditOutline}
155+
iconAs={Icon}
156+
onClick={() => handleOpenUpdateForm(REQUEST_TYPES.edit_update, post)}
157+
data-testid="release-note-edit-button"
158+
disabled={isFormOpen}
159+
/>
160+
<IconButtonWithTooltip
161+
tooltipContent={intl.formatMessage(messages.deleteButton)}
162+
src={DeleteOutline}
163+
iconAs={Icon}
164+
onClick={() => handleOpenDeleteForm(post)}
165+
data-testid="release-note-delete-button"
166+
disabled={isFormOpen}
167+
/>
168+
</div>
169+
)}
120170
</div>
121-
</OverlayTrigger>
122-
)}
123-
<div className="d-flex align-items-center mb-1 justify-content-between">
124-
<h6 className="m-0">{post.title}</h6>
125-
{administrator && (
126-
<div className="ml-3 d-flex">
127-
<IconButtonWithTooltip
128-
tooltipContent={intl.formatMessage(messages.editButton)}
129-
src={EditOutline}
130-
iconAs={Icon}
131-
onClick={() => handleOpenUpdateForm(REQUEST_TYPES.edit_update, post)}
132-
data-testid="release-note-edit-button"
133-
disabled={isFormOpen}
134-
/>
135-
<IconButtonWithTooltip
136-
tooltipContent={intl.formatMessage(messages.deleteButton)}
137-
src={DeleteOutline}
138-
iconAs={Icon}
139-
onClick={() => handleOpenDeleteForm(post)}
140-
data-testid="release-note-delete-button"
141-
disabled={isFormOpen}
142-
/>
171+
{/* eslint-disable-next-line react/no-danger */}
172+
<div className="post-description" dangerouslySetInnerHTML={{ __html: post.description }} />
173+
{post.created_by && (
174+
<div className="mt-3">
175+
<small>
176+
{intl.formatMessage({ id: 'release-notes.questions.contact', defaultMessage: 'Questions? Contact {email}' }, {
177+
email: post.created_by,
178+
})}
179+
</small>
143180
</div>
144181
)}
145182
</div>
146-
{/* eslint-disable-next-line react/no-danger */}
147-
<div className="post-description" dangerouslySetInnerHTML={{ __html: post.description }} />
148-
{post.created_by && (
149-
<div className="mt-3">
150-
<small>
151-
{intl.formatMessage({ id: 'release-notes.questions.contact', defaultMessage: 'Questions? Contact {email}' }, {
152-
email: post.created_by,
153-
})}
154-
</small>
155-
</div>
156-
)}
157-
</div>
158183

184+
</div>
159185
</div>
160-
</div>
161-
))}
162-
</div>
163-
))
164-
) : (
165-
<div className="text-center">
166-
<span className="small mr-2">{intl.formatMessage(messages.noReleaseNotes)}</span>
167-
</div>
168-
)}
169-
</section>
170-
</article>
171-
</Layout.Element>
172-
<Layout.Element>
173-
<ReleaseNotesSidebar notes={notes} />
174-
</Layout.Element>
175-
</Layout>
186+
))}
187+
</div>
188+
))}
189+
</section>
190+
</article>
191+
</Layout.Element>
192+
<Layout.Element>
193+
<ReleaseNotesSidebar notes={notes} />
194+
</Layout.Element>
195+
</Layout>
196+
) : (
197+
<div className="text-center py-5">
198+
<span className="small">{intl.formatMessage(messages.noReleaseNotes)}</span>
199+
</div>
200+
)
201+
)}
176202
</Container>
177203
{isFormOpen && (
178204
<ModalDialog
@@ -189,6 +215,11 @@ const ReleaseNotes = () => {
189215
</ModalDialog.Title>
190216
</ModalDialog.Header>
191217
<ModalDialog.Body>
218+
{(errors.savingNotes || errors.creatingNote) && (
219+
<Alert variant="danger" icon={Info} className="mb-3">
220+
{intl.formatMessage(messages.errorSavingPost)}
221+
</Alert>
222+
)}
192223
<ReleaseNoteForm
193224
initialValues={notesInitialValues}
194225
close={closeForm}
@@ -197,7 +228,12 @@ const ReleaseNotes = () => {
197228
</ModalDialog.Body>
198229
</ModalDialog>
199230
)}
200-
<DeleteModal isOpen={isDeleteModalOpen} close={closeDeleteModal} onDeleteSubmit={handleDeleteUpdateSubmit} />
231+
<DeleteModal
232+
isOpen={isDeleteModalOpen}
233+
close={closeDeleteModal}
234+
onDeleteSubmit={handleDeleteUpdateSubmit}
235+
errorDeleting={errors.deletingNotes}
236+
/>
201237
<StudioFooterSlot />
202238
</>
203239
);

src/release-notes/data/thunk.js

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -71,12 +71,14 @@ export function createReleaseNoteQuery(data) {
7171
status: { createReleaseNoteQuery: RequestStatus.SUCCESSFUL },
7272
error: { creatingNote: false },
7373
}));
74+
return { success: true };
7475
} catch (error) {
7576
dispatch(hideProcessingNotification());
7677
dispatch(updateSavingStatuses({
7778
status: { createReleaseNoteQuery: RequestStatus.FAILED },
7879
error: { creatingNote: true },
7980
}));
81+
return { success: false };
8082
}
8183
};
8284
}
@@ -105,12 +107,14 @@ export function editReleaseNoteQuery(data) {
105107
status: { editReleaseNoteQuery: RequestStatus.SUCCESSFUL },
106108
error: { savingNotes: false },
107109
}));
110+
return { success: true };
108111
} catch (error) {
109112
dispatch(hideProcessingNotification());
110113
dispatch(updateSavingStatuses({
111114
status: { editReleaseNoteQuery: RequestStatus.FAILED },
112115
error: { savingNotes: true },
113116
}));
117+
return { success: false };
114118
}
115119
};
116120
}
@@ -127,12 +131,14 @@ export function deleteReleaseNoteQuery(noteId) {
127131
status: { deleteReleaseNoteQuery: RequestStatus.SUCCESSFUL },
128132
error: { deletingNotes: false },
129133
}));
134+
return { success: true };
130135
} catch (error) {
131136
dispatch(hideProcessingNotification());
132137
dispatch(updateSavingStatuses({
133138
status: { deleteReleaseNoteQuery: RequestStatus.FAILED },
134139
error: { deletingNotes: true },
135140
}));
141+
return { success: false };
136142
}
137143
};
138144
}

src/release-notes/delete-modal/DeleteModal.jsx

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,12 +4,17 @@ import {
44
ActionRow,
55
Button,
66
AlertModal,
7+
Alert,
78
} from '@openedx/paragon';
9+
import { Info } from '@openedx/paragon/icons';
810
import { useIntl } from '@edx/frontend-platform/i18n';
911

1012
import messages from './messages';
13+
import releaseNotesMessages from '../messages';
1114

12-
const DeleteModal = ({ isOpen, close, onDeleteSubmit }) => {
15+
const DeleteModal = ({
16+
isOpen, close, onDeleteSubmit, errorDeleting,
17+
}) => {
1318
const intl = useIntl();
1419

1520
return (
@@ -33,6 +38,11 @@ const DeleteModal = ({ isOpen, close, onDeleteSubmit }) => {
3338
</ActionRow>
3439
)}
3540
>
41+
{errorDeleting && (
42+
<Alert variant="danger" icon={Info} className="mb-3">
43+
{intl.formatMessage(releaseNotesMessages.errorDeletingPost)}
44+
</Alert>
45+
)}
3646
<p>{intl.formatMessage(messages.deleteModalDescription)}</p>
3747
</AlertModal>
3848
);
@@ -42,6 +52,11 @@ DeleteModal.propTypes = {
4252
isOpen: PropTypes.bool.isRequired,
4353
close: PropTypes.func.isRequired,
4454
onDeleteSubmit: PropTypes.func.isRequired,
55+
errorDeleting: PropTypes.bool,
56+
};
57+
58+
DeleteModal.defaultProps = {
59+
errorDeleting: false,
4560
};
4661

4762
export default DeleteModal;

0 commit comments

Comments
 (0)