Skip to content
This repository was archived by the owner on Nov 6, 2025. It is now read-only.

Commit e3274d7

Browse files
authored
Merge pull request #2474 from umbraco/v15/feature/disable-readonly-languages-when-unpublishing
Feature: Add read only tag for read only languages in app language select
2 parents f1bedec + 02237d7 commit e3274d7

File tree

6 files changed

+164
-15
lines changed

6 files changed

+164
-15
lines changed

src/assets/lang/da-dk.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -901,6 +901,7 @@ export default {
901901
avatar: 'Avatar til',
902902
header: 'Overskrift',
903903
systemField: 'system felt',
904+
readOnly: 'Skrivebeskyttet',
904905
restore: 'Genskab',
905906
generic: 'Generic',
906907
media: 'Media',

src/assets/lang/en-us.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -936,6 +936,7 @@ export default {
936936
skipToMenu: 'Skip to menu',
937937
skipToContent: 'Skip to content',
938938
restore: 'Restore',
939+
readOnly: 'Read-only',
939940
newVersionAvailable: 'New version available',
940941
},
941942
colors: {

src/assets/lang/en.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,7 @@ export default {
4747
notify: 'Notifications',
4848
protect: 'Public Access',
4949
publish: 'Publish',
50+
readOnly: 'Read-only',
5051
refreshNode: 'Reload',
5152
remove: 'Remove',
5253
rename: 'Rename',

src/packages/core/workspace/components/workspace-split-view/workspace-split-view-variant-selector.element.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -307,7 +307,9 @@ export class UmbWorkspaceSplitViewVariantSelectorElement extends UmbLitElement {
307307

308308
#renderReadOnlyTag(culture?: string | null) {
309309
if (!culture) return nothing;
310-
return this.#isReadOnly(culture) ? html`<uui-tag look="secondary">Read-only</uui-tag>` : nothing;
310+
return this.#isReadOnly(culture)
311+
? html`<uui-tag look="secondary">${this.localize.term('general_readOnly')}</uui-tag>`
312+
: nothing;
311313
}
312314

313315
#renderSplitViewButton(variant: UmbDocumentVariantOptionModel) {

src/packages/language/app-language-select/app-language-select.element.ts

Lines changed: 66 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,18 @@ import { UmbLanguageCollectionRepository } from '../collection/index.js';
22
import type { UmbLanguageDetailModel } from '../types.js';
33
import { type UmbAppLanguageContext, UMB_APP_LANGUAGE_CONTEXT } from '../global-contexts/index.js';
44
import type { UUIMenuItemEvent, UUIPopoverContainerElement } from '@umbraco-cms/backoffice/external/uui';
5-
import { css, html, customElement, state, repeat, ifDefined, query } from '@umbraco-cms/backoffice/external/lit';
5+
import {
6+
css,
7+
html,
8+
customElement,
9+
state,
10+
repeat,
11+
ifDefined,
12+
query,
13+
nothing,
14+
} from '@umbraco-cms/backoffice/external/lit';
615
import { UmbLitElement } from '@umbraco-cms/backoffice/lit-element';
16+
import { UMB_CURRENT_USER_CONTEXT } from '@umbraco-cms/backoffice/current-user';
717

818
@customElement('umb-app-language-select')
919
export class UmbAppLanguageSelectElement extends UmbLitElement {
@@ -16,20 +26,52 @@ export class UmbAppLanguageSelectElement extends UmbLitElement {
1626
@state()
1727
private _appLanguage?: UmbLanguageDetailModel;
1828

29+
@state()
30+
private _appLanguageIsReadOnly = false;
31+
1932
@state()
2033
private _isOpen = false;
2134

2235
#collectionRepository = new UmbLanguageCollectionRepository(this);
2336
#appLanguageContext?: UmbAppLanguageContext;
2437
#languagesObserver?: any;
2538

39+
#currentUserAllowedLanguages?: Array<string>;
40+
#currentUserHasAccessToAllLanguages?: boolean;
41+
42+
@state()
43+
_disallowedLanguages: Array<UmbLanguageDetailModel> = [];
44+
2645
constructor() {
2746
super();
2847

2948
this.consumeContext(UMB_APP_LANGUAGE_CONTEXT, (instance) => {
3049
this.#appLanguageContext = instance;
3150
this.#observeAppLanguage();
3251
});
52+
53+
this.consumeContext(UMB_CURRENT_USER_CONTEXT, (context) => {
54+
this.observe(context.languages, (languages) => {
55+
this.#currentUserAllowedLanguages = languages;
56+
this.#checkForLanguageAccess();
57+
});
58+
59+
this.observe(context.hasAccessToAllLanguages, (hasAccessToAllLanguages) => {
60+
this.#currentUserHasAccessToAllLanguages = hasAccessToAllLanguages;
61+
this.#checkForLanguageAccess();
62+
});
63+
});
64+
}
65+
66+
#checkForLanguageAccess() {
67+
// find all disallowed languages
68+
this._disallowedLanguages = this._languages?.filter((language) => {
69+
if (this.#currentUserHasAccessToAllLanguages) {
70+
return false;
71+
}
72+
73+
return !this.#currentUserAllowedLanguages?.includes(language.unique);
74+
});
3375
}
3476

3577
async #observeAppLanguage() {
@@ -38,6 +80,10 @@ export class UmbAppLanguageSelectElement extends UmbLitElement {
3880
this.observe(this.#appLanguageContext.appLanguage, (language) => {
3981
this._appLanguage = language;
4082
});
83+
84+
this.observe(this.#appLanguageContext.appLanguageReadOnlyState.isReadOnly, (isReadOnly) => {
85+
this._appLanguageIsReadOnly = isReadOnly;
86+
});
4187
}
4288

4389
async #observeLanguages() {
@@ -46,6 +92,7 @@ export class UmbAppLanguageSelectElement extends UmbLitElement {
4692
// TODO: listen to changes
4793
if (data) {
4894
this._languages = data.items;
95+
this.#checkForLanguageAccess();
4996
}
5097
}
5198

@@ -83,7 +130,11 @@ export class UmbAppLanguageSelectElement extends UmbLitElement {
83130

84131
#renderTrigger() {
85132
return html`<button id="toggle" popovertarget="dropdown-popover">
86-
${this._appLanguage?.name} <uui-symbol-expand .open=${this._isOpen}></uui-symbol-expand>
133+
<span
134+
>${this._appLanguage?.name}
135+
${this._appLanguageIsReadOnly ? this.#renderReadOnlyTag(this._appLanguage?.unique) : nothing}</span
136+
>
137+
<uui-symbol-expand .open=${this._isOpen}></uui-symbol-expand>
87138
</button>`;
88139
}
89140

@@ -98,13 +149,25 @@ export class UmbAppLanguageSelectElement extends UmbLitElement {
98149
label=${ifDefined(language.name)}
99150
@click-label=${this.#onLabelClick}
100151
data-unique=${ifDefined(language.unique)}
101-
?active=${language.unique === this._appLanguage?.unique}></uui-menu-item>
152+
?active=${language.unique === this._appLanguage?.unique}>
153+
${this.#isLanguageReadOnly(language.unique) ? this.#renderReadOnlyTag(language.unique) : nothing}
154+
</uui-menu-item>
102155
`,
103156
)}
104157
</umb-popover-layout>
105158
</uui-popover-container>`;
106159
}
107160

161+
#isLanguageReadOnly(culture?: string) {
162+
if (!culture) return false;
163+
return this._disallowedLanguages.find((language) => language.unique === culture) ? true : false;
164+
}
165+
166+
#renderReadOnlyTag(culture?: string) {
167+
if (!culture) return nothing;
168+
return html`<uui-tag slot="badge" look="secondary">${this.localize.term('general_readOnly')}</uui-tag>`;
169+
}
170+
108171
static override styles = [
109172
css`
110173
:host {

src/packages/language/global-contexts/app-language.context.ts

Lines changed: 92 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -6,30 +6,35 @@ import type { UmbControllerHost } from '@umbraco-cms/backoffice/controller-api';
66
import { UmbContextBase } from '@umbraco-cms/backoffice/class-api';
77
import type { UmbApi } from '@umbraco-cms/backoffice/extension-api';
88
import { UMB_AUTH_CONTEXT } from '@umbraco-cms/backoffice/auth';
9+
import { UmbReadOnlyStateManager } from '@umbraco-cms/backoffice/utils';
10+
import { UMB_CURRENT_USER_CONTEXT } from '@umbraco-cms/backoffice/current-user';
911

1012
// TODO: Make a store for the App Languages.
1113
// TODO: Implement default language end-point, in progress at backend team, so we can avoid getting all languages.
1214
export class UmbAppLanguageContext extends UmbContextBase<UmbAppLanguageContext> implements UmbApi {
13-
#languageCollectionRepository: UmbLanguageCollectionRepository;
1415
#languages = new UmbArrayState<UmbLanguageDetailModel>([], (x) => x.unique);
15-
moreThanOneLanguage = this.#languages.asObservablePart((x) => x.length > 1);
1616

1717
#appLanguage = new UmbObjectState<UmbLanguageDetailModel | undefined>(undefined);
18-
appLanguage = this.#appLanguage.asObservable();
18+
public readonly appLanguage = this.#appLanguage.asObservable();
19+
public readonly appLanguageCulture = this.#appLanguage.asObservablePart((x) => x?.unique);
1920

20-
appLanguageCulture = this.#appLanguage.asObservablePart((x) => x?.unique);
21+
public readonly appLanguageReadOnlyState = new UmbReadOnlyStateManager(this);
2122

22-
appDefaultLanguage = createObservablePart(this.#languages.asObservable(), (languages) =>
23+
public readonly appDefaultLanguage = createObservablePart(this.#languages.asObservable(), (languages) =>
2324
languages.find((language) => language.isDefault),
2425
);
2526

26-
getAppCulture() {
27-
return this.#appLanguage.getValue()?.unique;
28-
}
27+
public readonly moreThanOneLanguage = this.#languages.asObservablePart((x) => x.length > 1);
28+
29+
#languageCollectionRepository = new UmbLanguageCollectionRepository(this);
30+
#currentUserAllowedLanguages: Array<string> = [];
31+
#currentUserHasAccessToAllLanguages = false;
32+
33+
#readOnlyStateIdentifier = 'UMB_LANGUAGE_PERMISSION_';
34+
#localStorageKey = 'umb:appLanguage';
2935

3036
constructor(host: UmbControllerHost) {
3137
super(host, UMB_APP_LANGUAGE_CONTEXT);
32-
this.#languageCollectionRepository = new UmbLanguageCollectionRepository(this);
3338

3439
// 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]
3540
this.consumeContext(UMB_AUTH_CONTEXT, (authContext) => {
@@ -38,12 +43,46 @@ export class UmbAppLanguageContext extends UmbContextBase<UmbAppLanguageContext>
3843
this.#observeLanguages();
3944
});
4045
});
46+
47+
this.consumeContext(UMB_CURRENT_USER_CONTEXT, (context) => {
48+
this.observe(context.languages, (languages) => {
49+
this.#currentUserAllowedLanguages = languages || [];
50+
this.#setIsReadOnly();
51+
});
52+
53+
this.observe(context.hasAccessToAllLanguages, (hasAccessToAllLanguages) => {
54+
this.#currentUserHasAccessToAllLanguages = hasAccessToAllLanguages || false;
55+
this.#setIsReadOnly();
56+
});
57+
});
58+
}
59+
60+
getAppCulture() {
61+
return this.#appLanguage.getValue()?.unique;
4162
}
4263

4364
setLanguage(unique: string) {
44-
const languages = this.#languages.getValue();
45-
const language = languages.find((x) => x.unique === unique);
65+
// clear the previous read-only state
66+
const appLanguage = this.#appLanguage.getValue();
67+
if (appLanguage?.unique) {
68+
this.appLanguageReadOnlyState.removeState(this.#readOnlyStateIdentifier + appLanguage.unique);
69+
}
70+
71+
// find the language
72+
const language = this.#findLanguage(unique);
73+
74+
if (!language) {
75+
throw new Error(`Language with unique ${unique} not found`);
76+
}
77+
78+
// set the new language
4679
this.#appLanguage.update(language);
80+
81+
// store the new language in local storage
82+
localStorage.setItem(this.#localStorageKey, language?.unique);
83+
84+
// set the new read-only state
85+
this.#setIsReadOnly();
4786
}
4887

4988
async #observeLanguages() {
@@ -61,13 +100,55 @@ export class UmbAppLanguageContext extends UmbContextBase<UmbAppLanguageContext>
61100
}
62101

63102
#initAppLanguage() {
103+
// get the selected language from local storage
104+
const uniqueFromLocalStorage = localStorage.getItem(this.#localStorageKey);
105+
106+
if (uniqueFromLocalStorage) {
107+
const language = this.#findLanguage(uniqueFromLocalStorage);
108+
if (language) {
109+
this.setLanguage(language.unique);
110+
return;
111+
}
112+
}
113+
64114
const defaultLanguage = this.#languages.getValue().find((x) => x.isDefault);
65115
// TODO: do we always have a default language?
66116
// do we always get the default language on the first request, or could it be on page 2?
67117
// in that case do we then need an endpoint to get the default language?
68118
if (!defaultLanguage?.unique) return;
69119
this.setLanguage(defaultLanguage.unique);
70120
}
121+
122+
#findLanguage(unique: string) {
123+
return this.#languages.getValue().find((x) => x.unique === unique);
124+
}
125+
126+
#setIsReadOnly() {
127+
const appLanguage = this.#appLanguage.getValue();
128+
129+
if (!appLanguage) {
130+
this.appLanguageReadOnlyState.clear();
131+
return;
132+
}
133+
134+
const unique = this.#readOnlyStateIdentifier + appLanguage.unique;
135+
this.appLanguageReadOnlyState.removeState(unique);
136+
137+
if (this.#currentUserHasAccessToAllLanguages) {
138+
return;
139+
}
140+
141+
const isReadOnly = !this.#currentUserAllowedLanguages.includes(appLanguage.unique);
142+
143+
if (isReadOnly) {
144+
const readOnlyState = {
145+
unique,
146+
message: 'You do not have permission to edit to this culture',
147+
};
148+
149+
this.appLanguageReadOnlyState.addState(readOnlyState);
150+
}
151+
}
71152
}
72153

73154
// Default export to enable this as a globalContext extension js:

0 commit comments

Comments
 (0)