Browse Source

Language configuration fixed

pull/1/head
Sebastian 9 years ago
parent
commit
335d8f709b
  1. 4
      src/Squidex.Read/Apps/IAppEntity.cs
  2. 4
      src/Squidex.Read/Apps/Services/Implementations/CachingAppProvider.cs
  3. 5
      src/Squidex.Store.MongoDb/Apps/MongoAppClientEntity.cs
  4. 24
      src/Squidex.Store.MongoDb/Apps/MongoAppEntity.cs
  5. 24
      src/Squidex.Store.MongoDb/Apps/MongoAppRepository.cs
  6. 21
      src/Squidex.Write/Apps/AppCommandHandler.cs
  7. 2
      src/Squidex.Write/Apps/AppLanguages.cs
  8. 2
      src/Squidex/Config/Identity/LazyClientStore.cs
  9. 6
      src/Squidex/Controllers/Api/Apps/AppClientsController.cs
  10. 11
      src/Squidex/Controllers/Api/Apps/AppLanguagesController.cs
  11. 32
      src/Squidex/Controllers/Api/Apps/Models/AppLanguageDto.cs
  12. 10
      src/Squidex/Controllers/Api/LanguageDto.cs
  13. BIN
      src/Squidex/app-libs/icomoon/fonts/icomoon.eot
  14. 2
      src/Squidex/app-libs/icomoon/fonts/icomoon.svg
  15. BIN
      src/Squidex/app-libs/icomoon/fonts/icomoon.ttf
  16. BIN
      src/Squidex/app-libs/icomoon/fonts/icomoon.woff
  17. 61
      src/Squidex/app-libs/icomoon/selection.json
  18. 16
      src/Squidex/app-libs/icomoon/style.css
  19. 80
      src/Squidex/app/components/internal/app/settings/client.component.html
  20. 42
      src/Squidex/app/components/internal/app/settings/client.component.scss
  21. 127
      src/Squidex/app/components/internal/app/settings/client.component.ts
  22. 112
      src/Squidex/app/components/internal/app/settings/clients-page.component.html
  23. 18
      src/Squidex/app/components/internal/app/settings/clients-page.component.scss
  24. 55
      src/Squidex/app/components/internal/app/settings/clients-page.component.ts
  25. 53
      src/Squidex/app/components/internal/app/settings/contributors-page.component.html
  26. 54
      src/Squidex/app/components/internal/app/settings/contributors-page.component.ts
  27. 72
      src/Squidex/app/components/internal/app/settings/languages-page.component.html
  28. 14
      src/Squidex/app/components/internal/app/settings/languages-page.component.scss
  29. 69
      src/Squidex/app/components/internal/app/settings/languages-page.component.ts
  30. 1
      src/Squidex/app/components/internal/declarations.ts
  31. 5
      src/Squidex/app/components/internal/module.ts
  32. 2
      src/Squidex/app/components/layout/app-form.component.ts
  33. 2
      src/Squidex/app/components/layout/apps-menu.component.html
  34. 2
      src/Squidex/app/framework/angular/drag-model.directive.ts
  35. 15
      src/Squidex/app/framework/angular/focus-on-change.directive.spec.ts
  36. 3
      src/Squidex/app/framework/angular/focus-on-change.directive.ts
  37. 51
      src/Squidex/app/framework/angular/focus-on-init.directive.spec.ts
  38. 29
      src/Squidex/app/framework/angular/focus-on-init.directive.ts
  39. 1
      src/Squidex/app/framework/declarations.ts
  40. 3
      src/Squidex/app/framework/module.ts
  41. 62
      src/Squidex/app/shared/services/app-clients.service.spec.ts
  42. 54
      src/Squidex/app/shared/services/app-clients.service.ts
  43. 2
      src/Squidex/app/shared/services/app-contributors.service.spec.ts
  44. 19
      src/Squidex/app/shared/services/app-contributors.service.ts
  45. 59
      src/Squidex/app/shared/services/app-languages.service.spec.ts
  46. 54
      src/Squidex/app/shared/services/app-languages.service.ts
  47. 53
      src/Squidex/app/shared/services/apps.service.spec.ts
  48. 30
      src/Squidex/app/shared/services/apps.service.ts
  49. 1
      src/Squidex/app/shared/services/auth.service.ts
  50. 56
      src/Squidex/app/shared/services/errors.ts
  51. 9
      src/Squidex/app/shared/services/languages.service.ts
  52. 8
      src/Squidex/app/shared/services/users.service.ts
  53. 91
      src/Squidex/app/theme/_bootstrap.scss
  54. 2
      src/Squidex/app/theme/_layout.scss
  55. 5
      src/Squidex/app/theme/_vars.scss
  56. 2
      src/Squidex/appsettings.json
  57. 45
      tests/Squidex.Write.Tests/Apps/AppCommandHandlerTests.cs
  58. 11
      tests/Squidex.Write.Tests/Apps/AppDomainObjectTests.cs

4
src/Squidex.Read/Apps/IAppEntity.cs

@ -15,10 +15,12 @@ namespace Squidex.Read.Apps
{
string Name { get; }
IEnumerable<Language> Languages { get; }
Language MasterLanguage { get; }
IEnumerable<IAppClientEntity> Clients { get; }
IEnumerable<IAppContributorEntity> Contributors { get; }
IEnumerable<Language> Languages { get; }
}
}

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

@ -69,7 +69,9 @@ namespace Squidex.Read.Apps.Services.Implementations
@event.Payload is AppClientAttached ||
@event.Payload is AppClientRevoked ||
@event.Payload is AppClientRenamed ||
@event.Payload is AppLanguageAdded)
@event.Payload is AppLanguageAdded ||
@event.Payload is AppLanguageRemoved ||
@event.Payload is AppMasterLanguageSet)
{
var appName = Cache.Get<string>(BuildNamesCacheKey(@event.Headers.AggregateId()));

5
src/Squidex.Store.MongoDb/Apps/MongoAppClientEntity.cs

@ -29,5 +29,10 @@ namespace Squidex.Store.MongoDb.Apps
[BsonRequired]
[BsonElement]
public string Name { get; set; }
string IAppClientEntity.Name
{
get { return Name ?? Id; }
}
}
}

24
src/Squidex.Store.MongoDb/Apps/MongoAppEntity.cs

@ -6,6 +6,7 @@
// All rights reserved.
// ==========================================================================
using System;
using System.Collections.Generic;
using System.Linq;
using MongoDB.Bson.Serialization.Attributes;
@ -23,7 +24,11 @@ namespace Squidex.Store.MongoDb.Apps
[BsonRequired]
[BsonElement]
public List<string> Languages { get; set; }
public string MasterLanguage { get; set; }
[BsonRequired]
[BsonElement]
public HashSet<string> Languages { get; set; }
[BsonRequired]
[BsonElement]
@ -33,11 +38,6 @@ namespace Squidex.Store.MongoDb.Apps
[BsonElement]
public Dictionary<string, MongoAppContributorEntity> Contributors { get; set; }
IEnumerable<Language> IAppEntity.Languages
{
get { return Languages.Select(Language.GetLanguage); }
}
IEnumerable<IAppClientEntity> IAppEntity.Clients
{
get { return Clients.Values; }
@ -48,11 +48,23 @@ namespace Squidex.Store.MongoDb.Apps
get { return Contributors.Values; }
}
IEnumerable<Language> IAppEntity.Languages
{
get { return Languages.Select(Language.GetLanguage); }
}
Language IAppEntity.MasterLanguage
{
get { return Language.GetLanguage(MasterLanguage); }
}
public MongoAppEntity()
{
Contributors = new Dictionary<string, MongoAppContributorEntity>();
Clients = new Dictionary<string, MongoAppClientEntity>();
Languages = new HashSet<string>();
}
}
}

24
src/Squidex.Store.MongoDb/Apps/MongoAppRepository.cs

@ -94,6 +94,30 @@ namespace Squidex.Store.MongoDb.Apps
});
}
protected Task On(AppLanguageAdded @event, EnvelopeHeaders headers)
{
return Collection.UpdateAsync(headers, a =>
{
a.Languages.Add(@event.Language.Iso2Code);
});
}
protected Task On(AppLanguageRemoved @event, EnvelopeHeaders headers)
{
return Collection.UpdateAsync(headers, a =>
{
a.Languages.Remove(@event.Language.Iso2Code);
});
}
protected Task On(AppMasterLanguageSet @event, EnvelopeHeaders headers)
{
return Collection.UpdateAsync(headers, a =>
{
a.MasterLanguage = @event.Language.Iso2Code;
});
}
protected Task On(AppContributorAssigned @event, EnvelopeHeaders headers)
{
return Collection.UpdateAsync(headers, a =>

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

@ -45,7 +45,7 @@ namespace Squidex.Write.Apps
if (await appRepository.FindAppByNameAsync(command.Name) != null)
{
var error =
new ValidationError($"A app with name '{command.Name}' already exists",
new ValidationError($"An app with name '{command.Name}' already exists",
nameof(CreateApp.Name));
throw new ValidationException("Cannot create a new app", error);
@ -64,7 +64,7 @@ namespace Squidex.Write.Apps
if (await userRepository.FindUserByIdAsync(command.ContributorId) == null)
{
var error =
new ValidationError($"Cannot find contributor '{command.ContributorId ?? "UNKNOWN"}'",
new ValidationError($"Cannot find contributor the contributor",
nameof(AssignContributor.ContributorId));
throw new ValidationException("Cannot assign contributor to app", error);
@ -100,7 +100,22 @@ namespace Squidex.Write.Apps
{
return handler.UpdateAsync<AppDomainObject>(command, x => x.RevokeClient(command));
}
protected Task On(AddLanguage command, CommandContext context)
{
return handler.UpdateAsync<AppDomainObject>(command, x => x.AddLanguage(command));
}
protected Task On(RemoveLanguage command, CommandContext context)
{
return handler.UpdateAsync<AppDomainObject>(command, x => x.RemoveLanguage(command));
}
protected Task On(SetMasterLanguage command, CommandContext context)
{
return handler.UpdateAsync<AppDomainObject>(command, x => x.SetMasterLanguage(command));
}
public Task<bool> HandleAsync(CommandContext context)
{
return context.IsHandled ? Task.FromResult(false) : this.DispatchActionAsync(context.Command, context);

2
src/Squidex.Write/Apps/AppLanguages.cs

@ -59,7 +59,7 @@ namespace Squidex.Write.Apps
{
if (languages.Contains(language))
{
var error = new ValidationError("Language id is alreay part of the app", "Language");
var error = new ValidationError("Language is already part of the app", "Language");
throw new ValidationException(message(), error);
}

2
src/Squidex/Config/Identity/LazyClientStore.cs

@ -71,7 +71,7 @@ namespace Squidex.Config.Identity
{
ClientId = id,
ClientName = id,
ClientSecrets = new List<Secret> { new Secret(appClient.Secret.Sha512(), appClient.ExpiresUtc) },
ClientSecrets = new List<Secret> { new Secret(appClient.Secret.Sha256(), appClient.ExpiresUtc) },
AccessTokenLifetime = (int)TimeSpan.FromDays(30).TotalSeconds,
AllowedGrantTypes = GrantTypes.ClientCredentials,
AllowedScopes = new List<string>

6
src/Squidex/Controllers/Api/Apps/AppClientsController.cs

@ -103,11 +103,11 @@ namespace Squidex.Controllers.Api.Apps
/// 404 => App not found or client not found.
/// </returns>
[HttpPut]
[Route("apps/{app}/clients/{client}/")]
[Route("apps/{app}/clients/{clientId}/")]
[ProducesResponseType(typeof(ClientDto[]), 201)]
public async Task<IActionResult> PutClient(string app, string clientId, [FromBody] RenameClientDto request)
{
await CommandBus.PublishAsync(SimpleMapper.Map(request, new RenameClient()));
await CommandBus.PublishAsync(SimpleMapper.Map(request, new RenameClient { Id = clientId }));
return NoContent();
}
@ -122,7 +122,7 @@ namespace Squidex.Controllers.Api.Apps
/// 204 => Client revoked.
/// </returns>
[HttpDelete]
[Route("apps/{app}/clients/{client}/")]
[Route("apps/{app}/clients/{clientId}/")]
public async Task<IActionResult> DeleteClient(string app, string clientId)
{
await CommandBus.PublishAsync(new RevokeClient { Id = clientId });

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

@ -58,7 +58,12 @@ namespace Squidex.Controllers.Api.Apps
return NotFound();
}
var model = entity.Languages.Select(x => SimpleMapper.Map(x, new LanguageDto())).ToList();
var model = entity.Languages.Select(x =>
{
var isMasterLanguage = x.Equals(entity.MasterLanguage);
return SimpleMapper.Map(x, new AppLanguageDto { IsMasterLanguage = isMasterLanguage });
}).ToList();
return Ok(model);
}
@ -75,13 +80,13 @@ namespace Squidex.Controllers.Api.Apps
/// </returns>
[HttpPost]
[Route("apps/{app}/languages/")]
[ProducesResponseType(typeof(LanguageDto), 201)]
[ProducesResponseType(typeof(AppLanguageDto), 201)]
[ProducesResponseType(typeof(ErrorDto), 400)]
public async Task<IActionResult> PostLanguage(string app, [FromBody] AddLanguageDto request)
{
await CommandBus.PublishAsync(SimpleMapper.Map(request, new AddLanguage()));
var response = SimpleMapper.Map(request.Language, new LanguageDto());
var response = SimpleMapper.Map(request.Language, new AppLanguageDto());
return StatusCode(201, response);
}

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

@ -0,0 +1,32 @@
// ==========================================================================
// AppLanguageDto.cs
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex Group
// All rights reserved.
// ==========================================================================
using System.ComponentModel.DataAnnotations;
namespace Squidex.Controllers.Api.Apps.Models
{
public class AppLanguageDto
{
/// <summary>
/// The iso code of the language.
/// </summary>
[Required]
public string Iso2Code { get; set; }
/// <summary>
/// The english name of the language.
/// </summary>
[Required]
public string EnglishName { get; set; }
/// <summary>
/// Indicates if the language is the master language.
/// </summary>
public bool IsMasterLanguage { get; set; }
}
}

10
src/Squidex/Controllers/Api/LanguageDto.cs

@ -6,12 +6,22 @@
// All rights reserved.
// ==========================================================================
using System.ComponentModel.DataAnnotations;
namespace Squidex.Controllers.Api
{
public class LanguageDto
{
/// <summary>
/// The iso code of the language.
/// </summary>
[Required]
public string Iso2Code { get; set; }
/// <summary>
/// The english name of the language.
/// </summary>
[Required]
public string EnglishName { get; set; }
}
}

BIN
src/Squidex/app-libs/icomoon/fonts/icomoon.eot

Binary file not shown.

2
src/Squidex/app-libs/icomoon/fonts/icomoon.svg

@ -9,8 +9,10 @@
<glyph unicode="&#x20;" horiz-adv-x="512" d="" />
<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" />
<glyph unicode="&#xe90e;" glyph-name="media" horiz-adv-x="1152" d="M1088 832h-64v64c0 35.2-28.8 64-64 64h-896c-35.2 0-64-28.8-64-64v-768c0-35.2 28.8-64 64-64h64v-64c0-35.2 28.8-64 64-64h896c35.2 0 64 28.8 64 64v768c0 35.2-28.8 64-64 64zM128 768v-640h-63.886c-0.040 0.034-0.082 0.076-0.114 0.116v767.77c0.034 0.040 0.076 0.082 0.114 0.114h895.77c0.040-0.034 0.082-0.076 0.116-0.116v-63.884h-768c-35.2 0-64-28.8-64-64v0zM1088 0.116c-0.034-0.040-0.076-0.082-0.116-0.116h-895.77c-0.040 0.034-0.082 0.076-0.114 0.116v767.77c0.034 0.040 0.076 0.082 0.114 0.114h895.77c0.040-0.034 0.082-0.076 0.116-0.116v-767.768zM960 608c0-53.020-42.98-96-96-96s-96 42.98-96 96 42.98 96 96 96 96-42.98 96-96zM1024 64h-768v128l224 384 256-320h64l224 192z" />
<glyph unicode="&#xe925;" glyph-name="content" d="M917.806 602.924c-22.21 30.292-53.174 65.7-87.178 99.704s-69.412 64.964-99.704 87.178c-51.574 37.82-76.592 42.194-90.924 42.194h-368c-44.114 0-80-35.888-80-80v-736c0-44.112 35.886-80 80-80h608c44.112 0 80 35.888 80 80v496c0 14.332-4.372 39.35-42.194 90.924zM785.374 657.374c30.7-30.7 54.8-58.398 72.58-81.374h-153.954v153.946c22.982-17.78 50.678-41.878 81.374-72.572v0zM896 16c0-8.672-7.328-16-16-16h-608c-8.672 0-16 7.328-16 16v736c0 8.672 7.328 16 16 16 0 0 367.956 0.002 368 0v-224c0-17.672 14.324-32 32-32h224v-496zM602.924 917.804c-51.574 37.822-76.592 42.196-90.924 42.196h-368c-44.112 0-80-35.888-80-80v-736c0-38.632 27.528-70.958 64-78.39v814.39c0 8.672 7.328 16 16 16h486.876c-9.646 7.92-19.028 15.26-27.952 21.804z" />
<glyph unicode="&#xe92c;" glyph-name="copy" d="M640 704v256h-448l-192-192v-576h384v-256h640v768h-384zM192 869.49v-101.49h-101.49l101.49 101.49zM64 256v448h192v192h320v-192l-192-192v-256h-320zM576 613.49v-101.49h-101.49l101.49 101.49zM960 0h-512v448h192v192h320v-640z" />
<glyph unicode="&#xe92e;" glyph-name="schemas" d="M1024 640l-512 256-512-256 512-256 512 256zM512 811.030l342.058-171.030-342.058-171.030-342.058 171.030 342.058 171.030zM921.444 499.278l102.556-51.278-512-256-512 256 102.556 51.278 409.444-204.722zM921.444 307.278l102.556-51.278-512-256-512 256 102.556 51.278 409.444-204.722z" />
<glyph unicode="&#xe941;" glyph-name="lifebuoy" d="M512 960c-282.77 0-512-229.23-512-512s229.23-512 512-512 512 229.23 512 512-229.23 512-512 512zM320 448c0 106.040 85.96 192 192 192s192-85.96 192-192-85.96-192-192-192-192 85.96-192 192zM925.98 276.524v0l-177.42 73.49c12.518 30.184 19.44 63.276 19.44 97.986s-6.922 67.802-19.44 97.986l177.42 73.49c21.908-52.822 34.020-110.73 34.020-171.476s-12.114-118.654-34.020-171.476v0zM683.478 861.98v0 0l-73.49-177.42c-30.184 12.518-63.276 19.44-97.988 19.44s-67.802-6.922-97.986-19.44l-73.49 177.422c52.822 21.904 110.732 34.018 171.476 34.018 60.746 0 118.654-12.114 171.478-34.020zM98.020 619.476l177.422-73.49c-12.518-30.184-19.442-63.276-19.442-97.986s6.922-67.802 19.44-97.986l-177.42-73.49c-21.906 52.822-34.020 110.73-34.020 171.476s12.114 118.654 34.020 171.476zM340.524 34.020l73.49 177.42c30.184-12.518 63.276-19.44 97.986-19.44s67.802 6.922 97.986 19.44l73.49-177.42c-52.822-21.904-110.73-34.020-171.476-34.020-60.744 0-118.654 12.114-171.476 34.020z" />
<glyph unicode="&#xe94d;" glyph-name="history" horiz-adv-x="1088" d="M640 896c247.424 0 448-200.576 448-448s-200.576-448-448-448v96c94.024 0 182.418 36.614 248.902 103.098s103.098 154.878 103.098 248.902c0 94.022-36.614 182.418-103.098 248.902s-154.878 103.098-248.902 103.098c-94.022 0-182.418-36.614-248.902-103.098-51.14-51.138-84.582-115.246-97.306-184.902h186.208l-224-256-224 256h164.57c31.060 217.102 217.738 384 443.43 384zM832 512v-128h-256v320h128v-192z" />

Before

Width:  |  Height:  |  Size: 9.0 KiB

After

Width:  |  Height:  |  Size: 9.5 KiB

BIN
src/Squidex/app-libs/icomoon/fonts/icomoon.ttf

Binary file not shown.

BIN
src/Squidex/app-libs/icomoon/fonts/icomoon.woff

Binary file not shown.

61
src/Squidex/app-libs/icomoon/selection.json

@ -1,6 +1,35 @@
{
"IcoMoonType": "selection",
"icons": [
{
"icon": {
"paths": [
"M864 0c88.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 736l-64 288 288-64 592-592-224-224-592 592zM715.578 363.578l-448 448-55.156-55.156 448-448 55.156 55.156z"
],
"attrs": [],
"isMulticolor": false,
"isMulticolor2": false,
"tags": [
"pencil",
"write",
"edit"
],
"defaultCode": 59653,
"grid": 16
},
"attrs": [],
"properties": {
"ligatures": "pencil, write",
"name": "pencil",
"id": 5,
"order": 14,
"prevSize": 32,
"code": 59653
},
"setIdx": 0,
"setId": 2,
"iconIdx": 5
},
{
"icon": {
"paths": [
@ -66,6 +95,38 @@
"setId": 2,
"iconIdx": 37
},
{
"icon": {
"paths": [
"M640 256v-256h-448l-192 192v576h384v256h640v-768h-384zM192 90.51v101.49h-101.49l101.49-101.49zM64 704v-448h192v-192h320v192l-192 192v256h-320zM576 346.51v101.49h-101.49l101.49-101.49zM960 960h-512v-448h192v-192h320v640z"
],
"attrs": [],
"isMulticolor": false,
"isMulticolor2": false,
"tags": [
"copy",
"duplicate",
"files",
"pages",
"papers",
"documents"
],
"defaultCode": 59692,
"grid": 16
},
"attrs": [],
"properties": {
"ligatures": "copy, duplicate",
"name": "copy",
"id": 44,
"order": 13,
"prevSize": 32,
"code": 59692
},
"setIdx": 0,
"setId": 2,
"iconIdx": 44
},
{
"icon": {
"paths": [

16
src/Squidex/app-libs/icomoon/style.css

@ -1,10 +1,10 @@
@font-face {
font-family: 'icomoon';
src: url('fonts/icomoon.eot?7p76ib');
src: url('fonts/icomoon.eot?7p76ib#iefix') format('embedded-opentype'),
url('fonts/icomoon.ttf?7p76ib') format('truetype'),
url('fonts/icomoon.woff?7p76ib') format('woff'),
url('fonts/icomoon.svg?7p76ib#icomoon') format('svg');
src: url('fonts/icomoon.eot?qb9mo3');
src: url('fonts/icomoon.eot?qb9mo3#iefix') format('embedded-opentype'),
url('fonts/icomoon.ttf?qb9mo3') format('truetype'),
url('fonts/icomoon.woff?qb9mo3') format('woff'),
url('fonts/icomoon.svg?qb9mo3#icomoon') format('svg');
font-weight: normal;
font-style: normal;
}
@ -24,12 +24,18 @@
-moz-osx-font-smoothing: grayscale;
}
.icon-pencil:before {
content: "\e905";
}
.icon-media:before {
content: "\e90e";
}
.icon-content:before {
content: "\e925";
}
.icon-copy:before {
content: "\e92c";
}
.icon-schemas:before {
content: "\e92e";
}

80
src/Squidex/app/components/internal/app/settings/client.component.html

@ -0,0 +1,80 @@
<div class="table-items-row">
<table class="table table-middle table-sm table-borderless table-fixed client-info">
<colgroup>
<col style="width: 160px; text-align: right;" />
<col style="width: 100%" />
<col style="width: 40px" />
</colgroup>
<tr>
<td colspan="2">
<div class="float-xs-right">
<button class="btn btn-default" (click)="createToken(client)">Create Token</button>
</div>
<div class="client-name">
<form *ngIf="isRenaming" class="form-inline" (submit)="rename()">
<div class="form-group">
<input type="text" class="form-control" name="client-name" id="client-name" sqxFocusOnInit [(ngModel)]="client.name" />
</div>
<button type="submit" class="btn btn-primary">Save</button>
<a class="btn btn-default" (click)="cancelRename()">Cancel</a>
</form>
<h3 *ngIf="!isRenaming">
{{client.name}} <i class="client-edit icon-pencil" (click)="startRename()"></i>
</h3>
</div>
<div class="client-expires">Expires: {{client.expiresUtc}}</div>
</td>
<td class="client-delete">
<button type="button" class="btn btn-link btn-danger" (click)="revokeClient(client)">
<i class="icon-bin"></i>
</button>
</td>
</tr>
<tr>
<td>Client Id:</td>
<td>
<input readonly class="form-control" #inputId [attr.value]="appName + ':' + client.id" />
</td>
<td>
<button type="button" class="btn btn-primary btn-link" (click)="copyId()">
<i class="icon-copy"></i>
</button>
</td>
</tr>
<tr>
<td>Client Secret:</td>
<td>
<input readonly class="form-control" name="inputSecret" [attr.value]="client.secret" />
</td>
<td>
<button type="button" class="btn btn-primary btn-link" (click)="copySecret()">
<i class="icon-copy"></i>
</button>
</td>
</tr>
</table>
</div>
<div class="modal" *sqxModalView="modalDialog" [@fade]>
<div class="modal-dialog" role="document">
<div class="modal-content">
<div class="modal-header">
<button type="button" class="close" data-dismiss="modal" aria-label="Close" (click)="modalDialog.hide()">
<span aria-hidden="true">&times;</span>
</button>
<h4 class="modal-title">Access Token</h4>
</div>
<div class="modal-body">
<textarea class="form-control access-token">{{appClientToken.tokenType}} {{appClientToken.accessToken}}</textarea>
</div>
</div>
</div>
</div>

42
src/Squidex/app/components/internal/app/settings/client.component.scss

@ -0,0 +1,42 @@
@import '_vars';
@import '_mixins';
.client {
&-info {
margin: 0;
}
&-delete {
vertical-align: top;
}
&-expires {
font-size: .8rem;
}
&-edit {
color: darken($color-border, 20%);
display: none;
font-size: .9rem;
font-weight: normal;
padding: .3rem;
background: transparent;
}
&-name {
& {
height: 40px;
}
&:hover {
.client-edit {
display: inline-block;
}
}
}
}
.access-token {
resize: none;
height: 300px;
}

127
src/Squidex/app/components/internal/app/settings/client.component.ts

@ -0,0 +1,127 @@
/*
* Squidex Headless CMS
*
* @license
* Copyright (c) Sebastian Stehle. All rights reserved
*/
import * as Ng2 from '@angular/core';
import * as Ng2Forms from '@angular/forms';
import {
AccessTokenDto,
AppsStoreService,
AppClientDto,
AppClientCreateDto,
AppClientsService,
fadeAnimation,
ModalView,
Notification,
NotificationService,
TitleService
} from 'shared';
@Ng2.Component({
selector: 'sqx-client',
styles,
template,
animations: [
fadeAnimation
]
})
export class ClientComponent {
private oldName: string;
public isRenaming = false;
public appClientToken: AccessTokenDto;
@Ng2.Input('appName')
public appName: string;
@Ng2.Input('client')
public client: AppClientDto;
@Ng2.ViewChild('inputId')
public inputId: Ng2.ElementRef;
@Ng2.ViewChild('inputSecret')
public inputSecret: Ng2.ElementRef;
public modalDialog = new ModalView();
constructor(
private readonly appClientsService: AppClientsService,
private readonly notifications: NotificationService
) {
}
public rename() {
this.appClientsService.renameClient(this.appName, this.client.id, this.client.name)
.subscribe(() => {
this.stopRename();
}, error => {
this.notifications.notify(Notification.error(error.displayMessage));
this.cancelRename();
});
}
public cancelRename() {
this.client.name = this.oldName;
this.isRenaming = false;
}
public stopRename() {
this.client.name = this.client.name || this.client.id;
this.isRenaming = false;
}
public startRename() {
this.oldName = this.client.name;
this.isRenaming = true;
}
public createToken(client: AppClientDto) {
this.appClientsService.createToken(this.appName, client)
.subscribe(token => {
this.appClientToken = token;
this.modalDialog.show();
}, error => {
this.notifications.notify(Notification.error('Failed to retrieve access token. Please retry.'));
});
}
public copyId() {
this.copyToClipbord(this.inputId.nativeElement);
}
public copySecret() {
this.copyToClipbord(this.inputSecret.nativeElement);
}
private copyToClipbord(element: HTMLInputElement | HTMLTextAreaElement) {
const currentFocus: any = document.activeElement;
const prevSelectionStart = element.selectionStart;
const prevSelectionEnd = element.selectionEnd;
element.focus();
element.setSelectionRange(0, element.value.length);
try {
document.execCommand('copy');
} catch (e) {
console.log('Copy failed');
}
if (currentFocus && typeof currentFocus.focus === 'function') {
currentFocus.focus();
}
element.setSelectionRange(prevSelectionStart, prevSelectionEnd);
}
}

112
src/Squidex/app/components/internal/app/settings/clients-page.component.html

@ -9,101 +9,37 @@
</h1>
</div>
<div class="layout-middle-content">
<div class="card">
<div class="card-block">
<div class="clients-empty" *ngIf="appClients && appClients.length === 0">
No client created yet.
</div>
<table class="table table-borderless-top table-fixed clients-table">
<colgroup>
<col style="width: 100%" />
<col style="width: 160px" />
</colgroup>
<div class="table-items-row" *ngIf="appClients && appClients.length === 0">
No client created yet.
</div>
<tr *ngFor="let client of appClients">
<td>
<table class="table table-middle table-sm table-borderless table-fixed client-info">
<colgroup>
<col style="width: 160px; text-align: right;" />
<col style="width: 100%" />
</colgroup>
<tr>
<td>Client Name:</td>
<td>
<input readonly class="form-control" [attr.value]="fullAppName(client)" />
</td>
</tr>
<tr>
<td>Client Secret:</td>
<td>
<input readonly class="form-control" [attr.value]="client.secret" />
</td>
</tr>
<tr>
<td>Expires:</td>
<td class="client-expires">
{{client.expiresUtc}}
</td>
</tr>
</table>
<div *ngFor="let client of appClients">
<sqx-client [client]="client" [appName]="appName"></sqx-client>
</div>
</td>
<td>
<button class="btn btn-block btn-danger client-delete" (click)="revokeClient(client)">Revoke</button>
<button class="btn btn-block btn-default" (click)="createToken(client)">Create Token</button>
</td>
</tr>
</table>
</div>
<div class="card-footer">
<div *ngIf="creationError" [@fade]>
<div class="form-error">
{{creationError}}
<div class="table-items-footer">
<form class="form-inline" [formGroup]="createForm" (submit)="attachClient()">
<div class="errors-box" *ngIf="createForm.get('name').invalid && createForm.get('name').dirty">
<div class="errors">
<span *ngIf="createForm.get('name').hasError('required')">
Name is required.
</span>
<span *ngIf="createForm.get('name').hasError('maxlength')">
Name can not have more than 40 characters.
</span>
<span *ngIf="createForm.get('name').hasError('pattern')">
Name can contain lower case letters (a-z), numbers and dashes (not at the end).
</span>
</div>
</div>
<form class="form-inline" [formGroup]="createForm" (submit)="attachClient()">
<div class="errors-box" *ngIf="createForm.get('name').invalid && createForm.get('name').dirty">
<div class="errors">
<span *ngIf="createForm.get('name').hasError('required')">
Name is required.
</span>
<span *ngIf="createForm.get('name').hasError('maxlength')">
Name can not have more than 40 characters.
</span>
<span *ngIf="createForm.get('name').hasError('pattern')">
Name can contain lower case letters (a-z), numbers and dashes (not at the end).
</span>
</div>
</div>
<div class="form-group">
<input type="text" class="form-control" id="app-name" formControlName="name" placeholder="Enter client name" />
</div>
<div class="form-group">
<input type="text" class="form-control" id="app-name" formControlName="name" placeholder="Enter client name" />
</div>
<button type="submit" class="btn btn-success" [disabled]="createForm.invalid">Add Client</button>
</form>
</div>
<button type="submit" class="btn btn-success" [disabled]="createForm.invalid">Add Client</button>
</form>
</div>
</div>
</div>
</div>
<div class="modal ng-animate" *sqxModalView="modalDialog" [@fade]>
<div class="modal-dialog" role="document">
<div class="modal-content">
<div class="modal-header">
<button type="button" class="close" data-dismiss="modal" aria-label="Close" (click)="modalDialog.hide()">
<span aria-hidden="true">&times;</span>
</button>
<h4 class="modal-title">Access Token</h4>
</div>
<div class="modal-body">
<textarea class="form-control access-token">{{appClientToken.tokenType}} {{appClientToken.accessToken}}</textarea>
</div>
</div>
</div>
</div>

18
src/Squidex/app/components/internal/app/settings/clients-page.component.scss

@ -23,24 +23,6 @@
font-weight: lighter;
padding: .2rem 1rem;
}
&-table {
margin: 0;
}
}
.client {
&-delete {
margin-top: .3rem;
}
&-info {
margin: 0;
}
&-expires {
padding: .3rem .8rem;
}
}
.access-token {

55
src/Squidex/app/components/internal/app/settings/clients-page.component.ts

@ -9,13 +9,10 @@ import * as Ng2 from '@angular/core';
import * as Ng2Forms from '@angular/forms';
import {
AccessTokenDto,
AppsStoreService,
AppClientDto,
AppClientCreateDto,
AppClientsService,
fadeAnimation,
ModalView,
Notification,
NotificationService,
TitleService
@ -24,21 +21,14 @@ import {
@Ng2.Component({
selector: 'sqx-clients-page',
styles,
template,
animations: [
fadeAnimation
]
template
})
export class ClientsPageComponent implements Ng2.OnInit {
private appSubscription: any | null = null;
private appName: string | null = null;
public modalDialog = new ModalView();
public appClients: AppClientDto[];
public appClientToken: AccessTokenDto;
public creationError = '';
public createForm =
this.formBuilder.group({
name: ['',
@ -58,6 +48,10 @@ export class ClientsPageComponent implements Ng2.OnInit {
) {
}
public ngOnDestroy() {
this.appSubscription.unsubscribe();
}
public ngOnInit() {
this.appSubscription =
this.appsStore.selectedApp.subscribe(app => {
@ -66,23 +60,19 @@ export class ClientsPageComponent implements Ng2.OnInit {
this.titles.setTitle('{appName} | Settings | Clients', { appName: app.name });
this.appClientsService.getClients(app.name)
.subscribe(clients => {
this.appClients = clients;
}, () => {
this.notifications.notify(Notification.error('Failed to load clients. Please reload squidex portal.'));
this.appClients = [];
});
this.load();
}
});
}
public ngOnDestroy() {
this.appSubscription.unsubscribe();
}
public fullAppName(client: AppClientDto): string {
return this.appName + ':' + client.id;
public load() {
this.appClientsService.getClients(this.appName)
.subscribe(clients => {
this.appClients = clients;
}, error => {
this.notifications.notify(Notification.error(error.displayMessage));
this.appClients = [];
});
}
public revokeClient(client: AppClientDto) {
@ -90,17 +80,7 @@ export class ClientsPageComponent implements Ng2.OnInit {
.subscribe(() => {
this.appClients.splice(this.appClients.indexOf(client), 1);
}, error => {
this.notifications.notify(Notification.error('Failed to revoke client. Please retry.'));
});
}
public createToken(client: AppClientDto) {
this.appClientsService.createToken(this.appName, client)
.subscribe(token => {
this.appClientToken = token;
this.modalDialog.show();
}, error => {
this.notifications.notify(Notification.error('Failed to retrieve access token. Please retry.'));
this.notifications.notify(Notification.error(error.displayMessage));
});
}
@ -114,11 +94,11 @@ export class ClientsPageComponent implements Ng2.OnInit {
this.appClientsService.postClient(this.appName, dto)
.subscribe(client => {
this.reset();
this.appClients.push(client);
this.reset();
}, error => {
this.notifications.notify(Notification.error(error.displayMessage));
this.reset();
this.creationError = error;
});
}
}
@ -126,7 +106,6 @@ export class ClientsPageComponent implements Ng2.OnInit {
private reset() {
this.createForm.reset();
this.createForm.enable();
this.creationError = '';
}
}

53
src/Squidex/app/components/internal/app/settings/contributors-page.component.html

@ -15,32 +15,35 @@
<col style="width: 50%" />
<col style="width: 50%" />
<col style="width: 150px" />
<col style="width: 60px" />
<col style="width: 80px" />
</colgroup>
<template ngFor let-contributor [ngForOf]="appContributors">
<tr>
<td>
<img class="user-picture" [attr.src]="pictureUrl(contributor) | async" />
</td>
<td>
<span class="user-name">{{displayName(contributor) | async}}</span>
</td>
<td>
<span class="user-email">{{email(contributor) | async}}</span>
</td>
<td>
<select class="form-control" [(ngModel)]="contributor.permission" (ngModelChange)="saveContributor(contributor)" [disabled]="currrentUserId === contributor.contributorId">
<option *ngFor="let permission of usersPermissions">{{permission}}</option>
</select>
</td>
<td>
<button class="btn btn-link btn-danger" [disabled]="currrentUserId === contributor.contributorId" (click)="removeContributor(contributor)">
<i class="icon-bin"></i>
</button>
</td>
</tr>
<tr class="spacer"></tr>
</template>
<tbody>
<template ngFor let-contributor [ngForOf]="appContributors">
<tr>
<td>
<img class="user-picture" [attr.src]="pictureUrl(contributor) | async" />
</td>
<td>
<span class="user-name">{{displayName(contributor) | async}}</span>
</td>
<td>
<span class="user-email">{{email(contributor) | async}}</span>
</td>
<td>
<select class="form-control" [(ngModel)]="contributor.permission" (ngModelChange)="changePermission(contributor)" [disabled]="currrentUserId === contributor.contributorId">
<option *ngFor="let permission of usersPermissions">{{permission}}</option>
</select>
</td>
<td>
<button type="button" class="btn btn-link btn-danger" [disabled]="currrentUserId === contributor.contributorId" (click)="removeContributor(contributor)">
<i class="icon-bin"></i>
</button>
</td>
</tr>
<tr class="spacer"></tr>
</template>
</tbody>
</table>
<div class="table-items-footer">

54
src/Squidex/app/components/internal/app/settings/contributors-page.component.ts

@ -75,7 +75,7 @@ export class ContributorsPageComponent implements Ng2.OnInit {
private appSubscription: any | null = null;
private appName: string;
public appContributors: AppContributorDto[];
public appContributors: AppContributorDto[] = [];
public selectedUserName: string | null = null;
public selectedUser: UserDto | null = null;
@ -101,6 +101,10 @@ export class ContributorsPageComponent implements Ng2.OnInit {
this.usersDataSource = new UsersDataSource(usersService, this);
}
public ngOnDestroy() {
this.appSubscription.unsubscribe();
}
public ngOnInit() {
this.currrentUserId = this.authService.user.id;
@ -111,18 +115,18 @@ export class ContributorsPageComponent implements Ng2.OnInit {
this.titles.setTitle('{appName} | Settings | Contributors', { appName: app.name });
this.appContributorsService.getContributors(app.name).retry(2)
.subscribe(contributors => {
this.appContributors = contributors;
}, error => {
this.notifications.notify(Notification.error('Failed to load app contributors. Please reload squidex portal.'));
});
this.load();
}
});
}
public ngOnDestroy() {
this.appSubscription.unsubscribe();
public load() {
this.appContributorsService.getContributors(this.appName).retry(2)
.subscribe(contributors => {
this.appContributors = contributors;
}, error => {
this.notifications.notify(Notification.error(error.displayMessage));
});
}
public assignContributor() {
@ -132,34 +136,30 @@ export class ContributorsPageComponent implements Ng2.OnInit {
const contributor = new AppContributorDto(this.selectedUser.id, 'Editor');
this.appContributorsService.postContributor(this.appName, contributor)
.catch(error => {
this.notifications.notify(Notification.error('Failed to assign contributors. Please retry.'));
return Observable.of(true);
}).subscribe();
this.appContributors.push(contributor);
this.selectedUser = null;
this.selectedUserName = null;
this.appContributorsService.postContributor(this.appName, contributor)
.subscribe(() => {
this.appContributors.push(contributor);
}, error => {
this.notifications.notify(Notification.error(error.displayMessage));
});
}
public removeContributor(contributor: AppContributorDto) {
this.appContributorsService.deleteContributor(this.appName, contributor.contributorId)
.catch(error => {
this.notifications.notify(Notification.error('Failed to remove contributors. Please retry.'));
return Observable.of(true);
}).subscribe();
this.appContributors.splice(this.appContributors.indexOf(contributor), 1);
.subscribe(() => {
this.appContributors.splice(this.appContributors.indexOf(contributor), 1);
}, error => {
this.notifications.notify(Notification.error(error.displayMessage));
});
}
public saveContributor(contributor: AppContributorDto) {
public changePermission(contributor: AppContributorDto) {
this.appContributorsService.postContributor(this.appName, contributor)
.catch(error => {
this.notifications.notify(Notification.error('Failed to update contributors. Please retry.'));
this.notifications.notify(Notification.error(error.displayMessage));
return Observable.of(true);
}).subscribe();

72
src/Squidex/app/components/internal/app/settings/languages-page.component.html

@ -4,25 +4,36 @@
</div>
<div class="layout-middle">
<div class="layout-middle-header">
<div class="float-xs-right">
<button class="btn btn-primary" (click)="saveLanguages()" [disabled]="isSaving">{{isSaving ? 'Saving...' : 'Save Changes'}}</button>
</div>
<h1>
<i class="layout-title-icon icon-settings"></i> Languages
</h1>
</div>
<div class="layout-middle-content">
<div class="card">
<div class="card-block">
<table class="table table-borderless-top table-fixed languages-table" dnd-sortable-container [sortableData]="appLanguages">
<colgroup>
<col style="width: 60px" />
<col style="width: 100%" />
<col style="width: 110px" />
</colgroup>
<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>
Options
</th>
</tr>
</thead>
<tr *ngFor="let language of appLanguages; let i = index" dnd-sortable [sortableIndex]="i">
<tbody>
<template ngFor let-language [ngForOf]="appLanguages">
<tr>
<td>
<span class="language-code">
{{language.iso2Code}}
@ -34,22 +45,31 @@
</span>
</td>
<td>
<button class="btn btn-block btn-sm btn-danger" [disabled]="i === 0" (click)="removeLanguage(language)">Remove</button>
<label class="language-default">
<input type="radio" [value]="true" [ngModel]="language.isMasterLanguage" (ngModelChange)="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-bin"></i>
</button>
</td>
</tr>
</table>
</div>
<div class="card-footer">
<form class="form-inline" (submit)="addLanguage()" name="addLanguageForm">
<div class="form-group">
<select class="form-control language-select" [(ngModel)]="selectedLanguage" name="newLanguage">
<option *ngFor="let language of allLanguages" [ngValue]="language">{{language.englishName}}</option>
</select>
</div>
<tr class="spacer"></tr>
</template>
</tbody>
</table>
<div class="table-items-footer">
<form class="form-inline" (submit)="addLanguage()" name="addLanguageForm">
<div class="form-group">
<select class="form-control language-select" [(ngModel)]="selectedLanguage" name="newLanguage">
<option *ngFor="let language of allLanguages" [ngValue]="language">{{language.englishName}}</option>
</select>
</div>
<button type="submit" class="btn btn-success" [disabled]="!selectedLanguage">Add</button>
</form>
</div>
<button type="submit" class="btn btn-success" [disabled]="!selectedLanguage">Add Language</button>
</form>
</div>
</div>
</div>

14
src/Squidex/app/components/internal/app/settings/languages-page.component.scss

@ -5,16 +5,6 @@
color: $color-section-settings;
}
.card {
max-width: 700px;
}
.languages {
&-table {
margin: 0;
}
}
.language {
&-select {
max-width: 200px;
@ -23,4 +13,8 @@
&-name {
@include truncate;
}
&-default {
margin: 0;
}
}

69
src/Squidex/app/components/internal/app/settings/languages-page.component.ts

@ -8,6 +8,7 @@
import * as Ng2 from '@angular/core';
import {
AppLanguageDto,
AppLanguagesService,
AppsStoreService,
LanguageDto,
@ -26,11 +27,10 @@ export class LanguagesPageComponent implements Ng2.OnInit {
private appSubscription: any | null = null;
private appName: string;
public allLanguages: LanguageDto[] = null;
public appLanguages: LanguageDto[] = [];
public selectedLanguage: LanguageDto | null = null;
public allLanguages: LanguageDto[] = [];
public appLanguages: AppLanguageDto[] = [];
public isSaving: boolean;
public selectedLanguage: LanguageDto | null = null;
public get newLanguages() {
return this.allLanguages.filter(x => !this.appLanguages.find(l => l.iso2Code === x.iso2Code));
@ -45,12 +45,16 @@ export class LanguagesPageComponent implements Ng2.OnInit {
) {
}
public ngOnDestroy() {
this.appSubscription.unsubscribe();
}
public ngOnInit() {
this.languagesService.getLanguages().retry(2)
.subscribe(languages => {
this.allLanguages = languages;
}, error => {
this.notifications.notify(Notification.error('Failed to load languages. Please reload squidex portal.'));
this.notifications.notify(Notification.error(error.displayMessage));
});
this.appSubscription =
@ -60,42 +64,51 @@ export class LanguagesPageComponent implements Ng2.OnInit {
this.titles.setTitle('{appName} | Settings | Languages', { appName: app.name });
this.appLanguagesService.getLanguages(app.name).retry(2)
.subscribe(appLanguages => {
this.appLanguages = appLanguages;
}, error => {
this.notifications.notify(Notification.error('Failed to load app languages. Please reload squidex portal.'));
});
this.load();
}
});
}
public ngOnDestroy() {
this.appSubscription.unsubscribe();
public load() {
this.appLanguagesService.getLanguages(this.appName).retry(2)
.subscribe(appLanguages => {
this.appLanguages = appLanguages;
}, error => {
this.notifications.notify(Notification.error(error.displayMessage));
});
}
public removeLanguage(language: LanguageDto) {
this.appLanguages.splice(this.appLanguages.indexOf(language), 1);
public setMasterLanguage(selectedLanguage: AppLanguageDto) {
for (let language of this.appLanguages) {
language.isMasterLanguage = false;
}
this.appLanguagesService.makeMasterLanguage(this.appName, selectedLanguage.iso2Code)
.subscribe(() => {
selectedLanguage.isMasterLanguage = true;
}, error => {
this.notifications.notify(Notification.error(error.displayMessage));
});
}
public addLanguage() {
this.appLanguages.push(this.selectedLanguage);
this.appLanguagesService.postLanguages(this.appName, this.selectedLanguage.iso2Code)
.subscribe(appLanguage => {
this.appLanguages.push(appLanguage);
}, error => {
this.notifications.notify(Notification.error(error.displayMessage));
});
this.selectedLanguage = null;
}
public saveLanguages() {
this.isSaving = true;
this.appLanguagesService.postLanguages(this.appName, this.appLanguages.map(l => l.iso2Code))
.delay(500)
.subscribe(() => {
this.isSaving = false;
}, error => {
this.isSaving = false;
this.notifications.notify(Notification.error('Failed to save app languages. Please retry.'));
});
public removeLanguage(selectedLanguage: AppLanguageDto) {
this.appLanguagesService.deleteLanguage(this.appName, selectedLanguage.iso2Code)
.subscribe(appLanguage => {
this.appLanguages.splice(this.appLanguages.indexOf(appLanguage), 1);
}, error => {
this.notifications.notify(Notification.error(error.displayMessage));
});
}
}

1
src/Squidex/app/components/internal/declarations.ts

@ -10,6 +10,7 @@ export * from './app/app-area.component';
export * from './app/left-menu.component';
export * from './app/dashboard/dashboard-page.component';
export * from './app/schemas/schemas-page.component';
export * from './app/settings/client.component';
export * from './app/settings/clients-page.component';
export * from './app/settings/contributors-page.component';
export * from './app/settings/languages-page.component';

5
src/Squidex/app/components/internal/module.ts

@ -9,14 +9,13 @@ import * as Ng2 from '@angular/core';
import { Ng2CompleterModule } from 'ng2-completer';
import { DndModule } from 'ng2-dnd';
import { SqxFrameworkModule } from 'shared';
import { SqxLayoutModule } from 'components/layout';
import {
AppAreaComponent,
AppsPageComponent,
ClientComponent,
ClientsPageComponent,
ContributorsPageComponent,
DashboardPageComponent,
@ -28,7 +27,6 @@ import {
@Ng2.NgModule({
imports: [
DndModule,
Ng2CompleterModule,
SqxFrameworkModule,
SqxLayoutModule
@ -39,6 +37,7 @@ import {
declarations: [
AppAreaComponent,
AppsPageComponent,
ClientComponent,
ClientsPageComponent,
ContributorsPageComponent,
DashboardPageComponent,

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

@ -73,7 +73,7 @@ export class AppFormComponent implements Ng2.OnInit {
this.created.emit(app);
}, error => {
this.reset();
this.creationError = error;
this.creationError = error.displayMessage;
});
}
}

2
src/Squidex/app/components/layout/apps-menu.component.html

@ -23,7 +23,7 @@
</li>
</ul>
<div class="modal ng-animate" *sqxModalView="modalDialog" [@fade]>
<div class="modal" *sqxModalView="modalDialog" [@fade]>
<div class="modal-dialog" role="document">
<div class="modal-content">
<div class="modal-header">

2
src/Squidex/app/framework/angular/drag-model.directive.ts

@ -11,7 +11,7 @@ import { DragService } from './../services/drag.service';
import { Vec2 } from './../utils/vec2';
@Ng2.Directive({
selector: '[gpDragModel]'
selector: '[sqxDragModel]'
})
export class DragModelDirective {
private startOffset: Vec2;

15
src/Squidex/app/framework/angular/focus-on-change.directive.spec.ts

@ -19,13 +19,13 @@ describe('FocusOnChangeDirective', () => {
});
it('should call focus on element when value changes', (done: any) => {
let calledMethod: any;
let calledElement: any;
const calledMethods: string[] = [];
const calledElements: any[] = [];
const renderer = {
invokeElementMethod: (element: any, method: any, args: any) => {
calledElement = element;
calledMethod = method;
calledElements.push(element);
calledMethods.push(method);
}
};
@ -35,12 +35,11 @@ describe('FocusOnChangeDirective', () => {
new FocusOnChangeDirective(element, renderer as Ng2.Renderer).ngOnChanges({});
expect(calledMethod).not.toBeDefined();
expect(calledElement).not.toBeDefined();
expect(calledMethods).toEqual([]);
setTimeout(() => {
expect(calledMethod).toBe('focus');
expect(calledElement).toBe(element.nativeElement);
expect(calledMethods).toEqual(['focus', 'select']);
expect(calledElements).toEqual([element.nativeElement, element.nativeElement]);
done();
}, 400);

3
src/Squidex/app/framework/angular/focus-on-change.directive.ts

@ -8,7 +8,7 @@
import * as Ng2 from '@angular/core';
@Ng2.Directive({
selector: '[gpFocusOnChange]'
selector: '[sqxFocusOnChange]'
})
export class FocusOnChangeDirective implements Ng2.OnChanges {
@Ng2.Input()
@ -23,6 +23,7 @@ export class FocusOnChangeDirective implements Ng2.OnChanges {
public ngOnChanges(changes: { [key: string]: Ng2.SimpleChange }) {
setTimeout(() => {
this.renderer.invokeElementMethod(this.elementRef.nativeElement, 'focus', []);
this.renderer.invokeElementMethod(this.elementRef.nativeElement, 'select', []);
}, 100);
}
}

51
src/Squidex/app/framework/angular/focus-on-init.directive.spec.ts

@ -0,0 +1,51 @@
/*
* Squidex Headless CMS
*
* @license
* Copyright (c) Sebastian Stehle. All rights reserved
*/
import * as Ng2 from '@angular/core';
import { FocusOnInitDirective } from './focus-on-init.directive';
describe('FocusOnInitDirective', () => {
let originalTimeout = 0;
beforeEach(() => {
originalTimeout = jasmine.DEFAULT_TIMEOUT_INTERVAL;
jasmine.DEFAULT_TIMEOUT_INTERVAL = 800;
});
it('should call focus on element when init', (done: any) => {
const calledMethods: string[] = [];
const calledElements: any[] = [];
const renderer = {
invokeElementMethod: (element: any, method: any, args: any) => {
calledElements.push(element);
calledMethods.push(method);
}
};
const element: Ng2.ElementRef = {
nativeElement: {}
};
new FocusOnInitDirective(element, renderer as Ng2.Renderer).ngOnInit();
expect(calledMethods).toEqual([]);
setTimeout(() => {
expect(calledMethods).toEqual(['focus', 'select']);
expect(calledElements).toEqual([element.nativeElement, element.nativeElement]);
done();
}, 400);
});
afterEach(() => {
jasmine.DEFAULT_TIMEOUT_INTERVAL = originalTimeout;
});
});

29
src/Squidex/app/framework/angular/focus-on-init.directive.ts

@ -0,0 +1,29 @@
/*
* Squidex Headless CMS
*
* @license
* Copyright (c) Sebastian Stehle. All rights reserved
*/
import * as Ng2 from '@angular/core';
@Ng2.Directive({
selector: '[sqxFocusOnInit]'
})
export class FocusOnInitDirective implements Ng2.OnInit {
@Ng2.Input()
public gpFocusOnChange: any;
constructor(
private readonly elementRef: Ng2.ElementRef,
private readonly renderer: Ng2.Renderer
) {
}
public ngOnInit() {
setTimeout(() => {
this.renderer.invokeElementMethod(this.elementRef.nativeElement, 'focus', []);
this.renderer.invokeElementMethod(this.elementRef.nativeElement, 'select', []);
});
}
}

1
src/Squidex/app/framework/declarations.ts

@ -13,6 +13,7 @@ export * from './angular/color-picker.component';
export * from './angular/date-time.pipes';
export * from './angular/drag-model.directive';
export * from './angular/focus-on-change.directive';
export * from './angular/focus-on-init.directive';
export * from './angular/image-drop.directive';
export * from './angular/modal-view.directive';
export * from './angular/money.pipe';

3
src/Squidex/app/framework/module.ts

@ -19,6 +19,7 @@ import {
DragModelDirective,
DurationPipe,
FocusOnChangeDirective,
FocusOnInitDirective,
ImageDropDirective,
ModalViewDirective,
MoneyPipe,
@ -47,6 +48,7 @@ import {
DragModelDirective,
DurationPipe,
FocusOnChangeDirective,
FocusOnInitDirective,
ImageDropDirective,
ModalViewDirective,
MoneyPipe,
@ -66,6 +68,7 @@ import {
DragModelDirective,
DurationPipe,
FocusOnChangeDirective,
FocusOnInitDirective,
ImageDropDirective,
ModalViewDirective,
MoneyPipe,

62
src/Squidex/app/shared/services/app-clients.service.spec.ts

@ -35,12 +35,12 @@ describe('AppClientsService', () => {
new Ng2Http.ResponseOptions({
body: [{
id: 'client1',
name: 'Client1',
name: 'Client 1',
secret: 'secret1',
expiresUtc: '2016-12-12T10:10'
}, {
id: 'client2',
name: 'Client2',
name: 'Client 2',
secret: 'secret2',
expiresUtc: '2016-11-11T10:10'
}]
@ -57,8 +57,8 @@ describe('AppClientsService', () => {
expect(clients).toEqual(
[
new AppClientDto('client1', 'Client1', 'secret1', DateTime.parseISO_UTC('2016-12-12T10:10')),
new AppClientDto('client2', 'Client2', 'secret2', DateTime.parseISO_UTC('2016-11-11T10:10')),
new AppClientDto('client1', 'secret1', 'Client 1', DateTime.parseISO_UTC('2016-12-12T10:10')),
new AppClientDto('client2', 'secret2', 'Client 2', DateTime.parseISO_UTC('2016-11-11T10:10')),
]);
authService.verifyAll();
@ -67,13 +67,13 @@ describe('AppClientsService', () => {
it('should make post request to create client', () => {
const createClient = new AppClientCreateDto('client1');
authService.setup(x => x.authPost('http://service/p/api/apps/my-app/clients', TypeMoq.It.is(c => c === createClient)))
authService.setup(x => x.authPost('http://service/p/api/apps/my-app/clients', TypeMoq.It.isValue(createClient)))
.returns(() => Observable.of(
new Ng2Http.Response(
new Ng2Http.ResponseOptions({
body: {
id: 'client1',
name: 'Client1',
name: 'Client 1',
secret: 'secret1',
expiresUtc: '2016-12-12T10:10'
}
@ -89,53 +89,21 @@ describe('AppClientsService', () => {
});
expect(client).toEqual(
new AppClientDto('client1', 'Client1', 'secret1', DateTime.parseISO_UTC('2016-12-12T10:10')));
new AppClientDto('client1', 'secret1', 'Client 1', DateTime.parseISO_UTC('2016-12-12T10:10')));
authService.verifyAll();
});
it('should throw fallback error on 500 when creating client failed', () => {
const createClient = new AppClientCreateDto('client1');
authService.setup(x => x.authPost('http://service/p/api/apps/my-app/clients', TypeMoq.It.is(c => c === createClient)))
.returns(() => Observable.throw(
new Ng2Http.Response(
new Ng2Http.ResponseOptions({
status: 500
})
)
));
let error = '';
appClientsService.postClient('my-app', createClient).subscribe(x => {}, result => {
error = result;
}).unsubscribe();
expect(error).toBe('A new client could not be created.');
authService.verifyAll();
});
it('should throw duplicate error on 400 when creating client failed', () => {
const createClient = new AppClientCreateDto('client1');
authService.setup(x => x.authPost('http://service/p/api/apps/my-app/clients', TypeMoq.It.is(c => c === createClient)))
.returns(() => Observable.throw(
new Ng2Http.Response(
new Ng2Http.ResponseOptions({
status: 400
})
it('should make put request to rename client', () => {
authService.setup(x => x.authPut('http://service/p/api/apps/my-app/clients/client1', TypeMoq.It.isValue({ name: 'Client 1' })))
.returns(() => Observable.of(
new Ng2Http.Response(
new Ng2Http.ResponseOptions()
)
));
let error = '';
appClientsService.postClient('my-app', createClient).subscribe(x => {}, result => {
error = result;
}).unsubscribe();
))
.verifiable(TypeMoq.Times.once());
expect(error).toBe('A client with the same name already exists.');
appClientsService.renameClient('my-app', 'client1', 'Client 1');
authService.verifyAll();
});

54
src/Squidex/app/shared/services/app-clients.service.ts

@ -13,11 +13,13 @@ import { Observable } from 'rxjs';
import { ApiUrlConfig, DateTime } from 'framework';
import { AuthService } from './auth.service';
import { handleError } from './errors';
export class AppClientDto {
constructor(
public readonly id: string,
public readonly name: string,
public readonly secret: string,
public name: string,
public readonly expiresUtc: DateTime
) {
}
@ -48,32 +50,51 @@ export class AppClientsService {
}
public getClients(appName: string): Observable<AppClientDto[]> {
return this.authService.authGet(this.apiUrl.buildUrl(`api/apps/${appName}/clients`))
const url = this.apiUrl.buildUrl(`api/apps/${appName}/clients`);
return this.authService.authGet(url)
.map(response => response.json())
.map(response => {
const items: any[] = response;
return items.map(item => {
return new AppClientDto(item.id, item.name, item.secret, DateTime.parseISO_UTC(item.expiresUtc));
return new AppClientDto(
item.id,
item.secret,
item.name,
DateTime.parseISO_UTC(item.expiresUtc));
});
});
})
.catch(response => handleError('Failed to load clients. Please reload.', response));
}
public postClient(appName: string, client: AppClientCreateDto): Observable<AppClientDto> {
return this.authService.authPost(this.apiUrl.buildUrl(`api/apps/${appName}/clients`), client)
const url = this.apiUrl.buildUrl(`api/apps/${appName}/clients`);
return this.authService.authPost(url, client)
.map(response => response.json())
.map(response => new AppClientDto(response.id, response.name, response.secret, DateTime.parseISO_UTC(response.expiresUtc)))
.catch(response => {
if (response.status === 400) {
return Observable.throw('A client with the same name already exists.');
} else {
return Observable.throw('A new client could not be created.');
}
});
.map(response => {
return new AppClientDto(
response.id,
response.secret,
response.name,
DateTime.parseISO_UTC(response.expiresUtc));
})
.catch(response => handleError('Failed to add client. Please reload.', response));
}
public deleteClient(appName: string, name: string): Observable<any> {
return this.authService.authDelete(this.apiUrl.buildUrl(`api/apps/${appName}/clients/${name}`));
public renameClient(appName: string, id: string, name: string): Observable<any> {
const url = this.apiUrl.buildUrl(`api/apps/${appName}/clients/${id}`);
return this.authService.authPut(url, { name: name })
.catch(response => handleError('Failed to revoke client. Please reload.', response));
}
public deleteClient(appName: string, id: string): Observable<any> {
const url = this.apiUrl.buildUrl(`api/apps/${appName}/clients/${id}`);
return this.authService.authDelete(url)
.catch(response => handleError('Failed to revoke client. Please reload.', response));
}
public createToken(appName: string, client: AppClientDto): Observable<AccessTokenDto> {
@ -84,8 +105,9 @@ export class AppClientsService {
});
const body = `grant_type=client_credentials&scope=squidex-api&client_id=${appName}:${client.id}&client_secret=${client.secret}`;
const url = this.apiUrl.buildUrl('identity-server/connect/token');
return this.http.post(this.apiUrl.buildUrl('identity-server/connect/token'), body, options)
return this.http.post(url, body, options)
.map(response => response.json())
.map(response => new AccessTokenDto(response.access_token, response.token_type));
}

2
src/Squidex/app/shared/services/app-contributors.service.spec.ts

@ -61,7 +61,7 @@ describe('AppContributorsService', () => {
it('should make post request to assign contributor', () => {
const contributor = new AppContributorDto('123', 'Owner');
authService.setup(x => x.authPost('http://service/p/api/apps/my-app/contributors', TypeMoq.It.is(c => c === contributor)))
authService.setup(x => x.authPost('http://service/p/api/apps/my-app/contributors', TypeMoq.It.isValue(contributor)))
.returns(() => Observable.of(
new Ng2Http.Response(
new Ng2Http.ResponseOptions()

19
src/Squidex/app/shared/services/app-contributors.service.ts

@ -12,6 +12,8 @@ import { Observable } from 'rxjs';
import { ApiUrlConfig } from 'framework';
import { AuthService } from './auth.service';
import { handleError } from './errors';
export class AppContributorDto {
constructor(
public readonly contributorId: string,
@ -29,7 +31,9 @@ export class AppContributorsService {
}
public getContributors(appName: string): Observable<AppContributorDto[]> {
return this.authService.authGet(this.apiUrl.buildUrl(`api/apps/${appName}/contributors`))
const url = this.apiUrl.buildUrl(`api/apps/${appName}/contributors`);
return this.authService.authGet(url)
.map(response => response.json())
.map(response => {
const items: any[] = response;
@ -39,14 +43,21 @@ export class AppContributorsService {
item.contributorId,
item.permission);
});
});
})
.catch(response => handleError('Failed to load contributors. Please reload.', response));
}
public postContributor(appName: string, contributor: AppContributorDto): Observable<any> {
return this.authService.authPost(this.apiUrl.buildUrl(`api/apps/${appName}/contributors`), contributor);
const url = this.apiUrl.buildUrl(`api/apps/${appName}/contributors`);
return this.authService.authPost(url, contributor)
.catch(response => handleError('Failed to add contributors. Please reload.', response));
}
public deleteContributor(appName: string, contributorId: string): Observable<any> {
return this.authService.authDelete(this.apiUrl.buildUrl(`api/apps/${appName}/contributors/${contributorId}`));
const url = this.apiUrl.buildUrl(`api/apps/${appName}/contributors/${contributorId}`);
return this.authService.authDelete(url)
.catch(response => handleError('Failed to delete contributors. Please reload.', response));
}
}

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

@ -12,9 +12,9 @@ import { Observable } from 'rxjs';
import {
ApiUrlConfig,
AppLanguageDto,
AppLanguagesService,
AuthService,
LanguageDto,
} from './../';
describe('AppLanguagesService', () => {
@ -33,7 +33,8 @@ describe('AppLanguagesService', () => {
new Ng2Http.ResponseOptions({
body: [{
iso2Code: 'de',
englishName: 'German'
englishName: 'German',
isMasterLanguage: true
}, {
iso2Code: 'en',
englishName: 'English'
@ -43,7 +44,7 @@ describe('AppLanguagesService', () => {
))
.verifiable(TypeMoq.Times.once());
let languages: LanguageDto[] = null;
let languages: AppLanguageDto[] = null;
appLanguagesService.getLanguages('my-app').subscribe(result => {
languages = result;
@ -51,17 +52,57 @@ describe('AppLanguagesService', () => {
expect(languages).toEqual(
[
new LanguageDto('de', 'German'),
new LanguageDto('en', 'English'),
new AppLanguageDto('de', 'German', true),
new AppLanguageDto('en', 'English', false),
]);
authService.verifyAll();
});
it('should make post request to configure languages', () => {
const languages = ['de', 'en'];
it('should make post request to add language', () => {
const newLanguage = 'de';
authService.setup(x => x.authPost('http://service/p/api/apps/my-app/languages', TypeMoq.It.is(y => y['language'] === newLanguage)))
.returns(() => Observable.of(
new Ng2Http.Response(
new Ng2Http.ResponseOptions({
body: {
iso2Code: 'de',
englishName: 'German'
}
})
)
))
.verifiable(TypeMoq.Times.once());
let language: AppLanguageDto;
appLanguagesService.postLanguages('my-app', newLanguage).subscribe(result => {
language = result;
});
expect(language).toEqual(
new AppLanguageDto('de', 'German', false));
authService.verifyAll();
});
it('should make put request to make master language', () => {
authService.setup(x => x.authPut('http://service/p/api/apps/my-app/languages/de', TypeMoq.It.isValue({ isMasterLanguage: true })))
.returns(() => Observable.of(
new Ng2Http.Response(
new Ng2Http.ResponseOptions()
)
))
.verifiable(TypeMoq.Times.once());
appLanguagesService.makeMasterLanguage('my-app', 'de');
authService.verifyAll();
});
authService.setup(x => x.authPost('http://service/p/api/apps/my-app/languages', TypeMoq.It.is(y => y['languages'] === languages)))
it('should make delete request to remove language', () => {
authService.setup(x => x.authDelete('http://service/p/api/apps/my-app/languages/de'))
.returns(() => Observable.of(
new Ng2Http.Response(
new Ng2Http.ResponseOptions()
@ -69,7 +110,7 @@ describe('AppLanguagesService', () => {
))
.verifiable(TypeMoq.Times.once());
appLanguagesService.postLanguages('my-app', languages);
appLanguagesService.deleteLanguage('my-app', 'de');
authService.verifyAll();
});

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

@ -11,7 +11,17 @@ import { Observable } from 'rxjs';
import { ApiUrlConfig } from 'framework';
import { AuthService } from './auth.service';
import { LanguageDto } from './languages.service';
import { handleError } from './errors';
export class AppLanguageDto {
constructor(
public readonly iso2Code: string,
public readonly englishName: string,
public isMasterLanguage: boolean
) {
}
}
@Ng2.Injectable()
export class AppLanguagesService {
@ -21,21 +31,49 @@ export class AppLanguagesService {
) {
}
public getLanguages(appName: string): Observable<LanguageDto[]> {
return this.authService.authGet(this.apiUrl.buildUrl(`api/apps/${appName}/languages`))
public getLanguages(appName: string): Observable<AppLanguageDto[]> {
const url = this.apiUrl.buildUrl(`api/apps/${appName}/languages`);
return this.authService.authGet(url)
.map(response => response.json())
.map(response => {
const items: any[] = response;
return items.map(item => {
return new LanguageDto(
return new AppLanguageDto(
item.iso2Code,
item.englishName);
item.englishName,
item.isMasterLanguage === true);
});
});
})
.catch(response => handleError('Failed to load languages. Please reload', response));
}
public postLanguages(appName: string, languageCodes: string[]): Observable<any> {
return this.authService.authPost(this.apiUrl.buildUrl(`api/apps/${appName}/languages`), { languages: languageCodes });
public postLanguages(appName: string, languageCode: string): Observable<AppLanguageDto> {
const url = this.apiUrl.buildUrl(`api/apps/${appName}/languages`);
return this.authService.authPost(url, { language: languageCode })
.map(response => response.json())
.map(response => {
return new AppLanguageDto(
response.iso2Code,
response.englishName,
response.isMasterLanguage === true);
})
.catch(response => handleError('Failed to add language. Please reload.', response));
}
public makeMasterLanguage(appName: string, languageCode: string): Observable<any> {
const url = this.apiUrl.buildUrl(`api/apps/${appName}/languages/${languageCode}`);
return this.authService.authPut(url, { isMasterLanguage: true })
.catch(response => handleError('Failed to change language. Please reload.', response));
}
public deleteLanguage(appName: string, languageCode: string): Observable<AppLanguageDto> {
const url = this.apiUrl.buildUrl(`api/apps/${appName}/languages/${languageCode}`);
return this.authService.authDelete(url)
.catch(response => handleError('Failed to add language. Please reload.', response));
}
}

53
src/Squidex/app/shared/services/apps.service.spec.ts

@ -69,7 +69,7 @@ describe('AppsService', () => {
const createApp = new AppCreateDto('new-app');
const now = DateTime.now();
authService.setup(x => x.authPost('http://service/p/api/apps', TypeMoq.It.is(a => a === createApp)))
authService.setup(x => x.authPost('http://service/p/api/apps', TypeMoq.It.isValue(createApp)))
.returns(() => Observable.of(
new Ng2Http.Response(
new Ng2Http.ResponseOptions({
@ -78,8 +78,9 @@ describe('AppsService', () => {
}
})
)
));
))
.verifiable(TypeMoq.Times.once());
let newApp: AppDto = null;
appsService.postApp(createApp, now).subscribe(result => {
@ -90,50 +91,4 @@ describe('AppsService', () => {
authService.verifyAll();
});
it('should throw fallback error on 500 when creating app failed', () => {
const createApp = new AppCreateDto('new-app');
authService.setup(x => x.authPost('http://service/p/api/apps', TypeMoq.It.is(a => a === createApp)))
.returns(() => Observable.throw(
new Ng2Http.Response(
new Ng2Http.ResponseOptions({
status: 500
})
)
));
let error = '';
appsService.postApp(createApp).subscribe(x => {}, result => {
error = result;
}).unsubscribe();
expect(error).toBe('A new app could not be created.');
authService.verifyAll();
});
it('should throw duplicate error on 400 when creating app failed', () => {
const createApp = new AppCreateDto('new-app');
authService.setup(x => x.authPost('http://service/p/api/apps', TypeMoq.It.is(a => a === createApp)))
.returns(() => Observable.throw(
new Ng2Http.Response(
new Ng2Http.ResponseOptions({
status: 400
})
)
));
let error = '';
appsService.postApp(createApp).subscribe(x => {}, result => {
error = result;
}).unsubscribe();
expect(error).toBe('An app with the same name already exists.');
authService.verifyAll();
});
});

30
src/Squidex/app/shared/services/apps.service.ts

@ -11,6 +11,8 @@ import { Observable } from 'rxjs';
import { ApiUrlConfig, DateTime } from 'framework';
import { AuthService } from './auth.service';
import { handleError } from './errors';
export class AppDto {
constructor(
public readonly id: string,
@ -38,7 +40,9 @@ export class AppsService {
}
public getApps(): Observable<AppDto[]> {
return this.authService.authGet(this.apiUrl.buildUrl('/api/apps'))
const url = this.apiUrl.buildUrl('/api/apps');
return this.authService.authGet(url)
.map(response => response.json())
.map(response => {
const items: any[] = response;
@ -52,21 +56,25 @@ export class AppsService {
item.permission
);
});
});
})
.catch(response => handleError('Failed to load apps. Please reload.', response));
}
public postApp(app: AppCreateDto, now?: DateTime): Observable<AppDto> {
now = now || DateTime.now();
return this.authService.authPost(this.apiUrl.buildUrl('api/apps'), app)
const url = this.apiUrl.buildUrl('api/apps');
return this.authService.authPost(url, app)
.map(response => response.json())
.map(response => new AppDto(response.id, app.name, now, now, 'Owner'))
.catch(response => {
if (response.status === 400) {
return Observable.throw('An app with the same name already exists.');
} else {
return Observable.throw('A new app could not be created.');
}
});
.map(response => {
return new AppDto(
response.id,
app.name,
now,
now,
'Owner');
})
.catch(response => handleError('Failed to create app. Please reload.', response));
}
}

1
src/Squidex/app/shared/services/auth.service.ts

@ -184,6 +184,7 @@ export class AuthService {
public authDelete(url: string, options?: Ng2Http.RequestOptions): Observable<Ng2Http.Response> {
options = this.setRequestOptions(options);
return this.checkResponse(this.http.delete(url, options));
}

56
src/Squidex/app/shared/services/errors.ts

@ -0,0 +1,56 @@
/*
* Squidex Headless CMS
*
* @license
* Copyright (c) Sebastian Stehle. All rights reserved
*/
import * as Ng2Http from '@angular/http';
import { Observable } from 'rxjs';
export class ErrorDto {
public get displayMessage(): string {
let result = this.message;
if (this.details && this.details.length > 0) {
const detailMessage = this.details[0];
const lastChar = result[result.length - 1];
if (lastChar !== '.' && lastChar !== ',') {
result += '.';
}
result += ' ';
result += detailMessage;
}
const lastChar = result[result.length - 1];
if (lastChar !== '.') {
result += '.';
}
return result;
}
constructor(
public readonly statusCode: number,
public readonly message: string,
public readonly details: string[] = []
) {
}
}
export function handleError(message: string, error: Ng2Http.Response | any) {
let result = new ErrorDto(500, message);
if (error instanceof Ng2Http.Response && error.status !== 500) {
const body = error.json();
result = new ErrorDto(error.status, body.message, body.details);
}
return Observable.throw(result);
}

9
src/Squidex/app/shared/services/languages.service.ts

@ -11,6 +11,8 @@ import { Observable } from 'rxjs';
import { ApiUrlConfig } from 'framework';
import { AuthService } from './auth.service';
import { handleError } from './errors';
export class LanguageDto {
constructor(
public readonly iso2Code: string,
@ -28,7 +30,9 @@ export class LanguageService {
}
public getLanguages(): Observable<LanguageDto[]> {
return this.authService.authGet(this.apiUrl.buildUrl('api/languages'))
const url = this.apiUrl.buildUrl('api/languages');
return this.authService.authGet(url)
.map(response => response.json())
.map(response => {
const items: any[] = response;
@ -38,6 +42,7 @@ export class LanguageService {
item.iso2Code,
item.englishName);
});
});
})
.catch(response => handleError('Failed to load languages. Please reload', response));
}
}

8
src/Squidex/app/shared/services/users.service.ts

@ -31,7 +31,9 @@ export class UsersService {
}
public getUsers(query?: string): Observable<UserDto[]> {
return this.authService.authGet(this.apiUrl.buildUrl(`api/users/?query=${query || ''}`))
const url = this.apiUrl.buildUrl(`api/users/?query=${query || ''}`);
return this.authService.authGet(url)
.map(response => response.json())
.map(response => {
const items: any[] = response;
@ -47,7 +49,9 @@ export class UsersService {
}
public getUser(id: string): Observable<UserDto> {
return this.authService.authGet(this.apiUrl.buildUrl(`api/users/${id}`))
const url = this.apiUrl.buildUrl(`api/users/${id}`);
return this.authService.authGet(url)
.map(response => response.json())
.map(response => {
return new UserDto(

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

@ -33,27 +33,61 @@
margin-bottom: .25rem;
}
th,
td {
padding: 10px 12px;
margin: 0;
margin-bottom: 10px;
vertical-align: middle;
& {
padding: 15px;
}
&:first-child {
padding-left: 20px;
}
&:last-child {
padding-right: 20px;
}
}
tr {
padding: 10px 12px;
background: $color-table;
border: 1px solid $color-border;
border-bottom: 2px solid $color-border;
margin-bottom: 10px;
thead {
th {
color: $color-table-header;
font-size: .8rem;
font-weight: normal;
border: 0;
padding-top: 0;
}
}
tbody {
td {
margin: 0;
margin-bottom: 10px;
vertical-align: middle;
}
tr {
background: $color-table;
border: 1px solid $color-border;
border-bottom: 3px solid $color-border;
margin-bottom: 10px;
}
}
&-row,
&-footer {
@include border-radius(.25rem);
padding: 10px 12px;
padding: 15px 20px;
background: $color-table-footer;
border: 1px solid $color-table-border;
border-bottom-width: 3px;
}
&-row {
background: $color-table;
border: 1px solid $color-border;
border-bottom: 2px solid $color-border;
margin-bottom: .25rem;
}
&-footer {
background: $color-table-footer;
}
.spacer {
@ -154,8 +188,35 @@
border: 0;
}
@mixin link-button($color) {
& {
color: $color;
}
&:focus {
outline-color: transparent;
}
&:disabled {
@include opacity(.4);
}
&:focus,
&:active,
&:hover {
color: darken($color, 8%);
border-color: transparent;
background: transparent;
text-decoration: none;
}
}
.btn-link {
&.btn-danger {
color: $color-theme-error;
@include link-button($color-theme-error);
}
&.btn-primary {
@include link-button($color-theme-blue);
}
}

2
src/Squidex/app/theme/_layout.scss

@ -21,7 +21,7 @@ h1 {
.layout {
& {
@include fixed(54px, 0, 0, 0);
@include absolute(54px, 0, 0, 0);
@include flex-box;
@include flex-flow(row);
}

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

@ -27,8 +27,13 @@ $color-theme-error: #f00;
$color-theme-error-dark: darken($color-theme-error, 5%);
$color-accent-dark: #fff;
$color-table: #fff;
$color-table-footer: #ecf2f6;
$color-table-border: #dbe4eb;
$color-table-header: #a0a0a0;
$color-card-footer: #fff;
$padding-layout-h: 30px;

2
src/Squidex/appsettings.json

@ -10,7 +10,7 @@
"eventStore": {
"ipAddress": "127.0.0.1",
"port": 1113,
"prefix": "squidex_v3",
"prefix": "squidex_v4",
"username": "admin",
"password": "changeit"
}

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

@ -34,6 +34,7 @@ namespace Squidex.Write.Apps
private readonly AppDomainObject app;
private readonly UserToken subjectId = new UserToken("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();
private readonly string clientSecret = Guid.NewGuid().ToString();
private readonly string clientName = "client";
@ -194,6 +195,50 @@ namespace Squidex.Write.Apps
});
}
[Fact]
public async Task AddLanguage_should_update_domain_object()
{
CreateApp();
var command = new AddLanguage { AggregateId = Id, Language = language };
var context = new CommandContext(command);
await TestUpdate(app, async _ =>
{
await sut.HandleAsync(context);
});
}
[Fact]
public async Task RemoveLanguage_should_update_domain_object()
{
CreateApp()
.AddLanguage(new AddLanguage { AggregateId = Id, Language = language });
var command = new RemoveLanguage { AggregateId = Id, Language = language };
var context = new CommandContext(command);
await TestUpdate(app, async _ =>
{
await sut.HandleAsync(context);
});
}
[Fact]
public async Task SetMasterLanguage_should_update_domain_object()
{
CreateApp()
.AddLanguage(new AddLanguage { AggregateId = Id, Language = language });
var command = new SetMasterLanguage { AggregateId = Id, Language = language };
var context = new CommandContext(command);
await TestUpdate(app, async _ =>
{
await sut.HandleAsync(context);
});
}
private AppDomainObject CreateApp()
{
app.Create(new CreateApp { Name = appName, User = subjectId });

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

@ -252,6 +252,17 @@ namespace Squidex.Write.Apps
Assert.Throws<DomainObjectNotFoundException>(() => sut.RenameClient(new RenameClient { Id = "not-found", Name = clientNewName }));
}
[Fact]
public void RenameClient_should_throw_if_same_client_name()
{
CreateApp();
sut.AttachClient(new AttachClient { Id = clientId }, clientSecret, expiresUtc);
sut.RenameClient(new RenameClient { Id = clientId, Name = clientNewName });
Assert.Throws<ValidationException>(() => sut.RenameClient(new RenameClient { Id = clientId, Name = clientNewName }));
}
[Fact]
public void RenameClient_should_create_events()
{

Loading…
Cancel
Save