Skip to content

Commit c158a0d

Browse files
Fix: Block validation (Refactor of context-api proxy from Clipboard Feature) (#18038)
* set data path * use prop setting * move prop * type * clean up * validation messages test * remove import * remove import * refactor property context proxy to property actions scope only * clean up * remove proxy * find block data * clean up block clipboard entry data * check if areas exisists * test with empty area --------- Co-authored-by: Mads Rasmussen <[email protected]>
1 parent 8105a99 commit c158a0d

File tree

10 files changed

+113
-87
lines changed

10 files changed

+113
-87
lines changed

src/Umbraco.Web.UI.Client/src/packages/block/block-grid/clipboard/block/copy/block-grid-to-block-copy-translator.ts

Lines changed: 24 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ export class UmbBlockGridToBlockClipboardCopyPropertyValueTranslator
2424
}
2525

2626
#constructGridBlockValue(propertyValue: UmbBlockGridValueModel): UmbGridBlockClipboardEntryValueModel {
27+
// TODO: investigate if structured can be remove here.
2728
const valueClone = structuredClone(propertyValue);
2829

2930
const gridBlockValue: UmbGridBlockClipboardEntryValueModel = {
@@ -38,17 +39,38 @@ export class UmbBlockGridToBlockClipboardCopyPropertyValueTranslator
3839
#constructBlockValue(propertyValue: UmbBlockGridValueModel): UmbBlockClipboardEntryValueModel {
3940
const gridBlockValue = this.#constructGridBlockValue(propertyValue);
4041

42+
const contentData: typeof gridBlockValue.contentData = [];
43+
const settingsData: typeof gridBlockValue.settingsData = [];
44+
4145
const layout: UmbBlockClipboardEntryValueModel['layout'] = gridBlockValue.layout?.map((gridLayout) => {
46+
const contentDataEntry = gridBlockValue.contentData.find(
47+
(contentData) => contentData.key === gridLayout.contentKey,
48+
);
49+
if (!contentDataEntry) {
50+
throw new Error('No content data found for layout entry');
51+
}
52+
contentData.push(contentDataEntry);
53+
54+
if (gridLayout.settingsKey) {
55+
const settingsDataEntry = gridBlockValue.settingsData.find(
56+
(settingsData) => settingsData.key === gridLayout.settingsKey,
57+
);
58+
if (!settingsDataEntry) {
59+
throw new Error('No settings data found for layout entry');
60+
}
61+
settingsData.push(settingsDataEntry);
62+
}
63+
4264
return {
4365
contentKey: gridLayout.contentKey,
4466
settingsKey: gridLayout.settingsKey,
4567
};
4668
});
4769

4870
return {
49-
contentData: gridBlockValue.contentData,
5071
layout: layout,
51-
settingsData: gridBlockValue.settingsData,
72+
contentData,
73+
settingsData,
5274
};
5375
}
5476
}

src/Umbraco.Web.UI.Client/src/packages/block/block-grid/clipboard/grid-block/copy/block-grid-to-grid-block-copy-translator.ts

Lines changed: 3 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -1,28 +1,17 @@
11
import type { UmbBlockGridValueModel } from '../../../types.js';
22
import { UMB_BLOCK_GRID_PROPERTY_EDITOR_SCHEMA_ALIAS } from '../../../property-editors/constants.js';
33
import type { UmbGridBlockClipboardEntryValueModel } from '../../types.js';
4-
import { forEachBlockLayoutEntryOf } from '../../../utils/index.js';
5-
import { UMB_BLOCK_GRID_MANAGER_CONTEXT } from '../../../context/constants.js';
64
import { UmbControllerBase } from '@umbraco-cms/backoffice/class-api';
75
import type { UmbClipboardCopyPropertyValueTranslator } from '@umbraco-cms/backoffice/clipboard';
86

97
export class UmbBlockGridToGridBlockClipboardCopyPropertyValueTranslator
108
extends UmbControllerBase
119
implements UmbClipboardCopyPropertyValueTranslator<UmbBlockGridValueModel, UmbGridBlockClipboardEntryValueModel>
1210
{
13-
#blockGridManager?: typeof UMB_BLOCK_GRID_MANAGER_CONTEXT.TYPE;
14-
1511
async translate(propertyValue: UmbBlockGridValueModel) {
1612
if (!propertyValue) {
1713
throw new Error('Property value is missing.');
1814
}
19-
20-
this.#blockGridManager = await this.getContext(UMB_BLOCK_GRID_MANAGER_CONTEXT);
21-
22-
return this.#constructGridBlockValue(propertyValue);
23-
}
24-
25-
#constructGridBlockValue(propertyValue: UmbBlockGridValueModel): UmbGridBlockClipboardEntryValueModel {
2615
const valueClone = structuredClone(propertyValue);
2716

2817
const layout = valueClone.layout?.[UMB_BLOCK_GRID_PROPERTY_EDITOR_SCHEMA_ALIAS] ?? undefined;
@@ -33,24 +22,9 @@ export class UmbBlockGridToGridBlockClipboardCopyPropertyValueTranslator
3322
throw new Error('No layouts found.');
3423
}
3524

36-
layout.forEach((layout) => {
37-
// Find sub Blocks and append their data:
38-
forEachBlockLayoutEntryOf(layout, async (entry) => {
39-
const content = this.#blockGridManager!.getContentOf(entry.contentKey);
40-
41-
if (!content) {
42-
throw new Error('No content found');
43-
}
44-
45-
contentData.push(structuredClone(content));
46-
47-
if (entry.settingsKey) {
48-
const settings = this.#blockGridManager!.getSettingsOf(entry.settingsKey);
49-
if (settings) {
50-
settingsData.push(structuredClone(settings));
51-
}
52-
}
53-
});
25+
layout?.forEach((layoutItem) => {
26+
// @ts-expect-error - We are removing the $type property from the layout item
27+
delete layoutItem.$type;
5428
});
5529

5630
const gridBlockValue: UmbGridBlockClipboardEntryValueModel = {

src/Umbraco.Web.UI.Client/src/packages/block/block-grid/components/block-grid-block-inline/block-grid-block-inline.element.ts

Lines changed: 21 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,8 @@ import {
2020
} from '@umbraco-cms/backoffice/extension-api';
2121
import { UmbLanguageItemRepository } from '@umbraco-cms/backoffice/language';
2222
import { UmbTextStyles } from '@umbraco-cms/backoffice/style';
23+
import { UmbDataPathPropertyValueQuery } from '@umbraco-cms/backoffice/validation';
24+
import type { UmbVariantId } from '@umbraco-cms/backoffice/variant';
2325

2426
const apiArgsCreator: UmbApiConstructorArgumentsMethodType<unknown> = (manifest: unknown) => {
2527
return [{ manifest }];
@@ -30,6 +32,7 @@ export class UmbBlockGridBlockInlineElement extends UmbLitElement {
3032
//
3133
#blockContext?: typeof UMB_BLOCK_GRID_ENTRY_CONTEXT.TYPE;
3234
#workspaceContext?: typeof UMB_BLOCK_WORKSPACE_CONTEXT.TYPE;
35+
#variantId: UmbVariantId | undefined;
3336
#contentKey?: string;
3437
#parentUnique?: string | null;
3538
#areaKey?: string | null;
@@ -52,6 +55,9 @@ export class UmbBlockGridBlockInlineElement extends UmbLitElement {
5255
@state()
5356
_inlineProperty?: UmbPropertyTypeModel;
5457

58+
@state()
59+
_inlinePropertyDataPath?: string;
60+
5561
@state()
5662
private _ownerContentTypeName?: string;
5763

@@ -82,7 +88,7 @@ export class UmbBlockGridBlockInlineElement extends UmbLitElement {
8288
UMB_BLOCK_WORKSPACE_ALIAS,
8389
apiArgsCreator,
8490
(permitted, ctrl) => {
85-
const context = ctrl.api as typeof UMB_BLOCK_WORKSPACE_CONTEXT.TYPE;
91+
const context = ctrl.api as typeof UMB_BLOCK_WORKSPACE_CONTEXT.TYPE | undefined;
8692
if (permitted && context) {
8793
// Risky business, cause here we are lucky that it seems to be consumed and set before this is called and there for this is acceptable for now. [NL]
8894
if (this.#parentUnique === undefined || this.#areaKey === undefined) {
@@ -101,6 +107,7 @@ export class UmbBlockGridBlockInlineElement extends UmbLitElement {
101107
this.#workspaceContext.content.structure.contentTypeProperties,
102108
(contentTypeProperties) => {
103109
this._inlineProperty = contentTypeProperties[0];
110+
this.#generatePropertyDataPath();
104111
},
105112
'observeProperties',
106113
);
@@ -116,9 +123,10 @@ export class UmbBlockGridBlockInlineElement extends UmbLitElement {
116123
this.observe(
117124
context.variantId,
118125
async (variantId) => {
126+
this.#variantId = variantId;
127+
this.#generatePropertyDataPath();
119128
if (variantId) {
120129
context.content.setup(this, variantId);
121-
// TODO: Support segment name?
122130
const culture = variantId.culture;
123131
if (culture) {
124132
const languageRepository = new UmbLanguageItemRepository(this);
@@ -142,6 +150,16 @@ export class UmbBlockGridBlockInlineElement extends UmbLitElement {
142150
this.#workspaceContext.load(this.#contentKey);
143151
}
144152

153+
#generatePropertyDataPath() {
154+
if (!this.#variantId || !this._inlineProperty) return;
155+
const property = this._inlineProperty;
156+
this._inlinePropertyDataPath = `$.values[${UmbDataPathPropertyValueQuery({
157+
alias: property.alias,
158+
culture: property.variesByCulture ? this.#variantId!.culture : null,
159+
segment: property.variesBySegment ? this.#variantId!.segment : null,
160+
})}].value`;
161+
}
162+
145163
#expose = () => {
146164
this.#workspaceContext?.expose();
147165
};
@@ -189,6 +207,7 @@ export class UmbBlockGridBlockInlineElement extends UmbLitElement {
189207
return html`<div id="inside">
190208
<umb-property-type-based-property
191209
.property=${this._inlineProperty}
210+
.dataPath=${this._inlinePropertyDataPath ?? ''}
192211
slot="areas"></umb-property-type-based-property>
193212
<umb-block-grid-areas-container slot="areas"></umb-block-grid-areas-container>
194213
</div>`;

src/Umbraco.Web.UI.Client/src/packages/block/block-grid/context/block-grid-entry.context.ts

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { closestColumnSpanOption } from '../utils/index.js';
1+
import { closestColumnSpanOption, forEachBlockLayoutEntryOf } from '../utils/index.js';
22
import type { UmbBlockGridValueModel } from '../types.js';
33
import { UMB_BLOCK_GRID_PROPERTY_EDITOR_SCHEMA_ALIAS, UMB_BLOCK_GRID_PROPERTY_EDITOR_UI_ALIAS } from '../constants.js';
44
import { UMB_BLOCK_GRID_MANAGER_CONTEXT } from './block-grid-manager.context-token.js';
@@ -296,6 +296,22 @@ export class UmbBlockGridEntryContext
296296
const settingsData = settings ? [structuredClone(settings)] : [];
297297
const exposes = expose ? [structuredClone(expose)] : [];
298298

299+
// Find sub Blocks and append their data:
300+
forEachBlockLayoutEntryOf(layout, async (entry) => {
301+
const content = this._manager!.getContentOf(entry.contentKey);
302+
if (!content) {
303+
throw new Error('No content found');
304+
}
305+
contentData.push(structuredClone(content));
306+
307+
if (entry.settingsKey) {
308+
const settings = this._manager!.getSettingsOf(entry.settingsKey);
309+
if (settings) {
310+
settingsData.push(structuredClone(settings));
311+
}
312+
}
313+
});
314+
299315
const propertyValue: UmbBlockGridValueModel = {
300316
layout: {
301317
[UMB_BLOCK_GRID_PROPERTY_EDITOR_SCHEMA_ALIAS]: layout ? [structuredClone(layout)] : undefined,

src/Umbraco.Web.UI.Client/src/packages/block/block-grid/property-value-cloner/property-value-cloner-block-grid.cloner.test.ts

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -49,7 +49,6 @@ describe('UmbBlockGridPropertyValueCloner', () => {
4949
{
5050
contentKey: 'content-3',
5151
settingsKey: 'settings-3',
52-
areas: [],
5352
},
5453
],
5554
},

src/Umbraco.Web.UI.Client/src/packages/block/block-grid/property-value-cloner/property-value-cloner-block-grid.cloner.ts

Lines changed: 11 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -19,15 +19,17 @@ export class UmbBlockGridPropertyValueCloner extends UmbBlockPropertyValueCloner
1919
#cloneLayoutEntry = async (layout: UmbBlockGridLayoutModel): Promise<UmbBlockGridLayoutModel> => {
2020
// Clone the specific layout entry:
2121
const entryClone = await this._cloneBlock(layout);
22-
// And then clone the items of its areas:
23-
entryClone.areas = await Promise.all(
24-
entryClone.areas.map(async (area) => {
25-
return {
26-
...area,
27-
items: await Promise.all(area.items.map(this.#cloneLayoutEntry)),
28-
};
29-
}),
30-
);
22+
if (entryClone.areas) {
23+
// And then clone the items of its areas:
24+
entryClone.areas = await Promise.all(
25+
entryClone.areas.map(async (area) => {
26+
return {
27+
...area,
28+
items: await Promise.all(area.items.map(this.#cloneLayoutEntry)),
29+
};
30+
}),
31+
);
32+
}
3133
return entryClone;
3234
};
3335
}

src/Umbraco.Web.UI.Client/src/packages/clipboard/property/context/clipboard.property-context-token.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,5 +2,6 @@ import type { UmbClipboardPropertyContext } from './clipboard.property-context.j
22
import { UmbContextToken } from '@umbraco-cms/backoffice/context-api';
33

44
export const UMB_CLIPBOARD_PROPERTY_CONTEXT = new UmbContextToken<UmbClipboardPropertyContext>(
5+
'UmbPropertyContext',
56
'UmbClipboardPropertyContext',
67
);

src/Umbraco.Web.UI.Client/src/packages/clipboard/property/context/clipboard.property-context.ts

Lines changed: 2 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -15,9 +15,8 @@ import type { UmbControllerHost } from '@umbraco-cms/backoffice/controller-api';
1515
import { UMB_MODAL_MANAGER_CONTEXT } from '@umbraco-cms/backoffice/modal';
1616
import { UMB_PROPERTY_CONTEXT, UmbPropertyValueCloneController } from '@umbraco-cms/backoffice/property';
1717
import { umbExtensionsRegistry } from '@umbraco-cms/backoffice/extension-registry';
18-
import type { ManifestPropertyEditorUi, UmbPropertyEditorUiElement } from '@umbraco-cms/backoffice/property-editor';
18+
import type { ManifestPropertyEditorUi } from '@umbraco-cms/backoffice/property-editor';
1919
import type { UmbEntityUnique } from '@umbraco-cms/backoffice/entity';
20-
import { UMB_CONTEXT_REQUEST_EVENT_TYPE, type UmbContextRequestEvent } from '@umbraco-cms/backoffice/context-api';
2120

2221
/**
2322
* Clipboard context for managing clipboard entries for property values
@@ -29,30 +28,15 @@ export class UmbClipboardPropertyContext extends UmbContextBase<UmbClipboardProp
2928
#init?: Promise<unknown>;
3029

3130
#modalManagerContext?: typeof UMB_MODAL_MANAGER_CONTEXT.TYPE;
32-
#propertyContext?: typeof UMB_PROPERTY_CONTEXT.TYPE;
33-
#hostElement?: Element;
34-
#propertyEditorElement?: UmbPropertyEditorUiElement;
3531

3632
constructor(host: UmbControllerHost) {
3733
super(host, UMB_CLIPBOARD_PROPERTY_CONTEXT);
3834

39-
this.#hostElement = host.getHostElement();
40-
4135
this.#init = Promise.all([
4236
this.consumeContext(UMB_MODAL_MANAGER_CONTEXT, (context) => {
4337
this.#modalManagerContext = context;
4438
}).asPromise(),
45-
46-
this.consumeContext(UMB_PROPERTY_CONTEXT, (context) => {
47-
this.#propertyContext = context;
48-
this.#propertyEditorElement = context.getEditor();
49-
}).asPromise(),
5039
]);
51-
52-
this.#hostElement.addEventListener(
53-
UMB_CONTEXT_REQUEST_EVENT_TYPE,
54-
this.#proxyContextRequest.bind(this) as EventListener,
55-
);
5640
}
5741

5842
/**
@@ -143,7 +127,7 @@ export class UmbClipboardPropertyContext extends UmbContextBase<UmbClipboardProp
143127

144128
const pasteTranslatorManifests = this.getPasteTranslatorManifests(args.propertyEditorUiAlias);
145129
const propertyEditorUiManifest = await this.#findPropertyEditorUiManifest(args.propertyEditorUiAlias);
146-
const config = this.#propertyContext?.getConfig();
130+
const config = (await this.getContext(UMB_PROPERTY_CONTEXT)).getConfig();
147131

148132
const valueResolver = new UmbClipboardPastePropertyValueTranslatorValueResolver(this);
149133

@@ -283,30 +267,6 @@ export class UmbClipboardPropertyContext extends UmbContextBase<UmbClipboardProp
283267

284268
return supportedManifests.length > 0;
285269
}
286-
287-
#proxyContextRequest(event: UmbContextRequestEvent) {
288-
const path = event.composedPath();
289-
290-
// Ignore events from the property editor element so we don't end up in a loop when proxying the requests.
291-
if (path.includes(this.#propertyEditorElement as EventTarget)) {
292-
return;
293-
}
294-
295-
// Proxy all context requests to the property editor element so the clipboard actions, translators and filters
296-
// can consume contexts from property editor root element.
297-
if (this.#propertyEditorElement) {
298-
event.stopImmediatePropagation();
299-
this.#propertyEditorElement.dispatchEvent(event.clone());
300-
}
301-
}
302-
303-
override destroy(): void {
304-
super.destroy();
305-
this.#hostElement?.removeEventListener(
306-
UMB_CONTEXT_REQUEST_EVENT_TYPE,
307-
this.#proxyContextRequest.bind(this) as EventListener,
308-
);
309-
}
310270
}
311271

312272
export { UmbClipboardPropertyContext as api };
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
import { expect } from '@open-wc/testing';
2+
import { UmbValidationMessagesManager } from './validation-messages.manager';
3+
import { UmbObserver } from '@umbraco-cms/backoffice/observable-api';
4+
5+
describe('UmbValidationMessagesManager', () => {
6+
let messages: UmbValidationMessagesManager;
7+
8+
beforeEach(() => {
9+
messages = new UmbValidationMessagesManager();
10+
});
11+
12+
it('knows if it has any messages', () => {
13+
messages.addMessage('server', '$.test', 'test');
14+
15+
expect(messages.getHasAnyMessages()).to.be.true;
16+
});
17+
18+
it('knows if it has any messages of a certain path or descending path', () => {
19+
messages.addMessage('server', `$.values[?(@.id == '123')].value`, 'test');
20+
21+
expect(messages.getHasMessagesOfPathAndDescendant(`$.values[?(@.id == '123')]`)).to.be.true;
22+
});
23+
24+
it('enables you to observe for path or descending path messages', async () => {
25+
messages.addMessage('server', `$.values[?(@.id == '123')].value`, 'test');
26+
27+
const observeable = messages.hasMessagesOfPathAndDescendant(`$.values[?(@.id == '123')]`);
28+
29+
const observer = new UmbObserver(observeable);
30+
const result = await observer.asPromise();
31+
32+
expect(result).to.be.true;
33+
});
34+
});

src/Umbraco.Web.UI.Client/src/packages/core/workspace/submittable/submittable-workspace-context-base.ts

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,6 @@ export abstract class UmbSubmittableWorkspaceContextBase<WorkspaceDataModelType>
1818
// TODO: We could make a base type for workspace modal data, and use this here: As well as a base for the result, to make sure we always include the unique (instead of the object type)
1919
public readonly modalContext?: UmbModalContext<{ preset: object }>;
2020

21-
//public readonly validation = new UmbValidationContext(this);
2221
#validationContexts: Array<UmbValidationController> = [];
2322

2423
/**

0 commit comments

Comments
 (0)