diff --git a/src/Squidex/Areas/Api/Controllers/News/Models/FeatureDto.cs b/src/Squidex/Areas/Api/Controllers/News/Models/FeatureDto.cs index 7f1897047..a484d21b5 100644 --- a/src/Squidex/Areas/Api/Controllers/News/Models/FeatureDto.cs +++ b/src/Squidex/Areas/Api/Controllers/News/Models/FeatureDto.cs @@ -21,6 +21,6 @@ namespace Squidex.Areas.Api.Controllers.News.Models /// The description text. /// [Required] - public string Description { get; set; } + public string Text { get; set; } } } diff --git a/src/Squidex/Areas/Api/Controllers/News/MyNewsOptions.cs b/src/Squidex/Areas/Api/Controllers/News/MyNewsOptions.cs new file mode 100644 index 000000000..6467423d1 --- /dev/null +++ b/src/Squidex/Areas/Api/Controllers/News/MyNewsOptions.cs @@ -0,0 +1,26 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +namespace Squidex.Areas.Api.Controllers.News +{ + public sealed class MyNewsOptions + { + public string AppName { get; set; } + + public string ClientId { get; set; } + + public string ClientSecret { get; set; } + + public bool IsConfigured() + { + return + !string.IsNullOrWhiteSpace(AppName) && + !string.IsNullOrWhiteSpace(ClientId) && + !string.IsNullOrWhiteSpace(ClientSecret); + } + } +} diff --git a/src/Squidex/Areas/Api/Controllers/News/NewsController.cs b/src/Squidex/Areas/Api/Controllers/News/NewsController.cs index 01d714501..9a5b3b06b 100644 --- a/src/Squidex/Areas/Api/Controllers/News/NewsController.cs +++ b/src/Squidex/Areas/Api/Controllers/News/NewsController.cs @@ -20,11 +20,12 @@ namespace Squidex.Areas.Api.Controllers.News [ApiExplorerSettings(GroupName = nameof(Languages))] public sealed class NewsController : ApiController { - private readonly FeaturesService featuresService = new FeaturesService(); + private FeaturesService featuresService; - public NewsController(ICommandBus commandBus) + public NewsController(ICommandBus commandBus, FeaturesService featuresService) : base(commandBus) { + this.featuresService = featuresService; } /// diff --git a/src/Squidex/Areas/Api/Controllers/News/Service/FeaturesService.cs b/src/Squidex/Areas/Api/Controllers/News/Service/FeaturesService.cs index 050c738d8..bcfd0f24e 100644 --- a/src/Squidex/Areas/Api/Controllers/News/Service/FeaturesService.cs +++ b/src/Squidex/Areas/Api/Controllers/News/Service/FeaturesService.cs @@ -8,6 +8,7 @@ using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; +using Microsoft.Extensions.Options; using Squidex.Areas.Api.Controllers.News.Models; using Squidex.ClientLibrary; @@ -15,9 +16,6 @@ namespace Squidex.Areas.Api.Controllers.News.Service { public sealed class FeaturesService { - private const string AppName = "squidex-website"; - private const string ClientId = "squidex-website:default"; - private const string ClientSecret = "QGgqxd7bDHBTEkpC6fj8sbdPWgZrPrPfr3xzb3LKoec="; private const int FeatureVersion = 1; private static readonly QueryContext Flatten = QueryContext.Default.Flatten(); private readonly SquidexClient client; @@ -26,20 +24,26 @@ namespace Squidex.Areas.Api.Controllers.News.Service { } - public FeaturesService() + public FeaturesService(IOptions options) { - var clientManager = new SquidexClientManager("https://cloud.squidex.io", AppName, ClientId, ClientSecret); + if (options.Value.IsConfigured()) + { + var clientManager = new SquidexClientManager("https://cloud.squidex.io", + options.Value.AppName, + options.Value.ClientId, + options.Value.ClientSecret); - client = clientManager.GetClient("feature-news"); + client = clientManager.GetClient("feature-news"); + } } public async Task GetFeaturesAsync(int version = 0) { var result = new FeaturesDto { Features = new List(), Version = FeatureVersion }; - if (version < FeatureVersion) + if (client != null && version < FeatureVersion) { - var entities = await client.GetAsync(filter: $"data/version/iv ge ${version}", context: Flatten); + var entities = await client.GetAsync(filter: $"data/version/iv ge {version}", context: Flatten); result.Features.AddRange(entities.Items.Select(x => x.Data)); } diff --git a/src/Squidex/Config/Domain/InfrastructureServices.cs b/src/Squidex/Config/Domain/InfrastructureServices.cs index 14d1128f9..bdfedbaf5 100644 --- a/src/Squidex/Config/Domain/InfrastructureServices.cs +++ b/src/Squidex/Config/Domain/InfrastructureServices.cs @@ -12,6 +12,7 @@ using Microsoft.AspNetCore.Mvc.Infrastructure; using Microsoft.Extensions.Caching.Memory; using Microsoft.Extensions.DependencyInjection; using NodaTime; +using Squidex.Areas.Api.Controllers.News.Service; using Squidex.Domain.Apps.Entities.Apps.Diagnostics; using Squidex.Domain.Users; using Squidex.Infrastructure; @@ -36,6 +37,9 @@ namespace Squidex.Config.Domain services.AddSingletonAs(SystemClock.Instance) .As(); + services.AddSingletonAs() + .AsSelf(); + services.AddSingletonAs() .AsSelf(); diff --git a/src/Squidex/Squidex.csproj b/src/Squidex/Squidex.csproj index cda49b8ed..397d4f57c 100644 --- a/src/Squidex/Squidex.csproj +++ b/src/Squidex/Squidex.csproj @@ -94,6 +94,7 @@ + diff --git a/src/Squidex/WebStartup.cs b/src/Squidex/WebStartup.cs index 717b8fd79..b657d4083 100644 --- a/src/Squidex/WebStartup.cs +++ b/src/Squidex/WebStartup.cs @@ -12,6 +12,7 @@ using Microsoft.Extensions.DependencyInjection; using Squidex.Areas.Api; using Squidex.Areas.Api.Config.Swagger; using Squidex.Areas.Api.Controllers.Contents; +using Squidex.Areas.Api.Controllers.News; using Squidex.Areas.Frontend; using Squidex.Areas.IdentityServer; using Squidex.Areas.IdentityServer.Config; @@ -92,6 +93,8 @@ namespace Squidex config.GetSection("ui")); services.Configure( config.GetSection("usage")); + services.Configure( + config.GetSection("news")); var provider = services.AddAndBuildOrleans(configuration, afterServices => { diff --git a/src/Squidex/app/features/apps/declarations.ts b/src/Squidex/app/features/apps/declarations.ts index 880671f15..17e2a5940 100644 --- a/src/Squidex/app/features/apps/declarations.ts +++ b/src/Squidex/app/features/apps/declarations.ts @@ -6,4 +6,5 @@ */ export * from './pages/apps-page.component'; +export * from './pages/news-dialog.component'; export * from './pages/onboarding-dialog.component'; \ No newline at end of file diff --git a/src/Squidex/app/features/apps/module.ts b/src/Squidex/app/features/apps/module.ts index 69c393429..16387df1f 100644 --- a/src/Squidex/app/features/apps/module.ts +++ b/src/Squidex/app/features/apps/module.ts @@ -12,6 +12,7 @@ import { SqxFrameworkModule, SqxSharedModule } from '@app/shared'; import { AppsPageComponent, + NewsDialogComponent, OnboardingDialogComponent } from './declarations'; @@ -30,6 +31,7 @@ const routes: Routes = [ ], declarations: [ AppsPageComponent, + NewsDialogComponent, OnboardingDialogComponent ] }) 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 ac8d5dba4..6bfb9fa97 100644 --- a/src/Squidex/app/features/apps/pages/apps-page.component.html +++ b/src/Squidex/app/features/apps/pages/apps-page.component.html @@ -99,6 +99,10 @@ - - + + + + + + \ No newline at end of file diff --git a/src/Squidex/app/features/apps/pages/apps-page.component.ts b/src/Squidex/app/features/apps/pages/apps-page.component.ts index c081ccb84..275437e7a 100644 --- a/src/Squidex/app/features/apps/pages/apps-page.component.ts +++ b/src/Squidex/app/features/apps/pages/apps-page.component.ts @@ -12,7 +12,9 @@ import { AppsState, AuthService, DialogModel, - ModalModel, + FeatureDto, + LocalStoreService, + NewsService, OnboardingService } from '@app/shared'; @@ -25,11 +27,16 @@ export class AppsPageComponent implements OnInit { public addAppDialog = new DialogModel(); public addAppTemplate = ''; - public onboardingModal = new ModalModel(); + public onboardingDialog = new DialogModel(); + + public newsFeatures: FeatureDto[]; + public newsDialog = new DialogModel(); constructor( public readonly appsState: AppsState, public readonly authState: AuthService, + private readonly localStore: LocalStoreService, + private readonly newsService: NewsService, private readonly onboardingService: OnboardingService ) { } @@ -38,9 +45,24 @@ export class AppsPageComponent implements OnInit { this.appsState.apps.pipe( take(1)) .subscribe(apps => { - if (this.onboardingService.shouldShow('dialog') && apps.length === 0) { - this.onboardingService.disable('dialog'); - this.onboardingModal.show(); + if (this.onboardingService.shouldShow('dialog')) { + if (apps.length === 0) { + this.onboardingService.disable('dialog'); + this.onboardingDialog.show(); + } + } else { + const newsVersion = this.localStore.getInt('squidex.news.version'); + + this.newsService.getFeatures(newsVersion).subscribe(result => { + if (result.version !== newsVersion) { + if (result.features.length > 0) { + this.newsFeatures = result.features; + this.newsDialog.show(); + } + + this.localStore.setInt('squidex.news.version', result.version); + } + }); } }); } diff --git a/src/Squidex/app/features/apps/pages/news-dialog.component.html b/src/Squidex/app/features/apps/pages/news-dialog.component.html new file mode 100644 index 000000000..1fbd2b569 --- /dev/null +++ b/src/Squidex/app/features/apps/pages/news-dialog.component.html @@ -0,0 +1,17 @@ + + + New Features + + + +
+

What's new?

+ +
+

{{feature.name}}

+ +
+
+
+
+
diff --git a/src/Squidex/app/features/apps/pages/news-dialog.component.scss b/src/Squidex/app/features/apps/pages/news-dialog.component.scss new file mode 100644 index 000000000..4f548fe98 --- /dev/null +++ b/src/Squidex/app/features/apps/pages/news-dialog.component.scss @@ -0,0 +1,16 @@ +@import '_vars'; +@import '_mixins'; + +:host /deep/ { + img { + @include box-shadow(0, 4px, 20px, .2); + width: 80%; + margin: 0 auto; + margin-top: 10px; + display: block; + } + + p { + margin-bottom: 1.5rem; + } +} \ No newline at end of file diff --git a/src/Squidex/app/features/apps/pages/news-dialog.component.ts b/src/Squidex/app/features/apps/pages/news-dialog.component.ts new file mode 100644 index 000000000..2a924341e --- /dev/null +++ b/src/Squidex/app/features/apps/pages/news-dialog.component.ts @@ -0,0 +1,27 @@ +/* + * Squidex Headless CMS + * + * @license + * Copyright (c) Squidex UG (haftungsbeschränkt). All rights reserved. + */ + +import { Component, EventEmitter, Input, Output } from '@angular/core'; + +import { FeatureDto } from '@app/shared'; + +@Component({ + selector: 'sqx-news-dialog', + styleUrls: ['./news-dialog.component.scss'], + templateUrl: './news-dialog.component.html' +}) +export class NewsDialogComponent { + @Input() + public features: FeatureDto[]; + + @Output() + public closed = new EventEmitter(); + + public close() { + this.closed.emit(); + } +} \ No newline at end of file diff --git a/src/Squidex/app/framework/services/local-store.service.spec.ts b/src/Squidex/app/framework/services/local-store.service.spec.ts index 20fa0d3a6..96e4dfcc0 100644 --- a/src/Squidex/app/framework/services/local-store.service.spec.ts +++ b/src/Squidex/app/framework/services/local-store.service.spec.ts @@ -85,4 +85,16 @@ describe('LocalStore', () => { expect(localStoreService.getBoolean('not_set')).toBe(false); }); + + it('should get int from local store', () => { + const localStoreService = new LocalStoreService(); + + localStoreService.set('key1', 'abc'); + localStoreService.setInt('key2', 2); + + expect(localStoreService.getInt('key1', 13)).toBe(13); + expect(localStoreService.getInt('key2', 13)).toBe(2); + + expect(localStoreService.getInt('not_set', 13)).toBe(13); + }); }); diff --git a/src/Squidex/app/framework/services/local-store.service.ts b/src/Squidex/app/framework/services/local-store.service.ts index bcf718002..ad4d39ecb 100644 --- a/src/Squidex/app/framework/services/local-store.service.ts +++ b/src/Squidex/app/framework/services/local-store.service.ts @@ -34,6 +34,12 @@ export class LocalStoreService { return value === 'true'; } + public getInt(key: string, fallback = 0): number { + const value = this.get(key); + + return value ? (parseInt(value, 10) || fallback) : fallback; + } + public set(key: string, value: string) { try { this.store.setItem(key, value); @@ -47,4 +53,10 @@ export class LocalStoreService { this.store.setItem(key, converted); } + + public setInt(key: string, value: number) { + const converted = `${value}`; + + this.store.setItem(key, converted); + } } \ No newline at end of file diff --git a/src/Squidex/app/shared/module.ts b/src/Squidex/app/shared/module.ts index 2e7a9c5e9..598413880 100644 --- a/src/Squidex/app/shared/module.ts +++ b/src/Squidex/app/shared/module.ts @@ -139,6 +139,7 @@ import { FileIconPipe, GeolocationEditorComponent, HelpComponent, + HelpMarkdownPipe, HistoryComponent, HistoryListComponent, HistoryMessagePipe, diff --git a/src/Squidex/app/shared/services/comments.service.ts b/src/Squidex/app/shared/services/comments.service.ts index e60a44f32..dc2dc762c 100644 --- a/src/Squidex/app/shared/services/comments.service.ts +++ b/src/Squidex/app/shared/services/comments.service.ts @@ -15,8 +15,7 @@ import { DateTime, Model, pretifyError, - Version, - HTTP + Version } from '@app/framework'; export class CommentsDto extends Model { @@ -63,27 +62,25 @@ export class CommentsService { public getComments(appName: string, commentsId: string, version: Version): Observable { const url = this.apiUrl.buildUrl(`api/apps/${appName}/comments/${commentsId}?version=${version.value}`); - return HTTP.getVersioned(this.http, url).pipe( + return this.http.get(url).pipe( map(response => { - const body: any = response.payload.body; - return new CommentsDto( - body.createdComments.map((item: any) => { + response.createdComments.map((item: any) => { return new CommentDto( item.id, DateTime.parseISO_UTC(item.time), item.text, item.user); }), - body.updatedComments.map((item: any) => { + response.updatedComments.map((item: any) => { return new CommentDto( item.id, DateTime.parseISO_UTC(item.time), item.text, item.user); }), - body.deletedComments, - new Version(body.version) + response.deletedComments, + new Version(response.version) ); }), pretifyError('Failed to load comments.')); diff --git a/src/Squidex/app/shared/services/news.service.spec.ts b/src/Squidex/app/shared/services/news.service.spec.ts index 9e3be0005..ccba7d3c9 100644 --- a/src/Squidex/app/shared/services/news.service.spec.ts +++ b/src/Squidex/app/shared/services/news.service.spec.ts @@ -37,11 +37,11 @@ describe('NewsService', () => { let features: FeaturesDto; - newsService.getFeatures().subscribe(result => { + newsService.getFeatures(13).subscribe(result => { features = result; }); - const req = httpMock.expectOne('http://service/p/api/news/features'); + const req = httpMock.expectOne('http://service/p/api/news/features?version=13'); expect(req.request.method).toEqual('GET'); expect(req.request.headers.get('If-Match')).toBeNull(); diff --git a/src/Squidex/app/shared/services/news.service.ts b/src/Squidex/app/shared/services/news.service.ts index 816a45997..406bdce96 100644 --- a/src/Squidex/app/shared/services/news.service.ts +++ b/src/Squidex/app/shared/services/news.service.ts @@ -40,8 +40,8 @@ export class NewsService { ) { } - public getFeatures(): Observable { - const url = this.apiUrl.buildUrl('api/news/features'); + public getFeatures(version: number): Observable { + const url = this.apiUrl.buildUrl(`api/news/features?version=${version}`); return HTTP.getVersioned(this.http, url).pipe( map(response => { diff --git a/src/Squidex/appsettings.json b/src/Squidex/appsettings.json index 14e289162..e022b1a26 100644 --- a/src/Squidex/appsettings.json +++ b/src/Squidex/appsettings.json @@ -314,5 +314,17 @@ * The client secret for twitter. */ "clientSecret": "Pdu9wdN72T33KJRFdFy1w4urBKDRzIyuKpc0OItQC2E616DuZD" + }, + + "news": { + /* + * The app name where the news are stored. + */ + "appName": "squidex-website", + /* + * The credentials to the app (Readonly). + */ + "clientId": "squidex-website:default", + "clientSecret": "QGgqxd7bDHBTEkpC6fj8sbdPWgZrPrPfr3xzb3LKoec=" } }