Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
71 commits
Select commit Hold shift + click to select a range
207c72f
todos
nielslyngsoe Mar 29, 2025
78b3d76
navigation context
nielslyngsoe Mar 29, 2025
af40088
replace raw manifests with view context
nielslyngsoe Mar 29, 2025
982c6b9
Array State has method
nielslyngsoe Mar 29, 2025
7365fc7
rename to hint and much more
nielslyngsoe Mar 29, 2025
d482a21
Notes for later
nielslyngsoe Mar 29, 2025
b492203
correcting one word
nielslyngsoe Mar 29, 2025
7724575
more notes
nielslyngsoe Mar 29, 2025
5d2aab7
update JS Docs
nielslyngsoe Mar 29, 2025
77d546c
Merge remote-tracking branch 'origin/main' into v16/feature/workspace…
nielslyngsoe May 6, 2025
e2d8c13
update tests for getHasOne
nielslyngsoe May 6, 2025
55c8a08
fix context api usage
nielslyngsoe May 6, 2025
62013f2
update code for v.16
nielslyngsoe May 6, 2025
dd83e84
correct test
nielslyngsoe May 6, 2025
4642e0e
export UMB_WORKSPACE_VIEW_CONTEXT
nielslyngsoe May 6, 2025
5f59f4b
minor corrections
nielslyngsoe May 6, 2025
1ba948e
rename to _hintMap
nielslyngsoe May 6, 2025
97533a4
refactor part 1
nielslyngsoe May 8, 2025
adfda51
Merge branch 'main' into v16/feature/workspace-view-navigation-context
nielslyngsoe Aug 11, 2025
61ceb57
update version number in comment
nielslyngsoe Aug 11, 2025
ff46b18
clear method for array states
nielslyngsoe Aug 14, 2025
fa5067c
declare hint import map
nielslyngsoe Aug 14, 2025
47f03e2
mega refactor
nielslyngsoe Aug 14, 2025
42f6863
final corrections for working POC
nielslyngsoe Aug 14, 2025
0408fa0
clean up path logic
nielslyngsoe Aug 14, 2025
536069a
implement scaffold
nielslyngsoe Aug 15, 2025
bebc5ec
propagation and inheritance from view to workspace
nielslyngsoe Aug 15, 2025
9b6a242
separate types from classes
nielslyngsoe Aug 15, 2025
843b889
refactor to view context
nielslyngsoe Aug 15, 2025
26cd519
rename editor navigation context to editor context
nielslyngsoe Aug 15, 2025
4b16be5
propagate removals
nielslyngsoe Aug 15, 2025
0758e5f
clean up notes
nielslyngsoe Aug 15, 2025
45a64fa
Merge branch 'main' into v16/feature/workspace-view-navigation-context
nielslyngsoe Aug 15, 2025
613aae7
Hints for Content Tabs
nielslyngsoe Aug 15, 2025
1d48d6a
use const path
nielslyngsoe Aug 15, 2025
421af86
handle gone parent
nielslyngsoe Aug 15, 2025
2ce17d3
added comments on something to be looked at
nielslyngsoe Aug 15, 2025
0936768
hints context types
nielslyngsoe Aug 15, 2025
4c15a85
contentTypeMergedContainers
nielslyngsoe Aug 15, 2025
d6395d0
lint fixes
nielslyngsoe Aug 19, 2025
4c1df67
public contentTypeMergedContainers
nielslyngsoe Aug 19, 2025
de765be
refactor property structure helper class
nielslyngsoe Aug 19, 2025
3b07192
a few notes for Presets
nielslyngsoe Aug 19, 2025
c275279
set variant ID instead of parsing it to the constructor
nielslyngsoe Aug 20, 2025
8191111
do not inject root to the path
nielslyngsoe Aug 21, 2025
db1d91d
adjust structure manager logic
nielslyngsoe Aug 21, 2025
14292f1
UmbPropertyTypeContainerMergedModel type update
nielslyngsoe Aug 21, 2025
bb934d9
correct mergedContainersOfParentIdAndType
nielslyngsoe Aug 21, 2025
915eeb0
Merge branch 'main' into v16/feature/workspace-view-navigation-context
nielslyngsoe Aug 21, 2025
ae46550
fix lint errors
nielslyngsoe Aug 21, 2025
34d6a1a
fix missing import
nielslyngsoe Aug 21, 2025
240dc36
Update src/Umbraco.Web.UI.Client/src/packages/core/hint/context/hints…
nielslyngsoe Aug 21, 2025
de7689d
Update src/Umbraco.Web.UI.Client/src/packages/content/content/workspa…
nielslyngsoe Aug 21, 2025
b395af8
Update src/Umbraco.Web.UI.Client/src/packages/content/content/workspa…
nielslyngsoe Aug 21, 2025
01885d0
clean up
nielslyngsoe Aug 21, 2025
da63fc9
remove console.log
nielslyngsoe Aug 21, 2025
5cba9a6
fix validation context initialization
nielslyngsoe Aug 21, 2025
d595289
add member workspace view consts
nielslyngsoe Aug 21, 2025
078675b
setup validation badges for member workspace root fields
nielslyngsoe Aug 21, 2025
982a811
declare new exports of core
nielslyngsoe Aug 21, 2025
52c7433
Merge branch 'v16/feature/workspace-view-navigation-context' into v16…
nielslyngsoe Aug 21, 2025
6b40b29
Update src/Umbraco.Web.UI.Client/src/packages/core/validation/control…
nielslyngsoe Aug 22, 2025
4afcdf6
Merge branch 'main' into v16/feature/implement-validation-hints-for-m…
nielslyngsoe Aug 25, 2025
b029e56
add comment
nielslyngsoe Aug 25, 2025
e20b8e6
fix example
nielslyngsoe Aug 25, 2025
534306d
fix comment
nielslyngsoe Aug 25, 2025
7d3ba5f
fix member workspace failed request
nielslyngsoe Aug 25, 2025
4774a05
remove console log
nielslyngsoe Aug 25, 2025
f343007
enable server side validation
nielslyngsoe Aug 25, 2025
3f6c395
fix circlular dependency
nielslyngsoe Aug 25, 2025
49c6da4
Merge branch 'main' into v16/feature/implement-validation-hints-for-m…
nielslyngsoe Aug 27, 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 @@ -46,7 +46,7 @@ export class ExampleHintWorkspaceView extends UmbElementMixin(LitElement) {
} else {
workspace.hints.addOne({
unique: 'exampleHintFromToggleAction',
path: ['Umb.WorkspaceView.Document.Edit', 'root'],
path: ['Umb.WorkspaceView.Document.Edit'],
text: 'Hi',
color: 'invalid',
weight: 100,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ export class UmbContentValidationToHintsManager<
> extends UmbControllerBase {
/*workspace.hints.addOne({
unique: 'exampleHintFromToggleAction',
path: ['Umb.WorkspaceView.Document.Edit', 'root'],
path: ['Umb.WorkspaceView.Document.Edit'],
text: 'Hi',
color: 'invalid',
weight: 100,
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
export * from './components/index.js';
export * from './conditions/index.js';
export * from './property-dataset-property-validator/index.js';
export * from './property-dataset/index.js';
export * from './property-guard-manager/index.js';
export * from './property-value-cloner/property-value-clone.controller.js';
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export * from './propertyDatasetPropertyValidator.controller.js';
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import type { UmbControllerHost } from '@umbraco-cms/backoffice/controller-api';
import { UmbValueValidator, type UmbValueValidatorArgs } from '@umbraco-cms/backoffice/validation';
import { UMB_PROPERTY_DATASET_CONTEXT } from '../property-dataset/property-dataset-context.token';

Check failure on line 3 in src/Umbraco.Web.UI.Client/src/packages/core/property/property-dataset-property-validator/propertyDatasetPropertyValidator.controller.ts

View workflow job for this annotation

GitHub Actions / build

Relative imports should use the ".js" file extension

export interface UmbPropertyDatasetPropertyValidator<ValueType = unknown> extends UmbValueValidatorArgs<ValueType> {
propertyAlias: string;
}

// The Example Workspace Context Controller:
export class UmbPropertyDatasetPropertyValidator<ValueType = unknown> extends UmbValueValidator<ValueType> {

Check failure on line 10 in src/Umbraco.Web.UI.Client/src/packages/core/property/property-dataset-property-validator/propertyDatasetPropertyValidator.controller.ts

View workflow job for this annotation

GitHub Actions / build

Unsafe declaration merging between classes and interfaces
//
#propertyAlias: string;

constructor(host: UmbControllerHost, args: UmbPropertyDatasetPropertyValidator<ValueType>) {
super(host, args);
this.#propertyAlias = args.propertyAlias;

this.consumeContext(UMB_PROPERTY_DATASET_CONTEXT, async (context) => {
this.observe(
await context?.propertyValueByAlias<ValueType>(this.#propertyAlias),
(value) => {
this.value = value;
},
'observeDatasetValue',
);
});
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,3 +3,4 @@ export * from './form-control-validator.controller.js';
export * from './observe-validation-state.controller.js';
export * from './validation-path-translation/index.js';
export * from './validation.controller.js';
export * from './value-validator/index.js';
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export * from './value-validator.controller.js';
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
import { UmbControllerBase } from '@umbraco-cms/backoffice/class-api';
import type { UmbControllerHost } from '@umbraco-cms/backoffice/controller-api';
import { UMB_VALIDATION_CONTEXT } from '../../context/validation.context-token';

Check failure on line 3 in src/Umbraco.Web.UI.Client/src/packages/core/validation/controllers/value-validator/value-validator.controller.ts

View workflow job for this annotation

GitHub Actions / build

Relative imports should use the ".js" file extension
import type { UmbValidator } from '../../interfaces/validator.interface';

Check failure on line 4 in src/Umbraco.Web.UI.Client/src/packages/core/validation/controllers/value-validator/value-validator.controller.ts

View workflow job for this annotation

GitHub Actions / build

Relative imports should use the ".js" file extension
import { UMB_VALIDATION_EMPTY_LOCALIZATION_KEY } from '../../const';

Check failure on line 5 in src/Umbraco.Web.UI.Client/src/packages/core/validation/controllers/value-validator/value-validator.controller.ts

View workflow job for this annotation

GitHub Actions / build

Relative imports should use the ".js" file extension

export interface UmbValueValidatorArgs<ValueType = unknown> {
dataPath?: string;
check?: (value: ValueType) => boolean;
message?: () => string;
navigateToError?: () => void;
}

/**
* UmbValueValidator is a controller that implements the UmbValidator interface.
* It validates a value based on a provided check function, manages validation state,
* and communicates validation messages to the validation context.
* It can also handle navigation to the first invalid element if needed.
*/
export class UmbValueValidator<ValueType = unknown> extends UmbControllerBase implements UmbValidator {
//
#validationContext?: typeof UMB_VALIDATION_CONTEXT.TYPE;
#validationMode: boolean = false;

#dataPath: undefined | string;
#check: (value: ValueType) => boolean;
#message: () => string;
#navigateToError?: () => void;

#isValid: boolean = false;
#value?: ValueType;

set value(value: ValueType | undefined) {
this.#value = value;
this.#checkValidation();
}
get value(): ValueType | undefined {
return this.#value;
}

constructor(host: UmbControllerHost, args: UmbValueValidatorArgs<ValueType>) {
super(host);
this.#dataPath = args.dataPath;
this.#check =
args.check ??
((value: ValueType) => {
return value === undefined || value === null || value === '';
});
this.#message = args.message ?? (() => UMB_VALIDATION_EMPTY_LOCALIZATION_KEY);
this.#navigateToError = args.navigateToError;

this.consumeContext(UMB_VALIDATION_CONTEXT, (context) => {
if (this.#validationContext !== context) {
this.#validationContext?.removeValidator(this);
this.#validationContext = context;
this.#validationContext?.addValidator(this);
this.#checkValidation();
}
});
}

#checkValidation() {
if (!this.#validationMode) return;
// Check validation:
this.#isValid = !this.#check(this.#value as ValueType);

// Update validation message:
if (this.#validationContext) {
if (this.#dataPath) {
if (this.#isValid) {
this.#validationContext.messages.removeMessagesByTypeAndPath('custom', this.#dataPath);
} else {
this.#validationContext.messages.addMessage('custom', this.#dataPath, this.#message());
}
}
}
}

async validate(): Promise<void> {
this.#validationMode = true;
// Validate is called when the validation state of this validator is asked to be fully resolved. Like when user clicks Save & Publish.
// If you need to ask the server then it could be done here, instead of asking the server each time the value changes.
// In this particular example we do not need to do anything, because our validation is represented via a message that we always set no matter the user interaction.
// If we did not like to only to check the Validation State when absolute needed then this method must be implemented.
return this.#checkValidation();
}

get isValid(): boolean {
return this.#isValid;
}

reset(): void {
this.#isValid = false;
this.#validationMode = false;
}

/**
* Focus the first invalid element.
*/
focusFirstInvalidElement(): void {
this.#navigateToError?.();
}

override destroy(): void {
this.#validationContext = undefined;
this.#value = undefined;
super.destroy();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ export class UmbViewContext extends UmbControllerBase {
viewAlias: viewAlias,
});
this.firstHintOfVariant = mergeObservables([this.variantId, this.hints.hints], ([variantId, hints]) => {
// Notice, because we in UI have invariant fields on Variants, then we will accept invariant hints on variants.
if (variantId) {
return hints.find((hint) =>
hint.variantId ? hint.variantId.equal(variantId!) || hint.variantId.isInvariant() : true,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@ import { UmbSubmitWorkspaceAction, UMB_WORKSPACE_CONDITION_ALIAS } from '@umbrac
import { UMB_CONTENT_HAS_PROPERTIES_WORKSPACE_CONDITION } from '@umbraco-cms/backoffice/content';

export const UMB_MEMBER_WORKSPACE_ALIAS = 'Umb.Workspace.Member';
export const UMB_MEMBER_WORKSPACE_VIEW_CONTENT_ALIAS = 'Umb.WorkspaceView.Member.Content';
export const UMB_MEMBER_WORKSPACE_VIEW_MEMBER_ALIAS = 'Umb.WorkspaceView.Member.Member';

const workspace: ManifestWorkspaces = {
type: 'workspace',
Expand Down Expand Up @@ -45,7 +47,7 @@ export const workspaceViews: Array<ManifestWorkspaceView> = [
{
type: 'workspaceView',
kind: 'contentEditor',
alias: 'Umb.WorkspaceView.Member.Content',
alias: UMB_MEMBER_WORKSPACE_VIEW_CONTENT_ALIAS,
name: 'Member Workspace Content View',
weight: 1000,
meta: {
Expand All @@ -65,7 +67,7 @@ export const workspaceViews: Array<ManifestWorkspaceView> = [
},
{
type: 'workspaceView',
alias: 'Umb.WorkspaceView.Member.Member',
alias: UMB_MEMBER_WORKSPACE_VIEW_MEMBER_ALIAS,
name: 'Member Workspace Member View',
js: () => import('./views/member/member-workspace-view-member.element.js'),
weight: 500,
Expand Down
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
import type { UmbMemberDetailRepository } from '../../repository/index.js';
import { UmbMemberValidationRepository, type UmbMemberDetailRepository } from '../../repository/index.js';
import type { UmbMemberDetailModel, UmbMemberVariantModel } from '../../types.js';
import { UmbMemberPropertyDatasetContext } from '../../property-dataset-context/member-property-dataset.context.js';
import { UMB_MEMBER_ENTITY_TYPE, UMB_MEMBER_ROOT_ENTITY_TYPE } from '../../entity.js';
import { UMB_MEMBER_DETAIL_REPOSITORY_ALIAS } from '../../repository/detail/manifests.js';
import { UMB_MEMBER_WORKSPACE_ALIAS } from './manifests.js';
import { UMB_MEMBER_WORKSPACE_ALIAS, UMB_MEMBER_WORKSPACE_VIEW_MEMBER_ALIAS } from './manifests.js';
import { UmbMemberWorkspaceEditorElement } from './member-workspace-editor.element.js';
import { UMB_MEMBER_DETAIL_MODEL_VARIANT_SCAFFOLD } from './constants.js';
import { UmbMemberTypeDetailRepository, type UmbMemberTypeDetailModel } from '@umbraco-cms/backoffice/member-type';
Expand All @@ -14,6 +14,8 @@ import {
import type { UmbControllerHost } from '@umbraco-cms/backoffice/controller-api';
import type { UmbVariantId } from '@umbraco-cms/backoffice/variant';
import { UmbContentDetailWorkspaceContextBase, type UmbContentWorkspaceContext } from '@umbraco-cms/backoffice/content';
import { ensurePathEndsWithSlash } from '@umbraco-cms/backoffice/utils';
import { UmbValueValidator } from '@umbraco-cms/backoffice/validation';

type ContentModel = UmbMemberDetailModel;
type ContentTypeModel = UmbMemberTypeDetailModel;
Expand All @@ -39,8 +41,7 @@ export class UmbMemberWorkspaceContext
workspaceAlias: UMB_MEMBER_WORKSPACE_ALIAS,
detailRepositoryAlias: UMB_MEMBER_DETAIL_REPOSITORY_ALIAS,
contentTypeDetailRepository: UmbMemberTypeDetailRepository,
// TODO: Enable Validation Repository when we have UI for showing validation issues on other tabs. [NL]
//contentValidationRepository: UmbMemberValidationRepository,
contentValidationRepository: UmbMemberValidationRepository,
contentVariantScaffold: UMB_MEMBER_DETAIL_MODEL_VARIANT_SCAFFOLD,
contentTypePropertyName: 'memberType',
});
Expand Down Expand Up @@ -83,6 +84,66 @@ export class UmbMemberWorkspaceContext
},
},
]);

this.#setupValidationCheckForRootProperty('username');
this.#setupValidationCheckForRootProperty('email');
//this.#setupValidationCheckForRootProperty('password');// Only when in create mode.
}

#hintedMsgs: Set<string> = new Set();

// A simple observation function to check for validation issues of a root property and map the validation message to a hint.
#setupValidationCheckForRootProperty(propertyName: string) {
const dataPath = `$.${propertyName}`;
const validator = new UmbValueValidator(this, {
dataPath,
navigateToError: () => {
const router = this.getHostElement().shadowRoot!.querySelector('umb-router-slot')!;
const routerPath = router.absoluteActiveViewPath;
if (routerPath) {
const newPath: string = ensurePathEndsWithSlash(routerPath) + 'invariant/view/info';
// Check that we are still part of the DOM and thereby relevant:
window.history.replaceState(null, '', newPath);
}
},
});
this.observe(
this._data.createObservablePartOfCurrent((data) => {
return data?.[propertyName as unknown as keyof ContentModel];
}),
(value) => {
validator.value = value;
},
`validateObserverFor_${propertyName}`,
);

this.validationContext.messages.debug('member');

// Observe Validation and turn it into hint:
this.observe(
this.validationContext.messages.messagesOfPathAndDescendant(dataPath),
(messages) => {
messages.forEach((message) => {
if (this.#hintedMsgs.has(message.key)) return;

this.hints.addOne({
unique: message.key,
path: [UMB_MEMBER_WORKSPACE_VIEW_MEMBER_ALIAS],
text: '!',
color: 'invalid',
weight: 1000,
});
this.#hintedMsgs.add(message.key);
});
this.#hintedMsgs.forEach((key) => {
if (!messages.some((msg) => msg.key === key)) {
this.#hintedMsgs.delete(key);
this.hints.removeOne(key);
}
});
},
`messageObserverFor_${propertyName}`,
);
}

async create(memberTypeUnique: string) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ import { createExtensionApiByAlias } from '@umbraco-cms/backoffice/extension-reg
@customElement('umb-member-workspace-view-member-info')
export class UmbMemberWorkspaceViewMemberInfoElement extends UmbLitElement implements UmbWorkspaceViewElement {
@state()
private _memberTypeUnique = '';
private _memberTypeUnique?: string;

@state()
private _memberTypeName = '';
Expand Down Expand Up @@ -58,16 +58,21 @@ export class UmbMemberWorkspaceViewMemberInfoElement extends UmbLitElement imple

this.consumeContext(UMB_MEMBER_WORKSPACE_CONTEXT, async (context) => {
this.#workspaceContext = context;
this.observe(this.#workspaceContext?.contentTypeUnique, (unique) => (this._memberTypeUnique = unique || ''));
this.observe(this.#workspaceContext?.contentTypeUnique, async (unique) => {
if (unique === this._memberTypeUnique) return;
this._memberTypeUnique = unique;

if (this._memberTypeUnique) {
const memberType = (await this.#memberTypeItemRepository.requestItems([this._memberTypeUnique])).data?.[0];
if (!memberType) return;
this._memberTypeName = memberType.name;
this._memberTypeIcon = memberType.icon;
}
});
this.observe(this.#workspaceContext?.createDate, (date) => (this._createDate = this.#setDateFormat(date)));
this.observe(this.#workspaceContext?.updateDate, (date) => (this._updateDate = this.#setDateFormat(date)));
this.observe(this.#workspaceContext?.unique, (unique) => (this._unique = unique || ''));
this.observe(this.#workspaceContext?.kind, (kind) => (this._memberKind = kind));

const memberType = (await this.#memberTypeItemRepository.requestItems([this._memberTypeUnique])).data?.[0];
if (!memberType) return;
this._memberTypeName = memberType.name;
this._memberTypeIcon = memberType.icon;
});

createExtensionApiByAlias(this, UMB_SECTION_USER_PERMISSION_CONDITION_ALIAS, [
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import type { UUIBooleanInputEvent } from '@umbraco-cms/backoffice/external/uui'
import './member-workspace-view-member-info.element.js';
import type { UmbInputMemberGroupElement } from '@umbraco-cms/backoffice/member-group';
import { UMB_CURRENT_USER_CONTEXT } from '@umbraco-cms/backoffice/current-user';
import { umbBindToValidation } from '@umbraco-cms/backoffice/validation';

@customElement('umb-member-workspace-view-member')
export class UmbMemberWorkspaceViewMemberElement extends UmbLitElement implements UmbWorkspaceViewElement {
Expand Down Expand Up @@ -88,7 +89,8 @@ export class UmbMemberWorkspaceViewMemberElement extends UmbLitElement implement
name="newPassword"
label=${this.localize.term('user_passwordEnterNew')}
type="password"
@input=${() => this.#onPasswordUpdate()}></uui-input>
@input=${() => this.#onPasswordUpdate()}
${umbBindToValidation(this, '$.password')}></uui-input>
</umb-property-layout>

<umb-property-layout label="Confirm password" mandatory>
Expand Down Expand Up @@ -157,6 +159,7 @@ export class UmbMemberWorkspaceViewMemberElement extends UmbLitElement implement
name="login"
label=${this.localize.term('general_username')}
value=${this._workspaceContext.username}
${umbBindToValidation(this, '$.username', this._workspaceContext.username)}
required
required-message=${this.localize.term('user_loginnameRequired')}
@input=${(e: Event) => this.#onChange('username', (e.target as HTMLInputElement).value)}></uui-input>
Expand All @@ -168,6 +171,7 @@ export class UmbMemberWorkspaceViewMemberElement extends UmbLitElement implement
name="email"
label=${this.localize.term('general_email')}
value=${this._workspaceContext.email}
${umbBindToValidation(this, '$.email', this._workspaceContext.email)}
required
required-message=${this.localize.term('user_emailRequired')}
@input=${(e: Event) => this.#onChange('email', (e.target as HTMLInputElement).value)}></uui-input>
Expand Down
Loading