mirror of https://github.com/Squidex/squidex.git
Browse Source
# Conflicts: # src/Squidex/Areas/Api/Controllers/UI/UIController.cs # src/Squidex/app/theme/icomoon/demo.html # src/Squidex/app/theme/icomoon/fonts/icomoon.eot # src/Squidex/app/theme/icomoon/fonts/icomoon.svg # src/Squidex/app/theme/icomoon/fonts/icomoon.ttf # src/Squidex/app/theme/icomoon/fonts/icomoon.woff # src/Squidex/app/theme/icomoon/selection.json # src/Squidex/app/theme/icomoon/style.css # src/Squidex/appsettings.jsonpull/314/head
67 changed files with 1573 additions and 585 deletions
@ -0,0 +1,117 @@ |
|||
// ==========================================================================
|
|||
// Squidex Headless CMS
|
|||
// ==========================================================================
|
|||
// Copyright (c) Squidex UG (haftungsbeschraenkt)
|
|||
// All rights reserved. Licensed under the MIT license.
|
|||
// ==========================================================================
|
|||
|
|||
using System; |
|||
using System.Linq; |
|||
using System.Threading.Tasks; |
|||
using Newtonsoft.Json.Linq; |
|||
using Squidex.Infrastructure; |
|||
using Squidex.Infrastructure.Orleans; |
|||
using Squidex.Infrastructure.States; |
|||
|
|||
namespace Squidex.Domain.Apps.Entities.Apps |
|||
{ |
|||
public sealed class AppUISettingsGrain : GrainOfGuid, IAppUISettingsGrain |
|||
{ |
|||
private readonly IStore<Guid> store; |
|||
private IPersistence<State> persistence; |
|||
private State state = new State(); |
|||
|
|||
[CollectionName("UISettings")] |
|||
public sealed class State |
|||
{ |
|||
public JObject Settings { get; set; } = new JObject(); |
|||
} |
|||
|
|||
public AppUISettingsGrain(IStore<Guid> store) |
|||
{ |
|||
Guard.NotNull(store, nameof(store)); |
|||
|
|||
this.store = store; |
|||
} |
|||
|
|||
public override Task OnActivateAsync(Guid key) |
|||
{ |
|||
persistence = store.WithSnapshots<State, Guid>(GetType(), key, x => state = x); |
|||
|
|||
return persistence.ReadAsync(); |
|||
} |
|||
|
|||
public Task<J<JObject>> GetAsync() |
|||
{ |
|||
return Task.FromResult(state.Settings.AsJ()); |
|||
} |
|||
|
|||
public Task SetAsync(J<JObject> settings) |
|||
{ |
|||
state.Settings = settings; |
|||
|
|||
return persistence.WriteSnapshotAsync(state); |
|||
} |
|||
|
|||
public Task SetAsync(string path, J<JToken> value) |
|||
{ |
|||
var container = GetContainer(path, out var key); |
|||
|
|||
if (container == null) |
|||
{ |
|||
throw new InvalidOperationException("Path does not lead to an object."); |
|||
} |
|||
|
|||
container[key] = value; |
|||
|
|||
return persistence.WriteSnapshotAsync(state); |
|||
} |
|||
|
|||
public Task RemoveAsync(string path) |
|||
{ |
|||
var container = GetContainer(path, out var key); |
|||
|
|||
if (container != null) |
|||
{ |
|||
container.Remove(key); |
|||
} |
|||
|
|||
return persistence.WriteSnapshotAsync(state); |
|||
} |
|||
|
|||
private JObject GetContainer(string path, out string key) |
|||
{ |
|||
Guard.NotNullOrEmpty(path, nameof(path)); |
|||
|
|||
var segments = path.Split('.'); |
|||
|
|||
key = segments[segments.Length - 1]; |
|||
|
|||
var current = state.Settings; |
|||
|
|||
if (segments.Length > 1) |
|||
{ |
|||
foreach (var segment in segments.Take(segments.Length - 1)) |
|||
{ |
|||
if (!current.TryGetValue(segment, out var temp)) |
|||
{ |
|||
temp = new JObject(); |
|||
|
|||
current[segment] = temp; |
|||
} |
|||
|
|||
if (temp is JObject next) |
|||
{ |
|||
current = next; |
|||
} |
|||
else |
|||
{ |
|||
return null; |
|||
} |
|||
} |
|||
} |
|||
|
|||
return current; |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,25 @@ |
|||
// ==========================================================================
|
|||
// Squidex Headless CMS
|
|||
// ==========================================================================
|
|||
// Copyright (c) Squidex UG (haftungsbeschraenkt)
|
|||
// All rights reserved. Licensed under the MIT license.
|
|||
// ==========================================================================
|
|||
|
|||
using System.Threading.Tasks; |
|||
using Newtonsoft.Json.Linq; |
|||
using Orleans; |
|||
using Squidex.Infrastructure.Orleans; |
|||
|
|||
namespace Squidex.Domain.Apps.Entities.Apps |
|||
{ |
|||
public interface IAppUISettingsGrain : IGrainWithGuidKey |
|||
{ |
|||
Task<J<JObject>> GetAsync(); |
|||
|
|||
Task SetAsync(string path, J<JToken> value); |
|||
|
|||
Task SetAsync(J<JObject> settings); |
|||
|
|||
Task RemoveAsync(string path); |
|||
} |
|||
} |
|||
@ -0,0 +1,19 @@ |
|||
// ==========================================================================
|
|||
// Squidex Headless CMS
|
|||
// ==========================================================================
|
|||
// Copyright (c) Squidex UG (haftungsbeschraenkt)
|
|||
// All rights reserved. Licensed under the MIT license.
|
|||
// ==========================================================================
|
|||
|
|||
using Newtonsoft.Json.Linq; |
|||
|
|||
namespace Squidex.Areas.Api.Controllers.UI.Models |
|||
{ |
|||
public sealed class UpdateSettingDto |
|||
{ |
|||
/// <summary>
|
|||
/// The value for the setting.
|
|||
/// </summary>
|
|||
public JToken Value { get; set; } |
|||
} |
|||
} |
|||
@ -1,60 +0,0 @@ |
|||
<ng-container *ngIf="enableShortcut"> |
|||
<sqx-shortcut keys="ctrl+shift+f" (trigger)="inputFind.focus()"></sqx-shortcut> |
|||
</ng-container> |
|||
|
|||
<form [class]="formClass" (ngSubmit)="search()"> |
|||
<input class="form-control form-control-expandable" #inputFind [formControl]="contentsFilter" placeholder="Search for content" /> |
|||
|
|||
<a class="expand-search" (click)="searchModal.toggle()" #archive> |
|||
<i class="icon-caret-down"></i> |
|||
</a> |
|||
</form> |
|||
|
|||
<sqx-onboarding-tooltip id="contentArchive" [for]="archive" position="bottomRight" after="60000"> |
|||
Click this icon to show the advanced search menu and to show the archive! |
|||
</sqx-onboarding-tooltip> |
|||
|
|||
<sqx-onboarding-tooltip id="contentFind" [for]="inputFind" position="bottomRight" after="120000"> |
|||
Search for content using full text search over all fields and languages! |
|||
</sqx-onboarding-tooltip> |
|||
|
|||
<div class="dropdown-menu" *sqxModalView="searchModal" [sqxModalTarget]="inputFind"> |
|||
<div class="form-horizontal"> |
|||
<div [formGroup]="searchForm"> |
|||
<div class="form-group row"> |
|||
<label class="col col-2 col-form-label" for="odataSearch">Text</label> |
|||
|
|||
<div class="col col-10"> |
|||
<input type="input" class="form-control" id="search" (blur)="updateQuery()" formControlName="odataSearch" placeholder="Fulltext search" /> |
|||
</div> |
|||
</div> |
|||
|
|||
<div class="form-group row"> |
|||
<label class="col col-2 col-form-label" for="filter">Filter</label> |
|||
|
|||
<div class="col col-10"> |
|||
<input type="input" class="form-control" id="filter" (blur)="updateQuery()" formControlName="odataFilter" placeholder="data/[MY_FIELD]/iv eq 100" /> |
|||
</div> |
|||
</div> |
|||
|
|||
<div class="form-group row"> |
|||
<label class="col col-2 col-form-label" for="orderBy">Order</label> |
|||
|
|||
<div class="col col-10"> |
|||
<input type="input" class="form-control" id="orderBy" (blur)="updateQuery()" formControlName="odataOrderBy" placeholder="data/[MY_FIELD]/iv desc" /> |
|||
</div> |
|||
</div> |
|||
</div> |
|||
|
|||
<div class="form-check" *ngIf="canArchive"> |
|||
<input class="form-check-input" type="checkbox" id="archivedItems" [ngModel]="archived" (ngModelChange)="archivedChanged.emit($event)" /> |
|||
<label class="form-check-label" for="archivedItems"> |
|||
Archived items |
|||
</label> |
|||
</div> |
|||
|
|||
<div class="link"> |
|||
Read more about filtering in the <a href="https://docs.squidex.io/04-guides/02-api.html" target="_blank">Documentation</a>. |
|||
</div> |
|||
</div> |
|||
</div> |
|||
@ -0,0 +1,99 @@ |
|||
<ng-container *ngIf="enableShortcut"> |
|||
<sqx-shortcut keys="ctrl+shift+f" (trigger)="inputFind.focus()"></sqx-shortcut> |
|||
</ng-container> |
|||
|
|||
<form [class]="formClass" (ngSubmit)="search()"> |
|||
<input class="form-control form-control-expandable" #inputFind [formControl]="contentsFilter" [placeholder]="placeholder" /> |
|||
|
|||
<ng-container *ngIf="expandable"> |
|||
<a class="expand-search" (click)="searchModal.toggle()" #expand> |
|||
<i class="icon-caret-down"></i> |
|||
</a> |
|||
|
|||
<sqx-onboarding-tooltip id="contentArchive" [for]="expand" position="bottomRight" after="60000"> |
|||
Click this icon to show the advanced search menu and to show the archive! |
|||
</sqx-onboarding-tooltip> |
|||
</ng-container> |
|||
|
|||
<ng-container *ngIf="queries"> |
|||
<ng-container *ngIf="saveKey | async; else notBookmarked; let key"> |
|||
<a class="save-search" (click)="queries.remove(key)"> |
|||
<i class="icon-star-full"></i> |
|||
</a> |
|||
</ng-container> |
|||
|
|||
<ng-template #notBookmarked> |
|||
<a class="save-search" (click)="saveQuery()" *ngIf="contentsFilterValue | async"> |
|||
<i class="icon-star-empty"></i> |
|||
</a> |
|||
</ng-template> |
|||
</ng-container> |
|||
</form> |
|||
|
|||
<sqx-onboarding-tooltip id="contentFind" [for]="inputFind" position="bottomRight" after="120000"> |
|||
Search for content using full text search over all fields and languages! |
|||
</sqx-onboarding-tooltip> |
|||
|
|||
<div class="dropdown-menu" *sqxModalView="searchModal" [sqxModalTarget]="inputFind"> |
|||
<div class="form-horizontal"> |
|||
<div [formGroup]="searchForm"> |
|||
<div class="form-group row"> |
|||
<label class="col col-2 col-form-label" for="odataSearch">Text</label> |
|||
|
|||
<div class="col col-10"> |
|||
<input type="input" class="form-control" id="search" (blur)="updateQuery()" formControlName="odataSearch" placeholder="Fulltext search" /> |
|||
</div> |
|||
</div> |
|||
|
|||
<div class="form-group row"> |
|||
<label class="col col-2 col-form-label" for="filter">Filter</label> |
|||
|
|||
<div class="col col-10"> |
|||
<input type="input" class="form-control" id="filter" (blur)="updateQuery()" formControlName="odataFilter" placeholder="{{fieldExample}} eq [VALUE]" /> |
|||
</div> |
|||
</div> |
|||
|
|||
<div class="form-group row"> |
|||
<label class="col col-2 col-form-label" for="orderBy">Order</label> |
|||
|
|||
<div class="col col-10"> |
|||
<input type="input" class="form-control" id="orderBy" (blur)="updateQuery()" formControlName="odataOrderBy" placeholder="{{fieldExample}} desc" /> |
|||
</div> |
|||
</div> |
|||
</div> |
|||
|
|||
<div class="form-check" *ngIf="enableArchive"> |
|||
<input class="form-check-input" type="checkbox" id="archivedItems" [ngModel]="archived" (ngModelChange)="archivedChanged.emit($event)" /> |
|||
<label class="form-check-label" for="archivedItems"> |
|||
Archived items |
|||
</label> |
|||
</div> |
|||
|
|||
<div class="link"> |
|||
Read more about filtering in the <a href="https://docs.squidex.io/04-guides/02-api.html" target="_blank">Documentation</a>. |
|||
</div> |
|||
</div> |
|||
</div> |
|||
|
|||
<ng-container *sqxModalView="saveQueryDialog;onRoot:true"> |
|||
<form [formGroup]="saveQueryForm.form" (ngSubmit)="saveQueryComplete()"> |
|||
<sqx-modal-dialog (closed)="saveQueryDialog.hide()"> |
|||
<ng-container title> |
|||
Name your query |
|||
</ng-container> |
|||
|
|||
<ng-container content> |
|||
<div class="form-group"> |
|||
<sqx-control-errors for="name" submitOnly="true" [submitted]="saveQueryForm.submitted | async"></sqx-control-errors> |
|||
|
|||
<input type="text" class="form-control" id="appName" formControlName="name" autocomplete="off" sqxFocusOnInit /> |
|||
</div> |
|||
</ng-container> |
|||
|
|||
<ng-container footer> |
|||
<button type="reset" class="float-left btn btn-secondary" (click)="saveQueryComplete()" [disabled]="saveQueryForm.submitted | async">Cancel</button> |
|||
<button type="submit" class="float-right btn btn-success">Create</button> |
|||
</ng-container> |
|||
</sqx-modal-dialog> |
|||
</form> |
|||
</ng-container> |
|||
@ -0,0 +1,79 @@ |
|||
/* |
|||
* Squidex Headless CMS |
|||
* |
|||
* @license |
|||
* Copyright (c) Squidex UG (haftungsbeschränkt). All rights reserved. |
|||
*/ |
|||
|
|||
import { BehaviorSubject } from 'rxjs'; |
|||
import { IMock, Mock, Times } from 'typemoq'; |
|||
|
|||
import { Queries, Query } from './queries'; |
|||
import { UIState } from './ui.state'; |
|||
|
|||
describe('Queries', () => { |
|||
const prefix = 'schemas.my-schema'; |
|||
|
|||
let uiState: IMock<UIState>; |
|||
let filter = new BehaviorSubject(''); |
|||
let queries$ = new BehaviorSubject({}); |
|||
let queries: Queries; |
|||
|
|||
beforeEach(() => { |
|||
uiState = Mock.ofType<UIState>(); |
|||
|
|||
uiState.setup(x => x.get('schemas.my-schema.queries', {})) |
|||
.returns(() => queries$); |
|||
|
|||
queries$.next({ |
|||
key1: 'query1', |
|||
key2: 'query2' |
|||
}); |
|||
|
|||
queries = new Queries(uiState.object, prefix); |
|||
}); |
|||
|
|||
it('should load queries', () => { |
|||
let converted: Query[]; |
|||
|
|||
queries.queries.subscribe(x => { |
|||
converted = x; |
|||
}); |
|||
|
|||
expect(converted!).toEqual([ |
|||
{ |
|||
name: 'key1', |
|||
nameSortable: 'KEY1', |
|||
filter: 'query1' |
|||
}, { |
|||
name: 'key2', |
|||
nameSortable: 'KEY2', |
|||
filter: 'query2' |
|||
} |
|||
]); |
|||
}); |
|||
|
|||
it('should provide key', () => { |
|||
let key: string; |
|||
|
|||
queries.getSaveKey(filter).subscribe(x => { |
|||
key = x!; |
|||
}); |
|||
|
|||
filter.next('query2'); |
|||
|
|||
expect(key!).toEqual('key2'); |
|||
}); |
|||
|
|||
it('should forward add call to state', () => { |
|||
queries.add('key3', 'filter3'); |
|||
|
|||
uiState.verify(x => x.set('schemas.my-schema.queries.key3', 'filter3'), Times.once()); |
|||
}); |
|||
|
|||
it('should forward remove call to state', () => { |
|||
queries.remove('key3'); |
|||
|
|||
uiState.verify(x => x.remove('schemas.my-schema.queries.key3'), Times.once()); |
|||
}); |
|||
}); |
|||
@ -0,0 +1,78 @@ |
|||
/* |
|||
* Squidex Headless CMS |
|||
* |
|||
* @license |
|||
* Copyright (c) Squidex UG (haftungsbeschränkt). All rights reserved. |
|||
*/ |
|||
|
|||
import { combineLatest, Observable } from 'rxjs'; |
|||
import { map } from 'rxjs/operators'; |
|||
|
|||
import { UIState } from './ui.state'; |
|||
|
|||
export interface Query { |
|||
name: string; |
|||
nameSortable?: string; |
|||
filter: string; |
|||
} |
|||
|
|||
export class Queries { |
|||
public queries: Observable<Query[]>; |
|||
|
|||
public defaultQueries: Query[] = [{ |
|||
name: 'All (newest first)', filter: '' |
|||
}, { |
|||
name: 'All (oldest first)', filter: '$orderby=lastModified asc' |
|||
}]; |
|||
|
|||
constructor( |
|||
private readonly uiState: UIState, |
|||
private readonly prefix: string |
|||
) { |
|||
this.queries = this.uiState.get(`${this.prefix}.queries`, {}).pipe( |
|||
map(x => { |
|||
let queries: Query[] = Object.keys(x).map(y => ({ name: y, filter: x[y] })); |
|||
|
|||
for (let query of queries) { |
|||
query.nameSortable = query.name.toUpperCase(); |
|||
} |
|||
|
|||
queries = queries.sort((a, b) => { |
|||
if (a.nameSortable! < b.nameSortable!) { |
|||
return -1; |
|||
} |
|||
if (a.nameSortable! > b.nameSortable!) { |
|||
return 1; |
|||
} |
|||
return 0; |
|||
}); |
|||
|
|||
return queries; |
|||
}) |
|||
); |
|||
} |
|||
|
|||
public add(key: string, filter: string) { |
|||
this.uiState.set(`${this.prefix}.queries.${key}`, filter); |
|||
} |
|||
|
|||
public remove(key: string) { |
|||
this.uiState.remove(`${this.prefix}.queries.${key}`); |
|||
} |
|||
|
|||
public getSaveKey(filter$: Observable<string>): Observable<string | null> { |
|||
return combineLatest(this.queries, filter$).pipe( |
|||
map(project => { |
|||
const filter = project[1]; |
|||
|
|||
if (filter) { |
|||
for (let query of project[0]) { |
|||
if (query.filter === filter) { |
|||
return query.name; |
|||
} |
|||
} |
|||
} |
|||
return null; |
|||
})); |
|||
} |
|||
} |
|||
@ -0,0 +1,102 @@ |
|||
/* |
|||
* Squidex Headless CMS |
|||
* |
|||
* @license |
|||
* Copyright (c) Squidex UG (haftungsbeschränkt). All rights reserved. |
|||
*/ |
|||
|
|||
import { of } from 'rxjs'; |
|||
import { IMock, It, Mock, Times } from 'typemoq'; |
|||
|
|||
import { AppsState } from '@app/shared'; |
|||
|
|||
import { UIService } from './../services/ui.service'; |
|||
import { UIState } from './ui.state'; |
|||
|
|||
describe('UIState', () => { |
|||
const app = 'my-app'; |
|||
|
|||
const oldSettings = { |
|||
mapType: 'OSM' |
|||
}; |
|||
|
|||
let appsState: IMock<AppsState>; |
|||
let uiService: IMock<UIService>; |
|||
let uiState: UIState; |
|||
|
|||
beforeEach(() => { |
|||
appsState = Mock.ofType<AppsState>(); |
|||
|
|||
appsState.setup(x => x.appName) |
|||
.returns(() => app); |
|||
|
|||
uiService = Mock.ofType<UIService>(); |
|||
|
|||
uiService.setup(x => x.getSettings(app)) |
|||
.returns(() => of(oldSettings)); |
|||
|
|||
uiService.setup(x => x.putSetting(app, It.isAnyString(), It.isAny())) |
|||
.returns(() => of({})); |
|||
|
|||
uiService.setup(x => x.deleteSetting(app, It.isAnyString())) |
|||
.returns(() => of({})); |
|||
|
|||
uiState = new UIState(appsState.object, uiService.object); |
|||
}); |
|||
|
|||
it('should load settings', () => { |
|||
expect(uiState.snapshot.settings).toEqual(oldSettings); |
|||
}); |
|||
|
|||
it('should add value to snapshot when set', () => { |
|||
uiState.set('root.nested', 123); |
|||
|
|||
expect(uiState.snapshot.settings).toEqual({ |
|||
mapType: 'OSM', |
|||
root: { |
|||
nested: 123 |
|||
} |
|||
}); |
|||
|
|||
uiState.get('root', {}).subscribe(x => { |
|||
expect(x).toEqual({ nested: 123 }); |
|||
}); |
|||
|
|||
uiState.get('root.nested', 0).subscribe(x => { |
|||
expect(x).toEqual(123); |
|||
}); |
|||
|
|||
uiState.get('root.notfound', 1337).subscribe(x => { |
|||
expect(x).toEqual(1337); |
|||
}); |
|||
|
|||
uiService.verify(x => x.putSetting(app, 'root.nested', 123), Times.once()); |
|||
}); |
|||
|
|||
it('should remove value from snapshot when removed', () => { |
|||
uiState.set('root.nested1', 123); |
|||
uiState.set('root.nested2', 123); |
|||
uiState.remove('root.nested1'); |
|||
|
|||
expect(uiState.snapshot.settings).toEqual({ |
|||
mapType: 'OSM', |
|||
root: { |
|||
nested2: 123 |
|||
} |
|||
}); |
|||
|
|||
uiState.get('root', {}).subscribe(x => { |
|||
expect(x).toEqual({ nested2: 123 }); |
|||
}); |
|||
|
|||
uiState.get('root.nested2', 0).subscribe(x => { |
|||
expect(x).toEqual(123); |
|||
}); |
|||
|
|||
uiState.get('root.nested1', 1337).subscribe(x => { |
|||
expect(x).toEqual(1337); |
|||
}); |
|||
|
|||
uiService.verify(x => x.deleteSetting(app, 'root.nested1'), Times.once()); |
|||
}); |
|||
}); |
|||
@ -0,0 +1,139 @@ |
|||
/* |
|||
* Squidex Headless CMS |
|||
* |
|||
* @license |
|||
* Copyright (c) Squidex UG (haftungsbeschränkt). All rights reserved. |
|||
*/ |
|||
|
|||
import { Injectable } from '@angular/core'; |
|||
import { distinctUntilChanged, map } from 'rxjs/operators'; |
|||
|
|||
import { State, Types } from '@app/framework'; |
|||
|
|||
import { AppsState } from './apps.state'; |
|||
|
|||
import { UIService, UISettingsDto } from './../services/ui.service'; |
|||
|
|||
interface Snapshot { |
|||
settings: object & any; |
|||
} |
|||
|
|||
@Injectable() |
|||
export class UIState extends State<Snapshot> { |
|||
public settings = |
|||
this.changes.pipe(map(x => x.settings), |
|||
distinctUntilChanged()); |
|||
|
|||
public get<T>(path: string, defaultValue: T) { |
|||
return this.settings.pipe(map(x => this.getValue(x, path, defaultValue)), |
|||
distinctUntilChanged()); |
|||
} |
|||
|
|||
constructor( |
|||
private readonly appsState: AppsState, |
|||
private readonly uiService: UIService |
|||
) { |
|||
super({ settings: { mapType: 'OSM' } }); |
|||
|
|||
if (appsState.selectedApp && Types.isFunction(appsState.selectedApp.subscribe)) { |
|||
appsState.selectedApp.subscribe(app => { |
|||
if (app) { |
|||
this.load(true); |
|||
} |
|||
}); |
|||
} else { |
|||
this.load(true); |
|||
} |
|||
} |
|||
|
|||
public load(reset = false) { |
|||
if (!reset) { |
|||
this.resetState(); |
|||
} |
|||
|
|||
this.uiService.getSettings(this.appName) |
|||
.subscribe(dtos => { |
|||
return this.next({ settings: dtos }); |
|||
}); |
|||
} |
|||
|
|||
public set(path: string, value: any) { |
|||
const { key, current, root } = this.getContainer(path); |
|||
|
|||
if (current && key) { |
|||
this.uiService.putSetting(this.appName, path, value).subscribe(); |
|||
|
|||
current[key] = value; |
|||
|
|||
this.next({ settings: root }); |
|||
} |
|||
} |
|||
|
|||
public remove(path: string) { |
|||
const { key, current, root } = this.getContainer(path); |
|||
|
|||
if (current && key) { |
|||
this.uiService.deleteSetting(this.appName, path).subscribe(); |
|||
|
|||
delete current[key]; |
|||
|
|||
this.next({ settings: root }); |
|||
} |
|||
} |
|||
|
|||
private getContainer(path: string) { |
|||
const segments = path.split('.'); |
|||
|
|||
let current = { ...this.snapshot.settings }; |
|||
|
|||
const root = current; |
|||
|
|||
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 }; |
|||
} |
|||
|
|||
private getValue<T>(setting: object & UISettingsDto, path: string, defaultValue: T) { |
|||
const segments = path.split('.'); |
|||
|
|||
let current = setting; |
|||
|
|||
for (let segment of segments) { |
|||
let temp = current[segment]; |
|||
|
|||
if (temp) { |
|||
current[segment] = temp; |
|||
} else { |
|||
return defaultValue; |
|||
} |
|||
|
|||
current = temp; |
|||
} |
|||
|
|||
return <T><any>current; |
|||
} |
|||
|
|||
private get appName() { |
|||
return this.appsState.appName; |
|||
} |
|||
} |
|||
Binary file not shown.
|
Before Width: | Height: | Size: 79 KiB After Width: | Height: | Size: 80 KiB |
Binary file not shown.
Binary file not shown.
@ -0,0 +1,125 @@ |
|||
// ==========================================================================
|
|||
// Squidex Headless CMS
|
|||
// ==========================================================================
|
|||
// Copyright (c) Squidex UG (haftungsbeschraenkt)
|
|||
// All rights reserved. Licensed under the MIT license.
|
|||
// ==========================================================================
|
|||
|
|||
using System; |
|||
using System.Threading.Tasks; |
|||
using FakeItEasy; |
|||
using Newtonsoft.Json.Linq; |
|||
using Squidex.Infrastructure.Orleans; |
|||
using Squidex.Infrastructure.States; |
|||
using Xunit; |
|||
|
|||
namespace Squidex.Domain.Apps.Entities.Apps |
|||
{ |
|||
public sealed class AppUISettingsGrainTests |
|||
{ |
|||
private readonly IStore<Guid> store = A.Fake<IStore<Guid>>(); |
|||
private readonly IPersistence<AppUISettingsGrain.State> persistence = A.Fake<IPersistence<AppUISettingsGrain.State>>(); |
|||
private readonly AppUISettingsGrain sut; |
|||
|
|||
public AppUISettingsGrainTests() |
|||
{ |
|||
A.CallTo(() => store.WithSnapshots(A<Type>.Ignored, A<Guid>.Ignored, A<Func<AppUISettingsGrain.State, Task>>.Ignored)) |
|||
.Returns(persistence); |
|||
|
|||
sut = new AppUISettingsGrain(store); |
|||
sut.OnActivateAsync(Guid.Empty).Wait(); |
|||
} |
|||
|
|||
[Fact] |
|||
public async Task Should_set_setting() |
|||
{ |
|||
await sut.SetAsync(new JObject(new JProperty("key", 15)).AsJ()); |
|||
|
|||
var actual = await sut.GetAsync(); |
|||
|
|||
var expected = |
|||
new JObject( |
|||
new JProperty("key", 15)); |
|||
|
|||
Assert.Equal(expected.ToString(), actual.Value.ToString()); |
|||
} |
|||
|
|||
[Fact] |
|||
public async Task Should_set_root_value() |
|||
{ |
|||
await sut.SetAsync("key", ((JToken)123).AsJ()); |
|||
|
|||
var actual = await sut.GetAsync(); |
|||
|
|||
var expected = |
|||
new JObject( |
|||
new JProperty("key", 123)); |
|||
|
|||
Assert.Equal(expected.ToString(), actual.Value.ToString()); |
|||
} |
|||
|
|||
[Fact] |
|||
public async Task Should_remove_root_value() |
|||
{ |
|||
await sut.SetAsync("key", ((JToken)123).AsJ()); |
|||
await sut.RemoveAsync("key"); |
|||
|
|||
var actual = await sut.GetAsync(); |
|||
|
|||
var expected = new JObject(); |
|||
|
|||
Assert.Equal(expected.ToString(), actual.Value.ToString()); |
|||
} |
|||
|
|||
[Fact] |
|||
public async Task Should_set_nested_value() |
|||
{ |
|||
await sut.SetAsync("root.nested", ((JToken)123).AsJ()); |
|||
|
|||
var actual = await sut.GetAsync(); |
|||
|
|||
var expected = |
|||
new JObject( |
|||
new JProperty("root", |
|||
new JObject( |
|||
new JProperty("nested", 123)))); |
|||
|
|||
Assert.Equal(expected.ToString(), actual.Value.ToString()); |
|||
} |
|||
|
|||
[Fact] |
|||
public async Task Should_remove_nested_value() |
|||
{ |
|||
await sut.SetAsync("root.nested", ((JToken)123).AsJ()); |
|||
await sut.RemoveAsync("root.nested"); |
|||
|
|||
var actual = await sut.GetAsync(); |
|||
|
|||
var expected = |
|||
new JObject( |
|||
new JProperty("root", new JObject())); |
|||
|
|||
Assert.Equal(expected.ToString(), actual.Value.ToString()); |
|||
} |
|||
|
|||
[Fact] |
|||
public async Task Should_throw_exception_if_nested_not_an_object() |
|||
{ |
|||
await sut.SetAsync("root.nested", ((JToken)123).AsJ()); |
|||
|
|||
await Assert.ThrowsAsync<InvalidOperationException>(() => sut.SetAsync("root.nested.value", ((JToken)123).AsJ())); |
|||
} |
|||
|
|||
[Fact] |
|||
public Task Should_do_nothing_if_deleting_and_nested_not_found() |
|||
{ |
|||
return sut.RemoveAsync("root.nested"); |
|||
} |
|||
|
|||
[Fact] |
|||
public Task Should_do_nothing_if_deleting_and_key_not_found() |
|||
{ |
|||
return sut.RemoveAsync("root"); |
|||
} |
|||
} |
|||
} |
|||
Loading…
Reference in new issue