Skip to content

Commit 7e59e76

Browse files
committed
feat: better pdf authoring form
1 parent 1f33696 commit 7e59e76

18 files changed

Lines changed: 82 additions & 173 deletions

File tree

src/course-unit/add-component/AddComponent.test.tsx

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -322,7 +322,7 @@ describe('<AddComponent />', () => {
322322

323323
it('adds a PDF block from the advanced selection in modal as an mfe-editable block', async () => {
324324
const user = userEvent.setup();
325-
const { getByRole, queryAllByRole } = renderComponent();
325+
const { getByRole, getByText, queryAllByRole, queryAllByText } = renderComponent();
326326
const advancedBtn = getByRole('button', {
327327
name: new RegExp(`${messages.buttonText.defaultMessage} Advanced`, 'i'),
328328
});
@@ -335,11 +335,14 @@ describe('<AddComponent />', () => {
335335
const confirmation = within(dialog).getByText('Select');
336336
await user.click(confirmation);
337337
await waitFor(() => expect(queryAllByRole('dialog')).toEqual([]));
338+
expect(queryAllByText('PDF')).toEqual([])
338339
expect(handleCreateNewCourseXBlockMock).toHaveBeenCalled();
339340
expect(handleCreateNewCourseXBlockMock).toHaveBeenCalledWith({
340341
parentLocator: '123',
341342
type: COMPONENT_TYPES.pdf,
342343
}, expect.any(Function));
344+
// Verify the block is provisioned and loads the editor.
345+
await waitFor(() => getByText('PDF'))
343346
});
344347

345348
it('adds a PDF block from the advanced selection in modal as a traditional block', async () => {

src/editors/containers/PdfEditor/components/PdfEditingModal.tsx

Lines changed: 11 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -7,10 +7,10 @@ import EditorContainer from '@src/editors/containers/EditorContainer';
77
import { PdfBlockContext, PdfState } from '@src/editors/containers/PdfEditor/contexts';
88
import { isEqual } from 'lodash';
99
import DownloadOptions from '@src/editors/containers/PdfEditor/components/sections/DownloadOptions';
10-
import { UploadWidget, defaultUploadMessages } from '@src/editors/sharedComponents/UploadWidget';
10+
import { UploadWidget } from '@src/editors/sharedComponents/UploadWidget';
1111
import { Spinner } from '@openedx/paragon';
1212
import { FormattedMessage, useIntl } from '@edx/frontend-platform/i18n';
13-
import { messages, fileUploadMessages } from './messages';
13+
import messages from './messages';
1414

1515
const EditorWrapper: React.FC<PropsWithChildren> = ({ children }) => {
1616
const intl = useIntl();
@@ -36,9 +36,8 @@ const EditorWrapper: React.FC<PropsWithChildren> = ({ children }) => {
3636
return <>{children}</>; /* eslint-disable-line react/jsx-no-useless-fragment */
3737
};
3838

39-
const uploadMessages = { ...defaultUploadMessages, ...fileUploadMessages };
40-
4139
const PdfEditingModal: React.FC<EditorComponent> = (props) => {
40+
const intl = useIntl();
4241
const { fields } = useContext(PdfBlockContext);
4342
const originalState = useRef({ ...fields });
4443
const { values, setValues } = useFormikContext<PdfState>();
@@ -61,7 +60,14 @@ const PdfEditingModal: React.FC<EditorComponent> = (props) => {
6160
return (
6261
<EditorContainer {...props} isDirty={isDirty} getContent={getContent}>
6362
<EditorWrapper>
64-
<UploadWidget supportedFileFormats="application/pdf" urlFieldName="url" messages={uploadMessages} />
63+
<div className="mt-2">
64+
<UploadWidget
65+
supportedFileFormats="application/pdf"
66+
urlFieldName="url"
67+
label={intl.formatMessage(messages.urlFieldLabel)}
68+
id="pdf-url"
69+
/>
70+
</div>
6571
<DownloadOptions />
6672
</EditorWrapper>
6773
</EditorContainer>
Lines changed: 2 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import { defineMessages } from '@edx/frontend-platform/i18n';
22

3-
export const messages = defineMessages({
3+
export default defineMessages({
44
blockFailed: {
55
id: 'authoring.pdfEditor.blockFailed',
66
defaultMessage: 'PDF block failed to load',
@@ -11,12 +11,9 @@ export const messages = defineMessages({
1111
defaultMessage: 'Loading PDF Editor',
1212
description: 'Message shown to screen readers when the PDF block is loading.',
1313
},
14-
});
15-
16-
export const fileUploadMessages = defineMessages({
1714
urlFieldLabel: {
1815
id: 'authoring.pdfEditor.urlFieldLabel',
19-
defaultMessage: 'PDF URL',
16+
defaultMessage: 'File',
2017
description: 'Label for the PDF URL field',
2118
},
2219
});

src/editors/containers/PdfEditor/components/sections/DownloadOptions.tsx

Lines changed: 22 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,4 @@
1-
import { Form } from '@openedx/paragon';
2-
import CollapsibleFormWidget
3-
from '@src/editors/sharedComponents/CollapsibleFormWidget/CollapsibleFormWidget';
1+
import React from 'react';
42
import { useFormikContext } from 'formik';
53
import { PdfState } from '@src/editors/containers/PdfEditor/contexts';
64
import { optional, useUrlValidator } from '@src/editors/utils/validators';
@@ -9,45 +7,40 @@ import CheckboxField from '@src/editors/sharedComponents/CheckboxField';
97
import TextField from '@src/editors/sharedComponents/TextField';
108
import messages from './messages';
119

12-
const DownloadOptions = () => {
10+
const DownloadOptions: React.FC = () => {
1311
const intl = useIntl();
14-
const { errors, values } = useFormikContext<PdfState>();
15-
const isError = !!(errors.allowDownload?.length || errors.sourceText?.length || errors.sourceUrl?.length);
12+
const { values } = useFormikContext<PdfState>();
1613
const urlValidator = optional(useUrlValidator());
1714
if (values.disableAllDownload) {
1815
// Download configuration is disabled at the instance-level, so don't even show these options.
1916
return <></>; // eslint-disable-line react/jsx-no-useless-fragment
2017
}
2118
return (
22-
<CollapsibleFormWidget
23-
fontSize="x-small"
24-
title={intl.formatMessage(messages.downloadOptions)}
25-
isError={isError}
26-
>
27-
<Form.Group>
19+
<>
20+
<div className="mt-5 mb-4">
2821
<CheckboxField
2922
label={intl.formatMessage(messages.allowDownloadLabel)}
3023
id="pdf-allow-download"
3124
hint={intl.formatMessage(messages.allowDownloadHint)}
3225
fieldConfig="allowDownload"
3326
/>
34-
<TextField
35-
label={intl.formatMessage(messages.sourceDocumentButtonTextLabel)}
36-
id="pdf-source-text"
37-
placeholder={intl.formatMessage(messages.sourceDocumentButtonTextPlaceholder)}
38-
name="sourceText"
39-
disabled={!values.allowDownload}
40-
/>
41-
<TextField
42-
label={intl.formatMessage(messages.sourceUrlLabel)}
43-
name="sourceUrl"
44-
id="pdf-source-url"
45-
hint={intl.formatMessage(messages.sourceUrlHint)}
46-
disabled={!values.allowDownload}
47-
fieldConfig={{ validate: urlValidator }}
48-
/>
49-
</Form.Group>
50-
</CollapsibleFormWidget>
27+
</div>
28+
<TextField
29+
label={intl.formatMessage(messages.sourceUrlLabel)}
30+
name="sourceUrl"
31+
id="pdf-source-url"
32+
hint={intl.formatMessage(messages.sourceUrlHint)}
33+
disabled={!values.allowDownload}
34+
fieldConfig={{ validate: urlValidator }}
35+
/>
36+
<TextField
37+
label={intl.formatMessage(messages.sourceDocumentButtonTextLabel)}
38+
id="pdf-source-text"
39+
placeholder={intl.formatMessage(messages.sourceDocumentButtonTextPlaceholder)}
40+
name="sourceText"
41+
disabled={!values.allowDownload}
42+
/>
43+
</>
5144
);
5245
};
5346

src/editors/containers/PdfEditor/components/sections/messages.ts

Lines changed: 4 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,6 @@
11
import { defineMessages } from '@edx/frontend-platform/i18n';
22

33
const messages = defineMessages({
4-
downloadOptions: {
5-
id: 'authoring.pdfEditor.formGroups.downloadOptions',
6-
defaultMessage: 'Download Options',
7-
description: 'Section header for configuring what options a learner has for downloading a PDF'
8-
+ ' or its source material.',
9-
},
104
allowDownloadLabel: {
115
id: 'authoring.pdfEditor.formGroups.downloadOptions.allowDownload.label',
126
defaultMessage: 'Show PDF download link',
@@ -20,25 +14,24 @@ const messages = defineMessages({
2014
},
2115
sourceDocumentButtonTextLabel: {
2216
id: 'authoring.pdfEditor.formGroups.downloadOptions.sourceDocumentButtonText.label',
23-
defaultMessage: 'Source document button text',
17+
defaultMessage: 'Original File Link Text',
2418
description: 'Label for the source document button text setting, which allows the instructor to customize '
2519
+ 'the text on the source download button for PDFs.',
2620
},
2721
sourceDocumentButtonTextPlaceholder: {
2822
id: 'authoring.pdfEditor.formGroups.downloadOptions.sourceDocumentButtonText.placeholder',
29-
defaultMessage: 'Default: Download the source document',
23+
defaultMessage: 'Download the source document',
3024
description: 'Placeholder text for the source document button text setting, which allows the instructor to '
3125
+ 'customize the text on the source download button for PDFs.',
3226
},
3327
sourceUrlLabel: {
3428
id: 'authoring.pdfEditor.formGroups.downloadOptions.sourceUrl.label',
35-
defaultMessage: 'Source document URL',
29+
defaultMessage: 'Original File URL',
3630
description: 'Label for the field used to specify the URL of a source document a PDF was generated from.',
3731
},
3832
sourceUrlHint: {
3933
id: 'authoring.pdfEditor.formGroups.downloadOptions.sourceUrl.hint',
40-
defaultMessage: 'Add a download link for the source file of your PDF. Use it, for example, to provide a '
41-
+ 'PowerPoint file used to create this PDF.',
34+
defaultMessage: 'Add a link to the original or editable file (e.g. Word or PowerPoint). Appears as a separate link.',
4235
description: 'Hint for the field used to specify the URL of a source document a PDF was generated from.',
4336
},
4437
});

src/editors/containers/PdfEditor/index.test.tsx

Lines changed: 3 additions & 81 deletions
Original file line numberDiff line numberDiff line change
@@ -3,10 +3,7 @@ import { initializeMocks } from '@src/testUtils';
33
import PdfEditor from '@src/editors/containers/PdfEditor/index';
44
import { editorRender } from '@src/editors/editorTestRender';
55
import { initialPdfState, PdfState } from '@src/editors/containers/PdfEditor/contexts';
6-
import {
7-
messages,
8-
fileUploadMessages as uploadOverrideMessages,
9-
} from '@src/editors/containers/PdfEditor/components/messages';
6+
import messages from '@src/editors/containers/PdfEditor/components/messages';
107
import downloadMessages from '@src/editors/containers/PdfEditor/components/sections/messages';
118
import uploadMessages from '@src/editors/sharedComponents/UploadWidget/messages';
129
import editorMessages from '@src/editors/containers/EditorContainer/messages';
@@ -24,85 +21,13 @@ const render = () => editorRender(
2421
display_name: 'PDF',
2522
category: 'pdf',
2623
has_children: false,
27-
edited_on: 'Mar 13, 2026 at 18:45 UTC',
28-
edited_on_raw: '2026-03-13 18:45:35.079000+00:00',
29-
published: false,
30-
published_on: null,
31-
studio_url: null,
32-
lms_url: 'http://local.openedx.io:8000/courses/course-v1:Test+TS102+2026/jump_to/block-v1:Test+TS102+2026+type@pdf+block@b7e674c9cc19465ea4e7e2c6d9c11513',
33-
embed_lms_url: 'http://local.openedx.io:8000/xblock/block-v1:Test+TS102+2026+type@pdf+block@b7e674c9cc19465ea4e7e2c6d9c11513',
34-
released_to_students: false,
35-
release_date: null,
36-
visibility_state: 'unscheduled',
37-
has_explicit_staff_lock: false,
38-
start: '2030-01-01T00:00:00Z',
39-
graded: false,
40-
due_date: '',
41-
due: null,
42-
relative_weeks_due: null,
43-
format: null,
44-
course_graders: [
45-
'Homework',
46-
'Lab',
47-
'Midterm Exam',
48-
'Final Exam',
49-
],
5024
has_changes: null,
51-
actions: {
52-
deletable: true,
53-
draggable: true,
54-
childAddable: true,
55-
duplicable: true,
56-
unlinkable: null,
57-
},
5825
explanatory_message: null,
5926
group_access: {},
60-
user_partitions: [
61-
{
62-
id: 50,
63-
name: 'Enrollment Track Groups',
64-
scheme: 'enrollment_track',
65-
groups: [
66-
{
67-
id: 1,
68-
name: 'Audit',
69-
selected: false,
70-
deleted: false,
71-
},
72-
],
73-
},
74-
],
75-
show_correctness: 'always',
76-
hide_from_toc: false,
77-
enable_hide_from_toc_ui: false,
78-
xblock_type: 'other',
79-
upstream_info: {
80-
upstream_ref: null,
81-
upstream_name: null,
82-
downstream_key: 'block-v1:Test+TS102+2026+type@pdf+block@b7e674c9cc19465ea4e7e2c6d9c11513',
83-
version_synced: null,
84-
version_available: null,
85-
version_declined: null,
86-
error_message: 'Content is not linked to a Content Library.',
87-
downstream_customized: [],
88-
top_level_parent_key: null,
89-
ready_to_sync: false,
90-
upstream_link: null,
91-
is_ready_to_sync_individually: false,
92-
},
9327
data: '',
9428
metadata: {
9529
display_name: 'PDF',
9630
},
97-
ancestor_has_staff_lock: false,
98-
is_tagging_feature_disabled: false,
99-
taxonomy_tags_widget_url: 'http://apps.local.openedx.io:2001/authoring/tagging/components/widget/',
100-
course_authoring_url: 'http://apps.local.openedx.io:2001/authoring',
101-
user_partition_info: {
102-
selectable_partitions: [],
103-
selected_partition_index: -1,
104-
selected_groups_label: '',
105-
},
10631
},
10732
},
10833
unitUrl: {
@@ -136,9 +61,6 @@ const render = () => editorRender(
13661
],
13762
},
13863
},
139-
blockContent: null,
140-
studioView: null,
141-
saveResponse: null,
14264
blockId: 'pdf-block-id',
14365
blockTitle: 'PDF',
14466
blockType: 'pdf',
@@ -176,7 +98,7 @@ describe('PdfEditor', () => {
17698
const screen = render();
17799
screen.getByText(messages.blockLoading.defaultMessage);
178100
// And then should show the block.
179-
await waitFor(() => screen.getByText(downloadMessages.downloadOptions.defaultMessage));
101+
await waitFor(() => screen.getByText(downloadMessages.allowDownloadLabel.defaultMessage));
180102
});
181103
it('handles failure gracefully.', async () => {
182104
axiosMock.onGet(
@@ -226,7 +148,7 @@ describe('PdfEditor', () => {
226148
await user.click(dropdown);
227149
const toggle = await waitFor(() => screen.getByText(uploadMessages.manualUrl.defaultMessage));
228150
await user.click(toggle);
229-
const field = await waitFor(() => screen.getByLabelText(uploadOverrideMessages.urlFieldLabel.defaultMessage));
151+
const field = await waitFor(() => screen.getByLabelText(uploadMessages.urlFieldLabel.defaultMessage));
230152
fireEvent.change(field, { target: { value: 'https://somewhere.com/stuff.pdf' } });
231153
const saveButton = screen.getByLabelText(editorMessages.saveButtonAriaLabel.defaultMessage);
232154
await user.click(saveButton);

src/editors/containers/VideoEditor/components/VideoSettingsModal/components/HandoutWidget/index.jsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,15 +14,15 @@ import { FileUpload, MoreHoriz } from '@openedx/paragon/icons';
1414
import { FormattedMessage, useIntl } from '@edx/frontend-platform/i18n';
1515

1616
import { actions, selectors } from '@src/editors/data/redux';
17-
import * as hooks from './hooks';
18-
import messages from './messages';
1917

2018
import { FileInput } from '@src/editors/sharedComponents/FileInput';
2119
import ErrorAlert from '@src/editors/sharedComponents/ErrorAlerts/ErrorAlert';
2220
import UploadErrorAlert from '@src/editors/sharedComponents/ErrorAlerts/UploadErrorAlert';
2321
import CollapsibleFormWidget from '@src/editors/sharedComponents/CollapsibleFormWidget/CollapsibleFormWidget';
2422
import { ErrorContext } from '@src/editors/containers/VideoEditor/hooks';
2523
import { RequestKeys } from '@src/editors/data/constants/requests';
24+
import messages from './messages';
25+
import * as hooks from './hooks';
2626

2727
/**
2828
* Collapsible Form widget controlling video handouts

src/editors/containers/VideoEditor/components/VideoSettingsModal/components/LicenseWidget/index.jsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,9 +12,9 @@ import {
1212
import { Add } from '@openedx/paragon/icons';
1313

1414
import { actions, selectors } from '@src/editors/data/redux';
15+
import CollapsibleFormWidget from '@src/editors/sharedComponents/CollapsibleFormWidget/CollapsibleFormWidget';
1516
import * as hooks from './hooks';
1617
import messages from './messages';
17-
import CollapsibleFormWidget from '@src/editors/sharedComponents/CollapsibleFormWidget/CollapsibleFormWidget';
1818
import LicenseBlurb from './LicenseBlurb';
1919
import LicenseSelector from './LicenseSelector';
2020
import LicenseDetails from './LicenseDetails';

src/editors/containers/VideoEditor/components/VideoSettingsModal/components/ThumbnailWidget/index.jsx

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -18,14 +18,13 @@ import { DeleteOutline, FileUpload } from '@openedx/paragon/icons';
1818
import { selectors } from '@src/editors/data/redux';
1919
import { isEdxVideo } from '@src/editors/data/services/cms/api';
2020

21-
import { acceptedImgKeys } from './constants';
22-
import * as hooks from './hooks';
23-
import messages from './messages';
24-
2521
import CollapsibleFormWidget from '@src/editors/sharedComponents/CollapsibleFormWidget/CollapsibleFormWidget';
2622
import { FileInput } from '@src/editors/sharedComponents/FileInput';
2723
import ErrorAlert from '@src/editors/sharedComponents/ErrorAlerts/ErrorAlert';
2824
import { ErrorContext } from '@src/editors/containers/VideoEditor/hooks';
25+
import messages from './messages';
26+
import * as hooks from './hooks';
27+
import { acceptedImgKeys } from './constants';
2928

3029
/**
3130
* Collapsible Form widget controlling video thumbnail

src/editors/containers/VideoEditor/components/VideoSettingsModal/components/TranscriptWidget/index.jsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -18,17 +18,17 @@ import {
1818
import { Add, InfoOutline } from '@openedx/paragon/icons';
1919

2020
import { thunkActions, actions, selectors } from '@src/editors/data/redux';
21-
import messages from './messages';
2221

2322
import { RequestKeys } from '@src/editors/data/constants/requests';
2423
import { in8lTranscriptLanguages } from '@src/editors/data/constants/video';
2524

2625
import ErrorAlert from '@src/editors/sharedComponents/ErrorAlerts/ErrorAlert';
2726
import CollapsibleFormWidget from '@src/editors/sharedComponents/CollapsibleFormWidget/CollapsibleFormWidget';
2827

28+
import { ErrorContext } from '@src/editors/containers/VideoEditor/hooks';
2929
import ImportTranscriptCard from './ImportTranscriptCard';
3030
import Transcript from './Transcript';
31-
import { ErrorContext } from '@src/editors/containers/VideoEditor/hooks';
31+
import messages from './messages';
3232
// This 'module' self-import hack enables mocking during tests.
3333
// See src/editors/decisions/0005-internal-editor-testability-decisions.md. The whole approach to how hooks are tested
3434
// should be re-thought and cleaned up to avoid this pattern.

0 commit comments

Comments
 (0)