Browse Source

Merge pull request #95 from SebastianStehle/feature-lock-fields

Locking fields
pull/100/head
Sebastian Stehle 8 years ago
committed by GitHub
parent
commit
6ca82f9426
  1. 38
      src/Squidex.Domain.Apps.Core/Schemas/Field.cs
  2. 2
      src/Squidex.Domain.Apps.Core/Schemas/Field_Generic.cs
  3. 3
      src/Squidex.Domain.Apps.Core/Schemas/Json/JsonFieldModel.cs
  4. 6
      src/Squidex.Domain.Apps.Core/Schemas/Json/SchemaJsonSerializer.cs
  5. 10
      src/Squidex.Domain.Apps.Core/Schemas/Schema.cs
  6. 17
      src/Squidex.Domain.Apps.Events/Schemas/FieldLocked.cs
  7. 2
      src/Squidex.Domain.Apps.Events/Schemas/SchemaCreatedField.cs
  8. 10
      src/Squidex.Domain.Apps.Events/Schemas/Utils/SchemaEventDispatcher.cs
  9. 13
      src/Squidex.Domain.Apps.Read.MongoDb/Schemas/MongoSchemaRepository_EventHandling.cs
  10. 13
      src/Squidex.Domain.Apps.Read/Schemas/SchemaHistoryEventsCreator.cs
  11. 2
      src/Squidex.Domain.Apps.Write/Schemas/Commands/CreateSchemaField.cs
  12. 14
      src/Squidex.Domain.Apps.Write/Schemas/Commands/LockField.cs
  13. 19
      src/Squidex.Domain.Apps.Write/Schemas/SchemaCommandMiddleware.cs
  14. 16
      src/Squidex.Domain.Apps.Write/Schemas/SchemaDomainObject.cs
  15. 5
      src/Squidex/Controllers/Api/Schemas/Models/CreateSchemaFieldDto.cs
  16. 5
      src/Squidex/Controllers/Api/Schemas/Models/FieldDto.cs
  17. 34
      src/Squidex/Controllers/Api/Schemas/SchemaFieldsController.cs
  18. 48
      src/Squidex/app/features/schemas/pages/schema/field.component.html
  19. 11
      src/Squidex/app/features/schemas/pages/schema/field.component.scss
  20. 26
      src/Squidex/app/features/schemas/pages/schema/field.component.ts
  21. 1
      src/Squidex/app/features/schemas/pages/schema/schema-page.component.html
  22. 6
      src/Squidex/app/features/schemas/pages/schema/schema-page.component.ts
  23. 4
      src/Squidex/app/features/webhooks/pages/webhook-events-page.component.html
  24. 30
      src/Squidex/app/features/webhooks/pages/webhook-events-page.component.scss
  25. 12
      src/Squidex/app/features/webhooks/pages/webhook-events-page.component.ts
  26. 9
      src/Squidex/app/shared/services/schemas.fields.spec.ts
  27. 61
      src/Squidex/app/shared/services/schemas.service.spec.ts
  28. 24
      src/Squidex/app/shared/services/schemas.service.ts
  29. 2
      src/Squidex/app/shell/pages/internal/apps-menu.component.html
  30. 8
      src/Squidex/app/shell/pages/internal/apps-menu.component.scss
  31. 9
      src/Squidex/app/theme/_bootstrap-vars.scss
  32. 48
      src/Squidex/app/theme/_bootstrap.scss
  33. 22
      src/Squidex/app/theme/_vars.scss
  34. 3
      tests/Squidex.Domain.Apps.Core.Tests/Schemas/Json/JsonSerializerTests.cs
  35. 46
      tests/Squidex.Domain.Apps.Core.Tests/Schemas/SchemaTests.cs
  36. 14
      tests/Squidex.Domain.Apps.Write.Tests/Schemas/SchemaCommandHandlerTests.cs
  37. 48
      tests/Squidex.Domain.Apps.Write.Tests/Schemas/SchemaDomainObjectTests.cs

38
src/Squidex.Domain.Apps.Core/Schemas/Field.cs

@ -28,6 +28,7 @@ namespace Squidex.Domain.Apps.Core.Schemas
private string fieldName;
private bool isDisabled;
private bool isHidden;
private bool isLocked;
public long Id
{
@ -39,6 +40,11 @@ namespace Squidex.Domain.Apps.Core.Schemas
get { return fieldName; }
}
public bool IsLocked
{
get { return isLocked; }
}
public bool IsHidden
{
get { return isHidden; }
@ -75,10 +81,15 @@ namespace Squidex.Domain.Apps.Core.Schemas
validators = new Lazy<List<IValidator>>(() => new List<IValidator>(CreateValidators()));
}
public abstract Field Update(FieldProperties newProperties);
protected abstract Field UpdateInternal(FieldProperties newProperties);
public abstract object ConvertValue(JToken value);
public Field Lock()
{
return Clone<Field>(clone => clone.isLocked = true);
}
public Field Hide()
{
return Clone<Field>(clone => clone.isHidden = true);
@ -99,7 +110,30 @@ namespace Squidex.Domain.Apps.Core.Schemas
return Clone<Field>(clone => clone.isDisabled = false);
}
public Field Update(FieldProperties newProperties)
{
ThrowIfLocked();
return UpdateInternal(newProperties);
}
public Field Rename(string newName)
{
ThrowIfLocked();
ThrowIfSameName(newName);
return Clone<Field>(clone => clone.fieldName = newName);
}
private void ThrowIfLocked()
{
if (isLocked)
{
throw new DomainException($"Field {fieldId} is locked.");
}
}
private void ThrowIfSameName(string newName)
{
if (!newName.IsSlug())
{
@ -107,8 +141,6 @@ namespace Squidex.Domain.Apps.Core.Schemas
throw new ValidationException($"Cannot rename the field '{fieldName}' ({fieldId})", error);
}
return Clone<Field>(clone => clone.fieldName = newName);
}
public void AddToEdmType(EdmStructuredType edmType, PartitionResolver partitionResolver, string schemaName, Func<EdmComplexType, EdmComplexType> typeResolver)

2
src/Squidex.Domain.Apps.Core/Schemas/Field_Generic.cs

@ -33,7 +33,7 @@ namespace Squidex.Domain.Apps.Core.Schemas
this.properties = ValidateProperties(properties);
}
public override Field Update(FieldProperties newProperties)
protected override Field UpdateInternal(FieldProperties newProperties)
{
var typedProperties = ValidateProperties(newProperties);

3
src/Squidex.Domain.Apps.Core/Schemas/Json/JsonFieldModel.cs

@ -18,6 +18,9 @@ namespace Squidex.Domain.Apps.Core.Schemas.Json
[JsonProperty]
public bool IsHidden { get; set; }
[JsonProperty]
public bool IsLocked { get; set; }
[JsonProperty]
public bool IsDisabled { get; set; }

6
src/Squidex.Domain.Apps.Core/Schemas/Json/SchemaJsonSerializer.cs

@ -42,6 +42,7 @@ namespace Squidex.Domain.Apps.Core.Schemas.Json
Id = x.Id,
Name = x.Name,
IsHidden = x.IsHidden,
IsLocked = x.IsLocked,
IsDisabled = x.IsDisabled,
Partitioning = x.Paritioning.Key,
Properties = x.RawProperties
@ -66,6 +67,11 @@ namespace Squidex.Domain.Apps.Core.Schemas.Json
field = field.Disable();
}
if (fieldModel.IsLocked)
{
field = field.Lock();
}
if (fieldModel.IsHidden)
{
field = field.Hide();

10
src/Squidex.Domain.Apps.Core/Schemas/Schema.cs

@ -101,6 +101,11 @@ namespace Squidex.Domain.Apps.Core.Schemas
return UpdateField(fieldId, field => field.Update(newProperties));
}
public Schema LockField(long fieldId)
{
return UpdateField(fieldId, field => field.Lock());
}
public Schema DisableField(long fieldId)
{
return UpdateField(fieldId, field => field.Disable());
@ -128,6 +133,11 @@ namespace Squidex.Domain.Apps.Core.Schemas
public Schema DeleteField(long fieldId)
{
if (fieldsById.TryGetValue(fieldId, out var field) && field.IsLocked)
{
throw new DomainException($"Field {fieldId} is locked.");
}
return new Schema(name, isPublished, properties, fields.Where(x => x.Id != fieldId).ToImmutableList());
}

17
src/Squidex.Domain.Apps.Events/Schemas/FieldLocked.cs

@ -0,0 +1,17 @@
// ==========================================================================
// FieldLocked.cs
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex Group
// All rights reserved.
// ==========================================================================
using Squidex.Infrastructure;
namespace Squidex.Domain.Apps.Events.Schemas
{
[TypeName("FieldLockedEvent")]
public class FieldLocked : FieldEvent
{
}
}

2
src/Squidex.Domain.Apps.Events/Schemas/SchemaCreatedField.cs

@ -18,6 +18,8 @@ namespace Squidex.Domain.Apps.Events.Schemas
public bool IsHidden { get; set; }
public bool IsLocked { get; set; }
public bool IsDisabled { get; set; }
public FieldProperties Properties { get; set; }

10
src/Squidex.Domain.Apps.Events/Schemas/Utils/SchemaEventDispatcher.cs

@ -44,6 +44,11 @@ namespace Squidex.Domain.Apps.Events.Schemas.Utils
field = field.Disable();
}
if (eventField.IsLocked)
{
field = field.Lock();
}
schema = schema.AddOrUpdateField(field);
fieldId++;
@ -68,6 +73,11 @@ namespace Squidex.Domain.Apps.Events.Schemas.Utils
return schema.UpdateField(@event.FieldId.Id, @event.Properties);
}
public static Schema Dispatch(FieldLocked @event, Schema schema)
{
return schema.LockField(@event.FieldId.Id);
}
public static Schema Dispatch(FieldHidden @event, Schema schema)
{
return schema.HideField(@event.FieldId.Id);

13
src/Squidex.Domain.Apps.Read.MongoDb/Schemas/MongoSchemaRepository_EventHandling.cs

@ -48,22 +48,27 @@ namespace Squidex.Domain.Apps.Read.MongoDb.Schemas
return UpdateSchema(@event, headers, s => SchemaEventDispatcher.Dispatch(@event, s));
}
protected Task On(FieldDisabled @event, EnvelopeHeaders headers)
protected Task On(FieldLocked @event, EnvelopeHeaders headers)
{
return UpdateSchema(@event, headers, s => SchemaEventDispatcher.Dispatch(@event, s));
}
protected Task On(FieldEnabled @event, EnvelopeHeaders headers)
protected Task On(FieldHidden @event, EnvelopeHeaders headers)
{
return UpdateSchema(@event, headers, s => SchemaEventDispatcher.Dispatch(@event, s));
}
protected Task On(FieldHidden @event, EnvelopeHeaders headers)
protected Task On(FieldShown @event, EnvelopeHeaders headers)
{
return UpdateSchema(@event, headers, s => SchemaEventDispatcher.Dispatch(@event, s));
}
protected Task On(FieldShown @event, EnvelopeHeaders headers)
protected Task On(FieldDisabled @event, EnvelopeHeaders headers)
{
return UpdateSchema(@event, headers, s => SchemaEventDispatcher.Dispatch(@event, s));
}
protected Task On(FieldEnabled @event, EnvelopeHeaders headers)
{
return UpdateSchema(@event, headers, s => SchemaEventDispatcher.Dispatch(@event, s));
}

13
src/Squidex.Domain.Apps.Read/Schemas/SchemaHistoryEventsCreator.cs

@ -46,11 +46,8 @@ namespace Squidex.Domain.Apps.Read.Schemas
AddEventMessage<FieldDeleted>(
"deleted field {[Field]} from schema {[Name]}");
AddEventMessage<FieldDisabled>(
"disabled field {[Field]} of schema {[Name]}");
AddEventMessage<FieldEnabled>(
"disabled field {[Field]} of schema {[Name]}");
AddEventMessage<FieldLocked>(
"has locked field {[Field]} of schema {[Name]}");
AddEventMessage<FieldHidden>(
"has hidden field {[Field]} of schema {[Name]}");
@ -58,6 +55,12 @@ namespace Squidex.Domain.Apps.Read.Schemas
AddEventMessage<FieldShown>(
"has shown field {[Field]} of schema {[Name]}");
AddEventMessage<FieldDisabled>(
"disabled field {[Field]} of schema {[Name]}");
AddEventMessage<FieldEnabled>(
"disabled field {[Field]} of schema {[Name]}");
AddEventMessage<FieldUpdated>(
"has updated field {[Field]} of schema {[Name]}");

2
src/Squidex.Domain.Apps.Write/Schemas/Commands/CreateSchemaField.cs

@ -21,6 +21,8 @@ namespace Squidex.Domain.Apps.Write.Schemas.Commands
public bool IsHidden { get; set; }
public bool IsLocked { get; set; }
public bool IsDisabled { get; set; }
public FieldProperties Properties { get; set; }

14
src/Squidex.Domain.Apps.Write/Schemas/Commands/LockField.cs

@ -0,0 +1,14 @@
// ==========================================================================
// LockField.cs
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex Group
// All rights reserved.
// ==========================================================================
namespace Squidex.Domain.Apps.Write.Schemas.Commands
{
public class LockField : FieldCommand
{
}
}

19
src/Squidex.Domain.Apps.Write/Schemas/SchemaCommandMiddleware.cs

@ -80,14 +80,9 @@ namespace Squidex.Domain.Apps.Write.Schemas
return handler.UpdateAsync<SchemaDomainObject>(context, s => s.DeleteField(command));
}
protected Task On(DisableField command, CommandContext context)
protected Task On(LockField command, CommandContext context)
{
return handler.UpdateAsync<SchemaDomainObject>(context, s => s.DisableField(command));
}
protected Task On(EnableField command, CommandContext context)
{
return handler.UpdateAsync<SchemaDomainObject>(context, s => s.EnableField(command));
return handler.UpdateAsync<SchemaDomainObject>(context, s => s.LockField(command));
}
protected Task On(HideField command, CommandContext context)
@ -100,6 +95,16 @@ namespace Squidex.Domain.Apps.Write.Schemas
return handler.UpdateAsync<SchemaDomainObject>(context, s => s.ShowField(command));
}
protected Task On(DisableField command, CommandContext context)
{
return handler.UpdateAsync<SchemaDomainObject>(context, s => s.DisableField(command));
}
protected Task On(EnableField command, CommandContext context)
{
return handler.UpdateAsync<SchemaDomainObject>(context, s => s.EnableField(command));
}
protected Task On(ReorderFields command, CommandContext context)
{
return handler.UpdateAsync<SchemaDomainObject>(context, s => s.Reorder(command));

16
src/Squidex.Domain.Apps.Write/Schemas/SchemaDomainObject.cs

@ -65,6 +65,11 @@ namespace Squidex.Domain.Apps.Write.Schemas
schema = SchemaEventDispatcher.Dispatch(@event, schema);
}
protected void On(FieldLocked @event)
{
schema = SchemaEventDispatcher.Dispatch(@event, schema);
}
protected void On(FieldHidden @event)
{
schema = SchemaEventDispatcher.Dispatch(@event, schema);
@ -217,6 +222,17 @@ namespace Squidex.Domain.Apps.Write.Schemas
return this;
}
public SchemaDomainObject LockField(LockField command)
{
Guard.NotNull(command, nameof(command));
VerifyCreatedAndNotDeleted();
RaiseEvent(command, new FieldLocked());
return this;
}
public SchemaDomainObject HideField(HideField command)
{
Guard.NotNull(command, nameof(command));

5
src/Squidex/Controllers/Api/Schemas/Models/CreateSchemaFieldDto.cs

@ -24,6 +24,11 @@ namespace Squidex.Controllers.Api.Schemas.Models
/// </summary>
public bool IsHidden { get; set; }
/// <summary>
/// Defines if the field is locked.
/// </summary>
public bool IsLocked { get; set; }
/// <summary>
/// Defines if the field is disabled.
/// </summary>

5
src/Squidex/Controllers/Api/Schemas/Models/FieldDto.cs

@ -29,6 +29,11 @@ namespace Squidex.Controllers.Api.Schemas.Models
/// </summary>
public bool IsHidden { get; set; }
/// <summary>
/// Defines if the field is locked.
/// </summary>
public bool IsLocked { get; set; }
/// <summary>
/// Defines if the field is disabled.
/// </summary>

34
src/Squidex/Controllers/Api/Schemas/SchemaFieldsController.cs

@ -98,7 +98,7 @@ namespace Squidex.Controllers.Api.Schemas
/// <param name="request">The field object that needs to be added to the schema.</param>
/// <returns>
/// 204 => Schema field updated.
/// 400 => Schema field properties not valid.
/// 400 => Schema field properties not valid or field is locked.
/// 404 => Schema, field or app not found.
/// </returns>
[HttpPut]
@ -115,6 +115,33 @@ namespace Squidex.Controllers.Api.Schemas
return NoContent();
}
/// <summary>
/// Lock a schema field.
/// </summary>
/// <param name="app">The name of the app.</param>
/// <param name="name">The name of the schema.</param>
/// <param name="id">The id of the field to lock.</param>
/// <returns>
/// 204 => Schema field shown.
/// 400 => Schema field already locked.
/// 404 => Schema, field or app not found.
/// </returns>
/// <remarks>
/// A hidden field is not part of the API response, but can still be edited in the portal.
/// </remarks>
[HttpPut]
[Route("apps/{app}/schemas/{name}/fields/{id:long}/lock/")]
[ProducesResponseType(typeof(ErrorDto), 400)]
[ApiCosts(1)]
public async Task<IActionResult> LockField(string app, string name, long id)
{
var command = new LockField { FieldId = id };
await CommandBus.PublishAsync(command);
return NoContent();
}
/// <summary>
/// Hide a schema field.
/// </summary>
@ -127,7 +154,7 @@ namespace Squidex.Controllers.Api.Schemas
/// 404 => Schema, field or app not found.
/// </returns>
/// <remarks>
/// A hidden field is not part of the API response, but can still be edited in the portal.
/// A locked field cannot be edited or deleted.
/// </remarks>
[HttpPut]
[Route("apps/{app}/schemas/{name}/fields/{id:long}/hide/")]
@ -147,7 +174,7 @@ namespace Squidex.Controllers.Api.Schemas
/// </summary>
/// <param name="app">The name of the app.</param>
/// <param name="name">The name of the schema.</param>
/// <param name="id">The id of the field to shows.</param>
/// <param name="id">The id of the field to show.</param>
/// <returns>
/// 204 => Schema field shown.
/// 400 => Schema field already visible.
@ -233,6 +260,7 @@ namespace Squidex.Controllers.Api.Schemas
/// <param name="id">The id of the field to disable.</param>
/// <returns>
/// 204 => Schema field deleted.
/// 400 => Field is locked.
/// 404 => Schema, field or app not found.
/// </returns>
[HttpDelete]

48
src/Squidex/app/features/schemas/pages/schema/field.component.html

@ -9,13 +9,14 @@
<span class="field-partitioning" *ngIf="field.partitioning === 'language'">localizable</span>
</span>
</div>
<div class="col col-3">
<div class="col col-tags">
<div class="float-right">
<span class="tag tag-success" *ngIf="!field.isDisabled">Enabled</span>
<span class="tag tag-danger" *ngIf="field.isDisabled">Disabled</span>
<span class="badge badge-pill badge-danger" *ngIf="field.isLocked">Locked</span>
<span class="badge badge-pill badge-success" *ngIf="!field.isDisabled">Enabled</span>
<span class="badge badge-pill badge-danger" *ngIf="field.isDisabled">Disabled</span>
</div>
</div>
<div class="col col-3">
<div class="col col-options">
<div class="float-right">
<button type="button" class="btn btn-secondary table-items-edit-button" [class.active]="isEditing" (click)="toggleEditing()">
<i class="icon-settings2"></i>
@ -26,19 +27,22 @@
<i class="icon-dots"></i>
</button>
<div class="dropdown-menu" *sqxModalView="dropdown" closeAlways="true" [sqxModalTarget]="optionsButton" position="right" [@fade]>
<a class="dropdown-item" (click)="enabling.emit()" *ngIf="field.isDisabled">
<a class="dropdown-item" (click)="askLock()" *ngIf="!field.isLocked">
Lock
</a>
<a class="dropdown-item" (click)="enabling.emit()" *ngIf="field.isDisabled" [class.disabled]="field.isLocked">
Enable
</a>
<a class="dropdown-item" (click)="disabling.emit()" *ngIf="!field.isDisabled">
<a class="dropdown-item" (click)="disabling.emit()" *ngIf="!field.isDisabled" [class.disabled]="field.isLocked">
Disable
</a>
<a class="dropdown-item" (click)="hiding.emit()" *ngIf="!field.isHidden">
<a class="dropdown-item" (click)="hiding.emit()" *ngIf="!field.isHidden" [class.disabled]="field.isLocked">
Hide
</a>
<a class="dropdown-item" (click)="showing.emit()" *ngIf="field.isHidden">
<a class="dropdown-item" (click)="showing.emit()" *ngIf="field.isHidden" [class.disabled]="field.isLocked">
Show
</a>
<a class="dropdown-item dropdown-item-delete" (click)="deleting.emit()">
<a class="dropdown-item dropdown-item-delete" (click)="deleting.emit()" [class.disabled]="field.isLocked">
Delete
</a>
</div>
@ -49,7 +53,7 @@
</div>
<div class="table-items-row-details" *ngIf="isEditing" (dragstart)="alert('ff');">
<form [formGroup]="editForm" (ngSubmit)="save()">
<form [formGroup]="editForm" (ngSubmit)="save()" [attr.disabled]="field.isLocked">
<div class="table-items-row-details-tabs clearfix">
<ul class="nav nav-inline nav-field-tabs">
<li class="nav-item">
@ -64,8 +68,8 @@
</ul>
<div class="float-right">
<button type="reset" class="btn btn-link" (click)="cancel()" [disabled]="editFormSubmitted">Cancel</button>
<button type="submit" class="btn btn-primary">Save</button>
<button [disabled]="editFormSubmitted || field.isLocked" type="reset" class="btn btn-link" (click)="cancel()">Cancel</button>
<button [disabled]="editFormSubmitted || field.isLocked" type="submit" class="btn btn-primary">Save</button>
</div>
</div>
@ -183,3 +187,23 @@
</form>
</div>
</div>
<div class="modal" *sqxModalView="lockDialog;onRoot:true" [@fade]>
<div class="modal-backdrop"></div>
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<h4 class="modal-title">Lock Field</h4>
</div>
<div class="modal-body">
Do you really want to lock the field? Lock fields cannot be changed and deleted.
</div>
<div class="modal-footer">
<button type="button" class="btn btn-danger" (click)="confirmLock()">Yes</button>
<button type="button" class="btn btn-secondary" (click)="cancelLock()">No</button>
</div>
</div>
</div>
</div>

11
src/Squidex/app/features/schemas/pages/schema/field.component.scss

@ -7,6 +7,17 @@ $field-header: #e7ebef;
cursor: default;
}
.col {
&-tags,
&-options {
white-space: nowrap;
}
&-options {
max-width: 140px;
}
}
.field {
&-icon {
margin-right: 1rem;

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

@ -31,6 +31,9 @@ export class FieldComponent implements OnInit {
@Input()
public schemas: SchemaDto[];
@Output()
public locking = new EventEmitter<FieldDto>();
@Output()
public hiding = new EventEmitter<FieldDto>();
@ -50,6 +53,7 @@ export class FieldComponent implements OnInit {
public deleting = new EventEmitter<FieldDto>();
public dropdown = new ModalView(false, true);
public lockDialog = new ModalView(false, true);
public isEditing = false;
public selectedTab = 0;
@ -104,6 +108,7 @@ export class FieldComponent implements OnInit {
new FieldDto(
this.field.fieldId,
this.field.name,
this.field.isLocked,
this.field.isHidden,
this.field.isHidden,
this.field.partitioning,
@ -113,6 +118,23 @@ export class FieldComponent implements OnInit {
}
}
public confirmLock() {
this.lockDialog.hide();
this.emitLocking(this.field);
}
public cancelLock() {
this.lockDialog.hide();
}
public askLock() {
this.lockDialog.show();
}
private emitLocking(field: FieldDto) {
this.locking.emit(field);
}
private emitSaving(field: FieldDto) {
this.saving.emit(field);
}
@ -121,6 +143,10 @@ export class FieldComponent implements OnInit {
this.editFormSubmitted = false;
this.editForm.reset(this.field.properties);
if (this.field.isLocked) {
this.editForm.disable();
}
this.isEditing = false;
}
}

1
src/Squidex/app/features/schemas/pages/schema/schema-page.component.html

@ -46,6 +46,7 @@
(disabling)="disableField(field)"
(deleting)="deleteField(field)"
(enabling)="enableField(field)"
(locking)="lockField(field)"
(showing)="showField(field)"
(hiding)="hideField(field)"
(saving)="saveField($event)"></sqx-field>

6
src/Squidex/app/features/schemas/pages/schema/schema-page.component.ts

@ -145,11 +145,11 @@ export class SchemaPageComponent extends AppComponentBase implements OnInit {
});
}
public showField(field: FieldDto) {
public lockField(field: FieldDto) {
this.appNameOnce()
.switchMap(app => this.schemasService.showField(app, this.schema.name, field.fieldId, this.schema.version)).retry(2)
.switchMap(app => this.schemasService.lockField(app, this.schema.name, field.fieldId, this.schema.version)).retry(2)
.subscribe(() => {
this.updateSchema(this.schema.updateField(field.show(), this.authService.user.token));
this.updateSchema(this.schema.updateField(field.lock(), this.authService.user.token));
}, error => {
this.notifyError(error);
});

4
src/Squidex/app/features/webhooks/pages/webhook-events-page.component.html

@ -51,7 +51,7 @@
<ng-template ngFor let-event [ngForOf]="eventsItems">
<tr [class.expanded]="selectedEventId === event.id">
<td>
<span class="badge badge-{{event.jobResult | lowercase}}">{{event.jobResult}}</span>
<span class="badge badge-pill badge-{{getBadgeClass(event.jobResult)}}">{{event.jobResult}}</span>
</td>
<td>
<span class="truncate">{{event.eventName}}</span>
@ -76,7 +76,7 @@
<div class="row event-stats">
<div class="col-3">
<span class="badge badge-small badge-{{event.result | lowercase}}">{{event.result}}</span>
<span class="badge badge-pill badge-{{getBadgeClass(event.result)}}">{{event.result}}</span>
</div>
<div class="col-3">
Attempts: {{event.numCalls}}

30
src/Squidex/app/features/webhooks/pages/webhook-events-page.component.scss

@ -5,36 +5,6 @@ h3 {
margin-bottom: 1rem;
}
.badge {
& {
@include border-radius(12px);
font-size: .9rem;
font-weight: normal;
line-height: 1.5rem;
padding: 0 .5rem;
}
&-small {
font-size: .8rem;
}
&-pending {
background: $color-border;
}
&-success {
background: $color-badge-success;
}
&-retry {
background: $color-badge-warning;
}
&-failed {
background: $color-badge-error;
}
}
.event {
&-stats {
font-size: .8rem;

12
src/Squidex/app/features/webhooks/pages/webhook-events-page.component.ts

@ -78,5 +78,17 @@ export class WebhookEventsPageComponent extends AppComponentBase implements OnIn
this.load();
}
public getBadgeClass(status: string) {
if (status === 'Retry') {
return 'warning';
} else if (status === 'Failed') {
return 'danger';
} else if (status === 'Pending') {
return 'default';
} else {
return status.toLowerCase();
}
}
}

9
src/Squidex/app/shared/services/schemas.fields.spec.ts

@ -20,6 +20,13 @@ import {
} from './../';
describe('FieldDto', () => {
it('should update isLocked property when locking', () => {
const field_1 = createField(createProperties('String'));
const field_2 = field_1.lock();
expect(field_2.isLocked).toBeTruthy();
});
it('should update isHidden property when hiding', () => {
const field_1 = createField(createProperties('String'));
const field_2 = field_1.hide();
@ -211,5 +218,5 @@ describe('NumberField', () => {
});
function createField(properties: FieldPropertiesDto) {
return new FieldDto(1, 'field1', false, false, 'languages', properties);
return new FieldDto(1, 'field1', false, false, false, 'languages', properties);
}

61
src/Squidex/app/shared/services/schemas.service.spec.ts

@ -103,8 +103,8 @@ describe('SchemaDetailsDto', () => {
});
it('should update fields property and user info when adding field', () => {
const field1 = new FieldDto(1, '1', false, false, 'l', createProperties('String'));
const field2 = new FieldDto(2, '2', false, false, 'l', createProperties('Number'));
const field1 = new FieldDto(1, '1', false, false, false, 'l', createProperties('String'));
const field2 = new FieldDto(2, '2', false, false, false, 'l', createProperties('Number'));
const now = DateTime.now();
@ -117,8 +117,8 @@ describe('SchemaDetailsDto', () => {
});
it('should update fields property and user info when removing field', () => {
const field1 = new FieldDto(1, '1', false, false, 'l', createProperties('String'));
const field2 = new FieldDto(2, '2', false, false, 'l', createProperties('Number'));
const field1 = new FieldDto(1, '1', false, false, false, 'l', createProperties('String'));
const field2 = new FieldDto(2, '2',false, false, false, 'l', createProperties('Number'));
const now = DateTime.now();
@ -131,8 +131,8 @@ describe('SchemaDetailsDto', () => {
});
it('should update fields property and user info when replacing fields', () => {
const field1 = new FieldDto(1, '1', false, false, 'l', createProperties('String'));
const field2 = new FieldDto(2, '2', false, false, 'l', createProperties('Number'));
const field1 = new FieldDto(1, '1', false, false, false, 'l', createProperties('String'));
const field2 = new FieldDto(2, '2', false, false, false, 'l', createProperties('Number'));
const now = DateTime.now();
@ -145,16 +145,16 @@ describe('SchemaDetailsDto', () => {
});
it('should update fields property and user info when updating field', () => {
const field1 = new FieldDto(1, '1', false, false, 'l', createProperties('String'));
const field2_1 = new FieldDto(2, '2', false, false, 'l', createProperties('Number'));
const field2_2 = new FieldDto(2, '2', false, false, 'l', createProperties('Boolean'));
const field1_0 = new FieldDto(1, '1', false, false, false, 'l', createProperties('String'));
const field2_1 = new FieldDto(2, '2', false, false, false, 'l', createProperties('Number'));
const field2_2 = new FieldDto(2, '2', false, false, false, 'l', createProperties('Boolean'));
const now = DateTime.now();
const schema_1 = new SchemaDetailsDto('1', 'name', properties, false, 'other', 'other', DateTime.now(), DateTime.now(), null, [field1, field2_1]);
const schema_1 = new SchemaDetailsDto('1', 'name', properties, false, 'other', 'other', DateTime.now(), DateTime.now(), null, [field1_0, field2_1]);
const schema_2 = schema_1.updateField(field2_2, 'me', now);
expect(schema_2.fields).toEqual([field1, field2_2]);
expect(schema_2.fields).toEqual([field1_0, field2_2]);
expect(schema_2.lastModified).toEqual(now);
expect(schema_2.lastModifiedBy).toEqual('me');
});
@ -276,6 +276,7 @@ describe('SchemasService', () => {
{
fieldId: 1,
name: 'field1',
isLocked: true,
isHidden: true,
isDisabled: true,
partitioning: 'language',
@ -286,6 +287,7 @@ describe('SchemasService', () => {
{
fieldId: 2,
name: 'field2',
isLocked: true,
isHidden: true,
isDisabled: true,
partitioning: 'language',
@ -296,6 +298,7 @@ describe('SchemasService', () => {
{
fieldId: 3,
name: 'field3',
isLocked: true,
isHidden: true,
isDisabled: true,
partitioning: 'language',
@ -306,6 +309,7 @@ describe('SchemasService', () => {
{
fieldId: 4,
name: 'field4',
isLocked: true,
isHidden: true,
isDisabled: true,
partitioning: 'language',
@ -316,6 +320,7 @@ describe('SchemasService', () => {
{
fieldId: 5,
name: 'field5',
isLocked: true,
isHidden: true,
isDisabled: true,
partitioning: 'language',
@ -326,6 +331,7 @@ describe('SchemasService', () => {
{
fieldId: 6,
name: 'field6',
isLocked: true,
isHidden: true,
isDisabled: true,
partitioning: 'language',
@ -336,6 +342,7 @@ describe('SchemasService', () => {
{
fieldId: 7,
name: 'field7',
isLocked: true,
isHidden: true,
isDisabled: true,
partitioning: 'language',
@ -346,6 +353,7 @@ describe('SchemasService', () => {
{
fieldId: 8,
name: 'field8',
isLocked: true,
isHidden: true,
isDisabled: true,
partitioning: 'language',
@ -362,14 +370,14 @@ describe('SchemasService', () => {
DateTime.parseISO_UTC('2017-12-12T10:10'),
new Version('11'),
[
new FieldDto(1, 'field1', true, true, 'language', createProperties('Number')),
new FieldDto(2, 'field2', true, true, 'language', createProperties('String')),
new FieldDto(3, 'field3', true, true, 'language', createProperties('Boolean')),
new FieldDto(4, 'field4', true, true, 'language', createProperties('DateTime')),
new FieldDto(5, 'field5', true, true, 'language', createProperties('Json')),
new FieldDto(6, 'field6', true, true, 'language', createProperties('Geolocation')),
new FieldDto(7, 'field7', true, true, 'language', createProperties('Assets')),
new FieldDto(8, 'field8', true, true, 'language', createProperties('References'))
new FieldDto(1, 'field1', true, true, true, 'language', createProperties('Number')),
new FieldDto(2, 'field2', true, true, true, 'language', createProperties('String')),
new FieldDto(3, 'field3', true, true, true, 'language', createProperties('Boolean')),
new FieldDto(4, 'field4', true, true, true, 'language', createProperties('DateTime')),
new FieldDto(5, 'field5', true, true, true, 'language', createProperties('Json')),
new FieldDto(6, 'field6', true, true, true, 'language', createProperties('Geolocation')),
new FieldDto(7, 'field7', true, true, true, 'language', createProperties('Assets')),
new FieldDto(8, 'field8', true, true, true, 'language', createProperties('References'))
]));
}));
@ -437,7 +445,7 @@ describe('SchemasService', () => {
req.flush({ id: 123 });
expect(field).toEqual(
new FieldDto(123, dto.name, false, false, dto.partitioning, dto.properties));
new FieldDto(123, dto.name, false, false, false, dto.partitioning, dto.properties));
}));
it('should make put request to update schema',
@ -537,6 +545,19 @@ describe('SchemasService', () => {
req.flush({});
}));
it('should make put request to lock field',
inject([SchemasService, HttpTestingController], (schemasService: SchemasService, httpMock: HttpTestingController) => {
schemasService.lockField('my-app', 'my-schema', 1, version).subscribe();
const req = httpMock.expectOne('http://service/p/api/apps/my-app/schemas/my-schema/fields/1/lock');
expect(req.request.method).toEqual('PUT');
expect(req.request.headers.get('If-Match')).toBe(version.value);
req.flush({});
}));
it('should make put request to show field',
inject([SchemasService, HttpTestingController], (schemasService: SchemasService, httpMock: HttpTestingController) => {

24
src/Squidex/app/shared/services/schemas.service.ts

@ -215,6 +215,7 @@ export class FieldDto {
constructor(
public readonly fieldId: number,
public readonly name: string,
public readonly isLocked: boolean,
public readonly isHidden: boolean,
public readonly isDisabled: boolean,
public readonly partitioning: string,
@ -222,24 +223,28 @@ export class FieldDto {
) {
}
public lock(): FieldDto {
return new FieldDto(this.fieldId, this.name, true, this.isHidden, this.isDisabled, this.partitioning, this.properties);
}
public show(): FieldDto {
return new FieldDto(this.fieldId, this.name, false, this.isDisabled, this.partitioning, this.properties);
return new FieldDto(this.fieldId, this.name, this.isLocked, false, this.isDisabled, this.partitioning, this.properties);
}
public hide(): FieldDto {
return new FieldDto(this.fieldId, this.name, true, this.isDisabled, this.partitioning, this.properties);
return new FieldDto(this.fieldId, this.name, this.isLocked, true, this.isDisabled, this.partitioning, this.properties);
}
public enable(): FieldDto {
return new FieldDto(this.fieldId, this.name, this.isHidden, false, this.partitioning, this.properties);
return new FieldDto(this.fieldId, this.name, this.isLocked, this.isHidden, false, this.partitioning, this.properties);
}
public disable(): FieldDto {
return new FieldDto(this.fieldId, this.name, this.isHidden, true, this.partitioning, this.properties);
return new FieldDto(this.fieldId, this.name, this.isLocked, this.isHidden, true, this.partitioning, this.properties);
}
public update(properties: FieldPropertiesDto): FieldDto {
return new FieldDto(this.fieldId, this.name, this.isHidden, this.isDisabled, this.partitioning, properties);
return new FieldDto(this.fieldId, this.name, this.isLocked, this.isHidden, this.isDisabled, this.partitioning, properties);
}
public formatValue(value: any): string {
@ -658,6 +663,7 @@ export class SchemasService {
return new FieldDto(
item.fieldId,
item.name,
item.isLocked,
item.isHidden,
item.isDisabled,
item.partitioning,
@ -727,6 +733,7 @@ export class SchemasService {
dto.name,
false,
false,
false,
dto.partitioning,
dto.properties);
})
@ -792,6 +799,13 @@ export class SchemasService {
.pretifyError('Failed to disable field. Please reload.');
}
public lockField(appName: string, schemaName: string, fieldId: number, version?: Version): Observable<any> {
const url = this.apiUrl.buildUrl(`api/apps/${appName}/schemas/${schemaName}/fields/${fieldId}/lock`);
return HTTP.putVersioned(this.http, url, {}, version)
.pretifyError('Failed to lock field. Please reload.');
}
public showField(appName: string, schemaName: string, fieldId: number, version?: Version): Observable<any> {
const url = this.apiUrl.buildUrl(`api/apps/${appName}/schemas/${schemaName}/fields/${fieldId}/show`);

2
src/Squidex/app/shell/pages/internal/apps-menu.component.html

@ -5,7 +5,7 @@
<div class="dropdown-menu" *sqxModalView="modalMenu" closeAlways="true" [@fade]>
<a class="dropdown-item all-apps" routerLink="/app">
<span class="all-apps-text">All Apps</span>
<span class="all-apps-pill tag tag-pill tag-default">{{apps.length}}</span>
<span class="all-apps-pill badge badge-pill badge-primary">{{apps.length}}</span>
</a>
<div class="dropdown-divider"></div>

8
src/Squidex/app/shell/pages/internal/apps-menu.component.scss

@ -51,13 +51,5 @@
&-pill {
@include absolute(.8rem, .625rem, auto, auto);
@include border-radius(8px);
padding: 0 .4rem;
background: $color-theme-blue-lightest;
border: 0;
line-height: 1.2rem;
font-size: .8rem;
font-weight: normal;
color: $color-theme-blue;
}
}

9
src/Squidex/app/theme/_bootstrap-vars.scss

@ -12,7 +12,14 @@ $input-bg-disabled: $color-disabled;
$input-border-color: $color-input;
$input-color-placeholder: $color-empty;
$badge-default-bg: $color-badge-default-background;
$badge-primary-bg: $color-badge-primary-background;
$badge-success-bg: $color-badge-success-background;
$badge-info-bg: $color-badge-info-background;
$badge-warning-bg: $color-badge-warning-background;
$badge-danger-bg: $color-badge-danger-background;
$dropdown-border-color: $color-border;
$card-border-color: $color-border;
$card-cap-bg: #fff;
$card-cap-bg: $panel-light-background;

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

@ -18,6 +18,49 @@ h3 {
font-size: 1.05rem;
}
//
// Bade colors
//
.badge {
& {
font-size: .9rem;
font-weight: normal;
padding: .3rem .6rem;
}
&-default {
color: $color-badge-default-foreground;
}
&-primary {
color: $color-badge-primary-foreground;
}
&-success {
color: $color-badge-success-foreground;
}
&-info {
color: $color-badge-info-foreground;
}
&-warning {
color: $color-badge-warning-foreground;
}
&-danger {
color: $color-badge-danger-foreground;
}
}
a {
&:disabled,
&.disabled {
@include opacity(.8);
pointer-events: none;
}
}
//
// Fix navbar.
//
@ -68,6 +111,11 @@ h3 {
&:active {
background: $color-theme-error-dark;
}
&:disabled,
&.disabled {
color: lighten($color-theme-error, 20%);
}
}
&.dropdown-item {

22
src/Squidex/app/theme/_vars.scss

@ -33,10 +33,6 @@ $color-theme-green-dark: #47b353;
$color-theme-error: #f00;
$color-theme-error-dark: darken($color-theme-error, 5%);
$color-badge-success: $color-theme-green-dark;
$color-badge-error: $color-theme-error;
$color-badge-warning: #ff7b00;
$color-dark1-background: #2e3842;
$color-dark1-foreground: #6a7681;
$color-dark1-border1: #37424c;
@ -55,6 +51,24 @@ $color-dark2-separator: #2e3842;
$color-panel-icon: #a2b0b6;
$color-badge-success-background: #d6ffdb;
$color-badge-success-foreground: #3bab48;
$color-badge-warning-background: #ffe8cc;
$color-badge-warning-foreground: #efa243;
$color-badge-danger-background: #fcc;
$color-badge-danger-foreground: #ef4343;
$color-badge-info-background: #d0f2fb;
$color-badge-info-foreground: #43ccef;
$color-badge-primary-background: #cce1ff;
$color-badge-primary-foreground: #438cef;
$color-badge-default-background: #e6e6e6;
$color-badge-default-foreground: #999;
$color-table: #fff;
$color-table-footer: #ecf2f6;
$color-table-border: $color-border;

3
tests/Squidex.Domain.Apps.Core.Tests/Schemas/Json/JsonSerializerTests.cs

@ -55,6 +55,9 @@ namespace Squidex.Domain.Apps.Core.Schemas.Json
new ReferencesFieldProperties { SchemaId = Guid.NewGuid() }))
.AddOrUpdateField(new GeolocationField(10, "my-geolocation", Partitioning.Invariant,
new GeolocationFieldProperties()))
.HideField(1)
.LockField(2)
.DisableField(3)
.Publish();
var deserialized = sut.Deserialize(sut.Serialize(schema));

46
tests/Squidex.Domain.Apps.Core.Tests/Schemas/SchemaTests.cs

@ -147,6 +147,52 @@ namespace Squidex.Domain.Apps.Core.Schemas
Assert.Throws<DomainObjectNotFoundException>(() => sut.EnableField(1));
}
[Fact]
public void Should_lock_field()
{
AddField();
sut = sut.LockField(1);
Assert.True(sut.FieldsById[1].IsLocked);
}
[Fact]
public void Should_throw_exception_if_field_to_lock_does_not_exist()
{
Assert.Throws<DomainObjectNotFoundException>(() => sut.LockField(1));
}
[Fact]
public void Should_throw_exception_if_updating_locked_field()
{
AddField();
sut = sut.LockField(1);
Assert.Throws<DomainException>(() => sut.UpdateField(1, new NumberFieldProperties { IsRequired = true }));
}
[Fact]
public void Should_throw_exception_if_renaming_locked_field()
{
AddField();
sut = sut.LockField(1);
Assert.Throws<DomainException>(() => sut.RenameField(1, "new-name"));
}
[Fact]
public void Should_throw_exception_if_deleting_locked_field()
{
AddField();
sut = sut.LockField(1);
Assert.Throws<DomainException>(() => sut.DeleteField(1));
}
[Fact]
public void Should_rename_field()
{

14
tests/Squidex.Domain.Apps.Write.Tests/Schemas/SchemaCommandHandlerTests.cs

@ -167,6 +167,20 @@ namespace Squidex.Domain.Apps.Write.Schemas
});
}
[Fact]
public async Task LockField_should_update_domain_object()
{
CreateSchema();
CreateField();
var context = CreateContextForCommand(new LockField { FieldId = 1 });
await TestUpdate(schema, async _ =>
{
await sut.HandleAsync(context);
});
}
[Fact]
public async Task HideField_should_update_domain_object()
{

48
tests/Squidex.Domain.Apps.Write.Tests/Schemas/SchemaDomainObjectTests.cs

@ -436,6 +436,54 @@ namespace Squidex.Domain.Apps.Write.Schemas
);
}
[Fact]
public void LockField_should_throw_exception_if_not_created()
{
Assert.Throws<DomainException>(() =>
{
sut.LockField(CreateCommand(new LockField { FieldId = 1 }));
});
}
[Fact]
public void LockField_should_throw_exception_if_field_is_not_found()
{
CreateSchema();
Assert.Throws<DomainObjectNotFoundException>(() =>
{
sut.LockField(CreateCommand(new LockField { FieldId = 2 }));
});
}
[Fact]
public void LockField_should_throw_exception_if_schema_is_deleted()
{
CreateSchema();
DeleteSchema();
Assert.Throws<DomainException>(() =>
{
sut.LockField(CreateCommand(new LockField { FieldId = 1 }));
});
}
[Fact]
public void LockField_should_update_schema_and_create_events()
{
CreateSchema();
CreateField();
sut.LockField(CreateCommand(new LockField { FieldId = 1 }));
Assert.False(sut.Schema.FieldsById[1].IsDisabled);
sut.GetUncomittedEvents()
.ShouldHaveSameEvents(
CreateEvent(new FieldLocked { FieldId = fieldId })
);
}
[Fact]
public void HideField_should_throw_exception_if_not_created()
{

Loading…
Cancel
Save