Browse Source

UI improvements.

pull/253/head
Sebastian Stehle 8 years ago
parent
commit
ec37f2fcec
  1. 2
      src/Squidex.Domain.Apps.Core.Model/Schemas/BooleanFieldProperties.cs
  2. 4
      src/Squidex/app/app.routes.ts
  3. 12
      src/Squidex/app/features/content/pages/content/content-field.component.html
  4. 39
      src/Squidex/app/features/content/pages/content/content-field.component.ts
  5. 34
      src/Squidex/app/features/content/pages/content/content-page.component.ts
  6. 7
      src/Squidex/app/features/content/pages/contents/contents-page.component.html
  7. 19
      src/Squidex/app/features/content/pages/contents/contents-page.component.ts
  8. 79
      src/Squidex/app/features/content/shared/content-item.component.html
  9. 4
      src/Squidex/app/features/content/shared/content-item.component.scss
  10. 103
      src/Squidex/app/features/content/shared/content-item.component.ts
  11. 2
      src/Squidex/app/features/content/shared/references-editor.component.html
  12. 3
      src/Squidex/app/features/content/shared/references-editor.component.ts
  13. 2
      src/Squidex/app/framework/angular/control-errors.component.html
  14. 98
      src/Squidex/app/framework/angular/control-errors.component.ts
  15. 6
      src/Squidex/app/framework/angular/http-extensions-impl.ts
  16. 62
      src/Squidex/app/framework/angular/modal-view.directive.ts
  17. 2
      src/Squidex/app/framework/utils/string-helper.ts
  18. 4
      src/Squidex/app/shared/components/language-selector.component.html
  19. 15
      src/Squidex/app/shared/services/contents.service.spec.ts
  20. 15
      src/Squidex/app/shared/services/contents.service.ts
  21. 12
      src/Squidex/app/shared/services/schemas.service.ts

2
src/Squidex.Domain.Apps.Core.Model/Schemas/BooleanFieldProperties.cs

@ -14,6 +14,8 @@ namespace Squidex.Domain.Apps.Core.Schemas
{
public bool? DefaultValue { get; set; }
public bool InlineEditable { get; set; }
public BooleanFieldEditor Editor { get; set; }
public override T Accept<T>(IFieldPropertiesVisitor<T> visitor)

4
src/Squidex/app/app.routes.ts

@ -6,7 +6,7 @@
*/
import { ModuleWithProviders } from '@angular/core';
import { PreloadAllModules, RouterModule, Routes } from '@angular/router';
import { RouterModule, Routes } from '@angular/router';
import {
AppAreaComponent,
@ -96,4 +96,4 @@ export const routes: Routes = [
}
];
export const routing: ModuleWithProviders = RouterModule.forRoot(routes, { useHash: false, preloadingStrategy: PreloadAllModules });
export const routing: ModuleWithProviders = RouterModule.forRoot(routes, { useHash: false });

12
src/Squidex/app/features/content/pages/content/content-field.component.html

@ -6,7 +6,7 @@
<span class="field-disabled" *ngIf="field.isDisabled">Disabled</span>
<div [formGroup]="fieldForm">
<div *ngIf="field.partitioning === 'language' && languages.length > 1">
<div *ngIf="field.isLocalizable && languages.length > 1">
<div class="languages-buttons" #buttonLanguages>
<sqx-language-selector size="sm" (selectedLanguageChanged)="selectLanguage($event)" [languages]="languages"></sqx-language-selector>
</div>
@ -24,7 +24,7 @@
<div *ngSwitchCase="'Number'">
<div [ngSwitch]="field.properties.editor">
<div *ngSwitchCase="'Input'">
<input class="form-control" type="number" [formControlName]="partition" [placeholder]="field.properties.placeholder || ''" />
<input class="form-control" type="number" [formControlName]="partition" [placeholder]="field.displayPlaceholder" />
</div>
<div *ngSwitchCase="'Stars'">
<sqx-stars [formControlName]="partition" [maximumStars]="field.properties.maxValue"></sqx-stars>
@ -48,13 +48,13 @@
<div *ngSwitchCase="'String'">
<div [ngSwitch]="field.properties.editor">
<div *ngSwitchCase="'Input'">
<input class="form-control" type="text" [formControlName]="partition" [placeholder]="field.properties.placeholder || ''" />
<input class="form-control" type="text" [formControlName]="partition" [placeholder]="field.displayPlaceholder" />
</div>
<div *ngSwitchCase="'Slug'">
<input class="form-control" type="text" [formControlName]="partition" [placeholder]="field.properties.placeholder || ''" sqxSlugifyInput />
<input class="form-control" type="text" [formControlName]="partition" [placeholder]="field.displayPlaceholder" sqxSlugifyInput />
</div>
<div *ngSwitchCase="'TextArea'">
<textarea class="form-control" [formControlName]="partition" rows="5" [placeholder]="field.properties.placeholder || ''"></textarea>
<textarea class="form-control" [formControlName]="partition" rows="5" [placeholder]="field.displayPlaceholder"></textarea>
</div>
<div *ngSwitchCase="'RichText'">
<sqx-rich-editor [formControlName]="partition" (assetPluginClicked)="assetPluginClicked()"></sqx-rich-editor>
@ -106,7 +106,7 @@
<sqx-tag-editor [formControlName]="partition"></sqx-tag-editor>
</div>
<div *ngSwitchCase="'References'">
<sqx-references-editor [formControlName]="partition" [languageCode]="selectFieldLanguage(partition)" [schemaId]="field.properties.schemaId"></sqx-references-editor>
<sqx-references-editor [formControlName]="partition" [language]="selectedLanguage" [schemaId]="field.properties.schemaId"></sqx-references-editor>
</div>
</div>
</div>

39
src/Squidex/app/features/content/pages/content/content-field.component.ts

@ -7,9 +7,9 @@
import { Component, Input, OnInit } from '@angular/core';
import { FormGroup } from '@angular/forms';
import { ActivatedRoute, Router } from '@angular/router';
import { AppLanguageDto, FieldDto } from 'shared';
import { ActivatedRoute, Router } from '@angular/router';
@Component({
selector: 'sqx-content-field',
@ -17,10 +17,6 @@ import { ActivatedRoute, Router } from '@angular/router';
templateUrl: './content-field.component.html'
})
export class ContentFieldComponent implements OnInit {
constructor(private readonly router: Router, private readonly route: ActivatedRoute) {
}
private masterLanguageCode: string;
@Input()
public field: FieldDto;
@ -33,35 +29,38 @@ export class ContentFieldComponent implements OnInit {
@Input()
public contentFormSubmitted: boolean;
public fieldPartitions: string[];
public fieldPartition: string;
public selectedFormControl: string;
public selectedLanguage: AppLanguageDto;
public selectLanguage(language: AppLanguageDto) {
this.fieldPartition = language.iso2Code;
constructor(
private readonly router: Router,
private readonly route: ActivatedRoute
) {
}
public ngOnInit() {
this.masterLanguageCode = this.languages.find(l => l.isMaster)!.iso2Code;
if (this.field.isDisabled) {
this.fieldForm.disable();
}
if (this.field.partitioning === 'language') {
this.fieldPartitions = this.languages.map(t => t.iso2Code);
this.fieldPartition = this.fieldPartitions[0];
const masterLanguage = this.languages.find(l => l.isMaster)!;
if (this.field.isLocalizable) {
this.selectedFormControl = masterLanguage.iso2Code;
} else {
this.fieldPartitions = ['iv'];
this.fieldPartition = 'iv';
this.selectedFormControl = 'iv';
}
this.selectedLanguage = masterLanguage;
}
public assetPluginClicked() {
this.router.navigate(['assets'], { relativeTo: this.route });
public selectLanguage(language: AppLanguageDto) {
this.selectedFormControl = language.iso2Code;
this.selectedLanguage = language;
}
public selectFieldLanguage(partition: string) {
return partition === 'iv' ? this.masterLanguageCode : partition;
public assetPluginClicked() {
this.router.navigate(['assets'], { relativeTo: this.route });
}
}

34
src/Squidex/app/features/content/pages/content/content-page.component.ts

@ -40,6 +40,7 @@ import {
export class ContentPageComponent implements CanComponentDeactivate, OnDestroy, OnInit {
private contentStatusChangedSubscription: Subscription;
private contentDeletedSubscription: Subscription;
private contentUpdatedSubscription: Subscription;
private contentVersionSelectedSubscription: Subscription;
public schema: SchemaDetailsDto;
@ -62,6 +63,7 @@ export class ContentPageComponent implements CanComponentDeactivate, OnDestroy,
public ngOnDestroy() {
this.contentVersionSelectedSubscription.unsubscribe();
this.contentStatusChangedSubscription.unsubscribe();
this.contentUpdatedSubscription.unsubscribe();
this.contentDeletedSubscription.unsubscribe();
}
@ -80,6 +82,14 @@ export class ContentPageComponent implements CanComponentDeactivate, OnDestroy,
}
});
this.contentUpdatedSubscription =
this.ctx.bus.of(ContentUpdated)
.subscribe(message => {
if (this.content && message.content.id === this.content.id) {
this.reloadContentForm(message.content);
}
});
this.contentStatusChangedSubscription =
this.ctx.bus.of(ContentStatusChanged)
.subscribe(message => {
@ -102,9 +112,7 @@ export class ContentPageComponent implements CanComponentDeactivate, OnDestroy,
this.ctx.route.data.map(d => d.content)
.subscribe((content: ContentDto) => {
this.content = content;
this.populateContentForm();
this.reloadContentForm(content);
});
}
@ -122,7 +130,7 @@ export class ContentPageComponent implements CanComponentDeactivate, OnDestroy,
this.contentOld = null;
this.emitContentUpdated(this.content);
this.populateContentForm();
this.reloadContentForm(this.content);
}
}
@ -159,13 +167,13 @@ export class ContentPageComponent implements CanComponentDeactivate, OnDestroy,
} else {
this.contentsService.putContent(this.ctx.appName, this.schema.name, this.content.id, requestDto, this.content.version)
.subscribe(dto => {
this.content = this.content.update(dto.payload, this.ctx.userToken, dto.version);
const content = this.content.update(dto.payload, this.ctx.userToken, dto.version);
this.ctx.notifyInfo('Content saved successfully.');
this.emitContentUpdated(this.content);
this.enableContentForm();
this.populateContentForm();
this.reloadContentForm(content);
}, error => {
this.ctx.notifyError(error);
@ -187,12 +195,9 @@ export class ContentPageComponent implements CanComponentDeactivate, OnDestroy,
this.contentOld = null;
}
this.content = this.content.setData(dto);
this.ctx.notifyInfo('Content version loaded successfully.');
this.emitContentUpdated(this.content);
this.populateContentForm();
this.reloadContentForm(this.content.setData(dto));
}, error => {
this.ctx.notifyError(error);
});
@ -235,12 +240,12 @@ export class ContentPageComponent implements CanComponentDeactivate, OnDestroy,
for (const field of schema.fields) {
const group = new FormGroup({});
if (field.partitioning === 'language') {
if (field.isLocalizable) {
for (let language of this.languages) {
group.addControl(language.iso2Code, new FormControl(undefined, field.createValidators(language.isOptional)));
group.setControl(language.iso2Code, new FormControl(undefined, field.createValidators(language.isOptional)));
}
} else {
group.addControl('iv', new FormControl(undefined, field.createValidators(false)));
group.setControl('iv', new FormControl(undefined, field.createValidators(false)));
}
controls[field.name] = group;
@ -249,7 +254,8 @@ export class ContentPageComponent implements CanComponentDeactivate, OnDestroy,
this.contentForm = new FormGroup(controls);
}
private populateContentForm() {
private reloadContentForm(content: ContentDto) {
this.content = content;
this.contentForm.markAsPristine();
this.isNewMode = !this.content;

7
src/Squidex/app/features/content/pages/contents/contents-page.component.html

@ -124,7 +124,7 @@
<tbody *ngIf="!isReadOnly">
<ng-template ngFor let-content [ngForOf]="contentItems">
<tr [sqxContent]="content" [routerLink]="[content.id]" routerLinkActive="active"
[languageCode]="languageSelected.iso2Code"
[language]="languageSelected"
[schemaFields]="contentFields"
[schema]="schema"
[selected]="isItemSelected(content)"
@ -133,7 +133,8 @@
(publishing)="publishContent(content)"
(archiving)="archiveContent(content)"
(restoring)="restoreContent(content)"
(deleting)="deleteContent(content)"></tr>
(deleting)="deleteContent(content)"
(saved)="onContentSaved(content, $event)"></tr>
<tr class="spacer"></tr>
</ng-template>
</tbody>
@ -141,7 +142,7 @@
<tbody *ngIf="isReadOnly">
<ng-template ngFor let-content [ngForOf]="contentItems">
<tr [sqxContent]="content" dnd-draggable [dragData]="dropData(content)"
[languageCode]="languageSelected.iso2Code"
[language]="languageSelected"
[schemaFields]="contentFields"
[schema]="schema"
isReadOnly="true"></tr>

19
src/Squidex/app/features/content/pages/contents/contents-page.component.ts

@ -27,7 +27,8 @@ import {
ImmutableArray,
ModalView,
Pager,
SchemaDetailsDto
SchemaDetailsDto,
Versioned
} from 'shared';
@Component({
@ -203,7 +204,9 @@ export class ContentsPageComponent implements OnDestroy, OnInit {
DateTime.parseISO_UTC(dueTime) :
null;
this.contentItems = this.contentItems.replaceBy('id', content.changeStatus(status, dt, this.ctx.userToken, dto.version));
content = content.changeStatus(status, dt, this.ctx.userToken, dto.version);
this.contentItems = this.contentItems.replaceBy('id', content);
this.emitContentStatusChanged(content);
}
@ -240,6 +243,14 @@ export class ContentsPageComponent implements OnDestroy, OnInit {
});
}
public onContentSaved(content: ContentDto, update: Versioned<any>) {
content = content.update(update.payload, this.ctx.userToken, update.version);
this.contentItems = this.contentItems.replaceBy('id', content);
this.emitContentUpdated(content);
}
public load(showInfo = false) {
this.contentsService.getContents(this.ctx.appName, this.schema.name, this.contentsPager.pageSize, this.contentsPager.skip, this.contentsQuery, undefined, this.isArchive)
.finally(() => {
@ -342,6 +353,10 @@ export class ContentsPageComponent implements OnDestroy, OnInit {
this.ctx.bus.emit(new ContentStatusChanged(content));
}
private emitContentUpdated(content: ContentDto) {
this.ctx.bus.emit(new ContentUpdated(content));
}
private emitContentRemoved(content: ContentDto) {
this.ctx.bus.emit(new ContentRemoved(content));
}

79
src/Squidex/app/features/content/shared/content-item.component.html

@ -1,15 +1,61 @@
<td class="cell-select" *ngIf="!isReadOnly">
<input type="checkbox" class="form-control"
<td class="cell-select" *ngIf="!isReadOnly" (click)="shouldStop($event)">
<input type="checkbox" class="form-control"
[ngModel]="selected"
(ngModelChange)="selectedChange.emit($event);"
(click)="$event.stopPropagation()" />
</td>
<td class="cell-auto" *ngFor="let value of values">
<span class="table-cell">
{{value}}
</span>
<td class="cell-auto" *ngFor="let field of schemaFields; let i = index" (click)="shouldStop($event)">
<div *ngIf="field.properties.inlineEditable && !isReadOnly" [formGroup]="form" (click)="$event.stopPropagation()">
<div [ngSwitch]="field.properties.fieldType">
<div *ngSwitchCase="'Number'">
<div [ngSwitch]="field.properties.editor">
<div *ngSwitchCase="'Input'">
<input class="form-control" type="number" [formControlName]="field.name" [placeholder]="field.displayPlaceholder" />
</div>
<div *ngSwitchCase="'Dropdown'">
<select class="form-control" [formControlName]="field.name">
<option [ngValue]="null"></option>
<option *ngFor="let value of field.properties.allowedValues" [ngValue]="value">{{value}}</option>
</select>
</div>
</div>
</div>
<div *ngSwitchCase="'String'">
<div [ngSwitch]="field.properties.editor">
<div *ngSwitchCase="'Input'">
<input class="form-control" type="text" [formControlName]="field.name" [placeholder]="field.displayPlaceholder" />
</div>
<div *ngSwitchCase="'Slug'">
<input class="form-control" type="text" [formControlName]="field.name" [placeholder]="field.displayPlaceholder" sqxSlugifyInput />
</div>
<div *ngSwitchCase="'Dropdown'">
<select class="form-control" [formControlName]="field.name">
<option [ngValue]="null"></option>
<option *ngFor="let value of field.properties.allowedValues" [ngValue]="value">{{value}}</option>
</select>
</div>
</div>
</div>
<div *ngSwitchCase="'Boolean'">
<div [ngSwitch]="field.properties.editor">
<div *ngSwitchCase="'Toggle'" [formControlName]="field.name">
<sqx-toggle></sqx-toggle>
</div>
<div *ngSwitchCase="'Checkbox'">
<div class="form-check form-check-inline">
<input class="form-check-input" type="checkbox" [formControlName]="field.name" sqxIndeterminateValue />
</div>
</div>
</div>
</div>
</div>
</div>
<div *ngIf="!field.properties.inlineEditable || isReadOnly" class="truncate">
{{values[i]}}
</div>
</td>
<td class="cell-time">
<td class="cell-time" (click)="shouldStop($event)">
<span *ngIf="!content.scheduledTo">
<span class="content-status content-status-{{content.status | lowercase}}" #statusIcon>
<i class="icon-circle"></i>
@ -28,10 +74,23 @@
<small class="item-modified">{{content.lastModified | sqxFromNow}}</small>
</td>
<td class="cell-user">
<td class="cell-user" *ngIf="form.dirty" (click)="shouldStop($event)">
<button type="button" class="btn btn-success" (click)="save(); $event.stopPropagation()">
<i class="icon-checkmark"></i>
</button>
</td>
<td class="cell-actions" *ngIf="form.dirty" (click)="shouldStop($event)">
<button type="button" class="btn btn-link btn-secondary btn-cancel" (click)="save(); $event.stopPropagation()">
<i class="icon-close"></i>
</button>
</td>
<td class="cell-user" *ngIf="form.pristine" (click)="shouldStop($event)">
<img class="user-picture" [attr.title]="content.lastModifiedBy | sqxUserNameRef" [attr.src]="content.lastModifiedBy | sqxUserPictureRef" />
</td>
<td class="cell-actions" *ngIf="!isReadOnly">
<td class="cell-actions" *ngIf="!isReadOnly && form.pristine" (click)="shouldStop($event)">
<div class="dropdown dropdown-options" *ngIf="content">
<button type="button" class="btn btn-link btn-secondary" (click)="dropdown.toggle(); $event.stopPropagation()" [class.active]="dropdown.isOpen | async" #optionsButton>
<i class="icon-dots"></i>
@ -58,7 +117,7 @@
</div>
</div>
</td>
<td class="cell-actions" *ngIf="isReference">
<td class="cell-actions" *ngIf="isReference" (click)="shouldStop($event)">
<button type="button" class="btn btn-link btn-secondary" (click)="deleting.emit(); $event.stopPropagation()">
<i class="icon-close"></i>
</button>

4
src/Squidex/app/features/content/shared/content-item.component.scss

@ -1,6 +1,10 @@
@import '_vars';
@import '_mixins';
.truncate {
@include truncate;
}
.content-status {
& {
vertical-align: middle;

103
src/Squidex/app/features/content/shared/content-item.component.ts

@ -5,15 +5,20 @@
* Copyright (c) Squidex UG (haftungsbeschränkt). All rights reserved.
*/
import { ChangeDetectionStrategy, Component, EventEmitter, Input, OnChanges, OnInit, Output } from '@angular/core';
import { Component, EventEmitter, Input, OnChanges, OnInit, Output } from '@angular/core';
import { FormControl, FormGroup } from '@angular/forms';
import {
AppContext,
AppLanguageDto,
ContentDto,
ContentsService,
fadeAnimation,
FieldDto,
ModalView,
SchemaDto
SchemaDto,
Types,
Versioned
} from 'shared';
/* tslint:disable:component-selector */
@ -27,8 +32,7 @@ import {
],
animations: [
fadeAnimation
],
changeDetection: ChangeDetectionStrategy.OnPush
]
})
export class ContentItemComponent implements OnInit, OnChanges {
@Output()
@ -46,6 +50,9 @@ export class ContentItemComponent implements OnInit, OnChanges {
@Output()
public deleting = new EventEmitter();
@Output()
public saved = new EventEmitter<Versioned<any>>();
@Output()
public selectedChange = new EventEmitter();
@ -53,10 +60,7 @@ export class ContentItemComponent implements OnInit, OnChanges {
public selected = false;
@Input()
public columnWidth: number;
@Input()
public languageCode: string;
public language: AppLanguageDto;
@Input()
public schemaFields: FieldDto[];
@ -73,11 +77,15 @@ export class ContentItemComponent implements OnInit, OnChanges {
@Input('sqxContent')
public content: ContentDto;
public formSubmitted = false;
public form: FormGroup = new FormGroup({});
public dropdown = new ModalView(false, true);
public values: any[] = [];
constructor(public readonly ctx: AppContext
constructor(public readonly ctx: AppContext,
private readonly contentsService: ContentsService
) {
}
@ -86,31 +94,96 @@ export class ContentItemComponent implements OnInit, OnChanges {
}
public ngOnInit() {
for (let field of this.schemaFields) {
if (field.properties['inlineEditable']) {
this.form.setControl(field.name, new FormControl(undefined, field.createValidators(this.language.isOptional)));
}
}
this.updateValues();
}
public shouldStop(event: Event) {
if (this.form.dirty) {
event.stopPropagation();
event.stopImmediatePropagation();
}
}
public save() {
this.formSubmitted = true;
if (this.form.dirty && this.form.valid) {
this.form.disable();
const request = {};
for (let field of this.schemaFields) {
if (field.properties['inlineEditable']) {
const value = this.form.controls[field.name].value;
if (field.partitioning === 'invariant') {
request[field.name] = { iv: value };
} else {
request[field.name] = { [this.language.iso2Code]: value };
}
}
}
this.contentsService.patchContent(this.ctx.appName, this.schema.name, this.content.id, request, this.content.version)
.finally(() => {
this.form.enable();
})
.subscribe(dto => {
this.form.markAsPristine();
this.emitSaved(dto);
}, error => {
this.ctx.notifyError(error);
});
}
}
private emitSaved(data: Versioned<any>) {
this.saved.emit(data);
}
private updateValues() {
this.values = [];
if (this.schemaFields) {
for (let field of this.schemaFields) {
this.values.push(this.getValue(field));
const value = this.getRawValue(field);
if (Types.isUndefined(value)) {
this.values.push('');
} else {
this.values.push(field.formatValue(value));
}
if (this.form) {
const formControl = this.form.controls[field.name];
if (formControl) {
formControl.setValue(value);
}
}
}
}
}
private getValue(field: FieldDto): any {
private getRawValue(field: FieldDto): any {
const contentField = this.content.data[field.name];
if (contentField) {
if (field.partitioning === 'language') {
return field.formatValue(contentField[this.languageCode]);
return contentField[this.language.iso2Code];
} else {
return field.formatValue(contentField['iv']);
return contentField['iv'];
}
} else {
return '';
}
return undefined;
}
}

2
src/Squidex/app/features/content/shared/references-editor.component.html

@ -13,7 +13,7 @@
<tbody dnd-sortable-container [sortableData]="contentItems.mutableValues">
<ng-template ngFor let-content let-i="index" [ngForOf]="contentItems">
<tr [sqxContent]="content" dnd-sortable [sortableIndex]="i" (sqxSorted)="onContentsSorted($event)"
[languageCode]="languageCode"
[language]="language"
[schemaFields]="contentFields"
[schema]="schema"
(deleting)="onContentRemoving(content)"

3
src/Squidex/app/features/content/shared/references-editor.component.ts

@ -12,6 +12,7 @@ import { ControlValueAccessor, NG_VALUE_ACCESSOR } from '@angular/forms';
import {
AppContext,
AppLanguageDto,
ContentDto,
ContentsService,
FieldDto,
@ -43,7 +44,7 @@ export class ReferencesEditorComponent implements ControlValueAccessor, OnInit {
public schemaId: string;
@Input()
public languageCode: string;
public language: AppLanguageDto;
public schema: SchemaDetailsDto;

2
src/Squidex/app/framework/angular/control-errors.component.html

@ -1,4 +1,4 @@
<div class="errors-container" *ngIf="errorMessages" @fade>
<div class="errors-container" *ngIf="errorMessages.length > 0" @fade>
<div class="errors">
<span *ngFor="let message of errorMessages">
{{message}}

98
src/Squidex/app/framework/angular/control-errors.component.ts

@ -5,8 +5,9 @@
* Copyright (c) Sebastian Stehle. All rights r vbeserved
*/
import { ChangeDetectionStrategy, Component, Host, Input, OnChanges, Optional } from '@angular/core';
import { ChangeDetectionStrategy, ChangeDetectorRef, Component, Host, Input, OnChanges, OnDestroy, Optional } from '@angular/core';
import { AbstractControl, FormGroupDirective } from '@angular/forms';
import { Observable, Subscription } from 'rxjs';
import { fadeAnimation } from './animations';
@ -33,9 +34,11 @@ const DEFAULT_ERRORS: { [key: string]: string } = {
],
changeDetection: ChangeDetectionStrategy.OnPush
})
export class ControlErrorsComponent implements OnChanges {
export class ControlErrorsComponent implements OnChanges, OnDestroy {
private displayFieldName: string;
private control: AbstractControl;
private controlSubscription: Subscription | null = null;
private originalMarkAsTouched: any;
@Input()
public for: string;
@ -52,17 +55,74 @@ export class ControlErrorsComponent implements OnChanges {
@Input()
public submitOnly = false;
public get errorMessages(): string[] | null {
if (!this.control) {
return null;
public errorMessages: string[] = [];
constructor(
@Optional() @Host() private readonly formGroupDirective: FormGroupDirective,
private readonly changeDetector: ChangeDetectorRef
) {
if (!this.formGroupDirective) {
throw new Error('control-errors must be used with a parent formGroup directive');
}
}
if (this.control.invalid && ((this.control.touched && !this.submitOnly) || this.submitted) && this.control.errors) {
const errors: string[] = [];
public ngOnDestroy() {
this.unsubscribe();
}
public ngOnChanges() {
if (this.fieldName) {
this.displayFieldName = this.fieldName;
} else if (this.for) {
this.displayFieldName = this.for.substr(0, 1).toUpperCase() + this.for.substr(1);
}
const control = this.formGroupDirective.form.controls[this.for];
if (this.control !== control) {
this.unsubscribe();
this.control = control;
if (control) {
const self = this;
this.controlSubscription =
Observable.merge(control.valueChanges, control.statusChanges)
.subscribe(() => {
this.createMessages();
});
this.originalMarkAsTouched = this.control.markAsTouched;
this.control['markAsTouched'] = function () {
self.originalMarkAsTouched.apply(this, arguments);
self.createMessages();
};
}
}
this.createMessages();
}
private unsubscribe() {
if (this.controlSubscription) {
this.controlSubscription.unsubscribe();
}
if (this.control && this.originalMarkAsTouched) {
this.control['markAsTouched'] = this.originalMarkAsTouched;
}
}
private createMessages() {
const errors: string[] = [];
if (this.control.invalid && ((this.control.touched && !this.submitOnly) || this.submitted) && this.control.errors) {
for (let key in <any>this.control.errors) {
if (this.control.errors.hasOwnProperty(key)) {
let message = (this.errors ? this.errors[key] : null) || DEFAULT_ERRORS[key];
let message = (this.errors ? this.errors[key] : null) || DEFAULT_ERRORS[key.toLowerCase()];
if (!message) {
continue;
@ -81,28 +141,10 @@ export class ControlErrorsComponent implements OnChanges {
errors.push(message);
}
}
return errors.length > 0 ? errors : null;
}
return null;
}
constructor(
@Optional() @Host() private readonly formGroupDirective: FormGroupDirective
) {
if (!this.formGroupDirective) {
throw new Error('control-errors must be used with a parent formGroup directive');
}
}
public ngOnChanges() {
if (this.fieldName) {
this.displayFieldName = this.fieldName;
} else if (this.for) {
this.displayFieldName = this.for.substr(0, 1).toUpperCase() + this.for.substr(1);
}
this.errorMessages = errors;
this.control = this.formGroupDirective.form.controls[this.for];
this.changeDetector.detectChanges();
}
}

6
src/Squidex/app/framework/angular/http-extensions-impl.ts

@ -87,6 +87,12 @@ export module HTTP {
return handleVersion(http.put<T>(url, body, { observe: 'response', headers }), version);
}
export function patchVersioned<T>(http: HttpClient, url: string, body: any, version?: Version): Observable<Versioned<HttpResponse<T>>> {
const headers = createHeaders(version);
return handleVersion(http.request<T>('PATCH', url, { body, observe: 'response', headers }), version);
}
export function deleteVersioned<T>(http: HttpClient, url: string, version?: Version): Observable<Versioned<HttpResponse<T>>> {
const headers = createHeaders(version);

62
src/Squidex/app/framework/angular/modal-view.directive.ts

@ -21,7 +21,7 @@ export class ModalViewDirective implements OnChanges, OnDestroy {
private renderedView: EmbeddedViewRef<any> | null = null;
@Input('sqxModalView')
public modalView: ModalView;
public modalView: ModalView | any;
@Input('sqxModalViewOnRoot')
public placeOnRoot = false;
@ -40,7 +40,9 @@ export class ModalViewDirective implements OnChanges, OnDestroy {
public ngOnDestroy() {
this.stopListening();
this.modalView.hide();
if (this.modalView instanceof ModalView) {
this.modalView.hide();
}
}
public ngOnChanges(changes: SimpleChanges) {
@ -53,36 +55,42 @@ export class ModalViewDirective implements OnChanges, OnDestroy {
this.subscription = null;
}
if (this.modalView) {
if (this.modalView instanceof ModalView) {
this.subscription =
this.modalView.isOpen.subscribe(isOpen => {
if (isOpen === (this.renderedView !== null)) {
return;
}
this.update(isOpen);
});
} else {
this.update(!!this.modalView);
}
}
if (isOpen && !this.renderedView) {
if (this.placeOnRoot) {
this.renderedView = this.rootContainer.createEmbeddedView(this.templateRef);
} else {
this.renderedView = this.viewContainer.createEmbeddedView(this.templateRef);
}
this.renderer.setElementStyle(this.renderedView.rootNodes[0], 'display', 'block');
setTimeout(() => {
this.startListening();
});
} else if (!isOpen && this.renderedView) {
this.renderedView = null;
if (this.placeOnRoot) {
this.rootContainer.clear();
} else {
this.viewContainer.clear();
}
private update(isOpen: boolean) {
if (isOpen === (this.renderedView !== null)) {
return;
}
this.stopListening();
}
if (isOpen && !this.renderedView) {
if (this.placeOnRoot) {
this.renderedView = this.rootContainer.createEmbeddedView(this.templateRef);
} else {
this.renderedView = this.viewContainer.createEmbeddedView(this.templateRef);
}
this.renderer.setElementStyle(this.renderedView.rootNodes[0], 'display', 'block');
setTimeout(() => {
this.startListening();
});
} else if (!isOpen && this.renderedView) {
this.renderedView = null;
if (this.placeOnRoot) {
this.rootContainer.clear();
} else {
this.viewContainer.clear();
}
this.stopListening();
}
}

2
src/Squidex/app/framework/utils/string-helper.ts

@ -6,7 +6,7 @@
*/
export module StringHelper {
export function firstNonEmpty(...values: string[]) {
export function firstNonEmpty(...values: (string | undefined | null)[]) {
for (let value of values) {
if (value) {
value = value.trim();

4
src/Squidex/app/shared/components/language-selector.component.html

@ -1,11 +1,11 @@
<div class="btn-group btn-group-{{size}}" *ngIf="isSmallMode">
<button type="button" class="btn btn-secondary" *ngFor="let language of languages" [attr.title]="language.englishName" [class.active]="language == selectedLanguage" (click)="selectLanguage(language)">
<button type="button" class="btn btn-secondary" *ngFor="let language of languages" [attr.title]="language.englishName" [class.active]="language == selectedLanguage" (click)="selectLanguage(language)" tabindex="-1">
<span class="iso-code">{{language.iso2Code}}</span>
</button>
</div>
<div class="dropdown-options btn-group btn-group-{{size}}" *ngIf="isLargeMode">
<button type="button" class="btn btn-secondary dropdown-toggle" [attr.title]="selectedLanguage.englishName" (click)="dropdown.toggle(); $event.stopPropagation()" #button>
<button type="button" class="btn btn-secondary dropdown-toggle" [attr.title]="selectedLanguage.englishName" (click)="dropdown.toggle(); $event.stopPropagation()" #button tabindex="-1">
{{selectedLanguage.iso2Code}}
</button>
<div class="dropdown-menu" *sqxModalView="dropdown" [sqxModalTarget]="button" @fade>

15
src/Squidex/app/shared/services/contents.service.spec.ts

@ -312,6 +312,21 @@ describe('ContentsService', () => {
req.flush({});
}));
it('should make patch request to update content',
inject([ContentsService, HttpTestingController], (contentsService: ContentsService, httpMock: HttpTestingController) => {
const dto = {};
contentsService.patchContent('my-app', 'my-schema', 'content1', dto, version).subscribe();
const req = httpMock.expectOne('http://service/p/api/content/my-app/my-schema/content1');
expect(req.request.method).toEqual('PATCH');
expect(req.request.headers.get('If-Match')).toBe(version.value);
req.flush({});
}));
it('should make put request to change content status',
inject([ContentsService, HttpTestingController], (contentsService: ContentsService, httpMock: HttpTestingController) => {

15
src/Squidex/app/shared/services/contents.service.ts

@ -239,6 +239,21 @@ export class ContentsService {
.pretifyError('Failed to update content. Please reload.');
}
public patchContent(appName: string, schemaName: string, id: string, dto: any, version: Version): Observable<Versioned<any>> {
const url = this.apiUrl.buildUrl(`/api/content/${appName}/${schemaName}/${id}`);
return HTTP.patchVersioned(this.http, url, dto, version)
.map(response => {
const body = response.payload.body;
return new Versioned(response.version, body);
})
.do(() => {
this.analytics.trackEvent('Content', 'Updated', appName);
})
.pretifyError('Failed to update content. Please reload.');
}
public deleteContent(appName: string, schemaName: string, id: string, version: Version): Observable<Versioned<any>> {
const url = this.apiUrl.buildUrl(`/api/content/${appName}/${schemaName}/${id}`);

12
src/Squidex/app/shared/services/schemas.service.ts

@ -22,6 +22,7 @@ import {
Version,
Versioned
} from 'framework';
import { partition } from 'rxjs/operator/partition';
export const fieldTypes: string[] = [
'Assets',
@ -78,9 +79,7 @@ export function createProperties(fieldType: string, values: Object | null = null
}
export class SchemaDto {
public get displayName() {
return StringHelper.firstNonEmpty(this.properties.label || '', this.name);
}
public readonly displayName = StringHelper.firstNonEmpty(this.properties.label, this.name);
constructor(
public readonly id: string,
@ -279,9 +278,10 @@ export class SchemaDetailsDto extends SchemaDto {
}
export class FieldDto {
public get displayName() {
return StringHelper.firstNonEmpty(this.properties.label || '', this.name);
}
public readonly displayName = StringHelper.firstNonEmpty(this.properties.label, this.name);
public readonly displayPlaceholder = this.properties.placeholder || '';
public readonly isLocalizable = this.partitioning !== 'invariant';
constructor(
public readonly fieldId: number,

Loading…
Cancel
Save