Browse Source

Merge pull request #243 from Squidex/feature-blog-sample

Blog sample.
pull/248/head
Sebastian Stehle 8 years ago
committed by GitHub
parent
commit
b3294b2467
No known key found for this signature in database GPG Key ID: 4AEE18F83AFDEB23
  1. 2
      README.md
  2. 11
      src/Squidex.Domain.Apps.Entities/Apps/AppCommandMiddleware.cs
  3. 2
      src/Squidex.Domain.Apps.Entities/Apps/Commands/CreateApp.cs
  4. 243
      src/Squidex.Domain.Apps.Entities/Apps/Templates/CreateBlogCommandMiddleware.cs
  5. 7
      src/Squidex.Domain.Apps.Entities/Contents/Commands/CreateContent.cs
  6. 6
      src/Squidex.Domain.Apps.Entities/Contents/ContentCommandMiddleware.cs
  7. 6
      src/Squidex.Domain.Apps.Entities/Rules/RuleCommandMiddleware.cs
  8. 6
      src/Squidex.Domain.Apps.Entities/Schemas/SchemaCommandMiddleware.cs
  9. 10
      src/Squidex.Infrastructure/Commands/CommandContext.cs
  10. 12
      src/Squidex.Infrastructure/Commands/InMemoryCommandBus.cs
  11. 5
      src/Squidex/Areas/Api/Controllers/Apps/Models/CreateAppDto.cs
  12. 4
      src/Squidex/Config/Domain/WriteServices.cs
  13. 59
      src/Squidex/app/features/apps/pages/apps-page.component.html
  14. 83
      src/Squidex/app/features/apps/pages/apps-page.component.scss
  15. 13
      src/Squidex/app/features/apps/pages/apps-page.component.ts
  16. 4
      src/Squidex/app/features/content/pages/content/content-field.component.html
  17. 2
      src/Squidex/app/features/content/pages/content/content-page.component.html
  18. 4
      src/Squidex/app/features/content/pages/contents/contents-page.component.html
  19. 2
      src/Squidex/app/features/content/pages/schemas/schemas-page.component.html
  20. 173
      src/Squidex/app/features/dashboard/pages/dashboard-page.component.ts
  21. 4
      src/Squidex/app/features/rules/pages/rules/rule-wizard.component.html
  22. 4
      src/Squidex/app/features/rules/pages/rules/rules-page.component.html
  23. 4
      src/Squidex/app/features/rules/pages/rules/triggers/content-changed-trigger.component.html
  24. 4
      src/Squidex/app/features/schemas/pages/schema/schema-page.component.html
  25. 2
      src/Squidex/app/features/schemas/pages/schema/types/references-validation.component.html
  26. 2
      src/Squidex/app/features/schemas/pages/schemas/schemas-page.component.html
  27. 7
      src/Squidex/app/shared/components/app-form.component.ts
  28. 3
      src/Squidex/app/shared/services/apps.service.ts
  29. 184
      src/Squidex/app/shared/services/rules.service.ts
  30. 9
      src/Squidex/app/shared/services/schemas.service.ts
  31. BIN
      src/Squidex/wwwroot/images/add-app.png
  32. BIN
      src/Squidex/wwwroot/images/add-blog.png
  33. 2
      tests/Squidex.Domain.Apps.Entities.Tests/Contents/GraphQL/GraphQLMutationTests.cs
  34. 2
      tests/Squidex.Domain.Apps.Entities.Tests/TestHelpers/HandlerTestBase.cs
  35. 4
      tests/Squidex.Infrastructure.Tests/Commands/AggregateHandlerTests.cs
  36. 3
      tests/Squidex.Infrastructure.Tests/Commands/CommandContextTests.cs
  37. 5
      tests/Squidex.Infrastructure.Tests/Commands/EnrichWithTimestampCommandMiddlewareTests.cs
  38. 7
      tests/Squidex.Infrastructure.Tests/Commands/LogCommandMiddlewareTests.cs

2
README.md

@ -1,6 +1,6 @@
![Squidex Logo](https://raw.githubusercontent.com/Squidex/squidex/master/media/logo-wide.png "Squidex") ![Squidex Logo](https://raw.githubusercontent.com/Squidex/squidex/master/media/logo-wide.png "Squidex")
# What is Squidex? # What is Squidex??
Squidex is an open source headless CMS and content management hub. In contrast to a traditional CMS Squidex provides a rich API with OData filter and Swagger definitions. It is up to you to build your UI on top of it. It can be website, a native app or just another server. We build it with ASP.NET Core and CQRS and is tested for Windows and Linux on modern browsers. Squidex is an open source headless CMS and content management hub. In contrast to a traditional CMS Squidex provides a rich API with OData filter and Swagger definitions. It is up to you to build your UI on top of it. It can be website, a native app or just another server. We build it with ASP.NET Core and CQRS and is tested for Windows and Linux on modern browsers.

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

@ -6,6 +6,7 @@
// ========================================================================== // ==========================================================================
using System; using System;
using System.Collections.Generic;
using System.Threading.Tasks; using System.Threading.Tasks;
using Squidex.Domain.Apps.Entities.Apps.Commands; using Squidex.Domain.Apps.Entities.Apps.Commands;
using Squidex.Domain.Apps.Entities.Apps.Guards; using Squidex.Domain.Apps.Entities.Apps.Guards;
@ -45,9 +46,9 @@ namespace Squidex.Domain.Apps.Entities.Apps
this.appPlansBillingManager = appPlansBillingManager; this.appPlansBillingManager = appPlansBillingManager;
} }
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); await GuardApp.CanCreate(command, appProvider);
@ -193,10 +194,8 @@ namespace Squidex.Domain.Apps.Entities.Apps
public async Task HandleAsync(CommandContext context, Func<Task> next) public async Task HandleAsync(CommandContext context, Func<Task> next)
{ {
if (!await this.DispatchActionAsync(context.Command, context)) await this.DispatchActionAsync(context.Command, context);
{ await next();
await next();
}
} }
} }
} }

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 Name { get; set; }
public string Template { get; set; }
Guid IAggregateCommand.AggregateId Guid IAggregateCommand.AggregateId
{ {
get { return AppId; } get { return AppId; }

243
src/Squidex.Domain.Apps.Entities/Apps/Templates/CreateBlogCommandMiddleware.cs

@ -0,0 +1,243 @@
// ==========================================================================
// 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;
using Squidex.Infrastructure.Tasks;
namespace Squidex.Domain.Apps.Entities.Apps.Templates
{
public sealed class CreateBlogCommandMiddleware : ICommandMiddleware
{
private const string TemplateName = "Blog";
private const string SlugScript = @"
var data = ctx.data;
data.slug = { iv: slugify(data.title.iv) };
replace(data);";
public Task HandleAsync(CommandContext context, Func<Task> next)
{
if (context.IsCompleted &&
context.Command is global::Squidex.Domain.Apps.Entities.Apps.Commands.CreateApp createApp &&
IsRightTemplate(createApp))
{
var appId = new NamedId<Guid>(createApp.AppId, createApp.Name);
Task publishAsync(AppCommand command)
{
command.AppId = appId;
return context.CommandBus.PublishAsync(command);
}
return Task.WhenAll(
CreatePagesAsync(publishAsync, appId),
CreatePostsAsync(publishAsync, appId),
CreateClientAsync(publishAsync, appId));
}
return TaskHelper.Done;
}
private static bool IsRightTemplate(CreateApp createApp)
{
return string.Equals(createApp.Template, TemplateName, StringComparison.OrdinalIgnoreCase);
}
private static async Task CreateClientAsync(Func<AppCommand, Task> publishAsync, NamedId<Guid> appId)
{
await publishAsync(new AttachClient { Id = "sample-client" });
}
private async Task CreatePostsAsync(Func<AppCommand, Task> publishAsync, NamedId<Guid> appId)
{
var postsId = await CreatePostsSchema(publishAsync);
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,
});
}
private async Task CreatePagesAsync(Func<AppCommand, Task> publishAsync, NamedId<Guid> appId)
{
var pagesId = await CreatePagesSchema(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
});
}
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. // All rights reserved. Licensed under the MIT license.
// ========================================================================== // ==========================================================================
using System;
namespace Squidex.Domain.Apps.Entities.Contents.Commands namespace Squidex.Domain.Apps.Entities.Contents.Commands
{ {
public sealed class CreateContent : ContentDataCommand public sealed class CreateContent : ContentDataCommand
{ {
public bool Publish { get; set; } public bool Publish { get; set; }
public CreateContent()
{
ContentId = Guid.NewGuid();
}
} }
} }

6
src/Squidex.Domain.Apps.Entities/Contents/ContentCommandMiddleware.cs

@ -133,10 +133,8 @@ namespace Squidex.Domain.Apps.Entities.Contents
public async Task HandleAsync(CommandContext context, Func<Task> next) public async Task HandleAsync(CommandContext context, Func<Task> next)
{ {
if (!await this.DispatchActionAsync(context.Command, context)) await this.DispatchActionAsync(context.Command, context);
{ await next();
await next();
}
} }
private async Task<ContentOperationContext> CreateContext(ContentCommand command, ContentDomainObject content, Func<string> message) private async Task<ContentOperationContext> CreateContext(ContentCommand command, ContentDomainObject content, Func<string> message)

6
src/Squidex.Domain.Apps.Entities/Rules/RuleCommandMiddleware.cs

@ -82,10 +82,8 @@ namespace Squidex.Domain.Apps.Entities.Rules
public async Task HandleAsync(CommandContext context, Func<Task> next) public async Task HandleAsync(CommandContext context, Func<Task> next)
{ {
if (!await this.DispatchActionAsync(context.Command, context)) await this.DispatchActionAsync(context.Command, context);
{ await next();
await next();
}
} }
} }
} }

6
src/Squidex.Domain.Apps.Entities/Schemas/SchemaCommandMiddleware.cs

@ -187,10 +187,8 @@ namespace Squidex.Domain.Apps.Entities.Schemas
public async Task HandleAsync(CommandContext context, Func<Task> next) public async Task HandleAsync(CommandContext context, Func<Task> next)
{ {
if (!await this.DispatchActionAsync(context.Command, context)) await this.DispatchActionAsync(context.Command, context);
{ await next();
await next();
}
} }
} }
} }

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

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

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

@ -15,24 +15,24 @@ namespace Squidex.Infrastructure.Commands
{ {
public sealed class InMemoryCommandBus : ICommandBus public sealed class InMemoryCommandBus : ICommandBus
{ {
private readonly List<ICommandMiddleware> handlers; private readonly List<ICommandMiddleware> middlewares;
public InMemoryCommandBus(IEnumerable<ICommandMiddleware> handlers) public InMemoryCommandBus(IEnumerable<ICommandMiddleware> middlewares)
{ {
Guard.NotNull(handlers, nameof(handlers)); Guard.NotNull(middlewares, nameof(middlewares));
this.handlers = handlers.Reverse().ToList(); this.middlewares = middlewares.Reverse().ToList();
} }
public async Task<CommandContext> PublishAsync(ICommand command) public async Task<CommandContext> PublishAsync(ICommand command)
{ {
Guard.NotNull(command, nameof(command)); Guard.NotNull(command, nameof(command));
var context = new CommandContext(command); var context = new CommandContext(command, this);
var next = new Func<Task>(() => TaskHelper.Done); var next = new Func<Task>(() => TaskHelper.Done);
foreach (var handler in handlers) foreach (var handler in middlewares)
{ {
next = Join(handler, context, next); next = Join(handler, context, next);
} }

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

@ -17,5 +17,10 @@ namespace Squidex.Areas.Api.Controllers.Apps.Models
[Required] [Required]
[RegularExpression("^[a-z0-9]+(\\-[a-z0-9]+)*$")] [RegularExpression("^[a-z0-9]+(\\-[a-z0-9]+)*$")]
public string Name { get; set; } 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.Apps;
using Squidex.Domain.Apps.Core.Scripting; using Squidex.Domain.Apps.Core.Scripting;
using Squidex.Domain.Apps.Entities.Apps; using Squidex.Domain.Apps.Entities.Apps;
using Squidex.Domain.Apps.Entities.Apps.Templates;
using Squidex.Domain.Apps.Entities.Assets; using Squidex.Domain.Apps.Entities.Assets;
using Squidex.Domain.Apps.Entities.Contents; using Squidex.Domain.Apps.Entities.Contents;
using Squidex.Domain.Apps.Entities.Rules; using Squidex.Domain.Apps.Entities.Rules;
@ -63,6 +64,9 @@ namespace Squidex.Config.Domain
services.AddSingletonAs<RuleCommandMiddleware>() services.AddSingletonAs<RuleCommandMiddleware>()
.As<ICommandMiddleware>(); .As<ICommandMiddleware>();
services.AddSingletonAs<CreateBlogCommandMiddleware>()
.As<ICommandMiddleware>();
services.AddTransientAs<Migration01_FromCqrs>() services.AddTransientAs<Migration01_FromCqrs>()
.As<IMigration>(); .As<IMigration>();

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

@ -1,17 +1,56 @@
<sqx-title message="Apps"></sqx-title> <sqx-title message="Apps"></sqx-title>
<div class="apps"> <div class="apps-section">
<div class="empty text-center" *ngIf="apps?.length === 0"> <h1 class="apps-title">Hi {{ctx.user.displayName}}</h1>
<div class="subtext">
Welcome to Squidex.
</div>
</div>
<div class="apps-section">
<div class="empty" *ngIf="apps?.length === 0">
<h3 class="empty-headline">You are not collaborating to any app yet</h3> <h3 class="empty-headline">You are not collaborating to any app yet</h3>
</div>
<button class="apps-empty-button btn btn-success" (click)="addAppDialog.show()"><i class="icon-plus"></i> Create New App</button> <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>
<div class="app card float-left" *ngFor="let app of apps" title="{{app.name}}"> <div class="apps-section">
<div class="card-body app-content"> <div class="card card-template card-href" (click)="createNewApp('')">
<h4 class="card-title app-title">{{app.name}}</h4> <div class="card-body">
<div class="card-image">
<img src="/images/add-app.png" />
</div>
<a [routerLink]="['/app', app.name]">Edit</a> <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-blog.png" />
</div>
<h4 class="card-title">New 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>
</div> </div>
@ -21,7 +60,8 @@
<div class="modal-dialog"> <div class="modal-dialog">
<div class="modal-content"> <div class="modal-content">
<div class="modal-header"> <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()"> <button type="button" class="close" data-dismiss="modal" aria-label="Close" (click)="addAppDialog.hide()">
<span aria-hidden="true">&times;</span> <span aria-hidden="true">&times;</span>
@ -29,7 +69,8 @@
</div> </div>
<div class="modal-body"> <div class="modal-body">
<sqx-app-form <sqx-app-form
[template]="template"
(created)="addAppDialog.hide()" (created)="addAppDialog.hide()"
(cancelled)="addAppDialog.hide()"> (cancelled)="addAppDialog.hide()">
</sqx-app-form> </sqx-app-form>

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

@ -2,14 +2,19 @@
@import '_mixins'; @import '_mixins';
.apps { .apps {
@include clearfix; &-title {
padding: 1.25rem; font-weight: light;
padding-left: $size-sidebar-width + .25rem; font-size: 1.4rem;
display: block; }
}
h4 { &-section {
line-height: 2rem; @include clearfix;
padding-top: 2rem;
padding-right: 1.25rem;
padding-bottom: 0;
padding-left: $size-sidebar-width + .25rem;
display: block;
}
} }
.app { .app {
@ -26,7 +31,65 @@ h4 {
} }
} }
.empty-headline { .card {
margin: 1.25rem; & {
margin-top: 6.25rem; 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 { Subscription } from 'rxjs';
import { import {
AppContext,
AppDto, AppDto,
AppsStoreService, AppsStoreService,
fadeAnimation, fadeAnimation,
@ -20,6 +21,9 @@ import {
selector: 'sqx-apps-page', selector: 'sqx-apps-page',
styleUrls: ['./apps-page.component.scss'], styleUrls: ['./apps-page.component.scss'],
templateUrl: './apps-page.component.html', templateUrl: './apps-page.component.html',
providers: [
AppContext
],
animations: [ animations: [
fadeAnimation fadeAnimation
] ]
@ -30,9 +34,12 @@ export class AppsPageComponent implements OnDestroy, OnInit {
public addAppDialog = new ModalView(); public addAppDialog = new ModalView();
public apps: AppDto[]; public apps: AppDto[];
public template = '';
public onboardingModal = new ModalView(); public onboardingModal = new ModalView();
constructor( constructor(
public readonly ctx: AppContext,
private readonly appsStore: AppsStoreService, private readonly appsStore: AppsStoreService,
private readonly onboardingService: OnboardingService private readonly onboardingService: OnboardingService
) { ) {
@ -54,4 +61,10 @@ export class AppsPageComponent implements OnDestroy, OnInit {
this.apps = apps; 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"> <div class="table-items-row" [class.invalid]="fieldForm.invalid">
<label> <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> </label>
<span class="field-disabled" *ngIf="field.isDisabled">Disabled</span> <span class="field-disabled" *ngIf="field.isDisabled">Disabled</span>
@ -18,7 +18,7 @@
<div *ngFor="let partition of fieldPartitions"> <div *ngFor="let partition of fieldPartitions">
<div *ngIf="partition == fieldPartition"> <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 [ngSwitch]="field.properties.fieldType">
<div *ngSwitchCase="'Number'"> <div *ngSwitchCase="'Number'">

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

@ -1,4 +1,4 @@
<sqx-title message="{app} | {schema} | Content" parameter1="app" parameter2="schema" [value1]="ctx.appName" [value2]="schema?.name"></sqx-title> <sqx-title message="{app} | {schema} | Content" parameter1="app" parameter2="schema" [value1]="ctx.appName" [value2]="schema?.displayName"></sqx-title>
<form [formGroup]="contentForm" (ngSubmit)="saveAndPublish()"> <form [formGroup]="contentForm" (ngSubmit)="saveAndPublish()">
<sqx-panel desiredWidth="53rem"> <sqx-panel desiredWidth="53rem">

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

@ -1,4 +1,4 @@
<sqx-title message="{app} | {schema} | Contents" parameter1="app" parameter2="schema" [value1]="ctx.appName" [value2]="schema?.name"></sqx-title> <sqx-title message="{app} | {schema} | Contents" parameter1="app" parameter2="schema" [value1]="ctx.appName" [value2]="schema?.displayName"></sqx-title>
<sqx-panel [desiredWidth]="isReadOnly ? '40rem' : '60rem'"> <sqx-panel [desiredWidth]="isReadOnly ? '40rem' : '60rem'">
<div class="panel-header"> <div class="panel-header">
@ -75,7 +75,7 @@
<input type="checkbox" class="form-control" [ngModel]="isAllSelected" (ngModelChange)="selectAll($event)" /> <input type="checkbox" class="form-control" [ngModel]="isAllSelected" (ngModelChange)="selectAll($event)" />
</th> </th>
<th class="cell-auto" *ngFor="let field of contentFields"> <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>
<th class="cell-time"> <th class="cell-time">
Updated Updated

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

@ -25,7 +25,7 @@
<div class="panel-content"> <div class="panel-content">
<ul class="nav nav-panel nav-dark nav-dark-bordered flex-column"> <ul class="nav nav-panel nav-dark nav-dark-bordered flex-column">
<li class="nav-item" *ngFor="let schema of schemasFiltered | async"> <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> </li>
</ul> </ul>
</div> </div>

173
src/Squidex/app/features/dashboard/pages/dashboard-page.component.ts

@ -5,7 +5,8 @@
* Copyright (c) Squidex UG (haftungsbeschränkt). All rights reserved. * Copyright (c) Squidex UG (haftungsbeschränkt). All rights reserved.
*/ */
import { Component, OnInit } from '@angular/core'; import { Component, OnDestroy, OnInit } from '@angular/core';
import { Subscription } from 'rxjs';
import { import {
AppContext, AppContext,
@ -28,7 +29,9 @@ declare var _urq: any;
fadeAnimation fadeAnimation
] ]
}) })
export class DashboardPageComponent implements OnInit { export class DashboardPageComponent implements OnDestroy, OnInit {
private subscriptions: Subscription[] = [];
public profileDisplayName = ''; public profileDisplayName = '';
public chartStorageCount: any; public chartStorageCount: any;
@ -68,84 +71,96 @@ export class DashboardPageComponent implements OnInit {
) { ) {
} }
public ngOnDestroy() {
for (let subscription of this.subscriptions) {
subscription.unsubscribe();
}
this.subscriptions = [];
}
public ngOnInit() { public ngOnInit() {
this.app this.subscriptions.push(
.switchMap(app => this.usagesService.getTodayStorage(app.name)) this.app
.subscribe(dto => { .switchMap(app => this.usagesService.getTodayStorage(app.name))
this.assetsCurrent = dto.size; .subscribe(dto => {
this.assetsMax = dto.maxAllowed; this.assetsCurrent = dto.size;
}); this.assetsMax = dto.maxAllowed;
}));
this.app
.switchMap(app => this.usagesService.getMonthCalls(app.name)) this.subscriptions.push(
.subscribe(dto => { this.app
this.callsCurrent = dto.count; .switchMap(app => this.usagesService.getMonthCalls(app.name))
this.callsMax = dto.maxAllowed; .subscribe(dto => {
}); this.callsCurrent = dto.count;
this.callsMax = dto.maxAllowed;
this.app }));
.switchMap(app => this.usagesService.getStorageUsages(app.name, DateTime.today().addDays(-20), DateTime.today()))
.subscribe(dtos => { this.subscriptions.push(
this.chartStorageCount = { this.app
labels: createLabels(dtos), .switchMap(app => this.usagesService.getStorageUsages(app.name, DateTime.today().addDays(-20), DateTime.today()))
datasets: [ .subscribe(dtos => {
{ this.chartStorageCount = {
label: 'Number of Assets', labels: createLabels(dtos),
lineTension: 0, datasets: [
fill: false, {
backgroundColor: 'rgba(51, 137, 213, 0.6)', label: 'Number of Assets',
borderColor: 'rgba(51, 137, 213, 1)', lineTension: 0,
borderWidth: 1, fill: false,
data: dtos.map(x => x.count) backgroundColor: 'rgba(51, 137, 213, 0.6)',
} borderColor: 'rgba(51, 137, 213, 1)',
] borderWidth: 1,
}; data: dtos.map(x => x.count)
}
this.chartStorageSize = { ]
labels: createLabels(dtos), };
datasets: [
{ this.chartStorageSize = {
label: 'Size of Assets (MB)', labels: createLabels(dtos),
lineTension: 0, datasets: [
fill: false, {
backgroundColor: 'rgba(51, 137, 213, 0.6)', label: 'Size of Assets (MB)',
borderColor: 'rgba(51, 137, 213, 1)', lineTension: 0,
borderWidth: 1, fill: false,
data: dtos.map(x => Math.round(10 * (x.size / (1024 * 1024))) / 10) backgroundColor: 'rgba(51, 137, 213, 0.6)',
} borderColor: 'rgba(51, 137, 213, 1)',
] borderWidth: 1,
}; data: dtos.map(x => Math.round(10 * (x.size / (1024 * 1024))) / 10)
}); }
]
this.app };
.switchMap(app => this.usagesService.getCallsUsages(app.name, DateTime.today().addDays(-20), DateTime.today())) }));
.subscribe(dtos => {
this.chartCallsCount = { this.subscriptions.push(
labels: createLabels(dtos), this.app
datasets: [ .switchMap(app => this.usagesService.getCallsUsages(app.name, DateTime.today().addDays(-20), DateTime.today()))
{ .subscribe(dtos => {
label: 'Number of API Calls', this.chartCallsCount = {
backgroundColor: 'rgba(51, 137, 213, 0.6)', labels: createLabels(dtos),
borderColor: 'rgba(51, 137, 213, 1)', datasets: [
borderWidth: 1, {
data: dtos.map(x => x.count) label: 'Number of API Calls',
} backgroundColor: 'rgba(51, 137, 213, 0.6)',
] borderColor: 'rgba(51, 137, 213, 1)',
}; borderWidth: 1,
data: dtos.map(x => x.count)
this.chartCallsPerformance = { }
labels: createLabels(dtos), ]
datasets: [ };
{
label: 'API Performance (Milliseconds)', this.chartCallsPerformance = {
backgroundColor: 'rgba(51, 137, 213, 0.6)', labels: createLabels(dtos),
borderColor: 'rgba(51, 137, 213, 1)', datasets: [
borderWidth: 1, {
data: dtos.map(x => x.averageMs) label: 'API Performance (Milliseconds)',
} backgroundColor: 'rgba(51, 137, 213, 0.6)',
] borderColor: 'rgba(51, 137, 213, 1)',
}; borderWidth: 1,
}); data: dtos.map(x => x.averageMs)
}
]
};
}));
} }
public showForum() { public showForum() {

4
src/Squidex/app/features/rules/pages/rules/rule-wizard.component.html

@ -32,7 +32,7 @@
<i class="icon-trigger-{{trigger}}"></i> <i class="icon-trigger-{{trigger}}"></i>
</span> </span>
<span class="rule-element-text"> <span class="rule-element-text">
{{ruleTriggers[trigger]}} {{ruleTriggers[trigger].name}}
</span> </span>
</span> </span>
</div> </div>
@ -61,7 +61,7 @@
<i class="icon-action-{{action}}"></i> <i class="icon-action-{{action}}"></i>
</span> </span>
<span class="rule-element-text"> <span class="rule-element-text">
{{ruleActions[action]}} {{ruleActions[action].name}}
</span> </span>
</span> </span>
</div> </div>

4
src/Squidex/app/features/rules/pages/rules/rules-page.component.html

@ -43,7 +43,7 @@
<i class="icon-trigger-{{rule.triggerType}}"></i> <i class="icon-trigger-{{rule.triggerType}}"></i>
</span> </span>
<span class="rule-element-text"> <span class="rule-element-text">
{{ruleTriggers[rule.triggerType]}} {{ruleTriggers[rule.triggerType].name}}
</span> </span>
</span> </span>
</td> </td>
@ -56,7 +56,7 @@
<i class="icon-action-{{rule.actionType}}"></i> <i class="icon-action-{{rule.actionType}}"></i>
</span> </span>
<span class="rule-element-text"> <span class="rule-element-text">
{{ruleActions[rule.actionType]}} {{ruleActions[rule.actionType].name}}
</span> </span>
</span> </span>
</td> </td>

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

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

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

@ -1,4 +1,4 @@
<sqx-title message="{app} | {schema}" parameter1="app" [value1]="ctx.appName" parameter2="schema" [value2]="schema?.name"></sqx-title> <sqx-title message="{app} | {schema}" parameter1="app" [value1]="ctx.appName" parameter2="schema" [value2]="schema?.displayName"></sqx-title>
<sqx-panel desiredWidth="56rem"> <sqx-panel desiredWidth="56rem">
<div class="panel-header"> <div class="panel-header">
@ -49,7 +49,7 @@
</div> </div>
<h3 class="panel-title"> <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> </h3>
</div> </div>

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

@ -4,7 +4,7 @@
<div class="col col-6"> <div class="col col-6">
<select class="form-control" formControlName="schemaId"> <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> </select>
</div> </div>
</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"> <a class="nav-link" [routerLink]="[schema.name]" routerLinkActive="active">
<div class="row"> <div class="row">
<div class="col col-4"> <div class="col col-4">
<span class="schema-name">{{schema | sqxDisplayName:'properties.label':'name'}}</span> <span class="schema-name">{{schema.displayName}}</span>
</div> </div>
<div class="col col-4"> <div class="col col-4">
<span class="schema-user"> <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. * 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 { FormBuilder, Validators } from '@angular/forms';
import { ApiUrlConfig, ValidatorsEx } from 'framework'; import { ApiUrlConfig, ValidatorsEx } from 'framework';
@ -30,6 +30,9 @@ export class AppFormComponent {
@Output() @Output()
public cancelled = new EventEmitter(); public cancelled = new EventEmitter();
@Input()
public template = '';
public createFormError = ''; public createFormError = '';
public createFormSubmitted = false; public createFormSubmitted = false;
public createForm = public createForm =
@ -64,7 +67,7 @@ export class AppFormComponent {
if (this.createForm.valid) { if (this.createForm.valid) {
this.createForm.disable(); 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) this.appsStore.createApp(request)
.subscribe(dto => { .subscribe(dto => {

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

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

184
src/Squidex/app/shared/services/rules.service.ts

@ -21,16 +21,30 @@ import {
} from 'framework'; } from 'framework';
export const ruleTriggers: any = { export const ruleTriggers: any = {
'AssetChanged': 'Asset changed', 'AssetChanged': {
'ContentChanged': 'Content changed' name: 'Asset changed'
},
'ContentChanged': {
name: 'Content changed'
}
}; };
export const ruleActions: any = { export const ruleActions: any = {
'Algolia': 'Populate Algolia Index', 'Algolia': {
'AzureQueue': 'Send to Azure Queue', name: 'Populate Algolia Index'
'Fastly': 'Purge fastly Cache', },
'Slack': 'Send to Slack', 'AzureQueue': {
'Webhook': 'Send Webhook' name: 'Send to Azure Queue'
},
'Fastly': {
name: 'Purge fastly Cache'
},
'Slack': {
name: 'Send to Slack'
},
'Webhook': {
name: 'Send Webhook'
}
}; };
export class RuleDto { export class RuleDto {
@ -154,125 +168,125 @@ export class RulesService {
const url = this.apiUrl.buildUrl(`api/apps/${appName}/rules`); const url = this.apiUrl.buildUrl(`api/apps/${appName}/rules`);
return HTTP.getVersioned<any>(this.http, url) return HTTP.getVersioned<any>(this.http, url)
.map(response => { .map(response => {
const items: any[] = response.payload.body; const items: any[] = response.payload.body;
return items.map(item => { return items.map(item => {
return new RuleDto( return new RuleDto(
item.id, item.id,
item.createdBy, item.createdBy,
item.lastModifiedBy, item.lastModifiedBy,
DateTime.parseISO_UTC(item.created), DateTime.parseISO_UTC(item.created),
DateTime.parseISO_UTC(item.lastModified), DateTime.parseISO_UTC(item.lastModified),
new Version(item.version.toString()), new Version(item.version.toString()),
item.isEnabled, item.isEnabled,
item.trigger, item.trigger,
item.trigger.triggerType, item.trigger.triggerType,
item.action, item.action,
item.action.actionType); item.action.actionType);
}); });
}) })
.pretifyError('Failed to load Rules. Please reload.'); .pretifyError('Failed to load Rules. Please reload.');
} }
public postRule(appName: string, dto: CreateRuleDto, user: string, now: DateTime): Observable<RuleDto> { public postRule(appName: string, dto: CreateRuleDto, user: string, now: DateTime): Observable<RuleDto> {
const url = this.apiUrl.buildUrl(`api/apps/${appName}/rules`); const url = this.apiUrl.buildUrl(`api/apps/${appName}/rules`);
return HTTP.postVersioned<any>(this.http, url, dto) return HTTP.postVersioned<any>(this.http, url, dto)
.map(response => { .map(response => {
const body = response.payload.body; const body = response.payload.body;
return new RuleDto( return new RuleDto(
body.id, body.id,
user, user,
user, user,
now, now,
now, now,
response.version, response.version,
true, true,
dto.trigger, dto.trigger,
dto.trigger.triggerType, dto.trigger.triggerType,
dto.action, dto.action,
dto.action.actionType); dto.action.actionType);
}) })
.do(() => { .do(() => {
this.analytics.trackEvent('Rule', 'Created', appName); this.analytics.trackEvent('Rule', 'Created', appName);
}) })
.pretifyError('Failed to create rule. Please reload.'); .pretifyError('Failed to create rule. Please reload.');
} }
public putRule(appName: string, id: string, dto: UpdateRuleDto, version: Version): Observable<Versioned<any>> { public putRule(appName: string, id: string, dto: UpdateRuleDto, version: Version): Observable<Versioned<any>> {
const url = this.apiUrl.buildUrl(`api/apps/${appName}/rules/${id}`); const url = this.apiUrl.buildUrl(`api/apps/${appName}/rules/${id}`);
return HTTP.putVersioned(this.http, url, dto, version) return HTTP.putVersioned(this.http, url, dto, version)
.do(() => { .do(() => {
this.analytics.trackEvent('Rule', 'Updated', appName); this.analytics.trackEvent('Rule', 'Updated', appName);
}) })
.pretifyError('Failed to update rule. Please reload.'); .pretifyError('Failed to update rule. Please reload.');
} }
public enableRule(appName: string, id: string, version: Version): Observable<Versioned<any>> { public enableRule(appName: string, id: string, version: Version): Observable<Versioned<any>> {
const url = this.apiUrl.buildUrl(`api/apps/${appName}/rules/${id}/enable`); const url = this.apiUrl.buildUrl(`api/apps/${appName}/rules/${id}/enable`);
return HTTP.putVersioned(this.http, url, {}, version) return HTTP.putVersioned(this.http, url, {}, version)
.do(() => { .do(() => {
this.analytics.trackEvent('Rule', 'Updated', appName); this.analytics.trackEvent('Rule', 'Updated', appName);
}) })
.pretifyError('Failed to enable rule. Please reload.'); .pretifyError('Failed to enable rule. Please reload.');
} }
public disableRule(appName: string, id: string, version: Version): Observable<Versioned<any>> { public disableRule(appName: string, id: string, version: Version): Observable<Versioned<any>> {
const url = this.apiUrl.buildUrl(`api/apps/${appName}/rules/${id}/disable`); const url = this.apiUrl.buildUrl(`api/apps/${appName}/rules/${id}/disable`);
return HTTP.putVersioned(this.http, url, {}, version) return HTTP.putVersioned(this.http, url, {}, version)
.do(() => { .do(() => {
this.analytics.trackEvent('Rule', 'Updated', appName); this.analytics.trackEvent('Rule', 'Updated', appName);
}) })
.pretifyError('Failed to disable rule. Please reload.'); .pretifyError('Failed to disable rule. Please reload.');
} }
public deleteRule(appName: string, id: string, version: Version): Observable<any> { public deleteRule(appName: string, id: string, version: Version): Observable<any> {
const url = this.apiUrl.buildUrl(`api/apps/${appName}/rules/${id}`); const url = this.apiUrl.buildUrl(`api/apps/${appName}/rules/${id}`);
return HTTP.deleteVersioned(this.http, url, version) return HTTP.deleteVersioned(this.http, url, version)
.do(() => { .do(() => {
this.analytics.trackEvent('Rule', 'Deleted', appName); this.analytics.trackEvent('Rule', 'Deleted', appName);
}) })
.pretifyError('Failed to delete rule. Please reload.'); .pretifyError('Failed to delete rule. Please reload.');
} }
public getEvents(appName: string, take: number, skip: number): Observable<RuleEventsDto> { public getEvents(appName: string, take: number, skip: number): Observable<RuleEventsDto> {
const url = this.apiUrl.buildUrl(`api/apps/${appName}/rules/events?take=${take}&skip=${skip}`); const url = this.apiUrl.buildUrl(`api/apps/${appName}/rules/events?take=${take}&skip=${skip}`);
return HTTP.getVersioned<any>(this.http, url) return HTTP.getVersioned<any>(this.http, url)
.map(response => { .map(response => {
const body = response.payload.body; const body = response.payload.body;
const items: any[] = body.items; const items: any[] = body.items;
return new RuleEventsDto(body.total, items.map(item => { return new RuleEventsDto(body.total, items.map(item => {
return new RuleEventDto( return new RuleEventDto(
item.id, item.id,
DateTime.parseISO_UTC(item.created), DateTime.parseISO_UTC(item.created),
item.nextAttempt ? DateTime.parseISO_UTC(item.nextAttempt) : null, item.nextAttempt ? DateTime.parseISO_UTC(item.nextAttempt) : null,
item.eventName, item.eventName,
item.description, item.description,
item.lastDump, item.lastDump,
item.result, item.result,
item.jobResult, item.jobResult,
item.numCalls); item.numCalls);
})); }));
}) })
.pretifyError('Failed to load events. Please reload.'); .pretifyError('Failed to load events. Please reload.');
} }
public enqueueEvent(appName: string, id: string): Observable<any> { public enqueueEvent(appName: string, id: string): Observable<any> {
const url = this.apiUrl.buildUrl(`api/apps/${appName}/rules/events/${id}`); const url = this.apiUrl.buildUrl(`api/apps/${appName}/rules/events/${id}`);
return HTTP.putVersioned(this.http, url, {}) return HTTP.putVersioned(this.http, url, {})
.do(() => { .do(() => {
this.analytics.trackEvent('Rule', 'EventEnqueued', appName); this.analytics.trackEvent('Rule', 'EventEnqueued', appName);
}) })
.pretifyError('Failed to enqueue rule event. Please reload.'); .pretifyError('Failed to enqueue rule event. Please reload.');
} }
} }

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

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

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.0 KiB

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

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

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

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

@ -16,6 +16,7 @@ namespace Squidex.Infrastructure.Commands
public class EnrichWithTimestampCommandMiddlewareTests public class EnrichWithTimestampCommandMiddlewareTests
{ {
private readonly IClock clock = A.Fake<IClock>(); private readonly IClock clock = A.Fake<IClock>();
private readonly ICommandBus commandBus = A.Dummy<ICommandBus>();
[Fact] [Fact]
public async Task Should_set_timestamp_for_timestamp_command() public async Task Should_set_timestamp_for_timestamp_command()
@ -28,7 +29,7 @@ namespace Squidex.Infrastructure.Commands
var command = new MyCommand(); var command = new MyCommand();
await sut.HandleAsync(new CommandContext(command)); await sut.HandleAsync(new CommandContext(command, commandBus));
Assert.Equal(utc, command.Timestamp); Assert.Equal(utc, command.Timestamp);
} }
@ -38,7 +39,7 @@ namespace Squidex.Infrastructure.Commands
{ {
var sut = new EnrichWithTimestampCommandMiddleware(clock); 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(); 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 MyLog log = new MyLog();
private readonly LogCommandMiddleware sut; private readonly LogCommandMiddleware sut;
private readonly ICommand command = A.Dummy<ICommand>(); private readonly ICommand command = A.Dummy<ICommand>();
private readonly ICommandBus commandBus = A.Dummy<ICommandBus>();
private sealed class MyLog : ISemanticLog private sealed class MyLog : ISemanticLog
{ {
@ -47,7 +48,7 @@ namespace Squidex.Infrastructure.Commands
[Fact] [Fact]
public async Task Should_log_before_and_after_request() public async Task Should_log_before_and_after_request()
{ {
var context = new CommandContext(command); var context = new CommandContext(command, commandBus);
await sut.HandleAsync(context, () => await sut.HandleAsync(context, () =>
{ {
@ -63,7 +64,7 @@ namespace Squidex.Infrastructure.Commands
[Fact] [Fact]
public async Task Should_log_error_if_command_failed() 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 () => await Assert.ThrowsAsync<InvalidOperationException>(async () =>
{ {
@ -78,7 +79,7 @@ namespace Squidex.Infrastructure.Commands
[Fact] [Fact]
public async Task Should_log_if_command_is_not_handled() 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); await sut.HandleAsync(context, () => TaskHelper.Done);

Loading…
Cancel
Save