Browse Source

A lot of refactorings for improved langauge handling.

pull/65/head
Sebastian Stehle 9 years ago
parent
commit
161f1b607c
  1. 15
      src/Squidex.Core/LanguagesConfig.cs
  2. 2
      src/Squidex.Events/Apps/AppLanguageUpdated.cs
  3. 1
      src/Squidex.Events/Apps/AppMasterLanguageSet.cs
  4. 19
      src/Squidex.Infrastructure/CQRS/Commands/DefaultDomainObjectRepository.cs
  5. 2
      src/Squidex.Infrastructure/CQRS/Events/EventReceiver.cs
  6. 29
      src/Squidex.Infrastructure/TypeNameNotFoundException.cs
  7. 4
      src/Squidex.Infrastructure/TypeNameRegistry.cs
  8. 11
      src/Squidex.Read.MongoDb/Apps/MongoAppEntity.cs
  9. 10
      src/Squidex.Read.MongoDb/Apps/MongoAppRepository_EventHandling.cs
  10. 12
      src/Squidex.Read/Apps/AppHistoryEventsCreator.cs
  11. 1
      src/Squidex.Read/Apps/Services/Implementations/CachingAppProvider.cs
  12. 5
      src/Squidex.Write/Apps/AppCommandHandler.cs
  13. 18
      src/Squidex.Write/Apps/AppDomainObject.cs
  14. 26
      src/Squidex.Write/Apps/Commands/SetMasterLanguage.cs
  15. 2
      src/Squidex.Write/Apps/Commands/UpdateLanguage.cs
  16. 14
      src/Squidex/Controllers/Api/Apps/AppLanguagesController.cs
  17. 8
      src/Squidex/Controllers/Api/Apps/Models/AppLanguageDto.cs
  18. 6
      src/Squidex/app/features/administration/pages/event-consumers/event-consumers-page.component.html
  19. 10
      src/Squidex/app/features/administration/pages/users/users-page.component.html
  20. 6
      src/Squidex/app/features/assets/pages/assets-page.component.html
  21. 2
      src/Squidex/app/features/content/pages/contents/content-item.component.html
  22. 6
      src/Squidex/app/features/content/pages/contents/contents-page.component.html
  23. 10
      src/Squidex/app/features/schemas/pages/schema/field.component.html
  24. 2
      src/Squidex/app/features/schemas/pages/schema/schema-page.component.html
  25. 1
      src/Squidex/app/features/settings/declarations.ts
  26. 4
      src/Squidex/app/features/settings/module.ts
  27. 2
      src/Squidex/app/features/settings/pages/clients/client.component.html
  28. 17
      src/Squidex/app/features/settings/pages/contributors/contributors-page.component.html
  29. 82
      src/Squidex/app/features/settings/pages/languages/language.component.html
  30. 110
      src/Squidex/app/features/settings/pages/languages/language.component.scss
  31. 131
      src/Squidex/app/features/settings/pages/languages/language.component.ts
  32. 55
      src/Squidex/app/features/settings/pages/languages/languages-page.component.html
  33. 40
      src/Squidex/app/features/settings/pages/languages/languages-page.component.ts
  34. 12
      src/Squidex/app/shared/services/app-languages.service.spec.ts
  35. 16
      src/Squidex/app/shared/services/app-languages.service.ts
  36. 10
      src/Squidex/app/theme/_bootstrap.scss
  37. 4
      src/Squidex/app/theme/_common.scss
  38. 2
      tests/Squidex.Core.Tests/ContentValidationTests.cs
  39. 2
      tests/Squidex.Core.Tests/Contents/ContentDataTests.cs
  40. 28
      tests/Squidex.Core.Tests/LanguagesConfigTests.cs
  41. 5
      tests/Squidex.Infrastructure.Tests/TypeNameRegistryTests.cs
  42. 14
      tests/Squidex.Write.Tests/Apps/AppCommandHandlerTests.cs
  43. 45
      tests/Squidex.Write.Tests/Apps/AppDomainObjectTests.cs

15
src/Squidex.Core/LanguagesConfig.cs

@ -81,24 +81,25 @@ namespace Squidex.Core
return new LanguagesConfig(newLanguages, master ?? newLanguages.Values.First());
}
public LanguagesConfig Update(Language language, bool isOptional, IEnumerable<Language> fallback)
public LanguagesConfig Update(Language language, bool isOptional, bool isMaster, IEnumerable<Language> fallback)
{
ThrowIfNotFound(language);
if (isOptional)
{
ThrowIfMaster(language, () => $"Cannot cannot make language '{language.Iso2Code}' optional");
ThrowIfMaster(language, isMaster, () => $"Cannot cannot make language '{language.Iso2Code}' optional");
}
var newLanguages = ValidateLanguages(languages.SetItem(language, new LanguageConfig(language, isOptional, fallback)));
var newLanguage = new LanguageConfig(language, isOptional, fallback);
var newLanguages = ValidateLanguages(languages.SetItem(language, newLanguage));
return new LanguagesConfig(newLanguages, master);
return new LanguagesConfig(newLanguages, isMaster ? newLanguage : master);
}
public LanguagesConfig Remove(Language language)
{
ThrowIfNotFound(language);
ThrowIfMaster(language, () => $"Cannot remove language '{language.Iso2Code}'");
ThrowIfMaster(language, false, () => $"Cannot remove language '{language.Iso2Code}'");
var newLanguages = languages.Remove(language);
@ -171,9 +172,9 @@ namespace Squidex.Core
}
}
private void ThrowIfMaster(Language language, Func<string> message)
private void ThrowIfMaster(Language language, bool isMaster, Func<string> message)
{
if (master?.Language == language)
if (master?.Language == language || isMaster)
{
var error = new ValidationError("Language is the master language", "Language");

2
src/Squidex.Events/Apps/AppLanguageUpdated.cs

@ -18,6 +18,8 @@ namespace Squidex.Events.Apps
public bool IsOptional { get; set; }
public bool IsMaster { get; set; }
public List<Language> Fallback { get; set; }
}
}

1
src/Squidex.Events/Apps/AppMasterLanguageSet.cs

@ -6,6 +6,7 @@
// All rights reserved.
// ==========================================================================
using System;
using Squidex.Infrastructure;
namespace Squidex.Events.Apps

19
src/Squidex.Infrastructure/CQRS/Commands/DefaultDomainObjectRepository.cs

@ -54,9 +54,12 @@ namespace Squidex.Infrastructure.CQRS.Commands
foreach (var storedEvent in events)
{
var envelope = formatter.Parse(storedEvent.Data);
var envelope = TryParseEvent(storedEvent);
domainObject.ApplyEvent(envelope);
if (envelope != null)
{
domainObject.ApplyEvent(envelope);
}
}
if (expectedVersion != null && domainObject.Version != expectedVersion.Value)
@ -87,5 +90,17 @@ namespace Squidex.Infrastructure.CQRS.Commands
throw new DomainObjectVersionException(domainObject.Id.ToString(), domainObject.GetType(), versionCurrent, versionExpected);
}
}
private Envelope<IEvent> TryParseEvent(StoredEvent storedEvent)
{
try
{
return formatter.Parse(storedEvent.Data);
}
catch (TypeNameNotFoundException)
{
return null;
}
}
}
}

2
src/Squidex.Infrastructure/CQRS/Events/EventReceiver.cs

@ -218,7 +218,7 @@ namespace Squidex.Infrastructure.CQRS.Events
return @event;
}
catch (ArgumentException)
catch (TypeNameNotFoundException)
{
return null;
}

29
src/Squidex.Infrastructure/TypeNameNotFoundException.cs

@ -0,0 +1,29 @@
// ==========================================================================
// TypeNameNotFoundException.cs
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex Group
// All rights reserved.
// ==========================================================================
using System;
namespace Squidex.Infrastructure
{
public class TypeNameNotFoundException : Exception
{
public TypeNameNotFoundException()
{
}
public TypeNameNotFoundException(string message)
: base(message)
{
}
public TypeNameNotFoundException(string message, Exception inner)
: base(message, inner)
{
}
}
}

4
src/Squidex.Infrastructure/TypeNameRegistry.cs

@ -90,7 +90,7 @@ namespace Squidex.Infrastructure
if (result == null)
{
throw new ArgumentException($"There is no name for type '{type}");
throw new TypeNameNotFoundException($"There is no name for type '{type}");
}
return result;
@ -102,7 +102,7 @@ namespace Squidex.Infrastructure
if (result == null)
{
throw new ArgumentException($"There is no type for name '{name}");
throw new TypeNameNotFoundException($"There is no type for name '{name}");
}
return result;

11
src/Squidex.Read.MongoDb/Apps/MongoAppEntity.cs

@ -22,7 +22,7 @@ namespace Squidex.Read.MongoDb.Apps
{
public sealed class MongoAppEntity : MongoEntity, IAppEntity
{
private LanguagesConfig languagesConfig = LanguagesConfig.Empty;
private LanguagesConfig languagesConfig;
[BsonRequired]
[BsonElement]
@ -78,7 +78,14 @@ namespace Squidex.Read.MongoDb.Apps
private LanguagesConfig CreateLanguagesConfig()
{
return LanguagesConfig.Create(Languages.Select(ToLanguageConfig).ToList()).MakeMaster(MasterLanguage);
languagesConfig = LanguagesConfig.Create(Languages.Select(ToLanguageConfig).ToList());
if (MasterLanguage != null)
{
languagesConfig = languagesConfig.MakeMaster(MasterLanguage);
}
return languagesConfig;
}
private static MongoAppLanguage FromLanguageConfig(LanguageConfig l)

10
src/Squidex.Read.MongoDb/Apps/MongoAppRepository_EventHandling.cs

@ -93,15 +93,7 @@ namespace Squidex.Read.MongoDb.Apps
{
return Collection.UpdateAsync(@event, headers, a =>
{
a.UpdateLanguages(c => c.Update(@event.Language, @event.IsOptional, @event.Fallback));
});
}
protected Task On(AppMasterLanguageSet @event, EnvelopeHeaders headers)
{
return Collection.UpdateAsync(@event, headers, a =>
{
a.UpdateLanguages(c => c.MakeMaster(@event.Language));
a.UpdateLanguages(c => c.Update(@event.Language, @event.IsOptional, @event.IsMaster, @event.Fallback));
});
}

12
src/Squidex.Read/Apps/AppHistoryEventsCreator.cs

@ -43,6 +43,9 @@ namespace Squidex.Read.Apps
AddEventMessage<AppLanguageRemoved>(
"removed language {[Language]}");
AddEventMessage<AppLanguageUpdated>(
"updated language {[Language]}");
AddEventMessage<AppMasterLanguageSet>(
"changed master language to {[Language]}");
}
@ -110,6 +113,15 @@ namespace Squidex.Read.Apps
.AddParameter("Language", @event.Language));
}
protected Task<HistoryEventToStore> On(AppLanguageUpdated @event, EnvelopeHeaders headers)
{
const string channel = "settings.languages";
return Task.FromResult(
ForEvent(@event, channel)
.AddParameter("Language", @event.Language));
}
protected Task<HistoryEventToStore> On(AppMasterLanguageSet @event, EnvelopeHeaders headers)
{
const string channel = "settings.languages";

1
src/Squidex.Read/Apps/Services/Implementations/CachingAppProvider.cs

@ -107,6 +107,7 @@ namespace Squidex.Read.Apps.Services.Implementations
@event.Payload is AppCreated ||
@event.Payload is AppLanguageAdded ||
@event.Payload is AppLanguageRemoved ||
@event.Payload is AppLanguageUpdated ||
@event.Payload is AppMasterLanguageSet)
{
Remove(((AppEvent)@event.Payload).AppId);

5
src/Squidex.Write/Apps/AppCommandHandler.cs

@ -114,11 +114,6 @@ namespace Squidex.Write.Apps
return handler.UpdateAsync<AppDomainObject>(context, a => a.UpdateLanguage(command));
}
protected Task On(SetMasterLanguage command, CommandContext context)
{
return handler.UpdateAsync<AppDomainObject>(context, a => a.SetMasterLanguage(command));
}
public Task<bool> HandleAsync(CommandContext context)
{
return context.IsHandled ? TaskHelper.False : this.DispatchActionAsync(context.Command, context);

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

@ -88,12 +88,7 @@ namespace Squidex.Write.Apps
protected void On(AppLanguageUpdated @event)
{
languagesConfig = languagesConfig.Update(@event.Language, @event.IsOptional, @event.Fallback);
}
protected void On(AppMasterLanguageSet @event)
{
languagesConfig = languagesConfig.MakeMaster(@event.Language);
languagesConfig = languagesConfig.Update(@event.Language, @event.IsOptional, @event.IsMaster, @event.Fallback);
}
protected override void DispatchEvent(Envelope<IEvent> @event)
@ -205,17 +200,6 @@ namespace Squidex.Write.Apps
return this;
}
public AppDomainObject SetMasterLanguage(SetMasterLanguage command)
{
Guard.Valid(command, nameof(command), () => "Cannot set master language");
ThrowIfNotCreated();
RaiseEvent(SimpleMapper.Map(command, new AppMasterLanguageSet()));
return this;
}
private void RaiseEvent(AppEvent @event)
{
if (@event.AppId == null)

26
src/Squidex.Write/Apps/Commands/SetMasterLanguage.cs

@ -1,26 +0,0 @@
// ==========================================================================
// SetMasterLanguage.cs
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex Group
// All rights reserved.
// ==========================================================================
using System.Collections.Generic;
using Squidex.Infrastructure;
namespace Squidex.Write.Apps.Commands
{
public sealed class SetMasterLanguage : AppAggregateCommand, IValidatable
{
public Language Language { get; set; }
public void Validate(IList<ValidationError> errors)
{
if (Language == null)
{
errors.Add(new ValidationError("Language cannot be null", nameof(Language)));
}
}
}
}

2
src/Squidex.Write/Apps/Commands/UpdateLanguage.cs

@ -17,6 +17,8 @@ namespace Squidex.Write.Apps.Commands
public bool IsOptional { get; set; }
public bool IsMaster { get; set; }
public List<Language> Fallback { get; set; }
public void Validate(IList<ValidationError> errors)

14
src/Squidex/Controllers/Api/Apps/AppLanguagesController.cs

@ -66,8 +66,9 @@ namespace Squidex.Controllers.Api.Apps
new AppLanguageDto
{
IsMaster = x == entity.LanguagesConfig.Master,
IsOptional = x.IsOptional
})).ToList();
IsOptional = x.IsOptional,
Fallback = x.Fallback.ToList()
})).OrderByDescending(x => x.IsMaster).ThenBy(x => x.Iso2Code).ToList();
Response.Headers["ETag"] = new StringValues(entity.Version.ToString());
@ -114,13 +115,8 @@ namespace Squidex.Controllers.Api.Apps
[Route("apps/{app}/languages/{language}")]
public async Task<IActionResult> Update(string app, string language, [FromBody] UpdateAppLanguageDto model)
{
await CommandBus.PublishAsync(SimpleMapper.Map(model, new UpdateLanguage()));
if (model.IsMaster == true)
{
await CommandBus.PublishAsync(new SetMasterLanguage { Language = ParseLanguage(language) });
}
await CommandBus.PublishAsync(SimpleMapper.Map(model, new UpdateLanguage { Language = language }));
return NoContent();
}

8
src/Squidex/Controllers/Api/Apps/Models/AppLanguageDto.cs

@ -6,7 +6,9 @@
// All rights reserved.
// ==========================================================================
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using Squidex.Infrastructure;
namespace Squidex.Controllers.Api.Apps.Models
{
@ -24,6 +26,12 @@ namespace Squidex.Controllers.Api.Apps.Models
[Required]
public string EnglishName { get; set; }
/// <summary>
/// The fallback languages.
/// </summary>
[Required]
public List<Language> Fallback { get; set; }
/// <summary>
/// Indicates if the language is the master language.
/// </summary>

6
src/Squidex/app/features/administration/pages/event-consumers/event-consumers-page.component.html

@ -48,13 +48,13 @@
<span>{{eventConsumer.lastHandledEventNumber}}</span>
</td>
<td class="col-right">
<button class="btn btn-simple" (click)="reset(eventConsumer.name)" *ngIf="!eventConsumer.isResetting" title="Reset Event Consumer">
<button class="btn btn-link" (click)="reset(eventConsumer.name)" *ngIf="!eventConsumer.isResetting" title="Reset Event Consumer">
<i class="icon icon-reset"></i>
</button>
<button class="btn btn-simple" (click)="start(eventConsumer.name)" *ngIf="eventConsumer.isStopped" title="Start Event Consumer">
<button class="btn btn-link" (click)="start(eventConsumer.name)" *ngIf="eventConsumer.isStopped" title="Start Event Consumer">
<i class="icon icon-play"></i>
</button>
<button class="btn btn-simple" (click)="stop(eventConsumer.name)" *ngIf="!eventConsumer.isStopped" title="Stop Event Consumer">
<button class="btn btn-link" (click)="stop(eventConsumer.name)" *ngIf="!eventConsumer.isStopped" title="Stop Event Consumer">
<i class="icon icon-pause"></i>
</button>
</td>

10
src/Squidex/app/features/administration/pages/users/users-page.component.html

@ -4,7 +4,7 @@
<div class="panel-header">
<div class="panel-title-row">
<div class="float-right">
<button class="btn btn-simple" (click)="load(true)" title="Refresh Users (CTRL + SHIFT + R)">
<button class="btn btn-decent" (click)="load(true)" title="Refresh Users (CTRL + SHIFT + R)">
<i class="icon-reset"></i> Refresh
</button>
@ -64,10 +64,10 @@
</td>
<td class="col-right">
<span *ngIf="user.id !== currentUserId">
<button class="btn btn-simple" (click)="lock(user.id)" *ngIf="!user.isLocked" title="Lock User">
<button class="btn btn-link" (click)="lock(user.id)" *ngIf="!user.isLocked" title="Lock User">
<i class="icon icon-unlocked"></i>
</button>
<button class="btn btn-simple" (click)="unlock(user.id)" *ngIf="user.isLocked" title="Unlock User">
<button class="btn btn-link" (click)="unlock(user.id)" *ngIf="user.isLocked" title="Unlock User">
<i class="icon icon-lock"></i>
</button>
</span>
@ -82,10 +82,10 @@
<div class="float-right pagination">
<span class="pagination-text">{{usersPager.itemFirst}}-{{usersPager.itemLast}} of {{usersPager.numberOfItems}}</span>
<button class="btn btn-simple pagination-button" [disabled]="!usersPager.canGoPrev" (click)="goPrev()">
<button class="btn btn-decent pagination-button" [disabled]="!usersPager.canGoPrev" (click)="goPrev()">
<i class="icon-angle-left"></i>
</button>
<button class="btn btn-simple pagination-button" [disabled]="!usersPager.canGoNext" (click)="goNext()">
<button class="btn btn-decent pagination-button" [disabled]="!usersPager.canGoNext" (click)="goNext()">
<i class="icon-angle-right"></i>
</button>
</div>

6
src/Squidex/app/features/assets/pages/assets-page.component.html

@ -4,7 +4,7 @@
<div class="panel-header">
<div class="panel-title-row">
<div class="float-right">
<button class="btn btn-simple" (click)="load(true)" title="Refresh Assets (CTRL + SHIFT + R)">
<button class="btn btn-decent" (click)="load(true)" title="Refresh Assets (CTRL + SHIFT + R)">
<i class="icon-reset"></i> Refresh
</button>
@ -56,10 +56,10 @@
<div class="float-right pagination">
<span class="pagination-text">{{assetsPager.itemFirst}}-{{assetsPager.itemLast}} of {{assetsPager.numberOfItems}}</span>
<button class="btn btn-simple pagination-button" [disabled]="!assetsPager.canGoPrev" (click)="goPrev()">
<button class="btn btn-decent pagination-button" [disabled]="!assetsPager.canGoPrev" (click)="goPrev()">
<i class="icon-angle-left"></i>
</button>
<button class="btn btn-simple pagination-button" [disabled]="!assetsPager.canGoNext" (click)="goNext()">
<button class="btn btn-decent pagination-button" [disabled]="!assetsPager.canGoNext" (click)="goNext()">
<i class="icon-angle-right"></i>
</button>
</div>

2
src/Squidex/app/features/content/pages/contents/content-item.component.html

@ -12,7 +12,7 @@
</td>
<td>
<div class="dropdown dropdown-options" *ngIf="content">
<button type="button" class="btn btn-simple" (click)="dropdown.toggle(); $event.stopPropagation()" [class.active]="dropdown.isOpen | async">
<button type="button" class="btn btn-decent" (click)="dropdown.toggle(); $event.stopPropagation()" [class.active]="dropdown.isOpen | async">
<i class="icon-dots"></i>
</button>
<div class="dropdown-menu" *sqxModalView="dropdown" closeAlways="true" [@fade]>

6
src/Squidex/app/features/content/pages/contents/contents-page.component.html

@ -4,7 +4,7 @@
<div class="panel-header">
<div class="panel-title-row">
<div class="float-right">
<button class="btn btn-simple" (click)="load(true)" title="Refresh Contents (CTRL + SHIFT + R)">
<button class="btn btn-decent" (click)="load(true)" title="Refresh Contents (CTRL + SHIFT + R)">
<i class="icon-reset"></i> Refresh
</button>
@ -77,10 +77,10 @@
<div class="float-right pagination">
<span class="pagination-text">{{contentsPager.itemFirst}}-{{contentsPager.itemLast}} of {{contentsPager.numberOfItems}}</span>
<button class="btn btn-simple pagination-button" [disabled]="!contentsPager.canGoPrev" (click)="goPrev()">
<button class="btn btn-decent pagination-button" [disabled]="!contentsPager.canGoPrev" (click)="goPrev()">
<i class="icon-angle-left"></i>
</button>
<button class="btn btn-simple pagination-button" [disabled]="!contentsPager.canGoNext" (click)="goNext()">
<button class="btn btn-decent pagination-button" [disabled]="!contentsPager.canGoNext" (click)="goNext()">
<i class="icon-angle-right"></i>
</button>
</div>

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

@ -22,7 +22,7 @@
</button>
<div class="dropdown dropdown-options">
<button type="button" class="btn btn-simple" (click)="dropdown.toggle()" [class.active]="dropdown.isOpen | async">
<button type="button" class="btn btn-decent" (click)="dropdown.toggle()" [class.active]="dropdown.isOpen | async">
<i class="icon-dots"></i>
</button>
<div class="dropdown-menu" *sqxModalView="dropdown" closeAlways="true" [@fade]>
@ -111,15 +111,19 @@
</div>
<div class="form-group row">
<div class="form-check offset-3 col col-6">
<div class="form-check offset-3 col col-9">
<label class="form-check-label">
<input class="form-check-input" type="checkbox" formControlName="isListField"> List Field
</label>
<div class="form-hint">
List fields are shown as a column in the content list. If no list field is defined, the first field is shown.
</div>
</div>
</div>
<div class="form-group row">
<div class="form-check offset-3 col col-6">
<div class="form-check offset-3 col col-9">
<label class="form-check-label">
<input class="form-check-input" type="checkbox" formControlName="isLocalizable"> Localizable
</label>

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

@ -14,7 +14,7 @@
</div>
<div class="dropdown dropdown-options">
<button type="button" class="btn btn-simple btn-sm" (click)="editOptionsDropdown.toggle()" [class.active]="editOptionsDropdown.isOpen | async">
<button type="button" class="btn btn-decent btn-sm" (click)="editOptionsDropdown.toggle()" [class.active]="editOptionsDropdown.isOpen | async">
<i class="icon-dots"></i>
</button>
<div class="dropdown-menu" *sqxModalView="editOptionsDropdown" closeAlways="true" [@fade]>

1
src/Squidex/app/features/settings/declarations.ts

@ -8,6 +8,7 @@
export * from './pages/clients/client.component';
export * from './pages/clients/clients-page.component';
export * from './pages/contributors/contributors-page.component';
export * from './pages/languages/language.component';
export * from './pages/languages/languages-page.component';
export * from './settings-area.component';

4
src/Squidex/app/features/settings/module.ts

@ -7,6 +7,7 @@
import { NgModule } from '@angular/core';
import { RouterModule, Routes } from '@angular/router';
import { DndModule } from 'ng2-dnd';
import {
HelpComponent,
@ -19,6 +20,7 @@ import {
ClientComponent,
ClientsPageComponent,
ContributorsPageComponent,
LanguageComponent,
LanguagesPageComponent,
SettingsAreaComponent
} from './declarations';
@ -97,6 +99,7 @@ const routes: Routes = [
@NgModule({
imports: [
DndModule,
SqxFrameworkModule,
SqxSharedModule,
RouterModule.forChild(routes)
@ -105,6 +108,7 @@ const routes: Routes = [
ClientComponent,
ClientsPageComponent,
ContributorsPageComponent,
LanguageComponent,
LanguagesPageComponent,
SettingsAreaComponent
]

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

@ -22,7 +22,7 @@
<button type="submit" class="btn btn-primary" [disabled]="!renameForm.valid">Save</button>
<button class="btn btn-simple btn-cancel" (click)="cancelRename()">
<button class="btn btn-decent btn-cancel" (click)="cancelRename()">
<i class="icon-close"></i>
</button>
</form>

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

@ -22,23 +22,6 @@
<col style="width: 80px" />
</colgroup>
<thead>
<tr>
<th>
&nbsp;
</th>
<th>
Name
</th>
<th>
Email
</th>
<th colspan="2">
Actions
</th>
</tr>
</thead>
<tbody>
<ng-template ngFor let-contributor [ngForOf]="appContributors">
<tr>

82
src/Squidex/app/features/settings/pages/languages/language.component.html

@ -0,0 +1,82 @@
<div class="table-items-row language">
<div class="language-summary">
<div class="row">
<div class="col col-2">
<span class="language-code" [class.language-master]="language.isMaster">{{language.iso2Code}}</span>
</div>
<div class="col col-6">
<span class="language-name" [class.language-master]="language.isMaster">{{language.englishName}}</span>
</div>
<div class="col col-4">
<div class="float-right">
<button type="button" class="btn btn-secondary language-edit-button" [class.active]="isEditing" (click)="toggleEditing()" *ngIf="!language.isMaster || allLanguages.length > 1">
<i class="icon-settings2"></i>
</button>
<button type="button" class="btn btn-link btn-danger" (click)="removing.emit(language)" [class.invisible]="language.isMaster">
<i class="icon-bin2"></i>
</button>
</div>
</div>
</div>
</div>
<div class="language-details" *ngIf="isEditing">
<form [formGroup]="editForm" (ngSubmit)="save()">
<div class="language-details-tabs clearfix">
<div class="float-right">
<button type="reset" class="btn btn-link" (click)="cancel()">Cancel</button>
<button type="submit" class="btn btn-primary">Save</button>
</div>
</div>
<div class="language-details-tab">
<div class="form-group row" *ngIf="allLanguages.length > 1">
<label for="field-label" class="col col-3 col-form-label fallback-label">Fallback</label>
<div class="col col-9">
<div class="fallback-languages" dnd-sortable-container [sortableData]="fallbackLanguages" *ngIf="fallbackLanguages.length > 0">
<div class="fallback-language" *ngFor="let language of fallbackLanguages; let i = index" dnd-sortable [sortableIndex]="i">
{{language.englishName}}
<button type="button" class="btn btn-link btn-sm float-right" (click)="removeFallbackLanguage(language)">
<i class="icon-bin2"></i>
</button>
</div>
</div>
<form class="form-inline fallback-form" [formGroup]="addLanguageForm" (ngSubmit)="addLanguage()" *ngIf="otherLanguages.length > 0">
<div class="form-group mr-2">
<select class="form-control fallback-select" formControlName="language">
<option *ngFor="let language of otherLanguages" [ngValue]="language">{{language.englishName}}</option>
</select>
</div>
<button type="submit" class="btn btn-success" [disabled]="!addLanguageForm.valid">Add Language</button>
</form>
</div>
</div>
<div class="form-check offset-3 col col-9" *ngIf="!language.isMaster">
<label class="form-check-label">
<input class="form-check-input" type="checkbox" formControlName="isMaster"> Is Master
</label>
<div class="form-hint">
Master language is the last fallback language, when no value for a content and a language is available.
</div>
</div>
<div class="form-check offset-3 col col-9" *ngIf="!language.isMaster">
<label class="form-check-label">
<input class="form-check-input" type="checkbox" formControlName="isOptional"> Is Optional
</label>
<div class="form-hint">
Values for optional languages must not be specified, even if the field is set to required.
</div>
</div>
</div>
</form>
</div>
</div>

110
src/Squidex/app/features/settings/pages/languages/language.component.scss

@ -0,0 +1,110 @@
@import '_vars';
@import '_mixins';
$field-header: #e7ebef;
.table-items-row {
padding: 0;
}
.language {
&-summary {
padding: 1rem 1.25rem;
position: relative;
line-height: 2.5rem;
}
&-edit-button {
& {
color: $color-theme-blue;
line-height: 1rem;
font-size: 1rem;
font-weight: normal;
}
&:hover {
color: $color-theme-blue-dark;
}
&.active {
background: $color-theme-blue;
border-color: $color-theme-blue;
color: $color-dark-foreground;
}
}
&-name {
@include truncate;
}
&-master {
font-weight: bold;
}
&-details {
& {
position: relative;
}
&::before {
@include caret-top;
@include absolute(-.5rem, 5.5rem, auto, auto);
border-color: transparent transparent $color-border;
border-width: .6rem;
}
&-tab {
padding: 1rem 1.25rem 1.25rem;
}
&-tabs {
background: $color-border;
position: relative;
padding: 1rem 1.25rem;
}
}
}
.btn-danger {
width: 3.2rem;
}
.master {
font-weight: bold;
}
.fallback {
&-languages {
@include border-radius;
min-height: 2rem;
background: $color-border;
border: 0;
padding: .5rem;
}
&-language {
& {
@include border-radius(2px);
padding: .5rem;
background: $color-dark-foreground;
border: 0;
margin-bottom: .5rem;
}
&:last-child {
margin: 0;
}
}
&-label {
margin-top: .4rem;
}
&-form {
margin-top: .5rem;
}
&-select {
width: 14rem;
}
}

131
src/Squidex/app/features/settings/pages/languages/language.component.ts

@ -0,0 +1,131 @@
/*
* Squidex Headless CMS
*
* @license
* Copyright (c) Sebastian Stehle. All rights reserved
*/
import { Component, EventEmitter, Input, OnChanges, Output, OnInit } from '@angular/core';
import { FormBuilder, FormGroup, Validators } from '@angular/forms';
import {
AppLanguageDto,
fadeAnimation,
ImmutableArray
} from 'shared';
@Component({
selector: 'sqx-language',
styleUrls: ['./language.component.scss'],
templateUrl: './language.component.html',
animations: [
fadeAnimation
]
})
export class LanguageComponent implements OnInit, OnChanges {
@Input()
public language: AppLanguageDto;
@Input()
public allLanguages: ImmutableArray<AppLanguageDto>;
@Output()
public removing = new EventEmitter<AppLanguageDto>();
@Output()
public saving = new EventEmitter<AppLanguageDto>();
public otherLanguages: ImmutableArray<AppLanguageDto>;
public fallbackLanguages: AppLanguageDto[] = [];
public isEditing = false;
public editFormSubmitted = false;
public editForm: FormGroup =
this.formBuilder.group({
isMaster: [false, []],
isOptional: [false, []]
});
public addLanguageForm: FormGroup =
this.formBuilder.group({
language: [null,
Validators.required
]
});
constructor(
private readonly formBuilder: FormBuilder
) {
}
public ngOnInit() {
this.resetForm();
}
public ngOnChanges() {
this.resetForm();
}
public cancel() {
this.resetForm();
}
public toggleEditing() {
this.isEditing = !this.isEditing;
}
public addLanguage() {
this.addFallbackLanguage(this.addLanguageForm.get('language')!.value);
}
public removeFallbackLanguage(language: AppLanguageDto) {
this.fallbackLanguages.splice(this.fallbackLanguages.indexOf(language), 1);
this.otherLanguages = this.otherLanguages.push(language);
}
public addFallbackLanguage(language: AppLanguageDto) {
this.fallbackLanguages.push(language);
this.otherLanguages = this.otherLanguages.filter(l => l.iso2Code !== language.iso2Code);
}
public save() {
this.editFormSubmitted = true;
if (this.editForm.valid) {
const newLanguage =
new AppLanguageDto(
this.language.iso2Code,
this.language.englishName,
this.editForm.get('isMaster')!.value,
this.editForm.get('isOptional')!.value,
this.fallbackLanguages.map(l => l.iso2Code));
this.saving.emit(newLanguage);
}
}
private resetForm() {
this.editFormSubmitted = false;
this.editForm.reset(this.language);
this.isEditing = false;
if (this.language && this.allLanguages) {
this.otherLanguages =
this.allLanguages.filter(l =>
this.language.iso2Code !== l.iso2Code &&
this.language.fallback.indexOf(l.iso2Code) < 0);
}
if (this.language) {
this.fallbackLanguages =
this.allLanguages.filter(l =>
this.language.fallback.indexOf(l.iso2Code) >= 0).values;
}
}
}

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

@ -13,56 +13,11 @@
<div class="panel-main">
<div class="panel-content panel-content-scroll">
<table class="table table-items table-fixed">
<colgroup>
<col style="width: 60px" />
<col style="width: 100%" />
<col style="width: 200px" />
<col style="width: 80px" />
</colgroup>
<thead>
<tr>
<th>
Code
</th>
<th>
Name
</th>
<th>
Actions
</th>
</tr>
</thead>
<tbody>
<ng-template ngFor let-language [ngForOf]="appLanguages">
<tr>
<td>
<span class="language-code">
{{language.iso2Code}}
</span>
</td>
<td>
<span class="language-name">
{{language.englishName}}
</span>
</td>
<td>
<label class="language-default">
<input type="radio" [value]="true" [checked]="language.isMasterLanguage" (click)="setMasterLanguage(language)"> Master Language
</label>
</td>
<td>
<button type="button" class="btn btn-link btn-danger" [disabled]="language.isMasterLanguage" (click)="removeLanguage(language)">
<i class="icon-bin2"></i>
</button>
</td>
</tr>
<tr class="spacer"></tr>
</ng-template>
</tbody>
</table>
<div *ngFor="let language of appLanguages">
<sqx-language [language]="language" [allLanguages]="appLanguages"
(saving)="updateLanguage($event)"
(removing)="removeLanguage($event)"></sqx-language>
</div>
<div class="table-items-footer">
<form class="form-inline" [formGroup]="addLanguageForm" (ngSubmit)="addLanguage()">

40
src/Squidex/app/features/settings/pages/languages/languages-page.component.ts

@ -20,7 +20,6 @@ import {
LanguageDto,
LanguageService,
NotificationService,
UpdateAppLanguageDto,
Version
} from 'shared';
@ -33,6 +32,7 @@ export class LanguagesPageComponent extends AppComponentBase implements OnInit {
private version = new Version();
public allLanguages: LanguageDto[] = [];
public newLanguages: LanguageDto[] = [];
public appLanguages = ImmutableArray.empty<AppLanguageDto>();
public addLanguageForm: FormGroup =
@ -42,10 +42,6 @@ export class LanguagesPageComponent extends AppComponentBase implements OnInit {
]
});
public get newLanguages(): LanguageDto[] {
return this.allLanguages.filter(x => !this.appLanguages.find(l => l.iso2Code === x.iso2Code));
}
constructor(apps: AppsStoreService, notifications: NotificationService,
private readonly appLanguagesService: AppLanguagesService,
private readonly languagesService: LanguageService,
@ -76,6 +72,16 @@ export class LanguagesPageComponent extends AppComponentBase implements OnInit {
});
}
public updateLanguage(language: AppLanguageDto) {
this.appNameOnce()
.switchMap(app => this.appLanguagesService.updateLanguage(app, language.iso2Code, language, this.version))
.subscribe(dto => {
this.updateLanguages(this.appLanguages.map(l => l.iso2Code === language.iso2Code ? language : l));
}, error => {
this.notifyError(error);
});
}
public removeLanguage(language: AppLanguageDto) {
this.appNameOnce()
.switchMap(app => this.appLanguagesService.deleteLanguage(app, language.iso2Code, this.version))
@ -98,32 +104,12 @@ export class LanguagesPageComponent extends AppComponentBase implements OnInit {
});
}
public setMasterLanguage(language: AppLanguageDto) {
const request = new UpdateAppLanguageDto(true);
this.appNameOnce()
.switchMap(app => this.appLanguagesService.updateLanguage(app, language.iso2Code, request, this.version))
.subscribe(() => {
this.updateLanguages(this.appLanguages.map(l => {
const isMasterLanguage = l === language;
if (isMasterLanguage !== l.isMasterLanguage) {
return new AppLanguageDto(l.iso2Code, l.englishName, isMasterLanguage);
} else {
return l;
}
}));
}, error => {
this.notifyError(error);
});
return false;
}
private updateLanguages(languages: ImmutableArray<AppLanguageDto>) {
this.addLanguageForm.reset();
this.appLanguages = languages;
this.newLanguages = this.allLanguages.filter(x => !this.appLanguages.find(l => l.iso2Code === x.iso2Code));
this.messageBus.publish(new HistoryChannelUpdated());
}
}

12
src/Squidex/app/shared/services/app-languages.service.spec.ts

@ -38,7 +38,9 @@ describe('AppLanguagesService', () => {
{
iso2Code: 'de',
englishName: 'German',
isMasterLanguage: true
isMaster: true,
isOptional: true,
fallback: ['de', 'en']
},
{
iso2Code: 'en',
@ -58,8 +60,8 @@ describe('AppLanguagesService', () => {
expect(languages).toEqual(
[
new AppLanguageDto('de', 'German', true),
new AppLanguageDto('en', 'English', false)
new AppLanguageDto('de', 'German', true, true, ['de', 'en']),
new AppLanguageDto('en', 'English', false, false, undefined)
]);
authService.verifyAll();
@ -88,13 +90,13 @@ describe('AppLanguagesService', () => {
});
expect(language).toEqual(
new AppLanguageDto('de', 'German', false));
new AppLanguageDto('de', 'German', false, false, undefined));
authService.verifyAll();
});
it('should make put request to make master language', () => {
const dto = new UpdateAppLanguageDto(true);
const dto = new UpdateAppLanguageDto(true, true, []);
authService.setup(x => x.authPut('http://service/p/api/apps/my-app/languages/de', dto, version))
.returns(() => Observable.of(

16
src/Squidex/app/shared/services/app-languages.service.ts

@ -17,7 +17,9 @@ export class AppLanguageDto {
constructor(
public readonly iso2Code: string,
public readonly englishName: string,
public readonly isMasterLanguage: boolean
public readonly isMaster: boolean,
public readonly isOptional: boolean,
public readonly fallback: string[]
) {
}
}
@ -31,7 +33,9 @@ export class AddAppLanguageDto {
export class UpdateAppLanguageDto {
constructor(
public readonly isMasterLanguage: boolean
public readonly isMaster: boolean,
public readonly isOptional: boolean,
public readonly fallback: string[]
) {
}
}
@ -56,7 +60,9 @@ export class AppLanguagesService {
return new AppLanguageDto(
item.iso2Code,
item.englishName,
item.isMasterLanguage === true);
item.isMaster === true,
item.isOptional === true,
item.fallback);
});
})
.catchError('Failed to load languages. Please reload.');
@ -71,7 +77,9 @@ export class AppLanguagesService {
return new AppLanguageDto(
response.iso2Code,
response.englishName,
response.isMasterLanguage === true);
response.isMaster === true,
response.isOptional === true,
response.fallback);
})
.catchError('Failed to add language. Please reload.');
}

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

@ -167,7 +167,7 @@ body {
}
.btn {
&-simple {
&-decent {
& {
background: transparent;
color: $color-border-dark;
@ -230,12 +230,16 @@ body {
}
&-link {
& {
@include link-button($color-theme-blue);
}
&.btn-danger {
@include link-button($color-theme-error);
}
&.btn-primary {
@include link-button($color-theme-blue);
&.btn-success {
@include link-button($color-theme-green);
}
}
}

4
src/Squidex/app/theme/_common.scss

@ -77,6 +77,10 @@ h1 {
display: none;
}
.invisible {
visibility: hidden;
}
.item-modified {
font-size: .8rem;
}

2
tests/Squidex.Core.Tests/ContentValidationTests.cs

@ -141,7 +141,7 @@ namespace Squidex.Core
public async Task Should_not_add_error_if_required_field_has_no_value_for_optional_language()
{
var optionalConfig =
LanguagesConfig.Create(Language.ES, Language.IT).Update(Language.IT, true, null);
LanguagesConfig.Create(Language.ES, Language.IT).Update(Language.IT, true, false, null);
schema = schema.AddOrUpdateField(new StringField(1, "my-field", new StringFieldProperties { IsLocalizable = true, IsRequired = true }));

2
tests/Squidex.Core.Tests/Contents/ContentDataTests.cs

@ -276,7 +276,7 @@ namespace Squidex.Core.Contents
var fallbackConfig =
LanguagesConfig.Create(Language.DE).Add(Language.EN)
.Update(Language.DE, false, new[] { Language.EN });
.Update(Language.DE, false, false, new[] { Language.EN });
var output = (Dictionary<string, JToken>)data.ToLanguageModel(fallbackConfig, new List<Language> { Language.DE });

28
tests/Squidex.Core.Tests/LanguagesConfigTests.cs

@ -136,8 +136,8 @@ namespace Squidex.Core
LanguagesConfig.Create(Language.DE)
.Add(Language.IT)
.Add(Language.RU)
.Update(Language.DE, false, new[] { Language.RU, Language.IT })
.Update(Language.RU, false, new[] { Language.DE, Language.IT })
.Update(Language.DE, false, false, new[] { Language.RU, Language.IT })
.Update(Language.RU, false, false, new[] { Language.DE, Language.IT })
.Remove(Language.IT);
config.ToList().ShouldBeEquivalentTo(
@ -167,7 +167,7 @@ namespace Squidex.Core
[Fact]
public void Should_update_language()
{
var config = LanguagesConfig.Create(Language.DE).Add(Language.IT).Update(Language.IT, true, new[] { Language.DE });
var config = LanguagesConfig.Create(Language.DE).Add(Language.IT).Update(Language.IT, true, false, new[] { Language.DE });
config.ToList().ShouldBeEquivalentTo(
new List<LanguageConfig>
@ -177,12 +177,20 @@ namespace Squidex.Core
});
}
[Fact]
public void Should_also_set_make_master_when_updating_language()
{
var config = LanguagesConfig.Create(Language.DE).Add(Language.IT).Update(Language.IT, true, true, null);
Assert.Equal(Language.IT, config.Master.Language);
}
[Fact]
public void Should_throw_exception_if_language_to_update_is_not_found()
{
var config = LanguagesConfig.Create(Language.DE);
Assert.Throws<DomainObjectNotFoundException>(() => config.Update(Language.EN, true, null));
Assert.Throws<DomainObjectNotFoundException>(() => config.Update(Language.EN, true, false, null));
}
[Fact]
@ -190,7 +198,7 @@ namespace Squidex.Core
{
var config = LanguagesConfig.Create(Language.DE);
Assert.Throws<ValidationException>(() => config.Update(Language.DE, true, new [] { Language.EN }));
Assert.Throws<ValidationException>(() => config.Update(Language.DE, true, false, new[] { Language.EN }));
}
[Fact]
@ -198,7 +206,15 @@ namespace Squidex.Core
{
var config = LanguagesConfig.Create(Language.DE);
Assert.Throws<ValidationException>(() => config.Update(Language.DE, true, null));
Assert.Throws<ValidationException>(() => config.Update(Language.DE, true, false, null));
}
[Fact]
public void Should_throw_exception_if_language_to_make_optional_must_be_set_to_master()
{
var config = LanguagesConfig.Create(Language.DE).Add(Language.IT);
Assert.Throws<ValidationException>(() => config.Update(Language.DE, true, true, null));
}
[Fact]

5
tests/Squidex.Infrastructure.Tests/TypeNameRegistryTests.cs

@ -7,6 +7,7 @@
// ==========================================================================
using System;
using System.Collections.Generic;
using System.Reflection;
using Xunit;
@ -83,13 +84,13 @@ namespace Squidex.Infrastructure
[Fact]
public void Should_throw_if_name_is_not_supported()
{
Assert.Throws<ArgumentException>(() => sut.GetType("unsupported"));
Assert.Throws<TypeNameNotFoundException>(() => sut.GetType("unsupported"));
}
[Fact]
public void Should_throw_if_type_is_not_supported()
{
Assert.Throws<ArgumentException>(() => sut.GetName<Guid>());
Assert.Throws<TypeNameNotFoundException>(() => sut.GetName<Guid>());
}
}
}

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

@ -213,20 +213,6 @@ namespace Squidex.Write.Apps
});
}
[Fact]
public async Task SetMasterLanguage_should_update_domain_object()
{
CreateApp()
.AddLanguage(CreateCommand(new AddLanguage { Language = language }));
var context = CreateContextForCommand(new SetMasterLanguage { Language = language });
await TestUpdate(app, async _ =>
{
await sut.HandleAsync(context);
});
}
[Fact]
public async Task UpdateLanguage_should_update_domain_object()
{

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

@ -442,51 +442,6 @@ namespace Squidex.Write.Apps
);
}
[Fact]
public void SetMasterLanguage_should_throw_if_not_created()
{
Assert.Throws<DomainException>(() =>
{
sut.SetMasterLanguage(CreateCommand(new SetMasterLanguage { Language = Language.EN }));
});
}
[Fact]
public void SetMasterLanguage_should_throw_if_command_is_not_valid()
{
CreateApp();
Assert.Throws<ValidationException>(() =>
{
sut.SetMasterLanguage(CreateCommand(new SetMasterLanguage()));
});
}
[Fact]
public void SetMasterLanguage_should_throw_if_language_not_found()
{
CreateApp();
Assert.Throws<DomainObjectNotFoundException>(() =>
{
sut.SetMasterLanguage(CreateCommand(new SetMasterLanguage { Language = Language.DE }));
});
}
[Fact]
public void SetMasterLanguage_should_create_events()
{
CreateApp();
CreateLanguage(Language.DE);
sut.SetMasterLanguage(CreateCommand(new SetMasterLanguage { Language = Language.DE }));
sut.GetUncomittedEvents()
.ShouldHaveSameEvents(
CreateEvent(new AppMasterLanguageSet { Language = Language.DE })
);
}
[Fact]
public void UpdateLanguage_should_throw_if_not_created()
{

Loading…
Cancel
Save