mirror of https://github.com/Squidex/squidex.git
Browse Source
* Unify pipe. * Custom views. * Using model for table view. * Custom view. * Better separation. * Refactoring. * Added some tests. * Style and code style improvements. * Names unified.pull/473/head
committed by
GitHub
58 changed files with 640 additions and 222 deletions
@ -0,0 +1,41 @@ |
|||
<div class="container"> |
|||
<div class="header"> |
|||
<button type="button" class="btn btn-secondary btn-sm" (click)="resetDefault()"> |
|||
Reset Default View |
|||
</button> |
|||
</div> |
|||
|
|||
<hr /> |
|||
|
|||
<div |
|||
cdkDropList |
|||
[cdkDropListData]="fieldNames" |
|||
(cdkDropListDropped)="drop($event)"> |
|||
|
|||
<div *ngFor="let field of fieldNames; trackBy: random" cdkDrag> |
|||
<i class="icon-drag2 drag-handle"></i> |
|||
|
|||
<div class="form-check"> |
|||
<input class="form-check-input" type="checkbox" checked (change)="removeField(field)" id="field_{{field}}"> |
|||
<label class="form-check-label" for="field_{{field}}"> |
|||
{{field}} |
|||
</label> |
|||
</div> |
|||
</div> |
|||
</div> |
|||
|
|||
<hr /> |
|||
|
|||
<div> |
|||
<div *ngFor="let field of fieldsNotAdded"> |
|||
<i class="icon-drag2 drag-handle invisible"></i> |
|||
|
|||
<div class="form-check"> |
|||
<input class="form-check-input" type="checkbox" (change)="addField(field)" id="field_{{field}}"> |
|||
<label class="form-check-label" for="field_{{field}}"> |
|||
{{field}} |
|||
</label> |
|||
</div> |
|||
</div> |
|||
</div> |
|||
</div> |
|||
@ -0,0 +1,23 @@ |
|||
@import '_vars'; |
|||
@import '_mixins'; |
|||
|
|||
.container { |
|||
max-height: 400px; |
|||
overflow-x: hidden; |
|||
overflow-y: auto; |
|||
padding: .5rem 1rem; |
|||
} |
|||
|
|||
.header { |
|||
text-align: right; |
|||
} |
|||
|
|||
.invisible { |
|||
visibility: hidden; |
|||
} |
|||
|
|||
.form-check { |
|||
display: inline-block; |
|||
padding-left: 2rem; |
|||
padding-right: .5rem; |
|||
} |
|||
@ -0,0 +1,60 @@ |
|||
/* |
|||
* Squidex Headless CMS |
|||
* |
|||
* @license |
|||
* Copyright (c) Squidex UG (haftungsbeschränkt). All rights reserved. |
|||
*/ |
|||
|
|||
// tslint:disable: readonly-array
|
|||
|
|||
import { CdkDragDrop, moveItemInArray } from '@angular/cdk/drag-drop'; |
|||
import { ChangeDetectionStrategy, Component, EventEmitter, Input, OnChanges, Output } from '@angular/core'; |
|||
|
|||
@Component({ |
|||
selector: 'sqx-custom-view-editor', |
|||
styleUrls: ['./custom-view-editor.component.scss'], |
|||
templateUrl: './custom-view-editor.component.html', |
|||
changeDetection: ChangeDetectionStrategy.OnPush |
|||
}) |
|||
export class CustomViewEditorComponent implements OnChanges { |
|||
@Input() |
|||
public allFields: ReadonlyArray<string>; |
|||
|
|||
@Input() |
|||
public fieldNames: ReadonlyArray<string>; |
|||
|
|||
@Output() |
|||
public fieldNamesChange = new EventEmitter<ReadonlyArray<string>>(); |
|||
|
|||
public fieldsNotAdded: ReadonlyArray<string>; |
|||
|
|||
public ngOnChanges() { |
|||
this.fieldsNotAdded = this.allFields.filter(n => this.fieldNames.indexOf(n) < 0); |
|||
} |
|||
|
|||
public random() { |
|||
return Math.random(); |
|||
} |
|||
|
|||
public drop(event: CdkDragDrop<string[]>) { |
|||
moveItemInArray(event.container.data, event.previousIndex, event.currentIndex); |
|||
|
|||
this.updateFields(event.container.data); |
|||
} |
|||
|
|||
public updateFields(fieldNames: ReadonlyArray<string>) { |
|||
this.fieldNamesChange.emit(fieldNames); |
|||
} |
|||
|
|||
public resetDefault() { |
|||
this.updateFields([]); |
|||
} |
|||
|
|||
public addField(field: string) { |
|||
this.updateFields([...this.fieldNames, field]); |
|||
} |
|||
|
|||
public removeField(field: string) { |
|||
this.updateFields(this.fieldNames.filter(x => x !== field)); |
|||
} |
|||
} |
|||
@ -0,0 +1,43 @@ |
|||
/* |
|||
* Squidex Headless CMS |
|||
* |
|||
* @license |
|||
* Copyright (c) Squidex UG (haftungsbeschränkt). All rights reserved. |
|||
*/ |
|||
|
|||
import { ChangeDetectionStrategy, Component, EventEmitter, Input, Output } from '@angular/core'; |
|||
|
|||
import { QueryModel } from '@app/shared/internal'; |
|||
|
|||
@Component({ |
|||
selector: 'sqx-query-path', |
|||
template: ` |
|||
<sqx-dropdown [items]="model.fields | sqxKeys" [ngModel]="path" (ngModelChange)="pathChange.emit($event)" [canSearch]="false" separated="true"> |
|||
<ng-template let-field="$implicit"> |
|||
<div class="row"> |
|||
<div class="col-auto"> |
|||
<div class="badge badge-pill badge-primary">{{model.fields[field].displayName}}</div> |
|||
</div> |
|||
<div class="col text-right"> |
|||
<small class="text-muted">{{model.fields[field].description}}</small> |
|||
</div> |
|||
</div> |
|||
</ng-template> |
|||
|
|||
<ng-template let-field="$implicit"> |
|||
{{model.fields[field].displayName}} |
|||
</ng-template> |
|||
</sqx-dropdown> |
|||
`,
|
|||
changeDetection: ChangeDetectionStrategy.OnPush |
|||
}) |
|||
export class QueryPathComponent { |
|||
@Output() |
|||
public pathChange = new EventEmitter<string>(); |
|||
|
|||
@Input() |
|||
public path: string; |
|||
|
|||
@Input() |
|||
public model: QueryModel; |
|||
} |
|||
@ -0,0 +1,123 @@ |
|||
/* |
|||
* Squidex Headless CMS |
|||
* |
|||
* @license |
|||
* Copyright (c) Squidex UG (haftungsbeschränkt). All rights reserved. |
|||
*/ |
|||
|
|||
import { of } from 'rxjs'; |
|||
import { IMock, Mock, Times } from 'typemoq'; |
|||
|
|||
import { DateTime, Version } from '@app/framework'; |
|||
|
|||
import { |
|||
createProperties, |
|||
MetaFields, |
|||
RootFieldDto, |
|||
SchemaDetailsDto, |
|||
TableField, |
|||
TableFields, |
|||
UIState |
|||
} from '@app/shared/internal'; |
|||
|
|||
describe('TableFielsd', () => { |
|||
let uiState: IMock<UIState>; |
|||
|
|||
const schema = |
|||
new SchemaDetailsDto({}, '1', 'my-schema', '', {}, |
|||
false, |
|||
false, |
|||
DateTime.now(), 'me', |
|||
DateTime.now(), 'me', |
|||
new Version('1'), |
|||
[ |
|||
new RootFieldDto({}, 1, 'string', createProperties('String'), 'invariant') |
|||
]); |
|||
|
|||
beforeEach(() => { |
|||
uiState = Mock.ofType<UIState>(); |
|||
}); |
|||
|
|||
const INVALID_CONFIGS = [ |
|||
{ case: 'empty', fields: [] }, |
|||
{ case: 'invalid', fields: ['invalid'] } |
|||
]; |
|||
|
|||
INVALID_CONFIGS.forEach(test => { |
|||
it(`should provide default fields if config is ${test.case}`, () => { |
|||
let fields: ReadonlyArray<TableField>; |
|||
let fieldNames: ReadonlyArray<string>; |
|||
|
|||
uiState.setup(x => x.getUser<string[]>('schemas.my-schema.view', [])) |
|||
.returns(() => of(test.fields)); |
|||
|
|||
const tableFields = new TableFields(uiState.object, schema); |
|||
|
|||
tableFields.listFields.subscribe(result => fields = result); |
|||
tableFields.listFieldNames.subscribe(result => fieldNames = result); |
|||
|
|||
expect(fields!).toEqual([ |
|||
MetaFields.lastModifiedByAvatar, |
|||
schema.fields[0], |
|||
MetaFields.statusColor, |
|||
MetaFields.lastModified |
|||
]); |
|||
|
|||
expect(fieldNames!).toEqual([ |
|||
MetaFields.lastModifiedByAvatar, |
|||
schema.fields[0].name, |
|||
MetaFields.statusColor, |
|||
MetaFields.lastModified |
|||
]); |
|||
}); |
|||
}); |
|||
|
|||
INVALID_CONFIGS.forEach(test => { |
|||
it(`should remove ui state if config is ${test.case}`, () => { |
|||
uiState.setup(x => x.getUser<string[]>('schemas.my-schema.view', [])) |
|||
.returns(() => of([])); |
|||
|
|||
const tableFields = new TableFields(uiState.object, schema); |
|||
|
|||
tableFields.updateFields(test.fields, true); |
|||
|
|||
uiState.verify(x => x.removeUser('schemas.my-schema.view'), Times.once()); |
|||
}); |
|||
}); |
|||
|
|||
it('should eliminate invalid fields from the config', () => { |
|||
let fields: ReadonlyArray<TableField>; |
|||
let fieldNames: ReadonlyArray<string>; |
|||
|
|||
const config = ['invalid', MetaFields.version]; |
|||
|
|||
uiState.setup(x => x.getUser<string[]>('schemas.my-schema.view', [])) |
|||
.returns(() => of(config)); |
|||
|
|||
const tableFields = new TableFields(uiState.object, schema); |
|||
|
|||
tableFields.listFields.subscribe(result => fields = result); |
|||
tableFields.listFieldNames.subscribe(result => fieldNames = result); |
|||
|
|||
expect(fields!).toEqual([ |
|||
MetaFields.version |
|||
]); |
|||
|
|||
expect(fieldNames!).toEqual([ |
|||
MetaFields.version |
|||
]); |
|||
}); |
|||
|
|||
it('should update config when fields are saved', () => { |
|||
uiState.setup(x => x.getUser<string[]>('schemas.my-schema.view', [])) |
|||
.returns(() => of([])); |
|||
|
|||
const tableFields = new TableFields(uiState.object, schema); |
|||
|
|||
const config = ['invalid', MetaFields.version]; |
|||
|
|||
tableFields.updateFields(config, true); |
|||
|
|||
uiState.verify(x => x.set('schemas.my-schema.view', [MetaFields.version], true), Times.once()); |
|||
}); |
|||
}); |
|||
@ -0,0 +1,72 @@ |
|||
/* |
|||
* Squidex Headless CMS |
|||
* |
|||
* @license |
|||
* Copyright (c) Squidex UG (haftungsbeschränkt). All rights reserved. |
|||
*/ |
|||
|
|||
// tslint:disable: readonly-array
|
|||
|
|||
import { BehaviorSubject, Observable } from 'rxjs'; |
|||
import { take } from 'rxjs/operators'; |
|||
|
|||
import { |
|||
MetaFields, |
|||
SchemaDetailsDto, |
|||
TableField |
|||
} from '../services/schemas.service'; |
|||
|
|||
import { UIState } from './ui.state'; |
|||
|
|||
const META_FIELD_NAMES = Object.values(MetaFields); |
|||
|
|||
export class TableFields { |
|||
private readonly listField$ = new BehaviorSubject<ReadonlyArray<TableField>>([]); |
|||
private readonly listFieldName$ = new BehaviorSubject<ReadonlyArray<string>>([]); |
|||
private readonly settingsKey: string; |
|||
|
|||
public readonly allFields: ReadonlyArray<string>; |
|||
|
|||
public get listFields(): Observable<ReadonlyArray<TableField>> { |
|||
return this.listField$; |
|||
} |
|||
|
|||
public get listFieldNames(): Observable<ReadonlyArray<string>> { |
|||
return this.listFieldName$; |
|||
} |
|||
|
|||
constructor( |
|||
private readonly uiState: UIState, |
|||
private readonly schema: SchemaDetailsDto |
|||
) { |
|||
this.allFields = [...this.schema.contentFields.map(x => x.name), ...META_FIELD_NAMES].sorted(); |
|||
|
|||
this.settingsKey = `schemas.${this.schema.name}.view`; |
|||
|
|||
this.uiState.getUser<string[]>(this.settingsKey, []).pipe(take(1)) |
|||
.subscribe(fieldNames => { |
|||
this.updateFields(fieldNames, false); |
|||
}); |
|||
} |
|||
|
|||
public updateFields(fieldNames: string[], save = true) { |
|||
fieldNames = fieldNames.filter(x => this.allFields.indexOf(x) >= 0); |
|||
|
|||
if (fieldNames.length === 0) { |
|||
fieldNames = this.schema.defaultListFields.map(x => x['name'] || x); |
|||
|
|||
if (save) { |
|||
this.uiState.removeUser(this.settingsKey); |
|||
} |
|||
} else { |
|||
if (save) { |
|||
this.uiState.set(this.settingsKey, fieldNames, true); |
|||
} |
|||
} |
|||
|
|||
const fields: ReadonlyArray<TableField> = fieldNames.map(n => this.schema.fields.find(f => f.name === n) || n); |
|||
|
|||
this.listField$.next(fields); |
|||
this.listFieldName$.next(fieldNames); |
|||
} |
|||
} |
|||
Loading…
Reference in new issue