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> |
|||
<ng-content></ng-content> |
|||
<ng-content /> |
|||
|
|||
@ -1,2 +1,6 @@ |
|||
@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 'vars'; |
|||
|
|||
.search-form { |
|||
display: inline-block; |
|||
} |
|||
|
|||
.form-inline { |
|||
position: relative; |
|||
} |
|||
|
|||
.form-horizontal { |
|||
min-width: 35rem; |
|||
} |
|||
|
|||
.buttons { |
|||
@include absolute(8px, 8px); |
|||
} |
|||
|
|||
.buttons2 { |
|||
@include absolute(.75rem, 1.75rem); |
|||
} |
|||
|
|||
.link { |
|||
font-size: $font-small; |
|||
font-weight: normal; |
|||
margin-top: 1.5rem; |
|||
} |
|||
|
|||
.col-form-label { |
|||
text-align: left; |
|||
::ng-deep { |
|||
.nf-details { |
|||
background-color: $color-white; |
|||
|
|||
input, select, button { |
|||
height: auto; |
|||
} |
|||
} |
|||
|
|||
.nf { |
|||
svg { |
|||
fill: $color-text-decent; |
|||
} |
|||
|
|||
.btn-success { |
|||
svg { |
|||
fill: $color-white; |
|||
} |
|||
} |
|||
} |
|||
|
|||
.nf-dropdown { |
|||
.nf-dropdown-description { |
|||
color: $color-text-decent |
|||
} |
|||
|
|||
.control-dropdown-item.active { |
|||
.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