Document URLs Data Resolver (#19316)

* remove padding

* add document urls data resolver

* use in url info app

* handle invariant cases

* do not render culture if all links have the same culture

* use if defined

* handle variant with no links

* Update types.ts

* fix lint errors

* get variant aware document data

* remove unused

* use media item repository

* temp remove check

* populate url

* add spacing to reference app

* reset the url when removing document or media

* add validator

* make url input required

---------

Co-authored-by: Niels Lyngsø <nsl@umbraco.dk>
This commit is contained in:
Mads Rasmussen
2025-05-19 12:36:40 +02:00
committed by GitHub
parent dda69a1ead
commit 930a29f3d5
8 changed files with 319 additions and 70 deletions

View File

@@ -22,10 +22,6 @@ export class UmbWorkspaceInfoAppLayoutElement extends UmbLitElement {
uui-box {
--uui-box-default-padding: 0;
}
#container {
padding-left: var(--uui-size-space-4);
}
`,
];
}

View File

@@ -0,0 +1,94 @@
import type { UmbDocumentUrlModel } from './repository/types.js';
import { UmbControllerBase } from '@umbraco-cms/backoffice/class-api';
import type { UmbControllerHost } from '@umbraco-cms/backoffice/controller-api';
import { UmbArrayState } from '@umbraco-cms/backoffice/observable-api';
import { UMB_PROPERTY_DATASET_CONTEXT } from '@umbraco-cms/backoffice/property';
import type { UmbVariantId } from '@umbraco-cms/backoffice/variant';
/**
* A controller for resolving data for document urls
* @exports
* @class UmbDocumentUrlsDataResolver
* @augments {UmbControllerBase}
*/
export class UmbDocumentUrlsDataResolver extends UmbControllerBase {
#appCulture?: string;
#propertyDataSetCulture?: UmbVariantId;
#data?: Array<UmbDocumentUrlModel> | undefined;
#init: Promise<unknown>;
#urls = new UmbArrayState<UmbDocumentUrlModel>([], (url) => url.url);
/**
* The urls for the current culture
* @returns {ObservableArray<UmbDocumentUrlModel>} The urls for the current culture
* @memberof UmbDocumentUrlsDataResolver
*/
public readonly urls = this.#urls.asObservable();
constructor(host: UmbControllerHost) {
super(host);
// TODO: listen for UMB_VARIANT_CONTEXT when available
this.#init = Promise.all([
this.consumeContext(UMB_PROPERTY_DATASET_CONTEXT, (context) => {
this.#propertyDataSetCulture = context?.getVariantId();
this.#setCultureAwareValues();
}).asPromise(),
]);
}
/**
* Get the current data
* @returns {Array<UmbDocumentUrlModel> | undefined} The current data
* @memberof UmbDocumentUrlsDataResolver
*/
getData(): Array<UmbDocumentUrlModel> | undefined {
return this.#data;
}
/**
* Set the current data
* @param {Array<UmbDocumentUrlModel> | undefined} data The current data
* @memberof UmbDocumentUrlsDataResolver
*/
setData(data: Array<UmbDocumentUrlModel> | undefined) {
this.#data = data;
if (!this.#data) {
this.#urls.setValue([]);
return;
}
this.#setCultureAwareValues();
}
/**
* Get the urls for the current culture
* @returns {(Promise<Array<UmbDocumentUrlModel> | []>)} The urls for the current culture
* @memberof UmbDocumentUrlsDataResolver
*/
async getUrls(): Promise<Array<UmbDocumentUrlModel> | []> {
await this.#init;
return this.#urls.getValue();
}
#setCultureAwareValues() {
this.#setUrls();
}
#setUrls() {
const data = this.#getDataForCurrentCulture();
this.#urls.setValue(data ?? []);
}
#getCurrentCulture(): string | undefined {
return this.#propertyDataSetCulture?.culture || this.#appCulture;
}
#getDataForCurrentCulture(): Array<UmbDocumentUrlModel> | undefined {
const culture = this.#getCurrentCulture();
// If there is no culture context (invariant data) we return all urls
return culture ? this.#data?.filter((x) => x.culture === culture) : this.#data;
}
}

View File

@@ -1,3 +1,4 @@
export { UmbDocumentUrlRepository, UMB_DOCUMENT_URL_REPOSITORY_ALIAS } from './repository/index.js';
export * from './constants.js';
export * from './document-urls-data-resolver.js';

View File

@@ -2,7 +2,17 @@ import { UmbDocumentUrlRepository } from '../repository/index.js';
import type { UmbDocumentVariantOptionModel } from '../../types.js';
import { UMB_DOCUMENT_WORKSPACE_CONTEXT } from '../../workspace/constants.js';
import type { UmbDocumentUrlModel } from '../repository/types.js';
import { css, customElement, html, nothing, repeat, state, when } from '@umbraco-cms/backoffice/external/lit';
import { UmbDocumentUrlsDataResolver } from '../document-urls-data-resolver.js';
import {
css,
customElement,
html,
ifDefined,
nothing,
repeat,
state,
when,
} from '@umbraco-cms/backoffice/external/lit';
import { UmbLitElement } from '@umbraco-cms/backoffice/lit-element';
import type { UmbEntityActionEvent } from '@umbraco-cms/backoffice/entity-action';
import { UmbRequestReloadStructureForEntityEvent } from '@umbraco-cms/backoffice/entity-action';
@@ -10,11 +20,12 @@ import { UMB_ACTION_EVENT_CONTEXT } from '@umbraco-cms/backoffice/action';
import { observeMultiple } from '@umbraco-cms/backoffice/observable-api';
import { DocumentVariantStateModel } from '@umbraco-cms/backoffice/external/backend-api';
import { debounce } from '@umbraco-cms/backoffice/utils';
import { UMB_APP_LANGUAGE_CONTEXT } from '@umbraco-cms/backoffice/language';
import { UMB_PROPERTY_DATASET_CONTEXT } from '@umbraco-cms/backoffice/property';
import type { UmbVariantId } from '@umbraco-cms/backoffice/variant';
interface UmbDocumentInfoViewLink {
culture: string;
url: string | undefined;
culture: string | null;
url: string | null | undefined;
state: DocumentVariantStateModel | null | undefined;
}
@@ -37,13 +48,13 @@ export class UmbDocumentLinksWorkspaceInfoAppElement extends UmbLitElement {
@state()
private _links: Array<UmbDocumentInfoViewLink> = [];
@state()
private _defaultCulture?: string;
#urls: Array<UmbDocumentUrlModel> = [];
#documentWorkspaceContext?: typeof UMB_DOCUMENT_WORKSPACE_CONTEXT.TYPE;
#eventContext?: typeof UMB_ACTION_EVENT_CONTEXT.TYPE;
#propertyDataSetVariantId?: UmbVariantId;
#documentUrlsDataResolver? = new UmbDocumentUrlsDataResolver(this);
constructor() {
super();
@@ -88,20 +99,26 @@ export class UmbDocumentLinksWorkspaceInfoAppElement extends UmbLitElement {
});
});
this.consumeContext(UMB_APP_LANGUAGE_CONTEXT, (instance) => {
this.observe(instance?.appDefaultLanguage, (value) => {
this._defaultCulture = value?.unique;
this.#setLinks();
});
this.consumeContext(UMB_PROPERTY_DATASET_CONTEXT, (context) => {
this.#propertyDataSetVariantId = context?.getVariantId();
this.#setLinks();
});
this.observe(this.#documentUrlsDataResolver?.urls, (urls) => {
this.#urls = urls ?? [];
this.#setLinks();
});
}
#setLinks() {
const links: Array<UmbDocumentInfoViewLink> = this.#urls.map((u) => {
const culture = u.culture ?? this._defaultCulture ?? '';
const url = u.url;
const links: Array<UmbDocumentInfoViewLink> = this.#urls.map((url) => {
const culture = url.culture;
const state = this._variantOptions?.find((variantOption) => variantOption.culture === culture)?.variant?.state;
return { culture, url, state };
return {
culture,
url: url.url,
state,
};
});
this._links = links;
@@ -124,14 +141,12 @@ export class UmbDocumentLinksWorkspaceInfoAppElement extends UmbLitElement {
if (!this._unique) return;
this._loading = true;
this.#urls = [];
this.#documentUrlsDataResolver?.setData([]);
const { data } = await this.#documentUrlRepository.requestItems([this._unique]);
if (data?.length) {
const item = data[0];
this.#urls = item.urls;
this.#setLinks();
this.#documentUrlsDataResolver?.setData(data[0].urls);
}
this._loading = false;
@@ -207,7 +222,7 @@ export class UmbDocumentLinksWorkspaceInfoAppElement extends UmbLitElement {
}
return html`
<a class="link-item" href=${this.#getTargetUrl(link.url)} target="_blank">
<a class="link-item" href=${ifDefined(this.#getTargetUrl(link.url))} target="_blank">
<span>
${this.#renderLinkCulture(link.culture)}
<span>${link.url}</span>
@@ -218,9 +233,9 @@ export class UmbDocumentLinksWorkspaceInfoAppElement extends UmbLitElement {
}
#renderNoLinks() {
return html` ${this._variantOptions?.map((variantOption) =>
this.#renderEmptyLink(variantOption.culture, variantOption.variant?.state),
)}`;
return html` ${this._variantOptions
?.filter((variantOption) => variantOption.culture === this.#propertyDataSetVariantId?.culture)
.map((variantOption) => this.#renderEmptyLink(variantOption.culture, variantOption.variant?.state))}`;
}
#renderEmptyLink(culture: string | null, state: DocumentVariantStateModel | null | undefined) {
@@ -235,6 +250,8 @@ export class UmbDocumentLinksWorkspaceInfoAppElement extends UmbLitElement {
#renderLinkCulture(culture: string | null) {
if (!culture) return nothing;
if (this._links.length === 1) return nothing;
const allLinksHaveSameCulture = this._links?.every((link) => link.culture === culture);
if (allLinksHaveSameCulture) return nothing;
return html`<span class="culture">${culture}</span>`;
}
@@ -249,10 +266,6 @@ export class UmbDocumentLinksWorkspaceInfoAppElement extends UmbLitElement {
static override styles = [
css`
uui-box {
--uui-box-default-padding: 0;
}
#loader-container {
display: flex;
justify-content: center;

View File

@@ -4,6 +4,6 @@ export interface UmbDocumentUrlsModel {
}
export interface UmbDocumentUrlModel {
culture?: string | null;
culture: string | null;
url?: string;
}

View File

@@ -21,6 +21,8 @@ import { UUIFormControlMixin } from '@umbraco-cms/backoffice/external/uui';
import type { UmbModalRouteBuilder } from '@umbraco-cms/backoffice/router';
import type { UmbVariantId } from '@umbraco-cms/backoffice/variant';
import type { UUIModalSidebarSize } from '@umbraco-cms/backoffice/external/uui';
import { UmbDocumentUrlRepository, UmbDocumentUrlsDataResolver } from '@umbraco-cms/backoffice/document';
import { UmbMediaUrlRepository } from '@umbraco-cms/backoffice/media';
/**
* @element umb-input-multi-url
@@ -129,6 +131,7 @@ export class UmbInputMultiUrlElement extends UUIFormControlMixin(UmbLitElement,
this.#urls = [...data]; // Unfreeze data coming from State, so we can manipulate it.
super.value = this.#urls.map((x) => x.url).join(',');
this.#sorter.setModel(this.#urls);
this.#populateLinksUrl();
}
get urls(): Array<UmbLinkPickerLink> {
return this.#urls;
@@ -160,6 +163,9 @@ export class UmbInputMultiUrlElement extends UUIFormControlMixin(UmbLitElement,
@state()
private _modalRoute?: UmbModalRouteBuilder;
@state()
_resolvedLinkUrls: Array<{ unique: string; url: string }> = [];
#linkPickerModal;
constructor() {
@@ -229,6 +235,49 @@ export class UmbInputMultiUrlElement extends UUIFormControlMixin(UmbLitElement,
});
}
#populateLinksUrl() {
// Documents and media have URLs saved in the local link format. Display the actual URL to align with what
// the user sees when they selected it initially.
this.#urls.forEach(async (link) => {
if (!link.unique) return;
let url: string | undefined = undefined;
switch (link.type) {
case 'document': {
url = await this.#getUrlForDocument(link.unique);
break;
}
case 'media': {
url = await this.#getUrlForMedia(link.unique);
break;
}
default:
break;
}
if (url) {
const resolvedUrl = { unique: link.unique, url };
this._resolvedLinkUrls = [...this._resolvedLinkUrls, resolvedUrl];
}
});
}
async #getUrlForDocument(unique: string) {
const documentUrlRepository = new UmbDocumentUrlRepository(this);
const { data: documentUrlData } = await documentUrlRepository.requestItems([unique]);
const urlsItem = documentUrlData?.[0];
const dataResolver = new UmbDocumentUrlsDataResolver(this);
dataResolver.setData(urlsItem?.urls);
const resolvedUrls = await dataResolver.getUrls();
return resolvedUrls?.[0]?.url ?? '';
}
async #getUrlForMedia(unique: string) {
const mediaUrlRepository = new UmbMediaUrlRepository(this);
const { data: mediaUrlData } = await mediaUrlRepository.requestItems([unique]);
return mediaUrlData?.[0].url ?? '';
}
async #requestRemoveItem(index: number) {
const item = this.#urls[index];
if (!item) throw new Error('Could not find item at index: ' + index);
@@ -307,12 +356,13 @@ export class UmbInputMultiUrlElement extends UUIFormControlMixin(UmbLitElement,
#renderItem(link: UmbLinkPickerLink, index: number) {
const unique = this.#getUnique(link);
const href = this.readonly ? undefined : (this._modalRoute?.({ index }) ?? undefined);
const resolvedUrl = this._resolvedLinkUrls.find((url) => url.unique === link.unique)?.url ?? '';
return html`
<uui-ref-node
id=${unique}
href=${ifDefined(href)}
name=${link.name || ''}
detail=${(link.url || '') + (link.queryString || '')}
detail=${resolvedUrl + (link.queryString || '')}
?readonly=${this.readonly}>
<umb-icon slot="icon" name=${link.icon || 'icon-link'}></umb-icon>
${when(

View File

@@ -6,13 +6,83 @@ import type {
} from './link-picker-modal.token.js';
import { css, customElement, html, nothing, query, state, when } from '@umbraco-cms/backoffice/external/lit';
import { isUmbracoFolder, UmbMediaTypeStructureRepository } from '@umbraco-cms/backoffice/media-type';
import { umbBindToValidation, UmbValidationContext } from '@umbraco-cms/backoffice/validation';
import {
UMB_VALIDATION_CONTEXT,
umbBindToValidation,
UmbObserveValidationStateController,
UmbValidationContext,
type UmbValidator,
} from '@umbraco-cms/backoffice/validation';
import { umbConfirmModal, UmbModalBaseElement } from '@umbraco-cms/backoffice/modal';
import { UmbDocumentDetailRepository, UmbDocumentUrlRepository } from '@umbraco-cms/backoffice/document';
import { UmbMediaDetailRepository, UmbMediaUrlRepository } from '@umbraco-cms/backoffice/media';
import {
UmbDocumentItemDataResolver,
UmbDocumentItemRepository,
UmbDocumentUrlRepository,
UmbDocumentUrlsDataResolver,
} from '@umbraco-cms/backoffice/document';
import { UmbMediaItemRepository, UmbMediaUrlRepository } from '@umbraco-cms/backoffice/media';
import type { UmbInputDocumentElement } from '@umbraco-cms/backoffice/document';
import type { UmbInputMediaElement } from '@umbraco-cms/backoffice/media';
import type { UUIBooleanInputEvent, UUIInputElement, UUIInputEvent } from '@umbraco-cms/backoffice/external/uui';
import type { UUIBooleanInputEvent, UUIInputEvent } from '@umbraco-cms/backoffice/external/uui';
import { UmbControllerBase } from '@umbraco-cms/backoffice/class-api';
import type { UmbControllerHost } from '@umbraco-cms/backoffice/controller-api';
import { umbFocus } from '@umbraco-cms/backoffice/lit-element';
class UmbLinkPickerValueValidator extends UmbControllerBase implements UmbValidator {
#context?: typeof UMB_VALIDATION_CONTEXT.TYPE;
#isValid = true;
get isValid(): boolean {
return this.#isValid;
}
#value: unknown;
#unique = 'UmbLinkPickerValueValidator';
setValue(value: unknown) {
this.#value = value;
this.validate();
}
getValue(): unknown {
return this.#value;
}
// The path to the data that this validator is validating.
readonly #dataPath: string;
constructor(host: UmbControllerHost, dataPath: string) {
super(host);
this.#dataPath = dataPath;
this.consumeContext(UMB_VALIDATION_CONTEXT, (context) => {
if (this.#context) {
this.#context.removeValidator(this);
}
this.#context = context;
context?.addValidator(this);
});
}
async validate(): Promise<void> {
this.#isValid = !!this.getValue();
if (this.#isValid) {
this.#context?.messages.removeMessageByKey(this.#unique);
} else {
this.#context?.messages.addMessage(
'client',
this.#dataPath,
'#linkPicker_modalAnchorValidationMessage',
this.#unique,
);
}
}
reset(): void {}
focusFirstInvalidElement(): void {}
}
type UmbInputPickerEvent = CustomEvent & { target: { value?: string } };
@@ -31,14 +101,22 @@ export class UmbLinkPickerModalElement extends UmbModalBaseElement<UmbLinkPicker
hideTarget: false,
};
@state()
private _missingLinkUrl = false;
@query('umb-input-document')
private _documentPickerElement?: UmbInputDocumentElement;
@query('umb-input-media')
private _mediaPickerElement?: UmbInputMediaElement;
@query('#link-anchor', true)
private _linkAnchorInput?: UUIInputElement;
constructor() {
super();
new UmbObserveValidationStateController(this, '$.type', (invalid) => {
this._missingLinkUrl = invalid;
});
}
override connectedCallback() {
super.connectedCallback();
@@ -57,14 +135,12 @@ export class UmbLinkPickerModalElement extends UmbModalBaseElement<UmbLinkPicker
this.#getMediaTypes();
this.populateLinkUrl();
}
protected override firstUpdated() {
this._linkAnchorInput?.addValidator(
'valueMissing',
() => this.localize.term('linkPicker_modalAnchorValidationMessage'),
() => !this.value.link.name && !this.value.link.queryString,
);
const validator = new UmbLinkPickerValueValidator(this, '$.type');
this.observe(this.modalContext?.value, (value) => {
validator.setValue(value?.link.type);
});
}
async #getMediaTypes() {
@@ -78,7 +154,7 @@ export class UmbLinkPickerModalElement extends UmbModalBaseElement<UmbLinkPicker
async populateLinkUrl() {
// Documents and media have URLs saved in the local link format. Display the actual URL to align with what
// the user sees when they selected it initially.
if (!this.value.link?.unique || this.value.link?.url?.indexOf('localLink') === -1) return;
if (!this.value.link?.unique) return;
let url: string | undefined = undefined;
switch (this.value.link.type) {
@@ -156,21 +232,25 @@ export class UmbLinkPickerModalElement extends UmbModalBaseElement<UmbLinkPicker
if (unique) {
switch (type) {
case 'document': {
const documentRepository = new UmbDocumentDetailRepository(this);
const { data: documentData } = await documentRepository.requestByUnique(unique);
if (documentData) {
icon = documentData.documentType.icon;
name = documentData.variants[0].name;
const documentRepository = new UmbDocumentItemRepository(this);
const { data: documentItems } = await documentRepository.requestItems([unique]);
const documentItem = documentItems?.[0];
if (documentItem) {
const itemDataResolver = new UmbDocumentItemDataResolver(this);
itemDataResolver.setData(documentItem);
icon = await itemDataResolver.getIcon();
name = await itemDataResolver.getName();
url = await this.#getUrlForDocument(unique);
}
break;
}
case 'media': {
const mediaRepository = new UmbMediaDetailRepository(this);
const { data: mediaData } = await mediaRepository.requestByUnique(unique);
if (mediaData) {
icon = mediaData.mediaType.icon;
name = mediaData.variants[0].name;
const mediaRepository = new UmbMediaItemRepository(this);
const { data: mediaData } = await mediaRepository.requestItems([unique]);
const mediaItem = mediaData?.[0];
if (mediaItem) {
icon = mediaItem.mediaType.icon;
name = mediaItem.variants[0].name;
url = await this.#getUrlForMedia(unique);
}
break;
@@ -178,6 +258,9 @@ export class UmbLinkPickerModalElement extends UmbModalBaseElement<UmbLinkPicker
default:
break;
}
// The selection was removed
} else {
this.#resetUrl();
}
const link = {
@@ -196,7 +279,11 @@ export class UmbLinkPickerModalElement extends UmbModalBaseElement<UmbLinkPicker
async #getUrlForDocument(unique: string) {
const documentUrlRepository = new UmbDocumentUrlRepository(this);
const { data: documentUrlData } = await documentUrlRepository.requestItems([unique]);
return documentUrlData && documentUrlData[0].urls.length > 0 ? (documentUrlData?.[0].urls[0].url ?? '') : '';
const urlsItem = documentUrlData?.[0];
const dataResolver = new UmbDocumentUrlsDataResolver(this);
dataResolver.setData(urlsItem?.urls);
const resolvedUrls = await dataResolver.getUrls();
return resolvedUrls?.[0]?.url ?? '';
}
async #getUrlForMedia(unique: string) {
@@ -215,6 +302,10 @@ export class UmbLinkPickerModalElement extends UmbModalBaseElement<UmbLinkPicker
});
}
this.#resetUrl();
}
#resetUrl() {
this.#partialUpdateLink({ type: null, url: null });
}
@@ -239,7 +330,7 @@ export class UmbLinkPickerModalElement extends UmbModalBaseElement<UmbLinkPicker
return html`
<umb-body-layout
headline=${this.localize.term(
this.modalContext?.data.isNew ? 'defaultdialogs_addLink' : 'defaultdialogs_updateLink',
this.modalContext?.data?.isNew ? 'defaultdialogs_addLink' : 'defaultdialogs_updateLink',
)}>
<uui-box>
${this.#renderLinkType()} ${this.#renderLinkAnchorInput()} ${this.#renderLinkTitleInput()}
@@ -250,8 +341,7 @@ export class UmbLinkPickerModalElement extends UmbModalBaseElement<UmbLinkPicker
<uui-button
color="positive"
look="primary"
label=${this.localize.term(this.modalContext?.data.isNew ? 'general_add' : 'general_update')}
?disabled=${!this.value.link.type}
label=${this.localize.term(this.modalContext?.data?.isNew ? 'general_add' : 'general_update')}
@click=${this.#onSubmit}></uui-button>
</div>
</umb-body-layout>
@@ -263,7 +353,8 @@ export class UmbLinkPickerModalElement extends UmbModalBaseElement<UmbLinkPicker
<umb-property-layout
orientation=${this.#propertyLayoutOrientation}
label=${this.localize.term('linkPicker_modalSource')}
mandatory>
mandatory
?invalid=${this._missingLinkUrl}>
<div slot="editor">
${this.#renderLinkTypeSelection()} ${this.#renderDocumentPicker()} ${this.#renderMediaPicker()}
${this.#renderLinkUrlInput()} ${this.#renderLinkUrlInputReadOnly()}
@@ -326,9 +417,10 @@ export class UmbLinkPickerModalElement extends UmbModalBaseElement<UmbLinkPicker
placeholder=${this.localize.term('placeholders_enterUrl')}
.value=${this.value.link.url ?? ''}
?disabled=${!!this.value.link.unique}
?required=${this._config.hideAnchor}
@change=${this.#onLinkUrlInput}
${umbBindToValidation(this)}>
required
@input=${this.#onLinkUrlInput}
${umbBindToValidation(this)}
${umbFocus()}>
${when(
!this.value.link.unique,
() => html`
@@ -361,12 +453,10 @@ export class UmbLinkPickerModalElement extends UmbModalBaseElement<UmbLinkPicker
<uui-input
data-mark="input:anchor"
slot="editor"
id="link-anchor"
label=${this.localize.term('placeholders_anchor')}
placeholder=${this.localize.term('placeholders_anchor')}
.value=${this.value.link.queryString ?? ''}
@change=${this.#onLinkAnchorInput}
${umbBindToValidation(this)}></uui-input>
@change=${this.#onLinkAnchorInput}></uui-input>
</umb-property-layout>
`;
}

View File

@@ -106,7 +106,7 @@ export class UmbEntityReferencesWorkspaceInfoAppElement extends UmbLitElement {
if (!this._items?.length) return nothing;
return html`
<umb-workspace-info-app-layout headline="#references_labelUsedByItems">
${this.#renderItems()} ${this.#renderReferencePagination()}
<div id="content">${this.#renderItems()} ${this.#renderReferencePagination()}</div>
</umb-workspace-info-app-layout>
`;
}
@@ -144,6 +144,11 @@ export class UmbEntityReferencesWorkspaceInfoAppElement extends UmbLitElement {
display: contents;
}
#content {
display: block;
padding: var(--uui-size-space-3) var(--uui-size-space-4);
}
.pagination-container {
display: flex;
justify-content: center;