Skip to content

[bug] BrandSettings.vue: file_url is never written to branding.data after upload, causing Website Settings.banner_image/favicon/app_logo to be set to NULL on save #2417

@JohanBermudez

Description

@JohanBermudez

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:

  1. Log in as Administrator.
  2. Navigate to /lms/settings → Branding tab.
  3. Click "Brand Image" → choose any .png (e.g., 200×60).
  4. Wait for the upload toast (success).
  5. Click "Update" at the top right.
  6. Open bench --site <site> console:
    frappe.db.get_single_value("Website Settings", "banner_image")
    # Expected: "/files/whatever.png"
    # Actual:   None
  7. 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.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions