Skip to content

feat: pdf authoring#2916

Open
Kelketek wants to merge 5 commits intoopenedx:masterfrom
open-craft:fox/pdf-authoring
Open

feat: pdf authoring#2916
Kelketek wants to merge 5 commits intoopenedx:masterfrom
open-craft:fox/pdf-authoring

Conversation

@Kelketek
Copy link
Copy Markdown

@Kelketek Kelketek commented Feb 27, 2026

Description

This pull request adds PDF Authoring.

Visual Changes

Before

pdf-old-screencast.mp4

After

pdf-block-demo.mp4

Supporting information

https://openedx.atlassian.net/wiki/spaces/OEPM/pages/5335908397/Proposal+Add+PDF+Block+to+Base+Installation

Testing instructions

  1. Follow the instructions in this repository's README to set up a dev environment for this block.
  2. Use this branch of edx-platform to get the backend to signal to the frontend that the PDF block can be edited in the new MFE.
  3. Use this version of xblocks-contrib to get the new data fetching endpoint
  4. Add the PDF block to your course by adding "pdf" to the advanced modules list.
  5. Add a PDF block
  6. Configure the settings. Try to break it. Save. Edit. Save again. Try manually specifying a URL. Be creative :)

Other information

These changes create much more generalizable parallel functionality to some of the functionality which already exists but is mired in Redux. They're worth examining and may be used by the next developer looking to add official MFE editing support for another block.

Best Practices Checklist

We're trying to move away from some deprecated patterns in this codebase. Please
check if your PR meets these recommendations before asking for a review:

  • Any new files are using TypeScript (.ts, .tsx).
  • Avoid propTypes and defaultProps in any new or modified code.
  • Tests should use the helpers in src/testUtils.tsx (specifically initializeMocks)
  • Do not add new fields to the Redux state/store. Use React Context to share state among multiple components.
  • Use React Query to load data from REST APIs. See any apiHooks.ts in this repo for examples.
  • All new i18n messages in messages.ts files have a description for translators to use.
  • Avoid using ../ in import paths. To import from parent folders, use @src, e.g. import { initializeMocks } from '@src/testUtils'; instead of from '../../../../testUtils'

@openedx-webhooks
Copy link
Copy Markdown

openedx-webhooks commented Feb 27, 2026

Thanks for the pull request, @Kelketek!

This repository is currently maintained by @bradenmacdonald.

Once you've gone through the following steps feel free to tag them in a comment and let them know that your changes are ready for engineering review.

🔘 Get product approval

If you haven't already, check this list to see if your contribution needs to go through the product review process.

  • If it does, you'll need to submit a product proposal for your contribution, and have it reviewed by the Product Working Group.
    • This process (including the steps you'll need to take) is documented here.
  • If it doesn't, simply proceed with the next step.
🔘 Provide context

To help your reviewers and other members of the community understand the purpose and larger context of your changes, feel free to add as much of the following information to the PR description as you can:

  • Dependencies

    This PR must be merged before / after / at the same time as ...

  • Blockers

    This PR is waiting for OEP-1234 to be accepted.

  • Timeline information

    This PR must be merged by XX date because ...

  • Partner information

    This is for a course on edx.org.

  • Supporting documentation
  • Relevant Open edX discussion forum threads
🔘 Get a green build

If one or more checks are failing, continue working on your changes until this is no longer the case and your build turns green.

Details
Where can I find more information?

If you'd like to get more details on all aspects of the review process for open source pull requests (OSPRs), check out the following resources:

When can I expect my changes to be merged?

Our goal is to get community contributions seen and reviewed as efficiently as possible.

However, the amount of time that it takes to review and merge a PR can vary significantly based on factors such as:

  • The size and impact of the changes that it introduces
  • The need for product review
  • Maintenance status of the parent repository

💡 As a result it may take up to several weeks or months to complete a review and merge your PR.

@openedx-webhooks openedx-webhooks added the open-source-contribution PR author is not from Axim or 2U label Feb 27, 2026
@github-project-automation github-project-automation bot moved this to Needs Triage in Contributions Feb 27, 2026
@Kelketek Kelketek marked this pull request as draft February 27, 2026 22:20
@mphilbrick211 mphilbrick211 moved this from Needs Triage to Waiting on Author in Contributions Mar 3, 2026
@Kelketek Kelketek force-pushed the fox/pdf-authoring branch from 1926631 to 1847a88 Compare March 3, 2026 21:30
@codecov
Copy link
Copy Markdown

codecov bot commented Mar 3, 2026

Codecov Report

❌ Patch coverage is 96.16725% with 11 lines in your changes missing coverage. Please review.
✅ Project coverage is 95.56%. Comparing base (652efb5) to head (2589a13).
⚠️ Report is 21 commits behind head on master.

Files with missing lines Patch % Lines
src/editors/utils/validators.ts 68.75% 5 Missing ⚠️
src/editors/containers/PdfEditor/api.ts 87.50% 3 Missing ⚠️
...rs/sharedComponents/CollapsibleFormWidget/index.ts 0.00% 2 Missing ⚠️
...ainers/PdfEditor/components/PdfEditorContainer.tsx 88.88% 1 Missing ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##           master    #2916      +/-   ##
==========================================
+ Coverage   95.51%   95.56%   +0.05%     
==========================================
  Files        1330     1368      +38     
  Lines       30582    31418     +836     
  Branches     6934     7108     +174     
==========================================
+ Hits        29211    30026     +815     
- Misses       1303     1330      +27     
+ Partials       68       62       -6     

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

@openedx-webhooks openedx-webhooks added the core contributor PR author is a Core Contributor (who may or may not have write access to this repo). label Mar 6, 2026
@Kelketek Kelketek force-pushed the fox/pdf-authoring branch 2 times, most recently from 01e348a to 129b7e5 Compare March 13, 2026 00:44
Copy link
Copy Markdown
Contributor

@samuelallan72 samuelallan72 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@Kelketek I tested this locally and found it worked as advertised, including testing with and without PDFXBLOCK_DISABLE_ALL_DOWNLOAD = True in openedx-platform settings. I've made some comments inline for consideration.

},
easyMode: {
id: 'authoring.pdfEditor.widgets.uploadWidget.easyMode',
defaultMessage: 'Easy Mode',
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I feel like "easy mode" is slightly confusing, although I'm not sure what it should be. Something for Cassie to look at in product review. :)

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Changed it to "simple mode."

{manualMode && <TextField label="PDF Url" id="pdf-url" name="url" />}
{!manualMode && (
<>
<FileInput supportedFileFormats={supportedFileFormats} fileInput={fileInput} />
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Currently the file input behaviour here when you select a file using the "replace" button is:

  • Immediately upload the new file to studio assets without confirmation. This means if you change your mind and pick a different file, or cancel the editor altogether, the file is still uploaded to studio assets.
  • Ignore any previously uploaded file, leaving it in studio assets.

I don't think this behaviour matches user expectations or the expected meaning of "replace" here. I'm not sure what would be best, but some inline information to explain the behaviour, and/or changing the behaviour, might be helpful here. Eg. adding a warning about how uploads work and where they're uploaded to, and maybe only performing the upload when the editor is saved?

Copy link
Copy Markdown
Author

@Kelketek Kelketek Mar 13, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@samuelallan72 This behavior matches the behavior of the video handout upload field. While I agree that the auto-upload could be surprising, I believe the reason for it makes sense here based on the following:

  1. The 'save' button isn't something the block can capture and modify the behavior of. That machinery is outside of the editor's control.
  2. The only way to determine the URL value for the field is to have performed the upload, so it must be done before the user hits save.
  3. No other field on a block editor requires an additional extra save action. Users expect that hitting 'save' saves everything they've done, and if they forget to hit an additional 'upload' button, they may think things broke.

One way that this field departs from the Handout widget is that there is no 'delete handout' equivalent, because the URL is a required field. Even when you select that option, though, it only sets the field 'null' and doesn't remove the file from the course assets.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks @Kelketek , that makes sense.

Perhaps just adding some informational text next to the field so users aren't surprised by the behaviour.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The 'save' button isn't something the block can capture and modify the behavior of. That machinery is outside of the editor's control.

Ah this must be different to the legacy studio editor views then? As I was reminded of for BB-10556, in the branching xblock, we do override the save method.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@samuelallan72 Yep, it's different. The state management and callbacks are all centralized in the redux definitions. All of the editors take the same path after the save button is hit, with a big if chain changing how the data is massaged before hitting the endpoint.

By contrast, the XBlocks loaded in the legacy view set up their own save buttons and handlers not connected to the redux code, and send a signal to close when they're done.

I'll add a blurb warning the user.

@samuelallan72
Copy link
Copy Markdown
Contributor

Some notes on how I set things up locally for testing this PR along with openedx/openedx-platform#38148 and openedx/xblocks-contrib#193 , since there were several steps.
A rough reconstruction of the commands:

# my master devstack with master branch of edx-platform mounted
cd devstack-master
cd edx-platform
git remote update
git checkout fox/pdf-editor
cd ..

g c git@github.com:openedx/frontend-app-authoring
cd frontend-app-authoring
g remote add opencraft git@github.com:open-craft/frontend-app-authoring
git remote update
git checkout fox/pdf-authoring
cd ..
tutor mounts add ./frontend-app-authoring/

tutor images build mfe
tutor images build openedx-dev
tutor dev launch -I

# install xblocks-contrib into the cms and lms environment.
# It must be done this way due it being a core requirement of openedx-platform (ie. it cannot be in the pip extra requirements)(?).
tutor dev exec cms pip install -e git+https://github.com/open-craft/xblocks-contrib.git@fox/pdf-struct-return#egg=xblocks_contrib
tutor dev exec lms pip install -e git+https://github.com/open-craft/xblocks-contrib.git@fox/pdf-struct-return#egg=xblocks_contrib

tutor dev stop
tutor dev launch -I

# not sure how this works, but authoring needs to be running locally (otherwise it's just a blank page for me...)
tutor dev stop authoring
cd frontend-app-authoring
npm ci
npm run dev

Waffle flag

for the legacy_studio.pdf_editor waffle flag editing, visit http://studio.local.openedx.io:8001/admin/waffle/flag/

This flag didn't work for me.

Testing the frontend

  • import the demo course, or make a new course
  • add "pdf" to the advanced components list
  • add a pdf component and play with it. :)

Testing globally disabling the pdf download button

I used this single-file tutor plugin:

from tutor import hooks

hooks.Filters.ENV_PATCHES.add_item(
    (
        "openedx-common-settings",
        "PDFXBLOCK_DISABLE_ALL_DOWNLOAD = True"
    )
)

And did

tutor dev stop
tutor dev launch -I

(This worked fine.)

@Kelketek Kelketek force-pushed the fox/pdf-authoring branch 5 times, most recently from edaaa5d to a2dfb41 Compare March 17, 2026 00:32
@cassiezamparini
Copy link
Copy Markdown

@Kelketek This is looking great! I have a couple of questions from a UX perspective, mostly to understand the intention a bit better:

  • Default PDF on creation: When adding a new PDF XBlock, there’s already a file attached by default. Do you know where this file comes from? I initially expected a blank state where I upload my own file. I know this is a common Open edX pattern, but I’ve never really questioned it before, and I found it a bit confusing in this context.

  • “Source document URL”: Is this field optional, and what’s the intended use case? If I’ve already uploaded a PDF, I wasn’t sure why I would also include a URL, is it mainly for referencing an original or editable version? If that’s the case, maybe it would be clearer to separate this from “Download options” and make the intent a bit more explicit.

@Kelketek
Copy link
Copy Markdown
Author

Kelketek commented Mar 17, 2026

@cassiezamparini

Default PDF on creation: When adding a new PDF XBlock, there’s already a file attached by default. Do you know where this file comes from? I initially expected a blank state where I upload my own file. I know this is a common Open edX pattern, but I’ve never really questioned it before, and I found it a bit confusing in this context.

This file is the default of the PDF XBlock, and predates this MR. It links to this PDF: https://tutorial.math.lamar.edu/pdf/Trig_Cheat_Sheet.pdf . You can learn more about the document by reading the home page: https://tutorial.math.lamar.edu/

Removing it is an option but would expand the scope, since it would mean changing the behavior of the block to accommodate not having the field set and giving the user reasonable feedback about it. I think that's a reasonable improvement, but I'd like to suggest it not be part of this PR.

“Source document URL”: Is this field optional, and what’s the intended use case? If I’ve already uploaded a PDF, I wasn’t sure why I would also include a URL, is it mainly for referencing an original or editable version? If that’s the case, maybe it would be clearer to separate this from “Download options” and make the intent a bit more explicit.

The source document URL is optional, and is there to give you the source the PDF is from, which might be a Powerpoint, Word Document, or other editable and/or accessible format. If we remove this from Download options, we should also remove Source Button Text, which sets the text of the source download link (perhaps it should be called 'source link text' since the source 'button' appears to just be a link when rendered, but that's what the previous version called it.)

That would leave only one option in download options, whether you can download at all. However, that toggle also implies whether you can download the source version, too, which is why they're in a group. When unchecking the 'allow download' button, the source fields disable, as they won't be used. There's no point in providing a source if you're not even providing the rendered document.

If you'd like an alternative arrangement (or maybe, an alternative label for the "Download Options" group?), please let me know what you think would work best.

@Kelketek Kelketek force-pushed the fox/pdf-authoring branch 6 times, most recently from 14f6985 to 1203335 Compare March 18, 2026 00:37
@cassiezamparini
Copy link
Copy Markdown

@Kelketek

Removing it is an option but would expand the scope, since it would mean changing the behavior of the block to accommodate not having the field set and giving the user reasonable feedback about it. I think that's a reasonable improvement, but I'd like to suggest it not be part of this PR.

Noted, and agreed. Let's look at this in future.

I mocked up a version to improve clarity and flow. If these changes don’t fit within budget, I think the most impactful improvement would be updating the labels and helper text.

Screenshot 2026-03-18 at 11 52 27
  1. I've moved the labels above fields, making things easier to understand before interacting. We've done this with other XBlocks.
  2. I've moved the helper text above fields, which explains what’s needed before users enter anything.
  3. I've removed “Download options” heading as all fields are related.
  4. I've moved the Source File URL above the Source File Button text
  5. I've removed the collapsible sections. With such a few fields, keeping everything visible is simpler and reduces friction.

@Kelketek Kelketek force-pushed the fox/pdf-authoring branch 2 times, most recently from 2e47927 to 1f33696 Compare March 18, 2026 16:19
@Kelketek
Copy link
Copy Markdown
Author

@cassiezamparini and cc @samuelallan72

Here's the revised interface, based on both of your feedback:

pdf-authoring-revised.mp4

@Kelketek Kelketek force-pushed the fox/pdf-authoring branch 2 times, most recently from 7e59e76 to e78610a Compare March 18, 2026 20:22
@Kelketek Kelketek marked this pull request as ready for review March 18, 2026 20:23
@Kelketek
Copy link
Copy Markdown
Author

@samuelallan72 This is ready for review now. Thanks!

Copy link
Copy Markdown
Contributor

@samuelallan72 samuelallan72 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@Kelketek 👍 looks good to me now! Thanks for addressing the comments and for adding thorough tests.

I left a suggestion for consideration. Also, please update the PR title now that it's no longer a wip.

  • I tested this: followed test instructions, spot checked the video editor too since some things have been moved around for that
  • I read through the code
  • I checked for accessibility issues
  • Includes documentation

fileHint: {
id: 'authoring.sharedComponents.uploadWidget.fileHint',
defaultMessage: 'This is the file your learners will see embedded in your course. Files are immediately '
+ 'uploaded to the file manager.',
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe something like this to clarify where it ends up?

Suggested change
+ 'uploaded to the file manager.',
+ 'uploaded to course assets (Files).',

@samuelallan72
Copy link
Copy Markdown
Contributor

@Kelketek is it intentional that the "Original File URL" fields are readonly when the "Show PDF download link" is unchecked?

@cassiezamparini
Copy link
Copy Markdown

Looks good @Kelketek. Thanks for making those UX changes

@Kelketek Kelketek changed the title feat: wip pdf authoring feat: pdf authoring Mar 19, 2026
@Kelketek Kelketek force-pushed the fox/pdf-authoring branch from e78610a to 5121d38 Compare March 19, 2026 19:06
@Kelketek
Copy link
Copy Markdown
Author

Thanks, @samuelallan72 . I think you're right-- I've removed the disabling of those fields. On reflection, it is conceivable that the PDF viewing of the block is really more for in-browser previewing but that the course author would want learners to download the original. I suppose that's the premise of the scope of the 'Document Block' on WGU's end, which is based on this work.

I also applied your wording suggestion.

This is ready for someone with commit rights to look at.

@Kelketek Kelketek force-pushed the fox/pdf-authoring branch from 5121d38 to 9a1ed3c Compare March 26, 2026 23:37
Copy link
Copy Markdown
Contributor

@bradenmacdonald bradenmacdonald left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@Kelketek Thanks for all the nice cleanups here!

This breaks adding PDFs to libraries (v2). Can you please take a look?

Library -> Image -> Advanced/Other -> PDF

Note that in libraries (but not courses), clicking the "PDF" button should open the editor modal without creating a new block, and pressing Cancel at that point does not add anything to the library (this is working with the other editors). I think this could be the main issue, that the PDF editor is trying to load its state from Studio before it has an ID assigned. (We want it to work the same way in course eventually, but I think it's more challenging to do so there.)

Also, the API for uploading files in libraries is different as it's scoped to the component, not the course/library. See the "Text" editor for examples.


The rest of my comments are very minor.

}, expect.any(Function));
});

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

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Since the only difference between these two new PDF tests is which editor gets auto-opened after it gets created, can you please clarify that in the test description, and maybe factor out the common code so it's more obvious that 95% of these are the same?

It's also non-obvious from this test code that calling handleCreateNewCourseXBlock with category: pdf opens the legacy editor and calling it with some function opens the React editor.

import { getConfig } from '@edx/frontend-platform';
import {
FC, useEffect, useState, useMemo, useCallback,
FC, useEffect, useState, useMemo, useCallback, Fragment,
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
FC, useEffect, useState, useMemo, useCallback, Fragment,
FC, useEffect, useState, useMemo, useCallback,

This change seems unnecessary ?

Comment on lines +46 to +62

const queryClient = useQueryClient();
// Drop state on unmount so that when we open again we have revised state.
useEffect(() => () => {
// We use a unique ID because a component higher up in the chain remounts quickly on
// startup, and cancelling or invalidating that transaction immediately results in
// the query key being recreated immediately, and then being populated with the result
// of being canceled.
//
// Invalidating will result in the user seeing the form with stale data before it's
// refetched. Additionally, the stale flag won't get properly cleared due to the
// race conditions for the remount.
//
// Forcing a unique ID appears to be the most practical workaround.
const queryKey = ['blockData', blockId, uniqueId];
queryClient.removeQueries({ queryKey });
}, []);
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This really feels like it shouldn't be necessary to me. I tried deleting all this code, and after doing so I couldn't figure out how to observe the race condition; I just saw the data loading as expected.

What do you mean by "cancelling or invalidating that transaction" ? Is this something that happens implicitly when the component is remounted?

From the docs is sounds like all you need to do is to modify your useBlockData to not use the cancellation signal provided by React Query:

By default, queries that unmount or become unused before their promises are resolved are not cancelled. This means that after the promise has resolved, the resulting data will be available in the cache. This is helpful if you've started receiving a query, but then unmount the component before it finishes. If you mount the component again and the query has not been garbage collected yet, data will be available.

However, if you consume the AbortSignal, the Promise will be cancelled (e.g. aborting the fetch) and therefore, also the Query must be cancelled. Cancelling the query will result in its state being reverted to its previous state.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@bradenmacdonald

So, this was a pain in the ass to figure out, but the issue is this:

A user who is editing a PDF block might have out of date information when fiddling with it. If they edit it in another window, we want the window they're in to be up to date when they hit the edit button again. This means we want the data evicted on unmount.

In fact, though, you don't even have to be editing it in another window. The information in the query will be out of date when you save. If you open the editor again, it will still be the info it received when it first loaded your block before your edits, meaning it will appear as though you made no changes at all. This is because the mutation is separate.

However, cancelling and attempting to remove the data on unmount when the component is quickly unmounted and remounted makes it to where if the query key is the same, the error bubbles up. And if you try to just do a clean removal without abort, the request eventually finishes, so if a user is currently editing the form, the data suddenly flashes back to the old version and removes your changes.

Copy link
Copy Markdown
Contributor

@bradenmacdonald bradenmacdonald Mar 31, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@Kelketek

If they edit it in another window, we want the window they're in to be up to date when they hit the edit button again.

In such cases, if they've edited it in another window, I think it's totally acceptable that they might see a flash of old data when opening the edit form, before the newest data is loaded in. I don't think that will happen too often, nor is worth complex workarounds. Assuming the configured staleTime is somewhere around the default of five minutes, that issue would only occur if they (1) opened the editor in window A, then (2) edited the same component and saved changes in window B, and then (3) within five minutes, opened the editor again in window A.

In fact, though, you don't even have to be editing it in another window. The information in the query will be out of date when you save. If you open the editor again, it will still be the info it received when it first loaded your block before your edits, meaning it will appear as though you made no changes at all. This is because the mutation is separate.

Hmm, something is fishy here. When the editor modal is open, if you click Save, we want to run the mutation (and invalidate the query!) before the modal editor is closed (because we want to show an error and keep the editor open if the mutation/save fails). Since the editor is still open when the mutation completes and gets invalidated, React Query should trigger a refetch before you close the editor, and when you next open the editor it should already be showing the correct information. No?

You can also use optimistic updates or other techniques to push the new data into the cache when the mutation completes, so that it doesn't matter if the data is ever refetched or not - the cache will hold the latest version as soon you successfully save.

In either case, I don't think you should need this workaround.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@bradenmacdonald I'll take another look at it, then. This might just be my general unfamiliarity with React Query-- it's my first time using it.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Apparently when you invalidate a query, you can also specify refetchType: 'all' to force them to refetch immediately, even if the query is not mounted/active. TIL.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@bradenmacdonald I started looking into this more:

Hmm, something is fishy here. When the editor modal is open, if you click Save, we want to run the mutation (and invalidate the query!) before the modal editor is closed (because we want to show an error and keep the editor open if the mutation/save fails). Since the editor is still open when the mutation completes and gets invalidated, React Query should trigger a refetch before you close the editor, and when you next open the editor it should already be showing the correct information. No?

No. Because the saving infrastructure here is still in Redux, and doesn't use React query. The code which should be invalidating the query is deep in the nested tangle of thunks, far from where I can pull the query client from the context, preventing me from sending the invalidation signal :/


export type PartialEditorState = RecursivePartial<EditorState>;

export function initializeStore(preloadedState?: PartialEditorState) {
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

^ This will probably have some conflicts with #1915 which removes initializeStore and consolidates on createStore for creating the editor redux store. Not a big deal.

`${studioEndpointUrl}/api/contentstore/v2/validate/numerical-input/`
)) satisfies UrlFunction;

// There's also a 'handlerUrl', but it's different for some reason. Not sure what one has to do to use the 'v2' URLs.
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please update the comments:

POSTing to the /handler_url/ endpoint does not call the handler directly; it returns some data that includes a URL, which you can then use to call the handler later. You can use the resulting URL to run the handler even without cookies / in an unauthenticated iframe.

POSTing to this /handler/ endpoint simply executes the handler directly, and returns the result.

So perhaps handlerUrl above should be renamed to handlerUrlUrl 😛

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@bradenmacdonald I've renamed the old one boundHandlerUrl, and the new one handlerUrl. Let me know if that works.

Comment on lines +39 to +41
// This is coded as a hard limit for handout uploads elsewhere-- seems like an arbitrary thing
// to have in the frontend, but if that's how it's done...
if (file.size > 20_000_000) {
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We have two issues related to this ;) Please link to one of them

#2958

#1661

[blockTypes.video_upload]: VideoUploadEditor,
// ADDED_EDITORS GO BELOW
[blockTypes.game]: GameEditor,
[blockTypes.pdf]: PdfEditor,
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm still unclear on what ADDED_EDITORS GO BELOW meant or what GameEditor even is (an example I think?) but I think pdf belongs with the other four above the line //

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@bradenmacdonald I think it's for the Games block. This is something edX has been working on and it looks like they left a stub in here for now. I can move the PDF editor up.

/>
</DataTable>
<FileInput key="generic-file-upload" fileInput={fileInputControl} supportedFileFormats={supportedFileFormats} />
<FileInput key="generic-file-upload" fileInput={fileInputControl} supportedFileFormats={supportedFileFormats} id="generic-file-upload-field" />
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Are we sure there will only be one <FileTable per page, and we can hard-code an ID like this?

</div>
)}
<FileInput key="transcript-input" fileInput={input} supportedFileFormats={['.srt']} />
<FileInput key="transcript-input" fileInput={input} supportedFileFormats={['.srt']} id="transcript-file-input" />
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this component ever rendered multiple times in the same page?

"start:with-theme": "paragon install-theme && npm start && npm install",
"dev": "PUBLIC_PATH=/authoring/ MFE_CONFIG_API_URL='http://localhost:8000/api/mfe_config/v1' fedx-scripts webpack-dev-server --progress --host apps.local.openedx.io",
"test": "TZ=UTC fedx-scripts jest --coverage --passWithNoTests",
"test:dev": "TZ=UTC fedx-scripts jest --coverage --watch --passWithNoTests",
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do you like having the coverage enabled in this mode? Isn't it slow and annoying? :p

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I do, when trying to make sure I'm getting the coverage up. It usually reruns with just the stuff I've changed, so the slowness isn't too bad. I've gone ahead and removed it, though.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That's fine, if you like it or use it, leave it in.

@Kelketek Kelketek force-pushed the fox/pdf-authoring branch 3 times, most recently from 5ffa555 to abafb73 Compare April 1, 2026 19:42
@Kelketek Kelketek force-pushed the fox/pdf-authoring branch from abafb73 to da0d8fd Compare April 1, 2026 22:32
@Kelketek
Copy link
Copy Markdown
Author

Kelketek commented Apr 1, 2026

@bradenmacdonald This is ready for you again.

@bradenmacdonald
Copy link
Copy Markdown
Contributor

bradenmacdonald commented Apr 2, 2026

@Kelketek Thanks! Nice work. It's much better. There is just some last issue with files in library mode.

When I create a new PDF block in a library, it says to save before I can upload files. Fine*. So I save and re-open, and then it says "Unknown filename", but it should say "No file attached". If I then choose "Replace" and upload a PDF, it says "There was an error uploading your file." and the form still shows "Unknown Filename". However, back in the component's side bar under Details tab > Advanced details > Assets, I can see that the PDF was successfully uploaded and attached to the component, so I'm not sure why it's telling me that an error occurred. I can see there are no errors in the CMS logs - the upload endpoint returned 200, and there's nothing in the JS console either.

Screenshot 2026-04-02 at 11 07 01 AM

*The "Text" block allows you to upload files even before saving in library mode, so it would be good if the PDF block eventually supported that too. I don't want to hold up this PR though; maybe add a TODO comment. You'd just need to store the new files as form data alongside the other changes from the user, rather than immediately uploading them when they're added.

@Kelketek
Copy link
Copy Markdown
Author

Kelketek commented Apr 2, 2026

@bradenmacdonald Found the issue, and fixed.

On my devstack, the iframe doesn't load like I would expect when in the Library preview, citing CORS issues. I'm not sure if there's anything I should do there.

Embedding it in a course shows the issue there as well-- so I'd presume whatever is serving the assets is not sending the expected X-Frame-options header.

@Kelketek
Copy link
Copy Markdown
Author

Kelketek commented Apr 2, 2026

@bradenmacdonald I've looked into it more and it may indeed be an issue.

The URL that the Library asset view returns goes to a Studio backend URL where the studio serves up the file directly.

This is NOT intended to be referenced by blocks directly, according to the docstring. The Text block embeds images using the 'path' instead, which is something like 'static/whatever.png' rather than a full URL. That's not a real path, though.

When rendering the block in the course, the embedded URL is transformed in the process somewhere (need to find where, haven't gotten there yet) into the actual target asset URL. However, this process is not automatic. It looks like I'd need to add support to the block in order for it to do this-- so I'm going to need to dive into the HTML code and determine how it can be made to pull the desired asset from the right place.

Likely, that will mean I need to adjust the code here to use the path instead, and then I will need to make a PR to xblock-utils to enable this transformation. I'll ping you once that's done.

@Kelketek
Copy link
Copy Markdown
Author

Kelketek commented Apr 3, 2026

@bradenmacdonald This is ready, but there is a caveat.

I've been able to determine that while the Text editor showed 'static/whatever.png', it actually saved it as /static/whatever.png, and that though the fragment generated by the HTML block uses the /static/whatever.png path, some template processor is changing this before returning the resulting HTML from the server.

Thus, changing the code to use the /static/ prefixed path fixes the issue of the wrong link being embedded. And if you save the PDF in the library, and then embed the library component in a course, it works.

HOWEVER, the loading of the asset using the library component's preview URL still has the iframe embedding header issue. So I'm going to see if I can sensibly modify the asset preview view to allow iFraming in openedx/openedx-platform#38148 .

Failing that, an alternative would be adding in PDF.js and rendering it without using an iFrame-- putting it in a vendor directory and loading it as part of the template.

Update: Done!

@mphilbrick211 mphilbrick211 moved this from Waiting on Author to In Eng Review in Contributions Apr 6, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

core contributor PR author is a Core Contributor (who may or may not have write access to this repo). open-source-contribution PR author is not from Axim or 2U

Projects

Status: In Eng Review

Development

Successfully merging this pull request may close these issues.

6 participants