diff --git a/backend/i18n/frontend_en.json b/backend/i18n/frontend_en.json index c5b37de45..a7e61cf80 100644 --- a/backend/i18n/frontend_en.json +++ b/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", diff --git a/backend/i18n/frontend_it.json b/backend/i18n/frontend_it.json index a1e7dbd57..bceba87c1 100644 --- a/backend/i18n/frontend_it.json +++ b/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", diff --git a/backend/i18n/frontend_nl.json b/backend/i18n/frontend_nl.json index 9d66bf493..cc0fa6cf5 100644 --- a/backend/i18n/frontend_nl.json +++ b/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", diff --git a/backend/i18n/frontend_pt.json b/backend/i18n/frontend_pt.json index 797153bf1..2b7f07b34 100644 --- a/backend/i18n/frontend_pt.json +++ b/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", diff --git a/backend/i18n/frontend_zh.json b/backend/i18n/frontend_zh.json index 149e8fda5..31e417bac 100644 --- a/backend/i18n/frontend_zh.json +++ b/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": "重命名", diff --git a/backend/i18n/source/frontend_en.json b/backend/i18n/source/frontend_en.json index c5b37de45..a7e61cf80 100644 --- a/backend/i18n/source/frontend_en.json +++ b/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", diff --git a/backend/src/Squidex.Domain.Apps.Entities/Schemas/SchemasSearchSource.cs b/backend/src/Squidex.Domain.Apps.Entities/Schemas/SchemasSearchSource.cs index 759dbaafa..b4b11e479 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Schemas/SchemasSearchSource.cs +++ b/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); } } } diff --git a/backend/src/Squidex/Areas/Api/Controllers/Contents/ContentsSharedController.cs b/backend/src/Squidex/Areas/Api/Controllers/Contents/ContentsSharedController.cs index 3445bbf2b..ebb9ffa26 100644 --- a/backend/src/Squidex/Areas/Api/Controllers/Contents/ContentsSharedController.cs +++ b/backend/src/Squidex/Areas/Api/Controllers/Contents/ContentsSharedController.cs @@ -60,7 +60,7 @@ public sealed class ContentsSharedController : ApiController /// Queries contents. /// /// The name of the app. - /// The required query object. + /// The query object. /// Contents returned.. /// App not found.. /// @@ -73,7 +73,7 @@ public sealed class ContentsSharedController : ApiController [ApiCosts(1)] public async Task 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(() => { diff --git a/backend/src/Squidex/Areas/Api/Controllers/Contents/Models/AllContentsByGetDto.cs b/backend/src/Squidex/Areas/Api/Controllers/Contents/Models/AllContentsByGetDto.cs index 2a2eab4ee..279fad30d 100644 --- a/backend/src/Squidex/Areas/Api/Controllers/Contents/Models/AllContentsByGetDto.cs +++ b/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() + /// + /// The ID of the referencing content item. + /// + [FromQuery] + public DomainId? Referencing { get; set; } + + /// + /// The ID of the reference content item. + /// + [FromQuery] + public DomainId? References { get; set; } + + /// + /// The optional json query. + /// + [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()); } } diff --git a/backend/src/Squidex/Areas/Api/Controllers/Contents/Models/AllContentsByPostDto.cs b/backend/src/Squidex/Areas/Api/Controllers/Contents/Models/AllContentsByPostDto.cs index 675837531..2b8d33040 100644 --- a/backend/src/Squidex/Areas/Api/Controllers/Contents/Models/AllContentsByPostDto.cs +++ b/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 /// public Instant? ScheduledTo { get; set; } + /// + /// The ID of the referencing content item. + /// + public DomainId? Referencing { get; set; } + + /// + /// The ID of the reference content item. + /// + public DomainId? References { get; set; } + + /// + /// The optional odata query. + /// + public string? OData { get; set; } + + /// + /// The optional json query. + /// + [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); } } diff --git a/backend/src/Squidex/Areas/Api/Controllers/QueryDto.cs b/backend/src/Squidex/Areas/Api/Controllers/QueryDto.cs index 923093a58..fa1d954bf 100644 --- a/backend/src/Squidex/Areas/Api/Controllers/QueryDto.cs +++ b/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); } } diff --git a/backend/src/Squidex/appsettings.json b/backend/src/Squidex/appsettings.json index 8966f63a5..6e162ea7f 100644 --- a/backend/src/Squidex/appsettings.json +++ b/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" diff --git a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Schemas/SchemasSearchSourceTests.cs b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Schemas/SchemasSearchSourceTests.cs index 314ff1887..a722d64fb 100644 --- a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Schemas/SchemasSearchSourceTests.cs +++ b/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 -{ private readonly IUrlGenerator urlGenerator = A.Fake(); +{ + private readonly IUrlGenerator urlGenerator = A.Fake(); private readonly SchemasSearchSource sut; public SchemasSearchSourceTests() diff --git a/frontend/src/app/features/content/declarations.ts b/frontend/src/app/features/content/declarations.ts index ffed01b58..0862ab669 100644 --- a/frontend/src/app/features/content/declarations.ts +++ b/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'; diff --git a/frontend/src/app/features/content/module.ts b/frontend/src/app/features/content/module.ts index 2ee7cf034..012e9fe40 100644 --- a/frontend/src/app/features/content/module.ts +++ b/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, diff --git a/frontend/src/app/features/content/pages/content/references/content-references.component.ts b/frontend/src/app/features/content/pages/content/references/content-references.component.ts index 760b79a94..dc7d56948 100644 --- a/frontend/src/app/features/content/pages/content/references/content-references.component.ts +++ b/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); } diff --git a/frontend/src/app/features/content/pages/contents/contents-page.component.ts b/frontend/src/app/features/content/pages/contents/contents-page.component.ts index 1a036109c..40ff2b627 100644 --- a/frontend/src/app/features/content/pages/contents/contents-page.component.ts +++ b/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); diff --git a/frontend/src/app/features/content/pages/references/references-page.component.html b/frontend/src/app/features/content/pages/references/references-page.component.html new file mode 100644 index 000000000..94bd3169a --- /dev/null +++ b/frontend/src/app/features/content/pages/references/references-page.component.html @@ -0,0 +1,52 @@ + + + + + + + + {{ 'common.refresh' | sqxTranslate }} + + + 1"> + + + + + + + + + + + + + + + + + {{ 'contents.noReferencing' | sqxTranslate }} + + + + + + + + + + + + \ No newline at end of file diff --git a/frontend/src/app/features/content/pages/references/references-page.component.scss b/frontend/src/app/features/content/pages/references/references-page.component.scss new file mode 100644 index 000000000..e69de29bb diff --git a/frontend/src/app/features/content/pages/references/references-page.component.ts b/frontend/src/app/features/content/pages/references/references-page.component.ts new file mode 100644 index 000000000..d98b17fc1 --- /dev/null +++ b/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; + + 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()); +} \ No newline at end of file diff --git a/frontend/src/app/shared/components/assets/asset-dialog.component.html b/frontend/src/app/shared/components/assets/asset-dialog.component.html index c44844a44..5c1d0901f 100644 --- a/frontend/src/app/shared/components/assets/asset-dialog.component.html +++ b/frontend/src/app/shared/components/assets/asset-dialog.component.html @@ -176,6 +176,12 @@ {{ 'assets.protectedHint' | sqxTranslate }} + + + + + {{ 'assets.viewReferences' | sqxTranslate }} + diff --git a/frontend/src/app/shared/services/assets.service.spec.ts b/frontend/src/app/shared/services/assets.service.spec.ts index 5ccef9cbe..eedf83814 100644 --- a/frontend/src/app/shared/services/assets.service.spec.ts +++ b/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; diff --git a/frontend/src/app/shared/services/assets.service.ts b/frontend/src/app/shared/services/assets.service.ts index f4b35be8e..5d1a81f91 100644 --- a/frontend/src/app/shared/services/assets.service.ts +++ b/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 { diff --git a/frontend/src/app/shared/services/contents.service.spec.ts b/frontend/src/app/shared/services/contents.service.spec.ts index 65910167c..74056e8cf 100644 --- a/frontend/src/app/shared/services/contents.service.spec.ts +++ b/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) => { diff --git a/frontend/src/app/shared/services/contents.service.ts b/frontend/src/app/shared/services/contents.service.ts index 36d12ee71..8a6d57e13 100644 --- a/frontend/src/app/shared/services/contents.service.ts +++ b/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; -}> & 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(url, body, buildHeaders(q, false)).pipe( + return this.http.post(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 { - 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 { - const { ...body } = q; + public getAllContents(appName: string, primary: FullQuery, q?: ContentsByQuery): Observable { + const body = buildFullQuery(primary, q); const url = this.apiUrl.buildUrl(`/api/content/${appName}`); - return this.http.post(url, body, buildHeaders(q, false)).pipe( + return this.http.post(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 { - 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(url, buildHeaders(q, false)).pipe( + return this.http.get(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 { - 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(url).pipe( + return this.http.get(url, buildHeaders(q)).pipe( map(body => { return parseContents(body); }), pretifyError('i18n:contents.loadFailed')); } + public getContent(appName: string, schemaName: string, id: string, language?: string): Observable { + 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> { 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 { diff --git a/frontend/src/app/shared/state/contents.state.ts b/frontend/src/app/shared/state/contents.state.ts index d8c538eee..a9a4672de 100644 --- a/frontend/src/app/shared/state/contents.state.ts +++ b/frontend/src/app/shared/state/contents.state.ts @@ -28,7 +28,7 @@ interface Snapshot extends ListState { referencing?: string; // The reference content id. - reference?: string; + references?: string; // The statuses. statuses?: ReadonlyArray; @@ -129,14 +129,14 @@ export abstract class ContentsStateBase extends State { })); } - public loadReference(contentId: string, update: Partial = {}) { - this.resetState({ reference: contentId, referencing: undefined, ...update }); + public loadReferences(contentId: string, update: Partial = {}) { + this.resetState({ references: contentId, referencing: undefined, ...update }); return this.loadInternal(false, true); } public loadReferencing(contentId: string, update: Partial = {}) { - 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 { } 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; - 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 | 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: {