Best way of dealing with file uploads in Bellissima UI?

Hello :waving_hand:
I am building a custom workspace view (previously known as Context app) at the moment.
The user needs to pick a ZIP file to upload to the server.

I have stumbled across a few different components that I could use, but I am not sure what is the best/right one to use and could do with some guidance please :slight_smile:

UUI library

CMS Components

Findings so far

Using <umb-input-upload-field></umb-input-upload-field> seems to handle some stuff for me where the file is POST’d to an API endpoint to upload it as a temporary file using the Management API /umbraco/management/api/v1/temporary-file

This file then ends up on disk at umbraco\Data\TEMP\TemporaryFile with a GUID as a folder name and inside is my file along with a file called .metadata which contains JSON with some expiry date which I assume the server will come along at that time and remove the file/s.

{"availableUntil":"2025-06-06T14:35:10.5132138+01:00"}

However I have not yet figured out, how I can be notified with an event from using this component has finished uploading the file, in order for me to then call a C# API myself from JS where I assume I would then need to persist the file myself?

Thoughts?

  • So am I using the right component out of these above?
  • Is using this component and the temporary-file thing the right approach?

Any pointers or advice would be fab.

Cheers,
Warren :slight_smile:

Update

I have found some events I can listen for on the component, however my code is not printing out the messages to the console as I would expect.

Render

render() {
  return html`
   ...
   <umb-input-upload-field 
    .allowedFileExtensions=${this.#allowedFileExtension} 
    id="uploader" 
    @ProgressEvent=${this.#progress}
    @UmbDropzoneChangeEvent=${this.#dropzoneChange}
    @UmbDropzoneSubmittedEvent=${this.#dropzoneSubmitted}></umb-input-upload-field>
`;

Events

#progress(event: ProgressEvent) {
    console.log('[PROGRESS] event:', event);
};

#dropzoneChange(event: UmbDropzoneChangeEvent){
    console.log('[CHANGE] Dropzone change event:', event);

    const items = event.items;
    for (const item of items) {
        console.log('[CHANGE] Item:', item);
    }
};

#dropzoneSubmitted(event: UmbDropzoneSubmittedEvent) {
    console.log('[SUBMITTED] Dropzone submitted event:', event);

    event.items.forEach((item) => {
        console.log('[SUBMITTED] Item:', item);
    });
};

Thoughts?

Any obvious clues as to why the events are not firing and I am not seeing anything in the console logs?

I will iterate here what Warren and I discussed on Slack:

The <umb-input-upload-field> is a good component to use. You will have to listen to its change event and then find its temporary file from the target: (event.target as UmbInputUploadFieldElement).temporaryFile.

You will then have to send the GUID of the temporary file somewhere to an API controller, which can move the temporary file into its final location. The Backoffice itself does this for its types typically on Save events, so that temporary files that end up not being used will be deleted after a certain time.

Yep thanks for posting on here Jacob.
For anyone wanting to follow along I have the following

Render

#allowedFileExtension = ['.zip'];

render() {
  return html`
   ...
   <umb-input-upload-field 
      .allowedFileExtensions=${this.#allowedFileExtension}
      @change=${this.#onUploadChange}></umb-input-upload-field>
`;

Change Event

#onUploadChange(event: Event) {
    // Fires when upload has been saved as temp file
    // And also if the file is cleared
    const uploaderEl = event.target as UmbInputUploadFieldElement;

    const uploadValue = uploaderEl.value;
    const tempFile = uploaderEl.temporaryFile;

    console.log('[CHANGE] Upload .value:', uploadValue);
    console.log('[CHANGE] Upload .temporaryFile:', tempFile);

    // Call C# API to Persist & Save file and store entry in DB
}

Will update the thread once I have done the work on the server dealing with the file.

C# Controller

So once you have the temporary ID, you can make a call to an API controller that does something along the lines of this.

using Asp.Versioning;

using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;

using Umbraco.Cms.Api.Common.Attributes;
using Umbraco.Cms.Core.IO;
using Umbraco.Cms.Core.Services;
using Umbraco.Cms.Infrastructure.Scoping;
using Umbraco.Cms.Web.Common.Authorization;
using Umbraco.Cms.Web.Common.Routing;
using Umbraco.Extensions;

namespace Custom.Backoffice.Controllers;

[ApiController]
[BackOfficeRoute("consilium/api/v{version:apiVersion}")]
[Authorize(Policy = AuthorizationPolicies.SectionAccessContent)]
[MapToApi(Constants.ApiName)]
[ApiVersion("1.0")]
[ApiExplorerSettings(GroupName = "Custom")]
public class MyCustomApiController
{
    private readonly ITemporaryFileService _temporaryFileService;
    private readonly IScopeProvider _scopeProvider;
    private readonly MediaFileManager _mediaFileManager;

    public MyCustomApiController(
        ITemporaryFileService temporaryFileService,
        IScopeProvider scopeProvider,
        MediaFileManager mediaFileManager)
    {
        _temporaryFileService = temporaryFileService;
        _scopeProvider = scopeProvider;
        _mediaFileManager = mediaFileManager;
    }


    [HttpGet("Add/{tempFileKey}")]
    [ProducesResponseType(StatusCodes.Status200OK)]
    [ProducesResponseType(StatusCodes.Status404NotFound)]
    public async Task<IActionResult> Add(Guid tempFileKey)
    {
        using var scope = _scopeProvider.CreateCoreScope();
        var tempFile = await _temporaryFileService.GetAsync(tempFileKey);
        _temporaryFileService.EnlistDeleteIfScopeCompletes(tempFileKey, _scopeProvider);

        if (tempFile is null)
        {
            return NotFound("Temporary file not found");
        }

        await using (Stream fileStream = tempFile.OpenReadStream())
        {
            _mediaFileManager.FileSystem.AddFile($"MyCustomThing/{tempFile.FileName}", fileStream, true);
        }

        scope.Complete();
        return Ok();
    }
}