Browse Source

Modal dialog for assets.

pull/271/head
Sebastian Stehle 8 years ago
parent
commit
b211c6c94a
  1. 8
      src/Squidex/app/features/administration/pages/users/user-page.component.ts
  2. 4
      src/Squidex/app/features/administration/state/users.state.ts
  3. 2
      src/Squidex/app/features/assets/module.ts
  4. 46
      src/Squidex/app/features/assets/pages/assets-page.component.html
  5. 50
      src/Squidex/app/features/assets/pages/assets-page.component.scss
  6. 102
      src/Squidex/app/features/assets/pages/assets-page.component.ts
  7. 8
      src/Squidex/app/features/content/module.ts
  8. 4
      src/Squidex/app/features/content/pages/content/content-field.component.html
  9. 11
      src/Squidex/app/features/content/pages/content/content-field.component.ts
  10. 7
      src/Squidex/app/features/content/pages/content/content-page.component.html
  11. 2
      src/Squidex/app/features/content/pages/contents/contents-page.component.html
  12. 15
      src/Squidex/app/features/content/shared/assets-editor.component.html
  13. 2
      src/Squidex/app/features/content/shared/assets-editor.component.scss
  14. 46
      src/Squidex/app/features/content/shared/assets-editor.component.ts
  15. 2
      src/Squidex/app/features/schemas/pages/schema/field.component.ts
  16. 2
      src/Squidex/app/features/settings/pages/patterns/patterns-page.component.html
  17. 2
      src/Squidex/app/framework/angular/modals/modal-dialog.component.html
  18. 3
      src/Squidex/app/framework/angular/modals/modal-dialog.component.ts
  19. 3
      src/Squidex/app/framework/angular/modals/tooltip.component.ts
  20. 2
      src/Squidex/app/framework/angular/pager.component.html
  21. 8
      src/Squidex/app/framework/angular/pager.component.ts
  22. 2
      src/Squidex/app/framework/angular/panel.component.html
  23. 41
      src/Squidex/app/shared/components/asset.component.html
  24. 24
      src/Squidex/app/shared/components/asset.component.scss
  25. 109
      src/Squidex/app/shared/components/asset.component.ts
  26. 34
      src/Squidex/app/shared/components/assets-list.component.html
  27. 40
      src/Squidex/app/shared/components/assets-list.component.scss
  28. 81
      src/Squidex/app/shared/components/assets-list.component.ts
  29. 24
      src/Squidex/app/shared/components/assets-selector.component.html
  30. 2
      src/Squidex/app/shared/components/assets-selector.component.scss
  31. 69
      src/Squidex/app/shared/components/assets-selector.component.ts
  32. 6
      src/Squidex/app/shared/components/markdown-editor.component.html
  33. 60
      src/Squidex/app/shared/components/markdown-editor.component.ts
  34. 6
      src/Squidex/app/shared/components/rich-editor.component.html
  35. 60
      src/Squidex/app/shared/components/rich-editor.component.ts
  36. 2
      src/Squidex/app/shared/declarations.ts
  37. 1
      src/Squidex/app/shared/internal.ts
  38. 12
      src/Squidex/app/shared/module.ts
  39. 132
      src/Squidex/app/shared/state/assets.state.spec.ts
  40. 138
      src/Squidex/app/shared/state/assets.state.ts
  41. 24
      src/Squidex/app/shared/utils/messages.ts
  42. 27
      src/Squidex/app/theme/_bootstrap.scss

8
src/Squidex/app/features/administration/pages/users/user-page.component.ts

@ -46,18 +46,18 @@ export class UserPageComponent implements OnDestroy, OnInit {
}
public save() {
const request = this.userForm.submit();
const value = this.userForm.submit();
if (request) {
if (value) {
if (this.user) {
this.usersState.updateUser(this.user, request)
this.usersState.updateUser(this.user, value)
.subscribe(user => {
this.userForm.submitCompleted();
}, error => {
this.userForm.submitFailed(error);
});
} else {
this.usersState.createUser(request)
this.usersState.createUser(value)
.subscribe(user => {
this.back();
}, error => {

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

@ -103,7 +103,7 @@ export class UsersState extends State<Snapshot> {
private readonly dialogs: DialogService,
private readonly usersService: UsersService
) {
super({ users: ImmutableArray.empty(), usersPager: new Pager(10) });
super({ users: ImmutableArray.empty(), usersPager: new Pager(0) });
}
public selectUser(id: string | null): Observable<UserDto | null> {
@ -150,7 +150,7 @@ export class UsersState extends State<Snapshot> {
}
}
return { ...s, users, usersPager, selectedUser, usersLoading: false };
return { ...s, users, usersPager, selectedUser };
});
})
.notify(this.dialogs);

2
src/Squidex/app/features/assets/module.ts

@ -7,7 +7,6 @@
import { NgModule } from '@angular/core';
import { RouterModule, Routes } from '@angular/router';
import { DndModule } from 'ng2-dnd';
import { SqxFrameworkModule, SqxSharedModule } from '@app/shared';
@ -26,7 +25,6 @@ const routes: Routes = [
imports: [
SqxFrameworkModule,
SqxSharedModule,
DndModule,
RouterModule.forChild(routes)
],
declarations: [

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

@ -1,11 +1,11 @@
<sqx-title message="{app} | Assets" parameter1="app" [value1]="ctx.appName"></sqx-title>
<sqx-title message="{app} | Assets" parameter1="app" [value1]="appsState.appName"></sqx-title>
<sqx-panel desiredWidth="60rem">
<sqx-panel desiredWidth="*">
<ng-container title>
Assets
</ng-container>
<ng-container header>
<ng-container menu>
<button class="btn btn-link btn-secondary" (click)="load(true)" title="Refresh Assets (CTRL + SHIFT + R)">
<i class="icon-reset"></i> Refresh
</button>
@ -19,44 +19,6 @@
</ng-container>
<ng-container content>
<div class="file-drop" (sqxFileDrop)="addFiles($event)">
<h3 class="file-drop-header">Drop files here to upload</h3>
<div class="file-drop-or">or</div>
<div class="file-drop-button">
<span class="btn btn-success" (click)="fileInput.click()">
<span>Select File(s)</span>
<input class="file-drop-button-input" type="file" (change)="addFiles($event.target.files)" #fileInput multiple />
</span>
</div>
<div class="file-drop-info">Drop file on existing item to replace the asset with a newer version.</div>
</div>
<div class="row">
<sqx-asset class="col-3" *ngFor="let file of newFiles" [initFile]="file"
(failed)="onAssetFailed(file)"
(loaded)="onAssetLoaded(file, $event)">
</sqx-asset>
<sqx-asset class="col-3" *ngFor="let asset of assetsItems" [asset]="asset"
(deleting)="onAssetDeleting($event)"
(updated)="onAssetUpdated($event)">
</sqx-asset>
</div>
<div class="clearfix" *ngIf="assetsPager.numberOfItems > 0">
<div class="float-right pagination">
<span class="pagination-text">{{assetsPager.itemFirst}}-{{assetsPager.itemLast}} of {{assetsPager.numberOfItems}}</span>
<button class="btn btn-link btn-secondary pagination-button" [disabled]="!assetsPager.canGoPrev" (click)="goPrev()">
<i class="icon-angle-left"></i>
</button>
<button class="btn btn-link btn-secondary pagination-button" [disabled]="!assetsPager.canGoNext" (click)="goNext()">
<i class="icon-angle-right"></i>
</button>
</div>
</div>
<sqx-assets-list [state]="assetsState"></sqx-assets-list>
</ng-container>
</sqx-panel>

50
src/Squidex/app/features/assets/pages/assets-page.component.scss

@ -1,50 +1,2 @@
@import '_vars';
@import '_mixins';
.file-drop {
& {
@include transition(border-color .4s ease);
border: 2px dashed $color-border;
background: transparent;
padding: 1rem;
text-align: center;
margin-bottom: 1rem;
margin-right: 0;
}
&.drag {
border-color: darken($color-border, 10%);
border-style: dashed;
cursor: copy;
}
&-button-input {
@include hidden;
}
&-button {
margin: .5rem 0;
}
&-or {
font-size: .8rem;
}
&-info {
color: darken($color-border, 30%);
}
}
.btn {
cursor: default;
}
.row {
margin-left: -8px;
margin-right: -8px;
}
.col-3 {
padding-left: 8px;
padding-right: 8px;
}
@import '_mixins';

102
src/Squidex/app/features/assets/pages/assets-page.component.ts

@ -7,120 +7,44 @@
// tslint:disable:prefer-for-of
import { Component, OnDestroy, OnInit } from '@angular/core';
import { Component, OnInit } from '@angular/core';
import { FormControl } from '@angular/forms';
import { Subscription } from 'rxjs';
import {
AppContext,
AssetDto,
AssetsService,
AssetUpdated,
ImmutableArray,
Pager
} from '@app/shared';
import { AppsState, AssetsState } from '@app/shared';
@Component({
selector: 'sqx-assets-page',
styleUrls: ['./assets-page.component.scss'],
templateUrl: './assets-page.component.html',
providers: [
AppContext
]
templateUrl: './assets-page.component.html'
})
export class AssetsPageComponent implements OnDestroy, OnInit {
private assetUpdatedSubscription: Subscription;
public newFiles = ImmutableArray.empty<File>();
public assetsItems = ImmutableArray.empty<AssetDto>();
public assetsPager = new Pager(0, 0, 12);
export class AssetsPageComponent implements OnInit {
public assetsFilter = new FormControl();
public assertQuery = '';
constructor(public readonly ctx: AppContext,
private readonly assetsService: AssetsService
constructor(
public readonly appsState: AppsState,
public readonly assetsState: AssetsState
) {
}
public ngOnDestroy() {
this.assetUpdatedSubscription.unsubscribe();
}
public ngOnInit() {
this.assetUpdatedSubscription =
this.ctx.bus.of(AssetUpdated)
.subscribe(event => {
if (event.sender !== this) {
this.assetsItems = this.assetsItems.replaceBy('id', event.assetDto);
}
});
this.load();
}
public search() {
this.assetsPager = new Pager(0, 0, 12);
this.assertQuery = this.assetsFilter.value;
this.load();
}
public load(showInfo = false) {
this.assetsService.getAssets(this.ctx.appName, this.assetsPager.pageSize, this.assetsPager.skip, this.assertQuery)
.subscribe(dtos => {
this.assetsItems = ImmutableArray.of(dtos.items);
this.assetsPager = this.assetsPager.setCount(dtos.total);
if (showInfo) {
this.ctx.notifyInfo('Assets reloaded.');
}
}, error => {
this.ctx.notifyError(error);
});
}
public onAssetDeleting(asset: AssetDto) {
this.assetsService.deleteAsset(this.ctx.appName, asset.id, asset.version)
.subscribe(dto => {
this.assetsItems = this.assetsItems.filter(x => x.id !== asset.id);
this.assetsPager = this.assetsPager.decrementCount();
}, error => {
this.ctx.notifyError(error);
});
}
public onAssetLoaded(file: File, asset: AssetDto) {
this.newFiles = this.newFiles.remove(file);
public load(notify = false) {
this.assetsState.loadAssets(notify).subscribe();
this.assetsItems = this.assetsItems.pushFront(asset);
this.assetsPager = this.assetsPager.incrementCount();
}
public onAssetUpdated(asset: AssetDto) {
this.ctx.bus.emit(new AssetUpdated(asset, this));
}
public onAssetFailed(file: File) {
this.newFiles = this.newFiles.remove(file);
public search() {
this.assetsState.search(this.assetsFilter.value).subscribe();
}
public goNext() {
this.assetsPager = this.assetsPager.goNext();
this.load();
this.assetsState.goNext().subscribe();
}
public goPrev() {
this.assetsPager = this.assetsPager.goPrev();
this.load();
}
public addFiles(files: FileList) {
for (let i = 0; i < files.length; i++) {
this.newFiles = this.newFiles.pushFront(files[i]);
}
this.assetsState.goPrev().subscribe();
}
}

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

@ -50,10 +50,6 @@ const routes: Routes = [
component: ContentPageComponent,
canDeactivate: [CanDeactivateGuard],
children: [
{
path: 'assets',
loadChildren: './../assets/module#SqxFeatureAssetsModule'
},
{
path: 'references/:schemaName/:language',
component: ContentsPageComponent,
@ -90,10 +86,6 @@ const routes: Routes = [
resolve: {
schema: ResolvePublishedSchemaGuard
}
},
{
path: 'assets',
loadChildren: './../assets/module#SqxFeatureAssetsModule'
}
]
}

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

@ -54,10 +54,10 @@
<textarea class="form-control" [id]="selectedFormName"[formControl]="selectedFormControl" rows="5" [placeholder]="field.displayPlaceholder"></textarea>
</div>
<div *ngSwitchCase="'RichText'">
<sqx-rich-editor [formControl]="selectedFormControl" (assetPluginClicked)="assetPluginClicked()"></sqx-rich-editor>
<sqx-rich-editor [formControl]="selectedFormControl"></sqx-rich-editor>
</div>
<div *ngSwitchCase="'Markdown'">
<sqx-markdown-editor [formControl]="selectedFormControl" (assetPluginClicked)="assetPluginClicked()"></sqx-markdown-editor>
<sqx-markdown-editor [formControl]="selectedFormControl"></sqx-markdown-editor>
</div>
<div *ngSwitchCase="'Dropdown'">
<select class="form-control" [formControl]="selectedFormControl">

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

@ -7,7 +7,6 @@
import { Component, Input, OnInit } from '@angular/core';
import { AbstractControl, FormGroup } from '@angular/forms';
import { ActivatedRoute, Router } from '@angular/router';
import {
AppLanguageDto,
@ -36,12 +35,6 @@ export class ContentFieldComponent implements OnInit {
public selectedFormControl: AbstractControl;
public selectedLanguage: AppLanguageDto;
constructor(
private readonly router: Router,
private readonly route: ActivatedRoute
) {
}
public ngOnInit() {
const masterLanguage = this.languages[0];
@ -60,9 +53,5 @@ export class ContentFieldComponent implements OnInit {
this.selectedFormControl = this.fieldForm.controls[language.iso2Code];
this.selectedLanguage = language;
}
public assetPluginClicked() {
this.router.navigate(['assets'], { relativeTo: this.route });
}
}

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

@ -1,7 +1,7 @@
<sqx-title message="{app} | {schema} | Content" parameter1="app" parameter2="schema" [value1]="ctx.appName" [value2]="schema?.displayName"></sqx-title>
<form [formGroup]="contentForm" (ngSubmit)="saveAndPublish()">
<sqx-panel desiredWidth="53rem">
<sqx-panel desiredWidth="53rem" showSidebar="true">
<ng-container title>
<ng-container *ngIf="isNewMode">
New Content
@ -14,7 +14,7 @@
</ng-container>
</ng-container>
<ng-container header>
<ng-container menu>
<ng-container *ngIf="isNewMode; else notNew">
<button type="button" class="btn btn-secondary" (click)="saveAsDraft()" title="CTRL + S">
Save as Draft
@ -50,9 +50,6 @@
<a class="panel-link" routerLink="history" routerLinkActive="active" #linkHistory *ngIf="!isNewMode">
<i class="icon-time"></i>
</a>
<a class="panel-link" routerLink="assets" routerLinkActive="active">
<i class="icon-media"></i>
</a>
<sqx-onboarding-tooltip id="history" [for]="linkHistory" position="leftTop" after="120000" *ngIf="!isNewMode">
The sidebar navigation contains useful context specific links. Here you can view the history how this schema has changed over time.

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

@ -13,7 +13,7 @@
</ng-container>
</ng-container>
<ng-container header>
<ng-container menu>
<button class="btn btn-link btn-secondary" (click)="load(true)" title="Refresh Contents (CTRL + SHIFT + R)">
<i class="icon-reset"></i> Refresh
</button>

15
src/Squidex/app/features/content/shared/assets-editor.component.html

@ -1,8 +1,8 @@
<div class="assets-container" [class.disabled]="isDisabled">
<div class="row">
<div class="col-4 drop-area-container">
<div class="drop-area" dnd-droppable (onDropSuccess)="onAssetDropped($event.dragData)" [allowDrop]="canDrop()" (sqxFileDrop)="addFiles($event)" routerLink="assets">
Drop files or assets here to add them.
<div class="drop-area" dnd-droppable (onDropSuccess)="onAssetDropped($event.dragData)" [allowDrop]="canDrop()" (sqxFileDrop)="addFiles($event)" (click)="selectorModal.show()">
Drop files here to add them.
</div>
</div>
@ -10,9 +10,12 @@
(failed)="onAssetFailed(file)"
(loaded)="onAssetLoaded(file, $event)">
</sqx-asset>
<sqx-asset class="col-4" *ngFor="let asset of oldAssets" [asset]="asset" [closeMode]="true"
(closing)="onAssetRemoving($event)"
(updated)="onAssetUpdated($event)">
<sqx-asset class="col-4" *ngFor="let asset of oldAssets" [asset]="asset" removeMode="true" isDisabled="true"
(removing)="onAssetRemoving($event)">
</sqx-asset>
</div>
</div>
</div>
<ng-container *sqxModalView="selectorModal;onRoot:true;closeAuto:false">
<sqx-assets-selector (selected)="onAssetsSelected($event)"></sqx-assets-selector>
</ng-container>

2
src/Squidex/app/features/content/shared/assets-editor.component.scss

@ -27,7 +27,7 @@
font-size: 1.2rem;
font-weight: normal;
text-align: center;
padding: 3rem 2rem;
padding: 3.5rem 2rem;
color: darken($color-border, 30%);
}

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

@ -7,15 +7,14 @@
// tslint:disable:prefer-for-of
import { Component, forwardRef, OnDestroy, OnInit } from '@angular/core';
import { Component, forwardRef } from '@angular/core';
import { ControlValueAccessor, NG_VALUE_ACCESSOR } from '@angular/forms';
import { Subscription } from 'rxjs';
import {
AppContext,
AppsState,
AssetDto,
AssetsService,
AssetUpdated,
ModalView,
ImmutableArray,
Types
} from '@app/shared';
@ -29,46 +28,33 @@ export const SQX_ASSETS_EDITOR_CONTROL_VALUE_ACCESSOR: any = {
styleUrls: ['./assets-editor.component.scss'],
templateUrl: './assets-editor.component.html',
providers: [
AppContext,
SQX_ASSETS_EDITOR_CONTROL_VALUE_ACCESSOR
]
})
export class AssetsEditorComponent implements ControlValueAccessor, OnDestroy, OnInit {
private assetUpdatedSubscription: Subscription;
export class AssetsEditorComponent implements ControlValueAccessor {
private callChange = (v: any) => { /* NOOP */ };
private callTouched = () => { /* NOOP */ };
public selectorModal = new ModalView();
public newAssets = ImmutableArray.empty<File>();
public oldAssets = ImmutableArray.empty<AssetDto>();
public isDisabled = false;
constructor(public readonly ctx: AppContext,
constructor(
private readonly appsState: AppsState,
private readonly assetsService: AssetsService
) {
}
public ngOnDestroy() {
this.assetUpdatedSubscription.unsubscribe();
}
public ngOnInit() {
this.assetUpdatedSubscription =
this.ctx.bus.of(AssetUpdated)
.subscribe(event => {
if (event.sender !== this) {
this.oldAssets = this.oldAssets.replaceBy('id', event.assetDto);
}
});
}
public writeValue(value: string[]) {
this.oldAssets = ImmutableArray.empty<AssetDto>();
if (Types.isArrayOfString(value) && value.length > 0) {
const assetIds: string[] = value;
this.assetsService.getAssets(this.ctx.appName, 0, 0, undefined, value)
this.assetsService.getAssets(this.appsState.appName, 0, 0, undefined, value)
.subscribe(dtos => {
this.oldAssets = ImmutableArray.of(assetIds.map(id => dtos.items.find(x => x.id === id)).filter(a => !!a).map(a => a!));
});
@ -101,12 +87,16 @@ export class AssetsEditorComponent implements ControlValueAccessor, OnDestroy, O
};
}
public onAssetDropped(asset: AssetDto) {
if (asset) {
this.oldAssets = this.oldAssets.pushFront(asset);
public onAssetsSelected(assets: AssetDto[]) {
for (let asset of assets) {
this.oldAssets = this.oldAssets.push(asset);
}
if (assets.length > 0) {
this.updateValue();
}
this.selectorModal.hide();
}
public onAssetRemoving(asset: AssetDto) {
@ -124,10 +114,6 @@ export class AssetsEditorComponent implements ControlValueAccessor, OnDestroy, O
this.updateValue();
}
public onAssetUpdated(asset: AssetDto) {
this.ctx.bus.emit(new AssetUpdated(asset, this));
}
public onAssetFailed(file: File) {
this.newAssets = this.newAssets.remove(file);
}

2
src/Squidex/app/features/schemas/pages/schema/field.component.ts

@ -33,7 +33,7 @@ export class FieldComponent implements OnInit {
public field: FieldDto;
@Input()
public patters: AppPatternDto;
public patterns: AppPatternDto;
@Input()
public schema: SchemaDetailsDto;

2
src/Squidex/app/features/settings/pages/patterns/patterns-page.component.html

@ -13,7 +13,7 @@
(updating)="updatePattern(pattern, $event)">
</sqx-pattern>
</div>
<sqx-pattern [isNew]="true"
<sqx-pattern isNew="true"
(updating)="addPattern($event)">
</sqx-pattern>
</ng-container>

2
src/Squidex/app/framework/angular/modals/modal-dialog.component.html

@ -1,7 +1,7 @@
<div class="modal" @fade>
<div class="modal-backdrop"></div>
<div class="modal-dialog {{large ? 'modal-lg' : ''}}">
<div class="modal-dialog" [class.modal-lg]="large" [class.modal-fh]="fullHeight">
<div class="modal-content">
<div class="modal-header" *ngIf="showHeader">
<h4 class="modal-title">

3
src/Squidex/app/framework/angular/modals/modal-dialog.component.ts

@ -28,6 +28,9 @@ export class ModalDialogComponent implements AfterViewInit {
@Input()
public large = false;
@Input()
public fullHeight = false;
@Output()
public close = new EventEmitter();

3
src/Squidex/app/framework/angular/modals/tooltip.component.ts

@ -14,7 +14,7 @@ import { fadeAnimation } from './../animations';
@Component({
selector: 'sqx-tooltip',
styleUrls: ['./tooltip.component.scss'],
template: './tooltip.component.html',
templateUrl: './tooltip.component.html',
animations: [
fadeAnimation
],
@ -49,7 +49,6 @@ export class TooltipComponent implements OnDestroy, OnInit {
this.targetMouseEnterListener =
this.renderer.listen(this.target, 'mouseenter', () => {
this.modal.show();
// this.changeDetector.detectChanges();
});
this.targetMouseLeaveListener =

2
src/Squidex/app/framework/angular/pager.component.html

@ -1,4 +1,4 @@
<div class="grid-footer clearfix" *ngIf="pager && pager.numberOfItems > 0">
<div class="grid-footer clearfix" *ngIf="pager && pager.numberOfItems > 0 && (!hideWhenButtonsDisabled || pager.canGoPrev || pager.canGoNext)">
<div class="float-right pagination">
<span class="pagination-text">{{pager.itemFirst}}-{{pager.itemLast}} of {{pager.numberOfItems}}</span>

8
src/Squidex/app/framework/angular/pager.component.ts

@ -5,19 +5,23 @@
* Copyright (c) Squidex UG (haftungsbeschränkt). All rights reserved.
*/
import { Component, EventEmitter, Input, Output } from '@angular/core';
import { ChangeDetectionStrategy, Component, EventEmitter, Input, Output } from '@angular/core';
import { Pager } from './../internal';
@Component({
selector: 'sqx-pager',
styleUrls: ['./pager.component.scss'],
templateUrl: './pager.component.html'
templateUrl: './pager.component.html',
changeDetection: ChangeDetectionStrategy.OnPush
})
export class PagerComponent {
@Input()
public pager: Pager;
@Input()
public hideWhenButtonsDisabled = false;
@Output()
public next = new EventEmitter();

2
src/Squidex/app/framework/angular/panel.component.html

@ -23,7 +23,7 @@
</div>
<div class="panel-main">
<div class="panel-content {{contentClass}} {{showScrollbar ? 'panel-content-scroll' : ''}} {{isBlank ? 'panel-content-blank' : ''}}">
<div class="panel-content {{contentClass}}" [class.panel-content-scroll]="showScrollbar" [class.panel-content-blank]="isBlank">
<ng-content select=[content]></ng-content>
</div>

41
src/Squidex/app/shared/components/asset.component.html

@ -1,7 +1,4 @@
<div class="card" (sqxFileDrop)="updateFile($event)" dnd-draggable [dragEnabled]="!!asset"
[dragData]="asset"
(onDragStart)="onAssetDragStart($event)"
(onDragEnd)="onAssetDragEnd($event)">
<div class="card" [class.selectable]="isSelectable" [class.border-primary]="isSelected" (click)="selected.emit(asset)" (sqxFileDrop)="updateFile($event)">
<div class="card-body">
<div class="file-preview" *ngIf="asset && progress == 0" @fade>
<span class="file-type" *ngIf="asset.fileType">
@ -18,19 +15,21 @@
<div class="file-overlay">
<div class="file-overlay-background"></div>
<a class="file-edit" (click)="renameDialog.show()">
<i class="icon-pencil"></i>
</a>
<a class="file-download" [attr.href]="asset | sqxAssetUrl" target="_blank">
<i class="icon-download"></i>
</a>
<a class="file-delete" (click)="deleting.emit(asset)" *ngIf="!closeMode">
<i class="icon-delete"></i>
</a>
<a class="file-delete" (click)="closing.emit(asset)" *ngIf="closeMode">
<i class="icon-close"></i>
</a>
<div class="file-menu">
<a class="file-edit" *ngIf="!isDisabled" (click)="renameDialog.show()">
<i class="icon-pencil"></i>
</a>
<a class="file-download" [attr.href]="asset | sqxAssetUrl" target="_blank" (click)="$event.stopPropagation()">
<i class="icon-download"></i>
</a>
<a class="file-delete" (click)="deleting.emit(asset); $event.stopPropagation()" *ngIf="!isDisabled && !removeMode">
<i class="icon-delete"></i>
</a>
<a class="file-delete" (click)="removing.emit(asset); $event.stopPropagation()" *ngIf="removeMode">
<i class="icon-close"></i>
</a>
</div>
<span class="file-overlay-type" *ngIf="asset.fileType">
{{asset.fileType}}
@ -44,7 +43,7 @@
</div>
</div>
</div>
<div class="card-footer" dnd-draggable-handle *ngIf="asset && progress == 0">
<div class="card-footer" *ngIf="asset && progress == 0">
<div class="file-name" [attr.title]="asset.fileName">
{{asset.fileName}}
</div>
@ -65,8 +64,8 @@
</div>
<ng-container *sqxModalView="renameDialog;onRoot:true">
<form [formGroup]="renameForm" (ngSubmit)="renameAsset()">
<sqx-modal-dialog (click)="cancelRenameAsset()">
<form [formGroup]="renameForm.form" (ngSubmit)="renameAsset()">
<sqx-modal-dialog (close)="cancelRenameAsset()">
<ng-container title>
Rename asset
</ng-container>
@ -75,7 +74,7 @@
<div class="form-group">
<label for="assetName">Name</label>
<sqx-control-errors for="name" [submitted]="renameFormSubmitted"></sqx-control-errors>
<sqx-control-errors for="name" [submitted]="renameForm.submitted | async"></sqx-control-errors>
<input type="text" class="form-control" id="assetName" formControlName="name" autocomplete="off" sqxFocusOnInit />
</div>

24
src/Squidex/app/shared/components/asset.component.scss

@ -50,7 +50,10 @@
}
:host {
width: 208px;
padding-bottom: 1rem;
padding-left: 8px;
padding-right: 8px;
}
.drop-overlay {
@ -73,10 +76,8 @@
height: $asset-height;
}
&.drag {
.drop-overlay {
@include opacity(1);
}
&.selectable {
cursor: pointer;
}
&-body {
@ -129,19 +130,12 @@
@include absolute(auto, auto, .5rem, .5rem);
}
&-delete {
@include asset-link;
&-menu {
@include absolute(.5rem, 1rem, auto, auto);
}
&-edit {
@include asset-link;
@include absolute(.5rem, 4rem, auto, auto);
}
&-download {
@include asset-link;
@include absolute(.5rem, 2.5rem, auto, auto);
a {
@include asset-link;
}
}
&-type {

109
src/Squidex/app/shared/components/asset.component.ts

@ -6,33 +6,31 @@
*/
import { Component, EventEmitter, Input, OnInit, Output } from '@angular/core';
import { FormBuilder, Validators } from '@angular/forms';
import { AppContext } from './app-context';
import { FormBuilder } from '@angular/forms';
import {
AppsState,
AssetDto,
AssetsService,
AssetDragged,
AuthService,
DateTime,
DialogService,
fadeAnimation,
ModalView,
RenameAssetForm,
UpdateAssetDto,
Version,
Versioned,
MessageBus
Versioned
} from '@app/shared/internal';
@Component({
selector: 'sqx-asset',
styleUrls: ['./asset.component.scss'],
templateUrl: './asset.component.html',
providers: [
AppContext
animations: [
fadeAnimation
]
})
export class AssetComponent implements OnInit {
private assetVersion: Version;
@Input()
public initFile: File;
@ -40,13 +38,22 @@ export class AssetComponent implements OnInit {
public asset: AssetDto;
@Input()
public closeMode = false;
public removeMode = false;
@Input()
public isDisabled = false;
@Input()
public isSelected = false;
@Input()
public isSelectable = false;
@Output()
public loaded = new EventEmitter<AssetDto>();
@Output()
public closing = new EventEmitter<AssetDto>();
public removing = new EventEmitter<AssetDto>();
@Output()
public updated = new EventEmitter<AssetDto>();
@ -55,28 +62,22 @@ export class AssetComponent implements OnInit {
public deleting = new EventEmitter<AssetDto>();
@Output()
public clicked = new EventEmitter<AssetDto>();
public selected = new EventEmitter<AssetDto>();
@Output()
public failed = new EventEmitter();
public renameDialog = new ModalView();
public renameFormSubmitted = false;
public renameForm =
this.formBuilder.group({
name: ['',
[
Validators.required
]
]
});
public renameForm = new RenameAssetForm(this.formBuilder);
public progress = 0;
constructor(public readonly ctx: AppContext,
private readonly formBuilder: FormBuilder,
constructor(
private readonly appsState: AppsState,
private readonly assetsService: AssetsService,
private readonly messageBus: MessageBus
private readonly authState: AuthService,
private readonly dialogs: DialogService,
private readonly formBuilder: FormBuilder
) {
}
@ -84,7 +85,7 @@ export class AssetComponent implements OnInit {
const initFile = this.initFile;
if (initFile) {
this.assetsService.uploadFile(this.ctx.appName, initFile, this.ctx.userToken, DateTime.now())
this.assetsService.uploadFile(this.appsState.appName, initFile, this.authState.user!.token, DateTime.now())
.subscribe(dto => {
if (dto instanceof AssetDto) {
this.emitLoaded(dto);
@ -92,7 +93,7 @@ export class AssetComponent implements OnInit {
this.progress = dto;
}
}, error => {
this.ctx.notifyError(error);
this.dialogs.notifyError(error);
this.emitFailed(error);
});
@ -103,15 +104,15 @@ export class AssetComponent implements OnInit {
public updateFile(files: FileList) {
if (files.length === 1) {
this.assetsService.replaceFile(this.ctx.appName, this.asset.id, files[0], this.assetVersion)
this.assetsService.replaceFile(this.appsState.appName, this.asset.id, files[0], this.asset.version)
.subscribe(dto => {
if (dto instanceof Versioned) {
this.updateAsset(this.asset.update(dto.payload, this.ctx.userToken, dto.version), true);
this.updateAsset(this.asset.update(dto.payload, this.authState.user!.token, dto.version), true);
} else {
this.setProgress(dto);
}
}, error => {
this.ctx.notifyError(error);
this.dialogs.notifyError(error);
this.setProgress();
});
@ -119,27 +120,28 @@ export class AssetComponent implements OnInit {
}
public renameAsset() {
this.renameFormSubmitted = true;
const value = this.renameForm.submit();
if (this.renameForm.valid) {
this.renameForm.disable();
if (value) {
const requestDto = new UpdateAssetDto(value.name);
const requestDto = new UpdateAssetDto(this.renameForm.controls['name'].value);
this.assetsService.putAsset(this.ctx.appName, this.asset.id, requestDto, this.assetVersion)
this.assetsService.putAsset(this.appsState.appName, this.asset.id, requestDto, this.asset.version)
.subscribe(dto => {
this.updateAsset(this.asset.rename(requestDto.fileName, this.ctx.userToken, dto.version), true);
this.resetRenameForm();
this.updateAsset(this.asset.rename(requestDto.fileName, this.authState.user!.token, dto.version), true);
this.renameForm.submitCompleted();
this.renameDialog.hide();
}, error => {
this.ctx.notifyError(error);
this.dialogs.notifyError(error);
this.enableRenameForm();
this.renameForm.submitFailed();
});
}
}
public cancelRenameAsset() {
this.resetRenameForm();
this.renameForm.submitCompleted();
this.renameDialog.hide();
}
private setProgress(progress = 0) {
@ -158,34 +160,15 @@ export class AssetComponent implements OnInit {
this.updated.emit(asset);
}
private enableRenameForm() {
this.renameForm.enable();
}
private resetRenameForm() {
this.renameForm.enable();
this.renameForm.controls['name'].setValue(this.asset.fileName);
this.renameFormSubmitted = false;
this.renameDialog.hide();
}
private updateAsset(asset: AssetDto, emitEvent: boolean) {
this.renameForm.load({ name: asset.fileName });
this.asset = asset;
this.assetVersion = asset.version;
this.progress = 0;
if (emitEvent) {
this.emitUpdated(asset);
}
this.resetRenameForm();
}
public onAssetDragStart(event: any) {
this.messageBus.emit(new AssetDragged(event.dragData, AssetDragged.DRAG_START, this));
}
public onAssetDragEnd(event: any) {
this.messageBus.emit(new AssetDragged(event.dragData, AssetDragged.DRAG_END, this));
this.cancelRenameAsset();
}
}

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

@ -0,0 +1,34 @@
<div class="file-drop" (sqxFileDrop)="addFiles($event)" *ngIf="!isDisabled">
<h3 class="file-drop-header">Drop files here to upload</h3>
<div class="file-drop-or">or</div>
<div class="file-drop-button">
<span class="btn btn-success" (click)="fileInput.click()">
<span>Select File(s)</span>
<input class="file-drop-button-input" type="file" (change)="addFiles($event.target.files)" #fileInput multiple />
</span>
</div>
<div class="file-drop-info">Drop file on existing item to replace the asset with a newer version.</div>
</div>
<div class="row">
<sqx-asset class="{{assetClass}}" *ngFor="let file of newFiles" [initFile]="file"
(failed)="onAssetFailed(file)"
(loaded)="onAssetLoaded(file, $event)">
</sqx-asset>
<ng-container *ngIf="state.assets | async; let assets">
<sqx-asset class="{{assetClass}}" *ngFor="let asset of assets" [asset]="asset"
[isDisabled]="isDisabled"
[isSelectable]="selectedIds"
[isSelected]="isSelected(asset)"
(selected)="onAssetSelected($event)"
(deleting)="onAssetDeleting($event)">
</sqx-asset>
</ng-container>
</div>
<sqx-pager [hideWhenButtonsDisabled]="true" [pager]="state.assetsPager | async" (prev)="goPrev()" (next)="goNext()"></sqx-pager>

40
src/Squidex/app/shared/components/assets-list.component.scss

@ -0,0 +1,40 @@
@import '_vars';
@import '_mixins';
.file-drop {
& {
@include transition(border-color .4s ease);
border: 2px dashed $color-border;
background: transparent;
padding: 1rem;
text-align: center;
margin-bottom: 1rem;
margin-right: 0;
}
&.drag {
border-color: darken($color-border, 10%);
border-style: dashed;
cursor: copy;
}
&-button-input {
@include hidden;
}
&-button {
margin: .5rem 0;
}
&-or {
font-size: .8rem;
}
&-info {
color: darken($color-border, 30%);
}
}
.btn {
cursor: default;
}

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

@ -0,0 +1,81 @@
/*
* Squidex Headless CMS
*
* @license
* Copyright (c) Squidex UG (haftungsbeschränkt). All rights reserved.
*/
// tslint:disable:prefer-for-of
import { Component, EventEmitter, Input, Output } from '@angular/core';
import {
AssetsState,
AssetDto,
ImmutableArray
} from '@app/shared/internal';
@Component({
selector: 'sqx-assets-list',
styleUrls: ['./assets-list.component.scss'],
templateUrl: './assets-list.component.html'
})
export class AssetsListComponent {
public newFiles = ImmutableArray.empty<File>();
@Input()
public state: AssetsState;
@Input()
public isDisabled: false;
@Input()
public selectedIds: object;
@Input()
public assetClass = '';
@Output()
public selected = new EventEmitter<AssetDto>();
public onAssetLoaded(file: File, asset: AssetDto) {
this.newFiles = this.newFiles.remove(file);
this.state.addAsset(asset);
}
public search() {
this.state.loadAssets().subscribe();
}
public onAssetDeleting(asset: AssetDto) {
this.state.delete(asset).subscribe();
}
public onAssetSelected(asset: AssetDto) {
this.selected.emit(asset);
}
public onAssetFailed(file: File) {
this.newFiles = this.newFiles.remove(file);
}
public goNext() {
this.state.goNext().subscribe();
}
public goPrev() {
this.state.goPrev().subscribe();
}
public isSelected(asset: AssetDto) {
return this.selectedIds && this.selectedIds[asset.id];
}
public addFiles(files: FileList) {
for (let i = 0; i < files.length; i++) {
this.newFiles = this.newFiles.pushFront(files[i]);
}
}
}

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

@ -0,0 +1,24 @@
<sqx-modal-dialog (close)="complete()" large="true" fullHeight="true">
<ng-container title>
Select assets
</ng-container>
<ng-container tabs>
<form class="form-inline" (ngSubmit)="search()">
<input class="form-control" [formControl]="assetsFilter" placeholder="Search for assets" />
</form>
</ng-container>
<ng-container content>
<sqx-assets-list assetClass="asset-default" size="4"
(selected)="onAssetSelected($event)"
[selectedIds]="selectedAssets"
[state]="state" isDisabled="true">
</sqx-assets-list>
</ng-container>
<ng-container footer>
<button type="reset" class="float-left btn btn-secondary" (click)="complete()">Cancel</button>
<button type="submit" class="float-right btn btn-success" (click)="select()" [disabled]="selectionCount === 0">Link selected assets ({{selectionCount}})</button>
</ng-container>
</sqx-modal-dialog>

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

@ -0,0 +1,2 @@
@import '_vars';
@import '_mixins';

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

@ -0,0 +1,69 @@
/*
* Squidex Headless CMS
*
* @license
* Copyright (c) Squidex UG (haftungsbeschränkt). All rights reserved.
*/
// tslint:disable:prefer-for-of
import { Component, EventEmitter, OnInit, Output } from '@angular/core';
import { FormControl } from '@angular/forms';
import {
AssetDto,
AssetsDialogState,
fadeAnimation
} from '@app/shared/internal';
@Component({
selector: 'sqx-assets-selector',
styleUrls: ['./assets-selector.component.scss'],
templateUrl: './assets-selector.component.html',
animations: [
fadeAnimation
]
})
export class AssetsSelectorComponent implements OnInit {
public selectedAssets: { [id: string]: AssetDto } = {};
public selectionCount = 0;
@Output()
public selected = new EventEmitter<AssetDto[]>();
public assetsFilter = new FormControl('');
constructor(
public readonly state: AssetsDialogState
) {
}
public ngOnInit() {
this.state.loadAssets(false, true).subscribe();
this.assetsFilter.setValue(this.state.snapshot.assetsQuery);
}
public search() {
this.state.search(this.assetsFilter.value).subscribe();
}
public complete() {
this.selected.emit([]);
}
public select() {
this.selected.emit(Object.values(this.selectedAssets));
}
public onAssetSelected(asset: AssetDto) {
if (this.selectedAssets[asset.id]) {
delete this.selectedAssets[asset.id];
} else {
this.selectedAssets[asset.id] = asset;
}
this.selectionCount = Object.keys(this.selectedAssets).length;
}
}

6
src/Squidex/app/shared/components/markdown-editor.component.html

@ -6,4 +6,8 @@
<div class="file-drop drag drop-area" [class.dragging]="draggedOver" dnd-droppable (onDropSuccess)="onItemDropped($event)">
<div class="drop-text">Drop assets here to add them.</div>
</div>
</div>
</div>
<ng-container *sqxModalView="selectorModal;onRoot:true;closeAuto:false">
<sqx-assets-selector (selected)="onAssetsSelected($event)"></sqx-assets-selector>
</ng-container>

60
src/Squidex/app/shared/components/markdown-editor.component.ts

@ -5,13 +5,12 @@
* Copyright (c) Squidex UG (haftungsbeschränkt). All rights reserved.
*/
import { AfterViewInit, Component, ElementRef, EventEmitter, forwardRef, OnDestroy, OnInit, Output, ViewChild } from '@angular/core';
import { AfterViewInit, Component, ElementRef, forwardRef, ViewChild } from '@angular/core';
import { ControlValueAccessor, NG_VALUE_ACCESSOR } from '@angular/forms';
import {
AssetDto,
AssetDragged,
MessageBus,
ModalView,
ResourceLoaderService,
Types
} from '@app/shared/internal';
@ -28,13 +27,14 @@ export const SQX_MARKDOWN_EDITOR_CONTROL_VALUE_ACCESSOR: any = {
templateUrl: './markdown-editor.component.html',
providers: [SQX_MARKDOWN_EDITOR_CONTROL_VALUE_ACCESSOR]
})
export class MarkdownEditorComponent implements ControlValueAccessor, AfterViewInit, OnDestroy, OnInit {
export class MarkdownEditorComponent implements ControlValueAccessor, AfterViewInit {
private callChange = (v: any) => { /* NOOP */ };
private callTouched = () => { /* NOOP */ };
private simplemde: any;
private value: string;
private isDisabled = false;
private assetDraggedSubscription: any;
public selectorModal = new ModalView();
@ViewChild('editor')
public editor: ElementRef;
@ -45,37 +45,14 @@ export class MarkdownEditorComponent implements ControlValueAccessor, AfterViewI
@ViewChild('inner')
public inner: ElementRef;
@Output()
public assetPluginClicked = new EventEmitter<any>();
public isFullscreen = false;
public draggedOver = false;
constructor(
private readonly resourceLoader: ResourceLoaderService,
private readonly messageBus: MessageBus
private readonly resourceLoader: ResourceLoaderService
) {
this.resourceLoader.loadStyle('https://cdn.jsdelivr.net/simplemde/latest/simplemde.min.css');
}
public ngOnDestroy() {
this.assetDraggedSubscription.unsubscribe();
}
public ngOnInit() {
this.assetDraggedSubscription =
this.messageBus.of(AssetDragged).subscribe(message => {
if (message.assetDto.isImage) {
if (message.dragEvent === AssetDragged.DRAG_START) {
this.draggedOver = true;
} else {
this.draggedOver = false;
}
}
});
}
public writeValue(value: string) {
this.value = Types.isString(value) ? value : '';
@ -100,9 +77,11 @@ export class MarkdownEditorComponent implements ControlValueAccessor, AfterViewI
this.callTouched = fn;
}
public ngAfterViewInit() {
const self = this;
private showSelector = () => {
this.selectorModal.show();
}
public ngAfterViewInit() {
this.resourceLoader.loadScript('https://cdn.jsdelivr.net/simplemde/latest/simplemde.min.js').then(() => {
this.simplemde = new SimpleMDE({
toolbar: [
@ -176,15 +155,14 @@ export class MarkdownEditorComponent implements ControlValueAccessor, AfterViewI
'|',
{
name: 'assets',
action: () => {
self.assetPluginClicked.emit();
},
action: this.showSelector,
className: 'icon-assets icon-bold',
title: 'Insert Assets'
}
],
element: this.editor.nativeElement
});
this.simplemde.value(this.value || '');
this.simplemde.codemirror.setOption('readOnly', this.isDisabled);
@ -214,15 +192,17 @@ export class MarkdownEditorComponent implements ControlValueAccessor, AfterViewI
});
}
public onItemDropped(event: any) {
const content = event.dragData;
public onAssetsSelected(assets: AssetDto[]) {
let content = '';
if (content instanceof AssetDto) {
const img = `![${content.fileName}](${content.url} '${content.fileName}')`;
for (let asset of assets) {
content += `![${asset.fileName}](${asset.url} '${asset.fileName}')`;
}
this.simplemde.codemirror.replaceSelection(img);
if (content.length > 0) {
this.simplemde.codemirror.replaceSelection(content);
}
this.draggedOver = false;
this.selectorModal.hide();
}
}

6
src/Squidex/app/shared/components/rich-editor.component.html

@ -4,4 +4,8 @@
<div class="file-drop drag drop-area" [class.dragging]="draggedOver" dnd-droppable (onDropSuccess)="onItemDropped($event)">
<div class="drop-text">Drop assets here to add them.</div>
</div>
</div>
</div>
<ng-container *sqxModalView="selectorModal;onRoot:true;closeAuto:false">
<sqx-assets-selector (selected)="onAssetsSelected($event)"></sqx-assets-selector>
</ng-container>

60
src/Squidex/app/shared/components/rich-editor.component.ts

@ -5,13 +5,12 @@
* Copyright (c) Squidex UG (haftungsbeschränkt). All rights reserved.
*/
import { AfterViewInit, Component, forwardRef, ElementRef, EventEmitter, OnDestroy, OnInit, Output, ViewChild } from '@angular/core';
import { AfterViewInit, Component, forwardRef, ElementRef, EventEmitter, OnDestroy, Output, ViewChild } from '@angular/core';
import { ControlValueAccessor, NG_VALUE_ACCESSOR, FormBuilder } from '@angular/forms';
import {
AssetDto,
AssetDragged,
MessageBus,
ModalView,
ResourceLoaderService,
Types
} from '@app/shared/internal';
@ -28,14 +27,15 @@ export const SQX_RICH_EDITOR_CONTROL_VALUE_ACCESSOR: any = {
templateUrl: './rich-editor.component.html',
providers: [SQX_RICH_EDITOR_CONTROL_VALUE_ACCESSOR]
})
export class RichEditorComponent implements ControlValueAccessor, AfterViewInit, OnInit, OnDestroy {
export class RichEditorComponent implements ControlValueAccessor, AfterViewInit, OnDestroy {
private callChange = (v: any) => { /* NOOP */ };
private callTouched = () => { /* NOOP */ };
private tinyEditor: any;
private tinyInitTimer: any;
private value: string;
private isDisabled = false;
private assetDraggedSubscription: any;
public selectorModal = new ModalView();
@ViewChild('editor')
public editor: ElementRef;
@ -43,15 +43,12 @@ export class RichEditorComponent implements ControlValueAccessor, AfterViewInit,
@Output()
public assetPluginClicked = new EventEmitter<any>();
public draggedOver = false;
public assetsForm = this.formBuilder.group({
name: ''
});
constructor(private readonly resourceLoader: ResourceLoaderService,
private readonly formBuilder: FormBuilder,
private readonly messageBus: MessageBus
private readonly formBuilder: FormBuilder
) {
}
@ -59,21 +56,6 @@ export class RichEditorComponent implements ControlValueAccessor, AfterViewInit,
clearTimeout(this.tinyInitTimer);
tinymce.remove(this.editor);
this.assetDraggedSubscription.unsubscribe();
}
public ngOnInit() {
this.assetDraggedSubscription =
this.messageBus.of(AssetDragged).subscribe(message => {
if (message.assetDto.isImage) {
if (message.dragEvent === AssetDragged.DRAG_START) {
this.draggedOver = true;
} else {
this.draggedOver = false;
}
}
});
}
public ngAfterViewInit() {
@ -84,28 +66,30 @@ export class RichEditorComponent implements ControlValueAccessor, AfterViewInit,
});
}
private showSelector = () => {
this.selectorModal.show();
}
private getEditorOptions() {
const self = this;
return {
convert_fonts_to_spans: true,
convert_urls: false,
plugins: 'code image media link',
plugins: 'code image media link lists advlist',
removed_menuitems: 'newdocument',
resize: true,
theme: 'modern',
toolbar: 'undo redo | styleselect | bold italic | alignleft aligncenter alignright alignjustify | bullist numlist outdent indent | link image media | assets',
toolbar: 'undo redo | styleselect | bold italic | alignleft aligncenter | bullist numlist outdent indent | link image media | assets',
setup: (editor: any) => {
self.tinyEditor = editor;
self.tinyEditor.setMode(this.isDisabled ? 'readonly' : 'design');
self.tinyEditor.addButton('assets', {
text: '',
onclick: this.showSelector,
icon: 'assets',
tooltip: 'Insert Assets',
onclick: (event: any) => {
self.assetPluginClicked.emit();
}
text: '',
tooltip: 'Insert Assets'
});
self.tinyEditor.on('change', () => {
@ -156,15 +140,17 @@ export class RichEditorComponent implements ControlValueAccessor, AfterViewInit,
this.callTouched = fn;
}
public onItemDropped(event: any) {
const content = event.dragData;
public onAssetsSelected(assets: AssetDto[]) {
let content = '';
if (content instanceof AssetDto) {
const img = `<img src="${content.url}" alt="${content.fileName}" />`;
for (let asset of assets) {
content += `<img src="${asset.url}" alt="${asset.fileName}" />`;
}
this.tinyEditor.execCommand('mceInsertContent', false, img);
if (content.length > 0) {
this.tinyEditor.execCommand('mceInsertContent', false, content);
}
this.draggedOver = false;
this.selectorModal.hide();
}
}

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

@ -8,6 +8,8 @@
export * from './components/app-context';
export * from './components/app-form.component';
export * from './components/asset.component';
export * from './components/assets-list.component';
export * from './components/assets-selector.component';
export * from './components/help.component';
export * from './components/geolocation-editor.component';
export * from './components/history.component';

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

@ -39,6 +39,7 @@ export * from './services/users-provider.service';
export * from './services/users.service';
export * from './state/apps.state';
export * from './state/assets.state';
export * from './state/schemas.state';
export * from './utils/messages';

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

@ -23,6 +23,10 @@ import {
AppsService,
AssetComponent,
AssetPreviewUrlPipe,
AssetsDialogState,
AssetsListComponent,
AssetsSelectorComponent,
AssetsState,
AssetsService,
AssetUrlPipe,
AuthInterceptor,
@ -75,6 +79,8 @@ import {
AssetComponent,
AssetPreviewUrlPipe,
AssetUrlPipe,
AssetsListComponent,
AssetsSelectorComponent,
FileIconPipe,
GeolocationEditorComponent,
HelpComponent,
@ -94,6 +100,8 @@ import {
AssetComponent,
AssetPreviewUrlPipe,
AssetUrlPipe,
AssetsListComponent,
AssetsSelectorComponent,
FileIconPipe,
GeolocationEditorComponent,
HelpComponent,
@ -108,6 +116,9 @@ import {
UserPicturePipe,
UserPictureRefPipe,
RichEditorComponent
],
providers: [
AssetsDialogState
]
})
export class SqxSharedModule {
@ -122,6 +133,7 @@ export class SqxSharedModule {
AppPatternsService,
AppsService,
AppsState,
AssetsState,
AssetsService,
AuthService,
BackupsService,

132
src/Squidex/app/shared/state/assets.state.spec.ts

@ -0,0 +1,132 @@
/*
* Squidex Headless CMS
*
* @license
* Copyright (c) Squidex UG (haftungsbeschränkt). All rights reserved.
*/
import { Observable } from 'rxjs';
import { IMock, It, Mock, Times } from 'typemoq';
import {
AppsState,
AssetDto,
AssetsDto,
AssetsService,
AssetsState,
DialogService,
DateTime,
Version,
Versioned
} from '@app/shared';
describe('AssetsState', () => {
const app = 'my-app';
const creation = DateTime.today();
const creator = 'not-me';
const modified = DateTime.now();
const modifier = 'me';
const version = new Version('1');
const newVersion = new Version('2');
const oldAssets = [
new AssetDto('id1', creator, creator, creation, creation, 'name1', 'type1', 1, 1, 'mime1', false, null, null, 'url1', version),
new AssetDto('id2', creator, creator, creation, creation, 'name2', 'type2', 2, 2, 'mime2', false, null, null, 'url2', version)
];
let dialogs: IMock<DialogService>;
let appsState: IMock<AppsState>;
let assetsService: IMock<AssetsService>;
let assetsState: AssetsState;
beforeEach(() => {
dialogs = Mock.ofType<DialogService>();
appsState = Mock.ofType<AppsState>();
appsState.setup(x => x.appName)
.returns(() => app);
assetsService = Mock.ofType<AssetsService>();
assetsService.setup(x => x.getAssets(app, 30, 0, undefined))
.returns(() => Observable.of(new AssetsDto(200, oldAssets)));
assetsState = new AssetsState(appsState.object, assetsService.object, dialogs.object);
assetsState.loadAssets().subscribe();
});
it('should load assets', () => {
assetsState.loadAssets().subscribe();
expect(assetsState.snapshot.assets.values).toEqual(oldAssets);
expect(assetsState.snapshot.assetsPager.numberOfItems).toEqual(200);
assetsService.verify(x => x.getAssets(app, 30, 0, undefined), Times.exactly(2));
});
it('should not reload when assets assets already loaded', () => {
assetsState.loadAssets(false, true).subscribe();
expect(assetsState.snapshot.assets.values).toEqual(oldAssets);
expect(assetsState.snapshot.assetsPager.numberOfItems).toEqual(200);
assetsService.verify(x => x.getAssets(app, 30, 0, undefined), Times.once());
});
it('should raise notification on load when notify is true', () => {
assetsState.loadAssets(true).subscribe();
dialogs.verify(x => x.notifyInfo(It.isAnyString()), Times.once());
});
it('should add asset to snapshot', () => {
const newAsset = new AssetDto('id3', creator, creator, creation, creation, 'name3', 'type3', 3, 3, 'mime3', true, 0, 0, 'url3', version);
assetsState.addAsset(newAsset);
expect(assetsState.snapshot.assets.values).toEqual([newAsset, ...oldAssets]);
expect(assetsState.snapshot.assetsPager.numberOfItems).toBe(201);
});
it('should update asset in snapshot', () => {
const newAsset = new AssetDto('id1', modifier, modifier, modified, modified, 'name3', 'type3', 3, 3, 'mime3', true, 0, 0, 'url3', version);
assetsState.updateAsset(newAsset);
const asset_1 = assetsState.snapshot.assets.at(0);
expect(asset_1).toBe(newAsset);
});
it('should load next page and prev page when paging', () => {
assetsService.setup(x => x.getAssets(app, 30, 30, undefined))
.returns(() => Observable.of(new AssetsDto(200, [])));
assetsState.goNext().subscribe();
assetsState.goPrev().subscribe();
assetsService.verify(x => x.getAssets(app, 30, 30, undefined), Times.once());
assetsService.verify(x => x.getAssets(app, 30, 0, undefined), Times.exactly(2));
});
it('should load with query when searching', () => {
assetsService.setup(x => x.getAssets(app, 30, 0, 'my-query'))
.returns(() => Observable.of(new AssetsDto(0, [])));
assetsState.search('my-query').subscribe();
expect(assetsState.snapshot.assetsQuery).toEqual('my-query');
assetsService.verify(x => x.getAssets(app, 30, 0, 'my-query'), Times.once());
});
it('should remove asset when deleted', () => {
assetsService.setup(x => x.deleteAsset(app, oldAssets[0].id, version))
.returns(() => Observable.of(new Versioned<any>(newVersion, {})));
assetsState.delete(oldAssets[0]).subscribe();
expect(assetsState.snapshot.assets.values.length).toBe(1);
expect(assetsState.snapshot.assetsPager.numberOfItems).toBe(199);
});
});

138
src/Squidex/app/shared/state/assets.state.ts

@ -0,0 +1,138 @@
/*
* Squidex Headless CMS
*
* @license
* Copyright (c) Squidex UG (haftungsbeschränkt). All rights reserved.
*/
import { Injectable } from '@angular/core';
import { FormBuilder, Validators, FormGroup } from '@angular/forms';
import { Observable } from 'rxjs';
import '@app/framework/utils/rxjs-extensions';
import {
DialogService,
ImmutableArray,
Pager,
Form,
State
} from '@app/framework';
import { AppsState } from './apps.state';
import { AssetDto, AssetsService} from './../services/assets.service';
export class RenameAssetForm extends Form<FormGroup> {
constructor(formBuilder: FormBuilder) {
super(formBuilder.group({
name: ['',
[
Validators.required
]
]
}));
}
}
interface Snapshot {
assets: ImmutableArray<AssetDto>;
assetsPager: Pager;
assetsQuery?: string;
loaded: false;
}
@Injectable()
export class AssetsState extends State<Snapshot> {
public assets =
this.changes.map(x => x.assets)
.distinctUntilChanged();
public assetsPager =
this.changes.map(x => x.assetsPager)
.distinctUntilChanged();
constructor(
private readonly appsState: AppsState,
private readonly assetsService: AssetsService,
private readonly dialogs: DialogService
) {
super({ assets: ImmutableArray.empty(), assetsPager: new Pager(0, 0, 30), loaded: false });
}
public loadAssets(notify = false, noReload = false): Observable<any> {
if (this.snapshot.loaded && noReload) {
return Observable.of({});
}
return this.assetsService.getAssets(this.appName, this.snapshot.assetsPager.pageSize, this.snapshot.assetsPager.skip, this.snapshot.assetsQuery)
.do(dtos => {
if (notify) {
this.dialogs.notifyInfo('Assets reloaded.');
}
this.next(s => {
const assets = ImmutableArray.of(dtos.items);
const assetsPager = s.assetsPager.setCount(dtos.total);
return { ...s, assets, assetsPager, loaded: true };
});
})
.notify(this.dialogs);
}
public addAsset(asset: AssetDto) {
this.next(s => {
const assets = s.assets.pushFront(asset);
const assetsPager = s.assetsPager.incrementCount();
return { ...s, assets, assetsPager };
});
}
public updateAsset(asset: AssetDto) {
this.next(s => {
const assets = s.assets.replaceBy('id', asset);
return { ...s, assets };
});
}
public delete(asset: AssetDto): Observable<any> {
return this.assetsService.deleteAsset(this.appName, asset.id, asset.version)
.do(dto => {
return this.next(s => {
const assets = s.assets.filter(x => x.id !== asset.id);
const assetsPager = s.assetsPager.decrementCount();
return { ...s, assets, assetsPager };
});
})
.notify(this.dialogs);
}
public search(query: string): Observable<any> {
this.next(s => ({ ...s, assetsPager: new Pager(0), assetsQuery: query }));
return this.loadAssets();
}
public goNext(): Observable<any> {
this.next(s => ({ ...s, assetsPager: s.assetsPager.goNext() }));
return this.loadAssets();
}
public goPrev(): Observable<any> {
this.next(s => ({ ...s, assetsPager: s.assetsPager.goPrev() }));
return this.loadAssets();
}
private get appName() {
return this.appsState.appName;
}
}
@Injectable()
export class AssetsDialogState extends AssetsState { }

24
src/Squidex/app/shared/utils/messages.ts

@ -5,26 +5,4 @@
* Copyright (c) Squidex UG (haftungsbeschränkt). All rights reserved.
*/
import { AssetDto } from './../services/assets.service';
export class HistoryChannelUpdated { }
export class AssetUpdated {
constructor(
public readonly assetDto: AssetDto,
public readonly sender: any
) {
}
}
export class AssetDragged {
public static readonly DRAG_START = 'Start';
public static readonly DRAG_END = 'End';
constructor(
public readonly assetDto: AssetDto,
public readonly dragEvent: string,
public readonly sender: any
) {
}
}
export class HistoryChannelUpdated { }

27
src/Squidex/app/theme/_bootstrap.scss

@ -421,6 +421,15 @@ a {
@include opacity(.5);
}
&-header,
&-footer {
@include flex-shrink(0);
}
&-body {
overflow-y: auto;
}
&-header {
@include border-radius-top(.25rem);
background: $color-modal-header-background;
@ -452,16 +461,26 @@ a {
&-content {
@include box-shadow(0, 6px, 16px, .4);
@include border-radiusn(.4rem, .35rem, .35rem, .4rem);
max-height: 100%;
}
&-fh {
.modal-content {
min-height: 100%;
}
}
&-lg {
@media (min-width: 992px) {
max-width: 854px;
}
}
&-dialog {
& {
@include absolute(0, 0, 0, 0);
z-index: 1100;
}
@media (min-width: 576px) {
margin-top: 4.5rem;
}
}
&-tabs {

Loading…
Cancel
Save