diff --git a/src/Squidex.Write/Contents/Commands/CreateContent.cs b/src/Squidex.Write/Contents/Commands/CreateContent.cs index 9b891b04c..cec3fbfcb 100644 --- a/src/Squidex.Write/Contents/Commands/CreateContent.cs +++ b/src/Squidex.Write/Contents/Commands/CreateContent.cs @@ -12,6 +12,8 @@ namespace Squidex.Write.Contents.Commands { public class CreateContent : ContentDataCommand { + public bool Publish { get; set; } + public CreateContent() { ContentId = Guid.NewGuid(); diff --git a/src/Squidex.Write/Contents/ContentDomainObject.cs b/src/Squidex.Write/Contents/ContentDomainObject.cs index f56eda13e..92d295367 100644 --- a/src/Squidex.Write/Contents/ContentDomainObject.cs +++ b/src/Squidex.Write/Contents/ContentDomainObject.cs @@ -75,6 +75,11 @@ namespace Squidex.Write.Contents RaiseEvent(SimpleMapper.Map(command, new ContentCreated())); + if (command.Publish) + { + RaiseEvent(SimpleMapper.Map(command, new ContentPublished())); + } + return this; } diff --git a/src/Squidex/Controllers/ContentApi/ContentsController.cs b/src/Squidex/Controllers/ContentApi/ContentsController.cs index 1bfc5d291..886350840 100644 --- a/src/Squidex/Controllers/ContentApi/ContentsController.cs +++ b/src/Squidex/Controllers/ContentApi/ContentsController.cs @@ -115,9 +115,9 @@ namespace Squidex.Controllers.ContentApi [HttpPost] [Route("content/{app}/{name}/")] - public async Task PostContent([FromBody] ContentData request) + public async Task PostContent([FromBody] ContentData request, [FromQuery] bool publish = false) { - var command = new CreateContent { ContentId = Guid.NewGuid(), Data = request.ToCleaned() }; + var command = new CreateContent { ContentId = Guid.NewGuid(), Data = request.ToCleaned(), Publish = publish }; var context = await CommandBus.PublishAsync(command); diff --git a/src/Squidex/Controllers/ContentApi/Generator/SchemasSwaggerGenerator.cs b/src/Squidex/Controllers/ContentApi/Generator/SchemasSwaggerGenerator.cs index fc487d0a1..d0fa3a0eb 100644 --- a/src/Squidex/Controllers/ContentApi/Generator/SchemasSwaggerGenerator.cs +++ b/src/Squidex/Controllers/ContentApi/Generator/SchemasSwaggerGenerator.cs @@ -266,6 +266,7 @@ namespace Squidex.Controllers.ContentApi.Generator var responseSchema = CreateContentSchema(schemaName, schemaIdentifier, dataSchema); operation.AddBodyParameter(dataSchema, "data", schemaBodyDescription); + operation.AddQueryParameter("publish", JsonObjectType.Boolean, "Set to true to autopublish content."); operation.AddResponse("201", $"{schemaName} created.", responseSchema); }); } diff --git a/src/Squidex/app/features/content/pages/content/content-page.component.html b/src/Squidex/app/features/content/pages/content/content-page.component.html index 5501c68c3..909ef43ba 100644 --- a/src/Squidex/app/features/content/pages/content/content-page.component.html +++ b/src/Squidex/app/features/content/pages/content/content-page.component.html @@ -1,13 +1,24 @@ -
+
- + + + + + + + +

diff --git a/src/Squidex/app/features/content/pages/content/content-page.component.ts b/src/Squidex/app/features/content/pages/content/content-page.component.ts index cd0d6e0cb..b83f594de 100644 --- a/src/Squidex/app/features/content/pages/content/content-page.component.ts +++ b/src/Squidex/app/features/content/pages/content/content-page.component.ts @@ -6,6 +6,7 @@ */ import { Component, OnDestroy, OnInit } from '@angular/core'; +import { Location } from '@angular/common'; import { AbstractControl, FormControl, FormGroup, ValidatorFn, Validators } from '@angular/forms'; import { ActivatedRoute, Router } from '@angular/router'; import { Subscription } from 'rxjs'; @@ -13,7 +14,9 @@ import { Subscription } from 'rxjs'; import { ContentCreated, ContentDeleted, - ContentUpdated + ContentPublished, + ContentUpdated, + ContentUnpublished } from './../messages'; import { @@ -38,7 +41,9 @@ import { templateUrl: './content-page.component.html' }) export class ContentPageComponent extends AppComponentBase implements OnDestroy, OnInit { - private messageSubscription: Subscription; + private contentDeletedSubscription: Subscription; + private contentPublishedSubscription: Subscription; + private contentUnpublishedSubscription: Subscription; private version: Version = new Version(''); public schema: SchemaDetailsDto; @@ -48,12 +53,14 @@ export class ContentPageComponent extends AppComponentBase implements OnDestroy, public contentData: any = null; public contentId: string; + public isPublished = false; public isNewMode = true; public languages: AppLanguageDto[] = []; constructor(apps: AppsStoreService, notifications: NotificationService, users: UsersProviderService, private readonly contentsService: ContentsService, + private readonly location: Location, private readonly route: ActivatedRoute, private readonly router: Router, private readonly messageBus: MessageBus @@ -62,11 +69,13 @@ export class ContentPageComponent extends AppComponentBase implements OnDestroy, } public ngOnDestroy() { - this.messageSubscription.unsubscribe(); + this.contentDeletedSubscription.unsubscribe(); + this.contentPublishedSubscription.unsubscribe(); + this.contentUnpublishedSubscription.unsubscribe(); } public ngOnInit() { - this.messageSubscription = + this.contentDeletedSubscription = this.messageBus.of(ContentDeleted) .subscribe(message => { if (message.id === this.contentId) { @@ -74,6 +83,22 @@ export class ContentPageComponent extends AppComponentBase implements OnDestroy, } }); + this.contentPublishedSubscription = + this.messageBus.of(ContentPublished) + .subscribe(message => { + if (message.id === this.contentId) { + this.isPublished = true; + } + }); + + this.contentUnpublishedSubscription = + this.messageBus.of(ContentUnpublished) + .subscribe(message => { + if (message.id === this.contentId) { + this.isPublished = false; + } + }); + this.route.parent.data.map(p => p['appLanguages']) .subscribe((languages: AppLanguageDto[]) => { this.languages = languages; @@ -90,7 +115,15 @@ export class ContentPageComponent extends AppComponentBase implements OnDestroy, }); } - public saveContent() { + public saveAndPublish() { + this.saveContent(true); + } + + public saveAsDraft() { + this.saveContent(false); + } + + private saveContent(publish: boolean) { this.contentFormSubmitted = true; if (this.contentForm.valid) { @@ -100,11 +133,17 @@ export class ContentPageComponent extends AppComponentBase implements OnDestroy, if (this.isNewMode) { this.appName() - .switchMap(app => this.contentsService.postContent(app, this.schema.name, data, this.version)) + .switchMap(app => this.contentsService.postContent(app, this.schema.name, data, publish, this.version)) .subscribe(created => { - this.messageBus.publish(new ContentCreated(created.id, created.data, this.version.value)); + this.contentId = created.id; - this.router.navigate(['../'], { relativeTo: this.route }); + this.messageBus.publish(new ContentCreated(created.id, created.data, this.version.value, publish)); + + this.enable(); + this.finishCreation(); + this.updateUrl(); + + this.notifyInfo('Content created successfully.'); }, error => { this.notifyError(error); this.enable(); @@ -115,7 +154,9 @@ export class ContentPageComponent extends AppComponentBase implements OnDestroy, .subscribe(() => { this.messageBus.publish(new ContentUpdated(this.contentId, data, this.version.value)); - this.router.navigate(['../'], { relativeTo: this.route }); + this.enable(); + + this.notifyInfo('Content saved successfully.'); }, error => { this.notifyError(error); this.enable(); @@ -124,6 +165,16 @@ export class ContentPageComponent extends AppComponentBase implements OnDestroy, } } + private finishCreation() { + this.isNewMode = false; + } + + private updateUrl() { + const newUrl = this.router.createUrlTree(['../', this.contentId], { relativeTo: this.route, replaceUrl: true }); + + this.location.replaceState(newUrl.toString()); + } + private enable() { for (const field of this.schema.fields.filter(f => !f.isDisabled)) { const fieldForm = this.contentForm.controls[field.name]; @@ -191,6 +242,7 @@ export class ContentPageComponent extends AppComponentBase implements OnDestroy, } else { this.contentData = content.data; this.contentId = content.id; + this.isPublished = content.isPublished; this.isNewMode = false; } diff --git a/src/Squidex/app/features/content/pages/contents/contents-page.component.ts b/src/Squidex/app/features/content/pages/contents/contents-page.component.ts index 4e8c89e4d..7c2c0404e 100644 --- a/src/Squidex/app/features/content/pages/contents/contents-page.component.ts +++ b/src/Squidex/app/features/content/pages/contents/contents-page.component.ts @@ -13,7 +13,9 @@ import { Subscription } from 'rxjs'; import { ContentCreated, ContentDeleted, - ContentUpdated + ContentPublished, + ContentUpdated, + ContentUnpublished } from './../messages'; import { @@ -40,8 +42,8 @@ import { templateUrl: './contents-page.component.html' }) export class ContentsPageComponent extends AppComponentBase implements OnDestroy, OnInit { - private messageCreatedSubscription: Subscription; - private messageUpdatedSubscription: Subscription; + private contentCreatedSubscription: Subscription; + private contentUpdatedSubscription: Subscription; public schema: SchemaDetailsDto; @@ -68,19 +70,19 @@ export class ContentsPageComponent extends AppComponentBase implements OnDestroy } public ngOnDestroy() { - this.messageCreatedSubscription.unsubscribe(); - this.messageUpdatedSubscription.unsubscribe(); + this.contentCreatedSubscription.unsubscribe(); + this.contentUpdatedSubscription.unsubscribe(); } public ngOnInit() { - this.messageCreatedSubscription = + this.contentCreatedSubscription = this.messageBus.of(ContentCreated) .subscribe(message => { - this.contentItems = this.contentItems.pushFront(this.createContent(message.id, message.data, message.version)); + this.contentItems = this.contentItems.pushFront(this.createContent(message.id, message.data, message.version, message.isPublished)); this.contentsPager = this.contentsPager.incrementCount(); }); - this.messageUpdatedSubscription = + this.contentUpdatedSubscription = this.messageBus.of(ContentUpdated) .subscribe(message => { this.updateContents(message.id, undefined, message.data, message.version); @@ -120,6 +122,8 @@ export class ContentsPageComponent extends AppComponentBase implements OnDestroy .switchMap(app => this.contentsService.publishContent(app, this.schema.name, content.id, content.version)) .subscribe(() => { this.updateContents(content.id, true, content.data, content.version.value); + + this.messageBus.publish(new ContentPublished(content.id)); }, error => { this.notifyError(error); }); @@ -130,6 +134,8 @@ export class ContentsPageComponent extends AppComponentBase implements OnDestroy .switchMap(app => this.contentsService.unpublishContent(app, this.schema.name, content.id, content.version)) .subscribe(() => { this.updateContents(content.id, false, content.data, content.version.value); + + this.messageBus.publish(new ContentUnpublished(content.id)); }, error => { this.notifyError(error); }); @@ -188,12 +194,13 @@ export class ContentsPageComponent extends AppComponentBase implements OnDestroy this.contentItems = this.contentItems.replaceAll(x => x.id === id, c => this.updateContent(c, p === undefined ? c.isPublished : p, data, version)); } - private createContent(id: string, data: any, version: string): ContentDto { + private createContent(id: string, data: any, version: string, isPublished: boolean): ContentDto { const me = `subject:${this.authService.user!.id}`; const newContent = new ContentDto( - id, false, + id, + isPublished, me, me, DateTime.now(), DateTime.now(), @@ -208,7 +215,8 @@ export class ContentsPageComponent extends AppComponentBase implements OnDestroy const newContent = new ContentDto( - content.id, isPublished, + content.id, + isPublished, content.createdBy, me, content.created, DateTime.now(), data, diff --git a/src/Squidex/app/features/content/pages/messages.ts b/src/Squidex/app/features/content/pages/messages.ts index 93c69a917..5a0b9aab5 100644 --- a/src/Squidex/app/features/content/pages/messages.ts +++ b/src/Squidex/app/features/content/pages/messages.ts @@ -9,7 +9,8 @@ export class ContentCreated { constructor( public readonly id: string, public readonly data: any, - public readonly version: string + public readonly version: string, + public readonly isPublished: boolean ) { } } @@ -23,6 +24,20 @@ export class ContentUpdated { } } +export class ContentPublished { + constructor( + public readonly id: string + ) { + } +} + +export class ContentUnpublished { + constructor( + public readonly id: string + ) { + } +} + export class ContentDeleted { constructor( public readonly id: string diff --git a/src/Squidex/app/features/schemas/pages/schemas/schemas-page.component.ts b/src/Squidex/app/features/schemas/pages/schemas/schemas-page.component.ts index 119d96b9b..888c3b77c 100644 --- a/src/Squidex/app/features/schemas/pages/schemas/schemas-page.component.ts +++ b/src/Squidex/app/features/schemas/pages/schemas/schemas-page.component.ts @@ -37,8 +37,8 @@ import { SchemaDeleted, SchemaUpdated } from './../messages'; ] }) export class SchemasPageComponent extends AppComponentBase implements OnDestroy, OnInit { - private messageUpdatedSubscription: Subscription; - private messageDeletedSubscription: Subscription; + private schemaUpdatedSubscription: Subscription; + private schemaDeletedSubscription: Subscription; public addSchemaDialog = new ModalView(); @@ -57,8 +57,8 @@ export class SchemasPageComponent extends AppComponentBase implements OnDestroy, } public ngOnDestroy() { - this.messageUpdatedSubscription.unsubscribe(); - this.messageDeletedSubscription.unsubscribe(); + this.schemaUpdatedSubscription.unsubscribe(); + this.schemaDeletedSubscription.unsubscribe(); } public ngOnInit() { @@ -76,13 +76,13 @@ export class SchemasPageComponent extends AppComponentBase implements OnDestroy, } }); - this.messageUpdatedSubscription = + this.schemaUpdatedSubscription = this.messageBus.of(SchemaUpdated) .subscribe(m => { this.updateSchemas(this.schemas.map(s => s.name === m.name ? updateSchema(s, this.authService, m) : s)); }); - this.messageDeletedSubscription = + this.schemaDeletedSubscription = this.messageBus.of(SchemaDeleted) .subscribe(m => { this.updateSchemas(this.schemas.filter(s => s.name !== m.name)); diff --git a/src/Squidex/app/shared/components/component-base.ts b/src/Squidex/app/shared/components/component-base.ts index 794f98877..1d0aa7310 100644 --- a/src/Squidex/app/shared/components/component-base.ts +++ b/src/Squidex/app/shared/components/component-base.ts @@ -76,6 +76,6 @@ export abstract class ComponentBase { } protected notifyInfo(error: string) { - this.notifications.notify(Notification.error(error)); + this.notifications.notify(Notification.info(error)); } } \ No newline at end of file diff --git a/src/Squidex/app/shared/services/contents.service.spec.ts b/src/Squidex/app/shared/services/contents.service.spec.ts index 396902b30..7f8ee4b09 100644 --- a/src/Squidex/app/shared/services/contents.service.spec.ts +++ b/src/Squidex/app/shared/services/contents.service.spec.ts @@ -169,7 +169,7 @@ describe('ContentsService', () => { it('should make post request to create content', () => { const dto = {}; - authService.setup(x => x.authPost('http://service/p/api/content/my-app/my-schema', dto, version)) + authService.setup(x => x.authPost('http://service/p/api/content/my-app/my-schema?publish=true', dto, version)) .returns(() => Observable.of( new Response( new ResponseOptions({ @@ -190,7 +190,7 @@ describe('ContentsService', () => { let content: ContentDto | null = null; - contentsService.postContent('my-app', 'my-schema', dto, version).subscribe(result => { + contentsService.postContent('my-app', 'my-schema', dto, true, version).subscribe(result => { content = result; }); diff --git a/src/Squidex/app/shared/services/contents.service.ts b/src/Squidex/app/shared/services/contents.service.ts index a7cf32983..c99f5b3d9 100644 --- a/src/Squidex/app/shared/services/contents.service.ts +++ b/src/Squidex/app/shared/services/contents.service.ts @@ -110,8 +110,8 @@ export class ContentsService { .catchError('Failed to load content. Please reload.'); } - public postContent(appName: string, schemaName: string, dto: any, version: Version): Observable { - const url = this.apiUrl.buildUrl(`/api/content/${appName}/${schemaName}`); + public postContent(appName: string, schemaName: string, dto: any, publish: boolean, version: Version): Observable { + const url = this.apiUrl.buildUrl(`/api/content/${appName}/${schemaName}?publish=${publish}`); return this.authService.authPost(url, dto, version) .map(response => response.json()) diff --git a/tests/Squidex.Write.Tests/Contents/ContentDomainObjectTests.cs b/tests/Squidex.Write.Tests/Contents/ContentDomainObjectTests.cs index 35f307a42..14610e0fc 100644 --- a/tests/Squidex.Write.Tests/Contents/ContentDomainObjectTests.cs +++ b/tests/Squidex.Write.Tests/Contents/ContentDomainObjectTests.cs @@ -72,6 +72,18 @@ namespace Squidex.Write.Contents ); } + [Fact] + public void Create_should_also_publish_if_set_to_true() + { + sut.Create(CreateContentCommand(new CreateContent { Data = data, Publish = true })); + + sut.GetUncomittedEvents() + .ShouldHaveSameEvents( + CreateContentEvent(new ContentCreated { Data = data }), + CreateContentEvent(new ContentPublished()) + ); + } + [Fact] public void Update_should_throw_if_not_created() {