From 2996e31ca28de62fdb537acdf020df802e513d0c Mon Sep 17 00:00:00 2001 From: Sebastian Stehle Date: Thu, 20 Jun 2024 20:51:00 +0200 Subject: [PATCH] Not filter in UI. (#1101) --- .../EventSourcing/EventCommit.cs | 4 +- .../Plugins/PluginManager.cs | 2 +- .../Queries/Json/JsonFilterVisitor.cs | 2 +- .../Queries/QueryModel.cs | 50 ++++++------ .../Reflection/TypeConfig.cs | 2 +- .../Validation/ValidationException.cs | 2 +- .../Contents/Models/ContentsDto.cs | 1 - .../Schemas/Models/QueryModelDto.cs | 42 ++++++++++ .../Controllers/Schemas/SchemasController.cs | 10 ++- .../{Contents/Models => }/StatusInfoDto.cs | 2 +- backend/src/Squidex/Squidex.csproj | 2 +- .../MongoDb/MongoQueryTests.cs | 24 ++++++ frontend/package-lock.json | 10 +-- frontend/package.json | 2 +- .../contents/contents-page.component.html | 3 +- .../content-selector.component.html | 3 +- .../queries/filter-comparison.component.html | 45 ++++++++--- .../queries/filter-comparison.component.ts | 67 ++++++++-------- .../queries/filter-logical.component.html | 11 ++- .../queries/filter-logical.component.ts | 79 +++++++++---------- .../search/queries/filter-node.component.html | 20 ++--- .../search/queries/filter-node.component.ts | 16 ++-- .../search/queries/query.component.html | 15 ++-- .../search/queries/query.component.ts | 37 ++++----- .../search/queries/sorting.component.ts | 20 +++-- .../search/search-form.component.html | 17 ++-- .../search/search-form.component.ts | 17 ++-- .../app/shared/services/contents.service.ts | 3 +- frontend/src/app/shared/services/query.ts | 46 +++++++++-- .../src/app/shared/state/contents.state.ts | 5 +- frontend/src/app/theme/_bootstrap.scss | 4 + 31 files changed, 332 insertions(+), 231 deletions(-) create mode 100644 backend/src/Squidex/Areas/Api/Controllers/Schemas/Models/QueryModelDto.cs rename backend/src/Squidex/Areas/Api/Controllers/{Contents/Models => }/StatusInfoDto.cs (93%) diff --git a/backend/src/Squidex.Infrastructure/EventSourcing/EventCommit.cs b/backend/src/Squidex.Infrastructure/EventSourcing/EventCommit.cs index b2bf43e31..da72eb5bb 100644 --- a/backend/src/Squidex.Infrastructure/EventSourcing/EventCommit.cs +++ b/backend/src/Squidex.Infrastructure/EventSourcing/EventCommit.cs @@ -13,7 +13,7 @@ public sealed record EventCommit(Guid Id, string StreamName, long Offset, IColle { public static EventCommit Create(Guid id, string streamName, long offset, EventData @event) { - return new EventCommit(id, streamName, offset, new List { @event }); + return new EventCommit(id, streamName, offset, [@event]); } public static EventCommit Create(string streamName, long offset, Envelope envelope, IEventFormatter eventFormatter) @@ -22,6 +22,6 @@ public sealed record EventCommit(Guid Id, string StreamName, long Offset, IColle var eventData = eventFormatter.ToEventData(envelope, id); - return new EventCommit(id, streamName, offset, new List { eventData }); + return new EventCommit(id, streamName, offset, [eventData]); } } diff --git a/backend/src/Squidex.Infrastructure/Plugins/PluginManager.cs b/backend/src/Squidex.Infrastructure/Plugins/PluginManager.cs index 7cdb95c42..736515781 100644 --- a/backend/src/Squidex.Infrastructure/Plugins/PluginManager.cs +++ b/backend/src/Squidex.Infrastructure/Plugins/PluginManager.cs @@ -17,7 +17,7 @@ public sealed class PluginManager : DisposableObjectBase { private readonly HashSet pluginLoaders = []; private readonly HashSet loadedPlugins = []; - private readonly List<(string Plugin, string Action, Exception Exception)> exceptions = new List<(string, string, Exception)>(); + private readonly List<(string Plugin, string Action, Exception Exception)> exceptions = []; public static readonly PluginManager Instance = new PluginManager(); diff --git a/backend/src/Squidex.Infrastructure/Queries/Json/JsonFilterVisitor.cs b/backend/src/Squidex.Infrastructure/Queries/Json/JsonFilterVisitor.cs index d7b1a977d..4d87b802c 100644 --- a/backend/src/Squidex.Infrastructure/Queries/Json/JsonFilterVisitor.cs +++ b/backend/src/Squidex.Infrastructure/Queries/Json/JsonFilterVisitor.cs @@ -40,7 +40,7 @@ public sealed class JsonFilterVisitor : FilterNodeVisitor, public override FilterNode Visit(NegateFilter nodeIn, Args args) { - return new NegateFilter(nodeIn.Accept(this, args)); + return new NegateFilter(nodeIn.Filter.Accept(this, args)); } public override FilterNode Visit(LogicalFilter nodeIn, Args args) diff --git a/backend/src/Squidex.Infrastructure/Queries/QueryModel.cs b/backend/src/Squidex.Infrastructure/Queries/QueryModel.cs index 8932d0ada..1b1a576d6 100644 --- a/backend/src/Squidex.Infrastructure/Queries/QueryModel.cs +++ b/backend/src/Squidex.Infrastructure/Queries/QueryModel.cs @@ -12,15 +12,15 @@ public sealed class QueryModel public static readonly IReadOnlyDictionary> DefaultOperators = new Dictionary> { [FilterSchemaType.Any] = Enum.GetValues(typeof(CompareOperator)).OfType().ToList(), - [FilterSchemaType.Boolean] = new List - { + [FilterSchemaType.Boolean] = + [ CompareOperator.Equals, CompareOperator.Exists, CompareOperator.In, CompareOperator.NotEquals - }, - [FilterSchemaType.DateTime] = new List - { + ], + [FilterSchemaType.DateTime] = + [ CompareOperator.Contains, CompareOperator.Empty, CompareOperator.Exists, @@ -34,14 +34,14 @@ public sealed class QueryModel CompareOperator.Matchs, CompareOperator.NotEquals, CompareOperator.StartsWith - }, - [FilterSchemaType.GeoObject] = new List - { + ], + [FilterSchemaType.GeoObject] = + [ CompareOperator.LessThan, CompareOperator.Exists - }, - [FilterSchemaType.Guid] = new List - { + ], + [FilterSchemaType.Guid] = + [ CompareOperator.Contains, CompareOperator.Empty, CompareOperator.Exists, @@ -55,18 +55,18 @@ public sealed class QueryModel CompareOperator.Matchs, CompareOperator.NotEquals, CompareOperator.StartsWith - }, - [FilterSchemaType.Object] = new List(), - [FilterSchemaType.ObjectArray] = new List - { + ], + [FilterSchemaType.Object] = [], + [FilterSchemaType.ObjectArray] = + [ CompareOperator.Empty, CompareOperator.Exists, CompareOperator.Equals, CompareOperator.In, CompareOperator.NotEquals - }, - [FilterSchemaType.Number] = new List - { + ], + [FilterSchemaType.Number] = + [ CompareOperator.Equals, CompareOperator.Exists, CompareOperator.LessThan, @@ -75,9 +75,9 @@ public sealed class QueryModel CompareOperator.GreaterThanOrEqual, CompareOperator.In, CompareOperator.NotEquals - }, - [FilterSchemaType.String] = new List - { + ], + [FilterSchemaType.String] = + [ CompareOperator.Contains, CompareOperator.Empty, CompareOperator.Exists, @@ -91,9 +91,9 @@ public sealed class QueryModel CompareOperator.Matchs, CompareOperator.NotEquals, CompareOperator.StartsWith - }, - [FilterSchemaType.StringArray] = new List - { + ], + [FilterSchemaType.StringArray] = + [ CompareOperator.Contains, CompareOperator.Empty, CompareOperator.Exists, @@ -107,7 +107,7 @@ public sealed class QueryModel CompareOperator.Matchs, CompareOperator.NotEquals, CompareOperator.StartsWith - } + ] }; public FilterSchema Schema { get; init; } = FilterSchema.Any; diff --git a/backend/src/Squidex.Infrastructure/Reflection/TypeConfig.cs b/backend/src/Squidex.Infrastructure/Reflection/TypeConfig.cs index 682096334..5176772f5 100644 --- a/backend/src/Squidex.Infrastructure/Reflection/TypeConfig.cs +++ b/backend/src/Squidex.Infrastructure/Reflection/TypeConfig.cs @@ -11,7 +11,7 @@ namespace Squidex.Infrastructure.Reflection; public sealed class TypeConfig { - private readonly List<(Type DerivedType, string TypeName)> derivedTypes = new List<(Type DervicedType, string TypeName)>(); + private readonly List<(Type DerivedType, string TypeName)> derivedTypes = []; private Dictionary? mapByName; private Dictionary? mapByType; diff --git a/backend/src/Squidex.Infrastructure/Validation/ValidationException.cs b/backend/src/Squidex.Infrastructure/Validation/ValidationException.cs index ee42ebc35..9eeb9a8f1 100644 --- a/backend/src/Squidex.Infrastructure/Validation/ValidationException.cs +++ b/backend/src/Squidex.Infrastructure/Validation/ValidationException.cs @@ -22,7 +22,7 @@ public class ValidationException : DomainException } public ValidationException(ValidationError error, Exception? inner = null) - : this(new List { error }, inner) + : this([error], inner) { } diff --git a/backend/src/Squidex/Areas/Api/Controllers/Contents/Models/ContentsDto.cs b/backend/src/Squidex/Areas/Api/Controllers/Contents/Models/ContentsDto.cs index 05ea1eadf..9aaa787dd 100644 --- a/backend/src/Squidex/Areas/Api/Controllers/Contents/Models/ContentsDto.cs +++ b/backend/src/Squidex/Areas/Api/Controllers/Contents/Models/ContentsDto.cs @@ -41,7 +41,6 @@ public sealed class ContentsDto : Resource if (schema != null) { await result.AssignStatusesAsync(workflow, schema); - await result.CreateLinksAsync(resources, workflow, schema); } diff --git a/backend/src/Squidex/Areas/Api/Controllers/Schemas/Models/QueryModelDto.cs b/backend/src/Squidex/Areas/Api/Controllers/Schemas/Models/QueryModelDto.cs new file mode 100644 index 000000000..67abdc4cd --- /dev/null +++ b/backend/src/Squidex/Areas/Api/Controllers/Schemas/Models/QueryModelDto.cs @@ -0,0 +1,42 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using Squidex.Areas.Api.Controllers.Contents.Models; +using Squidex.Domain.Apps.Core.Schemas; +using Squidex.Domain.Apps.Entities.Contents; +using Squidex.Infrastructure.Queries; +using Squidex.Infrastructure.Reflection; + +namespace Squidex.Areas.Api.Controllers.Schemas.Models; + +public sealed class QueryModelDto +{ + public FilterSchema Schema { get; init; } = FilterSchema.Any; + + public IReadOnlyDictionary> Operators { get; init; } + + public StatusInfoDto[] Statuses { get; set; } + + public static async Task FromModelAsync(QueryModel model, Schema? schema, IContentWorkflow workflow) + { + var result = SimpleMapper.Map(model, new QueryModelDto()); + + if (schema != null) + { + await result.AssignStatusesAsync(workflow, schema); + } + + return result; + } + + private async Task AssignStatusesAsync(IContentWorkflow workflow, Schema schema) + { + var allStatuses = await workflow.GetAllAsync(schema); + + Statuses = allStatuses.Select(StatusInfoDto.FromDomain).ToArray(); + } +} diff --git a/backend/src/Squidex/Areas/Api/Controllers/Schemas/SchemasController.cs b/backend/src/Squidex/Areas/Api/Controllers/Schemas/SchemasController.cs index 5df9b8404..ebe90b0ff 100644 --- a/backend/src/Squidex/Areas/Api/Controllers/Schemas/SchemasController.cs +++ b/backend/src/Squidex/Areas/Api/Controllers/Schemas/SchemasController.cs @@ -13,6 +13,7 @@ using Squidex.Domain.Apps.Core.GenerateFilters; using Squidex.Domain.Apps.Core.Schemas; using Squidex.Domain.Apps.Core.Scripting; using Squidex.Domain.Apps.Entities; +using Squidex.Domain.Apps.Entities.Contents; using Squidex.Domain.Apps.Entities.Schemas.Commands; using Squidex.Infrastructure; using Squidex.Infrastructure.Commands; @@ -29,11 +30,13 @@ namespace Squidex.Areas.Api.Controllers.Schemas; public sealed class SchemasController : ApiController { private readonly IAppProvider appProvider; + private readonly IContentWorkflow workflow; - public SchemasController(ICommandBus commandBus, IAppProvider appProvider) + public SchemasController(ICommandBus commandBus, IAppProvider appProvider, IContentWorkflow workflow) : base(commandBus) { this.appProvider = appProvider; + this.workflow = workflow; } /// @@ -369,9 +372,10 @@ public sealed class SchemasController : ApiController { var components = await appProvider.GetComponentsAsync(Schema, HttpContext.RequestAborted); - var filters = ContentQueryModel.Build(Schema, App.PartitionResolver(), components).Flatten(); + var result = ContentQueryModel.Build(Schema, App.PartitionResolver(), components).Flatten(); + var response = await QueryModelDto.FromModelAsync(result, Schema, workflow); - return Ok(filters); + return Ok(response); } private async Task BuildModel() diff --git a/backend/src/Squidex/Areas/Api/Controllers/Contents/Models/StatusInfoDto.cs b/backend/src/Squidex/Areas/Api/Controllers/StatusInfoDto.cs similarity index 93% rename from backend/src/Squidex/Areas/Api/Controllers/Contents/Models/StatusInfoDto.cs rename to backend/src/Squidex/Areas/Api/Controllers/StatusInfoDto.cs index 3820efaa0..5de9c4cf3 100644 --- a/backend/src/Squidex/Areas/Api/Controllers/Contents/Models/StatusInfoDto.cs +++ b/backend/src/Squidex/Areas/Api/Controllers/StatusInfoDto.cs @@ -7,7 +7,7 @@ using Squidex.Domain.Apps.Core.Contents; -namespace Squidex.Areas.Api.Controllers.Contents.Models; +namespace Squidex.Areas.Api.Controllers; public sealed class StatusInfoDto { diff --git a/backend/src/Squidex/Squidex.csproj b/backend/src/Squidex/Squidex.csproj index 52c11bd70..c240d277f 100644 --- a/backend/src/Squidex/Squidex.csproj +++ b/backend/src/Squidex/Squidex.csproj @@ -127,7 +127,7 @@ - + diff --git a/backend/tests/Squidex.Infrastructure.Tests/MongoDb/MongoQueryTests.cs b/backend/tests/Squidex.Infrastructure.Tests/MongoDb/MongoQueryTests.cs index 55f704fc7..f5f4d37a2 100644 --- a/backend/tests/Squidex.Infrastructure.Tests/MongoDb/MongoQueryTests.cs +++ b/backend/tests/Squidex.Infrastructure.Tests/MongoDb/MongoQueryTests.cs @@ -255,6 +255,30 @@ public class MongoQueryTests AssertQuery("{ 'Text' : { '$exists' : true, '$ne' : null } }", filter); } + [Fact] + public void Should_make_query_with_and() + { + var filter = ClrFilter.And(ClrFilter.Eq("A", 1), ClrFilter.Eq("B", 2)); + + AssertQuery("{ 'A' : 1, 'B' : 2 }", filter); + } + + [Fact] + public void Should_make_query_with_or() + { + var filter = ClrFilter.Or(ClrFilter.Eq("A", 1), ClrFilter.Eq("B", 2)); + + AssertQuery("{ '$or' : [{ 'A' : 1 }, { 'B' : 2 }] }", filter); + } + + [Fact] + public void Should_make_query_with_not() + { + var filter = ClrFilter.Not(ClrFilter.Lt("A", 1)); + + AssertQuery("{ 'A' : { '$not' : { '$lt' : 1 } } }", filter); + } + [Fact] public void Should_make_query_with_full_text() { diff --git a/frontend/package-lock.json b/frontend/package-lock.json index 27f31b31f..3c30ec1ee 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -30,7 +30,7 @@ "ace-builds": "^1.34.2", "angular-gridster2": "18.0.1", "angular-mentions": "1.5.0", - "bootstrap": "5.3.3", + "bootstrap": "5.2.3", "copy-webpack-plugin": "^12.0.2", "core-js": "3.37.1", "cropperjs": "2.0.0-alpha.1", @@ -12378,9 +12378,9 @@ "license": "ISC" }, "node_modules/bootstrap": { - "version": "5.3.3", - "resolved": "https://registry.npmjs.org/bootstrap/-/bootstrap-5.3.3.tgz", - "integrity": "sha512-8HLCdWgyoMguSO9o+aH+iuZ+aht+mzW0u3HIMzVu7Srrpv7EBBxTnrFlSCskwdY1+EOFQSm7uMJhNQHkdPcmjg==", + "version": "5.2.3", + "resolved": "https://registry.npmjs.org/bootstrap/-/bootstrap-5.2.3.tgz", + "integrity": "sha512-cEKPM+fwb3cT8NzQZYEu4HilJ3anCrWqh3CHAok1p9jXqMPsPTBhU25fBckEJHJ/p+tTxTFTsFQGM+gaHpi3QQ==", "funding": [ { "type": "github", @@ -12392,7 +12392,7 @@ } ], "peerDependencies": { - "@popperjs/core": "^2.11.8" + "@popperjs/core": "^2.11.6" } }, "node_modules/bootstrap.native": { diff --git a/frontend/package.json b/frontend/package.json index f89e6d09f..9db266cbf 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -37,7 +37,7 @@ "ace-builds": "^1.34.2", "angular-gridster2": "18.0.1", "angular-mentions": "1.5.0", - "bootstrap": "5.3.3", + "bootstrap": "5.2.3", "copy-webpack-plugin": "^12.0.2", "core-js": "3.37.1", "cropperjs": "2.0.0-alpha.1", diff --git a/frontend/src/app/features/content/pages/contents/contents-page.component.html b/frontend/src/app/features/content/pages/contents/contents-page.component.html index daf196e6d..55e64774c 100644 --- a/frontend/src/app/features/content/pages/contents/contents-page.component.html +++ b/frontend/src/app/features/content/pages/contents/contents-page.component.html @@ -27,8 +27,7 @@ [queriesTypes]="'common.contents' | sqxTranslate" [query]="contentsState.query | async" (queryChange)="search($event)" - [queryModel]="queryModel | async" - [statuses]="contentsState.statuses | async"> + [queryModel]="queryModel | async"> @if (languages.length > 1) {
diff --git a/frontend/src/app/shared/components/references/content-selector.component.html b/frontend/src/app/shared/components/references/content-selector.component.html index 4a2a821f9..845388a1a 100644 --- a/frontend/src/app/shared/components/references/content-selector.component.html +++ b/frontend/src/app/shared/components/references/content-selector.component.html @@ -43,8 +43,7 @@ placeholder="{{ 'contents.searchPlaceholder' | sqxTranslate }}" [query]="contentsState.query | async" (queryChange)="search($event)" - [queryModel]="queryModel | async" - [statuses]="contentsState.statuses | async"> + [queryModel]="queryModel | async">
@if (languages.length > 1) {
diff --git a/frontend/src/app/shared/components/search/queries/filter-comparison.component.html b/frontend/src/app/shared/components/search/queries/filter-comparison.component.html index 0c5e0a72c..7d00e4483 100644 --- a/frontend/src/app/shared/components/search/queries/filter-comparison.component.html +++ b/frontend/src/app/shared/components/search/queries/filter-comparison.component.html @@ -1,10 +1,25 @@ @if (field) {
+
+
+ +
+
- +
- @for (operator of operators; track operator) { } @@ -13,37 +28,41 @@
@switch (fieldUI) { @case ("Boolean") { - + } @case ("Date") { } @case ("DateTime") { } @case ("Number") { - + } @case ("Reference") { } @case ("Select") { - @for (value of field.schema.extra?.options; track value) { @@ -53,8 +72,8 @@ @case ("Status") { @@ -65,14 +84,14 @@ } @case ("String") { @if (!field.schema.extra) { - + } } @case ("User") { @if (contributorsState.isLoaded | async) { @@ -87,7 +106,7 @@ } @else { - + } } @case ("Unsupported") { diff --git a/frontend/src/app/shared/components/search/queries/filter-comparison.component.ts b/frontend/src/app/shared/components/search/queries/filter-comparison.component.ts index 418ccc021..7b0454576 100644 --- a/frontend/src/app/shared/components/search/queries/filter-comparison.component.ts +++ b/frontend/src/app/shared/components/search/queries/filter-comparison.component.ts @@ -9,7 +9,7 @@ import { AsyncPipe } from '@angular/common'; import { ChangeDetectionStrategy, Component, EventEmitter, Input, Output } from '@angular/core'; import { FormsModule } from '@angular/forms'; import { DateTimeEditorComponent, DropdownComponent, HighlightPipe, TranslatePipe } from '@app/framework'; -import { ContributorsState, FilterableField, FilterComparison, FilterFieldUI, getFilterUI, LanguageDto, QueryModel, StatusInfo } from '@app/shared/internal'; +import { ContributorsState, FilterableField, FilterComparison, FilterFieldUI, FilterNegation, getFilterUI, isNegation, LanguageDto, QueryModel } from '@app/shared/internal'; import { UserDtoPicture } from '../../pipes'; import { ReferenceInputComponent } from '../../references/reference-input.component'; import { QueryPathComponent } from './query-path.component'; @@ -36,7 +36,7 @@ import { FilterOperatorPipe } from './query.pipes'; }) export class FilterComparisonComponent { @Output() - public filterChange = new EventEmitter(); + public filterChange = new EventEmitter(); @Output() public remove = new EventEmitter(); @@ -47,14 +47,14 @@ export class FilterComparisonComponent { @Input({ required: true }) public languages!: ReadonlyArray; - @Input({ required: true }) - public statuses?: ReadonlyArray | null; - @Input({ required: true }) public model!: QueryModel; @Input({ required: true }) - public filter!: FilterComparison; + public filter!: FilterComparison | FilterNegation; + + public actualComparison!: FilterComparison; + public actualNegated = false; public field?: FilterableField; public fieldUI?: FilterFieldUI; @@ -66,48 +66,45 @@ export class FilterComparisonComponent { } public ngOnChanges() { - this.updatePath(false); + if (isNegation(this.filter)) { + this.actualComparison = this.filter.not; + this.actualNegated = true; + } else { + this.actualComparison = this.filter; + this.actualNegated = false; + } + + this.field = this.model.schema.fields.find(x => x.path === this.actualComparison.path); + this.fieldUI = getFilterUI(this.actualComparison, this.field!); + + this.operators = this.model.operators[this.field?.schema.type!] || []; + + if (!this.operators.includes(this.actualComparison.op)) { + this.actualComparison = { ...this.actualComparison, op: this.operators[0] }; + } } public changeValue(value: any) { - this.filter.value = value; - - this.emitChange(); + this.change({ value }); } public changeOp(op: string) { - this.filter.op = op; - - this.updatePath(false); - - this.emitChange(); + this.change({ op }); } public changePath(path: string) { - this.filter.path = path; - - this.updatePath(true); - - this.emitChange(); + this.change({ path, value: null }); } - private updatePath(updateValue: boolean) { - this.field = this.model.schema.fields.find(x => x.path === this.filter.path); - - this.operators = this.model.operators[this.field?.schema.type!] || []; - - if (!this.operators.includes(this.filter.op)) { - this.filter.op = this.operators[0]; - } - - if (updateValue) { - this.filter.value = null; - } + private change(update: Partial) { + this.emitChange({ ...this.actualComparison, ...update }, this.actualNegated); + } - this.fieldUI = getFilterUI(this.filter, this.field!); + public toggleNot() { + this.emitChange(this.actualComparison, !this.actualNegated); } - public emitChange() { - this.filterChange.emit(); + private emitChange(filter: FilterComparison, not: boolean) { + this.filterChange.emit(not ? { not: filter } : filter); } } diff --git a/frontend/src/app/shared/components/search/queries/filter-logical.component.html b/frontend/src/app/shared/components/search/queries/filter-logical.component.html index 22fb89713..89362c0cd 100644 --- a/frontend/src/app/shared/components/search/queries/filter-logical.component.html +++ b/frontend/src/app/shared/components/search/queries/filter-logical.component.html @@ -3,7 +3,7 @@
@@ -84,15 +84,14 @@
@if (showQueries) { - @if (queryModel && statuses) { + @if (queryModel) {
+ (queryChange)="changeQuery($event)">
diff --git a/frontend/src/app/shared/components/search/search-form.component.ts b/frontend/src/app/shared/components/search/search-form.component.ts index 457f41a0f..e9c6c5963 100644 --- a/frontend/src/app/shared/components/search/search-form.component.ts +++ b/frontend/src/app/shared/components/search/search-form.component.ts @@ -10,7 +10,7 @@ import { booleanAttribute, ChangeDetectionStrategy, Component, EventEmitter, Inp import { FormsModule, ReactiveFormsModule } from '@angular/forms'; import { Observable } from 'rxjs'; import { ControlErrorsComponent, FocusOnInitDirective, MarkdownPipe, ModalDialogComponent, ModalDirective, SafeHtmlPipe, ShortcutComponent, ShortcutDirective, TooltipDirective, TourStepDirective, TranslatePipe } from '@app/framework'; -import { DialogModel, equalsQuery, hasFilter, LanguageDto, Queries, Query, QueryModel, SaveQueryForm, StatusInfo, TypedSimpleChanges, Types } from '@app/shared/internal'; +import { DialogModel, equalsQuery, hasFilter, LanguageDto, Queries, Query, QueryModel, SaveQueryForm, TypedSimpleChanges } from '@app/shared/internal'; import { TourHintDirective } from '../tour-hint.directive'; import { QueryComponent } from './queries/query.component'; import { SavedQueriesComponent } from './shared-queries.component'; @@ -56,9 +56,6 @@ export class SearchFormComponent { @Input() public languages: ReadonlyArray = []; - @Input() - public statuses?: ReadonlyArray | null; - @Input() public queryModel?: QueryModel | null; @@ -77,7 +74,7 @@ export class SearchFormComponent { @Input() public formClass = 'form-inline search-form'; - public showQueries = false; + public showQueries = true; public saveKey!: Observable; public saveQueryDialog = new DialogModel(); @@ -93,19 +90,17 @@ export class SearchFormComponent { } if (changes.query) { - this.previousQuery = Types.clone(this.query); - - this.hasFilter = hasFilter(this.query); + this.previousQuery = this.query; } + + this.hasFilter = hasFilter(this.query); } public search(close = false) { this.hasFilter = hasFilter(this.query); if (this.query && !equalsQuery(this.query, this.previousQuery)) { - const clone = Types.clone(this.query); - - this.queryChange.emit(clone); + this.queryChange.emit(this.query); this.previousQuery = this.query; } diff --git a/frontend/src/app/shared/services/contents.service.ts b/frontend/src/app/shared/services/contents.service.ts index f26651a3f..21ec733da 100644 --- a/frontend/src/app/shared/services/contents.service.ts +++ b/frontend/src/app/shared/services/contents.service.ts @@ -10,10 +10,11 @@ import { Injectable } from '@angular/core'; import { Observable } from 'rxjs'; import { map } from 'rxjs/operators'; import { ApiUrlConfig, DateTime, ErrorDto, hasAnyLink, HTTP, mapVersioned, pretifyError, Resource, ResourceLinks, StringHelper, Version, Versioned } from '@app/framework'; -import { StatusInfo } from '../state/contents.state'; import { Query, sanitize } from './query'; import { parseField, RootFieldDto } from './schemas.service'; +export type StatusInfo = Readonly<{ status: string; color: string }>; + export class ScheduleDto { constructor( public readonly status: string, diff --git a/frontend/src/app/shared/services/query.ts b/frontend/src/app/shared/services/query.ts index a0dc8b5b4..3e863754f 100644 --- a/frontend/src/app/shared/services/query.ts +++ b/frontend/src/app/shared/services/query.ts @@ -6,6 +6,7 @@ */ import { QueryParams, RouteSynchronizer, Types } from '@app/framework'; +import { StatusInfo } from './contents.service'; export type FilterSchemaType = 'Any' | @@ -92,13 +93,17 @@ export interface QueryModel { // All available fields. readonly schema: FilterSchema; + // All available statuses. + readonly statuses: ReadonlyArray; + // The allowed operators. readonly operators: Readonly<{ [type: string]: ReadonlyArray }>; } -export type FilterNode = FilterComparison | FilterLogical; +export type FilterNode = FilterComparison | FilterLogical | FilterNegation; +export type FilterLogical = FilterAnd | FilterOr; -export interface FilterComparison { +export type FilterComparison = Readonly<{ // The full path to the property. path: string; @@ -107,14 +112,41 @@ export interface FilterComparison { // The value. value: any; -} +}>; -export interface FilterLogical { - // The child filters if the logical filter is a conjunction (AND). - and?: FilterNode[]; +export type FilterNegation = Readonly<{ + // The negated filter. + not: FilterComparison; +}>; +export type FilterAnd = Readonly<{ // The child filters if the logical filter is a conjunction (AND). - or?: FilterNode[]; + and: FilterNode[]; +}>; + +export type FilterOr = Readonly<{ + // The child filters if the logical filter is a disjunction (OR). + or: FilterNode[]; +}>; + +export function isNegation(input: FilterNode): input is FilterNegation { + return !!(input as any)['not']; +} + +export function isLogicalOr(input: FilterNode): input is FilterOr { + return !!(input as any)['or']; +} + +export function isLogicalAnd(input: FilterNode): input is FilterAnd { + return !!(input as any)['and']; +} + +export function isLogical(input: FilterNode): input is FilterLogical { + return isLogicalAnd(input) || isLogicalOr(input); +} + +export function isComparison(input: FilterNode): input is FilterComparison { + return !isNegation(input) &&! isComparison(input); } export interface QuerySorting { diff --git a/frontend/src/app/shared/state/contents.state.ts b/frontend/src/app/shared/state/contents.state.ts index 000ea305b..09dbdaa26 100644 --- a/frontend/src/app/shared/state/contents.state.ts +++ b/frontend/src/app/shared/state/contents.state.ts @@ -9,7 +9,7 @@ import { Injectable } from '@angular/core'; import { EMPTY, Observable, of } from 'rxjs'; import { catchError, finalize, map, switchMap, tap } from 'rxjs/operators'; import { debug, DialogService, ErrorDto, getPagingInfo, ListState, shareSubscribed, State, Types, Version, Versioned } from '@app/framework'; -import { BulkResultDto, BulkUpdateJobDto, ContentDto, ContentsDto, ContentsService } from '../services/contents.service'; +import { BulkResultDto, BulkUpdateJobDto, ContentDto, ContentsDto, ContentsService, StatusInfo } from '../services/contents.service'; import { Query } from '../services/query'; import { AppsState } from './apps.state'; import { SavedQuery } from './queries'; @@ -17,9 +17,6 @@ import { SchemasState } from './schemas.state'; /* eslint-disable @typescript-eslint/no-throw-literal */ -export type StatusInfo = - Readonly<{ status: string; color: string }>; - interface Snapshot extends ListState { // The current contents. contents: ReadonlyArray; diff --git a/frontend/src/app/theme/_bootstrap.scss b/frontend/src/app/theme/_bootstrap.scss index e8f9cf5d3..a46b844a1 100644 --- a/frontend/src/app/theme/_bootstrap.scss +++ b/frontend/src/app/theme/_bootstrap.scss @@ -335,6 +335,10 @@ a { box-shadow: none; } + &-code { + @include text-code; + } + &-outline-secondary { color: $color-text-decent;