Browse Source

Refactorings

pull/1/head
Sebastian 9 years ago
parent
commit
1e46482e04
  1. 20
      src/Squidex.Store.MongoDb/Contents/MongoContentRepository.cs
  2. 10
      src/Squidex.Write/Contents/ContentCommandHandler.cs
  3. 38
      src/Squidex.Write/Contents/ContentDomainObject.cs
  4. 4
      src/Squidex.Write/Schemas/SchemaDomainObject.cs
  5. 22
      src/Squidex/Controllers/ContentApi/ContentsController.cs
  6. 174
      src/Squidex/Controllers/ContentApi/Generator/SchemasSwaggerGenerator.cs
  7. 1
      src/Squidex/app/features/content/declarations.ts
  8. 2
      src/Squidex/app/features/content/module.ts
  9. 4
      src/Squidex/app/features/content/pages/content/content-field.component.html
  10. 14
      src/Squidex/app/features/content/pages/content/content-field.component.ts
  11. 1
      src/Squidex/app/features/content/pages/content/content-page.component.html
  12. 92
      src/Squidex/app/features/content/pages/content/content-page.component.ts
  13. 34
      src/Squidex/app/features/content/pages/contents/contents-page.component.html
  14. 14
      src/Squidex/app/features/content/pages/contents/contents-page.component.scss
  15. 117
      src/Squidex/app/features/content/pages/contents/contents-page.component.ts
  16. 23
      src/Squidex/app/features/content/pages/messages.ts
  17. 3
      src/Squidex/app/features/schemas/pages/schemas/schemas-page.component.html
  18. 7
      src/Squidex/app/framework/utils/immutable-array.ts
  19. 32
      src/Squidex/app/shared/services/contents.service.spec.ts
  20. 18
      src/Squidex/app/shared/services/contents.service.ts
  21. 20
      src/Squidex/app/theme/_bootstrap.scss
  22. 28
      tests/Squidex.Write.Tests/Contents/ContentCommandHandlerTests.cs
  23. 72
      tests/Squidex.Write.Tests/Contents/ContentDomainObjectTests.cs

20
src/Squidex.Store.MongoDb/Contents/MongoContentRepository.cs

@ -182,6 +182,26 @@ namespace Squidex.Store.MongoDb.Contents
});
}
protected Task On(ContentPublished @event, EnvelopeHeaders headers)
{
var collection = GetCollection(headers.SchemaId());
return collection.UpdateAsync(headers, x =>
{
x.IsPublished = true;
});
}
protected Task On(ContentUnpublished @event, EnvelopeHeaders headers)
{
var collection = GetCollection(headers.SchemaId());
return collection.UpdateAsync(headers, x =>
{
x.IsPublished = false;
});
}
protected Task On(ContentDeleted @event, EnvelopeHeaders headers)
{
var collection = GetCollection(headers.SchemaId());

10
src/Squidex.Write/Contents/ContentCommandHandler.cs

@ -57,6 +57,16 @@ namespace Squidex.Write.Contents
await handler.UpdateAsync<ContentDomainObject>(command, s => s.Update(command));
}
protected Task On(PublishContent command, CommandContext context)
{
return handler.UpdateAsync<ContentDomainObject>(command, s => s.Publish(command));
}
protected Task On(UnpublishContent command, CommandContext context)
{
return handler.UpdateAsync<ContentDomainObject>(command, s => s.Unpublish(command));
}
protected Task On(DeleteContent command, CommandContext context)
{
return handler.UpdateAsync<ContentDomainObject>(command, s => s.Delete(command));

38
src/Squidex.Write/Contents/ContentDomainObject.cs

@ -21,12 +21,18 @@ namespace Squidex.Write.Contents
{
private bool isDeleted;
private bool isCreated;
private bool isPublished;
public bool IsDeleted
{
get { return isDeleted; }
}
public bool IsPublished
{
get { return isPublished; }
}
public ContentDomainObject(Guid id, int version)
: base(id, version)
{
@ -37,6 +43,16 @@ namespace Squidex.Write.Contents
isCreated = true;
}
protected void On(ContentPublished @event)
{
isPublished = true;
}
protected void On(ContentUnpublished @event)
{
isPublished = false;
}
protected void On(ContentDeleted @event)
{
isDeleted = true;
@ -75,6 +91,28 @@ namespace Squidex.Write.Contents
return this;
}
public ContentDomainObject Publish(PublishContent command)
{
Guard.NotNull(command, nameof(command));
VerifyCreatedAndNotDeleted();
RaiseEvent(SimpleMapper.Map(command, new ContentPublished()));
return this;
}
public ContentDomainObject Unpublish(UnpublishContent command)
{
Guard.NotNull(command, nameof(command));
VerifyCreatedAndNotDeleted();
RaiseEvent(SimpleMapper.Map(command, new ContentUnpublished()));
return this;
}
private void VerifyNotCreated()
{
if (isCreated)

4
src/Squidex.Write/Schemas/SchemaDomainObject.cs

@ -207,6 +207,8 @@ namespace Squidex.Write.Schemas
public SchemaDomainObject Publish(PublishSchema command)
{
Guard.NotNull(command, nameof(command));
VerifyCreatedAndNotDeleted();
RaiseEvent(new SchemaPublished());
@ -216,6 +218,8 @@ namespace Squidex.Write.Schemas
public SchemaDomainObject Unpublish(UnpublishSchema command)
{
Guard.NotNull(command, nameof(command));
VerifyCreatedAndNotDeleted();
RaiseEvent(new SchemaUnpublished());

22
src/Squidex/Controllers/ContentApi/ContentsController.cs

@ -126,6 +126,28 @@ namespace Squidex.Controllers.ContentApi
return NoContent();
}
[HttpPut]
[Route("content/{app}/{name}/{id}/publish")]
public async Task<IActionResult> PublishContent(Guid id)
{
var command = new PublishContent { AggregateId = id };
await CommandBus.PublishAsync(command);
return NoContent();
}
[HttpPut]
[Route("content/{app}/{name}/{id}/unpublish")]
public async Task<IActionResult> UnpublishContent(Guid id)
{
var command = new UnpublishContent { AggregateId = id };
await CommandBus.PublishAsync(command);
return NoContent();
}
[HttpDelete]
[Route("content/{app}/{name}/{id}")]
public async Task<IActionResult> PutContent(Guid id)

174
src/Squidex/Controllers/ContentApi/Generator/SchemasSwaggerGenerator.cs

@ -24,6 +24,7 @@ using Squidex.Infrastructure;
using Squidex.Pipeline.Swagger;
using Squidex.Read.Apps;
using Squidex.Read.Schemas;
// ReSharper disable InvertIf
// ReSharper disable SuggestBaseTypeForParameter
// ReSharper disable PrivateFieldCanBeConvertedToLocalVariable
@ -81,6 +82,7 @@ When you change the field to be localizable the value will become the value for
GenerateSchemasOperations(schemas);
GenerateSecurityDefinitions();
GenerateSecurityRequirements();
GenerateDefaultErrors();
return document;
}
@ -152,6 +154,14 @@ When you change the field to be localizable the value will become the value for
}
}
private void GenerateDefaultErrors()
{
foreach (var operation in document.Paths.Values.SelectMany(x => x.Values))
{
operation.Responses.Add("500", new SwaggerResponse { Description = "Operations failed with internal server error.", Schema = errorDtoSchema });
}
}
private void GenerateSchemasOperations(IEnumerable<ISchemaEntityWithSchema> schemas)
{
foreach (var schema in schemas.Select(x => x.Schema))
@ -170,123 +180,135 @@ When you change the field to be localizable the value will become the value for
Name = schemaName, Description = $"API to managed {schemaName} content elements."
});
var noIdItemOperations =
document.Paths.GetOrAdd($"{appBasePath}/{schema.Name}/", k => new SwaggerOperations());
var idItemOperations =
document.Paths.GetOrAdd($"{appBasePath}/{schema.Name}/{{id}}/", k => new SwaggerOperations());
GenerateSchemaQueryOperation(noIdItemOperations, schema, schemaName);
GenerateSchemaCreateOperation(noIdItemOperations, schema, schemaName);
GenerateSchemaGetOperation(idItemOperations, schema, schemaName);
GenerateSchemaUpdateOperation(idItemOperations, schema, schemaName);
GenerateSchemaDeleteOperation(idItemOperations, schemaName);
var schemaOperations = new List<SwaggerOperations>
{
GenerateSchemaQueryOperation(schema, schemaName),
GenerateSchemaCreateOperation(schema, schemaName),
GenerateSchemaGetOperation(schema, schemaName),
GenerateSchemaUpdateOperation(schema, schemaName),
GenerateSchemaPublishOperation(schema, schemaName),
GenerateSchemaUnpublishOperation(schema, schemaName),
GenerateSchemaDeleteOperation(schema, schemaName)
};
foreach (var operation in idItemOperations.Values.Union(noIdItemOperations.Values))
foreach (var operation in schemaOperations.SelectMany(x => x.Values).Distinct())
{
operation.Tags = new List<string> { schemaName };
}
}
foreach (var operation in idItemOperations.Values)
private SwaggerOperations GenerateSchemaQueryOperation(Schema schema, string schemaName)
{
return AddOperation(SwaggerOperationMethod.Get, null, $"{appBasePath}/{schema.Name}", operation =>
{
operation.Responses.Add("404",
new SwaggerResponse { Description = $"App, schema or {schemaName} not found." });
operation.Summary = $"Queries {schemaName} content elements.";
operation.Parameters.AddPathParameter("id", JsonObjectType.String, $"The id of the {schemaName} (GUID).");
}
operation.Parameters.AddQueryParameter("take", JsonObjectType.Number, "The number of elements to take.");
operation.Parameters.AddQueryParameter("skip", JsonObjectType.Number, "The number of elements to skip.");
operation.Parameters.AddQueryParameter("query", JsonObjectType.String, "Optional full text query skip.");
var responseSchema = CreateContentsSchema(schema.BuildSchema(languages, AppendSchema), schemaName, schema.Name);
operation.Responses.Add("200",
new SwaggerResponse { Description = $"{schemaName} content elements retrieved.", Schema = responseSchema });
});
}
private void GenerateSchemaQueryOperation(SwaggerOperations operations, Schema schema, string schemaName)
private SwaggerOperations GenerateSchemaGetOperation(Schema schema, string schemaName)
{
var operation = new SwaggerOperation
return AddOperation(SwaggerOperationMethod.Get, schemaName, $"{appBasePath}/{schema.Name}/{{id}}", operation =>
{
Summary = $"Queries {schemaName} content elements."
};
operation.Summary = $"Get a {schemaName} content element.";
operation.Parameters.AddQueryParameter("take", JsonObjectType.Number, "The number of elements to take.");
operation.Parameters.AddQueryParameter("skip", JsonObjectType.Number, "The number of elements to skip.");
var responseSchema = CreateContentSchema(schema.BuildSchema(languages, AppendSchema), schemaName, schema.Name);
operation.Parameters.AddQueryParameter("query", JsonObjectType.String, "Optional full text query skip.");
operation.Responses.Add("200",
new SwaggerResponse { Description = $"{schemaName} element found.", Schema = responseSchema });
});
}
var responseSchema = CreateContentsSchema(schema.BuildSchema(languages, AppendSchema), schemaName, schema.Name);
private SwaggerOperations GenerateSchemaCreateOperation(Schema schema, string schemaName)
{
return AddOperation(SwaggerOperationMethod.Post, null, $"{appBasePath}/{schema.Name}", operation =>
{
operation.Summary = $"Create a {schemaName} content element.";
var bodySchema = AppendSchema($"{schema.Name}Dto", schema.BuildSchema(languages, AppendSchema));
operation.Responses.Add("200",
new SwaggerResponse { Description = $"{schemaName} content elements retrieved.", Schema = responseSchema });
operation.Responses.Add("500",
new SwaggerResponse { Description = $"Querying {schemaName} element failed with internal server error.", Schema = errorDtoSchema });
operation.Parameters.AddBodyParameter(bodySchema, "data", string.Format(BodyDescription, schemaName));
operations[SwaggerOperationMethod.Get] = operation;
operation.Responses.Add("201",
new SwaggerResponse { Description = $"{schemaName} created.", Schema = entityCreatedDtoSchema });
});
}
private void GenerateSchemaCreateOperation(SwaggerOperations operations, Schema schema, string schemaName)
private SwaggerOperations GenerateSchemaUpdateOperation(Schema schema, string schemaName)
{
var operation = new SwaggerOperation
return AddOperation(SwaggerOperationMethod.Put, schemaName, $"{appBasePath}/{schema.Name}/{{id}}", operation =>
{
Summary = $"Create a {schemaName} content element."
};
var bodySchema = AppendSchema($"{schema.Name}Dto", schema.BuildSchema(languages, AppendSchema));
operation.Summary = $"Update a {schemaName} content element.";
operation.Parameters.AddBodyParameter(bodySchema, "data", string.Format(BodyDescription, schemaName));
var bodySchema = AppendSchema($"{schema.Name}Dto", schema.BuildSchema(languages, AppendSchema));
operation.Responses.Add("201",
new SwaggerResponse { Description = $"{schemaName} created.", Schema = entityCreatedDtoSchema });
operation.Responses.Add("500",
new SwaggerResponse { Description = $"Creating {schemaName} element failed with internal server error.", Schema = errorDtoSchema });
operation.Parameters.AddBodyParameter(bodySchema, "data", string.Format(BodyDescription, schemaName));
operations[SwaggerOperationMethod.Post] = operation;
operation.Responses.Add("204",
new SwaggerResponse { Description = $"{schemaName} element updated." });
});
}
private void GenerateSchemaGetOperation(SwaggerOperations operations, Schema schema, string schemaName)
private SwaggerOperations GenerateSchemaPublishOperation(Schema schema, string schemaName)
{
var operation = new SwaggerOperation
return AddOperation(SwaggerOperationMethod.Put, schemaName, $"{appBasePath}/{schema.Name}/{{id}}/publish", operation =>
{
Summary = $"Gets a {schemaName} content element"
};
operation.Summary = $"Publish a {schemaName} content element.";
var responseSchema = CreateContentSchema(schema.BuildSchema(languages, AppendSchema), schemaName, schema.Name);
operation.Responses.Add("204",
new SwaggerResponse { Description = $"{schemaName} element published." });
});
}
operation.Responses.Add("209",
new SwaggerResponse { Description = $"{schemaName} element found.", Schema = responseSchema });
operation.Responses.Add("500",
new SwaggerResponse { Description = $"Retrieving {schemaName} element failed with internal server error.", Schema = errorDtoSchema });
private SwaggerOperations GenerateSchemaUnpublishOperation(Schema schema, string schemaName)
{
return AddOperation(SwaggerOperationMethod.Put, schemaName, $"{appBasePath}/{schema.Name}/{{id}}/unpublish", operation =>
{
operation.Summary = $"Unpublish a {schemaName} content element.";
operations[SwaggerOperationMethod.Get] = operation;
operation.Responses.Add("204",
new SwaggerResponse { Description = $"{schemaName} element unpublished." });
});
}
private void GenerateSchemaUpdateOperation(SwaggerOperations operations, Schema schema, string schemaName)
private SwaggerOperations GenerateSchemaDeleteOperation(Schema schema, string schemaName)
{
var operation = new SwaggerOperation
return AddOperation(SwaggerOperationMethod.Delete, schemaName, $"{appBasePath}/{schema.Name}/{{id}}/", operation =>
{
Summary = $"Update {schemaName} content element."
};
operation.Summary = $"Delete a {schemaName} content element.";
var bodySchema = AppendSchema($"{schema.Name}Dto", schema.BuildSchema(languages, AppendSchema));
operation.Responses.Add("204",
new SwaggerResponse { Description = $"{schemaName} element deleted." });
});
}
operation.Parameters.AddBodyParameter(bodySchema, "data", string.Format(BodyDescription, schemaName));
private SwaggerOperations AddOperation(SwaggerOperationMethod method, string entityName, string path, Action<SwaggerOperation> updater)
{
var operations = document.Paths.GetOrAdd(path, k => new SwaggerOperations());
var operation = new SwaggerOperation();
operation.Responses.Add("204",
new SwaggerResponse { Description = $"{schemaName} element updated." });
operation.Responses.Add("500",
new SwaggerResponse { Description = $"Updating {schemaName} element failed with internal server error.", Schema = errorDtoSchema });
updater(operation);
operations[SwaggerOperationMethod.Put] = operation;
}
operations[method] = operation;
private void GenerateSchemaDeleteOperation(SwaggerOperations operations, string schemaName)
{
var operation = new SwaggerOperation
if (entityName != null)
{
Summary = $"Delete a {schemaName} content element."
};
operation.Responses.Add("204",
new SwaggerResponse { Description = $"{schemaName} element deleted." });
operation.Responses.Add("500",
new SwaggerResponse { Description = $"Deleting {schemaName} element failed with internal server error.", Schema = errorDtoSchema });
operation.Parameters.AddPathParameter("id", JsonObjectType.String, $"The id of the {entityName} (GUID).");
operation.Responses.Add("404",
new SwaggerResponse { Description = $"App, schema or {entityName} not found." });
}
operations[SwaggerOperationMethod.Delete] = operation;
return operations;
}
private JsonSchema4 CreateContentsSchema(JsonSchema4 dataSchema, string schemaName, string id)

1
src/Squidex/app/features/content/declarations.ts

@ -7,5 +7,6 @@
export * from './pages/content/content-field.component';
export * from './pages/content/content-page.component';
export * from './pages/contents/content-item.component';
export * from './pages/contents/contents-page.component';
export * from './pages/schemas/schemas-page.component';

2
src/Squidex/app/features/content/module.ts

@ -20,6 +20,7 @@ import {
import {
ContentFieldComponent,
ContentPageComponent,
ContentItemComponent,
ContentsPageComponent,
SchemasPageComponent
} from './declarations';
@ -77,6 +78,7 @@ const routes: Routes = [
],
declarations: [
ContentFieldComponent,
ContentItemComponent,
ContentPageComponent,
ContentsPageComponent,
SchemasPageComponent

4
src/Squidex/app/features/content/pages/content/content-field.component.html

@ -10,14 +10,14 @@
<div class="btn-group btn-group-sm languages-buttons" role="group">
<button type="button" class="btn btn-secondary" *ngFor="let language of languages" [attr.title]="language.englishName"
[class.btn-danger]="fieldForm.controls[language.iso2Code].invalid && (fieldForm.controls[language.iso2Code].touched || contentFormSubmitted)"
[class.active]="language.iso2Code == selectedLanguage" (click)="selectLanguage(language)">
[class.active]="language.iso2Code == fieldLanguage" (click)="selectLanguage(language)">
{{language.iso2Code}}
</button>
</div>
</div>
<div *ngFor="let language of fieldLanguages">
<div *ngIf="language == selectedLanguage">
<div *ngIf="language == fieldLanguage">
<sqx-control-errors [for]="language" fieldName="{{field|displayName:'properties.label':'name'}}" [submitted]="contentFormSubmitted"></sqx-control-errors>
<div [ngSwitch]="field.properties.fieldType">

14
src/Squidex/app/features/content/pages/content/content-field.component.ts

@ -8,10 +8,7 @@
import { Component, Input, OnInit } from '@angular/core';
import { FormGroup } from '@angular/forms';
import {
AppLanguageDto,
FieldDto
} from 'shared';
import { AppLanguageDto, FieldDto } from 'shared';
@Component({
selector: 'sqx-content-field',
@ -32,11 +29,10 @@ export class ContentFieldComponent implements OnInit {
public contentFormSubmitted: boolean;
public fieldLanguages: string[];
public selectedLanguage: string;
public fieldLanguage: string;
public selectLanguage(language: AppLanguageDto) {
this.selectedLanguage = language.iso2Code;
this.fieldLanguage = language.iso2Code;
}
public ngOnInit() {
@ -46,10 +42,10 @@ export class ContentFieldComponent implements OnInit {
if (this.field.properties.isLocalizable) {
this.fieldLanguages = this.languages.map(t => t.iso2Code);
this.selectedLanguage = this.fieldLanguages[0];
this.fieldLanguage = this.fieldLanguages[0];
} else {
this.fieldLanguages = ['iv'];
this.selectedLanguage = 'iv';
this.fieldLanguage = 'iv';
}
}
}

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

@ -11,6 +11,7 @@
</div>
<h3 class="panel-title" *ngIf="isNewMode">New {{schema|displayName}}</h3>
<h3 class="panel-title" *ngIf="!isNewMode">Edit {{schema|displayName}}</h3>
<a class="panel-close" routerLink="../">
<i class="icon-close"></i>

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

@ -5,11 +5,16 @@
* Copyright (c) Sebastian Stehle. All rights reserved
*/
import { Component } from '@angular/core';
import { Component, OnDestroy, OnInit } from '@angular/core';
import { AbstractControl, FormControl, FormGroup, ValidatorFn, Validators } from '@angular/forms';
import { ActivatedRoute, Router } from '@angular/router';
import { Subscription } from 'rxjs';
import { ContentChanged } from './../messages';
import {
ContentCreated,
ContentDeleted,
ContentUpdated
} from './../messages';
import {
AppComponentBase,
@ -31,17 +36,20 @@ import {
styleUrls: ['./content-page.component.scss'],
templateUrl: './content-page.component.html'
})
export class ContentPageComponent extends AppComponentBase {
export class ContentPageComponent extends AppComponentBase implements OnDestroy, OnInit {
private messageSubscription: Subscription;
public schema: SchemaDetailsDto;
public contentFormSubmitted = false;
public contentForm: FormGroup;
public content: ContentDto = null;
public contentData: any = null;
public contentId: string;
public languages: AppLanguageDto[] = [];
public get isNewMode() {
return this.content !== null;
return !this.contentData;
}
constructor(apps: AppsStoreService, notifications: NotificationService, users: UsersProviderService,
@ -53,13 +61,24 @@ export class ContentPageComponent extends AppComponentBase {
super(apps, notifications, users);
}
public ngOnDestroy() {
this.messageSubscription.unsubscribe();
}
public ngOnInit() {
this.messageSubscription =
this.messageBus.of(ContentDeleted).subscribe(message => {
if (message.id === this.contentId) {
this.router.navigate(['../'], { relativeTo: this.route });
}
});
this.route.parent.data.map(p => p['appLanguages']).subscribe((languages: AppLanguageDto[]) => {
this.languages = languages;
});
this.route.parent.data.map(p => p['schema']).subscribe((schema: SchemaDetailsDto) => {
this.setupForm(schema, this.route.snapshot.data['content']);
this.setupForm(schema);
});
this.route.data.map(p => p['content']).subscribe((content: ContentDto) => {
@ -75,33 +94,33 @@ export class ContentPageComponent extends AppComponentBase {
const data = this.contentForm.value;
this.appName()
.switchMap(app => {
if (this.isNewMode) {
return this.contentsService.postContent(app, this.schema.name, data);
} else {
return this.contentsService.putContent(app, this.schema.name, data, this.content.id);
}
})
.subscribe(() => {
this.router.navigate(['../'], { relativeTo: this.route });
this.messageBus.publish(new ContentChanged());
}, error => {
this.notifyError(error);
this.enable();
});
if (this.isNewMode) {
this.appName()
.switchMap(app => this.contentsService.postContent(app, this.schema.name, data))
.subscribe(created => {
this.messageBus.publish(new ContentCreated(created.id, data));
this.router.navigate(['../'], { relativeTo: this.route });
}, error => {
this.notifyError(error);
this.enable();
});
} else {
this.appName()
.switchMap(app => this.contentsService.putContent(app, this.schema.name, this.contentId, data))
.subscribe(() => {
this.messageBus.publish(new ContentUpdated(this.contentId, data));
this.router.navigate(['../'], { relativeTo: this.route });
}, error => {
this.notifyError(error);
this.enable();
});
}
}
}
public reset() {
this.enable();
this.contentForm.reset();
this.contentFormSubmitted = false;
}
public enable() {
private enable() {
for (const field of this.schema.fields.filter(f => !f.isDisabled)) {
const fieldForm = this.contentForm.controls[field.name];
@ -109,7 +128,7 @@ export class ContentPageComponent extends AppComponentBase {
}
}
public disable() {
private disable() {
for (const field of this.schema.fields.filter(f => !f.isDisabled)) {
const fieldForm = this.contentForm.controls[field.name];
@ -117,7 +136,7 @@ export class ContentPageComponent extends AppComponentBase {
}
}
private setupForm(schema: SchemaDetailsDto, content?: ContentDto) {
private setupForm(schema: SchemaDetailsDto) {
this.schema = schema;
const controls: { [key: string]: AbstractControl } = {};
@ -160,7 +179,14 @@ export class ContentPageComponent extends AppComponentBase {
}
private populateForm(content: ContentDto) {
this.content = content;
if (!content) {
this.contentData = undefined;
this.contentId = undefined;
return;
} else {
this.contentData = content.data;
this.contentId = content.id;
}
for (const field of this.schema.fields) {
const fieldValue = content.data[field.name] || {};

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

@ -5,7 +5,7 @@
<div class="panel-header-title-row">
<div class="float-xs-right">
<div class="btn-group languages-buttons" role="group">
<button type="button" class="btn btn-secondary" *ngFor="let language of languages" [attr.title]="language.englishName" [class.active]="language == selectedLanguage" (click)="selectLanguage(language)">
<button type="button" class="btn btn-secondary" *ngFor="let language of languages" [attr.title]="language.englishName" [class.active]="language == languageSelected" (click)="selectLanguage(language)">
{{language.iso2Code}}
</button>
</div>
@ -25,10 +25,10 @@
<div class="panel-main">
<div class="panel-content">
<table class="table table-items table-fixed" *ngIf="contents">
<table class="table table-items table-fixed" *ngIf="contentItems">
<colgroup>
<col *ngFor="let field of contentFields" [style.width]="columnWidth + '%'" />
<col style="width: 170px" />
<col style="width: 190px" />
<col style="width: 80px" />
<col style="width: 80px" />
</colgroup>
@ -51,25 +51,15 @@
</thead>
<tbody>
<template ngFor let-content [ngForOf]="contents.items">
<tr [routerLink]="[content.id]" class="content">
<td *ngFor="let field of contentFields">
<span class="field">
{{getFieldContent(content, field)}}
</span>
</td>
<td>
{{content.lastModified|fromNow}}
</td>
<td>
<img class="user-picture" [attr.title]="userName(content.lastModifiedBy) | async" [attr.src]="userPicture(content.lastModifiedBy, true) | async" />
</td>
<td>
<button type="button" class="btn btn-simple">
<i class="icon-dots"></i>
</button>
</td>
</tr>
<template ngFor let-content [ngForOf]="contentItems">
<tr [routerLink]="[content.id]" routerLinkActive="active" class="content"
[sqxContent]="content"
[language]="languageSelected"
[fields]="contentFields"
[schema]="schema"
(unpublished)="unpublishContent(content)"
(published)="publishContent(content)"
(deleted)="deleteContent(content)"></tr>
<tr class="spacer"></tr>
</template>
</tbody>

14
src/Squidex/app/features/content/pages/contents/contents-page.component.scss

@ -6,6 +6,10 @@
max-width: 64rem;
}
.panel-content {
overflow-y: scroll;
}
.languages-buttons {
margin-right: 1rem;
}
@ -16,14 +20,4 @@
.content {
cursor: pointer;
}
.user-picture {
& {
@include circle(2.2rem);
}
&:not([src]) {
@include opacity(0);
}
}

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

@ -5,20 +5,26 @@
* Copyright (c) Sebastian Stehle. All rights reserved
*/
import { Component, OnInit } from '@angular/core';
import { Component, OnDestroy, OnInit } from '@angular/core';
import { ActivatedRoute } from '@angular/router';
import { Subscription } from 'rxjs';
import { ContentChanged } from './../messages';
import {
ContentCreated,
ContentDeleted,
ContentUpdated
} from './../messages';
import {
AppComponentBase,
AppLanguageDto,
AppsStoreService,
AuthService,
ContentDto,
ContentsDto,
ContentsService,
DateTime,
FieldDto,
ImmutableArray,
MessageBus,
NotificationService,
SchemaDetailsDto,
@ -30,17 +36,18 @@ import {
styleUrls: ['./contents-page.component.scss'],
templateUrl: './contents-page.component.html'
})
export class ContentsPageComponent extends AppComponentBase implements OnInit {
private messageSubscription: Subscription;
export class ContentsPageComponent extends AppComponentBase implements OnDestroy, OnInit {
private messageCreatedSubscription: Subscription;
private messageUpdatedSubscription: Subscription;
public schema: SchemaDetailsDto;
public contents: ContentsDto;
public contentItems: ImmutableArray<ContentDto>;
public contentFields: FieldDto[];
public contentTotal = 0;
public languages: AppLanguageDto[] = [];
public selectedLanguage: AppLanguageDto;
public languageSelected: AppLanguageDto;
public page = 0;
public query = '';
@ -50,26 +57,34 @@ export class ContentsPageComponent extends AppComponentBase implements OnInit {
}
constructor(apps: AppsStoreService, notifications: NotificationService, users: UsersProviderService,
private readonly authService: AuthService,
private readonly contentsService: ContentsService,
private readonly route: ActivatedRoute,
private readonly messageBus: MessageBus
) {
super(apps, notifications, users);
}
public selectLanguage(language: AppLanguageDto) {
this.selectedLanguage = language;
public ngOnDestroy() {
this.messageCreatedSubscription.unsubscribe();
this.messageUpdatedSubscription.unsubscribe();
}
public ngOnInit() {
this.messageSubscription =
this.messageBus.of(ContentChanged).delay(2000).subscribe(message => {
this.load();
this.messageUpdatedSubscription =
this.messageBus.of(ContentUpdated).subscribe(message => {
this.contentItems = this.contentItems.replaceAll(x => x.id === message.id, c => this.updateContent(c, true, message.data));
});
this.messageCreatedSubscription =
this.messageBus.of(ContentCreated).subscribe(message => {
this.contentTotal++;
this.contentItems = this.contentItems.pushFront(this.createContent(message.id, message.data));
});
this.route.data.map(p => p['appLanguages']).subscribe((languages: AppLanguageDto[]) => {
this.languages = languages;
this.selectedLanguage = languages.filter(t => t.isMasterLanguage)[0];
this.languageSelected = languages.filter(t => t.isMasterLanguage)[0];
});
this.route.data.map(p => p['schema']).subscribe(schema => {
@ -81,18 +96,40 @@ export class ContentsPageComponent extends AppComponentBase implements OnInit {
});
}
public getFieldContent(content: ContentDto, field: FieldDto): any {
const contentField = content.data[field.name];
public publishContent(content: ContentDto) {
this.appName()
.switchMap(app => this.contentsService.publishContent(app, this.schema.name, content.id))
.subscribe(() => {
this.contentItems = this.contentItems.replaceAll(x => x.id === content.id, c => this.updateContent(c, true, content.data));
}, error => {
this.notifyError(error);
});
}
if (!contentField) {
return '';
}
public unpublishContent(content: ContentDto) {
this.appName()
.switchMap(app => this.contentsService.unpublishContent(app, this.schema.name, content.id))
.subscribe(() => {
this.contentItems = this.contentItems.replaceAll(x => x.id === content.id, c => this.updateContent(c, false, content.data));
}, error => {
this.notifyError(error);
});
}
if (field.properties.isLocalizable) {
return contentField[this.selectedLanguage.iso2Code];
} else {
return contentField['iv'];
}
public deleteContent(content: ContentDto) {
this.appName()
.switchMap(app => this.contentsService.deleteContent(app, this.schema.name, content.id))
.subscribe(() => {
this.contentItems = this.contentItems.removeAll(x => x.id === content.id);
this.messageBus.publish(new ContentDeleted(content.id));
}, error => {
this.notifyError(error);
});
}
public selectLanguage(language: AppLanguageDto) {
this.languageSelected = language;
}
private reset() {
@ -111,10 +148,38 @@ export class ContentsPageComponent extends AppComponentBase implements OnInit {
this.appName()
.switchMap(app => this.contentsService.getContents(app, this.schema.name, 20, this.page * 20, this.query))
.subscribe(dtos => {
this.contents = dtos;
this.contentItems = ImmutableArray.of(dtos.items);
this.contentTotal = dtos.total;
}, error => {
this.notifyError(error);
});
}
private createContent(id: string, data: any): ContentDto {
const me = `subject:${this.authService.user!.id}`;
const newContent =
new ContentDto(
id, false,
me, me,
DateTime.now(),
DateTime.now(),
data);
return newContent;
}
private updateContent(content: ContentDto, isPublished: boolean, data: any): ContentDto {
const me = `subject:${this.authService.user!.id}`;
const newContent =
new ContentDto(
content.id, isPublished,
content.createdBy, me,
content.created, DateTime.now(),
data);
return newContent;
}
}

23
src/Squidex/app/features/content/pages/messages.ts

@ -5,4 +5,25 @@
* Copyright (c) Sebastian Stehle. All rights reserved
*/
export class ContentChanged { }
export class ContentCreated {
constructor(
public readonly id: string,
public readonly data: any
) {
}
}
export class ContentUpdated {
constructor(
public readonly id: string,
public readonly data: any
) {
}
}
export class ContentDeleted {
constructor(
public readonly id: string
) {
}
}

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

@ -38,9 +38,8 @@
</span>
</div>
<div class="col-xs-4 schema-col-right">
<span class="schema-modified">{{schema.lastModified | fromNow}}</span>
<span class="schema-published" [class.unpublished]="!schema.isPublished"></span>
<span class="schema-modified">{{schema.lastModified | fromNow}}</span>
</div>
</div>
</div>

7
src/Squidex/app/framework/utils/immutable-array.ts

@ -59,6 +59,13 @@ export class ImmutableArray<T> implements Iterable<T> {
return new ImmutableArray<T>(clone);
}
public pushFront(...items: T[]): ImmutableArray<T> {
if (!items || items.length === 0) {
return this;
}
return new ImmutableArray<T>([...freeze(items), ...this.items]);
}
public push(...items: T[]): ImmutableArray<T> {
if (!items || items.length === 0) {
return this;

32
src/Squidex/app/shared/services/contents.service.spec.ts

@ -147,6 +147,38 @@ describe('ContentsService', () => {
authService.verifyAll();
});
it('should make put request to publish content', () => {
const dto = {};
authService.setup(x => x.authPut('http://service/p/api/content/my-app/my-schema/content1/publish', dto))
.returns(() => Observable.of(
new Response(
new ResponseOptions()
)
))
.verifiable(Times.once());
contentsService.publishContent('my-app', 'my-schema', 'content1');
authService.verifyAll();
});
it('should make put request to unpublish content', () => {
const dto = {};
authService.setup(x => x.authPut('http://service/p/api/content/my-app/my-schema/content1/unpublish', dto))
.returns(() => Observable.of(
new Response(
new ResponseOptions()
)
))
.verifiable(Times.once());
contentsService.unpublishContent('my-app', 'my-schema', 'content1');
authService.verifyAll();
});
it('should make delete request to delete content', () => {
authService.setup(x => x.authDelete('http://service/p/api/content/my-app/my-schema/content1'))
.returns(() => Observable.of(

18
src/Squidex/app/shared/services/contents.service.ts

@ -102,13 +102,27 @@ export class ContentsService {
const url = this.apiUrl.buildUrl(`/api/content/${appName}/${schemaName}/${id}`);
return this.authService.authPut(url, dto)
.catchError('Failed to update Content. Please reload.');
.catchError('Failed to update content. Please reload.');
}
public publishContent(appName: string, schemaName: string, id: string): Observable<any> {
const url = this.apiUrl.buildUrl(`/api/content/${appName}/${schemaName}/${id}/publish`);
return this.authService.authPut(url, {})
.catchError('Failed to publish content. Please reload.');
}
public unpublishContent(appName: string, schemaName: string, id: string): Observable<any> {
const url = this.apiUrl.buildUrl(`/api/content/${appName}/${schemaName}/${id}/unpublish`);
return this.authService.authPut(url, {})
.catchError('Failed to unpublish content. Please reload.');
}
public deleteContent(appName: string, schemaName: string, id: string): Observable<any> {
const url = this.apiUrl.buildUrl(`/api/content/${appName}/${schemaName}/${id}`);
return this.authService.authDelete(url)
.catchError('Failed to delete Content. Please reload.');
.catchError('Failed to delete content. Please reload.');
}
}

20
src/Squidex/app/theme/_bootstrap.scss

@ -70,10 +70,22 @@
}
tr {
background: $color-table;
border: 1px solid $color-border;
border-bottom: 2px solid $color-border;
margin-bottom: .5rem;
& {
background: $color-table;
border: 1px solid $color-border;
border-bottom: 2px solid $color-border;
margin-bottom: .5rem;
}
&:hover {
background: $color-table-footer;
}
&.active {
background: $color-theme-blue;
border-color: $color-theme-blue;
color: $color-accent-dark;
}
}
}

28
tests/Squidex.Write.Tests/Contents/ContentCommandHandlerTests.cs

@ -109,6 +109,34 @@ namespace Squidex.Write.Contents
});
}
[Fact]
public async Task Publish_should_publish_domain_object()
{
CreateContent();
var command = new PublishContent { AggregateId = Id, AppId = appId, SchemaId = schemaId };
var context = new CommandContext(command);
await TestUpdate(content, async _ =>
{
await sut.HandleAsync(context);
});
}
[Fact]
public async Task Unpublish_should_unpublish_domain_object()
{
CreateContent();
var command = new UnpublishContent { AggregateId = Id, AppId = appId, SchemaId = schemaId };
var context = new CommandContext(command);
await TestUpdate(content, async _ =>
{
await sut.HandleAsync(context);
});
}
[Fact]
public async Task Delete_should_update_domain_object()
{

72
tests/Squidex.Write.Tests/Contents/ContentDomainObjectTests.cs

@ -98,6 +98,71 @@ namespace Squidex.Write.Contents
});
}
[Fact]
public void Publish_should_throw_if_not_created()
{
Assert.Throws<DomainException>(() => sut.Publish(new PublishContent()));
}
[Fact]
public void Publish_should_throw_if_schema_is_deleted()
{
CreateContent();
DeleteContent();
Assert.Throws<DomainException>(() => sut.Publish(new PublishContent()));
}
[Fact]
public void Publish_should_refresh_properties_and_create_events()
{
CreateContent();
sut.Publish(new PublishContent());
Assert.True(sut.IsPublished);
sut.GetUncomittedEvents().Select(x => x.Payload).ToArray()
.ShouldBeEquivalentTo(
new IEvent[]
{
new ContentPublished()
});
}
[Fact]
public void Unpublish_should_throw_if_not_created()
{
Assert.Throws<DomainException>(() => sut.Unpublish(new UnpublishContent()));
}
[Fact]
public void Unpublish_should_throw_if_schema_is_deleted()
{
CreateContent();
DeleteContent();
Assert.Throws<DomainException>(() => sut.Unpublish(new UnpublishContent()));
}
[Fact]
public void Unpublish_should_refresh_properties_and_create_events()
{
CreateContent();
PublishContent();
sut.Unpublish(new UnpublishContent());
Assert.False(sut.IsPublished);
sut.GetUncomittedEvents().Select(x => x.Payload).ToArray()
.ShouldBeEquivalentTo(
new IEvent[]
{
new ContentUnpublished()
});
}
[Fact]
public void Delete_should_throw_if_not_created()
{
@ -137,6 +202,13 @@ namespace Squidex.Write.Contents
((IAggregate)sut).ClearUncommittedEvents();
}
private void PublishContent()
{
sut.Publish(new PublishContent());
((IAggregate)sut).ClearUncommittedEvents();
}
private void DeleteContent()
{
sut.Delete(new DeleteContent());

Loading…
Cancel
Save