Images not updating order in custom block preview

Hello :waving_hand:
I am currently building a custom view for an item in the block grid in Bellissima (aka WebComponent era of the Umbraco backoffice)

I have a simple element/block type that contains two properties

  • Heading (Textstring)
  • Images (Media Picker - Limited to a max of two images)

Problem

When changing the order of the images in the media picker it does not update the images displayed, however printing the two GUID/Keys of the picked media items correctly display and update in the component.

Video

Questions

  • Should I be using umb-imaging-thumbnail to help render out the picked image or using an alternative approach with some context that I need to consume to resolve/convert the picked GUID to an image URL ?

  • Why when I drag and drop and change the order of the images or pick a new image to replace one, that it does not update the image rendered with <umb-imaging-thumbnail>?

Code

manifest.ts

export const manifests: Array<UmbExtensionManifest> = [
    {
        name: "Block Preview - Images",
        alias: "blockpreview.images",
        type: "blockEditorCustomView",
        forContentTypeAlias: "imagesBlock",
        js: () => import("./images-block"),
    }
];

images-block.ts

import { html, customElement, LitElement, property, css, when } from '@umbraco-cms/backoffice/external/lit';
import { UmbElementMixin } from '@umbraco-cms/backoffice/element-api';
import { type UmbBlockDataType } from '@umbraco-cms/backoffice/block';
import type { UmbBlockEditorCustomViewConfiguration, UmbBlockEditorCustomViewElement } from '@umbraco-cms/backoffice/block-custom-view';
import { commonStyles } from './common-styles';

@customElement('images-block-custom-view')
export class ImagesBlockCustomView extends UmbElementMixin(LitElement) implements UmbBlockEditorCustomViewElement {
	
	@property({ attribute: false })
	content?: UmbBlockDataType & {
        images: Array<{ mediaKey: string }>;
    }

    @property({ attribute: false })
    label?: string;

	@property({ attribute: false })
	settings?: UmbBlockDataType;

    @property({ attribute: false })
    config?: UmbBlockEditorCustomViewConfiguration;

    constructor() {
        super();
    }

	render() {
        const leftImageMediaKey = this.content?.images?.[0]?.mediaKey ?? '';
        const rightImageMediaKey = this.content?.images?.[1]?.mediaKey ?? '';

		return html`
            <fieldset>
                <legend>${this.label ?? 'Images'}</legend>
                <a href=${this.config?.editContentPath ?? ''} class="edit">
                    <figure>
                        <h2>${this.content?.heading}</h2>

                        <pre>
                            Left = ${leftImageMediaKey}
                            Right = ${rightImageMediaKey}
                        </pre>

                        <div id="grid">
                            <div>
                                 ${when(
                                    leftImageMediaKey,
                                    () => html`<umb-imaging-thumbnail .unique=${leftImageMediaKey!} width="600" mode="Crop"></umb-imaging-thumbnail>`,
                                    () => html`<p>No image selected</p>`
                                )}
                            </div>
                            <div>
                                ${when(
                                    rightImageMediaKey,
                                    () => html`<umb-imaging-thumbnail .unique=${rightImageMediaKey!} width="600" mode="Crop"></umb-imaging-thumbnail>`,
                                    () => html`<p>No image selected</p>`
                                )}
                            </div>
                        </div>
                    </figure>
                </a>
            </fieldset>

            <!-- <pre>${JSON.stringify(this.content, null, 2)}</pre> -->
		`;
	}

	static styles = [
        commonStyles,
		css`
            h2 {
                color: var(--colour-black);
                font-family: var(--font-family-bravo);
                font-size: var(--font-size-heading-3);

                text-align: center;
            }

            #grid {
                display:grid;
                grid-template-columns: 1fr 1fr;
                gap: 24px;

                div {
                    background: var(--colour-grey--mid-light);
                }

                p {
                    text-align: center;
                }
            }
		`,
	];
	
}
export default ImagesBlockCustomView;

declare global {
	interface HTMLElementTagNameMap {
		'images-block-custom-view': ImagesBlockCustomView;
	}
}

Update

Well poking around the code, I stumbled across more things such as:

  • #imagingRepository = new UmbImagingRepository(this);
  • A store context UMB_IMAGING_STORE_CONTEXT
  • #mediaRepository = new UmbMediaUrlRepository(this);

I tried some of these and in the end I went with newing up a UmbMediaUrlRepository and was then able to give it an array of GUIDs to request and get information about the media item URLs.

Approach

#mediaRepository = new UmbMediaUrlRepository(this);
...

const mediaKeys = ['4500f413-253d-4204-a047-cd5e2170cf58', '01b17128-82c3-48d3-a84a-58cb5623c174'];
const mediaItemsObservable = await (await this.#mediaRepository.requestItems(mediaKeys)).asObservable();

this.observe(mediaItemsObservable, (mediaItems) => {
   console.log('Media Items', mediaItems);
});

Solution

This is the approach I took in the end. If this is the right solution I am not 100% sure, but it works.

images-block.ts

import { html, customElement, LitElement, property, css, when, state } from '@umbraco-cms/backoffice/external/lit';
import { UmbElementMixin } from '@umbraco-cms/backoffice/element-api';
import { type UmbBlockDataType } from '@umbraco-cms/backoffice/block';
import type { UmbBlockEditorCustomViewConfiguration, UmbBlockEditorCustomViewElement } from '@umbraco-cms/backoffice/block-custom-view';
import { commonStyles } from './common-styles';
import { UmbMediaUrlRepository } from '@umbraco-cms/backoffice/media';

@customElement('images-block-custom-view')
export class ImagesBlockCustomView extends UmbElementMixin(LitElement) implements UmbBlockEditorCustomViewElement {
	
	@property({ attribute: false })
	content?: UmbBlockDataType & {
        images: Array<{ mediaKey: string }>;
    }

    @property({ attribute: false })
    label?: string;

	@property({ attribute: false })
	settings?: UmbBlockDataType;

    @property({ attribute: false })
    config?: UmbBlockEditorCustomViewConfiguration;

    @state()
    private _mediaUrlMap: Map<string, string> = new Map();

    #mediaRepository = new UmbMediaUrlRepository(this);

    constructor() {
        super();
    }

    updated(changedProperties: Map<string | number | symbol, unknown>) {
        super.updated(changedProperties);
        
        if (changedProperties.has('content')) {
            const mediaKeys = [this.content?.images?.[0]?.mediaKey!, this.content?.images?.[1]?.mediaKey!].filter(key => key !== undefined);
            this.#loadMediaItems(mediaKeys);
        }
    }

    async #loadMediaItems(mediaKeys: string[]) {
        try {
            const mediaItemsObservable = await (await this.#mediaRepository.requestItems(mediaKeys)).asObservable();
            this.observe(mediaItemsObservable, (mediaItems) => {
                if (!mediaItems) return;

                // Create a map of media key to URL
                const newMediaUrlMap = new Map<string, string>();
                mediaItems.forEach(mediaItem => {
                    if (mediaItem.unique && mediaItem.url) {
                        newMediaUrlMap.set(mediaItem.unique, mediaItem.url);
                    }
                });
                
                this._mediaUrlMap = newMediaUrlMap;
            });
        } catch (error) {
            console.error('Error loading media items:', error);
            this._mediaUrlMap = new Map();
        }
    };

    #getMediaUrl(mediaKey: string): string {
        return this._mediaUrlMap.get(mediaKey) || '';
    }

	render() {
        const leftImageUrl = this.#getMediaUrl(this.content?.images?.[0]?.mediaKey || '');
        const rightImageUrl = this.#getMediaUrl(this.content?.images?.[1]?.mediaKey || '');

		return html`
            <fieldset>
                <legend>${this.label ?? 'Images'}</legend>
                <a href=${this.config?.editContentPath ?? ''} class="edit">
                    <figure>
                        <h2>${this.content?.heading}</h2>

                        <div id="grid">
                            <div class="img-container">
                                 ${when(
                                    leftImageUrl,
                                    () => html`<img src="${leftImageUrl}" />`,
                                    () => html`<p>No image selected</p>`
                                )}
                            </div>
                            <div class="img-container">
                                ${when(
                                    rightImageUrl,
                                    () => html`<img src="${rightImageUrl}" />`,
                                    () => html`<p>No image selected</p>`
                                )}
                            </div>
                        </div>
                    </figure>
                </a>
            </fieldset>

            <!-- <pre>${JSON.stringify(this.content, null, 2)}</pre> -->
        `;
	}

	static styles = [
        commonStyles,
		css`
            h2 {
                color: var(--colour-black);
                font-family: var(--font-family-bravo);
                font-size: var(--font-size-heading-3);

                text-align: center;
            }

            #grid {
                display:grid;
                grid-template-columns: 1fr 1fr;
                gap: 24px;
            }

            .img-container {
                background: var(--colour-grey--mid-light);
                height: 300px; /* Fixed height for all containers */
                overflow: hidden;
                display: flex;
                align-items: center;
                justify-content: center;
            }

            .img-container img {
                width: 100%;
                height: 100%;
                object-fit: cover; /* Crops to fill container while maintaining aspect ratio */
            }

            .img-container p {
                text-align: center;
                margin: 0;
            }
		`,
	];
	
}
export default ImagesBlockCustomView;

declare global {
	interface HTMLElementTagNameMap {
		'images-block-custom-view': ImagesBlockCustomView;
	}
}