diff --git a/src/Squidex.Domain.Apps.Entities/Apps/AppUISettings.cs b/src/Squidex.Domain.Apps.Entities/Apps/AppUISettings.cs new file mode 100644 index 000000000..e9182b39d --- /dev/null +++ b/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 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(Key(appId, userId)); + } + + private string Key(Guid appId, string userId) + { + if (!string.IsNullOrWhiteSpace(userId)) + { + return $"{appId}_{userId}"; + } + else + { + return $"{appId}"; + } + } + } +} diff --git a/src/Squidex.Domain.Apps.Entities/Apps/BackupApps.cs b/src/Squidex.Domain.Apps.Entities/Apps/BackupApps.cs index d6f389ecc..d4555fcb7 100644 --- a/src/Squidex.Domain.Apps.Entities/Apps/BackupApps.cs +++ b/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 contributors = new HashSet(); @@ -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(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(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(SettingsFile); - await grainFactory.GetGrain(appId).SetAsync(json); + await appUISettings.SetAsync(appId, null, json); } public override async Task CompleteRestoreAsync(Guid appId, BackupReader reader) diff --git a/src/Squidex.Domain.Apps.Entities/Apps/IAppUISettings.cs b/src/Squidex.Domain.Apps.Entities/Apps/IAppUISettings.cs new file mode 100644 index 000000000..d9e0f8d45 --- /dev/null +++ b/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 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); + } +} diff --git a/src/Squidex/Areas/Api/Controllers/UI/UIController.cs b/src/Squidex/Areas/Api/Controllers/UI/UIController.cs index c33c1775e..7257f70f6 100644 --- a/src/Squidex/Areas/Api/Controllers/UI/UIController.cs +++ b/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 uiOptions, IGrainFactory grainFactory) + public UIController(ICommandBus commandBus, IOptions uiOptions, IAppUISettings appUISettings) : base(commandBus) { this.uiOptions = uiOptions.Value; - this.grainFactory = grainFactory; + this.appUISettings = appUISettings; } /// @@ -70,9 +67,9 @@ namespace Squidex.Areas.Api.Controllers.UI [ApiPermission] public async Task GetSettings(string app) { - var result = await GetSettingsGrain(AppKey()).GetAsync(); + var result = await appUISettings.GetAsync(AppId, null); - return Ok(result.Value); + return Ok(result); } /// @@ -89,9 +86,9 @@ namespace Squidex.Areas.Api.Controllers.UI [ApiPermission] public async Task GetUserSettings(string app) { - var result = await GetSettingsGrain(UserKey()).GetAsync(); + var result = await appUISettings.GetAsync(AppId, UserId()); - return Ok(result.Value); + return Ok(result); } /// @@ -109,7 +106,7 @@ namespace Squidex.Areas.Api.Controllers.UI [ApiPermission] public async Task 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 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 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 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(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; } } } diff --git a/src/Squidex/Config/Domain/EntitiesServices.cs b/src/Squidex/Config/Domain/EntitiesServices.cs index b831a6da2..0865df176 100644 --- a/src/Squidex/Config/Domain/EntitiesServices.cs +++ b/src/Squidex/Config/Domain/EntitiesServices.cs @@ -96,6 +96,9 @@ namespace Squidex.Config.Domain services.AddSingletonAs() .As(); + services.AddSingletonAs() + .As(); + services.AddSingletonAs() .As(); diff --git a/src/Squidex/app/features/assets/pages/assets-filters-page.component.html b/src/Squidex/app/features/assets/pages/assets-filters-page.component.html index 8c18d686f..99919b491 100644 --- a/src/Squidex/app/features/assets/pages/assets-filters-page.component.html +++ b/src/Squidex/app/features/assets/pages/assets-filters-page.component.html @@ -28,28 +28,9 @@
- + + \ No newline at end of file diff --git a/src/Squidex/app/features/assets/pages/assets-filters-page.component.ts b/src/Squidex/app/features/assets/pages/assets-filters-page.component.ts index 9fcc66ab2..58991e740 100644 --- a/src/Squidex/app/features/assets/pages/assets-filters-page.component.ts +++ b/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; - } } \ No newline at end of file diff --git a/src/Squidex/app/features/content/pages/content/content-page.component.html b/src/Squidex/app/features/content/pages/content/content-page.component.html index 1a56a578a..81a5d4e06 100644 --- a/src/Squidex/app/features/content/pages/content/content-page.component.html +++ b/src/Squidex/app/features/content/pages/content/content-page.component.html @@ -100,7 +100,7 @@ Viewing version {{version.value}}. - + ; + 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') }; } diff --git a/src/Squidex/app/features/content/pages/contents/contents-filters-page.component.html b/src/Squidex/app/features/content/pages/contents/contents-filters-page.component.html index cc0943fa2..78da01c95 100644 --- a/src/Squidex/app/features/content/pages/contents/contents-filters-page.component.html +++ b/src/Squidex/app/features/content/pages/contents/contents-filters-page.component.html @@ -4,8 +4,8 @@ - + {{default.name}} @@ -15,7 +15,7 @@

Status Queries

+ [class.active]="isQueryUsed(status)"> {{status.name}} @@ -23,28 +23,9 @@
- + +
\ No newline at end of file diff --git a/src/Squidex/app/features/content/pages/contents/contents-filters-page.component.ts b/src/Squidex/app/features/content/pages/contents/contents-filters-page.component.ts index 4f4726d30..82e4969bb 100644 --- a/src/Squidex/app/features/content/pages/contents/contents-filters-page.component.ts +++ b/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 }) { diff --git a/src/Squidex/app/features/content/shared/content-item.component.html b/src/Squidex/app/features/content/shared/content-item.component.html index 6a82009ef..f5ed992d7 100644 --- a/src/Squidex/app/features/content/shared/content-item.component.html +++ b/src/Squidex/app/features/content/shared/content-item.component.html @@ -57,7 +57,7 @@ - + diff --git a/src/Squidex/app/features/content/shared/content-item.component.ts b/src/Squidex/app/features/content/shared/content-item.component.ts index 4dd5e0182..f5f195645 100644 --- a/src/Squidex/app/features/content/shared/content-item.component.ts +++ b/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) { diff --git a/src/Squidex/app/features/schemas/pages/schema/field.component.html b/src/Squidex/app/features/schemas/pages/schema/field.component.html index 2abde3801..921d3d8bc 100644 --- a/src/Squidex/app/features/schemas/pages/schema/field.component.html +++ b/src/Squidex/app/features/schemas/pages/schema/field.component.html @@ -98,7 +98,7 @@ [sqxSortDisabled]="!isEditable" [sqxSortModel]="nested" (sqxSort)="sortFields($event)"> -
+
diff --git a/src/Squidex/app/features/schemas/pages/schema/field.component.ts b/src/Squidex/app/features/schemas/pages/schema/field.component.ts index e64adf94e..d223ab8ed 100644 --- a/src/Squidex/app/features/schemas/pages/schema/field.component.ts +++ b/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) { diff --git a/src/Squidex/app/features/schemas/pages/schema/schema-page.component.html b/src/Squidex/app/features/schemas/pages/schema/schema-page.component.html index d55b9e6f6..c2a72eea0 100644 --- a/src/Squidex/app/features/schemas/pages/schema/schema-page.component.html +++ b/src/Squidex/app/features/schemas/pages/schema/schema-page.component.html @@ -79,7 +79,7 @@ [sqxSortDisabled]="!schema.canOrderFields" [sqxSortModel]="schema.fields" (sqxSort)="sortFields($event)"> -
+
diff --git a/src/Squidex/app/features/schemas/pages/schema/schema-page.component.ts b/src/Squidex/app/features/schemas/pages/schema/schema-page.component.ts index acd0faf18..250aedc9d 100644 --- a/src/Squidex/app/features/schemas/pages/schema/schema-page.component.ts +++ b/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() { diff --git a/src/Squidex/app/shared/components/saved-queries.component.ts b/src/Squidex/app/shared/components/saved-queries.component.ts new file mode 100644 index 000000000..f5cce5ecf --- /dev/null +++ b/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: ` + + +
+ + + ` +}) +export class SavedQueriesComponent { + @Input() + public queries: Queries; + + @Input() + public types: string; + + @Input() + public queryUsed: (saved: SavedQuery) => boolean; + + @Output() + public search = new EventEmitter(); + + public isSelectedQuery(saved: SavedQuery) { + return this.queryUsed && this.queryUsed(saved); + } + + public trackByQuery(index: number, query: { name: string }) { + return query.name; + } +} \ No newline at end of file diff --git a/src/Squidex/app/shared/declarations.ts b/src/Squidex/app/shared/declarations.ts index 722dbe3eb..40116db0c 100644 --- a/src/Squidex/app/shared/declarations.ts +++ b/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'; diff --git a/src/Squidex/app/shared/module.ts b/src/Squidex/app/shared/module.ts index 4e23fc2ee..ef19639e2 100644 --- a/src/Squidex/app/shared/module.ts +++ b/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, diff --git a/src/Squidex/app/shared/state/queries.spec.ts b/src/Squidex/app/shared/state/queries.spec.ts index 3c8d98840..b20d466c5 100644 --- a/src/Squidex/app/shared/state/queries.spec.ts +++ b/src/Squidex/app/shared/state/queries.spec.ts @@ -21,25 +21,38 @@ describe('Queries', () => { let uiState: IMock; - let queries$ = new BehaviorSubject({}); let queries: Queries; beforeEach(() => { uiState = Mock.ofType(); - 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()); }); }); \ No newline at end of file diff --git a/src/Squidex/app/shared/state/queries.ts b/src/Squidex/app/shared/state/queries.ts index d3f50047e..66b8208ab 100644 --- a/src/Squidex/app/shared/state/queries.ts +++ b/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; + public queriesShared: Observable; + public queriesUser: Observable; 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 { @@ -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; diff --git a/src/Squidex/app/shared/state/ui.state.ts b/src/Squidex/app/shared/state/ui.state.ts index 1bcd288f3..879a95cf0 100644 --- a/src/Squidex/app/shared/state/ui.state.ts +++ b/src/Squidex/app/shared/state/ui.state.ts @@ -52,6 +52,12 @@ export class UIState extends State { 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 { distinctUntilChanged()); } + public getShared(path: string, defaultValue: T) { + return this.settingsShared.pipe(map(x => this.getValue(x, path, defaultValue)), + distinctUntilChanged()); + } + + public getUser(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 { 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 { return false; } - private removeShared(path: string) { + public removeShared(path: string) { const { key, current, root } = getContainer(this.snapshot.settingsShared, path); if (current && key && current[key]) { diff --git a/tests/Squidex.Domain.Apps.Entities.Tests/Apps/AppUISettingsGrainTests.cs b/tests/Squidex.Domain.Apps.Entities.Tests/Apps/AppUISettingsGrainTests.cs index 03b406e53..78d94e85c 100644 --- a/tests/Squidex.Domain.Apps.Entities.Tests/Apps/AppUISettingsGrainTests.cs +++ b/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] diff --git a/tests/Squidex.Domain.Apps.Entities.Tests/Apps/AppUISettingsTests.cs b/tests/Squidex.Domain.Apps.Entities.Tests/Apps/AppUISettingsTests.cs new file mode 100644 index 000000000..77a1d4b83 --- /dev/null +++ b/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(); + private readonly IAppUISettingsGrain grain = A.Fake(); + private readonly AppUISettings sut; + + public AppUISettingsTests() + { + A.CallTo(() => grainFactory.GetGrain(A.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>.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>.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(); + } + } +}