From 023de9998f972b8b8bcd690fbc547170b026dc72 Mon Sep 17 00:00:00 2001 From: leekelleher Date: Wed, 28 Aug 2024 17:59:45 +0100 Subject: [PATCH 1/9] UFM feature additions + refactor - Re-structures "ufm-components" folder - Adds `ufmFilter` extension type - Adds `UmbUfmElementBase` to support reusing filters --- .../core/extension-registry/models/index.ts | 3 + .../models/ufm-filter.model.ts | 14 ++++ .../content-name/content-name.component.ts | 15 ++++ .../content-name/content-name.element.ts | 74 +++++++++++++++++++ src/packages/ufm/components/index.ts | 3 + .../label-value/label-value.component.ts | 15 ++++ .../label-value}/label-value.element.ts | 19 ++--- .../components/localize/localize.component.ts | 15 ++++ .../components/localize/localize.element.ts | 26 +++++++ src/packages/ufm/components/manifests.ts | 25 +++++++ .../ufm/components/ufm-component-base.ts | 25 +++++++ .../ufm/components/ufm-element-base.ts | 70 ++++++++++++++++++ src/packages/ufm/index.ts | 3 +- src/packages/ufm/manifests.ts | 21 +----- src/packages/ufm/plugins/marked-ufm.test.ts | 11 ++- .../ufm-components/document-name.component.ts | 12 --- .../ufm-components/document-name.element.ts | 54 -------------- .../ufm-components/label-value.component.ts | 13 ---- .../ufm/ufm-components/localize.component.ts | 11 --- .../ufm/ufm-components/ufm-component-base.ts | 7 -- 20 files changed, 304 insertions(+), 132 deletions(-) create mode 100644 src/packages/core/extension-registry/models/ufm-filter.model.ts create mode 100644 src/packages/ufm/components/content-name/content-name.component.ts create mode 100644 src/packages/ufm/components/content-name/content-name.element.ts create mode 100644 src/packages/ufm/components/index.ts create mode 100644 src/packages/ufm/components/label-value/label-value.component.ts rename src/packages/ufm/{ufm-components => components/label-value}/label-value.element.ts (51%) create mode 100644 src/packages/ufm/components/localize/localize.component.ts create mode 100644 src/packages/ufm/components/localize/localize.element.ts create mode 100644 src/packages/ufm/components/manifests.ts create mode 100644 src/packages/ufm/components/ufm-component-base.ts create mode 100644 src/packages/ufm/components/ufm-element-base.ts delete mode 100644 src/packages/ufm/ufm-components/document-name.component.ts delete mode 100644 src/packages/ufm/ufm-components/document-name.element.ts delete mode 100644 src/packages/ufm/ufm-components/label-value.component.ts delete mode 100644 src/packages/ufm/ufm-components/localize.component.ts delete mode 100644 src/packages/ufm/ufm-components/ufm-component-base.ts diff --git a/src/packages/core/extension-registry/models/index.ts b/src/packages/core/extension-registry/models/index.ts index adc53de04c..bcec4417ea 100644 --- a/src/packages/core/extension-registry/models/index.ts +++ b/src/packages/core/extension-registry/models/index.ts @@ -46,6 +46,7 @@ import type { ManifestTinyMcePlugin } from './tinymce-plugin.model.js'; import type { ManifestTree } from './tree.model.js'; import type { ManifestTreeItem } from './tree-item.model.js'; import type { ManifestUfmComponent } from './ufm-component.model.js'; +import type { ManifestUfmFilter } from './ufm-filter.model.js'; import type { ManifestUserProfileApp } from './user-profile-app.model.js'; import type { ManifestWorkspace, ManifestWorkspaceRoutableKind } from './workspace.model.js'; import type { ManifestWorkspaceAction, ManifestWorkspaceActionDefaultKind } from './workspace-action.model.js'; @@ -117,6 +118,7 @@ export type * from './tinymce-plugin.model.js'; export type * from './tree-item.model.js'; export type * from './tree.model.js'; export type * from './ufm-component.model.js'; +export type * from './ufm-filter.model.js'; export type * from './user-granular-permission.model.js'; export type * from './user-profile-app.model.js'; export type * from './workspace-action-menu-item.model.js'; @@ -210,6 +212,7 @@ export type ManifestTypes = | ManifestTreeItem | ManifestTreeStore | ManifestUfmComponent + | ManifestUfmFilter | ManifestUserProfileApp | ManifestWorkspaceActionMenuItem | ManifestWorkspaceActions diff --git a/src/packages/core/extension-registry/models/ufm-filter.model.ts b/src/packages/core/extension-registry/models/ufm-filter.model.ts new file mode 100644 index 0000000000..5cabfe21df --- /dev/null +++ b/src/packages/core/extension-registry/models/ufm-filter.model.ts @@ -0,0 +1,14 @@ +import type { ManifestApi, UmbApi } from '@umbraco-cms/backoffice/extension-api'; + +export interface UmbUfmFilterApi extends UmbApi { + filter(...args: Array): string; +} + +export interface MetaUfmFilter { + alias: string; +} + +export interface ManifestUfmFilter extends ManifestApi { + type: 'ufmFilter'; + meta: MetaUfmFilter; +} diff --git a/src/packages/ufm/components/content-name/content-name.component.ts b/src/packages/ufm/components/content-name/content-name.component.ts new file mode 100644 index 0000000000..dfcd5c6e2b --- /dev/null +++ b/src/packages/ufm/components/content-name/content-name.component.ts @@ -0,0 +1,15 @@ +import type { UfmToken } from '../../plugins/marked-ufm.plugin.js'; +import { UmbUfmComponentBase } from '../ufm-component-base.js'; + +import './content-name.element.js'; + +export class UmbUfmContentNameComponent extends UmbUfmComponentBase { + render(token: UfmToken) { + if (!token.text) return; + + const attributes = super.getAttributes(token.text); + return ``; + } +} + +export { UmbUfmContentNameComponent as api }; diff --git a/src/packages/ufm/components/content-name/content-name.element.ts b/src/packages/ufm/components/content-name/content-name.element.ts new file mode 100644 index 0000000000..4335c2bb28 --- /dev/null +++ b/src/packages/ufm/components/content-name/content-name.element.ts @@ -0,0 +1,74 @@ +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 { UmbDocumentItemRepository } from '@umbraco-cms/backoffice/document'; +import { UmbMediaItemRepository } from '@umbraco-cms/backoffice/media'; +import { UmbMemberItemRepository } from '@umbraco-cms/backoffice/member'; + +const elementName = 'ufm-content-name'; + +@customElement(elementName) +export class UmbUfmContentNameElement extends UmbUfmElementBase { + @property() + alias?: string; + + #documentRepository?: UmbDocumentItemRepository; + #mediaRepository?: UmbMediaItemRepository; + #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] as string) + : (value as string); + + const entityType = Array.isArray(temp) && temp.length > 0 ? temp[0].type : null; + const uniques = Array.isArray(temp) ? temp.map((x) => x.unique) : temp ? [temp] : []; + + if (uniques?.length) { + const repository = this.#getRepository(entityType); + if (repository) { + const { data } = await repository.requestItems(uniques); + this.value = data ? data.map((item) => item.name).join(', ') : ''; + return; + } + } + + this.value = ''; + }, + 'observeValue', + ); + }); + } + + #getRepository(entityType?: string | null) { + switch (entityType) { + case 'media': + if (!this.#mediaRepository) this.#mediaRepository = new UmbMediaItemRepository(this); + return this.#mediaRepository; + + case 'member': + if (!this.#memberRepository) this.#memberRepository = new UmbMemberItemRepository(this); + return this.#memberRepository; + + case 'document': + default: + if (!this.#documentRepository) this.#documentRepository = new UmbDocumentItemRepository(this); + return this.#documentRepository; + } + } +} + +export { UmbUfmContentNameElement as element }; + +declare global { + interface HTMLElementTagNameMap { + [elementName]: UmbUfmContentNameElement; + } +} diff --git a/src/packages/ufm/components/index.ts b/src/packages/ufm/components/index.ts new file mode 100644 index 0000000000..7f1f30e709 --- /dev/null +++ b/src/packages/ufm/components/index.ts @@ -0,0 +1,3 @@ +export * from './ufm-render/index.js'; +export * from './ufm-component-base.js'; +export * from './ufm-element-base.js'; diff --git a/src/packages/ufm/components/label-value/label-value.component.ts b/src/packages/ufm/components/label-value/label-value.component.ts new file mode 100644 index 0000000000..bbadd36dc5 --- /dev/null +++ b/src/packages/ufm/components/label-value/label-value.component.ts @@ -0,0 +1,15 @@ +import type { UfmToken } from '../../plugins/marked-ufm.plugin.js'; +import { UmbUfmComponentBase } from '../ufm-component-base.js'; + +import './label-value.element.js'; + +export class UmbUfmLabelValueComponent extends UmbUfmComponentBase { + render(token: UfmToken) { + if (!token.text) return; + + const attributes = super.getAttributes(token.text); + return ``; + } +} + +export { UmbUfmLabelValueComponent as api }; diff --git a/src/packages/ufm/ufm-components/label-value.element.ts b/src/packages/ufm/components/label-value/label-value.element.ts similarity index 51% rename from src/packages/ufm/ufm-components/label-value.element.ts rename to src/packages/ufm/components/label-value/label-value.element.ts index cb28e01989..64081048ac 100644 --- a/src/packages/ufm/ufm-components/label-value.element.ts +++ b/src/packages/ufm/components/label-value/label-value.element.ts @@ -1,17 +1,14 @@ -import { UMB_UFM_RENDER_CONTEXT } from '../components/ufm-render/index.js'; -import { customElement, nothing, property, state } from '@umbraco-cms/backoffice/external/lit'; -import { UmbLitElement } from '@umbraco-cms/backoffice/lit-element'; +import { UMB_UFM_RENDER_CONTEXT } from '../ufm-render/ufm-render.context.js'; +import { UmbUfmElementBase } from '../ufm-element-base.js'; +import { customElement, property } from '@umbraco-cms/backoffice/external/lit'; const elementName = 'ufm-label-value'; @customElement(elementName) -export class UmbUfmLabelValueElement extends UmbLitElement { +export class UmbUfmLabelValueElement extends UmbUfmElementBase { @property() alias?: string; - @state() - private _value?: unknown; - constructor() { super(); @@ -20,19 +17,15 @@ export class UmbUfmLabelValueElement extends UmbLitElement { context.value, (value) => { if (this.alias !== undefined && value !== undefined && typeof value === 'object') { - this._value = (value as Record)[this.alias]; + this.value = (value as Record)[this.alias]; } else { - this._value = value; + this.value = value; } }, 'observeValue', ); }); } - - override render() { - return this._value !== undefined ? this._value : nothing; - } } export { UmbUfmLabelValueElement as element }; diff --git a/src/packages/ufm/components/localize/localize.component.ts b/src/packages/ufm/components/localize/localize.component.ts new file mode 100644 index 0000000000..8789e05455 --- /dev/null +++ b/src/packages/ufm/components/localize/localize.component.ts @@ -0,0 +1,15 @@ +import type { UfmToken } from '../../plugins/marked-ufm.plugin.js'; +import { UmbUfmComponentBase } from '../ufm-component-base.js'; + +import './localize.element.js'; + +export class UmbUfmLocalizeComponent extends UmbUfmComponentBase { + render(token: UfmToken) { + if (!token.text) return; + + const attributes = super.getAttributes(token.text); + return ``; + } +} + +export { UmbUfmLocalizeComponent as api }; diff --git a/src/packages/ufm/components/localize/localize.element.ts b/src/packages/ufm/components/localize/localize.element.ts new file mode 100644 index 0000000000..506a9cda6c --- /dev/null +++ b/src/packages/ufm/components/localize/localize.element.ts @@ -0,0 +1,26 @@ +import { UmbUfmElementBase } from '../ufm-element-base.js'; +import { customElement, property } from '@umbraco-cms/backoffice/external/lit'; + +const elementName = 'ufm-localize'; + +@customElement(elementName) +export class UmbUfmLocalizeElement extends UmbUfmElementBase { + @property() + public set alias(value: string | undefined) { + if (!value) return; + this.#alias = value; + this.value = this.localize.term(value); + } + public get alias(): string | undefined { + return this.#alias; + } + #alias?: string | undefined; +} + +export { UmbUfmLocalizeElement as element }; + +declare global { + interface HTMLElementTagNameMap { + [elementName]: UmbUfmLocalizeElement; + } +} diff --git a/src/packages/ufm/components/manifests.ts b/src/packages/ufm/components/manifests.ts new file mode 100644 index 0000000000..a1120024a0 --- /dev/null +++ b/src/packages/ufm/components/manifests.ts @@ -0,0 +1,25 @@ +import type { ManifestUfmComponent } from '@umbraco-cms/backoffice/extension-registry'; + +export const manifests: Array = [ + { + type: 'ufmComponent', + alias: 'Umb.Markdown.LabelValue', + name: 'Label Value UFM Component', + api: () => import('./label-value/label-value.component.js'), + meta: { marker: '=' }, + }, + { + type: 'ufmComponent', + alias: 'Umb.Markdown.Localize', + name: 'Localize UFM Component', + api: () => import('./localize/localize.component.js'), + meta: { marker: '#' }, + }, + { + type: 'ufmComponent', + alias: 'Umb.Markdown.ContentName', + name: 'Content Name UFM Component', + api: () => import('./content-name/content-name.component.js'), + meta: { marker: '~' }, + }, +]; diff --git a/src/packages/ufm/components/ufm-component-base.ts b/src/packages/ufm/components/ufm-component-base.ts new file mode 100644 index 0000000000..6b1d35217d --- /dev/null +++ b/src/packages/ufm/components/ufm-component-base.ts @@ -0,0 +1,25 @@ +import type { UfmToken } from '../plugins/marked-ufm.plugin.js'; +import type { UmbUfmComponentApi } from '@umbraco-cms/backoffice/extension-registry'; + +export abstract class UmbUfmComponentBase implements UmbUfmComponentApi { + protected getAttributes(text: string): string | null { + if (!text) return null; + + const pipeIndex = text.indexOf('|'); + + if (pipeIndex === -1) { + return `alias="${text.trim()}"`; + } + + const alias = text.substring(0, pipeIndex).trim(); + const filters = text.substring(pipeIndex + 1).trim(); + + return Object.entries({ alias, filters }) + .map(([key, value]) => (value ? `${key}="${value.trim()}"` : null)) + .join(' '); + } + + abstract render(token: UfmToken): string | undefined; + + destroy() {} +} diff --git a/src/packages/ufm/components/ufm-element-base.ts b/src/packages/ufm/components/ufm-element-base.ts new file mode 100644 index 0000000000..4555cc1a9d --- /dev/null +++ b/src/packages/ufm/components/ufm-element-base.ts @@ -0,0 +1,70 @@ +import { nothing, property, state } from '@umbraco-cms/backoffice/external/lit'; +import { UmbExtensionsApiInitializer } from '@umbraco-cms/backoffice/extension-api'; +import { umbExtensionsRegistry } from '@umbraco-cms/backoffice/extension-registry'; +import { UmbLitElement } from '@umbraco-cms/backoffice/lit-element'; +import type { ManifestUfmFilter } from '@umbraco-cms/backoffice/extension-registry'; +import type { UmbExtensionApiInitializer } from '@umbraco-cms/backoffice/extension-api'; + +// eslint-disable-next-line local-rules/enforce-element-suffix-on-element-class-name +export abstract class UmbUfmElementBase extends UmbLitElement { + #filterFuncArgs?: Array<{ key: string; args: Array }>; + #functions?: Record) => string>; + + @property() + public set filters(value: string | undefined) { + this.#filters = value; + + this.#filterFuncArgs = value + ?.split('|') + .filter((item) => item) + .map((item) => { + const [key, ...args] = item.split(':').map((x) => x.trim()); + return { key, args }; + }); + } + public get filters(): string | undefined { + return this.#filters; + } + #filters?: string | undefined; + + @state() + value?: unknown; + + @state() + private _initialized = false; + + constructor() { + super(); + + // TODO: [LK] Review if this could be initialized elsewhere (upwards, in a context), + // as it's called mulitple times, and often not all the filters are loaded. + new UmbExtensionsApiInitializer(this, umbExtensionsRegistry, 'ufmFilter', [], undefined, (controllers) => { + this.#functions = Object.fromEntries( + controllers + .map((controller) => { + const ctrl = controller as unknown as UmbExtensionApiInitializer; + if (!ctrl.manifest || !ctrl.api) return []; + return [ctrl.manifest.meta.alias, ctrl.api.filter]; + }) + .filter((x) => x), + ); + + this._initialized = true; + }); + } + + override render() { + if (!this._initialized) return nothing; + let values = Array.isArray(this.value) ? this.value : [this.value]; + if (this.#functions && this.#filterFuncArgs) { + for (const item of this.#filterFuncArgs) { + const func = this.#functions[item.key]; + if (func) { + values = values.map((value) => func(value, ...item.args)); + } + } + } + + return values.join(', '); + } +} diff --git a/src/packages/ufm/index.ts b/src/packages/ufm/index.ts index 6ef5cd49bd..f1bbeb0c76 100644 --- a/src/packages/ufm/index.ts +++ b/src/packages/ufm/index.ts @@ -1,3 +1,2 @@ -export * from './components/ufm-render/index.js'; export * from './plugins/marked-ufm.plugin.js'; -export * from './ufm-components/ufm-component-base.js'; +export * from './components/index.js'; diff --git a/src/packages/ufm/manifests.ts b/src/packages/ufm/manifests.ts index ec604e0842..860406d686 100644 --- a/src/packages/ufm/manifests.ts +++ b/src/packages/ufm/manifests.ts @@ -1,18 +1,5 @@ -import type { ManifestUfmComponent } from '@umbraco-cms/backoffice/extension-registry'; +import { manifests as ufmComponents } from './components/manifests.js'; +import { manifests as ufmFilters } from './filters/manifests.js'; +import type { ManifestTypes } from '@umbraco-cms/backoffice/extension-registry'; -export const manifests: Array = [ - { - type: 'ufmComponent', - alias: 'Umb.Markdown.LabelValue', - name: 'Label Value UFM Component', - api: () => import('./ufm-components/label-value.component.js'), - meta: { marker: '=' }, - }, - { - type: 'ufmComponent', - alias: 'Umb.Markdown.Localize', - name: 'Localize UFM Component', - api: () => import('./ufm-components/localize.component.js'), - meta: { marker: '#' }, - }, -]; +export const manifests: Array = [...ufmComponents, ...ufmFilters]; diff --git a/src/packages/ufm/plugins/marked-ufm.test.ts b/src/packages/ufm/plugins/marked-ufm.test.ts index ef01944675..990645b8ff 100644 --- a/src/packages/ufm/plugins/marked-ufm.test.ts +++ b/src/packages/ufm/plugins/marked-ufm.test.ts @@ -1,8 +1,9 @@ import { expect } from '@open-wc/testing'; import { ufm } from './marked-ufm.plugin.js'; import { UmbMarked } from '../index.js'; -import { UmbUfmLabelValueComponent } from '../ufm-components/label-value.component.js'; -import { UmbUfmLocalizeComponent } from '../ufm-components/localize.component.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'; describe('UmbMarkedUfm', () => { describe('UFM parsing', () => { @@ -11,7 +12,11 @@ describe('UmbMarkedUfm', () => { { ufm: '{= prop1}', expected: '' }, { ufm: '{= prop1 }', expected: '' }, { ufm: '{{=prop1}}', expected: '{}' }, - { ufm: '{#general_add}', expected: '' }, + { + ufm: '{= prop1 | strip-html | truncate:30}', + expected: '', + }, + { ufm: '{#general_add}', expected: '' }, ]; // Manually configuring the UFM components for testing. diff --git a/src/packages/ufm/ufm-components/document-name.component.ts b/src/packages/ufm/ufm-components/document-name.component.ts deleted file mode 100644 index 8b7e5ec47e..0000000000 --- a/src/packages/ufm/ufm-components/document-name.component.ts +++ /dev/null @@ -1,12 +0,0 @@ -// import { UmbUfmComponentBase } from './ufm-component-base.js'; -// import type { Tokens } from '@umbraco-cms/backoffice/external/marked'; - -// import './document-name.element.js'; - -// export class UmbUfmDocumentNameComponent extends UmbUfmComponentBase { -// render(token: Tokens.Generic) { -// return ``; -// } -// } - -// export { UmbUfmDocumentNameComponent as api }; diff --git a/src/packages/ufm/ufm-components/document-name.element.ts b/src/packages/ufm/ufm-components/document-name.element.ts deleted file mode 100644 index d4f59d15cc..0000000000 --- a/src/packages/ufm/ufm-components/document-name.element.ts +++ /dev/null @@ -1,54 +0,0 @@ -// import { UMB_UFM_RENDER_CONTEXT } from '@umbraco-cms/backoffice/components'; -// import { UmbDocumentItemRepository } from '@umbraco-cms/backoffice/document'; -// import { customElement, property, state } from '@umbraco-cms/backoffice/external/lit'; -// import { UmbLitElement } from '@umbraco-cms/backoffice/lit-element'; - -// const elementName = 'ufm-document-name'; - -// @customElement(elementName) -// export class UmbUfmDocumentNameElement extends UmbLitElement { -// @property() -// alias?: string; - -// @state() -// private _value?: unknown; - -// #documentRepository = new UmbDocumentItemRepository(this); - -// constructor() { -// super(); - -// this.consumeContext(UMB_UFM_RENDER_CONTEXT, (context) => { -// this.observe( -// context.value, -// async (value) => { -// if (!value) return; - -// const unique = -// this.alias && typeof value === 'object' -// ? ((value as Record)[this.alias] as string) -// : (value as string); - -// if (!unique) return; - -// const { data } = await this.#documentRepository.requestItems([unique]); - -// this._value = data?.[0]?.name; -// }, -// 'observeValue', -// ); -// }); -// } - -// override render() { -// return this._value ?? this.alias; -// } -// } - -// export { UmbUfmDocumentNameElement as element }; - -// declare global { -// interface HTMLElementTagNameMap { -// [elementName]: UmbUfmDocumentNameElement; -// } -// } diff --git a/src/packages/ufm/ufm-components/label-value.component.ts b/src/packages/ufm/ufm-components/label-value.component.ts deleted file mode 100644 index 7fa6b5d815..0000000000 --- a/src/packages/ufm/ufm-components/label-value.component.ts +++ /dev/null @@ -1,13 +0,0 @@ -import type { UfmToken } from '../plugins/marked-ufm.plugin.js'; -import { UmbUfmComponentBase } from './ufm-component-base.js'; - -import './label-value.element.js'; - -export class UmbUfmLabelValueComponent extends UmbUfmComponentBase { - render(token: UfmToken) { - if (!token.text) return; - return ``; - } -} - -export { UmbUfmLabelValueComponent as api }; diff --git a/src/packages/ufm/ufm-components/localize.component.ts b/src/packages/ufm/ufm-components/localize.component.ts deleted file mode 100644 index 79890f75f5..0000000000 --- a/src/packages/ufm/ufm-components/localize.component.ts +++ /dev/null @@ -1,11 +0,0 @@ -import type { UfmToken } from '../plugins/marked-ufm.plugin.js'; -import { UmbUfmComponentBase } from './ufm-component-base.js'; - -export class UmbUfmLocalizeComponent extends UmbUfmComponentBase { - render(token: UfmToken) { - if (!token.text) return; - return ``; - } -} - -export { UmbUfmLocalizeComponent as api }; diff --git a/src/packages/ufm/ufm-components/ufm-component-base.ts b/src/packages/ufm/ufm-components/ufm-component-base.ts deleted file mode 100644 index 71f552363e..0000000000 --- a/src/packages/ufm/ufm-components/ufm-component-base.ts +++ /dev/null @@ -1,7 +0,0 @@ -import type { UfmToken } from '../plugins/marked-ufm.plugin.js'; -import type { UmbUfmComponentApi } from '@umbraco-cms/backoffice/extension-registry'; - -export abstract class UmbUfmComponentBase implements UmbUfmComponentApi { - abstract render(token: UfmToken): string | undefined; - destroy() {} -} From 86247f3123fc545bcc80d3205866626bfc6e839f Mon Sep 17 00:00:00 2001 From: leekelleher Date: Wed, 28 Aug 2024 18:08:25 +0100 Subject: [PATCH 2/9] Code tidy-up --- src/packages/ufm/components/ufm-render/ufm-render.context.ts | 2 +- src/packages/ufm/components/ufm-render/ufm-render.element.ts | 5 +++-- src/packages/ufm/plugins/marked-ufm.plugin.ts | 3 ++- 3 files changed, 6 insertions(+), 4 deletions(-) diff --git a/src/packages/ufm/components/ufm-render/ufm-render.context.ts b/src/packages/ufm/components/ufm-render/ufm-render.context.ts index 1ba6e4fe14..77de0e84a0 100644 --- a/src/packages/ufm/components/ufm-render/ufm-render.context.ts +++ b/src/packages/ufm/components/ufm-render/ufm-render.context.ts @@ -1,7 +1,7 @@ import { UmbContextBase } from '@umbraco-cms/backoffice/class-api'; import { UmbContextToken } from '@umbraco-cms/backoffice/context-api'; -import type { UmbControllerHost } from '@umbraco-cms/backoffice/controller-api'; import { UmbObjectState } from '@umbraco-cms/backoffice/observable-api'; +import type { UmbControllerHost } from '@umbraco-cms/backoffice/controller-api'; export class UmbUfmRenderContext extends UmbContextBase { #value = new UmbObjectState(undefined); diff --git a/src/packages/ufm/components/ufm-render/ufm-render.element.ts b/src/packages/ufm/components/ufm-render/ufm-render.element.ts index 56173caf27..49f75f770b 100644 --- a/src/packages/ufm/components/ufm-render/ufm-render.element.ts +++ b/src/packages/ufm/components/ufm-render/ufm-render.element.ts @@ -4,12 +4,12 @@ import { UmbUfmRenderContext } from './ufm-render.context.js'; import { css, customElement, nothing, property, unsafeHTML, until } from '@umbraco-cms/backoffice/external/lit'; import { DOMPurify } from '@umbraco-cms/backoffice/external/dompurify'; import { Marked } from '@umbraco-cms/backoffice/external/marked'; -import type { UmbExtensionApiInitializer } from '@umbraco-cms/backoffice/extension-api'; import { UmbExtensionsApiInitializer } from '@umbraco-cms/backoffice/extension-api'; +import { umbExtensionsRegistry } from '@umbraco-cms/backoffice/extension-registry'; import { UmbLitElement } from '@umbraco-cms/backoffice/lit-element'; import { UmbTextStyles } from '@umbraco-cms/backoffice/style'; -import { umbExtensionsRegistry } from '@umbraco-cms/backoffice/extension-registry'; import type { ManifestUfmComponent } from '@umbraco-cms/backoffice/extension-registry'; +import type { UmbExtensionApiInitializer } from '@umbraco-cms/backoffice/extension-api'; const UmbDomPurify = DOMPurify(window); const UmbDomPurifyConfig: DOMPurify.Config = { @@ -62,6 +62,7 @@ export class UmbUfmRenderElement extends UmbLitElement { constructor() { super(); + // TODO: [LK] Review if this could be initialized elsewhere (upwards, in a context), as it's called mulitple times. new UmbExtensionsApiInitializer(this, umbExtensionsRegistry, 'ufmComponent', [], undefined, (controllers) => { UmbMarked.use( ufm( diff --git a/src/packages/ufm/plugins/marked-ufm.plugin.ts b/src/packages/ufm/plugins/marked-ufm.plugin.ts index 65b57912a0..edebdefcca 100644 --- a/src/packages/ufm/plugins/marked-ufm.plugin.ts +++ b/src/packages/ufm/plugins/marked-ufm.plugin.ts @@ -12,7 +12,8 @@ export interface UfmToken extends Tokens.Generic { /** * - * @param plugins + * @param {Array} plugins - An array of UFM plugins. + * @returns {MarkedExtension} A Marked extension object. */ export function ufm(plugins: Array = []): MarkedExtension { return { From c8b6cc3426cd51c44afbc09696ee1dd7bca98b1f Mon Sep 17 00:00:00 2001 From: leekelleher Date: Wed, 28 Aug 2024 18:09:00 +0100 Subject: [PATCH 3/9] Adds test for `{~contentPicker}` "ufm-content-name" syntax --- src/packages/ufm/plugins/marked-ufm.test.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/packages/ufm/plugins/marked-ufm.test.ts b/src/packages/ufm/plugins/marked-ufm.test.ts index 990645b8ff..bdf4c6cd6c 100644 --- a/src/packages/ufm/plugins/marked-ufm.test.ts +++ b/src/packages/ufm/plugins/marked-ufm.test.ts @@ -17,11 +17,13 @@ describe('UmbMarkedUfm', () => { expected: '', }, { ufm: '{#general_add}', expected: '' }, + { ufm: '{~contentPicker}', expected: '' }, ]; // Manually configuring the UFM components for testing. UmbMarked.use( ufm([ + { alias: 'Umb.Markdown.ContentName', marker: '~', render: new UmbUfmContentNameComponent().render }, { alias: 'Umb.Markdown.LabelValue', marker: '=', render: new UmbUfmLabelValueComponent().render }, { alias: 'Umb.Markdown.Localize', marker: '#', render: new UmbUfmLocalizeComponent().render }, ]), From 6de6c47297103e79e49e6a79eff046d880cd226d Mon Sep 17 00:00:00 2001 From: leekelleher Date: Wed, 28 Aug 2024 18:09:44 +0100 Subject: [PATCH 4/9] Exports `UfmPlugin` and `UfmToken` types --- src/packages/ufm/index.ts | 2 +- src/packages/ufm/plugins/index.ts | 2 ++ 2 files changed, 3 insertions(+), 1 deletion(-) create mode 100644 src/packages/ufm/plugins/index.ts diff --git a/src/packages/ufm/index.ts b/src/packages/ufm/index.ts index f1bbeb0c76..42b3ed554d 100644 --- a/src/packages/ufm/index.ts +++ b/src/packages/ufm/index.ts @@ -1,2 +1,2 @@ -export * from './plugins/marked-ufm.plugin.js'; +export * from './plugins/index.js'; export * from './components/index.js'; diff --git a/src/packages/ufm/plugins/index.ts b/src/packages/ufm/plugins/index.ts new file mode 100644 index 0000000000..09836813d3 --- /dev/null +++ b/src/packages/ufm/plugins/index.ts @@ -0,0 +1,2 @@ +export { ufm } from './marked-ufm.plugin.js'; +export type { UfmPlugin, UfmToken } from './marked-ufm.plugin.js'; From 6cde55a96dd589d7d739cf0130ba6232992499c5 Mon Sep 17 00:00:00 2001 From: leekelleher Date: Wed, 28 Aug 2024 18:10:53 +0100 Subject: [PATCH 5/9] Adds UFM filter extensions/functions - Fallback - Lowercase - Strip HTML - Title Case - Truncate - Uppercase - Word Limit --- src/packages/ufm/filters/fallback.filter.ts | 11 ++++ src/packages/ufm/filters/lowercase.filter.ts | 11 ++++ src/packages/ufm/filters/manifests.ts | 53 +++++++++++++++++++ src/packages/ufm/filters/strip-html.filter.ts | 17 ++++++ src/packages/ufm/filters/title-case.filter.ts | 11 ++++ src/packages/ufm/filters/truncate.filter.ts | 15 ++++++ src/packages/ufm/filters/uppercase.filter.ts | 11 ++++ src/packages/ufm/filters/word-limit.filter.ts | 12 +++++ 8 files changed, 141 insertions(+) create mode 100644 src/packages/ufm/filters/fallback.filter.ts create mode 100644 src/packages/ufm/filters/lowercase.filter.ts create mode 100644 src/packages/ufm/filters/manifests.ts create mode 100644 src/packages/ufm/filters/strip-html.filter.ts create mode 100644 src/packages/ufm/filters/title-case.filter.ts create mode 100644 src/packages/ufm/filters/truncate.filter.ts create mode 100644 src/packages/ufm/filters/uppercase.filter.ts create mode 100644 src/packages/ufm/filters/word-limit.filter.ts diff --git a/src/packages/ufm/filters/fallback.filter.ts b/src/packages/ufm/filters/fallback.filter.ts new file mode 100644 index 0000000000..2302334d10 --- /dev/null +++ b/src/packages/ufm/filters/fallback.filter.ts @@ -0,0 +1,11 @@ +import type { UmbUfmFilterApi } from '@umbraco-cms/backoffice/extension-registry'; + +class UmbUfmFallbackFilterApi implements UmbUfmFilterApi { + filter(str: string, fallback: string) { + return typeof str !== 'string' || str ? str : fallback; + } + + destroy() {} +} + +export { UmbUfmFallbackFilterApi as api }; diff --git a/src/packages/ufm/filters/lowercase.filter.ts b/src/packages/ufm/filters/lowercase.filter.ts new file mode 100644 index 0000000000..7c6cd7484a --- /dev/null +++ b/src/packages/ufm/filters/lowercase.filter.ts @@ -0,0 +1,11 @@ +import type { UmbUfmFilterApi } from '@umbraco-cms/backoffice/extension-registry'; + +class UmbUfmLowercaseFilterApi implements UmbUfmFilterApi { + filter(str: string) { + return str.toLocaleLowerCase(); + } + + destroy() {} +} + +export { UmbUfmLowercaseFilterApi as api }; diff --git a/src/packages/ufm/filters/manifests.ts b/src/packages/ufm/filters/manifests.ts new file mode 100644 index 0000000000..c80c5ffd35 --- /dev/null +++ b/src/packages/ufm/filters/manifests.ts @@ -0,0 +1,53 @@ +import type { ManifestUfmFilter } from '@umbraco-cms/backoffice/extension-registry'; + +export const manifests: Array = [ + { + type: 'ufmFilter', + alias: 'Umb.Filter.Fallback', + name: 'Fallback UFM Filter', + api: () => import('./fallback.filter.js'), + meta: { alias: 'fallback' }, + }, + { + type: 'ufmFilter', + alias: 'Umb.Filter.Lowercase', + name: 'Lowercase UFM Filter', + api: () => import('./lowercase.filter.js'), + meta: { alias: 'lowercase' }, + }, + { + type: 'ufmFilter', + alias: 'Umb.Filter.StripHtml', + name: 'Strip HTML UFM Filter', + api: () => import('./strip-html.filter.js'), + meta: { alias: 'strip-html' }, + }, + { + type: 'ufmFilter', + alias: 'Umb.Filter.TitleCase', + name: 'Title Case UFM Filter', + api: () => import('./title-case.filter.js'), + meta: { alias: 'title-case' }, + }, + { + type: 'ufmFilter', + alias: 'Umb.Filter.Truncate', + name: 'Truncate UFM Filter', + api: () => import('./truncate.filter.js'), + meta: { alias: 'truncate' }, + }, + { + type: 'ufmFilter', + alias: 'Umb.Filter.Uppercase', + name: 'Uppercase UFM Filter', + api: () => import('./uppercase.filter.js'), + meta: { alias: 'uppercase' }, + }, + { + type: 'ufmFilter', + alias: 'Umb.Filter.WordLimit', + name: 'Word Limit UFM Filter', + api: () => import('./word-limit.filter.js'), + meta: { alias: 'word-limit' }, + }, +]; diff --git a/src/packages/ufm/filters/strip-html.filter.ts b/src/packages/ufm/filters/strip-html.filter.ts new file mode 100644 index 0000000000..eaa6d3c1c8 --- /dev/null +++ b/src/packages/ufm/filters/strip-html.filter.ts @@ -0,0 +1,17 @@ +import type { UmbUfmFilterApi } from '@umbraco-cms/backoffice/extension-registry'; + +class UmbUfmStripHtmlFilterApi implements UmbUfmFilterApi { + filter(value: string | { markup: string } | undefined | null) { + if (!value) return ''; + + const markup = typeof value === 'object' && Object.hasOwn(value, 'markup') ? value.markup : (value as string); + const parser = new DOMParser(); + const doc = parser.parseFromString(markup, 'text/html'); + + return doc.body.textContent ?? ''; + } + + destroy() {} +} + +export { UmbUfmStripHtmlFilterApi as api }; diff --git a/src/packages/ufm/filters/title-case.filter.ts b/src/packages/ufm/filters/title-case.filter.ts new file mode 100644 index 0000000000..7633b0a9f3 --- /dev/null +++ b/src/packages/ufm/filters/title-case.filter.ts @@ -0,0 +1,11 @@ +import type { UmbUfmFilterApi } from '@umbraco-cms/backoffice/extension-registry'; + +class UmbUfmTitleCaseFilterApi implements UmbUfmFilterApi { + filter(str: string) { + return str.replace(/\w\S*/g, (txt) => txt.charAt(0).toUpperCase() + txt.substring(1).toLowerCase()); + } + + destroy() {} +} + +export { UmbUfmTitleCaseFilterApi as api }; diff --git a/src/packages/ufm/filters/truncate.filter.ts b/src/packages/ufm/filters/truncate.filter.ts new file mode 100644 index 0000000000..9a48a236e5 --- /dev/null +++ b/src/packages/ufm/filters/truncate.filter.ts @@ -0,0 +1,15 @@ +import type { UmbUfmFilterApi } from '@umbraco-cms/backoffice/extension-registry'; + +class UmbUfmTruncateFilterApi implements UmbUfmFilterApi { + filter(str: string, length: number, tail?: string) { + if (tail === 'false') tail = ''; + if (tail === 'true') tail = '…'; + tail = !tail && tail !== '' ? '…' : tail; + + return str.slice(0, length).trim() + tail; + } + + destroy() {} +} + +export { UmbUfmTruncateFilterApi as api }; diff --git a/src/packages/ufm/filters/uppercase.filter.ts b/src/packages/ufm/filters/uppercase.filter.ts new file mode 100644 index 0000000000..033c35ede9 --- /dev/null +++ b/src/packages/ufm/filters/uppercase.filter.ts @@ -0,0 +1,11 @@ +import type { UmbUfmFilterApi } from '@umbraco-cms/backoffice/extension-registry'; + +class UmbUfmUppercaseFilterApi implements UmbUfmFilterApi { + filter(str: string) { + return str.toLocaleUpperCase(); + } + + destroy() {} +} + +export { UmbUfmUppercaseFilterApi as api }; diff --git a/src/packages/ufm/filters/word-limit.filter.ts b/src/packages/ufm/filters/word-limit.filter.ts new file mode 100644 index 0000000000..b457932285 --- /dev/null +++ b/src/packages/ufm/filters/word-limit.filter.ts @@ -0,0 +1,12 @@ +import type { UmbUfmFilterApi } from '@umbraco-cms/backoffice/extension-registry'; + +class UmbUfmWordLimitFilterApi implements UmbUfmFilterApi { + filter(str: string, limit: number) { + const words = str.split(/\s+/); + return words.length > limit ? words.slice(0, limit).join(' ') : str; + } + + destroy() {} +} + +export { UmbUfmWordLimitFilterApi as api }; From d803bda3a83c123b3313810f9461146db141e27b Mon Sep 17 00:00:00 2001 From: leekelleher Date: Wed, 28 Aug 2024 18:56:06 +0100 Subject: [PATCH 6/9] Resolved a couple of @sonarcloud issues --- src/packages/ufm/components/localize/localize.element.ts | 2 +- src/packages/ufm/components/ufm-element-base.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/packages/ufm/components/localize/localize.element.ts b/src/packages/ufm/components/localize/localize.element.ts index 506a9cda6c..9f440de11a 100644 --- a/src/packages/ufm/components/localize/localize.element.ts +++ b/src/packages/ufm/components/localize/localize.element.ts @@ -14,7 +14,7 @@ export class UmbUfmLocalizeElement extends UmbUfmElementBase { public get alias(): string | undefined { return this.#alias; } - #alias?: string | undefined; + #alias?: string; } export { UmbUfmLocalizeElement as element }; diff --git a/src/packages/ufm/components/ufm-element-base.ts b/src/packages/ufm/components/ufm-element-base.ts index 4555cc1a9d..02e4c4b2ef 100644 --- a/src/packages/ufm/components/ufm-element-base.ts +++ b/src/packages/ufm/components/ufm-element-base.ts @@ -25,7 +25,7 @@ export abstract class UmbUfmElementBase extends UmbLitElement { public get filters(): string | undefined { return this.#filters; } - #filters?: string | undefined; + #filters?: string; @state() value?: unknown; From ea0630d9486306008ab1c26e136b112cbe989839 Mon Sep 17 00:00:00 2001 From: leekelleher Date: Mon, 9 Sep 2024 16:57:48 +0100 Subject: [PATCH 7/9] Adds base class for UFM filters --- .../core/extension-registry/models/ufm-filter.model.ts | 2 +- src/packages/ufm/filters/fallback.filter.ts | 6 ++---- src/packages/ufm/filters/lowercase.filter.ts | 10 ++++------ src/packages/ufm/filters/strip-html.filter.ts | 6 ++---- src/packages/ufm/filters/title-case.filter.ts | 10 ++++------ src/packages/ufm/filters/truncate.filter.ts | 6 ++---- src/packages/ufm/filters/uppercase.filter.ts | 10 ++++------ src/packages/ufm/filters/word-limit.filter.ts | 10 ++++------ src/packages/ufm/index.ts | 3 ++- src/packages/ufm/types.ts | 7 +++++++ 10 files changed, 32 insertions(+), 38 deletions(-) create mode 100644 src/packages/ufm/types.ts diff --git a/src/packages/core/extension-registry/models/ufm-filter.model.ts b/src/packages/core/extension-registry/models/ufm-filter.model.ts index 5cabfe21df..8144601021 100644 --- a/src/packages/core/extension-registry/models/ufm-filter.model.ts +++ b/src/packages/core/extension-registry/models/ufm-filter.model.ts @@ -1,7 +1,7 @@ import type { ManifestApi, UmbApi } from '@umbraco-cms/backoffice/extension-api'; export interface UmbUfmFilterApi extends UmbApi { - filter(...args: Array): string; + filter(...args: Array): string | undefined | null; } export interface MetaUfmFilter { diff --git a/src/packages/ufm/filters/fallback.filter.ts b/src/packages/ufm/filters/fallback.filter.ts index 2302334d10..ceb8001754 100644 --- a/src/packages/ufm/filters/fallback.filter.ts +++ b/src/packages/ufm/filters/fallback.filter.ts @@ -1,11 +1,9 @@ -import type { UmbUfmFilterApi } from '@umbraco-cms/backoffice/extension-registry'; +import { UmbUfmFilterBase } from '../types.js'; -class UmbUfmFallbackFilterApi implements UmbUfmFilterApi { +class UmbUfmFallbackFilterApi extends UmbUfmFilterBase { filter(str: string, fallback: string) { return typeof str !== 'string' || str ? str : fallback; } - - destroy() {} } export { UmbUfmFallbackFilterApi as api }; diff --git a/src/packages/ufm/filters/lowercase.filter.ts b/src/packages/ufm/filters/lowercase.filter.ts index 7c6cd7484a..bf175a2a65 100644 --- a/src/packages/ufm/filters/lowercase.filter.ts +++ b/src/packages/ufm/filters/lowercase.filter.ts @@ -1,11 +1,9 @@ -import type { UmbUfmFilterApi } from '@umbraco-cms/backoffice/extension-registry'; +import { UmbUfmFilterBase } from '../types.js'; -class UmbUfmLowercaseFilterApi implements UmbUfmFilterApi { - filter(str: string) { - return str.toLocaleLowerCase(); +class UmbUfmLowercaseFilterApi extends UmbUfmFilterBase { + filter(str?: string) { + return str?.toLocaleLowerCase(); } - - destroy() {} } export { UmbUfmLowercaseFilterApi as api }; diff --git a/src/packages/ufm/filters/strip-html.filter.ts b/src/packages/ufm/filters/strip-html.filter.ts index eaa6d3c1c8..6d19708e59 100644 --- a/src/packages/ufm/filters/strip-html.filter.ts +++ b/src/packages/ufm/filters/strip-html.filter.ts @@ -1,6 +1,6 @@ -import type { UmbUfmFilterApi } from '@umbraco-cms/backoffice/extension-registry'; +import { UmbUfmFilterBase } from '../types.js'; -class UmbUfmStripHtmlFilterApi implements UmbUfmFilterApi { +class UmbUfmStripHtmlFilterApi extends UmbUfmFilterBase { filter(value: string | { markup: string } | undefined | null) { if (!value) return ''; @@ -10,8 +10,6 @@ class UmbUfmStripHtmlFilterApi implements UmbUfmFilterApi { return doc.body.textContent ?? ''; } - - destroy() {} } export { UmbUfmStripHtmlFilterApi as api }; diff --git a/src/packages/ufm/filters/title-case.filter.ts b/src/packages/ufm/filters/title-case.filter.ts index 7633b0a9f3..ab546b7ee3 100644 --- a/src/packages/ufm/filters/title-case.filter.ts +++ b/src/packages/ufm/filters/title-case.filter.ts @@ -1,11 +1,9 @@ -import type { UmbUfmFilterApi } from '@umbraco-cms/backoffice/extension-registry'; +import { UmbUfmFilterBase } from '../types.js'; -class UmbUfmTitleCaseFilterApi implements UmbUfmFilterApi { - filter(str: string) { - return str.replace(/\w\S*/g, (txt) => txt.charAt(0).toUpperCase() + txt.substring(1).toLowerCase()); +class UmbUfmTitleCaseFilterApi extends UmbUfmFilterBase { + filter(str?: string) { + return str?.replace(/\w\S*/g, (txt) => txt.charAt(0).toUpperCase() + txt.substring(1).toLowerCase()); } - - destroy() {} } export { UmbUfmTitleCaseFilterApi as api }; diff --git a/src/packages/ufm/filters/truncate.filter.ts b/src/packages/ufm/filters/truncate.filter.ts index 9a48a236e5..922122acdf 100644 --- a/src/packages/ufm/filters/truncate.filter.ts +++ b/src/packages/ufm/filters/truncate.filter.ts @@ -1,6 +1,6 @@ -import type { UmbUfmFilterApi } from '@umbraco-cms/backoffice/extension-registry'; +import { UmbUfmFilterBase } from '../types.js'; -class UmbUfmTruncateFilterApi implements UmbUfmFilterApi { +class UmbUfmTruncateFilterApi extends UmbUfmFilterBase { filter(str: string, length: number, tail?: string) { if (tail === 'false') tail = ''; if (tail === 'true') tail = '…'; @@ -8,8 +8,6 @@ class UmbUfmTruncateFilterApi implements UmbUfmFilterApi { return str.slice(0, length).trim() + tail; } - - destroy() {} } export { UmbUfmTruncateFilterApi as api }; diff --git a/src/packages/ufm/filters/uppercase.filter.ts b/src/packages/ufm/filters/uppercase.filter.ts index 033c35ede9..76af187aa9 100644 --- a/src/packages/ufm/filters/uppercase.filter.ts +++ b/src/packages/ufm/filters/uppercase.filter.ts @@ -1,11 +1,9 @@ -import type { UmbUfmFilterApi } from '@umbraco-cms/backoffice/extension-registry'; +import { UmbUfmFilterBase } from '../types.js'; -class UmbUfmUppercaseFilterApi implements UmbUfmFilterApi { - filter(str: string) { - return str.toLocaleUpperCase(); +class UmbUfmUppercaseFilterApi extends UmbUfmFilterBase { + filter(str?: string) { + return str?.toLocaleUpperCase(); } - - destroy() {} } export { UmbUfmUppercaseFilterApi as api }; diff --git a/src/packages/ufm/filters/word-limit.filter.ts b/src/packages/ufm/filters/word-limit.filter.ts index b457932285..8924386638 100644 --- a/src/packages/ufm/filters/word-limit.filter.ts +++ b/src/packages/ufm/filters/word-limit.filter.ts @@ -1,12 +1,10 @@ -import type { UmbUfmFilterApi } from '@umbraco-cms/backoffice/extension-registry'; +import { UmbUfmFilterBase } from '../types.js'; -class UmbUfmWordLimitFilterApi implements UmbUfmFilterApi { +class UmbUfmWordLimitFilterApi extends UmbUfmFilterBase { filter(str: string, limit: number) { - const words = str.split(/\s+/); - return words.length > limit ? words.slice(0, limit).join(' ') : str; + const words = str?.split(/\s+/) ?? []; + return limit && words.length > limit ? words.slice(0, limit).join(' ') : str; } - - destroy() {} } export { UmbUfmWordLimitFilterApi as api }; diff --git a/src/packages/ufm/index.ts b/src/packages/ufm/index.ts index 42b3ed554d..470da0f695 100644 --- a/src/packages/ufm/index.ts +++ b/src/packages/ufm/index.ts @@ -1,2 +1,3 @@ -export * from './plugins/index.js'; +export * from './types.js'; export * from './components/index.js'; +export * from './plugins/index.js'; diff --git a/src/packages/ufm/types.ts b/src/packages/ufm/types.ts new file mode 100644 index 0000000000..449e4078c5 --- /dev/null +++ b/src/packages/ufm/types.ts @@ -0,0 +1,7 @@ +import type { UmbUfmFilterApi } from '@umbraco-cms/backoffice/extension-registry'; + +export abstract class UmbUfmFilterBase implements UmbUfmFilterApi { + abstract filter(...args: Array): string | undefined | null; + destroy() {} +} + From f1084c6d34f52708f7827d5e2232d713ffe633b1 Mon Sep 17 00:00:00 2001 From: leekelleher Date: Mon, 9 Sep 2024 17:02:06 +0100 Subject: [PATCH 8/9] Adds UFM global context This moves the `UmbExtensionsApiInitializer` code from the `ufm-render` and `ufm-element-base` components to the context, for optimization. --- .../ufm/components/ufm-element-base.ts | 42 +++----- .../ufm-render/ufm-render.element.ts | 63 ++---------- src/packages/ufm/contexts/index.ts | 1 + src/packages/ufm/contexts/manifest.ts | 8 ++ src/packages/ufm/contexts/ufm.context.ts | 96 +++++++++++++++++++ src/packages/ufm/index.ts | 1 + src/packages/ufm/manifests.ts | 3 +- src/packages/ufm/plugins/marked-ufm.test.ts | 2 +- src/packages/ufm/types.ts | 1 - 9 files changed, 129 insertions(+), 88 deletions(-) create mode 100644 src/packages/ufm/contexts/index.ts create mode 100644 src/packages/ufm/contexts/manifest.ts create mode 100644 src/packages/ufm/contexts/ufm.context.ts diff --git a/src/packages/ufm/components/ufm-element-base.ts b/src/packages/ufm/components/ufm-element-base.ts index 02e4c4b2ef..524d281f81 100644 --- a/src/packages/ufm/components/ufm-element-base.ts +++ b/src/packages/ufm/components/ufm-element-base.ts @@ -1,14 +1,10 @@ +import { UMB_UFM_CONTEXT } from '../contexts/ufm.context.js'; import { nothing, property, state } from '@umbraco-cms/backoffice/external/lit'; -import { UmbExtensionsApiInitializer } from '@umbraco-cms/backoffice/extension-api'; -import { umbExtensionsRegistry } from '@umbraco-cms/backoffice/extension-registry'; import { UmbLitElement } from '@umbraco-cms/backoffice/lit-element'; -import type { ManifestUfmFilter } from '@umbraco-cms/backoffice/extension-registry'; -import type { UmbExtensionApiInitializer } from '@umbraco-cms/backoffice/extension-api'; // eslint-disable-next-line local-rules/enforce-element-suffix-on-element-class-name export abstract class UmbUfmElementBase extends UmbLitElement { - #filterFuncArgs?: Array<{ key: string; args: Array }>; - #functions?: Record) => string>; + #filterFuncArgs?: Array<{ alias: string; args: Array }>; @property() public set filters(value: string | undefined) { @@ -18,8 +14,8 @@ export abstract class UmbUfmElementBase extends UmbLitElement { ?.split('|') .filter((item) => item) .map((item) => { - const [key, ...args] = item.split(':').map((x) => x.trim()); - return { key, args }; + const [alias, ...args] = item.split(':').map((x) => x.trim()); + return { alias, args }; }); } public get filters(): string | undefined { @@ -30,37 +26,25 @@ export abstract class UmbUfmElementBase extends UmbLitElement { @state() value?: unknown; - @state() - private _initialized = false; + #ufmContext?: typeof UMB_UFM_CONTEXT.TYPE; constructor() { super(); - // TODO: [LK] Review if this could be initialized elsewhere (upwards, in a context), - // as it's called mulitple times, and often not all the filters are loaded. - new UmbExtensionsApiInitializer(this, umbExtensionsRegistry, 'ufmFilter', [], undefined, (controllers) => { - this.#functions = Object.fromEntries( - controllers - .map((controller) => { - const ctrl = controller as unknown as UmbExtensionApiInitializer; - if (!ctrl.manifest || !ctrl.api) return []; - return [ctrl.manifest.meta.alias, ctrl.api.filter]; - }) - .filter((x) => x), - ); - - this._initialized = true; + this.consumeContext(UMB_UFM_CONTEXT, (ufmContext) => { + this.#ufmContext = ufmContext; }); } override render() { - if (!this._initialized) return nothing; + if (!this.#ufmContext) return nothing; + let values = Array.isArray(this.value) ? this.value : [this.value]; - if (this.#functions && this.#filterFuncArgs) { + if (this.#filterFuncArgs) { for (const item of this.#filterFuncArgs) { - const func = this.#functions[item.key]; - if (func) { - values = values.map((value) => func(value, ...item.args)); + const filter = this.#ufmContext.getFilterByAlias(item.alias); + if (filter) { + values = values.map((value) => filter(value, ...item.args)); } } } diff --git a/src/packages/ufm/components/ufm-render/ufm-render.element.ts b/src/packages/ufm/components/ufm-render/ufm-render.element.ts index 49f75f770b..6bf24160e5 100644 --- a/src/packages/ufm/components/ufm-render/ufm-render.element.ts +++ b/src/packages/ufm/components/ufm-render/ufm-render.element.ts @@ -1,43 +1,8 @@ -import type { UfmPlugin } from '../../plugins/marked-ufm.plugin.js'; -import { ufm } from '../../plugins/marked-ufm.plugin.js'; +import { UMB_UFM_CONTEXT } from '../../contexts/ufm.context.js'; import { UmbUfmRenderContext } from './ufm-render.context.js'; import { css, customElement, nothing, property, unsafeHTML, until } from '@umbraco-cms/backoffice/external/lit'; -import { DOMPurify } from '@umbraco-cms/backoffice/external/dompurify'; -import { Marked } from '@umbraco-cms/backoffice/external/marked'; -import { UmbExtensionsApiInitializer } from '@umbraco-cms/backoffice/extension-api'; -import { umbExtensionsRegistry } from '@umbraco-cms/backoffice/extension-registry'; import { UmbLitElement } from '@umbraco-cms/backoffice/lit-element'; import { UmbTextStyles } from '@umbraco-cms/backoffice/style'; -import type { ManifestUfmComponent } from '@umbraco-cms/backoffice/extension-registry'; -import type { UmbExtensionApiInitializer } from '@umbraco-cms/backoffice/extension-api'; - -const UmbDomPurify = DOMPurify(window); -const UmbDomPurifyConfig: DOMPurify.Config = { - USE_PROFILES: { html: true }, - CUSTOM_ELEMENT_HANDLING: { - tagNameCheck: /^(?:ufm|umb|uui)-.*$/, - attributeNameCheck: /.+/, - allowCustomizedBuiltInElements: false, - }, -}; - -UmbDomPurify.addHook('afterSanitizeAttributes', function (node) { - // set all elements owning target to target=_blank - if ('target' in node) { - node.setAttribute('target', '_blank'); - } -}); - -export const UmbMarked = new Marked({ - async: true, - gfm: true, - breaks: true, - hooks: { - postprocess: (markup) => { - return UmbDomPurify.sanitize(markup, UmbDomPurifyConfig) as string; - }, - }, -}); const elementName = 'umb-ufm-render'; @@ -59,27 +24,13 @@ export class UmbUfmRenderElement extends UmbLitElement { return this.#context.getValue(); } + #ufmContext?: typeof UMB_UFM_CONTEXT.TYPE; + constructor() { super(); - // TODO: [LK] Review if this could be initialized elsewhere (upwards, in a context), as it's called mulitple times. - new UmbExtensionsApiInitializer(this, umbExtensionsRegistry, 'ufmComponent', [], undefined, (controllers) => { - UmbMarked.use( - ufm( - controllers - .map((controller) => { - const ctrl = controller as unknown as UmbExtensionApiInitializer; - if (!ctrl.manifest || !ctrl.api) return; - return { - alias: ctrl.manifest.alias, - marker: ctrl.manifest.meta.marker, - render: ctrl.api.render, - }; - }) - .filter((x) => x) as Array, - ), - ); - this.requestUpdate('markdown'); + this.consumeContext(UMB_UFM_CONTEXT, (ufmContext) => { + this.#ufmContext = ufmContext; }); } @@ -88,8 +39,8 @@ export class UmbUfmRenderElement extends UmbLitElement { } async #renderMarkdown() { - if (!this.markdown) return null; - const markup = !this.inline ? await UmbMarked.parse(this.markdown) : await UmbMarked.parseInline(this.markdown); + if (!this.#ufmContext || !this.markdown) return null; + const markup = await this.#ufmContext.parse(this.markdown, this.inline); return markup ? unsafeHTML(markup) : nothing; } diff --git a/src/packages/ufm/contexts/index.ts b/src/packages/ufm/contexts/index.ts new file mode 100644 index 0000000000..dcc7961ddc --- /dev/null +++ b/src/packages/ufm/contexts/index.ts @@ -0,0 +1 @@ +export * from './ufm.context.js'; diff --git a/src/packages/ufm/contexts/manifest.ts b/src/packages/ufm/contexts/manifest.ts new file mode 100644 index 0000000000..98c747273f --- /dev/null +++ b/src/packages/ufm/contexts/manifest.ts @@ -0,0 +1,8 @@ +import type { ManifestGlobalContext } from '@umbraco-cms/backoffice/extension-registry'; + +export const manifest: ManifestGlobalContext = { + type: 'globalContext', + alias: 'Umb.GlobalContext.Ufm', + name: 'UFM Context', + api: () => import('./ufm.context.js'), +}; diff --git a/src/packages/ufm/contexts/ufm.context.ts b/src/packages/ufm/contexts/ufm.context.ts new file mode 100644 index 0000000000..4a8512fc41 --- /dev/null +++ b/src/packages/ufm/contexts/ufm.context.ts @@ -0,0 +1,96 @@ +import { ufm } from '../plugins/marked-ufm.plugin.js'; +import type { UfmPlugin } from '../plugins/marked-ufm.plugin.js'; +import { DOMPurify } from '@umbraco-cms/backoffice/external/dompurify'; +import { Marked } from '@umbraco-cms/backoffice/external/marked'; +import { UmbArrayState } from '@umbraco-cms/backoffice/observable-api'; +import { UmbContextBase } from '@umbraco-cms/backoffice/class-api'; +import { UmbContextToken } from '@umbraco-cms/backoffice/context-api'; +import { UmbExtensionsApiInitializer } from '@umbraco-cms/backoffice/extension-api'; +import { umbExtensionsRegistry } from '@umbraco-cms/backoffice/extension-registry'; +import type { ManifestUfmFilter, ManifestUfmComponent } from '@umbraco-cms/backoffice/extension-registry'; +import type { UmbControllerHost } from '@umbraco-cms/backoffice/controller-api'; +import type { UmbExtensionApiInitializer } from '@umbraco-cms/backoffice/extension-api'; + +const UmbDomPurify = DOMPurify(window); +const UmbDomPurifyConfig: DOMPurify.Config = { + USE_PROFILES: { html: true }, + CUSTOM_ELEMENT_HANDLING: { + tagNameCheck: /^(?:ufm|umb|uui)-.*$/, + attributeNameCheck: /.+/, + allowCustomizedBuiltInElements: false, + }, +}; + +UmbDomPurify.addHook('afterSanitizeAttributes', function (node) { + // set all elements owning target to target=_blank + if ('target' in node) { + node.setAttribute('target', '_blank'); + } +}); + +export const UmbMarked = new Marked({ + async: true, + gfm: true, + breaks: true, + hooks: { + postprocess: (markup) => { + return UmbDomPurify.sanitize(markup, UmbDomPurifyConfig) as string; + }, + }, +}); + +type UmbUfmFilterType = { + alias: string; + filter: ((...args: Array) => string | undefined | null) | undefined; +}; + +export class UmbUfmContext extends UmbContextBase { + #filters = new UmbArrayState([], (x) => x.alias); + public readonly filters = this.#filters.asObservable(); + + constructor(host: UmbControllerHost) { + super(host, UMB_UFM_CONTEXT); + + new UmbExtensionsApiInitializer(this, umbExtensionsRegistry, 'ufmComponent', [], undefined, (controllers) => { + UmbMarked.use( + ufm( + controllers + .map((controller) => { + const ctrl = controller as unknown as UmbExtensionApiInitializer; + if (!ctrl.manifest || !ctrl.api) return; + return { + alias: ctrl.manifest.alias, + marker: ctrl.manifest.meta.marker, + render: ctrl.api.render, + }; + }) + .filter((x) => x) as Array, + ), + ); + }); + + new UmbExtensionsApiInitializer(this, umbExtensionsRegistry, 'ufmFilter', [], undefined, (controllers) => { + const filters = controllers + .map((controller) => { + const ctrl = controller as unknown as UmbExtensionApiInitializer; + if (!ctrl.manifest || !ctrl.api) return null; + return { alias: ctrl.manifest.meta.alias, filter: ctrl.api.filter }; + }) + .filter((x) => x) as Array; + + this.#filters.setValue(filters); + }); + } + + public getFilterByAlias(alias: string) { + return this.#filters.getValue().find((x) => x.alias === alias)?.filter; + } + + public async parse(markdown: string, inline: boolean) { + return !inline ? await UmbMarked.parse(markdown) : await UmbMarked.parseInline(markdown); + } +} + +export const UMB_UFM_CONTEXT = new UmbContextToken('UmbUfmContext'); + +export { UmbUfmContext as api }; diff --git a/src/packages/ufm/index.ts b/src/packages/ufm/index.ts index 470da0f695..56ba93470c 100644 --- a/src/packages/ufm/index.ts +++ b/src/packages/ufm/index.ts @@ -1,3 +1,4 @@ export * from './types.js'; export * from './components/index.js'; +export * from './contexts/index.js'; export * from './plugins/index.js'; diff --git a/src/packages/ufm/manifests.ts b/src/packages/ufm/manifests.ts index 860406d686..930ae37b3a 100644 --- a/src/packages/ufm/manifests.ts +++ b/src/packages/ufm/manifests.ts @@ -1,5 +1,6 @@ +import { manifest as ufmContext } from './contexts/manifest.js'; import { manifests as ufmComponents } from './components/manifests.js'; import { manifests as ufmFilters } from './filters/manifests.js'; import type { ManifestTypes } from '@umbraco-cms/backoffice/extension-registry'; -export const manifests: Array = [...ufmComponents, ...ufmFilters]; +export const manifests: Array = [ufmContext, ...ufmComponents, ...ufmFilters]; diff --git a/src/packages/ufm/plugins/marked-ufm.test.ts b/src/packages/ufm/plugins/marked-ufm.test.ts index bdf4c6cd6c..71a2177ddb 100644 --- a/src/packages/ufm/plugins/marked-ufm.test.ts +++ b/src/packages/ufm/plugins/marked-ufm.test.ts @@ -1,6 +1,6 @@ import { expect } from '@open-wc/testing'; import { ufm } from './marked-ufm.plugin.js'; -import { UmbMarked } from '../index.js'; +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'; diff --git a/src/packages/ufm/types.ts b/src/packages/ufm/types.ts index 449e4078c5..94c6966cc1 100644 --- a/src/packages/ufm/types.ts +++ b/src/packages/ufm/types.ts @@ -4,4 +4,3 @@ export abstract class UmbUfmFilterBase implements UmbUfmFilterApi { abstract filter(...args: Array): string | undefined | null; destroy() {} } - From 43da4eaee9e9b584561c323746876cafe54b39ec Mon Sep 17 00:00:00 2001 From: Jacob Overgaard <752371+iOvergaard@users.noreply.github.com> Date: Wed, 11 Sep 2024 09:13:52 +0200 Subject: [PATCH 9/9] feat: evaluate the truncate value to make sure its a string and has a length, otherwise the function might return "..." leading other filters to be confused --- src/packages/ufm/filters/truncate.filter.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/src/packages/ufm/filters/truncate.filter.ts b/src/packages/ufm/filters/truncate.filter.ts index 922122acdf..34360ae41d 100644 --- a/src/packages/ufm/filters/truncate.filter.ts +++ b/src/packages/ufm/filters/truncate.filter.ts @@ -2,6 +2,7 @@ import { UmbUfmFilterBase } from '../types.js'; class UmbUfmTruncateFilterApi extends UmbUfmFilterBase { filter(str: string, length: number, tail?: string) { + if (typeof str !== 'string' || !str.length) return str; if (tail === 'false') tail = ''; if (tail === 'true') tail = '…'; tail = !tail && tail !== '' ? '…' : tail;