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