Skip to content
Open
Original file line number Diff line number Diff line change
Expand Up @@ -635,7 +635,10 @@ export class UmbBlockGridEntryElement extends UmbLitElement implements UmbProper

#renderCopyToClipboardAction() {
return html`
<uui-button label="Copy to clipboard" look="secondary" @click=${() => this.#context.copyToClipboard()}>
<uui-button
label=${this.localize.term('clipboard_labelForCopyToClipboard')}
look="secondary"
@click=${() => this.#context.copyToClipboard()}>
<uui-icon name="icon-clipboard-copy"></uui-icon>
</uui-button>
`;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -501,9 +501,14 @@ export class UmbBlockListEntryElement extends UmbLitElement implements UmbProper
}

#renderCopyToClipboardAction() {
return html`<uui-button label="Copy to clipboard" look="secondary" @click=${() => this.#copyToClipboard()}>
<uui-icon name="icon-clipboard-copy"></uui-icon>
</uui-button>`;
return html`
<uui-button
label=${this.localize.term('clipboard_labelForCopyToClipboard')}
look="secondary"
@click=${() => this.#copyToClipboard()}>
<uui-icon name="icon-clipboard-copy"></uui-icon>
</uui-button>
`;
}

override render() {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import type { UmbBlockRteLayoutModel } from '../../types.js';
import { UMB_BLOCK_RTE } from '../../constants.js';
import type { UmbBlockRteLayoutModel, UmbBlockRteValueModel } from '../../types.js';
import { UMB_BLOCK_RTE, UMB_BLOCK_RTE_PROPERTY_EDITOR_SCHEMA_ALIAS } from '../../constants.js';
import { UmbBlockRteEntryContext } from '../../context/block-rte-entry.context.js';
import { UmbLitElement } from '@umbraco-cms/backoffice/lit-element';
import { html, css, property, state, customElement, nothing } from '@umbraco-cms/backoffice/external/lit';
Expand All @@ -10,11 +10,13 @@
UmbBlockEditorCustomViewProperties,
} from '@umbraco-cms/backoffice/block-custom-view';
import { stringOrStringArrayContains } from '@umbraco-cms/backoffice/utils';

import '../ref-rte-block/index.js';
import { UmbObserveValidationStateController } from '@umbraco-cms/backoffice/validation';
import { UmbDataPathBlockElementDataQuery } from '@umbraco-cms/backoffice/block';
import type { UmbExtensionElementInitializer } from '@umbraco-cms/backoffice/extension-api';
import { UMB_CLIPBOARD_PROPERTY_CONTEXT } from '@umbraco-cms/backoffice/clipboard';
import { UMB_PROPERTY_DATASET_CONTEXT, UMB_PROPERTY_CONTEXT } from '@umbraco-cms/backoffice/property';

import '../ref-rte-block/index.js';

/**
* @class UmbBlockRteEntryElement
Expand Down Expand Up @@ -95,12 +97,18 @@
this.requestUpdate('_blockViewProps');
}

#clipboardContext?: typeof UMB_CLIPBOARD_PROPERTY_CONTEXT.TYPE;

constructor() {
super();

// We do not have index for RTE Blocks at the moment.
this.#context.setIndex(0);

this.consumeContext(UMB_CLIPBOARD_PROPERTY_CONTEXT, (clipboardContext) => {
this.#clipboardContext = clipboardContext;
});

Check warning on line 111 in src/Umbraco.Web.UI.Client/src/packages/block/block-rte/components/block-rte-entry/block-rte-entry.element.ts

View check run for this annotation

CodeScene Delta Analysis / CodeScene Code Health Review (main)

❌ Getting worse: Large Method

UmbBlockRteEntryElement.constructor increases from 115 to 118 lines of code, threshold = 70. Large functions with many lines of code are generally harder to understand and lower the code health. Avoid adding more lines to this function.
this.observe(
this.#context.showContentEdit,
(showContentEdit) => {
Expand Down Expand Up @@ -242,6 +250,42 @@
this.setAttribute('contenteditable', 'false');
}

async #copyToClipboard() {
if (!this.#clipboardContext) {
console.warn('Clipboard context is not available.');
return;
}

const propertyDatasetContext = await this.getContext(UMB_PROPERTY_DATASET_CONTEXT);
const propertyContext = await this.getContext(UMB_PROPERTY_CONTEXT);
if (!propertyDatasetContext || !propertyContext) {
throw new Error('Could not get required contexts to copy.');
}

const workspaceName = propertyDatasetContext?.getName();
const propertyLabel = propertyContext?.getLabel();
const blockLabel = this.#context.getName();
const entryName = [workspaceName, propertyLabel, blockLabel].filter(Boolean).join(' - ');
const content = this.#context.getContent();
const layout = this.#context.getLayout();
const settings = this.#context.getSettings();
const expose = this.#context.getExpose();
const propertyValue: UmbBlockRteValueModel = {
contentData: content ? [structuredClone(content)] : [],
layout: { [UMB_BLOCK_RTE_PROPERTY_EDITOR_SCHEMA_ALIAS]: layout ? [structuredClone(layout)] : undefined },
settingsData: settings ? [structuredClone(settings)] : [],
expose: expose ? [structuredClone(expose)] : [],
};
const editorUiManifest = propertyContext.getEditorManifest();

this.#clipboardContext.write({
icon: this._icon,
name: entryName,
propertyValue,
propertyEditorUiAlias: editorUiManifest?.alias ?? '',
});
}

Check warning on line 287 in src/Umbraco.Web.UI.Client/src/packages/block/block-rte/components/block-rte-entry/block-rte-entry.element.ts

View check run for this annotation

CodeScene Delta Analysis / CodeScene Code Health Review (main)

❌ New issue: Complex Method

UmbBlockRteEntryElement.copyToClipboard has a cyclomatic complexity of 12, threshold = 9. This function has many conditional statements (e.g. if, for, while), leading to lower code health. Avoid adding more conditionals and code to it without refactoring.

readonly #filterBlockCustomViews = (manifest: ManifestBlockEditorCustomView) => {
const elementTypeAlias = this._contentTypeAlias ?? '';
const isForBlockEditor =
Expand Down Expand Up @@ -291,9 +335,12 @@
}

#renderActionBar() {
return this._showActions
? html`<uui-action-bar>${this.#renderEditAction()}${this.#renderEditSettingsAction()}</uui-action-bar>`
: nothing;
if (!this._showActions) return nothing;
return html`
<uui-action-bar>
${this.#renderEditAction()} ${this.#renderEditSettingsAction()} ${this.#renderCopyToClipboardAction()}
</uui-action-bar>
`;
}

#renderBuiltinBlockView = () => {
Expand Down Expand Up @@ -354,6 +401,18 @@
`;
}

#renderCopyToClipboardAction() {
if (!this.#clipboardContext) return nothing;
return html`
<uui-button
label=${this.localize.term('clipboard_labelForCopyToClipboard')}
look="secondary"
@click=${() => this.#copyToClipboard()}>
<uui-icon name="icon-clipboard-copy"></uui-icon>
</uui-button>
`;
}

override render() {
return this.#renderBlock();
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,21 +1,31 @@
import type { UmbBlockRteLayoutModel, UmbBlockRteTypeModel, UmbBlockRteValueModel } from '../types.js';
import {
UMB_BLOCK_RTE_WORKSPACE_MODAL,
type UmbBlockRteWorkspaceOriginData,
} from '../workspace/block-rte-workspace.modal-token.js';
import { UMB_BLOCK_RTE_WORKSPACE_MODAL } from '../workspace/block-rte-workspace.modal-token.js';

Check warning on line 1 in src/Umbraco.Web.UI.Client/src/packages/block/block-rte/context/block-rte-entries.context.ts

View check run for this annotation

CodeScene Delta Analysis / CodeScene Code Health Review (main)

❌ New issue: Overall Code Complexity

This module has a mean cyclomatic complexity of 4.09 across 11 functions. The mean complexity threshold is 4. This file has many conditional statements (e.g. if, for, while) across its implementation, leading to lower code health. Avoid adding more conditionals.
import type { UmbBlockRteLayoutModel, UmbBlockRteTypeModel } from '../types.js';
import type { UmbBlockRteWorkspaceOriginData } from '../workspace/block-rte-workspace.modal-token.js';
import { UMB_BLOCK_RTE_MANAGER_CONTEXT } from './block-rte-manager.context-token.js';
import { UmbBooleanState } from '@umbraco-cms/backoffice/observable-api';
import {
UmbClipboardPastePropertyValueTranslatorValueResolver,
UMB_CLIPBOARD_PROPERTY_CONTEXT,
} from '@umbraco-cms/backoffice/clipboard';
import { UmbModalRouteRegistrationController } from '@umbraco-cms/backoffice/router';
import { UMB_BLOCK_CATALOGUE_MODAL, UmbBlockEntriesContext } from '@umbraco-cms/backoffice/block';
import { UMB_PROPERTY_CONTEXT } from '@umbraco-cms/backoffice/property';
import type { UmbBlockDataModel } from '@umbraco-cms/backoffice/block';
import { UmbBooleanState } from '@umbraco-cms/backoffice/observable-api';
import type { UmbControllerHost } from '@umbraco-cms/backoffice/controller-api';
import { UmbModalRouteRegistrationController } from '@umbraco-cms/backoffice/router';
import type { UmbPropertyEditorRteValueType } from '@umbraco-cms/backoffice/rte';

/**
* Copied from the 'rte' package to avoid a circular dependency.
* @internal
*/
const UMB_BLOCK_RTE_PROPERTY_EDITOR_SCHEMA_ALIAS = 'Umbraco.RichText';

/**
* Copied from the 'tiptap' package to avoid a circular dependency.
* @internal
*/
const UMB_BLOCK_RTE_PROPERTY_EDITOR_UI_ALIAS = 'Umb.PropertyEditorUi.Tiptap';

export class UmbBlockRteEntriesContext extends UmbBlockEntriesContext<
typeof UMB_BLOCK_RTE_MANAGER_CONTEXT,
typeof UMB_BLOCK_RTE_MANAGER_CONTEXT.TYPE,
Expand All @@ -33,34 +43,92 @@

new UmbModalRouteRegistrationController(this, UMB_BLOCK_CATALOGUE_MODAL)
.addAdditionalPath('_catalogue/:view')
.onSetup((routingInfo) => {
.onSetup(async (routingInfo) => {
await this._retrieveManager;
if (!this._manager) return false;

const clipboardContext = await this.getContext(UMB_CLIPBOARD_PROPERTY_CONTEXT);
if (!clipboardContext) {
throw new Error('Clipboard context not found');
}

const pasteTranslatorManifests = clipboardContext.getPasteTranslatorManifests(
UMB_BLOCK_RTE_PROPERTY_EDITOR_UI_ALIAS,
);

// TODO: consider moving some of this logic to the clipboard property context
const propertyContext = await this.getContext(UMB_PROPERTY_CONTEXT);
if (!propertyContext) {
throw new Error('Property context not found');
}

const config = propertyContext.getConfig();
const valueResolver = new UmbClipboardPastePropertyValueTranslatorValueResolver(this);

return {
data: {
blocks: this._manager?.getBlockTypes() ?? [],
blockGroups: [],
openClipboard: routingInfo.view === 'clipboard',
clipboardFilter: async (clipboardEntryDetail) => {
const hasSupportedPasteTranslator = clipboardContext.hasSupportedPasteTranslator(
pasteTranslatorManifests,
clipboardEntryDetail.values,
);

if (!hasSupportedPasteTranslator) {
return false;
}

const pasteTranslator = await valueResolver.getPasteTranslator(
clipboardEntryDetail.values,
UMB_BLOCK_RTE_PROPERTY_EDITOR_UI_ALIAS,
);

if (pasteTranslator.isCompatibleValue) {
const value = await valueResolver.resolve(
clipboardEntryDetail.values,
UMB_BLOCK_RTE_PROPERTY_EDITOR_UI_ALIAS,
);
return pasteTranslator.isCompatibleValue(value, config);
}

return true;
},
originData: {},
createBlockInWorkspace: true,
},
};
})
.onSubmit(async (value, data) => {
if (value?.create && data) {
const created = await this.create(
value.create.contentElementTypeKey,
// We can parse an empty object, cause the rest will be filled in by others.
{} as any,
{},
data.originData as UmbBlockRteWorkspaceOriginData,
);
if (created) {
this.insert(
created.layout,
created.content,
created.settings,
data.originData as UmbBlockRteWorkspaceOriginData,
);
} else {
throw new Error('Failed to create block');
}
} else if (value?.clipboard && value.clipboard.selection?.length && data) {

Check warning on line 120 in src/Umbraco.Web.UI.Client/src/packages/block/block-rte/context/block-rte-entries.context.ts

View check run for this annotation

CodeScene Delta Analysis / CodeScene Code Health Review (main)

❌ New issue: Complex Conditional

UmbBlockRteEntriesContext.constructor has 1 complex conditionals with 2 branches, threshold = 2. A complex conditional is an expression inside a branch (e.g. if, for, while) which consists of multiple, logical operators such as AND/OR. The more logical operators in an expression, the more severe the code smell.
const clipboardContext = await this.getContext(UMB_CLIPBOARD_PROPERTY_CONTEXT);
if (!clipboardContext) {
throw new Error('Clipboard context not found');
}

const propertyValues = await clipboardContext.readMultiple<UmbPropertyEditorRteValueType>(
value.clipboard.selection,
UMB_BLOCK_RTE_PROPERTY_EDITOR_UI_ALIAS,
);

this.#insertFromRtePropertyValues(propertyValues, data.originData as UmbBlockRteWorkspaceOriginData);

Check warning on line 131 in src/Umbraco.Web.UI.Client/src/packages/block/block-rte/context/block-rte-entries.context.ts

View check run for this annotation

CodeScene Delta Analysis / CodeScene Code Health Review (main)

❌ New issue: Complex Method

UmbBlockRteEntriesContext.constructor has a cyclomatic complexity of 21, threshold = 9. This function has many conditional statements (e.g. if, for, while), leading to lower code health. Avoid adding more conditionals and code to it without refactoring.

Check warning on line 131 in src/Umbraco.Web.UI.Client/src/packages/block/block-rte/context/block-rte-entries.context.ts

View check run for this annotation

CodeScene Delta Analysis / CodeScene Code Health Review (main)

❌ New issue: Bumpy Road Ahead

UmbBlockRteEntriesContext.constructor has 2 blocks with nested conditional logic. Any nesting of 2 or deeper is considered. Threshold is 2 blocks per function. The Bumpy Road code smell is a function that contains multiple chunks of nested conditional logic. The deeper the nesting and the more bumps, the lower the code health.
}
})
.observeRouteBuilder((routeBuilder) => {
Expand Down Expand Up @@ -137,20 +205,46 @@
this._manager?.deleteLayoutElement(contentKey);
}

protected async _insertFromPropertyValue(value: UmbBlockRteValueModel, originData: UmbBlockRteWorkspaceOriginData) {
const layoutEntries = value.layout[UMB_BLOCK_RTE_PROPERTY_EDITOR_SCHEMA_ALIAS];
async #insertFromRtePropertyValues(
values: Array<UmbPropertyEditorRteValueType>,
originData: UmbBlockRteWorkspaceOriginData,
) {
for (const value of values) {
originData = await this.#insertFromRtePropertyValue(value, originData);
}
}

async #insertFromRtePropertyValue(
value: UmbPropertyEditorRteValueType,
originData: UmbBlockRteWorkspaceOriginData,
): Promise<UmbBlockRteWorkspaceOriginData> {
if (!value.blocks) {
throw new Error('No blocks found in property value');
}

const layoutEntries = value.blocks.layout[UMB_BLOCK_RTE_PROPERTY_EDITOR_SCHEMA_ALIAS];

if (!layoutEntries) {
throw new Error('No layout entries found');
}

await Promise.all(
layoutEntries.map(async (layoutEntry) => {
this._insertBlockFromPropertyValue(layoutEntry, value, originData);
this._insertBlockFromPropertyValue(layoutEntry, value.blocks!, originData);
// TODO: Missing some way to insert a Block HTML Element into the RTE at the current cursor point. (hopefully the responsibility can be avoided here, but there is some connection missing at this point) [NL]
}),
);

return originData;
}

// This method is required by the base class but is not used for RTE blocks.
// RTE blocks use `#insertFromRtePropertyValue` instead because they expect
// `UmbPropertyEditorRteValueType` (with markup and blocks) rather than `UmbBlockValueType`.
protected override _insertFromPropertyValue(
_value: unknown,
_originData: UmbBlockRteWorkspaceOriginData,
): Promise<UmbBlockRteWorkspaceOriginData> {
throw new Error('Use #insertFromRtePropertyValue for RTE blocks');
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -10,23 +10,32 @@ export class UmbBlockRTEPropertyValueCloner implements UmbPropertyValueCloner<Um
#markupDoc?: Document;

async cloneValue(value: UmbPropertyEditorRteValueType) {
if (value) {
this.#markup = value.markup;
if (!value) return value;

const parser = new DOMParser();
this.#markupDoc = parser.parseFromString(this.#markup, 'text/html');
const result = {} as UmbPropertyEditorRteValueType;

const cloner = new UmbFlatLayoutBlockPropertyValueCloner(UMB_BLOCK_RTE_PROPERTY_EDITOR_SCHEMA_ALIAS, {
contentIdUpdatedCallback: this.#replaceContentKeyInMarkup,
});
const result = {} as UmbPropertyEditorRteValueType;
if (value.blocks) {
if (value.blocks) {
if (value.markup) {
// Full path - parse DOM and update content keys in markup
this.#markup = value.markup;
this.#markupDoc = new DOMParser().parseFromString(this.#markup, 'text/html');

const cloner = new UmbFlatLayoutBlockPropertyValueCloner(UMB_BLOCK_RTE_PROPERTY_EDITOR_SCHEMA_ALIAS, {
contentIdUpdatedCallback: this.#replaceContentKeyInMarkup,
});
result.blocks = await cloner.cloneValue(value.blocks);
result.markup = this.#markup;
} else {
// Fast path - no markup to update, just clone blocks
const cloner = new UmbFlatLayoutBlockPropertyValueCloner(UMB_BLOCK_RTE_PROPERTY_EDITOR_SCHEMA_ALIAS);
result.blocks = await cloner.cloneValue(value.blocks);
result.markup = '';
}
result.markup = this.#markup;
return result;
} else {
result.markup = value.markup ?? '';
}
return value;

return result;
}

#replaceContentKeyInMarkup = (contentKey: string, newContentKey: string) => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -487,9 +487,14 @@ export class UmbBlockSingleEntryElement extends UmbLitElement implements UmbProp
}

#renderCopyToClipboardAction() {
return html`<uui-button label="Copy to clipboard" look="secondary" @click=${() => this.#copyToClipboard()}>
<uui-icon name="icon-clipboard-copy"></uui-icon>
</uui-button>`;
return html`
<uui-button
label=${this.localize.term('clipboard_labelForCopyToClipboard')}
look="secondary"
@click=${() => this.#copyToClipboard()}>
<uui-icon name="icon-clipboard-copy"></uui-icon>
</uui-button>
`;
}

override render() {
Expand Down
Loading
Loading