Skip to content

Commit 930a29f

Browse files
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ø <[email protected]>
1 parent dda69a1 commit 930a29f

File tree

8 files changed

+319
-70
lines changed

8 files changed

+319
-70
lines changed

src/Umbraco.Web.UI.Client/src/packages/core/workspace/info-app/global-components/workspace-info-app-layout.element.ts

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -22,10 +22,6 @@ export class UmbWorkspaceInfoAppLayoutElement extends UmbLitElement {
2222
uui-box {
2323
--uui-box-default-padding: 0;
2424
}
25-
26-
#container {
27-
padding-left: var(--uui-size-space-4);
28-
}
2925
`,
3026
];
3127
}
Lines changed: 94 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,94 @@
1+
import type { UmbDocumentUrlModel } from './repository/types.js';
2+
import { UmbControllerBase } from '@umbraco-cms/backoffice/class-api';
3+
import type { UmbControllerHost } from '@umbraco-cms/backoffice/controller-api';
4+
import { UmbArrayState } from '@umbraco-cms/backoffice/observable-api';
5+
import { UMB_PROPERTY_DATASET_CONTEXT } from '@umbraco-cms/backoffice/property';
6+
import type { UmbVariantId } from '@umbraco-cms/backoffice/variant';
7+
8+
/**
9+
* A controller for resolving data for document urls
10+
* @exports
11+
* @class UmbDocumentUrlsDataResolver
12+
* @augments {UmbControllerBase}
13+
*/
14+
export class UmbDocumentUrlsDataResolver extends UmbControllerBase {
15+
#appCulture?: string;
16+
#propertyDataSetCulture?: UmbVariantId;
17+
#data?: Array<UmbDocumentUrlModel> | undefined;
18+
19+
#init: Promise<unknown>;
20+
21+
#urls = new UmbArrayState<UmbDocumentUrlModel>([], (url) => url.url);
22+
/**
23+
* The urls for the current culture
24+
* @returns {ObservableArray<UmbDocumentUrlModel>} The urls for the current culture
25+
* @memberof UmbDocumentUrlsDataResolver
26+
*/
27+
public readonly urls = this.#urls.asObservable();
28+
29+
constructor(host: UmbControllerHost) {
30+
super(host);
31+
32+
// TODO: listen for UMB_VARIANT_CONTEXT when available
33+
this.#init = Promise.all([
34+
this.consumeContext(UMB_PROPERTY_DATASET_CONTEXT, (context) => {
35+
this.#propertyDataSetCulture = context?.getVariantId();
36+
this.#setCultureAwareValues();
37+
}).asPromise(),
38+
]);
39+
}
40+
41+
/**
42+
* Get the current data
43+
* @returns {Array<UmbDocumentUrlModel> | undefined} The current data
44+
* @memberof UmbDocumentUrlsDataResolver
45+
*/
46+
getData(): Array<UmbDocumentUrlModel> | undefined {
47+
return this.#data;
48+
}
49+
50+
/**
51+
* Set the current data
52+
* @param {Array<UmbDocumentUrlModel> | undefined} data The current data
53+
* @memberof UmbDocumentUrlsDataResolver
54+
*/
55+
setData(data: Array<UmbDocumentUrlModel> | undefined) {
56+
this.#data = data;
57+
58+
if (!this.#data) {
59+
this.#urls.setValue([]);
60+
return;
61+
}
62+
63+
this.#setCultureAwareValues();
64+
}
65+
66+
/**
67+
* Get the urls for the current culture
68+
* @returns {(Promise<Array<UmbDocumentUrlModel> | []>)} The urls for the current culture
69+
* @memberof UmbDocumentUrlsDataResolver
70+
*/
71+
async getUrls(): Promise<Array<UmbDocumentUrlModel> | []> {
72+
await this.#init;
73+
return this.#urls.getValue();
74+
}
75+
76+
#setCultureAwareValues() {
77+
this.#setUrls();
78+
}
79+
80+
#setUrls() {
81+
const data = this.#getDataForCurrentCulture();
82+
this.#urls.setValue(data ?? []);
83+
}
84+
85+
#getCurrentCulture(): string | undefined {
86+
return this.#propertyDataSetCulture?.culture || this.#appCulture;
87+
}
88+
89+
#getDataForCurrentCulture(): Array<UmbDocumentUrlModel> | undefined {
90+
const culture = this.#getCurrentCulture();
91+
// If there is no culture context (invariant data) we return all urls
92+
return culture ? this.#data?.filter((x) => x.culture === culture) : this.#data;
93+
}
94+
}
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
11
export { UmbDocumentUrlRepository, UMB_DOCUMENT_URL_REPOSITORY_ALIAS } from './repository/index.js';
22

33
export * from './constants.js';
4+
export * from './document-urls-data-resolver.js';

src/Umbraco.Web.UI.Client/src/packages/documents/documents/url/info-app/document-links-workspace-info-app.element.ts

Lines changed: 41 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -2,19 +2,30 @@ import { UmbDocumentUrlRepository } from '../repository/index.js';
22
import type { UmbDocumentVariantOptionModel } from '../../types.js';
33
import { UMB_DOCUMENT_WORKSPACE_CONTEXT } from '../../workspace/constants.js';
44
import type { UmbDocumentUrlModel } from '../repository/types.js';
5-
import { css, customElement, html, nothing, repeat, state, when } from '@umbraco-cms/backoffice/external/lit';
5+
import { UmbDocumentUrlsDataResolver } from '../document-urls-data-resolver.js';
6+
import {
7+
css,
8+
customElement,
9+
html,
10+
ifDefined,
11+
nothing,
12+
repeat,
13+
state,
14+
when,
15+
} from '@umbraco-cms/backoffice/external/lit';
616
import { UmbLitElement } from '@umbraco-cms/backoffice/lit-element';
717
import type { UmbEntityActionEvent } from '@umbraco-cms/backoffice/entity-action';
818
import { UmbRequestReloadStructureForEntityEvent } from '@umbraco-cms/backoffice/entity-action';
919
import { UMB_ACTION_EVENT_CONTEXT } from '@umbraco-cms/backoffice/action';
1020
import { observeMultiple } from '@umbraco-cms/backoffice/observable-api';
1121
import { DocumentVariantStateModel } from '@umbraco-cms/backoffice/external/backend-api';
1222
import { debounce } from '@umbraco-cms/backoffice/utils';
13-
import { UMB_APP_LANGUAGE_CONTEXT } from '@umbraco-cms/backoffice/language';
23+
import { UMB_PROPERTY_DATASET_CONTEXT } from '@umbraco-cms/backoffice/property';
24+
import type { UmbVariantId } from '@umbraco-cms/backoffice/variant';
1425

1526
interface UmbDocumentInfoViewLink {
16-
culture: string;
17-
url: string | undefined;
27+
culture: string | null;
28+
url: string | null | undefined;
1829
state: DocumentVariantStateModel | null | undefined;
1930
}
2031

@@ -37,13 +48,13 @@ export class UmbDocumentLinksWorkspaceInfoAppElement extends UmbLitElement {
3748
@state()
3849
private _links: Array<UmbDocumentInfoViewLink> = [];
3950

40-
@state()
41-
private _defaultCulture?: string;
42-
4351
#urls: Array<UmbDocumentUrlModel> = [];
4452

4553
#documentWorkspaceContext?: typeof UMB_DOCUMENT_WORKSPACE_CONTEXT.TYPE;
4654
#eventContext?: typeof UMB_ACTION_EVENT_CONTEXT.TYPE;
55+
#propertyDataSetVariantId?: UmbVariantId;
56+
57+
#documentUrlsDataResolver? = new UmbDocumentUrlsDataResolver(this);
4758

4859
constructor() {
4960
super();
@@ -88,20 +99,26 @@ export class UmbDocumentLinksWorkspaceInfoAppElement extends UmbLitElement {
8899
});
89100
});
90101

91-
this.consumeContext(UMB_APP_LANGUAGE_CONTEXT, (instance) => {
92-
this.observe(instance?.appDefaultLanguage, (value) => {
93-
this._defaultCulture = value?.unique;
94-
this.#setLinks();
95-
});
102+
this.consumeContext(UMB_PROPERTY_DATASET_CONTEXT, (context) => {
103+
this.#propertyDataSetVariantId = context?.getVariantId();
104+
this.#setLinks();
105+
});
106+
107+
this.observe(this.#documentUrlsDataResolver?.urls, (urls) => {
108+
this.#urls = urls ?? [];
109+
this.#setLinks();
96110
});
97111
}
98112

99113
#setLinks() {
100-
const links: Array<UmbDocumentInfoViewLink> = this.#urls.map((u) => {
101-
const culture = u.culture ?? this._defaultCulture ?? '';
102-
const url = u.url;
114+
const links: Array<UmbDocumentInfoViewLink> = this.#urls.map((url) => {
115+
const culture = url.culture;
103116
const state = this._variantOptions?.find((variantOption) => variantOption.culture === culture)?.variant?.state;
104-
return { culture, url, state };
117+
return {
118+
culture,
119+
url: url.url,
120+
state,
121+
};
105122
});
106123

107124
this._links = links;
@@ -124,14 +141,12 @@ export class UmbDocumentLinksWorkspaceInfoAppElement extends UmbLitElement {
124141
if (!this._unique) return;
125142

126143
this._loading = true;
127-
this.#urls = [];
144+
this.#documentUrlsDataResolver?.setData([]);
128145

129146
const { data } = await this.#documentUrlRepository.requestItems([this._unique]);
130147

131148
if (data?.length) {
132-
const item = data[0];
133-
this.#urls = item.urls;
134-
this.#setLinks();
149+
this.#documentUrlsDataResolver?.setData(data[0].urls);
135150
}
136151

137152
this._loading = false;
@@ -207,7 +222,7 @@ export class UmbDocumentLinksWorkspaceInfoAppElement extends UmbLitElement {
207222
}
208223

209224
return html`
210-
<a class="link-item" href=${this.#getTargetUrl(link.url)} target="_blank">
225+
<a class="link-item" href=${ifDefined(this.#getTargetUrl(link.url))} target="_blank">
211226
<span>
212227
${this.#renderLinkCulture(link.culture)}
213228
<span>${link.url}</span>
@@ -218,9 +233,9 @@ export class UmbDocumentLinksWorkspaceInfoAppElement extends UmbLitElement {
218233
}
219234

220235
#renderNoLinks() {
221-
return html` ${this._variantOptions?.map((variantOption) =>
222-
this.#renderEmptyLink(variantOption.culture, variantOption.variant?.state),
223-
)}`;
236+
return html` ${this._variantOptions
237+
?.filter((variantOption) => variantOption.culture === this.#propertyDataSetVariantId?.culture)
238+
.map((variantOption) => this.#renderEmptyLink(variantOption.culture, variantOption.variant?.state))}`;
224239
}
225240

226241
#renderEmptyLink(culture: string | null, state: DocumentVariantStateModel | null | undefined) {
@@ -235,6 +250,8 @@ export class UmbDocumentLinksWorkspaceInfoAppElement extends UmbLitElement {
235250
#renderLinkCulture(culture: string | null) {
236251
if (!culture) return nothing;
237252
if (this._links.length === 1) return nothing;
253+
const allLinksHaveSameCulture = this._links?.every((link) => link.culture === culture);
254+
if (allLinksHaveSameCulture) return nothing;
238255
return html`<span class="culture">${culture}</span>`;
239256
}
240257

@@ -249,10 +266,6 @@ export class UmbDocumentLinksWorkspaceInfoAppElement extends UmbLitElement {
249266

250267
static override styles = [
251268
css`
252-
uui-box {
253-
--uui-box-default-padding: 0;
254-
}
255-
256269
#loader-container {
257270
display: flex;
258271
justify-content: center;

src/Umbraco.Web.UI.Client/src/packages/documents/documents/url/repository/types.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,6 @@ export interface UmbDocumentUrlsModel {
44
}
55

66
export interface UmbDocumentUrlModel {
7-
culture?: string | null;
7+
culture: string | null;
88
url?: string;
99
}

src/Umbraco.Web.UI.Client/src/packages/multi-url-picker/components/input-multi-url/input-multi-url.element.ts

Lines changed: 51 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,8 @@ import { UUIFormControlMixin } from '@umbraco-cms/backoffice/external/uui';
2121
import type { UmbModalRouteBuilder } from '@umbraco-cms/backoffice/router';
2222
import type { UmbVariantId } from '@umbraco-cms/backoffice/variant';
2323
import type { UUIModalSidebarSize } from '@umbraco-cms/backoffice/external/uui';
24+
import { UmbDocumentUrlRepository, UmbDocumentUrlsDataResolver } from '@umbraco-cms/backoffice/document';
25+
import { UmbMediaUrlRepository } from '@umbraco-cms/backoffice/media';
2426

2527
/**
2628
* @element umb-input-multi-url
@@ -129,6 +131,7 @@ export class UmbInputMultiUrlElement extends UUIFormControlMixin(UmbLitElement,
129131
this.#urls = [...data]; // Unfreeze data coming from State, so we can manipulate it.
130132
super.value = this.#urls.map((x) => x.url).join(',');
131133
this.#sorter.setModel(this.#urls);
134+
this.#populateLinksUrl();
132135
}
133136
get urls(): Array<UmbLinkPickerLink> {
134137
return this.#urls;
@@ -160,6 +163,9 @@ export class UmbInputMultiUrlElement extends UUIFormControlMixin(UmbLitElement,
160163
@state()
161164
private _modalRoute?: UmbModalRouteBuilder;
162165

166+
@state()
167+
_resolvedLinkUrls: Array<{ unique: string; url: string }> = [];
168+
163169
#linkPickerModal;
164170

165171
constructor() {
@@ -229,6 +235,49 @@ export class UmbInputMultiUrlElement extends UUIFormControlMixin(UmbLitElement,
229235
});
230236
}
231237

238+
#populateLinksUrl() {
239+
// Documents and media have URLs saved in the local link format. Display the actual URL to align with what
240+
// the user sees when they selected it initially.
241+
this.#urls.forEach(async (link) => {
242+
if (!link.unique) return;
243+
244+
let url: string | undefined = undefined;
245+
switch (link.type) {
246+
case 'document': {
247+
url = await this.#getUrlForDocument(link.unique);
248+
break;
249+
}
250+
case 'media': {
251+
url = await this.#getUrlForMedia(link.unique);
252+
break;
253+
}
254+
default:
255+
break;
256+
}
257+
258+
if (url) {
259+
const resolvedUrl = { unique: link.unique, url };
260+
this._resolvedLinkUrls = [...this._resolvedLinkUrls, resolvedUrl];
261+
}
262+
});
263+
}
264+
265+
async #getUrlForDocument(unique: string) {
266+
const documentUrlRepository = new UmbDocumentUrlRepository(this);
267+
const { data: documentUrlData } = await documentUrlRepository.requestItems([unique]);
268+
const urlsItem = documentUrlData?.[0];
269+
const dataResolver = new UmbDocumentUrlsDataResolver(this);
270+
dataResolver.setData(urlsItem?.urls);
271+
const resolvedUrls = await dataResolver.getUrls();
272+
return resolvedUrls?.[0]?.url ?? '';
273+
}
274+
275+
async #getUrlForMedia(unique: string) {
276+
const mediaUrlRepository = new UmbMediaUrlRepository(this);
277+
const { data: mediaUrlData } = await mediaUrlRepository.requestItems([unique]);
278+
return mediaUrlData?.[0].url ?? '';
279+
}
280+
232281
async #requestRemoveItem(index: number) {
233282
const item = this.#urls[index];
234283
if (!item) throw new Error('Could not find item at index: ' + index);
@@ -307,12 +356,13 @@ export class UmbInputMultiUrlElement extends UUIFormControlMixin(UmbLitElement,
307356
#renderItem(link: UmbLinkPickerLink, index: number) {
308357
const unique = this.#getUnique(link);
309358
const href = this.readonly ? undefined : (this._modalRoute?.({ index }) ?? undefined);
359+
const resolvedUrl = this._resolvedLinkUrls.find((url) => url.unique === link.unique)?.url ?? '';
310360
return html`
311361
<uui-ref-node
312362
id=${unique}
313363
href=${ifDefined(href)}
314364
name=${link.name || ''}
315-
detail=${(link.url || '') + (link.queryString || '')}
365+
detail=${resolvedUrl + (link.queryString || '')}
316366
?readonly=${this.readonly}>
317367
<umb-icon slot="icon" name=${link.icon || 'icon-link'}></umb-icon>
318368
${when(

0 commit comments

Comments
 (0)