diff --git a/backend/src/Squidex.Domain.Apps.Entities.MongoDb/Contents/MongoContentCollection.cs b/backend/src/Squidex.Domain.Apps.Entities.MongoDb/Contents/MongoContentCollection.cs
index 22120a31d..d9bbaa630 100644
--- a/backend/src/Squidex.Domain.Apps.Entities.MongoDb/Contents/MongoContentCollection.cs
+++ b/backend/src/Squidex.Domain.Apps.Entities.MongoDb/Contents/MongoContentCollection.cs
@@ -133,7 +133,7 @@ namespace Squidex.Domain.Apps.Entities.MongoDb.Contents
foreach (var entity in contentItems)
{
- var schema = schemas.FirstOrDefault(x => x.Id == entity.IndexedSchemaId);
+ var schema = schemas.FirstOrDefault(x => x?.Id == entity.IndexedSchemaId);
if (schema != null)
{
diff --git a/backend/src/Squidex/Areas/Api/Controllers/Apps/AppsController.cs b/backend/src/Squidex/Areas/Api/Controllers/Apps/AppsController.cs
index 49c9ebb52..fff15b861 100644
--- a/backend/src/Squidex/Areas/Api/Controllers/Apps/AppsController.cs
+++ b/backend/src/Squidex/Areas/Api/Controllers/Apps/AppsController.cs
@@ -81,6 +81,8 @@ namespace Squidex.Areas.Api.Controllers.Apps
var response = Deferred.Response(() =>
{
+ var userPermissions = HttpContext.Permissions();
+
return apps.OrderBy(x => x.Name).Select(a => AppDto.FromApp(a, userOrClientId, userPermissions, appPlansProvider, this)).ToArray();
});
@@ -89,6 +91,34 @@ namespace Squidex.Areas.Api.Controllers.Apps
return Ok(response);
}
+ ///
+ /// Get an app by name.
+ ///
+ /// The name of the app.
+ ///
+ /// 200 => Apps returned.
+ /// 404 => App not found.
+ ///
+ [HttpGet]
+ [Route("apps/{app}")]
+ [ProducesResponseType(typeof(AppDto), 200)]
+ [ApiPermission]
+ [ApiCosts(0)]
+ public IActionResult GetApp(string app)
+ {
+ var response = Deferred.Response(() =>
+ {
+ var userOrClientId = HttpContext.User.UserOrClientId()!;
+ var userPermissions = HttpContext.Permissions();
+
+ return AppDto.FromApp(App, userOrClientId, userPermissions, appPlansProvider, this);
+ });
+
+ Response.Headers[HeaderNames.ETag] = App.ToEtag();
+
+ return Ok(response);
+ }
+
///
/// Create a new app.
///
diff --git a/frontend/app/features/settings/pages/more/more-page.component.ts b/frontend/app/features/settings/pages/more/more-page.component.ts
index d293e06a8..cbe2b32ad 100644
--- a/frontend/app/features/settings/pages/more/more-page.component.ts
+++ b/frontend/app/features/settings/pages/more/more-page.component.ts
@@ -8,6 +8,7 @@
import { Component, OnInit } from '@angular/core';
import { FormBuilder } from '@angular/forms';
import { Router } from '@angular/router';
+import { map } from 'rxjs/operators';
import {
AppDto,
@@ -44,7 +45,7 @@ export class MorePageComponent extends ResourceOwner implements OnInit {
public ngOnInit() {
this.own(
- this.appsState.selectedApp
+ this.appsState.reloadSelected().pipe(map(x => x!))
.subscribe(app => {
this.app = app;
@@ -66,8 +67,8 @@ export class MorePageComponent extends ResourceOwner implements OnInit {
if (value) {
this.appsState.update(this.app, value)
- .subscribe(user => {
- this.updateForm.submitCompleted({ newValue: user });
+ .subscribe(app => {
+ this.updateForm.submitCompleted({ newValue: app });
}, error => {
this.updateForm.submitFailed(error);
});
diff --git a/frontend/app/framework/angular/modals/dialog-renderer.component.scss b/frontend/app/framework/angular/modals/dialog-renderer.component.scss
index 2e7f2fc21..c486e537c 100644
--- a/frontend/app/framework/angular/modals/dialog-renderer.component.scss
+++ b/frontend/app/framework/angular/modals/dialog-renderer.component.scss
@@ -46,7 +46,7 @@
.overlay {
@include absolute(0, auto, 0, 0);
animation: width 10s 1 linear;
- background: $color-white;
+ background: $color-black;
border: 0;
opacity: .1;
overflow: hidden;
diff --git a/frontend/app/shared/services/apps.service.spec.ts b/frontend/app/shared/services/apps.service.spec.ts
index b947aacbe..31c7c49ad 100644
--- a/frontend/app/shared/services/apps.service.spec.ts
+++ b/frontend/app/shared/services/apps.service.spec.ts
@@ -62,6 +62,25 @@ describe('AppsService', () => {
expect(apps!).toEqual([createApp(12), createApp(13)]);
}));
+ it('should make get request to get app',
+ inject([AppsService, HttpTestingController], (appsService: AppsService, httpMock: HttpTestingController) => {
+
+ let app: AppDto;
+
+ appsService.getApp('my-app').subscribe(result => {
+ app = result;
+ });
+
+ const req = httpMock.expectOne('http://service/p/api/apps/my-app');
+
+ expect(req.request.method).toEqual('GET');
+ expect(req.request.headers.get('If-Match')).toBeNull();
+
+ req.flush(appResponse(12));
+
+ expect(app!).toEqual(createApp(12));
+ }));
+
it('should make post request to create app',
inject([AppsService, HttpTestingController], (appsService: AppsService, httpMock: HttpTestingController) => {
diff --git a/frontend/app/shared/services/apps.service.ts b/frontend/app/shared/services/apps.service.ts
index fb54ec4c9..321fc6de9 100644
--- a/frontend/app/shared/services/apps.service.ts
+++ b/frontend/app/shared/services/apps.service.ts
@@ -119,6 +119,18 @@ export class AppsService {
pretifyError('Failed to load apps. Please reload.'));
}
+ public getApp(name: string): Observable {
+ const url = this.apiUrl.buildUrl(`/api/apps/${name}`);
+
+ return this.http.get(url).pipe(
+ map(body => {
+ const app = parseApp(body);
+
+ return app;
+ }),
+ pretifyError('Failed to load app. Please reload.'));
+ }
+
public postApp(dto: CreateAppDto): Observable {
const url = this.apiUrl.buildUrl('api/apps');
diff --git a/frontend/app/shared/state/apps.state.spec.ts b/frontend/app/shared/state/apps.state.spec.ts
index fd5689443..b85b7a1ae 100644
--- a/frontend/app/shared/state/apps.state.spec.ts
+++ b/frontend/app/shared/state/apps.state.spec.ts
@@ -5,7 +5,7 @@
* Copyright (c) Squidex UG (haftungsbeschränkt). All rights reserved.
*/
-import { of } from 'rxjs';
+import { of, throwError } from 'rxjs';
import { IMock, Mock } from 'typemoq';
import {
@@ -56,7 +56,7 @@ describe('AppsState', () => {
expect(appsState.snapshot.selectedApp).toBe(app1);
});
- it('should return null on select when unselecting user', () => {
+ it('should return null on select when unselecting app', () => {
let selectedApp: AppDto;
appsState.select(null).subscribe(x => {
@@ -67,9 +67,40 @@ describe('AppsState', () => {
expect(appsState.snapshot.selectedApp).toBeNull();
});
- it('should return null on select when apps is not found', () => {
+ it('should return new app when loaded', () => {
+ const newApp = createApp(1, '_new');
+
+ appsService.setup(x => x.getApp(app1.name))
+ .returns(() => of(newApp));
+
+ let selectedApp: AppDto;
+
+ appsState.loadApp(app1.name).subscribe(x => {
+ selectedApp = x!;
+ });
+
+ expect(selectedApp!).toEqual(newApp);
+ expect(appsState.snapshot.selectedApp).toBeNull();
+ });
+
+ it('should return new app when reloaded', () => {
+ const newApp = createApp(1, '_new');
+
+ appsService.setup(x => x.getApp(app1.name))
+ .returns(() => of(newApp));
+
+ appsState.select(app1.name).subscribe();
+ appsState.reloadSelected();
+
+ expect(appsState.snapshot.selectedApp).toEqual(newApp);
+ });
+
+ it('should return null on select when app is not found', () => {
let selectedApp: AppDto;
+ appsService.setup(x => x.getApp('unknown'))
+ .returns(() => throwError(new Error('404')));
+
appsState.select('unknown').subscribe(x => {
selectedApp = x!;
});
@@ -152,6 +183,9 @@ describe('AppsState', () => {
appsService.setup(x => x.deleteApp(app1))
.returns(() => of({})).verifiable();
+ appsService.setup(x => x.getApp(app1.name))
+ .returns(() => of(app1));
+
appsState.select(app1.name).subscribe();
appsState.delete(app1).subscribe();
diff --git a/frontend/app/shared/state/apps.state.ts b/frontend/app/shared/state/apps.state.ts
index 5b9b9a958..dbc60840e 100644
--- a/frontend/app/shared/state/apps.state.ts
+++ b/frontend/app/shared/state/apps.state.ts
@@ -7,7 +7,7 @@
import { Injectable } from '@angular/core';
import { Observable, of } from 'rxjs';
-import { tap } from 'rxjs/operators';
+import { catchError, tap } from 'rxjs/operators';
import {
defined,
@@ -58,18 +58,40 @@ export class AppsState extends State {
super({ apps: [], selectedApp: null });
}
- public select(name: string | null): Observable {
- const observable =
- !name ?
- of(null) :
- of(this.snapshot.apps.find(x => x.name === name) || null);
+ public reloadSelected() {
+ return this.loadApp(this.appName).pipe(
+ shareSubscribed(this.dialogs));
+ }
- return observable.pipe(
+ public select(name: string | null): Observable {
+ return this.loadApp(name, true).pipe(
tap(selectedApp => {
- this.next(s => ({ ...s, selectedApp }));
+ this.next(s => {
+ return { ...s, selectedApp };
+ });
}));
}
+ public loadApp(name: string | null, cached = false) {
+ if (!name) {
+ return of(null);
+ }
+
+ if (cached) {
+ const found = this.snapshot.apps.find(x => x.name === name);
+
+ if (found) {
+ return of(found);
+ }
+ }
+
+ return this.appsService.getApp(name).pipe(
+ tap(app => {
+ this.replaceApp(app, app);
+ }),
+ catchError(() => of(null)));
+ }
+
public load(): Observable {
return this.appsService.getApps().pipe(
tap(apps => {
diff --git a/frontend/app/shared/state/schemas.state.spec.ts b/frontend/app/shared/state/schemas.state.spec.ts
index 94023646c..6515f2732 100644
--- a/frontend/app/shared/state/schemas.state.spec.ts
+++ b/frontend/app/shared/state/schemas.state.spec.ts
@@ -236,7 +236,7 @@ describe('SchemasState', () => {
expect().nothing();
});
- it('should return null on select when loading failed', () => {
+ it('should return null on select when loading failed', () => {
schemasService.setup(x => x.getSchema(app, 'failed'))
.returns(() => throwError({})).verifiable();
diff --git a/frontend/app/theme/_vars.scss b/frontend/app/theme/_vars.scss
index 90d90a126..a6d6d1c91 100644
--- a/frontend/app/theme/_vars.scss
+++ b/frontend/app/theme/_vars.scss
@@ -35,6 +35,7 @@ $color-theme-orange-dark: #a65b00;
$color-theme-error: #eb3142;
$color-theme-error-dark: #c00;
+$color-black: #000;
$color-white: #fff;
$color-theme-info: #5bc0de;