Skip to content

Conversation

@ArgoZhang
Copy link
Member

@ArgoZhang ArgoZhang commented Jun 5, 2025

Link issues

fixes #6156

Summary By Copilot

Regression?

  • Yes
  • No

Risk

  • High
  • Medium
  • Low

Verification

  • Manual (required)
  • Automated

Packaging changes reviewed?

  • Yes
  • No
  • N/A

☑️ Self Check before Merge

⚠️ Please check all items below before review. ⚠️

  • Doc is updated/provided or not needed
  • Demo is updated/provided or not needed
  • Merge the latest code from the main branch

Summary by Sourcery

Implement local preview for image uploads by replacing base64 image requests with Blob URL previews via a new JavaScript API, update upload components and samples to use the new mechanism, and adjust tests accordingly.

New Features:

  • Add client-side image preview using Blob URLs instead of base64 encoding
  • Introduce getPreviewUrl JS function and readFileAsync utility for file reading
  • Expose CanPreviewCallback parameter to allow custom preview logic

Enhancements:

  • Update AvatarUpload and CardUpload components to fetch preview URLs via JS invocation rather than base64 processing
  • Simplify sample code by removing base64 preview demo and adding OnValidSubmit/OnInValidSubmit handlers

Tests:

  • Enhance unit tests for AvatarUpload to include CanPreviewCallback parameter

@bb-auto bb-auto bot added the enhancement New feature or request label Jun 5, 2025
@sourcery-ai
Copy link
Contributor

sourcery-ai bot commented Jun 5, 2025

Reviewer's Guide

This PR replaces the existing Base64-based image preview workflow with a faster, local object-URL approach by adding a file reader utility in JavaScript, wiring up a client-side preview URL API, and adjusting C# upload components and samples to use that API instead of Base64.

File-Level Changes

Change Details Files
Implement local object-URL previews in JavaScript upload modules
  • Import and expose readFileAsync in utility.js
  • Track file inputs and expose getPreviewUrl(id, fileName) to return URL.createObjectURL of a Blob
  • Hook file input ‘change’ event and clean up listener in dispose
  • Add readFileAsync to read files as ArrayBuffer and wrap as Blob
wwwroot/modules/upload.js
wwwroot/modules/utility.js
Update C# upload components to consume local previews
  • Add CanPreviewCallback parameter to AvatarUpload
  • Override TriggerOnChanged in AvatarUpload and CardUpload to call JS interop getPreviewUrl and set file.PrevUrl
  • Rename UploadFileExtensions.IsImage callback parameter and route through CanPreviewCallback
Components/Upload/AvatarUpload.razor.cs
Components/Upload/CardUpload.razor.cs
Extensions/UploadFileExtensions.cs
Remove Base64 sample logic and update demos/tests
  • Strip out RequestBase64ImageFileAsync code and simplify OnAvatarValidSubmit/OnAvatarInValidSubmit to just show toasts
  • Update sample razor markup to drop OnChange, add OnInValidSubmit on ValidateForm
  • Extend unit test to pass CanPreviewCallback argument
Server/Components/Samples/UploadAvatars.razor.cs
Server/Components/Samples/UploadAvatars.razor
test/UnitTest/UploadAvatarTest.cs

Assessment against linked issues

Issue Objective Addressed Explanation
#6156 Replace base64 image format with local preview for image uploads.

Possibly linked issues


Tips and commands

Interacting with Sourcery

  • Trigger a new review: Comment @sourcery-ai review on the pull request.
  • Continue discussions: Reply directly to Sourcery's review comments.
  • Generate a GitHub issue from a review comment: Ask Sourcery to create an
    issue from a review comment by replying to it. You can also reply to a
    review comment with @sourcery-ai issue to create an issue from it.
  • Generate a pull request title: Write @sourcery-ai anywhere in the pull
    request title to generate a title at any time. You can also comment
    @sourcery-ai title on the pull request to (re-)generate the title at any time.
  • Generate a pull request summary: Write @sourcery-ai summary anywhere in
    the pull request body to generate a PR summary at any time exactly where you
    want it. You can also comment @sourcery-ai summary on the pull request to
    (re-)generate the summary at any time.
  • Generate reviewer's guide: Comment @sourcery-ai guide on the pull
    request to (re-)generate the reviewer's guide at any time.
  • Resolve all Sourcery comments: Comment @sourcery-ai resolve on the
    pull request to resolve all Sourcery comments. Useful if you've already
    addressed all the comments and don't want to see them anymore.
  • Dismiss all Sourcery reviews: Comment @sourcery-ai dismiss on the pull
    request to dismiss all existing Sourcery reviews. Especially useful if you
    want to start fresh with a new review - don't forget to comment
    @sourcery-ai review to trigger a new review!

Customizing Your Experience

Access your dashboard to:

  • Enable or disable review features such as the Sourcery-generated pull request
    summary, the reviewer's guide, and others.
  • Change the review language.
  • Add, remove or edit custom review instructions.
  • Adjust other review settings.

Getting Help

@bb-auto bb-auto bot added this to the 9.7.0 milestone Jun 5, 2025
@ArgoZhang ArgoZhang merged commit 16cc7d4 into main Jun 5, 2025
3 checks passed
@ArgoZhang ArgoZhang deleted the refactor-uploader branch June 5, 2025 04:46
@codecov
Copy link

codecov bot commented Jun 5, 2025

Codecov Report

All modified and coverable lines are covered by tests ✅

Project coverage is 99.99%. Comparing base (7e580f9) to head (5f86566).
Report is 1 commits behind head on main.

Additional details and impacted files
@@           Coverage Diff           @@
##             main    #6157   +/-   ##
=======================================
  Coverage   99.99%   99.99%           
=======================================
  Files         704      704           
  Lines       31093    31099    +6     
  Branches     4394     4395    +1     
=======================================
+ Hits        31092    31098    +6     
  Partials        1        1           

☔ 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.

Copy link
Contributor

@sourcery-ai sourcery-ai bot left a comment

Choose a reason for hiding this comment

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

Hey @ArgoZhang - I've reviewed your changes - here's some feedback:

  • Consider revoking the created object URLs (via URL.revokeObjectURL) when they’re no longer needed to avoid accumulating memory leaks.
  • You can simplify the preview logic by calling URL.createObjectURL(file) directly instead of reading into an ArrayBuffer and re-wrapping into a Blob.
Here's what I looked at during the review
  • 🟡 General issues: 4 issues found
  • 🟢 Security: all looks good
  • 🟢 Testing: all looks good
  • 🟡 Complexity: 2 issues found
  • 🟢 Documentation: all looks good

Sourcery is free for open source - if you like our reviews please consider sharing them ✨
Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.

const preventHandler = e => e.preventDefault();
const body = el.querySelector('.upload-drop-body');
const upload = { el, body, preventHandler }
const inputFile = el.querySelector('[type="file"]');
Copy link
Contributor

Choose a reason for hiding this comment

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

issue (bug_risk): inputFile can be null; add a null-check before usage

Add a guard or early return to handle cases where inputFile is null to avoid runtime errors.


/**
* @param {File} file
* @returns {Blob}
Copy link
Contributor

Choose a reason for hiding this comment

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

nitpick: JSDoc @returns annotation is inaccurate

Change the annotation to @returns {Promise} to accurately describe the return type.

/// </summary>
/// <param name="file"></param>
/// <returns></returns>
protected override async Task TriggerOnChanged(UploadFile file)
Copy link
Contributor

Choose a reason for hiding this comment

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

suggestion: Image preview logic duplicated here as well

Consider refactoring this logic into a shared base class or utility method to avoid duplication.

Comment on lines +142 to +147
// 从客户端获得预览地址不使用 base64 编码
if (file.IsImage(AllowExtensions, CanPreviewCallback))
{
await OnChange(file);
file.PrevUrl = await InvokeAsync<string?>("getPreviewUrl", Id, file.OriginFileName);
}
await base.TriggerOnChanged(file);
Copy link
Contributor

Choose a reason for hiding this comment

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

suggestion (bug_risk): Non-image files retain previous PrevUrl

Reset PrevUrl for non-image files to prevent showing outdated previews.

Suggested change
// 从客户端获得预览地址不使用 base64 编码
if (file.IsImage(AllowExtensions, CanPreviewCallback))
{
await OnChange(file);
file.PrevUrl = await InvokeAsync<string?>("getPreviewUrl", Id, file.OriginFileName);
}
await base.TriggerOnChanged(file);
// 从客户端获得预览地址不使用 base64 编码
if (file.IsImage(AllowExtensions, CanPreviewCallback))
{
file.PrevUrl = await InvokeAsync<string?>("getPreviewUrl", Id, file.OriginFileName);
}
else
{
// 非图片文件重置预览地址,防止显示过期预览
file.PrevUrl = null;
}
await base.TriggerOnChanged(file);

})
}

export async function getPreviewUrl(id, fileName) {
Copy link
Contributor

Choose a reason for hiding this comment

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

issue (complexity): Consider refactoring getPreviewUrl to use early returns and optional chaining to reduce nesting and intermediate variables.

Here’s a way to flatten out the nesting in getPreviewUrl (and even drop the manual let url = '' accumulator) by using early-returns and optional chaining:

export async function getPreviewUrl(id, fileName) {
  const files = Data.get(id)?.files;
  if (!files) return '';

  // find the File object
  const file = Array.from(files).find(f => f.name === fileName);
  if (!file) return '';

  // readFileAsync → blob URL
  const blob = await readFileAsync(file);
  return blob ? URL.createObjectURL(blob) : '';
}

If you’re not actually transforming the file (just wrapping it in a blob URL), you can simplify further and drop readFileAsync entirely:

export function getPreviewUrl(id, fileName) {
  const files = Data.get(id)?.files || [];
  const file = files.find(f => f.name === fileName);
  return file ? URL.createObjectURL(file) : '';
}

Both versions preserve the current behavior while reducing indentation, intermediate variables, and nested if blocks.

* @param {File} file
* @returns {Blob}
*/
export function readFileAsync(file) {
Copy link
Contributor

Choose a reason for hiding this comment

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

issue (complexity): Consider removing or simplifying the readFileAsync helper to avoid unnecessary FileReader logic.

Suggested change
export function readFileAsync(file) {
The new `readFileAsync` helper isn’t needed since `File` already implements the `Blob` interface. You can either:
1. Drop the helper entirely and pass `file` wherever a `Blob` is expected, or
2. If you really need an immutable copy, use `File.prototype.slice` instead of a `FileReader` round‐trip.
Example (identity / no‐op helper):
```js
/** @param {File} file
* @returns {Promise<Blob>}
*/
export function readFileAsync(file) {
return Promise.resolve(file);
}

Or (clone via slice):

/** @param {File} file
 *  @returns {Promise<Blob>}
 */
export function readFileAsync(file) {
  const blobCopy = file.slice(0, file.size, file.type);
  return Promise.resolve(blobCopy);
}

Both eliminate the 20+ lines of FileReader logic while preserving all functionality.```

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

enhancement New feature or request

Projects

None yet

Development

Successfully merging this pull request may close these issues.

feat(Upload): use local preview instead of base64 format

2 participants