diff --git a/src/Squidex.Domain.Apps.Entities/Apps/AppCommandMiddleware.cs b/src/Squidex.Domain.Apps.Entities/Apps/AppCommandMiddleware.cs index 51c460837..c4f934b48 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; @@ -24,30 +25,34 @@ namespace Squidex.Domain.Apps.Entities.Apps private readonly IAppPlansProvider appPlansProvider; private readonly IAppPlanBillingManager appPlansBillingManager; private readonly IUserResolver userResolver; + private readonly IEnumerable templateBuilders; public AppCommandMiddleware( IAggregateHandler handler, IAppProvider appProvider, IAppPlansProvider appPlansProvider, IAppPlanBillingManager appPlansBillingManager, - IUserResolver userResolver) + IUserResolver userResolver, + IEnumerable templateBuilders) { Guard.NotNull(handler, nameof(handler)); Guard.NotNull(appProvider, nameof(appProvider)); Guard.NotNull(userResolver, nameof(userResolver)); Guard.NotNull(appPlansProvider, nameof(appPlansProvider)); Guard.NotNull(appPlansBillingManager, nameof(appPlansBillingManager)); + Guard.NotNull(templateBuilders, nameof(templateBuilders)); this.handler = handler; this.userResolver = userResolver; this.appProvider = appProvider; this.appPlansProvider = appPlansProvider; this.appPlansBillingManager = appPlansBillingManager; + this.templateBuilders = templateBuilders; } - protected Task On(CreateApp command, CommandContext context) + protected async Task On(CreateApp command, CommandContext context) { - return handler.CreateSyncedAsync(context, async a => + var app = await handler.CreateSyncedAsync(context, async a => { await GuardApp.CanCreate(command, appProvider); @@ -55,6 +60,14 @@ namespace Squidex.Domain.Apps.Entities.Apps context.Complete(EntityCreatedResult.Create(command.AppId, a.Version)); }); + + if (!string.IsNullOrWhiteSpace(command.Template)) + { + foreach (var templateBuilder in templateBuilders) + { + await templateBuilder.PopulateTemplate(app.Snapshot, command.Template, context.CommandBus); + } + } } protected Task On(AssignContributor command, CommandContext context) 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/IAppTemplateBuilder.cs b/src/Squidex.Domain.Apps.Entities/Apps/IAppTemplateBuilder.cs new file mode 100644 index 000000000..42b4b3439 --- /dev/null +++ b/src/Squidex.Domain.Apps.Entities/Apps/IAppTemplateBuilder.cs @@ -0,0 +1,17 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System.Threading.Tasks; +using Squidex.Infrastructure.Commands; + +namespace Squidex.Domain.Apps.Entities.Apps +{ + public interface IAppTemplateBuilder + { + Task PopulateTemplate(IAppEntity app, string name, ICommandBus bus); + } +} diff --git a/src/Squidex.Domain.Apps.Entities/Apps/Templates/Blog.cs b/src/Squidex.Domain.Apps.Entities/Apps/Templates/Blog.cs new file mode 100644 index 000000000..79d9df9f3 --- /dev/null +++ b/src/Squidex.Domain.Apps.Entities/Apps/Templates/Blog.cs @@ -0,0 +1,217 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System; +using System.Collections.Generic; +using System.Threading.Tasks; +using Squidex.Domain.Apps.Core; +using Squidex.Domain.Apps.Core.Contents; +using Squidex.Domain.Apps.Core.Schemas; +using Squidex.Domain.Apps.Entities.Apps.Commands; +using Squidex.Domain.Apps.Entities.Contents.Commands; +using Squidex.Domain.Apps.Entities.Schemas.Commands; +using Squidex.Infrastructure; +using Squidex.Infrastructure.Commands; + +namespace Squidex.Domain.Apps.Entities.Apps.Templates +{ + public sealed class Blog : IAppTemplateBuilder + { + private const string SlugScript = @" + var data = ctx.data; + + data.slug = { iv: slugify(data.title.iv) }; + + replace(data);"; + + public async Task PopulateTemplate(IAppEntity app, string name, ICommandBus bus) + { + if (string.Equals("Blog", name, StringComparison.OrdinalIgnoreCase)) + { + var appId = new NamedId(app.Id, app.Name); + + Task publishAsync(AppCommand command) + { + command.AppId = appId; + + return bus.PublishAsync(command); + } + + var pagesId = await CreatePagesSchema(publishAsync); + var postsId = await CreatePostsSchema(publishAsync); + + await publishAsync(new CreateContent + { + SchemaId = pagesId, + Data = + new NamedContentData() + .AddField("title", + new ContentFieldData() + .AddValue("iv", "About Me")) + .AddField("text", + new ContentFieldData() + .AddValue("iv", "I love Squidex and SciFi!")), + Publish = true + }); + + await publishAsync(new CreateContent + { + SchemaId = postsId, + Data = + new NamedContentData() + .AddField("title", + new ContentFieldData() + .AddValue("iv", "My first post with Squidex")) + .AddField("text", + new ContentFieldData() + .AddValue("iv", "Just created a blog with Squidex. I love it!")), + Publish = true, + }); + + await publishAsync(new AttachClient { Id = "sample-client" }); + } + } + + private async Task> 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.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..13892b92e 100644 --- a/src/Squidex.Infrastructure/Commands/InMemoryCommandBus.cs +++ b/src/Squidex.Infrastructure/Commands/InMemoryCommandBus.cs @@ -28,7 +28,7 @@ namespace Squidex.Infrastructure.Commands { Guard.NotNull(command, nameof(command)); - var context = new CommandContext(command); + var context = new CommandContext(command, this); var next = new Func(() => TaskHelper.Done); 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..957d1c1da 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; @@ -75,6 +76,9 @@ namespace Squidex.Config.Domain services.AddTransientAs() .As(); + services.AddTransientAs() + .As(); + services.AddTransientAs() .AsSelf(); 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..cd36226d0 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. +
+
+
+ +
+
+
+ +
+ +

Blog Sample

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