diff --git a/src/Umbraco.Web.UI.Client/src/assets/lang/da-dk.ts b/src/Umbraco.Web.UI.Client/src/assets/lang/da-dk.ts
index 0b05cfbb8d..224cf316de 100644
--- a/src/Umbraco.Web.UI.Client/src/assets/lang/da-dk.ts
+++ b/src/Umbraco.Web.UI.Client/src/assets/lang/da-dk.ts
@@ -901,6 +901,7 @@ export default {
avatar: 'Avatar til',
header: 'Overskrift',
systemField: 'system felt',
+ readOnly: 'Skrivebeskyttet',
restore: 'Genskab',
generic: 'Generic',
media: 'Media',
diff --git a/src/Umbraco.Web.UI.Client/src/assets/lang/en-us.ts b/src/Umbraco.Web.UI.Client/src/assets/lang/en-us.ts
index 1c296f2a86..6f53119e14 100644
--- a/src/Umbraco.Web.UI.Client/src/assets/lang/en-us.ts
+++ b/src/Umbraco.Web.UI.Client/src/assets/lang/en-us.ts
@@ -936,6 +936,7 @@ export default {
skipToMenu: 'Skip to menu',
skipToContent: 'Skip to content',
restore: 'Restore',
+ readOnly: 'Read-only',
newVersionAvailable: 'New version available',
},
colors: {
diff --git a/src/Umbraco.Web.UI.Client/src/assets/lang/en.ts b/src/Umbraco.Web.UI.Client/src/assets/lang/en.ts
index 1830171106..27fb01708b 100644
--- a/src/Umbraco.Web.UI.Client/src/assets/lang/en.ts
+++ b/src/Umbraco.Web.UI.Client/src/assets/lang/en.ts
@@ -47,6 +47,7 @@ export default {
notify: 'Notifications',
protect: 'Public Access',
publish: 'Publish',
+ readOnly: 'Read-only',
refreshNode: 'Reload',
remove: 'Remove',
rename: 'Rename',
diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/workspace/components/workspace-split-view/workspace-split-view-variant-selector.element.ts b/src/Umbraco.Web.UI.Client/src/packages/core/workspace/components/workspace-split-view/workspace-split-view-variant-selector.element.ts
index d7be226d99..f32f98bc68 100644
--- a/src/Umbraco.Web.UI.Client/src/packages/core/workspace/components/workspace-split-view/workspace-split-view-variant-selector.element.ts
+++ b/src/Umbraco.Web.UI.Client/src/packages/core/workspace/components/workspace-split-view/workspace-split-view-variant-selector.element.ts
@@ -307,7 +307,9 @@ export class UmbWorkspaceSplitViewVariantSelectorElement extends UmbLitElement {
#renderReadOnlyTag(culture?: string | null) {
if (!culture) return nothing;
- return this.#isReadOnly(culture) ? html`Read-only` : nothing;
+ return this.#isReadOnly(culture)
+ ? html`${this.localize.term('general_readOnly')}`
+ : nothing;
}
#renderSplitViewButton(variant: UmbDocumentVariantOptionModel) {
diff --git a/src/Umbraco.Web.UI.Client/src/packages/language/app-language-select/app-language-select.element.ts b/src/Umbraco.Web.UI.Client/src/packages/language/app-language-select/app-language-select.element.ts
index 940bd928a5..4b76521814 100644
--- a/src/Umbraco.Web.UI.Client/src/packages/language/app-language-select/app-language-select.element.ts
+++ b/src/Umbraco.Web.UI.Client/src/packages/language/app-language-select/app-language-select.element.ts
@@ -2,8 +2,18 @@ import { UmbLanguageCollectionRepository } from '../collection/index.js';
import type { UmbLanguageDetailModel } from '../types.js';
import { type UmbAppLanguageContext, UMB_APP_LANGUAGE_CONTEXT } from '../global-contexts/index.js';
import type { UUIMenuItemEvent, UUIPopoverContainerElement } from '@umbraco-cms/backoffice/external/uui';
-import { css, html, customElement, state, repeat, ifDefined, query } from '@umbraco-cms/backoffice/external/lit';
+import {
+ css,
+ html,
+ customElement,
+ state,
+ repeat,
+ ifDefined,
+ query,
+ nothing,
+} from '@umbraco-cms/backoffice/external/lit';
import { UmbLitElement } from '@umbraco-cms/backoffice/lit-element';
+import { UMB_CURRENT_USER_CONTEXT } from '@umbraco-cms/backoffice/current-user';
@customElement('umb-app-language-select')
export class UmbAppLanguageSelectElement extends UmbLitElement {
@@ -16,6 +26,9 @@ export class UmbAppLanguageSelectElement extends UmbLitElement {
@state()
private _appLanguage?: UmbLanguageDetailModel;
+ @state()
+ private _appLanguageIsReadOnly = false;
+
@state()
private _isOpen = false;
@@ -23,6 +36,12 @@ export class UmbAppLanguageSelectElement extends UmbLitElement {
#appLanguageContext?: UmbAppLanguageContext;
#languagesObserver?: any;
+ #currentUserAllowedLanguages?: Array;
+ #currentUserHasAccessToAllLanguages?: boolean;
+
+ @state()
+ _disallowedLanguages: Array = [];
+
constructor() {
super();
@@ -30,6 +49,29 @@ export class UmbAppLanguageSelectElement extends UmbLitElement {
this.#appLanguageContext = instance;
this.#observeAppLanguage();
});
+
+ this.consumeContext(UMB_CURRENT_USER_CONTEXT, (context) => {
+ this.observe(context.languages, (languages) => {
+ this.#currentUserAllowedLanguages = languages;
+ this.#checkForLanguageAccess();
+ });
+
+ this.observe(context.hasAccessToAllLanguages, (hasAccessToAllLanguages) => {
+ this.#currentUserHasAccessToAllLanguages = hasAccessToAllLanguages;
+ this.#checkForLanguageAccess();
+ });
+ });
+ }
+
+ #checkForLanguageAccess() {
+ // find all disallowed languages
+ this._disallowedLanguages = this._languages?.filter((language) => {
+ if (this.#currentUserHasAccessToAllLanguages) {
+ return false;
+ }
+
+ return !this.#currentUserAllowedLanguages?.includes(language.unique);
+ });
}
async #observeAppLanguage() {
@@ -38,6 +80,10 @@ export class UmbAppLanguageSelectElement extends UmbLitElement {
this.observe(this.#appLanguageContext.appLanguage, (language) => {
this._appLanguage = language;
});
+
+ this.observe(this.#appLanguageContext.appLanguageReadOnlyState.isReadOnly, (isReadOnly) => {
+ this._appLanguageIsReadOnly = isReadOnly;
+ });
}
async #observeLanguages() {
@@ -46,6 +92,7 @@ export class UmbAppLanguageSelectElement extends UmbLitElement {
// TODO: listen to changes
if (data) {
this._languages = data.items;
+ this.#checkForLanguageAccess();
}
}
@@ -83,7 +130,11 @@ export class UmbAppLanguageSelectElement extends UmbLitElement {
#renderTrigger() {
return html``;
}
@@ -98,13 +149,25 @@ export class UmbAppLanguageSelectElement extends UmbLitElement {
label=${ifDefined(language.name)}
@click-label=${this.#onLabelClick}
data-unique=${ifDefined(language.unique)}
- ?active=${language.unique === this._appLanguage?.unique}>
+ ?active=${language.unique === this._appLanguage?.unique}>
+ ${this.#isLanguageReadOnly(language.unique) ? this.#renderReadOnlyTag(language.unique) : nothing}
+
`,
)}
`;
}
+ #isLanguageReadOnly(culture?: string) {
+ if (!culture) return false;
+ return this._disallowedLanguages.find((language) => language.unique === culture) ? true : false;
+ }
+
+ #renderReadOnlyTag(culture?: string) {
+ if (!culture) return nothing;
+ return html`${this.localize.term('general_readOnly')}`;
+ }
+
static override styles = [
css`
:host {
diff --git a/src/Umbraco.Web.UI.Client/src/packages/language/global-contexts/app-language.context.ts b/src/Umbraco.Web.UI.Client/src/packages/language/global-contexts/app-language.context.ts
index 6a0d4b7d8b..bb6da03b19 100644
--- a/src/Umbraco.Web.UI.Client/src/packages/language/global-contexts/app-language.context.ts
+++ b/src/Umbraco.Web.UI.Client/src/packages/language/global-contexts/app-language.context.ts
@@ -6,30 +6,35 @@ import type { UmbControllerHost } from '@umbraco-cms/backoffice/controller-api';
import { UmbContextBase } from '@umbraco-cms/backoffice/class-api';
import type { UmbApi } from '@umbraco-cms/backoffice/extension-api';
import { UMB_AUTH_CONTEXT } from '@umbraco-cms/backoffice/auth';
+import { UmbReadOnlyStateManager } from '@umbraco-cms/backoffice/utils';
+import { UMB_CURRENT_USER_CONTEXT } from '@umbraco-cms/backoffice/current-user';
// TODO: Make a store for the App Languages.
// TODO: Implement default language end-point, in progress at backend team, so we can avoid getting all languages.
export class UmbAppLanguageContext extends UmbContextBase implements UmbApi {
- #languageCollectionRepository: UmbLanguageCollectionRepository;
#languages = new UmbArrayState([], (x) => x.unique);
- moreThanOneLanguage = this.#languages.asObservablePart((x) => x.length > 1);
#appLanguage = new UmbObjectState(undefined);
- appLanguage = this.#appLanguage.asObservable();
+ public readonly appLanguage = this.#appLanguage.asObservable();
+ public readonly appLanguageCulture = this.#appLanguage.asObservablePart((x) => x?.unique);
- appLanguageCulture = this.#appLanguage.asObservablePart((x) => x?.unique);
+ public readonly appLanguageReadOnlyState = new UmbReadOnlyStateManager(this);
- appDefaultLanguage = createObservablePart(this.#languages.asObservable(), (languages) =>
+ public readonly appDefaultLanguage = createObservablePart(this.#languages.asObservable(), (languages) =>
languages.find((language) => language.isDefault),
);
- getAppCulture() {
- return this.#appLanguage.getValue()?.unique;
- }
+ public readonly moreThanOneLanguage = this.#languages.asObservablePart((x) => x.length > 1);
+
+ #languageCollectionRepository = new UmbLanguageCollectionRepository(this);
+ #currentUserAllowedLanguages: Array = [];
+ #currentUserHasAccessToAllLanguages = false;
+
+ #readOnlyStateIdentifier = 'UMB_LANGUAGE_PERMISSION_';
+ #localStorageKey = 'umb:appLanguage';
constructor(host: UmbControllerHost) {
super(host, UMB_APP_LANGUAGE_CONTEXT);
- this.#languageCollectionRepository = new UmbLanguageCollectionRepository(this);
// 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) => {
@@ -38,12 +43,46 @@ export class UmbAppLanguageContext extends UmbContextBase
this.#observeLanguages();
});
});
+
+ this.consumeContext(UMB_CURRENT_USER_CONTEXT, (context) => {
+ this.observe(context.languages, (languages) => {
+ this.#currentUserAllowedLanguages = languages || [];
+ this.#setIsReadOnly();
+ });
+
+ this.observe(context.hasAccessToAllLanguages, (hasAccessToAllLanguages) => {
+ this.#currentUserHasAccessToAllLanguages = hasAccessToAllLanguages || false;
+ this.#setIsReadOnly();
+ });
+ });
+ }
+
+ getAppCulture() {
+ return this.#appLanguage.getValue()?.unique;
}
setLanguage(unique: string) {
- const languages = this.#languages.getValue();
- const language = languages.find((x) => x.unique === unique);
+ // clear the previous read-only state
+ const appLanguage = this.#appLanguage.getValue();
+ if (appLanguage?.unique) {
+ this.appLanguageReadOnlyState.removeState(this.#readOnlyStateIdentifier + appLanguage.unique);
+ }
+
+ // find the language
+ const language = this.#findLanguage(unique);
+
+ if (!language) {
+ throw new Error(`Language with unique ${unique} not found`);
+ }
+
+ // set the new language
this.#appLanguage.update(language);
+
+ // store the new language in local storage
+ localStorage.setItem(this.#localStorageKey, language?.unique);
+
+ // set the new read-only state
+ this.#setIsReadOnly();
}
async #observeLanguages() {
@@ -61,6 +100,17 @@ export class UmbAppLanguageContext extends UmbContextBase
}
#initAppLanguage() {
+ // get the selected language from local storage
+ const uniqueFromLocalStorage = localStorage.getItem(this.#localStorageKey);
+
+ if (uniqueFromLocalStorage) {
+ const language = this.#findLanguage(uniqueFromLocalStorage);
+ if (language) {
+ this.setLanguage(language.unique);
+ return;
+ }
+ }
+
const defaultLanguage = this.#languages.getValue().find((x) => x.isDefault);
// TODO: do we always have a default language?
// do we always get the default language on the first request, or could it be on page 2?
@@ -68,6 +118,37 @@ export class UmbAppLanguageContext extends UmbContextBase
if (!defaultLanguage?.unique) return;
this.setLanguage(defaultLanguage.unique);
}
+
+ #findLanguage(unique: string) {
+ return this.#languages.getValue().find((x) => x.unique === unique);
+ }
+
+ #setIsReadOnly() {
+ const appLanguage = this.#appLanguage.getValue();
+
+ if (!appLanguage) {
+ this.appLanguageReadOnlyState.clear();
+ return;
+ }
+
+ const unique = this.#readOnlyStateIdentifier + appLanguage.unique;
+ this.appLanguageReadOnlyState.removeState(unique);
+
+ if (this.#currentUserHasAccessToAllLanguages) {
+ return;
+ }
+
+ const isReadOnly = !this.#currentUserAllowedLanguages.includes(appLanguage.unique);
+
+ if (isReadOnly) {
+ const readOnlyState = {
+ unique,
+ message: 'You do not have permission to edit to this culture',
+ };
+
+ this.appLanguageReadOnlyState.addState(readOnlyState);
+ }
+ }
}
// Default export to enable this as a globalContext extension js: