Browse Source

App UI integrated.

pull/208/head
Sebastian Stehle 8 years ago
parent
commit
afe55e3549
  1. 4
      src/Squidex.Domain.Apps.Core.Model/Apps/AppPattern.cs
  2. 3
      src/Squidex.Domain.Apps.Core.Model/Apps/Json/JsonAppPattern.cs
  3. 6
      src/Squidex.Domain.Apps.Entities/Apps/AppCommandMiddleware.cs
  4. 2
      src/Squidex.Infrastructure.MongoDb/Migrations/MongoMigrationStatus.cs
  5. 2
      src/Squidex/Areas/Api/Controllers/Apps/AppPatternsController.cs
  6. 1
      src/Squidex/Config/Domain/SerializationServices.cs
  7. 2
      src/Squidex/app/features/schemas/pages/schema/types/string-validation.component.ts
  8. 2
      src/Squidex/app/features/settings/declarations.ts
  9. 24
      src/Squidex/app/features/settings/module.ts
  10. 45
      src/Squidex/app/features/settings/pages/patterns/pattern.component.html
  11. 13
      src/Squidex/app/features/settings/pages/patterns/pattern.component.scss
  12. 95
      src/Squidex/app/features/settings/pages/patterns/pattern.component.ts
  13. 41
      src/Squidex/app/features/settings/pages/patterns/patterns-page.component.html
  14. 2
      src/Squidex/app/features/settings/pages/patterns/patterns-page.component.scss
  15. 87
      src/Squidex/app/features/settings/pages/patterns/patterns-page.component.ts
  16. 6
      src/Squidex/app/features/settings/settings-area.component.html
  17. 18
      src/Squidex/app/shared/services/app-patterns.service.spec.ts
  18. 11
      src/Squidex/app/shared/services/app-patterns.service.ts
  19. 41
      tools/Migrate_01/Migration01.cs

4
src/Squidex.Domain.Apps.Core.Model/Apps/AppPattern.cs

@ -43,9 +43,9 @@ namespace Squidex.Domain.Apps.Core.Apps
}
[Pure]
public AppPattern Update(string name, string pattern, string defaultMessage)
public AppPattern Update(string name, string pattern, string message)
{
return new AppPattern(name, pattern, defaultMessage);
return new AppPattern(name, pattern, message);
}
}
}

3
src/Squidex.Domain.Apps.Core.Model/Apps/Json/JsonAppPattern.cs

@ -13,9 +13,6 @@ namespace Squidex.Domain.Apps.Core.Apps.Json
{
public class JsonAppPattern
{
[JsonProperty]
public Guid Id { get; set; }
[JsonProperty]
public string Name { get; set; }

6
src/Squidex.Domain.Apps.Entities/Apps/AppCommandMiddleware.cs

@ -140,7 +140,7 @@ namespace Squidex.Domain.Apps.Entities.Apps
protected Task On(AddPattern command, CommandContext context)
{
return handler.UpdateAsync<AppDomainObject>(context, a =>
return handler.UpdateSyncedAsync<AppDomainObject>(context, a =>
{
GuardAppPattern.CanAdd(a.State.Patterns, command);
@ -150,7 +150,7 @@ namespace Squidex.Domain.Apps.Entities.Apps
protected Task On(DeletePattern command, CommandContext context)
{
return handler.UpdateAsync<AppDomainObject>(context, a =>
return handler.UpdateSyncedAsync<AppDomainObject>(context, a =>
{
GuardAppPattern.CanDelete(a.State.Patterns, command);
@ -160,7 +160,7 @@ namespace Squidex.Domain.Apps.Entities.Apps
protected async Task On(UpdatePattern command, CommandContext context)
{
await handler.UpdateAsync<AppDomainObject>(context, a =>
await handler.UpdateSyncedAsync<AppDomainObject>(context, a =>
{
GuardAppPattern.CanUpdate(a.State.Patterns, command);

2
src/Squidex.Infrastructure.MongoDb/Migrations/MongoMigrationStatus.cs

@ -40,7 +40,7 @@ namespace Squidex.Infrastructure.Migrations
await Collection.FindOneAndUpdateAsync<MongoMigrationEntity>(x => x.Id == DefaultId,
Update
.Set(x => x.IsLocked, true)
.Set(x => x.Version, 0),
.SetOnInsert(x => x.Version, 0),
UpsertFind);
return entity == null || entity.IsLocked == false;

2
src/Squidex/Areas/Api/Controllers/Apps/AppPatternsController.cs

@ -93,7 +93,7 @@ namespace Squidex.Areas.Api.Controllers.Apps
/// 404 => App not found or pattern not found.
/// </returns>
[HttpPut]
[Route("apps/{app}/patterns/{name}")]
[Route("apps/{app}/patterns/{id}/")]
[ProducesResponseType(typeof(AppPatternDto), 201)]
[ApiCosts(1)]
public async Task<IActionResult> UpdatePattern(string app, Guid id, [FromBody] UpdatePatternDto request)

1
src/Squidex/Config/Domain/SerializationServices.cs

@ -43,6 +43,7 @@ namespace Squidex.Config.Domain
settings.ContractResolver = new ConverterContractResolver(
new AppClientsConverter(),
new AppContributorsConverter(),
new AppPatternsConverter(),
new ClaimsPrincipalConverter(),
new InstantConverter(),
new LanguageConverter(),

2
src/Squidex/app/features/schemas/pages/schema/types/string-validation.component.ts

@ -80,6 +80,8 @@ export class StringValidationComponent implements OnDestroy, OnInit {
}
this.setPatternName();
});
this.setPatternName();
}
public setPattern(pattern: AppPatternDto) {

2
src/Squidex/app/features/settings/declarations.ts

@ -10,6 +10,8 @@ export * from './pages/clients/clients-page.component';
export * from './pages/contributors/contributors-page.component';
export * from './pages/languages/language.component';
export * from './pages/languages/languages-page.component';
export * from './pages/patterns/pattern.component';
export * from './pages/patterns/patterns-page.component';
export * from './pages/plans/plans-page.component';
export * from './settings-area.component';

24
src/Squidex/app/features/settings/module.ts

@ -22,6 +22,8 @@ import {
ContributorsPageComponent,
LanguageComponent,
LanguagesPageComponent,
PatternComponent,
PatternsPageComponent,
PlansPageComponent,
SettingsAreaComponent
} from './declarations';
@ -97,6 +99,26 @@ const routes: Routes = [
}
}
]
},
{
path: 'patterns',
component: PatternsPageComponent,
children: [
{
path: 'history',
component: HistoryComponent,
data: {
channel: 'settings.patterns'
}
},
{
path: 'help',
component: HelpComponent,
data: {
helpPage: '05-integrated/patterns'
}
}
]
}
]
}
@ -115,6 +137,8 @@ const routes: Routes = [
ContributorsPageComponent,
LanguageComponent,
LanguagesPageComponent,
PatternComponent,
PatternsPageComponent,
PlansPageComponent,
SettingsAreaComponent
]

45
src/Squidex/app/features/settings/pages/patterns/pattern.component.html

@ -0,0 +1,45 @@
<div class="table-items-row" [class.table-items-footer]="!pattern">
<form [formGroup]="editForm" (ngSubmit)="save()" class="row no-gutters">
<div class="col col-name">
<sqx-control-errors for="name" [submitted]="editFormSubmitted"></sqx-control-errors>
<input type="text" class="form-control" id="pattern-name" maxlength="100" formControlName="name" placeholder="Name" />
</div>
<div class="col pl-2 pr-2">
<sqx-control-errors for="pattern" [submitted]="editFormSubmitted"></sqx-control-errors>
<input type="text" class="form-control" id="pattern-pattern" maxlength="1000" formControlName="pattern" placeholder="Pattern" />
</div>
<div class="col col-message">
<sqx-control-errors for="message" [submitted]="editFormSubmitted"></sqx-control-errors>
<input type="text" class="form-control" id="pattern-message" maxlength="1000" formControlName="message" placeholder="Message" />
</div>
<div class="col col-auto pl-2 col-options" *ngIf="pattern">
<button type="submit" class="btn btn-primary" [class.disabled]="!editForm.touched">
<i class="icon-checkmark"></i>
</button>
<button type="button" class="btn btn-link btn-danger"
(sqxConfirmClick)="removing.emit(pattern)"
confirmTitle="Remove pattern"
confirmText="Do you really want to remove this pattern?">
<i class="icon-bin2"></i>
</button>
</div>
<div class="col col-auto pl-2 col-options" *ngIf="!pattern">
<button type="submit" class="btn btn-success">
<i class="icon-add"></i>
</button>
<button type="reset" class="btn btn-link btn-decent" (click)="cancel()">
<i class="icon-close"></i>
</button>
</div>
</form>
</div>

13
src/Squidex/app/features/settings/pages/patterns/pattern.component.scss

@ -0,0 +1,13 @@
@import '_vars';
@import '_mixins';
.col-options {
min-width: 7.5rem;
max-width: 7.5rem;
}
.col-name,
.col-message {
min-width: 10rem;
max-width: 10rem;
}

95
src/Squidex/app/features/settings/pages/patterns/pattern.component.ts

@ -0,0 +1,95 @@
/*
* Squidex Headless CMS
*
* @license
* Copyright (c) Sebastian Stehle. All rights reserved
*/
import { Component, EventEmitter, Input, OnInit, Output } from '@angular/core';
import { FormBuilder, Validators } from '@angular/forms';
import {
AppPatternDto,
fadeAnimation,
ValidatorsEx,
UpdatePatternDto
} from 'shared';
@Component({
selector: 'sqx-pattern',
styleUrls: ['./pattern.component.scss'],
templateUrl: './pattern.component.html',
animations: [
fadeAnimation
]
})
export class PatternComponent implements OnInit {
@Input()
public isNew = false;
@Input()
public pattern: AppPatternDto;
@Output()
public removing = new EventEmitter<any>();
@Output()
public updating = new EventEmitter<UpdatePatternDto>();
public editFormSubmitted = false;
public editForm =
this.formBuilder.group({
name: [
'',
[
Validators.required,
Validators.maxLength(100),
ValidatorsEx.pattern('[A-z0-9]+[A-z0-9\- ]*[A-z0-9]', 'Name can only contain letters, numbers, dashes and spaces.')
]
],
pattern: [
'',
[
Validators.required
]
],
message: [
'',
[
Validators.maxLength(1000)
]
]
});
constructor(
private readonly formBuilder: FormBuilder
) {
}
public ngOnInit() {
const pattern = this.pattern;
if (pattern) {
this.editForm.setValue({ name: pattern.name, pattern: pattern.pattern, message: pattern.message || '' });
}
}
public cancel() {
this.editFormSubmitted = false;
this.editForm.reset();
}
public save() {
this.editFormSubmitted = true;
if (this.editForm.valid) {
const requestDto = new UpdatePatternDto(
this.editForm.controls['name'].value,
this.editForm.controls['pattern'].value,
this.editForm.controls['message'].value);
this.updating.emit(requestDto);
}
}
}

41
src/Squidex/app/features/settings/pages/patterns/patterns-page.component.html

@ -0,0 +1,41 @@
<sqx-title message="{app} | Patterns | Settings" parameter1="app" [value1]="ctx.appName"></sqx-title>
<sqx-panel desiredWidth="60rem">
<div class="panel-header">
<div class="panel-title-row">
<h3 class="panel-title">Patterns</h3>
</div>
<a class="panel-close" sqxParentLink>
<i class="icon-close"></i>
</a>
</div>
<div class="panel-main">
<div class="panel-content panel-content-scroll">
<div *ngIf="appPatterns">
<div *ngFor="let pattern of appPatterns.patterns">
<sqx-pattern [pattern]="pattern"
(removing)="removePattern(pattern)"
(updating)="updatePattern(pattern, $event)">
</sqx-pattern>
</div>
<sqx-pattern [isNew]="true"
(updating)="addPattern($event)">
</sqx-pattern>
</div>
</div>
<div class="panel-sidebar">
<a class="panel-link" routerLink="history" routerLinkActive="active">
<i class="icon-time"></i>
</a>
<a class="panel-link" routerLink="help" routerLinkActive="active">
<i class="icon-help"></i>
</a>
</div>
</div>
</sqx-panel>
<router-outlet></router-outlet>

2
src/Squidex/app/features/settings/pages/patterns/patterns-page.component.scss

@ -0,0 +1,2 @@
@import '_vars';
@import '_mixins';

87
src/Squidex/app/features/settings/pages/patterns/patterns-page.component.ts

@ -0,0 +1,87 @@
/*
* Squidex Headless CMS
*
* @license
* Copyright (c) Sebastian Stehle. All rights reserved
*/
import { Component, OnInit } from '@angular/core';
import {
AppContext,
AppPatternDto,
AppPatternsDto,
AppPatternsService,
HistoryChannelUpdated,
UpdatePatternDto
} from 'shared';
@Component({
selector: 'sqx-patterns-page',
styleUrls: ['./patterns-page.component.scss'],
templateUrl: './patterns-page.component.html',
providers: [
AppContext
]
})
export class PatternsPageComponent implements OnInit {
public appPatterns: AppPatternsDto;
constructor(public readonly ctx: AppContext,
private readonly appPatternsService: AppPatternsService
) {
}
public ngOnInit() {
this.load();
}
public load() {
this.appPatternsService.getPatterns(this.ctx.appName).retry(2)
.subscribe(dtos => {
this.updatePatterns(dtos);
}, error => {
this.ctx.notifyError(error);
});
}
public addPattern(pattern: AppPatternDto) {
const requestDto = new UpdatePatternDto(pattern.name, pattern.pattern, pattern.message);
this.appPatternsService.postPattern(this.ctx.appName, requestDto, this.appPatterns.version)
.subscribe(dto => {
this.updatePatterns(this.appPatterns.addPattern(dto.payload, dto.version));
}, error => {
this.ctx.notifyError(error);
});
}
public updatePattern(pattern: AppPatternDto, update: UpdatePatternDto) {
this.appPatternsService.putPattern(this.ctx.appName, pattern.patternId, update, this.appPatterns.version)
.subscribe(dto => {
this.updatePatterns(this.appPatterns.updatePattern(pattern.update(update), dto.version));
}, error => {
this.ctx.notifyError(error);
});
}
public removePattern(pattern: AppPatternDto) {
this.appPatternsService.deletePattern(this.ctx.appName, pattern.patternId, this.appPatterns.version)
.subscribe(dto => {
this.updatePatterns(this.appPatterns.deletePattern(pattern, dto.version));
}, error => {
this.ctx.notifyError(error);
});
}
private updatePatterns(patterns: AppPatternsDto) {
this.appPatterns =
new AppPatternsDto(
patterns.patterns.sort((a, b) => {
return a.name.localeCompare(b.name);
}),
patterns.version);
this.ctx.bus.emit(new HistoryChannelUpdated());
}
}

6
src/Squidex/app/features/settings/settings-area.component.html

@ -32,6 +32,12 @@
<i class="icon-angle-right"></i>
</a>
</li>
<li class="nav-item">
<a class="nav-link" routerLink="patterns" routerLinkActive="active">
Patterns
<i class="icon-angle-right"></i>
</a>
</li>
<li class="nav-item" *ngIf="ctx.app.permission === 'Owner'">
<a class="nav-link" routerLink="plans" routerLinkActive="active">
Update Plan

18
src/Squidex/app/shared/services/app-patterns.service.spec.ts

@ -34,7 +34,7 @@ describe('ApppatternsDto', () => {
it('should update patterns when removing pattern', () => {
const patterns_1 = new AppPatternsDto([pattern1, pattern2], version);
const patterns_2 = patterns_1.removePattern(pattern1, newVersion);
const patterns_2 = patterns_1.deletePattern(pattern1, newVersion);
expect(patterns_2.patterns).toEqual([pattern2]);
expect(patterns_2.version).toEqual(newVersion);
@ -61,6 +61,8 @@ describe('AppPatternDto', () => {
});
describe('AppPatternsService', () => {
const version = new Version('1');
beforeEach(() => {
TestBed.configureTestingModule({
imports: [
@ -123,14 +125,14 @@ describe('AppPatternsService', () => {
let pattern: AppPatternDto | null = null;
patternService.postPattern('my-app', dto, new Version()).subscribe(result => {
pattern = result;
patternService.postPattern('my-app', dto, version).subscribe(result => {
pattern = result.payload;
});
const req = httpMock.expectOne('http://service/p/api/apps/my-app/patterns');
expect(req.request.method).toEqual('POST');
expect(req.request.headers.get('E-Tag')).toBeNull();
expect(req.request.headers.get('If-Match')).toEqual(version.value);
req.flush({
patternId: '1',
@ -147,12 +149,12 @@ describe('AppPatternsService', () => {
const dto = new UpdatePatternDto('Number', '[0-9]', 'Message1');
patternService.updatePattern('my-app', '1', dto, new Version()).subscribe();
patternService.putPattern('my-app', '1', dto, version).subscribe();
const req = httpMock.expectOne('http://service/p/api/apps/my-app/patterns/1');
expect(req.request.method).toEqual('PUT');
expect(req.request.headers.get('E-Tag')).toBeNull();
expect(req.request.headers.get('If-Match')).toEqual(version.value);
req.flush({});
}));
@ -160,12 +162,12 @@ describe('AppPatternsService', () => {
it('should make delete request to remove pattern',
inject([AppPatternsService, HttpTestingController], (patternService: AppPatternsService, httpMock: HttpTestingController) => {
patternService.deletePattern('my-app', '1', new Version('1')).subscribe();
patternService.deletePattern('my-app', '1', version).subscribe();
const req = httpMock.expectOne('http://service/p/api/apps/my-app/patterns/1');
expect(req.request.method).toEqual('DELETE');
expect(req.request.headers.get('If-Match')).toEqual(new Version('1').value);
expect(req.request.headers.get('If-Match')).toEqual(version.value);
req.flush({});
}));

11
src/Squidex/app/shared/services/app-patterns.service.ts

@ -14,7 +14,8 @@ import 'framework/angular/http-extensions';
import {
ApiUrlConfig,
HTTP,
Version
Version,
Versioned
} from 'framework';
export class AppPatternsDto {
@ -32,7 +33,7 @@ export class AppPatternsDto {
return new AppPatternsDto(this.patterns.map(p => p.patternId === pattern.patternId ? pattern : p), version);
}
public removePattern(pattern: AppPatternDto, version: Version) {
public deletePattern(pattern: AppPatternDto, version: Version) {
return new AppPatternsDto(this.patterns.filter(c => c.patternId !== pattern.patternId), version);
}
}
@ -95,7 +96,7 @@ export class AppPatternsService {
.pretifyError('Failed to add pattern. Please reload.');
}
public postPattern(appName: string, pattern: UpdatePatternDto, version: Version): Observable<AppPatternDto> {
public postPattern(appName: string, pattern: UpdatePatternDto, version: Version): Observable<Versioned<AppPatternDto>> {
const url = this.apiUrl.buildUrl(`api/apps/${appName}/patterns`);
return HTTP.postVersioned<any>(this.http, url, pattern, version)
@ -111,14 +112,14 @@ export class AppPatternsService {
.pretifyError('Failed to add pattern. Please reload.');
}
public updatePattern(appName: string, id: string, pattern: UpdatePatternDto, version: Version): Observable<any> {
public putPattern(appName: string, id: string, pattern: UpdatePatternDto, version: Version): Observable<Versioned<any>> {
const url = this.apiUrl.buildUrl(`api/apps/${appName}/patterns/${id}`);
return HTTP.putVersioned(this.http, url, pattern, version)
.pretifyError('Failed to update pattern. Please reload.');
}
public deletePattern(appName: string, id: string, version: Version): Observable<AppPatternDto> {
public deletePattern(appName: string, id: string, version: Version): Observable<Versioned<any>> {
const url = this.apiUrl.buildUrl(`api/apps/${appName}/patterns/${id}`);
return HTTP.deleteVersioned(this.http, url, version)

41
tools/Migrate_01/Migration01.cs

@ -6,7 +6,6 @@
// All rights reserved.
// ==========================================================================
using System;
using System.Threading;
using System.Threading.Tasks;
using Squidex.Domain.Apps.Core.Schemas;
@ -21,18 +20,15 @@ using Squidex.Infrastructure;
using Squidex.Infrastructure.EventSourcing;
using Squidex.Infrastructure.Migrations;
using Squidex.Infrastructure.States;
using Squidex.Infrastructure.Tasks;
namespace Migrate_01
{
public sealed class Migration01 : IMigration, IEventSubscriber
public sealed class Migration01 : IMigration
{
private readonly FieldRegistry fieldRegistry;
private readonly IEventStore eventStore;
private readonly IEventDataFormatter eventDataFormatter;
private readonly IStateFactory stateFactory;
private readonly Timer timer;
private readonly TaskCompletionSource<object> subscriptionTcs = new TaskCompletionSource<object>();
public int FromVersion { get; } = 0;
@ -48,30 +44,12 @@ namespace Migrate_01
this.eventDataFormatter = eventDataFormatter;
this.eventStore = eventStore;
this.stateFactory = stateFactory;
timer = new Timer(d => subscriptionTcs.TrySetResult(true));
}
public async Task UpdateAsync()
{
var subscription = eventStore.CreateSubscription(this, ".*");
try
{
await subscriptionTcs.Task;
}
finally
{
await subscription.StopAsync();
}
}
public async Task OnEventAsync(IEventSubscription subscription, StoredEvent storedEvent)
{
try
await eventStore.GetEventsAsync(async storedEvent =>
{
timer.Change(Timeout.Infinite, Timeout.Infinite);
var @event = ParseKnownEvent(storedEvent);
if (@event != null)
@ -111,20 +89,7 @@ namespace Migrate_01
await app.WriteStateAsync(version);
}
}
timer.Change(5000, 0);
}
catch (Exception ex)
{
subscriptionTcs.SetException(ex);
}
}
public Task OnErrorAsync(IEventSubscription subscription, Exception exception)
{
subscriptionTcs.TrySetException(exception);
return TaskHelper.Done;
}, CancellationToken.None);
}
private Envelope<IEvent> ParseKnownEvent(StoredEvent storedEvent)

Loading…
Cancel
Save