From 91c5053ff8819774dda6a5b96e3b76e446416f80 Mon Sep 17 00:00:00 2001 From: Sebastian Stehle Date: Fri, 9 Feb 2018 23:09:41 +0100 Subject: [PATCH] UI fixed and simplified. --- .../Controllers/Content/Models/ContentDto.cs | 15 +++ .../pages/content/content-page.component.ts | 35 +++--- .../pages/contents/contents-page.component.ts | 70 +++--------- .../app/features/content/pages/messages.ts | 11 +- .../shared/services/assets.service.spec.ts | 2 +- .../shared/services/contents.service.spec.ts | 94 ++++++---------- .../app/shared/services/contents.service.ts | 104 ++++++++---------- 7 files changed, 134 insertions(+), 197 deletions(-) diff --git a/src/Squidex/Areas/Api/Controllers/Content/Models/ContentDto.cs b/src/Squidex/Areas/Api/Controllers/Content/Models/ContentDto.cs index eee19d42f..f2c16cedd 100644 --- a/src/Squidex/Areas/Api/Controllers/Content/Models/ContentDto.cs +++ b/src/Squidex/Areas/Api/Controllers/Content/Models/ContentDto.cs @@ -40,6 +40,21 @@ namespace Squidex.Areas.Api.Controllers.Contents.Models [Required] public object Data { get; set; } + /// + /// The scheduled status. + /// + public Status? ScheduledTo { get; } + + /// + /// The scheduled date. + /// + public Instant? ScheduledAt { get; } + + /// + /// The user that has scheduled the content. + /// + public RefToken ScheduledBy { get; } + /// /// The date and time when the content item has been created. /// 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 0b6467631..4dd2a16a4 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 @@ -12,9 +12,8 @@ import { Observable, Subscription } from 'rxjs'; import { ContentCreated, - ContentPublished, ContentRemoved, - ContentUnpublished, + ContentStatusChanged, ContentUpdated, ContentVersionSelected } from './../messages'; @@ -39,8 +38,7 @@ import { ] }) export class ContentPageComponent implements CanComponentDeactivate, OnDestroy, OnInit { - private contentPublishedSubscription: Subscription; - private contentUnpublishedSubscription: Subscription; + private contentStatusChangedSubscription: Subscription; private contentDeletedSubscription: Subscription; private contentVersionSelectedSubscription: Subscription; @@ -63,8 +61,7 @@ export class ContentPageComponent implements CanComponentDeactivate, OnDestroy, public ngOnDestroy() { this.contentVersionSelectedSubscription.unsubscribe(); - this.contentUnpublishedSubscription.unsubscribe(); - this.contentPublishedSubscription.unsubscribe(); + this.contentStatusChangedSubscription.unsubscribe(); this.contentDeletedSubscription.unsubscribe(); } @@ -75,27 +72,25 @@ export class ContentPageComponent implements CanComponentDeactivate, OnDestroy, this.loadVersion(message.version); }); - this.contentPublishedSubscription = - this.ctx.bus.of(ContentPublished) - .subscribe(message => { - if (this.content && message.content.id === this.content.id) { - this.content = this.content.publish(message.content.lastModifiedBy, message.content.version, message.content.lastModified); - } - }); - - this.contentUnpublishedSubscription = - this.ctx.bus.of(ContentUnpublished) + this.contentDeletedSubscription = + this.ctx.bus.of(ContentRemoved) .subscribe(message => { if (this.content && message.content.id === this.content.id) { - this.content = this.content.unpublish(message.content.lastModifiedBy, message.content.version, message.content.lastModified); + this.router.navigate(['../'], { relativeTo: this.ctx.route }); } }); - this.contentDeletedSubscription = - this.ctx.bus.of(ContentRemoved) + this.contentStatusChangedSubscription = + this.ctx.bus.of(ContentStatusChanged) .subscribe(message => { if (this.content && message.content.id === this.content.id) { - this.router.navigate(['../'], { relativeTo: this.ctx.route }); + this.content = + this.content.changeStatus( + message.content.scheduledTo || message.content.status, + message.content.lastModifiedBy, + message.content.version, + message.content.lastModified, + message.content.scheduledAt); } }); 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 a00c846b8..ec262312c 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 @@ -11,9 +11,8 @@ import { Observable, Subscription } from 'rxjs'; import { ContentCreated, - ContentPublished, ContentRemoved, - ContentUnpublished, + ContentStatusChanged, ContentUpdated } from './../messages'; @@ -27,7 +26,8 @@ import { ImmutableArray, ModalView, Pager, - SchemaDetailsDto + SchemaDetailsDto, + DateTime } from 'shared'; @Component({ @@ -118,7 +118,7 @@ export class ContentsPageComponent implements OnDestroy, OnInit { } public publishContent(content: ContentDto) { - this.publishContentItem(content).subscribe(); + this.changeContentItem(content, 'publish', 'Published').subscribe(); } public publishSelected() { @@ -126,29 +126,15 @@ export class ContentsPageComponent implements OnDestroy, OnInit { this.contentItems.values .filter(c => this.selectedItems[c.id]) .filter(c => c.status !== 'Published') - .map(c => this.publishContentItem(c))) + .map(c => this.changeContentItem(c, 'publish', 'Published'))) .finally(() => { this.updateSelectionSummary(); }) .subscribe(); } - private publishContentItem(content: ContentDto): Observable { - return this.contentsService.publishContent(this.ctx.appName, this.schema.name, content.id, content.version) - .catch(error => { - this.ctx.notifyError(error); - - return Observable.throw(error); - }) - .do(dto => { - this.contentItems = this.contentItems.replaceBy('id', content.publish(this.ctx.userToken, dto.version)); - - this.emitContentPublished(content); - }); - } - public unpublishContent(content: ContentDto) { - this.unpublishContentItem(content).subscribe(); + this.changeContentItem(content, 'unpublish', 'Draft').subscribe(); } public unpublishSelected() { @@ -156,31 +142,31 @@ export class ContentsPageComponent implements OnDestroy, OnInit { this.contentItems.values .filter(c => this.selectedItems[c.id]) .filter(c => c.status !== 'Unpublished') - .map(c => this.unpublishContentItem(c))) + .map(c => this.changeContentItem(c, 'unpublish', 'Draft'))) .finally(() => { this.updateSelectionSummary(); }) .subscribe(); } - private unpublishContentItem(content: ContentDto): Observable { - return this.contentsService.unpublishContent(this.ctx.appName, this.schema.name, content.id, content.version) + private changeContentItem(content: ContentDto, action: string, status: string): Observable { + return this.contentsService.changeContentStatus(this.ctx.appName, this.schema.name, content.id, action, content.version) .catch(error => { this.ctx.notifyError(error); return Observable.throw(error); }) .do(dto => { - this.contentItems = this.contentItems.replaceBy('id', content.unpublish(this.ctx.userToken, dto.version)); + this.contentItems = this.contentItems.replaceBy('id', content.changeStatus(status, this.ctx.userToken, dto.version)); - this.emitContentUnpublished(content); + this.emitContentStatusChanged(content); }); } public archiveSelected() { Observable.forkJoin( this.contentItems.values.filter(c => this.selectedItems[c.id]) - .map(c => this.archiveContentItem(c))) + .map(c => this.changeContentItem(c, 'archive', 'Archived'))) .finally(() => { this.load(); }) @@ -188,26 +174,17 @@ export class ContentsPageComponent implements OnDestroy, OnInit { } public archiveContent(content: ContentDto) { - this.archiveContentItem(content) + this.changeContentItem(content, 'archive', 'Archived') .finally(() => { this.load(); }) .subscribe(); } - public archiveContentItem(content: ContentDto): Observable { - return this.contentsService.archiveContent(this.ctx.appName, this.schema.name, content.id, content.version) - .catch(error => { - this.ctx.notifyError(error); - - return Observable.throw(error); - }); - } - public restoreSelected() { Observable.forkJoin( this.contentItems.values.filter(c => this.selectedItems[c.id]) - .map(c => this.restoreContentItem(c))) + .map(c => this.changeContentItem(c, 'restore', 'Draft'))) .finally(() => { this.load(); }) @@ -215,22 +192,13 @@ export class ContentsPageComponent implements OnDestroy, OnInit { } public restoreContent(content: ContentDto) { - this.restoreContentItem(content) + this.changeContentItem(content, 'restore', 'Draft') .finally(() => { this.load(); }) .subscribe(); } - public restoreContentItem(content: ContentDto): Observable { - return this.contentsService.restoreContent(this.ctx.appName, this.schema.name, content.id, content.version) - .catch(error => { - this.ctx.notifyError(error); - - return Observable.throw(error); - }); - } - public deleteSelected(content: ContentDto) { Observable.forkJoin( this.contentItems.values.filter(c => this.selectedItems[c.id]) @@ -359,12 +327,8 @@ export class ContentsPageComponent implements OnDestroy, OnInit { this.languageSelected = language; } - private emitContentPublished(content: ContentDto) { - this.ctx.bus.emit(new ContentPublished(content)); - } - - private emitContentUnpublished(content: ContentDto) { - this.ctx.bus.emit(new ContentUnpublished(content)); + private emitContentStatusChanged(content: ContentDto) { + this.ctx.bus.emit(new ContentStatusChanged(content)); } private emitContentRemoved(content: ContentDto) { diff --git a/src/Squidex/app/features/content/pages/messages.ts b/src/Squidex/app/features/content/pages/messages.ts index 896f677e2..ca5a19fa4 100644 --- a/src/Squidex/app/features/content/pages/messages.ts +++ b/src/Squidex/app/features/content/pages/messages.ts @@ -5,7 +5,7 @@ * Copyright (c) Squidex UG (haftungsbeschränkt). All rights reserved. */ -import { ContentDto } from 'shared'; +import { ContentDto, DateTime } from 'shared'; export class ContentCreated { constructor( @@ -35,14 +35,7 @@ export class ContentVersionSelected { } } -export class ContentPublished { - constructor( - public readonly content: ContentDto - ) { - } -} - -export class ContentUnpublished { +export class ContentStatusChanged { constructor( public readonly content: ContentDto ) { diff --git a/src/Squidex/app/shared/services/assets.service.spec.ts b/src/Squidex/app/shared/services/assets.service.spec.ts index 1fb922072..09cb9af92 100644 --- a/src/Squidex/app/shared/services/assets.service.spec.ts +++ b/src/Squidex/app/shared/services/assets.service.spec.ts @@ -89,7 +89,7 @@ describe('AssetsService', () => { }); const req = httpMock.expectOne('http://service/p/api/apps/my-app/assets?$top=17&$skip=13'); - + expect(req.request.method).toEqual('GET'); expect(req.request.headers.get('If-Match')).toBeNull(); diff --git a/src/Squidex/app/shared/services/contents.service.spec.ts b/src/Squidex/app/shared/services/contents.service.spec.ts index 396fd3a2c..af7b1dc45 100644 --- a/src/Squidex/app/shared/services/contents.service.spec.ts +++ b/src/Squidex/app/shared/services/contents.service.spec.ts @@ -23,11 +23,12 @@ describe('ContentDto', () => { const creator = 'not-me'; const modified = DateTime.now(); const modifier = 'me'; + const dueTime = DateTime.now().addDays(1); const version = new Version('1'); const newVersion = new Version('2'); it('should update data property and user info when updating', () => { - const content_1 = new ContentDto('1', 'Published', creator, creator, creation, creation, { data: 1 }, version); + const content_1 = new ContentDto('1', 'Published', creator, creator, creation, creation, null, null, null, { data: 1 }, version); const content_2 = content_1.update({ data: 2 }, modifier, newVersion, modified); expect(content_2.data).toEqual({ data: 2 }); @@ -36,9 +37,9 @@ describe('ContentDto', () => { expect(content_2.version).toEqual(newVersion); }); - it('should update status property and user info when publishing', () => { - const content_1 = new ContentDto('1', 'Draft', creator, creator, creation, creation, { data: 1 }, version); - const content_2 = content_1.publish(modifier, newVersion, modified); + it('should update status property and user info when changing status', () => { + const content_1 = new ContentDto('1', 'Draft', creator, creator, creation, creation, null, null, null, { data: 1 }, version); + const content_2 = content_1.changeStatus('Published', modifier, newVersion, modified); expect(content_2.status).toEqual('Published'); expect(content_2.lastModified).toEqual(modified); @@ -46,40 +47,23 @@ describe('ContentDto', () => { expect(content_2.version).toEqual(newVersion); }); - it('should update status property and user info when unpublishing', () => { - const content_1 = new ContentDto('1', 'Published', creator, creator, creation, creation, { data: 1 }, version); - const content_2 = content_1.unpublish(modifier, newVersion, modified); - - expect(content_2.status).toEqual('Draft'); - expect(content_2.lastModified).toEqual(modified); - expect(content_2.lastModifiedBy).toEqual(modifier); - expect(content_2.version).toEqual(newVersion); - }); - - it('should update status property and user info when archiving', () => { - const content_1 = new ContentDto('1', 'Draft', creator, creator, creation, creation, { data: 1 }, version); - const content_2 = content_1.archive(modifier, newVersion, modified); - - expect(content_2.status).toEqual('Archived'); - expect(content_2.lastModified).toEqual(modified); - expect(content_2.lastModifiedBy).toEqual(modifier); - expect(content_2.version).toEqual(newVersion); - }); - - it('should update status property and user info when restoring', () => { - const content_1 = new ContentDto('1', 'Archived', creator, creator, creation, creation, { data: 1 }, version); - const content_2 = content_1.restore(modifier, newVersion, modified); + it('should update schedules property and user info when changing status with due time', () => { + const content_1 = new ContentDto('1', 'Draft', creator, creator, creation, creation, null, null, null, { data: 1 }, version); + const content_2 = content_1.changeStatus('Published', modifier, newVersion, modified, dueTime); expect(content_2.status).toEqual('Draft'); expect(content_2.lastModified).toEqual(modified); expect(content_2.lastModifiedBy).toEqual(modifier); + expect(content_2.scheduledAt).toEqual(dueTime); + expect(content_2.scheduledBy).toEqual(modifier); + expect(content_2.scheduledTo).toEqual('Published'); expect(content_2.version).toEqual(newVersion); }); it('should update data property when setting data', () => { const newData = {}; - const content_1 = new ContentDto('1', 'Published', creator, creator, creation, creation, { data: 1 }, version); + const content_1 = new ContentDto('1', 'Published', creator, creator, creation, creation, null, null, null, { data: 1 }, version); const content_2 = content_1.setData(newData); expect(content_2.data).toBe(newData); @@ -130,6 +114,9 @@ describe('ContentsService', () => { createdBy: 'Created1', lastModified: '2017-12-12T10:10', lastModifiedBy: 'LastModifiedBy1', + scheduledTo: 'Draft', + scheduledBy: 'Scheduler1', + scheduledAt: '2018-12-12T10:10', version: 11, data: {} }, @@ -151,11 +138,17 @@ describe('ContentsService', () => { new ContentDto('id1', 'Published', 'Created1', 'LastModifiedBy1', DateTime.parseISO_UTC('2016-12-12T10:10'), DateTime.parseISO_UTC('2017-12-12T10:10'), + 'Draft', + 'Scheduler1', + DateTime.parseISO_UTC('2018-12-12T10:10'), {}, new Version('11')), new ContentDto('id2', 'Published', 'Created2', 'LastModifiedBy2', DateTime.parseISO_UTC('2016-10-12T10:10'), DateTime.parseISO_UTC('2017-10-12T10:10'), + null, + null, + null, {}, new Version('22')) ])); @@ -221,6 +214,9 @@ describe('ContentsService', () => { createdBy: 'Created1', lastModified: '2017-12-12T10:10', lastModifiedBy: 'LastModifiedBy1', + scheduledTo: 'Draft', + scheduledBy: 'Scheduler1', + scheduledAt: '2018-12-12T10:10', data: {} }, { headers: { @@ -232,6 +228,9 @@ describe('ContentsService', () => { new ContentDto('id1', 'Published', 'Created1', 'LastModifiedBy1', DateTime.parseISO_UTC('2016-12-12T10:10'), DateTime.parseISO_UTC('2017-12-12T10:10'), + 'Draft', + 'Scheduler1', + DateTime.parseISO_UTC('2018-12-12T10:10'), {}, new Version('2'))); })); @@ -270,6 +269,9 @@ describe('ContentsService', () => { new ContentDto('id1', 'Published', 'Created1', 'LastModifiedBy1', DateTime.parseISO_UTC('2016-12-12T10:10'), DateTime.parseISO_UTC('2017-12-12T10:10'), + null, + null, + null, {}, new Version('2'))); })); @@ -310,10 +312,10 @@ describe('ContentsService', () => { req.flush({}); })); - it('should make put request to publish content', + it('should make put request to change content status', inject([ContentsService, HttpTestingController], (contentsService: ContentsService, httpMock: HttpTestingController) => { - contentsService.publishContent('my-app', 'my-schema', 'content1', version).subscribe(); + contentsService.changeContentStatus('my-app', 'my-schema', 'content1', 'publish', version).subscribe(); const req = httpMock.expectOne('http://service/p/api/content/my-app/my-schema/content1/publish'); @@ -323,38 +325,14 @@ describe('ContentsService', () => { req.flush({}); })); - it('should make put request to unpublish content', - inject([ContentsService, HttpTestingController], (contentsService: ContentsService, httpMock: HttpTestingController) => { - - contentsService.unpublishContent('my-app', 'my-schema', 'content1', version).subscribe(); - - const req = httpMock.expectOne('http://service/p/api/content/my-app/my-schema/content1/unpublish'); - - expect(req.request.method).toEqual('PUT'); - expect(req.request.headers.get('If-Match')).toEqual(version.value); - - req.flush({}); - })); - - it('should make put request to archive content', + it('should make put request with due time when status change is scheduled', inject([ContentsService, HttpTestingController], (contentsService: ContentsService, httpMock: HttpTestingController) => { - contentsService.archiveContent('my-app', 'my-schema', 'content1', version).subscribe(); - - const req = httpMock.expectOne('http://service/p/api/content/my-app/my-schema/content1/archive'); - - expect(req.request.method).toEqual('PUT'); - expect(req.request.headers.get('If-Match')).toEqual(version.value); - - req.flush({}); - })); - - it('should make put request to restore content', - inject([ContentsService, HttpTestingController], (contentsService: ContentsService, httpMock: HttpTestingController) => { + const dueTime = DateTime.parseISO_UTC('2016-12-12T10:10'); - contentsService.restoreContent('my-app', 'my-schema', 'content1', version).subscribe(); + contentsService.changeContentStatus('my-app', 'my-schema', 'content1', 'publish', version, dueTime).subscribe(); - const req = httpMock.expectOne('http://service/p/api/content/my-app/my-schema/content1/restore'); + const req = httpMock.expectOne('http://service/p/api/content/my-app/my-schema/content1/publish?dueTime=2016-12-12T10:10:00.000Z'); expect(req.request.method).toEqual('PUT'); expect(req.request.headers.get('If-Match')).toEqual(version.value); diff --git a/src/Squidex/app/shared/services/contents.service.ts b/src/Squidex/app/shared/services/contents.service.ts index 553912441..3b87d6874 100644 --- a/src/Squidex/app/shared/services/contents.service.ts +++ b/src/Squidex/app/shared/services/contents.service.ts @@ -36,6 +36,9 @@ export class ContentDto { public readonly lastModifiedBy: string, public readonly created: DateTime, public readonly lastModified: DateTime, + public readonly scheduledTo: string | null, + public readonly scheduledBy: string | null, + public readonly scheduledAt: DateTime | null, public readonly data: any, public readonly version: Version ) { @@ -49,34 +52,37 @@ export class ContentDto { this.lastModifiedBy, this.created, this.lastModified, + this.scheduledTo, + this.scheduledBy, + this.scheduledAt, data, this.version); } - public publish(user: string, version: Version, now?: DateTime): ContentDto { - return this.changeStatus('Published', user, version, now); - } - - public unpublish(user: string, version: Version, now?: DateTime): ContentDto { - return this.changeStatus('Draft', user, version, now); - } - - public archive(user: string, version: Version, now?: DateTime): ContentDto { - return this.changeStatus('Archived', user, version, now); - } - - public restore(user: string, version: Version, now?: DateTime): ContentDto { - return this.changeStatus('Draft', user, version, now); - } - - private changeStatus(status: string, user: string, version: Version, now?: DateTime): ContentDto { - return new ContentDto( - this.id, - status, - this.createdBy, user, - this.created, now || DateTime.now(), - this.data, - version); + public changeStatus(status: string, user: string, version: Version, now?: DateTime, dueTime: DateTime | null = null): ContentDto { + if (dueTime) { + return new ContentDto( + this.id, + this.status, + this.createdBy, user, + this.created, now || DateTime.now(), + status, + user, + dueTime, + this.data, + version); + } else { + return new ContentDto( + this.id, + status, + this.createdBy, user, + this.created, now || DateTime.now(), + null, + null, + null, + this.data, + version); + } } public update(data: any, user: string, version: Version, now?: DateTime): ContentDto { @@ -85,6 +91,9 @@ export class ContentDto { this.status, this.createdBy, user, this.created, now || DateTime.now(), + this.scheduledTo, + this.scheduledBy, + this.scheduledAt, data, version); } @@ -146,6 +155,9 @@ export class ContentsService { item.lastModifiedBy, DateTime.parseISO_UTC(item.created), DateTime.parseISO_UTC(item.lastModified), + item.scheduledTo || null, + item.scheduledBy || null, + item.scheduledAt ? DateTime.parseISO_UTC(item.scheduledAt) : null, item.data, new Version(item.version.toString())); })); @@ -167,6 +179,9 @@ export class ContentsService { body.lastModifiedBy, DateTime.parseISO_UTC(body.created), DateTime.parseISO_UTC(body.lastModified), + body.scheduledTo || null, + body.scheduledBy || null, + body.scheduledAt || null ? DateTime.parseISO_UTC(body.scheduledAt) : null, body.data, response.version); }) @@ -197,6 +212,9 @@ export class ContentsService { body.lastModifiedBy, DateTime.parseISO_UTC(body.created), DateTime.parseISO_UTC(body.lastModified), + null, + null, + null, body.data, response.version); }) @@ -231,43 +249,17 @@ export class ContentsService { .pretifyError('Failed to delete content. Please reload.'); } - public publishContent(appName: string, schemaName: string, id: string, version: Version): Observable> { - const url = this.apiUrl.buildUrl(`/api/content/${appName}/${schemaName}/${id}/publish`); - - return HTTP.putVersioned(this.http, url, {}, version) - .do(() => { - this.analytics.trackEvent('Content', 'Published', appName); - }) - .pretifyError('Failed to publish content. Please reload.'); - } - - public unpublishContent(appName: string, schemaName: string, id: string, version: Version): Observable> { - const url = this.apiUrl.buildUrl(`/api/content/${appName}/${schemaName}/${id}/unpublish`); - - return HTTP.putVersioned(this.http, url, {}, version) - .do(() => { - this.analytics.trackEvent('Content', 'Unpublished', appName); - }) - .pretifyError('Failed to unpublish content. Please reload.'); - } + public changeContentStatus(appName: string, schemaName: string, id: string, action: string, version: Version, dueTime?: DateTime): Observable> { + let url = this.apiUrl.buildUrl(`/api/content/${appName}/${schemaName}/${id}/${action}`); - public archiveContent(appName: string, schemaName: string, id: string, version: Version): Observable> { - const url = this.apiUrl.buildUrl(`/api/content/${appName}/${schemaName}/${id}/archive`); + if (dueTime) { + url += `?dueTime=${dueTime.toISOString()}`; + } return HTTP.putVersioned(this.http, url, {}, version) .do(() => { this.analytics.trackEvent('Content', 'Archived', appName); }) - .pretifyError('Failed to archive content. Please reload.'); - } - - public restoreContent(appName: string, schemaName: string, id: string, version: Version): Observable> { - const url = this.apiUrl.buildUrl(`/api/content/${appName}/${schemaName}/${id}/restore`); - - return HTTP.putVersioned(this.http, url, {}, version) - .do(() => { - this.analytics.trackEvent('Content', 'Restored', appName); - }) - .pretifyError('Failed to restore content. Please reload.'); + .pretifyError(`Failed to ${action} content. Please reload.`); } } \ No newline at end of file