Describe the bug
In apps/lms/frontend/src/components/Settings/BrandSettings.vue (Frappe Learning v2.54.1), the upload flow for branding images (banner_image, favicon, app_logo) does not write the returned file_url back into the local Vue state branding.data[field].file_url. As a consequence, when the user clicks "Update", the helper getFieldsToSave() falls back to null for every image field, and frappe.client.set_value overwrites Website Settings with empty values. The uploaded File records also remain orphan in tabFile (no attached_to_doctype / attached_to_name).
To Reproduce
Steps to reproduce the behavior:
- Log in as Administrator.
- Navigate to
/lms/settings → Branding tab.
- Click "Brand Image" → choose any
.png (e.g., 200×60).
- Wait for the upload toast (success).
- Click "Update" at the top right.
- Open
bench --site <site> console:
frappe.db.get_single_value("Website Settings", "banner_image")
# Expected: "/files/whatever.png"
# Actual: None
- Inspect
tabFile:
SELECT name, file_url, attached_to_doctype, attached_to_name
FROM tabFile WHERE creation > NOW() - INTERVAL 5 MINUTE;
The new file row is present but attached_to_* columns are NULL.
Expected behavior
After clicking "Update", Website Settings.banner_image (and favicon, app_logo) should be persisted with the file_url of the uploaded image, identical to the behaviour of /app/website-settings in Desk.
Root cause (suggested)
BrandSettings.vue lines 86-107: getFieldsToSave() reads branding.data[field.name].file_url, but the upload handler (likely in SettingFields.vue / the FileUploader it embeds) never writes the new file_url back into the parent's branding.data. The fallback : null on line 97 then nulls the field.
// BrandSettings.vue:93-97 (current behavior)
fieldsToSave[field.name] =
branding.data[field.name] && branding.data[field.name].file_url
? branding.data[field.name].file_url
: null // ← always falls here because file_url is never written after upload
Suggested fix
In the onSuccess callback of the underlying FileUploader, mutate branding.data[field.name] = { file_url: <returned file_url>, ... } before the isDirty watcher fires. Alternatively, expose a v-model contract from SettingFields.vue that propagates the upload result back.
Reference for the upload chain: apps/frappe/frappe/handler.py::uploadfile returns a File doc whose file_url is the correct value to persist.
Workaround (for users hitting this in v2.54.1)
Edit Website Settings.banner_image and Website Settings.favicon directly via Desk: /app/website-settings → Brand section → upload via the native form. This uses the canonical frappe.client.set_value path on the Attach Image field (stored as file_url string) and works as expected. The uploaded file gets the correct attached_to_* link, and the LMS portal (/lms/) picks up the new logo after bench --site <site> clear-cache.
Desktop
- OS: macOS Sequoia 15.2
- Browser: Chromium 130 (verified via Playwright MCP)
- Version: latest stable
Versions
- Frappe Framework: v15.107.5
- Frappe Learning: v2.54.1
Additional context
Verified end-to-end with Playwright MCP automation: the upload network request returns a valid file_url, but the subsequent set_value request sends {"banner_image": null} in its payload. This rules out any backend issue — the bug is purely in the Vue state propagation between the file uploader and the parent branding.data reactive object.
Describe the bug
In
apps/lms/frontend/src/components/Settings/BrandSettings.vue(Frappe Learning v2.54.1), the upload flow for branding images (banner_image,favicon,app_logo) does not write the returnedfile_urlback into the local Vue statebranding.data[field].file_url. As a consequence, when the user clicks "Update", the helpergetFieldsToSave()falls back tonullfor every image field, andfrappe.client.set_valueoverwritesWebsite Settingswith empty values. The uploadedFilerecords also remain orphan intabFile(noattached_to_doctype/attached_to_name).To Reproduce
Steps to reproduce the behavior:
/lms/settings→ Branding tab..png(e.g., 200×60).bench --site <site> console:tabFile:attached_to_*columns are NULL.Expected behavior
After clicking "Update",
Website Settings.banner_image(andfavicon,app_logo) should be persisted with thefile_urlof the uploaded image, identical to the behaviour of/app/website-settingsin Desk.Root cause (suggested)
BrandSettings.vuelines 86-107:getFieldsToSave()readsbranding.data[field.name].file_url, but the upload handler (likely inSettingFields.vue/ theFileUploaderit embeds) never writes the newfile_urlback into the parent'sbranding.data. The fallback: nullon line 97 then nulls the field.Suggested fix
In the
onSuccesscallback of the underlyingFileUploader, mutatebranding.data[field.name] = { file_url: <returned file_url>, ... }before theisDirtywatcher fires. Alternatively, expose av-modelcontract fromSettingFields.vuethat propagates the upload result back.Reference for the upload chain:
apps/frappe/frappe/handler.py::uploadfilereturns aFiledoc whosefile_urlis the correct value to persist.Workaround (for users hitting this in v2.54.1)
Edit
Website Settings.banner_imageandWebsite Settings.favicondirectly via Desk:/app/website-settings→ Brand section → upload via the native form. This uses the canonicalfrappe.client.set_valuepath on theAttach Imagefield (stored asfile_urlstring) and works as expected. The uploaded file gets the correctattached_to_*link, and the LMS portal (/lms/) picks up the new logo afterbench --site <site> clear-cache.Desktop
Versions
Additional context
Verified end-to-end with Playwright MCP automation: the upload network request returns a valid
file_url, but the subsequentset_valuerequest sends{"banner_image": null}in its payload. This rules out any backend issue — the bug is purely in the Vue state propagation between the file uploader and the parentbranding.datareactive object.