Browse Source

User settings.

pull/414/head
Sebastian 7 years ago
parent
commit
829e84d206
  1. 29
      src/Squidex.Domain.Apps.Entities/Apps/AppUISettingsGrain.cs
  2. 2
      src/Squidex.Domain.Apps.Entities/Apps/IAppUISettingsGrain.cs
  3. 88
      src/Squidex/Areas/Api/Controllers/UI/UIController.cs
  4. 4
      src/Squidex/app/features/content/shared/due-time-selector.component.html
  5. 21
      src/Squidex/app/framework/utils/types.ts
  6. 7
      src/Squidex/app/shared/components/search-form.component.html
  7. 78
      src/Squidex/app/shared/services/ui.service.spec.ts
  8. 27
      src/Squidex/app/shared/services/ui.service.ts
  9. 3
      src/Squidex/app/shared/state/contents.forms.ts
  10. 4
      src/Squidex/app/shared/state/queries.spec.ts
  11. 4
      src/Squidex/app/shared/state/queries.ts
  12. 149
      src/Squidex/app/shared/state/ui.state.spec.ts
  13. 144
      src/Squidex/app/shared/state/ui.state.ts
  14. 21
      tests/Squidex.Domain.Apps.Entities.Tests/Apps/AppUISettingsGrainTests.cs

29
src/Squidex.Domain.Apps.Entities/Apps/AppUISettingsGrain.cs

@ -15,7 +15,7 @@ using Squidex.Infrastructure.States;
namespace Squidex.Domain.Apps.Entities.Apps
{
public sealed class AppUISettingsGrain : GrainOfGuid, IAppUISettingsGrain
public sealed class AppUISettingsGrain : GrainOfString, IAppUISettingsGrain
{
private readonly IGrainState<GrainState> state;
@ -46,7 +46,7 @@ namespace Squidex.Domain.Apps.Entities.Apps
public Task SetAsync(string path, J<IJsonValue> value)
{
var container = GetContainer(path, out var key);
var container = GetContainer(path, true, out var key);
if (container == null)
{
@ -58,19 +58,19 @@ namespace Squidex.Domain.Apps.Entities.Apps
return state.WriteAsync();
}
public Task RemoveAsync(string path)
public async Task RemoveAsync(string path)
{
var container = GetContainer(path, out var key);
var container = GetContainer(path, false, out var key);
if (container != null)
if (container?.ContainsKey(path) == true)
{
container.Remove(key);
}
return state.WriteAsync();
await state.WriteAsync();
}
}
private JsonObject GetContainer(string path, out string key)
private JsonObject GetContainer(string path, bool add, out string key)
{
Guard.NotNullOrEmpty(path, nameof(path));
@ -86,9 +86,16 @@ namespace Squidex.Domain.Apps.Entities.Apps
{
if (!current.TryGetValue(segment, out var temp))
{
temp = JsonValue.Object();
current[segment] = temp;
if (add)
{
temp = JsonValue.Object();
current[segment] = temp;
}
else
{
return null;
}
}
if (temp is JsonObject next)

2
src/Squidex.Domain.Apps.Entities/Apps/IAppUISettingsGrain.cs

@ -12,7 +12,7 @@ using Squidex.Infrastructure.Orleans;
namespace Squidex.Domain.Apps.Entities.Apps
{
public interface IAppUISettingsGrain : IGrainWithGuidKey
public interface IAppUISettingsGrain : IGrainWithStringKey
{
Task<J<JsonObject>> GetAsync();

88
src/Squidex/Areas/Api/Controllers/UI/UIController.cs

@ -5,6 +5,7 @@
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
using System;
using System.Collections.Generic;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Mvc;
@ -12,6 +13,7 @@ using Microsoft.Extensions.Options;
using Orleans;
using Squidex.Areas.Api.Controllers.UI.Models;
using Squidex.Domain.Apps.Entities.Apps;
using Squidex.Infrastructure;
using Squidex.Infrastructure.Commands;
using Squidex.Infrastructure.Orleans;
using Squidex.Infrastructure.Security;
@ -68,7 +70,26 @@ namespace Squidex.Areas.Api.Controllers.UI
[ApiPermission]
public async Task<IActionResult> GetSettings(string app)
{
var result = await grainFactory.GetGrain<IAppUISettingsGrain>(AppId).GetAsync();
var result = await GetSettingsGrain(AppKey()).GetAsync();
return Ok(result.Value);
}
/// <summary>
/// Get my ui settings.
/// </summary>
/// <param name="app">The name of the app.</param>
/// <returns>
/// 200 => UI settings returned.
/// 404 => App not found.
/// </returns>
[HttpGet]
[Route("apps/{app}/ui/settings/me")]
[ProducesResponseType(typeof(Dictionary<string, string>), 200)]
[ApiPermission]
public async Task<IActionResult> GetUserSettings(string app)
{
var result = await GetSettingsGrain(UserKey()).GetAsync();
return Ok(result.Value);
}
@ -88,7 +109,27 @@ namespace Squidex.Areas.Api.Controllers.UI
[ApiPermission]
public async Task<IActionResult> PutSetting(string app, string key, [FromBody] UpdateSettingDto request)
{
await grainFactory.GetGrain<IAppUISettingsGrain>(AppId).SetAsync(key, request.Value.AsJ());
await GetSettingsGrain(AppKey()).SetAsync(key, request.Value.AsJ());
return NoContent();
}
/// <summary>
/// Set my ui settings.
/// </summary>
/// <param name="app">The name of the app.</param>
/// <param name="key">The name of the setting.</param>
/// <param name="request">The request with the value to update.</param>
/// <returns>
/// 200 => UI setting set.
/// 404 => App not found.
/// </returns>
[HttpPut]
[Route("apps/{app}/ui/settings/me/{key}")]
[ApiPermission]
public async Task<IActionResult> PutUserSetting(string app, string key, [FromBody] UpdateSettingDto request)
{
await GetSettingsGrain(UserKey()).SetAsync(key, request.Value.AsJ());
return NoContent();
}
@ -107,9 +148,50 @@ namespace Squidex.Areas.Api.Controllers.UI
[ApiPermission]
public async Task<IActionResult> DeleteSetting(string app, string key)
{
await grainFactory.GetGrain<IAppUISettingsGrain>(AppId).RemoveAsync(key);
await GetSettingsGrain(AppKey()).RemoveAsync(key);
return NoContent();
}
/// <summary>
/// Remove my ui settings.
/// </summary>
/// <param name="app">The name of the app.</param>
/// <param name="key">The name of the setting.</param>
/// <returns>
/// 200 => UI setting removed.
/// 404 => App not found.
/// </returns>
[HttpDelete]
[Route("apps/{app}/ui/settings/me/{key}")]
[ApiPermission]
public async Task<IActionResult> DeleteUserSetting(string app, string key)
{
await GetSettingsGrain(UserKey()).RemoveAsync(key);
return NoContent();
}
private IAppUISettingsGrain GetSettingsGrain(string key)
{
return grainFactory.GetGrain<IAppUISettingsGrain>(key);
}
private string AppKey()
{
return $"{AppId}";
}
private string UserKey()
{
var subject = User.OpenIdSubject();
if (string.IsNullOrWhiteSpace(subject))
{
throw new DomainForbiddenException("Not allowed for clients.");
}
return $"{AppId}_{subject}";
}
}
}

4
src/Squidex/app/features/content/shared/due-time-selector.component.html

@ -6,14 +6,14 @@
<ng-container content>
<div class="form-check">
<input class="form-check-input" type="radio" [(ngModel)]="dueTimeMode" value="Immediately" id="immediately" name="dueTimeMode">
<input class="form-check-input" type="radio" [(ngModel)]="dueTimeMode" value="Immediately" id="immediately" name="dueTimeMode" />
<label class="form-check-label" for="immediately">
Set to {{dueTimeAction}} immediately.
</label>
</div>
<div class="form-check">
<input class="form-check-input" type="radio" [(ngModel)]="dueTimeMode" value="Scheduled" id="scheduled" name="dueTimeMode">
<input class="form-check-input" type="radio" [(ngModel)]="dueTimeMode" value="Scheduled" id="scheduled" name="dueTimeMode" />
<label class="form-check-label" for="scheduled">
Set to {{dueTimeAction}} at a later point date and time.
</label>

21
src/Squidex/app/framework/utils/types.ts

@ -127,4 +127,25 @@ export module Types {
return Types.isUndefined(value) === true || Types.isNull(value) === true;
}
}
export function mergeInto(target: object, source: object) {
if (!Types.isObject(target) || !Types.isObject(source)) {
return source;
}
Object.keys(source).forEach(key => {
const targetValue = target[key];
const sourceValue = source[key];
if (Types.isArray(targetValue) && Types.isArray(sourceValue)) {
target[key] = targetValue.concat(sourceValue);
} else if (Types.isObject(targetValue) && Types.isObject(sourceValue)) {
target[key] = mergeInto({ ...targetValue }, sourceValue);
} else {
target[key] = sourceValue;
}
});
return target;
}

7
src/Squidex/app/shared/components/search-form.component.html

@ -78,6 +78,13 @@
<input type="text" class="form-control" id="appName" formControlName="name" autocomplete="off" sqxFocusOnInit />
</div>
<div class="form-check">
<input class="form-check-input" type="checkbox" formControlName="user" id="user" />
<label class="form-check-label" for="user">
Save the query only for myself.
</label>
</div>
</ng-container>
<ng-container footer>

78
src/Squidex/app/shared/services/ui.service.spec.ts

@ -71,12 +71,12 @@ describe('UIService', () => {
expect(settings!).toEqual({ mapType: 'OSM', mapKey: '', canCreateApps: true });
}));
it('should make get request to get settings',
it('should make get request to get shared settings',
inject([UIService, HttpTestingController], (uiService: UIService, httpMock: HttpTestingController) => {
let settings: object;
uiService.getSettings('my-app').subscribe(result => {
uiService.getSharedSettings('my-app').subscribe(result => {
settings = result;
});
@ -92,12 +92,12 @@ describe('UIService', () => {
expect(settings!).toEqual(response);
}));
it('should return default settings when error occurs',
it('should return default shared settings when error occurs',
inject([UIService, HttpTestingController], (uiService: UIService, httpMock: HttpTestingController) => {
let settings: object;
uiService.getSettings('my-app').subscribe(result => {
uiService.getSharedSettings('my-app').subscribe(result => {
settings = result;
});
@ -111,10 +111,50 @@ describe('UIService', () => {
expect(settings!).toBeDefined();
}));
it('should make put request to set value',
it('should make get request to get user settings',
inject([UIService, HttpTestingController], (uiService: UIService, httpMock: HttpTestingController) => {
uiService.putSetting('my-app', 'root.nested', 123).subscribe();
let settings: object;
uiService.getUserSettings('my-app').subscribe(result => {
settings = result;
});
const response = { mapType: 'OSM', mapKey: '' };
const req = httpMock.expectOne('http://service/p/api/apps/my-app/ui/settings/me');
expect(req.request.method).toEqual('GET');
expect(req.request.headers.get('If-Match')).toBeNull();
req.flush(response);
expect(settings!).toEqual(response);
}));
it('should return default user settings when error occurs',
inject([UIService, HttpTestingController], (uiService: UIService, httpMock: HttpTestingController) => {
let settings: object;
uiService.getUserSettings('my-app').subscribe(result => {
settings = result;
});
const req = httpMock.expectOne('http://service/p/api/apps/my-app/ui/settings/me');
expect(req.request.method).toEqual('GET');
expect(req.request.headers.get('If-Match')).toBeNull();
req.error(new ErrorEvent('500'));
expect(settings!).toBeDefined();
}));
it('should make put request to set shared value',
inject([UIService, HttpTestingController], (uiService: UIService, httpMock: HttpTestingController) => {
uiService.putSharedSetting('my-app', 'root.nested', 123).subscribe();
const req = httpMock.expectOne('http://service/p/api/apps/my-app/ui/settings/root.nested');
@ -122,14 +162,36 @@ describe('UIService', () => {
expect(req.request.headers.get('If-Match')).toBeNull();
}));
it('should make delete request to remove value',
it('should make put request to set user value',
inject([UIService, HttpTestingController], (uiService: UIService, httpMock: HttpTestingController) => {
uiService.deleteSetting('my-app', 'root.nested').subscribe();
uiService.putUserSetting('my-app', 'root.nested', 123).subscribe();
const req = httpMock.expectOne('http://service/p/api/apps/my-app/ui/settings/me/root.nested');
expect(req.request.method).toEqual('PUT');
expect(req.request.headers.get('If-Match')).toBeNull();
}));
it('should make delete request to remove shared value',
inject([UIService, HttpTestingController], (uiService: UIService, httpMock: HttpTestingController) => {
uiService.deleteSharedSetting('my-app', 'root.nested').subscribe();
const req = httpMock.expectOne('http://service/p/api/apps/my-app/ui/settings/root.nested');
expect(req.request.method).toEqual('DELETE');
expect(req.request.headers.get('If-Match')).toBeNull();
}));
it('should make delete request to remove user value',
inject([UIService, HttpTestingController], (uiService: UIService, httpMock: HttpTestingController) => {
uiService.deleteUserSetting('my-app', 'root.nested').subscribe();
const req = httpMock.expectOne('http://service/p/api/apps/my-app/ui/settings/me/root.nested');
expect(req.request.method).toEqual('DELETE');
expect(req.request.headers.get('If-Match')).toBeNull();
}));
});

27
src/Squidex/app/shared/services/ui.service.ts

@ -33,7 +33,7 @@ export class UIService {
}));
}
public getSettings(appName: string): Observable<object> {
public getSharedSettings(appName: string): Observable<object> {
const url = this.apiUrl.buildUrl(`api/apps/${appName}/ui/settings`);
return this.http.get<object>(url).pipe(
@ -42,15 +42,36 @@ export class UIService {
}));
}
public putSetting(appName: string, key: string, value: any): Observable<any> {
public getUserSettings(appName: string): Observable<object> {
const url = this.apiUrl.buildUrl(`api/apps/${appName}/ui/settings/me`);
return this.http.get<object>(url).pipe(
catchError(() => {
return of({});
}));
}
public putSharedSetting(appName: string, key: string, value: any): Observable<any> {
const url = this.apiUrl.buildUrl(`api/apps/${appName}/ui/settings/${key}`);
return this.http.put(url, { value });
}
public deleteSetting(appName: string, key: string): Observable<any> {
public putUserSetting(appName: string, key: string, value: any): Observable<any> {
const url = this.apiUrl.buildUrl(`api/apps/${appName}/ui/settings/me/${key}`);
return this.http.put(url, { value });
}
public deleteSharedSetting(appName: string, key: string): Observable<any> {
const url = this.apiUrl.buildUrl(`api/apps/${appName}/ui/settings/${key}`);
return this.http.delete(url);
}
public deleteUserSetting(appName: string, key: string): Observable<any> {
const url = this.apiUrl.buildUrl(`api/apps/${appName}/ui/settings/me/${key}`);
return this.http.delete(url);
}
}

3
src/Squidex/app/shared/state/contents.forms.ts

@ -53,7 +53,8 @@ export class SaveQueryForm extends Form<FormGroup, any> {
[
Validators.required
]
]
],
user: false
}));
}
}

4
src/Squidex/app/shared/state/queries.spec.ts

@ -74,11 +74,11 @@ describe('Queries', () => {
});
it('should forward add call to state', () => {
queries.add('key3', { fullText: 'text3' });
queries.add('key3', { fullText: 'text3' }, true);
expect(true).toBeTruthy();
uiState.verify(x => x.set('schemas.my-schema.queries.key3', '{"fullText":"text3"}'), Times.once());
uiState.verify(x => x.set('schemas.my-schema.queries.key3', '{"fullText":"text3"}', true), Times.once());
});
it('should forward remove call to state', () => {

4
src/Squidex/app/shared/state/queries.ts

@ -52,8 +52,8 @@ export class Queries {
);
}
public add(key: string, query: Query) {
this.uiState.set(`${this.prefix}.queries.${key}`, JSON.stringify(query));
public add(key: string, query: Query, user = false) {
this.uiState.set(`${this.prefix}.queries.${key}`, JSON.stringify(query), user);
}
public remove(saved: SavedQuery) {

149
src/Squidex/app/shared/state/ui.state.spec.ts

@ -6,7 +6,7 @@
*/
import { of } from 'rxjs';
import { IMock, It, Mock, Times } from 'typemoq';
import { IMock, Mock } from 'typemoq';
import {
ResourceLinks,
@ -24,18 +24,30 @@ describe('UIState', () => {
appsState
} = TestValues;
const appSettings = {
mapType: 'GM',
mapSize: 1024,
const common = {
key: 'xx',
map: {
type: 'GSM',
sizeX: 800,
sizeY: 600
},
canCreateApps: true
};
const commonSettings = {
mapType: 'OSM',
mapKey: 'Key',
const shared = {
map: {
type: 'GM', key: 'xyz'
},
canCreateApps: true
};
const user = {
map: {
sizeX: 1000
},
canCustomize: true
};
const resources: ResourceLinks = {
['admin/events']: { method: 'GET', href: '/api/events' },
['admin/restore']: { method: 'GET', href: '/api/restore' },
@ -49,17 +61,14 @@ describe('UIState', () => {
beforeEach(() => {
uiService = Mock.ofType<UIService>();
uiService.setup(x => x.getSettings(app))
.returns(() => of(appSettings));
uiService.setup(x => x.getCommonSettings())
.returns(() => of(commonSettings));
.returns(() => of(common));
uiService.setup(x => x.putSetting(app, It.isAnyString(), It.isAny()))
.returns(() => of({}));
uiService.setup(x => x.getSharedSettings(app))
.returns(() => of(shared));
uiService.setup(x => x.deleteSetting(app, It.isAnyString()))
.returns(() => of({}));
uiService.setup(x => x.getUserSettings(app))
.returns(() => of(user));
usersService = Mock.ofType<UsersService>();
@ -71,10 +80,15 @@ describe('UIState', () => {
it('should load settings', () => {
expect(uiState.snapshot.settings).toEqual({
mapType: 'GM',
mapKey: 'Key',
mapSize: 1024,
canCreateApps: true
key: 'xx',
map: {
type: 'GM',
sizeX: 1000,
sizeY: 600,
key: 'xyz'
},
canCreateApps: true,
canCustomize: true
});
expect(uiState.snapshot.canReadEvents).toBeTruthy();
@ -82,17 +96,25 @@ describe('UIState', () => {
expect(uiState.snapshot.canRestore).toBeTruthy();
});
it('should add value to snapshot when set', () => {
it('should add value to snapshot when set as shared', () => {
uiService.setup(x => x.putSharedSetting(app, 'root.nested', 123))
.returns(() => of({})).verifiable();
uiState.set('root.nested', 123);
expect(uiState.snapshot.settings).toEqual({
mapType: 'GM',
mapKey: 'Key',
mapSize: 1024,
key: 'xx',
map: {
type: 'GM',
sizeX: 1000,
sizeY: 600,
key: 'xyz'
},
canCreateApps: true,
canCustomize: true,
root: {
nested: 123
},
canCreateApps: true
}
});
uiState.get('root', {}).subscribe(x => {
@ -107,36 +129,83 @@ describe('UIState', () => {
expect(x).toEqual(1337);
});
uiService.verify(x => x.putSetting(app, 'root.nested', 123), Times.once());
uiService.verifyAll();
});
it('should remove value from snapshot when removed', () => {
uiState.set('root.nested1', 123);
uiState.set('root.nested2', 123);
uiState.remove('root.nested1');
it('should add value to snapshot when set as user', () => {
uiService.setup(x => x.putUserSetting(app, 'root.nested', 123))
.returns(() => of({})).verifiable();
uiState.set('root.nested', 123, true);
expect(uiState.snapshot.settings).toEqual({
mapType: 'GM',
mapKey: 'Key',
mapSize: 1024,
root: {
nested2: 123
key: 'xx',
map: {
type: 'GM',
sizeX: 1000,
sizeY: 600,
key: 'xyz'
},
canCreateApps: true
canCreateApps: true,
canCustomize: true,
root: {
nested: 123
}
});
uiState.get('root', {}).subscribe(x => {
expect(x).toEqual({ nested2: 123 });
expect(x).toEqual({ nested: 123 });
});
uiState.get('root.nested2', 0).subscribe(x => {
uiState.get('root.nested', 0).subscribe(x => {
expect(x).toEqual(123);
});
uiState.get('root.nested1', 1337).subscribe(x => {
uiState.get('root.notfound', 1337).subscribe(x => {
expect(x).toEqual(1337);
});
uiService.verify(x => x.deleteSetting(app, 'root.nested1'), Times.once());
uiService.verifyAll();
});
it('should remove value from snapshot and shared settings when removed', () => {
uiService.setup(x => x.deleteSharedSetting(app, 'map.key'))
.returns(() => of({})).verifiable();
uiState.remove('map.key');
expect(uiState.snapshot.settings).toEqual({
key: 'xx',
map: {
type: 'GM',
sizeX: 1000,
sizeY: 600
},
canCreateApps: true,
canCustomize: true
});
uiService.verifyAll();
});
it('should remove value from snapshot and user settings when removed', () => {
uiService.setup(x => x.deleteUserSetting(app, 'map.sizeX'))
.returns(() => of({})).verifiable();
uiState.remove('map.sizeX');
expect(uiState.snapshot.settings).toEqual({
key: 'xx',
map: {
type: 'GM',
sizeX: 800,
sizeY: 600,
key: 'xyz'
},
canCreateApps: true,
canCustomize: true
});
uiService.verifyAll();
});
});

144
src/Squidex/app/shared/state/ui.state.ts

@ -10,6 +10,7 @@ import { distinctUntilChanged, map } from 'rxjs/operators';
import {
hasAnyLink,
mergeInto,
State,
Types
} from '@app/framework';
@ -24,8 +25,11 @@ interface Snapshot {
// All common settings.
settingsCommon: object & any;
// All app settings.
settingsApp: object & any;
// All shared app settings.
settingsShared: object & any;
// All user app settings.
settingsUser: object & any;
// The merged settings of app and common settings.
settings: object & any;
@ -70,29 +74,39 @@ export class UIState extends State<Snapshot> {
private readonly uiService: UIService,
private readonly usersService: UsersService
) {
super({ settings: {}, settingsCommon: {}, settingsApp: {} });
super({
settings: {},
settingsCommon: {},
settingsShared: {},
settingsUser: {}
});
this.loadResources();
this.loadCommon();
appsState.selectedValidApp.subscribe(app => {
this.load();
this.load(app.name);
});
}
private load() {
this.next(s => updateAppSettings(s, {}));
private load(app: string) {
this.next(s => updateSettings(s, {}));
this.uiService.getSharedSettings(app)
.subscribe(payload => {
this.next(s => updateSettings(s, { settingsShared: payload }));
});
this.uiService.getSettings(this.appName)
this.uiService.getUserSettings(app)
.subscribe(payload => {
this.next(s => updateAppSettings(s, payload));
this.next(s => updateSettings(s, { settingsUser: payload }));
});
}
private loadCommon() {
this.uiService.getCommonSettings()
.subscribe(payload => {
this.next(s => updateCommonSettings(s, payload));
this.next(s => updateSettings(s, { settingsCommon: payload }));
});
}
@ -107,60 +121,72 @@ export class UIState extends State<Snapshot> {
});
}
public set(path: string, value: any) {
const { key, current, root } = this.getContainer(path);
public set(path: string, value: any, user = false) {
if (user) {
this.setUser(path, value);
} else {
this.setShared(path, value);
}
}
private setUser(path: string, value: any) {
const { key, current, root } = getContainer(this.snapshot.settingsUser, path);
if (current && key) {
this.uiService.putSetting(this.appName, path, value).subscribe();
this.uiService.putUserSetting(this.appName, path, value).subscribe();
current[key] = value;
this.next(s => updateAppSettings(s, root));
this.next(s => updateSettings(s, { settingsUser: root }));
}
}
public remove(path: string) {
const { key, current, root } = this.getContainer(path);
private setShared(path: string, value: any) {
const { key, current, root } = getContainer(this.snapshot.settingsShared, path);
if (current && key) {
this.uiService.deleteSetting(this.appName, path).subscribe();
this.uiService.putSharedSetting(this.appName, path, value).subscribe();
delete current[key];
current[key] = value;
this.next(s => updateAppSettings(s, root));
this.next(s => updateSettings(s, { settingsShared: root }));
}
}
private getContainer(path: string) {
const segments = path.split('.');
public remove(path: string) {
return this.removeUser(path) || this.removeShared(path);
}
private removeUser(path: string) {
const { key, current, root } = getContainer(this.snapshot.settingsUser, path);
let current = { ...this.snapshot.settingsApp };
if (current && key && current[key]) {
this.uiService.deleteUserSetting(this.appName, path).subscribe();
const root = current;
delete current[key];
if (segments.length > 0) {
for (let i = 0; i < segments.length - 1; i++) {
const segment = segments[i];
this.next(s => updateSettings(s, { settingsUser: root }));
let temp = current[segment];
return true;
}
if (!temp) {
temp = {};
} else {
temp = { ...temp };
}
return false;
}
current[segment] = temp;
private removeShared(path: string) {
const { key, current, root } = getContainer(this.snapshot.settingsShared, path);
if (!Types.isObject(temp)) {
return { key: null, current: null, root: null };
}
if (current && key && current[key]) {
this.uiService.deleteSharedSetting(this.appName, path).subscribe();
current = temp;
}
delete current[key];
this.next(s => updateSettings(s, { settingsShared: root }));
return true;
}
return { key: segments[segments.length - 1], current, root };
return false;
}
private getValue<T>(setting: object & UISettingsDto, path: string, defaultValue: T) {
@ -188,14 +214,44 @@ export class UIState extends State<Snapshot> {
}
}
function updateAppSettings(state: Snapshot, settingsApp: object & any) {
const { settingsCommon } = state;
function getContainer(settings: object, path: string) {
const segments = path.split('.');
let current = { ...settings };
const root = current;
return { ...state, settings: { ...settingsCommon, ...settingsApp }, settingsApp, settingsCommon };
if (segments.length > 0) {
for (let i = 0; i < segments.length - 1; i++) {
const segment = segments[i];
let temp = current[segment];
if (!temp) {
temp = {};
} else {
temp = { ...temp };
}
current[segment] = temp;
if (!Types.isObject(temp)) {
return { key: null, current: null, root: null };
}
current = temp;
}
}
return { key: segments[segments.length - 1], current, root };
}
function updateCommonSettings(state: Snapshot, settingsCommon: object & any) {
const { settingsApp } = state;
function updateSettings(state: Snapshot, update: Partial<Snapshot>) {
const settings = {};
mergeInto(settings, update.settingsCommon || state.settingsCommon);
mergeInto(settings, update.settingsShared || state.settingsShared);
mergeInto(settings, update.settingsUser || state.settingsUser);
return { ...state, settings: { ...settingsCommon, ...settingsApp }, settingsApp, settingsCommon };
return { ...state, settings, ...update };
}

21
tests/Squidex.Domain.Apps.Entities.Tests/Apps/AppUISettingsGrainTests.cs

@ -36,6 +36,9 @@ namespace Squidex.Domain.Apps.Entities.Apps
JsonValue.Object().Add("key", 15);
Assert.Equal(expected.ToString(), actual.Value.ToString());
A.CallTo(() => grainState.WriteAsync())
.MustHaveHappened();
}
[Fact]
@ -49,6 +52,9 @@ namespace Squidex.Domain.Apps.Entities.Apps
JsonValue.Object().Add("key", 123);
Assert.Equal(expected.ToString(), actual.Value.ToString());
A.CallTo(() => grainState.WriteAsync())
.MustHaveHappened();
}
[Fact]
@ -62,6 +68,9 @@ namespace Squidex.Domain.Apps.Entities.Apps
var expected = JsonValue.Object();
Assert.Equal(expected.ToString(), actual.Value.ToString());
A.CallTo(() => grainState.WriteAsync())
.MustHaveHappenedTwiceExactly();
}
[Fact]
@ -76,6 +85,9 @@ namespace Squidex.Domain.Apps.Entities.Apps
JsonValue.Object().Add("nested", 123));
Assert.Equal(expected.ToString(), actual.Value.ToString());
A.CallTo(() => grainState.WriteAsync())
.MustHaveHappened();
}
[Fact]
@ -91,6 +103,9 @@ namespace Squidex.Domain.Apps.Entities.Apps
JsonValue.Object());
Assert.Equal(expected.ToString(), actual.Value.ToString());
A.CallTo(() => grainState.WriteAsync())
.MustHaveHappenedTwiceExactly();
}
[Fact]
@ -105,12 +120,18 @@ namespace Squidex.Domain.Apps.Entities.Apps
public async Task Should_do_nothing_if_deleting_and_nested_not_found()
{
await sut.RemoveAsync("root.nested");
A.CallTo(() => grainState.WriteAsync())
.MustNotHaveHappened();
}
[Fact]
public async Task Should_do_nothing_if_deleting_and_key_not_found()
{
await sut.RemoveAsync("root");
A.CallTo(() => grainState.WriteAsync())
.MustNotHaveHappened();
}
}
}

Loading…
Cancel
Save