Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
104 commits
Select commit Hold shift + click to select a range
50c5d4e
remove inheritance of readonly state
nielslyngsoe Apr 17, 2026
f67135a
keep rendering edit in read-only mode
nielslyngsoe Apr 21, 2026
dc2b471
INVARIANT variant id as static
nielslyngsoe Apr 22, 2026
49f8aab
parse readonly state, without variant ids as origin is the property r…
nielslyngsoe Apr 22, 2026
7351409
stop inheriting read only
nielslyngsoe Apr 22, 2026
e49387c
no need for async
nielslyngsoe Apr 22, 2026
651574d
setup read only state based on user permissions
nielslyngsoe Apr 22, 2026
2cb015f
Merge branch 'main' into v17/hotfix/22472
nielslyngsoe Apr 22, 2026
4679d9d
simplify document-block-property-level-permissions
nielslyngsoe Apr 22, 2026
2ff73f5
make isPermittedForObservableVariant return undefined in bad case
nielslyngsoe Apr 22, 2026
8f1d4c4
revert
nielslyngsoe Apr 22, 2026
cffac81
improve life cycle for extension initializer
nielslyngsoe Apr 22, 2026
e9f85e1
fix and clean-up
nielslyngsoe Apr 22, 2026
d3526d3
clean up
nielslyngsoe Apr 22, 2026
3878bb2
unit test for the actual problem
nielslyngsoe Apr 22, 2026
2811758
clean up
nielslyngsoe Apr 22, 2026
21dd725
clean up
nielslyngsoe Apr 22, 2026
5117e1e
Merge branch 'main' into v17/hotfix/22472
nielslyngsoe Apr 22, 2026
52c29e3
revert logic
nielslyngsoe Apr 22, 2026
1a83d95
transform access context into local controller
nielslyngsoe Apr 23, 2026
7b613a3
re-introduce submit create button
nielslyngsoe Apr 24, 2026
443b50b
simplify match
nielslyngsoe Apr 24, 2026
570597b
update js docs
nielslyngsoe Apr 24, 2026
409c098
strict compare on config object level, to cover multiple conditions o…
nielslyngsoe Apr 24, 2026
c5425fe
Revert "transform access context into local controller"
nielslyngsoe Apr 24, 2026
fbd6c61
rename file in manifest
nielslyngsoe Apr 24, 2026
dee32a4
RTE: set manager readOnly
nielslyngsoe Apr 24, 2026
9dcc2e9
set fallback on readOnly
nielslyngsoe Apr 24, 2026
cccfc33
inherit readOnly state when block workspace is invariant
nielslyngsoe Apr 24, 2026
7de9a85
read-only tag for Block Workspace
nielslyngsoe Apr 24, 2026
a218780
make guard fallback reactive
nielslyngsoe Apr 24, 2026
ba80e12
observe readOnly languages
nielslyngsoe Apr 24, 2026
0c55587
no if sentence
nielslyngsoe Apr 24, 2026
d127289
observe fallback for property + name guards
nielslyngsoe Apr 24, 2026
03a6336
prevent cancelled context get to cause problems
nielslyngsoe Apr 24, 2026
ca73e81
revert removal of || this._isReadOnly check for component rendering
nielslyngsoe Apr 24, 2026
41a6bf3
add comment for clarification
nielslyngsoe Apr 24, 2026
c8b08f7
remove style import
nielslyngsoe Apr 24, 2026
83dd3e2
mark as readonly and make js-const
nielslyngsoe Apr 24, 2026
831593c
remove `as const`
nielslyngsoe Apr 24, 2026
2415d72
unit test for reactive fallback feature
nielslyngsoe Apr 24, 2026
d6e5ff1
more guard unit tests
nielslyngsoe Apr 24, 2026
0f2ffb9
more variantId tests
nielslyngsoe Apr 24, 2026
427b32f
move block language access controller to block package
madsrasmussen Apr 27, 2026
c364d0b
Update base-extension-initializer.controller.ts
madsrasmussen Apr 27, 2026
de01efe
fix test
nielslyngsoe Apr 27, 2026
cfe5ea4
improve switch condition
nielslyngsoe Apr 27, 2026
f292972
offset condition
nielslyngsoe Apr 27, 2026
e4c8909
Block Workspace: Add data-mark for acceptance test locator
andr317c Apr 27, 2026
b4e4a6d
apply entity-type to the workspace data-mark
nielslyngsoe Apr 28, 2026
4df3fc7
layout-headline
nielslyngsoe Apr 28, 2026
632b0ae
Updated locator to use new data-mark
andr317c Apr 28, 2026
9bdc070
Updated tests to make them less fragile
andr317c Apr 28, 2026
cd0a8b2
null ctrl alias for constructor initiated observations
nielslyngsoe Apr 28, 2026
f30178e
import directly
nielslyngsoe Apr 28, 2026
7b351b1
do not react to not existing user-data or missing context
nielslyngsoe Apr 28, 2026
24177dc
add comment
nielslyngsoe Apr 28, 2026
323a731
refactor package registration logic
nielslyngsoe Apr 29, 2026
89f5e49
package name for code editor
nielslyngsoe Apr 29, 2026
2572f6f
leave unregistere out
nielslyngsoe Apr 29, 2026
044950e
await load all bundles
nielslyngsoe Apr 29, 2026
22c7e49
move initializer to app element
madsrasmussen Apr 29, 2026
4897373
Batch register extensions with validation
madsrasmussen Apr 29, 2026
87d8cab
remove await on load for extension initializers
nielslyngsoe Apr 29, 2026
586052b
Debounce extension updates and set loaded flag
madsrasmussen Apr 29, 2026
6306f3d
Merge branch 'v17/hotfix/22472' of https://github.com/umbraco/Umbraco…
madsrasmussen Apr 29, 2026
fc93fed
remove unused imports
nielslyngsoe Apr 29, 2026
402e5df
refactor backoffice -> app
nielslyngsoe Apr 29, 2026
4d6b4b1
clean up imports
nielslyngsoe Apr 29, 2026
69258aa
rename comment
nielslyngsoe Apr 29, 2026
ce0f5e7
base extension initializer is loaded update
nielslyngsoe Apr 29, 2026
e65bacb
app loader
nielslyngsoe Apr 29, 2026
d56c57c
embed umbraco-packages
nielslyngsoe Apr 29, 2026
172ea1a
remove lazy loads from dataSourceDataMapper
nielslyngsoe Apr 29, 2026
2174b5f
Merge remote-tracking branch 'origin/release/17.4.0' into v17/hotfix/…
nielslyngsoe Apr 29, 2026
22d1244
revert
nielslyngsoe Apr 29, 2026
e27c16e
enable routes to be undefined
nielslyngsoe Apr 30, 2026
b08e23d
comment
nielslyngsoe Apr 30, 2026
a1620c9
make sure load only calls once
nielslyngsoe Apr 30, 2026
c1f7a37
comments and todos
nielslyngsoe Apr 30, 2026
db5bd9e
destroy consumer if existing
nielslyngsoe Apr 30, 2026
5cd048f
block language access tests
nielslyngsoe Apr 30, 2026
151d96f
load user at the end of loading all package modules
nielslyngsoe Apr 30, 2026
68b19a5
assign symbol for is-trashed observer
nielslyngsoe May 1, 2026
e61e0b6
revert language readonly rules
nielslyngsoe May 1, 2026
1568589
is-trashed context + observation
nielslyngsoe May 1, 2026
efc862d
read-only as view prop for block list
nielslyngsoe May 1, 2026
2a4cdcf
readonly as view prop
nielslyngsoe May 1, 2026
a00d38e
readonly prop for grid,rte,single
nielslyngsoe May 1, 2026
59432bb
Merge branch 'release/17.4.0' into v17/hotfix/22472
nielslyngsoe May 1, 2026
1845a61
Makes helpers more robust by adding a hover step
andr317c May 1, 2026
afa0fab
back out if not available
nielslyngsoe May 1, 2026
715831a
remove unused import
nielslyngsoe May 1, 2026
424a506
fix typescript typings
nielslyngsoe May 1, 2026
62436a4
resolve load promise feedback
nielslyngsoe May 1, 2026
93a2d65
JSDocs for INVARIANT umbVariantId
nielslyngsoe May 1, 2026
41ab7cb
remove type cast
nielslyngsoe May 1, 2026
6037f76
remove trash context for blocks
nielslyngsoe May 1, 2026
ab8e59a
remove unused import
nielslyngsoe May 1, 2026
06fb49f
make unit test only test output
nielslyngsoe May 1, 2026
ef5d95a
Merge branch 'release/17.4.0' into v17/hotfix/22472
lauraneto May 1, 2026
64525c2
specify app loader + acceptance test queries
nielslyngsoe May 1, 2026
f61adcc
improve acceptance test
nielslyngsoe May 1, 2026
a1c6a36
Merge branch 'release/17.4.0' into v17/hotfix/22472
nielslyngsoe May 4, 2026
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
147 changes: 140 additions & 7 deletions src/Umbraco.Web.UI.Client/src/apps/app/app.element.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { onInit } from '../../packages/core/entry-point.js';

Check notice on line 1 in src/Umbraco.Web.UI.Client/src/apps/app/app.element.ts

View check run for this annotation

CodeScene Delta Analysis / CodeScene Code Health Review (release/17.4.0)

✅ Getting better: Overall Code Complexity

The mean cyclomatic complexity decreases from 4.13 to 4.09, threshold = 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 { UmbAppErrorElement } from './app-error.element.js';
import { UmbAppAuthController } from './app-auth.controller.js';
import { UmbAppAuthElement } from './app-auth.element.js';
Expand All @@ -14,17 +14,62 @@
import { pathWithoutBasePath } from '@umbraco-cms/backoffice/router';
import { RuntimeLevelModel } from '@umbraco-cms/backoffice/external/backend-api';
import { UmbContextDebugController } from '@umbraco-cms/backoffice/debug';
import { UmbBundleExtensionInitializer, UmbServerExtensionRegistrator } from '@umbraco-cms/backoffice/extension-api';
import {
UmbBundleExtensionInitializer,
UmbServerExtensionRegistrator,
type ManifestBase,
} from '@umbraco-cms/backoffice/extension-api';
import {
UmbAppEntryPointExtensionInitializer,
umbExtensionsRegistry,
type UmbExtensionManifestKind,
} from '@umbraco-cms/backoffice/extension-registry';
import { firstValueFrom } from '@umbraco-cms/backoffice/external/rxjs';
import { redirectToStoredPath } from '@umbraco-cms/backoffice/utils';
import { umbHttpClient } from '@umbraco-cms/backoffice/http-client';
import { UmbViewContext } from '@umbraco-cms/backoffice/view';

import './app-logo.element.js';
import { UMB_CURRENT_USER_CONTEXT } from '@umbraco-cms/backoffice/current-user';

const CORE_PACKAGES: Array<Promise<{ name: string; extensions: Array<ManifestBase | UmbExtensionManifestKind> }>> = [
import('../../packages/block/umbraco-package.js'),
import('../../packages/clipboard/umbraco-package.js'),
import('../../packages/code-editor/umbraco-package.js'),
import('../../packages/content/umbraco-package.js'),
import('../../packages/data-type/umbraco-package.js'),
import('../../packages/dictionary/umbraco-package.js'),
import('../../packages/documents/umbraco-package.js'),
import('../../packages/embedded-media/umbraco-package.js'),
import('../../packages/extension-insights/umbraco-package.js'),
import('../../packages/health-check/umbraco-package.js'),
import('../../packages/help/umbraco-package.js'),
import('../../packages/language/umbraco-package.js'),
import('../../packages/log-viewer/umbraco-package.js'),
import('../../packages/management-api/umbraco-package.js'),
import('../../packages/markdown-editor/umbraco-package.js'),
import('../../packages/media/umbraco-package.js'),
import('../../packages/members/umbraco-package.js'),
import('../../packages/models-builder/umbraco-package.js'),
import('../../packages/multi-url-picker/umbraco-package.js'),
import('../../packages/packages/umbraco-package.js'),
import('../../packages/performance-profiling/umbraco-package.js'),
import('../../packages/property-editors/umbraco-package.js'),
import('../../packages/publish-cache/umbraco-package.js'),
import('../../packages/relations/umbraco-package.js'),
import('../../packages/rte/umbraco-package.js'),
import('../../packages/settings/umbraco-package.js'),
import('../../packages/static-file/umbraco-package.js'),
import('../../packages/sysinfo/umbraco-package.js'),
import('../../packages/tags/umbraco-package.js'),
import('../../packages/telemetry/umbraco-package.js'),
import('../../packages/templating/umbraco-package.js'),
import('../../packages/tiptap/umbraco-package.js'),
import('../../packages/translation/umbraco-package.js'),
import('../../packages/ufm/umbraco-package.js'),
import('../../packages/umbraco-news/umbraco-package.js'),
import('../../packages/user/umbraco-package.js'),
import('../../packages/webhook/umbraco-package.js'),
];

@customElement('umb-app')
export class UmbAppElement extends UmbLitElement {
Expand Down Expand Up @@ -129,18 +174,22 @@
{
path: '**',
component: () => import('../backoffice/backoffice.element.js'),
guards: [this.#isAuthorizedGuard()],
guards: [this.#isAuthorizedGuard(), this.#loadedGuard()],
},
];

#authContext?: typeof UMB_AUTH_CONTEXT.TYPE;
#serverConnection?: UmbServerConnection;
#authController = new UmbAppAuthController(this);
#bundleInitializer: UmbBundleExtensionInitializer;

#currentUser?: typeof UMB_CURRENT_USER_CONTEXT.TYPE;
#packageModules?: Promise<Array<{ name: string; extensions: Array<ManifestBase | UmbExtensionManifestKind> }>>;

constructor() {
super();

new UmbBundleExtensionInitializer(this, umbExtensionsRegistry);
this.#bundleInitializer = new UmbBundleExtensionInitializer(this, umbExtensionsRegistry);

new UUIIconRegistryEssential().attach(this);

Expand All @@ -149,6 +198,13 @@
new UmbNetworkConnectionStatusManager(this);

new UmbViewContext(this, null);

this.consumeContext(UMB_CURRENT_USER_CONTEXT, (userContext) => {
this.#currentUser = userContext;
if (userContext) {
this.#loadCurrentUser();
}
});
}

override connectedCallback(): void {
Expand All @@ -166,6 +222,21 @@
);
this.#authContext.configureClient(umbHttpClient);

this.observe(
this.#authContext.isAuthorized,
async (isAuthorized) => {
if (isAuthorized === undefined) return;
if (isAuthorized) {
// TODO: Remove dependency on current user context from the app element in future [MR]
this.#loadCurrentUser();
} else {
// TODO: Unregistering all extensions from v.18 [NL]
//void this.#unregisterExtensions();
}
},
null,
);

this.#serverConnection = await new UmbServerConnection(this, this.serverUrl).connect();
new UmbServerContext(this, {
backofficePath: this.backofficePath,
Expand All @@ -178,8 +249,7 @@

// Register public extensions (login extensions)
await new UmbServerExtensionRegistrator(this, umbExtensionsRegistry).registerPublicExtensions();
const initializer = new UmbAppEntryPointExtensionInitializer(this, umbExtensionsRegistry);
await firstValueFrom(initializer.loaded);
new UmbAppEntryPointExtensionInitializer(this, umbExtensionsRegistry);

// Try to initialise the auth flow and get the runtime status
try {
Expand Down Expand Up @@ -238,6 +308,33 @@
await this.#authContext.setInitialState();
}

async #registerExtensions() {
if (this.#packageModules === undefined) {
this.#packageModules = Promise.all(CORE_PACKAGES);
this.#packageModules.then(() => {
this.#loadCurrentUser();
});
}

umbExtensionsRegistry.registerMany((await this.#packageModules).flatMap((modules) => modules.extensions));
}

// TODO (V18): Unregister extensions on sign-out. [NL]
/*
async #unregisterExtensions() {
if (!this.#packageModules) return;
(await this.#packageModules).forEach((packageModule) => {
const aliases = packageModule.extensions.map((extension) => extension.alias);
umbExtensionsRegistry.unregisterMany(aliases);
});
}
*/

#loadCurrentUser() {
if (!this.#currentUser || !this.#packageModules) return;
this.#currentUser.load();
}

#redirect() {
const pathname = pathWithoutBasePath({ start: true, end: false });

Expand Down Expand Up @@ -292,6 +389,26 @@
return () => this.#authController.isAuthorized() ?? false;
}

#loadedGuard(): Guard {
return async () => {
const results = await Promise.allSettled([
this.observe(this.#bundleInitializer?.loaded).asPromise(),
this.#registerExtensions(),
new UmbServerExtensionRegistrator(this, umbExtensionsRegistry).registerPrivateExtensions(),
]);

const result = results.reduce((acc, curr) => acc && curr.status === 'fulfilled', true);
if (result === false) {
this.#errorPage(
'Extensions failed loading, this might be due to a network issue or a server error. Check that extensions registered on the server are valid.',
undefined,
{ headline: 'Failed to load extensions' },
);
}
return result;
};
}

#errorPage(errorMsg: string, error?: unknown, options?: { headline?: string; hideBackButton?: boolean }) {
// Redirect to the error page
this._routes = [
Expand All @@ -312,7 +429,9 @@
}

override render() {
return html`<umb-router-slot id="router-slot" .routes=${this._routes}></umb-router-slot>`;
return html`<umb-router-slot id="router-slot" .routes=${this._routes}
><div id="loader"><uui-loader data-mark="app-router-loader"></uui-loader></div
></umb-router-slot>`;
}

static override styles = css`
Expand All @@ -327,6 +446,20 @@
width: 100%;
height: 100vh;
}

#loader {
display: flex;
height: 100%;
justify-content: center;
align-items: center;
opacity: 0;
animation: fadeIn 240ms forwards;
}
@keyframes fadeIn {
to {
opacity: 1;
}
}
`;
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,6 @@ import { UmbContextBase } from '@umbraco-cms/backoffice/class-api';
import { UmbContextToken } from '@umbraco-cms/backoffice/context-api';
import { UmbExtensionsManifestInitializer } from '@umbraco-cms/backoffice/extension-api';
import { UmbSysinfoRepository } from '@umbraco-cms/backoffice/sysinfo';
import { UMB_AUTH_CONTEXT } from '@umbraco-cms/backoffice/auth';
import { UMB_CURRENT_USER_CONTEXT } from '@umbraco-cms/backoffice/current-user';
import type { ManifestSection } from '@umbraco-cms/backoffice/section';
import type { UmbControllerHost } from '@umbraco-cms/backoffice/controller-api';
Expand All @@ -26,13 +25,7 @@ export class UmbBackofficeContext extends UmbContextBase {
constructor(host: UmbControllerHost) {
super(host, UMB_BACKOFFICE_CONTEXT);

// TODO: We need to ensure this request is called every time the user logs in, but this should be done somewhere across the app and not here [JOV]
this.consumeContext(UMB_AUTH_CONTEXT, (authContext) => {
this.observe(authContext?.isAuthorized, (isAuthorized) => {
if (!isAuthorized) return;
this.#getVersion();
});
});
this.#getVersion();

this.consumeContext(UMB_CURRENT_USER_CONTEXT, (userContext) => {
this.observe(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,52 +5,10 @@ import {
UmbEntryPointExtensionInitializer,
umbExtensionsRegistry,
} from '@umbraco-cms/backoffice/extension-registry';
import { UmbServerExtensionRegistrator } from '@umbraco-cms/backoffice/extension-api';
import { UmbLitElement } from '@umbraco-cms/backoffice/lit-element';
import { UMB_AUTH_CONTEXT } from '@umbraco-cms/backoffice/auth';

import './components/index.js';

const CORE_PACKAGES = [
import('../../packages/block/umbraco-package.js'),
import('../../packages/clipboard/umbraco-package.js'),
import('../../packages/code-editor/umbraco-package.js'),
import('../../packages/content/umbraco-package.js'),
import('../../packages/data-type/umbraco-package.js'),
import('../../packages/dictionary/umbraco-package.js'),
import('../../packages/documents/umbraco-package.js'),
import('../../packages/embedded-media/umbraco-package.js'),
import('../../packages/extension-insights/umbraco-package.js'),
import('../../packages/health-check/umbraco-package.js'),
import('../../packages/help/umbraco-package.js'),
import('../../packages/language/umbraco-package.js'),
import('../../packages/log-viewer/umbraco-package.js'),
import('../../packages/management-api/umbraco-package.js'),
import('../../packages/markdown-editor/umbraco-package.js'),
import('../../packages/media/umbraco-package.js'),
import('../../packages/members/umbraco-package.js'),
import('../../packages/models-builder/umbraco-package.js'),
import('../../packages/multi-url-picker/umbraco-package.js'),
import('../../packages/packages/umbraco-package.js'),
import('../../packages/performance-profiling/umbraco-package.js'),
import('../../packages/property-editors/umbraco-package.js'),
import('../../packages/publish-cache/umbraco-package.js'),
import('../../packages/relations/umbraco-package.js'),
import('../../packages/rte/umbraco-package.js'),
import('../../packages/settings/umbraco-package.js'),
import('../../packages/static-file/umbraco-package.js'),
import('../../packages/sysinfo/umbraco-package.js'),
import('../../packages/tags/umbraco-package.js'),
import('../../packages/telemetry/umbraco-package.js'),
import('../../packages/templating/umbraco-package.js'),
import('../../packages/tiptap/umbraco-package.js'),
import('../../packages/translation/umbraco-package.js'),
import('../../packages/ufm/umbraco-package.js'),
import('../../packages/umbraco-news/umbraco-package.js'),
import('../../packages/user/umbraco-package.js'),
import('../../packages/webhook/umbraco-package.js'),
];

@customElement('umb-backoffice')
export class UmbBackofficeElement extends UmbLitElement {
/**
Expand All @@ -61,33 +19,11 @@ export class UmbBackofficeElement extends UmbLitElement {

constructor() {
super();

new UmbBackofficeContext(this);

new UmbBackofficeEntryPointExtensionInitializer(this, umbExtensionsRegistry);
new UmbEntryPointExtensionInitializer(this, umbExtensionsRegistry);
}

override async firstUpdated() {
// TODO: Move this logic into the Context? [NL]
await this.#extensionsAfterAuth();

// So far local packages are this simple to register, so no need for a manager to do that:
CORE_PACKAGES.forEach(async (packageImport) => {
const packageModule = await packageImport;
umbExtensionsRegistry.registerMany(packageModule.extensions);
});
}

async #extensionsAfterAuth() {
const authContext = await this.getContext(UMB_AUTH_CONTEXT, { preventTimeout: true });
if (!authContext) {
throw new Error('UmbBackofficeElement requires the UMB_AUTH_CONTEXT to be set.');
}
await this.observe(authContext.isAuthorized).asPromise();
await new UmbServerExtensionRegistrator(this, umbExtensionsRegistry).registerPrivateExtensions();
}

override render() {
return html`
<umb-backoffice-header></umb-backoffice-header>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ import type { PageComponent, UmbRoute } from '@umbraco-cms/backoffice/router';
@customElement('umb-backoffice-main')
export class UmbBackofficeMainElement extends UmbLitElement {
@state()
private _routes: Array<UmbRoute> = [];
private _routes?: Array<UmbRoute>;

@state()
private _sections: Array<ManifestSection> = [];
Expand Down Expand Up @@ -82,7 +82,9 @@ export class UmbBackofficeMainElement extends UmbLitElement {
}

override render() {
if (!this._routes.length) return;
if (!this._routes || !this._routes.length) {
return html`<div id="loader"><uui-loader></uui-loader></div>`;
}
return html`<umb-router-slot .routes=${this._routes}></umb-router-slot>`;
}

Expand All @@ -96,6 +98,19 @@ export class UmbBackofficeMainElement extends UmbLitElement {
100% - 60px
); /* 60 => top header height, TODO: Make sure this comes from somewhere so it is maintainable and eventually responsive. */
}

#loader {
display: flex;
justify-content: center;
align-items: center;
opacity: 0;
animation: fadeIn 240ms forwards;
}
@keyframes fadeIn {
to {
opacity: 1;
}
}
`,
];
}
Expand Down
Loading
Loading