Browse Source

Fixes to editor sdk and asset history.

pull/501/head
Sebastian 6 years ago
parent
commit
971706873c
  1. 40
      backend/src/Squidex.Domain.Apps.Entities/Assets/AssetHistoryEventsCreator.cs
  2. 6
      backend/src/Squidex/Areas/Api/Controllers/Assets/Models/AssetDto.cs
  3. 4
      backend/src/Squidex/Config/Domain/AssetServices.cs
  4. 14
      backend/src/Squidex/wwwroot/scripts/editor-sdk.js
  5. 49
      backend/src/Squidex/wwwroot/scripts/simple-log.html
  6. 76
      frontend/app/framework/angular/http/http-extensions.spec.ts
  7. 64
      frontend/app/framework/angular/http/http-extensions.ts
  8. 12
      frontend/app/framework/utils/string-helper.spec.ts
  9. 12
      frontend/app/framework/utils/string-helper.ts
  10. 3
      frontend/app/shared/components/assets/asset-dialog.component.html
  11. 7
      frontend/app/shared/components/assets/asset-dialog.component.ts
  12. 19
      frontend/app/shared/components/assets/asset-history.component.html
  13. 33
      frontend/app/shared/components/assets/asset-history.component.scss
  14. 63
      frontend/app/shared/components/assets/asset-history.component.ts
  15. 16
      frontend/app/shared/components/assets/pipes.ts
  16. 1
      frontend/app/shared/declarations.ts
  17. 2
      frontend/app/shared/module.ts
  18. 5
      frontend/app/shared/services/assets.service.ts

40
backend/src/Squidex.Domain.Apps.Entities/Assets/AssetHistoryEventsCreator.cs

@ -0,0 +1,40 @@
// ==========================================================================
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex UG (haftungsbeschränkt)
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
using System.Threading.Tasks;
using Squidex.Domain.Apps.Entities.History;
using Squidex.Domain.Apps.Events.Assets;
using Squidex.Infrastructure.EventSourcing;
using Squidex.Infrastructure.Reflection;
namespace Squidex.Domain.Apps.Entities.Assets
{
public sealed class AssetHistoryEventsCreator : HistoryEventsCreatorBase
{
public AssetHistoryEventsCreator(TypeNameRegistry typeNameRegistry)
: base(typeNameRegistry)
{
AddEventMessage<AssetCreated>(
"uploaded asset.");
AddEventMessage<AssetUpdated>(
"replaced asset.");
AddEventMessage<AssetAnnotated>(
"updated asset.");
}
protected override Task<HistoryEvent?> CreateEventCoreAsync(Envelope<IEvent> @event)
{
var channel = $"assets.{@event.Headers.AggregateId()}";
var result = ForEvent(@event.Payload, channel);
return Task.FromResult<HistoryEvent?>(result);
}
}
}

6
backend/src/Squidex/Areas/Api/Controllers/Assets/Models/AssetDto.cs

@ -204,12 +204,12 @@ namespace Squidex.Areas.Api.Controllers.Assets.Models
if (!string.IsNullOrWhiteSpace(response.Slug))
{
response.AddGetLink("content", controller.Url<AssetContentController>(x => nameof(x.GetAssetContentBySlug), new { app, idOrSlug = response.Id, version, more = response.Slug }));
response.AddGetLink("content/slug", controller.Url<AssetContentController>(x => nameof(x.GetAssetContentBySlug), new { app, idOrSlug = response.Slug, version }));
response.AddGetLink("content", controller.Url<AssetContentController>(x => nameof(x.GetAssetContentBySlug), new { app, idOrSlug = response.Id, more = response.Slug }));
response.AddGetLink("content/slug", controller.Url<AssetContentController>(x => nameof(x.GetAssetContentBySlug), new { app, idOrSlug = response.Slug }));
}
else
{
response.AddGetLink("content", controller.Url<AssetContentController>(x => nameof(x.GetAssetContentBySlug), new { app, idOrSlug = response.Id, version }));
response.AddGetLink("content", controller.Url<AssetContentController>(x => nameof(x.GetAssetContentBySlug), new { app, idOrSlug = response.Id }));
}
return response;

4
backend/src/Squidex/Config/Domain/AssetServices.cs

@ -13,6 +13,7 @@ using MongoDB.Driver;
using MongoDB.Driver.GridFS;
using Squidex.Domain.Apps.Entities.Assets;
using Squidex.Domain.Apps.Entities.Assets.Queries;
using Squidex.Domain.Apps.Entities.History;
using Squidex.Domain.Apps.Entities.Search;
using Squidex.Infrastructure;
using Squidex.Infrastructure.Assets;
@ -44,6 +45,9 @@ namespace Squidex.Config.Domain
services.AddSingletonAs<AssetQueryParser>()
.AsSelf();
services.AddTransientAs<AssetHistoryEventsCreator>()
.As<IHistoryEventsCreator>();
services.AddSingletonAs<AssetsSearchSource>()
.As<ISearchSource>();

14
backend/src/Squidex/wwwroot/scripts/editor-sdk.js

@ -3,7 +3,7 @@ function SquidexFormField() {
var initHandler;
var initCalled = false;
var disabledHandler;
var disabled;
var disabled = false;
var valueHandler;
var value;
var formValueHandler;
@ -42,9 +42,11 @@ function SquidexFormField() {
var type = event.data.type;
if (type === 'disabled') {
disabled = event.data.isDisabled;
if (disabled !== event.data.isDisabled) {
disabled = event.data.isDisabled;
raiseDisabled();
raiseDisabled();
}
} else if (type === 'valueChanged') {
value = event.data.value;
@ -113,9 +115,11 @@ function SquidexFormField() {
/**
* Notifies the control container that the value has been changed.
*/
valueChanged: function (value) {
valueChanged: function (newValue) {
value = newValue;
if (window.parent) {
window.parent.postMessage({ type: 'valueChanged', value: value }, '*');
window.parent.postMessage({ type: 'valueChanged', value: newValue }, '*');
}
},

49
backend/src/Squidex/wwwroot/scripts/simple-log.html

@ -0,0 +1,49 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<!-- Load the editor sdk from the local folder or https://cloud.squidex.io/scripts/editor-sdk.js -->
<script src="editor-sdk.js"></script>
</head>
<body>
<button id="button">New Value</button>
<script>
var numberGenerator = 1;
var button = document.getElementById('button');
var field = new SquidexFormField();
function logState(message) {
console.log(`${message}. Value: <${JSON.stringify(field.getValue(), 2)}>, Form Value: <${JSON.stringify(field.getFormValue())}>`);
}
logState('Init');
if (button) {
button.addEventListener('click', function () {
numberGenerator++;
field.valueChanged(numberGenerator);
logState('Click');
});
}
// Handle the value change event and set the text to the editor.
field.onValueChanged(function (value) {
logState(`Value changed: <${JSON.stringify(value, 2)}>`);
});
// Disable the editor when it should be disabled.
field.onDisabled(function (disabled) {
logState(`Disabled: <${JSON.stringify(disabled, 2)}>`);
});
</script>
</body>
</html>

76
frontend/app/framework/angular/http/http-extensions.spec.ts

@ -0,0 +1,76 @@
/*
* Squidex Headless CMS
*
* @license
* Copyright (c) Squidex UG (haftungsbeschränkt). All rights reserved.
*/
import { parseError } from './http-extensions';
import { ErrorDto } from './../../utils/error';
describe('ErrorParsing', () => {
it('should return default when error is javascript exception', () => {
const response: any = new Error();
const result = parseError(response, 'Fallback');
expect(result).toEqual(new ErrorDto(500, 'Fallback', [], response));
});
it('should just forward error dto', () => {
const response: any = new ErrorDto(500, 'error', []);
const result = parseError(response, 'Fallback');
expect(result).toBe(response);
});
it('should return default 412 error', () => {
const response: any = { status: 412 };
const result = parseError(response, 'Fallback');
expect(result).toEqual(new ErrorDto(412, 'Failed to make the update. Another user has made a change. Please reload.', [], response));
});
it('should return default 429 error', () => {
const response: any = { status: 429 };
const result = parseError(response, 'Fallback');
expect(result).toEqual(new ErrorDto(429, 'You have exceeded the maximum limit of API calls.', [], response));
});
it('should return error from error object', () => {
const error = { message: 'My-Message', details: ['My-Detail'] };
const response: any = { status: 400, error };
const result = parseError(response, 'Fallback');
expect(result).toEqual(new ErrorDto(400, 'My-Message', ['My-Detail'], response));
});
it('should return error from error json', () => {
const error = { message: 'My-Message', details: ['My-Detail'] };
const response: any = { status: 400, error: JSON.stringify(error) };
const result = parseError(response, 'Fallback');
expect(result).toEqual(new ErrorDto(400, 'My-Message', ['My-Detail'], response));
});
it('should return default when object is invalid', () => {
const error = { text: 'invalid' };
const response: any = { status: 400, error };
const result = parseError(response, 'Fallback');
expect(result).toEqual(new ErrorDto(500, 'Fallback', [], response));
});
it('should return default when json is invalid', () => {
const error = '{{';
const response: any = { status: 400, error };
const result = parseError(response, 'Fallback');
expect(result).toEqual(new ErrorDto(500, 'Fallback', [], response));
});
});

64
frontend/app/framework/angular/http/http-extensions.ts

@ -86,37 +86,41 @@ export module HTTP {
export const pretifyError = (message: string) => <T>(source: Observable<T>) =>
source.pipe(catchError((response: HttpErrorResponse) => {
if (Types.is(response, ErrorDto)) {
return throwError(response);
}
const error = parseError(response, message);
return throwError(error);
}));
export function parseError(response: HttpErrorResponse, fallback: string) {
if (Types.is(response, ErrorDto)) {
return response;
}
const { error, status } = response;
if (status === 412) {
return new ErrorDto(412, 'Failed to make the update. Another user has made a change. Please reload.', [], response);
}
if (status === 429) {
return new ErrorDto(429, 'You have exceeded the maximum limit of API calls.', [], response);
}
let result: ErrorDto | null = null;
if (!Types.is(response.error, Error)) {
try {
let errorDto = Types.isObject(response.error) ? response.error : JSON.parse(response.error);
if (!errorDto) {
errorDto = { message: 'Failed to make the request.', details: [] };
}
switch (response.status) {
case 412:
result = new ErrorDto(response.status, 'Failed to make the update. Another user has made a change. Please reload.', [], response);
break;
case 429:
result = new ErrorDto(response.status, 'You have exceeded the maximum limit of API calls.', [], response);
break;
case 500:
result = new ErrorDto(response.status, errorDto.message, errorDto.details, response);
break;
}
} catch (e) {
result = new ErrorDto(500, 'Failed to make the request.', [], response);
}
let parsed: any;
if (Types.isObject(error)) {
parsed = error;
} else if (Types.isString(error)) {
try {
parsed = JSON.parse(error);
} catch (e) {
parsed = undefined;
}
}
result = result || new ErrorDto(500, message);
if (parsed && Types.isString(parsed.message)) {
return new ErrorDto(status, parsed.message, parsed.details, response);
}
return throwError(result);
}));
return new ErrorDto(500, fallback, [], response);
}

12
frontend/app/framework/utils/string-helper.spec.ts

@ -38,4 +38,16 @@ describe('StringHelper', () => {
it('should return empty string if also fallback not found', () => {
expect(StringHelper.firstNonEmpty(null!, undefined!, '')).toBe('');
});
it('should append query string to url when url already contains query', () => {
const url = StringHelper.appendToUrl('http://squidex.io?query=value', 'other', 1);
expect(url).toEqual('http://squidex.io?query=value&other=1');
});
it('should append query string to url when url already contains no query', () => {
const url = StringHelper.appendToUrl('http://squidex.io', 'other', 1);
expect(url).toEqual('http://squidex.io?other=1');
});
});

12
frontend/app/framework/utils/string-helper.ts

@ -19,4 +19,16 @@ export module StringHelper {
return '';
}
export function appendToUrl(url: string, key: string, value: any) {
if (url.indexOf('?') > 0) {
url += '&';
} else {
url += '?';
}
url += `${key}=${value}`;
return url;
}
}

3
frontend/app/shared/components/assets/asset-dialog.component.html

@ -123,6 +123,9 @@
</div>
</div>
</ng-container>
<ng-container *ngSwitchCase="'History'">
<sqx-asset-history [asset]="asset"></sqx-asset-history>
</ng-container>
</ng-container>
</ng-container>
</sqx-modal-dialog>

7
frontend/app/shared/components/assets/asset-dialog.component.ts

@ -23,6 +23,9 @@ import {
import { ImageCropperComponent } from './image-cropper.component';
import { ImageFocusPointComponent } from './image-focus-point.component';
const TABS_IMAGE: ReadonlyArray<string> = ['Metadata', 'Image', 'Focus Point', 'History'];
const TABS_DEFAULT: ReadonlyArray<string> = ['Metadata', 'History'];
@Component({
selector: 'sqx-asset-dialog',
styleUrls: ['./asset-dialog.component.scss'],
@ -77,9 +80,9 @@ export class AssetDialogComponent implements OnChanges {
this.annotateForm.setEnabled(this.isEditable);
if (this.asset.type === 'Image') {
this.selectableTabs = ['Metadata', 'Image', 'Focus Point'];
this.selectableTabs = TABS_IMAGE;
} else {
this.selectableTabs = ['Metadata'];
this.selectableTabs = TABS_DEFAULT;
}
if (this.selectableTabs.indexOf(this.selectedTab) < 0) {

19
frontend/app/shared/components/assets/asset-history.component.html

@ -0,0 +1,19 @@
<div class="events">
<div class="event row no-gutters" *ngFor="let assetEvent of assetEvents | async; trackBy: trackByEvent">
<div class="col-auto">
<img class="user-picture" title="{{assetEvent.event.actor | sqxUserNameRef}}" [src]="assetEvent.event.actor | sqxUserPictureRef" />
</div>
<div class="col pl-2">
<div class="event-message">
<span class="event-actor user-ref mr-1">{{assetEvent.event.actor | sqxUserNameRef:null}}</span>
<span [innerHTML]="assetEvent.event | sqxHistoryMessage"></span>
</div>
<div class="event-created">{{assetEvent.event.created | sqxFromNow}}</div>
<ng-container *ngIf="assetEvent.canDownload">
<a class="event-load force" [href]="asset | sqxAssetUrl:assetEvent.fileVersion" sqxExternalLink="noicon">Download this Version</a>
</ng-container>
</div>
</div>
</div>

33
frontend/app/shared/components/assets/asset-history.component.scss

@ -0,0 +1,33 @@
:host ::ng-deep {
.user-ref {
font-weight: 500;
}
.marker-ref {
font-weight: 500;
}
}
.events {
margin-left: auto;
margin-right: auto;
max-width: 400px;
}
.event {
& {
font-size: .9rem;
font-weight: normal;
margin-bottom: 1.5rem;
}
&-created {
font-size: .75rem;
font-weight: normal;
margin: .25rem 0;
}
}
.user-picture {
margin-top: .25rem;
}

63
frontend/app/shared/components/assets/asset-history.component.ts

@ -0,0 +1,63 @@
/*
* Squidex Headless CMS
*
* @license
* Copyright (c) Squidex UG (haftungsbeschränkt). All rights reserved.
*/
import { Component, Input, OnChanges } from '@angular/core';
import { Observable } from 'rxjs';
import { map } from 'rxjs/operators';
import {
AppsState,
AssetDto,
HistoryEventDto,
HistoryService
} from '@app/shared/internal';
interface AssetEvent { event: HistoryEventDto; fileVersion: number; canDownload: boolean; }
@Component({
selector: 'sqx-asset-history',
styleUrls: ['./asset-history.component.scss'],
templateUrl: './asset-history.component.html'
})
export class AssetHistoryComponent implements OnChanges {
@Input()
public asset: AssetDto;
public assetEvents: Observable<ReadonlyArray<AssetEvent>>;
constructor(
private readonly appsState: AppsState,
private readonly historyService: HistoryService
) {
}
public ngOnChanges() {
const channel = `assets.${this.asset.id}`;
this.assetEvents =
this.historyService.getHistory(this.appsState.appName, channel).pipe(
map(events => {
let fileVersion = -1;
return events.map(event => {
const canDownload =
event.eventType === 'AssetUpdatedEventV2' ||
event.eventType === 'AssetCreatedEventV2';
if (canDownload) {
fileVersion++;
}
return { event, fileVersion, canDownload };
});
}));
}
public trackByEvent(index: number, assetEvent: AssetEvent) {
return assetEvent.event.eventId;
}
}

16
frontend/app/shared/components/assets/pipes.ts

@ -11,7 +11,9 @@ import {
ApiUrlConfig,
AssetDto,
AuthService,
MathHelper
MathHelper,
StringHelper,
Types
} from '@app/shared/internal';
@Pipe({
@ -24,8 +26,16 @@ export class AssetUrlPipe implements PipeTransform {
) {
}
public transform(asset: AssetDto): string {
return `${asset.fullUrl(this.apiUrl)}&sq=${MathHelper.guid()}`;
public transform(asset: AssetDto, version?: number): string {
let url = asset.fullUrl(this.apiUrl);
url = StringHelper.appendToUrl(url, 'sq', MathHelper.guid());
if (Types.isNumber(version)) {
url = StringHelper.appendToUrl(url, 'version', version);
}
return url;
}
}

1
frontend/app/shared/declarations.ts

@ -13,6 +13,7 @@ export * from './components/table-header.component';
export * from './components/assets/asset-dialog.component';
export * from './components/assets/asset-folder-dialog.component';
export * from './components/assets/asset-folder.component';
export * from './components/assets/asset-history.component';
export * from './components/assets/asset-path.component';
export * from './components/assets/asset-uploader.component';
export * from './components/assets/asset.component';

2
frontend/app/shared/module.ts

@ -23,6 +23,7 @@ import {
AssetDialogComponent,
AssetFolderComponent,
AssetFolderDialogComponent,
AssetHistoryComponent,
AssetPathComponent,
AssetPreviewUrlPipe,
AssetsDialogState,
@ -131,6 +132,7 @@ import { SearchService } from './services/search.service';
AssetDialogComponent,
AssetFolderComponent,
AssetFolderDialogComponent,
AssetHistoryComponent,
AssetPathComponent,
AssetPreviewUrlPipe,
AssetsListComponent,

5
frontend/app/shared/services/assets.service.ts

@ -22,6 +22,7 @@ import {
Resource,
ResourceLinks,
ResultSet,
StringHelper,
Types,
Version,
Versioned
@ -94,7 +95,7 @@ export class AssetDto {
let url = apiUrl.buildUrl(this.contentUrl);
if (this.isProtected && authService && authService.user) {
url += `&access_token=${authService.user.accessToken}`;
url = StringHelper.appendToUrl(url, 'access_token', authService.user.accessToken);
}
return url;
@ -215,7 +216,7 @@ export class AssetsService {
fullQuery = `q=${encodeQuery(queryObj)}`;
if (parentId) {
fullQuery += `&parentId=${parentId}`;
fullQuery = StringHelper.appendToUrl(fullQuery, 'parentId', parentId);
}
}

Loading…
Cancel
Save