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")
# 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.

11
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;
@ -45,9 +46,9 @@ namespace Squidex.Domain.Apps.Entities.Apps
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);
@ -193,10 +194,8 @@ namespace Squidex.Domain.Apps.Entities.Apps
public async Task HandleAsync(CommandContext context, Func<Task> next)
{
if (!await this.DispatchActionAsync(context.Command, context))
{
await next();
}
await this.DispatchActionAsync(context.Command, context);
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 Template { get; set; }
Guid IAggregateCommand.AggregateId
{
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.
// ==========================================================================
using System;
namespace Squidex.Domain.Apps.Entities.Contents.Commands
{
public sealed class CreateContent : ContentDataCommand
{
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)
{
if (!await this.DispatchActionAsync(context.Command, context))
{
await next();
}
await this.DispatchActionAsync(context.Command, context);
await next();
}
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)
{
if (!await this.DispatchActionAsync(context.Command, context))
{
await next();
}
await this.DispatchActionAsync(context.Command, context);
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)
{
if (!await this.DispatchActionAsync(context.Command, context))
{
await next();
}
await this.DispatchActionAsync(context.Command, context);
await next();
}
}
}

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)

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

@ -15,24 +15,24 @@ namespace Squidex.Infrastructure.Commands
{
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)
{
Guard.NotNull(command, nameof(command));
var context = new CommandContext(command);
var context = new CommandContext(command, this);
var next = new Func<Task>(() => TaskHelper.Done);
foreach (var handler in handlers)
foreach (var handler in middlewares)
{
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]
[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;
@ -63,6 +64,9 @@ namespace Squidex.Config.Domain
services.AddSingletonAs<RuleCommandMiddleware>()
.As<ICommandMiddleware>();
services.AddSingletonAs<CreateBlogCommandMiddleware>()
.As<ICommandMiddleware>();
services.AddTransientAs<Migration01_FromCqrs>()
.As<IMigration>();

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

@ -1,17 +1,56 @@
<sqx-title message="Apps"></sqx-title>
<div class="apps">
<div class="empty text-center" *ngIf="apps?.length === 0">
<div class="apps-section">
<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>
</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 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="apps-section">
<div class="card card-template card-href" (click)="createNewApp('')">
<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>
@ -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>
@ -29,7 +69,8 @@
</div>
<div class="modal-body">
<sqx-app-form
<sqx-app-form
[template]="template"
(created)="addAppDialog.hide()"
(cancelled)="addAppDialog.hide()">
</sqx-app-form>

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

@ -2,14 +2,19 @@
@import '_mixins';
.apps {
@include clearfix;
padding: 1.25rem;
padding-left: $size-sidebar-width + .25rem;
display: block;
}
&-title {
font-weight: light;
font-size: 1.4rem;
}
h4 {
line-height: 2rem;
&-section {
@include clearfix;
padding-top: 2rem;
padding-right: 1.25rem;
padding-bottom: 0;
padding-left: $size-sidebar-width + .25rem;
display: block;
}
}
.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/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()">
<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'">
<div class="panel-header">
@ -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>

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

@ -5,7 +5,8 @@
* 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 {
AppContext,
@ -28,7 +29,9 @@ declare var _urq: any;
fadeAnimation
]
})
export class DashboardPageComponent implements OnInit {
export class DashboardPageComponent implements OnDestroy, OnInit {
private subscriptions: Subscription[] = [];
public profileDisplayName = '';
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() {
this.app
.switchMap(app => this.usagesService.getTodayStorage(app.name))
.subscribe(dto => {
this.assetsCurrent = dto.size;
this.assetsMax = dto.maxAllowed;
});
this.app
.switchMap(app => this.usagesService.getMonthCalls(app.name))
.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.chartStorageCount = {
labels: createLabels(dtos),
datasets: [
{
label: 'Number of Assets',
lineTension: 0,
fill: false,
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: [
{
label: 'Size of Assets (MB)',
lineTension: 0,
fill: false,
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 = {
labels: createLabels(dtos),
datasets: [
{
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)',
backgroundColor: 'rgba(51, 137, 213, 0.6)',
borderColor: 'rgba(51, 137, 213, 1)',
borderWidth: 1,
data: dtos.map(x => x.averageMs)
}
]
};
});
this.subscriptions.push(
this.app
.switchMap(app => this.usagesService.getTodayStorage(app.name))
.subscribe(dto => {
this.assetsCurrent = dto.size;
this.assetsMax = dto.maxAllowed;
}));
this.subscriptions.push(
this.app
.switchMap(app => this.usagesService.getMonthCalls(app.name))
.subscribe(dto => {
this.callsCurrent = dto.count;
this.callsMax = dto.maxAllowed;
}));
this.subscriptions.push(
this.app
.switchMap(app => this.usagesService.getStorageUsages(app.name, DateTime.today().addDays(-20), DateTime.today()))
.subscribe(dtos => {
this.chartStorageCount = {
labels: createLabels(dtos),
datasets: [
{
label: 'Number of Assets',
lineTension: 0,
fill: false,
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: [
{
label: 'Size of Assets (MB)',
lineTension: 0,
fill: false,
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.subscriptions.push(
this.app
.switchMap(app => this.usagesService.getCallsUsages(app.name, DateTime.today().addDays(-20), DateTime.today()))
.subscribe(dtos => {
this.chartCallsCount = {
labels: createLabels(dtos),
datasets: [
{
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)',
backgroundColor: 'rgba(51, 137, 213, 0.6)',
borderColor: 'rgba(51, 137, 213, 1)',
borderWidth: 1,
data: dtos.map(x => x.averageMs)
}
]
};
}));
}
public showForum() {

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

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

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

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

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">
<div class="panel-header">
@ -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
) {
}
}

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

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

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

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 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