Browse Source

POST fallback for queries. (#496)

* POST fallback for queries.
pull/498/head
Sebastian Stehle 6 years ago
committed by GitHub
parent
commit
ea82ff4ca0
No known key found for this signature in database GPG Key ID: 4AEE18F83AFDEB23
  1. 8
      backend/src/Squidex/Areas/Api/Controllers/Assets/AssetContentController.cs
  2. 43
      backend/src/Squidex/Areas/Api/Controllers/Assets/AssetsController.cs
  3. 2
      backend/src/Squidex/Areas/Api/Controllers/Assets/Models/AssetContentQueryDto.cs
  4. 75
      backend/src/Squidex/Areas/Api/Controllers/Contents/ContentsController.cs
  5. 22
      backend/src/Squidex/Areas/Api/Controllers/Contents/Models/ContentsIdsQueryDto.cs
  6. 61
      backend/src/Squidex/Areas/Api/Controllers/QueryDto.cs
  7. 2
      frontend/app/features/content/shared/forms/assets-editor.component.ts
  8. 6
      frontend/app/shared/components/assets/asset-dialog.component.scss
  9. 5
      frontend/app/shared/components/comments/comments.component.html
  10. 6
      frontend/app/shared/components/comments/comments.component.ts
  11. 2
      frontend/app/shared/components/forms/references-dropdown.component.ts
  12. 2
      frontend/app/shared/components/forms/references-tags.component.ts
  13. 63
      frontend/app/shared/services/assets.service.spec.ts
  14. 48
      frontend/app/shared/services/assets.service.ts
  15. 118
      frontend/app/shared/services/contents.service.spec.ts
  16. 81
      frontend/app/shared/services/contents.service.ts
  17. 25
      frontend/app/shared/state/assets.state.spec.ts
  18. 23
      frontend/app/shared/state/assets.state.ts
  19. 14
      frontend/app/shared/state/contents.state.ts
  20. 6
      frontend/app/shared/state/query.ts
  21. 78
      frontend/package-lock.json
  22. 22
      frontend/package.json

8
backend/src/Squidex/Areas/Api/Controllers/Assets/AssetContentController.cs

@ -68,7 +68,7 @@ namespace Squidex.Areas.Api.Controllers.Assets
[ApiPermission]
[ApiCosts(0.5)]
[AllowAnonymous]
public async Task<IActionResult> GetAssetContentBySlug(string app, string idOrSlug, string more, [FromQuery] AssetQuery query)
public async Task<IActionResult> GetAssetContentBySlug(string app, string idOrSlug, string more, [FromQuery] AssetContentQueryDto query)
{
IAssetEntity? asset;
@ -99,16 +99,16 @@ namespace Squidex.Areas.Api.Controllers.Assets
[ApiPermission]
[ApiCosts(0.5)]
[AllowAnonymous]
public async Task<IActionResult> GetAssetContent(Guid id, [FromQuery] AssetQuery query)
public async Task<IActionResult> GetAssetContent(Guid id, [FromQuery] AssetContentQueryDto query)
{
var asset = await assetRepository.FindAssetAsync(id);
return DeliverAsset(asset, query);
}
private IActionResult DeliverAsset(IAssetEntity? asset, AssetQuery query)
private IActionResult DeliverAsset(IAssetEntity? asset, AssetContentQueryDto query)
{
query ??= new AssetQuery();
query ??= new AssetContentQueryDto();
if (asset == null || asset.FileVersion < query.Version)
{

43
backend/src/Squidex/Areas/Api/Controllers/Assets/AssetsController.cs

@ -96,11 +96,36 @@ namespace Squidex.Areas.Api.Controllers.Assets
[ApiCosts(1)]
public async Task<IActionResult> GetAssets(string app, [FromQuery] Guid? parentId, [FromQuery] string? ids = null, [FromQuery] string? q = null)
{
var assets = await assetQuery.QueryAsync(Context, parentId,
Q.Empty
.WithIds(ids)
.WithJsonQuery(q)
.WithODataQuery(Request.QueryString.ToString()));
var assets = await assetQuery.QueryAsync(Context, parentId, CreateQuery(ids, q));
var response = Deferred.Response(() =>
{
return AssetsDto.FromAssets(assets, this, app);
});
return Ok(response);
}
/// <summary>
/// Get assets.
/// </summary>
/// <param name="app">The name of the app.</param>
/// <param name="query">The required query object.
/// <returns>
/// 200 => Assets returned.
/// 404 => App not found.
/// </returns>
/// <remarks>
/// Get all assets for the app.
/// </remarks>
[HttpPost]
[Route("apps/{app}/assets/query")]
[ProducesResponseType(typeof(AssetsDto), 200)]
[ApiPermission(Permissions.AppAssetsRead)]
[ApiCosts(1)]
public async Task<IActionResult> GetAssetsPost(string app, [FromBody] QueryDto query)
{
var assets = await assetQuery.QueryAsync(Context, query?.ParentId, query?.ToQuery() ?? Q.Empty);
var response = Deferred.Response(() =>
{
@ -309,5 +334,13 @@ namespace Squidex.Areas.Api.Controllers.Assets
return file.ToAssetFile();
}
private Q CreateQuery(string? ids, string? q)
{
return Q.Empty
.WithIds(ids)
.WithJsonQuery(q)
.WithODataQuery(Request.QueryString.ToString());
}
}
}

2
backend/src/Squidex/Areas/Api/Controllers/Assets/Models/AssetQuery.cs → backend/src/Squidex/Areas/Api/Controllers/Assets/Models/AssetContentQueryDto.cs

@ -13,7 +13,7 @@ using Squidex.Infrastructure.Reflection;
namespace Squidex.Areas.Api.Controllers.Assets.Models
{
public sealed class AssetQuery
public sealed class AssetContentQueryDto
{
/// <summary>
/// The optional version of the asset.

75
backend/src/Squidex/Areas/Api/Controllers/Contents/ContentsController.cs

@ -130,6 +130,35 @@ namespace Squidex.Areas.Api.Controllers.Contents
return Ok(response);
}
/// <summary>
/// Queries contents.
/// </summary>
/// <param name="app">The name of the app.</param>
/// <param name="query">The required query object.</param>
/// <returns>
/// 200 => Contents retrieved.
/// 404 => App not found.
/// </returns>
/// <remarks>
/// You can read the generated documentation for your app at /api/content/{appName}/docs.
/// </remarks>
[HttpPost]
[Route("content/{app}/")]
[ProducesResponseType(typeof(ContentsDto), 200)]
[ApiPermission]
[ApiCosts(1)]
public async Task<IActionResult> GetAllContentsPost(string app, [FromBody] ContentsIdsQueryDto query)
{
var contents = await contentQuery.QueryAsync(Context, query.Ids);
var response = Deferred.AsyncResponse(() =>
{
return ContentsDto.FromContentsAsync(contents, Context, this, null, contentWorkflow);
});
return Ok(response);
}
/// <summary>
/// Queries contents.
/// </summary>
@ -153,11 +182,39 @@ namespace Squidex.Areas.Api.Controllers.Contents
{
var schema = await contentQuery.GetSchemaOrThrowAsync(Context, name);
var contents = await contentQuery.QueryAsync(Context, name,
Q.Empty
.WithIds(ids)
.WithJsonQuery(q)
.WithODataQuery(Request.QueryString.ToString()));
var contents = await contentQuery.QueryAsync(Context, name, CreateQuery(ids, q));
var response = Deferred.AsyncResponse(async () =>
{
return await ContentsDto.FromContentsAsync(contents, Context, this, schema, contentWorkflow);
});
return Ok(response);
}
/// <summary>
/// Queries contents.
/// </summary>
/// <param name="app">The name of the app.</param>
/// <param name="name">The name of the schema.</param>
/// <param name="query">The required query object.</param>
/// <returns>
/// 200 => Contents retrieved.
/// 404 => Schema or app not found.
/// </returns>
/// <remarks>
/// You can read the generated documentation for your app at /api/content/{appName}/docs.
/// </remarks>
[HttpPost]
[Route("content/{app}/{name}/query")]
[ProducesResponseType(typeof(ContentsDto), 200)]
[ApiPermission]
[ApiCosts(1)]
public async Task<IActionResult> GetContentsPost(string app, string name, [FromBody] QueryDto query)
{
var schema = await contentQuery.GetSchemaOrThrowAsync(Context, name);
var contents = await contentQuery.QueryAsync(Context, name, query?.ToQuery() ?? Q.Empty);
var response = Deferred.AsyncResponse(async () =>
{
@ -474,5 +531,13 @@ namespace Squidex.Areas.Api.Controllers.Contents
return response;
}
private Q CreateQuery(string? ids, string? q)
{
return Q.Empty
.WithIds(ids)
.WithJsonQuery(q)
.WithODataQuery(Request.QueryString.ToString());
}
}
}

22
backend/src/Squidex/Areas/Api/Controllers/Contents/Models/ContentsIdsQueryDto.cs

@ -0,0 +1,22 @@
// ==========================================================================
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex UG (haftungsbeschraenkt)
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
namespace Squidex.Areas.Api.Controllers.Contents.Models
{
public sealed class ContentsIdsQueryDto
{
/// <summary>
/// The list of ids to query.
/// </summary>
[Required]
public List<Guid> Ids { get; set; }
}
}

61
backend/src/Squidex/Areas/Api/Controllers/QueryDto.cs

@ -0,0 +1,61 @@
// ==========================================================================
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex UG (haftungsbeschraenkt)
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
using System;
using System.Collections.Generic;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
using Squidex.Domain.Apps.Entities;
namespace Squidex.Areas.Api.Controllers
{
public sealed class QueryDto
{
/// <summary>
/// The optional list of ids to query.
/// </summary>
public List<Guid>? Ids { get; set; }
/// <summary>
/// The optional odata query.
/// </summary>
public string? OData { get; set; }
/// <summary>
/// The optional json query.
/// </summary>
[JsonProperty("q")]
public JObject? JsonQuery { get; set; }
/// <summary>
/// The parent id (for assets).
/// </summary>
public Guid? ParentId { get; set; }
public Q ToQuery()
{
var result = Q.Empty;
if (Ids != null)
{
result = result.WithIds(Ids);
}
if (OData != null)
{
result = result.WithODataQuery(OData);
}
if (JsonQuery != null)
{
result = result.WithJsonQuery(JsonQuery.ToString());
}
return result;
}
}
}

2
frontend/app/features/content/shared/forms/assets-editor.component.ts

@ -79,7 +79,7 @@ export class AssetsEditorComponent extends StatefulControlComponent<State, Reado
if (!Types.equals(obj, this.snapshot.assets.map(x => x.id))) {
const assetIds: string[] = obj;
this.assetsService.getAssets(this.appsState.appName, 0, 0, undefined, undefined, obj)
this.assetsService.getAssets(this.appsState.appName, { ids: obj })
.subscribe(dtos => {
this.setAssets(assetIds.map(id => dtos.items.find(x => x.id === id)!).filter(a => !!a));

6
frontend/app/shared/components/assets/asset-dialog.component.scss

@ -16,6 +16,12 @@
padding-bottom: 1.25rem;
}
.nav-tabs2 {
.nav-link {
color: inherit;
}
}
.invisible {
visibility: hidden;
}

5
frontend/app/shared/components/comments/comments.component.html

@ -15,10 +15,15 @@
</div>
<div class="comments-footer">
<ng-template #mentionListTemplate let-item="item">
{{item['contributorEmail']}}
</ng-template>
<form [formGroup]="commentForm.form" (ngSubmit)="comment()">
<input class="form-control" name="text" formControlName="text" placeholder="Create a comment"
[mention]="mentionUsers | async"
[mentionConfig]="mentionConfig"
[mentionListTemplate]="mentionListTemplate"
autocomplete="off"
autocorrect="off"
autocapitalize="off" />

6
frontend/app/shared/components/comments/comments.component.ts

@ -9,7 +9,7 @@ import { ChangeDetectorRef, Component, Input, OnInit } from '@angular/core';
import { FormBuilder } from '@angular/forms';
import { Router } from '@angular/router';
import { timer } from 'rxjs';
import { filter, map, onErrorResumeNext, switchMap } from 'rxjs/operators';
import { onErrorResumeNext, switchMap } from 'rxjs/operators';
import {
AppsState,
@ -36,8 +36,8 @@ export class CommentsComponent extends ResourceOwner implements OnInit {
public commentsState: CommentsState;
public commentForm = new UpsertCommentForm(this.formBuilder);
public mentionUsers = this.contributorsState.contributors.pipe(map(x => x.map(c => c.contributorEmail), filter(x => !!x)));
public mentionConfig = { dropUp: true };
public mentionUsers = this.contributorsState.contributors;
public mentionConfig = { dropUp: true, labelKey: 'contributorEmail' };
public userToken: string;

2
frontend/app/shared/components/forms/references-dropdown.component.ts

@ -113,7 +113,7 @@ export class ReferencesDropdownComponent extends StatefulControlComponent<State,
this.resetState();
if (this.isValid) {
this.contentsService.getContents(this.appsState.appName, this.schemaId, this.itemCount, 0)
this.contentsService.getContents(this.appsState.appName, this.schemaId, { take: this.itemCount })
.subscribe(contents => {
const contentItems = contents.items;
const contentNames = this.createContentNames(contentItems);

2
frontend/app/shared/components/forms/references-tags.component.ts

@ -121,7 +121,7 @@ export class ReferencesTagsComponent extends StatefulControlComponent<State, Rea
this.resetState();
if (this.isValid) {
this.contentsService.getContents(this.appsState.appName, this.schemaId, this.itemCount, 0)
this.contentsService.getContents(this.appsState.appName, this.schemaId, { take: this.itemCount })
.subscribe(contents => {
this.contentItems = contents.items;

63
frontend/app/shared/services/assets.service.spec.ts

@ -22,6 +22,7 @@ import {
MathHelper,
Resource,
ResourceLinks,
sanitize,
Version
} from '@app/shared/internal';
@ -75,7 +76,7 @@ describe('AssetsService', () => {
let assets: AssetsDto;
assetsService.getAssets('my-app', 17, 13).subscribe(result => {
assetsService.getAssets('my-app', { take: 17, skip: 13 }).subscribe(result => {
assets = result;
});
@ -153,14 +154,16 @@ describe('AssetsService', () => {
expect(asset!).toEqual(createAsset(12));
}));
it('should append query to find by name',
it('should make get request to get assets by name',
inject([AssetsService, HttpTestingController], (assetsService: AssetsService, httpMock: HttpTestingController) => {
assetsService.getAssets('my-app', 17, 13, { fullText: 'my-query' }).subscribe();
const query = { fullText: 'my-query' };
const query = { filter: { and: [{ path: 'fileName', op: 'contains', value: 'my-query' }] }, take: 17, skip: 13 };
assetsService.getAssets('my-app', { take: 17, skip: 13, query }).subscribe();
const req = httpMock.expectOne(`http://service/p/api/apps/my-app/assets?q=${encodeQuery(query)}`);
const expectedQuery = { filter: { and: [{ path: 'fileName', op: 'contains', value: 'my-query' }] }, take: 17, skip: 13 };
const req = httpMock.expectOne(`http://service/p/api/apps/my-app/assets?q=${encodeQuery(expectedQuery)}`);
expect(req.request.method).toEqual('GET');
expect(req.request.headers.get('If-Match')).toBeNull();
@ -168,14 +171,32 @@ describe('AssetsService', () => {
req.flush({ total: 10, items: [] });
}));
it('should append query to find by tag',
it('should make post request to get assets by name when request limit reached',
inject([AssetsService, HttpTestingController], (assetsService: AssetsService, httpMock: HttpTestingController) => {
assetsService.getAssets('my-app', 17, 13, undefined, ['tag1']).subscribe();
const query = { fullText: 'my-query' };
const query = { filter: { and: [{ path: 'tags', op: 'eq', value: 'tag1' }] }, take: 17, skip: 13 };
assetsService.getAssets('my-app', { take: 17, skip: 13, query, maxLength: 5 }).subscribe();
const req = httpMock.expectOne(`http://service/p/api/apps/my-app/assets?q=${encodeQuery(query)}`);
const expectedQuery = { filter: { and: [{ path: 'fileName', op: 'contains', value: 'my-query' }] }, take: 17, skip: 13 };
const req = httpMock.expectOne(`http://service/p/api/apps/my-app/assets/query`);
expect(req.request.method).toEqual('POST');
expect(req.request.headers.get('If-Match')).toBeNull();
expect(req.request.body).toEqual({ q: sanitize(expectedQuery) });
req.flush({ total: 10, items: [] });
}));
it('should make get request to get assets by tag',
inject([AssetsService, HttpTestingController], (assetsService: AssetsService, httpMock: HttpTestingController) => {
assetsService.getAssets('my-app', { take: 17, skip: 13, tags: ['tag1'] }).subscribe();
const expectedQuery = { filter: { and: [{ path: 'tags', op: 'eq', value: 'tag1' }] }, take: 17, skip: 13 };
const req = httpMock.expectOne(`http://service/p/api/apps/my-app/assets?q=${encodeQuery(expectedQuery)}`);
expect(req.request.method).toEqual('GET');
expect(req.request.headers.get('If-Match')).toBeNull();
@ -183,12 +204,30 @@ describe('AssetsService', () => {
req.flush({ total: 10, items: [] });
}));
it('should append ids query to find by ids',
it('should make get request to get assets by tag when request limit reached',
inject([AssetsService, HttpTestingController], (assetsService: AssetsService, httpMock: HttpTestingController) => {
assetsService.getAssets('my-app', 0, 0, undefined, undefined, ['12', '23']).subscribe();
assetsService.getAssets('my-app', { take: 17, skip: 13, tags: ['tag1'], maxLength: 5 }).subscribe();
const expectedQuery = { filter: { and: [{ path: 'tags', op: 'eq', value: 'tag1' }] }, take: 17, skip: 13 };
const req = httpMock.expectOne(`http://service/p/api/apps/my-app/assets/query`);
expect(req.request.method).toEqual('POST');
expect(req.request.headers.get('If-Match')).toBeNull();
expect(req.request.body).toEqual({ q: sanitize(expectedQuery) });
req.flush({ total: 10, items: [] });
}));
it('should make get request to get assets by ids',
inject([AssetsService, HttpTestingController], (assetsService: AssetsService, httpMock: HttpTestingController) => {
const ids = ['1', '2'];
assetsService.getAssets('my-app', { ids }).subscribe();
const req = httpMock.expectOne('http://service/p/api/apps/my-app/assets?ids=12,23');
const req = httpMock.expectOne('http://service/p/api/apps/my-app/assets?ids=1,2');
expect(req.request.method).toEqual('GET');
expect(req.request.headers.get('If-Match')).toBeNull();

48
frontend/app/shared/services/assets.service.ts

@ -149,6 +149,16 @@ export interface MoveAssetItemDto {
readonly parentId?: string;
}
export interface AssetQueryDto {
readonly ids?: ReadonlyArray<string>;
readonly maxLength?: number;
readonly parentId?: string;
readonly query?: Query;
readonly skip?: number;
readonly tags?: ReadonlyArray<string>;
readonly take?: number;
}
@Injectable()
export class AssetsService {
constructor(
@ -164,13 +174,17 @@ export class AssetsService {
return this.http.get<{ [name: string]: number }>(url);
}
public getAssets(appName: string, take: number, skip: number, query?: Query, tags?: ReadonlyArray<string>, ids?: ReadonlyArray<string>, parentId?: string): Observable<AssetsDto> {
public getAssets(appName: string, q?: AssetQueryDto): Observable<AssetsDto> {
const { ids, maxLength, parentId, query, skip, tags, take } = q || {};
let fullQuery = '';
if (ids) {
let queryObj: Query | undefined = undefined;
if (ids && ids.length > 0) {
fullQuery = `ids=${ids.join(',')}`;
} else {
const queryObj: Query = {};
queryObj = {};
const filters: any[] = [];
@ -190,11 +204,11 @@ export class AssetsService {
queryObj.filter = { and: filters };
}
if (take > 0) {
if (take && take > 0) {
queryObj.take = take;
}
if (skip > 0) {
if (skip && skip > 0) {
queryObj.skip = skip;
}
@ -205,6 +219,29 @@ export class AssetsService {
}
}
if (fullQuery.length > (maxLength || 2000)) {
const body: any = {};
if (ids && ids.length > 0) {
body.ids = ids;
} else if (queryObj) {
body.q = queryObj;
}
if (parentId) {
body.parentId = parentId;
}
const url = this.apiUrl.buildUrl(`api/apps/${appName}/assets/query`);
return this.http.post<{ total: number, items: any[], folders: any[] } & Resource>(url, body).pipe(
map(({ total, items, _links }) => {
const assets = items.map(item => parseAsset(item));
return new AssetsDto(total, assets, _links);
}),
pretifyError('Failed to load assets. Please reload.'));
} else {
const url = this.apiUrl.buildUrl(`api/apps/${appName}/assets?${fullQuery}`);
return this.http.get<{ total: number, items: any[], folders: any[] } & Resource>(url).pipe(
@ -215,6 +252,7 @@ export class AssetsService {
}),
pretifyError('Failed to load assets. Please reload.'));
}
}
public getAssetFolders(appName: string, parentId?: string): Observable<AssetFoldersDto> {
const url = this.apiUrl.buildUrl(`api/apps/${appName}/assets/folders?parentId=${parentId}`);

118
frontend/app/shared/services/contents.service.spec.ts

@ -21,7 +21,8 @@ import {
Version,
Versioned
} from '@app/shared/internal';
import { encodeQuery } from './../state/query';
import { encodeQuery, sanitize } from './../state/query';
describe('ContentsService', () => {
const version = new Version('1');
@ -48,7 +49,7 @@ describe('ContentsService', () => {
let contents: ContentsDto;
contentsService.getContents('my-app', 'my-schema', 17, 13).subscribe(result => {
contentsService.getContents('my-app', 'my-schema', { take: 17, skip: 13 }).subscribe(result => {
contents = result;
});
@ -75,12 +76,16 @@ describe('ContentsService', () => {
]));
}));
it('should append query to get request as search',
it('should make get request to get contents with json query',
inject([ContentsService, HttpTestingController], (contentsService: ContentsService, httpMock: HttpTestingController) => {
contentsService.getContents('my-app', 'my-schema', 17, 13, { fullText: 'my-query' }).subscribe();
const query = { fullText: 'my-query' };
contentsService.getContents('my-app', 'my-schema', { take: 17, skip: 13, query }).subscribe();
const expectedQuery = { ...query, take: 17, skip: 13 };
const req = httpMock.expectOne(`http://service/p/api/content/my-app/my-schema?q=${encodeQuery({ fullText: 'my-query', take: 17, skip: 13 })}`);
const req = httpMock.expectOne(`http://service/p/api/content/my-app/my-schema?q=${encodeQuery(expectedQuery)}`);
expect(req.request.method).toEqual('GET');
expect(req.request.headers.get('If-Match')).toBeNull();
@ -88,12 +93,32 @@ describe('ContentsService', () => {
req.flush({ total: 10, items: [] });
}));
it('should append ids to get request with ids',
it('should make post request to get contents with json query when request limit reached',
inject([ContentsService, HttpTestingController], (contentsService: ContentsService, httpMock: HttpTestingController) => {
contentsService.getContents('my-app', 'my-schema', 17, 13, undefined, ['id1', 'id2']).subscribe();
const query = { fullText: 'my-query' };
contentsService.getContents('my-app', 'my-schema', { take: 17, skip: 13, query, maxLength: 5 }).subscribe();
const expectedQuery = { ...query, take: 17, skip: 13 };
const req = httpMock.expectOne('http://service/p/api/content/my-app/my-schema?ids=id1,id2');
const req = httpMock.expectOne(`http://service/p/api/content/my-app/my-schema/query`);
expect(req.request.method).toEqual('POST');
expect(req.request.headers.get('If-Match')).toBeNull();
expect(req.request.body).toEqual({ q: sanitize(expectedQuery) });
req.flush({ total: 10, items: [] });
}));
it('should make get request to get contents with ids',
inject([ContentsService, HttpTestingController], (contentsService: ContentsService, httpMock: HttpTestingController) => {
const ids = ['1', '2'];
contentsService.getContents('my-app', 'my-schema', { take: 17, skip: 13, ids }).subscribe();
const req = httpMock.expectOne('http://service/p/api/content/my-app/my-schema?ids=1,2');
expect(req.request.method).toEqual('GET');
expect(req.request.headers.get('If-Match')).toBeNull();
@ -101,10 +126,28 @@ describe('ContentsService', () => {
req.flush({ total: 10, items: [] });
}));
it('should append odata query to get request as plain query string',
it('should make post request to get contents with ids when request limit reached',
inject([ContentsService, HttpTestingController], (contentsService: ContentsService, httpMock: HttpTestingController) => {
const ids = ['1', '2'];
contentsService.getContents('my-app', 'my-schema', { take: 17, skip: 13, ids, maxLength: 5 }).subscribe();
const req = httpMock.expectOne('http://service/p/api/content/my-app/my-schema/query');
expect(req.request.method).toEqual('POST');
expect(req.request.headers.get('If-Match')).toBeNull();
expect(req.request.body).toEqual({ ids });
req.flush({ total: 10, items: [] });
}));
it('should make get request to get contents with odata filter',
inject([ContentsService, HttpTestingController], (contentsService: ContentsService, httpMock: HttpTestingController) => {
contentsService.getContents('my-app', 'my-schema', 17, 13, { fullText: '$filter=my-filter' }).subscribe();
const query = { fullText: '$filter=my-filter' };
contentsService.getContents('my-app', 'my-schema', { take: 17, skip: 13, query }).subscribe();
const req = httpMock.expectOne('http://service/p/api/content/my-app/my-schema?$filter=my-filter&$top=17&$skip=13');
@ -114,36 +157,51 @@ describe('ContentsService', () => {
req.flush({ total: 10, items: [] });
}));
it('should make post request to get contents with odata filter when request limit reached',
inject([ContentsService, HttpTestingController], (contentsService: ContentsService, httpMock: HttpTestingController) => {
const query = { fullText: '$filter=my-filter' };
contentsService.getContents('my-app', 'my-schema', { take: 17, skip: 13, query, maxLength: 5 }).subscribe();
const req = httpMock.expectOne('http://service/p/api/content/my-app/my-schema/query');
expect(req.request.method).toEqual('POST');
expect(req.request.headers.get('If-Match')).toBeNull();
expect(req.request.body).toEqual({ odataQuery: '$filter=my-filter&$top=17&$skip=13' });
req.flush({ total: 10, items: [] });
}));
it('should make get request to get contents by ids',
inject([ContentsService, HttpTestingController], (contentsService: ContentsService, httpMock: HttpTestingController) => {
let contents: ContentsDto;
const ids = ['1', '2', '3'];
contentsService.getContentsByIds('my-app', ['1', '2', '3']).subscribe(result => {
contents = result;
});
contentsService.getContentsByIds('my-app', ids).subscribe();
const req = httpMock.expectOne(`http://service/p/api/content/my-app/?ids=1,2,3`);
const req = httpMock.expectOne(`http://service/p/api/content/my-app?ids=1,2,3`);
expect(req.request.method).toEqual('GET');
expect(req.request.headers.get('If-Match')).toBeNull();
req.flush({
total: 10,
items: [
contentResponse(12),
contentResponse(13)
],
statuses: [{
status: 'Draft', color: 'Gray'
}]
});
req.flush({ total: 10, items: [] });
}));
expect(contents!).toEqual(
new ContentsDto([{ status: 'Draft', color: 'Gray' }], 10, [
createContent(12),
createContent(13)
]));
it('should make post request to get contents by ids when request limit reached',
inject([ContentsService, HttpTestingController], (contentsService: ContentsService, httpMock: HttpTestingController) => {
const ids = ['1', '2', '3'];
contentsService.getContentsByIds('my-app', ids, 5).subscribe();
const req = httpMock.expectOne(`http://service/p/api/content/my-app`);
expect(req.request.method).toEqual('POST');
expect(req.request.headers.get('If-Match')).toBeNull();
expect(req.request.body).toEqual({ ids });
req.flush({ total: 10, items: [] });
}));
it('should make get request to get content',

81
frontend/app/shared/services/contents.service.ts

@ -104,6 +104,14 @@ export class ContentDto {
}
}
export interface ContentQueryDto {
readonly ids?: ReadonlyArray<string>;
readonly maxLength?: number;
readonly query?: Query;
readonly skip?: number;
readonly take?: number;
}
@Injectable()
export class ContentsService {
constructor(
@ -113,30 +121,36 @@ export class ContentsService {
) {
}
public getContents(appName: string, schemaName: string, take: number, skip: number, query?: Query, ids?: ReadonlyArray<string>): Observable<ContentsDto> {
public getContents(appName: string, schemaName: string, q?: ContentQueryDto): Observable<ContentsDto> {
const { ids, maxLength, query, skip, take } = q || {};
const queryParts: string[] = [];
const queryOdataParts: string[] = [];
let queryObj: Query | undefined;
if (ids && ids.length > 0) {
queryParts.push(`ids=${ids.join(',')}`);
} else {
const queryObj: Query = { ...query };
if (queryObj.fullText && queryObj.fullText.indexOf('$') >= 0) {
queryParts.push(`${queryObj.fullText.trim()}`);
if (query && query.fullText && query.fullText.indexOf('$') >= 0) {
queryOdataParts.push(`${query.fullText.trim()}`);
if (take > 0) {
queryParts.push(`$top=${take}`);
if (take && take > 0) {
queryOdataParts.push(`$top=${take}`);
}
if (skip > 0) {
queryParts.push(`$skip=${skip}`);
if (skip && skip > 0) {
queryOdataParts.push(`$skip=${skip}`);
}
} else {
if (take > 0) {
queryObj = { ...query };
if (take && take > 0) {
queryObj.take = take;
}
if (skip > 0) {
if (skip && skip > 0) {
queryObj.skip = skip;
}
@ -144,8 +158,31 @@ export class ContentsService {
}
}
const fullQuery = queryParts.join('&');
let fullQuery = [...queryParts, ...queryOdataParts].join('&');
if (fullQuery.length > (maxLength || 2000)) {
const body: any = {};
if (ids && ids.length > 0) {
body.ids = ids;
} else {
if (queryOdataParts.length > 0) {
body.odataQuery = queryOdataParts.join('&');
} else if (queryObj) {
body.q = queryObj;
}
}
const url = this.apiUrl.buildUrl(`/api/content/${appName}/${schemaName}/query`);
return this.http.post<{ total: number, items: [], statuses: StatusInfo[] } & Resource>(url, body).pipe(
map(({ total, items, statuses, _links }) => {
const contents = items.map(x => parseContent(x));
return new ContentsDto(statuses, total, contents, _links);
}),
pretifyError('Failed to load contents. Please reload.'));
} else {
const url = this.apiUrl.buildUrl(`/api/content/${appName}/${schemaName}?${fullQuery}`);
return this.http.get<{ total: number, items: [], statuses: StatusInfo[] } & Resource>(url).pipe(
@ -156,9 +193,26 @@ export class ContentsService {
}),
pretifyError('Failed to load contents. Please reload.'));
}
}
public getContentsByIds(appName: string, ids: ReadonlyArray<string>, maxLength?: number): Observable<ContentsDto> {
const fullQuery = `ids=${ids.join(',')}`;
if (fullQuery.length > (maxLength || 2000)) {
const body = { ids };
const url = this.apiUrl.buildUrl(`/api/content/${appName}`);
return this.http.post<{ total: number, items: [], statuses: StatusInfo[] } & Resource>(url, body).pipe(
map(({ total, items, statuses, _links }) => {
const contents = items.map(x => parseContent(x));
return new ContentsDto(statuses, total, contents, _links);
}),
pretifyError('Failed to load contents. Please reload.'));
public getContentsByIds(appName: string, ids: ReadonlyArray<string>): Observable<ContentsDto> {
const url = this.apiUrl.buildUrl(`/api/content/${appName}/?ids=${ids.join(',')}`);
} else {
const url = this.apiUrl.buildUrl(`/api/content/${appName}?${fullQuery}`);
return this.http.get<{ total: number, items: [], statuses: StatusInfo[] } & Resource>(url).pipe(
map(({ total, items, statuses, _links }) => {
@ -168,6 +222,7 @@ export class ContentsService {
}),
pretifyError('Failed to load contents. Please reload.'));
}
}
public getContent(appName: string, schemaName: string, id: string): Observable<ContentDto> {
const url = this.apiUrl.buildUrl(`/api/content/${appName}/${schemaName}/${id}`);

25
frontend/app/shared/state/assets.state.spec.ts

@ -71,7 +71,7 @@ describe('AssetsState', () => {
});
it('should load assets', () => {
assetsService.setup(x => x.getAssets(app, 30, 0, undefined, It.isValue([]), undefined, MathHelper.EMPTY_GUID))
assetsService.setup(x => x.getAssets(app, { take: 30, skip: 0, parentId: MathHelper.EMPTY_GUID }))
.returns(() => of(new AssetsDto(200, [asset1, asset2]))).verifiable();
assetsState.load().subscribe();
@ -85,7 +85,7 @@ describe('AssetsState', () => {
});
it('should show notification on load when reload is true', () => {
assetsService.setup(x => x.getAssets(app, 30, 0, undefined, It.isValue([]), undefined, MathHelper.EMPTY_GUID))
assetsService.setup(x => x.getAssets(app, { take: 30, skip: 0, parentId: MathHelper.EMPTY_GUID }))
.returns(() => of(new AssetsDto(200, [asset1, asset2]))).verifiable();
assetsState.load(true).subscribe();
@ -96,10 +96,10 @@ describe('AssetsState', () => {
});
it('should load without tags when tag untoggled', () => {
assetsService.setup(x => x.getAssets(app, 30, 0, undefined, It.isValue(['tag1']), undefined, undefined))
assetsService.setup(x => x.getAssets(app, { take: 30, skip: 0, tags: ['tag1'] }))
.returns(() => of(new AssetsDto(0, []))).verifiable();
assetsService.setup(x => x.getAssets(app, 30, 0, undefined, It.isValue([]), undefined, MathHelper.EMPTY_GUID))
assetsService.setup(x => x.getAssets(app, { take: 30, skip: 0, parentId: MathHelper.EMPTY_GUID }))
.returns(() => of(new AssetsDto(0, []))).verifiable();
assetsState.toggleTag('tag1').subscribe();
@ -109,7 +109,7 @@ describe('AssetsState', () => {
});
it('should load without tags when tags reset', () => {
assetsService.setup(x => x.getAssets(app, 30, 0, undefined, It.isValue([]), undefined, MathHelper.EMPTY_GUID))
assetsService.setup(x => x.getAssets(app, { take: 30, skip: 0, parentId: MathHelper.EMPTY_GUID }))
.returns(() => of(new AssetsDto(0, []))).verifiable();
assetsState.resetTags().subscribe();
@ -118,7 +118,7 @@ describe('AssetsState', () => {
});
it('should load with new pagination when paging', () => {
assetsService.setup(x => x.getAssets(app, 30, 30, undefined, It.isValue([]), undefined, MathHelper.EMPTY_GUID))
assetsService.setup(x => x.getAssets(app, { take: 30, skip: 30, parentId: MathHelper.EMPTY_GUID }))
.returns(() => of(new AssetsDto(200, []))).verifiable();
assetsState.setPager(new Pager(200, 1, 30)).subscribe();
@ -127,7 +127,7 @@ describe('AssetsState', () => {
});
it('should update page size in local store', () => {
assetsService.setup(x => x.getAssets(app, 50, 0, undefined, It.isValue([]), undefined, MathHelper.EMPTY_GUID))
assetsService.setup(x => x.getAssets(app, { take: 50, skip: 0, parentId: MathHelper.EMPTY_GUID }))
.returns(() => of(new AssetsDto(200, []))).verifiable();
assetsState.setPager(new Pager(0, 0, 50));
@ -140,7 +140,7 @@ describe('AssetsState', () => {
describe('Navigating', () => {
beforeEach(() => {
assetsService.setup(x => x.getAssets(app, 30, 0, undefined, It.isAny(), undefined, It.isAny()))
assetsService.setup(x => x.getAssets(app, It.isAny()))
.returns(() => of(new AssetsDto(0, [])));
assetsService.setup(x => x.getAssetFolders(app, It.isAny()))
@ -183,7 +183,7 @@ describe('AssetsState', () => {
describe('Searching', () => {
it('should load with tags when tag toggled', () => {
assetsService.setup(x => x.getAssets(app, 30, 0, undefined, It.isValue(['tag1']), undefined, undefined))
assetsService.setup(x => x.getAssets(app, { take: 30, skip: 0, tags: ['tag1'] }))
.returns(() => of(new AssetsDto(0, []))).verifiable();
assetsState.toggleTag('tag1').subscribe();
@ -192,7 +192,7 @@ describe('AssetsState', () => {
});
it('should load with tags when tags selected', () => {
assetsService.setup(x => x.getAssets(app, 30, 0, undefined, It.isValue(['tag1', 'tag2']), undefined, undefined))
assetsService.setup(x => x.getAssets(app, { take: 30, skip: 0, tags: ['tag1', 'tag2'] }))
.returns(() => of(new AssetsDto(0, []))).verifiable();
assetsState.selectTags(['tag1', 'tag2']).subscribe();
@ -203,7 +203,7 @@ describe('AssetsState', () => {
it('should load with query when searching', () => {
const query = { fullText: 'my-query' };
assetsService.setup(x => x.getAssets(app, 30, 0, query, It.isValue([]), undefined, undefined))
assetsService.setup(x => x.getAssets(app, { take: 30, skip: 0, query }))
.returns(() => of(new AssetsDto(0, []))).verifiable();
assetsState.search(query).subscribe();
@ -216,7 +216,8 @@ describe('AssetsState', () => {
beforeEach(() => {
assetsService.setup(x => x.getAssetFolders(app, MathHelper.EMPTY_GUID))
.returns(() => of(new AssetFoldersDto(2, [assetFolder1, assetFolder2])));
assetsService.setup(x => x.getAssets(app, 30, 0, undefined, It.isValue([]), undefined, MathHelper.EMPTY_GUID))
assetsService.setup(x => x.getAssets(app, { take: 30, skip: 0, parentId: MathHelper.EMPTY_GUID }))
.returns(() => of(new AssetsDto(200, [asset1, asset2]))).verifiable();
assetsState.load(true).subscribe();

23
frontend/app/shared/state/assets.state.ts

@ -162,14 +162,27 @@ export class AssetsState extends State<Snapshot> {
private loadInternal(isReload: boolean): Observable<any> {
this.next({ isLoading: true });
const query: any = {
take: this.snapshot.assetsPager.pageSize,
skip: this.snapshot.assetsPager.skip
};
if (this.parentId) {
query.parentId = this.parentId;
}
if (this.snapshot.assetsQuery) {
query.query = this.snapshot.assetsQuery;
}
const searchTags = Object.keys(this.snapshot.tagsSelected);
if (searchTags.length > 0) {
query.tags = searchTags;
}
const assets$ =
this.assetsService.getAssets(this.appName,
this.snapshot.assetsPager.pageSize,
this.snapshot.assetsPager.skip,
this.snapshot.assetsQuery,
searchTags, undefined, this.parentId);
this.assetsService.getAssets(this.appName, query);
const assetFolders$ =
this.snapshot.path.length === 0 ?

14
frontend/app/shared/state/contents.state.ts

@ -164,10 +164,16 @@ export abstract class ContentsStateBase extends State<Snapshot> {
this.previousId = this.schemaId;
return this.contentsService.getContents(this.appName, this.schemaId,
this.snapshot.contentsPager.pageSize,
this.snapshot.contentsPager.skip,
this.snapshot.contentsQuery, undefined).pipe(
const query: any = {
take: this.snapshot.contentsPager.pageSize,
skip: this.snapshot.contentsPager.skip
};
if (this.snapshot.contentsQuery) {
query.query = this.snapshot.contentsQuery;
}
return this.contentsService.getContents(this.appName, this.schemaId, query).pipe(
tap(({ total, items: contents, canCreate, canCreateAndPublish, statuses }) => {
if (isReload) {
this.dialogs.notifyInfo('Contents reloaded.');

6
frontend/app/shared/state/query.ts

@ -113,7 +113,7 @@ const DEFAULT_QUERY = {
sort: []
};
function santize(query?: Query) {
export function sanitize(query?: Query) {
if (!query) {
return DEFAULT_QUERY;
}
@ -130,11 +130,11 @@ function santize(query?: Query) {
}
export function equalsQuery(lhs?: Query, rhs?: Query) {
return Types.equals(santize(lhs), santize(rhs));
return Types.equals(sanitize(lhs), sanitize(rhs));
}
export function encodeQuery(query?: Query) {
return encodeURIComponent(JSON.stringify(santize(query)));
return encodeURIComponent(JSON.stringify(sanitize(query)));
}
export function decodeQuery(raw?: string): Query | undefined {

78
frontend/package-lock.json

@ -148,33 +148,33 @@
}
},
"@angular/animations": {
"version": "9.0.2",
"resolved": "https://registry.npmjs.org/@angular/animations/-/animations-9.0.2.tgz",
"integrity": "sha512-vj4N8nSLytQI45TtGy2tJb0Yc7uqlyap+qhghc+jdyG41w18KQUnIneEWKOfHWnp8VJEfzgzaY7zr/1QPlxWgA=="
"version": "9.0.5",
"resolved": "https://registry.npmjs.org/@angular/animations/-/animations-9.0.5.tgz",
"integrity": "sha512-WGs4Jxw5sr8GCpxMcwEVuZnDIkdNp9qtmuI2j13v/XAaMjvJ7jssCj9+JG5uI8joCi7PFVAWokPT1DdPwWb13Q=="
},
"@angular/cdk": {
"version": "9.0.1",
"resolved": "https://registry.npmjs.org/@angular/cdk/-/cdk-9.0.1.tgz",
"integrity": "sha512-slhYG9lOX7JoxcULdfIvXspkDRjYTBG8PH6B2Slxi8CpV42x0t+yQnBQxp/U3ud1m1BWVrlxwKZywaPFe1tSeA==",
"version": "9.1.1",
"resolved": "https://registry.npmjs.org/@angular/cdk/-/cdk-9.1.1.tgz",
"integrity": "sha512-yzssAqbllGYgX+WeSYLjmEWtXVG5UPZwA0+dPlh+g85nG7b70DVRVYBi8PJySydsfPX/JMherFUU9h0QOWhhZw==",
"requires": {
"parse5": "^5.0.0"
}
},
"@angular/common": {
"version": "9.0.2",
"resolved": "https://registry.npmjs.org/@angular/common/-/common-9.0.2.tgz",
"integrity": "sha512-KYOov8fg5WX/bAMkemlcAZxqiq/6ga1BoxjaiZXBj07KDq8i5Nwcm6RmNkeDByCuXd2UHVm1w5t897wEUi6fnw=="
"version": "9.0.5",
"resolved": "https://registry.npmjs.org/@angular/common/-/common-9.0.5.tgz",
"integrity": "sha512-AwZKYK5M/3762woK+3290JnBdlBvZXqxX5vVze6wk23IiBlwIV+l79+Lyfjo/4s031kibq47taaZdC7qkkBkNA=="
},
"@angular/compiler": {
"version": "9.0.2",
"resolved": "https://registry.npmjs.org/@angular/compiler/-/compiler-9.0.2.tgz",
"integrity": "sha512-IWlKn5v3y7k1Z2K2wfNNzbn9xgA4fFlWyPe9QpdS8iy6/bPe5GfKhHbMYIza5eIhPXDN93zdEptiAIXGP3kLBQ==",
"version": "9.0.5",
"resolved": "https://registry.npmjs.org/@angular/compiler/-/compiler-9.0.5.tgz",
"integrity": "sha512-TeyhRGefTOtA9N3udMrvheafoXcz/dvTTdZLcieeZQxm1SSeaQDUQ/rUH6QTOiHVNMtjOCrZ9J5rk1A4mPYuag==",
"dev": true
},
"@angular/compiler-cli": {
"version": "9.0.2",
"resolved": "https://registry.npmjs.org/@angular/compiler-cli/-/compiler-cli-9.0.2.tgz",
"integrity": "sha512-2AAZr1jX72OG9k1viVShiDGAwV9PZEcoDt80PXkUjLTUwWxicuSBKYauph47PzK/qzqWhdbvMbh8wCWHSLq0Qg==",
"version": "9.0.5",
"resolved": "https://registry.npmjs.org/@angular/compiler-cli/-/compiler-cli-9.0.5.tgz",
"integrity": "sha512-lFlasm8UBApTq4/MkxnYrRAMfpOvvg3YYBEMibuEGlaJjW/Xd1JcisUuFiooCxCIKF5phyORHmxjywGPhHqQgQ==",
"dev": true,
"requires": {
"canonical-path": "1.0.0",
@ -187,6 +187,7 @@
"reflect-metadata": "^0.1.2",
"semver": "^6.3.0",
"source-map": "^0.6.1",
"sourcemap-codec": "^1.4.8",
"yargs": "13.1.0"
},
"dependencies": {
@ -199,38 +200,38 @@
}
},
"@angular/core": {
"version": "9.0.2",
"resolved": "https://registry.npmjs.org/@angular/core/-/core-9.0.2.tgz",
"integrity": "sha512-ccVPR6RZo2s9O9phO0TJ60QZ0WA7qfUMzo0xnpBW0XGcbTzLEn9upvs+0PX64f9UpnHz/MQo0wsqYvTLuoz7Yw=="
"version": "9.0.5",
"resolved": "https://registry.npmjs.org/@angular/core/-/core-9.0.5.tgz",
"integrity": "sha512-7VznrYjaAIzeq/zQ7v6tbeoOI7JJKgChKwG7s8jRoEpENu+w2pRlRdyQul88iJLsXgObR+/TfBNm/K+G4cqAFw=="
},
"@angular/forms": {
"version": "9.0.2",
"resolved": "https://registry.npmjs.org/@angular/forms/-/forms-9.0.2.tgz",
"integrity": "sha512-qXEth7yeCd+5i6QyvllXnh/Rkzh16raFX5nfI7mgKHjWMik15Ua8wkVhDX9b5gizWeEyZtZcMGsTbbOQy0Ft3Q=="
"version": "9.0.5",
"resolved": "https://registry.npmjs.org/@angular/forms/-/forms-9.0.5.tgz",
"integrity": "sha512-579PXAfT92J4mghjWKiZ3Zj3xee4h3RP70YHSlsfbi94MONvryWDrnXxvUZ0zJJCVnEJQ7x+nGEp3wwWqR12Jw=="
},
"@angular/platform-browser": {
"version": "9.0.2",
"resolved": "https://registry.npmjs.org/@angular/platform-browser/-/platform-browser-9.0.2.tgz",
"integrity": "sha512-RMivdtJtspYLH/96AzLwLj3v0O9ck0sL6R1uh5JacfBkmedqJzmLn+AOxTdjaGdIpFtw9tisT+0Aw/nkG14vlA=="
"version": "9.0.5",
"resolved": "https://registry.npmjs.org/@angular/platform-browser/-/platform-browser-9.0.5.tgz",
"integrity": "sha512-24QGcQXthYXB/wT8okJjxqss/JOk4A6O1/Fmva79k0AvwtYkl2tikcyEc5T3xZtjoi8g32AN9nbZAobtkxlqTA=="
},
"@angular/platform-browser-dynamic": {
"version": "9.0.2",
"resolved": "https://registry.npmjs.org/@angular/platform-browser-dynamic/-/platform-browser-dynamic-9.0.2.tgz",
"integrity": "sha512-RJa+Y83hIFcf7pFcbbaCi7M5Y9nUUcQVuazWbQtiUe+BY5pikyug4RsF2B10pctcxb6LFLElfmkvatmmwEQ9aQ=="
"version": "9.0.5",
"resolved": "https://registry.npmjs.org/@angular/platform-browser-dynamic/-/platform-browser-dynamic-9.0.5.tgz",
"integrity": "sha512-NRfsAwbgxOvEcpqlERDAG0wap5xJa0wKwnudTCnyvf4B0D6kLkT1Idjqv22NDW5rfM2oDWaZ/qpgpDnAo6/ZBQ=="
},
"@angular/platform-server": {
"version": "9.0.2",
"resolved": "https://registry.npmjs.org/@angular/platform-server/-/platform-server-9.0.2.tgz",
"integrity": "sha512-GXpEKMACf648vEGt5n/5XXHWd5Tf5D/x4sjbPCasgZ4uIrtSy3SDrJrQl8rxvFQkv804eBnC5CH9ckEFj8g9QQ==",
"version": "9.0.5",
"resolved": "https://registry.npmjs.org/@angular/platform-server/-/platform-server-9.0.5.tgz",
"integrity": "sha512-5iEugPj0oZgw6JHS5s8m4WejCnEoNeWgttzsCQuyCaVmIOQGCbTdqSsxD+AgBO7A5lrzxYQOgil/XCM/up5Smw==",
"requires": {
"domino": "^2.1.2",
"xhr2": "^0.1.4"
}
},
"@angular/router": {
"version": "9.0.2",
"resolved": "https://registry.npmjs.org/@angular/router/-/router-9.0.2.tgz",
"integrity": "sha512-jDKq9K0pOgaMtocg7VCfIQX8jTyBSb+0hjOcj6kXQVCcmnxeBzGPRX2THQyFkyzG8owTBZlHIrJA3H2thDU2LQ=="
"version": "9.0.5",
"resolved": "https://registry.npmjs.org/@angular/router/-/router-9.0.5.tgz",
"integrity": "sha512-Sz3DQUxlzAk9aZ9eVtZRh6xF5SMg/Gb3rc5I7dL1M+mycSNoFJ4HPTXleZkKM69mMkKQ5fEtza4x26MSlF+O9w=="
},
"@babel/code-frame": {
"version": "7.8.3",
@ -494,6 +495,7 @@
"version": "3.3.33",
"resolved": "https://registry.npmjs.org/@types/jquery/-/jquery-3.3.33.tgz",
"integrity": "sha512-U6IdXYGkfUI42SR79vB2Spj+h1Ly3J3UZjpd8mi943lh126TK7CB+HZOxGh2nM3IySor7wqVQdemD/xtydsBKA==",
"dev": true,
"requires": {
"@types/sizzle": "*"
}
@ -568,12 +570,14 @@
"@types/sizzle": {
"version": "2.3.2",
"resolved": "https://registry.npmjs.org/@types/sizzle/-/sizzle-2.3.2.tgz",
"integrity": "sha512-7EJYyKTL7tFR8+gDbB6Wwz/arpGa0Mywk1TJbNzKzHtzbwVmY4HR9WqS5VV7dsBUKQmPNr192jHr/VpBluj/hg=="
"integrity": "sha512-7EJYyKTL7tFR8+gDbB6Wwz/arpGa0Mywk1TJbNzKzHtzbwVmY4HR9WqS5VV7dsBUKQmPNr192jHr/VpBluj/hg==",
"dev": true
},
"@types/tinymce": {
"version": "4.5.24",
"resolved": "https://registry.npmjs.org/@types/tinymce/-/tinymce-4.5.24.tgz",
"integrity": "sha512-g2aFp+/GHTD6P2ZI2tBwZGIDi+64oocoDleCHtAWLYlFOR9fjPO+pdvmGjpJG3C7XeMZvugFx2NhRBL+Jb/wbQ==",
"dev": true,
"requires": {
"@types/jquery": "*"
}
@ -7160,9 +7164,9 @@
}
},
"magic-string": {
"version": "0.25.6",
"resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.25.6.tgz",
"integrity": "sha512-3a5LOMSGoCTH5rbqobC2HuDNRtE2glHZ8J7pK+QZYppyWA36yuNpsX994rIY2nCuyP7CZYy7lQq/X2jygiZ89g==",
"version": "0.25.7",
"resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.25.7.tgz",
"integrity": "sha512-4CrMT5DOHTDk4HYDlzmwu4FVCcIYI8gauveasrdCu2IKIFOJ3f0v/8MDGJCDL9oD2ppz/Av1b0Nj345H9M+XIA==",
"dev": true,
"requires": {
"sourcemap-codec": "^1.4.4"

22
frontend/package.json

@ -16,15 +16,15 @@
"postinstall": "ngcc --properties es2015 browser module main --first-only --create-ivy-entry-points"
},
"dependencies": {
"@angular/animations": "9.0.2",
"@angular/cdk": "9.0.1",
"@angular/common": "9.0.2",
"@angular/core": "9.0.2",
"@angular/forms": "9.0.2",
"@angular/platform-browser": "9.0.2",
"@angular/platform-browser-dynamic": "9.0.2",
"@angular/platform-server": "9.0.2",
"@angular/router": "9.0.2",
"@angular/animations": "9.0.5",
"@angular/cdk": "9.1.1",
"@angular/common": "9.0.5",
"@angular/core": "9.0.5",
"@angular/forms": "9.0.5",
"@angular/platform-browser": "9.0.5",
"@angular/platform-browser-dynamic": "9.0.5",
"@angular/platform-server": "9.0.5",
"@angular/router": "9.0.5",
"angular-mentions": "^1.1.3",
"angular2-chartjs": "0.5.1",
"babel-polyfill": "6.26.0",
@ -51,8 +51,8 @@
},
"devDependencies": {
"@angular-devkit/build-optimizer": "^0.900.3",
"@angular/compiler": "9.0.2",
"@angular/compiler-cli": "9.0.2",
"@angular/compiler": "9.0.5",
"@angular/compiler-cli": "9.0.5",
"@ngtools/webpack": "9.0.3",
"@types/core-js": "2.5.3",
"@types/jasmine": "3.5.5",

Loading…
Cancel
Save