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