Browse Source

References view (#963)

* First implementation.

* Tests simplified.

* API test and UI fixes

* References view.

* Improve tests.

* Formatting.

* Fix formatting.
pull/965/head
Sebastian Stehle 3 years ago
committed by GitHub
parent
commit
d1c62606fd
No known key found for this signature in database GPG Key ID: 4AEE18F83AFDEB23
  1. 2
      backend/i18n/frontend_en.json
  2. 2
      backend/i18n/frontend_it.json
  3. 2
      backend/i18n/frontend_nl.json
  4. 2
      backend/i18n/frontend_pt.json
  5. 2
      backend/i18n/frontend_zh.json
  6. 2
      backend/i18n/source/frontend_en.json
  7. 29
      backend/src/Squidex.Domain.Apps.Entities/Schemas/SchemasSearchSource.cs
  8. 4
      backend/src/Squidex/Areas/Api/Controllers/Contents/ContentsSharedController.cs
  9. 47
      backend/src/Squidex/Areas/Api/Controllers/Contents/Models/AllContentsByGetDto.cs
  10. 50
      backend/src/Squidex/Areas/Api/Controllers/Contents/Models/AllContentsByPostDto.cs
  11. 7
      backend/src/Squidex/Areas/Api/Controllers/QueryDto.cs
  12. 6
      backend/src/Squidex/appsettings.json
  13. 3
      backend/tests/Squidex.Domain.Apps.Entities.Tests/Schemas/SchemasSearchSourceTests.cs
  14. 1
      frontend/src/app/features/content/declarations.ts
  15. 7
      frontend/src/app/features/content/module.ts
  16. 2
      frontend/src/app/features/content/pages/content/references/content-references.component.ts
  17. 4
      frontend/src/app/features/content/pages/contents/contents-page.component.ts
  18. 52
      frontend/src/app/features/content/pages/references/references-page.component.html
  19. 0
      frontend/src/app/features/content/pages/references/references-page.component.scss
  20. 78
      frontend/src/app/features/content/pages/references/references-page.component.ts
  21. 6
      frontend/src/app/shared/components/assets/asset-dialog.component.html
  22. 218
      frontend/src/app/shared/services/assets.service.spec.ts
  23. 25
      frontend/src/app/shared/services/assets.service.ts
  24. 200
      frontend/src/app/shared/services/contents.service.spec.ts
  25. 96
      frontend/src/app/shared/services/contents.service.ts
  26. 60
      frontend/src/app/shared/state/contents.state.ts

2
backend/i18n/frontend_en.json

@ -129,6 +129,7 @@
"assets.uploaderUploadHere": "No upload in progress, drop files here.",
"assets.uploadFailed": "Failed to upload asset. Please reload.",
"assets.uploadHint": "Drop file on existing item to replace the asset with a newer version.",
"assets.viewReferences": "View all content items referencing this asset.",
"assetScripts.reloaded": "Asset Scripts reloaded.",
"backups.backupCountAssetsLabel": "Assets",
"backups.backupCountAssetsTooltip": "Archived assets",
@ -356,6 +357,7 @@
"common.queryOperators.matchs": "matchs",
"common.queryOperators.ne": "is not equal to",
"common.queryOperators.startsWith": "starts with",
"common.references": "References",
"common.refresh": "Refresh",
"common.remember": "Don't ask again",
"common.rename": "Rename",

2
backend/i18n/frontend_it.json

@ -129,6 +129,7 @@
"assets.uploaderUploadHere": "Nessun caricamento in corso, trascina qui i file.",
"assets.uploadFailed": "Non è stato possibile caricare la risorsa. Per favore ricarica.",
"assets.uploadHint": "Trascina il file sull'elemento esistente per poterlo sostituire con una versione più recente.",
"assets.viewReferences": "View all content items referencing this asset.",
"assetScripts.reloaded": "Asset Scripts reloaded.",
"backups.backupCountAssetsLabel": "Risorse",
"backups.backupCountAssetsTooltip": "Risorse archiviate",
@ -356,6 +357,7 @@
"common.queryOperators.matchs": "matchs",
"common.queryOperators.ne": "non è uguale a",
"common.queryOperators.startsWith": "inizia con",
"common.references": "References",
"common.refresh": "Aggiorna",
"common.remember": "Ricorda la mia decisione",
"common.rename": "Rinomina",

2
backend/i18n/frontend_nl.json

@ -129,6 +129,7 @@
"assets.uploaderUploadHere": "Geen upload bezig, zet bestanden hier neer.",
"assets.uploadFailed": "Uploaden van item is mislukt. Laad opnieuw.",
"assets.uploadHint": "Zet het bestand neer op bestaand item om het bestand te vervangen door een nieuwere versie.",
"assets.viewReferences": "View all content items referencing this asset.",
"assetScripts.reloaded": "Asset Scripts reloaded.",
"backups.backupCountAssetsLabel": "Bestanden",
"backups.backupCountAssetsTooltip": "Gearchiveerde middelen",
@ -356,6 +357,7 @@
"common.queryOperators.matchs": "komt overeen met",
"common.queryOperators.ne": "is niet gelijk aan",
"common.queryOperators.startsWith": "begint met",
"common.references": "References",
"common.refresh": "Vernieuwen",
"common.remember": "Onthoud mijn keuze",
"common.rename": "Hernoemen",

2
backend/i18n/frontend_pt.json

@ -129,6 +129,7 @@
"assets.uploaderUploadHere": "Sem upload em andamento, deixe cair os ficheiros aqui.",
"assets.uploadFailed": "Falhou no upload do ficheiro. Por favor, recarregue.",
"assets.uploadHint": "Deixe cair o ficheiro no item existente para substituir o ficheiro por uma versão mais recente.",
"assets.viewReferences": "View all content items referencing this asset.",
"assetScripts.reloaded": "Scripts de ficheiros recarregados.",
"backups.backupCountAssetsLabel": "Ficheiros",
"backups.backupCountAssetsTooltip": "Ficheiros arquivados",
@ -356,6 +357,7 @@
"common.queryOperators.matchs": "combina com",
"common.queryOperators.ne": "não é igual a",
"common.queryOperators.startsWith": "começa com",
"common.references": "References",
"common.refresh": "Refrescar",
"common.remember": "Não pergunte de novo.",
"common.rename": "Renomear",

2
backend/i18n/frontend_zh.json

@ -129,6 +129,7 @@
"assets.uploaderUploadHere": "没有正在进行的上传,将文件拖到这里。",
"assets.uploadFailed": "资源上传失败,请重新加载。",
"assets.uploadHint": "在现有项目上放置文件以使用更新版本替换资源。",
"assets.viewReferences": "View all content items referencing this asset.",
"assetScripts.reloaded": "Asset Scripts reloaded.",
"backups.backupCountAssetsLabel": "资源",
"backups.backupCountAssetsTooltip": "存档资源",
@ -356,6 +357,7 @@
"common.queryOperators.matchs": "匹配",
"common.queryOperators.ne": "不等于",
"common.queryOperators.startsWith": "开始于",
"common.references": "References",
"common.refresh": "刷新",
"common.remember": "不要再问了",
"common.rename": "重命名",

2
backend/i18n/source/frontend_en.json

@ -129,6 +129,7 @@
"assets.uploaderUploadHere": "No upload in progress, drop files here.",
"assets.uploadFailed": "Failed to upload asset. Please reload.",
"assets.uploadHint": "Drop file on existing item to replace the asset with a newer version.",
"assets.viewReferences": "View all content items referencing this asset.",
"assetScripts.reloaded": "Asset Scripts reloaded.",
"backups.backupCountAssetsLabel": "Assets",
"backups.backupCountAssetsTooltip": "Archived assets",
@ -356,6 +357,7 @@
"common.queryOperators.matchs": "matchs",
"common.queryOperators.ne": "is not equal to",
"common.queryOperators.startsWith": "starts with",
"common.references": "References",
"common.refresh": "Refresh",
"common.remember": "Don't ask again",
"common.rename": "Rename",

29
backend/src/Squidex.Domain.Apps.Entities/Schemas/SchemasSearchSource.cs

@ -23,7 +23,6 @@ public sealed class SchemasSearchSource : ISearchSource
public SchemasSearchSource(IAppProvider appProvider, IUrlGenerator urlGenerator)
{
this.appProvider = appProvider;
this.urlGenerator = urlGenerator;
}
@ -34,24 +33,26 @@ public sealed class SchemasSearchSource : ISearchSource
var schemas = await appProvider.GetSchemasAsync(context.App.Id, ct);
if (schemas.Count > 0)
if (schemas.Count <= 0)
{
var appId = context.App.NamedId();
return result;
}
foreach (var schema in schemas)
{
var schemaId = schema.NamedId();
var appId = context.App.NamedId();
foreach (var schema in schemas)
{
var schemaId = schema.NamedId();
var name = schema.SchemaDef.DisplayNameUnchanged();
var name = schema.SchemaDef.DisplayNameUnchanged();
if (name.Contains(query, StringComparison.OrdinalIgnoreCase))
{
AddSchemaUrl(result, appId, schemaId, name);
if (name.Contains(query, StringComparison.OrdinalIgnoreCase))
{
AddSchemaUrl(result, appId, schemaId, name);
if (schema.SchemaDef.Type != SchemaType.Component && HasPermission(context, schemaId))
{
AddContentsUrl(result, appId, schema, schemaId, name);
}
if (schema.SchemaDef.Type != SchemaType.Component && HasPermission(context, schemaId))
{
AddContentsUrl(result, appId, schema, schemaId, name);
}
}
}

4
backend/src/Squidex/Areas/Api/Controllers/Contents/ContentsSharedController.cs

@ -60,7 +60,7 @@ public sealed class ContentsSharedController : ApiController
/// Queries contents.
/// </summary>
/// <param name="app">The name of the app.</param>
/// <param name="query">The required query object.</param>
/// <param name="query">The query object.</param>
/// <response code="200">Contents returned.</response>.
/// <response code="404">App not found.</response>.
/// <remarks>
@ -73,7 +73,7 @@ public sealed class ContentsSharedController : ApiController
[ApiCosts(1)]
public async Task<IActionResult> GetAllContents(string app, AllContentsByGetDto query)
{
var contents = await contentQuery.QueryAsync(Context, query?.ToQuery() ?? Q.Empty, HttpContext.RequestAborted);
var contents = await contentQuery.QueryAsync(Context, (query ?? new AllContentsByGetDto()).ToQuery(Request), HttpContext.RequestAborted);
var response = Deferred.AsyncResponse(() =>
{

47
backend/src/Squidex/Areas/Api/Controllers/Contents/Models/AllContentsByGetDto.cs

@ -8,6 +8,7 @@
using Microsoft.AspNetCore.Mvc;
using NodaTime;
using Squidex.Domain.Apps.Entities;
using Squidex.Infrastructure;
using Squidex.Infrastructure.Translations;
using Squidex.Infrastructure.Validation;
@ -33,18 +34,54 @@ public sealed class AllContentsByGetDto
[FromQuery]
public Instant? ScheduledTo { get; set; }
public Q ToQuery()
/// <summary>
/// The ID of the referencing content item.
/// </summary>
[FromQuery]
public DomainId? Referencing { get; set; }
/// <summary>
/// The ID of the reference content item.
/// </summary>
[FromQuery]
public DomainId? References { get; set; }
/// <summary>
/// The optional json query.
/// </summary>
[FromQuery(Name = "q")]
public string? JsonQuery { get; set; }
public Q ToQuery(HttpRequest request)
{
var result = Q.Empty;
if (!string.IsNullOrWhiteSpace(Ids))
{
return Q.Empty.WithIds(Ids);
result = result.WithIds(Ids);
}
else if (ScheduledFrom != null && ScheduledTo != null)
{
result = result.WithSchedule(ScheduledFrom.Value, ScheduledTo.Value);
}
else if (Referencing != null)
{
result = result.WithReferencing(Referencing.Value);
}
else if (References != null)
{
result = result.WithReference(References.Value);
}
else
{
throw new ValidationException(T.Get("contents.invalidAllQuery"));
}
if (ScheduledFrom != null && ScheduledTo != null)
if (JsonQuery != null)
{
return Q.Empty.WithSchedule(ScheduledFrom.Value, ScheduledTo.Value);
result = result.WithJsonQuery(JsonQuery);
}
throw new ValidationException(T.Get("contents.invalidAllQuery"));
return result.WithODataQuery(request.Query.ToString());
}
}

50
backend/src/Squidex/Areas/Api/Controllers/Contents/Models/AllContentsByPostDto.cs

@ -8,8 +8,11 @@
using NodaTime;
using Squidex.Domain.Apps.Entities;
using Squidex.Infrastructure;
using Squidex.Infrastructure.Queries;
using Squidex.Infrastructure.Translations;
using Squidex.Infrastructure.Validation;
using System.Text.Json;
using System.Text.Json.Serialization;
namespace Squidex.Areas.Api.Controllers.Contents.Models;
@ -30,18 +33,57 @@ public sealed class AllContentsByPostDto
/// </summary>
public Instant? ScheduledTo { get; set; }
/// <summary>
/// The ID of the referencing content item.
/// </summary>
public DomainId? Referencing { get; set; }
/// <summary>
/// The ID of the reference content item.
/// </summary>
public DomainId? References { get; set; }
/// <summary>
/// The optional odata query.
/// </summary>
public string? OData { get; set; }
/// <summary>
/// The optional json query.
/// </summary>
[JsonPropertyName("q")]
public JsonDocument? JsonQuery { get; set; }
public Q ToQuery()
{
var result = Q.Empty;
if (Ids?.Length > 0)
{
return Q.Empty.WithIds(Ids);
result = result.WithIds(Ids);
}
else if (ScheduledFrom != null && ScheduledTo != null)
{
result = result.WithSchedule(ScheduledFrom.Value, ScheduledTo.Value);
}
else if (Referencing != null)
{
result = result.WithReferencing(Referencing.Value);
}
else if (References != null)
{
result = result.WithReference(References.Value);
}
else
{
throw new ValidationException(T.Get("contents.invalidAllQuery"));
}
if (ScheduledFrom != null && ScheduledTo != null)
if (JsonQuery != null)
{
return Q.Empty.WithSchedule(ScheduledFrom.Value, ScheduledTo.Value);
result = result.WithJsonQuery(JsonQuery.RootElement.ToString());
}
throw new ValidationException(T.Get("contents.invalidAllQuery"));
return result.WithODataQuery(OData);
}
}

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

@ -44,16 +44,11 @@ public sealed class QueryDto
result = result.WithIds(Ids);
}
if (OData != null)
{
result = result.WithODataQuery(OData);
}
if (JsonQuery != null)
{
result = result.WithJsonQuery(JsonQuery.RootElement.ToString());
}
return result;
return result.WithODataQuery(OData);
}
}

6
backend/src/Squidex/appsettings.json

@ -314,12 +314,10 @@
// The log level of the default log adapter.
"logLevel": {
"Default": "Information",
"default": "Information",
// Only logs issued tokens.
// Only logs issued tokens and general request information.
"OpenIddict": "Warning",
// Only logs request infos.
"Microsoft.AspNetCore": "Warning",
"Microsoft.Identity": "Warning",
"Runtime": "Warning"

3
backend/tests/Squidex.Domain.Apps.Entities.Tests/Schemas/SchemasSearchSourceTests.cs

@ -16,7 +16,8 @@ using Squidex.Shared;
namespace Squidex.Domain.Apps.Entities.Schemas;
public class SchemasSearchSourceTests : GivenContext, IClassFixture<TranslationsFixture>
{ private readonly IUrlGenerator urlGenerator = A.Fake<IUrlGenerator>();
{
private readonly IUrlGenerator urlGenerator = A.Fake<IUrlGenerator>();
private readonly SchemasSearchSource sut;
public SchemasSearchSourceTests()

1
frontend/src/app/features/content/declarations.ts

@ -22,6 +22,7 @@ export * from './pages/contents/contents-page.component';
export * from './pages/contents/custom-view-editor.component';
export * from './pages/schemas/schemas-page.component';
export * from './pages/sidebar/sidebar-page.component';
export * from './pages/references/references-page.component';
export * from './shared/content-extension.component';
export * from './shared/due-time-selector.component';
export * from './shared/forms/array-editor.component';

7
frontend/src/app/features/content/module.ts

@ -9,7 +9,7 @@ import { NgModule } from '@angular/core';
import { RouterModule, Routes } from '@angular/router';
import { VirtualScrollerModule } from 'ngx-virtual-scroller';
import { CanDeactivateGuard, ContentMustExistGuard, LoadLanguagesGuard, LoadSchemasGuard, SchemaMustExistPublishedGuard, SchemaMustNotBeSingletonGuard, SqxFrameworkModule, SqxSharedModule } from '@app/shared';
import { ArrayEditorComponent, ArrayItemComponent, AssetsEditorComponent, CalendarPageComponent, CommentsPageComponent, ComponentComponent, ComponentSectionComponent, ContentComponent, ContentCreatorComponent, ContentEditorComponent, ContentEventComponent, ContentExtensionComponent, ContentFieldComponent, ContentHistoryPageComponent, ContentInspectionComponent, ContentPageComponent, ContentReferencesComponent, ContentSectionComponent, ContentsFiltersPageComponent, ContentsPageComponent, CustomViewEditorComponent, DueTimeSelectorComponent, FieldCopyButtonComponent, FieldEditorComponent, FieldLanguagesComponent, IFrameEditorComponent, PreviewButtonComponent, ReferenceDropdownComponent, ReferenceItemComponent, ReferencesCheckboxesComponent, ReferencesEditorComponent, ReferencesTagsComponent, SchemasPageComponent, SidebarPageComponent, StockPhotoEditorComponent } from './declarations';
import { ArrayEditorComponent, ArrayItemComponent, AssetsEditorComponent, CalendarPageComponent, CommentsPageComponent, ComponentComponent, ComponentSectionComponent, ContentComponent, ContentCreatorComponent, ContentEditorComponent, ContentEventComponent, ContentExtensionComponent, ContentFieldComponent, ContentHistoryPageComponent, ContentInspectionComponent, ContentPageComponent, ContentReferencesComponent, ContentSectionComponent, ContentsFiltersPageComponent, ContentsPageComponent, CustomViewEditorComponent, DueTimeSelectorComponent, FieldCopyButtonComponent, FieldEditorComponent, FieldLanguagesComponent, IFrameEditorComponent, PreviewButtonComponent, ReferenceDropdownComponent, ReferenceItemComponent, ReferencesCheckboxesComponent, ReferencesEditorComponent, ReferencesPageComponent, ReferencesTagsComponent, SchemasPageComponent, SidebarPageComponent, StockPhotoEditorComponent } from './declarations';
const routes: Routes = [
{
@ -21,6 +21,10 @@ const routes: Routes = [
path: '__calendar',
component: CalendarPageComponent,
},
{
path: '__references/:referenceId',
component: ReferencesPageComponent,
},
{
path: ':schemaName',
canActivate: [SchemaMustExistPublishedGuard],
@ -121,6 +125,7 @@ const routes: Routes = [
ReferencesCheckboxesComponent,
ReferencesEditorComponent,
ReferencesEditorComponent,
ReferencesPageComponent,
ReferencesTagsComponent,
SchemasPageComponent,
SidebarPageComponent,

2
frontend/src/app/features/content/pages/content/references/content-references.component.ts

@ -67,7 +67,7 @@ export class ContentReferencesComponent implements OnChanges, OnInit, OnDestroy
.getInitial();
if (this.mode === 'references') {
this.contentsState.loadReference(this.content.id, initial);
this.contentsState.loadReferences(this.content.id, initial);
} else {
this.contentsState.loadReferencing(this.content.id, initial);
}

4
frontend/src/app/features/content/pages/contents/contents-page.component.ts

@ -96,8 +96,6 @@ export class ContentsPageComponent extends ResourceOwner implements OnInit {
this.schema = schema;
this.tableSettings = new TableSettings(this.uiState, schema);
const initial =
this.contentsRoute.mapTo(this.contentsState)
.withPaging('contents', 10)
@ -107,6 +105,8 @@ export class ContentsPageComponent extends ResourceOwner implements OnInit {
this.contentsState.load(false, true, initial);
this.contentsRoute.listen();
this.tableSettings = new TableSettings(this.uiState, schema);
const languageKey = this.localStore.get(this.languageKey());
const language = this.languages.find(x => x.iso2Code === languageKey);

52
frontend/src/app/features/content/pages/references/references-page.component.html

@ -0,0 +1,52 @@
<sqx-title message="i18n:common.references"></sqx-title>
<sqx-layout layout="main" titleText="i18n:common.references" titleIcon="contents">
<ng-container menu>
<div class="row flex-nowrap flex-grow-1 gx-2">
<div class="col-auto ms-8">
<button type="button" class="btn btn-text-secondary" (click)="reload()" title="i18n:contents.refreshTooltip" shortcut="CTRL + B">
<i class="icon-reset"></i> {{ 'common.refresh' | sqxTranslate }}
</button>
</div>
<div class="col-auto" *ngIf="languages.length > 1">
<sqx-language-selector class="languages-buttons"
(languageChange)="changeLanguage($event)"
[language]="language"
[languages]="languages">
</sqx-language-selector>
</div>
</div>
</ng-container>
<ng-container>
<sqx-list-view [isLoading]="contentsState.isLoading | async" [table]="true">
<ng-container>
<table class="table table-items table-fixed" *ngIf="contentsState.contents | async; let contents">
<tbody *ngFor="let content of contents; trackBy: trackByContent"
[sqxReferenceItem]="content"
[canRemove]="false"
[columns]="contents | sqxContentsColumns"
[isCompact]="false"
[isDisabled]="false"
[language]="language"
[languages]="languages"
[validations]="(contentsState.validationResults | async)!"
[validityVisible]="true">
</tbody>
<tbody *ngIf="(contentsState.isLoaded | async) && contents.length === 0">
<tr>
<td class="table-items-row-empty">
{{ 'contents.noReferencing' | sqxTranslate }}
</td>
</tr>
</tbody>
</table>
</ng-container>
<ng-container footer>
<sqx-pager [paging]="contentsState.paging | async" (pagingChange)="contentsState.page($event)"></sqx-pager>
</ng-container>
</sqx-list-view>
</ng-container>
</sqx-layout>

0
frontend/src/app/features/content/pages/references/references-page.component.scss

78
frontend/src/app/features/content/pages/references/references-page.component.ts

@ -0,0 +1,78 @@
/*
* Squidex Headless CMS
*
* @license
* Copyright (c) Squidex UG (haftungsbeschränkt). All rights reserved.
*/
import { ChangeDetectionStrategy, Component, OnInit } from '@angular/core';
import { ActivatedRoute } from '@angular/router';
import { distinctUntilChanged, map } from 'rxjs/operators';
import { AppLanguageDto, ComponentContentsState, ContentDto, LanguagesState, QuerySynchronizer, ResourceOwner, Router2State } from '@app/shared';
@Component({
selector: 'sqx-references-page',
styleUrls: ['./references-page.component.scss'],
templateUrl: './references-page.component.html',
changeDetection: ChangeDetectionStrategy.OnPush,
providers: [
Router2State, ComponentContentsState,
],
})
export class ReferencesPageComponent extends ResourceOwner implements OnInit {
public language!: AppLanguageDto;
public languages!: ReadonlyArray<AppLanguageDto>;
constructor(
public readonly contentsRoute: Router2State,
public readonly contentsState: ComponentContentsState,
public readonly languagesState: LanguagesState,
private readonly route: ActivatedRoute,
) {
super();
}
public ngOnInit() {
this.own(
this.languagesState.isoMasterLanguage
.subscribe(language => {
this.language = language;
}));
this.own(
this.languagesState.isoLanguages
.subscribe(languages => {
this.languages = languages;
}));
this.own(
getReferenceId(this.route)
.subscribe(referenceId => {
const initial =
this.contentsRoute.mapTo(this.contentsState)
.withPaging('contents', 10)
.withSynchronizer(QuerySynchronizer.INSTANCE)
.getInitial();
this.contentsState.schema = { name: null! };
this.contentsState.loadReferences(referenceId, initial);
this.contentsRoute.listen();
}));
}
public reload() {
this.contentsState.load(true);
}
public changeLanguage(language: AppLanguageDto) {
this.language = language;
}
public trackByContent(_index: number, content: ContentDto) {
return content.id;
}
}
function getReferenceId(route: ActivatedRoute) {
return route.params.pipe(map(x => x['referenceId'] as string), distinctUntilChanged());
}

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

@ -176,6 +176,12 @@
{{ 'assets.protectedHint' | sqxTranslate }}
</sqx-form-hint>
</div>
<hr />
<div class="form-group">
<a [routerLink]="['../content/__references', asset.id]" target="_blank">{{ 'assets.viewReferences' | sqxTranslate }}</a>
</div>
</div>
</ng-container>
<ng-container *ngSwitchCase="1">

218
frontend/src/app/shared/services/assets.service.spec.ts

@ -28,6 +28,58 @@ describe('AssetsService', () => {
httpMock.verify();
}));
const tests = [
{
name: 'basic query',
query: { take: 17, skip: 13 },
requestBody: { q: sanitize({ take: 17, skip: 13 }), parentId: undefined },
noSlowTotal: null,
noTotal: null,
},
{
name: 'basic query without total',
query: { take: 17, skip: 13, noTotal: true, noSlowTotal: true },
requestBody: { q: sanitize({ take: 17, skip: 13 }), parentId: undefined },
noSlowTotal: '1',
noTotal: '1',
},
{
name: 'query by parent',
query: { take: 17, skip: 13, parentId: '1' },
requestBody: { q: sanitize({ take: 17, skip: 13 }), parentId: '1' },
noSlowTotal: null,
noTotal: null,
},
{
name: 'query by name',
query: { take: 17, skip: 13, query: { fullText: 'my-query' } },
requestBody: { q: sanitize({ filter: { and: [{ path: 'fileName', op: 'contains', value: 'my-query' }] }, take: 17, skip: 13 }), parentId: undefined },
noSlowTotal: null,
noTotal: null,
},
{
name: 'query by tag',
query: { take: 17, skip: 13, tags: ['tag1'] },
requestBody: { q: sanitize({ filter: { and: [{ path: 'tags', op: 'eq', value: 'tag1' }] }, take: 17, skip: 13 }), parentId: undefined },
noSlowTotal: null,
noTotal: null,
},
{
name: 'query by ids',
query: { ids: ['1', '2'] },
requestBody: { ids: ['1', '2'] },
noSlowTotal: null,
noTotal: null,
},
{
name: 'query by ref',
query: { ref: '1' },
requestBody: { q: sanitize({ filter: { or: [{ path: 'id', op: 'eq', value: '1' }, { path: 'slug', op: 'eq', value: '1' }] }, take: 1 }) },
noSlowTotal: null,
noTotal: '1',
},
];
it('should make get request to get asset tags',
inject([AssetsService, HttpTestingController], (assetsService: AssetsService, httpMock: HttpTestingController) => {
let tags: any;
@ -79,46 +131,46 @@ describe('AssetsService', () => {
});
}));
it('should make post request to get assets',
inject([AssetsService, HttpTestingController], (assetsService: AssetsService, httpMock: HttpTestingController) => {
let assets: AssetsDto;
assetsService.getAssets('my-app', { take: 17, skip: 13 }).subscribe(result => {
assets = result;
});
const expectedQuery = { 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.headers.get('X-NoSlowTotal')).toBeNull();
expect(req.request.headers.get('X-NoTotal')).toBeNull();
expect(req.request.body).toEqual({ q: sanitize(expectedQuery) });
req.flush({
total: 10,
items: [
assetResponse(12),
assetResponse(13),
],
folders: [
assetFolderResponse(22),
assetFolderResponse(23),
],
});
expect(assets!).toEqual({
items: [
createAsset(12),
createAsset(13),
],
total: 10,
canCreate: false,
canRenameTag: false,
});
}));
tests.forEach(x => {
it(`should make post request to get assets with ${x.name}`,
inject([AssetsService, HttpTestingController], (assetsService: AssetsService, httpMock: HttpTestingController) => {
let assets: AssetsDto;
assetsService.getAssets('my-app', x.query).subscribe(result => {
assets = result;
});
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.headers.get('X-NoSlowTotal')).toEqual(x.noSlowTotal);
expect(req.request.headers.get('X-NoTotal')).toEqual(x.noTotal);
expect(req.request.body).toEqual(x.requestBody);
req.flush({
total: 10,
items: [
assetResponse(12),
assetResponse(13),
],
folders: [
assetFolderResponse(22),
assetFolderResponse(23),
],
});
expect(assets!).toEqual({
items: [
createAsset(12),
createAsset(13),
],
total: 10,
canCreate: false,
canRenameTag: false,
});
}));
});
it('should make get request to get asset folders',
inject([AssetsService, HttpTestingController], (assetsService: AssetsService, httpMock: HttpTestingController) => {
@ -174,92 +226,6 @@ describe('AssetsService', () => {
expect(asset!).toEqual(createAsset(12));
}));
it('should make post request to get assets by name',
inject([AssetsService, HttpTestingController], (assetsService: AssetsService, httpMock: HttpTestingController) => {
const query = { fullText: 'my-query' };
assetsService.getAssets('my-app', { take: 17, skip: 13, query }).subscribe();
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.headers.get('X-NoSlowTotal')).toBeNull();
expect(req.request.headers.get('X-NoTotal')).toBeNull();
expect(req.request.body).toEqual({ q: sanitize(expectedQuery) });
req.flush({ total: 10, items: [] });
}));
it('should make post 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/query');
expect(req.request.method).toEqual('POST');
expect(req.request.headers.get('If-Match')).toBeNull();
expect(req.request.headers.get('X-NoSlowTotal')).toBeNull();
expect(req.request.headers.get('X-NoTotal')).toBeNull();
expect(req.request.body).toEqual({ q: sanitize(expectedQuery) });
req.flush({ total: 10, items: [] });
}));
it('should make post 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/query');
expect(req.request.method).toEqual('POST');
expect(req.request.headers.get('If-Match')).toBeNull();
expect(req.request.headers.get('X-NoSlowTotal')).toBeNull();
expect(req.request.headers.get('X-NoTotal')).toBeNull();
expect(req.request.body).toEqual({ ids });
req.flush({ total: 10, items: [] });
}));
it('should make post request to get assets by ref',
inject([AssetsService, HttpTestingController], (assetsService: AssetsService, httpMock: HttpTestingController) => {
const value = '1', op = 'eq';
assetsService.getAssets('my-app', { ref: value }).subscribe();
const expectedQuery = { filter: { or: [{ path: 'id', op, value }, { path: 'slug', op, value }] }, take: 1 };
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.headers.get('X-NoTotal')).toEqual('1');
expect(req.request.headers.get('X-NoSlowTotal')).toBeNull();
expect(req.request.body).toEqual({ q: sanitize(expectedQuery) });
req.flush({ total: 10, items: [] });
}));
it('should make post request to get assets without total',
inject([AssetsService, HttpTestingController], (assetsService: AssetsService, httpMock: HttpTestingController) => {
assetsService.getAssets('my-app', { take: 17, skip: 13, noSlowTotal: true, noTotal: true }).subscribe();
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.headers.get('X-NoTotal')).toBe('1');
expect(req.request.headers.get('X-NoSlowTotal')).toBe('1');
req.flush({ total: 10, items: [] });
}));
it('should make post request to create asset',
inject([AssetsService, HttpTestingController], (assetsService: AssetsService, httpMock: HttpTestingController) => {
let asset: AssetDto;

25
frontend/src/app/shared/services/assets.service.ts

@ -418,12 +418,6 @@ function buildHeaders(q: AssetsQuery | undefined, noTotal: boolean) {
function buildQuery(q?: AssetsQuery & AssetsByQuery & AssetsByIds & AssetsByRef) {
const { ids, parentId, query, ref, skip, tags, take } = q || {};
const body: any = {};
if (parentId) {
body.parentId = parentId;
}
if (ref) {
const queryObj: Query = {
filter: {
@ -440,28 +434,27 @@ function buildQuery(q?: AssetsQuery & AssetsByQuery & AssetsByIds & AssetsByRef)
take: 1,
};
body.q = sanitize(queryObj);
return { q: sanitize(queryObj) };
} else if (Types.isArray(ids)) {
body.ids = ids;
return { ids };
} else {
const queryObj: Query = {};
const filters: any[] = [];
const queryFilters: any[] = [];
if (query && query.fullText && query.fullText.length > 0) {
filters.push({ path: 'fileName', op: 'contains', value: query.fullText });
queryFilters.push({ path: 'fileName', op: 'contains', value: query.fullText });
}
if (tags) {
for (const tag of tags) {
if (tag && tag.length > 0) {
filters.push({ path: 'tags', op: 'eq', value: tag });
queryFilters.push({ path: 'tags', op: 'eq', value: tag });
}
}
}
if (filters.length > 0) {
queryObj.filter = { and: filters };
if (queryFilters.length > 0) {
queryObj.filter = { and: queryFilters };
}
if (take && take > 0) {
@ -472,10 +465,8 @@ function buildQuery(q?: AssetsQuery & AssetsByQuery & AssetsByIds & AssetsByRef)
queryObj.skip = skip;
}
body.q = sanitize(queryObj);
return { q: sanitize(queryObj), parentId };
}
return body;
}
function parseAssets(response: { items: any[]; total: number } & Resource): AssetsDto {

200
frontend/src/app/shared/services/contents.service.spec.ts

@ -31,112 +31,126 @@ describe('ContentsService', () => {
httpMock.verify();
}));
it('should make post request to get contents with json query',
inject([ContentsService, HttpTestingController], (contentsService: ContentsService, httpMock: HttpTestingController) => {
const query = { fullText: 'my-query' };
let contents: ContentsDto;
contentsService.getContents('my-app', 'my-schema', { take: 17, skip: 13, query }).subscribe(result => {
contents = result;
});
const expectedQuery = { ...query, take: 17, skip: 13 };
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.headers.get('X-NoSlowTotal')).toBeNull();
expect(req.request.headers.get('X-NoTotal')).toBeNull();
expect(req.request.body).toEqual({ q: sanitize(expectedQuery) });
req.flush({
total: 10,
items: [
contentResponse(12),
contentResponse(13),
],
statuses: [{
status: 'Draft', color: 'Gray',
}],
});
expect(contents!).toEqual({
items: [
createContent(12),
createContent(13),
],
total: 10,
statuses: [
{ status: 'Draft', color: 'Gray' },
],
canCreate: false,
canCreateAndPublish: false,
});
}));
it('should make post request to get contents with odata filter',
inject([ContentsService, HttpTestingController], (contentsService: ContentsService, httpMock: HttpTestingController) => {
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/query');
expect(req.request.method).toEqual('POST');
expect(req.request.headers.get('If-Match')).toBeNull();
expect(req.request.headers.get('X-NoSlowTotal')).toBeNull();
expect(req.request.headers.get('X-NoTotal')).toBeNull();
expect(req.request.body).toEqual({ odata: '$filter=my-filter&$top=17&$skip=13' });
const tests = [
{
name: 'json query',
query: { take: 17, skip: 13, query: { fullText: 'my-query' } },
requestBody: { q: sanitize({ fullText: 'my-query', take: 17, skip: 13 }) },
requestString: `q=${JSON.stringify(sanitize({ fullText: 'my-query', take: 17, skip: 13 }))}`,
noSlowTotal: null,
noTotal: null,
},
{
name: 'odata filter',
query: { take: 17, skip: 13, query: { fullText: '$filter=my-filter' } },
requestBody: { odata: '$filter=my-filter&$top=17&$skip=13' },
requestString: '$filter=my-filter&$top=17&$skip=13',
noSlowTotal: null,
noTotal: null,
},
{
name: 'json query without total',
query: { take: 17, skip: 13, query: { fullText: 'my-query' }, noTotal: true, noSlowTotal: true },
requestBody: { q: sanitize({ fullText: 'my-query', take: 17, skip: 13 }) },
requestString: `q=${JSON.stringify(sanitize({ fullText: 'my-query', take: 17, skip: 13 }))}`,
noSlowTotal: '1',
noTotal: '1',
},
];
tests.forEach(x => {
it(`should make post request to get contents using ${x.name}`,
inject([ContentsService, HttpTestingController], (contentsService: ContentsService, httpMock: HttpTestingController) => {
let contents: ContentsDto;
contentsService.getContents('my-app', 'my-schema', x.query).subscribe(result => {
contents = result;
});
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.headers.get('X-NoSlowTotal')).toEqual(x.noSlowTotal);
expect(req.request.headers.get('X-NoTotal')).toEqual(x.noTotal);
expect(req.request.body).toEqual({ ...x.requestBody });
req.flush({
total: 10,
items: [
contentResponse(12),
contentResponse(13),
],
statuses: [{
status: 'Draft', color: 'Gray',
}],
});
expect(contents!).toEqual({
items: [
createContent(12),
createContent(13),
],
total: 10,
statuses: [
{ status: 'Draft', color: 'Gray' },
],
canCreate: false,
canCreateAndPublish: false,
});
}));
});
req.flush({ total: 10, items: [] });
}));
tests.forEach(x => {
it(`should make post request to get all contents using ${x.name}`,
inject([ContentsService, HttpTestingController], (contentsService: ContentsService, httpMock: HttpTestingController) => {
const ids = ['1', '2', '3'];
it('should make post request to get all contents by ids',
inject([ContentsService, HttpTestingController], (contentsService: ContentsService, httpMock: HttpTestingController) => {
const ids = ['1', '2', '3'];
contentsService.getAllContents('my-app', { ids }, x.query).subscribe();
contentsService.getAllContents('my-app', { ids }).subscribe();
const req = httpMock.expectOne('http://service/p/api/content/my-app');
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.headers.get('X-NoSlowTotal')).toEqual(x.noSlowTotal);
expect(req.request.headers.get('X-NoTotal')).toEqual(x.noTotal);
expect(req.request.body).toEqual({ ids, ...x.requestBody });
expect(req.request.method).toEqual('POST');
expect(req.request.headers.get('If-Match')).toBeNull();
expect(req.request.headers.get('X-NoSlowTotal')).toBeNull();
expect(req.request.headers.get('X-NoTotal')).toBeNull();
expect(req.request.body).toEqual({ ids });
req.flush({ total: 10, items: [] });
}));
});
req.flush({ total: 10, items: [] });
}));
tests.forEach(x => {
it(`should make get request to get references with using ${x.name}`,
inject([ContentsService, HttpTestingController], (contentsService: ContentsService, httpMock: HttpTestingController) => {
contentsService.getContentReferences('my-app', 'my-schema', '42', x.query).subscribe();
it('should make post request to get contents without total',
inject([ContentsService, HttpTestingController], (contentsService: ContentsService, httpMock: HttpTestingController) => {
contentsService.getContents('my-app', 'my-schema', { noTotal: true, noSlowTotal: true }).subscribe();
const req = httpMock.expectOne(`http://service/p/api/content/my-app/my-schema/42/references?${x.requestString}`);
const req = httpMock.expectOne('http://service/p/api/content/my-app/my-schema/query');
expect(req.request.method).toEqual('GET');
expect(req.request.headers.get('If-Match')).toBeNull();
expect(req.request.headers.get('X-NoSlowTotal')).toEqual(x.noSlowTotal);
expect(req.request.headers.get('X-NoTotal')).toEqual(x.noTotal);
expect(req.request.method).toEqual('POST');
expect(req.request.headers.get('If-Match')).toBeNull();
expect(req.request.headers.get('X-NoSlowTotal')).toBe('1');
expect(req.request.headers.get('X-NoTotal')).toBe('1');
req.flush({ total: 10, items: [] });
}));
});
req.flush({ total: 10, items: [] });
}));
tests.forEach(x => {
it(`should make get request to get referencing with using ${x.name}`,
inject([ContentsService, HttpTestingController], (contentsService: ContentsService, httpMock: HttpTestingController) => {
contentsService.getContentReferencing('my-app', 'my-schema', '42', x.query).subscribe();
it('should make post request to get all contents without total',
inject([ContentsService, HttpTestingController], (contentsService: ContentsService, httpMock: HttpTestingController) => {
contentsService.getAllContents('my-app', { ids: [], noTotal: true, noSlowTotal: true }).subscribe();
const req = httpMock.expectOne(`http://service/p/api/content/my-app/my-schema/42/referencing?${x.requestString}`);
const req = httpMock.expectOne('http://service/p/api/content/my-app');
expect(req.request.method).toEqual('GET');
expect(req.request.headers.get('If-Match')).toBeNull();
expect(req.request.headers.get('X-NoSlowTotal')).toEqual(x.noSlowTotal);
expect(req.request.headers.get('X-NoTotal')).toEqual(x.noTotal);
expect(req.request.method).toEqual('POST');
expect(req.request.headers.get('If-Match')).toBeNull();
expect(req.request.headers.get('X-NoSlowTotal')).toBe('1');
expect(req.request.headers.get('X-NoTotal')).toBe('1');
req.flush({ total: 10, items: [] });
}));
req.flush({ total: 10, items: [] });
}));
});
it('should make get request to get content',
inject([ContentsService, HttpTestingController], (contentsService: ContentsService, httpMock: HttpTestingController) => {

96
frontend/src/app/shared/services/contents.service.ts

@ -168,7 +168,7 @@ export type ContentsQuery = Readonly<{
export type ContentsByIds = Readonly<{
// The Ids of the contents to query.
ids: ReadonlyArray<string>;
}> & ContentsQuery;
}>;
export type ContentsBySchedule = Readonly<{
// The start of the time frame for scheduled content items.
@ -176,7 +176,17 @@ export type ContentsBySchedule = Readonly<{
// The end of the time frame for scheduled content items.
scheduledTo: string | null;
}> & ContentsQuery;
}>;
export type ContentsByReferences = Readonly<{
// The reference content id.
references: string;
}>;
export type ContentsByReferencing = Readonly<{
// The referencing content id.
referencing: string;
}>;
export type ContentsByQuery = Readonly<{
// The JSON query.
@ -189,6 +199,8 @@ export type ContentsByQuery = Readonly<{
take?: number;
}> & ContentsQuery;
type FullQuery = ContentsByIds | ContentsBySchedule | ContentsByReferences | ContentsByReferencing;
@Injectable()
export class ContentsService {
constructor(
@ -202,36 +214,19 @@ export class ContentsService {
const url = this.apiUrl.buildUrl(`/api/content/${appName}/${schemaName}/query`);
return this.http.post<any>(url, body, buildHeaders(q, false)).pipe(
return this.http.post<any>(url, body, buildHeaders(q)).pipe(
map(body => {
return parseContents(body);
}),
pretifyError('i18n:contents.loadFailed'));
}
public getContent(appName: string, schemaName: string, id: string, language?: string): Observable<ContentDto> {
const url = this.apiUrl.buildUrl(`/api/content/${appName}/${schemaName}/${id}`);
let headers = new HttpHeaders();
if (language) {
headers = headers.set('X-Flatten', '1');
headers = headers.set('X-Languages', language);
}
return HTTP.getVersioned(this.http, url, undefined, headers).pipe(
map(({ payload }) => {
return parseContent(payload.body);
}),
pretifyError('i18n:contents.loadContentFailed'));
}
public getAllContents(appName: string, q: ContentsByIds | ContentsBySchedule): Observable<ContentsDto> {
const { ...body } = q;
public getAllContents(appName: string, primary: FullQuery, q?: ContentsByQuery): Observable<ContentsDto> {
const body = buildFullQuery(primary, q);
const url = this.apiUrl.buildUrl(`/api/content/${appName}`);
return this.http.post<any>(url, body, buildHeaders(q, false)).pipe(
return this.http.post<any>(url, body, buildHeaders(q)).pipe(
map(body => {
return parseContents(body);
}),
@ -239,11 +234,11 @@ export class ContentsService {
}
public getContentReferences(appName: string, schemaName: string, id: string, q?: ContentsByQuery): Observable<ContentsDto> {
const { fullQuery } = buildQuery(q);
const query = buildQuery(q);
const url = this.apiUrl.buildUrl(`/api/content/${appName}/${schemaName}/${id}/references?${fullQuery}`);
const url = this.apiUrl.buildUrl(`/api/content/${appName}/${schemaName}/${id}/references?${buildQueryString(query)}`);
return this.http.get<any>(url, buildHeaders(q, false)).pipe(
return this.http.get<any>(url, buildHeaders(q)).pipe(
map(body => {
return parseContents(body);
}),
@ -251,17 +246,34 @@ export class ContentsService {
}
public getContentReferencing(appName: string, schemaName: string, id: string, q?: ContentsByQuery): Observable<ContentsDto> {
const { fullQuery } = buildQuery(q);
const query = buildQuery(q);
const url = this.apiUrl.buildUrl(`/api/content/${appName}/${schemaName}/${id}/referencing?${fullQuery}`);
const url = this.apiUrl.buildUrl(`/api/content/${appName}/${schemaName}/${id}/referencing?${buildQueryString(query)}`);
return this.http.get<any>(url).pipe(
return this.http.get<any>(url, buildHeaders(q)).pipe(
map(body => {
return parseContents(body);
}),
pretifyError('i18n:contents.loadFailed'));
}
public getContent(appName: string, schemaName: string, id: string, language?: string): Observable<ContentDto> {
const url = this.apiUrl.buildUrl(`/api/content/${appName}/${schemaName}/${id}`);
let headers = new HttpHeaders();
if (language) {
headers = headers.set('X-Flatten', '1');
headers = headers.set('X-Languages', language);
}
return HTTP.getVersioned(this.http, url, undefined, headers).pipe(
map(({ payload }) => {
return parseContent(payload.body);
}),
pretifyError('i18n:contents.loadContentFailed'));
}
public getVersionData(appName: string, schemaName: string, id: string, version: Version): Observable<Versioned<any>> {
const url = this.apiUrl.buildUrl(`/api/content/${appName}/${schemaName}/${id}/${version.value}`);
@ -353,12 +365,12 @@ export class ContentsService {
}
}
function buildHeaders(q: ContentsQuery | undefined, noTotal: boolean) {
function buildHeaders(q: ContentsQuery | undefined) {
let options = {
headers: {},
};
if (q?.noTotal || noTotal) {
if (q?.noTotal) {
options.headers['X-NoTotal'] = '1';
}
@ -369,10 +381,20 @@ function buildHeaders(q: ContentsQuery | undefined, noTotal: boolean) {
return options;
}
function buildQuery(q?: ContentsByQuery) {
const { query, skip, take } = q || {};
function buildFullQuery(primary: FullQuery, q?: ContentsByQuery) {
const query = buildQuery(q);
return { ...query, ...primary };
}
function buildQueryString(input: { q?: object; odata?: string }) {
const { odata, q } = input;
return q ? `q=${JSON.stringify(q)}` : odata;
}
const body: any = {};
function buildQuery(q?: ContentsByQuery): { q?: object; odata?: string } {
const { query, skip, take } = q || {};
if (query && query.fullText && query.fullText.indexOf('$') >= 0) {
const odataParts: string[] = [
@ -387,7 +409,7 @@ function buildQuery(q?: ContentsByQuery) {
odataParts.push(`$skip=${skip}`);
}
body.odata = odataParts.join('&');
return { odata: odataParts.join('&') };
} else {
const queryObj: Query = { ...query };
@ -399,10 +421,8 @@ function buildQuery(q?: ContentsByQuery) {
queryObj.skip = skip;
}
body.q = sanitize(queryObj);
return { q: sanitize(queryObj) };
}
return body;
}
function parseContents(response: { items: any[]; total: number; statuses: any } & Resource): ContentsDto {

60
frontend/src/app/shared/state/contents.state.ts

@ -28,7 +28,7 @@ interface Snapshot extends ListState<Query> {
referencing?: string;
// The reference content id.
reference?: string;
references?: string;
// The statuses.
statuses?: ReadonlyArray<StatusInfo>;
@ -129,14 +129,14 @@ export abstract class ContentsStateBase extends State<Snapshot> {
}));
}
public loadReference(contentId: string, update: Partial<Snapshot> = {}) {
this.resetState({ reference: contentId, referencing: undefined, ...update });
public loadReferences(contentId: string, update: Partial<Snapshot> = {}) {
this.resetState({ references: contentId, referencing: undefined, ...update });
return this.loadInternal(false, true);
}
public loadReferencing(contentId: string, update: Partial<Snapshot> = {}) {
this.resetState({ referencing: contentId, reference: undefined, ...update });
this.resetState({ referencing: contentId, references: undefined, ...update });
return this.loadInternal(false, true);
}
@ -162,32 +162,29 @@ export abstract class ContentsStateBase extends State<Snapshot> {
}
private loadInternalCore(isReload: boolean, noSlowTotal: boolean) {
if (!this.appName || !this.schemaName) {
if (!this.appName) {
return EMPTY;
}
this.next({ isLoading: true }, 'Loading Started');
const { page, pageSize, query, reference, referencing, total } = this.snapshot;
const q: any = { take: pageSize, skip: pageSize * page, noSlowTotal };
if (query) {
q.query = query;
}
if (page > 0 && total > 0) {
q.noTotal = true;
}
const { references, referencing } = this.snapshot;
const query = createQuery(this.snapshot, noSlowTotal);
let content$: Observable<ContentsDto>;
if (referencing) {
content$ = this.contentsService.getContentReferencing(this.appName, this.schemaName, referencing, q);
} else if (reference) {
content$ = this.contentsService.getContentReferences(this.appName, this.schemaName, reference, q);
if (referencing && this.schemaName) {
content$ = this.contentsService.getContentReferencing(this.appName, this.schemaName, referencing, query);
} else if (referencing) {
content$ = this.contentsService.getAllContents(this.appName, { referencing }, query);
} else if (references && this.schemaName) {
content$ = this.contentsService.getContentReferences(this.appName, this.schemaName, references, query);
} else if (references) {
content$ = this.contentsService.getAllContents(this.appName, { references }, query);
} else if (this.schemaName) {
content$ = this.contentsService.getContents(this.appName, this.schemaName, query);
} else {
content$ = this.contentsService.getContents(this.appName, this.schemaName, q);
return EMPTY;
}
return content$.pipe(
@ -481,6 +478,27 @@ function getStatusQueries(statuses: ReadonlyArray<StatusInfo> | undefined): Read
return statuses?.map(buildStatusQuery) || [];
}
function createQuery(snapshot: Snapshot, noSlowTotal: boolean) {
const {
page,
pageSize,
query,
total,
} = snapshot;
const result: any = { take: pageSize, skip: pageSize * page, noSlowTotal };
if (query) {
result.query = query;
}
if (page > 0 && total > 0) {
result.noTotal = true;
}
return result;
}
function buildStatusQuery(s: StatusInfo) {
const query = {
filter: {

Loading…
Cancel
Save