diff --git a/src/Umbraco.Web.UI.Client/src/packages/ufm/components/manifests.ts b/src/Umbraco.Web.UI.Client/src/packages/ufm/components/manifests.ts index cefac27f0d59..fc4dae305161 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/ufm/components/manifests.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/ufm/components/manifests.ts @@ -29,4 +29,11 @@ export const manifests: Array = [ api: () => import('./link/link.component.js'), meta: { alias: 'umbLink' }, }, + { + type: 'ufmComponent', + alias: 'Umb.Markdown.MemberName', + name: 'Member Name UFM Component', + api: () => import('./member-name/member-name.component.js'), + meta: { alias: 'umbMemberName' }, + }, ]; diff --git a/src/Umbraco.Web.UI.Client/src/packages/ufm/components/member-name/member-name.component.ts b/src/Umbraco.Web.UI.Client/src/packages/ufm/components/member-name/member-name.component.ts new file mode 100644 index 000000000000..f1e22210c1c5 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/ufm/components/member-name/member-name.component.ts @@ -0,0 +1,15 @@ +import { UmbUfmComponentBase } from '../ufm-component-base.js'; +import type { UfmToken } from '../../plugins/types.js'; + +import './member-name.element.js'; + +export class UmbUfmMemberNameComponent extends UmbUfmComponentBase { + render(token: UfmToken) { + if (!token.text) return; + + const attributes = super.getAttributes(token.text); + return ``; + } +} + +export { UmbUfmMemberNameComponent as api }; diff --git a/src/Umbraco.Web.UI.Client/src/packages/ufm/components/member-name/member-name.element.ts b/src/Umbraco.Web.UI.Client/src/packages/ufm/components/member-name/member-name.element.ts new file mode 100644 index 000000000000..f31f1cfc9fc3 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/ufm/components/member-name/member-name.element.ts @@ -0,0 +1,96 @@ +import { UmbUfmElementBase } from '../ufm-element-base.js'; +import { UMB_UFM_RENDER_CONTEXT } from '../ufm-render/ufm-render.context.js'; +import { customElement, property } from '@umbraco-cms/backoffice/external/lit'; +import { getGuidFromUdi } from '@umbraco-cms/backoffice/utils'; +import { UmbId } from '@umbraco-cms/backoffice/id'; +import { UmbMemberItemRepository } from '@umbraco-cms/backoffice/member'; + +@customElement('ufm-member-name') +export class UmbUfmMemberNameElement extends UmbUfmElementBase { + @property() + alias?: string; + + #memberRepository?: UmbMemberItemRepository; + + constructor() { + super(); + + this.consumeContext(UMB_UFM_RENDER_CONTEXT, (context) => { + this.observe( + context?.value, + async (value) => { + const temp = + this.alias && typeof value === 'object' + ? (value as Record)[this.alias] + : (value as unknown); + + if (!temp) { + this.value = ''; + return; + } + + const uniques = this.#getUniques(temp); + + this.value = await this.#getNames(uniques); + }, + 'observeValue', + ); + }); + } + + #getUniques(value: unknown): Array { + const entries = Array.isArray(value) ? value : [value]; + return entries.flatMap((entry) => this.#normalizeEntry(entry)).filter((x) => x.length > 0); + } + + // Returns an array because a single string entry may be comma-separated. + #normalizeEntry(entry: unknown): Array { + if (entry && typeof entry === 'object') { + const unique = (entry as Record).unique; + return typeof unique === 'string' ? this.#normalizeEntry(unique) : []; + } + if (typeof entry === 'string') { + return entry + .split(',') + .map((token) => this.#extractGuid(token.trim())) + .filter((x): x is string => !!x); + } + return []; + } + + #extractGuid(token: string): string | undefined { + if (token.startsWith('umb://')) { + try { + const guid = getGuidFromUdi(token); + return UmbId.validate(guid) ? guid : undefined; + } catch { + return undefined; + } + } + return UmbId.validate(token) ? token : undefined; + } + + async #getNames(uniques?: Array) { + if (uniques?.length) { + if (!this.#memberRepository) { + this.#memberRepository = new UmbMemberItemRepository(this); + } + + const { data } = await this.#memberRepository.requestItems(uniques); + if (Array.isArray(data) && data.length > 0) { + // TODO: Review usage of `item.variants[0].name` as this needs to be implemented properly for member items [LK] + return data.map((item) => item.variants[0]?.name ?? item.name).join(', '); + } + } + + return ''; + } +} + +export { UmbUfmMemberNameElement as element }; + +declare global { + interface HTMLElementTagNameMap { + 'ufm-member-name': UmbUfmMemberNameElement; + } +} diff --git a/src/Umbraco.Web.UI.Client/src/packages/ufm/plugins/marked-ufm.test.ts b/src/Umbraco.Web.UI.Client/src/packages/ufm/plugins/marked-ufm.test.ts index ec4144f7c8f7..ae9be9452b2e 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/ufm/plugins/marked-ufm.test.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/ufm/plugins/marked-ufm.test.ts @@ -4,6 +4,7 @@ import { UmbMarked } from '../contexts/ufm.context.js'; import { UmbUfmContentNameComponent } from '../components/content-name/content-name.component.js'; import { UmbUfmLabelValueComponent } from '../components/label-value/label-value.component.js'; import { UmbUfmLocalizeComponent } from '../components/localize/localize.component.js'; +import { UmbUfmMemberNameComponent } from '../components/member-name/member-name.component.js'; describe('UmbMarkedUfm', () => { describe('UFM parsing', () => { @@ -26,6 +27,11 @@ describe('UmbMarkedUfm', () => { ufm: '{umbContentName: contentPicker}', expected: '', }, + { ufm: '{umbMemberName:memberPicker}', expected: '' }, + { + ufm: '{umbMemberName: memberPicker}', + expected: '', + }, ]; // Manually configuring the UFM components for testing. @@ -34,6 +40,7 @@ describe('UmbMarkedUfm', () => { { alias: 'umbContentName', marker: '~', render: new UmbUfmContentNameComponent().render }, { alias: 'umbValue', marker: '=', render: new UmbUfmLabelValueComponent().render }, { alias: 'umbLocalize', marker: '#', render: new UmbUfmLocalizeComponent().render }, + { alias: 'umbMemberName', render: new UmbUfmMemberNameComponent().render }, ]), );