From 054ec56a51a0afc3b586267a7a31cc4804dfbfcb Mon Sep 17 00:00:00 2001 From: Sebastian Date: Fri, 16 Dec 2016 20:59:41 +0100 Subject: [PATCH] User => Actor --- src/Squidex/Config/Domain/Serializers.cs | 2 +- src/Squidex/Config/Domain/WriteModule.cs | 4 +- .../Config/EventStore/MyEventStoreOptions.cs | 1 + .../Config/Identity/IdentityServices.cs | 4 +- src/Squidex/Config/Swagger/SwaggerUsage.cs | 3 +- .../Controllers/Api/Apps/AppController.cs | 1 + .../Api/History/Models/HistoryEventDto.cs | 6 ++ ...erHandler.cs => EnrichWithActorHandler.cs} | 29 +++++---- src/Squidex/app/app.module.ts | 2 + src/Squidex/app/features/settings/module.ts | 39 +++++++++-- .../pages/clients/clients-page.component.html | 13 +++- .../contributors-page.component.html | 13 +++- .../languages/languages-page.component.html | 6 +- .../angular/color-picker.component.spec.ts | 6 +- .../framework/angular/date-time.pipes.spec.ts | 12 ++++ .../app/framework/angular/date-time.pipes.ts | 9 +++ .../angular/focus-on-change.directive.spec.ts | 4 +- .../angular/focus-on-init.directive.spec.ts | 4 +- src/Squidex/app/framework/module.ts | 3 + .../app/framework/utils/date-time.spec.ts | 6 ++ src/Squidex/app/framework/utils/date-time.ts | 4 ++ .../shared/components/history.component.html | 19 ++++++ .../shared/components/history.component.scss | 7 ++ .../shared/components/history.component.ts | 59 +++++++++++++++++ src/Squidex/app/shared/declarations.ts | 2 + src/Squidex/app/shared/module.ts | 9 ++- .../app/shared/services/apps.service.ts | 3 +- .../shared/services/history.service.spec.ts | 65 +++++++++++++++++++ .../app/shared/services/history.service.ts | 55 ++++++++++++++++ src/Squidex/app/theme/_panels.scss | 5 ++ src/Squidex/appsettings.json | 2 +- src/Squidex/project.json | 4 +- 32 files changed, 360 insertions(+), 41 deletions(-) rename src/Squidex/Pipeline/CommandHandlers/{EnrichWithUserHandler.cs => EnrichWithActorHandler.cs} (59%) create mode 100644 src/Squidex/app/shared/components/history.component.html create mode 100644 src/Squidex/app/shared/components/history.component.scss create mode 100644 src/Squidex/app/shared/components/history.component.ts create mode 100644 src/Squidex/app/shared/services/history.service.spec.ts create mode 100644 src/Squidex/app/shared/services/history.service.ts diff --git a/src/Squidex/Config/Domain/Serializers.cs b/src/Squidex/Config/Domain/Serializers.cs index 9c2422a1b..574d84a53 100644 --- a/src/Squidex/Config/Domain/Serializers.cs +++ b/src/Squidex/Config/Domain/Serializers.cs @@ -26,7 +26,7 @@ namespace Squidex.Config.Domain settings.ContractResolver = new CamelCasePropertyNamesContractResolver(); settings.Converters.Add(new LanguageConverter()); settings.Converters.Add(new PropertiesBagConverter()); - settings.Converters.Add(new UserTokenConverter()); + settings.Converters.Add(new RefTokenConverter()); settings.NullValueHandling = NullValueHandling.Ignore; settings.DateFormatHandling = DateFormatHandling.IsoDateFormat; settings.DateParseHandling = DateParseHandling.DateTime; diff --git a/src/Squidex/Config/Domain/WriteModule.cs b/src/Squidex/Config/Domain/WriteModule.cs index fddd6a631..e8849b9ac 100644 --- a/src/Squidex/Config/Domain/WriteModule.cs +++ b/src/Squidex/Config/Domain/WriteModule.cs @@ -23,7 +23,7 @@ namespace Squidex.Config.Domain .As() .SingleInstance(); - builder.RegisterType() + builder.RegisterType() .As() .SingleInstance(); @@ -43,7 +43,7 @@ namespace Squidex.Config.Domain .As() .SingleInstance(); - builder.RegisterType() + builder.RegisterType() .As() .SingleInstance(); diff --git a/src/Squidex/Config/EventStore/MyEventStoreOptions.cs b/src/Squidex/Config/EventStore/MyEventStoreOptions.cs index 890598bd2..b002fbd81 100644 --- a/src/Squidex/Config/EventStore/MyEventStoreOptions.cs +++ b/src/Squidex/Config/EventStore/MyEventStoreOptions.cs @@ -5,6 +5,7 @@ // Copyright (c) Squidex Group // All rights reserved. // ========================================================================== + namespace Squidex.Config.EventStore { public sealed class MyEventStoreOptions diff --git a/src/Squidex/Config/Identity/IdentityServices.cs b/src/Squidex/Config/Identity/IdentityServices.cs index 7434b3165..07030c5f9 100644 --- a/src/Squidex/Config/Identity/IdentityServices.cs +++ b/src/Squidex/Config/Identity/IdentityServices.cs @@ -52,10 +52,10 @@ namespace Squidex.Config.Identity { options.UserInteractionOptions.ErrorUrl = "/account/error/"; }) + .AddAspNetIdentity() .AddInMemoryApiResources(GetApiResources()) .AddInMemoryIdentityResources(GetIdentityResources()) - .AddSigningCredential(certificate) - .AddAspNetIdentity(); + .AddSigningCredential(certificate); return services; } diff --git a/src/Squidex/Config/Swagger/SwaggerUsage.cs b/src/Squidex/Config/Swagger/SwaggerUsage.cs index 35ed2a082..3bf50b2f7 100644 --- a/src/Squidex/Config/Swagger/SwaggerUsage.cs +++ b/src/Squidex/Config/Swagger/SwaggerUsage.cs @@ -55,7 +55,8 @@ namespace Squidex.Config.Swagger settings.TypeMappers = new List { - new PrimitiveTypeMapper(typeof(Language), s => s.Type = JsonObjectType.String) + new PrimitiveTypeMapper(typeof(Language), s => s.Type = JsonObjectType.String), + new PrimitiveTypeMapper(typeof(RefToken), s => s.Type = JsonObjectType.String) }; settings.DocumentProcessors.Add(new XmlTagProcessor()); diff --git a/src/Squidex/Controllers/Api/Apps/AppController.cs b/src/Squidex/Controllers/Api/Apps/AppController.cs index 7af80751b..c7b72f9a5 100644 --- a/src/Squidex/Controllers/Api/Apps/AppController.cs +++ b/src/Squidex/Controllers/Api/Apps/AppController.cs @@ -54,6 +54,7 @@ namespace Squidex.Controllers.Api.Apps public async Task GetApps() { var subject = HttpContext.User.OpenIdSubject(); + var schemas = await appRepository.QueryAllAsync(subject); var response = schemas.Select(s => diff --git a/src/Squidex/Controllers/Api/History/Models/HistoryEventDto.cs b/src/Squidex/Controllers/Api/History/Models/HistoryEventDto.cs index c0e8dc4c4..839950555 100644 --- a/src/Squidex/Controllers/Api/History/Models/HistoryEventDto.cs +++ b/src/Squidex/Controllers/Api/History/Models/HistoryEventDto.cs @@ -19,6 +19,12 @@ namespace Squidex.Controllers.Api.History.Models [Required] public string Message { get; set; } + /// + /// The user who called the action. + /// + [Required] + public string Actor { get; set; } + /// /// Gets a unique id for the event. /// diff --git a/src/Squidex/Pipeline/CommandHandlers/EnrichWithUserHandler.cs b/src/Squidex/Pipeline/CommandHandlers/EnrichWithActorHandler.cs similarity index 59% rename from src/Squidex/Pipeline/CommandHandlers/EnrichWithUserHandler.cs rename to src/Squidex/Pipeline/CommandHandlers/EnrichWithActorHandler.cs index 3c7656c7e..f4c3d6590 100644 --- a/src/Squidex/Pipeline/CommandHandlers/EnrichWithUserHandler.cs +++ b/src/Squidex/Pipeline/CommandHandlers/EnrichWithActorHandler.cs @@ -1,5 +1,5 @@ // ========================================================================== -// EnrichWithSubjectHandler.cs +// EnrichWithActorHandler.cs // Squidex Headless CMS // ========================================================================== // Copyright (c) Squidex Group @@ -12,52 +12,53 @@ using Microsoft.AspNetCore.Http; using Squidex.Infrastructure; using Squidex.Infrastructure.CQRS.Commands; using Squidex.Infrastructure.Security; + // ReSharper disable InvertIf namespace Squidex.Pipeline.CommandHandlers { - public class EnrichWithUserHandler : ICommandHandler + public class EnrichWithActorHandler : ICommandHandler { private readonly IHttpContextAccessor httpContextAccessor; - public EnrichWithUserHandler(IHttpContextAccessor httpContextAccessor) + public EnrichWithActorHandler(IHttpContextAccessor httpContextAccessor) { this.httpContextAccessor = httpContextAccessor; } public Task HandleAsync(CommandContext context) { - var subjectCommand = context.Command as IUserCommand; + var subjectCommand = context.Command as IActorCommand; if (subjectCommand != null) { - var userToken = - FindUserFromSubject() ?? - FindUserFromClient(); + var actorToken = + FindActorFromSubject() ?? + FindActorFromClient(); - if (userToken == null) + if (actorToken == null) { - throw new SecurityException("No user with subject or client id available"); + throw new SecurityException("No actor with subject or client id available"); } - subjectCommand.User = userToken; + subjectCommand.Actor = actorToken; } return Task.FromResult(false); } - private UserToken FindUserFromSubject() + private RefToken FindActorFromSubject() { var subjectId = httpContextAccessor.HttpContext.User.OpenIdSubject(); - return subjectId == null ? null : new UserToken("subject", subjectId); + return subjectId == null ? null : new RefToken("subject", subjectId); } - private UserToken FindUserFromClient() + private RefToken FindActorFromClient() { var clientId = httpContextAccessor.HttpContext.User.OpenIdClientId(); - return clientId == null ? null : new UserToken("client", clientId); + return clientId == null ? null : new RefToken("client", clientId); } } } diff --git a/src/Squidex/app/app.module.ts b/src/Squidex/app/app.module.ts index 9fdc7bbf6..522ecfa96 100644 --- a/src/Squidex/app/app.module.ts +++ b/src/Squidex/app/app.module.ts @@ -22,6 +22,7 @@ import { CurrencyConfig, DecimalSeparatorConfig, DragService, + HistoryService, LanguageService, LocalStoreService, MustBeAuthenticatedGuard, @@ -76,6 +77,7 @@ export function configCurrency() { AppMustExistGuard, AuthService, DragService, + HistoryService, LanguageService, LocalStoreService, MustBeAuthenticatedGuard, diff --git a/src/Squidex/app/features/settings/module.ts b/src/Squidex/app/features/settings/module.ts index 1bc418744..87dcf03ec 100644 --- a/src/Squidex/app/features/settings/module.ts +++ b/src/Squidex/app/features/settings/module.ts @@ -8,7 +8,11 @@ import { NgModule } from '@angular/core'; import { RouterModule, Routes } from '@angular/router'; -import { SqxFrameworkModule, SqxSharedModule } from 'shared'; +import { + HistoryComponent, + SqxFrameworkModule, + SqxSharedModule +} from 'shared'; import { ClientComponent, @@ -28,13 +32,40 @@ const routes: Routes = [ }, { path: 'clients', - component: ClientsPageComponent + component: ClientsPageComponent, + children: [ + { + path: 'history', + component: HistoryComponent, + data: { + channel: 'settings.clients' + } + } + ] }, { path: 'contributors', - component: ContributorsPageComponent + component: ContributorsPageComponent, + children: [ + { + path: 'history', + component: HistoryComponent, + data: { + channel: 'settings.contributors' + } + } + ] }, { path: 'languages', - component: LanguagesPageComponent + component: LanguagesPageComponent, + children: [ + { + path: 'history', + component: HistoryComponent, + data: { + channel: 'settings.languages' + } + } + ] } ] } diff --git a/src/Squidex/app/features/settings/pages/clients/clients-page.component.html b/src/Squidex/app/features/settings/pages/clients/clients-page.component.html index 2e041c7d8..d16645f16 100644 --- a/src/Squidex/app/features/settings/pages/clients/clients-page.component.html +++ b/src/Squidex/app/features/settings/pages/clients/clients-page.component.html @@ -43,5 +43,16 @@ +
+ +
- \ No newline at end of file + + + \ No newline at end of file diff --git a/src/Squidex/app/features/settings/pages/contributors/contributors-page.component.html b/src/Squidex/app/features/settings/pages/contributors/contributors-page.component.html index 726fe9eab..2f789a9ca 100644 --- a/src/Squidex/app/features/settings/pages/contributors/contributors-page.component.html +++ b/src/Squidex/app/features/settings/pages/contributors/contributors-page.component.html @@ -63,5 +63,16 @@ +
+ +
- \ No newline at end of file + + + \ No newline at end of file diff --git a/src/Squidex/app/features/settings/pages/languages/languages-page.component.html b/src/Squidex/app/features/settings/pages/languages/languages-page.component.html index 0aa35f9df..24a87ec26 100644 --- a/src/Squidex/app/features/settings/pages/languages/languages-page.component.html +++ b/src/Squidex/app/features/settings/pages/languages/languages-page.component.html @@ -77,11 +77,13 @@ - \ No newline at end of file + + + \ No newline at end of file diff --git a/src/Squidex/app/framework/angular/color-picker.component.spec.ts b/src/Squidex/app/framework/angular/color-picker.component.spec.ts index bb57c763c..c3236318c 100644 --- a/src/Squidex/app/framework/angular/color-picker.component.spec.ts +++ b/src/Squidex/app/framework/angular/color-picker.component.spec.ts @@ -87,7 +87,7 @@ describe('ColorPickerComponent', () => { colorPicker.color = selectedColor; - colorPicker.ngOnChanges({}); + colorPicker.ngOnChanges(); expect(colorPicker.selectedColor).toBe(selectedColor); }); @@ -97,7 +97,7 @@ describe('ColorPickerComponent', () => { colorPicker.color = 'invalid'; - colorPicker.ngOnChanges({}); + colorPicker.ngOnChanges(); expect(colorPicker.selectedColor).toBe(colorPicker.palette.defaultColor); }); @@ -108,7 +108,7 @@ describe('ColorPickerComponent', () => { colorPicker.palette = undefined!; colorPicker.color = 'invalid'; - colorPicker.ngOnChanges({}); + colorPicker.ngOnChanges(); expect(colorPicker.selectedColor).toBe(Color.BLACK); }); diff --git a/src/Squidex/app/framework/angular/date-time.pipes.spec.ts b/src/Squidex/app/framework/angular/date-time.pipes.spec.ts index e5f5028af..52169dd55 100644 --- a/src/Squidex/app/framework/angular/date-time.pipes.spec.ts +++ b/src/Squidex/app/framework/angular/date-time.pipes.spec.ts @@ -11,6 +11,7 @@ import { DayOfWeekPipe, DayPipe, DurationPipe, + FromNowPipe, MonthPipe, ShortDatePipe, ShortTimePipe @@ -53,6 +54,17 @@ describe('MonthPipe', () => { }); }); +describe('FromNowPipe', () => { + it('should format to from now string', () => { + const pipe = new FromNowPipe(); + + const actual = pipe.transform(DateTime.now().addMinutes(-4), []); + const expected = '4 minutes ago'; + + expect(actual).toBe(expected); + }); +}); + describe('DayOfWeekPipe', () => { it('should format to short week of day string', () => { const pipe = new DayOfWeekPipe(); diff --git a/src/Squidex/app/framework/angular/date-time.pipes.ts b/src/Squidex/app/framework/angular/date-time.pipes.ts index efe47d056..469025a6b 100644 --- a/src/Squidex/app/framework/angular/date-time.pipes.ts +++ b/src/Squidex/app/framework/angular/date-time.pipes.ts @@ -28,6 +28,15 @@ export class MonthPipe { } } +@Pipe({ + name: 'fromNow' +}) +export class FromNowPipe { + public transform(value: DateTime, args: string[]): any { + return value.toFromNow(); + } +} + @Pipe({ name: 'dayOfWeek' }) diff --git a/src/Squidex/app/framework/angular/focus-on-change.directive.spec.ts b/src/Squidex/app/framework/angular/focus-on-change.directive.spec.ts index 133aa0a06..a5e3bbc53 100644 --- a/src/Squidex/app/framework/angular/focus-on-change.directive.spec.ts +++ b/src/Squidex/app/framework/angular/focus-on-change.directive.spec.ts @@ -33,7 +33,9 @@ describe('FocusOnChangeDirective', () => { nativeElement: {} }; - new FocusOnChangeDirective(element, renderer as Renderer).ngOnChanges({}); + const directive = new FocusOnChangeDirective(element, renderer as Renderer); + directive.select = true; + directive.ngOnChanges(); expect(calledMethods).toEqual([]); diff --git a/src/Squidex/app/framework/angular/focus-on-init.directive.spec.ts b/src/Squidex/app/framework/angular/focus-on-init.directive.spec.ts index e5f5310ea..c4c760e8e 100644 --- a/src/Squidex/app/framework/angular/focus-on-init.directive.spec.ts +++ b/src/Squidex/app/framework/angular/focus-on-init.directive.spec.ts @@ -33,7 +33,9 @@ describe('FocusOnInitDirective', () => { nativeElement: {} }; - new FocusOnInitDirective(element, renderer as Renderer).ngOnInit(); + const directive = new FocusOnInitDirective(element, renderer as Renderer); + directive.select = true; + directive.ngOnInit(); expect(calledMethods).toEqual([]); diff --git a/src/Squidex/app/framework/module.ts b/src/Squidex/app/framework/module.ts index b95782e43..88b2f8f21 100644 --- a/src/Squidex/app/framework/module.ts +++ b/src/Squidex/app/framework/module.ts @@ -22,6 +22,7 @@ import { DurationPipe, FocusOnChangeDirective, FocusOnInitDirective, + FromNowPipe, ImageDropDirective, ModalViewDirective, MoneyPipe, @@ -57,6 +58,7 @@ import { DurationPipe, FocusOnChangeDirective, FocusOnInitDirective, + FromNowPipe, ImageDropDirective, ModalViewDirective, MoneyPipe, @@ -83,6 +85,7 @@ import { DurationPipe, FocusOnChangeDirective, FocusOnInitDirective, + FromNowPipe, ImageDropDirective, ModalViewDirective, MoneyPipe, diff --git a/src/Squidex/app/framework/utils/date-time.spec.ts b/src/Squidex/app/framework/utils/date-time.spec.ts index 794f44658..c79345186 100644 --- a/src/Squidex/app/framework/utils/date-time.spec.ts +++ b/src/Squidex/app/framework/utils/date-time.spec.ts @@ -97,6 +97,12 @@ describe('DateTime', () => { expect(actual).toEqual(expected); }); + it('should print to from now string', () => { + const value = DateTime.now().addMinutes(-4); + + expect(value.toFromNow()).toBe('4 minutes ago'); + }); + it('should print to valid utc string', () => { const value = DateTime.parseISO_UTC('2013-10-16T12:13:14'); diff --git a/src/Squidex/app/framework/utils/date-time.ts b/src/Squidex/app/framework/utils/date-time.ts index 742bcc99e..8fc266420 100644 --- a/src/Squidex/app/framework/utils/date-time.ts +++ b/src/Squidex/app/framework/utils/date-time.ts @@ -232,4 +232,8 @@ export class DateTime { public toStringFormat(format: string): string { return moment(this.value).format(format); } + + public toFromNow(): string { + return moment(this.value).fromNow(); + } } \ No newline at end of file diff --git a/src/Squidex/app/shared/components/history.component.html b/src/Squidex/app/shared/components/history.component.html new file mode 100644 index 000000000..fc3be669f --- /dev/null +++ b/src/Squidex/app/shared/components/history.component.html @@ -0,0 +1,19 @@ + + +
+
+

History

+ + + + +
+ +
+
+
+ {{event.message}} +
+
+
+
\ No newline at end of file diff --git a/src/Squidex/app/shared/components/history.component.scss b/src/Squidex/app/shared/components/history.component.scss new file mode 100644 index 000000000..992daf82e --- /dev/null +++ b/src/Squidex/app/shared/components/history.component.scss @@ -0,0 +1,7 @@ +@import '_vars'; +@import '_mixins'; + +.panel { + min-width: 220px; + max-width: 1220px80px; +} \ No newline at end of file diff --git a/src/Squidex/app/shared/components/history.component.ts b/src/Squidex/app/shared/components/history.component.ts new file mode 100644 index 000000000..753d094e4 --- /dev/null +++ b/src/Squidex/app/shared/components/history.component.ts @@ -0,0 +1,59 @@ +/* + * Squidex Headless CMS + * + * @license + * Copyright (c) Sebastian Stehle. All rights reserved + */ + +import { Component, OnDestroy, OnInit } from '@angular/core'; +import { ActivatedRoute } from '@angular/router'; + +import { ImmutableArray, NotificationService } from 'framework'; + +import { AppComponentBase } from './../app-component-base'; +import { AppsStoreService } from './../services/apps-store.service'; +import { HistoryEventDto, HistoryService } from './../services/history.service'; +import { UsersProviderService } from './../services/users-provider.service'; + +const FALLBACK_NAME = 'my-app'; + +@Component({ + selector: 'sqx-history', + styleUrls: ['./history.component.scss'], + templateUrl: './history.component.html' +}) +export class HistoryComponent extends AppComponentBase implements OnDestroy, OnInit { + private interval: any; + + public events = ImmutableArray.empty(); + + constructor(appsStore: AppsStoreService, notifications: NotificationService, usersProvider: UsersProviderService, + private readonly historyService: HistoryService, + private readonly route: ActivatedRoute + ) { + super(appsStore, notifications, usersProvider); + } + + public ngOnDestroy() { + clearInterval(this.interval); + } + + public ngOnInit() { + this.load(); + + this.interval = + setInterval(() => { + this.load(); + }, 10000); + } + + public load() { + const channel = this.route.snapshot.data['channel']; + + this.appName() + .switchMap(app => this.historyService.getHistory(app, channel).retry(2)) + .subscribe(dtos => { + this.events = ImmutableArray.of(dtos); + }); + } +} \ No newline at end of file diff --git a/src/Squidex/app/shared/declarations.ts b/src/Squidex/app/shared/declarations.ts index 89537e0f7..28211a8f7 100644 --- a/src/Squidex/app/shared/declarations.ts +++ b/src/Squidex/app/shared/declarations.ts @@ -7,6 +7,7 @@ export * from './components/app-form.component'; export * from './components/dashboard-link.directive'; +export * from './components/history.component'; export * from './guards/app-must-exist.guard'; export * from './guards/must-be-authenticated.guard'; @@ -18,6 +19,7 @@ export * from './services/app-languages.service'; export * from './services/apps-store.service'; export * from './services/apps.service'; export * from './services/auth.service'; +export * from './services/history.service'; export * from './services/languages.service'; export * from './services/users-provider.service'; export * from './services/users.service'; diff --git a/src/Squidex/app/shared/module.ts b/src/Squidex/app/shared/module.ts index 676e1a37d..6d6aafb7c 100644 --- a/src/Squidex/app/shared/module.ts +++ b/src/Squidex/app/shared/module.ts @@ -11,7 +11,8 @@ import { SqxFrameworkModule } from 'framework'; import { AppFormComponent, - DashboardLinkDirective + DashboardLinkDirective, + HistoryComponent } from './declarations'; @NgModule({ @@ -20,11 +21,13 @@ import { ], declarations: [ AppFormComponent, - DashboardLinkDirective + DashboardLinkDirective, + HistoryComponent ], exports: [ AppFormComponent, - DashboardLinkDirective + DashboardLinkDirective, + HistoryComponent ] }) export class SqxSharedModule { } \ No newline at end of file diff --git a/src/Squidex/app/shared/services/apps.service.ts b/src/Squidex/app/shared/services/apps.service.ts index 6e9562bac..0a43c3cc4 100644 --- a/src/Squidex/app/shared/services/apps.service.ts +++ b/src/Squidex/app/shared/services/apps.service.ts @@ -57,8 +57,7 @@ export class AppsService { item.name, DateTime.parseISO(item.created), DateTime.parseISO(item.lastModified), - item.permission - ); + item.permission); }); }) .catch(response => handleError('Failed to load apps. Please reload.', response)); diff --git a/src/Squidex/app/shared/services/history.service.spec.ts b/src/Squidex/app/shared/services/history.service.spec.ts new file mode 100644 index 000000000..a5b296037 --- /dev/null +++ b/src/Squidex/app/shared/services/history.service.spec.ts @@ -0,0 +1,65 @@ +/* + * Squidex Headless CMS + * + * @license + * Copyright (c) Sebastian Stehle. All rights reserved + */ + +import { Response, ResponseOptions } from '@angular/http'; +import { Observable } from 'rxjs'; +import { Mock, Times } from 'typemoq'; + +import { DateTime } from 'framework'; + +import { + ApiUrlConfig, + AuthService, + HistoryEventDto, + HistoryService +} from './../'; + +describe('HistoryService', () => { + let authService: Mock; + let languageService: HistoryService; + + beforeEach(() => { + authService = Mock.ofType(AuthService); + languageService = new HistoryService(authService.object, new ApiUrlConfig('http://service/p/')); + }); + + it('should make get request to get history events', () => { + authService.setup(x => x.authGet('http://service/p/api/apps/my-app/history?channel=settings.contributors')) + .returns(() => Observable.of( + new Response( + new ResponseOptions({ + body: [{ + user: 'User1', + eventId: '1', + message: 'Message 1', + created: '2016-12-12T10:10' + }, { + user: 'User2', + eventId: '2', + message: 'Message 2', + created: '2016-12-13T10:10' + }] + }) + ) + )) + .verifiable(Times.once()); + + let events: HistoryEventDto[] = null; + + languageService.getHistory('my-app', 'settings.contributors').subscribe(result => { + events = result; + }).unsubscribe(); + + expect(events).toEqual( + [ + new HistoryEventDto('1', 'User1', 'Message 1', DateTime.parseISO_UTC('2016-12-12T10:10')), + new HistoryEventDto('2', 'User2', 'Message 2', DateTime.parseISO_UTC('2016-12-13T10:10')) + ]); + + authService.verifyAll(); + }); +}); \ No newline at end of file diff --git a/src/Squidex/app/shared/services/history.service.ts b/src/Squidex/app/shared/services/history.service.ts new file mode 100644 index 000000000..4d51c6d31 --- /dev/null +++ b/src/Squidex/app/shared/services/history.service.ts @@ -0,0 +1,55 @@ +/* + * Squidex Headless CMS + * + * @license + * Copyright (c) Sebastian Stehle. All rights reserved + */ + +import { Injectable } from '@angular/core'; +import { Observable } from 'rxjs'; + +import { + ApiUrlConfig, + DateTime, + handleError +} from 'framework'; + +import { AuthService } from './auth.service'; + +export class HistoryEventDto { + constructor( + public readonly eventId: string, + public readonly user: string, + public readonly message: string, + public readonly created: DateTime + ) { + } +} + +@Injectable() +export class HistoryService { + constructor( + private readonly authService: AuthService, + private readonly apiUrl: ApiUrlConfig + ) { + } + + public getHistory(appName: string, channel: string): Observable { + const url = this.apiUrl.buildUrl(`api/apps/${appName}/history?channel=${channel}`); + + return this.authService.authGet(url) + .map(response => response.json()) + .map(response => { + const items: any[] = response; + + return items.map(item => { + return new HistoryEventDto( + item.eventId, + item.user, + item.message, + DateTime.parseISO_UTC(item.created)); + }); + }) + .catch(response => handleError('Failed to load history. Please reload', response)); + } +} \ No newline at end of file diff --git a/src/Squidex/app/theme/_panels.scss b/src/Squidex/app/theme/_panels.scss index 4c59be0bd..bf20f50e9 100644 --- a/src/Squidex/app/theme/_panels.scss +++ b/src/Squidex/app/theme/_panels.scss @@ -117,6 +117,11 @@ $panel-sidebar: 60px; border-top: 1px solid $color-border; } + .panel-content-blank { + border: 0; + background: $color-card-footer; + } + .panel-sidebar { background: $color-card-footer; border-left: 1px solid $color-border; diff --git a/src/Squidex/appsettings.json b/src/Squidex/appsettings.json index 77f8bc5d1..f9545d4fb 100644 --- a/src/Squidex/appsettings.json +++ b/src/Squidex/appsettings.json @@ -10,7 +10,7 @@ "eventStore": { "ipAddress": "127.0.0.1", "port": 1113, - "prefix": "squidex_v4", + "prefix": "squidex_v5", "username": "admin", "password": "changeit" } diff --git a/src/Squidex/project.json b/src/Squidex/project.json index 6c3ed6c1e..d38ab314f 100644 --- a/src/Squidex/project.json +++ b/src/Squidex/project.json @@ -23,8 +23,8 @@ "Microsoft.Extensions.Logging.Debug": "1.1.0", "Microsoft.Extensions.Options.ConfigurationExtensions": "1.1.0", "MongoDB.Driver": "2.4.0", - "NJsonSchema": "6.3.6185.19861", - "NSwag.AspNetCore": "8.0.0", + "NJsonSchema": "6.6.6192.39835", + "NSwag.AspNetCore": "8.1.0", "OpenCover": "4.6.519", "ReportGenerator": "2.5.2-beta2", "Squidex.Core": "1.0.0-*",