Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
27 commits
Select commit Hold shift + click to select a range
4360516
POC browser title
nielslyngsoe Sep 5, 2025
7e55742
support view-alias as null
nielslyngsoe Sep 7, 2025
f805526
provide tab view contexts
nielslyngsoe Sep 7, 2025
68af124
refactor view context
nielslyngsoe Sep 7, 2025
56edd13
refactor workspace implementation of view context
nielslyngsoe Sep 7, 2025
a0c1940
clean up context + revert title order
nielslyngsoe Sep 7, 2025
4b5b979
Merge branch 'main' into v16/feature/view-context-browser-title
nielslyngsoe Sep 15, 2025
1573c22
view context for section context
nielslyngsoe Sep 15, 2025
cf7027d
update type
nielslyngsoe Sep 15, 2025
32fa5c7
disable and re-active parent views
nielslyngsoe Sep 15, 2025
4f01e27
remove unused import
nielslyngsoe Sep 16, 2025
e83a7ac
remove log
nielslyngsoe Sep 16, 2025
8227963
Implementation of Browser Title
nielslyngsoe Sep 16, 2025
fd0e242
implement more browser titles
nielslyngsoe Sep 16, 2025
82245d9
sort imports
nielslyngsoe Sep 16, 2025
4080bcd
Merge branch 'main' into v16/feature/view-context-browser-title
nielslyngsoe Sep 16, 2025
501534b
remove unused imports
nielslyngsoe Sep 16, 2025
abb1fcf
Merge branch 'main' into v16/feature/view-context-browser-title
nielslyngsoe Sep 17, 2025
a4c1660
use _internal_
nielslyngsoe Sep 17, 2025
5b461fc
lint updates
nielslyngsoe Sep 17, 2025
f452df5
reactive titles
nielslyngsoe Sep 17, 2025
29cb7f5
Merge branch 'main' into v16/feature/view-context-browser-title
nielslyngsoe Sep 17, 2025
6af1c72
fix hints for root tab
nielslyngsoe Sep 17, 2025
9103886
Merge branch 'main' into v16/feature/view-context-browser-title
nielslyngsoe Sep 17, 2025
fa2a1d5
implement use of UmbEntityDetailWorkspaceContextBase
nielslyngsoe Sep 17, 2025
96ab5e1
fix view context lifecycle
nielslyngsoe Sep 17, 2025
c1a01ab
Linting, `import`s tidy-up
leekelleher Sep 18, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -41,10 +41,10 @@ export class ExampleHintWorkspaceView extends UmbElementMixin(LitElement) {
throw new Error('Could not find the workspace');
}

if (workspace.hints.has('exampleHintFromToggleAction')) {
workspace.hints.removeOne('exampleHintFromToggleAction');
if (workspace.view.hints.has('exampleHintFromToggleAction')) {
workspace.view.hints.removeOne('exampleHintFromToggleAction');
} else {
workspace.hints.addOne({
workspace.view.hints.addOne({
unique: 'exampleHintFromToggleAction',
path: ['Umb.WorkspaceView.Document.Edit'],
text: 'Hi',
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import {
UmbRequestReloadChildrenOfEntityEvent,
UmbRequestReloadStructureForEntityEvent,
} from '@umbraco-cms/backoffice/entity-action';
import { UmbViewContext } from '@umbraco-cms/backoffice/view';
import { UMB_ACTION_EVENT_CONTEXT } from '@umbraco-cms/backoffice/action';
import type { Observable } from '@umbraco-cms/backoffice/observable-api';
import type { UmbControllerHost } from '@umbraco-cms/backoffice/controller-api';
Expand Down Expand Up @@ -52,6 +53,8 @@ export abstract class UmbContentTypeWorkspaceContextBase<

public readonly structure: UmbContentTypeStructureManager<DetailModelType>;

public readonly view = new UmbViewContext(this, null);

constructor(host: UmbControllerHost, args: UmbContentTypeWorkspaceContextArgs) {
super(host, args);

Expand All @@ -70,7 +73,9 @@ export abstract class UmbContentTypeWorkspaceContextBase<
this.collection = this.structure.ownerContentTypeObservablePart((data) => data?.collection);

// Keep current data in sync with the owner content type - This is used for the discard changes feature
this.observe(this.structure.ownerContentType, (data) => this._data.setCurrent(data));
this.observe(this.structure.ownerContentType, (data) => this._data.setCurrent(data), null);
this.observe(this.name, (name) => this.view.setBrowserTitle(name), null);
// TODO: sometimes the browserTitle for a parent view is set later than the child is updating. We need to fix this as well enable a parent browser title to be updating on the go. [NL]
}

/**
Expand Down
Original file line number Diff line number Diff line change
@@ -1,13 +1,13 @@
import type { UmbContentTypeCompositionModel, UmbContentTypeModel, UmbContentTypeSortModel } from '../types.js';
import type { UmbContentTypeStructureManager } from '../structure/index.js';
import type { Observable } from '@umbraco-cms/backoffice/external/rxjs';
import type { UmbSubmittableWorkspaceContext } from '@umbraco-cms/backoffice/workspace';
import type { UmbNamableWorkspaceContext, UmbSubmittableWorkspaceContext } from '@umbraco-cms/backoffice/workspace';

export interface UmbContentTypeWorkspaceContext<ContentTypeType extends UmbContentTypeModel = UmbContentTypeModel>
extends UmbSubmittableWorkspaceContext {
extends UmbSubmittableWorkspaceContext,
UmbNamableWorkspaceContext {
readonly IS_CONTENT_TYPE_WORKSPACE_CONTEXT: true;

readonly name: Observable<string | undefined>;
readonly alias: Observable<string | undefined>;
readonly description: Observable<string | undefined>;
readonly icon: Observable<string | undefined>;
Expand All @@ -32,7 +32,4 @@ export interface UmbContentTypeWorkspaceContext<ContentTypeType extends UmbConte

getIcon(): string | undefined;
setIcon(icon: string): void;

getName(): string | undefined;
setName(name: string): void;
}
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,12 @@
import type { UmbContentWorkspaceContext } from './content-workspace-context.interface.js';
import { UmbContentDetailValidationPathTranslator } from './content-detail-validation-path-translator.js';
import { UmbContentValidationToHintsManager } from './content-validation-to-hints.manager.js';
import { appendToFrozenArray, mergeObservables, UmbArrayState } from '@umbraco-cms/backoffice/observable-api';
import {
appendToFrozenArray,
mergeObservables,
observeMultiple,
UmbArrayState,
} from '@umbraco-cms/backoffice/observable-api';
import { firstValueFrom, map } from '@umbraco-cms/backoffice/external/rxjs';
import { umbOpenModal } from '@umbraco-cms/backoffice/modal';
import { UmbContentTypeStructureManager } from '@umbraco-cms/backoffice/content-type';
Expand All @@ -21,14 +26,14 @@
UmbRequestReloadChildrenOfEntityEvent,
UmbRequestReloadStructureForEntityEvent,
} from '@umbraco-cms/backoffice/entity-action';
import { UmbHintContext } from '@umbraco-cms/backoffice/hint';
import { UmbLanguageCollectionRepository } from '@umbraco-cms/backoffice/language';
import {
UmbPropertyValuePresetVariantBuilderController,
UmbVariantPropertyGuardManager,
} from '@umbraco-cms/backoffice/property';
import { UmbSegmentCollectionRepository } from '@umbraco-cms/backoffice/segment';
import { UmbVariantId } from '@umbraco-cms/backoffice/variant';
import { UmbViewContext } from '@umbraco-cms/backoffice/view';
import { UMB_ACTION_EVENT_CONTEXT } from '@umbraco-cms/backoffice/action';
import {
UMB_VALIDATION_CONTEXT,
Expand All @@ -52,7 +57,6 @@
import type { UmbPropertyTypePresetModel, UmbPropertyTypePresetModelTypeModel } from '@umbraco-cms/backoffice/property';
import type { UmbModalToken } from '@umbraco-cms/backoffice/modal';
import type { UmbSegmentCollectionItemModel } from '@umbraco-cms/backoffice/segment';
import type { UmbVariantHint } from '@umbraco-cms/backoffice/hint';

export interface UmbContentDetailWorkspaceContextArgs<
DetailModelType extends UmbContentDetailModel<VariantModelType>,
Expand Down Expand Up @@ -141,8 +145,8 @@

readonly collection: UmbContentCollectionManager;

/* Hints */
readonly hints = new UmbHintContext<UmbVariantHint>(this);
/* View */
readonly view = new UmbViewContext(this, null);

/* Variant Options */
// TODO: Optimize this so it uses either a App Language Context? [NL]
Expand Down Expand Up @@ -221,119 +225,130 @@
this,
this.structure,
this.validationContext,
this.hints,
this.view.hints,
);

this.variantOptions = mergeObservables(
[this.variesByCulture, this.variesBySegment, this.variants, this.languages, this._segments],
([variesByCulture, variesBySegment, variants, languages, segments]) => {
if ((variesByCulture || variesBySegment) === undefined) {
return [];
}

const varies = variesByCulture || variesBySegment;

// No variation
if (!varies) {
return [
{
variant: variants.find((x) => new UmbVariantId(x.culture, x.segment).isInvariant()),
language: languages.find((x) => x.isDefault),
culture: null,
segment: null,
unique: new UmbVariantId().toString(),
} as VariantOptionModelType,
];
}

// Only culture variation
if (variesByCulture && !variesBySegment) {
return languages.map((language) => {
return {
variant: variants.find((x) => x.culture === language.unique),
language,
culture: language.unique,
segment: null,
unique: new UmbVariantId(language.unique).toString(),
} as VariantOptionModelType;
});
}

// Only segment variation
if (!variesByCulture && variesBySegment) {
const invariantCulture = {
variant: variants.find((x) => new UmbVariantId(x.culture, x.segment).isInvariant()),
language: languages.find((x) => x.isDefault),
culture: null,
segment: null,
unique: new UmbVariantId().toString(),
} as VariantOptionModelType;

const segmentsForInvariantCulture = segments.map((segment) => {
return {
variant: variants.find((x) => x.culture === null && x.segment === segment.unique),
language: languages.find((x) => x.isDefault),
segmentInfo: segment,
culture: null,
segment: segment.unique,
unique: new UmbVariantId(null, segment.unique).toString(),
} as VariantOptionModelType;
});

return [invariantCulture, ...segmentsForInvariantCulture] as Array<VariantOptionModelType>;
}

// Culture and segment variation
if (variesByCulture && variesBySegment) {
return languages.flatMap((language) => {
const culture = {
variant: variants.find((x) => x.culture === language.unique),
language,
culture: language.unique,
segment: null,
unique: new UmbVariantId(language.unique).toString(),
} as VariantOptionModelType;

const segmentsForCulture = segments.map((segment) => {
return {
variant: variants.find((x) => x.culture === language.unique && x.segment === segment.unique),
language,
segmentInfo: segment,
culture: language.unique,
segment: segment.unique,
unique: new UmbVariantId(language.unique, segment.unique).toString(),
} as VariantOptionModelType;
});

return [culture, ...segmentsForCulture] as Array<VariantOptionModelType>;
});
}

return [] as Array<VariantOptionModelType>;
},
).pipe(map((options) => options.filter((option) => this._variantOptionsFilter(option))));

this.observe(
this.variantOptions,
(variantOptions) => {
variantOptions.forEach((variantOption) => {
const missingThis = !this.#variantValidationContexts.some((x) => {
const variantId = x.getVariantId();
if (!variantId) return;
return variantId.culture === variantOption.culture && variantId.segment === variantOption.segment;
});
if (missingThis) {
const context = new UmbValidationController(this);
context.inheritFrom(this.validationContext, '$');
context.setVariantId(UmbVariantId.Create(variantOption));
context.autoReport();
this.#variantValidationContexts.push(context);
}
});
},
null,
);

this.observe(
observeMultiple([this.splitView.activeVariantByIndex(0), this.variants]),
([activeVariant, variants]) => {
const variantName = variants.find(
(v) => v.culture === activeVariant?.culture && v.segment === activeVariant?.segment,
)?.name;
this.view.setBrowserTitle(variantName);
},
null,
);

Check warning on line 351 in src/Umbraco.Web.UI.Client/src/packages/content/content/workspace/content-detail-workspace-base.ts

View check run for this annotation

CodeScene Delta Analysis / CodeScene Cloud Delta Analysis (main)

❌ Getting worse: Complex Method

constructor increases in cyclomatic complexity from 21 to 25, 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.
this.observe(
this.varies,
(varies) => {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,22 +1,28 @@
import type { UmbContentWorkspaceViewEditTabElement } from './content-editor-tab.element.js';

Check warning on line 1 in src/Umbraco.Web.UI.Client/src/packages/content/content/workspace/views/edit/content-editor.element.ts

View check run for this annotation

CodeScene Delta Analysis / CodeScene Cloud Delta Analysis (main)

❌ New issue: Overall Code Complexity

This module has a mean cyclomatic complexity of 5.29 across 7 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 { css, html, customElement, state, repeat, nothing } from '@umbraco-cms/backoffice/external/lit';
import { encodeFolderName } from '@umbraco-cms/backoffice/router';
import {
UmbContentTypeContainerStructureHelper,
UMB_PROPERTY_STRUCTURE_WORKSPACE_CONTEXT,
} from '@umbraco-cms/backoffice/content-type';
import { UmbLitElement } from '@umbraco-cms/backoffice/lit-element';
import { UmbTextStyles } from '@umbraco-cms/backoffice/style';
import { UMB_VIEW_CONTEXT, UmbViewContext } from '@umbraco-cms/backoffice/view';
import type {
PageComponent,
UmbRoute,
UmbRouterSlotChangeEvent,
UmbRouterSlotInitEvent,
} from '@umbraco-cms/backoffice/router';
import type {
UmbContentTypeModel,
UmbContentTypeStructureManager,
UmbPropertyTypeContainerMergedModel,
} from '@umbraco-cms/backoffice/content-type';
import {
UmbContentTypeContainerStructureHelper,
UMB_PROPERTY_STRUCTURE_WORKSPACE_CONTEXT,
} from '@umbraco-cms/backoffice/content-type';
import type { UmbRoute, UmbRouterSlotChangeEvent, UmbRouterSlotInitEvent } from '@umbraco-cms/backoffice/router';
import { encodeFolderName } from '@umbraco-cms/backoffice/router';
import { UmbLitElement } from '@umbraco-cms/backoffice/lit-element';
import type { UmbVariantHint } from '@umbraco-cms/backoffice/hint';
import type { UmbWorkspaceViewElement } from '@umbraco-cms/backoffice/workspace';

import './content-editor-tab.element.js';
import type { UmbVariantHint } from '@umbraco-cms/backoffice/hint';
import { UMB_VIEW_CONTEXT, UmbViewContext } from '@umbraco-cms/backoffice/view';

@customElement('umb-content-workspace-view-edit')
export class UmbContentWorkspaceViewEditElement extends UmbLitElement implements UmbWorkspaceViewElement {
Expand All @@ -43,7 +49,7 @@
private _activePath = '';

@state()
private _hintMap: Map<string, UmbVariantHint> = new Map();
private _hintMap: Map<string | null, UmbVariantHint> = new Map();

#tabViewContexts: Array<UmbViewContext> = [];

Expand Down Expand Up @@ -104,9 +110,10 @@
component: () => import('./content-editor-tab.element.js'),
setup: (component) => {
(component as UmbContentWorkspaceViewEditTabElement).containerId = null;
this.#provideViewContext(null, component);
},
});
this.#createViewContext('root');
this.#createViewContext(null, '#general_generic');
}

if (this._tabs.length > 0) {
Expand All @@ -118,9 +125,10 @@
component: () => import('./content-editor-tab.element.js'),
setup: (component) => {
(component as UmbContentWorkspaceViewEditTabElement).containerId = tab.ownerId ?? tab.ids[0];
this.#provideViewContext(path, component);
},
});
this.#createViewContext(path);
this.#createViewContext(path, tabName);
});
}

Expand All @@ -140,11 +148,17 @@
this._routes = routes;
}

#createViewContext(viewAlias: string) {
#createViewContext(viewAlias: string | null, tabName: string) {
if (!this.#tabViewContexts.find((context) => context.viewAlias === viewAlias)) {
const view = new UmbViewContext(this, viewAlias);
this.#tabViewContexts.push(view);

if (viewAlias === null) {
// for the root tab, we need to filter hints, so in this case we do accept everything that is not in a tab: [NL]
view.hints.setPathFilter((paths) => paths[0].includes('tab/') === false);
}

view.setBrowserTitle(tabName);
view.inheritFrom(this.#viewContext);

this.observe(
Expand All @@ -162,13 +176,28 @@
}
}

#currentProvidedView?: UmbViewContext;

#provideViewContext(viewAlias: string | null, component: PageComponent) {
const view = this.#tabViewContexts.find((context) => context.viewAlias === viewAlias);
if (this.#currentProvidedView === view) {
return;
}
this.#currentProvidedView?.unprovide();
if (!view) {
throw new Error(`View context with alias ${viewAlias} not found`);
}
this.#currentProvidedView = view;
view.provideAt(component as any);
}

override render() {
if (!this._routes || !this._tabs) return;
return html`
<umb-body-layout header-fit-height>
${this._routerPath && (this._tabs.length > 1 || (this._tabs.length === 1 && this._hasRootGroups))
? html` <uui-tab-group slot="header">
${this._hasRootGroups && this._tabs.length > 0 ? this.#renderTab('root', '#general_generic') : nothing}
${this._hasRootGroups && this._tabs.length > 0 ? this.#renderTab(null, '#general_generic') : nothing}
${repeat(
this._tabs,
(tab) => tab.name,
Expand All @@ -194,17 +223,18 @@
`;
}

#renderTab(path: string, name: string, index = 0) {
#renderTab(path: string | null, name: string, index = 0) {
const hint = this._hintMap.get(path);
const fullPath = this._routerPath + '/' + path;
const fullPath = this._routerPath + '/' + (path ? path : 'root');
const active =
fullPath === this._activePath ||
(!this._hasRootGroups && index === 0 && this._routerPath + '/' === this._activePath);
(!this._hasRootGroups && index === 0 && this._routerPath + '/' === this._activePath) ||
(this._hasRootGroups && index === 0 && path === null && this._routerPath + '/' === this._activePath);
return html`<uui-tab
label=${this.localize.string(name ?? '#general_unnamed')}
.active=${active}
href=${fullPath}
data-mark="content-tab:${path}"
data-mark="content-tab:${path ?? 'root'}"

Check warning on line 237 in src/Umbraco.Web.UI.Client/src/packages/content/content/workspace/views/edit/content-editor.element.ts

View check run for this annotation

CodeScene Delta Analysis / CodeScene Cloud Delta Analysis (main)

❌ New issue: Complex Method

UmbContentWorkspaceViewEditElement.renderTab has a cyclomatic complexity of 10, 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.
>${hint && !active
? html`<uui-badge slot="extra" .color=${hint.color ?? 'default'} ?attention=${hint.color === 'invalid'}
>${hint.text}</uui-badge
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -211,7 +211,7 @@ export class UmbEntityItemRefElement extends UmbLitElement {
}

:host([drag-placeholder]) {
--uui-color-focus:transparent;
--uui-color-focus: transparent;
}

:host([drag-placeholder])::after {
Expand All @@ -233,7 +233,6 @@ export class UmbEntityItemRefElement extends UmbLitElement {
transition: opacity 50ms 16ms;
opacity: 0;
}

`,
];
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
import type { UmbContextProviderController } from '@umbraco-cms/backoffice/context-api';

export interface UmbHintControllerArgs<HintType extends UmbHint = UmbHint> {
viewAlias?: string;
viewAlias?: string | null;
scaffold?: Partial<HintType>;
}

Expand All @@ -16,10 +16,15 @@
IncomingHintType extends UmbIncomingHintBase = UmbPartialSome<HintType, 'unique' | 'weight' | 'path'>,
> extends UmbControllerBase {
//
#viewAlias?: string;
getViewAlias(): string | undefined {
#viewAlias: string | null;
getViewAlias(): string | null {
return this.#viewAlias;
}
#pathFilter?: (path: Array<string>) => boolean;
setPathFilter(filter: (path: Array<string>) => boolean) {
this.#pathFilter = filter;
}

#scaffold = new UmbObjectState<Partial<HintType>>({});
readonly scaffold = this.#scaffold.asObservable();
#inUnprovidingState?: boolean;
Expand All @@ -43,7 +48,7 @@
constructor(host: UmbControllerHost, args?: UmbHintControllerArgs<HintType>) {
super(host);

this.#viewAlias = args?.viewAlias;
this.#viewAlias = args?.viewAlias ?? null;
if (args?.scaffold) {
this.#scaffold.setValue(args?.scaffold);
}
Expand Down Expand Up @@ -82,7 +87,7 @@
return this.#hints.asObservablePart(fn);
}

descendingHints(viewAlias?: string): Observable<Array<UmbHint> | undefined> {
descendingHints(viewAlias?: string | null): Observable<Array<UmbHint> | undefined> {
if (viewAlias) {
return this.#hints.asObservablePart((hints) => {
return hints.filter((hint) => hint.path[0] === viewAlias);
Expand All @@ -92,7 +97,22 @@
}
}

/**
* @internal
* @param {(path: Array<string>) => boolean} filter - A filter function to filter the hints by their path.
* @returns {Observable<Array<UmbHint> | undefined>} An observable of an array of hints that match the filter.
*/
// eslint-disable-next-line @typescript-eslint/naming-convention
_internal_descendingHintsByFilter(filter: (path: Array<string>) => boolean): Observable<Array<UmbHint> | undefined> {
return this.#hints.asObservablePart((hints) => {
return hints.filter((hint) => filter(hint.path));
});
}

inherit(): void {
if (this.#viewAlias === null && this.#pathFilter === undefined) {
throw new Error('A Hint Controller needs a view alias or pathFilter to be able to inherit from a parent.');
}
this.consumeContext(UMB_HINT_CONTEXT, (parent) => {
this.inheritFrom(parent);
}).skipHost();
Expand All @@ -101,13 +121,24 @@

inheritFrom(parent: UmbHintController | undefined): void {
if (this.#parent === parent) return;
if (this.#viewAlias === null && this.#pathFilter === undefined) {
throw new Error('A Hint Controller needs a view alias or pathFilter to be able to inherit from a parent.');
}
this.#parent = parent;
this.observe(this.#parent?.scaffold, (scaffold) => {
if (scaffold) {
this.#scaffold.update(scaffold as any);
}
});
this.observe(parent?.descendingHints(this.#viewAlias), this.#receiveHints, 'observeParentHints');
if (this.#viewAlias) {
this.observe(parent?.descendingHints(this.#viewAlias), this.#receiveHints, 'observeParentHints');
} else if (this.#pathFilter) {
this.observe(
parent?._internal_descendingHintsByFilter(this.#pathFilter),
this.#receiveHints,
'observeParentHints',
);
}

Check warning on line 141 in src/Umbraco.Web.UI.Client/src/packages/core/hint/context/hints.controller.ts

View check run for this annotation

CodeScene Delta Analysis / CodeScene Cloud Delta Analysis (main)

❌ New issue: Complex Method

UmbHintController.inheritFrom has a cyclomatic complexity of 11, 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.
this.observe(this.hints, this.#propagateHints, 'observeLocalMessages');
}

Expand Down
Loading
Loading