mirror of https://github.com/Squidex/squidex.git
committed by
GitHub
33 changed files with 833 additions and 295 deletions
@ -0,0 +1,46 @@ |
|||||
|
// ==========================================================================
|
||||
|
// Squidex Headless CMS
|
||||
|
// ==========================================================================
|
||||
|
// Copyright (c) Squidex UG (haftungsbeschraenkt)
|
||||
|
// All rights reserved. Licensed under the MIT license.
|
||||
|
// ==========================================================================
|
||||
|
|
||||
|
using System.Collections.Generic; |
||||
|
using System.Reflection; |
||||
|
|
||||
|
namespace Squidex.Domain.Apps.Core.Schemas |
||||
|
{ |
||||
|
public static class MetaFields |
||||
|
{ |
||||
|
private static readonly HashSet<string> AllList = new HashSet<string>(); |
||||
|
|
||||
|
public static ISet<string> All |
||||
|
{ |
||||
|
get { return AllList; } |
||||
|
} |
||||
|
|
||||
|
public static readonly string Id = "meta.id"; |
||||
|
public static readonly string Created = "meta.created"; |
||||
|
public static readonly string CreatedByAvatar = "meta.createdBy.avatar"; |
||||
|
public static readonly string CreatedByName = "meta.createdBy.name"; |
||||
|
public static readonly string LastModified = "meta.lastModified"; |
||||
|
public static readonly string LastModifiedByAvatar = "meta.lastModifiedBy.avatar"; |
||||
|
public static readonly string LastModifiedByName = "meta.lastModifiedBy.name"; |
||||
|
public static readonly string Status = "meta.status"; |
||||
|
public static readonly string StatusColor = "meta.status.color"; |
||||
|
public static readonly string Version = "meta.version"; |
||||
|
|
||||
|
static MetaFields() |
||||
|
{ |
||||
|
foreach (var field in typeof(MetaFields).GetFields(BindingFlags.Public | BindingFlags.Static)) |
||||
|
{ |
||||
|
if (field.FieldType == typeof(string)) |
||||
|
{ |
||||
|
var value = field.GetValue(null) as string; |
||||
|
|
||||
|
AllList.Add(value!); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,109 @@ |
|||||
|
/* |
||||
|
* Squidex Headless CMS |
||||
|
* |
||||
|
* @license |
||||
|
* Copyright (c) Squidex UG (haftungsbeschränkt). All rights reserved. |
||||
|
*/ |
||||
|
|
||||
|
import { Directive, ElementRef, Input, OnChanges, Pipe, PipeTransform, Renderer2 } from '@angular/core'; |
||||
|
|
||||
|
import { |
||||
|
MetaFields, |
||||
|
RootFieldDto, |
||||
|
SchemaDetailsDto, |
||||
|
TableField, |
||||
|
Types |
||||
|
} from '@app/shared'; |
||||
|
|
||||
|
export function getTableWidth(fields: ReadonlyArray<TableField>) { |
||||
|
let result = 0; |
||||
|
|
||||
|
for (let field of fields) { |
||||
|
result += getCellWidth(field); |
||||
|
} |
||||
|
|
||||
|
return result; |
||||
|
} |
||||
|
|
||||
|
export function getCellWidth(field: TableField) { |
||||
|
if (Types.is(field, RootFieldDto)) { |
||||
|
return 220; |
||||
|
} else { |
||||
|
switch (field) { |
||||
|
case MetaFields.id: |
||||
|
return 280; |
||||
|
case MetaFields.created: |
||||
|
return 150; |
||||
|
case MetaFields.createdByAvatar: |
||||
|
return 55; |
||||
|
case MetaFields.createdByName: |
||||
|
return 150; |
||||
|
case MetaFields.lastModified: |
||||
|
return 150; |
||||
|
case MetaFields.lastModifiedByAvatar: |
||||
|
return 55; |
||||
|
case MetaFields.lastModifiedByName: |
||||
|
return 150; |
||||
|
case MetaFields.status: |
||||
|
return 160; |
||||
|
case MetaFields.statusColor: |
||||
|
return 50; |
||||
|
case MetaFields.version: |
||||
|
return 80; |
||||
|
} |
||||
|
|
||||
|
return 0; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
@Pipe({ |
||||
|
name: 'sqxContentListWidth', |
||||
|
pure: true |
||||
|
}) |
||||
|
export class ContentListWidthPipe implements PipeTransform { |
||||
|
public transform(value: SchemaDetailsDto) { |
||||
|
if (!value) { |
||||
|
return 0; |
||||
|
} |
||||
|
|
||||
|
return `${getTableWidth(value.referenceFields) + 100}px`; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
@Pipe({ |
||||
|
name: 'sqxContentReferencesWidth', |
||||
|
pure: true |
||||
|
}) |
||||
|
export class ContentReferencesWidthPipe implements PipeTransform { |
||||
|
public transform(value: SchemaDetailsDto) { |
||||
|
if (!value) { |
||||
|
return 0; |
||||
|
} |
||||
|
|
||||
|
return `${getTableWidth(value.referenceFields) + 300}px`; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
@Directive({ |
||||
|
selector: '[sqxContentListCell]' |
||||
|
}) |
||||
|
export class ContentListCellDirective implements OnChanges { |
||||
|
@Input('sqxContentListCell') |
||||
|
public field: TableField; |
||||
|
|
||||
|
constructor( |
||||
|
private readonly element: ElementRef, |
||||
|
private readonly renderer: Renderer2 |
||||
|
) { |
||||
|
} |
||||
|
|
||||
|
public ngOnChanges() { |
||||
|
if (Types.isString(this.field) && this.field) { |
||||
|
const width = `${getCellWidth(this.field)}px`; |
||||
|
|
||||
|
this.renderer.setStyle(this.element.nativeElement, 'min-width', width); |
||||
|
this.renderer.setStyle(this.element.nativeElement, 'max-width', width); |
||||
|
this.renderer.setStyle(this.element.nativeElement, 'width', width); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,132 @@ |
|||||
|
/* |
||||
|
* Squidex Headless CMS |
||||
|
* |
||||
|
* @license |
||||
|
* Copyright (c) Squidex UG (haftungsbeschränkt). All rights reserved. |
||||
|
*/ |
||||
|
|
||||
|
import { ChangeDetectionStrategy, Component, Input, OnChanges } from '@angular/core'; |
||||
|
import { FormGroup } from '@angular/forms'; |
||||
|
|
||||
|
import { |
||||
|
ContentDto, |
||||
|
getContentValue, |
||||
|
LanguageDto, |
||||
|
MetaFields, |
||||
|
RootFieldDto, |
||||
|
TableField, |
||||
|
Types |
||||
|
} from '@app/shared'; |
||||
|
|
||||
|
@Component({ |
||||
|
selector: 'sqx-content-list-field', |
||||
|
template: ` |
||||
|
<ng-container [ngSwitch]="fieldName"> |
||||
|
<ng-container *ngSwitchCase="metaFields.id"> |
||||
|
<small class="truncate">{{content.id}}</small> |
||||
|
</ng-container> |
||||
|
<ng-container *ngSwitchCase="metaFields.created"> |
||||
|
<small class="truncate">{{content.created | sqxFromNow}}</small> |
||||
|
</ng-container> |
||||
|
<ng-container *ngSwitchCase="metaFields.createdByAvatar"> |
||||
|
<img class="user-picture" title="{{content.createdBy | sqxUserNameRef}}" [attr.src]="content.createdBy | sqxUserPictureRef" /> |
||||
|
</ng-container> |
||||
|
<ng-container *ngSwitchCase="metaFields.createdByName"> |
||||
|
<small class="truncate">{{content.createdBy | sqxUserNameRef}}</small> |
||||
|
</ng-container> |
||||
|
<ng-container *ngSwitchCase="metaFields.lastModified"> |
||||
|
<small class="truncate">{{content.lastModified | sqxFromNow}}</small> |
||||
|
</ng-container> |
||||
|
<ng-container *ngSwitchCase="metaFields.lastModifiedByAvatar"> |
||||
|
<img class="user-picture" title="{{content.lastModifiedBy | sqxUserNameRef}}" [attr.src]="content.lastModifiedBy | sqxUserPictureRef" /> |
||||
|
</ng-container> |
||||
|
<ng-container *ngSwitchCase="metaFields.lastModifiedByName"> |
||||
|
<small class="truncate">{{content.lastModifiedBy | sqxUserNameRef}}</small> |
||||
|
</ng-container> |
||||
|
<ng-container *ngSwitchCase="metaFields.status"> |
||||
|
<span class="truncate"> |
||||
|
<sqx-content-status |
||||
|
[status]="content.status" |
||||
|
[statusColor]="content.statusColor" |
||||
|
[scheduledTo]="content.scheduleJob?.status" |
||||
|
[scheduledAt]="content.scheduleJob?.dueTime" |
||||
|
[isPending]="content.isPending"> |
||||
|
</sqx-content-status> |
||||
|
|
||||
|
{{content.status}} |
||||
|
</span> |
||||
|
</ng-container> |
||||
|
<ng-container *ngSwitchCase="metaFields.statusColor"> |
||||
|
<sqx-content-status |
||||
|
[status]="content.status" |
||||
|
[statusColor]="content.statusColor" |
||||
|
[scheduledTo]="content.scheduleJob?.status" |
||||
|
[scheduledAt]="content.scheduleJob?.dueTime" |
||||
|
[isPending]="content.isPending"> |
||||
|
</sqx-content-status> |
||||
|
</ng-container> |
||||
|
<ng-container *ngSwitchCase="metaFields.version"> |
||||
|
<small class="truncate">{{content.version.value}}</small> |
||||
|
</ng-container> |
||||
|
<ng-container *ngSwitchDefault> |
||||
|
<ng-container *ngIf="isInlineEditable && patchAllowed; else displayTemplate"> |
||||
|
<sqx-content-value-editor [form]="patchForm" [field]="field"></sqx-content-value-editor> |
||||
|
</ng-container> |
||||
|
|
||||
|
<ng-template #displayTemplate> |
||||
|
<sqx-content-value [value]="value"></sqx-content-value> |
||||
|
</ng-template> |
||||
|
</ng-container> |
||||
|
</ng-container>`, |
||||
|
changeDetection: ChangeDetectionStrategy.OnPush |
||||
|
}) |
||||
|
export class ContentListFieldComponent implements OnChanges { |
||||
|
@Input() |
||||
|
public field: TableField; |
||||
|
|
||||
|
@Input() |
||||
|
public content: ContentDto; |
||||
|
|
||||
|
@Input() |
||||
|
public patchAllowed: boolean; |
||||
|
|
||||
|
@Input() |
||||
|
public patchForm: FormGroup; |
||||
|
|
||||
|
@Input() |
||||
|
public language: LanguageDto; |
||||
|
|
||||
|
public value: any; |
||||
|
|
||||
|
public ngOnChanges() { |
||||
|
this.reset(); |
||||
|
} |
||||
|
|
||||
|
public reset() { |
||||
|
if (Types.is(this.field, RootFieldDto)) { |
||||
|
const { value, formatted } = getContentValue(this.content, this.language, this.field); |
||||
|
|
||||
|
if (this.patchForm) { |
||||
|
const formControl = this.patchForm.controls[this.field.name]; |
||||
|
|
||||
|
if (formControl) { |
||||
|
formControl.setValue(value); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
this.value = formatted; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
public get metaFields() { |
||||
|
return MetaFields; |
||||
|
} |
||||
|
|
||||
|
public get isInlineEditable() { |
||||
|
return Types.is(this.field, RootFieldDto) ? this.field.isInlineEditable : false; |
||||
|
} |
||||
|
|
||||
|
public get fieldName() { |
||||
|
return Types.is(this.field, RootFieldDto) ? this.field.name : this.field; |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,115 @@ |
|||||
|
/* |
||||
|
* Squidex Headless CMS |
||||
|
* |
||||
|
* @license |
||||
|
* Copyright (c) Squidex UG (haftungsbeschränkt). All rights reserved. |
||||
|
*/ |
||||
|
|
||||
|
import { ChangeDetectionStrategy, Component, EventEmitter, Input, Output } from '@angular/core'; |
||||
|
|
||||
|
import { |
||||
|
LanguageDto, |
||||
|
MetaFields, |
||||
|
Query, |
||||
|
RootFieldDto, |
||||
|
TableField, |
||||
|
Types |
||||
|
} from '@app/shared'; |
||||
|
|
||||
|
@Component({ |
||||
|
selector: 'sqx-content-list-header', |
||||
|
template: ` |
||||
|
<ng-container [ngSwitch]="fieldName"> |
||||
|
<ng-container *ngSwitchCase="metaFields.id"> |
||||
|
<sqx-table-header text="Id"></sqx-table-header> |
||||
|
</ng-container> |
||||
|
<ng-container *ngSwitchCase="metaFields.created"> |
||||
|
<sqx-table-header text="Created" |
||||
|
[sortable]="true" |
||||
|
[fieldPath]="'created'" |
||||
|
[query]="query" |
||||
|
(queryChange)="queryChange.emit($event)" |
||||
|
[language]="language"> |
||||
|
</sqx-table-header> |
||||
|
</ng-container> |
||||
|
<ng-container *ngSwitchCase="metaFields.createdByAvatar"> |
||||
|
<sqx-table-header text="By"></sqx-table-header> |
||||
|
</ng-container> |
||||
|
<ng-container *ngSwitchCase="metaFields.createdByName"> |
||||
|
<sqx-table-header text="Created By"></sqx-table-header> |
||||
|
</ng-container> |
||||
|
<ng-container *ngSwitchCase="metaFields.lastModified"> |
||||
|
<sqx-table-header text="Updated" |
||||
|
[sortable]="true" |
||||
|
[fieldPath]="'lastModified'" |
||||
|
[query]="query" |
||||
|
(queryChange)="queryChange.emit($event)" |
||||
|
[language]="language"> |
||||
|
</sqx-table-header> |
||||
|
</ng-container> |
||||
|
<ng-container *ngSwitchCase="metaFields.lastModifiedByAvatar"> |
||||
|
<sqx-table-header text="By"></sqx-table-header> |
||||
|
</ng-container> |
||||
|
<ng-container *ngSwitchCase="metaFields.lastModifiedByName"> |
||||
|
<sqx-table-header text="Modified By"></sqx-table-header> |
||||
|
</ng-container> |
||||
|
<ng-container *ngSwitchCase="metaFields.status"> |
||||
|
<sqx-table-header text="Status"></sqx-table-header> |
||||
|
</ng-container> |
||||
|
<ng-container *ngSwitchCase="metaFields.statusColor"> |
||||
|
<sqx-table-header text="Status"></sqx-table-header> |
||||
|
</ng-container> |
||||
|
<ng-container *ngSwitchCase="metaFields.version"> |
||||
|
<sqx-table-header text="Version"></sqx-table-header> |
||||
|
</ng-container> |
||||
|
<ng-container *ngSwitchDefault> |
||||
|
<sqx-table-header [text]="fieldDisplayName" |
||||
|
[sortable]="isSortable" |
||||
|
[fieldPath]="fieldPath" |
||||
|
[query]="query" |
||||
|
(queryChange)="queryChange.emit($event)" |
||||
|
[language]="language"> |
||||
|
</sqx-table-header> |
||||
|
</ng-container> |
||||
|
</ng-container>`, |
||||
|
changeDetection: ChangeDetectionStrategy.OnPush |
||||
|
}) |
||||
|
export class ContentListHeaderComponent { |
||||
|
@Input() |
||||
|
public field: TableField; |
||||
|
|
||||
|
@Output() |
||||
|
public queryChange = new EventEmitter<Query>(); |
||||
|
|
||||
|
@Input() |
||||
|
public query: Query; |
||||
|
|
||||
|
@Input() |
||||
|
public language: LanguageDto; |
||||
|
|
||||
|
public get metaFields() { |
||||
|
return MetaFields; |
||||
|
} |
||||
|
|
||||
|
public get isSortable() { |
||||
|
return Types.is(this.field, RootFieldDto) ? this.field.properties.isSortable : false; |
||||
|
} |
||||
|
|
||||
|
public get fieldName() { |
||||
|
return Types.is(this.field, RootFieldDto) ? this.field.name : this.field; |
||||
|
} |
||||
|
|
||||
|
public get fieldDisplayName() { |
||||
|
return Types.is(this.field, RootFieldDto) ? this.field.displayName : ''; |
||||
|
} |
||||
|
|
||||
|
public get fieldPath() { |
||||
|
if (Types.isString(this.field)) { |
||||
|
return this.field; |
||||
|
} else if (this.field.isLocalizable && this.language) { |
||||
|
return `data.${this.field.name}.${this.language.iso2Code}`; |
||||
|
} else { |
||||
|
return `data.${this.field.name}.iv`; |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -1,2 +1,19 @@ |
|||||
@import '_vars'; |
@import '_vars'; |
||||
@import '_mixins'; |
@import '_mixins'; |
||||
|
|
||||
|
.inline-edit { |
||||
|
& { |
||||
|
position: relative; |
||||
|
} |
||||
|
|
||||
|
.edit-menu { |
||||
|
@include absolute(.75rem, auto, auto, .25rem); |
||||
|
border-right: 1px solid $color-border; |
||||
|
min-height: 2.4rem; |
||||
|
max-height: 2.4rem; |
||||
|
padding-right: 1rem; |
||||
|
white-space: nowrap; |
||||
|
background: $color-table-background; |
||||
|
z-index: 100; |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,64 @@ |
|||||
|
/* |
||||
|
* Squidex Headless CMS |
||||
|
* |
||||
|
* @license |
||||
|
* Copyright (c) Squidex UG (haftungsbeschränkt). All rights reserved. |
||||
|
*/ |
||||
|
|
||||
|
describe('ArrayExtensions', () => { |
||||
|
it('should return same array when replaying by property with null value', () => { |
||||
|
const array_0 = [{ id: 1 }, { id: 2 }]; |
||||
|
const array_1 = array_0.replaceBy('id', null!); |
||||
|
|
||||
|
expect(array_1).toBe(array_0); |
||||
|
}); |
||||
|
|
||||
|
it('should return new array when replaying by property', () => { |
||||
|
const array_0 = [{ id: 1, v: 10 }, { id: 2, v: 20 }]; |
||||
|
const array_1 = array_0.replaceBy('id', { id: 1, v: 30 }); |
||||
|
|
||||
|
expect(array_1).toEqual([{ id: 1, v: 30 }, { id: 2, v: 20 }]); |
||||
|
}); |
||||
|
|
||||
|
it('should return same array when removing by property with null value', () => { |
||||
|
const array_0 = [{ id: 1 }, { id: 2 }]; |
||||
|
const array_1 = array_0.removeBy('id', null!); |
||||
|
|
||||
|
expect(array_1).toBe(array_0); |
||||
|
}); |
||||
|
|
||||
|
it('should return new array when removing by property', () => { |
||||
|
const array_0 = [{ id: 1 }, { id: 2 }]; |
||||
|
const array_1 = array_0.removeBy('id', { id: 1 }); |
||||
|
|
||||
|
expect(array_1).toEqual([{ id: 2 }]); |
||||
|
}); |
||||
|
|
||||
|
it('should return same array when removing with null value', () => { |
||||
|
const array_0 = [1, 2, 3]; |
||||
|
const array_1 = array_0.removed(null!); |
||||
|
|
||||
|
expect(array_1).toBe(array_0); |
||||
|
}); |
||||
|
|
||||
|
it('should return new array when removing', () => { |
||||
|
const array_0 = [1, 2, 3]; |
||||
|
const array_1 = array_0.removed(2); |
||||
|
|
||||
|
expect(array_1).toEqual([1, 3]); |
||||
|
}); |
||||
|
|
||||
|
it('should sort by value', () => { |
||||
|
const array_0 = [3, 1, 2]; |
||||
|
const array_1 = array_0.sorted(); |
||||
|
|
||||
|
expect(array_1).toEqual([1, 2, 3]); |
||||
|
}); |
||||
|
|
||||
|
it('should sort by property', () => { |
||||
|
const array_0 = [{ id: 'C' }, { id: 'b' }, { id: 'A' }]; |
||||
|
const array_1 = array_0.sortedByString(x => x.id); |
||||
|
|
||||
|
expect(array_1).toEqual([{ id: 'A' }, { id: 'b' }, { id: 'C' }]); |
||||
|
}); |
||||
|
}); |
||||
@ -0,0 +1,73 @@ |
|||||
|
|
||||
|
/* |
||||
|
* Squidex Headless CMS |
||||
|
* |
||||
|
* @license |
||||
|
* Copyright (c) Squidex UG (haftungsbeschränkt). All rights reserved. |
||||
|
*/ |
||||
|
|
||||
|
// tslint:disable: readonly-array
|
||||
|
|
||||
|
interface ReadonlyArray<T> { |
||||
|
replaceBy(field: string, value: T): ReadonlyArray<T>; |
||||
|
|
||||
|
removeBy(field: string, value: T): ReadonlyArray<T>; |
||||
|
|
||||
|
removed(value?: T): ReadonlyArray<T>; |
||||
|
|
||||
|
sorted(): ReadonlyArray<T>; |
||||
|
|
||||
|
sortedByString(selector: (value: T) => string): ReadonlyArray<T>; |
||||
|
} |
||||
|
|
||||
|
interface Array<T> { |
||||
|
replaceBy(field: string, value: T): Array<T>; |
||||
|
|
||||
|
removeBy(field: string, value: T): Array<T>; |
||||
|
|
||||
|
removed(value?: T): Array<T>; |
||||
|
|
||||
|
sorted(): Array<T>; |
||||
|
|
||||
|
sortedByString(selector: (value: T) => string): Array<T>; |
||||
|
} |
||||
|
|
||||
|
Array.prototype.replaceBy = function<T>(field: string, value: T) { |
||||
|
if (!value) { |
||||
|
return this; |
||||
|
} |
||||
|
|
||||
|
return this.map((v: T) => v[field] === value[field] ? value : v); |
||||
|
}; |
||||
|
|
||||
|
Array.prototype.removeBy = function<T>(field: string, value: T) { |
||||
|
if (!value) { |
||||
|
return this; |
||||
|
} |
||||
|
|
||||
|
return this.filter((v: T) => v[field] !== value[field]); |
||||
|
}; |
||||
|
|
||||
|
Array.prototype.removed = function<T>(value?: T) { |
||||
|
if (!value) { |
||||
|
return this; |
||||
|
} |
||||
|
|
||||
|
return this.filter((v: T) => v !== value); |
||||
|
}; |
||||
|
|
||||
|
Array.prototype.sorted = function() { |
||||
|
const copy = [...this]; |
||||
|
|
||||
|
copy.sort(); |
||||
|
|
||||
|
return copy; |
||||
|
}; |
||||
|
|
||||
|
Array.prototype.sortedByString = function<T>(selector: (value: T) => string) { |
||||
|
const copy = [...this]; |
||||
|
|
||||
|
copy.sort((a, b) => selector(a).localeCompare(selector(b), undefined, { sensitivity: 'base' })); |
||||
|
|
||||
|
return copy; |
||||
|
}; |
||||
@ -0,0 +1,10 @@ |
|||||
|
/* |
||||
|
* Squidex Headless CMS |
||||
|
* |
||||
|
* @license |
||||
|
* Copyright (c) Squidex UG (haftungsbeschränkt). All rights reserved. |
||||
|
*/ |
||||
|
|
||||
|
export function compareStrings(a: string, b: string) { |
||||
|
return a.localeCompare(b, undefined, { sensitivity: 'base' }); |
||||
|
} |
||||
Loading…
Reference in new issue