mirror of https://github.com/Squidex/squidex.git
Browse Source
* Started with migration. * Search form integrated * Update dependencies. * Self closing. * Fix build. * Fix tests * Playwright fix.pull/1232/head
committed by
GitHub
99 changed files with 496 additions and 1251 deletions
@ -1,40 +0,0 @@ |
|||||
// ==========================================================================
|
|
||||
// Squidex Headless CMS
|
|
||||
// ==========================================================================
|
|
||||
// Copyright (c) Squidex UG (haftungsbeschraenkt)
|
|
||||
// All rights reserved. Licensed under the MIT license.
|
|
||||
// ==========================================================================
|
|
||||
|
|
||||
namespace Squidex.Infrastructure.Translations; |
|
||||
|
|
||||
public sealed class MissingKeys |
|
||||
{ |
|
||||
private const string MissingFileName = "__missing.txt"; |
|
||||
private readonly object lockObject = new object(); |
|
||||
private readonly HashSet<string> missingTranslations; |
|
||||
|
|
||||
public MissingKeys() |
|
||||
{ |
|
||||
if (File.Exists(MissingFileName)) |
|
||||
{ |
|
||||
var missing = File.ReadAllLines(MissingFileName); |
|
||||
|
|
||||
missingTranslations = new HashSet<string>(missing); |
|
||||
} |
|
||||
else |
|
||||
{ |
|
||||
missingTranslations = []; |
|
||||
} |
|
||||
} |
|
||||
|
|
||||
public void Log(string key) |
|
||||
{ |
|
||||
lock (lockObject) |
|
||||
{ |
|
||||
if (!missingTranslations.Add(key)) |
|
||||
{ |
|
||||
File.AppendAllLines(MissingFileName, [key]); |
|
||||
} |
|
||||
} |
|
||||
} |
|
||||
} |
|
||||
@ -1 +1 @@ |
|||||
<ng-content></ng-content> |
<ng-content /> |
||||
|
|||||
@ -1,2 +1,2 @@ |
|||||
<div #element></div> |
<div #element></div> |
||||
<ng-content></ng-content> |
<ng-content /> |
||||
|
|||||
@ -1,2 +1,6 @@ |
|||||
@import 'mixins'; |
@import 'mixins'; |
||||
@import 'vars'; |
@import 'vars'; |
||||
|
|
||||
|
input { |
||||
|
cursor: default; |
||||
|
} |
||||
@ -1,112 +0,0 @@ |
|||||
@if (field) { |
|
||||
<div class="row gx-2 mb-1 align-items-center"> |
|
||||
<div class="col-auto"> |
|
||||
<div class="btn-group"> |
|
||||
<button class="btn btn-secondary btn-toggle btn-code text-sm" [class.btn-primary]="actualNegated" (click)="toggleNot()" type="button"> |
|
||||
NOT |
|
||||
</button> |
|
||||
</div> |
|
||||
</div> |
|
||||
|
|
||||
<div class="col-auto path"> |
|
||||
<sqx-query-path [model]="model" [path]="actualComparison.path" (pathChange)="changePath($event)" /> |
|
||||
</div> |
|
||||
|
|
||||
<div class="col-auto operator"> |
|
||||
<select class="form-select" [disabled]="operators.length === 0" [ngModel]="actualComparison.op" (ngModelChange)="changeOp($event)"> |
|
||||
@for (operator of operators; track operator) { |
|
||||
<option [ngValue]="operator">{{ operator | sqxFilterOperator | sqxTranslate }}</option> |
|
||||
} |
|
||||
</select> |
|
||||
</div> |
|
||||
|
|
||||
<div class="col align-items-center"> |
|
||||
@switch (fieldUI) { |
|
||||
@case ("Boolean") { |
|
||||
<input class="form-check-input" [ngModel]="actualComparison.value" (ngModelChange)="changeValue($event)" type="checkbox" /> |
|
||||
} |
|
||||
|
|
||||
@case ("Date") { |
|
||||
<sqx-date-time-editor hideDateButtons="true" mode="Date" [ngModel]="actualComparison.value" (ngModelChange)="changeValue($event)" /> |
|
||||
} |
|
||||
|
|
||||
@case ("DateTime") { |
|
||||
<sqx-date-time-editor hideDateButtons="true" mode="DateTime" [ngModel]="actualComparison.value" (ngModelChange)="changeValue($event)" /> |
|
||||
} |
|
||||
|
|
||||
@case ("Number") { |
|
||||
<input class="form-control" [ngModel]="actualComparison.value" (ngModelChange)="changeValue($event)" type="number" /> |
|
||||
} |
|
||||
|
|
||||
@case ("Reference") { |
|
||||
<sqx-reference-input |
|
||||
[language]="language" |
|
||||
[languages]="languages" |
|
||||
mode="Single" |
|
||||
[ngModel]="actualComparison.value" |
|
||||
(ngModelChange)="changeValue($event)" |
|
||||
[query]="undefined" |
|
||||
[schemaIds]="field.schema.extra?.schemaIds" /> |
|
||||
} |
|
||||
|
|
||||
@case ("Select") { |
|
||||
<select class="form-select" [ngModel]="actualComparison.value" (ngModelChange)="changeValue($event)"> |
|
||||
<option [ngValue]="null"></option> |
|
||||
|
|
||||
@for (value of field.schema.extra?.options; track value) { |
|
||||
<option [ngValue]="value">{{ value }}</option> |
|
||||
} |
|
||||
</select> |
|
||||
} |
|
||||
|
|
||||
@case ("Status") { |
|
||||
<sqx-dropdown |
|
||||
canSearch="false" |
|
||||
[items]="model.statuses" |
|
||||
[ngModel]="actualComparison.value" |
|
||||
(ngModelChange)="changeValue($event)" |
|
||||
valueProperty="status"> |
|
||||
<ng-template let-status="$implicit"> <i class="icon-circle" [style.color]="status.color"></i> {{ status.status }} </ng-template> |
|
||||
</sqx-dropdown> |
|
||||
} |
|
||||
|
|
||||
@case ("String") { |
|
||||
@if (!field.schema.extra) { |
|
||||
<input class="form-control" [ngModel]="actualComparison.value" (ngModelChange)="changeValue($event)" /> |
|
||||
} |
|
||||
} |
|
||||
|
|
||||
@case ("User") { |
|
||||
@if (contributorsState.isLoaded | async) { |
|
||||
<sqx-dropdown |
|
||||
[items]="contributorsState.contributors | async" |
|
||||
[ngModel]="actualComparison.value" |
|
||||
(ngModelChange)="changeValue($event)" |
|
||||
searchProperty="contributorName" |
|
||||
valueProperty="token"> |
|
||||
<ng-template let-context="context" let-user="$implicit"> |
|
||||
<span class="dropdown-user"> |
|
||||
<img class="user-picture" [src]="user | sqxUserDtoPicture" /> |
|
||||
<span class="user-name" [innerHTML]="user.contributorName | sqxHighlight: context"></span> |
|
||||
</span> |
|
||||
</ng-template> |
|
||||
<ng-template let-user="$implicit"> |
|
||||
<span class="user-name">{{ user.contributorName }}</span> |
|
||||
</ng-template> |
|
||||
</sqx-dropdown> |
|
||||
} @else { |
|
||||
<input class="form-control" [ngModel]="actualComparison.value" (ngModelChange)="changeValue($event)" /> |
|
||||
} |
|
||||
} |
|
||||
|
|
||||
@case ("Unsupported") { |
|
||||
{{ "common.notSupported" | sqxTranslate }} |
|
||||
} |
|
||||
} |
|
||||
</div> |
|
||||
|
|
||||
<div class="col-auto ps-2"> |
|
||||
<button class="btn btn-text-danger" (click)="remove.emit()" type="button"><i class="icon-bin2"></i></button> |
|
||||
</div> |
|
||||
</div> |
|
||||
} |
|
||||
@ -1,18 +0,0 @@ |
|||||
@import 'mixins'; |
|
||||
@import 'vars'; |
|
||||
|
|
||||
.path { |
|
||||
width: 10rem; |
|
||||
} |
|
||||
|
|
||||
.operator { |
|
||||
width: 10rem; |
|
||||
} |
|
||||
|
|
||||
.dropdown-user { |
|
||||
@include truncate; |
|
||||
|
|
||||
.user-name { |
|
||||
margin-left: .25rem; |
|
||||
} |
|
||||
} |
|
||||
@ -1,109 +0,0 @@ |
|||||
/* |
|
||||
* Squidex Headless CMS |
|
||||
* |
|
||||
* @license |
|
||||
* Copyright (c) Squidex UG (haftungsbeschränkt). All rights reserved. |
|
||||
*/ |
|
||||
|
|
||||
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 { AppLanguageDto, ContributorsState, FilterableField, FilterComparison, FilterFieldUI, FilterNegation, getFilterUI, isNegation, QueryModel } from '@app/shared/internal'; |
|
||||
import { UserDtoPicture } from '../../pipes'; |
|
||||
import { ReferenceInputComponent } from '../../references/reference-input.component'; |
|
||||
import { QueryPathComponent } from './query-path.component'; |
|
||||
import { FilterOperatorPipe } from './query.pipes'; |
|
||||
|
|
||||
@Component({ |
|
||||
selector: 'sqx-filter-comparison', |
|
||||
styleUrls: ['./filter-comparison.component.scss'], |
|
||||
templateUrl: './filter-comparison.component.html', |
|
||||
changeDetection: ChangeDetectionStrategy.OnPush, |
|
||||
imports: [ |
|
||||
AsyncPipe, |
|
||||
DateTimeEditorComponent, |
|
||||
DropdownComponent, |
|
||||
FilterOperatorPipe, |
|
||||
FormsModule, |
|
||||
HighlightPipe, |
|
||||
QueryPathComponent, |
|
||||
ReferenceInputComponent, |
|
||||
TranslatePipe, |
|
||||
UserDtoPicture, |
|
||||
], |
|
||||
}) |
|
||||
export class FilterComparisonComponent { |
|
||||
@Output() |
|
||||
public filterChange = new EventEmitter<FilterComparison | FilterNegation>(); |
|
||||
|
|
||||
@Output() |
|
||||
public remove = new EventEmitter(); |
|
||||
|
|
||||
@Input({ required: true }) |
|
||||
public language!: AppLanguageDto; |
|
||||
|
|
||||
@Input({ required: true }) |
|
||||
public languages!: ReadonlyArray<AppLanguageDto>; |
|
||||
|
|
||||
@Input({ required: true }) |
|
||||
public model!: QueryModel; |
|
||||
|
|
||||
@Input({ required: true }) |
|
||||
public filter!: FilterComparison | FilterNegation; |
|
||||
|
|
||||
public actualComparison!: FilterComparison; |
|
||||
public actualNegated = false; |
|
||||
|
|
||||
public field?: FilterableField; |
|
||||
public fieldUI?: FilterFieldUI; |
|
||||
public operators: ReadonlyArray<string> = []; |
|
||||
|
|
||||
constructor( |
|
||||
public readonly contributorsState: ContributorsState, |
|
||||
) { |
|
||||
} |
|
||||
|
|
||||
public ngOnChanges() { |
|
||||
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.change({ value }); |
|
||||
} |
|
||||
|
|
||||
public changeOp(op: string) { |
|
||||
this.change({ op }); |
|
||||
} |
|
||||
|
|
||||
public changePath(path: string) { |
|
||||
this.change({ path, value: null }); |
|
||||
} |
|
||||
|
|
||||
private change(update: Partial<FilterComparison>) { |
|
||||
this.emitChange({ ...this.actualComparison, ...update }, this.actualNegated); |
|
||||
} |
|
||||
|
|
||||
public toggleNot() { |
|
||||
this.emitChange(this.actualComparison, !this.actualNegated); |
|
||||
} |
|
||||
|
|
||||
private emitChange(filter: FilterComparison, not: boolean) { |
|
||||
this.filterChange.emit(not ? { not: filter } : filter); |
|
||||
} |
|
||||
} |
|
||||
@ -1,47 +0,0 @@ |
|||||
<div class="group"> |
|
||||
<div class="row g-0"> |
|
||||
<div class="col"> |
|
||||
<div class="btn-group"> |
|
||||
<button class="btn btn-secondary btn-toggle btn-code" [class.btn-primary]="isAnd" (click)="toggleType()" [disabled]="isAnd" type="button"> |
|
||||
AND |
|
||||
</button> |
|
||||
<button class="btn btn-secondary btn-toggle btn-code" [class.btn-primary]="isOr" (click)="toggleType()" [disabled]="isOr" type="button"> |
|
||||
OR |
|
||||
</button> |
|
||||
</div> |
|
||||
</div> |
|
||||
|
|
||||
@if (!isRoot) { |
|
||||
<div class="col-auto ps-2"> |
|
||||
<button class="btn btn-text-danger" (click)="remove.emit()" type="button"><i class="icon-bin2"></i></button> |
|
||||
</div> |
|
||||
} |
|
||||
</div> |
|
||||
|
|
||||
<div class="filters"> |
|
||||
<span class="filter-line-v"></span> |
|
||||
@for (filter of filters; track $index; let i = $index) { |
|
||||
<div class="filter mt-3"> |
|
||||
<span class="filter-line-h"></span> |
|
||||
<sqx-filter-node |
|
||||
[filter]="filter" |
|
||||
(filterChange)="replaceNode(i, $event)" |
|
||||
[language]="language" |
|
||||
[languages]="languages" |
|
||||
[level]="level + 1" |
|
||||
[model]="model" |
|
||||
(remove)="removeNode(i)" /> |
|
||||
</div> |
|
||||
} |
|
||||
|
|
||||
<div class="filter filter-add mt-3"> |
|
||||
<span class="filter-line-h"></span> |
|
||||
<button class="btn btn-outline-success btn-sm me-2" (click)="addComparison()"> |
|
||||
{{ "search.addFilter" | sqxTranslate }} |
|
||||
</button> |
|
||||
@if (level < 1) { |
|
||||
<button class="btn btn-outline-success btn-sm" (click)="addLogical()">{{ "search.addGroup" | sqxTranslate }}</button> |
|
||||
} |
|
||||
</div> |
|
||||
</div> |
|
||||
</div> |
|
||||
@ -1,33 +0,0 @@ |
|||||
@import 'mixins'; |
|
||||
@import 'vars'; |
|
||||
|
|
||||
$field-line: #e1e8ef; |
|
||||
|
|
||||
.filters { |
|
||||
padding-left: 3rem; |
|
||||
position: relative; |
|
||||
} |
|
||||
|
|
||||
.filter { |
|
||||
position: relative; |
|
||||
|
|
||||
&-line-v { |
|
||||
@include absolute(-.5rem, auto, 1.8rem, 1.5rem); |
|
||||
border: 0; |
|
||||
border-left: 2px dashed $field-line; |
|
||||
width: 2px; |
|
||||
} |
|
||||
|
|
||||
&-line-h { |
|
||||
@include absolute(1.2rem, auto, auto, -1.5rem); |
|
||||
border: 0; |
|
||||
border-bottom: 2px dashed $field-line; |
|
||||
width: 1.5rem; |
|
||||
} |
|
||||
|
|
||||
&-add { |
|
||||
background: none; |
|
||||
border: 2px dashed $field-line; |
|
||||
padding: .5rem; |
|
||||
} |
|
||||
} |
|
||||
@ -1,124 +0,0 @@ |
|||||
/* |
|
||||
* Squidex Headless CMS |
|
||||
* |
|
||||
* @license |
|
||||
* Copyright (c) Squidex UG (haftungsbeschränkt). All rights reserved. |
|
||||
*/ |
|
||||
|
|
||||
|
|
||||
import { booleanAttribute, ChangeDetectionStrategy, Component, EventEmitter, Input, numberAttribute, Output } from '@angular/core'; |
|
||||
import { TranslatePipe } from '@app/framework'; |
|
||||
import { AppLanguageDto, FilterLogical, FilterNode, isLogicalAnd, isLogicalOr, QueryModel } from '@app/shared/internal'; |
|
||||
import { FilterNodeComponent } from './filter-node.component'; |
|
||||
|
|
||||
@Component({ |
|
||||
selector: 'sqx-filter-logical', |
|
||||
styleUrls: ['./filter-logical.component.scss'], |
|
||||
templateUrl: './filter-logical.component.html', |
|
||||
changeDetection: ChangeDetectionStrategy.OnPush, |
|
||||
imports: [ |
|
||||
FilterNodeComponent, |
|
||||
TranslatePipe, |
|
||||
], |
|
||||
}) |
|
||||
export class FilterLogicalComponent { |
|
||||
@Output() |
|
||||
public filterChange = new EventEmitter<FilterLogical>(); |
|
||||
|
|
||||
@Output() |
|
||||
public remove = new EventEmitter(); |
|
||||
|
|
||||
@Input({ required: true }) |
|
||||
public language!: AppLanguageDto; |
|
||||
|
|
||||
@Input({ required: true }) |
|
||||
public languages!: ReadonlyArray<AppLanguageDto>; |
|
||||
|
|
||||
@Input({ transform: numberAttribute }) |
|
||||
public level = 0; |
|
||||
|
|
||||
@Input({ transform: booleanAttribute }) |
|
||||
public isRoot?: boolean | null; |
|
||||
|
|
||||
@Input({ required: true }) |
|
||||
public model!: QueryModel; |
|
||||
|
|
||||
@Input({ required: true }) |
|
||||
public filter!: FilterLogical; |
|
||||
|
|
||||
public get filters() { |
|
||||
return isLogicalAnd(this.filter) ? this.filter.and : this.filter.or; |
|
||||
} |
|
||||
|
|
||||
public get isAnd() { |
|
||||
return isLogicalAnd(this.filter); |
|
||||
} |
|
||||
|
|
||||
public get isOr() { |
|
||||
return isLogicalOr(this.filter); |
|
||||
} |
|
||||
|
|
||||
public addComparison() { |
|
||||
this.addNode({ path: this.model.schema.fields[0].path } as any); |
|
||||
} |
|
||||
|
|
||||
public addLogical() { |
|
||||
this.addNode({ and: [] }); |
|
||||
} |
|
||||
|
|
||||
public addNode(node: FilterNode) { |
|
||||
const filter = this.filter; |
|
||||
|
|
||||
let change: FilterLogical; |
|
||||
if (isLogicalAnd(filter)) { |
|
||||
change = { and: [...filter.and, node] }; |
|
||||
} else { |
|
||||
change = { or: [...filter.or, node] }; |
|
||||
} |
|
||||
|
|
||||
this.emitChange(change); |
|
||||
} |
|
||||
|
|
||||
public removeNode(index: number) { |
|
||||
const filter = this.filter; |
|
||||
|
|
||||
let change: FilterLogical; |
|
||||
if (isLogicalAnd(filter)) { |
|
||||
change = { and: filter.and.filter((_, i) => i !== index) }; |
|
||||
} else { |
|
||||
change = { or: filter.or.filter((_, i) => i !== index) }; |
|
||||
} |
|
||||
|
|
||||
this.emitChange(change); |
|
||||
} |
|
||||
|
|
||||
public replaceNode(index: number, node: FilterNode) { |
|
||||
const filter = this.filter; |
|
||||
|
|
||||
let change: FilterLogical; |
|
||||
if (isLogicalAnd(filter)) { |
|
||||
change = { and: filter.and.map((x, i) => i === index ? node : x) }; |
|
||||
} else { |
|
||||
change = { or: filter.or.map((x, i) => i === index ? node : x) }; |
|
||||
} |
|
||||
|
|
||||
this.emitChange(change); |
|
||||
} |
|
||||
|
|
||||
public toggleType() { |
|
||||
const filter = this.filter; |
|
||||
|
|
||||
let change: FilterLogical; |
|
||||
if (isLogicalAnd(filter)) { |
|
||||
change = { or: filter.and }; |
|
||||
} else { |
|
||||
change = { and: filter.or }; |
|
||||
} |
|
||||
|
|
||||
this.emitChange(change); |
|
||||
} |
|
||||
|
|
||||
private emitChange(filter: FilterLogical) { |
|
||||
this.filterChange.emit(filter); |
|
||||
} |
|
||||
} |
|
||||
@ -1,18 +0,0 @@ |
|||||
@if (actualLogical) { |
|
||||
<sqx-filter-logical |
|
||||
[filter]="actualLogical" |
|
||||
(filterChange)="filterChange.emit($event)" |
|
||||
[language]="language" |
|
||||
[languages]="languages" |
|
||||
[level]="level" |
|
||||
[model]="model" |
|
||||
(remove)="remove.emit()" /> |
|
||||
} @else if (actualComparison) { |
|
||||
<sqx-filter-comparison |
|
||||
[filter]="actualComparison" |
|
||||
(filterChange)="filterChange.emit($event)" |
|
||||
[language]="language" |
|
||||
[languages]="languages" |
|
||||
[model]="model" |
|
||||
(remove)="remove.emit()" /> |
|
||||
} |
|
||||
@ -1,2 +0,0 @@ |
|||||
@import 'mixins'; |
|
||||
@import 'vars'; |
|
||||
@ -1,54 +0,0 @@ |
|||||
/* |
|
||||
* Squidex Headless CMS |
|
||||
* |
|
||||
* @license |
|
||||
* Copyright (c) Squidex UG (haftungsbeschränkt). All rights reserved. |
|
||||
*/ |
|
||||
|
|
||||
|
|
||||
import { ChangeDetectionStrategy, Component, EventEmitter, forwardRef, Input, numberAttribute, Output } from '@angular/core'; |
|
||||
import { AppLanguageDto, FilterComparison, FilterLogical, FilterNegation, FilterNode, isLogical, QueryModel } from '@app/shared/internal'; |
|
||||
import { FilterComparisonComponent } from './filter-comparison.component'; |
|
||||
import { FilterLogicalComponent } from './filter-logical.component'; |
|
||||
|
|
||||
@Component({ |
|
||||
selector: 'sqx-filter-node', |
|
||||
styleUrls: ['./filter-node.component.scss'], |
|
||||
templateUrl: './filter-node.component.html', |
|
||||
changeDetection: ChangeDetectionStrategy.OnPush, |
|
||||
imports: [ |
|
||||
forwardRef(() => FilterComparisonComponent), |
|
||||
forwardRef(() => FilterLogicalComponent), |
|
||||
], |
|
||||
}) |
|
||||
export class FilterNodeComponent { |
|
||||
@Output() |
|
||||
public filterChange = new EventEmitter(); |
|
||||
|
|
||||
@Output() |
|
||||
public remove = new EventEmitter(); |
|
||||
|
|
||||
@Input({ required: true }) |
|
||||
public language!: AppLanguageDto; |
|
||||
|
|
||||
@Input({ required: true }) |
|
||||
public languages!: ReadonlyArray<AppLanguageDto>; |
|
||||
|
|
||||
@Input({ transform: numberAttribute }) |
|
||||
public level = 0; |
|
||||
|
|
||||
@Input({ required: true }) |
|
||||
public model!: QueryModel; |
|
||||
|
|
||||
@Input() |
|
||||
public set filter(value: FilterNode) { |
|
||||
if (isLogical(value)) { |
|
||||
this.actualLogical = value; |
|
||||
} else { |
|
||||
this.actualComparison = value; |
|
||||
} |
|
||||
} |
|
||||
|
|
||||
public actualComparison?: FilterComparison | FilterNegation; |
|
||||
public actualLogical?: FilterLogical; |
|
||||
} |
|
||||
@ -1,21 +0,0 @@ |
|||||
<sqx-dropdown |
|
||||
[disabled]="model.schema.fields.length === 0" |
|
||||
[dropdownStyles]="{ maxWidth: '40rem' }" |
|
||||
[items]="model.schema.fields" |
|
||||
itemSeparator="true" |
|
||||
[ngModel]="field" |
|
||||
(ngModelChange)="pathChange.emit($event?.path)" |
|
||||
searchProperty="path"> |
|
||||
<ng-template let-field="$implicit"> |
|
||||
<div class="row"> |
|
||||
<div class="col-auto"> |
|
||||
<div class="badge rounded-pill badge-primary">{{ field.path }}</div> |
|
||||
</div> |
|
||||
|
|
||||
<div class="col text-end"> |
|
||||
<sqx-form-hint>{{ field.description }}</sqx-form-hint> |
|
||||
</div> |
|
||||
</div> |
|
||||
</ng-template> |
|
||||
<ng-template let-field="$implicit"> {{ field.path }} </ng-template> |
|
||||
</sqx-dropdown> |
|
||||
@ -1,2 +0,0 @@ |
|||||
@import 'mixins'; |
|
||||
@import 'vars'; |
|
||||
@ -1,39 +0,0 @@ |
|||||
/* |
|
||||
* Squidex Headless CMS |
|
||||
* |
|
||||
* @license |
|
||||
* Copyright (c) Squidex UG (haftungsbeschränkt). All rights reserved. |
|
||||
*/ |
|
||||
|
|
||||
import { ChangeDetectionStrategy, Component, EventEmitter, Input, Output } from '@angular/core'; |
|
||||
import { FormsModule } from '@angular/forms'; |
|
||||
import { DropdownComponent, FormHintComponent } from '@app/framework'; |
|
||||
import { FilterableField, QueryModel } from '@app/shared/internal'; |
|
||||
|
|
||||
@Component({ |
|
||||
selector: 'sqx-query-path', |
|
||||
styleUrls: ['./query-path.component.scss'], |
|
||||
templateUrl: './query-path.component.html', |
|
||||
changeDetection: ChangeDetectionStrategy.OnPush, |
|
||||
imports: [ |
|
||||
DropdownComponent, |
|
||||
FormHintComponent, |
|
||||
FormsModule, |
|
||||
], |
|
||||
}) |
|
||||
export class QueryPathComponent { |
|
||||
@Output() |
|
||||
public pathChange = new EventEmitter<string>(); |
|
||||
|
|
||||
@Input() |
|
||||
public path = ''; |
|
||||
|
|
||||
@Input({ required: true }) |
|
||||
public model!: QueryModel; |
|
||||
|
|
||||
public field?: FilterableField; |
|
||||
|
|
||||
public ngOnChanges() { |
|
||||
this.field = this.model.schema.fields.find(x => x.path === this.path); |
|
||||
} |
|
||||
} |
|
||||
@ -1,15 +0,0 @@ |
|||||
<sqx-filter-logical |
|
||||
[filter]="actualQuery.filter" |
|
||||
(filterChange)="changeFilter($event)" |
|
||||
isRoot="true" |
|
||||
[language]="language" |
|
||||
[languages]="languages" |
|
||||
[model]="model" /> |
|
||||
<h4 class="mt-4">{{ "search.sorting" | sqxTranslate }}</h4> |
|
||||
|
|
||||
@for (sorting of actualQuery.sort; track sorting; let i = $index) { |
|
||||
<div class="mb-2"> |
|
||||
<sqx-sorting [model]="model" (remove)="removeSorting(i)" [sorting]="sorting" (sortingChange)="replaceSorting(i, $event)" /> |
|
||||
</div> |
|
||||
} |
|
||||
<button class="btn btn-outline-success btn-sm me-2" (click)="addSorting()">{{ "search.addSorting" | sqxTranslate }}</button> |
|
||||
@ -1,2 +0,0 @@ |
|||||
@import 'mixins'; |
|
||||
@import 'vars'; |
|
||||
@ -1,83 +0,0 @@ |
|||||
/* |
|
||||
* Squidex Headless CMS |
|
||||
* |
|
||||
* @license |
|
||||
* Copyright (c) Squidex UG (haftungsbeschränkt). All rights reserved. |
|
||||
*/ |
|
||||
|
|
||||
|
|
||||
import { ChangeDetectionStrategy, Component, EventEmitter, Input, Output } from '@angular/core'; |
|
||||
import { TranslatePipe } from '@app/framework'; |
|
||||
import { AppLanguageDto, FilterLogical, Query, QueryModel, QuerySorting } from '@app/shared/internal'; |
|
||||
import { FilterLogicalComponent } from './filter-logical.component'; |
|
||||
import { SortingComponent } from './sorting.component'; |
|
||||
|
|
||||
@Component({ |
|
||||
selector: 'sqx-query', |
|
||||
styleUrls: ['./query.component.scss'], |
|
||||
templateUrl: './query.component.html', |
|
||||
changeDetection: ChangeDetectionStrategy.OnPush, |
|
||||
imports: [ |
|
||||
FilterLogicalComponent, |
|
||||
SortingComponent, |
|
||||
TranslatePipe, |
|
||||
], |
|
||||
}) |
|
||||
export class QueryComponent { |
|
||||
@Output() |
|
||||
public queryChange = new EventEmitter<Query>(); |
|
||||
|
|
||||
@Input({ required: true }) |
|
||||
public language!: AppLanguageDto; |
|
||||
|
|
||||
@Input({ required: true }) |
|
||||
public languages!: ReadonlyArray<AppLanguageDto>; |
|
||||
|
|
||||
@Input({ required: true }) |
|
||||
public model!: QueryModel; |
|
||||
|
|
||||
@Input() |
|
||||
public set query(query: Query | undefined | null) { |
|
||||
if (!query) { |
|
||||
query = {}; |
|
||||
} |
|
||||
|
|
||||
if (!query.filter) { |
|
||||
query = { ...query, filter: { and: [] } }; |
|
||||
} |
|
||||
|
|
||||
if (!query.sort) { |
|
||||
query = { ...query, sort: [] }; |
|
||||
} |
|
||||
|
|
||||
this.actualQuery = query as any; |
|
||||
} |
|
||||
|
|
||||
public actualQuery!: RequireKeys<Query, 'sort' | 'filter'>; |
|
||||
|
|
||||
public addSorting() { |
|
||||
const path = Object.keys(this.model.schema.fields)[0]; |
|
||||
|
|
||||
this.change({ sort: [...this.actualQuery.sort, { path, order: 'ascending' }] }); |
|
||||
} |
|
||||
|
|
||||
public replaceSorting(index: number, sorting: QuerySorting) { |
|
||||
this.change({ sort: this.actualQuery.sort.map((x, i) => i === index ? sorting : x) }); |
|
||||
} |
|
||||
|
|
||||
public removeSorting(index: number) { |
|
||||
this.change({ sort: this.actualQuery.sort.filter((_, i) => i !== index) }); |
|
||||
} |
|
||||
|
|
||||
public changeFilter(filter: FilterLogical ) { |
|
||||
this.change({ filter }); |
|
||||
} |
|
||||
|
|
||||
private change(update: Partial<Query>) { |
|
||||
this.actualQuery = { ...this.actualQuery, ...update }; |
|
||||
|
|
||||
this.queryChange.emit(this.actualQuery); |
|
||||
} |
|
||||
} |
|
||||
|
|
||||
type RequireKeys<T extends object, K extends keyof T> = Required<Pick<T, K>> & Omit<T, K>; |
|
||||
@ -1,22 +0,0 @@ |
|||||
/* |
|
||||
* Squidex Headless CMS |
|
||||
* |
|
||||
* @license |
|
||||
* Copyright (c) Squidex UG (haftungsbeschränkt). All rights reserved. |
|
||||
*/ |
|
||||
|
|
||||
import { Pipe, PipeTransform } from '@angular/core'; |
|
||||
|
|
||||
@Pipe({ |
|
||||
name: 'sqxFilterOperator', |
|
||||
pure: true, |
|
||||
}) |
|
||||
export class FilterOperatorPipe implements PipeTransform { |
|
||||
public transform(value: any) { |
|
||||
if (!value) { |
|
||||
return null; |
|
||||
} |
|
||||
|
|
||||
return `common.queryOperators.${value}`; |
|
||||
} |
|
||||
} |
|
||||
@ -1,15 +0,0 @@ |
|||||
<div class="row g-1"> |
|
||||
<div class="col"><sqx-query-path [model]="model" [path]="sorting.path" (pathChange)="changePath($event)" /></div> |
|
||||
|
|
||||
<div class="col-auto"> |
|
||||
<select class="form-select ms-2" [ngModel]="sorting.order" (ngModelChange)="changeOrder($event)"> |
|
||||
@for (mode of modes; track mode) { |
|
||||
<option [ngValue]="mode">{{ mode }}</option> |
|
||||
} |
|
||||
</select> |
|
||||
</div> |
|
||||
|
|
||||
<div class="col-auto"> |
|
||||
<button class="btn btn-text-danger" (click)="remove.emit()" type="button"><i class="icon-bin2"></i></button> |
|
||||
</div> |
|
||||
</div> |
|
||||
@ -1,2 +0,0 @@ |
|||||
@import 'mixins'; |
|
||||
@import 'vars'; |
|
||||
@ -1,52 +0,0 @@ |
|||||
/* |
|
||||
* Squidex Headless CMS |
|
||||
* |
|
||||
* @license |
|
||||
* Copyright (c) Squidex UG (haftungsbeschränkt). All rights reserved. |
|
||||
*/ |
|
||||
|
|
||||
|
|
||||
import { ChangeDetectionStrategy, Component, EventEmitter, Input, Output } from '@angular/core'; |
|
||||
import { FormsModule } from '@angular/forms'; |
|
||||
import { QueryModel, QuerySorting, SORT_MODES } from '@app/shared/internal'; |
|
||||
import { QueryPathComponent } from './query-path.component'; |
|
||||
|
|
||||
@Component({ |
|
||||
selector: 'sqx-sorting', |
|
||||
styleUrls: ['./sorting.component.scss'], |
|
||||
templateUrl: './sorting.component.html', |
|
||||
changeDetection: ChangeDetectionStrategy.OnPush, |
|
||||
imports: [ |
|
||||
FormsModule, |
|
||||
QueryPathComponent, |
|
||||
], |
|
||||
}) |
|
||||
export class SortingComponent { |
|
||||
public readonly modes = SORT_MODES; |
|
||||
|
|
||||
@Output() |
|
||||
public sortingChange = new EventEmitter<QuerySorting>(); |
|
||||
|
|
||||
@Output() |
|
||||
public remove = new EventEmitter(); |
|
||||
|
|
||||
@Input({ required: true }) |
|
||||
public model!: QueryModel; |
|
||||
|
|
||||
@Input({ required: true }) |
|
||||
public sorting!: QuerySorting; |
|
||||
|
|
||||
public changeOrder(order: any) { |
|
||||
this.change({ order }); |
|
||||
} |
|
||||
|
|
||||
public changePath(path: string) { |
|
||||
this.change({ path }); |
|
||||
} |
|
||||
|
|
||||
private change(update: Partial<QuerySorting>) { |
|
||||
this.sorting = { ...this.sorting, ...update }; |
|
||||
|
|
||||
this.sortingChange.emit(this.sorting); |
|
||||
} |
|
||||
} |
|
||||
@ -1,32 +1,70 @@ |
|||||
@import 'mixins'; |
@import 'mixins'; |
||||
@import 'vars'; |
@import 'vars'; |
||||
|
|
||||
.search-form { |
::ng-deep { |
||||
display: inline-block; |
.nf-details { |
||||
} |
background-color: $color-white; |
||||
|
|
||||
.form-inline { |
input, select, button { |
||||
position: relative; |
height: auto; |
||||
} |
} |
||||
|
} |
||||
.form-horizontal { |
|
||||
min-width: 35rem; |
.nf { |
||||
} |
svg { |
||||
|
fill: $color-text-decent; |
||||
.buttons { |
} |
||||
@include absolute(8px, 8px); |
|
||||
} |
.btn-success { |
||||
|
svg { |
||||
.buttons2 { |
fill: $color-white; |
||||
@include absolute(.75rem, 1.75rem); |
} |
||||
} |
} |
||||
|
} |
||||
.link { |
|
||||
font-size: $font-small; |
.nf-dropdown { |
||||
font-weight: normal; |
.nf-dropdown-description { |
||||
margin-top: 1.5rem; |
color: $color-text-decent |
||||
} |
} |
||||
|
|
||||
.col-form-label { |
.control-dropdown-item.active { |
||||
text-align: left; |
.nf-dropdown-description { |
||||
|
color: $color-white; |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
.nf-button { |
||||
|
&.btn-sm { |
||||
|
padding-top: .25rem; |
||||
|
padding-bottom: .25rem; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
.nf-comparison-inline { |
||||
|
font-size: $font-small; |
||||
|
|
||||
|
* { |
||||
|
font-size: .9rem; |
||||
|
} |
||||
|
|
||||
|
input { |
||||
|
box-shadow: none; |
||||
|
} |
||||
|
|
||||
|
.btn, |
||||
|
.form-control, |
||||
|
.form-select { |
||||
|
background: none !important; |
||||
|
border: 0; |
||||
|
border-radius: 0; |
||||
|
height: calc(1.75rem - 2px); |
||||
|
padding-top: 0.1875rem; |
||||
|
padding-bottom: .25rem; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
.dropdown-user { |
||||
|
margin-right: .5rem; |
||||
|
} |
||||
} |
} |
||||
@ -1 +1 @@ |
|||||
<router-outlet></router-outlet> |
<router-outlet /> |
||||
|
|||||
Loading…
Reference in new issue