Browse Source

Blog sample.

pull/243/head
Sebastian Stehle 8 years ago
parent
commit
6aaf918c13
  1. 19
      src/Squidex.Domain.Apps.Entities/Apps/AppCommandMiddleware.cs
  2. 2
      src/Squidex.Domain.Apps.Entities/Apps/Commands/CreateApp.cs
  3. 17
      src/Squidex.Domain.Apps.Entities/Apps/IAppTemplateBuilder.cs
  4. 217
      src/Squidex.Domain.Apps.Entities/Apps/Templates/Blog.cs
  5. 7
      src/Squidex.Domain.Apps.Entities/Contents/Commands/CreateContent.cs
  6. 10
      src/Squidex.Infrastructure/Commands/CommandContext.cs
  7. 2
      src/Squidex.Infrastructure/Commands/InMemoryCommandBus.cs
  8. 5
      src/Squidex/Areas/Api/Controllers/Apps/Models/CreateAppDto.cs
  9. 4
      src/Squidex/Config/Domain/WriteServices.cs
  10. 57
      src/Squidex/app/features/apps/pages/apps-page.component.html
  11. 79
      src/Squidex/app/features/apps/pages/apps-page.component.scss
  12. 13
      src/Squidex/app/features/apps/pages/apps-page.component.ts
  13. 4
      src/Squidex/app/features/content/pages/content/content-field.component.html
  14. 2
      src/Squidex/app/features/content/pages/contents/contents-page.component.html
  15. 2
      src/Squidex/app/features/content/pages/schemas/schemas-page.component.html
  16. 4
      src/Squidex/app/features/rules/pages/rules/triggers/content-changed-trigger.component.html
  17. 2
      src/Squidex/app/features/schemas/pages/schema/schema-page.component.html
  18. 2
      src/Squidex/app/features/schemas/pages/schema/types/references-validation.component.html
  19. 2
      src/Squidex/app/features/schemas/pages/schemas/schemas-page.component.html
  20. 7
      src/Squidex/app/shared/components/app-form.component.ts
  21. 3
      src/Squidex/app/shared/services/apps.service.ts
  22. 9
      src/Squidex/app/shared/services/schemas.service.ts
  23. BIN
      src/Squidex/wwwroot/images/add-app.png
  24. 33
      tests/Squidex.Domain.Apps.Entities.Tests/Apps/AppCommandMiddlewareTests.cs
  25. 2
      tests/Squidex.Domain.Apps.Entities.Tests/Contents/GraphQL/GraphQLMutationTests.cs
  26. 2
      tests/Squidex.Domain.Apps.Entities.Tests/TestHelpers/HandlerTestBase.cs
  27. 4
      tests/Squidex.Infrastructure.Tests/Commands/AggregateHandlerTests.cs
  28. 3
      tests/Squidex.Infrastructure.Tests/Commands/CommandContextTests.cs
  29. 5
      tests/Squidex.Infrastructure.Tests/Commands/EnrichWithTimestampCommandMiddlewareTests.cs
  30. 7
      tests/Squidex.Infrastructure.Tests/Commands/LogCommandMiddlewareTests.cs

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

@ -6,6 +6,7 @@
// ==========================================================================
using System;
using System.Collections.Generic;
using System.Threading.Tasks;
using Squidex.Domain.Apps.Entities.Apps.Commands;
using Squidex.Domain.Apps.Entities.Apps.Guards;
@ -24,30 +25,34 @@ namespace Squidex.Domain.Apps.Entities.Apps
private readonly IAppPlansProvider appPlansProvider;
private readonly IAppPlanBillingManager appPlansBillingManager;
private readonly IUserResolver userResolver;
private readonly IEnumerable<IAppTemplateBuilder> templateBuilders;
public AppCommandMiddleware(
IAggregateHandler handler,
IAppProvider appProvider,
IAppPlansProvider appPlansProvider,
IAppPlanBillingManager appPlansBillingManager,
IUserResolver userResolver)
IUserResolver userResolver,
IEnumerable<IAppTemplateBuilder> templateBuilders)
{
Guard.NotNull(handler, nameof(handler));
Guard.NotNull(appProvider, nameof(appProvider));
Guard.NotNull(userResolver, nameof(userResolver));
Guard.NotNull(appPlansProvider, nameof(appPlansProvider));
Guard.NotNull(appPlansBillingManager, nameof(appPlansBillingManager));
Guard.NotNull(templateBuilders, nameof(templateBuilders));
this.handler = handler;
this.userResolver = userResolver;
this.appProvider = appProvider;
this.appPlansProvider = appPlansProvider;
this.appPlansBillingManager = appPlansBillingManager;
this.templateBuilders = templateBuilders;
}
protected Task On(CreateApp command, CommandContext context)
protected async Task On(CreateApp command, CommandContext context)
{
return handler.CreateSyncedAsync<AppDomainObject>(context, async a =>
var app = await handler.CreateSyncedAsync<AppDomainObject>(context, async a =>
{
await GuardApp.CanCreate(command, appProvider);
@ -55,6 +60,14 @@ namespace Squidex.Domain.Apps.Entities.Apps
context.Complete(EntityCreatedResult.Create(command.AppId, a.Version));
});
if (!string.IsNullOrWhiteSpace(command.Template))
{
foreach (var templateBuilder in templateBuilders)
{
await templateBuilder.PopulateTemplate(app.Snapshot, command.Template, context.CommandBus);
}
}
}
protected Task On(AssignContributor command, CommandContext context)

2
src/Squidex.Domain.Apps.Entities/Apps/Commands/CreateApp.cs

@ -16,6 +16,8 @@ namespace Squidex.Domain.Apps.Entities.Apps.Commands
public string Name { get; set; }
public string Template { get; set; }
Guid IAggregateCommand.AggregateId
{
get { return AppId; }

17
src/Squidex.Domain.Apps.Entities/Apps/IAppTemplateBuilder.cs

@ -0,0 +1,17 @@
// ==========================================================================
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex UG (haftungsbeschraenkt)
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
using System.Threading.Tasks;
using Squidex.Infrastructure.Commands;
namespace Squidex.Domain.Apps.Entities.Apps
{
public interface IAppTemplateBuilder
{
Task PopulateTemplate(IAppEntity app, string name, ICommandBus bus);
}
}

217
src/Squidex.Domain.Apps.Entities/Apps/Templates/Blog.cs

@ -0,0 +1,217 @@
// ==========================================================================
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex UG (haftungsbeschraenkt)
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
using System;
using System.Collections.Generic;
using System.Threading.Tasks;
using Squidex.Domain.Apps.Core;
using Squidex.Domain.Apps.Core.Contents;
using Squidex.Domain.Apps.Core.Schemas;
using Squidex.Domain.Apps.Entities.Apps.Commands;
using Squidex.Domain.Apps.Entities.Contents.Commands;
using Squidex.Domain.Apps.Entities.Schemas.Commands;
using Squidex.Infrastructure;
using Squidex.Infrastructure.Commands;
namespace Squidex.Domain.Apps.Entities.Apps.Templates
{
public sealed class Blog : IAppTemplateBuilder
{
private const string SlugScript = @"
var data = ctx.data;
data.slug = { iv: slugify(data.title.iv) };
replace(data);";
public async Task PopulateTemplate(IAppEntity app, string name, ICommandBus bus)
{
if (string.Equals("Blog", name, StringComparison.OrdinalIgnoreCase))
{
var appId = new NamedId<Guid>(app.Id, app.Name);
Task publishAsync(AppCommand command)
{
command.AppId = appId;
return bus.PublishAsync(command);
}
var pagesId = await CreatePagesSchema(publishAsync);
var postsId = await CreatePostsSchema(publishAsync);
await publishAsync(new CreateContent
{
SchemaId = pagesId,
Data =
new NamedContentData()
.AddField("title",
new ContentFieldData()
.AddValue("iv", "About Me"))
.AddField("text",
new ContentFieldData()
.AddValue("iv", "I love Squidex and SciFi!")),
Publish = true
});
await publishAsync(new CreateContent
{
SchemaId = postsId,
Data =
new NamedContentData()
.AddField("title",
new ContentFieldData()
.AddValue("iv", "My first post with Squidex"))
.AddField("text",
new ContentFieldData()
.AddValue("iv", "Just created a blog with Squidex. I love it!")),
Publish = true,
});
await publishAsync(new AttachClient { Id = "sample-client" });
}
}
private async Task<NamedId<Guid>> CreatePostsSchema(Func<AppCommand, Task> publishAsync)
{
var command = new CreateSchema
{
Name = "posts",
Properties = new SchemaProperties
{
Label = "Posts"
},
Fields = new List<CreateSchemaField>
{
new CreateSchemaField
{
Name = "title",
Partitioning = Partitioning.Invariant.Key,
Properties = new StringFieldProperties
{
Editor = StringFieldEditor.Input,
IsRequired = true,
IsListField = true,
MaxLength = 100,
MinLength = 0,
Label = "Title"
}
},
new CreateSchemaField
{
Name = "slug",
Partitioning = Partitioning.Invariant.Key,
Properties = new StringFieldProperties
{
Editor = StringFieldEditor.Slug,
IsRequired = false,
IsListField = true,
MaxLength = 100,
MinLength = 0,
Label = "Slug"
}
},
new CreateSchemaField
{
Name = "text",
Partitioning = Partitioning.Invariant.Key,
Properties = new StringFieldProperties
{
Editor = StringFieldEditor.RichText,
IsRequired = true,
IsListField = false,
Label = "Text"
}
}
}
};
await publishAsync(command);
var schemaId = new NamedId<Guid>(command.SchemaId, command.Name);
await publishAsync(new PublishSchema { SchemaId = schemaId });
await publishAsync(new ConfigureScripts
{
SchemaId = schemaId,
ScriptCreate = SlugScript,
ScriptUpdate = SlugScript
});
return schemaId;
}
private async Task<NamedId<Guid>> CreatePagesSchema(Func<AppCommand, Task> publishAsync)
{
var command = new CreateSchema
{
Name = "pages",
Properties = new SchemaProperties
{
Label = "Pages"
},
Fields = new List<CreateSchemaField>
{
new CreateSchemaField
{
Name = "title",
Partitioning = Partitioning.Invariant.Key,
Properties = new StringFieldProperties
{
Editor = StringFieldEditor.Input,
IsRequired = true,
IsListField = true,
MaxLength = 100,
MinLength = 0,
Label = "Title"
}
},
new CreateSchemaField
{
Name = "slug",
Partitioning = Partitioning.Invariant.Key,
Properties = new StringFieldProperties
{
Editor = StringFieldEditor.Slug,
IsRequired = false,
IsListField = true,
MaxLength = 100,
MinLength = 0,
Label = "Slug"
}
},
new CreateSchemaField
{
Name = "text",
Partitioning = Partitioning.Invariant.Key,
Properties = new StringFieldProperties
{
Editor = StringFieldEditor.RichText,
IsRequired = true,
IsListField = false,
Label = "Text"
}
}
}
};
await publishAsync(command);
var schemaId = new NamedId<Guid>(command.SchemaId, command.Name);
await publishAsync(new PublishSchema { SchemaId = schemaId });
await publishAsync(new ConfigureScripts
{
SchemaId = schemaId,
ScriptCreate = SlugScript,
ScriptUpdate = SlugScript
});
return schemaId;
}
}
}

7
src/Squidex.Domain.Apps.Entities/Contents/Commands/CreateContent.cs

@ -5,10 +5,17 @@
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
using System;
namespace Squidex.Domain.Apps.Entities.Contents.Commands
{
public sealed class CreateContent : ContentDataCommand
{
public bool Publish { get; set; }
public CreateContent()
{
ContentId = Guid.NewGuid();
}
}
}

10
src/Squidex.Infrastructure/Commands/CommandContext.cs

@ -12,6 +12,7 @@ namespace Squidex.Infrastructure.Commands
public sealed class CommandContext
{
private readonly ICommand command;
private readonly ICommandBus commandBus;
private readonly Guid contextId = Guid.NewGuid();
private Tuple<object> result;
@ -20,6 +21,11 @@ namespace Squidex.Infrastructure.Commands
get { return command; }
}
public ICommandBus CommandBus
{
get { return commandBus; }
}
public Guid ContextId
{
get { return contextId; }
@ -30,11 +36,13 @@ namespace Squidex.Infrastructure.Commands
get { return result != null; }
}
public CommandContext(ICommand command)
public CommandContext(ICommand command, ICommandBus commandBus)
{
Guard.NotNull(command, nameof(command));
Guard.NotNull(commandBus, nameof(commandBus));
this.command = command;
this.commandBus = commandBus;
}
public void Complete(object resultValue = null)

2
src/Squidex.Infrastructure/Commands/InMemoryCommandBus.cs

@ -28,7 +28,7 @@ namespace Squidex.Infrastructure.Commands
{
Guard.NotNull(command, nameof(command));
var context = new CommandContext(command);
var context = new CommandContext(command, this);
var next = new Func<Task>(() => TaskHelper.Done);

5
src/Squidex/Areas/Api/Controllers/Apps/Models/CreateAppDto.cs

@ -17,5 +17,10 @@ namespace Squidex.Areas.Api.Controllers.Apps.Models
[Required]
[RegularExpression("^[a-z0-9]+(\\-[a-z0-9]+)*$")]
public string Name { get; set; }
/// <summary>
/// Initialize the app with the inbuilt template.
/// </summary>
public string Template { get; set; }
}
}

4
src/Squidex/Config/Domain/WriteServices.cs

@ -12,6 +12,7 @@ using Migrate_01;
using Squidex.Domain.Apps.Core.Apps;
using Squidex.Domain.Apps.Core.Scripting;
using Squidex.Domain.Apps.Entities.Apps;
using Squidex.Domain.Apps.Entities.Apps.Templates;
using Squidex.Domain.Apps.Entities.Assets;
using Squidex.Domain.Apps.Entities.Contents;
using Squidex.Domain.Apps.Entities.Rules;
@ -75,6 +76,9 @@ namespace Squidex.Config.Domain
services.AddTransientAs<Migration04_FlattenAssetEntity>()
.As<IMigration>();
services.AddTransientAs<Blog>()
.As<IAppTemplateBuilder>();
services.AddTransientAs<Rebuilder>()
.AsSelf();

57
src/Squidex/app/features/apps/pages/apps-page.component.html

@ -1,19 +1,58 @@
<sqx-title message="Apps"></sqx-title>
<div class="apps">
<div class="empty text-center" *ngIf="apps?.length === 0">
<h3 class="empty-headline">You are not collaborating to any app yet</h3>
<div class="apps-section">
<h1 class="apps-title">Hi {{ctx.user.displayName}}</h1>
<div class="subtext">
Welcome to Squidex.
</div>
</div>
<button class="apps-empty-button btn btn-success" (click)="addAppDialog.show()"><i class="icon-plus"></i> Create New App</button>
<div class="apps-section">
<div class="empty" *ngIf="apps?.length === 0">
<h3 class="empty-headline">You are not collaborating to any app yet</h3>
</div>
<div class="app card float-left" *ngFor="let app of apps" title="{{app.name}}">
<div class="card-body app-content">
<h4 class="card-title app-title">{{app.name}}</h4>
<div class="card card-href card-app float-left" *ngFor="let app of apps" [routerLink]="['/app', app.name]">
<div class="card-body">
<h4 class="card-title">{{app.name}}</h4>
<div class="card-text">
<a [routerLink]="['/app', app.name]">Edit</a>
</div>
</div>
</div>
</div>
<div class="apps-section">
<div class="card card-template card-href" (click)="createNewApp(null)">
<div class="card-body">
<div class="card-image">
<img src="/images/add-app.png" />
</div>
<h4 class="card-title">New App</h4>
<div class="card-text">
Create a new blank app without content and schemas.
</div>
</div>
</div>
<div class="card card-template card-href" (click)="createNewApp('Blog')">
<div class="card-body">
<div class="card-image">
<img src="/images/add-app.png" />
</div>
<h4 class="card-title">Blog Sample</h4>
<div class="card-text">
<div>Start with our ready to use blog.</div>
<div>Sample Code: <a href="https://github.com/Squidex/squidex-samples/tree/master/csharp/Sample.Blog" (click)="$event.stopPropagation()" target="_blank">ASP.NET Core</a></div>
</div>
</div>
</div>
</div>
<div class="modal" *sqxModalView="addAppDialog;onRoot:true" @fade>
@ -21,7 +60,8 @@
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<h4 class="modal-title">Create App</h4>
<h4 class="modal-title" *ngIf="template">Create {{template}} Sample</h4>
<h4 class="modal-title" *ngIf="!template">Create App</h4>
<button type="button" class="close" data-dismiss="modal" aria-label="Close" (click)="addAppDialog.hide()">
<span aria-hidden="true">&times;</span>
@ -30,6 +70,7 @@
<div class="modal-body">
<sqx-app-form
[template]="template"
(created)="addAppDialog.hide()"
(cancelled)="addAppDialog.hide()">
</sqx-app-form>

79
src/Squidex/app/features/apps/pages/apps-page.component.scss

@ -2,14 +2,19 @@
@import '_mixins';
.apps {
&-title {
font-weight: light;
font-size: 1.4rem;
}
&-section {
@include clearfix;
padding: 1.25rem;
padding-top: 2rem;
padding-right: 1.25rem;
padding-bottom: 0;
padding-left: $size-sidebar-width + .25rem;
display: block;
}
h4 {
line-height: 2rem;
}
}
.app {
@ -26,7 +31,65 @@ h4 {
}
}
.empty-headline {
margin: 1.25rem;
margin-top: 6.25rem;
.card {
& {
margin-right: 1rem;
margin-bottom: 1rem;
width: 16rem;
float: left;
}
&-lg {
width: 33rem;
}
&-image {
text-align: center;
}
&-text {
color: $color-text-decent;
font-weight: normal;
font-size: .9rem;
}
&-more {
color: $color-text-decent;
font-weight: normal;
font-size: .8rem;
margin-top: .4rem;
}
&-title {
color: $color-title;
font-weight: light;
font-size: 1.2rem;
margin-top: .4rem;
}
&-template {
.card-body {
min-height: 15.5rem;
}
}
&-href {
& {
cursor: pointer;
}
&:hover {
@include box-shadow(0, 3px, 16px, .2px);
}
&:focus {
outline: none;
}
&:hover,
&:focus,
&:active {
text-decoration: none;
}
}
}

13
src/Squidex/app/features/apps/pages/apps-page.component.ts

@ -9,6 +9,7 @@ import { Component, OnDestroy, OnInit } from '@angular/core';
import { Subscription } from 'rxjs';
import {
AppContext,
AppDto,
AppsStoreService,
fadeAnimation,
@ -20,6 +21,9 @@ import {
selector: 'sqx-apps-page',
styleUrls: ['./apps-page.component.scss'],
templateUrl: './apps-page.component.html',
providers: [
AppContext
],
animations: [
fadeAnimation
]
@ -30,9 +34,12 @@ export class AppsPageComponent implements OnDestroy, OnInit {
public addAppDialog = new ModalView();
public apps: AppDto[];
public template = '';
public onboardingModal = new ModalView();
constructor(
public readonly ctx: AppContext,
private readonly appsStore: AppsStoreService,
private readonly onboardingService: OnboardingService
) {
@ -54,4 +61,10 @@ export class AppsPageComponent implements OnDestroy, OnInit {
this.apps = apps;
});
}
public createNewApp(template: string) {
this.template = template;
this.addAppDialog.show();
}
}

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

@ -1,6 +1,6 @@
<div class="table-items-row" [class.invalid]="fieldForm.invalid">
<label>
{{field | sqxDisplayName:'properties.label':'name'}} <span class="field-required" [class.hidden]="!field.properties.isRequired">*</span>
{{field.displayName}} <span class="field-required" [class.hidden]="!field.properties.isRequired">*</span>
</label>
<span class="field-disabled" *ngIf="field.isDisabled">Disabled</span>
@ -18,7 +18,7 @@
<div *ngFor="let partition of fieldPartitions">
<div *ngIf="partition == fieldPartition">
<sqx-control-errors [for]="partition" fieldName="{{field | sqxDisplayName:'properties.label':'name'}}" [submitted]="contentFormSubmitted"></sqx-control-errors>
<sqx-control-errors [for]="partition" fieldName="{{field.displayName}}" [submitted]="contentFormSubmitted"></sqx-control-errors>
<div [ngSwitch]="field.properties.fieldType">
<div *ngSwitchCase="'Number'">

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

@ -75,7 +75,7 @@
<input type="checkbox" class="form-control" [ngModel]="isAllSelected" (ngModelChange)="selectAll($event)" />
</th>
<th class="cell-auto" *ngFor="let field of contentFields">
<span class="field">{{field | sqxDisplayName:'properties.label':'name'}}</span>
<span class="field">{{field.displayName}}</span>
</th>
<th class="cell-time">
Updated

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

@ -25,7 +25,7 @@
<div class="panel-content">
<ul class="nav nav-panel nav-dark nav-dark-bordered flex-column">
<li class="nav-item" *ngFor="let schema of schemasFiltered | async">
<a class="nav-link" [routerLink]="schema.name" routerLinkActive="active">{{schema | sqxDisplayName}} <i class="icon-angle-right"></i></a>
<a class="nav-link" [routerLink]="schema.name" routerLinkActive="active">{{schema.displayName}} <i class="icon-angle-right"></i></a>
</li>
</ul>
</div>

4
src/Squidex/app/features/rules/pages/rules/triggers/content-changed-trigger.component.html

@ -35,7 +35,7 @@
<tr *ngFor="let schema of triggerSchemas">
<td>
<span class="truncate">{{schema.schema.name}}</span>
<span class="truncate">{{schema.schema.displayName}}</span>
</td>
<td class="text-center" title="Created">
<input type="checkbox" [ngModel]="schema.sendAll" (ngModelChange)="toggleAll(schema)" />
@ -64,7 +64,7 @@
<form class="form-inline" (ngSubmit)="addSchema()">
<div class="form-group mr-1">
<select class="form-control schemas-control" [(ngModel)]="schemaToAdd" name="schema">
<option *ngFor="let schema of schemasToAdd" [ngValue]="schema">{{schema.name}}</option>
<option *ngFor="let schema of schemasToAdd" [ngValue]="schema">{{schema.displayName}}</option>
</select>
</div>

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

@ -49,7 +49,7 @@
</div>
<h3 class="panel-title">
<i class="schema-edit icon-pencil" (click)="editSchemaDialog.show()"></i> {{schema | sqxDisplayName:'properties.label':'name'}}
<i class="schema-edit icon-pencil" (click)="editSchemaDialog.show()"></i> {{schema.displayName}}
</h3>
</div>

2
src/Squidex/app/features/schemas/pages/schema/types/references-validation.component.html

@ -4,7 +4,7 @@
<div class="col col-6">
<select class="form-control" formControlName="schemaId">
<option *ngFor="let schema of schemas" [ngValue]="schema.id">{{schema.name}}</option>
<option *ngFor="let schema of schemas" [ngValue]="schema.id">{{schema.displayName}}</option>
</select>
</div>
</div>

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

@ -33,7 +33,7 @@
<a class="nav-link" [routerLink]="[schema.name]" routerLinkActive="active">
<div class="row">
<div class="col col-4">
<span class="schema-name">{{schema | sqxDisplayName:'properties.label':'name'}}</span>
<span class="schema-name">{{schema.displayName}}</span>
</div>
<div class="col col-4">
<span class="schema-user">

7
src/Squidex/app/shared/components/app-form.component.ts

@ -5,7 +5,7 @@
* Copyright (c) Squidex UG (haftungsbeschränkt). All rights reserved.
*/
import { Component, EventEmitter, Output } from '@angular/core';
import { Component, EventEmitter, Input, Output } from '@angular/core';
import { FormBuilder, Validators } from '@angular/forms';
import { ApiUrlConfig, ValidatorsEx } from 'framework';
@ -30,6 +30,9 @@ export class AppFormComponent {
@Output()
public cancelled = new EventEmitter();
@Input()
public template = '';
public createFormError = '';
public createFormSubmitted = false;
public createForm =
@ -64,7 +67,7 @@ export class AppFormComponent {
if (this.createForm.valid) {
this.createForm.disable();
const request = new CreateAppDto(this.createForm.controls['name'].value);
const request = new CreateAppDto(this.createForm.controls['name'].value, this.template);
this.appsStore.createApp(request)
.subscribe(dto => {

3
src/Squidex/app/shared/services/apps.service.ts

@ -33,7 +33,8 @@ export class AppDto {
export class CreateAppDto {
constructor(
public readonly name: string
public readonly name: string,
public readonly template?: string
) {
}
}

9
src/Squidex/app/shared/services/schemas.service.ts

@ -17,6 +17,7 @@ import {
ApiUrlConfig,
DateTime,
HTTP,
StringHelper,
ValidatorsEx,
Version,
Versioned
@ -77,6 +78,10 @@ export function createProperties(fieldType: string, values: Object | null = null
}
export class SchemaDto {
public get displayName() {
return StringHelper.firstNonEmpty(this.properties.label || '', this.name);
}
constructor(
public readonly id: string,
public readonly name: string,
@ -274,6 +279,10 @@ export class SchemaDetailsDto extends SchemaDto {
}
export class FieldDto {
public get displayName() {
return StringHelper.firstNonEmpty(this.properties.label || '', this.name);
}
constructor(
public readonly fieldId: number,
public readonly name: string,

BIN
src/Squidex/wwwroot/images/add-app.png

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.8 KiB

After

Width:  |  Height:  |  Size: 5.0 KiB

33
tests/Squidex.Domain.Apps.Entities.Tests/Apps/AppCommandMiddlewareTests.cs

@ -25,6 +25,8 @@ namespace Squidex.Domain.Apps.Entities.Apps
private readonly IAppPlansProvider appPlansProvider = A.Fake<IAppPlansProvider>();
private readonly IAppPlanBillingManager appPlansBillingManager = A.Fake<IAppPlanBillingManager>();
private readonly IUserResolver userResolver = A.Fake<IUserResolver>();
private readonly IAppTemplateBuilder templateBuilder1 = A.Fake<IAppTemplateBuilder>();
private readonly IAppTemplateBuilder templateBuilder2 = A.Fake<IAppTemplateBuilder>();
private readonly Language language = Language.DE;
private readonly string contributorId = Guid.NewGuid().ToString();
private readonly string clientName = "client";
@ -45,7 +47,13 @@ namespace Squidex.Domain.Apps.Entities.Apps
A.CallTo(() => userResolver.FindByIdAsync(contributorId))
.Returns(A.Fake<IUser>());
sut = new AppCommandMiddleware(Handler, appProvider, appPlansProvider, appPlansBillingManager, userResolver);
var templateBuilders = new[]
{
templateBuilder1,
templateBuilder2
};
sut = new AppCommandMiddleware(Handler, appProvider, appPlansProvider, appPlansBillingManager, userResolver, templateBuilders);
}
[Fact]
@ -59,6 +67,29 @@ namespace Squidex.Domain.Apps.Entities.Apps
});
Assert.Equal(AppId, context.Result<EntityCreatedResult<Guid>>().IdOrValue);
A.CallTo(() => templateBuilder1.PopulateTemplate(A<IAppEntity>.Ignored, A<string>.Ignored, context.CommandBus))
.MustNotHaveHappened();
A.CallTo(() => templateBuilder2.PopulateTemplate(A<IAppEntity>.Ignored, A<string>.Ignored, context.CommandBus))
.MustNotHaveHappened();
}
[Fact]
public async Task Create_should_call_template_builders_with_template_name()
{
var context = CreateContextForCommand(new CreateApp { Name = AppName, AppId = AppId, Template = "Blog" });
await TestCreate(app, async _ =>
{
await sut.HandleAsync(context);
});
Assert.Equal(AppId, context.Result<EntityCreatedResult<Guid>>().IdOrValue);
A.CallTo(() => templateBuilder1.PopulateTemplate(A<IAppEntity>.Ignored, "Blog", context.CommandBus))
.MustHaveHappened();
A.CallTo(() => templateBuilder2.PopulateTemplate(A<IAppEntity>.Ignored, "Blog", context.CommandBus))
.MustHaveHappened();
}
[Fact]

2
tests/Squidex.Domain.Apps.Entities.Tests/Contents/GraphQL/GraphQLMutationTests.cs

@ -22,7 +22,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL
{
private readonly Guid contentId = Guid.NewGuid();
private readonly IContentEntity content;
private readonly CommandContext commandContext = new CommandContext(new PatchContent());
private readonly CommandContext commandContext = new CommandContext(new PatchContent(), A.Dummy<ICommandBus>());
public GraphQLMutationTests()
{

2
tests/Squidex.Domain.Apps.Entities.Tests/TestHelpers/HandlerTestBase.cs

@ -98,7 +98,7 @@ namespace Squidex.Domain.Apps.Entities.TestHelpers
protected CommandContext CreateContextForCommand<TCommand>(TCommand command) where TCommand : SquidexCommand
{
return new CommandContext(CreateCommand(command));
return new CommandContext(CreateCommand(command), A.Dummy<ICommandBus>());
}
protected async Task TestCreate(T domainObject, Func<T, Task> action, bool shouldCreate = true)

4
tests/Squidex.Infrastructure.Tests/Commands/AggregateHandlerTests.cs

@ -26,7 +26,7 @@ namespace Squidex.Infrastructure.Commands
private readonly Envelope<IEvent> event1 = new Envelope<IEvent>(new MyEvent());
private readonly Envelope<IEvent> event2 = new Envelope<IEvent>(new MyEvent());
private readonly CommandContext context;
private readonly CommandContext invalidContext = new CommandContext(A.Dummy<ICommand>());
private readonly CommandContext invalidContext = new CommandContext(A.Dummy<ICommand>(), A.Dummy<ICommandBus>());
private readonly Guid domainObjectId = Guid.NewGuid();
private readonly MyCommand command;
private readonly MyDomainObject domainObject = new MyDomainObject();
@ -35,7 +35,7 @@ namespace Squidex.Infrastructure.Commands
public AggregateHandlerTests()
{
command = new MyCommand { AggregateId = domainObjectId, ExpectedVersion = EtagVersion.Any };
context = new CommandContext(command);
context = new CommandContext(command, A.Dummy<ICommandBus>());
A.CallTo(() => store.WithSnapshotsAndEventSourcing(domainObjectId, A<Func<MyDomainState, Task>>.Ignored, A<Func<Envelope<IEvent>, Task>>.Ignored))
.Returns(persistence);

3
tests/Squidex.Infrastructure.Tests/Commands/CommandContextTests.cs

@ -6,6 +6,7 @@
// ==========================================================================
using System;
using FakeItEasy;
using Squidex.Infrastructure.TestHelpers;
using Xunit;
@ -18,7 +19,7 @@ namespace Squidex.Infrastructure.Commands
public CommandContextTests()
{
sut = new CommandContext(command);
sut = new CommandContext(command, A.Dummy<ICommandBus>());
}
[Fact]

5
tests/Squidex.Infrastructure.Tests/Commands/EnrichWithTimestampCommandMiddlewareTests.cs

@ -16,6 +16,7 @@ namespace Squidex.Infrastructure.Commands
public class EnrichWithTimestampCommandMiddlewareTests
{
private readonly IClock clock = A.Fake<IClock>();
private readonly ICommandBus commandBus = A.Dummy<ICommandBus>();
[Fact]
public async Task Should_set_timestamp_for_timestamp_command()
@ -28,7 +29,7 @@ namespace Squidex.Infrastructure.Commands
var command = new MyCommand();
await sut.HandleAsync(new CommandContext(command));
await sut.HandleAsync(new CommandContext(command, commandBus));
Assert.Equal(utc, command.Timestamp);
}
@ -38,7 +39,7 @@ namespace Squidex.Infrastructure.Commands
{
var sut = new EnrichWithTimestampCommandMiddleware(clock);
await sut.HandleAsync(new CommandContext(A.Dummy<ICommand>()));
await sut.HandleAsync(new CommandContext(A.Dummy<ICommand>(), commandBus));
A.CallTo(() => clock.GetCurrentInstant()).MustNotHaveHappened();
}

7
tests/Squidex.Infrastructure.Tests/Commands/LogCommandMiddlewareTests.cs

@ -20,6 +20,7 @@ namespace Squidex.Infrastructure.Commands
private readonly MyLog log = new MyLog();
private readonly LogCommandMiddleware sut;
private readonly ICommand command = A.Dummy<ICommand>();
private readonly ICommandBus commandBus = A.Dummy<ICommandBus>();
private sealed class MyLog : ISemanticLog
{
@ -47,7 +48,7 @@ namespace Squidex.Infrastructure.Commands
[Fact]
public async Task Should_log_before_and_after_request()
{
var context = new CommandContext(command);
var context = new CommandContext(command, commandBus);
await sut.HandleAsync(context, () =>
{
@ -63,7 +64,7 @@ namespace Squidex.Infrastructure.Commands
[Fact]
public async Task Should_log_error_if_command_failed()
{
var context = new CommandContext(command);
var context = new CommandContext(command, commandBus);
await Assert.ThrowsAsync<InvalidOperationException>(async () =>
{
@ -78,7 +79,7 @@ namespace Squidex.Infrastructure.Commands
[Fact]
public async Task Should_log_if_command_is_not_handled()
{
var context = new CommandContext(command);
var context = new CommandContext(command, commandBus);
await sut.HandleAsync(context, () => TaskHelper.Done);

Loading…
Cancel
Save