Browse Source

Continued.

pull/414/head
Sebastian 7 years ago
parent
commit
219d1df624
  1. 67
      src/Squidex.Domain.Apps.Entities/Apps/AppUISettings.cs
  2. 9
      src/Squidex.Domain.Apps.Entities/Apps/BackupApps.cs
  3. 24
      src/Squidex.Domain.Apps.Entities/Apps/IAppUISettings.cs
  4. 39
      src/Squidex/Areas/Api/Controllers/UI/UIController.cs
  5. 3
      src/Squidex/Config/Domain/EntitiesServices.cs
  6. 27
      src/Squidex/app/features/assets/pages/assets-filters-page.component.html
  7. 16
      src/Squidex/app/features/assets/pages/assets-filters-page.component.ts
  8. 2
      src/Squidex/app/features/content/pages/content/content-page.component.html
  9. 4
      src/Squidex/app/features/content/pages/content/content-page.component.ts
  10. 33
      src/Squidex/app/features/content/pages/contents/contents-filters-page.component.html
  11. 12
      src/Squidex/app/features/content/pages/contents/contents-filters-page.component.ts
  12. 2
      src/Squidex/app/features/content/shared/content-item.component.html
  13. 3
      src/Squidex/app/features/content/shared/content-item.component.ts
  14. 2
      src/Squidex/app/features/schemas/pages/schema/field.component.html
  15. 3
      src/Squidex/app/features/schemas/pages/schema/field.component.ts
  16. 2
      src/Squidex/app/features/schemas/pages/schema/schema-page.component.html
  17. 4
      src/Squidex/app/features/schemas/pages/schema/schema-page.component.ts
  18. 87
      src/Squidex/app/shared/components/saved-queries.component.ts
  19. 1
      src/Squidex/app/shared/declarations.ts
  20. 3
      src/Squidex/app/shared/module.ts
  21. 81
      src/Squidex/app/shared/state/queries.spec.ts
  22. 39
      src/Squidex/app/shared/state/queries.ts
  23. 20
      src/Squidex/app/shared/state/ui.state.ts
  24. 1
      tests/Squidex.Domain.Apps.Entities.Tests/Apps/AppUISettingsGrainTests.cs
  25. 76
      tests/Squidex.Domain.Apps.Entities.Tests/Apps/AppUISettingsTests.cs

67
src/Squidex.Domain.Apps.Entities/Apps/AppUISettings.cs

@ -0,0 +1,67 @@
// ==========================================================================
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex UG (haftungsbeschraenkt)
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
using System;
using System.Threading.Tasks;
using Orleans;
using Squidex.Infrastructure;
using Squidex.Infrastructure.Json.Objects;
using Squidex.Infrastructure.Orleans;
namespace Squidex.Domain.Apps.Entities.Apps
{
public sealed class AppUISettings : IAppUISettings
{
private readonly IGrainFactory grainFactory;
public AppUISettings(IGrainFactory grainFactory)
{
Guard.NotNull(grainFactory, nameof(grainFactory));
this.grainFactory = grainFactory;
}
public async Task<JsonObject> GetAsync(Guid appId, string userId)
{
var result = await GetGrain(appId, userId).GetAsync();
return result.Value;
}
public Task RemoveAsync(Guid appId, string userId, string path)
{
return GetGrain(appId, userId).RemoveAsync(path);
}
public Task SetAsync(Guid appId, string userId, string path, IJsonValue value)
{
return GetGrain(appId, userId).SetAsync(path, value.AsJ());
}
public Task SetAsync(Guid appId, string userId, JsonObject settings)
{
return GetGrain(appId, userId).SetAsync(settings.AsJ());
}
private IAppUISettingsGrain GetGrain(Guid appId, string userId)
{
return grainFactory.GetGrain<IAppUISettingsGrain>(Key(appId, userId));
}
private string Key(Guid appId, string userId)
{
if (!string.IsNullOrWhiteSpace(userId))
{
return $"{appId}_{userId}";
}
else
{
return $"{appId}";
}
}
}
}

9
src/Squidex.Domain.Apps.Entities/Apps/BackupApps.cs

@ -26,6 +26,7 @@ namespace Squidex.Domain.Apps.Entities.Apps
private const string UsersFile = "Users.json";
private const string SettingsFile = "Settings.json";
private readonly IGrainFactory grainFactory;
private readonly IAppUISettings appUISettings;
private readonly IUserResolver userResolver;
private readonly IAppsByNameIndex appsByNameIndex;
private readonly HashSet<string> contributors = new HashSet<string>();
@ -36,12 +37,14 @@ namespace Squidex.Domain.Apps.Entities.Apps
public override string Name { get; } = "Apps";
public BackupApps(IGrainFactory grainFactory, IUserResolver userResolver)
public BackupApps(IGrainFactory grainFactory, IAppUISettings appUISettings, IUserResolver userResolver)
{
Guard.NotNull(grainFactory, nameof(grainFactory));
Guard.NotNull(userResolver, nameof(userResolver));
Guard.NotNull(appUISettings, nameof(appUISettings));
this.grainFactory = grainFactory;
this.appUISettings = appUISettings;
this.userResolver = userResolver;
appsByNameIndex = grainFactory.GetGrain<IAppsByNameIndex>(SingleGrain.Id);
@ -182,7 +185,7 @@ namespace Squidex.Domain.Apps.Entities.Apps
private async Task WriteSettingsAsync(BackupWriter writer, Guid appId)
{
var json = await grainFactory.GetGrain<IAppUISettingsGrain>(appId).GetAsync();
var json = await appUISettings.GetAsync(appId, null);
await writer.WriteJsonAsync(SettingsFile, json);
}
@ -191,7 +194,7 @@ namespace Squidex.Domain.Apps.Entities.Apps
{
var json = await reader.ReadJsonAttachmentAsync<JsonObject>(SettingsFile);
await grainFactory.GetGrain<IAppUISettingsGrain>(appId).SetAsync(json);
await appUISettings.SetAsync(appId, null, json);
}
public override async Task CompleteRestoreAsync(Guid appId, BackupReader reader)

24
src/Squidex.Domain.Apps.Entities/Apps/IAppUISettings.cs

@ -0,0 +1,24 @@
// ==========================================================================
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex UG (haftungsbeschraenkt)
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
using System;
using System.Threading.Tasks;
using Squidex.Infrastructure.Json.Objects;
namespace Squidex.Domain.Apps.Entities.Apps
{
public interface IAppUISettings
{
Task<JsonObject> GetAsync(Guid appId, string userId);
Task SetAsync(Guid appId, string userId, string path, IJsonValue value);
Task SetAsync(Guid appId, string userId, JsonObject settings);
Task RemoveAsync(Guid appId, string userId, string path);
}
}

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

@ -5,17 +5,14 @@
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
using System;
using System.Collections.Generic;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Mvc;
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;
using Squidex.Shared;
using Squidex.Web;
@ -26,14 +23,14 @@ namespace Squidex.Areas.Api.Controllers.UI
{
private static readonly Permission CreateAppPermission = new Permission(Permissions.AdminAppCreate);
private readonly MyUIOptions uiOptions;
private readonly IGrainFactory grainFactory;
private readonly IAppUISettings appUISettings;
public UIController(ICommandBus commandBus, IOptions<MyUIOptions> uiOptions, IGrainFactory grainFactory)
public UIController(ICommandBus commandBus, IOptions<MyUIOptions> uiOptions, IAppUISettings appUISettings)
: base(commandBus)
{
this.uiOptions = uiOptions.Value;
this.grainFactory = grainFactory;
this.appUISettings = appUISettings;
}
/// <summary>
@ -70,9 +67,9 @@ namespace Squidex.Areas.Api.Controllers.UI
[ApiPermission]
public async Task<IActionResult> GetSettings(string app)
{
var result = await GetSettingsGrain(AppKey()).GetAsync();
var result = await appUISettings.GetAsync(AppId, null);
return Ok(result.Value);
return Ok(result);
}
/// <summary>
@ -89,9 +86,9 @@ namespace Squidex.Areas.Api.Controllers.UI
[ApiPermission]
public async Task<IActionResult> GetUserSettings(string app)
{
var result = await GetSettingsGrain(UserKey()).GetAsync();
var result = await appUISettings.GetAsync(AppId, UserId());
return Ok(result.Value);
return Ok(result);
}
/// <summary>
@ -109,7 +106,7 @@ namespace Squidex.Areas.Api.Controllers.UI
[ApiPermission]
public async Task<IActionResult> PutSetting(string app, string key, [FromBody] UpdateSettingDto request)
{
await GetSettingsGrain(AppKey()).SetAsync(key, request.Value.AsJ());
await appUISettings.SetAsync(AppId, null, key, request.Value);
return NoContent();
}
@ -129,7 +126,7 @@ namespace Squidex.Areas.Api.Controllers.UI
[ApiPermission]
public async Task<IActionResult> PutUserSetting(string app, string key, [FromBody] UpdateSettingDto request)
{
await GetSettingsGrain(UserKey()).SetAsync(key, request.Value.AsJ());
await appUISettings.SetAsync(AppId, UserId(), key, request.Value);
return NoContent();
}
@ -148,7 +145,7 @@ namespace Squidex.Areas.Api.Controllers.UI
[ApiPermission]
public async Task<IActionResult> DeleteSetting(string app, string key)
{
await GetSettingsGrain(AppKey()).RemoveAsync(key);
await appUISettings.RemoveAsync(AppId, null, key);
return NoContent();
}
@ -167,22 +164,12 @@ namespace Squidex.Areas.Api.Controllers.UI
[ApiPermission]
public async Task<IActionResult> DeleteUserSetting(string app, string key)
{
await GetSettingsGrain(UserKey()).RemoveAsync(key);
await appUISettings.RemoveAsync(AppId, UserId(), key);
return NoContent();
}
private IAppUISettingsGrain GetSettingsGrain(string key)
{
return grainFactory.GetGrain<IAppUISettingsGrain>(key);
}
private string AppKey()
{
return $"{AppId}";
}
private string UserKey()
private string UserId()
{
var subject = User.OpenIdSubject();
@ -191,7 +178,7 @@ namespace Squidex.Areas.Api.Controllers.UI
throw new DomainForbiddenException("Not allowed for clients.");
}
return $"{AppId}_{subject}";
return subject;
}
}
}

3
src/Squidex/Config/Domain/EntitiesServices.cs

@ -96,6 +96,9 @@ namespace Squidex.Config.Domain
services.AddSingletonAs<AppProvider>()
.As<IAppProvider>();
services.AddSingletonAs<AppUISettings>()
.As<IAppUISettings>();
services.AddSingletonAs<AssetEnricher>()
.As<IAssetEnricher>();

27
src/Squidex/app/features/assets/pages/assets-filters-page.component.html

@ -28,28 +28,9 @@
<hr />
<div class="sidebar-section">
<h3>Saved queries</h3>
<ng-container *ngIf="queries.queries | async; let assetQueries">
<ng-container *ngIf="assetQueries.length > 0; else noQuery">
<a class="sidebar-item" *ngFor="let saved of assetQueries; trackBy: trackByQuery" (click)="search(saved.query)"
[class.active]="isSelectedQuery(saved)">
{{saved.name}}
<a class="sidebar-item-remove float-right" (click)="queries.remove(saved)" sqxStopClick>
<i class="icon-close"></i>
</a>
</a>
</ng-container>
<ng-template #noQuery>
<div class="sidebar-item text-muted">
Search for assets and use <i class="icon-star-empty"></i> icon in search form to save query for all contributors.
</div>
</ng-template>
</ng-container>
</div>
<sqx-shared-queries types="contents"
[queries]="assetsQueries"
[queryUsed]="isQueryUsed">
</sqx-shared-queries>
</ng-container>
</sqx-panel>

16
src/Squidex/app/features/assets/pages/assets-filters-page.component.ts

@ -21,7 +21,7 @@ import {
templateUrl: './assets-filters-page.component.html'
})
export class AssetsFiltersPageComponent {
public queries = new Queries(this.uiState, 'assets');
public assetsQueries = new Queries(this.uiState, 'assets');
constructor(
public readonly assetsState: AssetsState,
@ -29,6 +29,10 @@ export class AssetsFiltersPageComponent {
) {
}
public isQueryUsed = (query: SavedQuery) => {
return this.assetsState.isQueryUsed(query);
}
public search(query: Query) {
this.assetsState.search(query);
}
@ -45,15 +49,7 @@ export class AssetsFiltersPageComponent {
this.assetsState.resetTags();
}
public isSelectedQuery(saved: SavedQuery) {
return this.assetsState.isQueryUsed(saved);
}
public trackByTag(index: number, tag: { name: string }) {
public trackByTag(tag: { name: string }) {
return tag.name;
}
public trackByQuery(index: number, query: { name: string }) {
return query.name;
}
}

2
src/Squidex/app/features/content/pages/content/content-page.component.html

@ -100,7 +100,7 @@
Viewing <strong>version {{version.value}}</strong>.
</div>
<ng-container *ngFor="let field of schema.fields; trackBy: trackByField.bind(this)">
<ng-container *ngFor="let field of schema.fields; trackBy: trackByFieldFn">
<sqx-content-field
[form]="contentForm"
[formContext]="formContext"

4
src/Squidex/app/features/content/pages/content/content-page.component.ts

@ -64,6 +64,8 @@ export class ContentPageComponent extends ResourceOwner implements CanComponentD
public language: AppLanguageDto;
public languages: ImmutableArray<AppLanguageDto>;
public trackByFieldFn: Function;
@ViewChild('dueTimeSelector', { static: false })
public dueTimeSelector: DueTimeSelectorComponent;
@ -80,6 +82,8 @@ export class ContentPageComponent extends ResourceOwner implements CanComponentD
) {
super();
this.trackByFieldFn = this.trackByField.bind(this);
this.formContext = { user: authService.user, apiUrl: apiUrl.buildUrl('api') };
}

33
src/Squidex/app/features/content/pages/contents/contents-filters-page.component.html

@ -4,8 +4,8 @@
</ng-container>
<ng-container content>
<a class="sidebar-item" *ngFor="let default of schemaQueries.defaultQueries; trackBy: trackByTag" (click)="search(default.query)"
[class.active]="isSelectedQuery(default)">
<a class="sidebar-item" *ngFor="let default of schemaQueries.defaultQueries; trackBy: trackByQuery" (click)="search(default.query)"
[class.active]="isQueryUsed(default)">
{{default.name}}
</a>
@ -15,7 +15,7 @@
<h3>Status Queries</h3>
<a class="sidebar-item status" *ngFor="let status of contentsState.statusQueries | async; trackBy: trackByQuery" (click)="search(status.query)"
[class.active]="isSelectedQuery(status)">
[class.active]="isQueryUsed(status)">
<i class="icon-circle" [style.color]="status.color"></i> {{status.name}}
</a>
@ -23,28 +23,9 @@
<hr />
<div class="sidebar-section">
<h3>Saved queries</h3>
<ng-container *ngIf="schemaQueries.queries | async; let queries">
<ng-container *ngIf="queries.length > 0; else noQuery">
<a class="sidebar-item" *ngFor="let saved of queries; trackBy: trackByQuery" (click)="search(saved.query)"
[class.active]="isSelectedQuery(saved)">
{{saved.name}}
<a class="sidebar-item-remove float-right" (click)="schemaQueries.remove(saved)" sqxStopClick>
<i class="icon-close"></i>
</a>
</a>
</ng-container>
<ng-template #noQuery>
<div class="sidebar-item text-muted">
Search for contents and use <i class="icon-star-empty"></i> icon in search form to save query for all contributors.
</div>
</ng-template>
</ng-container>
</div>
<sqx-shared-queries types="contents"
[queries]="schemaQueries"
[queryUsed]="isQueryUsed">
</sqx-shared-queries>
</ng-container>
</sqx-panel>

12
src/Squidex/app/features/content/pages/contents/contents-filters-page.component.ts

@ -43,16 +43,12 @@ export class ContentsFiltersPageComponent extends ResourceOwner implements OnIni
}));
}
public search(query: Query) {
this.contentsState.search(query);
}
public isSelectedQuery(saved: SavedQuery) {
return this.contentsState.isQueryUsed(saved);
public isQueryUsed = (query: SavedQuery) => {
return this.contentsState.isQueryUsed(query);
}
public trackByTag(index: number, tag: { name: string }) {
return tag.name;
public search(query: Query) {
this.contentsState.search(query);
}
public trackByQuery(index: number, query: { name: string }) {

2
src/Squidex/app/features/content/shared/content-item.component.html

@ -57,7 +57,7 @@
<img class="user-picture" title="{{content.lastModifiedBy | sqxUserNameRef}}" [attr.src]="content.lastModifiedBy | sqxUserPictureRef" />
</td>
<td class="cell-auto cell-content" *ngFor="let field of schemaFields; let i = index; trackBy: trackByField.bind(this)" [sqxStopClick]="isDirty || (field.isInlineEditable && patchAllowed)">
<td class="cell-auto cell-content" *ngFor="let field of schemaFields; let i = index; trackBy: trackByFieldFn" [sqxStopClick]="isDirty || (field.isInlineEditable && patchAllowed)">
<ng-container *ngIf="field.isInlineEditable && patchAllowed; else displayTemplate">
<sqx-content-value-editor [form]="patchForm.form" [field]="field"></sqx-content-value-editor>
</ng-container>

3
src/Squidex/app/features/content/shared/content-item.component.ts

@ -74,6 +74,8 @@ export class ContentItemComponent implements OnChanges {
@Input('sqxContent')
public content: ContentDto;
public trackByFieldFn: Function;
public patchForm: PatchContentForm;
public patchAllowed = false;
@ -89,6 +91,7 @@ export class ContentItemComponent implements OnChanges {
private readonly changeDetector: ChangeDetectorRef,
private readonly contentsState: ContentsState
) {
this.trackByFieldFn = this.trackByField.bind(this);
}
public ngOnChanges(changes: SimpleChanges) {

2
src/Squidex/app/features/schemas/pages/schema/field.component.html

@ -98,7 +98,7 @@
[sqxSortDisabled]="!isEditable"
[sqxSortModel]="nested"
(sqxSort)="sortFields($event)">
<div class="nested-field" *ngFor="let nested of nested; trackBy: trackByField.bind(this)">
<div class="nested-field" *ngFor="let nested of nested; trackBy: trackByFieldFn">
<span class="nested-field-line-h"></span>
<sqx-field [field]="nested" [schema]="schema" [parent]="field" [patterns]="patterns"></sqx-field>

3
src/Squidex/app/features/schemas/pages/schema/field.component.ts

@ -46,6 +46,8 @@ export class FieldComponent implements OnChanges {
public dropdown = new ModalModel();
public trackByFieldFn: Function;
public isEditing = false;
public isEditable = false;
@ -58,6 +60,7 @@ export class FieldComponent implements OnChanges {
private readonly formBuilder: FormBuilder,
private readonly schemasState: SchemasState
) {
this.trackByFieldFn = this.trackByField.bind(this);
}
public ngOnChanges(changes: SimpleChanges) {

2
src/Squidex/app/features/schemas/pages/schema/schema-page.component.html

@ -79,7 +79,7 @@
[sqxSortDisabled]="!schema.canOrderFields"
[sqxSortModel]="schema.fields"
(sqxSort)="sortFields($event)">
<div *ngFor="let field of schema.fields; trackBy: trackByField.bind(this)">
<div *ngFor="let field of schema.fields; trackBy: trackByFieldFn">
<sqx-field [field]="field" [schema]="schema" [patterns]="patterns"></sqx-field>
</div>
</div>

4
src/Squidex/app/features/schemas/pages/schema/schema-page.component.ts

@ -48,6 +48,8 @@ export class SchemaPageComponent extends ResourceOwner implements OnInit {
public editSchemaDialog = new DialogModel();
public exportDialog = new DialogModel();
public trackByFieldFn: Function;
constructor(
public readonly appsState: AppsState,
public readonly schemasState: SchemasState,
@ -57,6 +59,8 @@ export class SchemaPageComponent extends ResourceOwner implements OnInit {
private readonly messageBus: MessageBus
) {
super();
this.trackByFieldFn = this.trackByField.bind(this);
}
public ngOnInit() {

87
src/Squidex/app/shared/components/saved-queries.component.ts

@ -0,0 +1,87 @@
/*
* Squidex Headless CMS
*
* @license
* Copyright (c) Squidex UG (haftungsbeschränkt). All rights reserved.
*/
import { Component, EventEmitter, Input, Output } from '@angular/core';
import { Queries } from '@app/shared/internal';
import { SavedQuery } from '../state/queries';
@Component({
selector: 'sqx-shared-queries',
template: `
<div class="sidebar-section">
<h3>Shared queries</h3>
<ng-container *ngIf="queries.queriesShared | async; let queries">
<ng-container *ngIf="queries.length > 0; else noQuery">
<a class="sidebar-item" *ngFor="let saved of queries; trackBy: trackByQuery" (click)="search.emit(saved)"
[class.active]="isSelectedQuery(saved)">
{{saved.name}}
<a class="sidebar-item-remove float-right" (click)="queries.remove(saved)" sqxStopClick>
<i class="icon-close"></i>
</a>
</a>
</ng-container>
<ng-template #noQuery>
<div class="sidebar-item text-muted">
Search for {{types}} and use <i class="icon-star-empty"></i> icon in search form to save query for all contributors.
</div>
</ng-template>
</ng-container>
</div>
<hr />
<div class="sidebar-section">
<h3>My queries</h3>
<ng-container *ngIf="queries.queriesUser | async; let queries">
<ng-container *ngIf="queries.length > 0; else noQuery">
<a class="sidebar-item" *ngFor="let saved of queries; trackBy: trackByQuery" (click)="search.emit(saved)"
[class.active]="isSelectedQuery(saved)">
{{saved.name}}
<a class="sidebar-item-remove float-right" (click)="queries.removeUser(saved)" sqxStopClick>
<i class="icon-close"></i>
</a>
</a>
</ng-container>
<ng-template #noQuery>
<div class="sidebar-item text-muted">
Search for {{types}} and use <i class="icon-star-empty"></i> icon in search form to save query for yourself.
</div>
</ng-template>
</ng-container>
</div>
`
})
export class SavedQueriesComponent {
@Input()
public queries: Queries;
@Input()
public types: string;
@Input()
public queryUsed: (saved: SavedQuery) => boolean;
@Output()
public search = new EventEmitter<SavedQuery>();
public isSelectedQuery(saved: SavedQuery) {
return this.queryUsed && this.queryUsed(saved);
}
public trackByQuery(index: number, query: { name: string }) {
return query.name;
}
}

1
src/Squidex/app/shared/declarations.ts

@ -24,6 +24,7 @@ export * from './components/pipes';
export * from './components/references-dropdown.component';
export * from './components/rich-editor.component';
export * from './components/schema-category.component';
export * from './components/saved-queries.component';
export * from './components/search-form.component';
export * from './components/table-header.component';

3
src/Squidex/app/shared/module.ts

@ -78,6 +78,7 @@ import {
RuleEventsState,
RulesService,
RulesState,
SavedQueriesComponent,
SchemaCategoryComponent,
SchemaMustExistGuard,
SchemaMustExistPublishedGuard,
@ -136,6 +137,7 @@ import {
MarkdownEditorComponent,
QueryComponent,
ReferencesDropdownComponent,
SavedQueriesComponent,
SchemaCategoryComponent,
SortingComponent,
UserDtoPicture,
@ -171,6 +173,7 @@ import {
ReferencesDropdownComponent,
RichEditorComponent,
RouterModule,
SavedQueriesComponent,
SchemaCategoryComponent,
SearchFormComponent,
UserDtoPicture,

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

@ -21,25 +21,38 @@ describe('Queries', () => {
let uiState: IMock<UIState>;
let queries$ = new BehaviorSubject({});
let queries: Queries;
beforeEach(() => {
uiState = Mock.ofType<UIState>();
uiState.setup(x => x.get('schemas.my-schema.queries', {}))
.returns(() => queries$);
const shared$ = new BehaviorSubject({
key1: '{ "fullText": "shared1" }'
});
queries$.next({
key1: '{ "fullText": "text1" }',
key2: 'text2',
const user$ = new BehaviorSubject({
key1: '{ "fullText": "user1" }'
});
const merged$ = new BehaviorSubject({
key1: '{ "fullText": "merged1" }',
key2: 'merged2',
key3: undefined
});
uiState.setup(x => x.get('schemas.my-schema.queries', {}))
.returns(() => merged$);
uiState.setup(x => x.getShared('schemas.my-schema.queries', {}))
.returns(() => shared$);
uiState.setup(x => x.getUser('schemas.my-schema.queries', {}))
.returns(() => user$);
queries = new Queries(uiState.object, prefix);
});
it('should load queries', () => {
it('should load merged queries', () => {
let converted: SavedQuery[];
queries.queries.subscribe(x => {
@ -49,12 +62,12 @@ describe('Queries', () => {
expect(converted!).toEqual([
{
name: 'key1',
query: { fullText: 'text1' },
queryJson: encodeQuery({ fullText: 'text1' })
query: { fullText: 'merged1' },
queryJson: encodeQuery({ fullText: 'merged1' })
}, {
name: 'key2',
query: { fullText: 'text2' },
queryJson: encodeQuery({ fullText: 'text2' })
query: { fullText: 'merged2' },
queryJson: encodeQuery({ fullText: 'merged2' })
}, {
name: 'key3',
query: undefined,
@ -63,6 +76,38 @@ describe('Queries', () => {
]);
});
it('should load shared queries', () => {
let converted: SavedQuery[];
queries.queriesShared.subscribe(x => {
converted = x;
});
expect(converted!).toEqual([
{
name: 'key1',
query: { fullText: 'shared1' },
queryJson: encodeQuery({ fullText: 'shared1' })
}
]);
});
it('should load user queries', () => {
let converted: SavedQuery[];
queries.queriesUser.subscribe(x => {
converted = x;
});
expect(converted!).toEqual([
{
name: 'key1',
query: { fullText: 'user1' },
queryJson: encodeQuery({ fullText: 'user1' })
}
]);
});
it('should provide key', () => {
let key: string;
@ -81,11 +126,19 @@ describe('Queries', () => {
uiState.verify(x => x.set('schemas.my-schema.queries.key3', '{"fullText":"text3"}', true), Times.once());
});
it('should forward remove call to state', () => {
queries.remove({ name: 'key3' });
it('should forward remove shared call to state', () => {
queries.removeShared({ name: 'key3' });
expect(true).toBeTruthy();
uiState.verify(x => x.removeShared('schemas.my-schema.queries.key3'), Times.once());
});
it('should forward remove user call to state', () => {
queries.removeUser({ name: 'key3' });
expect(true).toBeTruthy();
uiState.verify(x => x.remove('schemas.my-schema.queries.key3'), Times.once());
uiState.verify(x => x.removeUser('schemas.my-schema.queries.key3'), Times.once());
});
});

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

@ -6,7 +6,7 @@
*/
import { Observable } from 'rxjs';
import { map } from 'rxjs/operators';
import { map, shareReplay } from 'rxjs/operators';
import { compareStringsAsc, Types } from '@app/framework';
@ -33,6 +33,8 @@ const OLDEST_FIRST: Query = {
export class Queries {
public queries: Observable<SavedQuery[]>;
public queriesShared: Observable<SavedQuery[]>;
public queriesUser: Observable<SavedQuery[]>;
public defaultQueries: SavedQuery[] = [
{ name: 'All (newest first)', queryJson: '' },
@ -43,21 +45,32 @@ export class Queries {
private readonly uiState: UIState,
private readonly prefix: string
) {
this.queries = this.uiState.get(`${this.prefix}.queries`, {}).pipe(
map(settings => {
let queries = Object.keys(settings).map(name => parseStored(name, settings[name]));
const path = `${prefix}.queries`;
return queries.sort((a, b) => compareStringsAsc(a.name, b.name));
})
);
this.queries = this.uiState.get(path, {}).pipe(
map(settings => parseQueries(settings)), shareReplay(1));
this.queriesShared = this.uiState.getShared(path, {}).pipe(
map(settings => parseQueries(settings)), shareReplay(1));
this.queriesUser = this.uiState.getUser(path, {}).pipe(
map(settings => parseQueries(settings)), shareReplay(1));
}
public add(key: string, query: Query, user = false) {
this.uiState.set(`${this.prefix}.queries.${key}`, JSON.stringify(query), user);
this.uiState.set(this.getPath(key), JSON.stringify(query), user);
}
public remove(saved: SavedQuery) {
this.uiState.remove(`${this.prefix}.queries.${saved.name}`);
public removeShared(saved: SavedQuery) {
this.uiState.removeShared(this.getPath(saved.name));
}
public removeUser(saved: SavedQuery) {
this.uiState.removeUser(this.getPath(saved.name));
}
private getPath(key: string): string {
return `${this.prefix}.queries.${key}`;
}
public getSaveKey(query: Query): Observable<string | undefined> {
@ -76,6 +89,12 @@ export class Queries {
}
}
function parseQueries(settings: {}) {
let queries = Object.keys(settings).map(name => parseStored(name, settings[name]));
return queries.sort((a, b) => compareStringsAsc(a.name, b.name));
}
export function parseStored(name: string, raw?: string) {
if (Types.isString(raw)) {
let query: Query;

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

@ -52,6 +52,12 @@ export class UIState extends State<Snapshot> {
public settings =
this.project(x => x.settings);
public settingsShared =
this.project(x => x.settingsShared);
public settingsUser =
this.project(x => x.settingsUser);
public canReadEvents =
this.project(x => x.canReadEvents === true);
@ -69,6 +75,16 @@ export class UIState extends State<Snapshot> {
distinctUntilChanged());
}
public getShared<T>(path: string, defaultValue: T) {
return this.settingsShared.pipe(map(x => this.getValue(x, path, defaultValue)),
distinctUntilChanged());
}
public getUser<T>(path: string, defaultValue: T) {
return this.settingsUser.pipe(map(x => this.getValue(x, path, defaultValue)),
distinctUntilChanged());
}
constructor(
private readonly appsState: AppsState,
private readonly uiService: UIService,
@ -157,7 +173,7 @@ export class UIState extends State<Snapshot> {
return this.removeUser(path) || this.removeShared(path);
}
private removeUser(path: string) {
public removeUser(path: string) {
const { key, current, root } = getContainer(this.snapshot.settingsUser, path);
if (current && key && current[key]) {
@ -173,7 +189,7 @@ export class UIState extends State<Snapshot> {
return false;
}
private removeShared(path: string) {
public removeShared(path: string) {
const { key, current, root } = getContainer(this.snapshot.settingsShared, path);
if (current && key && current[key]) {

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

@ -22,7 +22,6 @@ namespace Squidex.Domain.Apps.Entities.Apps
public AppUISettingsGrainTests()
{
sut = new AppUISettingsGrain(grainState);
sut.ActivateAsync(Guid.Empty).Wait();
}
[Fact]

76
tests/Squidex.Domain.Apps.Entities.Tests/Apps/AppUISettingsTests.cs

@ -0,0 +1,76 @@
// ==========================================================================
// 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 Orleans;
using Squidex.Infrastructure.Json.Objects;
using Squidex.Infrastructure.Orleans;
using Xunit;
namespace Squidex.Domain.Apps.Entities.Apps
{
public class AppUISettingsTests
{
private readonly IGrainFactory grainFactory = A.Fake<IGrainFactory>();
private readonly IAppUISettingsGrain grain = A.Fake<IAppUISettingsGrain>();
private readonly AppUISettings sut;
public AppUISettingsTests()
{
A.CallTo(() => grainFactory.GetGrain<IAppUISettingsGrain>(A<string>.Ignored, null))
.Returns(grain);
sut = new AppUISettings(grainFactory);
}
[Fact]
public async Task Should_call_grain_when_retrieving_settings()
{
var settings = JsonValue.Object();
A.CallTo(() => grain.GetAsync())
.Returns(settings.AsJ());
var result = await sut.GetAsync(Guid.NewGuid(), "user");
Assert.Same(settings, result);
}
[Fact]
public async Task Should_call_grain_when_setting_value()
{
var value = JsonValue.Object();
await sut.SetAsync(Guid.NewGuid(), "user", "the.path", value);
A.CallTo(() => grain.SetAsync("the.path", A<J<IJsonValue>>.That.IsSameAs(value)))
.MustHaveHappened();
}
[Fact]
public async Task Should_call_grain_when_replacing_settings()
{
var value = JsonValue.Object();
await sut.SetAsync(Guid.NewGuid(), "user", value);
A.CallTo(() => grain.SetAsync(A<J<JsonObject>>.That.IsSameAs(value)))
.MustHaveHappened();
}
[Fact]
public async Task Should_call_grain_when_removing_value()
{
await sut.RemoveAsync(Guid.NewGuid(), "user", "the.path");
A.CallTo(() => grain.RemoveAsync("the.path"))
.MustHaveHappened();
}
}
}
Loading…
Cancel
Save