Browse Source

History V1

pull/1/head
Sebastian 9 years ago
parent
commit
721db58288
  1. 5
      src/Squidex.Events/EventExtensions.cs
  2. 6
      src/Squidex.Infrastructure/CQRS/Commands/IActorCommand.cs
  3. 2
      src/Squidex.Infrastructure/CQRS/CommonHeaders.cs
  4. 8
      src/Squidex.Infrastructure/CQRS/EnvelopeExtensions.cs
  5. 8
      src/Squidex.Infrastructure/CQRS/Events/EnrichWithActorProcessor.cs
  6. 8
      src/Squidex.Infrastructure/Json/RefTokenConverter.cs
  7. 14
      src/Squidex.Infrastructure/RefToken.cs
  8. 3
      src/Squidex.Read/History/IHistoryEventEntity.cs
  9. 4
      src/Squidex.Read/IAppRefEntity.cs
  10. 13
      src/Squidex.Read/ITrackCreatedByEntity.cs
  11. 17
      src/Squidex.Read/ITrackLastModifiedByEntity.cs
  12. 2
      src/Squidex.Read/Schemas/Repositories/ISchemaEntity.cs
  13. 4
      src/Squidex.Store.MongoDb/History/MessagesEN.cs
  14. 22
      src/Squidex.Store.MongoDb/History/MongoHistoryEventEntity.cs
  15. 10
      src/Squidex.Store.MongoDb/History/MongoHistoryEventRepository.cs
  16. 6
      src/Squidex.Store.MongoDb/History/ParsedHistoryEvent.cs
  17. 12
      src/Squidex.Store.MongoDb/MongoDbModule.cs
  18. 81
      src/Squidex.Store.MongoDb/Utils/EntityMapper.cs
  19. 60
      src/Squidex.Store.MongoDb/Utils/RefTokenSerializer.cs
  20. 4
      src/Squidex.Write/Apps/AppDomainObject.cs
  21. 4
      src/Squidex.Write/Apps/Commands/CreateApp.cs
  22. 4
      src/Squidex.Write/SquidexCommand.cs
  23. 2
      src/Squidex/app/features/settings/pages/clients/clients-page.component.html
  24. 2
      src/Squidex/app/features/settings/pages/contributors/contributors-page.component.html
  25. 2
      src/Squidex/app/features/settings/pages/languages/languages-page.component.html
  26. 6
      src/Squidex/app/framework/utils/date-time.spec.ts
  27. 4
      src/Squidex/app/framework/utils/date-time.ts
  28. 4
      src/Squidex/app/shared/components/app-form.component.html
  29. 2
      src/Squidex/app/shared/components/app-form.component.ts
  30. 7
      src/Squidex/app/shared/components/history.component.html
  31. 20
      src/Squidex/app/shared/components/history.component.scss
  32. 32
      src/Squidex/app/shared/components/history.component.ts
  33. 4
      src/Squidex/app/shared/services/history.service.spec.ts
  34. 4
      src/Squidex/app/shared/services/history.service.ts
  35. 9
      src/Squidex/app/shared/services/users-provider.service.ts
  36. 6
      src/Squidex/app/theme/_history.scss
  37. 5
      src/Squidex/app/theme/_panels.scss
  38. BIN
      src/Squidex/app/theme/icomoon/fonts/icomoon.eot
  39. 1
      src/Squidex/app/theme/icomoon/fonts/icomoon.svg
  40. BIN
      src/Squidex/app/theme/icomoon/fonts/icomoon.ttf
  41. BIN
      src/Squidex/app/theme/icomoon/fonts/icomoon.woff
  42. 27
      src/Squidex/app/theme/icomoon/selection.json
  43. 13
      src/Squidex/app/theme/icomoon/style.css
  44. 1
      src/Squidex/app/theme/theme.scss
  45. 10
      tests/Squidex.Infrastructure.Tests/CQRS/EnvelopeExtensionsTests.cs
  46. 18
      tests/Squidex.Infrastructure.Tests/CQRS/Events/EnrichWithActorProcessorTests.cs
  47. 46
      tests/Squidex.Infrastructure.Tests/RefTokenTests.cs
  48. 8
      tests/Squidex.Write.Tests/Apps/AppCommandHandlerTests.cs
  49. 6
      tests/Squidex.Write.Tests/Apps/AppDomainObjectTests.cs

5
src/Squidex.Events/EventExtensions.cs

@ -14,6 +14,11 @@ namespace Squidex.Events
{
public static class EventExtensions
{
public static bool HasAppId(this EnvelopeHeaders headers)
{
return headers.Contains("AppId");
}
public static Guid AppId(this EnvelopeHeaders headers)
{
return headers["AppId"].ToGuid(CultureInfo.InvariantCulture);

6
src/Squidex.Infrastructure/CQRS/Commands/IUserCommand.cs → src/Squidex.Infrastructure/CQRS/Commands/IActorCommand.cs

@ -1,5 +1,5 @@
// ==========================================================================
// IUserCommand.cs
// IActorCommand.cs
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex Group
@ -8,8 +8,8 @@
namespace Squidex.Infrastructure.CQRS.Commands
{
public interface IUserCommand : ICommand
public interface IActorCommand : ICommand
{
UserToken User { get; set; }
RefToken Actor { get; set; }
}
}

2
src/Squidex.Infrastructure/CQRS/CommonHeaders.cs

@ -20,6 +20,6 @@ namespace Squidex.Infrastructure.CQRS
public const string Timestamp = "Timestamp";
public const string User = "User";
public const string Actor = "Actor";
}
}

8
src/Squidex.Infrastructure/CQRS/EnvelopeExtensions.cs

@ -62,14 +62,14 @@ namespace Squidex.Infrastructure.CQRS
return envelope;
}
public static UserToken User(this EnvelopeHeaders headers)
public static RefToken Actor(this EnvelopeHeaders headers)
{
return UserToken.Parse(headers[CommonHeaders.User].ToString());
return RefToken.Parse(headers[CommonHeaders.Actor].ToString());
}
public static Envelope<T> SetUser<T>(this Envelope<T> envelope, UserToken value) where T : class
public static Envelope<T> SetActor<T>(this Envelope<T> envelope, RefToken value) where T : class
{
envelope.Headers.Set(CommonHeaders.User, value.ToString());
envelope.Headers.Set(CommonHeaders.Actor, value.ToString());
return envelope;
}

8
src/Squidex.Infrastructure/CQRS/Events/EnrichWithUserProcessor.cs → src/Squidex.Infrastructure/CQRS/Events/EnrichWithActorProcessor.cs

@ -1,5 +1,5 @@
// ==========================================================================
// EnrichWithUserProcessor.cs
// EnrichWithActorProcessor.cs
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex Group
@ -12,15 +12,15 @@ using Squidex.Infrastructure.Tasks;
namespace Squidex.Infrastructure.CQRS.Events
{
public sealed class EnrichWithUserProcessor : IEventProcessor
public sealed class EnrichWithActorProcessor : IEventProcessor
{
public Task ProcessEventAsync(Envelope<IEvent> @event, IAggregate aggregate, ICommand command)
{
var userCommand = command as IUserCommand;
var userCommand = command as IActorCommand;
if (userCommand != null)
{
@event.SetUser(userCommand.User);
@event.SetActor(userCommand.Actor);
}
return TaskHelper.Done;

8
src/Squidex.Infrastructure/Json/UserTokenConverter.cs → src/Squidex.Infrastructure/Json/RefTokenConverter.cs

@ -1,5 +1,5 @@
// ==========================================================================
// UserTokenConverter.cs
// RefTokenConverter.cs
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex Group
@ -11,7 +11,7 @@ using Newtonsoft.Json;
namespace Squidex.Infrastructure.Json
{
public sealed class UserTokenConverter : JsonConverter
public sealed class RefTokenConverter : JsonConverter
{
public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer)
{
@ -20,12 +20,12 @@ namespace Squidex.Infrastructure.Json
public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer)
{
return reader.TokenType == JsonToken.Null ? null : UserToken.Parse((string)reader.Value);
return reader.TokenType == JsonToken.Null ? null : RefToken.Parse((string)reader.Value);
}
public override bool CanConvert(Type objectType)
{
return objectType == typeof(UserToken);
return objectType == typeof(RefToken);
}
}
}

14
src/Squidex.Infrastructure/UserToken.cs → src/Squidex.Infrastructure/RefToken.cs

@ -1,5 +1,5 @@
// ==========================================================================
// UserToken.cs
// RefToken.cs
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex Group
@ -11,13 +11,13 @@ using System.Linq;
namespace Squidex.Infrastructure
{
public sealed class UserToken : IEquatable<UserToken>
public sealed class RefToken : IEquatable<RefToken>
{
public string Type { get; }
public string Identifier { get; }
public UserToken(string type, string identifier)
public RefToken(string type, string identifier)
{
Guard.NotNullOrEmpty(type, nameof(type));
Guard.NotNullOrEmpty(identifier, nameof(identifier));
@ -27,7 +27,7 @@ namespace Squidex.Infrastructure
Identifier = identifier;
}
public static UserToken Parse(string input)
public static RefToken Parse(string input)
{
Guard.NotNullOrEmpty(input, nameof(input));
@ -38,7 +38,7 @@ namespace Squidex.Infrastructure
throw new ArgumentException("Input must have more than 2 parts divided by colon", nameof(input));
}
return new UserToken(parts[0], string.Join(":", parts.Skip(1)));
return new RefToken(parts[0], string.Join(":", parts.Skip(1)));
}
public override string ToString()
@ -48,10 +48,10 @@ namespace Squidex.Infrastructure
public override bool Equals(object obj)
{
return Equals(obj as UserToken);
return Equals(obj as RefToken);
}
public bool Equals(UserToken other)
public bool Equals(RefToken other)
{
return other != null && (ReferenceEquals(this, other) || (Type.Equals(other.Type) && Identifier.Equals(other.Identifier)));
}

3
src/Squidex.Read/History/IHistoryEventEntity.cs

@ -7,6 +7,7 @@
// ==========================================================================
using System;
using Squidex.Infrastructure;
namespace Squidex.Read.History
{
@ -15,5 +16,7 @@ namespace Squidex.Read.History
Guid EventId { get; }
string Message { get; }
RefToken Actor { get; }
}
}

4
src/Squidex.Read/IAppEntity.cs → src/Squidex.Read/IAppRefEntity.cs

@ -1,5 +1,5 @@
// ==========================================================================
// IAppEntity.cs
// IAppRefEntity.cs
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex Group
@ -10,7 +10,7 @@ using System;
namespace Squidex.Read
{
public interface IAppEntity : IEntity
public interface IAppRefEntity : IEntity
{
Guid AppId { get; set; }
}

13
src/Squidex.Read/ITrackCreatedByEntity.cs

@ -0,0 +1,13 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Squidex.Infrastructure;
namespace Squidex.Read
{
public interface ITrackCreatedByEntity
{
RefToken CreatedBy { get; set; }
}
}

17
src/Squidex.Read/ITrackLastModifiedByEntity.cs

@ -0,0 +1,17 @@
// ==========================================================================
// ITrackLastModifiedByEntity.cs
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex Group
// All rights reserved.
// ==========================================================================
using Squidex.Infrastructure;
namespace Squidex.Read
{
public interface ITrackLastModifiedByEntity
{
RefToken LastModifiedBy { get; set; }
}
}

2
src/Squidex.Read/Schemas/Repositories/ISchemaEntity.cs

@ -7,7 +7,7 @@
// ==========================================================================
namespace Squidex.Read.Schemas.Repositories
{
public interface ISchemaEntity : IAppEntity
public interface ISchemaEntity : IAppRefEntity
{
string Name { get; }
}

4
src/Squidex.Store.MongoDb/History/MessagesEN.cs

@ -19,11 +19,11 @@ namespace Squidex.Store.MongoDb.History
{
{
TypeNameRegistry.GetName<AppContributorAssigned>(),
"[User] assigned [Contributor] to app with permission [Permission]"
"assigned {user:[Contributor]} to app with permission [Permission]"
},
{
TypeNameRegistry.GetName<AppContributorRemoved>(),
"[User] removed [Contributor] from app"
"removed {user:[Contributor]} from app"
}
};
}

22
src/Squidex.Store.MongoDb/History/MongoHistoryEventEntity.cs

@ -11,11 +11,12 @@ using System.Collections.Generic;
using MongoDB.Bson.Serialization.Attributes;
using Squidex.Infrastructure;
using Squidex.Infrastructure.CQRS;
using Squidex.Read;
using Squidex.Store.MongoDb.Utils;
namespace Squidex.Store.MongoDb.History
{
public sealed class MongoHistoryEventEntity : MongoEntity
public sealed class MongoHistoryEventEntity : MongoEntity, IAppRefEntity, ITrackCreatedByEntity
{
[BsonRequired]
[BsonElement]
@ -31,12 +32,24 @@ namespace Squidex.Store.MongoDb.History
[BsonRequired]
[BsonElement]
public UserToken User { get; set; }
public RefToken Actor { get; set; }
[BsonRequired]
[BsonElement]
public Dictionary<string, string> Parameters { get; set; }
RefToken ITrackCreatedByEntity.CreatedBy
{
get
{
return Actor;
}
set
{
Actor = value;
}
}
public MongoHistoryEventEntity()
{
Parameters = new Dictionary<string, string>();
@ -46,11 +59,6 @@ namespace Squidex.Store.MongoDb.History
{
Channel = channel;
if (headers.Contains(CommonHeaders.User))
{
AddParameter("User", headers[CommonHeaders.User].ToString());
}
Message = TypeNameRegistry.GetName<T>();
return this;

10
src/Squidex.Store.MongoDb/History/MongoHistoryEventRepository.cs

@ -44,7 +44,7 @@ namespace Squidex.Store.MongoDb.History
public async Task<List<IHistoryEventEntity>> QueryEventsByChannel(Guid appId, string channelPrefix, int count)
{
var entities =
await Collection.Find(x => x.AppId == appId && x.Channel.StartsWith(channelPrefix)).Limit(count).ToListAsync();
await Collection.Find(x => x.AppId == appId && x.Channel.StartsWith(channelPrefix)).SortByDescending(x => x.Created).Limit(count).ToListAsync();
return entities.Select(x => (IHistoryEventEntity)new ParsedHistoryEvent(x, MessagesEN.Texts)).ToList();
}
@ -53,23 +53,23 @@ namespace Squidex.Store.MongoDb.History
{
return Collection.CreateAsync(headers, x =>
{
var channel = $"Apps.{headers.AggregateId()}.Contributors";
const string channel = "settings.contributors";
x.Setup<AppContributorAssigned>(headers, channel)
.AddParameter("Contributor", @event.ContributorId)
.AddParameter("Permission", @event.Permission.ToString());
});
}, false);
}
protected Task On(AppContributorRemoved @event, EnvelopeHeaders headers)
{
return Collection.CreateAsync(headers, x =>
{
var channel = $"Apps.{headers.AggregateId()}.Contributors";
const string channel = "settings.contributors";
x.Setup<AppContributorRemoved>(headers, channel)
.AddParameter("Contributor", @event.ContributorId);
});
}, false);
}
public Task On(Envelope<IEvent> @event)

6
src/Squidex.Store.MongoDb/History/ParsedHistoryEvent.cs

@ -8,6 +8,7 @@
using System;
using System.Collections.Generic;
using Squidex.Infrastructure;
using Squidex.Read.History;
namespace Squidex.Store.MongoDb.History
@ -27,6 +28,11 @@ namespace Squidex.Store.MongoDb.History
get { return inner.Id; }
}
public RefToken Actor
{
get { return inner.Actor; }
}
public DateTime Created
{
get { return inner.Created; }

12
src/Squidex.Store.MongoDb/MongoDbModule.cs

@ -23,6 +23,7 @@ using Squidex.Store.MongoDb.History;
using Squidex.Store.MongoDb.Infrastructure;
using Squidex.Store.MongoDb.Schemas;
using Squidex.Store.MongoDb.Users;
using Squidex.Store.MongoDb.Utils;
namespace Squidex.Store.MongoDb
{
@ -30,6 +31,8 @@ namespace Squidex.Store.MongoDb
{
protected override void Load(ContainerBuilder builder)
{
RefTokenSerializer.Register();
builder.Register(context =>
{
var options = context.Resolve<IOptions<MyMongoDbOptions>>().Value;
@ -67,12 +70,13 @@ namespace Squidex.Store.MongoDb
.As<IStreamPositionStorage>()
.SingleInstance();
builder.RegisterType<MongoHistoryEventRepository>()
.As<IHistoryEventRepository>()
.SingleInstance();
builder.RegisterType<MongoUserRepository>()
.As<IUserRepository>()
.InstancePerLifetimeScope();
builder.RegisterType<MongoHistoryEventRepository>()
.As<IHistoryEventRepository>()
.As<ICatchEventConsumer>()
.SingleInstance();
builder.RegisterType<MongoSchemaRepository>()

81
src/Squidex.Store.MongoDb/Utils/EntityMapper.cs

@ -14,25 +14,83 @@ using Newtonsoft.Json.Linq;
using Squidex.Events;
using Squidex.Infrastructure.CQRS;
using Squidex.Read;
// ReSharper disable ConvertIfStatementToConditionalTernaryExpression
// ReSharper disable SuspiciousTypeConversion.Global
namespace Squidex.Store.MongoDb.Utils
{
public static class EntityMapper
{
public static T Create<T>(EnvelopeHeaders headers) where T : MongoEntity, new()
public static T Create<T>(EnvelopeHeaders headers, bool useAggregateId = true) where T : MongoEntity, new()
{
var timestamp = headers.Timestamp().ToDateTimeUtc();
var entity = new T();
var entity = new T { Id = headers.AggregateId(), Created = timestamp };
AssignId(headers, entity, useAggregateId);
AssignAppId(headers, entity);
AssignCreated(headers, entity);
AssignCreatedBy(headers, entity);
var appEntity = entity as IAppEntity;
return Update(entity, headers);
}
public static T Update<T>(T entity, EnvelopeHeaders headers) where T : MongoEntity
{
AssignLastModified(headers, entity);
AssignLastModifiedBy(headers, entity);
return entity;
}
private static void AssignCreated(EnvelopeHeaders headers, MongoEntity entity)
{
entity.Created = headers.Timestamp().ToDateTimeUtc();
}
private static void AssignLastModified(EnvelopeHeaders headers, MongoEntity entity)
{
entity.LastModified = headers.Timestamp().ToDateTimeUtc();
}
private static void AssignCreatedBy(EnvelopeHeaders headers, MongoEntity entity)
{
var createdBy = entity as ITrackCreatedByEntity;
if (createdBy != null)
{
createdBy.CreatedBy = headers.Actor();
}
}
private static void AssignLastModifiedBy(EnvelopeHeaders headers, MongoEntity entity)
{
var modifiedBy = entity as ITrackLastModifiedByEntity;
if (modifiedBy != null)
{
modifiedBy.LastModifiedBy = headers.Actor();
}
}
private static void AssignAppId(EnvelopeHeaders headers, MongoEntity entity)
{
var appEntity = entity as IAppRefEntity;
if (appEntity != null)
{
appEntity.AppId = headers.AppId();
}
}
return Update(entity, headers);
private static void AssignId(EnvelopeHeaders headers, MongoEntity entity, bool useAggregateId)
{
if (useAggregateId)
{
entity.Id = headers.AggregateId();
}
else
{
entity.Id = Guid.NewGuid();
}
}
public static BsonDocument ToBsonDocument(this JToken value)
@ -49,18 +107,9 @@ namespace Squidex.Store.MongoDb.Utils
return JToken.Parse(json);
}
public static T Update<T>(T entity, EnvelopeHeaders headers) where T : MongoEntity
{
var timestamp = headers.Timestamp().ToDateTimeUtc();
entity.LastModified = timestamp;
return entity;
}
public static Task CreateAsync<T>(this IMongoCollection<T> collection, EnvelopeHeaders headers, Action<T> updater) where T : MongoEntity, new()
public static Task CreateAsync<T>(this IMongoCollection<T> collection, EnvelopeHeaders headers, Action<T> updater, bool useAggregateId = true) where T : MongoEntity, new()
{
var entity = Create<T>(headers);
var entity = Create<T>(headers, useAggregateId);
updater(entity);

60
src/Squidex.Store.MongoDb/Utils/RefTokenSerializer.cs

@ -0,0 +1,60 @@
// ==========================================================================
// RefTokenSerializer.cs
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex Group
// All rights reserved.
// ==========================================================================
using MongoDB.Bson.Serialization;
using MongoDB.Bson.Serialization.Serializers;
using Squidex.Infrastructure;
// ReSharper disable InvertIf
namespace Squidex.Store.MongoDb.Utils
{
public class RefTokenSerializer : SerializerBase<RefToken>
{
private static bool isRegistered;
private static readonly object LockObject = new object();
public static bool Register()
{
if (!isRegistered)
{
lock (LockObject)
{
if (!isRegistered)
{
BsonSerializer.RegisterSerializer(new RefTokenSerializer());
isRegistered = true;
return true;
}
}
}
return false;
}
public override RefToken Deserialize(BsonDeserializationContext context, BsonDeserializationArgs args)
{
var value = context.Reader.ReadString();
return value != null ? RefToken.Parse(value) : null;
}
public override void Serialize(BsonSerializationContext context, BsonSerializationArgs args, RefToken value)
{
if (value != null)
{
context.Writer.WriteString(value.ToString());
}
else
{
context.Writer.WriteNull();
}
}
}
}

4
src/Squidex.Write/Apps/AppDomainObject.cs

@ -207,9 +207,9 @@ namespace Squidex.Write.Apps
return new AppMasterLanguageSet { Language = DefaultLanguage };
}
private static AppContributorAssigned CreateInitialOwner(IUserCommand command)
private static AppContributorAssigned CreateInitialOwner(IActorCommand command)
{
return new AppContributorAssigned { ContributorId = command.User.Identifier, Permission = PermissionLevel.Owner };
return new AppContributorAssigned { ContributorId = command.Actor.Identifier, Permission = PermissionLevel.Owner };
}
private void ThrowIfNotCreated()

4
src/Squidex.Write/Apps/Commands/CreateApp.cs

@ -13,11 +13,11 @@ using Squidex.Infrastructure.CQRS.Commands;
namespace Squidex.Write.Apps.Commands
{
public sealed class CreateApp : AggregateCommand, IUserCommand, IValidatable
public sealed class CreateApp : AggregateCommand, IActorCommand, IValidatable
{
public string Name { get; set; }
public UserToken User { get; set; }
public RefToken Actor { get; set; }
public CreateApp()
{

4
src/Squidex.Write/SquidexCommand.cs

@ -11,8 +11,8 @@ using Squidex.Infrastructure.CQRS.Commands;
namespace Squidex.Write
{
public abstract class SquidexCommand : AggregateCommand, IUserCommand
public abstract class SquidexCommand : AggregateCommand, IActorCommand
{
public UserToken User { get; set; }
public RefToken Actor { get; set; }
}
}

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

@ -47,7 +47,7 @@
<div class="nav nav-pills nav-stacked nav-light">
<li class="nav-item">
<a class="nav-link" routerLink="history" routerLinkActive="active">
<i class="icon-filter"></i>
<i class="icon-time"></i>
</a>
</li>
</div>

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

@ -67,7 +67,7 @@
<div class="nav nav-pills nav-stacked nav-light">
<li class="nav-item">
<a class="nav-link" routerLink="history" routerLinkActive="active">
<i class="icon-filter"></i>
<i class="icon-time"></i>
</a>
</li>
</div>

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

@ -78,7 +78,7 @@
<div class="nav nav-pills nav-stacked nav-light">
<li class="nav-item">
<a class="nav-link" routerLink="history" routerLinkActive="active">
<i class="icon-filter"></i>
<i class="icon-time"></i>
</a>
</li>
</div>

6
src/Squidex/app/framework/utils/date-time.spec.ts

@ -81,6 +81,12 @@ describe('DateTime', () => {
expect(actual).toEqual(expected);
});
it('should convert to local time', () => {
const value = DateTime.parseISO_UTC('2013-10-16T12:13:14');
expect(value.toLocal().ne(value)).toBeTruthy();
});
it('should print to formatted string', () => {
const value = DateTime.parseISO_UTC('2013-10-16T12:13:14');
const actual = value.toStringFormat('hh:mm');

4
src/Squidex/app/framework/utils/date-time.ts

@ -221,6 +221,10 @@ export class DateTime {
return new DateTime(clone);
}
public toLocal(): DateTime {
return new DateTime(new Date(this.value.getTime() - this.value.getTimezoneOffset() * 60 * 1000));
}
public toUTCString(): string {
return this.value.toUTCString();
}

4
src/Squidex/app/shared/components/app-form.component.html

@ -8,7 +8,7 @@
<div class="form-group">
<label for="app-name">Name</label>
<div class="errors-box" *ngIf="createForm.get('name').invalid && createForm.get('name').dirty" [@fade]>
<div class="errors-box" *ngIf="createForm.get('name').invalid && createForm.get('name').touched" [@fade]>
<div class="errors">
<span *ngIf="createForm.get('name').hasError('required')">
Name is required.
@ -33,6 +33,6 @@
<div class="form-group clearfix">
<button type="reset" class="float-xs-left btn btn-secondary" (click)="cancel()">Cancel</button>
<button type="submit" class="float-xs-right btn btn-primary" [disabled]="createForm.invalid">Create</button>
<button type="submit" class="float-xs-right btn btn-primary" >Create</button>
</div>
</form>

2
src/Squidex/app/shared/components/app-form.component.ts

@ -59,7 +59,7 @@ export class AppFormComponent implements OnInit {
}
public createApp() {
this.createForm.markAsDirty();
this.createForm.markAsTouched();
if (this.createForm.valid) {
this.createForm.disable();

7
src/Squidex/app/shared/components/history.component.html

@ -11,8 +11,11 @@
<div class="panel-main">
<div class="panel-content panel-content-blank">
<div *ngFor="let event of events">
{{event.message}}
<div *ngFor="let event of events" class="event">
<div class="event-created">{{event.created.toLocal() | fromNow}}</div>
<div class="event-message">
<span class="event-actor">{{actorName(event.actor) | async}}</span> <span [innerHTML]="format(event.message) | async"></span>
</div>
</div>
</div>
</div>

20
src/Squidex/app/shared/components/history.component.scss

@ -2,6 +2,22 @@
@import '_mixins';
.panel {
min-width: 220px;
max-width: 1220px80px;
min-width: 250px;
max-width: 250px;
}
.event {
& {
margin-bottom: .8rem;
}
&-message {
font-size: .9rem;
}
&-created {
font-size: .65rem;
font-weight: normal;
color: $color-empty;
}
}

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

@ -7,6 +7,7 @@
import { Component, OnDestroy, OnInit } from '@angular/core';
import { ActivatedRoute } from '@angular/router';
import { Observable } from 'rxjs';
import { ImmutableArray, NotificationService } from 'framework';
@ -16,6 +17,8 @@ import { HistoryEventDto, HistoryService } from './../services/history.service';
import { UsersProviderService } from './../services/users-provider.service';
const FALLBACK_NAME = 'my-app';
const REPLACEMENT_REGEXP = new RegExp('{([^\s:]*):([^}]*)}');
const REPLACEMENT_TEMP = '$TEMP$';
@Component({
selector: 'sqx-history',
@ -56,4 +59,33 @@ export class HistoryComponent extends AppComponentBase implements OnDestroy, OnI
this.events = ImmutableArray.of(dtos);
});
}
public actorName(actor: string): Observable<string> {
const parts = actor.split(':');
if (parts[0] === 'subject') {
return this.userName(parts[1]).map(n => n === 'Me' ? 'I' : n);
}
return Observable.of(parts[1]);
}
public format(message: string): Observable<string> {
let foundUserId: string;
message = message.replace(/{([^\s:]*):([^}]*)}/, (match: string, type: string, id: string) => {
if (type === 'user') {
foundUserId = id;
return REPLACEMENT_TEMP;
} else {
return id;
}
});
if (foundUserId) {
return this.userName(foundUserId).map(t => message.replace(REPLACEMENT_TEMP, `<span class="user-ref">${t}</span>`));
}
return Observable.of(message);
}
}

4
src/Squidex/app/shared/services/history.service.spec.ts

@ -33,12 +33,12 @@ describe('HistoryService', () => {
new Response(
new ResponseOptions({
body: [{
user: 'User1',
actor: 'User1',
eventId: '1',
message: 'Message 1',
created: '2016-12-12T10:10'
}, {
user: 'User2',
actor: 'User2',
eventId: '2',
message: 'Message 2',
created: '2016-12-13T10:10'

4
src/Squidex/app/shared/services/history.service.ts

@ -19,7 +19,7 @@ import { AuthService } from './auth.service';
export class HistoryEventDto {
constructor(
public readonly eventId: string,
public readonly user: string,
public readonly actor: string,
public readonly message: string,
public readonly created: DateTime
) {
@ -45,7 +45,7 @@ export class HistoryService {
return items.map(item => {
return new HistoryEventDto(
item.eventId,
item.user,
item.actor,
item.message,
DateTime.parseISO_UTC(item.created));
});

9
src/Squidex/app/shared/services/users-provider.service.ts

@ -31,12 +31,11 @@ export class UsersProviderService {
.catch(err => {
return Observable.of(new UserDto('NOT FOUND', 'NOT FOUND', 'NOT FOUND', ''));
})
.map(u => {
if (this.authService.user && u.id === this.authService.user.id) {
return new UserDto(u.id, u.email, 'Me', u.pictureUrl);
} else {
return u;
.map(dto => {
if (this.authService.user && dto.id === this.authService.user.id) {
dto = new UserDto(dto.id, dto.email, 'Me', dto.pictureUrl);
}
return dto;
})
.publishLast();

6
src/Squidex/app/theme/_history.scss

@ -0,0 +1,6 @@
@import '_mixins';
@import '_vars';
.user-ref {
color: $color-theme-blue-dark;
}

5
src/Squidex/app/theme/_panels.scss

@ -3,7 +3,7 @@
$panel-padding: 20px;
$panel-header: 70px;
$panel-sidebar: 60px;
$panel-sidebar: 61px;
.panel-container {
@include absolute($size-navbar-height, 0, 0, $size-sidebar-width);
@ -68,12 +68,13 @@ $panel-sidebar: 60px;
& .nav-link {
text-align: center;
color: $color-text;
}
}
&-close {
& {
@include absolute($panel-padding, $panel-padding, auto, auto);
@include absolute($panel-padding, $panel-padding - 2px, auto, auto);
font-size: 1.5rem;
font-weight: 400;
cursor: pointer;

BIN
src/Squidex/app/theme/icomoon/fonts/icomoon.eot

Binary file not shown.

1
src/Squidex/app/theme/icomoon/fonts/icomoon.svg

@ -8,6 +8,7 @@
<missing-glyph horiz-adv-x="1024" />
<glyph unicode="&#x20;" horiz-adv-x="512" d="" />
<glyph unicode="&#xe5cd;" glyph-name="close" d="M810 664.667l-238-238 238-238-60-60-238 238-238-238-60 60 238 238-238 238 60 60 238-238 238 238z" />
<glyph unicode="&#xe8b5;" glyph-name="time" d="M534 640.667v-224l192-114-32-54-224 136v256h64zM512 84.667c188 0 342 154 342 342s-154 342-342 342-342-154-342-342 154-342 342-342zM512 852.667c236 0 426-190 426-426s-190-426-426-426-426 190-426 426 190 426 426 426z" />
<glyph unicode="&#xe900;" glyph-name="logo" d="M512.34 939.52c-0.174-0.065-226.41-96.5-283.902-294.74-5.545-19.035 40.453-10.673 38.399-31.6-2.517-25.409-55.264-35.821-48.385-108.78 7.001-74.289 74.617-149.342 84.791-194.72v-31.78c-0.615-9.802-5.639-36.405-22.285-49.4-9.13-7.105-21.442-9.661-37.671-7.78-22.528 2.612-31.493 16.604-35.078 27.9-5.881 18.616-0.409 40.331 12.793 50.52 13.271 10.243 15.084 28.513 4.029 40.82-11.055 12.296-30.785 13.965-44.056 3.7-32.168-24.839-45.65-70.615-32.785-111.34 12.146-38.328 44.789-64.147 87.363-69.080 6.067-0.699 11.848-1.040 17.335-1.040 32.945 0 55.27 11.669 68.785 22.32 40.671 32.105 43.867 85.623 44.099 91.62 0.011 0.355 0.022 0.705 0.022 1.060v24.36h0.129v1.64c0 14.177 12.394 25.66 27.707 25.66 14.869 0 26.889-10.843 27.578-24.46v-232.2c-0.255-3.343-3.155-34.297-22.157-49.28-9.118-7.201-21.512-9.802-37.799-7.9-22.54 2.612-31.526 16.605-35.099 27.88-5.893 18.627-0.387 40.341 12.814 50.52 13.271 10.254 15.062 28.523 4.007 40.84-11.044 12.274-30.764 13.945-44.035 3.68-32.191-24.828-45.65-70.615-32.785-111.34 12.122-38.328 44.789-64.136 87.363-69.080 6.067-0.699 11.848-1.040 17.335-1.040 32.945 0 55.262 11.669 68.742 22.32 40.683 32.105 43.879 85.623 44.099 91.62 0.024 0.376 0.042 0.696 0.042 1.040v259l0.129 0.060v1.14c0 14.456 12.65 26.18 28.264 26.18 15.288 0 27.657-11.292 28.135-25.36v-261.020c0-0.355-0.002-0.675 0.022-1.040 0.232-5.987 3.438-59.515 44.121-91.62 13.504-10.652 35.819-22.32 68.764-22.32 5.499 0 11.258 0.341 17.314 1.040 42.562 4.944 75.24 30.763 87.363 69.080 12.876 40.725-0.584 86.501-32.764 111.34-13.294 10.265-33.013 8.584-44.056-3.68-11.055-12.328-9.264-30.586 4.007-40.84 13.201-10.179 18.697-31.893 12.793-50.52-3.561-11.275-12.55-25.268-35.078-27.88-16.217-1.892-28.531 0.675-37.649 7.78-16.716 13.038-21.715 39.783-22.307 49.36v231.8c0.445 13.816 12.612 24.9 27.642 24.9 15.313 0 27.707-11.472 27.707-25.66v-1.64h0.085v-24.36c0-0.365-0.002-0.716 0.022-1.060 0.22-5.987 3.438-59.515 44.121-91.62 13.503-10.651 35.818-22.32 68.763-22.32 5.487 0 11.259 0.332 17.314 1.020 42.562 4.933 75.24 30.783 87.363 69.1 12.876 40.725-0.606 86.49-32.785 111.34-13.294 10.254-33.003 8.576-44.035-3.72-11.067-12.307-9.285-30.557 3.986-40.8 13.201-10.189 18.719-31.904 12.814-50.52-3.561-11.296-12.571-25.299-35.099-27.9-16.194-1.892-28.51 0.686-37.628 7.78-16.716 13.048-21.727 39.785-22.307 49.34v24.24c6.634 62.066 78.084 123.637 85.499 202.32 6.844 72.959-45.943 83.371-48.449 108.78-2.065 20.927 43.943 12.565 38.421 31.6-57.503 198.24-283.718 294.675-283.88 294.74z" />
<glyph unicode="&#xe901;" glyph-name="plus" d="M810 384.667h-256v-256h-84v256h-256v84h256v256h84v-256h256v-84z" />
<glyph unicode="&#xe905;" glyph-name="pencil" d="M864 960c88.364 0 160-71.634 160-160 0-36.020-11.91-69.258-32-96l-64-64-224 224 64 64c26.742 20.090 59.978 32 96 32zM64 224l-64-288 288 64 592 592-224 224-592-592zM715.578 596.422l-448-448-55.156 55.156 448 448 55.156-55.156z" />

Before

Width:  |  Height:  |  Size: 9.7 KiB

After

Width:  |  Height:  |  Size: 9.9 KiB

BIN
src/Squidex/app/theme/icomoon/fonts/icomoon.ttf

Binary file not shown.

BIN
src/Squidex/app/theme/icomoon/fonts/icomoon.woff

Binary file not shown.

27
src/Squidex/app/theme/icomoon/selection.json

@ -1,6 +1,33 @@
{
"IcoMoonType": "selection",
"icons": [
{
"icon": {
"paths": [
"M534 298v224l192 114-32 54-224-136v-256h64zM512 854c188 0 342-154 342-342s-154-342-342-342-342 154-342 342 154 342 342 342zM512 86c236 0 426 190 426 426s-190 426-426 426-426-190-426-426 190-426 426-426z"
],
"attrs": [],
"isMulticolor": false,
"isMulticolor2": false,
"tags": [
"schedule"
],
"defaultCode": 59573,
"grid": 24
},
"attrs": [],
"properties": {
"order": 17,
"ligatures": "access_time, query_builder, schedule",
"id": 4,
"prevSize": 24,
"code": 59573,
"name": "time"
},
"setIdx": 0,
"setId": 2,
"iconIdx": 4
},
{
"icon": {
"paths": [

13
src/Squidex/app/theme/icomoon/style.css

@ -1,10 +1,10 @@
@font-face {
font-family: 'icomoon';
src: url('fonts/icomoon.eot?sylov9');
src: url('fonts/icomoon.eot?sylov9#iefix') format('embedded-opentype'),
url('fonts/icomoon.ttf?sylov9') format('truetype'),
url('fonts/icomoon.woff?sylov9') format('woff'),
url('fonts/icomoon.svg?sylov9#icomoon') format('svg');
src: url('fonts/icomoon.eot?2yz9dj');
src: url('fonts/icomoon.eot?2yz9dj#iefix') format('embedded-opentype'),
url('fonts/icomoon.ttf?2yz9dj') format('truetype'),
url('fonts/icomoon.woff?2yz9dj') format('woff'),
url('fonts/icomoon.svg?2yz9dj#icomoon') format('svg');
font-weight: normal;
font-style: normal;
}
@ -24,6 +24,9 @@
-moz-osx-font-smoothing: grayscale;
}
.icon-time:before {
content: "\e8b5";
}
.icon-close:before {
content: "\e5cd";
}

1
src/Squidex/app/theme/theme.scss

@ -1,3 +1,4 @@
@import '_bootstrap.scss';
@import '_layout.scss';
@import '_panels.scss';
@import '_history.scss';

10
tests/Squidex.Infrastructure.Tests/CQRS/EnvelopeExtensionsTests.cs

@ -63,14 +63,14 @@ namespace Squidex.Infrastructure.CQRS
}
[Fact]
public void Should_set_and_get_user()
public void Should_set_and_get_actor()
{
var user = new UserToken("subject", "123");
var actor = new RefToken("subject", "123");
sut.SetUser(user);
sut.SetActor(actor);
Assert.Equal(user, sut.Headers.User());
Assert.Equal(user, UserToken.Parse(sut.Headers["User"].ToString()));
Assert.Equal(actor, sut.Headers.Actor());
Assert.Equal(actor, RefToken.Parse(sut.Headers["User"].ToString()));
}
[Fact]

18
tests/Squidex.Infrastructure.Tests/CQRS/Events/EnrichWithUserProcessorTests.cs → tests/Squidex.Infrastructure.Tests/CQRS/Events/EnrichWithActorProcessorTests.cs

@ -12,11 +12,11 @@ using Xunit;
namespace Squidex.Infrastructure.CQRS.Events
{
public class EnrichWithUserProcessorTests
public class EnrichWithActorProcessorTests
{
public sealed class MyUserCommand : IUserCommand
public sealed class MyActorCommand : IActorCommand
{
public UserToken User { get; set; }
public RefToken Actor { get; set; }
}
public sealed class MyNormalCommand : ICommand
@ -27,10 +27,10 @@ namespace Squidex.Infrastructure.CQRS.Events
{
}
private readonly EnrichWithUserProcessor sut = new EnrichWithUserProcessor();
private readonly EnrichWithActorProcessor sut = new EnrichWithActorProcessor();
[Fact]
public async Task Should_not_do_anything_if_not_user_command()
public async Task Should_not_do_anything_if_not_actor_command()
{
var envelope = new Envelope<IEvent>(new MyEvent());
@ -42,14 +42,14 @@ namespace Squidex.Infrastructure.CQRS.Events
[Fact]
public async Task Should_attach_user_to_event_envelope()
{
var user = new UserToken("subject", "123");
var userCommand = new MyUserCommand { User = user };
var actorToken = new RefToken("subject", "123");
var actorCommand = new MyActorCommand { Actor = actorToken };
var envelope = new Envelope<IEvent>(new MyEvent());
await sut.ProcessEventAsync(envelope, null, userCommand);
await sut.ProcessEventAsync(envelope, null, actorCommand);
Assert.Equal(user, envelope.Headers.User());
Assert.Equal(actorToken, envelope.Headers.Actor());
}
}
}

46
tests/Squidex.Infrastructure.Tests/UserTokenTests.cs → tests/Squidex.Infrastructure.Tests/RefTokenTests.cs

@ -1,5 +1,5 @@
// ==========================================================================
// UserTokenTests.cs
// RefTokenTests.cs
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex Group
@ -14,13 +14,13 @@ using Xunit;
namespace Squidex.Infrastructure
{
public class UserTokenTests
public class RefTokenTests
{
private static readonly JsonSerializerSettings serializerSettings = new JsonSerializerSettings();
static UserTokenTests()
static RefTokenTests()
{
serializerSettings.Converters.Add(new UserTokenConverter());
serializerSettings.Converters.Add(new RefTokenConverter());
serializerSettings.NullValueHandling = NullValueHandling.Include;
}
@ -31,13 +31,13 @@ namespace Squidex.Infrastructure
[InlineData("user")]
public void Should_throw_if_parsing_invalid_input(string input)
{
Assert.Throws<ArgumentException>(() => UserToken.Parse(input));
Assert.Throws<ArgumentException>(() => RefToken.Parse(input));
}
[Fact]
public void Should_instantiate_token()
{
var token = new UserToken("client", "client1");
var token = new RefToken("client", "client1");
Assert.Equal("client", token.Type);
Assert.Equal("client1", token.Identifier);
@ -46,7 +46,7 @@ namespace Squidex.Infrastructure
[Fact]
public void Should_instantiate_token_and_lower_type()
{
var token = new UserToken("Client", "client1");
var token = new RefToken("Client", "client1");
Assert.Equal("client", token.Type);
Assert.Equal("client1", token.Identifier);
@ -55,7 +55,7 @@ namespace Squidex.Infrastructure
[Fact]
public void Should_parse_user_token_from_string()
{
var token = UserToken.Parse("client:client1");
var token = RefToken.Parse("client:client1");
Assert.Equal("client", token.Type);
Assert.Equal("client1", token.Identifier);
@ -64,7 +64,7 @@ namespace Squidex.Infrastructure
[Fact]
public void Should_parse_user_token_with_colon_in_identifier()
{
var token = UserToken.Parse("client:client1:app");
var token = RefToken.Parse("client:client1:app");
Assert.Equal("client", token.Type);
Assert.Equal("client1:app", token.Identifier);
@ -73,7 +73,7 @@ namespace Squidex.Infrastructure
[Fact]
public void Should_convert_user_token_to_string()
{
var token = UserToken.Parse("client:client1");
var token = RefToken.Parse("client:client1");
Assert.Equal("client:client1", token.ToString());
}
@ -81,9 +81,9 @@ namespace Squidex.Infrastructure
[Fact]
public void Should_make_correct_equal_comparisons()
{
var token1a = UserToken.Parse("client:client1");
var token1b = UserToken.Parse("client:client1");
var token2 = UserToken.Parse("client:client2");
var token1a = RefToken.Parse("client:client1");
var token1b = RefToken.Parse("client:client1");
var token2 = RefToken.Parse("client:client2");
Assert.True(token1a.Equals(token1b));
@ -93,10 +93,10 @@ namespace Squidex.Infrastructure
[Fact]
public void Should_make_correct_object_equal_comparisons()
{
var token1a = UserToken.Parse("client:client1");
var token1a = RefToken.Parse("client:client1");
object token1b = UserToken.Parse("client:client1");
object token2 = UserToken.Parse("client:client2");
object token1b = RefToken.Parse("client:client1");
object token2 = RefToken.Parse("client:client2");
Assert.True(token1a.Equals(token1b));
@ -106,9 +106,9 @@ namespace Squidex.Infrastructure
[Fact]
public void Should_provide_correct_hash_codes()
{
var token1a = UserToken.Parse("client:client1");
var token1b = UserToken.Parse("client:client1");
var token2 = UserToken.Parse("client:client2");
var token1a = RefToken.Parse("client:client1");
var token1b = RefToken.Parse("client:client1");
var token2 = RefToken.Parse("client:client2");
Assert.Equal(token1a.GetHashCode(), token1b.GetHashCode());
@ -118,9 +118,9 @@ namespace Squidex.Infrastructure
[Fact]
public void Should_serialize_and_deserialize_null_token()
{
var input = Tuple.Create<UserToken>(null);
var input = Tuple.Create<RefToken>(null);
var json = JsonConvert.SerializeObject(input, serializerSettings);
var output = JsonConvert.DeserializeObject<Tuple<UserToken>>(json, serializerSettings);
var output = JsonConvert.DeserializeObject<Tuple<RefToken>>(json, serializerSettings);
Assert.Equal(output.Item1, input.Item1);
}
@ -128,9 +128,9 @@ namespace Squidex.Infrastructure
[Fact]
public void Should_serialize_and_deserialize_valid_token()
{
var input = Tuple.Create(UserToken.Parse("client:client1"));
var input = Tuple.Create(RefToken.Parse("client:client1"));
var json = JsonConvert.SerializeObject(input, serializerSettings);
var output = JsonConvert.DeserializeObject<Tuple<UserToken>>(json, serializerSettings);
var output = JsonConvert.DeserializeObject<Tuple<RefToken>>(json, serializerSettings);
Assert.Equal(output.Item1, input.Item1);
}

8
tests/Squidex.Write.Tests/Apps/AppCommandHandlerTests.cs

@ -32,7 +32,7 @@ namespace Squidex.Write.Apps
private readonly Mock<IUserRepository> userRepository = new Mock<IUserRepository>();
private readonly AppCommandHandler sut;
private readonly AppDomainObject app;
private readonly UserToken subjectId = new UserToken("subject", Guid.NewGuid().ToString());
private readonly RefToken subjectId = new RefToken("subject", Guid.NewGuid().ToString());
private readonly DateTime expiresUtc = DateTime.UtcNow.AddYears(1);
private readonly Language language = Language.GetLanguage("de");
private readonly string contributorId = Guid.NewGuid().ToString();
@ -50,7 +50,7 @@ namespace Squidex.Write.Apps
[Fact]
public async Task Create_should_throw_if_a_name_with_same_name_already_exists()
{
var command = new CreateApp { Name = appName, AggregateId = Id, User = subjectId };
var command = new CreateApp { Name = appName, AggregateId = Id, Actor = subjectId };
var context = new CommandContext(command);
appRepository.Setup(x => x.FindAppByNameAsync(appName)).Returns(Task.FromResult(new Mock<IAppEntity>().Object)).Verifiable();
@ -66,7 +66,7 @@ namespace Squidex.Write.Apps
[Fact]
public async Task Create_should_create_app_if_name_is_free()
{
var command = new CreateApp { Name = appName, AggregateId = Id, User = subjectId };
var command = new CreateApp { Name = appName, AggregateId = Id, Actor = subjectId };
var context = new CommandContext(command);
appRepository.Setup(x => x.FindAppByNameAsync(appName)).Returns(Task.FromResult<IAppEntity>(null)).Verifiable();
@ -241,7 +241,7 @@ namespace Squidex.Write.Apps
private AppDomainObject CreateApp()
{
app.Create(new CreateApp { Name = appName, User = subjectId });
app.Create(new CreateApp { Name = appName, Actor = subjectId });
return app;
}

6
tests/Squidex.Write.Tests/Apps/AppDomainObjectTests.cs

@ -25,7 +25,7 @@ namespace Squidex.Write.Apps
{
private const string TestName = "app";
private readonly AppDomainObject sut;
private readonly UserToken user = new UserToken("subject", Guid.NewGuid().ToString());
private readonly RefToken user = new RefToken("subject", Guid.NewGuid().ToString());
private readonly DateTime expiresUtc = DateTime.UtcNow.AddYears(1);
private readonly string contributorId = Guid.NewGuid().ToString();
private readonly string clientSecret = Guid.NewGuid().ToString();
@ -54,7 +54,7 @@ namespace Squidex.Write.Apps
[Fact]
public void Create_should_specify_name_and_owner()
{
sut.Create(new CreateApp { Name = TestName, User = user });
sut.Create(new CreateApp { Name = TestName, Actor = user });
Assert.Equal(TestName, sut.Name);
@ -410,7 +410,7 @@ namespace Squidex.Write.Apps
private void CreateApp()
{
sut.Create(new CreateApp { Name = TestName, User = user });
sut.Create(new CreateApp { Name = TestName, Actor = user });
((IAggregate)sut).ClearUncommittedEvents();
}

Loading…
Cancel
Save