I had to do something similar where I wanted the Name from the content picker, but the value being stored was a UDI, and used this:
In the collection view label template I put this {~clinicID}
where ~ is the marker mentioned in the umbraco-package.json
clinicID is the field alias
{
"$schema": "../umbraco-package-schema.json",
"name": "CustomListViewFilters",
"version": "1.0.0",
"extensions": [
{
"type": "ufmComponent",
"alias": "CustomListViewFilters.UfmComponent.UdiToName",
"name": "UDI to Name UFM Component",
"api": "/App_Plugins/CustomListViewFilters/udi-to-name-column.js",
"meta": {
"alias": "umbDocumentName",
"marker": "~"
}
}
]
}
and then the filter itself:
import { html, css } from "@umbraco-cms/backoffice/external/lit";
import { UmbLitElement } from "@umbraco-cms/backoffice/lit-element";
import { UMB_AUTH_CONTEXT } from "@umbraco-cms/backoffice/auth";
// ─── Name Cache (shared across all instances) ───
const nameCache = new Map();
// ─── Custom element that resolves UDIs to names asynchronously ───
class UfmUdiToNameElement extends UmbLitElement {
static properties = {
alias: { type: String },
_displayName: { state: true },
};
#authContext;
constructor() {
super();
this.alias = "";
this._displayName = "";
this.consumeContext(UMB_AUTH_CONTEXT, (ctx) => {
this.#authContext = ctx;
});
this.consumeContext("UmbUfmRenderContext", (ctx) => {
this.observe(ctx?.value, (val) => {
// val is { value: <propertyValue> } from the collection column
// The property value for a content picker is an array of UDI strings
const rawValue = val?.value ?? val;
this.#resolve(rawValue);
});
});
}
async #getAuthToken() {
if (!this.#authContext) return null;
try {
const config = this.#authContext.getOpenApiConfiguration();
if (typeof config.token === "function") {
return await config.token({ name: "", scopes: [] });
}
if (config.token) {
return await config.token;
}
} catch {
// Fall back to cookie auth
}
return null;
}
#udiToGuid(udi) {
if (!udi || typeof udi !== "string") return null;
const match = udi.match(
/umb:\/\/document\/([0-9a-f]{32}|[0-9a-f\-]{36})/i
);
if (!match) return null;
const hex = match[1].replace(/-/g, "");
return [
hex.substring(0, 8),
hex.substring(8, 12),
hex.substring(12, 16),
hex.substring(16, 20),
hex.substring(20, 32),
].join("-");
}
async #resolveNameFromGuid(guid) {
if (nameCache.has(guid)) {
return nameCache.get(guid);
}
try {
const headers = {};
const token = await this.#getAuthToken();
if (token) {
headers["Authorization"] = `Bearer ${token}`;
}
const response = await fetch(
`/umbraco/management/api/v1/document/${guid}`,
{ credentials: "same-origin", headers }
);
if (response.ok) {
const data = await response.json();
const name =
data.variants && data.variants.length > 0
? data.variants[0].name || guid
: guid;
nameCache.set(guid, name);
return name;
}
} catch (err) {
console.error("Failed to resolve UDI to name", err);
}
return guid;
}
async #resolve(rawValue) {
if (!rawValue) {
this._displayName = "";
return;
}
let udis;
try {
const parsed =
typeof rawValue === "string" ? JSON.parse(rawValue) : rawValue;
udis = Array.isArray(parsed) ? parsed : [parsed];
} catch {
udis = typeof rawValue === "string" ? [rawValue] : [];
}
const names = [];
for (const udi of udis) {
if (!udi || typeof udi !== "string") continue;
const guid = this.#udiToGuid(udi);
if (guid) {
names.push(await this.#resolveNameFromGuid(guid));
}
}
this._displayName = names.join(", ");
}
static styles = css`
:host { display: inline; }
`;
render() {
return html`${this._displayName}`;
}
}
if (!customElements.get("ufm-udi-to-name")) {
customElements.define("ufm-udi-to-name", UfmUdiToNameElement);
}
// ─── UFM Component API ───
// Umbraco calls render() which returns HTML string containing our async custom element.
export class UfmUdiToNameComponent {
render(token) {
if (!token.text) return undefined;
const alias = token.text.trim();
return `<ufm-udi-to-name alias="${alias}"></ufm-udi-to-name>`;
}
destroy() {}
}
export { UfmUdiToNameComponent as api };