diff --git a/README.md b/README.md index d7b94d7fc..9c83fa2af 100644 --- a/README.md +++ b/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. diff --git a/src/Squidex.Domain.Apps.Entities/Apps/AppCommandMiddleware.cs b/src/Squidex.Domain.Apps.Entities/Apps/AppCommandMiddleware.cs index 51c460837..02bb242bd 100644 --- a/src/Squidex.Domain.Apps.Entities/Apps/AppCommandMiddleware.cs +++ b/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(context, async a => + var app = await handler.CreateSyncedAsync(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 next) { - if (!await this.DispatchActionAsync(context.Command, context)) - { - await next(); - } + await this.DispatchActionAsync(context.Command, context); + await next(); } } } diff --git a/src/Squidex.Domain.Apps.Entities/Apps/Commands/CreateApp.cs b/src/Squidex.Domain.Apps.Entities/Apps/Commands/CreateApp.cs index b49d54c59..5e8050247 100644 --- a/src/Squidex.Domain.Apps.Entities/Apps/Commands/CreateApp.cs +++ b/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; } diff --git a/src/Squidex.Domain.Apps.Entities/Apps/Templates/CreateBlogCommandMiddleware.cs b/src/Squidex.Domain.Apps.Entities/Apps/Templates/CreateBlogCommandMiddleware.cs new file mode 100644 index 000000000..64ea26bf5 --- /dev/null +++ b/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 next) + { + if (context.IsCompleted && + context.Command is global::Squidex.Domain.Apps.Entities.Apps.Commands.CreateApp createApp && + IsRightTemplate(createApp)) + { + var appId = new NamedId(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 publishAsync, NamedId appId) + { + await publishAsync(new AttachClient { Id = "sample-client" }); + } + + private async Task CreatePostsAsync(Func publishAsync, NamedId 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 publishAsync, NamedId 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> CreatePostsSchema(Func publishAsync) + { + var command = new CreateSchema + { + Name = "posts", + Properties = new SchemaProperties + { + Label = "Posts" + }, + Fields = new List + { + 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(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> CreatePagesSchema(Func publishAsync) + { + var command = new CreateSchema + { + Name = "pages", + Properties = new SchemaProperties + { + Label = "Pages" + }, + Fields = new List + { + 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(command.SchemaId, command.Name); + + await publishAsync(new PublishSchema { SchemaId = schemaId }); + await publishAsync(new ConfigureScripts + { + SchemaId = schemaId, + ScriptCreate = SlugScript, + ScriptUpdate = SlugScript + }); + + return schemaId; + } + } +} diff --git a/src/Squidex.Domain.Apps.Entities/Contents/Commands/CreateContent.cs b/src/Squidex.Domain.Apps.Entities/Contents/Commands/CreateContent.cs index 1837b977f..71e0a11e7 100644 --- a/src/Squidex.Domain.Apps.Entities/Contents/Commands/CreateContent.cs +++ b/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(); + } } } diff --git a/src/Squidex.Domain.Apps.Entities/Contents/ContentCommandMiddleware.cs b/src/Squidex.Domain.Apps.Entities/Contents/ContentCommandMiddleware.cs index 6923e5005..6b10ba6df 100644 --- a/src/Squidex.Domain.Apps.Entities/Contents/ContentCommandMiddleware.cs +++ b/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 next) { - if (!await this.DispatchActionAsync(context.Command, context)) - { - await next(); - } + await this.DispatchActionAsync(context.Command, context); + await next(); } private async Task CreateContext(ContentCommand command, ContentDomainObject content, Func message) diff --git a/src/Squidex.Domain.Apps.Entities/Rules/RuleCommandMiddleware.cs b/src/Squidex.Domain.Apps.Entities/Rules/RuleCommandMiddleware.cs index e252b5632..c0ee1cf5b 100644 --- a/src/Squidex.Domain.Apps.Entities/Rules/RuleCommandMiddleware.cs +++ b/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 next) { - if (!await this.DispatchActionAsync(context.Command, context)) - { - await next(); - } + await this.DispatchActionAsync(context.Command, context); + await next(); } } } diff --git a/src/Squidex.Domain.Apps.Entities/Schemas/SchemaCommandMiddleware.cs b/src/Squidex.Domain.Apps.Entities/Schemas/SchemaCommandMiddleware.cs index 18c468c0f..b58f2c815 100644 --- a/src/Squidex.Domain.Apps.Entities/Schemas/SchemaCommandMiddleware.cs +++ b/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 next) { - if (!await this.DispatchActionAsync(context.Command, context)) - { - await next(); - } + await this.DispatchActionAsync(context.Command, context); + await next(); } } } diff --git a/src/Squidex.Infrastructure/Commands/CommandContext.cs b/src/Squidex.Infrastructure/Commands/CommandContext.cs index 278c7eebf..a83ba04fb 100644 --- a/src/Squidex.Infrastructure/Commands/CommandContext.cs +++ b/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 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) diff --git a/src/Squidex.Infrastructure/Commands/InMemoryCommandBus.cs b/src/Squidex.Infrastructure/Commands/InMemoryCommandBus.cs index 498a41a28..7c72e8fba 100644 --- a/src/Squidex.Infrastructure/Commands/InMemoryCommandBus.cs +++ b/src/Squidex.Infrastructure/Commands/InMemoryCommandBus.cs @@ -15,24 +15,24 @@ namespace Squidex.Infrastructure.Commands { public sealed class InMemoryCommandBus : ICommandBus { - private readonly List handlers; + private readonly List middlewares; - public InMemoryCommandBus(IEnumerable handlers) + public InMemoryCommandBus(IEnumerable middlewares) { - Guard.NotNull(handlers, nameof(handlers)); + Guard.NotNull(middlewares, nameof(middlewares)); - this.handlers = handlers.Reverse().ToList(); + this.middlewares = middlewares.Reverse().ToList(); } public async Task PublishAsync(ICommand command) { Guard.NotNull(command, nameof(command)); - var context = new CommandContext(command); + var context = new CommandContext(command, this); var next = new Func(() => TaskHelper.Done); - foreach (var handler in handlers) + foreach (var handler in middlewares) { next = Join(handler, context, next); } diff --git a/src/Squidex/Areas/Api/Controllers/Apps/Models/CreateAppDto.cs b/src/Squidex/Areas/Api/Controllers/Apps/Models/CreateAppDto.cs index 362932862..92cab9c82 100644 --- a/src/Squidex/Areas/Api/Controllers/Apps/Models/CreateAppDto.cs +++ b/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; } + + /// + /// Initialize the app with the inbuilt template. + /// + public string Template { get; set; } } } diff --git a/src/Squidex/Config/Domain/WriteServices.cs b/src/Squidex/Config/Domain/WriteServices.cs index 69bfce8c0..7c890b235 100644 --- a/src/Squidex/Config/Domain/WriteServices.cs +++ b/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() .As(); + services.AddSingletonAs() + .As(); + services.AddTransientAs() .As(); diff --git a/src/Squidex/app/features/apps/pages/apps-page.component.html b/src/Squidex/app/features/apps/pages/apps-page.component.html index 36c34a5f2..9c65780f2 100644 --- a/src/Squidex/app/features/apps/pages/apps-page.component.html +++ b/src/Squidex/app/features/apps/pages/apps-page.component.html @@ -1,17 +1,56 @@  -
-
+
+

Hi {{ctx.user.displayName}}

+ +
+ Welcome to Squidex. +
+
+ +
+

You are not collaborating to any app yet

+
- +
+
+

{{app.name}}

+ +
+ Edit +
+
+
-
-
-

{{app.name}}

+
+
+
+
+ +
- Edit +

New App

+ +
+ Create a new blank app without content and schemas. +
+
+
+ +
+
+
+ +
+ +

New Blog Sample

+ +
+
Start with our ready to use blog.
+
Sample Code: ASP.NET Core
+
@@ -21,7 +60,8 @@