Show file download/upload/print affordances in app context - #15050
Conversation
npm Package VersionsWarning The following packages have changed files but no version bump:
If these changes affect published code, consider bumping the version. |
Build Artifacts
Smoke test screenshot |
rtibbles
left a comment
There was a problem hiding this comment.
A little bit more cleanup, but the code here looks broadly sensible.
rtibbles
left a comment
There was a problem hiding this comment.
Just one more bit of cleanup, then please tidy up the commit history.
01a946c to
963dc96
Compare
rtibbles
left a comment
There was a problem hiding this comment.
Code makes sense - needs commit history tidy up, ready for manual QA though.
963dc96 to
c4918c6
Compare
|
Tidied the commit history — squashed the four per-component commits into a single commit ( |
c4918c6 to
972e8a8
Compare
|
APK installed on Redmi10C with Android 13: All printing/saving as PDF, plus saving/downloading CSV files is working beautifully, even those on the Facility/Data page that are failing in Windows app! 🎉 🎆 And I even had the Android Studio ready and connected to the device to capture any potential issues... 😅 Just need to restart and try the CSV file import feature, but looking great for now 👏🏽 |
|
Thanks — both traced to a concrete cause and are fixed in 1ba7521 and 6c19c40. Needs a fresh build to re-verify on device. Windows downloads. Session logs, summary logs and the generated user CSV were the only three call sites using Android CSV import. If Android still greys out CSVs after this build, the chooser intent is built shell-side and belongs to kolibri-installer-android#267. |
6c19c40 to
f15a758
Compare
Several file upload/download/print UI affordances were gated behind isAppContext, hiding them in the installed apps. That was a workaround for the assumption that apps could not handle file downloads; the Windows and Mac apps handle this natively and the Android WebView work is tracked in kolibri-installer-android#267. Remove the gates so the affordances render regardless of context: - DownloadButton "Save to device" button visibility no longer checks isAppContext. - ReportsControls coach report CSV export drops isAppContext from exportDisabled (and the stale "disable in app mode" comment). - DataPage facility CSV import/export and session/summary log download sections drop the canUploadDownloadFiles gate. - AllPasswordsPage "Print all passwords" button always renders and the title row always uses the two-column layout, on both the coach and facility passwords pages. Behaviour in non-app context is unchanged. Blocking Jest tests are removed and replaced with app-context render assertions. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
f15a758 to
9cfe9dc
Compare
|
Still hitting some bumps with importing CSV in Kolibri on Android:
logs.zip
|
|
Thanks — the logcat had both answers in it. One is fixed here in de97ef7; the other is a separate bug I've filed as #15094. Greyed-out local CSVs — fixed. I had this wrong when I said it was the String[] mimeTypes = Arrays.stream(accepted != null ? accepted : new String[0])
.filter(t -> t != null && !t.isEmpty())
.toArray(String[]::new);
Server disconnect on It then comes back on a different port (46655 → 45791) and the shell reloads the WebView, which discards the picked file. So the file selection was fine; the page underneath it was replaced. That's a service-lifecycle problem affecting any handoff to another activity, not just CSV import, so I've filed #15094 rather than widen this PR. That means CSV import on Android still won't complete until #15094 lands — worth knowing before you re-test. What this build should show is that local CSVs are selectable rather than greyed. If you pick one quickly enough to stay inside the idle window, the import should go through. Needs a fresh build. |
|
One follow-up on that: the new Shout if you'd rather that went in its own PR. |
ee55fb7 to
b9e6660
Compare
|
Mac app:
Here's a video for both issues: mac.app.mp4Mac app logs: mac-app-logs.zip The Android app:
Android.download.mp4 |
|
Thanks — the two Mac problems are one cause, and both are fixed in 911cd52. The Android feedback point is 2c68bda. Mac. wxPython's The app now intercepts I have no macOS here, so this one is unverified beyond unit tests on the message handling. It needs a fresh DMG. Android download feedback. Fair — the notification shade is the only signal today. There is now a toast on start, on completion and on failure, so something lands in front of the user without them going looking. Android stuck button + tooltip. This one I'm leaving alone, and I don't think it belongs to this PR. It's sticky Both platforms need fresh builds. Separately, a correction to my earlier note about |
1cf19f3 to
9ed1683
Compare
9ed1683 to
15b909b
Compare
|
Confirming that importing CSV files in latest Kolibri APK asset on Android is now working correctly for files located on the device. ✔️
|
rtibbles
left a comment
There was a problem hiding this comment.
The manual QA shows this is working, so mostly I am just interested in some cleanup here, while preserving the code as is.
If any of the tests are not giving us helpful coverage they should be removed or replaced.
When things have been fixed up, we should tidy up the commit history to give a logical story of how things are.
| # backend; intercept it and route to ICoreWebView2_16::ShowPrintUI via | ||
| # webview2_native. | ||
| PRINT_SCRIPT = _bridge_script( | ||
| """ |
There was a problem hiding this comment.
This feels concise enough to be inline, so this is OK.
There was a problem hiding this comment.
Left inline as-is.
| # exporter builds, is dropped entirely. Read the resource in the page, where the | ||
| # session cookie lives, and hand the bytes to Python to write out. | ||
| DOWNLOAD_SCRIPT = _bridge_script( | ||
| r""" |
There was a problem hiding this comment.
I think this script is extensive enough that I would prefer it be stored in a .js file and loaded to be used. If needs be. It may need some linting exclusion exceptions.
There was a problem hiding this comment.
Moved to src/kolibri_app/scripts/download.js, read at import time via importlib.resources.files and passed through the same __BRIDGE__/__TYPE__ substitution as the inline print shim. Added the directory to datas_list in kolibri.spec so PyInstaller bundles it.
No lint exclusion turned out to be needed: lint-frontend globs {kolibri*/**/frontend,packages,build_tools}/**/*.{js,vue,...}, so nothing under platforms/ is linted (verified with a deliberately broken probe file — prek passed it). I kept a /* eslint-disable no-console */ at the top of the file anyway so editor ESLint integrations, which resolve the root config regardless of that glob, do not flag the console.error in the catch.
| @@ -0,0 +1,98 @@ | |||
| import base64 | |||
There was a problem hiding this comment.
I have been iffy in the past about tests for the desktop app, as they have involved significant mocking of the wx APIs - how much value are these actually adding?
There was a problem hiding this comment.
Fair question. I went through them and cut the ones that were really only asserting the shape of a mock call:
test_offers_the_sanitised_name_as_the_default— deleted. It assertedwx.FileDialog.call_args.kwargs["defaultFile"], which is a mock-shape assertion that duplicates the direct_download_filenametests.test_print_message_defers_the_dialog— folded intotest_repeated_print_messages_enqueue_one_dialog, which now asserts the target as well as the count, so nothing is lost.- Added
TestBridgeScripts— no wx at all; it catchesscripts/download.jsfailing to load or shipping with the__BRIDGE__/__TYPE__placeholders unsubstituted, which is now a real packaging failure mode after the change above.
On the mocking: the module-level sys.modules wx stub is not new here — it is the pattern already in test_kolibri_home_readable.py on develop; this branch only hoisted it into conftest.py so both modules share it. Beyond that the tests touch exactly two wx names, wx.CallAfter (a scheduling primitive) and wx.FileDialog (the OS file picker — a hard boundary), and what they assert is our logic, not wx:
TestDownloadFilename— the path-traversal guard on a page-supplied filename. Pure function, no wx, and the one place a bug is a security bug.TestScriptMessages— the bridge dispatch contract: malformed or unknown JSON from the page must not blow up the handler, and repeatedwindow.print()calls must not stack up dialogs (that coalescing is the non-obvious bit).TestSaveDownload— writes real bytes to a real temp file and asserts the content; the mock only stands in for the picker returning a path. Also covers "cancel writes nothing" and "undecodable base64 never opens the dialog".
If you would still rather not carry the two wx-touching classes, I am happy to drop TestScriptMessages and TestSaveDownload and keep only TestDownloadFilename and TestBridgeScripts — just say the word.
7872f77 to
843cc0f
Compare
rtibbles
left a comment
There was a problem hiding this comment.
QA checks out, post-QA code changes minimal and make sense.
The session log, summary log and user CSV downloads called window.open(url, '_blank'). The installed apps render Kolibri in an embedded webview that does not open new windows, so nothing happened — QA saw exactly these three downloads fail on Windows while every other CSV export worked, because the others click an <a download> instead. Route all three through a shared downloadCsvFile helper that uses that same link-click mechanism. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
accept=".csv" is a browser-only shorthand. The installed apps' webviews pass the first accept entry straight to the system file picker as a MIME type, and no file matches the literal string ".csv" — on Android QA saw the picker open with every CSV greyed out. Put text/csv first; browsers take either form. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
onShowFileChooser passed the web accept entries straight into OpenDocument, which filters on EXTRA_MIME_TYPES. A bare ".csv" is not a MIME type and matches no document, so it greyed out every file the picker offered. Android's document providers derive a file's type from its extension via MimeTypeMap, so resolving accept extensions through that same map is what makes those files match. This is why QA saw the split: Drive reports text/csv and stayed selectable, while local files reported whatever MimeTypeMap maps .csv to and greyed out. The comment added alongside accept="text/csv,.csv" said webviews use only the first accept entry. The logcat shows all entries reaching the intent, so it is replaced with the actual reason both forms are listed. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The WebKit backends wxPython's html2 uses on macOS and Linux have no download support: an anchor's `download` attribute is ignored, so the click either navigates the webview to the file — showing a facility CSV as text in place of the page — or, for the data: URLs the coach report exporter builds, is dropped on the floor and nothing happens at all. Intercept `a[download]` clicks in an injected script, read the resource in the page where the session cookie lives, and hand the bytes to Python to write out through a save dialog. WebView2 already has its own download UI, so Windows keeps the print shim it had and nothing else. Bridge messages are JSON objects keyed on "type" now that there is more than one of them.
A download leaves the page as soon as the button is pressed and finishes in the notification shade, so all the page itself shows is a button that looks stuck. Toast on start, completion and failure so the feedback lands where the user is already looking.
843cc0f to
6e42b68
Compare





Summary
isAppContextgates hid file upload/download/print affordances in installed apps — a workaround from when apps couldn't handle downloads. Removes the four gates so the affordances render in every context.Once visible in-app, several of them didn't actually work. Fixed here:
window.open(url, '_blank')is a no-op in the webview. NewDataPage/downloadCsvFile.jsclicks a synthetic<a download>instead — the same idiom the coach export already used viacsv-generator-client.onShowFileChooserpassed the webacceptentries straight intoOpenDocument, which filters onEXTRA_MIME_TYPES— a bare.csvis not a MIME type and matches nothing. NewFileChooserUtils.toPickerMimeTypesresolves extensions throughMimeTypeMap, the same map document providers type files with. The input listsaccept="text/csv,.csv"so providers that report a type rather than derive one also match.download: an http(s) link navigated the webview to the CSV in place of the page, and thedata:URLs the coach exporter builds were dropped entirely. An injected script now interceptsa[download]clicks, reads the resource in the page where the session cookie lives, and hands the bytes to Python to write out through a save dialog. WebView2 has its own download UI, so Windows keeps only the print shim it had. Bridge messages are JSON keyed ontypenow that there is more than one.References
Fixes #14654. Android WebView download support: learningequality/kolibri-installer-android#267.
Reviewer guidance
Confirm each of:
On each of:
AI usage
Used Claude Code to remove the gates and update tests following a pre-approved plan. Verified with the affected Jest, pytest and JVM suites and prek lint. The four platform fixes came out of manual QA in the Windows, Mac and Android apps.
Deviations from the issue spec
@rtibblesbot's comments are generated by an LLM, and should be evaluated accordingly
How was this generated?
🟡 Waiting for feedback
Last updated: 2026-07-29 17:12 UTC