Browse Source

Show only published content in UI.

pull/361/head
Sebastian Stehle 7 years ago
parent
commit
9451919761
  1. 1
      Squidex.ruleset
  2. 20
      src/Squidex.Domain.Apps.Entities.MongoDb/Contents/MongoContentCollection.cs
  3. 41
      src/Squidex.Domain.Apps.Entities.MongoDb/Contents/MongoContentRepository.cs
  4. 12
      src/Squidex.Domain.Apps.Entities.MongoDb/Contents/Visitors/FilterFactory.cs
  5. 66
      src/Squidex.Domain.Apps.Entities/Contents/ContentQueryService.cs
  6. 8
      src/Squidex.Domain.Apps.Entities/Contents/Repositories/IContentRepository.cs
  7. 15
      src/Squidex.Domain.Apps.Entities/Contents/StatusForApi.cs
  8. 16
      src/Squidex.Domain.Apps.Entities/Contents/StatusForFrontend.cs
  9. 32
      src/Squidex.Domain.Apps.Entities/QueryContext.cs
  10. 5
      src/Squidex.Infrastructure/CollectionExtensions.cs
  11. 12
      src/Squidex/Areas/Api/Controllers/Contents/ContentsController.cs
  12. 5
      src/Squidex/app/features/content/pages/contents/contents-page.component.html
  13. 10
      src/Squidex/app/features/content/pages/contents/contents-page.component.ts
  14. 7
      src/Squidex/app/features/content/shared/contents-selector.component.html
  15. 8
      src/Squidex/app/features/content/shared/contents-selector.component.ts
  16. 14
      src/Squidex/app/shared/components/search-form.component.html
  17. 7
      src/Squidex/app/shared/components/search-form.component.scss
  18. 10
      src/Squidex/app/shared/components/search-form.component.ts
  19. 10
      src/Squidex/app/shared/services/contents.service.spec.ts
  20. 8
      src/Squidex/app/shared/services/contents.service.ts
  21. 32
      src/Squidex/app/shared/state/contents.state.ts
  22. 206
      tests/Squidex.Domain.Apps.Entities.Tests/Contents/ContentQueryServiceTests.cs
  23. 4
      tests/Squidex.Domain.Apps.Entities.Tests/Contents/GraphQL/GraphQLQueriesTests.cs

1
Squidex.ruleset

@ -76,6 +76,7 @@
<Rule Id="RECS0026" Action="Error" />
<Rule Id="RECS0145" Action="None" />
<Rule Id="RECS0129" Action="None" />
<Rule Id="RECS0070" Action="None" />
</Rules>
<Rules AnalyzerId="Roslyn.Core" RuleNamespace="Roslyn.Core">
<Rule Id="AD0001" Action="None" />

20
src/Squidex.Domain.Apps.Entities.MongoDb/Contents/MongoContentCollection.cs

@ -68,18 +68,18 @@ namespace Squidex.Domain.Apps.Entities.MongoDb.Contents
return "State_Contents";
}
public async Task<IResultList<IContentEntity>> QueryAsync(IAppEntity app, ISchemaEntity schema, Query query, List<Guid> ids, Status[] status, bool useDraft)
public async Task<IResultList<IContentEntity>> QueryAsync(ISchemaEntity schema, Query query, List<Guid> ids, Status[] status, bool inDraft, bool includeDraft = true)
{
try
{
query = query.AdjustToModel(schema.SchemaDef, useDraft);
query = query.AdjustToModel(schema.SchemaDef, inDraft);
var filter = query.ToFilter(schema.Id, ids, status);
var contentCount = Collection.Find(filter).CountDocumentsAsync();
var contentItems =
Collection.Find(filter)
.WithoutDraft(useDraft)
.WithoutDraft(includeDraft)
.ContentTake(query)
.ContentSkip(query)
.ContentSort(query)
@ -107,11 +107,11 @@ namespace Squidex.Domain.Apps.Entities.MongoDb.Contents
}
}
public async Task<List<(IContentEntity Content, ISchemaEntity Schema)>> QueryAsync(IAppEntity app, HashSet<Guid> ids, Status[] status, bool useDraft)
public async Task<List<(IContentEntity Content, ISchemaEntity Schema)>> QueryAsync(IAppEntity app, HashSet<Guid> ids, Status[] status, bool includeDraft)
{
var find = Collection.Find(FilterFactory.IdsByApp(app.Id, ids, status));
var contentItems = await find.WithoutDraft(useDraft).ToListAsync();
var contentItems = await find.WithoutDraft(includeDraft).ToListAsync();
var schemaIds = contentItems.Select(x => x.IndexedSchemaId).ToList();
var schemas = await Task.WhenAll(schemaIds.Select(x => appProvider.GetSchemaAsync(app.Id, x)));
@ -133,11 +133,11 @@ namespace Squidex.Domain.Apps.Entities.MongoDb.Contents
return result;
}
public async Task<IResultList<IContentEntity>> QueryAsync(IAppEntity app, ISchemaEntity schema, HashSet<Guid> ids, Status[] status, bool useDraft)
public async Task<IResultList<IContentEntity>> QueryAsync(ISchemaEntity schema, HashSet<Guid> ids, Status[] status, bool includeDraft)
{
var find = Collection.Find(FilterFactory.IdsBySchema(schema.Id, ids, status));
var contentItems = find.WithoutDraft(useDraft).ToListAsync();
var contentItems = find.WithoutDraft(includeDraft).ToListAsync();
var contentCount = find.CountDocumentsAsync();
await Task.WhenAll(contentItems, contentCount);
@ -150,11 +150,11 @@ namespace Squidex.Domain.Apps.Entities.MongoDb.Contents
return ResultList.Create<IContentEntity>(contentCount.Result, contentItems.Result);
}
public async Task<IContentEntity> FindContentAsync(IAppEntity app, ISchemaEntity schema, Guid id, Status[] status, bool useDraft)
public async Task<IContentEntity> FindContentAsync(ISchemaEntity schema, Guid id, Status[] status, bool includeDraft)
{
var find = Collection.Find(FilterFactory.Build(schema.Id, id, status));
var contentEntity = await find.WithoutDraft(useDraft).FirstOrDefaultAsync();
var contentEntity = await find.WithoutDraft(includeDraft).FirstOrDefaultAsync();
contentEntity?.ParseData(schema.SchemaDef, serializer);
@ -172,7 +172,7 @@ namespace Squidex.Domain.Apps.Entities.MongoDb.Contents
});
}
public async Task<IReadOnlyList<Guid>> QueryIdsAsync(Guid appId, ISchemaEntity schema, FilterNode filterNode)
public async Task<IReadOnlyList<Guid>> QueryIdsAsync(ISchemaEntity schema, FilterNode filterNode)
{
var filter = filterNode.AdjustToModel(schema.SchemaDef, true).ToFilter(schema.Id);

41
src/Squidex.Domain.Apps.Entities.MongoDb/Contents/MongoContentRepository.cs

@ -60,7 +60,7 @@ namespace Squidex.Domain.Apps.Entities.MongoDb.Contents
return contents.InitializeAsync(ct);
}
public async Task<IResultList<IContentEntity>> QueryAsync(IAppEntity app, ISchemaEntity schema, Status[] status, Query query)
public async Task<IResultList<IContentEntity>> QueryAsync(IAppEntity app, ISchemaEntity schema, Status[] status, bool inDraft, Query query, bool includeDraft = true)
{
Guard.NotNull(app, nameof(app));
Guard.NotNull(schema, nameof(schema));
@ -69,20 +69,18 @@ namespace Squidex.Domain.Apps.Entities.MongoDb.Contents
using (Profiler.TraceMethod<MongoContentRepository>("QueryAsyncByQuery"))
{
var useDraft = UseDraft(status);
var fullTextIds = await indexer.SearchAsync(query.FullText, app, schema.Id, useDraft ? Scope.Draft : Scope.Published);
var fullTextIds = await indexer.SearchAsync(query.FullText, app, schema.Id, inDraft ? Scope.Draft : Scope.Published);
if (fullTextIds?.Count == 0)
{
return ResultList.Create<IContentEntity>(0);
}
return await contents.QueryAsync(app, schema, query, fullTextIds, status, true);
return await contents.QueryAsync(schema, query, fullTextIds, status, inDraft, includeDraft);
}
}
public async Task<IResultList<IContentEntity>> QueryAsync(IAppEntity app, ISchemaEntity schema, Status[] status, HashSet<Guid> ids)
public async Task<IResultList<IContentEntity>> QueryAsync(IAppEntity app, ISchemaEntity schema, Status[] status, HashSet<Guid> ids, bool includeDraft = true)
{
Guard.NotNull(app, nameof(app));
Guard.NotNull(schema, nameof(schema));
@ -91,13 +89,11 @@ namespace Squidex.Domain.Apps.Entities.MongoDb.Contents
using (Profiler.TraceMethod<MongoContentRepository>("QueryAsyncByIds"))
{
var useDraft = UseDraft(status);
return await contents.QueryAsync(app, schema, ids, status, useDraft);
return await contents.QueryAsync(schema, ids, status, includeDraft);
}
}
public async Task<List<(IContentEntity Content, ISchemaEntity Schema)>> QueryAsync(IAppEntity app, Status[] status, HashSet<Guid> ids)
public async Task<List<(IContentEntity Content, ISchemaEntity Schema)>> QueryAsync(IAppEntity app, Status[] status, HashSet<Guid> ids, bool includeDraft = true)
{
Guard.NotNull(app, nameof(app));
Guard.NotNull(status, nameof(status));
@ -105,13 +101,11 @@ namespace Squidex.Domain.Apps.Entities.MongoDb.Contents
using (Profiler.TraceMethod<MongoContentRepository>("QueryAsyncByIdsWithoutSchema"))
{
var useDraft = UseDraft(status);
return await contents.QueryAsync(app, ids, status, useDraft);
return await contents.QueryAsync(app, ids, status, includeDraft);
}
}
public async Task<IContentEntity> FindContentAsync(IAppEntity app, ISchemaEntity schema, Status[] status, Guid id)
public async Task<IContentEntity> FindContentAsync(IAppEntity app, ISchemaEntity schema, Status[] status, Guid id, bool includeDraft = true)
{
Guard.NotNull(app, nameof(app));
Guard.NotNull(schema, nameof(schema));
@ -119,9 +113,7 @@ namespace Squidex.Domain.Apps.Entities.MongoDb.Contents
using (Profiler.TraceMethod<MongoContentRepository>())
{
var useDraft = UseDraft(status);
return await contents.FindContentAsync(app, schema, id, status, useDraft);
return await contents.FindContentAsync(schema, id, status, includeDraft);
}
}
@ -129,15 +121,7 @@ namespace Squidex.Domain.Apps.Entities.MongoDb.Contents
{
using (Profiler.TraceMethod<MongoContentRepository>())
{
return await contents.QueryIdsAsync(appId, await appProvider.GetSchemaAsync(appId, schemaId), filterNode);
}
}
public async Task<IReadOnlyList<Guid>> QueryIdsAsync(Guid appId)
{
using (Profiler.TraceMethod<MongoContentRepository>())
{
return await contents.QueryIdsAsync(appId);
return await contents.QueryIdsAsync(await appProvider.GetSchemaAsync(appId, schemaId), filterNode);
}
}
@ -158,10 +142,5 @@ namespace Squidex.Domain.Apps.Entities.MongoDb.Contents
{
return database.DropCollectionAsync("States_Contents_Archive");
}
private static bool UseDraft(Status[] status)
{
return status.Length != 1 || status[0] != Status.Published;
}
}
}

12
src/Squidex.Domain.Apps.Entities.MongoDb/Contents/Visitors/FilterFactory.cs

@ -42,14 +42,14 @@ namespace Squidex.Domain.Apps.Entities.MongoDb.Contents.Visitors
return query;
}
public static FilterNode AdjustToModel(this FilterNode filterNode, Schema schema, bool useDraft)
public static FilterNode AdjustToModel(this FilterNode filterNode, Schema schema, bool inDraft)
{
var pathConverter = PathConverter(schema, useDraft);
var pathConverter = PathConverter(schema, inDraft);
return filterNode.Accept(new AdaptionVisitor(pathConverter));
}
private static Func<IReadOnlyList<string>, IReadOnlyList<string>> PathConverter(Schema schema, bool useDraft)
private static Func<IReadOnlyList<string>, IReadOnlyList<string>> PathConverter(Schema schema, bool inDraft)
{
return propertyNames =>
{
@ -88,7 +88,7 @@ namespace Squidex.Domain.Apps.Entities.MongoDb.Contents.Visitors
{
if (result[0].Equals("Data", StringComparison.CurrentCultureIgnoreCase))
{
if (useDraft)
if (inDraft)
{
result[0] = "dd";
}
@ -122,9 +122,9 @@ namespace Squidex.Domain.Apps.Entities.MongoDb.Contents.Visitors
return cursor.Skip(query);
}
public static IFindFluent<MongoContentEntity, MongoContentEntity> WithoutDraft(this IFindFluent<MongoContentEntity, MongoContentEntity> cursor, bool useDraft)
public static IFindFluent<MongoContentEntity, MongoContentEntity> WithoutDraft(this IFindFluent<MongoContentEntity, MongoContentEntity> cursor, bool includeDraft)
{
return !useDraft ? cursor.Not(x => x.DataDraftByIds, x => x.IsDeleted) : cursor;
return !includeDraft ? cursor.Not(x => x.DataDraftByIds, x => x.IsDeleted) : cursor;
}
public static FilterDefinition<MongoContentEntity> Build(Guid schemaId, Guid id, Status[] status)

66
src/Squidex.Domain.Apps.Entities/Contents/ContentQueryService.cs

@ -35,8 +35,8 @@ namespace Squidex.Domain.Apps.Entities.Contents
{
private static readonly Status[] StatusAll = { Status.Archived, Status.Draft, Status.Published };
private static readonly Status[] StatusArchived = { Status.Archived };
private static readonly Status[] StatusPublished = { Status.Published };
private static readonly Status[] StatusDraftOrPublished = { Status.Draft, Status.Published };
private static readonly Status[] StatusPublishedOnly = { Status.Published };
private static readonly Status[] StatusPublishedDraft = { Status.Published, Status.Draft };
private readonly IContentRepository contentRepository;
private readonly IContentVersionLoader contentVersionLoader;
private readonly IAppProvider appProvider;
@ -125,14 +125,14 @@ namespace Squidex.Domain.Apps.Entities.Contents
if (query.Ids?.Count > 0)
{
contents = await contentRepository.QueryAsync(context.App, schema, status, new HashSet<Guid>(query.Ids));
contents = await QueryAsync(context, schema, query.Ids.ToHashSet(), status);
contents = SortSet(contents, query.Ids);
}
else
{
var parsedQuery = ParseQuery(context, query.ODataQuery, schema);
contents = await contentRepository.QueryAsync(context.App, schema, status, parsedQuery);
contents = await QueryAsync(context, schema, parsedQuery, status);
}
return Transform(context, schema, contents);
@ -151,7 +151,7 @@ namespace Squidex.Domain.Apps.Entities.Contents
if (ids?.Count > 0)
{
var contents = await contentRepository.QueryAsync(context.App, status, new HashSet<Guid>(ids));
var contents = await QueryAsync(context, ids, status);
var permissions = context.User.Permissions();
@ -217,7 +217,7 @@ namespace Squidex.Domain.Apps.Entities.Contents
result.Data = result.Data.ConvertName2Name(schema.SchemaDef, converters);
}
if (result.DataDraft != null && (context.Unpublished || context.IsFrontendClient))
if (result.DataDraft != null && (context.ApiStatus == StatusForApi.PublishedDraft || context.IsFrontendClient))
{
result.DataDraft = result.DataDraft.ConvertName2Name(schema.SchemaDef, converters);
}
@ -346,13 +346,13 @@ namespace Squidex.Domain.Apps.Entities.Contents
{
return StatusAll;
}
else if (context.Unpublished)
else if (context.ApiStatus == StatusForApi.PublishedDraft)
{
return StatusDraftOrPublished;
return StatusPublishedDraft;
}
else
{
return StatusPublished;
return StatusPublishedOnly;
}
}
@ -360,36 +360,56 @@ namespace Squidex.Domain.Apps.Entities.Contents
{
if (context.IsFrontendClient)
{
if (context.Archived)
switch (context.FrontendStatus)
{
return StatusArchived;
}
else
{
return StatusDraftOrPublished;
case StatusForFrontend.Archived:
return StatusArchived;
case StatusForFrontend.PublishedOnly:
return StatusPublishedOnly;
default:
return StatusPublishedDraft;
}
}
else
{
if (context.Unpublished)
{
return StatusDraftOrPublished;
}
else
switch (context.ApiStatus)
{
return StatusPublished;
case StatusForApi.PublishedDraft:
return StatusPublishedDraft;
default:
return StatusPublishedOnly;
}
}
}
private Task<List<(IContentEntity Content, ISchemaEntity Schema)>> QueryAsync(QueryContext context, IReadOnlyList<Guid> ids, Status[] status)
{
return contentRepository.QueryAsync(context.App, status, new HashSet<Guid>(ids), ShouldIncludeDraft(context));
}
private Task<IResultList<IContentEntity>> QueryAsync(QueryContext context, ISchemaEntity schema, Query query, Status[] status)
{
return contentRepository.QueryAsync(context.App, schema, status, context.IsFrontendClient, query, ShouldIncludeDraft(context));
}
private Task<IResultList<IContentEntity>> QueryAsync(QueryContext context, ISchemaEntity schema, HashSet<Guid> ids, Status[] status)
{
return contentRepository.QueryAsync(context.App, schema, status, ids, ShouldIncludeDraft(context));
}
private Task<IContentEntity> FindContentAsync(QueryContext context, Guid id, Status[] status, ISchemaEntity schema)
{
return contentRepository.FindContentAsync(context.App, schema, status, id, ShouldIncludeDraft(context));
}
private Task<IContentEntity> FindContentByVersionAsync(Guid id, long version)
{
return contentVersionLoader.LoadAsync(id, version);
}
private Task<IContentEntity> FindContentAsync(QueryContext context, Guid id, Status[] status, ISchemaEntity schema)
private static bool ShouldIncludeDraft(QueryContext context)
{
return contentRepository.FindContentAsync(context.App, schema, status, id);
return context.ApiStatus == StatusForApi.PublishedDraft || context.IsFrontendClient;
}
}
}

8
src/Squidex.Domain.Apps.Entities/Contents/Repositories/IContentRepository.cs

@ -19,15 +19,15 @@ namespace Squidex.Domain.Apps.Entities.Contents.Repositories
{
public interface IContentRepository
{
Task<List<(IContentEntity Content, ISchemaEntity Schema)>> QueryAsync(IAppEntity app, Status[] status, HashSet<Guid> ids);
Task<List<(IContentEntity Content, ISchemaEntity Schema)>> QueryAsync(IAppEntity app, Status[] status, HashSet<Guid> ids, bool includeDraft);
Task<IResultList<IContentEntity>> QueryAsync(IAppEntity app, ISchemaEntity schema, Status[] status, HashSet<Guid> ids);
Task<IResultList<IContentEntity>> QueryAsync(IAppEntity app, ISchemaEntity schema, Status[] status, HashSet<Guid> ids, bool includeDraft);
Task<IResultList<IContentEntity>> QueryAsync(IAppEntity app, ISchemaEntity schema, Status[] status, Query query);
Task<IResultList<IContentEntity>> QueryAsync(IAppEntity app, ISchemaEntity schema, Status[] status, bool inDraft, Query query, bool includeDraft);
Task<IReadOnlyList<Guid>> QueryIdsAsync(Guid appId, Guid schemaId, FilterNode filterNode);
Task<IContentEntity> FindContentAsync(IAppEntity app, ISchemaEntity schema, Status[] status, Guid id);
Task<IContentEntity> FindContentAsync(IAppEntity app, ISchemaEntity schema, Status[] status, Guid id, bool includeDraft);
Task QueryScheduledWithoutDataAsync(Instant now, Func<IContentEntity, Task> callback);
}

15
src/Squidex.Domain.Apps.Entities/Contents/StatusForApi.cs

@ -0,0 +1,15 @@
// ==========================================================================
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex UG (haftungsbeschraenkt)
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
namespace Squidex.Domain.Apps.Entities.Contents
{
public enum StatusForApi
{
PublishedOnly,
PublishedDraft,
}
}

16
src/Squidex.Domain.Apps.Entities/Contents/StatusForFrontend.cs

@ -0,0 +1,16 @@
// ==========================================================================
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex UG (haftungsbeschraenkt)
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
namespace Squidex.Domain.Apps.Entities.Contents
{
public enum StatusForFrontend
{
PublishedDraft,
PublishedOnly,
Archived
}
}

32
src/Squidex.Domain.Apps.Entities/QueryContext.cs

@ -9,6 +9,7 @@ using System;
using System.Collections.Generic;
using System.Security.Claims;
using Squidex.Domain.Apps.Entities.Apps;
using Squidex.Domain.Apps.Entities.Contents;
using Squidex.Infrastructure;
using Squidex.Infrastructure.Security;
@ -22,11 +23,11 @@ namespace Squidex.Domain.Apps.Entities
public IAppEntity App { get; private set; }
public bool Archived { get; private set; }
public bool Flatten { get; set; }
public bool Flatten { get; private set; }
public StatusForApi ApiStatus { get; private set; }
public bool Unpublished { get; set; }
public StatusForFrontend FrontendStatus { get; private set; }
public IReadOnlyCollection<string> AssetUrlsToResolve { get; private set; }
@ -41,19 +42,34 @@ namespace Squidex.Domain.Apps.Entities
return new QueryContext { App = app, User = user };
}
public QueryContext WithFlatten(bool flatten)
{
return Clone(c => c.Flatten = flatten);
}
public QueryContext WithUnpublished(bool unpublished)
{
return Clone(c => c.Unpublished = unpublished);
return WithApiStatus(unpublished ? StatusForApi.PublishedDraft : StatusForApi.PublishedOnly);
}
public QueryContext WithArchived(bool archived)
public QueryContext WithApiStatus(StatusForApi status)
{
return Clone(c => c.Archived = archived);
return Clone(c => c.ApiStatus = status);
}
public QueryContext WithFlatten(bool flatten)
public QueryContext WithFrontendStatus(StatusForFrontend status)
{
return Clone(c => c.Flatten = flatten);
return Clone(c => c.FrontendStatus = status);
}
public QueryContext WithFrontendStatus(string status)
{
if (status != null && Enum.TryParse<StatusForFrontend>(status, out var result))
{
return WithFrontendStatus(result);
}
return this;
}
public QueryContext WithAssetUrlsToResolve(IEnumerable<string> fieldNames)

5
src/Squidex.Infrastructure/CollectionExtensions.cs

@ -20,6 +20,11 @@ namespace Squidex.Infrastructure
return enumerable.OrderBy(x => random.Next()).ToList();
}
public static HashSet<T> ToHashSet<T>(this IEnumerable<T> enumerable)
{
return new HashSet<T>(enumerable);
}
public static IEnumerable<T> OrEmpty<T>(this IEnumerable<T> source)
{
return source ?? Enumerable.Empty<T>();

12
src/Squidex/Areas/Api/Controllers/Contents/ContentsController.cs

@ -111,7 +111,7 @@ namespace Squidex.Areas.Api.Controllers.Contents
/// </summary>
/// <param name="app">The name of the app.</param>
/// <param name="ids">The optional ids of the content to fetch.</param>
/// <param name="archived">Indicates whether to query content items from the archive.</param>
/// <param name="status">The requested status, only for frontend client.</param>
/// <returns>
/// 200 => Contents retrieved.
/// 404 => App not found.
@ -123,9 +123,9 @@ namespace Squidex.Areas.Api.Controllers.Contents
[Route("content/{app}/")]
[ApiPermission]
[ApiCosts(1)]
public async Task<IActionResult> GetAllContents(string app, [FromQuery] string ids, [FromQuery] bool archived = false)
public async Task<IActionResult> GetAllContents(string app, [FromQuery] string ids, [FromQuery] string status = null)
{
var context = Context().WithArchived(archived);
var context = Context().WithFrontendStatus(status);
var result = await contentQuery.QueryAsync(context, Q.Empty.WithIds(ids).Ids);
@ -151,7 +151,7 @@ namespace Squidex.Areas.Api.Controllers.Contents
/// <param name="app">The name of the app.</param>
/// <param name="name">The name of the schema.</param>
/// <param name="ids">The optional ids of the content to fetch.</param>
/// <param name="archived">Indicates whether to query content items from the archive.</param>
/// <param name="status">The requested status, only for frontend client.</param>
/// <returns>
/// 200 => Contents retrieved.
/// 404 => Schema or app not found.
@ -163,9 +163,9 @@ namespace Squidex.Areas.Api.Controllers.Contents
[Route("content/{app}/{name}/")]
[ApiPermission]
[ApiCosts(1)]
public async Task<IActionResult> GetContents(string app, string name, [FromQuery] string ids = null, [FromQuery] bool archived = false)
public async Task<IActionResult> GetContents(string app, string name, [FromQuery] string ids = null, [FromQuery] string status = null)
{
var context = Context().WithArchived(archived);
var context = Context().WithFrontendStatus(status);
var result = await contentQuery.QueryAsync(context, name, Q.Empty.WithIds(ids).WithODataQuery(Request.QueryString.ToString()));

5
src/Squidex/app/features/content/pages/contents/contents-page.component.html

@ -25,8 +25,9 @@
[filter]="filter"
(querySubmit)="search()"
[queries]="schemaQueries"
(archivedChange)="goArchive($event)"
[archived]="contentsState.isArchive | async"
(statusChange)="filterStatus($event)"
[status]="contentsState.status | async"
[statuses]="statuses"
expandable="true"
enableArchive="true"
enableShortcut="true">

10
src/Squidex/app/features/content/pages/contents/contents-page.component.ts

@ -11,7 +11,9 @@ import { onErrorResumeNext, switchMap, tap } from 'rxjs/operators';
import {
AppLanguageDto,
AppsState,
CONTENT_STATUSES,
ContentDto,
ContentQueryStatus,
ContentsState,
FilterState,
ImmutableArray,
@ -45,6 +47,8 @@ export class ContentsPageComponent extends ResourceOwner implements OnInit {
public canUnpublish = false;
public canPublish = false;
public statuses = CONTENT_STATUSES;
public language: AppLanguageDto;
public languages: ImmutableArray<AppLanguageDto>;
@ -77,7 +81,7 @@ export class ContentsPageComponent extends ResourceOwner implements OnInit {
this.schema = schema!;
this.schemaQueries = new Queries(this.uiState, `schemas.${this.schema.name}`);
this.contentsState.init();
this.contentsState.load();
}));
this.own(
@ -164,8 +168,8 @@ export class ContentsPageComponent extends ResourceOwner implements OnInit {
.subscribe();
}
public goArchive(isArchive: boolean) {
this.contentsState.goArchive(isArchive);
public filterStatus(status: ContentQueryStatus) {
this.contentsState.filterStatus(status);
}
public goPrev() {

7
src/Squidex/app/features/content/shared/contents-selector.component.html

@ -11,7 +11,12 @@
</button>
</div>
<div class="col pl-1">
<sqx-search-form formClass="form" placeholder="Search for content" fieldExample="data/[MY_FIELD]/iv" [filter]="filter" (querySubmit)="search()"
<sqx-search-form formClass="form" placeholder="Search for content" fieldExample="data/[MY_FIELD]/iv"
[filter]="filter"
(statusChange)="filterStatus($event)"
[status]="contentsState.status | async"
[statuses]="statuses"
(querySubmit)="search()"
expandable="true">
</sqx-search-form>
</div>

8
src/Squidex/app/features/content/shared/contents-selector.component.ts

@ -8,7 +8,9 @@
import { Component, EventEmitter, Input, OnInit, Output } from '@angular/core';
import {
CONTENT_STATUSES,
ContentDto,
ContentQueryStatus,
FilterState,
LanguageDto,
ManualContentsState,
@ -46,6 +48,8 @@ export class ContentsSelectorComponent implements OnInit {
public filter = new FilterState();
public statuses = CONTENT_STATUSES;
public selectedItems: { [id: string]: ContentDto; } = {};
public selectionCount = 0;
@ -70,6 +74,10 @@ export class ContentsSelectorComponent implements OnInit {
this.contentsState.search(this.filter.apiFilter);
}
public filterStatus(status: ContentQueryStatus) {
this.contentsState.filterStatus(status);
}
public goNext() {
this.contentsState.goNext();
}

14
src/Squidex/app/shared/components/search-form.component.html

@ -69,11 +69,15 @@
</div>
</div>
<div class="form-check" *ngIf="enableArchive">
<input class="form-check-input" type="checkbox" id="archivedItems" [ngModel]="archived" (ngModelChange)="archivedChange.emit($event)" />
<label class="form-check-label" for="archivedItems">
Archived items
</label>
<div class="form-group row" *ngIf="statuses">
<div class="col-10 offset-2">
<div class="form-check" *ngFor="let key of statuses | sqxKeys">
<input class="form-check-input" type="radio" name="status" id="status_{{key}}" [value]="key" [ngModel]="status" (ngModelChange)="statusChange.emit($event)" />
<label class="form-check-label" for="status_{{key}}">
{{statuses[key]}}
</label>
</div>
</div>
</div>
<div class="link">

7
src/Squidex/app/shared/components/search-form.component.scss

@ -19,13 +19,6 @@
max-width: 25rem;
}
.form-check {
margin-top: 1rem;
font-size: 1rem;
font-weight: normal;
text-align: right;
}
.save-search {
@include absolute(8px, 24px, auto, auto);
color: $color-border-dark !important;

10
src/Squidex/app/shared/components/search-form.component.ts

@ -40,17 +40,17 @@ export class SearchFormComponent implements OnInit {
public filter: FilterState;
@Input()
public archived = false;
public statuses: { [status: string]: string };
@Input()
public status: string;
@Output()
public archivedChange = new EventEmitter<boolean>();
public statusChange = new EventEmitter<string>();
@Input()
public schemaName = '';
@Input()
public enableArchive = false;
@Input()
public enableShortcut = false;

10
src/Squidex/app/shared/services/contents.service.spec.ts

@ -45,11 +45,11 @@ describe('ContentsService', () => {
let contents: ContentsDto;
contentsService.getContents('my-app', 'my-schema', 17, 13, undefined, undefined, true).subscribe(result => {
contentsService.getContents('my-app', 'my-schema', 17, 13, undefined, undefined, 'Archived').subscribe(result => {
contents = result;
});
const req = httpMock.expectOne('http://service/p/api/content/my-app/my-schema?$top=17&$skip=13&archived=true');
const req = httpMock.expectOne('http://service/p/api/content/my-app/my-schema?$top=17&$skip=13&status=Archived');
expect(req.request.method).toEqual('GET');
expect(req.request.headers.get('If-Match')).toBeNull();
@ -114,7 +114,7 @@ describe('ContentsService', () => {
contentsService.getContents('my-app', 'my-schema', 17, 13, 'my-query').subscribe();
const req = httpMock.expectOne('http://service/p/api/content/my-app/my-schema?$search="my-query"&$top=17&$skip=13');
const req = httpMock.expectOne('http://service/p/api/content/my-app/my-schema?$search="my-query"&$top=17&$skip=13&status=PublishedDraft');
expect(req.request.method).toEqual('GET');
expect(req.request.headers.get('If-Match')).toBeNull();
@ -127,7 +127,7 @@ describe('ContentsService', () => {
contentsService.getContents('my-app', 'my-schema', 17, 13, undefined, ['id1', 'id2']).subscribe();
const req = httpMock.expectOne('http://service/p/api/content/my-app/my-schema?$top=17&$skip=13&ids=id1,id2');
const req = httpMock.expectOne('http://service/p/api/content/my-app/my-schema?$top=17&$skip=13&ids=id1,id2&status=PublishedDraft');
expect(req.request.method).toEqual('GET');
expect(req.request.headers.get('If-Match')).toBeNull();
@ -140,7 +140,7 @@ describe('ContentsService', () => {
contentsService.getContents('my-app', 'my-schema', 17, 13, '$filter=my-filter').subscribe();
const req = httpMock.expectOne('http://service/p/api/content/my-app/my-schema?$filter=my-filter&$top=17&$skip=13');
const req = httpMock.expectOne('http://service/p/api/content/my-app/my-schema?$filter=my-filter&$top=17&$skip=13&status=PublishedDraft');
expect(req.request.method).toEqual('GET');
expect(req.request.headers.get('If-Match')).toBeNull();

8
src/Squidex/app/shared/services/contents.service.ts

@ -57,6 +57,8 @@ export class ContentDto extends Model<ContentDto> {
}
}
export type ContentQueryStatus = 'Archived' | 'PublishedOnly' | 'PublishedDraft';
@Injectable()
export class ContentsService {
constructor(
@ -66,7 +68,7 @@ export class ContentsService {
) {
}
public getContents(appName: string, schemaName: string, take: number, skip: number, query?: string, ids?: string[], archived = false): Observable<ContentsDto> {
public getContents(appName: string, schemaName: string, take: number, skip: number, query?: string, ids?: string[], status: ContentQueryStatus = 'PublishedDraft'): Observable<ContentsDto> {
const queryParts: string[] = [];
if (query && query.length > 0) {
@ -91,8 +93,8 @@ export class ContentsService {
queryParts.push(`ids=${ids.join(',')}`);
}
if (archived) {
queryParts.push('archived=true');
if (status) {
queryParts.push(`status=${status}`);
}
const fullQuery = queryParts.join('&');

32
src/Squidex/app/shared/state/contents.state.ts

@ -26,7 +26,7 @@ import { SchemaDto } from './../services/schemas.service';
import { AppsState } from './apps.state';
import { SchemasState } from './schemas.state';
import { ContentDto, ContentsService, ScheduleDto } from './../services/contents.service';
import { ContentDto, ContentQueryStatus, ContentsService, ScheduleDto } from './../services/contents.service';
interface Snapshot {
// The current comments.
@ -41,13 +41,19 @@ interface Snapshot {
// Indicates if the contents are loaded.
isLoaded?: boolean;
// Indicates if the archive is shown.
isArchive?: boolean;
// Indicates which status is shown.
status: ContentQueryStatus;
// The selected content.
selectedContent?: ContentDto | null;
}
export const CONTENT_STATUSES = {
'PublishedDraft': 'Published and Drafts (Default)',
'PublishedOnly': 'Published only',
'Archived': 'Archived'
};
function sameContent(lhs: ContentDto, rhs?: ContentDto): boolean {
return lhs === rhs || (!!lhs && !!rhs && lhs.id === rhs.id && lhs.version === rhs.version);
}
@ -74,7 +80,11 @@ export abstract class ContentsStateBase extends State<Snapshot> {
distinctUntilChanged());
public isArchive =
this.changes.pipe(map(x => !!x.isArchive),
this.changes.pipe(map(x => x.status === 'Archived'),
distinctUntilChanged());
public status =
this.changes.pipe(map(x => x.status),
distinctUntilChanged());
constructor(
@ -83,7 +93,7 @@ export abstract class ContentsStateBase extends State<Snapshot> {
private readonly contentsService: ContentsService,
private readonly dialogs: DialogService
) {
super({ contents: ImmutableArray.of(), contentsPager: new Pager(0) });
super({ contents: ImmutableArray.of(), contentsPager: new Pager(0), status: 'PublishedDraft' });
}
public select(id: string | null): Observable<ContentDto | null> {
@ -127,7 +137,7 @@ export abstract class ContentsStateBase extends State<Snapshot> {
this.snapshot.contentsPager.pageSize,
this.snapshot.contentsPager.skip,
this.snapshot.contentsQuery, undefined,
this.snapshot.isArchive).pipe(
this.snapshot.status).pipe(
tap(({ total, items }) => {
if (isReload) {
this.dialogs.notifyInfo('Contents reloaded.');
@ -288,14 +298,8 @@ export abstract class ContentsStateBase extends State<Snapshot> {
}
}
public goArchive(isArchive: boolean): Observable<any> {
this.next(s => ({ ...s, contentsPager: new Pager(0), contentsQuery: undefined, isArchive }));
return this.loadInternal();
}
public init(): Observable<any> {
this.next(s => ({ contents: ImmutableArray.of(), contentsPager: new Pager(0) }));
public filterStatus(status: ContentQueryStatus): Observable<any> {
this.next(s => ({ ...s, contentsPager: new Pager(0), contentsQuery: undefined, status }));
return this.loadInternal();
}

206
tests/Squidex.Domain.Apps.Entities.Tests/Contents/ContentQueryServiceTests.cs

@ -13,7 +13,6 @@ using System.Threading.Tasks;
using FakeItEasy;
using Microsoft.Extensions.Caching.Memory;
using Microsoft.Extensions.Options;
using Microsoft.OData;
using Squidex.Domain.Apps.Core.Apps;
using Squidex.Domain.Apps.Core.Contents;
using Squidex.Domain.Apps.Core.ConvertContent;
@ -96,7 +95,7 @@ namespace Squidex.Domain.Apps.Entities.Contents
[Fact]
public async Task Should_return_schema_from_id_if_string_is_guid()
{
SetupSchema();
SetupSchemaFound();
var result = await sut.GetSchemaAsync(context, schemaId.Name);
@ -106,7 +105,7 @@ namespace Squidex.Domain.Apps.Entities.Contents
[Fact]
public async Task Should_return_schema_from_name_if_string_not_guid()
{
SetupSchema();
SetupSchemaFound();
var result = await sut.GetSchemaAsync(context, schemaId.Name);
@ -136,28 +135,30 @@ namespace Squidex.Domain.Apps.Entities.Contents
[Fact]
public async Task Should_apply_default_page_size()
{
SetupClaims();
SetupSchema();
SetupClaims(isFrontend: false);
SetupSchemaFound();
var query = Q.Empty;
await sut.QueryAsync(context, schemaId.Name, query);
A.CallTo(() => contentRepository.QueryAsync(app, schema, A<Status[]>.That.Is(Status.Published), A<Query>.That.Is("Take: 30; Sort: lastModified Descending")))
A.CallTo(() => contentRepository.QueryAsync(app, schema, A<Status[]>.That.Is(Status.Published), false, A<Query>.That.Is("Take: 30; Sort: lastModified Descending"), false))
.MustHaveHappened();
}
[Fact]
public async Task Should_limit_number_of_contents()
{
SetupClaims();
SetupSchema();
var status = new[] { Status.Published };
SetupClaims(isFrontend: false);
SetupSchemaFound();
var query = Q.Empty.WithODataQuery("$top=300&$skip=20");
await sut.QueryAsync(context, schemaId.Name, query);
A.CallTo(() => contentRepository.QueryAsync(app, schema, A<Status[]>.That.Is(Status.Published), A<Query>.That.Is("Skip: 20; Take: 200; Sort: lastModified Descending")))
A.CallTo(() => contentRepository.QueryAsync(app, schema, A<Status[]>.That.Is(status), false, A<Query>.That.Is("Skip: 20; Take: 200; Sort: lastModified Descending"), false))
.MustHaveHappened();
}
@ -165,7 +166,7 @@ namespace Squidex.Domain.Apps.Entities.Contents
public async Task Should_throw_for_single_content_if_no_permission()
{
SetupClaims(false, false);
SetupSchema();
SetupSchemaFound();
var ctx = context;
@ -175,11 +176,11 @@ namespace Squidex.Domain.Apps.Entities.Contents
[Fact]
public async Task Should_throw_404_for_single_content_if_content_not_found()
{
SetupClaims();
SetupSchema();
var status = new[] { Status.Published };
A.CallTo(() => contentRepository.FindContentAsync(app, schema, A<Status[]>.That.Is(Status.Published), contentId))
.Returns((IContentEntity)null);
SetupClaims(isFrontend: false);
SetupSchemaFound();
SetupContent(status, null, includeDraft: false);
var ctx = context;
@ -188,30 +189,29 @@ namespace Squidex.Domain.Apps.Entities.Contents
public static IEnumerable<object[]> SingleDataFrontend = new[]
{
new object[] { true, new[] { Status.Archived, Status.Draft, Status.Published } },
new object[] { false, new[] { Status.Archived, Status.Draft, Status.Published } }
new object[] { StatusForFrontend.PublishedOnly, new[] { Status.Archived, Status.Draft, Status.Published } },
new object[] { StatusForFrontend.PublishedDraft, new[] { Status.Archived, Status.Draft, Status.Published } },
new object[] { StatusForFrontend.Archived, new[] { Status.Archived, Status.Draft, Status.Published } },
};
public static IEnumerable<object[]> SingleDataApi = new[]
{
new object[] { true, new[] { Status.Draft, Status.Published } },
new object[] { false, new[] { Status.Published } }
new object[] { StatusForApi.PublishedOnly, 0, new[] { Status.Published } },
new object[] { StatusForApi.PublishedDraft, 1, new[] { Status.Published, Status.Draft } }
};
[Theory]
[MemberData(nameof(SingleDataFrontend))]
public async Task Should_return_single_content_for_frontend_without_transform(bool unpublished, params Status[] status)
public async Task Should_return_single_content_for_frontend_without_transform(StatusForFrontend request, Status[] status)
{
var content = CreateContent(contentId);
SetupClaims(true);
SetupSchema();
SetupClaims(isFrontend: true);
SetupSchemaFound();
SetupScripting(contentId);
SetupContent(status, content, includeDraft: true);
A.CallTo(() => contentRepository.FindContentAsync(app, schema, A<Status[]>.That.Is(status), contentId))
.Returns(content);
var ctx = context.WithUnpublished(unpublished);
var ctx = context.WithFrontendStatus(request);
var result = await sut.FindContentAsync(ctx, schemaId.Name, contentId);
@ -224,18 +224,16 @@ namespace Squidex.Domain.Apps.Entities.Contents
[Theory]
[MemberData(nameof(SingleDataApi))]
public async Task Should_return_single_content_for_api_with_transform(bool unpublished, params Status[] status)
public async Task Should_return_single_content_for_api_with_transform(StatusForApi request, int includeDraft, Status[] status)
{
var content = CreateContent(contentId);
SetupClaims();
SetupSchema();
SetupClaims(isFrontend: false);
SetupSchemaFound();
SetupScripting(contentId);
SetupContent(status, content, includeDraft == 1);
A.CallTo(() => contentRepository.FindContentAsync(app, schema, A<Status[]>.That.Is(status), contentId))
.Returns(content);
var ctx = context.WithUnpublished(unpublished);
var ctx = context.WithApiStatus(request);
var result = await sut.FindContentAsync(ctx, schemaId.Name, contentId);
@ -252,7 +250,7 @@ namespace Squidex.Domain.Apps.Entities.Contents
var content = CreateContent(contentId);
SetupClaims(true);
SetupSchema();
SetupSchemaFound();
SetupScripting(contentId);
A.CallTo(() => contentVersionLoader.LoadAsync(contentId, 10))
@ -266,27 +264,11 @@ namespace Squidex.Domain.Apps.Entities.Contents
Assert.Equal(content.Id, result.Id);
}
public static IEnumerable<object[]> ManyDataFrontend = new[]
{
new object[] { true, true, new[] { Status.Archived } },
new object[] { true, false, new[] { Status.Archived } },
new object[] { false, true, new[] { Status.Draft, Status.Published } },
new object[] { false, false, new[] { Status.Draft, Status.Published } }
};
public static IEnumerable<object[]> ManyDataApi = new[]
{
new object[] { true, true, new[] { Status.Draft, Status.Published } },
new object[] { false, true, new[] { Status.Draft, Status.Published } },
new object[] { false, false, new[] { Status.Published } },
new object[] { true, false, new[] { Status.Published } }
};
[Fact]
public async Task Should_throw_for_query_if_no_permission()
{
SetupClaims(false, false);
SetupSchema();
SetupSchemaFound();
var ctx = context;
@ -295,18 +277,18 @@ namespace Squidex.Domain.Apps.Entities.Contents
[Theory]
[MemberData(nameof(ManyDataFrontend))]
public async Task Should_query_contents_by_query_for_frontend_without_transform(bool archive, bool unpublished, params Status[] status)
public async Task Should_query_contents_by_query_for_frontend_without_transform(StatusForFrontend request, Status[] status)
{
const int count = 5, total = 200;
var content = CreateContent(contentId);
SetupClaims(true);
SetupSchema();
SetupClaims(isFrontend: true);
SetupSchemaFound();
SetupScripting(contentId);
SetupContents(status, count, total, content);
SetupContents(status, count, total, content, inDraft: true, includeDraft: true);
var ctx = context.WithArchived(archive).WithUnpublished(unpublished);
var ctx = context.WithFrontendStatus(request);
var result = await sut.QueryAsync(ctx, schemaId.Name, Q.Empty);
@ -321,18 +303,18 @@ namespace Squidex.Domain.Apps.Entities.Contents
[Theory]
[MemberData(nameof(ManyDataApi))]
public async Task Should_query_contents_by_query_for_api_and_transform(bool archive, bool unpublished, params Status[] status)
public async Task Should_query_contents_by_query_for_api_and_transform(StatusForApi request, int includeDraft, Status[] status)
{
const int count = 5, total = 200;
var content = CreateContent(contentId);
SetupClaims();
SetupSchema();
SetupClaims(isFrontend: false);
SetupSchemaFound();
SetupScripting(contentId);
SetupContents(status, count, total, content);
SetupContents(status, count, total, content, inDraft: false, includeDraft == 1);
var ctx = context.WithArchived(archive).WithUnpublished(unpublished);
var ctx = context.WithApiStatus(request);
var result = await sut.QueryAsync(ctx, schemaId.Name, Q.Empty);
@ -348,44 +330,41 @@ namespace Squidex.Domain.Apps.Entities.Contents
[Fact]
public Task Should_throw_if_query_is_invalid()
{
SetupClaims();
SetupSchema();
SetupClaims(isFrontend: false);
SetupSchemaFound();
var query = Q.Empty.WithODataQuery("$filter=invalid");
return Assert.ThrowsAsync<ValidationException>(() => sut.QueryAsync(context, schemaId.Name, query));
}
public static IEnumerable<object[]> ManyIdDataFrontend = new[]
public static IEnumerable<object[]> ManyDataFrontend = new[]
{
new object[] { true, true, new[] { Status.Archived } },
new object[] { true, false, new[] { Status.Archived } },
new object[] { false, true, new[] { Status.Draft, Status.Published } },
new object[] { false, false, new[] { Status.Draft, Status.Published } }
new object[] { StatusForFrontend.PublishedOnly, new[] { Status.Published } },
new object[] { StatusForFrontend.PublishedDraft, new[] { Status.Published, Status.Draft } },
new object[] { StatusForFrontend.Archived, new[] { Status.Archived } }
};
public static IEnumerable<object[]> ManyIdDataApi = new[]
public static IEnumerable<object[]> ManyDataApi = new[]
{
new object[] { true, true, new[] { Status.Draft, Status.Published } },
new object[] { false, true, new[] { Status.Draft, Status.Published } },
new object[] { false, false, new[] { Status.Published } },
new object[] { true, false, new[] { Status.Published } }
new object[] { StatusForApi.PublishedOnly, 0, new[] { Status.Published } },
new object[] { StatusForApi.PublishedDraft, 1, new[] { Status.Published, Status.Draft } }
};
[Theory]
[MemberData(nameof(ManyIdDataFrontend))]
public async Task Should_query_contents_by_id_for_frontend_and_transform(bool archive, bool unpublished, params Status[] status)
[MemberData(nameof(ManyDataFrontend))]
public async Task Should_query_contents_by_id_for_frontend_and_transform(StatusForFrontend request, Status[] status)
{
const int count = 5, total = 200;
var ids = Enumerable.Range(0, count).Select(x => Guid.NewGuid()).ToList();
SetupClaims(true);
SetupContents(status, total, ids);
SetupSchema();
SetupClaims(isFrontend: true);
SetupSchemaFound();
SetupScripting(ids.ToArray());
SetupContents(status, total, ids, includeDraft: true);
var ctx = context.WithArchived(archive).WithUnpublished(unpublished);
var ctx = context.WithFrontendStatus(request);
var result = await sut.QueryAsync(ctx, schemaId.Name, Q.Empty.WithIds(ids));
@ -397,19 +376,19 @@ namespace Squidex.Domain.Apps.Entities.Contents
}
[Theory]
[MemberData(nameof(ManyIdDataApi))]
public async Task Should_query_contents_by_id_for_api_and_transform(bool archive, bool unpublished, params Status[] status)
[MemberData(nameof(ManyDataApi))]
public async Task Should_query_contents_by_id_for_api_and_transform(StatusForApi request, int includeDraft, Status[] status)
{
const int count = 5, total = 200;
var ids = Enumerable.Range(0, count).Select(x => Guid.NewGuid()).ToList();
SetupClaims();
SetupContents(status, total, ids);
SetupSchema();
SetupClaims(isFrontend: false);
SetupSchemaFound();
SetupScripting(ids.ToArray());
SetupContents(status, total, ids, includeDraft == 1);
var ctx = context.WithArchived(archive).WithUnpublished(unpublished);
var ctx = context.WithApiStatus(request);
var result = await sut.QueryAsync(ctx, schemaId.Name, Q.Empty.WithIds(ids));
@ -421,19 +400,19 @@ namespace Squidex.Domain.Apps.Entities.Contents
}
[Theory]
[MemberData(nameof(ManyIdDataFrontend))]
public async Task Should_query_all_contents_by_id_for_frontend_and_transform(bool archive, bool unpublished, params Status[] status)
[MemberData(nameof(ManyDataFrontend))]
public async Task Should_query_all_contents_by_id_for_frontend_and_transform(StatusForFrontend request, Status[] status)
{
const int count = 5;
var ids = Enumerable.Range(0, count).Select(x => Guid.NewGuid()).ToList();
SetupClaims(true);
SetupContents(status, ids);
SetupSchema();
SetupClaims(isFrontend: true);
SetupSchemaFound();
SetupScripting(ids.ToArray());
SetupContents(status, ids, includeDraft: true);
var ctx = context.WithArchived(archive).WithUnpublished(unpublished);
var ctx = context.WithFrontendStatus(request);
var result = await sut.QueryAsync(ctx, ids);
@ -444,19 +423,19 @@ namespace Squidex.Domain.Apps.Entities.Contents
}
[Theory]
[MemberData(nameof(ManyIdDataApi))]
public async Task Should_query_all_contents_by_id_for_api_and_transform(bool archive, bool unpublished, params Status[] status)
[MemberData(nameof(ManyDataApi))]
public async Task Should_query_all_contents_by_id_for_api_and_transform(StatusForApi request, int includeDraft, Status[] status)
{
const int count = 5;
var ids = Enumerable.Range(0, count).Select(x => Guid.NewGuid()).ToList();
SetupClaims();
SetupContents(status, ids);
SetupSchema();
SetupClaims(isFrontend: false);
SetupSchemaFound();
SetupScripting(ids.ToArray());
SetupContents(status, ids, includeDraft == 1);
var ctx = context.WithArchived(archive).WithUnpublished(unpublished);
var ctx = context.WithApiStatus(request);
var result = await sut.QueryAsync(ctx, ids);
@ -471,9 +450,10 @@ namespace Squidex.Domain.Apps.Entities.Contents
{
var ids = Enumerable.Range(0, 1).Select(x => Guid.NewGuid()).ToList();
SetupClaims(false, false);
SetupContents(new Status[0], ids);
SetupSchema();
SetupClaims(isFrontend: false, allowSchema: false);
SetupSchemaFound();
SetupScripting(ids.ToArray());
SetupContents(new Status[0], ids, includeDraft: false);
var ctx = context;
@ -487,8 +467,8 @@ namespace Squidex.Domain.Apps.Entities.Contents
{
var ids = new List<Guid>();
SetupClaims(false, false);
SetupSchema();
SetupClaims(isFrontend: false, allowSchema: false);
SetupSchemaFound();
var ctx = context;
@ -496,11 +476,11 @@ namespace Squidex.Domain.Apps.Entities.Contents
Assert.Empty(result);
A.CallTo(() => contentRepository.QueryAsync(app, A<Status[]>.Ignored, A<HashSet<Guid>>.Ignored))
A.CallTo(() => contentRepository.QueryAsync(app, A<Status[]>.Ignored, A<HashSet<Guid>>.Ignored, A<bool>.Ignored))
.MustNotHaveHappened();
}
private void SetupClaims(bool isFrontend = false, bool allowSchema = true)
private void SetupClaims(bool isFrontend, bool allowSchema = true)
{
if (isFrontend)
{
@ -522,25 +502,31 @@ namespace Squidex.Domain.Apps.Entities.Contents
}
}
private void SetupContents(Status[] status, int count, int total, IContentEntity content)
private void SetupContents(Status[] status, int count, int total, IContentEntity content, bool inDraft, bool includeDraft)
{
A.CallTo(() => contentRepository.QueryAsync(app, schema, A<Status[]>.That.Is(status), A<Query>.Ignored))
A.CallTo(() => contentRepository.QueryAsync(app, schema, A<Status[]>.That.Is(status), inDraft, A<Query>.Ignored, includeDraft))
.Returns(ResultList.Create(total, Enumerable.Repeat(content, count)));
}
private void SetupContents(Status[] status, int total, List<Guid> ids)
private void SetupContents(Status[] status, int total, List<Guid> ids, bool includeDraft)
{
A.CallTo(() => contentRepository.QueryAsync(app, schema, A<Status[]>.That.Is(status), A<HashSet<Guid>>.Ignored))
A.CallTo(() => contentRepository.QueryAsync(app, schema, A<Status[]>.That.Is(status), A<HashSet<Guid>>.Ignored, includeDraft))
.Returns(ResultList.Create(total, ids.Select(x => CreateContent(x)).Shuffle()));
}
private void SetupContents(Status[] status, List<Guid> ids)
private void SetupContents(Status[] status, List<Guid> ids, bool includeDraft)
{
A.CallTo(() => contentRepository.QueryAsync(app, A<Status[]>.That.Is(status), A<HashSet<Guid>>.Ignored))
A.CallTo(() => contentRepository.QueryAsync(app, A<Status[]>.That.Is(status), A<HashSet<Guid>>.Ignored, includeDraft))
.Returns(ids.Select(x => (CreateContent(x), schema)).ToList());
}
private void SetupSchema()
private void SetupContent(Status[] status, IContentEntity content, bool includeDraft)
{
A.CallTo(() => contentRepository.FindContentAsync(app, schema, A<Status[]>.That.Is(status), contentId, includeDraft))
.Returns(content);
}
private void SetupSchemaFound()
{
A.CallTo(() => appProvider.GetSchemaAsync(appId.Id, schemaId.Name))
.Returns(schema);

4
tests/Squidex.Domain.Apps.Entities.Tests/Contents/GraphQL/GraphQLQueriesTests.cs

@ -930,12 +930,12 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL
private QueryContext MatchsAssetContext()
{
return A<QueryContext>.That.Matches(x => x.App == app && x.User == user && !x.Archived);
return A<QueryContext>.That.Matches(x => x.App == app && x.User == user);
}
private QueryContext MatchsContentContext()
{
return A<QueryContext>.That.Matches(x => x.App == app && x.User == user && !x.Archived);
return A<QueryContext>.That.Matches(x => x.App == app && x.User == user);
}
}
}

Loading…
Cancel
Save