After getting my head round consuming multiple observables from different contexts and ensuring both of these have values before calling another method/observable that needs these values to be present.
Fetch Current Document Key/GUID
Fetch Current User Key/GUID
Pass these two values to a method on another Context
Sometimes it seems that the context for getting the current document or current user may have not returned back from their observable value before my custom Context is consumed/returned.
Current code
import type { UmbControllerHost } from '@umbraco-cms/backoffice/controller-api';
import { UmbConditionConfigBase, UmbConditionControllerArguments, UmbExtensionCondition } from "@umbraco-cms/backoffice/extension-api";
import { UmbConditionBase } from '@umbraco-cms/backoffice/extension-registry';
import { UMB_ENTITY_CONTEXT, UmbEntityUnique } from '@umbraco-cms/backoffice/entity';
import { CONTENTLOCK_SIGNALR_CONTEXT } from '../globalContexts/contentlock.signalr.context';
import { UMB_CURRENT_USER_CONTEXT } from '@umbraco-cms/backoffice/current-user';
export default class CanShowCommonActionsCondition extends UmbConditionBase<UmbConditionConfigBase> implements UmbExtensionCondition
{
#unique?: UmbEntityUnique;
#currentUserUnique?: string;
constructor(host: UmbControllerHost, args: UmbConditionControllerArguments<UmbConditionConfigBase>) {
super(host, args);
this.consumeContext(UMB_ENTITY_CONTEXT, (entityCtx) => {
this.observe(entityCtx?.unique, (unique) => {
this.#unique = unique;
});
});
this.consumeContext(UMB_CURRENT_USER_CONTEXT, (currentUserCtx) => {
this.observe(currentUserCtx?.currentUser, (currentUser) => {
this.#currentUserUnique = currentUser?.unique;
});
});
this.consumeContext(CONTENTLOCK_SIGNALR_CONTEXT, (signalrCtx) => {
if(!this.#unique) {
console.warn('Unique identifier of document is not available for SignalR context');
return;
}
if(!this.#currentUserUnique) {
console.warn('Current User Unique identifier is not available for SignalR context');
return;
}
if (!signalrCtx) {
console.warn('Content Lock SignalR Context is not available');
return;
}
this.observe(signalrCtx?.userCanSeeCommonActions(this.#unique, this.#currentUserUnique), (canSeeCommonActions) => {
this.permitted = canSeeCommonActions;
});
});
}
}
export const CONTENTLOCK_CAN_SHOW_COMMON_ACTIONS_CONDITION_ALIAS = 'contentlock.condition.canShowCommonActions';
Initial thoughts
My initial thoughts was to change the code so that we do the following:
Consume UMB_ENTITY_CONTEXT
Observe unique value
In the arrow function then…
Consume UMB_CURRENT_USER_CONTEXT
Observe currentUser value
In the arrow function then…
Consume UMB_CURRENT_USER_CONTEXT
Observe method userCanSeeCommonActions() passing the values in
But reading the flow of this above feels a bit weird and dirty perhaps of nesting it like this.
So what is the best way to solve this?
Should I be using something from RXJS itself to help solve this
Is there something in Umbraco that is abstracting RXJS to help with this such as observeMany ?
I can’t use observeMany as I need to wait until both consumeContexts return back with something as far as I know.
So happy and open to suggestions and ideas on how this should be done in the right way
The first thing comes to mind is that you have two events:
The current user changes
The selected entity changes
So if either of these two changes, you need to update your SignalR observe anyway. So what I would do I have a function that gets called when either of these two changes:
export default class CanShowCommonActionsCondition extends UmbConditionBase<UmbConditionConfigBase> implements UmbExtensionCondition
{
#unique?: UmbEntityUnique;
#currentUserUnique?: string;
//Obviously any needs to be the correct type
#signalrContext?: any;
constructor(host: UmbControllerHost, args: UmbConditionControllerArguments<UmbConditionConfigBase>) {
super(host, args);
this.consumeContext(UMB_ENTITY_CONTEXT, (entityCtx) => {
this.observe(entityCtx?.unique, (unique) => {
this.#unique = unique;
this.#updateSignalR();
});
});
this.consumeContext(UMB_CURRENT_USER_CONTEXT, (currentUserCtx) => {
this.observe(currentUserCtx?.currentUser, (currentUser) => {
this.#currentUserUnique = currentUser?.unique;
this.#updateSignalR();
});
});
this.consumeContext(CONTENTLOCK_SIGNALR_CONTEXT, (signalrCtx) => {
if (!signalrCtx) {
console.warn('Content Lock SignalR Context is not available');
return;
}
//Just save the context for later, but don't perform something here yet
this.#signalrContext = signalrCtx;
});
}
#updateSignalR() {
if(!this.#unique) {
console.warn('Unique identifier of document is not available for SignalR context');
return;
}
if(!this.#currentUserUnique) {
console.warn('Current User Unique identifier is not available for SignalR context');
return;
}
if(!this.#signalrContext) {
console.warn('Current User Unique identifier is not available for SignalR context');
return;
}
this.observe(this.#signalrContext.userCanSeeCommonActions(this.#unique, this.#currentUserUnique), (canSeeCommonActions) => {
this.permitted = canSeeCommonActions;
});
}
}
export const CONTENTLOCK_CAN_SHOW_COMMON_ACTIONS_CONDITION_ALIAS = 'contentlock.condition.canShowCommonActions';
Not sure if you need to de-observe something in this instance.
Yes, there is observeMultiple() but as you say, your observables are not immediately available due to your contexts. We had a consumeMultiple() in earlier versions of the Backoffice for contexts, but using contexts, you need to know that the values are initially undefined, so you need some checking back and forth anyway.
I think @Luuk‘s answer cuts it pretty close to what you need.
Whilst you were both replying I was trying stuff out and perhaps this is too complex, but I wanted a 2nd opinion on this please @jacob
If its not the right way or perhaps too complex then I will try out the approach/suggestion from Luuk
import type { UmbControllerHost } from '@umbraco-cms/backoffice/controller-api';
import { UmbConditionConfigBase, UmbConditionControllerArguments, UmbExtensionCondition } from "@umbraco-cms/backoffice/extension-api";
import { UmbConditionBase } from '@umbraco-cms/backoffice/extension-registry';
import { UMB_ENTITY_CONTEXT, UmbEntityContext } from '@umbraco-cms/backoffice/entity';
import ContentLockSignalrContext, { CONTENTLOCK_SIGNALR_CONTEXT } from '../globalContexts/contentlock.signalr.context';
import { UMB_CURRENT_USER_CONTEXT, UmbCurrentUserContext } from '@umbraco-cms/backoffice/current-user';
import { observeMultiple } from '@umbraco-cms/backoffice/observable-api';
import { filter, switchMap } from '@umbraco-cms/backoffice/external/rxjs';
export default class CanShowCommonActionsCondition extends UmbConditionBase<UmbConditionConfigBase> implements UmbExtensionCondition
{
#entityCtx?: UmbEntityContext;
#currentUserCtx?: UmbCurrentUserContext;
#signalrCtx?: ContentLockSignalrContext;
constructor(host: UmbControllerHost, args: UmbConditionControllerArguments<UmbConditionConfigBase>) {
super(host, args);
this.consumeContext(UMB_ENTITY_CONTEXT, (entityCtx) => {
// Store the reference to the context
this.#entityCtx = entityCtx;
// Try to setup observers each time a context is set
this.trySetupObservers();
});
this.consumeContext(UMB_CURRENT_USER_CONTEXT, (currentUserCtx) => {
// Store the reference to the context
this.#currentUserCtx = currentUserCtx;
// Try to setup observers each time a context is set
this.trySetupObservers();
});
this.consumeContext(CONTENTLOCK_SIGNALR_CONTEXT, (signalrCtx) => {
// Store the reference to the context
this.#signalrCtx = signalrCtx;
// Try to setup observers each time a context is set
this.trySetupObservers();
});
}
trySetupObservers(): void {
// Only setup observation when all contexts are available to use
if (!this.#entityCtx || !this.#currentUserCtx || !this.#signalrCtx) {
return;
}
this.observe(
observeMultiple([
this.#entityCtx?.unique,
this.#currentUserCtx?.unique
]).pipe(
// Filter out observable values being emitted where either value is undefined
// No point in checking permissions if we don't have BOTH values
filter(([documentUnique, currentUserUnique]) => !!documentUnique && !!currentUserUnique),
// switchMap takes the [documentUnique, currentUserUnique] pair and creates a NEW observable
switchMap(([documentUnique, currentUserUnique]) =>
// This will return a NEW observable that watches for permission changes
this.#signalrCtx!.userCanSeeCommonActions(documentUnique!, currentUserUnique!)
)
),
(canSeeCommonActions) => {
// The final observable value from the switchMap - userCanSeeCommonActions
this.permitted = canSeeCommonActions;
}
);
}
}
export const CONTENTLOCK_CAN_SHOW_COMMON_ACTIONS_CONDITION_ALIAS = 'contentlock.condition.canShowCommonActions';
That should indeed work. I would give a controller alias to this.observe(observeMultiple(), '_canShowCommonActionsObserver') so you don’t risk starting multiple observers on the same thing. With the alias, any new observers will replace the old ones, so when one of your consumers fires off a new value, it won’t matter what was previously being observed.
Thanks Jacob.
I did not know about giving the observer a name/alias.
I was looking at disctinctUntilChanged filtering method from RxJS last night and was considering using that, but if I understand correctly if I supply an name/alias string like you have shown this would achieve the same thing?
distinctUntilChanged is for the active subscription if values change inside that stream. The observer alias is meant to overwrite the subscription if a new context arrives. Depending on the flow, you might need both, but if your streams are based on Umb*State (like UmbArrayState), and you provide them with a memoize function, then they, too, use distinctUntilChanged, so you don’t need to supply that manually.
Complicated stuff, I know, but your implementation looks solid as it is, especially if based on Umb*State’s.
haha yep sure is starting to melt my mind a little
This is the final code I have - thanks for the suggestions and feedback. I just need to get a minor version out of Content Lock to see if this has definitely fixed the bug that has been reported.
Which seemed to be because of timing issue with how the code was before with certain contexts being available before others.
Solution
import type { UmbControllerHost } from '@umbraco-cms/backoffice/controller-api';
import { UmbConditionConfigBase, UmbConditionControllerArguments, UmbExtensionCondition } from "@umbraco-cms/backoffice/extension-api";
import { UmbConditionBase } from '@umbraco-cms/backoffice/extension-registry';
import { UMB_ENTITY_CONTEXT, UmbEntityContext } from '@umbraco-cms/backoffice/entity';
import ContentLockSignalrContext, { CONTENTLOCK_SIGNALR_CONTEXT } from '../globalContexts/contentlock.signalr.context';
import { UMB_CURRENT_USER_CONTEXT, UmbCurrentUserContext } from '@umbraco-cms/backoffice/current-user';
import { observeMultiple } from '@umbraco-cms/backoffice/observable-api';
import { filter, switchMap } from '@umbraco-cms/backoffice/external/rxjs';
export default class CanShowCommonActionsCondition extends UmbConditionBase<UmbConditionConfigBase> implements UmbExtensionCondition
{
#entityCtx?: UmbEntityContext;
#currentUserCtx?: UmbCurrentUserContext;
#signalrCtx?: ContentLockSignalrContext;
constructor(host: UmbControllerHost, args: UmbConditionControllerArguments<UmbConditionConfigBase>) {
super(host, args);
this.consumeContext(UMB_ENTITY_CONTEXT, (entityCtx) => {
// Store the reference to the context
this.#entityCtx = entityCtx;
// Try to setup observers each time a context is set
this.trySetupObservers();
});
this.consumeContext(UMB_CURRENT_USER_CONTEXT, (currentUserCtx) => {
// Store the reference to the context
this.#currentUserCtx = currentUserCtx;
// Try to setup observers each time a context is set
this.trySetupObservers();
});
this.consumeContext(CONTENTLOCK_SIGNALR_CONTEXT, (signalrCtx) => {
// Store the reference to the context
this.#signalrCtx = signalrCtx;
// Try to setup observers each time a context is set
this.trySetupObservers();
});
}
trySetupObservers(): void {
// Only setup observation when all contexts are available to use
if (!this.#entityCtx || !this.#currentUserCtx || !this.#signalrCtx) {
return;
}
this.observe(
observeMultiple([
this.#entityCtx?.unique,
this.#currentUserCtx?.unique
]).pipe(
// Filter out observable values being emitted where either value is undefined
// No point in checking permissions if we don't have BOTH values
filter(([documentUnique, currentUserUnique]) => !!documentUnique && !!currentUserUnique),
// switchMap takes the [documentUnique, currentUserUnique] pair and creates a NEW observable
switchMap(([documentUnique, currentUserUnique]) => {
// This will return a NEW observable that watches for permission changes
return this.#signalrCtx!.userCanSeeCommonActions(documentUnique!, currentUserUnique!);
})
),
(canSeeCommonActions) => {
// The final observable value from the switchMap - userCanSeeCommonActions
this.permitted = canSeeCommonActions;
},
'_canShowCommonActionsConditionObserver' // Added an alias to the observer - The observer alias is meant to overwrite the subscription if a new context arrives
);
}
}
export const CONTENTLOCK_CAN_SHOW_COMMON_ACTIONS_CONDITION_ALIAS = 'contentlock.condition.canShowCommonActions';