mirror of https://github.com/Squidex/squidex.git
committed by
GitHub
126 changed files with 1961 additions and 699 deletions
@ -0,0 +1,17 @@ |
|||||
|
// ==========================================================================
|
||||
|
// ContentArchived.cs
|
||||
|
// Squidex Headless CMS
|
||||
|
// ==========================================================================
|
||||
|
// Copyright (c) Squidex Group
|
||||
|
// All rights reserved.
|
||||
|
// ==========================================================================
|
||||
|
|
||||
|
using Squidex.Infrastructure.CQRS.Events; |
||||
|
|
||||
|
namespace Squidex.Domain.Apps.Events.Contents |
||||
|
{ |
||||
|
[EventType(nameof(ContentArchived))] |
||||
|
public sealed class ContentArchived : ContentEvent |
||||
|
{ |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,17 @@ |
|||||
|
// ==========================================================================
|
||||
|
// ContentRestored.cs
|
||||
|
// Squidex Headless CMS
|
||||
|
// ==========================================================================
|
||||
|
// Copyright (c) Squidex Group
|
||||
|
// All rights reserved.
|
||||
|
// ==========================================================================
|
||||
|
|
||||
|
using Squidex.Infrastructure.CQRS.Events; |
||||
|
|
||||
|
namespace Squidex.Domain.Apps.Events.Contents |
||||
|
{ |
||||
|
[EventType(nameof(ContentRestored))] |
||||
|
public sealed class ContentRestored : ContentEvent |
||||
|
{ |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,14 @@ |
|||||
|
// ==========================================================================
|
||||
|
// ArchiveContent.cs
|
||||
|
// Squidex Headless CMS
|
||||
|
// ==========================================================================
|
||||
|
// Copyright (c) Squidex Group
|
||||
|
// All rights reserved.
|
||||
|
// ==========================================================================
|
||||
|
|
||||
|
namespace Squidex.Domain.Apps.Write.Contents.Commands |
||||
|
{ |
||||
|
public sealed class ArchiveContent : ContentCommand |
||||
|
{ |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,14 @@ |
|||||
|
// ==========================================================================
|
||||
|
// RestoreContent.cs
|
||||
|
// Squidex Headless CMS
|
||||
|
// ==========================================================================
|
||||
|
// Copyright (c) Squidex Group
|
||||
|
// All rights reserved.
|
||||
|
// ==========================================================================
|
||||
|
|
||||
|
namespace Squidex.Domain.Apps.Write.Contents.Commands |
||||
|
{ |
||||
|
public sealed class RestoreContent : ContentCommand |
||||
|
{ |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,86 @@ |
|||||
|
// ==========================================================================
|
||||
|
// ContentVersionLoader.cs
|
||||
|
// Squidex Headless CMS
|
||||
|
// ==========================================================================
|
||||
|
// Copyright (c) Squidex Group
|
||||
|
// All rights reserved.
|
||||
|
// ==========================================================================
|
||||
|
|
||||
|
using System; |
||||
|
using System.Linq; |
||||
|
using System.Threading.Tasks; |
||||
|
using Squidex.Domain.Apps.Core.Contents; |
||||
|
using Squidex.Domain.Apps.Events.Contents; |
||||
|
using Squidex.Infrastructure; |
||||
|
using Squidex.Infrastructure.CQRS.Events; |
||||
|
|
||||
|
namespace Squidex.Domain.Apps.Write.Contents |
||||
|
{ |
||||
|
public sealed class ContentVersionLoader : IContentVersionLoader |
||||
|
{ |
||||
|
private readonly IStreamNameResolver nameResolver; |
||||
|
private readonly IEventStore eventStore; |
||||
|
private readonly EventDataFormatter formatter; |
||||
|
|
||||
|
public ContentVersionLoader(IEventStore eventStore, IStreamNameResolver nameResolver, EventDataFormatter formatter) |
||||
|
{ |
||||
|
Guard.NotNull(formatter, nameof(formatter)); |
||||
|
Guard.NotNull(eventStore, nameof(eventStore)); |
||||
|
Guard.NotNull(nameResolver, nameof(nameResolver)); |
||||
|
|
||||
|
this.formatter = formatter; |
||||
|
this.eventStore = eventStore; |
||||
|
this.nameResolver = nameResolver; |
||||
|
} |
||||
|
|
||||
|
public async Task<NamedContentData> LoadAsync(Guid appId, Guid id, long version) |
||||
|
{ |
||||
|
var streamName = nameResolver.GetStreamName(typeof(ContentDomainObject), id); |
||||
|
|
||||
|
var events = await eventStore.GetEventsAsync(streamName); |
||||
|
|
||||
|
if (events.Count == 0 || events.Count < version - 1) |
||||
|
{ |
||||
|
throw new DomainObjectNotFoundException(id.ToString(), typeof(ContentDomainObject)); |
||||
|
} |
||||
|
|
||||
|
NamedContentData contentData = null; |
||||
|
|
||||
|
foreach (var storedEvent in events.Where(x => x.EventStreamNumber <= version)) |
||||
|
{ |
||||
|
var envelope = ParseKnownEvent(storedEvent); |
||||
|
|
||||
|
if (envelope != null) |
||||
|
{ |
||||
|
if (envelope.Payload is ContentCreated contentCreated) |
||||
|
{ |
||||
|
if (contentCreated.AppId.Id != appId) |
||||
|
{ |
||||
|
throw new DomainObjectNotFoundException(id.ToString(), typeof(ContentDomainObject)); |
||||
|
} |
||||
|
|
||||
|
contentData = contentCreated.Data; |
||||
|
} |
||||
|
else if (envelope.Payload is ContentUpdated contentUpdated) |
||||
|
{ |
||||
|
contentData = contentUpdated.Data; |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
return contentData; |
||||
|
} |
||||
|
|
||||
|
private Envelope<IEvent> ParseKnownEvent(StoredEvent storedEvent) |
||||
|
{ |
||||
|
try |
||||
|
{ |
||||
|
return formatter.Parse(storedEvent.Data); |
||||
|
} |
||||
|
catch (TypeNameNotFoundException) |
||||
|
{ |
||||
|
return null; |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,19 @@ |
|||||
|
// ==========================================================================
|
||||
|
// IContentVersionLoader.cs
|
||||
|
// Squidex Headless CMS
|
||||
|
// ==========================================================================
|
||||
|
// Copyright (c) Squidex Group
|
||||
|
// All rights reserved.
|
||||
|
// ==========================================================================
|
||||
|
|
||||
|
using System; |
||||
|
using System.Threading.Tasks; |
||||
|
using Squidex.Domain.Apps.Core.Contents; |
||||
|
|
||||
|
namespace Squidex.Domain.Apps.Write.Contents |
||||
|
{ |
||||
|
public interface IContentVersionLoader |
||||
|
{ |
||||
|
Task<NamedContentData> LoadAsync(Guid appId, Guid id, long version); |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,29 @@ |
|||||
|
<sqx-panel panelWidth="16rem"> |
||||
|
<div class="panel-header"> |
||||
|
<div class="panel-title-row"> |
||||
|
<h3 class="panel-title">Activity</h3> |
||||
|
</div> |
||||
|
|
||||
|
<a class="panel-close" sqxParentLink> |
||||
|
<i class="icon-close"></i> |
||||
|
</a> |
||||
|
</div> |
||||
|
|
||||
|
<div class="panel-main"> |
||||
|
<div class="panel-content panel-content-blank"> |
||||
|
<div *ngFor="let event of events | async" class="event"> |
||||
|
<div class="event-left"> |
||||
|
<img class="user-picture" [attr.title]="event.actor | sqxUserNameRef:'I'" [attr.src]="event.actor | sqxUserPictureRef" /> |
||||
|
</div> |
||||
|
<div class="event-main"> |
||||
|
<div class="event-message"> |
||||
|
<span class="event-actor user-ref">{{event.actor | sqxUserNameRef:'I'}}</span> <span [innerHTML]="format(event.message) | async"></span> |
||||
|
</div> |
||||
|
<div class="event-created">{{event.created | sqxFromNow}}</div> |
||||
|
|
||||
|
<a class="event-load" (click)="loadVersion(event.version)">Load this Version</a> |
||||
|
</div> |
||||
|
</div> |
||||
|
</div> |
||||
|
</div> |
||||
|
</sqx-panel> |
||||
@ -0,0 +1,39 @@ |
|||||
|
@import '_vars'; |
||||
|
@import '_mixins'; |
||||
|
|
||||
|
.event { |
||||
|
& { |
||||
|
@include flex-box; |
||||
|
margin-bottom: 1rem; |
||||
|
} |
||||
|
|
||||
|
&-main { |
||||
|
@include flex-grow(1); |
||||
|
} |
||||
|
|
||||
|
&-load { |
||||
|
& { |
||||
|
font-size: .9rem; |
||||
|
font-weight: normal; |
||||
|
cursor: pointer; |
||||
|
color: $color-theme-blue !important; |
||||
|
} |
||||
|
|
||||
|
&:focus, |
||||
|
&:hover { |
||||
|
text-decoration: underline !important; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
&-left { |
||||
|
min-width: 2.8rem; |
||||
|
max-width: 2.8rem; |
||||
|
margin-top: .3rem; |
||||
|
} |
||||
|
|
||||
|
&-created { |
||||
|
font-size: .65rem; |
||||
|
font-weight: normal; |
||||
|
color: $color-text-decent; |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,107 @@ |
|||||
|
/* |
||||
|
* Squidex Headless CMS |
||||
|
* |
||||
|
* @license |
||||
|
* Copyright (c) Sebastian Stehle. All rights reserved |
||||
|
*/ |
||||
|
|
||||
|
import { Component } from '@angular/core'; |
||||
|
import { ActivatedRoute } from '@angular/router'; |
||||
|
import { Observable } from 'rxjs'; |
||||
|
|
||||
|
import { |
||||
|
allParams, |
||||
|
AppComponentBase, |
||||
|
AppsStoreService, |
||||
|
DialogService, |
||||
|
HistoryChannelUpdated, |
||||
|
HistoryEventDto, |
||||
|
HistoryService, |
||||
|
MessageBus, |
||||
|
UsersProviderService |
||||
|
} from 'shared'; |
||||
|
|
||||
|
import { ContentVersionSelected } from './../messages'; |
||||
|
|
||||
|
const REPLACEMENT_TEMP = '$TEMP$'; |
||||
|
|
||||
|
@Component({ |
||||
|
selector: 'sqx-history', |
||||
|
styleUrls: ['./content-history.component.scss'], |
||||
|
templateUrl: './content-history.component.html' |
||||
|
}) |
||||
|
export class ContentHistoryComponent extends AppComponentBase { |
||||
|
public get channel(): string { |
||||
|
let channelPath = this.route.snapshot.data['channel']; |
||||
|
|
||||
|
if (channelPath) { |
||||
|
const params = allParams(this.route); |
||||
|
|
||||
|
for (let key in params) { |
||||
|
if (params.hasOwnProperty(key)) { |
||||
|
const value = params[key]; |
||||
|
|
||||
|
channelPath = channelPath.replace(`{${key}}`, value); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
return channelPath; |
||||
|
} |
||||
|
|
||||
|
public events: Observable<HistoryEventDto[]> = |
||||
|
Observable.timer(0, 10000) |
||||
|
.merge(this.messageBus.of(HistoryChannelUpdated).delay(1000)) |
||||
|
.switchMap(() => this.appNameOnce()) |
||||
|
.switchMap(app => this.historyService.getHistory(app, this.channel).retry(2)); |
||||
|
|
||||
|
constructor(appsStore: AppsStoreService, dialogs: DialogService, |
||||
|
private readonly users: UsersProviderService, |
||||
|
private readonly historyService: HistoryService, |
||||
|
private readonly messageBus: MessageBus, |
||||
|
private readonly route: ActivatedRoute |
||||
|
) { |
||||
|
super(dialogs, appsStore); |
||||
|
} |
||||
|
|
||||
|
private userName(userId: string): Observable<string> { |
||||
|
const parts = userId.split(':'); |
||||
|
|
||||
|
if (parts[0] === 'subject') { |
||||
|
return this.users.getUser(parts[1], 'Me').map(u => u.displayName); |
||||
|
} else { |
||||
|
if (parts[1].endsWith('client')) { |
||||
|
return Observable.of(parts[1]); |
||||
|
} else { |
||||
|
return Observable.of(`${parts[1]}-client`); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
public loadVersion(version: number) { |
||||
|
this.messageBus.emit(new ContentVersionSelected(version)); |
||||
|
} |
||||
|
|
||||
|
public format(message: string): Observable<string> { |
||||
|
let foundUserId: string | null = null; |
||||
|
|
||||
|
message = message.replace(/{([^\s:]*):([^}]*)}/, (match: string, type: string, id: string) => { |
||||
|
if (type === 'user') { |
||||
|
foundUserId = id; |
||||
|
return REPLACEMENT_TEMP; |
||||
|
} else { |
||||
|
return id; |
||||
|
} |
||||
|
}); |
||||
|
|
||||
|
message = message.replace(/{([^}]*)}/g, (match: string, marker: string) => { |
||||
|
return `<span class="marker-ref">${marker}</span>`; |
||||
|
}); |
||||
|
|
||||
|
if (foundUserId) { |
||||
|
return this.userName(foundUserId).map(t => message.replace(REPLACEMENT_TEMP, `<span class="user-ref">${t}</span>`)); |
||||
|
} |
||||
|
|
||||
|
return Observable.of(message); |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,92 @@ |
|||||
|
/* |
||||
|
* Squidex Headless CMS |
||||
|
* |
||||
|
* @license |
||||
|
* Copyright (c) Sebastian Stehle. All rights reserved |
||||
|
*/ |
||||
|
|
||||
|
import { Types } from './../'; |
||||
|
|
||||
|
describe('Types', () => { |
||||
|
it('should make string check', () => { |
||||
|
expect(Types.isString('')).toBeTruthy(); |
||||
|
expect(Types.isString('string')).toBeTruthy(); |
||||
|
|
||||
|
expect(Types.isString(false)).toBeFalsy(); |
||||
|
}); |
||||
|
|
||||
|
it('should make number check', () => { |
||||
|
expect(Types.isNumber(0)).toBeTruthy(); |
||||
|
expect(Types.isNumber(1)).toBeTruthy(); |
||||
|
|
||||
|
expect(Types.isNumber(NaN)).toBeFalsy(); |
||||
|
expect(Types.isNumber(Infinity)).toBeFalsy(); |
||||
|
expect(Types.isNumber(false)).toBeFalsy(); |
||||
|
}); |
||||
|
|
||||
|
it('should make boolean check', () => { |
||||
|
expect(Types.isBoolean(true)).toBeTruthy(); |
||||
|
expect(Types.isBoolean(false)).toBeTruthy(); |
||||
|
|
||||
|
expect(Types.isBoolean(0)).toBeFalsy(); |
||||
|
expect(Types.isBoolean(1)).toBeFalsy(); |
||||
|
}); |
||||
|
|
||||
|
it('should make number array check', () => { |
||||
|
expect(Types.isArrayOfNumber([])).toBeTruthy(); |
||||
|
expect(Types.isArrayOfNumber([0, 1])).toBeTruthy(); |
||||
|
|
||||
|
expect(Types.isArrayOfNumber(['0', 1])).toBeFalsy(); |
||||
|
}); |
||||
|
|
||||
|
it('should make string array check', () => { |
||||
|
expect(Types.isArrayOfString([])).toBeTruthy(); |
||||
|
expect(Types.isArrayOfString(['0', '1'])).toBeTruthy(); |
||||
|
|
||||
|
expect(Types.isArrayOfString(['0', 1])).toBeFalsy(); |
||||
|
}); |
||||
|
|
||||
|
it('should make array check', () => { |
||||
|
expect(Types.isArray([])).toBeTruthy(); |
||||
|
expect(Types.isArray([0])).toBeTruthy(); |
||||
|
|
||||
|
expect(Types.isArray({})).toBeFalsy(); |
||||
|
}); |
||||
|
|
||||
|
it('should make object check', () => { |
||||
|
expect(Types.isObject({})).toBeTruthy(); |
||||
|
expect(Types.isObject({ v: 1 })).toBeTruthy(); |
||||
|
|
||||
|
expect(Types.isObject([])).toBeFalsy(); |
||||
|
}); |
||||
|
|
||||
|
it('should make RegExp check', () => { |
||||
|
expect(Types.isRegExp(/[.*]/)).toBeTruthy(); |
||||
|
|
||||
|
expect(Types.isRegExp('/[.*]/')).toBeFalsy(); |
||||
|
}); |
||||
|
|
||||
|
it('should make Date check', () => { |
||||
|
expect(Types.isDate(new Date())).toBeTruthy(); |
||||
|
|
||||
|
expect(Types.isDate(new Date().getDate())).toBeFalsy(); |
||||
|
}); |
||||
|
|
||||
|
it('should make undefined check', () => { |
||||
|
expect(Types.isUndefined(undefined)).toBeTruthy(); |
||||
|
|
||||
|
expect(Types.isUndefined(null)).toBeFalsy(); |
||||
|
}); |
||||
|
|
||||
|
it('should make null check', () => { |
||||
|
expect(Types.isNull(null)).toBeTruthy(); |
||||
|
|
||||
|
expect(Types.isNull(undefined)).toBeFalsy(); |
||||
|
}); |
||||
|
|
||||
|
it('should make function check', () => { |
||||
|
expect(Types.isFunction(() => { /* NOOP */ })).toBeTruthy(); |
||||
|
|
||||
|
expect(Types.isFunction([])).toBeFalsy(); |
||||
|
}); |
||||
|
}); |
||||
@ -0,0 +1,70 @@ |
|||||
|
/* |
||||
|
* Squidex Headless CMS |
||||
|
* |
||||
|
* @license |
||||
|
* Copyright (c) Sebastian Stehle. All rights reserved |
||||
|
*/ |
||||
|
|
||||
|
export module Types { |
||||
|
export function isString(value: any): boolean { |
||||
|
return typeof value === 'string' || value instanceof String; |
||||
|
} |
||||
|
|
||||
|
export function isNumber(value: any): boolean { |
||||
|
return typeof value === 'number' && isFinite(value); |
||||
|
} |
||||
|
|
||||
|
export function isArray(value: any): boolean { |
||||
|
return Array.isArray(value); |
||||
|
} |
||||
|
|
||||
|
export function isFunction(value: any): boolean { |
||||
|
return typeof value === 'function'; |
||||
|
} |
||||
|
|
||||
|
export function isObject(value: any): boolean { |
||||
|
return value && typeof value === 'object' && value.constructor === Object; |
||||
|
} |
||||
|
|
||||
|
export function isBoolean(value: any): boolean { |
||||
|
return typeof value === 'boolean'; |
||||
|
}; |
||||
|
|
||||
|
export function isNull(value: any): boolean { |
||||
|
return value === null; |
||||
|
} |
||||
|
|
||||
|
export function isUndefined(value: any): boolean { |
||||
|
return typeof value === 'undefined'; |
||||
|
} |
||||
|
|
||||
|
export function isRegExp(value: any): boolean { |
||||
|
return value && typeof value === 'object' && value.constructor === RegExp; |
||||
|
} |
||||
|
|
||||
|
export function isDate(value: any): boolean { |
||||
|
return value instanceof Date; |
||||
|
} |
||||
|
|
||||
|
export function isArrayOfNumber(value: any): boolean { |
||||
|
return isArrayOf(value, v => isNumber(v)); |
||||
|
} |
||||
|
|
||||
|
export function isArrayOfString(value: any): boolean { |
||||
|
return isArrayOf(value, v => isString(v)); |
||||
|
} |
||||
|
|
||||
|
export function isArrayOf(value: any, validator: (v: any) => boolean): boolean { |
||||
|
if (!Array.isArray(value)) { |
||||
|
return false; |
||||
|
} |
||||
|
|
||||
|
for (let v of value) { |
||||
|
if (!validator(v)) { |
||||
|
return false; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
return true; |
||||
|
} |
||||
|
} |
||||
Some files were not shown because too many files changed in this diff
Loading…
Reference in new issue