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 '_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