Browse Source

App contributor management

pull/1/head
Sebastian 9 years ago
parent
commit
d0a7027a2f
  1. 1
      Squidex.sln.DotSettings
  2. 46
      src/Squidex.Core/Squidex.Core.csproj
  3. 63
      src/Squidex.Infrastructure/Squidex.Infrastructure.csproj
  4. 2
      src/Squidex.Read/Apps/IAppContributorEntity.cs
  5. 2
      src/Squidex.Read/Apps/Repositories/IAppRepository.cs
  6. 34
      src/Squidex.Read/Apps/Services/Implementations/CachingAppProvider.cs
  7. 8
      src/Squidex.Read/Schemas/Services/Implementations/CachingSchemaProvider.cs
  8. 2
      src/Squidex.Read/Users/Repositories/IUserRepository.cs
  9. 2
      src/Squidex.Store.MongoDb/Apps/MongoAppContributorEntity.cs
  10. 10
      src/Squidex.Store.MongoDb/Apps/MongoAppRepository.cs
  11. 4
      src/Squidex.Store.MongoDb/Users/MongoUserRepository.cs
  12. 2
      src/Squidex.Write/Apps/AppCommandHandler.cs
  13. 1
      src/Squidex/Configurations/Domain/ReadModule.cs
  14. 21
      src/Squidex/Modules/Api/Apps/AppContributorsController.cs
  15. 12
      src/Squidex/Modules/Api/Apps/AppController.cs
  16. 3
      src/Squidex/Modules/Api/Apps/Models/AppContributorDto.cs
  17. 3
      src/Squidex/Modules/Api/Apps/Models/AssignContributorDto.cs
  18. 6
      src/Squidex/Modules/Api/Apps/Models/ListAppDto.cs
  19. 13
      src/Squidex/Modules/Api/Schemas/SchemaFieldsController.cs
  20. 11
      src/Squidex/Modules/Api/Schemas/SchemasController.cs
  21. 4
      src/Squidex/Modules/Api/Users/Models/UserDto.cs
  22. 4
      src/Squidex/Modules/Api/Users/UsersController.cs
  23. 8
      src/Squidex/Modules/UI/Account/AccountController.cs
  24. 6
      src/Squidex/Pipeline/AppFilterAttribute.cs
  25. 2
      src/Squidex/Views/Account/Login.cshtml
  26. 2
      src/Squidex/app/app.module.ts
  27. 15
      src/Squidex/app/app.routes.ts
  28. 16
      src/Squidex/app/components/internal/app/dashboard/dashboard-page.component.html
  29. 32
      src/Squidex/app/components/internal/app/left-menu.component.html
  30. 102
      src/Squidex/app/components/internal/app/left-menu.component.scss
  31. 39
      src/Squidex/app/components/internal/app/left-menu.component.ts
  32. 20
      src/Squidex/app/components/internal/app/schemas/schemas-page.component.html
  33. 67
      src/Squidex/app/components/internal/app/settings/contributors-page.component.html
  34. 28
      src/Squidex/app/components/internal/app/settings/contributors-page.component.scss
  35. 162
      src/Squidex/app/components/internal/app/settings/contributors-page.component.ts
  36. 14
      src/Squidex/app/components/internal/app/settings/credentials-page.component.html
  37. 6
      src/Squidex/app/components/internal/app/settings/credentials-page.component.scss
  38. 39
      src/Squidex/app/components/internal/app/settings/credentials-page.component.ts
  39. 14
      src/Squidex/app/components/internal/app/settings/languages-page.component.html
  40. 6
      src/Squidex/app/components/internal/app/settings/languages-page.component.scss
  41. 39
      src/Squidex/app/components/internal/app/settings/languages-page.component.ts
  42. 3
      src/Squidex/app/components/internal/declarations.ts
  43. 9
      src/Squidex/app/components/internal/module.ts
  44. 12
      src/Squidex/app/components/layout/app-form.component.html
  45. 2
      src/Squidex/app/components/layout/apps-menu.component.html
  46. 9
      src/Squidex/app/components/layout/apps-menu.component.scss
  47. 6
      src/Squidex/app/components/layout/profile-menu.component.html
  48. 43
      src/Squidex/app/components/layout/profile-menu.component.scss
  49. 25
      src/Squidex/app/framework/angular/animations.ts
  50. 13
      src/Squidex/app/framework/services/local-store.service.ts
  51. 16
      src/Squidex/app/shared/services/app-contributors.service.spec.ts
  52. 10
      src/Squidex/app/shared/services/app-contributors.service.ts
  53. 6
      src/Squidex/app/shared/services/apps-store.service.spec.ts
  54. 12
      src/Squidex/app/shared/services/apps.service.spec.ts
  55. 8
      src/Squidex/app/shared/services/apps.service.ts
  56. 8
      src/Squidex/app/shared/services/languages.service.spec.ts
  57. 6
      src/Squidex/app/shared/services/languages.service.ts
  58. 16
      src/Squidex/app/shared/services/users-provider.service.spec.ts
  59. 16
      src/Squidex/app/shared/services/users-provider.service.ts
  60. 43
      src/Squidex/app/shared/services/users.service.spec.ts
  61. 25
      src/Squidex/app/shared/services/users.service.ts
  62. 12
      src/Squidex/app/theme/_bootstrap.scss
  63. 39
      src/Squidex/app/theme/_completer.scss
  64. 67
      src/Squidex/app/theme/_layout.scss
  65. 28
      src/Squidex/app/theme/_mixins.scss
  66. 5
      src/Squidex/app/theme/_vars.scss
  67. 9
      src/Squidex/app/theme/_vendor-overrides.scss
  68. 3
      src/Squidex/app/theme/theme.scss
  69. 1
      src/Squidex/package.json

1
Squidex.sln.DotSettings

@ -90,4 +90,5 @@
<s:String x:Key="/Default/CodeStyle/Naming/XamlNaming/UserRules/=XAML_005FFIELD/@EntryIndexedValue">&lt;Policy Inspect="True" Prefix="" Suffix="" Style="AaBb" /&gt;</s:String>
<s:String x:Key="/Default/CodeStyle/Naming/XamlNaming/UserRules/=XAML_005FRESOURCE/@EntryIndexedValue">&lt;Policy Inspect="True" Prefix="" Suffix="" Style="AaBb" /&gt;</s:String>
<s:Boolean x:Key="/Default/CodeStyle/TypeScriptCodeStyle/ExplicitPublicModifier/@EntryValue">True</s:Boolean>
<s:Boolean x:Key="/Default/Environment/SettingsMigration/IsMigratorApplied/=JetBrains_002EReSharper_002EPsi_002EJavaScript_002ECodeStyle_002ESettingsUpgrade_002EJsParsFormattingSettingsUpgrader/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/Environment/SettingsMigration/IsMigratorApplied/=JetBrains_002EReSharper_002EPsi_002EJavaScript_002ECodeStyle_002ESettingsUpgrade_002EJsWrapperSettingsUpgrader/@EntryIndexedValue">True</s:Boolean></wpf:ResourceDictionary>

46
src/Squidex.Core/Squidex.Core.csproj

@ -0,0 +1,46 @@
<Project ToolsVersion="15.0" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<Import Project="$(MSBuildExtensionsPath)\$(MSBuildToolsVersion)\Microsoft.Common.props" />
<PropertyGroup>
<TargetFramework>netcoreapp1.0</TargetFramework>
<AssemblyName>Squidex.Core</AssemblyName>
</PropertyGroup>
<ItemGroup>
<Compile Include="**\*.cs" />
<EmbeddedResource Include="**\*.resx" />
<EmbeddedResource Include="compiler\resources\**\*" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\Squidex.Infrastructure\Squidex.Infrastructure.csproj" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="Microsoft.NET.Sdk">
<Version>1.0.0-alpha-20161104-2</Version>
<PrivateAssets>All</PrivateAssets>
</PackageReference>
<PackageReference Include="NETStandard.Library">
<Version>1.6.0</Version>
</PackageReference>
<PackageReference Include="NodaTime">
<Version>2.0.0-alpha20160729</Version>
</PackageReference>
<PackageReference Include="protobuf-net">
<Version>2.1.0</Version>
</PackageReference>
</ItemGroup>
<ItemGroup Condition=" '$(TargetFramework)' == 'netcoreapp1.0' ">
<PackageReference Include="Microsoft.NETCore.App">
<Version>1.0.1</Version>
</PackageReference>
</ItemGroup>
<PropertyGroup Condition=" '$(Configuration)' == 'Release' ">
<DefineConstants>$(DefineConstants);RELEASE</DefineConstants>
</PropertyGroup>
<Import Project="$(MSBuildToolsPath)\Microsoft.CSharp.targets" />
</Project>

63
src/Squidex.Infrastructure/Squidex.Infrastructure.csproj

@ -0,0 +1,63 @@
<Project ToolsVersion="15.0" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<Import Project="$(MSBuildExtensionsPath)\$(MSBuildToolsVersion)\Microsoft.Common.props" />
<PropertyGroup>
<TargetFramework>netcoreapp1.0</TargetFramework>
<AssemblyName>Squidex.Infrastructure</AssemblyName>
</PropertyGroup>
<ItemGroup>
<Compile Include="**\*.cs" />
<EmbeddedResource Include="**\*.resx" Exclude="bin\**;obj\**;**\*.xproj;packages\**" />
<EmbeddedResource Include="*.csv;compiler\resources\**\*" Exclude="bin\**;obj\**;**\*.xproj;packages\**" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="Microsoft.NET.Sdk">
<Version>1.0.0-alpha-20161104-2</Version>
<PrivateAssets>All</PrivateAssets>
</PackageReference>
<PackageReference Include="Autofac">
<Version>4.1.0</Version>
</PackageReference>
<PackageReference Include="EventStore.ClientAPI.NetCore">
<Version>0.0.1-alpha</Version>
</PackageReference>
<PackageReference Include="Microsoft.Extensions.Logging">
<Version>1.0.0</Version>
</PackageReference>
<PackageReference Include="NETStandard.Library">
<Version>1.6.0</Version>
</PackageReference>
<PackageReference Include="Newtonsoft.Json">
<Version>9.0.2-beta1</Version>
</PackageReference>
<PackageReference Include="NodaTime">
<Version>2.0.0-alpha20160729</Version>
</PackageReference>
<PackageReference Include="protobuf-net">
<Version>2.1.0</Version>
</PackageReference>
<PackageReference Include="System.Linq">
<Version>4.1.0</Version>
</PackageReference>
<PackageReference Include="System.Reflection.TypeExtensions">
<Version>4.1.0</Version>
</PackageReference>
<PackageReference Include="System.Security.Claims">
<Version>4.0.1</Version>
</PackageReference>
</ItemGroup>
<ItemGroup Condition=" '$(TargetFramework)' == 'netcoreapp1.0' ">
<PackageReference Include="Microsoft.NETCore.App">
<Version>1.0.1</Version>
</PackageReference>
</ItemGroup>
<PropertyGroup Condition=" '$(Configuration)' == 'Release' ">
<DefineConstants>$(DefineConstants);RELEASE</DefineConstants>
</PropertyGroup>
<Import Project="$(MSBuildToolsPath)\Microsoft.CSharp.targets" />
</Project>

2
src/Squidex.Read/Apps/IAppContributorEntity.cs

@ -12,7 +12,7 @@ namespace Squidex.Read.Apps
{
public interface IAppContributorEntity
{
string SubjectId { get; }
string ContributorId { get; }
PermissionLevel Permission { get; }
}

2
src/Squidex.Read/Apps/Repositories/IAppRepository.cs

@ -13,7 +13,7 @@ namespace Squidex.Read.Apps.Repositories
{
public interface IAppRepository
{
Task<IReadOnlyList<IAppEntity>> QueryAllAsync(string currentSubjectId);
Task<IReadOnlyList<IAppEntity>> QueryAllAsync(string subjectId);
Task<IAppEntity> FindAppByNameAsync(string name);
}

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

@ -9,7 +9,10 @@
using System;
using System.Threading.Tasks;
using Microsoft.Extensions.Caching.Memory;
using Squidex.Events.Apps;
using Squidex.Infrastructure;
using Squidex.Infrastructure.CQRS;
using Squidex.Infrastructure.CQRS.Events;
using Squidex.Read.Apps.Repositories;
using Squidex.Read.Utils;
@ -17,7 +20,7 @@ using Squidex.Read.Utils;
namespace Squidex.Read.Apps.Services.Implementations
{
public class CachingAppProvider : CachingProvider, IAppProvider
public class CachingAppProvider : CachingProvider, IAppProvider, ILiveEventConsumer
{
private static readonly TimeSpan CacheDuration = TimeSpan.FromMinutes(30);
private readonly IAppRepository appRepository;
@ -39,7 +42,7 @@ namespace Squidex.Read.Apps.Services.Implementations
{
Guard.NotNullOrEmpty(name, nameof(name));
var cacheKey = BulidCacheKey(name);
var cacheKey = BuildModelCacheKey(name);
var cacheItem = Cache.Get<CacheItem>(cacheKey);
if (cacheItem == null)
@ -49,12 +52,37 @@ namespace Squidex.Read.Apps.Services.Implementations
cacheItem = new CacheItem { Entity = app };
Cache.Set(cacheKey, cacheItem, new MemoryCacheEntryOptions { SlidingExpiration = CacheDuration });
if (cacheItem.Entity != null)
{
Cache.Set(BuildNamesCacheKey(cacheItem.Entity.Id), cacheItem.Entity.Name, CacheDuration);
}
}
return cacheItem.Entity;
}
private static string BulidCacheKey(string name)
public Task On(Envelope<IEvent> @event)
{
if (@event.Payload is AppContributorAssigned || @event.Payload is AppContributorRemoved)
{
var appName = Cache.Get<string>(BuildNamesCacheKey(@event.Headers.AggregateId()));
if (appName != null)
{
Cache.Remove(BuildModelCacheKey(appName));
}
}
return Task.FromResult(true);
}
private static string BuildNamesCacheKey(Guid schemaId)
{
return $"App_Names_{schemaId}";
}
private static string BuildModelCacheKey(string name)
{
return $"App_{name}";
}

8
src/Squidex.Read/Schemas/Services/Implementations/CachingSchemaProvider.cs

@ -42,7 +42,7 @@ namespace Squidex.Read.Schemas.Services.Implementations
{
Guard.NotNullOrEmpty(name, nameof(name));
var cacheKey = BuildModelsCacheKey(appId, name);
var cacheKey = BuildModelCacheKey(appId, name);
var cacheItem = Cache.Get<CacheItem>(cacheKey);
if (cacheItem == null)
@ -70,16 +70,16 @@ namespace Squidex.Read.Schemas.Services.Implementations
if (oldName != null)
{
Cache.Remove(BuildModelsCacheKey(@event.Headers.AppId(), oldName));
Cache.Remove(BuildModelCacheKey(@event.Headers.AppId(), oldName));
}
}
return Task.FromResult(true);
}
private static string BuildModelsCacheKey(Guid appId, string name)
private static string BuildModelCacheKey(Guid appId, string name)
{
return $"Schemas_Models_{appId}_{name}";
return $"Schema_{appId}_{name}";
}
private static string BuildNamesCacheKey(Guid schemaId)

2
src/Squidex.Read/Users/Repositories/IUserRepository.cs

@ -13,7 +13,7 @@ namespace Squidex.Read.Users.Repositories
{
public interface IUserRepository
{
Task<List<IUserEntity>> FindUsersByEmail(string email);
Task<List<IUserEntity>> FindUsersByQuery(string query);
Task<IUserEntity> FindUserByIdAsync(string id);
}

2
src/Squidex.Store.MongoDb/Apps/MongoAppContributorEntity.cs

@ -16,7 +16,7 @@ namespace Squidex.Store.MongoDb.Apps
{
[BsonRequired]
[BsonElement]
public string SubjectId { get; set; }
public string ContributorId { get; set; }
[BsonRequired]
[BsonElement]

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

@ -38,10 +38,10 @@ namespace Squidex.Store.MongoDb.Apps
return collection.Indexes.CreateOneAsync(IndexKeys.Ascending(x => x.Name));
}
public async Task<IReadOnlyList<IAppEntity>> QueryAllAsync(string currentSubjectId)
public async Task<IReadOnlyList<IAppEntity>> QueryAllAsync(string subjectId)
{
var entities =
await Collection.Find(s => s.Contributors.Any(c => c.SubjectId == currentSubjectId)).ToListAsync();
await Collection.Find(s => s.Contributors.Any(c => c.ContributorId == subjectId)).ToListAsync();
return entities;
}
@ -61,18 +61,18 @@ namespace Squidex.Store.MongoDb.Apps
public Task On(AppContributorRemoved @event, EnvelopeHeaders headers)
{
return Collection.UpdateAsync(headers, a => a.Contributors.RemoveAll(c => c.SubjectId == @event.ContributorId));
return Collection.UpdateAsync(headers, a => a.Contributors.RemoveAll(c => c.ContributorId == @event.ContributorId));
}
public Task On(AppContributorAssigned @event, EnvelopeHeaders headers)
{
return Collection.UpdateAsync(headers, a =>
{
var contributor = a.Contributors.Find(x => x.SubjectId == @event.ContributorId);
var contributor = a.Contributors.Find(x => x.ContributorId == @event.ContributorId);
if (contributor == null)
{
contributor = new MongoAppContributorEntity { SubjectId = @event.ContributorId };
contributor = new MongoAppContributorEntity { ContributorId = @event.ContributorId };
a.Contributors.Add(contributor);
}

4
src/Squidex.Store.MongoDb/Users/MongoUserRepository.cs

@ -28,9 +28,9 @@ namespace Squidex.Store.MongoDb.Users
this.userManager = userManager;
}
public Task<List<IUserEntity>> FindUsersByEmail(string email)
public Task<List<IUserEntity>> FindUsersByQuery(string query)
{
var users = userManager.Users.Where(x => x.NormalizedEmail.Contains(email)).Take(10).ToList();
var users = userManager.Users.Where(x => x.NormalizedEmail.Contains(query.ToUpper())).Take(10).ToList();
return Task.FromResult(users.Select(x => (IUserEntity)new MongoUserEntity(x)).ToList());
}

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

@ -56,7 +56,7 @@ namespace Squidex.Write.Apps
{
if (await userRepository.FindUserByIdAsync(command.ContributorId) == null)
{
var error = new ValidationError($"Cannot find contributor '{command.ContributorId}", nameof(AssignContributor.ContributorId));
var error = new ValidationError($"Cannot find contributor '{command.ContributorId ?? "UNKNOWN"}'", nameof(AssignContributor.ContributorId));
throw new ValidationException("Cannot assign contributor to app", error);
}

1
src/Squidex/Configurations/Domain/ReadModule.cs

@ -21,6 +21,7 @@ namespace Squidex.Configurations.Domain
{
builder.RegisterType<CachingAppProvider>()
.As<IAppProvider>()
.As<ILiveEventConsumer>()
.SingleInstance();
builder.RegisterType<CachingSchemaProvider>()

21
src/Squidex/Modules/Api/Apps/AppContributorsController.cs

@ -14,7 +14,7 @@ using Squidex.Infrastructure.CQRS.Commands;
using Squidex.Infrastructure.Reflection;
using Squidex.Modules.Api.Apps.Models;
using Squidex.Pipeline;
using Squidex.Read.Apps.Repositories;
using Squidex.Read.Apps.Services;
using Squidex.Write.Apps.Commands;
namespace Squidex.Modules.Api.Apps
@ -22,22 +22,21 @@ namespace Squidex.Modules.Api.Apps
[Authorize]
[ApiExceptionFilter]
[ServiceFilter(typeof(AppFilterAttribute))]
[Route("apps/{app}")]
public class AppContributorsController : ControllerBase
{
private readonly IAppRepository appRepository;
private readonly IAppProvider appProvider;
public AppContributorsController(ICommandBus commandBus, IAppRepository appRepository)
public AppContributorsController(ICommandBus commandBus, IAppProvider appProvider)
: base(commandBus)
{
this.appRepository = appRepository;
this.appProvider = appProvider;
}
[HttpGet]
[Route("contributors")]
[Route("apps/{app}/contributors/")]
public async Task<IActionResult> GetContributors(string app)
{
var entity = await appRepository.FindAppByNameAsync(app);
var entity = await appProvider.FindAppByNameAsync(app);
if (entity == null)
{
@ -49,9 +48,9 @@ namespace Squidex.Modules.Api.Apps
return Ok(model);
}
[HttpPut]
[Route("contributors")]
public async Task<IActionResult> PutContributor([FromBody] AssignContributorDto model)
[HttpPost]
[Route("apps/{app}/contributors/")]
public async Task<IActionResult> PostContributor([FromBody] AssignContributorDto model)
{
await CommandBus.PublishAsync(SimpleMapper.Map(model, new AssignContributor()));
@ -59,7 +58,7 @@ namespace Squidex.Modules.Api.Apps
}
[HttpDelete]
[Route("contributors/{contributorId}")]
[Route("apps/{app}/contributors/{contributorId}/")]
public async Task<IActionResult> PutContributor(string contributorId)
{
await CommandBus.PublishAsync(new RemoveContributor { ContributorId = contributorId });

12
src/Squidex/Modules/Api/Apps/AppController.cs

@ -38,9 +38,17 @@ namespace Squidex.Modules.Api.Apps
[Route("apps/")]
public async Task<List<ListAppDto>> Query()
{
var schemas = await appRepository.QueryAllAsync(HttpContext.User.OpenIdSubject());
var subject = HttpContext.User.OpenIdSubject();
var schemas = await appRepository.QueryAllAsync(subject);
return schemas.Select(s => SimpleMapper.Map(s, new ListAppDto())).ToList();
return schemas.Select(s =>
{
var dto = SimpleMapper.Map(s, new ListAppDto());
dto.Permission = s.Contributors.Single(x => x.ContributorId == subject).Permission;
return dto;
}).ToList();
}
[HttpPost]

3
src/Squidex/Modules/Api/Apps/Models/AppContributorDto.cs

@ -6,6 +6,8 @@
// All rights reserved.
// ==========================================================================
using Newtonsoft.Json;
using Newtonsoft.Json.Converters;
using Squidex.Core.Apps;
namespace Squidex.Modules.Api.Apps.Models
@ -14,6 +16,7 @@ namespace Squidex.Modules.Api.Apps.Models
{
public string ContributorId { get; set; }
[JsonConverter(typeof(StringEnumConverter))]
public PermissionLevel Permission { get; set; }
}
}

3
src/Squidex/Modules/Api/Apps/Models/AssignContributorDto.cs

@ -6,6 +6,8 @@
// All rights reserved.
// ==========================================================================
using Newtonsoft.Json;
using Newtonsoft.Json.Converters;
using Squidex.Core.Apps;
namespace Squidex.Modules.Api.Apps.Models
@ -14,6 +16,7 @@ namespace Squidex.Modules.Api.Apps.Models
{
public string ContributorId { get; set; }
[JsonConverter(typeof(StringEnumConverter))]
public PermissionLevel Permission { get; set; }
}
}

6
src/Squidex/Modules/Api/Apps/Models/ListAppDto.cs

@ -7,6 +7,9 @@
// ==========================================================================
using System;
using Newtonsoft.Json;
using Newtonsoft.Json.Converters;
using Squidex.Core.Apps;
namespace Squidex.Modules.Api.Apps.Models
{
@ -19,5 +22,8 @@ namespace Squidex.Modules.Api.Apps.Models
public DateTime Created { get; set; }
public DateTime LastModified { get; set; }
[JsonConverter(typeof(StringEnumConverter))]
public PermissionLevel Permission { get; set; }
}
}

13
src/Squidex/Modules/Api/Schemas/SchemaFieldsController.cs

@ -20,7 +20,6 @@ namespace Squidex.Modules.Api.Schemas
[Authorize]
[ApiExceptionFilter]
[ServiceFilter(typeof(AppFilterAttribute))]
[Route("apps/{app}")]
public class SchemasFieldsController : ControllerBase
{
public SchemasFieldsController(ICommandBus commandBus)
@ -29,7 +28,7 @@ namespace Squidex.Modules.Api.Schemas
}
[HttpPost]
[Route("schemas/{name}/fields/")]
[Route("apps/{app}/schemas/{name}/fields/")]
public Task Add(string name, [FromBody] CreateFieldDto model)
{
var command = SimpleMapper.Map(model, new AddField());
@ -38,7 +37,7 @@ namespace Squidex.Modules.Api.Schemas
}
[HttpPut]
[Route("schemas/{name}/fields/{fieldId:long}/")]
[Route("apps/{app}/schemas/{name}/fields/{fieldId:long}/")]
public Task Update(string name, long fieldId, [FromBody] UpdateFieldDto model)
{
var command = SimpleMapper.Map(model, new UpdateField());
@ -47,7 +46,7 @@ namespace Squidex.Modules.Api.Schemas
}
[HttpPut]
[Route("schemas/{name}/fields/{fieldId:long}/hide/")]
[Route("apps/{app}/schemas/{name}/fields/{fieldId:long}/hide/")]
public Task Hide(string name, long fieldId)
{
var command = new HideField { FieldId = fieldId };
@ -56,7 +55,7 @@ namespace Squidex.Modules.Api.Schemas
}
[HttpPut]
[Route("schemas/{name}/fields/{fieldId:long}/show/")]
[Route("apps/{app}/schemas/{name}/fields/{fieldId:long}/show/")]
public Task Show(string name, long fieldId)
{
var command = new ShowField { FieldId = fieldId };
@ -74,7 +73,7 @@ namespace Squidex.Modules.Api.Schemas
}
[HttpPut]
[Route("schemas/{name}/fields/{fieldId:long}/disable/")]
[Route("apps/{app}/schemas/{name}/fields/{fieldId:long}/disable/")]
public Task Disable(string name, long fieldId)
{
var command = new DisableField { FieldId = fieldId };
@ -83,7 +82,7 @@ namespace Squidex.Modules.Api.Schemas
}
[HttpDelete]
[Route("schemas/{name}/fields/{fieldId:long}/")]
[Route("apps/{app}/schemas/{name}/fields/{fieldId:long}/")]
public Task Delete(string name, long fieldId)
{
var command = new DeleteField { FieldId = fieldId };

11
src/Squidex/Modules/Api/Schemas/SchemasController.cs

@ -25,7 +25,6 @@ namespace Squidex.Modules.Api.Schemas
[Authorize]
[ApiExceptionFilter]
[ServiceFilter(typeof(AppFilterAttribute))]
[Route("apps/{app}")]
public class SchemasController : ControllerBase
{
private readonly ISchemaRepository schemaRepository;
@ -37,7 +36,7 @@ namespace Squidex.Modules.Api.Schemas
}
[HttpGet]
[Route("schemas/")]
[Route("apps/{app}/schemas/")]
public async Task<List<ListSchemaDto>> Query()
{
var schemas = await schemaRepository.QueryAllAsync(AppId);
@ -46,7 +45,7 @@ namespace Squidex.Modules.Api.Schemas
}
[HttpGet]
[Route("schemas/{name}/")]
[Route("apps/{app}/schemas/{name}/")]
public async Task<ActionResult> Get(string name)
{
var entity = await schemaRepository.FindSchemaAsync(AppId, name);
@ -62,7 +61,7 @@ namespace Squidex.Modules.Api.Schemas
}
[HttpPost]
[Route("schemas/")]
[Route("apps/{app}/schemas/")]
public async Task<ActionResult> Create([FromBody] CreateSchemaDto model)
{
var command = SimpleMapper.Map(model, new CreateSchema { AggregateId = Guid.NewGuid() });
@ -73,7 +72,7 @@ namespace Squidex.Modules.Api.Schemas
}
[HttpPut]
[Route("schemas/{name}/")]
[Route("apps/{app}/schemas/{name}/")]
public async Task<ActionResult> Update(string name, [FromBody] UpdateSchemaDto model)
{
var command = SimpleMapper.Map(model, new UpdateSchema());
@ -84,7 +83,7 @@ namespace Squidex.Modules.Api.Schemas
}
[HttpDelete]
[Route("schemas/{name}/")]
[Route("apps/{app}/schemas/{name}/")]
public async Task<ActionResult> Delete(string name)
{
await CommandBus.PublishAsync(new DeleteSchema());

4
src/Squidex/Modules/Api/Users/Models/UserDto.cs

@ -11,8 +11,10 @@ namespace Squidex.Modules.Api.Users.Models
public sealed class UserDto
{
public string Id { get; set; }
public string Email { get; set; }
public string ProfileUrl { get; set; }
public string PictureUrl { get; set; }
public string DisplayName { get; set; }
}

4
src/Squidex/Modules/Api/Users/UsersController.cs

@ -30,9 +30,9 @@ namespace Squidex.Modules.Api.Users
[HttpGet]
[Route("users")]
public async Task<IActionResult> GetUsers(string email)
public async Task<IActionResult> GetUsers(string query)
{
var entities = await userRepository.FindUsersByEmail(email);
var entities = await userRepository.FindUsersByQuery(query ?? string.Empty);
var model = entities.Select(x => SimpleMapper.Map(x, new UserDto())).ToList();

8
src/Squidex/Modules/UI/Account/AccountController.cs

@ -42,7 +42,7 @@ namespace Squidex.Modules.UI.Account
[Route("account/forbidden")]
public IActionResult Forbidden()
{
return View();
return View("Error");
}
[HttpGet]
@ -164,10 +164,10 @@ namespace Squidex.Modules.UI.Account
var user = new IdentityUser { Email = mail, UserName = mail };
var profileUrl = externalLogin.Principal.Claims.FirstOrDefault(x => x.Type == ExtendedClaimTypes.SquidexPictureUrl);
if (profileUrl != null)
var pictureUrl = externalLogin.Principal.Claims.FirstOrDefault(x => x.Type == ExtendedClaimTypes.SquidexPictureUrl);
if (pictureUrl != null)
{
user.AddClaim(profileUrl);
user.AddClaim(pictureUrl);
}
var displayName = externalLogin.Principal.Claims.FirstOrDefault(x => x.Type == ExtendedClaimTypes.SquidexDisplayName);

6
src/Squidex/Pipeline/AppFilterAttribute.cs

@ -7,10 +7,10 @@
// ==========================================================================
using System.Linq;
using System.Security.Claims;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.Filters;
using Squidex.Infrastructure.Security;
using Squidex.Read.Apps.Services;
namespace Squidex.Pipeline
@ -38,9 +38,9 @@ namespace Squidex.Pipeline
return;
}
var subject = context.HttpContext.User.FindFirst(ClaimTypes.NameIdentifier)?.Value;
var subject = context.HttpContext.User.FindFirst(OpenIdClaims.Subject)?.Value;
if (subject == null || app.Contributors.Any(x => x.SubjectId == subject))
if (subject == null || app.Contributors.All(x => x.ContributorId != subject))
{
context.Result = new NotFoundResult();
return;

2
src/Squidex/Views/Account/Login.cshtml

@ -20,7 +20,7 @@
<p>
@foreach (var provider in Model.ExternalProviders)
{
<button type="submit" name="provider" id="loginButton" value="@provider.AuthenticationScheme" title="Log in using your @provider.DisplayDisplayName account">@provider.AuthenticationScheme</button>
<button type="submit" name="provider" id="loginButton" value="@provider.AuthenticationScheme" title="Log in using your @provider.DisplayName account">@provider.AuthenticationScheme</button>
}
</p>
</div>

2
src/Squidex/app/app.module.ts

@ -24,6 +24,7 @@ import {
MustBeAuthenticatedGuard,
MustBeNotAuthenticatedGuard,
LanguageService,
LocalStoreService,
SqxFrameworkModule,
TitlesConfig,
TitleService,
@ -62,6 +63,7 @@ const baseUrl = window.location.protocol + '//' + window.location.host + '/';
AppMustExistGuard,
AuthService,
LanguageService,
LocalStoreService,
MustBeAuthenticatedGuard,
MustBeNotAuthenticatedGuard,
TitleService,

15
src/Squidex/app/app.routes.ts

@ -11,9 +11,12 @@ import * as Ng2Router from '@angular/router';
import {
AppsPageComponent,
AppAreaComponent,
ContributorsPageComponent,
CredentialsPageComponent,
DashboardPageComponent,
InternalAreaComponent,
HomePageComponent,
LanguagesPageComponent,
LogoutPageComponent,
NotFoundPageComponent,
SchemasPageComponent
@ -56,6 +59,18 @@ export const routes: Ng2Router.Routes = [
{
path: 'schemas',
component: SchemasPageComponent
},
{
path: 'contributors',
component: ContributorsPageComponent
},
{
path: 'credentials',
component: CredentialsPageComponent
},
{
path: 'languages',
component: LanguagesPageComponent
}
]
}

16
src/Squidex/app/components/internal/app/dashboard/dashboard-page.component.html

@ -1,10 +1,14 @@
<div class="layout-content">
<div class="layout-content-left layout-content-left--no-button">
<div class="layout">
<div class="layout-left">
<sqx-left-menu></sqx-left-menu>
</div>
<div class="layout-content-main">
<h1>
<i class="layout-title-icon icon-dashboard"></i> Dashboard
</h1>
<div class="layout-middle">
<div class="layout-middle-header">
<h1>
<i class="layout-title-icon icon-dashboard"></i> Dashboard
</h1>
</div>
<div class="layout-middle-content">
</div>
</div>
</div>

32
src/Squidex/app/components/internal/app/left-menu.component.html

@ -1,26 +1,38 @@
<ul class="nav">
<li class="nav-item">
<a class="nav-link nav-link--dashboard" routerLink="../dashboard" routerLinkActive="active">
<li class="nav-item nav-item--dashboard">
<a class="nav-link" routerLink="../dashboard" routerLinkActive="active">
<i class="icon-dashboard"></i> Dashboard
</a>
</li>
<li class="nav-item">
<a class="nav-link nav-link--schemas" routerLink="../schemas" routerLinkActive="active">
<li class="nav-items nav-item--schemas" *ngIf="permission !== 'Editor'">
<a class="nav-link" routerLink="../schemas" routerLinkActive="active">
<i class="icon-schemas"></i> Schemas
</a>
</li>
<li class="nav-item">
<a class="nav-link nav-link--content">
<li class="nav-item nav-item--content">
<a class="nav-link">
<i class="icon-content"></i> Content
</a>
</li>
<li class="nav-item">
<a class="nav-link nav-link--media">
<li class="nav-item nav-item--media">
<a class="nav-link">
<i class="icon-media"></i> Media</a>
</li>
<li class="nav-item">
<a class="nav-link nav-link--settings">
<li class="nav-item nav-item--settings nav-item-group" *ngIf="permission === 'Owner'">
<a class="nav-link" (click)="toggleSettingsMenu()">
<i class="icon-settings"></i> Settings
</a>
<ul class="subnav" *ngIf="showSettingsMenu">
<li class="subnav-item">
<a class="nav-link" routerLink="../contributors" routerLinkActive="active">Contributors</a>
</li>
<li class="subnav-item">
<a class="nav-link" routerLink="../credentials" routerLinkActive="active">Credentials</a>
</li>
<li class="subnav-item">
<a class="nav-link" routerLink="../languages" routerLinkActive="active">Languages</a>
</li>
</ul>
</li>
</ul>

102
src/Squidex/app/components/internal/app/left-menu.component.scss

@ -2,16 +2,20 @@
@import '_mixins';
$color-selection-background: #ebf0f2;
$color-subnav-text: #818181;
$color-subnav-dot: #bdc6d0;
$color-border-left-width: 4px;
@mixin build-item($color) {
&:hover,
&.active {
border-color: $color;
& .nav-link {
&:hover,
&.active {
border-color: $color;
}
}
& i {
margin-right: .5rem;
color: $color;
}
}
@ -20,9 +24,41 @@ $color-border-left-width: 4px;
& {
list-style: none;
padding: 0;
margin-top: 40px;
margin-left: -$padding-layout - 1px;
margin-right: -$padding-layout;
margin-left: -$padding-layout-h - 1px;
margin-right: -$padding-layout-h;
}
&-item {
&--media {
@include build-item($color-section-media);
}
&--content {
@include build-item($color-section-content);
}
&--dashboard {
@include build-item($color-section-dashboard);
}
&--schemas {
@include build-item($color-section-schemas);
}
&--settings {
@include build-item($color-section-settings);
}
}
&-item-group > a {
& {
position: relative;
}
&::after {
@include absolute(50%, 30px, auto, auto);
@include caret-bottom;
}
}
&-link {
@ -35,16 +71,8 @@ $color-border-left-width: 4px;
font-size: 1rem;
font-weight: 450;
line-height: 3rem;
padding-left: $padding-layout - $color-border-left-width;
padding-right: $padding-layout;
}
&.active {
font-weight: bold;
}
&:hover {
text-decoration: none;
padding-left: $padding-layout-h - $color-border-left-width;
padding-right: $padding-layout-h;
}
&:hover,
@ -52,24 +80,42 @@ $color-border-left-width: 4px;
background: $color-selection-background;
}
&--media {
@include build-item($color-section-media);
&.active {
font-weight: bold;
}
&--content {
@include build-item($color-section-content);
&:hover {
text-decoration: none;
}
}
}
&--dashboard {
@include build-item($color-section-dashboard);
}
.subnav {
& {
list-style: none;
margin: 0;
padding: 0;
overflow: hidden;
}
&--schemas {
@include build-item($color-section-schemas);
.nav-link {
& {
color: $color-subnav-text;
padding-left: 55px;
font-size: .9rem;
font-weight: 450;
line-height: 2rem;
}
}
&--settings {
@include build-item($color-section-settings);
&-item {
&::before {
content: '';
color: $color-subnav-dot;
float: left;
font-weight: bold;
line-height: 2rem;
margin-left: 35px;
}
}
}

39
src/Squidex/app/components/internal/app/left-menu.component.ts

@ -7,9 +7,46 @@
import * as Ng2 from '@angular/core';
import { AppsStoreService, LocalStoreService } from 'shared';
@Ng2.Component({
selector: 'sqx-left-menu',
styles,
template
})
export class LeftMenuComponent { }
export class LeftMenuComponent implements Ng2.OnInit, Ng2.OnDestroy {
private appSubscription: any | null = null;
public get showSettingsMenu(): boolean {
return this.localStore.get('squidex:showSettingsMenu') === 'true';
}
public set showSettingsMenu(value: boolean) {
this.localStore.set('squidex:showSettingsMenu', value);
}
public permission: string | null = null;
constructor(
private readonly localStore: LocalStoreService,
private readonly appsStore: AppsStoreService
) {
}
public ngOnInit() {
this.appSubscription =
this.appsStore.selectedApp.subscribe(app => {
if (app) {
this.permission = app.permission;
}
});
}
public ngOnDestroy() {
this.appSubscription.unsubscribe();
}
public toggleSettingsMenu() {
this.showSettingsMenu = !this.showSettingsMenu;
}
}

20
src/Squidex/app/components/internal/app/schemas/schemas-page.component.html

@ -1,12 +1,16 @@
<div class="layout-content">
<div class="layout-content-left">
<button class="layout-new-button btn btn-success">Create Schema</button>
<div class="layout">
<div class="layout-left">
<sqx-left-menu></sqx-left-menu>
</div>
<div class="layout-content-main">
<h1>
<i class="layout-title-icon icon-schemas"></i> Schemas
</h1>
<div class="layout-middle">
<div class="layout-middle-header">
<button class="layout-new-button btn btn-success pull-right">Create Schema</button>
<h1>
<i class="layout-title-icon icon-schemas"></i> Schemas
</h1>
</div>
<div class="layout-middle-content">
</div>
</div>
</div>

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

@ -0,0 +1,67 @@
<div class="layout">
<div class="layout-left">
<sqx-left-menu></sqx-left-menu>
</div>
<div class="layout-middle">
<div class="layout-middle-header">
<h1>
<i class="layout-title-icon icon-settings"></i> Contributors
</h1>
</div>
<div class="layout-middle-content">
<div class="card">
<div class="card-block">
<table class="table table-borderless table-fixed">
<colgroup>
<col style="width: 100%" />
<col style="width: 150px" />
<col style="width: 110px" />
</colgroup>
<tr *ngFor="let contributor of appContributors">
<td class="col-xs-7">
<img class="user-picture" [attr.src]="pictureUrl(contributor) | async" />
<span class="user-name">
{{displayName(contributor) | async}}
</span>
<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-block btn-danger" [disabled]="currrentUserId === contributor.contributorId" (click)="revokeContributor(contributor)">Revoke</button>
</td>
</tr>
</table>
</div>
<div class="card-footer">
<form class="form-inline" (submit)="addContributor()" >
<div class="form-group">
<ng2-completer
[autoMatch]="true"
[dataService]="usersDataSource"
[minSearchLength]="3"
[placeholder]="'Search user by e-email'"
[pause]="300"
[clearSelected]="false"
[textSearching]="'Searching...'"
[inputName]="contributor"
[ngModel]="selectedUserName"
[ngModelOptions]="{standalone: true}"
(selected)="selectUser($event)">
</ng2-completer>
</div>
<button type="submit" class="btn btn-primary" [disabled]="!selectedUser">Add</button>
</form>
</div>
</div>
</div>
</div>
</div>

28
src/Squidex/app/components/internal/app/settings/contributors-page.component.scss

@ -0,0 +1,28 @@
@import '_vars';
@import '_mixins';
.layout-title-icon {
color: $color-section-settings;
}
.card {
max-width: 700px;
}
.user {
&-picture {
@include circle(2.2rem);
float: left;
}
&-name,
&-email {
@include truncate;
padding-left: 10px;
}
&-email {
font-size: .8rem;
font-style: italic;
}
}

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

@ -0,0 +1,162 @@
/*
* Squidex Headless CMS
*
* @license
* Copyright (c) Sebastian Stehle. All rights reserved
*/
import * as Ng2 from '@angular/core';
import { Observable, Subscription } from 'rxjs';
import { CompleterBaseData, CompleterItem } from 'ng2-completer';
import {
AppContributorDto,
AppContributorsService,
AppsStoreService,
AuthService,
TitleService,
UserDto,
UsersService,
UsersProviderService
} from 'shared';
class UsersDataSource extends CompleterBaseData {
private remoteSearch: Subscription;
constructor(
private readonly usersService: UsersService,
private readonly component: ContributorsPageComponent,
) {
super();
}
public search(term: string): void {
this.cancel();
this.remoteSearch =
this.usersService.getUsers(term)
.map(users => {
const results: CompleterItem[] = [];
for (let u of users) {
if (!this.component.appContributors || !this.component.appContributors.find(t => t.contributorId === u.id)) {
results.push({ title: u.displayName, image: u.pictureUrl, originalObject: u, description: u.email });
}
}
this.next(results);
return results;
})
.catch(err => {
this.error(err);
return null;
}).subscribe();
}
public cancel() {
if (this.remoteSearch) {
this.remoteSearch.unsubscribe();
}
}
}
@Ng2.Component({
selector: 'sqx-contributor-page',
styles,
template
})
export class ContributorsPageComponent implements Ng2.OnInit {
private appSubscription: any | null = null;
private appName: string;
public appContributors: AppContributorDto[];
public selectedUserName: string | null = null;
public selectedUser: UserDto | null = null;
public currrentUserId: string;
public usersDataSource: UsersDataSource;
public usersPermissions = [
'Owner',
'Developer',
'Editor'
];
constructor(
private readonly titles: TitleService,
private readonly authService: AuthService,
private readonly appsStore: AppsStoreService,
private readonly appContributorsService: AppContributorsService,
private readonly usersProvider: UsersProviderService,
private readonly usersService: UsersService
) {
this.usersDataSource = new UsersDataSource(usersService, this);
}
public ngOnInit() {
this.currrentUserId = this.authService.user.id;
this.appSubscription =
this.appsStore.selectedApp.subscribe(app => {
if (app) {
this.appName = app.name;
this.titles.setTitle('{appName} | Settings | Contributors', { appName: app.name });
this.appContributorsService.getContributors(app.name).subscribe(contributors => {
this.appContributors = contributors;
});
}
});
}
public ngOnDestroy() {
this.appSubscription.unsubscribe();
}
public addContributor() {
if (!this.selectedUser) {
return;
}
const contributor = new AppContributorDto(this.selectedUser.id, 'Editor');
this.appContributorsService.postContributor(this.appName, contributor).subscribe();
this.appContributors.push(contributor);
this.selectedUser = null;
this.selectedUserName = null;
}
public revokeContributor(contributor: AppContributorDto) {
this.appContributorsService.deleteContributor(this.appName, contributor.contributorId).subscribe();
this.appContributors.splice(this.appContributors.indexOf(contributor), 1);
}
public saveContributor(contributor: AppContributorDto) {
this.appContributorsService.postContributor(this.appName, contributor).subscribe();
}
public selectUser(selection: CompleterItem | null) {
this.selectedUser = selection ? selection.originalObject : null;
}
public email(contributor: AppContributorDto): Observable<string> {
return this.usersProvider.getUser(contributor.contributorId).map(u => u.email);
}
public displayName(contributor: AppContributorDto): Observable<string> {
return this.usersProvider.getUser(contributor.contributorId).map(u => u.displayName);
}
public pictureUrl(contributor: AppContributorDto): Observable<string> {
return this.usersProvider.getUser(contributor.contributorId).map(u => u.pictureUrl);
}
}

14
src/Squidex/app/components/internal/app/settings/credentials-page.component.html

@ -0,0 +1,14 @@
<div class="layout">
<div class="layout-left">
<sqx-left-menu></sqx-left-menu>
</div>
<div class="layout-middle">
<div class="layout-middle-header">
<h1>
<i class="layout-title-icon icon-settings"></i> Credentials
</h1>
</div>
<div class="layout-middle-content">
</div>
</div>
</div>

6
src/Squidex/app/components/internal/app/settings/credentials-page.component.scss

@ -0,0 +1,6 @@
@import '_vars';
@import '_mixins';
.layout-title-icon {
color: $color-section-settings;
}

39
src/Squidex/app/components/internal/app/settings/credentials-page.component.ts

@ -0,0 +1,39 @@
/*
* Squidex Headless CMS
*
* @license
* Copyright (c) Sebastian Stehle. All rights reserved
*/
import * as Ng2 from '@angular/core';
import { AppsStoreService, TitleService } from 'shared';
@Ng2.Component({
selector: 'sqx-credentials-page',
styles,
template
})
export class CredentialsPageComponent implements Ng2.OnInit {
private appSubscription: any | null = null;
constructor(
private readonly titles: TitleService,
private readonly appsStore: AppsStoreService
) {
}
public ngOnInit() {
this.appSubscription =
this.appsStore.selectedApp.subscribe(app => {
if (app) {
this.titles.setTitle('{appName} | Settings | Credentials', { appName: app.name });
}
});
}
public ngOnDestroy() {
this.appSubscription.unsubscribe();
}
}

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

@ -0,0 +1,14 @@
<div class="layout">
<div class="layout-left">
<sqx-left-menu></sqx-left-menu>
</div>
<div class="layout-middle">
<div class="layout-middle-header">
<h1>
<i class="layout-title-icon icon-settings"></i> Languages
</h1>
</div>
<div class="layout-middle-content">
</div>
</div>
</div>

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

@ -0,0 +1,6 @@
@import '_vars';
@import '_mixins';
.layout-title-icon {
color: $color-section-settings;
}

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

@ -0,0 +1,39 @@
/*
* Squidex Headless CMS
*
* @license
* Copyright (c) Sebastian Stehle. All rights reserved
*/
import * as Ng2 from '@angular/core';
import { AppsStoreService, TitleService } from 'shared';
@Ng2.Component({
selector: 'sqx-languages-page',
styles,
template
})
export class LanguagesPageComponent implements Ng2.OnInit {
private appSubscription: any | null = null;
constructor(
private readonly titles: TitleService,
private readonly appsStore: AppsStoreService
) {
}
public ngOnInit() {
this.appSubscription =
this.appsStore.selectedApp.subscribe(app => {
if (app) {
this.titles.setTitle('{appName} | Settings | Languages', { appName: app.name });
}
});
}
public ngOnDestroy() {
this.appSubscription.unsubscribe();
}
}

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

@ -10,5 +10,8 @@ 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/contributors-page.component';
export * from './app/settings/credentials-page.component';
export * from './app/settings/languages-page.component';
export * from './internal-area.component';

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

@ -7,20 +7,26 @@
import * as Ng2 from '@angular/core';
import { Ng2CompleterModule } from 'ng2-completer';
import { SqxFrameworkModule } from 'shared';
import { SqxLayoutModule } from 'components/layout';
import {
AppAreaComponent,
AppsPageComponent,
ContributorsPageComponent,
CredentialsPageComponent,
DashboardPageComponent,
InternalAreaComponent,
LeftMenuComponent,
LanguagesPageComponent,
SchemasPageComponent
} from './declarations';
@Ng2.NgModule({
imports: [
Ng2CompleterModule,
SqxFrameworkModule,
SqxLayoutModule
],
@ -30,8 +36,11 @@ import {
declarations: [
AppAreaComponent,
AppsPageComponent,
ContributorsPageComponent,
CredentialsPageComponent,
DashboardPageComponent,
InternalAreaComponent,
LanguagesPageComponent,
LeftMenuComponent,
SchemasPageComponent
]

12
src/Squidex/app/components/layout/app-form.component.html

@ -6,26 +6,26 @@
</div>
<div class="form-group">
<label for="app-name">DisplayName</label>
<label for="app-name">Name</label>
<div class="errors-box" *ngIf="createForm.get('name').invalid && createForm.get('name').dirty" [@fade]>
<div class="errors">
<span *ngIf="createForm.get('name').hasError('required')">
DisplayName is required.
Name is required.
</span>
<span *ngIf="createForm.get('name').hasError('maxlength')">
DisplayName can not have more than 40 characters.
Name can not have more than 40 characters.
</span>
<span *ngIf="createForm.get('name').hasError('pattern')">
DisplayName can contain lower case letters (a-z), numbers and dashes only.
Name can contain lower case letters (a-z), numbers and dashes only.
</span>
</div>
</div>
<input type="text" class="form-control" id="app-name" formControlDisplayName="name" />
<input type="text" class="form-control" id="app-name" formControlName="name" />
<span class="form-hint">
The app name becomes part of the api url, e.g, https://<b>{{appDisplayName}}</b>.squidex.io/.<br />
The app name becomes part of the api url, e.g, https://<b>{{appName}}</b>.squidex.io/.<br />
It must contain lower case letters (a-z), numbers and dashes only, and cannot be longer than 40 characters. The name cannot be changed later.
</span>
</div>

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

@ -1,6 +1,6 @@
<ul class="nav navbar-nav" *ngIf="apps">
<li class="nav-item dropdown">
<span class="nav-link dropdown-toggle" id="app-name" (click)="modalMenu.toggle()">{{appDisplayName}}</span>
<span class="nav-link dropdown-toggle" id="app-name" (click)="modalMenu.toggle()">{{appName}}</span>
<div class="dropdown-menu" *sqxModalView="modalMenu" closeAlways="true" [@fade]>
<a class="dropdown-item all-apps" [routerLink]="['/app']">

9
src/Squidex/app/components/layout/apps-menu.component.scss

@ -23,13 +23,10 @@
}
&::before {
@include absolute(-18px, auto, auto, 10px);
@include absolute(-10px, auto, auto, 10px);
@include caret-top;
border-color: transparent transparent $color-accent-dark;
border-style: solid;
border-width: 10px;
content: '';
height: 0;
width: 0;
}
}
@ -56,8 +53,8 @@
@include opacity(.95);
@include no-selection;
padding-right: 15px;
cursor: pointer;
color: $color-accent-dark;
cursor: pointer;
width: 200px;
}

6
src/Squidex/app/components/layout/profile-menu.component.html

@ -1,9 +1,11 @@
<ul class="nav navbar-nav">
<li class="nav-item dropdown">
<span class="nav-link dropdown-toggle" (click)="modalMenu.toggle()">
<img [attr.src]="profilePictureUrl" />
<span class="user">
<img class="user-picture" [attr.src]="profilePictureUrl" />
<span>{{profileDisplayName}}</span>
<span class="user-name">{{profileDisplayName}}</span>
</span>
</span>
<div class="dropdown-menu" *sqxModalView="modalMenu" closeAlways="true" [@fade]>

43
src/Squidex/app/components/layout/profile-menu.component.scss

@ -3,14 +3,10 @@
$size-avatar: 2.2rem;
a {
cursor: pointer;
}
img {
@include border-radius($size-avatar * .5);
height: $size-avatar;
width: $size-avatar;
.user {
&-picture {
@include circle(2.2rem);
}
}
.navbar-nav {
@ -23,18 +19,27 @@ img {
}
}
.dropdown-menu {
& {
@include absolute(44px, 0, auto, auto);
.nav-link {
&::after {
color: $color-theme-blue-light;
}
}
.dropdown {
&-item {
cursor: pointer;
}
&::before {
@include absolute(-18px, 10px, auto, auto);
border-color: transparent transparent $color-accent-dark;
border-style: solid;
border-width: 10px;
content: '';
height: 0;
width: 0;
&-menu {
& {
@include absolute(44px, 0, auto, auto);
}
&::before {
@include absolute(-10px, 10px, auto, auto);
@include caret-top;
border-color: transparent transparent $color-accent-dark;
border-width: 10px;
}
}
}

25
src/Squidex/app/framework/angular/animations.ts

@ -15,7 +15,7 @@ export const fadeAnimation = (name = 'fade', timing = '200ms'): Ng2.AnimationEnt
Ng2.animate(timing, Ng2.style({ opacity: 1 }))
]),
Ng2.transition(':leave', [
Ng2.style({ 'opacity': 1 }),
Ng2.style({ opacity: 1 }),
Ng2.animate(timing, Ng2.style({ opacity: 0 }))
]),
Ng2.state('true',
@ -29,4 +29,27 @@ export const fadeAnimation = (name = 'fade', timing = '200ms'): Ng2.AnimationEnt
]
);
};
export const heightAnimation = (name = 'height', timing = '200ms'): Ng2.AnimationEntryMetadata => {
return Ng2.trigger(
name, [
Ng2.transition(':enter', [
Ng2.style({ height: '0px' }),
Ng2.animate(timing, Ng2.style({ height: '*' }))
]),
Ng2.transition(':leave', [
Ng2.style({ height: '*' }),
Ng2.animate(timing, Ng2.style({ height: '0px' }))
]),
Ng2.state('true',
Ng2.style({ height: '*' })
),
Ng2.state('false',
Ng2.style({ height: '0px' })
),
Ng2.transition('1 => 0', Ng2.animate(timing)),
Ng2.transition('0 => 1', Ng2.animate(timing))
]
);
};

13
src/Squidex/app/framework/services/local-store.service.ts

@ -13,6 +13,7 @@ export const LocalStoreServiceFactory = () => {
@Ng2.Injectable()
export class LocalStoreService {
private readonly fallback = {};
private store: any = localStorage;
public configureStore(store: any) {
@ -20,10 +21,18 @@ export class LocalStoreService {
}
public get(key: string): any {
return this.store.getItem(key);
try {
return this.store.getItem(key);
} catch (e) {
return this.fallback[key];
}
}
public set(key: string, value: any) {
this.store.setItem(key, value);
try {
this.store.setItem(key, value);
} catch (e) {
this.fallback[key] = value;
}
}
}

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

@ -12,7 +12,7 @@ import { Observable } from 'rxjs';
import {
ApiUrlConfig,
AppContributor,
AppContributorDto,
AppContributorsService,
AuthService
} from './../';
@ -43,7 +43,7 @@ describe('AppContributorsService', () => {
))
.verifiable(TypeMoq.Times.once());
let contributors: AppContributor[] = null;
let contributors: AppContributorDto[] = null;
appContributorsService.getContributors('my-app').subscribe(result => {
contributors = result;
@ -51,22 +51,23 @@ describe('AppContributorsService', () => {
expect(contributors).toEqual(
[
new AppContributor('123', 'Owner'),
new AppContributor('456', 'Editor'),
new AppContributorDto('123', 'Owner'),
new AppContributorDto('456', 'Editor'),
]);
authService.verifyAll();
});
it('should make post request to assign contributor', () => {
const contributor = new AppContributor('123', 'Owner');
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)))
.returns(() => Observable.of(
new Ng2Http.Response(
new Ng2Http.ResponseOptions()
)
));
))
.verifiable(TypeMoq.Times.once());
appContributorsService.postContributor('my-app', contributor);
@ -79,7 +80,8 @@ describe('AppContributorsService', () => {
new Ng2Http.Response(
new Ng2Http.ResponseOptions()
)
));
))
.verifiable(TypeMoq.Times.once());
appContributorsService.deleteContributor('my-app', '123');

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

@ -13,7 +13,7 @@ import { ApiUrlConfig } from 'framework';
import { AuthService } from './auth.service';
export class AppContributor {
export class AppContributorDto {
constructor(
public readonly contributorId: string,
public readonly permission: string
@ -29,24 +29,24 @@ export class AppContributorsService {
) {
}
public getContributors(appName: string): Observable<AppContributor[]> {
public getContributors(appName: string): Observable<AppContributorDto[]> {
return this.authService.authGet(this.apiUrl.buildUrl(`api/apps/${appName}/contributors`))
.map(response => {
const body: any[] = response.json();
return body.map(item => {
return new AppContributor(
return new AppContributorDto(
item.contributorId,
item.permission);
});
});
}
public postContributor(appName: string, contributor: AppContributor): Observable<any> {
public postContributor(appName: string, contributor: AppContributorDto): Observable<any> {
return this.authService.authPost(this.apiUrl.buildUrl(`api/apps/${appName}/contributors`), contributor);
}
public deleteContributor(appName: string, contributorId: string): Observable<any> {
return this.authService.authDelete(this.apiUrl.buildUrl(`api/apps/${appName}/contributors/{contributorId}`));
return this.authService.authDelete(this.apiUrl.buildUrl(`api/apps/${appName}/contributors/${contributorId}`));
}
}

6
src/Squidex/app/shared/services/apps-store.service.spec.ts

@ -18,8 +18,8 @@ import {
} from './../';
describe('AppsStoreService', () => {
const oldApps = [new AppDto('id', 'name', null, null)];
const newApp = new AppDto('id', 'new-name', null, null);
const oldApps = [new AppDto('id', 'old-name', null, null, 'Owner')];
const newApp = new AppDto('id', 'new-name', null, null, 'Owner');
let appsService: TypeMoq.Mock<AppsService>;
let authService: TypeMoq.Mock<AuthService>;
@ -156,7 +156,7 @@ describe('AppsStoreService', () => {
const store = new AppsStoreService(authService.object, appsService.object);
store.selectApp('name').then((isSelected) => {
store.selectApp('old-name').then((isSelected) => {
expect(isSelected).toBeTruthy();
appsService.verifyAll();

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

@ -37,12 +37,14 @@ describe('AppsService', () => {
id: '123',
name: 'name1',
created: '2016-01-01',
lastModified: '2016-02-02'
lastModified: '2016-02-02',
permission: 'Owner'
}, {
id: '456',
name: 'name2',
created: '2017-01-01',
lastModified: '2017-02-02'
lastModified: '2017-02-02',
permission: 'Editor'
}]
})
)
@ -56,8 +58,8 @@ describe('AppsService', () => {
}).unsubscribe();
expect(apps).toEqual([
new AppDto('123', 'name1', DateTime.parseISO('2016-01-01'), DateTime.parseISO('2016-02-02')),
new AppDto('456', 'name2', DateTime.parseISO('2017-01-01'), DateTime.parseISO('2017-02-02')),
new AppDto('123', 'name1', DateTime.parseISO('2016-01-01'), DateTime.parseISO('2016-02-02'), 'Owner'),
new AppDto('456', 'name2', DateTime.parseISO('2017-01-01'), DateTime.parseISO('2017-02-02'), 'Editor'),
]);
authService.verifyAll();
@ -83,7 +85,7 @@ describe('AppsService', () => {
newApp = result;
}).unsubscribe();
expect(newApp).toEqual(new AppDto('123', 'new-app', now, now));
expect(newApp).toEqual(new AppDto('123', 'new-app', now, now, 'Owner'));
authService.verifyAll();
});

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

@ -17,7 +17,8 @@ export class AppDto {
public readonly id: string,
public readonly name: string,
public readonly created: DateTime,
public readonly lastModified: DateTime
public readonly lastModified: DateTime,
public readonly permission: string
) {
}
}
@ -47,7 +48,8 @@ export class AppsService {
item.id,
item.name,
DateTime.parseISO(item.created),
DateTime.parseISO(item.lastModified)
DateTime.parseISO(item.lastModified),
item.permission
);
});
});
@ -58,7 +60,7 @@ export class AppsService {
return this.authService.authPost(this.apiUrl.buildUrl('api/apps'), appToCreate)
.map(response => response.json())
.map(response => new AppDto(response.id, appToCreate.name, now, now))
.map(response => new AppDto(response.id, appToCreate.name, now, now, 'Owner'))
.catch(response => {
if (response.status === 400) {
return Observable.throw('An app with the same name already exists.');

8
src/Squidex/app/shared/services/languages.service.spec.ts

@ -13,7 +13,7 @@ import { Observable } from 'rxjs';
import {
ApiUrlConfig,
AuthService,
Language,
LanguageDto,
LanguageService
} from './../';
@ -43,7 +43,7 @@ describe('LanguageService', () => {
))
.verifiable(TypeMoq.Times.once());
let languages: Language[] = null;
let languages: LanguageDto[] = null;
languageService.getLanguages().subscribe(result => {
languages = result;
@ -51,8 +51,8 @@ describe('LanguageService', () => {
expect(languages).toEqual(
[
new Language('de', 'German'),
new Language('en', 'English'),
new LanguageDto('de', 'German'),
new LanguageDto('en', 'English'),
]);
authService.verifyAll();

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

@ -12,7 +12,7 @@ import { ApiUrlConfig } from 'framework';
import { AuthService } from './auth.service';
export class Language {
export class LanguageDto {
constructor(
public readonly iso2Code: string,
public readonly englishName: string
@ -28,13 +28,13 @@ export class LanguageService {
) {
}
public getLanguages(): Observable<Language[]> {
public getLanguages(): Observable<LanguageDto[]> {
return this.authService.authGet(this.apiUrl.buildUrl('api/languages'))
.map(response => {
const body: any[] = response.json();
return body.map(item => {
return new Language(
return new LanguageDto(
item.iso2Code,
item.englishName);
});

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

@ -12,7 +12,7 @@ import { Observable } from 'rxjs';
import {
AuthService,
Profile,
User,
UserDto,
UsersProviderService,
UsersService,
} from './../';
@ -29,13 +29,13 @@ describe('UsersProviderService', () => {
});
it('Should return users service when user not cached', () => {
const user = new User('123', 'path/to/image', 'mail@domain.com');
const user = new UserDto('123', 'mail@domain.com', 'User1', 'path/to/image', );
usersService.setup(x => x.getUser('123'))
.returns(() => Observable.of(user))
.verifiable(TypeMoq.Times.once());
let resultingUser: User = null;
let resultingUser: UserDto = null;
usersProviderService.getUser('123').subscribe(result => {
resultingUser = result;
@ -47,7 +47,7 @@ describe('UsersProviderService', () => {
});
it('Should return provide user from cache', () => {
const user = new User('123', 'path/to/image', 'mail@domain.com');
const user = new UserDto('123', 'mail@domain.com', 'User1', 'path/to/image', );
usersService.setup(x => x.getUser('123'))
.returns(() => Observable.of(user))
@ -55,7 +55,7 @@ describe('UsersProviderService', () => {
usersProviderService.getUser('123');
let resultingUser: User = null;
let resultingUser: UserDto = null;
usersProviderService.getUser('123').subscribe(result => {
resultingUser = result;
@ -67,7 +67,7 @@ describe('UsersProviderService', () => {
});
it('Should return Me when user is current user', () => {
const user = new User('123', 'path/to/image', 'mail@domain.com');
const user = new UserDto('123', 'mail@domain.com', 'User1', 'path/to/image', );
authService.setup(x => x.user)
.returns(() => new Profile(<any>{ profile: { sub: '123'}}));
@ -76,13 +76,13 @@ describe('UsersProviderService', () => {
.returns(() => Observable.of(user))
.verifiable(TypeMoq.Times.once());
let resultingUser: User = null;
let resultingUser: UserDto = null;
usersProviderService.getUser('123').subscribe(result => {
resultingUser = result;
}).unsubscribe();
expect(resultingUser).toEqual(new User('123', 'path/to/image', 'Me'));
expect(resultingUser).toEqual(new UserDto('123', 'mail@domain.com', 'Me', 'path/to/image', ));
usersService.verifyAll();
});

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

@ -9,13 +9,13 @@ import * as Ng2 from '@angular/core';
import { Observable, } from 'rxjs';
import { User, UsersService } from './users.service';
import { UserDto, UsersService } from './users.service';
import { AuthService } from './auth.service';
@Ng2.Injectable()
export class UsersProviderService {
private readonly caches: { [id: string]: Observable<User> } = {};
private readonly caches: { [id: string]: Observable<UserDto> } = {};
constructor(
private readonly usersService: UsersService,
@ -23,20 +23,24 @@ export class UsersProviderService {
) {
}
public getUser(id: string): Observable<User> {
public getUser(id: string): Observable<UserDto> {
let result = this.caches[id];
if (!result) {
result = this.caches[id] =
const request =
this.usersService.getUser(id)
.map(u => {
if (this.authService.user && u.id === this.authService.user.id) {
return new User(u.id, u.profileUrl, 'Me');
return new UserDto(u.id, u.email, 'Me', u.pictureUrl);
} else {
return u;
}
})
.publishLast().refCount();
.publishLast();
request.connect();
result = this.caches[id] = request;
}
return result;

43
src/Squidex/app/shared/services/users.service.spec.ts

@ -13,7 +13,7 @@ import { Observable } from 'rxjs';
import {
ApiUrlConfig,
AuthService,
User,
UserDto,
UsersService,
} from './../';
@ -33,19 +33,21 @@ describe('UsersService', () => {
new Ng2Http.ResponseOptions({
body: [{
id: '123',
profileUrl: 'path/to/image1',
displayName: 'mail1@domain.com'
email: 'mail1@domain.com',
displayName: 'User1',
pictureUrl: 'path/to/image1'
}, {
id: '456',
profileUrl: 'path/to/image2',
displayName: 'mail2@domain.com'
email: 'mail2@domain.com',
displayName: 'User2',
pictureUrl: 'path/to/image2'
}]
})
)
))
.verifiable(TypeMoq.Times.once());
let user: User[] = null;
let user: UserDto[] = null;
usersService.getUsers().subscribe(result => {
user = result;
@ -53,8 +55,8 @@ describe('UsersService', () => {
expect(user).toEqual(
[
new User('123', 'path/to/image1', 'mail1@domain.com'),
new User('456', 'path/to/image2', 'mail2@domain.com')
new UserDto('123', 'mail1@domain.com', 'User1', 'path/to/image1'),
new UserDto('456', 'mail2@domain.com', 'User2', 'path/to/image2')
]);
authService.verifyAll();
@ -67,19 +69,21 @@ describe('UsersService', () => {
new Ng2Http.ResponseOptions({
body: [{
id: '123',
profileUrl: 'path/to/image1',
displayName: 'mail1@domain.com'
email: 'mail1@domain.com',
displayName: 'User1',
pictureUrl: 'path/to/image1'
}, {
id: '456',
profileUrl: 'path/to/image2',
displayName: 'mail2@domain.com'
email: 'mail2@domain.com',
displayName: 'User2',
pictureUrl: 'path/to/image2'
}]
})
)
))
.verifiable(TypeMoq.Times.once());
let user: User[] = null;
let user: UserDto[] = null;
usersService.getUsers('my-query').subscribe(result => {
user = result;
@ -87,8 +91,8 @@ describe('UsersService', () => {
expect(user).toEqual(
[
new User('123', 'path/to/image1', 'mail1@domain.com'),
new User('456', 'path/to/image2', 'mail2@domain.com')
new UserDto('123', 'mail1@domain.com', 'User1', 'path/to/image1'),
new UserDto('456', 'mail2@domain.com', 'User2', 'path/to/image2')
]);
authService.verifyAll();
@ -101,21 +105,22 @@ describe('UsersService', () => {
new Ng2Http.ResponseOptions({
body: {
id: '123',
profileUrl: 'path/to/image',
displayName: 'mail@domain.com'
email: 'mail1@domain.com',
displayName: 'User1',
pictureUrl: 'path/to/image1'
}
})
)
))
.verifiable(TypeMoq.Times.once());
let user: User = null;
let user: UserDto = null;
usersService.getUser('123').subscribe(result => {
user = result;
}).unsubscribe();
expect(user).toEqual(new User('123', 'path/to/image', 'mail@domain.com'));
expect(user).toEqual(new UserDto('123', 'mail1@domain.com', 'User1', 'path/to/image1'));
authService.verifyAll();
});

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

@ -13,11 +13,12 @@ import { ApiUrlConfig } from 'framework';
import { AuthService } from './auth.service';
export class User {
export class UserDto {
constructor(
public readonly id: string,
public readonly profileUrl: string,
public readonly displayName: string
public readonly email: string,
public readonly displayName: string,
public readonly pictureUrl: string
) {
}
}
@ -30,29 +31,31 @@ export class UsersService {
) {
}
public getUsers(query?: string): Observable<User[]> {
public getUsers(query?: string): Observable<UserDto[]> {
return this.authService.authGet(this.apiUrl.buildUrl(`api/users/?query=${query || ''}`))
.map(response => {
const body: any[] = response.json() || [];
return body.map(item => {
return new User(
return new UserDto(
item.id,
item.profileUrl,
item.displayName);
item.email,
item.displayName,
item.pictureUrl);
});
});
}
public getUser(id: string): Observable<User> {
public getUser(id: string): Observable<UserDto> {
return this.authService.authGet(this.apiUrl.buildUrl(`api/users/${id}`))
.map(response => {
const body: any = response.json();
return new User(
return new UserDto(
body.id,
body.profileUrl,
body.displayName);
body.email,
body.displayName,
body.pictureUrl);
});
}
}

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

@ -1,6 +1,18 @@
@import '_mixins';
@import '_vars';
.table {
&-fixed {
table-layout: fixed;
}
&-borderless {
td {
border: none;
}
}
}
.form-hint {
font-size: .8rem;
}

39
src/Squidex/app/theme/_completer.scss

@ -0,0 +1,39 @@
@import '_mixins';
@import '_vars';
.completer-dropdown {
& {
margin-top: 2px !important;
width: 400px !important;
}
.completer {
&-image {
@include circle(2.2rem);
}
&-row {
& {
@include clearfix;
width: auto !important;
margin: 0;
margin-bottom: 0 !important;
display: block !important;
}
}
&-selected-row {
background: $color-theme-blue-dark !important;
}
&-description {
font-style: italic;
}
&-title,
&-description {
@include truncate;
padding-left: .3rem;
}
}
}

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

@ -15,48 +15,61 @@ h1 {
display: none;
}
.card {
@include box-shadow(0, 2px, 2px, .1);
}
.layout {
& {
@include fixed(54px, 0, 0, 0);
@include flex-box;
@include flex-flow(row);
}
&-title-icon {
margin-right: .6rem;
font-size: 1.7rem;
font-weight: normal;
}
&-left,
&-right,
&-middle-header,
&-middle-content {
padding: $padding-layout-v $padding-layout-h;
}
&-left,
&-right {
background: $sidebar-color;
position: relative;
width: 220px;
}
&-new-button {
display: block;
width: 100%;
&-right {
border-left: 1px solid $color-border;
}
&-content {
&-left {
border-right: 1px solid $color-border;
}
&-middle {
& {
@include fixed(54px, 0, 0, 0);
@include flex-grow(1);
@include flex-box;
@include flex-flow(row);
@include flex-flow(column);
}
&-left,
&-right {
&-header {
@include box-shadow(0, 2px, 2px, .1);
background: $sidebar-color;
padding: $padding-layout;
position: relative;
width: 220px;
border-bottom: 1px solid $color-border;
}
&-right {
border-left: 1px solid $color-border;
}
&-left {
& {
border-right: 1px solid $color-border;
}
&--no-button {
padding-top: $padding-layout + 38px;
}
}
&-main {
&-content {
@include flex-grow(1);
padding: $padding-layout;
padding: $padding-layout-v;
}
}
}

28
src/Squidex/app/theme/_mixins.scss

@ -1,6 +1,6 @@
@mixin clearfix() {
&::after {
content: "";
content: '';
display: table;
clear: both;
}
@ -72,6 +72,32 @@
box-sizing: border-box;
}
@mixin caret-top() {
display: inline-block;
width: 0;
height: 0;
content: '';
border-bottom: .3em solid;
border-right: .3em solid transparent;
border-left: .3em solid transparent;
}
@mixin caret-bottom() {
display: inline-block;
width: 0;
height: 0;
content: '';
border-top: .3em solid;
border-right: .3em solid transparent;
border-left: .3em solid transparent;
}
@mixin circle($size) {
@include border-radius($size * .5);
width: $size;
height: $size;
}
@mixin wrap() {
-ms-word-break: normal;
word-break: normal;

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

@ -27,5 +27,8 @@ $color-theme-error-dark: darken($color-theme-error, 5%);
$color-accent-dark: #fff;
$padding-layout: 30px;
$color-card-footer: #fff;
$padding-layout-h: 30px;
$padding-layout-v: 20px;

9
src/Squidex/app/theme/_vendor-overrides.scss

@ -7,4 +7,11 @@ $brand-primary: $color-theme-blue;
$brand-success: $color-theme-green;
$btn-secondary-bg: #dce5e8;
$btn-secondary-border: #dce5e8;
$btn-secondary-border: #dce5e8;
$card-border-color: $color-border;
$card-cap-bg: $color-card-footer;
.completer-input {
@extend .form-control;
}

3
src/Squidex/app/theme/theme.scss

@ -1,2 +1,3 @@
@import '_bootstrap.scss';
@import '_layout.scss';
@import '_layout.scss';
@import '_completer.scss';

1
src/Squidex/package.json

@ -29,6 +29,7 @@
"immutable": "^3.8.1",
"moment": "^2.14.0",
"mousetrap": "^1.6.0",
"ng2-completer": "^0.2.3",
"oidc-client": "^1.2.1-beta.3",
"reflect-metadata": "^0.1.3",
"rxjs": "5.0.0-beta.12",

Loading…
Cancel
Save