Browse Source

Continues with refactoring.

pull/282/head
Sebastian Stehle 8 years ago
parent
commit
5e17bc3db2
  1. 5
      src/Squidex/app/features/administration/state/users.state.ts
  2. 10
      src/Squidex/app/features/content/module.ts
  3. 28
      src/Squidex/app/features/content/pages/content/content-field.component.html
  4. 24
      src/Squidex/app/features/content/pages/content/content-field.component.ts
  5. 31
      src/Squidex/app/features/content/pages/content/content-history.component.ts
  6. 31
      src/Squidex/app/features/content/pages/content/content-page.component.html
  7. 232
      src/Squidex/app/features/content/pages/content/content-page.component.ts
  8. 46
      src/Squidex/app/features/content/pages/contents/contents-page.component.html
  9. 278
      src/Squidex/app/features/content/pages/contents/contents-page.component.ts
  10. 36
      src/Squidex/app/features/content/pages/contents/search-form.component.ts
  11. 4
      src/Squidex/app/features/content/pages/messages.ts
  12. 10
      src/Squidex/app/features/content/shared/assets-editor.component.ts
  13. 22
      src/Squidex/app/features/content/shared/content-item.component.html
  14. 84
      src/Squidex/app/features/content/shared/content-item.component.ts
  15. 18
      src/Squidex/app/features/content/shared/contents-selector.component.html
  16. 71
      src/Squidex/app/features/content/shared/contents-selector.component.ts
  17. 40
      src/Squidex/app/features/content/shared/references-editor.component.html
  18. 30
      src/Squidex/app/features/content/shared/references-editor.component.scss
  19. 17
      src/Squidex/app/features/content/shared/references-editor.component.ts
  20. 2
      src/Squidex/app/features/rules/pages/events/rule-events-page.component.html
  21. 24
      src/Squidex/app/features/rules/pages/events/rule-events-page.component.ts
  22. 18
      src/Squidex/app/framework/utils/types.ts
  23. 69
      src/Squidex/app/shared/components/app-context.ts
  24. 12
      src/Squidex/app/shared/components/assets-list.component.html
  25. 23
      src/Squidex/app/shared/components/assets-list.component.ts
  26. 4
      src/Squidex/app/shared/components/assets-selector.component.html
  27. 2
      src/Squidex/app/shared/components/assets-selector.component.ts
  28. 25
      src/Squidex/app/shared/components/history.component.ts
  29. 4
      src/Squidex/app/shared/components/language-selector.component.ts
  30. 1
      src/Squidex/app/shared/declarations.ts
  31. 62
      src/Squidex/app/shared/guards/content-must-exist.guard.spec.ts
  32. 38
      src/Squidex/app/shared/guards/content-must-exist.guard.ts
  33. 46
      src/Squidex/app/shared/guards/resolve-content.guard.ts
  34. 37
      src/Squidex/app/shared/guards/unset-content.guard.spec.ts
  35. 24
      src/Squidex/app/shared/guards/unset-content.guard.ts
  36. 4
      src/Squidex/app/shared/internal.ts
  37. 8
      src/Squidex/app/shared/module.ts
  38. 52
      src/Squidex/app/shared/services/contents.service.spec.ts
  39. 54
      src/Squidex/app/shared/services/contents.service.ts
  40. 315
      src/Squidex/app/shared/services/schemas.service.ts
  41. 382
      src/Squidex/app/shared/state/contents.state.ts

5
src/Squidex/app/features/administration/state/users.state.ts

@ -134,7 +134,10 @@ export class UsersState extends State<Snapshot> {
} }
public load(notifyLoad = false): Observable<any> { public load(notifyLoad = false): Observable<any> {
return this.usersService.getUsers(this.snapshot.usersPager.pageSize, this.snapshot.usersPager.skip, this.snapshot.usersQuery) return this.usersService.getUsers(
this.snapshot.usersPager.pageSize,
this.snapshot.usersPager.skip,
this.snapshot.usersQuery)
.do(dtos => { .do(dtos => {
if (notifyLoad) { if (notifyLoad) {
this.dialogs.notifyInfo('Users reloaded.'); this.dialogs.notifyInfo('Users reloaded.');

10
src/Squidex/app/features/content/module.ts

@ -11,11 +11,12 @@ import { DndModule } from 'ng2-dnd';
import { import {
CanDeactivateGuard, CanDeactivateGuard,
ContentMustExistGuard,
LoadLanguagesGuard, LoadLanguagesGuard,
ResolveContentGuard,
SchemaMustExistPublishedGuard, SchemaMustExistPublishedGuard,
SqxFrameworkModule, SqxFrameworkModule,
SqxSharedModule SqxSharedModule,
UnsetContentGuard
} from '@app/shared'; } from '@app/shared';
import { import {
@ -52,15 +53,14 @@ const routes: Routes = [
{ {
path: 'new', path: 'new',
component: ContentPageComponent, component: ContentPageComponent,
canActivate: [UnsetContentGuard],
canDeactivate: [CanDeactivateGuard] canDeactivate: [CanDeactivateGuard]
}, },
{ {
path: ':contentId', path: ':contentId',
component: ContentPageComponent, component: ContentPageComponent,
canActivate: [ContentMustExistGuard],
canDeactivate: [CanDeactivateGuard], canDeactivate: [CanDeactivateGuard],
resolve: {
content: ResolveContentGuard
},
children: [ children: [
{ {
path: 'history', path: 'history',

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

@ -7,7 +7,11 @@
<ng-container *ngIf="field.isLocalizable && languages.length > 1"> <ng-container *ngIf="field.isLocalizable && languages.length > 1">
<div class="languages-buttons" #buttonLanguages> <div class="languages-buttons" #buttonLanguages>
<sqx-language-selector size="sm" (selectedLanguageChanged)="selectLanguage($event)" [languages]="languages.values"></sqx-language-selector> <sqx-language-selector size="sm"
[selectedLanguage]="language"
(selectedLanguageChange)="languageChange.emit($event)"
[languages]="languages.values">
</sqx-language-selector>
</div> </div>
<sqx-onboarding-tooltip id="languages" [for]="buttonLanguages" position="topRight" after="120000"> <sqx-onboarding-tooltip id="languages" [for]="buttonLanguages" position="topRight" after="120000">
@ -19,21 +23,21 @@
<div [ngSwitch]="field.properties.fieldType"> <div [ngSwitch]="field.properties.fieldType">
<div *ngSwitchCase="'Number'"> <div *ngSwitchCase="'Number'">
<div [ngSwitch]="field.properties.editor"> <div [ngSwitch]="field.properties['editor']">
<div *ngSwitchCase="'Input'"> <div *ngSwitchCase="'Input'">
<input class="form-control" type="number" [formControl]="selectedFormControl" [placeholder]="field.displayPlaceholder" /> <input class="form-control" type="number" [formControl]="selectedFormControl" [placeholder]="field.displayPlaceholder" />
</div> </div>
<div *ngSwitchCase="'Stars'"> <div *ngSwitchCase="'Stars'">
<sqx-stars [formControl]="selectedFormControl" [maximumStars]="field.properties.maxValue"></sqx-stars> <sqx-stars [formControl]="selectedFormControl" [maximumStars]="field.properties['maxValue']"></sqx-stars>
</div> </div>
<div *ngSwitchCase="'Dropdown'"> <div *ngSwitchCase="'Dropdown'">
<select class="form-control" [formControl]="selectedFormControl"> <select class="form-control" [formControl]="selectedFormControl">
<option [ngValue]="null"></option> <option [ngValue]="null"></option>
<option *ngFor="let value of field.properties.allowedValues" [ngValue]="value">{{value}}</option> <option *ngFor="let value of field.properties['allowedValues']" [ngValue]="value">{{value}}</option>
</select> </select>
</div> </div>
<div *ngSwitchCase="'Radio'"> <div *ngSwitchCase="'Radio'">
<div class="form-check form-check-inline" *ngFor="let value of field.properties.allowedValues"> <div class="form-check form-check-inline" *ngFor="let value of field.properties['allowedValues']">
<input class="form-check-input" type="radio" [value]="value" [formControl]="selectedFormControl" /> <input class="form-check-input" type="radio" [value]="value" [formControl]="selectedFormControl" />
<label class="form-check-label"> <label class="form-check-label">
{{value}} {{value}}
@ -43,7 +47,7 @@
</div> </div>
</div> </div>
<div *ngSwitchCase="'String'"> <div *ngSwitchCase="'String'">
<div [ngSwitch]="field.properties.editor"> <div [ngSwitch]="field.properties['editor']">
<div *ngSwitchCase="'Input'"> <div *ngSwitchCase="'Input'">
<input class="form-control" type="text" [formControl]="selectedFormControl" [placeholder]="field.displayPlaceholder" /> <input class="form-control" type="text" [formControl]="selectedFormControl" [placeholder]="field.displayPlaceholder" />
</div> </div>
@ -62,11 +66,11 @@
<div *ngSwitchCase="'Dropdown'"> <div *ngSwitchCase="'Dropdown'">
<select class="form-control" [formControl]="selectedFormControl"> <select class="form-control" [formControl]="selectedFormControl">
<option [ngValue]="null"></option> <option [ngValue]="null"></option>
<option *ngFor="let value of field.properties.allowedValues" [ngValue]="value">{{value}}</option> <option *ngFor="let value of field.properties['allowedValues']" [ngValue]="value">{{value}}</option>
</select> </select>
</div> </div>
<div *ngSwitchCase="'Radio'"> <div *ngSwitchCase="'Radio'">
<div class="form-check form-check-inline" *ngFor="let value of field.properties.allowedValues"> <div class="form-check form-check-inline" *ngFor="let value of field.properties['allowedValues']">
<input class="form-check-input" type="radio" value="{{value}}" [formControl]="selectedFormControl" /> <input class="form-check-input" type="radio" value="{{value}}" [formControl]="selectedFormControl" />
<label class="form-check-label"> <label class="form-check-label">
{{value}} {{value}}
@ -76,7 +80,7 @@
</div> </div>
</div> </div>
<div *ngSwitchCase="'Boolean'"> <div *ngSwitchCase="'Boolean'">
<div [ngSwitch]="field.properties.editor"> <div [ngSwitch]="field.properties['editor']">
<div *ngSwitchCase="'Toggle'"> <div *ngSwitchCase="'Toggle'">
<sqx-toggle [formControl]="selectedFormControl"></sqx-toggle> <sqx-toggle [formControl]="selectedFormControl"></sqx-toggle>
</div> </div>
@ -88,7 +92,7 @@
</div> </div>
</div> </div>
<div *ngSwitchCase="'DateTime'"> <div *ngSwitchCase="'DateTime'">
<sqx-date-time-editor enforceTime="true" [mode]="field.properties.editor" [formControl]="selectedFormControl"></sqx-date-time-editor> <sqx-date-time-editor enforceTime="true" [mode]="field.properties['editor']" [formControl]="selectedFormControl"></sqx-date-time-editor>
</div> </div>
<div *ngSwitchCase="'Geolocation'"> <div *ngSwitchCase="'Geolocation'">
<sqx-geolocation-editor [formControl]="selectedFormControl"></sqx-geolocation-editor> <sqx-geolocation-editor [formControl]="selectedFormControl"></sqx-geolocation-editor>
@ -107,12 +111,12 @@
[formControl]="selectedFormControl" [formControl]="selectedFormControl"
[language]="language" [language]="language"
[languages]="languages" [languages]="languages"
[schemaId]="field.properties.schemaId"> [schemaId]="field.properties['schemaId']">
</sqx-references-editor> </sqx-references-editor>
</div> </div>
</div> </div>
<small class="form-text text-muted" *ngIf="field.properties.hints && field.properties.hints.length > 0"> <small class="form-text text-muted" *ngIf="field.properties['hints'] && field.properties['hints'].length > 0">
{{field.properties.hints}} {{field.properties.hints}}
</small> </small>
</div> </div>

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

@ -5,7 +5,7 @@
* Copyright (c) Squidex UG (haftungsbeschränkt). All rights reserved. * Copyright (c) Squidex UG (haftungsbeschränkt). All rights reserved.
*/ */
import { Component, Input, OnInit } from '@angular/core'; import { ChangeDetectionStrategy, Component, EventEmitter, Input, OnChanges, Output } from '@angular/core';
import { AbstractControl, FormGroup } from '@angular/forms'; import { AbstractControl, FormGroup } from '@angular/forms';
import { import {
@ -18,9 +18,10 @@ import {
@Component({ @Component({
selector: 'sqx-content-field', selector: 'sqx-content-field',
styleUrls: ['./content-field.component.scss'], styleUrls: ['./content-field.component.scss'],
templateUrl: './content-field.component.html' templateUrl: './content-field.component.html',
changeDetection: ChangeDetectionStrategy.OnPush
}) })
export class ContentFieldComponent implements OnInit { export class ContentFieldComponent implements OnChanges {
@Input() @Input()
public field: FieldDto; public field: FieldDto;
@ -30,6 +31,9 @@ export class ContentFieldComponent implements OnInit {
@Input() @Input()
public language: AppLanguageDto; public language: AppLanguageDto;
@Output()
public languageChange = new EventEmitter<AppLanguageDto>();
@Input() @Input()
public languages: ImmutableArray<AppLanguageDto>; public languages: ImmutableArray<AppLanguageDto>;
@ -38,23 +42,13 @@ export class ContentFieldComponent implements OnInit {
public selectedFormControl: AbstractControl; public selectedFormControl: AbstractControl;
public ngOnInit() { public ngOnChanges() {
if (!this.language) {
this.language = this.languages.at(0);
}
if (this.field.isLocalizable) { if (this.field.isLocalizable) {
this.selectedFormControl = this.fieldForm.controls[this.language.iso2Code]; this.selectedFormControl = this.fieldForm.controls[this.language.iso2Code];
this.selectedFormControl['_clearChangeFns']();
} else { } else {
this.selectedFormControl = this.fieldForm.controls[fieldInvariant]; this.selectedFormControl = this.fieldForm.controls[fieldInvariant];
} }
} }
public selectLanguage(language: AppLanguageDto) {
this.selectedFormControl['_clearChangeFns']();
this.selectedFormControl = this.fieldForm.controls[language.iso2Code];
this.language = language;
}
} }

31
src/Squidex/app/features/content/pages/content/content-history.component.ts

@ -6,16 +6,19 @@
*/ */
import { Component } from '@angular/core'; import { Component } from '@angular/core';
import { ActivatedRoute } from '@angular/router';
import { Observable } from 'rxjs'; import { Observable } from 'rxjs';
import { import {
allParams, allParams,
AppContext, AppsState,
formatHistoryMessage, formatHistoryMessage,
HistoryChannelUpdated, HistoryChannelUpdated,
HistoryEventDto, HistoryEventDto,
HistoryService, HistoryService,
UsersProviderService MessageBus,
UsersProviderService,
Version
} from '@app/shared'; } from '@app/shared';
import { ContentVersionSelected } from './../messages'; import { ContentVersionSelected } from './../messages';
@ -23,17 +26,14 @@ import { ContentVersionSelected } from './../messages';
@Component({ @Component({
selector: 'sqx-history', selector: 'sqx-history',
styleUrls: ['./content-history.component.scss'], styleUrls: ['./content-history.component.scss'],
templateUrl: './content-history.component.html', templateUrl: './content-history.component.html'
providers: [
AppContext
]
}) })
export class ContentHistoryComponent { export class ContentHistoryComponent {
public get channel(): string { public get channel(): string {
let channelPath = this.ctx.route.snapshot.data['channel']; let channelPath = this.route.snapshot.data['channel'];
if (channelPath) { if (channelPath) {
const params = allParams(this.ctx.route); const params = allParams(this.route);
for (let key in params) { for (let key in params) {
if (params.hasOwnProperty(key)) { if (params.hasOwnProperty(key)) {
@ -48,17 +48,20 @@ export class ContentHistoryComponent {
} }
public events: Observable<HistoryEventDto[]> = public events: Observable<HistoryEventDto[]> =
Observable.timer(0, 10000).merge(this.ctx.bus.of(HistoryChannelUpdated).delay(1000)) Observable.timer(0, 10000).merge(this.messageBus.of(HistoryChannelUpdated).delay(1000))
.switchMap(app => this.historyService.getHistory(this.ctx.appName, this.channel)); .switchMap(app => this.historyService.getHistory(this.appsState.appName, this.channel));
constructor(public readonly ctx: AppContext, constructor(
private readonly users: UsersProviderService, private readonly appsState: AppsState,
private readonly historyService: HistoryService private readonly historyService: HistoryService,
private readonly messageBus: MessageBus,
private readonly route: ActivatedRoute,
private readonly users: UsersProviderService
) { ) {
} }
public loadVersion(version: number) { public loadVersion(version: number) {
this.ctx.bus.emit(new ContentVersionSelected(version)); this.messageBus.emit(new ContentVersionSelected(new Version(version.toString())));
} }
public format(message: string): Observable<string> { public format(message: string): Observable<string> {

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

@ -1,25 +1,25 @@
<sqx-title message="{app} | {schema} | Content" parameter1="app" parameter2="schema" [value1]="ctx.appName" [value2]="schema?.displayName"></sqx-title> <sqx-title message="{app} | {schema} | Content" parameter1="app" parameter2="schema" [value1]="appsState.appName" [value2]="schema.displayName"></sqx-title>
<form [formGroup]="contentForm" (ngSubmit)="saveAndPublish()"> <form [formGroup]="contentForm.form" (ngSubmit)="saveAndPublish()">
<sqx-panel desiredWidth="*" showSidebar="true"> <sqx-panel desiredWidth="*" [showSidebar]="content">
<ng-container title> <ng-container title>
<a class="btn btn-link" (click)="back()"> <a class="btn btn-link" (click)="back()">
<i class="icon-angle-left"></i> <i class="icon-angle-left"></i>
</a> </a>
<ng-container *ngIf="isNewMode"> <ng-container *ngIf="!content">
New Content New Content
</ng-container> </ng-container>
<ng-container *ngIf="!isNewMode && content.status !== 'Archived'"> <ng-container *ngIf="content && content.status !== 'Archived'">
Edit Content Edit Content
</ng-container> </ng-container>
<ng-container *ngIf="!isNewMode && content.status === 'Archived'"> <ng-container *ngIf="content && content.status === 'Archived'">
Show Content Show Content
</ng-container> </ng-container>
</ng-container> </ng-container>
<ng-container menu> <ng-container menu>
<ng-container *ngIf="isNewMode; else notNew"> <ng-container *ngIf="!content; else notNew">
<button type="button" class="btn btn-secondary" (click)="saveAsDraft()" title="CTRL + S"> <button type="button" class="btn btn-secondary" (click)="saveAsDraft()" title="CTRL + S">
Save as Draft Save as Draft
</button> </button>
@ -38,24 +38,31 @@
</ng-container> </ng-container>
<ng-container content> <ng-container content>
<div class="panel-alert panel-alert-danger" *ngIf="contentOld"> <div class="panel-alert panel-alert-danger" *ngIf="contentVersion">
<div class="float-right"> <div class="float-right">
<a class="force" (click)="showLatest()">View latest</a> <a class="force" (click)="showLatest()">View latest</a>
</div> </div>
Viewing <strong>{{content.lastModifiedBy | sqxUserNameRef:null}}'s</strong> changes of {{content.lastModified | sqxShortDate}}.
Viewing <strong>version {{contentVersion.value</strong>.
</div> </div>
<div *ngFor="let field of schema.fields"> <div *ngFor="let field of schema.fields">
<sqx-content-field [field]="field" [fieldForm]="contentForm.controls[field.name]" [languages]="languages" [contentFormSubmitted]="contentFormSubmitted"></sqx-content-field> <sqx-content-field
[field]="field"
[fieldForm]="contentForm.form.controls[field.name]"
[(language)]="language"
[languages]="languages"
[contentFormSubmitted]="contentForm.submitted | async">
</sqx-content-field>
</div> </div>
</ng-container> </ng-container>
<ng-container sidebar> <ng-container sidebar>
<a class="panel-link" routerLink="history" routerLinkActive="active" #linkHistory *ngIf="!isNewMode"> <a class="panel-link" routerLink="history" routerLinkActive="active" #linkHistory>
<i class="icon-time"></i> <i class="icon-time"></i>
</a> </a>
<sqx-onboarding-tooltip id="history" [for]="linkHistory" position="leftTop" after="120000" *ngIf="!isNewMode"> <sqx-onboarding-tooltip id="history" [for]="linkHistory" position="leftTop" after="120000">
The sidebar navigation contains useful context specific links. Here you can view the history how this schema has changed over time. The sidebar navigation contains useful context specific links. Here you can view the history how this schema has changed over time.
</sqx-onboarding-tooltip> </sqx-onboarding-tooltip>
</ng-container> </ng-container>

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

@ -6,21 +6,22 @@
*/ */
import { Component, OnDestroy, OnInit } from '@angular/core'; import { Component, OnDestroy, OnInit } from '@angular/core';
import { AbstractControl, FormControl, FormGroup } from '@angular/forms'; import { ActivatedRoute, Router } from '@angular/router';
import { Router } from '@angular/router';
import { Observable, Subscription } from 'rxjs'; import { Observable, Subscription } from 'rxjs';
import { ContentVersionSelected } from './../messages'; import { ContentVersionSelected } from './../messages';
import { import {
AppContext,
AppLanguageDto, AppLanguageDto,
AppsState,
CanComponentDeactivate, CanComponentDeactivate,
ContentDto, ContentDto,
ContentsService, ContentsState,
fieldInvariant, DialogService,
EditContentForm,
ImmutableArray, ImmutableArray,
LanguagesState, LanguagesState,
MessageBus,
SchemaDetailsDto, SchemaDetailsDto,
SchemasState, SchemasState,
Version Version
@ -29,81 +30,78 @@ import {
@Component({ @Component({
selector: 'sqx-content-page', selector: 'sqx-content-page',
styleUrls: ['./content-page.component.scss'], styleUrls: ['./content-page.component.scss'],
templateUrl: './content-page.component.html', templateUrl: './content-page.component.html'
providers: [
AppContext
]
}) })
export class ContentPageComponent implements CanComponentDeactivate, OnDestroy, OnInit { export class ContentPageComponent implements CanComponentDeactivate, OnDestroy, OnInit {
private contentVersionSelectedSubscription: Subscription;
private languagesSubscription: Subscription; private languagesSubscription: Subscription;
private contentSubscription: Subscription;
private contentVersionSelectedSubscription: Subscription;
private selectedSchemaSubscription: Subscription; private selectedSchemaSubscription: Subscription;
public schema: SchemaDetailsDto; public schema: SchemaDetailsDto;
public content: ContentDto; public content: ContentDto;
public contentOld: ContentDto | null; public contentVersion: Version | null;
public contentFormSubmitted = false; public contentForm: EditContentForm;
public contentForm: FormGroup;
public isNewMode = true;
public language: AppLanguageDto;
public languages: ImmutableArray<AppLanguageDto>; public languages: ImmutableArray<AppLanguageDto>;
constructor( constructor(
public readonly ctx: AppContext, public readonly appsState: AppsState,
public readonly languagesState: LanguagesState, private readonly contentsState: ContentsState,
private readonly contentsService: ContentsService, private readonly dialogs: DialogService,
private readonly languagesState: LanguagesState,
private readonly messageBus: MessageBus,
private readonly route: ActivatedRoute,
private readonly router: Router, private readonly router: Router,
private readonly schemasState: SchemasState private readonly schemasState: SchemasState
) { ) {
} }
public ngOnDestroy() { public ngOnDestroy() {
this.contentVersionSelectedSubscription.unsubscribe();
this.languagesSubscription.unsubscribe(); this.languagesSubscription.unsubscribe();
this.contentSubscription.unsubscribe();
this.contentVersionSelectedSubscription.unsubscribe();
this.selectedSchemaSubscription.unsubscribe(); this.selectedSchemaSubscription.unsubscribe();
} }
public ngOnInit() { public ngOnInit() {
this.contentVersionSelectedSubscription =
this.ctx.bus.of(ContentVersionSelected)
.subscribe(message => {
this.loadVersion(message.version);
});
this.languagesSubscription = this.languagesSubscription =
this.languagesState.languages this.languagesState.languages
.subscribe(languages => { .subscribe(languages => {
this.languages = languages.map(x => x.language); this.languages = languages.map(x => x.language);
this.language = this.languages.at(0);
}); });
this.selectedSchemaSubscription = this.selectedSchemaSubscription =
this.schemasState.selectedSchema this.schemasState.selectedSchema.filter(s => !!s)
.subscribe(schema => { .subscribe(schema => {
this.setupContentForm(schema!); this.schema = schema!;
this.contentForm = new EditContentForm(this.schema, this.languages);
});
this.contentSubscription =
this.contentsState.selectedContent.filter(c => !!c)
.subscribe(content => {
this.content = content!;
this.loadContent(content!.data);
}); });
this.ctx.route.data.map(d => d.content) this.contentVersionSelectedSubscription =
.subscribe((content: ContentDto) => { this.messageBus.of(ContentVersionSelected)
this.reloadContentForm(content); .subscribe(message => {
}); this.loadVersion(message.version);
});
} }
public canDeactivate(): Observable<boolean> { public canDeactivate(): Observable<boolean> {
if (!this.contentForm.dirty || this.isNewMode) { if (!this.contentForm.form.dirty || !this.content) {
return Observable.of(true); return Observable.of(true);
} else { } else {
return this.ctx.confirmUnsavedChanges(); return this.dialogs.confirmUnsavedChanges();
}
}
public showLatest() {
if (this.contentOld) {
this.content = this.contentOld;
this.contentOld = null;
this.reloadContentForm(this.content);
} }
} }
@ -116,153 +114,59 @@ export class ContentPageComponent implements CanComponentDeactivate, OnDestroy,
} }
private saveContent(publish: boolean) { private saveContent(publish: boolean) {
this.contentFormSubmitted = true; const value = this.contentForm.submit();
if (this.contentForm.valid) { if (value) {
this.disableContentForm(); if (!this.content) {
this.contentsState.create(value, publish)
const requestDto = this.contentForm.value;
if (this.isNewMode) {
this.contentsService.postContent(this.ctx.appName, this.schema.name, requestDto, publish)
.subscribe(dto => { .subscribe(dto => {
this.content = dto;
this.ctx.notifyInfo('Content created successfully.');
this.back(); this.back();
}, error => { }, error => {
this.ctx.notifyError(error); this.contentForm.submitFailed(error);
this.enableContentForm();
}); });
} else { } else {
this.contentsService.putContent(this.ctx.appName, this.schema.name, this.content.id, requestDto, this.content.version) this.contentsState.create(value, publish)
.subscribe(dto => { .subscribe(dto => {
const content = this.content.update(dto.payload, this.ctx.userToken, dto.version); this.contentForm.submitCompleted();
this.ctx.notifyInfo('Content saved successfully.');
this.enableContentForm();
this.reloadContentForm(content);
}, error => { }, error => {
this.ctx.notifyError(error); this.contentForm.submitFailed(error);
this.enableContentForm();
}); });
} }
} else { } else {
this.ctx.notifyError('Content element not valid, please check the field with the red bar on the left in all languages (if localizable).'); this.dialogs.notifyError('Content element not valid, please check the field with the red bar on the left in all languages (if localizable).');
}
}
private loadVersion(version: number) {
if (!this.isNewMode && this.content) {
this.contentsService.getVersionData(this.ctx.appName, this.schema.name, this.content.id, new Version(version.toString()))
.subscribe(dto => {
if (this.content.version.value !== version.toString()) {
this.contentOld = this.content;
} else {
this.contentOld = null;
}
this.ctx.notifyInfo('Content version loaded successfully.');
this.reloadContentForm(this.content.setData(dto));
}, error => {
this.ctx.notifyError(error);
});
} }
} }
public back() { public back() {
this.router.navigate([this.schema.name], { relativeTo: this.ctx.route.parent!.parent, replaceUrl: true }); this.router.navigate([this.schema.name], { relativeTo: this.route.parent!.parent, replaceUrl: true });
} }
private disableContentForm() { private loadContent(data: any) {
this.contentForm.disable(); this.contentForm.load(data);
}
private enableContentForm() {
this.contentForm.markAsPristine();
if (this.schema.fields.length === 0) {
this.contentForm.enable();
} else {
for (const field of this.schema.fields) {
const fieldForm = <FormGroup>this.contentForm.controls[field.name];
if (field.isDisabled) {
fieldForm.disable();
} else {
fieldForm.enable();
}
}
}
} }
private setupContentForm(schema: SchemaDetailsDto) { private loadVersion(version: Version) {
this.schema = schema; if (this.content) {
this.contentsState.loadVersion(this.content, version)
const controls: { [key: string]: AbstractControl } = {}; .subscribe(dto => {
if (this.content.version.value !== version.toString()) {
this.contentVersion = version;
} else {
this.contentVersion = null;
}
for (const field of schema.fields) { this.dialogs.notifyInfo('Content version loaded successfully.');
const fieldForm = new FormGroup({});
if (field.isLocalizable) { this.loadContent(dto);
for (let language of this.languages.values) { });
fieldForm.setControl(language.iso2Code, new FormControl(undefined, field.createValidators(language.isOptional)));
}
} else {
fieldForm.setControl(fieldInvariant, new FormControl(undefined, field.createValidators(false)));
}
controls[field.name] = fieldForm;
} }
this.contentForm = new FormGroup(controls);
this.enableContentForm();
} }
private reloadContentForm(content: ContentDto) { public showLatest() {
this.content = content; if (this.contentVersion) {
this.contentForm.markAsPristine(); this.contentVersion = null;
this.isNewMode = !this.content;
if (!this.isNewMode) {
for (const field of this.schema.fields) {
const fieldValue = this.content.data[field.name] || {};
const fieldForm = <FormGroup>this.contentForm.controls[field.name];
if (field.isLocalizable) {
for (let language of this.languages.values) {
fieldForm.controls[language.iso2Code].setValue(fieldValue[language.iso2Code]);
}
} else {
fieldForm.controls[fieldInvariant].setValue(fieldValue[fieldInvariant] === undefined ? null : fieldValue[fieldInvariant]);
}
}
if (this.content.status === 'Archived') {
this.contentForm.disable();
}
} else {
for (const field of this.schema.fields) {
const defaultValue = field.defaultValue();
if (defaultValue) {
const fieldForm = <FormGroup>this.contentForm.controls[field.name];
if (field.isLocalizable) { this.loadContent(this.content.data);
for (let language of this.languages.values) {
fieldForm.controls[language.iso2Code].setValue(defaultValue);
}
} else {
fieldForm.controls[fieldInvariant].setValue(defaultValue);
}
}
}
} }
} }
} }

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

@ -1,8 +1,8 @@
<sqx-title message="{app} | {schema} | Contents" parameter1="app" parameter2="schema" [value1]="ctx.appName" [value2]="schema?.displayName"></sqx-title> <sqx-title message="{app} | {schema} | Contents" parameter1="app" parameter2="schema" [value1]="appsState.appName" [value2]="schema.displayName"></sqx-title>
<sqx-panel desiredWidth="*" contentClass="grid"> <sqx-panel desiredWidth="*" contentClass="grid">
<ng-container title> <ng-container title>
<ng-container *ngIf="isArchive; else noArchive"> <ng-container *ngIf="contentsState.isArchive | async; else noArchive">
Archive Archive
</ng-container> </ng-container>
@ -12,26 +12,34 @@
</ng-container> </ng-container>
<ng-container menu> <ng-container menu>
<button class="btn btn-link btn-secondary" (click)="load(true)" title="Refresh Contents (CTRL + SHIFT + R)"> <button class="btn btn-link btn-secondary" (click)="reload()" title="Refresh Contents (CTRL + SHIFT + R)">
<i class="icon-reset"></i> Refresh <i class="icon-reset"></i> Refresh
</button> </button>
<sqx-shortcut keys="ctrl+shift+r" (trigger)="load(true)"></sqx-shortcut> <sqx-shortcut keys="ctrl+shift+r" (trigger)="reload()"></sqx-shortcut>
<sqx-search-form (queryChanged)="search($event)" [query]="contentsQuery" enableShortcut="true"></sqx-search-form> <sqx-search-form
(queryChanged)="search($event)"
[query]="contentsState.contentsQuery | async"
(archivedChanged)="goArchive($event)"
[archived]="contentsState.isArchive | async"
enableShortcut="true">
</sqx-search-form>
<ng-container *ngIf="languages.length > 1"> <ng-container *ngIf="languages.length > 1">
<sqx-language-selector class="languages-buttons" (selectedLanguageChanged)="selectLanguage($event)" [languages]="languages.values"></sqx-language-selector> <sqx-language-selector class="languages-buttons" (selectedLanguageChange)="selectLanguage($event)" [languages]="languages.values"></sqx-language-selector>
</ng-container> </ng-container>
<button class="btn btn-success" #newButton routerLink="new" title="New Content (CTRL + SHIFT + G)"> <button class="btn btn-success" #newButton routerLink="new" title="New Content (CTRL + SHIFT + G)">
<i class="icon-plus"></i> New <i class="icon-plus"></i> New
</button> </button>
<sqx-shortcut keys="ctrl+shift+g" (trigger)="newButton.click()"></sqx-shortcut>
</ng-container> </ng-container>
<ng-container content> <ng-container content>
<div class="grid-header"> <div class="grid-header">
<table class="table table-items table-fixed" *ngIf="contentItems"> <table class="table table-items table-fixed">
<thead> <thead>
<tr> <tr>
<th class="cell-select" *ngIf="!isReadOnly"> <th class="cell-select" *ngIf="!isReadOnly">
@ -65,11 +73,11 @@
Unpublish Unpublish
</button> </button>
<button class="btn btn-secondary" (click)="archiveSelected()" *ngIf="!isArchive"> <button class="btn btn-secondary" (click)="archiveSelected()" *ngIf="(contentsState.isArchive | async) === false">
Archive Archive
</button> </button>
<button class="btn btn-secondary" (click)="restoreSelected()" *ngIf="isArchive"> <button class="btn btn-secondary" (click)="restoreSelected()" *ngIf="contentsState.isArchive | async">
Restore Restore
</button> </button>
@ -83,20 +91,20 @@
<div class="grid-content"> <div class="grid-content">
<div sqxIgnoreScrollbar> <div sqxIgnoreScrollbar>
<table class="table table-items table-fixed" *ngIf="contentItems"> <table class="table table-items table-fixed">
<tbody> <tbody>
<ng-template ngFor let-content [ngForOf]="contentItems" [ngForTrackBy]="trackByContent"> <ng-template ngFor let-content [ngForOf]="contentsState.contents | async" [ngForTrackBy]="trackByContent">
<tr [sqxContent]="content" [routerLink]="[content.id]" routerLinkActive="active" <tr [sqxContent]="content" [routerLink]="[content.id]" routerLinkActive="active"
[language]="language" [language]="language"
[schema]="schema" [schema]="schema"
[selected]="isItemSelected(content)" [selected]="isItemSelected(content)"
(selectedChange)="selectItem(content, $event)" (selectedChange)="selectItem(content, $event)"
(unpublishing)="unpublishContent(content)" (unpublishing)="unpublish(content)"
(publishing)="publishContent(content)" (publishing)="publish(content)"
(archiving)="archiveContent(content)" (archiving)="archive(content)"
(restoring)="restoreContent(content)" (restoring)="restore(content)"
(deleting)="deleteContent(content)" (deleting)="delete(content)">
(saved)="onContentSaved(content, $event)"></tr> </tr>
<tr class="spacer"></tr> <tr class="spacer"></tr>
</ng-template> </ng-template>
</tbody> </tbody>
@ -105,7 +113,7 @@
</div> </div>
<div class="grid-footer"> <div class="grid-footer">
<sqx-pager [pager]="contentsPager"></sqx-pager> <sqx-pager [pager]="contentsState.contentsPager | async"></sqx-pager>
</div> </div>
</ng-container> </ng-container>
</sqx-panel> </sqx-panel>
@ -136,7 +144,7 @@
<ng-container footer> <ng-container footer>
<button type="button" class="float-left btn btn-secondary" (click)="cancelStatusChange()">Cancel</button> <button type="button" class="float-left btn btn-secondary" (click)="cancelStatusChange()">Cancel</button>
<button type="button" class="float-right btn btn-primary" [disabled]="dueTimeMode === 'Scheduled' && !dueTime" (click)="confirmStatusChange()">Confirm</button> <button type="button" class="float-right btn btn-primary" autofocus [disabled]="dueTimeMode === 'Scheduled' && !dueTime" (click)="confirmStatusChange()">Confirm</button>
</ng-container> </ng-container>
</sqx-modal-dialog> </sqx-modal-dialog>
</ng-container> </ng-container>

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

@ -6,43 +6,34 @@
*/ */
import { Component, OnDestroy, OnInit } from '@angular/core'; import { Component, OnDestroy, OnInit } from '@angular/core';
import { Observable, Subscription } from 'rxjs'; import { Subscription } from 'rxjs';
import { import {
AppContext,
AppLanguageDto, AppLanguageDto,
AppsState,
ContentDto, ContentDto,
ContentsService, ContentsState,
DateTime,
ImmutableArray, ImmutableArray,
LanguagesState, LanguagesState,
ModalView, ModalView,
Pager,
SchemaDetailsDto, SchemaDetailsDto,
SchemasState, SchemasState
Versioned
} from '@app/shared'; } from '@app/shared';
@Component({ @Component({
selector: 'sqx-contents-page', selector: 'sqx-contents-page',
styleUrls: ['./contents-page.component.scss'], styleUrls: ['./contents-page.component.scss'],
templateUrl: './contents-page.component.html', templateUrl: './contents-page.component.html'
providers: [
AppContext
]
}) })
export class ContentsPageComponent implements OnDestroy, OnInit { export class ContentsPageComponent implements OnDestroy, OnInit {
private selectedSchemaSubscription: Subscription; private contentsSubscription: Subscription;
private languagesSubscription: Subscription; private languagesSubscription: Subscription;
private selectedSchemaSubscription: Subscription;
public schema: SchemaDetailsDto; public schema: SchemaDetailsDto;
public searchModal = new ModalView(); public searchModal = new ModalView();
public contentItems: ImmutableArray<ContentDto>;
public contentsQuery = '';
public contentsPager = new Pager(0);
public dueTimeDialog = new ModalView(); public dueTimeDialog = new ModalView();
public dueTime: string | null = ''; public dueTime: string | null = '';
public dueTimeFunction: Function | null; public dueTimeFunction: Function | null;
@ -57,19 +48,19 @@ export class ContentsPageComponent implements OnDestroy, OnInit {
public language: AppLanguageDto; public language: AppLanguageDto;
public languages: ImmutableArray<AppLanguageDto>; public languages: ImmutableArray<AppLanguageDto>;
public languageParameter: string;
public isAllSelected = false; public isAllSelected = false;
public isArchive = false;
constructor(public readonly ctx: AppContext, constructor(
public readonly appsState: AppsState,
public readonly contentsState: ContentsState,
private readonly languagesState: LanguagesState, private readonly languagesState: LanguagesState,
private readonly contentsService: ContentsService,
private readonly schemasState: SchemasState private readonly schemasState: SchemasState
) { ) {
} }
public ngOnDestroy() { public ngOnDestroy() {
this.contentsSubscription.unsubscribe();
this.languagesSubscription.unsubscribe(); this.languagesSubscription.unsubscribe();
this.selectedSchemaSubscription.unsubscribe(); this.selectedSchemaSubscription.unsubscribe();
} }
@ -78,10 +69,17 @@ export class ContentsPageComponent implements OnDestroy, OnInit {
this.selectedSchemaSubscription = this.selectedSchemaSubscription =
this.schemasState.selectedSchema this.schemasState.selectedSchema
.subscribe(schema => { .subscribe(schema => {
this.resetSelection();
this.schema = schema!; this.schema = schema!;
this.resetContents(); this.contentsState.load().onErrorResumeNext().subscribe();
this.load(); });
this.contentsSubscription =
this.contentsState.contents
.subscribe(() => {
this.updateSelectionSummary();
}); });
this.languagesSubscription = this.languagesSubscription =
@ -90,180 +88,105 @@ export class ContentsPageComponent implements OnDestroy, OnInit {
this.languages = languages.map(x => x.language); this.languages = languages.map(x => x.language);
this.language = this.languages.at(0); this.language = this.languages.at(0);
}); });
this.contentsState.load().onErrorResumeNext().subscribe();
} }
public publishContent(content: ContentDto) { public reload() {
this.changeContentItems([content], 'Publish', 'Published', false); this.contentsState.load(true).onErrorResumeNext().subscribe();
} }
public publishSelected(scheduled: boolean) { public publish(content: ContentDto) {
const contents = this.contentItems.filter(c => c.status !== 'Published' && this.selectedItems[c.id]).values; this.changeContentItems([content], 'Publish', false);
}
this.changeContentItems(contents, 'Publish', 'Published', false); public publishSelected(scheduled: boolean) {
this.changeContentItems(this.s(c => c.status !== 'Published'), 'Publish', false);
} }
public unpublishContent(content: ContentDto) { public unpublish(content: ContentDto) {
this.changeContentItems([content], 'Unpublish', 'Draft', false); this.changeContentItems([content], 'Unpublish', false);
} }
public unpublishSelected(scheduled: boolean) { public unpublishSelected(scheduled: boolean) {
const contents = this.contentItems.filter(c => c.status === 'Published' && this.selectedItems[c.id]).values; this.changeContentItems(this.s(c => c.status === 'Published'), 'Unpublish', false);
this.changeContentItems(contents, 'Unpublish', 'Draft', false);
} }
public archiveContent(content: ContentDto) { public archive(content: ContentDto) {
this.changeContentItems([content], 'Archive', 'Archived', true); this.changeContentItems([content], 'Archive', true);
} }
public archiveSelected(scheduled: boolean) { public archiveSelected(scheduled: boolean) {
const contents = this.contentItems.filter(c => this.selectedItems[c.id]).values; this.changeContentItems(this.s(), 'Archive', true);
this.changeContentItems(contents, 'Archive', 'Archived', true);
} }
public restoreContent(content: ContentDto) { public restore(content: ContentDto) {
this.changeContentItems([content], 'Restore', 'Draft', true); this.changeContentItems([content], 'Restore', true);
} }
public restoreSelected(scheduled: boolean) { public restoreSelected(scheduled: boolean) {
const contents = this.contentItems.filter(c => this.selectedItems[c.id]).values; this.changeContentItems(this.s(), 'Restore', true);
this.changeContentItems(contents, 'Restore', 'Draft', true);
} }
private changeContentItems(contents: ContentDto[], action: string, status: string, reload: boolean) { private changeContentItems(contents: ContentDto[], action: string, reload: boolean) {
if (contents.length === 0) { if (contents.length === 0) {
return; return;
} }
this.dueTimeFunction = () => { this.dueTimeFunction = () => {
if (this.dueTime) { this.resetSelection();
reload = false;
} this.contentsState.changeStatus(contents, action, this.dueTime).onErrorResumeNext().subscribe();
Observable.forkJoin(
contents
.map(c => this.changeContentItem(c, action, status, this.dueTime, reload)))
.finally(() => {
if (reload) {
this.load();
} else {
this.updateSelectionSummary();
}
})
.subscribe();
}; };
this.dueTimeAction = action; this.dueTimeAction = action;
this.dueTimeDialog.show(); this.dueTimeDialog.show();
} }
private changeContentItem(content: ContentDto, action: string, status: string, dueTime: string | null, reload: boolean): Observable<any> { public deleteSelected() {
return this.contentsService.changeContentStatus(this.ctx.appName, this.schema.name, content.id, action, dueTime, content.version) this.resetSelection();
.catch(error => {
this.ctx.notifyError(error);
return Observable.throw(error);
})
.do(dto => {
if (!reload) {
const dt =
dueTime ?
DateTime.parseISO_UTC(dueTime) :
null;
content = content.changeStatus(status, dt, this.ctx.userToken, dto.version);
this.contentItems = this.contentItems.replaceBy('id', content);
}
});
}
public deleteSelected(content: ContentDto) {
Observable.forkJoin(
this.contentItems.values.filter(c => this.selectedItems[c.id])
.map(c => this.deleteContentItem(c)))
.finally(() => {
this.load();
})
.subscribe();
}
public deleteContent(content: ContentDto) { this.contentsState.delete(this.s()).onErrorResumeNext().subscribe();
this.deleteContentItem(content)
.finally(() => {
this.load();
})
.subscribe();
} }
public deleteContentItem(content: ContentDto): Observable<any> { public delete(content: ContentDto) {
return this.contentsService.deleteContent(this.ctx.appName, this.schema.name, content.id, content.version) this.resetSelection();
.catch(error => {
this.ctx.notifyError(error);
return Observable.throw(error); this.contentsState.delete([content]).onErrorResumeNext().subscribe();
});
} }
public onContentSaved(content: ContentDto, update: Versioned<any>) { public goArchive(isArchive: boolean) {
content = content.update(update.payload, this.ctx.userToken, update.version); this.resetSelection();
this.contentItems = this.contentItems.replaceBy('id', content); this.contentsState.goArchive(isArchive).onErrorResumeNext().subscribe();
} }
public load(showInfo = false) { public goPrev() {
this.contentsService.getContents(this.ctx.appName, this.schema.name, this.contentsPager.pageSize, this.contentsPager.skip, this.contentsQuery, undefined, this.isArchive) this.resetSelection();
.finally(() => {
this.selectedItems = {};
this.updateSelectionSummary();
})
.subscribe(dtos => {
this.contentItems = ImmutableArray.of(dtos.items);
this.contentsPager = this.contentsPager.setCount(dtos.total);
if (showInfo) {
this.ctx.notifyInfo('Contents reloaded.');
}
}, error => {
this.ctx.notifyError(error);
});
}
public updateArchive(isArchive: boolean) {
this.contentsPager = new Pager(0);
this.isArchive = isArchive;
this.searchModal.hide();
this.load();
}
public search(query: string) {
this.contentsQuery = query;
this.contentsPager = new Pager(0);
this.load(); this.contentsState.goPrev().onErrorResumeNext().subscribe();
} }
public goNext() { public goNext() {
this.contentsPager = this.contentsPager.goNext(); this.resetSelection();
this.load(); this.contentsState.goNext().onErrorResumeNext().subscribe();
} }
public goPrev() { public search(query: string) {
this.contentsPager = this.contentsPager.goPrev(); this.resetSelection();
this.load(); this.contentsState.search(query).onErrorResumeNext().subscribe();
} }
public isItemSelected(content: ContentDto): boolean { public isItemSelected(content: ContentDto): boolean {
return !!this.selectedItems[content.id]; return !!this.selectedItems[content.id];
} }
public selectLanguage(language: AppLanguageDto) {
this.language = language;
}
public selectItem(content: ContentDto, isSelected: boolean) { public selectItem(content: ContentDto, isSelected: boolean) {
this.selectedItems[content.id] = isSelected; this.selectedItems[content.id] = isSelected;
@ -274,29 +197,60 @@ export class ContentsPageComponent implements OnDestroy, OnInit {
this.selectedItems = {}; this.selectedItems = {};
if (isSelected) { if (isSelected) {
for (let c of this.contentItems.values) { for (let content of this.contentsState.snapshot.contents.values) {
this.selectedItems[c.id] = true; this.selectedItems[content.id] = true;
} }
} }
this.updateSelectionSummary(); this.updateSelectionSummary();
} }
public confirmStatusChange() {
this.dueTimeFunction!();
this.dueTimeFunction = null;
this.dueTimeMode = 'Immediately';
this.dueTimeDialog.hide();
this.dueTime = null;
}
public cancelStatusChange() {
this.dueTimeMode = 'Immediately';
this.dueTimeDialog.hide();
this.dueTimeFunction = null;
this.dueTime = null;
}
public trackByContent(content: ContentDto): string {
return content.id;
}
private s(predicate?: (content: ContentDto) => boolean) {
return this.contentsState.snapshot.contents.values.filter(c => this.selectedItems[c.id] && (!predicate || predicate(c)));
}
private resetSelection() {
this.selectedItems = {};
this.updateSelectionSummary();
}
private updateSelectionSummary() { private updateSelectionSummary() {
this.isAllSelected = this.contentItems.length > 0; this.isAllSelected = this.contentsState.snapshot.contents.length > 0;
this.selectionCount = 0; this.selectionCount = 0;
this.canPublish = true; this.canPublish = true;
this.canUnpublish = true; this.canUnpublish = true;
for (let c of this.contentItems.values) { for (let content of this.contentsState.snapshot.contents.values) {
if (this.selectedItems[c.id]) { if (this.selectedItems[content.id]) {
this.selectionCount++; this.selectionCount++;
if (c.status !== 'Published') { if (content.status !== 'Published') {
this.canUnpublish = false; this.canUnpublish = false;
} }
if (c.status === 'Published') { if (content.status === 'Published') {
this.canPublish = false; this.canPublish = false;
} }
} else { } else {
@ -304,35 +258,5 @@ export class ContentsPageComponent implements OnDestroy, OnInit {
} }
} }
} }
public selectLanguage(language: AppLanguageDto) {
this.language = language;
}
public trackByContent(content: ContentDto): string {
return content.id;
}
private resetContents() {
this.contentItems = ImmutableArray.empty<ContentDto>();
this.contentsQuery = '';
this.contentsPager = new Pager(0);
this.selectedItems = {};
this.updateSelectionSummary();
}
public confirmStatusChange() {
this.dueTimeFunction!();
this.cancelStatusChange();
}
public cancelStatusChange() {
this.dueTimeMode = 'Immediately';
this.dueTimeDialog.hide();
this.dueTimeFunction = null;
this.dueTime = null;
}
} }

36
src/Squidex/app/features/content/pages/contents/search-form.component.ts

@ -69,23 +69,25 @@ export class SearchFormComponent implements OnChanges {
let odataFilter = ''; let odataFilter = '';
let odataSearch = ''; let odataSearch = '';
const parts = this.query.split('&'); if (this.query) {
const parts = this.query.split('&');
if (parts.length === 1 && parts[0][0] !== '$') {
odataSearch = parts[0]; if (parts.length === 1 && parts[0][0] !== '$') {
} else { odataSearch = parts[0];
for (let part of parts) { } else {
const kvp = part.split('='); for (let part of parts) {
const kvp = part.split('=');
if (kvp.length === 2) {
const key = kvp[0].toLowerCase(); if (kvp.length === 2) {
const key = kvp[0].toLowerCase();
if (key === '$filter') {
odataFilter = kvp[1]; if (key === '$filter') {
} else if (key === '$orderby') { odataFilter = kvp[1];
odataOrderBy = kvp[1]; } else if (key === '$orderby') {
} else if (key === '$search') { odataOrderBy = kvp[1];
odataSearch = kvp[1]; } else if (key === '$search') {
odataSearch = kvp[1];
}
} }
} }
} }

4
src/Squidex/app/features/content/pages/messages.ts

@ -5,9 +5,11 @@
* Copyright (c) Squidex UG (haftungsbeschränkt). All rights reserved. * Copyright (c) Squidex UG (haftungsbeschränkt). All rights reserved.
*/ */
import { Version } from '@app/shared';
export class ContentVersionSelected { export class ContentVersionSelected {
constructor( constructor(
public readonly version: number public readonly version: Version
) { ) {
} }
} }

10
src/Squidex/app/features/content/shared/assets-editor.component.ts

@ -49,14 +49,18 @@ export class AssetsEditorComponent implements ControlValueAccessor {
} }
public writeValue(value: string[]) { public writeValue(value: string[]) {
this.oldAssets = ImmutableArray.empty<AssetDto>(); if (Types.isArrayOfString(value) && !Types.isEquals(value, this.oldAssets.map(x => x.id).values)) {
if (Types.isArrayOfString(value) && value.length > 0) {
const assetIds: string[] = value; const assetIds: string[] = value;
this.assetsService.getAssets(this.appsState.appName, 0, 0, undefined, value) this.assetsService.getAssets(this.appsState.appName, 0, 0, undefined, value)
.subscribe(dtos => { .subscribe(dtos => {
this.oldAssets = ImmutableArray.of(assetIds.map(id => dtos.items.find(x => x.id === id)).filter(a => !!a).map(a => a!)); this.oldAssets = ImmutableArray.of(assetIds.map(id => dtos.items.find(x => x.id === id)).filter(a => !!a).map(a => a!));
if (this.oldAssets.length !== assetIds.length) {
this.updateValue();
}
}, () => {
this.oldAssets = ImmutableArray.empty<AssetDto>();
}); });
} }
} }

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

@ -6,23 +6,23 @@
</td> </td>
<td class="cell-auto" *ngFor="let field of schema.listFields; let i = index" (click)="shouldStop($event)"> <td class="cell-auto" *ngFor="let field of schema.listFields; let i = index" (click)="shouldStop($event)">
<div *ngIf="field.properties.inlineEditable && !isReadOnly" [formGroup]="form" (click)="$event.stopPropagation()"> <div *ngIf="field.properties['inlineEditable'] && !isReadOnly" [formGroup]="patchForm.form" (click)="$event.stopPropagation()">
<div [ngSwitch]="field.properties.fieldType"> <div [ngSwitch]="field.properties.fieldType">
<div *ngSwitchCase="'Number'"> <div *ngSwitchCase="'Number'">
<div [ngSwitch]="field.properties.editor"> <div [ngSwitch]="field.properties['editor']">
<div *ngSwitchCase="'Input'"> <div *ngSwitchCase="'Input'">
<input class="form-control" type="number" [formControlName]="field.name" [placeholder]="field.displayPlaceholder" /> <input class="form-control" type="number" [formControlName]="field.name" [placeholder]="field.displayPlaceholder" />
</div> </div>
<div *ngSwitchCase="'Dropdown'"> <div *ngSwitchCase="'Dropdown'">
<select class="form-control" [formControlName]="field.name"> <select class="form-control" [formControlName]="field.name">
<option [ngValue]="null"></option> <option [ngValue]="null"></option>
<option *ngFor="let value of field.properties.allowedValues" [ngValue]="value">{{value}}</option> <option *ngFor="let value of field.properties['allowedValues']" [ngValue]="value">{{value}}</option>
</select> </select>
</div> </div>
</div> </div>
</div> </div>
<div *ngSwitchCase="'String'"> <div *ngSwitchCase="'String'">
<div [ngSwitch]="field.properties.editor"> <div [ngSwitch]="field.properties['editor']">
<div *ngSwitchCase="'Input'"> <div *ngSwitchCase="'Input'">
<input class="form-control" type="text" [formControlName]="field.name" [placeholder]="field.displayPlaceholder" /> <input class="form-control" type="text" [formControlName]="field.name" [placeholder]="field.displayPlaceholder" />
</div> </div>
@ -32,13 +32,13 @@
<div *ngSwitchCase="'Dropdown'"> <div *ngSwitchCase="'Dropdown'">
<select class="form-control" [formControlName]="field.name"> <select class="form-control" [formControlName]="field.name">
<option [ngValue]="null"></option> <option [ngValue]="null"></option>
<option *ngFor="let value of field.properties.allowedValues" [ngValue]="value">{{value}}</option> <option *ngFor="let value of field.properties['allowedValues']" [ngValue]="value">{{value}}</option>
</select> </select>
</div> </div>
</div> </div>
</div> </div>
<div *ngSwitchCase="'Boolean'"> <div *ngSwitchCase="'Boolean'">
<div [ngSwitch]="field.properties.editor"> <div [ngSwitch]="field.properties['editor']">
<div *ngSwitchCase="'Toggle'"> <div *ngSwitchCase="'Toggle'">
<sqx-toggle [formControlName]="field.name"></sqx-toggle> <sqx-toggle [formControlName]="field.name"></sqx-toggle>
</div> </div>
@ -51,7 +51,7 @@
</div> </div>
</div> </div>
</div> </div>
<div *ngIf="!field.properties.inlineEditable || isReadOnly" class="truncate"> <div *ngIf="!field.properties['inlineEditable'] || isReadOnly" class="truncate">
{{values[i]}} {{values[i]}}
</div> </div>
</td> </td>
@ -75,22 +75,22 @@
<small class="item-modified">{{content.lastModified | sqxFromNow}}</small> <small class="item-modified">{{content.lastModified | sqxFromNow}}</small>
</td> </td>
<td class="cell-user" *ngIf="form.dirty" (click)="shouldStop($event)"> <td class="cell-user" *ngIf="patchForm.form.dirty" (click)="shouldStop($event)">
<button type="button" class="btn btn-success" (click)="save(); $event.stopPropagation()"> <button type="button" class="btn btn-success" (click)="save(); $event.stopPropagation()">
<i class="icon-checkmark"></i> <i class="icon-checkmark"></i>
</button> </button>
</td> </td>
<td class="cell-actions" *ngIf="form.dirty" (click)="shouldStop($event)"> <td class="cell-actions" *ngIf="patchForm.form.dirty" (click)="shouldStop($event)">
<button type="button" class="btn btn-link btn-secondary btn-cancel" (click)="save(); $event.stopPropagation()"> <button type="button" class="btn btn-link btn-secondary btn-cancel" (click)="save(); $event.stopPropagation()">
<i class="icon-close"></i> <i class="icon-close"></i>
</button> </button>
</td> </td>
<td class="cell-user" *ngIf="form.pristine" (click)="shouldStop($event)"> <td class="cell-user" *ngIf="patchForm.form.pristine" (click)="shouldStop($event)">
<img class="user-picture" [attr.title]="content.lastModifiedBy | sqxUserNameRef" [attr.src]="content.lastModifiedBy | sqxUserPictureRef" /> <img class="user-picture" [attr.title]="content.lastModifiedBy | sqxUserNameRef" [attr.src]="content.lastModifiedBy | sqxUserPictureRef" />
</td> </td>
<td class="cell-actions" *ngIf="!isReadOnly && form.pristine" (click)="shouldStop($event)"> <td class="cell-actions" *ngIf="!isReadOnly && patchForm.form.pristine" (click)="shouldStop($event)">
<div class="dropdown dropdown-options" *ngIf="content"> <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> <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> <i class="icon-dots"></i>

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

@ -5,21 +5,19 @@
* Copyright (c) Squidex UG (haftungsbeschränkt). All rights reserved. * Copyright (c) Squidex UG (haftungsbeschränkt). All rights reserved.
*/ */
import { Component, EventEmitter, Input, OnChanges, OnInit, Output } from '@angular/core'; import { Component, EventEmitter, Input, OnChanges, Output } from '@angular/core';
import { FormControl, FormGroup } from '@angular/forms';
import { import {
AppContext,
AppLanguageDto, AppLanguageDto,
ContentDto, ContentDto,
ContentsService, ContentsState,
fadeAnimation, fadeAnimation,
FieldDto, FieldDto,
fieldInvariant, fieldInvariant,
ModalView, ModalView,
PatchContentForm,
SchemaDetailsDto, SchemaDetailsDto,
Types, Types
Versioned
} from '@app/shared'; } from '@app/shared';
/* tslint:disable:component-selector */ /* tslint:disable:component-selector */
@ -28,19 +26,13 @@ import {
selector: '[sqxContent]', selector: '[sqxContent]',
styleUrls: ['./content-item.component.scss'], styleUrls: ['./content-item.component.scss'],
templateUrl: './content-item.component.html', templateUrl: './content-item.component.html',
providers: [
AppContext
],
animations: [ animations: [
fadeAnimation fadeAnimation
] ]
}) })
export class ContentItemComponent implements OnInit, OnChanges { export class ContentItemComponent implements OnChanges {
@Output() @Output()
public publishing = new EventEmitter(); public deleting = new EventEmitter();
@Output()
public unpublishing = new EventEmitter();
@Output() @Output()
public archiving = new EventEmitter(); public archiving = new EventEmitter();
@ -49,10 +41,10 @@ export class ContentItemComponent implements OnInit, OnChanges {
public restoring = new EventEmitter(); public restoring = new EventEmitter();
@Output() @Output()
public deleting = new EventEmitter(); public publishing = new EventEmitter();
@Output() @Output()
public saved = new EventEmitter<Versioned<any>>(); public unpublishing = new EventEmitter();
@Output() @Output()
public selectedChange = new EventEmitter(); public selectedChange = new EventEmitter();
@ -75,77 +67,43 @@ export class ContentItemComponent implements OnInit, OnChanges {
@Input('sqxContent') @Input('sqxContent')
public content: ContentDto; public content: ContentDto;
public formSubmitted = false; public patchForm: PatchContentForm;
public form: FormGroup = new FormGroup({});
public dropdown = new ModalView(false, true); public dropdown = new ModalView(false, true);
public values: any[] = []; public values: any[] = [];
constructor(public readonly ctx: AppContext, constructor(
private readonly contentsService: ContentsService private readonly contentsState: ContentsState
) { ) {
} }
public ngOnChanges() { public ngOnChanges() {
this.updateValues(); this.patchForm = new PatchContentForm(this.schema, this.language);
}
public ngOnInit() {
for (let field of this.schema.listFields) {
if (field.properties['inlineEditable']) {
this.form.setControl(field.name, new FormControl(undefined, field.createValidators(this.language.isOptional)));
}
}
this.updateValues(); this.updateValues();
} }
public shouldStop(event: Event) { public shouldStop(event: Event) {
if (this.form.dirty) { if (this.patchForm.form.dirty) {
event.stopPropagation(); event.stopPropagation();
event.stopImmediatePropagation(); event.stopImmediatePropagation();
} }
} }
public save() { public save() {
this.formSubmitted = true; const value = this.patchForm.submit();
if (this.form.dirty && this.form.valid) {
this.form.disable();
const request = {}; if (value) {
this.contentsState.patch(this.content, value)
for (let field of this.schema.listFields) { .subscribe(() => {
if (field.properties['inlineEditable']) { this.patchForm.submitCompleted();
const value = this.form.controls[field.name].value;
if (field.isLocalizable) {
request[field.name] = { [this.language.iso2Code]: value };
} else {
request[field.name] = { iv: 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 => { }, error => {
this.ctx.notifyError(error); this.patchForm.submitFailed(error);
}); });
} }
} }
private emitSaved(data: Versioned<any>) {
this.saved.emit(data);
}
private updateValues() { private updateValues() {
this.values = []; this.values = [];
@ -158,8 +116,8 @@ export class ContentItemComponent implements OnInit, OnChanges {
this.values.push(field.formatValue(value)); this.values.push(field.formatValue(value));
} }
if (this.form) { if (this.patchForm) {
const formControl = this.form.controls[field.name]; const formControl = this.patchForm.form.controls[field.name];
if (formControl) { if (formControl) {
formControl.setValue(value); formControl.setValue(value);

18
src/Squidex/app/features/content/shared/contents-selector.component.html

@ -9,17 +9,21 @@
<i class="icon-reset"></i> Refresh <i class="icon-reset"></i> Refresh
</button> </button>
<sqx-search-form (queryChanged)="search($event)" [canArchive]="false"></sqx-search-form> <sqx-search-form
[query]="contentsState.contentsQuery | async"
(queryChanged)="search($event)"
[canArchive]="false">
</sqx-search-form>
<ng-container *ngIf="languages.length > 1"> <ng-container *ngIf="languages.length > 1">
<sqx-language-selector class="languages-buttons" (selectedLanguageChanged)="selectLanguage($event)" [languages]="languages.values"></sqx-language-selector> <sqx-language-selector class="languages-buttons" (selectedLanguageChange)="selectLanguage($event)" [languages]="languages.values"></sqx-language-selector>
</ng-container> </ng-container>
</div> </div>
</ng-container> </ng-container>
<ng-container content> <ng-container content>
<div class="grid-header"> <div class="grid-header">
<table class="table table-items table-fixed" *ngIf="contentItems"> <table class="table table-items table-fixed">
<thead> <thead>
<tr> <tr>
<th class="cell-select"> <th class="cell-select">
@ -41,12 +45,12 @@
<div class="grid-content"> <div class="grid-content">
<div sqxIgnoreScrollbar> <div sqxIgnoreScrollbar>
<table class="table table-items table-fixed" *ngIf="contentItems"> <table class="table table-items table-fixed">
<tbody> <tbody>
<ng-template ngFor let-content [ngForOf]="contentItems" [ngForTrackBy]="trackByContent"> <ng-template ngFor let-content [ngForOf]="contentsState.contents | async" [ngForTrackBy]="trackByContent">
<tr [sqxContent]="content" <tr [sqxContent]="content"
[selected]="isItemSelected(content)" [selected]="isItemSelected(content)"
(selectedChange)="onContentSelected(content)" (selectedChange)="selectContent(content)"
[language]="language" [language]="language"
[schema]="schema" [schema]="schema"
isReadOnly="true"></tr> isReadOnly="true"></tr>
@ -58,7 +62,7 @@
</div> </div>
<div class="grid-footer"> <div class="grid-footer">
<sqx-pager [pager]="contentsPager"></sqx-pager> <sqx-pager [pager]="contentsState.contentsPager | async"></sqx-pager>
</div> </div>
</ng-container> </ng-container>

71
src/Squidex/app/features/content/shared/contents-selector.component.ts

@ -8,21 +8,20 @@
import { Component, EventEmitter, Input, OnInit, Output } from '@angular/core'; import { Component, EventEmitter, Input, OnInit, Output } from '@angular/core';
import { import {
AppsState,
ContentDto, ContentDto,
ContentsService,
DialogService,
ImmutableArray,
LanguageDto, LanguageDto,
ManualContentsState,
ModalView, ModalView,
Pager,
SchemaDetailsDto SchemaDetailsDto
} from '@app/shared'; } from '@app/shared';
@Component({ @Component({
selector: 'sqx-contents-selector', selector: 'sqx-contents-selector',
styleUrls: ['./contents-selector.component.scss'], styleUrls: ['./contents-selector.component.scss'],
templateUrl: './contents-selector.component.html' templateUrl: './contents-selector.component.html',
providers: [
ManualContentsState
]
}) })
export class ContentsSelectorComponent implements OnInit { export class ContentsSelectorComponent implements OnInit {
@Input() @Input()
@ -39,76 +38,42 @@ export class ContentsSelectorComponent implements OnInit {
public searchModal = new ModalView(); public searchModal = new ModalView();
public contentItems: ImmutableArray<ContentDto>;
public contentsQuery = '';
public contentsPager = new Pager(0);
public selectedItems: { [id: string]: ContentDto; } = {}; public selectedItems: { [id: string]: ContentDto; } = {};
public selectionCount = 0; public selectionCount = 0;
public isAllSelected = false; public isAllSelected = false;
constructor( constructor(
private readonly appsState: AppsState, public readonly contentsState: ManualContentsState
private readonly contentsService: ContentsService,
private readonly dialogs: DialogService
) { ) {
} }
public ngOnInit() { public ngOnInit() {
this.load(); this.contentsState.schema = this.schema;
}
public reload() { this.contentsState.load().onErrorResumeNext().subscribe();
this.load(true);
} }
private load(notifyLod = false) { public reload() {
this.contentsService.getContents(this.appsState.appName, this.schema.name, this.contentsPager.pageSize, this.contentsPager.skip, this.contentsQuery, undefined, false) this.contentsState.load(true).onErrorResumeNext().subscribe();
.finally(() => {
this.selectedItems = {};
this.updateSelectionSummary();
})
.subscribe(dtos => {
this.contentItems = ImmutableArray.of(dtos.items);
this.contentsPager = this.contentsPager.setCount(dtos.total);
if (notifyLod) {
this.dialogs.notifyInfo('Contents reloaded.');
}
}, error => {
this.dialogs.notifyError(error);
});
} }
public search(query: string) { public search(query: string) {
this.contentsQuery = query; this.contentsState.search(query).onErrorResumeNext().subscribe();
this.contentsPager = new Pager(0);
this.load();
} }
public goNext() { public goNext() {
this.contentsPager = this.contentsPager.goNext(); this.contentsState.goNext().onErrorResumeNext().subscribe();
this.load();
} }
public goPrev() { public goPrev() {
this.contentsPager = this.contentsPager.goPrev(); this.contentsState.goPrev().onErrorResumeNext().subscribe();
this.load();
} }
public isItemSelected(content: ContentDto) { public isItemSelected(content: ContentDto) {
return this.selectedItems[content.id]; return this.selectedItems[content.id];
} }
public selectLanguage(language: LanguageDto) {
this.language = language;
}
public complete() { public complete() {
this.selected.emit([]); this.selected.emit([]);
} }
@ -117,11 +82,15 @@ export class ContentsSelectorComponent implements OnInit {
this.selected.emit(Object.values(this.selectedItems)); this.selected.emit(Object.values(this.selectedItems));
} }
public selectLanguage(language: LanguageDto) {
this.language = language;
}
public selectAll(isSelected: boolean) { public selectAll(isSelected: boolean) {
this.selectedItems = {}; this.selectedItems = {};
if (isSelected) { if (isSelected) {
for (let content of this.contentItems.values) { for (let content of this.contentsState.snapshot.contents.values) {
this.selectedItems[content.id] = content; this.selectedItems[content.id] = content;
} }
} }
@ -129,7 +98,7 @@ export class ContentsSelectorComponent implements OnInit {
this.updateSelectionSummary(); this.updateSelectionSummary();
} }
public onContentSelected(content: ContentDto) { public selectContent(content: ContentDto) {
if (this.selectedItems[content.id]) { if (this.selectedItems[content.id]) {
delete this.selectedItems[content.id]; delete this.selectedItems[content.id];
} else { } else {
@ -142,7 +111,7 @@ export class ContentsSelectorComponent implements OnInit {
private updateSelectionSummary() { private updateSelectionSummary() {
this.selectionCount = Object.keys(this.selectedItems).length; this.selectionCount = Object.keys(this.selectedItems).length;
this.isAllSelected = this.selectionCount === this.contentItems.length; this.isAllSelected = this.selectionCount === this.contentsState.snapshot.contents.length;
} }
public trackByContent(content: ContentDto): string { public trackByContent(content: ContentDto): string {

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

@ -1,27 +1,29 @@
<div class="references-container" [class.disabled]="isDisabled"> <div class="references-container" [class.disabled]="isDisabled">
<div class="drop-area-container" *ngIf="schema"> <ng-container *ngIf="schema">
<div class="drop-area" (click)="showModal()"> <div class="drop-area-container">
Click here to link a content. <div class="drop-area" (click)="showModal()">
Click here to link a content.
</div>
</div> </div>
</div>
<table class="table table-items table-fixed" [class.disabled]="isDisabled" *ngIf="schema && contentItems && contentItems.length > 0">
<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)="sort($event)"
[language]="language"
[schema]="schema"
(removing)="remove(content)"
isReadOnly="true"
isReference="true"></tr>
<tr class="spacer"></tr>
</ng-template>
</tbody>
</table>
</ng-container>
<div class="invalid" *ngIf="isInvalidSchema"> <div class="invalid" *ngIf="isInvalidSchema">
Schema not found or not configured yet. Schema not found or not configured yet.
</div> </div>
<table class="table table-items table-fixed" [class.disabled]="isDisabled" *ngIf="contentItems && contentItems.length > 0">
<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)"
[language]="language"
[schema]="schema"
(deleting)="onContentRemoving(content)"
isReadOnly="true"
isReference="true"></tr>
<tr class="spacer"></tr>
</ng-template>
</tbody>
</table>
</div> </div>
<ng-container *sqxModalView="isModalVisibible;onRoot:true;closeAuto:false"> <ng-container *sqxModalView="isModalVisibible;onRoot:true;closeAuto:false">
@ -29,6 +31,6 @@
[language]="language" [language]="language"
[languages]="languages" [languages]="languages"
[schema]="schema" [schema]="schema"
(selected)="onContentsSelected($event)"> (selected)="select($event)">
</sqx-contents-selector> </sqx-contents-selector>
</ng-container> </ng-container>

30
src/Squidex/app/features/content/shared/references-editor.component.scss

@ -5,17 +5,6 @@
pointer-events: none; pointer-events: none;
} }
.references {
&-container {
& {
background: $color-background;
overflow-x: hidden;
overflow-y: scroll;
padding: 1rem;
}
}
}
.invalid { .invalid {
padding: 2rem; padding: 2rem;
font-size: 1.2rem; font-size: 1.2rem;
@ -24,6 +13,15 @@
color: darken($color-border, 30%); color: darken($color-border, 30%);
} }
.references-container {
background: $color-background;
overflow-x: hidden;
overflow-y: auto;
padding: 1rem;
min-height: 2rem;
max-height: 20rem;
}
.drop-area { .drop-area {
& { & {
@include transition(border-color .4s ease); @include transition(border-color .4s ease);
@ -43,12 +41,6 @@
} }
.table { .table {
& { margin-bottom: -.25rem;
margin-bottom: -.25rem; margin-top: 1rem;
margin-top: 1rem;
}
&.disabled {
margin-top: 0;
}
} }

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

@ -65,6 +65,7 @@ export class ReferencesEditorComponent implements ControlValueAccessor, OnInit {
public ngOnInit() { public ngOnInit() {
if (this.schemaId === MathHelper.EMPTY_GUID) { if (this.schemaId === MathHelper.EMPTY_GUID) {
this.isInvalidSchema = true;
return; return;
} }
@ -77,14 +78,18 @@ export class ReferencesEditorComponent implements ControlValueAccessor, OnInit {
} }
public writeValue(value: string[]) { public writeValue(value: string[]) {
this.contentItems = ImmutableArray.empty<ContentDto>(); if (Types.isArrayOfString(value) && !Types.isEquals(value, this.contentItems.map(x => x.id).values)) {
if (Types.isArrayOfString(value) && value.length > 0) {
const contentIds: string[] = value; const contentIds: string[] = value;
this.contentsService.getContents(this.appsState.appName, this.schemaId, 10000, 0, undefined, contentIds) this.contentsService.getContents(this.appsState.appName, this.schemaId, 10000, 0, undefined, contentIds)
.subscribe(dtos => { .subscribe(dtos => {
this.contentItems = ImmutableArray.of(contentIds.map(id => dtos.items.find(c => c.id === id)).filter(r => !!r).map(r => r!)); this.contentItems = ImmutableArray.of(contentIds.map(id => dtos.items.find(c => c.id === id)).filter(r => !!r).map(r => r!));
if (this.contentItems.length !== contentIds.length) {
this.updateValue();
}
}, () => {
this.contentItems = ImmutableArray.empty<ContentDto>();
}); });
} }
} }
@ -109,7 +114,7 @@ export class ReferencesEditorComponent implements ControlValueAccessor, OnInit {
this.isModalVisibible = false; this.isModalVisibible = false;
} }
public onContentsSelected(contents: ContentDto[]) { public select(contents: ContentDto[]) {
for (let content of contents) { for (let content of contents) {
this.contentItems = this.contentItems.push(content); this.contentItems = this.contentItems.push(content);
} }
@ -121,7 +126,7 @@ export class ReferencesEditorComponent implements ControlValueAccessor, OnInit {
this.hideModal(); this.hideModal();
} }
public onContentRemoving(content: ContentDto) { public remove(content: ContentDto) {
if (content) { if (content) {
this.contentItems = this.contentItems.remove(content); this.contentItems = this.contentItems.remove(content);
@ -129,7 +134,7 @@ export class ReferencesEditorComponent implements ControlValueAccessor, OnInit {
} }
} }
public onContentsSorted(contents: ContentDto[]) { public sort(contents: ContentDto[]) {
if (contents) { if (contents) {
this.contentItems = ImmutableArray.of(contents); this.contentItems = ImmutableArray.of(contents);

2
src/Squidex/app/features/rules/pages/events/rule-events-page.component.html

@ -1,4 +1,4 @@
<sqx-title message="{app} | Rules Events" parameter1="app" [value1]="ctx.appName"></sqx-title> <sqx-title message="{app} | Rules Events" parameter1="app" [value1]="appsState.appName"></sqx-title>
<sqx-panel desiredWidth="63rem"> <sqx-panel desiredWidth="63rem">
<ng-container title> <ng-container title>

24
src/Squidex/app/features/rules/pages/events/rule-events-page.component.ts

@ -8,7 +8,8 @@
import { Component, OnInit } from '@angular/core'; import { Component, OnInit } from '@angular/core';
import { import {
AppContext, AppsState,
DialogService,
ImmutableArray, ImmutableArray,
Pager, Pager,
RuleEventDto, RuleEventDto,
@ -18,10 +19,7 @@ import {
@Component({ @Component({
selector: 'sqx-rule-events-page', selector: 'sqx-rule-events-page',
styleUrls: ['./rule-events-page.component.scss'], styleUrls: ['./rule-events-page.component.scss'],
templateUrl: './rule-events-page.component.html', templateUrl: './rule-events-page.component.html'
providers: [
AppContext
]
}) })
export class RuleEventsPageComponent implements OnInit { export class RuleEventsPageComponent implements OnInit {
public eventsItems = ImmutableArray.empty<RuleEventDto>(); public eventsItems = ImmutableArray.empty<RuleEventDto>();
@ -29,7 +27,9 @@ export class RuleEventsPageComponent implements OnInit {
public selectedEventId: string | null = null; public selectedEventId: string | null = null;
constructor(public readonly ctx: AppContext, constructor(
public readonly appsState: AppsState,
private readonly dialogs: DialogService,
private readonly rulesService: RulesService private readonly rulesService: RulesService
) { ) {
} }
@ -39,25 +39,25 @@ export class RuleEventsPageComponent implements OnInit {
} }
public load(notifyLoad = false) { public load(notifyLoad = false) {
this.rulesService.getEvents(this.ctx.appName, this.eventsPager.pageSize, this.eventsPager.skip) this.rulesService.getEvents(this.appsState.appName, this.eventsPager.pageSize, this.eventsPager.skip)
.subscribe(dtos => { .subscribe(dtos => {
this.eventsItems = ImmutableArray.of(dtos.items); this.eventsItems = ImmutableArray.of(dtos.items);
this.eventsPager = this.eventsPager.setCount(dtos.total); this.eventsPager = this.eventsPager.setCount(dtos.total);
if (notifyLoad) { if (notifyLoad) {
this.ctx.notifyInfo('Events reloaded.'); this.dialogs.notifyInfo('Events reloaded.');
} }
}, error => { }, error => {
this.ctx.notifyError(error); this.dialogs.notifyError(error);
}); });
} }
public enqueueEvent(event: RuleEventDto) { public enqueueEvent(event: RuleEventDto) {
this.rulesService.enqueueEvent(this.ctx.appName, event.id) this.rulesService.enqueueEvent(this.appsState.appName, event.id)
.subscribe(() => { .subscribe(() => {
this.ctx.notifyInfo('Events enqueued. Will be resend in a few seconds.'); this.dialogs.notifyInfo('Events enqueued. Will be resend in a few seconds.');
}, error => { }, error => {
this.ctx.notifyError(error); this.dialogs.notifyError(error);
}); });
} }

18
src/Squidex/app/framework/utils/types.ts

@ -67,4 +67,22 @@ export module Types {
return true; return true;
} }
export function isEquals<T>(lhs: T[], rhs: T[]) {
if (!lhs && !rhs) {
return true;
}
if (lhs.length !== rhs.length) {
return false;
}
for (let i = 0; i < lhs.length; i++) {
if (rhs[i] !== lhs[i]) {
return false;
}
}
return true;
}
} }

69
src/Squidex/app/shared/components/app-context.ts

@ -1,69 +0,0 @@
/*
* Squidex Headless CMS
*
* @license
* Copyright (c) Squidex UG (haftungsbeschränkt). All rights reserved.
*/
import { Injectable } from '@angular/core';
import { ActivatedRoute } from '@angular/router';
import { Observable } from 'rxjs';
import { MessageBus } from '@app/framework';
import {
AppDto,
AppsState,
AuthService,
DialogService,
ErrorDto,
Profile
} from '@app/shared/internal';
@Injectable()
export class AppContext {
public get app(): AppDto {
return this.appsState.snapshot.selectedApp!;
}
public get appChanges(): Observable<AppDto | null> {
return this.appsState.selectedApp;
}
public get appName(): string {
return this.app.name;
}
public get userToken(): string {
return this.authService.user!.token;
}
public get userId(): string {
return this.authService.user!.id;
}
public get user(): Profile {
return this.authService.user!;
}
constructor(
public readonly dialogs: DialogService,
public readonly authService: AuthService,
public readonly appsState: AppsState,
public readonly route: ActivatedRoute,
public readonly bus: MessageBus
) {
}
public confirmUnsavedChanges(): Observable<boolean> {
return this.dialogs.confirmUnsavedChanges();
}
public notifyInfo(error: string) {
this.dialogs.notifyInfo(error);
}
public notifyError(error: string | ErrorDto) {
this.dialogs.notifyError(error);
}
}

12
src/Squidex/app/shared/components/assets-list.component.html

@ -15,18 +15,18 @@
</div> </div>
<div class="row"> <div class="row">
<sqx-asset class="{{assetClass}}" *ngFor="let file of newFiles" [initFile]="file" <sqx-asset *ngFor="let file of newFiles" [initFile]="file"
(failed)="onAssetFailed(file)" (failed)="remove(file)"
(loaded)="onAssetLoaded(file, $event)"> (loaded)="add(file, $event)">
</sqx-asset> </sqx-asset>
<ng-container *ngIf="state.assets | async; let assets"> <ng-container *ngIf="state.assets | async; let assets">
<sqx-asset class="{{assetClass}}" *ngFor="let asset of assets; trackBy: trackByAsset" [asset]="asset" <sqx-asset *ngFor="let asset of assets; trackBy: trackByAsset" [asset]="asset"
[isDisabled]="isDisabled" [isDisabled]="isDisabled"
[isSelectable]="selectedIds" [isSelectable]="selectedIds"
[isSelected]="isSelected(asset)" [isSelected]="isSelected(asset)"
(selected)="onAssetSelected($event)" (selected)="select($event)"
(deleting)="onAssetDeleting($event)"> (deleting)="delete($event)">
</sqx-asset> </sqx-asset>
</ng-container> </ng-container>
</div> </div>

23
src/Squidex/app/shared/components/assets-list.component.ts

@ -32,13 +32,10 @@ export class AssetsListComponent {
@Input() @Input()
public selectedIds: object; public selectedIds: object;
@Input()
public assetClass = '';
@Output() @Output()
public selected = new EventEmitter<AssetDto>(); public selected = new EventEmitter<AssetDto>();
public onAssetLoaded(file: File, asset: AssetDto) { public add(file: File, asset: AssetDto) {
this.newFiles = this.newFiles.remove(file); this.newFiles = this.newFiles.remove(file);
this.state.add(asset); this.state.add(asset);
@ -48,18 +45,10 @@ export class AssetsListComponent {
this.state.load().onErrorResumeNext().subscribe(); this.state.load().onErrorResumeNext().subscribe();
} }
public onAssetDeleting(asset: AssetDto) { public delete(asset: AssetDto) {
this.state.delete(asset).onErrorResumeNext().subscribe(); this.state.delete(asset).onErrorResumeNext().subscribe();
} }
public onAssetSelected(asset: AssetDto) {
this.selected.emit(asset);
}
public onAssetFailed(file: File) {
this.newFiles = this.newFiles.remove(file);
}
public goNext() { public goNext() {
this.state.goNext().onErrorResumeNext().subscribe(); this.state.goNext().onErrorResumeNext().subscribe();
} }
@ -72,10 +61,18 @@ export class AssetsListComponent {
return asset.id; return asset.id;
} }
public select(asset: AssetDto) {
this.selected.emit(asset);
}
public isSelected(asset: AssetDto) { public isSelected(asset: AssetDto) {
return this.selectedIds && this.selectedIds[asset.id]; return this.selectedIds && this.selectedIds[asset.id];
} }
public remove(file: File) {
this.newFiles = this.newFiles.remove(file);
}
public addFiles(files: FileList) { public addFiles(files: FileList) {
for (let i = 0; i < files.length; i++) { for (let i = 0; i < files.length; i++) {
this.newFiles = this.newFiles.pushFront(files[i]); this.newFiles = this.newFiles.pushFront(files[i]);

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

@ -10,8 +10,8 @@
</ng-container> </ng-container>
<ng-container content> <ng-container content>
<sqx-assets-list assetClass="asset-default" size="4" <sqx-assets-list
(selected)="onAssetSelected($event)" (selected)="selectAsset($event)"
[selectedIds]="selectedAssets" [selectedIds]="selectedAssets"
[state]="state" isDisabled="true"> [state]="state" isDisabled="true">
</sqx-assets-list> </sqx-assets-list>

2
src/Squidex/app/shared/components/assets-selector.component.ts

@ -56,7 +56,7 @@ export class AssetsSelectorComponent implements OnInit {
this.selected.emit(Object.values(this.selectedAssets)); this.selected.emit(Object.values(this.selectedAssets));
} }
public onAssetSelected(asset: AssetDto) { public selectAsset(asset: AssetDto) {
if (this.selectedAssets[asset.id]) { if (this.selectedAssets[asset.id]) {
delete this.selectedAssets[asset.id]; delete this.selectedAssets[asset.id];
} else { } else {

25
src/Squidex/app/shared/components/history.component.ts

@ -6,33 +6,31 @@
*/ */
import { Component } from '@angular/core'; import { Component } from '@angular/core';
import { ActivatedRoute } from '@angular/router';
import { Observable } from 'rxjs'; import { Observable } from 'rxjs';
import { AppContext } from './app-context';
import { import {
allParams, allParams,
AppsState,
formatHistoryMessage, formatHistoryMessage,
HistoryChannelUpdated, HistoryChannelUpdated,
HistoryEventDto, HistoryEventDto,
HistoryService, HistoryService,
MessageBus,
UsersProviderService UsersProviderService
} from '@app/shared/internal'; } from '@app/shared/internal';
@Component({ @Component({
selector: 'sqx-history', selector: 'sqx-history',
styleUrls: ['./history.component.scss'], styleUrls: ['./history.component.scss'],
templateUrl: './history.component.html', templateUrl: './history.component.html'
providers: [
AppContext
]
}) })
export class HistoryComponent { export class HistoryComponent {
public get channel(): string { public get channel(): string {
let channelPath = this.ctx.route.snapshot.data['channel']; let channelPath = this.route.snapshot.data['channel'];
if (channelPath) { if (channelPath) {
const params = allParams(this.ctx.route); const params = allParams(this.route);
for (let key in params) { for (let key in params) {
if (params.hasOwnProperty(key)) { if (params.hasOwnProperty(key)) {
@ -47,12 +45,15 @@ export class HistoryComponent {
} }
public events: Observable<HistoryEventDto[]> = public events: Observable<HistoryEventDto[]> =
Observable.timer(0, 10000).merge(this.ctx.bus.of(HistoryChannelUpdated).delay(1000)) Observable.timer(0, 10000).merge(this.messageBus.of(HistoryChannelUpdated).delay(1000))
.switchMap(app => this.historyService.getHistory(this.ctx.appName, this.channel)); .switchMap(app => this.historyService.getHistory(this.appsState.appName, this.channel));
constructor(public readonly ctx: AppContext, constructor(
private readonly appsState: AppsState,
private readonly users: UsersProviderService, private readonly users: UsersProviderService,
private readonly historyService: HistoryService private readonly historyService: HistoryService,
private readonly messageBus: MessageBus,
private readonly route: ActivatedRoute
) { ) {
} }

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

@ -32,7 +32,7 @@ export class LanguageSelectorComponent implements OnChanges, OnInit {
public selectedLanguage: Language; public selectedLanguage: Language;
@Output() @Output()
public selectedLanguageChanged = new EventEmitter<Language>(); public selectedLanguageChange = new EventEmitter<Language>();
public get isSmallMode(): boolean { public get isSmallMode(): boolean {
return this.languages && this.languages.length > 0 && this.languages.length <= 3; return this.languages && this.languages.length > 0 && this.languages.length <= 3;
@ -62,6 +62,6 @@ export class LanguageSelectorComponent implements OnChanges, OnInit {
public selectLanguage(language: Language) { public selectLanguage(language: Language) {
this.selectedLanguage = language; this.selectedLanguage = language;
this.selectedLanguageChanged.emit(language); this.selectedLanguageChange.emit(language);
} }
} }

1
src/Squidex/app/shared/declarations.ts

@ -5,7 +5,6 @@
* Copyright (c) Squidex UG (haftungsbeschränkt). All rights reserved. * Copyright (c) Squidex UG (haftungsbeschränkt). All rights reserved.
*/ */
export * from './components/app-context';
export * from './components/app-form.component'; export * from './components/app-form.component';
export * from './components/asset.component'; export * from './components/asset.component';
export * from './components/assets-list.component'; export * from './components/assets-list.component';

62
src/Squidex/app/shared/guards/content-must-exist.guard.spec.ts

@ -0,0 +1,62 @@
/*
* Squidex Headless CMS
*
* @license
* Copyright (c) Squidex UG (haftungsbeschränkt). All rights reserved.
*/
import { Router } from '@angular/router';
import { Observable } from 'rxjs';
import { IMock, Mock, Times } from 'typemoq';
import { ContentDto } from './../services/contents.service';
import { ContentsState } from './../state/contents.state';
import { ContentMustExistGuard } from './content-must-exist.guard';
describe('ContentMustExistGuard', () => {
const route: any = {
params: {
contentId: '123'
}
};
let contentsState: IMock<ContentsState>;
let router: IMock<Router>;
let contentGuard: ContentMustExistGuard;
beforeEach(() => {
router = Mock.ofType<Router>();
contentsState = Mock.ofType<ContentsState>();
contentGuard = new ContentMustExistGuard(contentsState.object, router.object);
});
it('should load content and return true when found', () => {
contentsState.setup(x => x.select('123'))
.returns(() => Observable.of(<ContentDto>{}));
let result: boolean;
contentGuard.canActivate(route).subscribe(x => {
result = x;
}).unsubscribe();
expect(result!).toBeTruthy();
contentsState.verify(x => x.select('123'), Times.once());
});
it('should load content and return false when not found', () => {
contentsState.setup(x => x.select('123'))
.returns(() => Observable.of(null));
let result: boolean;
contentGuard.canActivate(route).subscribe(x => {
result = x;
}).unsubscribe();
expect(result!).toBeFalsy();
router.verify(x => x.navigate(['/404']), Times.once());
});
});

38
src/Squidex/app/shared/guards/content-must-exist.guard.ts

@ -0,0 +1,38 @@
/*
* Squidex Headless CMS
*
* @license
* Copyright (c) Squidex UG (haftungsbeschränkt). All rights reserved.
*/
import { Injectable } from '@angular/core';
import { ActivatedRouteSnapshot, CanActivate, Router } from '@angular/router';
import { Observable } from 'rxjs';
import { allParams } from '@app/framework';
import { ContentsState } from './../state/contents.state';
@Injectable()
export class ContentMustExistGuard implements CanActivate {
constructor(
private readonly contentsState: ContentsState,
private readonly router: Router
) {
}
public canActivate(route: ActivatedRouteSnapshot): Observable<boolean> {
const contentId = allParams(route)['contentId'];
const result =
this.contentsState.select(contentId)
.do(dto => {
if (!dto) {
this.router.navigate(['/404']);
}
})
.map(u => u !== null);
return result;
}
}

46
src/Squidex/app/shared/guards/resolve-content.guard.ts

@ -1,46 +0,0 @@
/*
* Squidex Headless CMS
*
* @license
* Copyright (c) Squidex UG (haftungsbeschränkt). All rights reserved.
*/
import { Injectable } from '@angular/core';
import { ActivatedRouteSnapshot, Resolve, Router } from '@angular/router';
import { Observable } from 'rxjs';
import { allParams } from '@app/framework';
import { ContentDto, ContentsService } from './../services/contents.service';
@Injectable()
export class ResolveContentGuard implements Resolve<ContentDto | null> {
constructor(
private readonly contentsService: ContentsService,
private readonly router: Router
) {
}
public resolve(route: ActivatedRouteSnapshot): Observable<ContentDto | null> {
const params = allParams(route);
const appName = params['appName'];
const contentId = params['contentId'];
const schemaName = params['schemaName'];
const result =
this.contentsService.getContent(appName, schemaName, contentId)
.do(dto => {
if (!dto) {
this.router.navigate(['/404']);
}
})
.catch(error => {
this.router.navigate(['/404']);
return Observable.of(null);
});
return result;
}
}

37
src/Squidex/app/shared/guards/unset-content.guard.spec.ts

@ -0,0 +1,37 @@
/*
* Squidex Headless CMS
*
* @license
* Copyright (c) Squidex UG (haftungsbeschränkt). All rights reserved.
*/
import { Observable } from 'rxjs';
import { IMock, Mock, Times } from 'typemoq';
import { ContentsState } from './../state/contents.state';
import { UnsetContentGuard } from './unset-content.guard';
describe('UnsetContentGuard', () => {
let contentsState: IMock<ContentsState>;
let contentGuard: UnsetContentGuard;
beforeEach(() => {
contentsState = Mock.ofType<ContentsState>();
contentGuard = new UnsetContentGuard(contentsState.object);
});
it('should unset content', () => {
contentsState.setup(x => x.select(null))
.returns(() => Observable.of(null));
let result: boolean;
contentGuard.canActivate().subscribe(x => {
result = x;
}).unsubscribe();
expect(result!).toBeTruthy();
contentsState.verify(x => x.select(null), Times.once());
});
});

24
src/Squidex/app/shared/guards/unset-content.guard.ts

@ -0,0 +1,24 @@
/*
* Squidex Headless CMS
*
* @license
* Copyright (c) Squidex UG (haftungsbeschränkt). All rights reserved.
*/
import { Injectable } from '@angular/core';
import { CanActivate } from '@angular/router';
import { Observable } from 'rxjs';
import { ContentsState } from './../state/contents.state';
@Injectable()
export class UnsetContentGuard implements CanActivate {
constructor(
private readonly usersState: ContentsState
) {
}
public canActivate(): Observable<boolean> {
return this.usersState.select(null).map(u => u === null);
}
}

4
src/Squidex/app/shared/internal.ts

@ -6,14 +6,15 @@
*/ */
export * from './guards/app-must-exist.guard'; export * from './guards/app-must-exist.guard';
export * from './guards/content-must-exist.guard';
export * from './guards/load-apps.guard'; export * from './guards/load-apps.guard';
export * from './guards/load-languages.guard'; export * from './guards/load-languages.guard';
export * from './guards/must-be-authenticated.guard'; export * from './guards/must-be-authenticated.guard';
export * from './guards/must-be-not-authenticated.guard'; export * from './guards/must-be-not-authenticated.guard';
export * from './guards/resolve-content.guard';
export * from './guards/schema-must-exist-published.guard'; export * from './guards/schema-must-exist-published.guard';
export * from './guards/schema-must-exist.guard'; export * from './guards/schema-must-exist.guard';
export * from './guards/unset-app.guard'; export * from './guards/unset-app.guard';
export * from './guards/unset-content.guard';
export * from './interceptors/auth.interceptor'; export * from './interceptors/auth.interceptor';
@ -42,6 +43,7 @@ export * from './state/apps.state';
export * from './state/assets.state'; export * from './state/assets.state';
export * from './state/backups.state'; export * from './state/backups.state';
export * from './state/clients.state'; export * from './state/clients.state';
export * from './state/contents.state';
export * from './state/contributors.state'; export * from './state/contributors.state';
export * from './state/languages.state'; export * from './state/languages.state';
export * from './state/patterns.state'; export * from './state/patterns.state';

8
src/Squidex/app/shared/module.ts

@ -34,7 +34,9 @@ import {
BackupsService, BackupsService,
BackupsState, BackupsState,
ClientsState, ClientsState,
ContentMustExistGuard,
ContentsService, ContentsService,
ContentsState,
ContributorsState, ContributorsState,
FileIconPipe, FileIconPipe,
GeolocationEditorComponent, GeolocationEditorComponent,
@ -54,7 +56,6 @@ import {
PatternsState, PatternsState,
PlansService, PlansService,
PlansState, PlansState,
ResolveContentGuard,
RichEditorComponent, RichEditorComponent,
RulesService, RulesService,
RulesState, RulesState,
@ -64,6 +65,7 @@ import {
SchemasState, SchemasState,
UIService, UIService,
UnsetAppGuard, UnsetAppGuard,
UnsetContentGuard,
UsagesService, UsagesService,
UserDtoPicture, UserDtoPicture,
UserIdPicturePipe, UserIdPicturePipe,
@ -146,7 +148,9 @@ export class SqxSharedModule {
BackupsService, BackupsService,
BackupsState, BackupsState,
ClientsState, ClientsState,
ContentMustExistGuard,
ContentsService, ContentsService,
ContentsState,
ContributorsState, ContributorsState,
GraphQlService, GraphQlService,
HelpService, HelpService,
@ -160,7 +164,6 @@ export class SqxSharedModule {
PatternsState, PatternsState,
PlansService, PlansService,
PlansState, PlansState,
ResolveContentGuard,
RulesService, RulesService,
RulesState, RulesState,
SchemaMustExistGuard, SchemaMustExistGuard,
@ -169,6 +172,7 @@ export class SqxSharedModule {
SchemasState, SchemasState,
UIService, UIService,
UnsetAppGuard, UnsetAppGuard,
UnsetContentGuard,
UsagesService, UsagesService,
UsersProviderService, UsersProviderService,
UsersService, UsersService,

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

@ -18,58 +18,6 @@ import {
Version Version
} from './../'; } from './../';
describe('ContentDto', () => {
const creation = DateTime.today();
const creator = 'not-me';
const modified = DateTime.now();
const modifier = 'me';
const dueTime = DateTime.now().addDays(1);
const version = new Version('1');
const newVersion = new Version('2');
it('should update data property and user info when updating', () => {
const content_1 = new ContentDto('1', 'Published', creator, creator, creation, creation, null, null, null, { data: 1 }, version);
const content_2 = content_1.update({ data: 2 }, modifier, newVersion, modified);
expect(content_2.data).toEqual({ data: 2 });
expect(content_2.lastModified).toEqual(modified);
expect(content_2.lastModifiedBy).toEqual(modifier);
expect(content_2.version).toEqual(newVersion);
});
it('should update status property and user info when changing status', () => {
const content_1 = new ContentDto('1', 'Draft', creator, creator, creation, creation, null, null, null, { data: 1 }, version);
const content_2 = content_1.changeStatus('Published', null, modifier, newVersion, modified);
expect(content_2.status).toEqual('Published');
expect(content_2.lastModified).toEqual(modified);
expect(content_2.lastModifiedBy).toEqual(modifier);
expect(content_2.version).toEqual(newVersion);
});
it('should update schedules property and user info when changing status with due time', () => {
const content_1 = new ContentDto('1', 'Draft', creator, creator, creation, creation, null, null, null, { data: 1 }, version);
const content_2 = content_1.changeStatus('Published', dueTime, modifier, newVersion, modified);
expect(content_2.status).toEqual('Draft');
expect(content_2.lastModified).toEqual(modified);
expect(content_2.lastModifiedBy).toEqual(modifier);
expect(content_2.scheduledAt).toEqual(dueTime);
expect(content_2.scheduledBy).toEqual(modifier);
expect(content_2.scheduledTo).toEqual('Published');
expect(content_2.version).toEqual(newVersion);
});
it('should update data property when setting data', () => {
const newData = {};
const content_1 = new ContentDto('1', 'Published', creator, creator, creation, creation, null, null, null, { data: 1 }, version);
const content_2 = content_1.setData(newData);
expect(content_2.data).toBe(newData);
});
});
describe('ContentsService', () => { describe('ContentsService', () => {
const version = new Version('1'); const version = new Version('1');

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

@ -43,60 +43,6 @@ export class ContentDto {
public readonly version: Version public readonly version: Version
) { ) {
} }
public setData(data: any): ContentDto {
return new ContentDto(
this.id,
this.status,
this.createdBy,
this.lastModifiedBy,
this.created,
this.lastModified,
this.scheduledTo,
this.scheduledBy,
this.scheduledAt,
data,
this.version);
}
public changeStatus(status: string, dueTime: DateTime | null, user: string, version: Version, now?: DateTime): ContentDto {
if (dueTime) {
return new ContentDto(
this.id,
this.status,
this.createdBy, user,
this.created, now || DateTime.now(),
status,
user,
dueTime,
this.data,
version);
} else {
return new ContentDto(
this.id,
status,
this.createdBy, user,
this.created, now || DateTime.now(),
null,
null,
null,
this.data,
version);
}
}
public update(data: any, user: string, version: Version, now?: DateTime): ContentDto {
return new ContentDto(
this.id,
this.status,
this.createdBy, user,
this.created, now || DateTime.now(),
this.scheduledTo,
this.scheduledBy,
this.scheduledAt,
data,
version);
}
} }
@Injectable() @Injectable()

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

@ -113,17 +113,6 @@ export class SchemaDto {
public readonly version: Version public readonly version: Version
) { ) {
} }
public publish(user: string, version: Version, now?: DateTime): SchemaDto {
return new SchemaDto(
this.id,
this.name,
this.properties,
true,
this.createdBy, user,
this.created, now || DateTime.now(),
version);
}
} }
export class SchemaDetailsDto extends SchemaDto { export class SchemaDetailsDto extends SchemaDto {
@ -146,7 +135,7 @@ export class SchemaDetailsDto extends SchemaDto {
} }
if (this.listFields.length === 0) { if (this.listFields.length === 0) {
this.listFields = [<any>{}]; this.listFields = [<any>{ properties: {} }];
} }
} }
} }
@ -659,255 +648,255 @@ export class SchemasService {
const url = this.apiUrl.buildUrl(`api/apps/${appName}/schemas`); const url = this.apiUrl.buildUrl(`api/apps/${appName}/schemas`);
return HTTP.getVersioned<any>(this.http, url) return HTTP.getVersioned<any>(this.http, url)
.map(response => { .map(response => {
const body = response.payload.body; const body = response.payload.body;
const items: any[] = body; const items: any[] = body;
return items.map(item => { return items.map(item => {
const properties = new SchemaPropertiesDto(item.properties.label, item.properties.hints); const properties = new SchemaPropertiesDto(item.properties.label, item.properties.hints);
return new SchemaDto( return new SchemaDto(
item.id, item.id,
item.name, properties, item.name, properties,
item.isPublished, item.isPublished,
item.createdBy, item.createdBy,
item.lastModifiedBy, item.lastModifiedBy,
DateTime.parseISO_UTC(item.created), DateTime.parseISO_UTC(item.created),
DateTime.parseISO_UTC(item.lastModified), DateTime.parseISO_UTC(item.lastModified),
new Version(item.version.toString())); new Version(item.version.toString()));
}); });
}) })
.pretifyError('Failed to load schemas. Please reload.'); .pretifyError('Failed to load schemas. Please reload.');
} }
public getSchema(appName: string, id: string): Observable<SchemaDetailsDto> { public getSchema(appName: string, id: string): Observable<SchemaDetailsDto> {
const url = this.apiUrl.buildUrl(`api/apps/${appName}/schemas/${id}`); const url = this.apiUrl.buildUrl(`api/apps/${appName}/schemas/${id}`);
return HTTP.getVersioned<any>(this.http, url) return HTTP.getVersioned<any>(this.http, url)
.map(response => { .map(response => {
const body = response.payload.body; const body = response.payload.body;
const fields = body.fields.map((item: any) => { const fields = body.fields.map((item: any) => {
const propertiesDto = const propertiesDto =
createProperties( createProperties(
item.properties.fieldType, item.properties.fieldType,
item.properties); item.properties);
return new FieldDto( return new FieldDto(
item.fieldId, item.fieldId,
item.name, item.name,
item.isLocked, item.isLocked,
item.isHidden, item.isHidden,
item.isDisabled, item.isDisabled,
item.partitioning, item.partitioning,
propertiesDto); propertiesDto);
}); });
const properties = new SchemaPropertiesDto(body.properties.label, body.properties.hints); const properties = new SchemaPropertiesDto(body.properties.label, body.properties.hints);
return new SchemaDetailsDto( return new SchemaDetailsDto(
body.id, body.id,
body.name, properties, body.name, properties,
body.isPublished, body.isPublished,
body.createdBy, body.createdBy,
body.lastModifiedBy, body.lastModifiedBy,
DateTime.parseISO_UTC(body.created), DateTime.parseISO_UTC(body.created),
DateTime.parseISO_UTC(body.lastModified), DateTime.parseISO_UTC(body.lastModified),
response.version, response.version,
fields, fields,
body.scriptQuery, body.scriptQuery,
body.scriptCreate, body.scriptCreate,
body.scriptUpdate, body.scriptUpdate,
body.scriptDelete, body.scriptDelete,
body.scriptChange); body.scriptChange);
}) })
.pretifyError('Failed to load schema. Please reload.'); .pretifyError('Failed to load schema. Please reload.');
} }
public postSchema(appName: string, dto: CreateSchemaDto, user: string, now: DateTime): Observable<SchemaDetailsDto> { public postSchema(appName: string, dto: CreateSchemaDto, user: string, now: DateTime): Observable<SchemaDetailsDto> {
const url = this.apiUrl.buildUrl(`api/apps/${appName}/schemas`); const url = this.apiUrl.buildUrl(`api/apps/${appName}/schemas`);
return HTTP.postVersioned<any>(this.http, url, dto) return HTTP.postVersioned<any>(this.http, url, dto)
.map(response => { .map(response => {
const body = response.payload.body; const body = response.payload.body;
now = now || DateTime.now(); now = now || DateTime.now();
return new SchemaDetailsDto( return new SchemaDetailsDto(
body.id, body.id,
dto.name, dto.name,
dto.properties || new SchemaPropertiesDto(), dto.properties || new SchemaPropertiesDto(),
false, false,
user, user,
user, user,
now, now,
now, now,
response.version, response.version,
dto.fields || [], dto.fields || [],
body.scriptQuery, body.scriptQuery,
body.scriptCreate, body.scriptCreate,
body.scriptUpdate, body.scriptUpdate,
body.scriptDelete, body.scriptDelete,
body.scriptChange); body.scriptChange);
}) })
.do(schema => { .do(schema => {
this.analytics.trackEvent('Schema', 'Created', appName); this.analytics.trackEvent('Schema', 'Created', appName);
}) })
.pretifyError('Failed to create schema. Please reload.'); .pretifyError('Failed to create schema. Please reload.');
} }
public postField(appName: string, schemaName: string, dto: AddFieldDto, version: Version): Observable<Versioned<FieldDto>> { public postField(appName: string, schemaName: string, dto: AddFieldDto, version: Version): Observable<Versioned<FieldDto>> {
const url = this.apiUrl.buildUrl(`api/apps/${appName}/schemas/${schemaName}/fields`); const url = this.apiUrl.buildUrl(`api/apps/${appName}/schemas/${schemaName}/fields`);
return HTTP.postVersioned<any>(this.http, url, dto, version) return HTTP.postVersioned<any>(this.http, url, dto, version)
.map(response => { .map(response => {
const body = response.payload.body; const body = response.payload.body;
const field = new FieldDto( const field = new FieldDto(
body.id, body.id,
dto.name, dto.name,
false, false,
false, false,
false, false,
dto.partitioning, dto.partitioning,
dto.properties); dto.properties);
return new Versioned(response.version, field); return new Versioned(response.version, field);
}) })
.do(() => { .do(() => {
this.analytics.trackEvent('Schema', 'FieldCreated', appName); this.analytics.trackEvent('Schema', 'FieldCreated', appName);
}) })
.pretifyError('Failed to add field. Please reload.'); .pretifyError('Failed to add field. Please reload.');
} }
public deleteSchema(appName: string, schemaName: string, version: Version): Observable<Versioned<any>> { public deleteSchema(appName: string, schemaName: string, version: Version): Observable<Versioned<any>> {
const url = this.apiUrl.buildUrl(`api/apps/${appName}/schemas/${schemaName}`); const url = this.apiUrl.buildUrl(`api/apps/${appName}/schemas/${schemaName}`);
return HTTP.deleteVersioned(this.http, url, version) return HTTP.deleteVersioned(this.http, url, version)
.do(() => { .do(() => {
this.analytics.trackEvent('Schema', 'Deleted', appName); this.analytics.trackEvent('Schema', 'Deleted', appName);
}) })
.pretifyError('Failed to delete schema. Please reload.'); .pretifyError('Failed to delete schema. Please reload.');
} }
public putSchemaScripts(appName: string, schemaName: string, dto: UpdateSchemaScriptsDto, version: Version): Observable<Versioned<any>> { public putSchemaScripts(appName: string, schemaName: string, dto: UpdateSchemaScriptsDto, version: Version): Observable<Versioned<any>> {
const url = this.apiUrl.buildUrl(`api/apps/${appName}/schemas/${schemaName}/scripts`); const url = this.apiUrl.buildUrl(`api/apps/${appName}/schemas/${schemaName}/scripts`);
return HTTP.putVersioned(this.http, url, dto, version) return HTTP.putVersioned(this.http, url, dto, version)
.do(() => { .do(() => {
this.analytics.trackEvent('Schema', 'ScriptsConfigured', appName); this.analytics.trackEvent('Schema', 'ScriptsConfigured', appName);
}) })
.pretifyError('Failed to update schema scripts. Please reload.'); .pretifyError('Failed to update schema scripts. Please reload.');
} }
public putSchema(appName: string, schemaName: string, dto: UpdateSchemaDto, version: Version): Observable<Versioned<any>> { public putSchema(appName: string, schemaName: string, dto: UpdateSchemaDto, version: Version): Observable<Versioned<any>> {
const url = this.apiUrl.buildUrl(`api/apps/${appName}/schemas/${schemaName}`); const url = this.apiUrl.buildUrl(`api/apps/${appName}/schemas/${schemaName}`);
return HTTP.putVersioned(this.http, url, dto, version) return HTTP.putVersioned(this.http, url, dto, version)
.do(() => { .do(() => {
this.analytics.trackEvent('Schema', 'Updated', appName); this.analytics.trackEvent('Schema', 'Updated', appName);
}) })
.pretifyError('Failed to update schema. Please reload.'); .pretifyError('Failed to update schema. Please reload.');
} }
public putFieldOrdering(appName: string, schemaName: string, dto: number[], version: Version): Observable<Versioned<any>> { public putFieldOrdering(appName: string, schemaName: string, dto: number[], version: Version): Observable<Versioned<any>> {
const url = this.apiUrl.buildUrl(`api/apps/${appName}/schemas/${schemaName}/fields/ordering`); const url = this.apiUrl.buildUrl(`api/apps/${appName}/schemas/${schemaName}/fields/ordering`);
return HTTP.putVersioned(this.http, url, { fieldIds: dto }, version) return HTTP.putVersioned(this.http, url, { fieldIds: dto }, version)
.do(() => { .do(() => {
this.analytics.trackEvent('Schema', 'FieldsReordered', appName); this.analytics.trackEvent('Schema', 'FieldsReordered', appName);
}) })
.pretifyError('Failed to reorder fields. Please reload.'); .pretifyError('Failed to reorder fields. Please reload.');
} }
public publishSchema(appName: string, schemaName: string, version: Version): Observable<Versioned<any>> { public publishSchema(appName: string, schemaName: string, version: Version): Observable<Versioned<any>> {
const url = this.apiUrl.buildUrl(`api/apps/${appName}/schemas/${schemaName}/publish`); const url = this.apiUrl.buildUrl(`api/apps/${appName}/schemas/${schemaName}/publish`);
return HTTP.putVersioned(this.http, url, {}, version) return HTTP.putVersioned(this.http, url, {}, version)
.do(() => { .do(() => {
this.analytics.trackEvent('Schema', 'Published', appName); this.analytics.trackEvent('Schema', 'Published', appName);
}) })
.pretifyError('Failed to publish schema. Please reload.'); .pretifyError('Failed to publish schema. Please reload.');
} }
public unpublishSchema(appName: string, schemaName: string, version: Version): Observable<Versioned<any>> { public unpublishSchema(appName: string, schemaName: string, version: Version): Observable<Versioned<any>> {
const url = this.apiUrl.buildUrl(`api/apps/${appName}/schemas/${schemaName}/unpublish`); const url = this.apiUrl.buildUrl(`api/apps/${appName}/schemas/${schemaName}/unpublish`);
return HTTP.putVersioned(this.http, url, {}, version) return HTTP.putVersioned(this.http, url, {}, version)
.do(() => { .do(() => {
this.analytics.trackEvent('Schema', 'Unpublished', appName); this.analytics.trackEvent('Schema', 'Unpublished', appName);
}) })
.pretifyError('Failed to unpublish schema. Please reload.'); .pretifyError('Failed to unpublish schema. Please reload.');
} }
public putField(appName: string, schemaName: string, fieldId: number, dto: UpdateFieldDto, version: Version): Observable<Versioned<any>> { public putField(appName: string, schemaName: string, fieldId: number, dto: UpdateFieldDto, version: Version): Observable<Versioned<any>> {
const url = this.apiUrl.buildUrl(`api/apps/${appName}/schemas/${schemaName}/fields/${fieldId}`); const url = this.apiUrl.buildUrl(`api/apps/${appName}/schemas/${schemaName}/fields/${fieldId}`);
return HTTP.putVersioned(this.http, url, dto, version) return HTTP.putVersioned(this.http, url, dto, version)
.do(() => { .do(() => {
this.analytics.trackEvent('Schema', 'FieldUpdated', appName); this.analytics.trackEvent('Schema', 'FieldUpdated', appName);
}) })
.pretifyError('Failed to update field. Please reload.'); .pretifyError('Failed to update field. Please reload.');
} }
public enableField(appName: string, schemaName: string, fieldId: number, version: Version): Observable<Versioned<any>> { public enableField(appName: string, schemaName: string, fieldId: number, version: Version): Observable<Versioned<any>> {
const url = this.apiUrl.buildUrl(`api/apps/${appName}/schemas/${schemaName}/fields/${fieldId}/enable`); const url = this.apiUrl.buildUrl(`api/apps/${appName}/schemas/${schemaName}/fields/${fieldId}/enable`);
return HTTP.putVersioned(this.http, url, {}, version) return HTTP.putVersioned(this.http, url, {}, version)
.do(() => { .do(() => {
this.analytics.trackEvent('Schema', 'FieldEnabled', appName); this.analytics.trackEvent('Schema', 'FieldEnabled', appName);
}) })
.pretifyError('Failed to enable field. Please reload.'); .pretifyError('Failed to enable field. Please reload.');
} }
public disableField(appName: string, schemaName: string, fieldId: number, version: Version): Observable<Versioned<any>> { public disableField(appName: string, schemaName: string, fieldId: number, version: Version): Observable<Versioned<any>> {
const url = this.apiUrl.buildUrl(`api/apps/${appName}/schemas/${schemaName}/fields/${fieldId}/disable`); const url = this.apiUrl.buildUrl(`api/apps/${appName}/schemas/${schemaName}/fields/${fieldId}/disable`);
return HTTP.putVersioned(this.http, url, {}, version) return HTTP.putVersioned(this.http, url, {}, version)
.do(() => { .do(() => {
this.analytics.trackEvent('Schema', 'FieldDisabled', appName); this.analytics.trackEvent('Schema', 'FieldDisabled', appName);
}) })
.pretifyError('Failed to disable field. Please reload.'); .pretifyError('Failed to disable field. Please reload.');
} }
public lockField(appName: string, schemaName: string, fieldId: number, version: Version): Observable<Versioned<any>> { public lockField(appName: string, schemaName: string, fieldId: number, version: Version): Observable<Versioned<any>> {
const url = this.apiUrl.buildUrl(`api/apps/${appName}/schemas/${schemaName}/fields/${fieldId}/lock`); const url = this.apiUrl.buildUrl(`api/apps/${appName}/schemas/${schemaName}/fields/${fieldId}/lock`);
return HTTP.putVersioned(this.http, url, {}, version) return HTTP.putVersioned(this.http, url, {}, version)
.do(() => { .do(() => {
this.analytics.trackEvent('Schema', 'FieldLocked', appName); this.analytics.trackEvent('Schema', 'FieldLocked', appName);
}) })
.pretifyError('Failed to lock field. Please reload.'); .pretifyError('Failed to lock field. Please reload.');
} }
public showField(appName: string, schemaName: string, fieldId: number, version: Version): Observable<Versioned<any>> { public showField(appName: string, schemaName: string, fieldId: number, version: Version): Observable<Versioned<any>> {
const url = this.apiUrl.buildUrl(`api/apps/${appName}/schemas/${schemaName}/fields/${fieldId}/show`); const url = this.apiUrl.buildUrl(`api/apps/${appName}/schemas/${schemaName}/fields/${fieldId}/show`);
return HTTP.putVersioned(this.http, url, {}, version) return HTTP.putVersioned(this.http, url, {}, version)
.do(() => { .do(() => {
this.analytics.trackEvent('Schema', 'FieldShown', appName); this.analytics.trackEvent('Schema', 'FieldShown', appName);
}) })
.pretifyError('Failed to show field. Please reload.'); .pretifyError('Failed to show field. Please reload.');
} }
public hideField(appName: string, schemaName: string, fieldId: number, version: Version): Observable<Versioned<any>> { public hideField(appName: string, schemaName: string, fieldId: number, version: Version): Observable<Versioned<any>> {
const url = this.apiUrl.buildUrl(`api/apps/${appName}/schemas/${schemaName}/fields/${fieldId}/hide`); const url = this.apiUrl.buildUrl(`api/apps/${appName}/schemas/${schemaName}/fields/${fieldId}/hide`);
return HTTP.putVersioned(this.http, url, {}, version) return HTTP.putVersioned(this.http, url, {}, version)
.do(() => { .do(() => {
this.analytics.trackEvent('Schema', 'FieldHidden', appName); this.analytics.trackEvent('Schema', 'FieldHidden', appName);
}) })
.pretifyError('Failed to hide field. Please reload.'); .pretifyError('Failed to hide field. Please reload.');
} }
public deleteField(appName: string, schemaName: string, fieldId: number, version: Version): Observable<Versioned<any>> { public deleteField(appName: string, schemaName: string, fieldId: number, version: Version): Observable<Versioned<any>> {
const url = this.apiUrl.buildUrl(`api/apps/${appName}/schemas/${schemaName}/fields/${fieldId}`); const url = this.apiUrl.buildUrl(`api/apps/${appName}/schemas/${schemaName}/fields/${fieldId}`);
return HTTP.deleteVersioned(this.http, url, version) return HTTP.deleteVersioned(this.http, url, version)
.do(() => { .do(() => {
this.analytics.trackEvent('Schema', 'FieldDeleted', appName); this.analytics.trackEvent('Schema', 'FieldDeleted', appName);
}) })
.pretifyError('Failed to delete field. Please reload.'); .pretifyError('Failed to delete field. Please reload.');
} }
} }

382
src/Squidex/app/shared/state/contents.state.ts

@ -0,0 +1,382 @@
/*
* Squidex Headless CMS
*
* @license
* Copyright (c) Squidex UG (haftungsbeschränkt). All rights reserved.
*/
import { Injectable } from '@angular/core';
import { FormControl, FormGroup } from '@angular/forms';
import { Observable } from 'rxjs';
import '@app/framework/utils/rxjs-extensions';
import {
DateTime,
DialogService,
ErrorDto,
Form,
ImmutableArray,
Pager,
State,
Version,
Versioned
} from '@app/framework';
import { AppLanguageDto } from './../services/app-languages.service';
import { AuthService } from './../services/auth.service';
import { fieldInvariant, SchemaDetailsDto, SchemaDto } from './../services/schemas.service';
import { AppsState } from './apps.state';
import { SchemasState } from './schemas.state';
import {
ContentDto,
ContentsService
} from './../services/contents.service';
export class EditContentForm extends Form<FormGroup> {
constructor(
private readonly schema: SchemaDetailsDto,
private readonly languages: ImmutableArray<AppLanguageDto>
) {
super(new FormGroup({}));
for (const field of schema.fields) {
const fieldForm = new FormGroup({});
const defaultValue = field.defaultValue();
if (field.isLocalizable) {
for (let language of this.languages.values) {
fieldForm.setControl(language.iso2Code, new FormControl(defaultValue, field.createValidators(language.isOptional)));
}
} else {
fieldForm.setControl(fieldInvariant, new FormControl(defaultValue, field.createValidators(false)));
}
this.form.setControl(field.name, fieldForm);
}
this.enableContentForm();
}
public submitCompleted(newValue?: any) {
super.submitCompleted(newValue);
this.enableContentForm();
}
public submitFailed(error?: string | ErrorDto) {
super.submitFailed(error);
this.enableContentForm();
}
private enableContentForm() {
if (this.schema.fields.length === 0) {
this.form.enable();
} else {
for (const field of this.schema.fields) {
const fieldForm = this.form.controls[field.name];
if (field.isDisabled) {
fieldForm.disable();
} else {
fieldForm.enable();
}
}
}
}
}
export class PatchContentForm extends Form<FormGroup> {
constructor(
private readonly schema: SchemaDetailsDto,
private readonly language: AppLanguageDto
) {
super(new FormGroup({}));
for (let field of this.schema.listFields) {
if (field.properties && field.properties['inlineEditable']) {
this.form.setControl(field.name, new FormControl(undefined, field.createValidators(this.language.isOptional)));
}
}
}
public submit() {
const result = super.submit();
if (result) {
const request = {};
for (let field of this.schema.listFields) {
if (field.properties['inlineEditable']) {
const value = result[field.name];
if (field.isLocalizable) {
request[field.name] = { [this.language.iso2Code]: value };
} else {
request[field.name] = { iv: value };
}
}
}
return request;
}
return result;
}
}
interface Snapshot {
contents: ImmutableArray<ContentDto>;
contentsPager: Pager;
contentsQuery?: string;
isLoaded?: boolean;
isArchive?: boolean;
selectedContent?: ContentDto | null;
}
export abstract class ContentsStateBase extends State<Snapshot> {
public selectedContent =
this.changes.map(x => x.selectedContent)
.distinctUntilChanged();
public contents =
this.changes.map(x => x.contents)
.distinctUntilChanged();
public contentsPager =
this.changes.map(x => x.contentsPager)
.distinctUntilChanged();
public contentsQuery =
this.changes.map(x => x.contentsQuery)
.distinctUntilChanged();
public isLoaded =
this.changes.map(x => !!x.isLoaded)
.distinctUntilChanged();
public isArchive =
this.changes.map(x => !!x.isArchive)
.distinctUntilChanged();
constructor(
private readonly appsState: AppsState,
private readonly authState: AuthService,
private readonly contentsService: ContentsService,
private readonly dialogs: DialogService
) {
super({ contents: ImmutableArray.of(), contentsPager: new Pager(0) });
}
public select(id: string | null): Observable<ContentDto | null> {
return this.loadContent(id)
.do(content => {
this.next(s => {
const contents = content ? s.contents.replaceBy('id', content) : s.contents;
return { ...s, selectedContent: content, contents };
});
});
}
private loadContent(id: string | null) {
return !id ?
Observable.of(null) :
Observable.of(this.snapshot.contents.find(x => x.id === id))
.switchMap(content => {
if (!content) {
return this.contentsService.getContent(this.appName, this.schemaName, id).catch(() => Observable.of(null));
} else {
return Observable.of(content);
}
});
}
public load(notifyLoad = false): Observable<any> {
return this.contentsService.getContents(this.appName, this.schemaName,
this.snapshot.contentsPager.pageSize,
this.snapshot.contentsPager.skip,
this.snapshot.contentsQuery, undefined,
this.snapshot.isArchive)
.do(dtos => {
if (notifyLoad) {
this.dialogs.notifyInfo('Contents reloaded.');
}
return this.next(s => {
const contents = ImmutableArray.of(dtos.items);
const contentsPager = s.contentsPager.setCount(dtos.total);
let selectedContent = s.selectedContent;
if (selectedContent) {
selectedContent = contents.find(x => x.id === selectedContent!.id) || selectedContent;
}
return { ...s, contents, contentsPager, selectedContent, isLoaded: true };
});
})
.notify(this.dialogs);
}
public create(request: any, publish: boolean, now?: DateTime) {
return this.contentsService.postContent(this.appName, this.schemaName, request, publish)
.do(dto => {
this.dialogs.notifyInfo('Contents created successfully.');
return this.next(s => {
const contents = s.contents.pushFront(dto);
const contentsPager = s.contentsPager.incrementCount();
return { ...s, contents, contentsPager };
});
});
}
public changeStatus(contents: ContentDto[], action: string, dueTime: string | null): Observable<any> {
return Observable.forkJoin(
contents.map(c =>
this.contentsService.changeContentStatus(this.appName, this.schemaName, c.id, action, dueTime, c.version)
.catch(error => Observable.of(error))))
.do(results => {
const error = results.find(x => !!x.error);
if (error) {
this.dialogs.notifyError(error);
}
return Observable.of(error);
})
.switchMap(() => this.load());
}
public delete(contents: ContentDto[]): Observable<any> {
return Observable.forkJoin(
contents.map(c =>
this.contentsService.deleteContent(this.appName, this.schemaName, c.id, c.version)
.catch(error => Observable.of(error))))
.do(results => {
const error = results.find(x => !!x.error);
if (error) {
this.dialogs.notifyError(error);
}
return Observable.of(error);
})
.switchMap(() => this.load());
}
public update(content: ContentDto, request: any, now?: DateTime): Observable<any> {
return this.contentsService.putContent(this.appName, this.schemaName, content.id, request, content.version)
.do(dto => {
this.dialogs.notifyInfo('Contents updated successfully.');
this.replaceContent(updateData(content, request, this.user, dto.version, now));
})
.notify(this.dialogs);
}
public patch(content: ContentDto, request: any, now?: DateTime): Observable<any> {
return this.contentsService.patchContent(this.appName, this.schemaName, content.id, request, content.version)
.do(dto => {
this.dialogs.notifyInfo('Contents updated successfully.');
this.replaceContent(updateData(content, request, this.user, dto.version, now));
})
.notify(this.dialogs);
}
private replaceContent(content: ContentDto) {
return this.next(s => {
const contents = s.contents.replaceBy('id', content);
const selectedContent = s.selectedContent && s.selectedContent.id === content.id ? content : s.selectedContent;
return { ...s, contents, selectedContent };
});
}
public goArchive(isArchive: boolean): Observable<any> {
this.next(s => ({ ...s, contentsPager: new Pager(0), contentsQuery: undefined, isArchive }));
return this.load();
}
public search(query: string): Observable<any> {
this.next(s => ({ ...s, contentsPager: new Pager(0), contentsQuery: query }));
return this.load();
}
public goNext(): Observable<any> {
this.next(s => ({ ...s, contentsPager: s.contentsPager.goNext() }));
return this.load();
}
public goPrev(): Observable<any> {
this.next(s => ({ ...s, contentsPager: s.contentsPager.goPrev() }));
return this.load();
}
public loadVersion(content: ContentDto, version: Version): Observable<Versioned<any>> {
return this.contentsService.getVersionData(this.appName, this.schemaName, content.id, new Version(version.toString()))
.notify(this.dialogs);
}
private get appName() {
return this.appsState.appName;
}
private get user() {
return this.authState.user!.token;
}
protected abstract get schemaName(): string;
}
@Injectable()
export class ContentsState extends ContentsStateBase {
constructor(appsState: AppsState, authState: AuthService, contentsService: ContentsService, dialogs: DialogService,
private readonly schemasState: SchemasState
) {
super(appsState, authState, contentsService, dialogs);
}
protected get schemaName() {
return this.schemasState.schemaName;
}
}
@Injectable()
export class ManualContentsState extends ContentsStateBase {
public schema: SchemaDto;
constructor(
appsState: AppsState, authState: AuthService, contentsService: ContentsService, dialogs: DialogService
) {
super(appsState, authState, contentsService, dialogs);
}
protected get schemaName() {
return this.schema.name;
}
}
const updateData = (content: ContentDto, data: any, user: string, version: Version, now?: DateTime) =>
new ContentDto(
content.id,
content.status,
content.createdBy, user,
content.created, now || DateTime.now(),
content.scheduledTo,
content.scheduledBy,
content.scheduledAt,
data,
version);
Loading…
Cancel
Save