Browse Source

Merge pull request #327 from Squidex/refactorings

Refactorings
pull/329/head
Sebastian Stehle 8 years ago
committed by GitHub
parent
commit
d646e0beb7
No known key found for this signature in database GPG Key ID: 4AEE18F83AFDEB23
  1. 7
      Nuget.config
  2. 2
      extensions/Squidex.Extensions/Squidex.Extensions.csproj
  3. 4
      src/Squidex.Domain.Apps.Core.Model/Squidex.Domain.Apps.Core.Model.csproj
  4. 2
      src/Squidex.Domain.Apps.Core.Operations/Squidex.Domain.Apps.Core.Operations.csproj
  5. 6
      src/Squidex.Domain.Apps.Entities/Squidex.Domain.Apps.Entities.csproj
  6. 2
      src/Squidex.Domain.Users.MongoDb/Squidex.Domain.Users.MongoDb.csproj
  7. 2
      src/Squidex.Domain.Users/Squidex.Domain.Users.csproj
  8. 8
      src/Squidex.Infrastructure/Squidex.Infrastructure.csproj
  9. 20
      src/Squidex/Squidex.csproj
  10. 2
      src/Squidex/app/features/administration/guards/user-must-exist.guard.ts
  11. 3
      src/Squidex/app/features/administration/pages/event-consumers/event-consumers-page.component.ts
  12. 8
      src/Squidex/app/features/administration/pages/restore/restore-page.component.ts
  13. 4
      src/Squidex/app/features/content/module.ts
  14. 2
      src/Squidex/app/features/content/pages/content/content-field.component.html
  15. 13
      src/Squidex/app/features/content/pages/content/content-field.component.ts
  16. 5
      src/Squidex/app/features/content/pages/content/content-history.component.html
  17. 9
      src/Squidex/app/features/content/pages/content/content-history.component.ts
  18. 12
      src/Squidex/app/features/content/pages/content/content-page.component.ts
  19. 8
      src/Squidex/app/features/content/pages/contents/contents-page.component.html
  20. 49
      src/Squidex/app/features/content/pages/contents/contents-page.component.ts
  21. 1
      src/Squidex/app/features/content/pages/schemas/schemas-page.component.html
  22. 10
      src/Squidex/app/features/content/shared/assets-editor.component.ts
  23. 5
      src/Squidex/app/features/content/shared/content-item.component.ts
  24. 8
      src/Squidex/app/features/content/shared/contents-selector.component.html
  25. 20
      src/Squidex/app/features/content/shared/references-editor.component.ts
  26. 2
      src/Squidex/app/features/rules/pages/rules/triggers/content-changed-trigger.component.ts
  27. 6
      src/Squidex/app/features/schemas/pages/schema/schema-page.component.ts
  28. 5
      src/Squidex/app/framework/angular/forms/autocomplete.component.ts
  29. 16
      src/Squidex/app/framework/angular/forms/date-time-editor.component.ts
  30. 5
      src/Squidex/app/framework/angular/forms/dropdown.component.ts
  31. 2
      src/Squidex/app/framework/angular/forms/iframe-editor.component.html
  32. 16
      src/Squidex/app/framework/angular/forms/iframe-editor.component.ts
  33. 5
      src/Squidex/app/framework/angular/forms/jscript-editor.component.ts
  34. 5
      src/Squidex/app/framework/angular/forms/json-editor.component.ts
  35. 13
      src/Squidex/app/framework/angular/forms/progress-bar.component.ts
  36. 5
      src/Squidex/app/framework/angular/forms/slider.component.ts
  37. 7
      src/Squidex/app/framework/angular/forms/stars.component.ts
  38. 1
      src/Squidex/app/framework/angular/forms/tag-editor.component.html
  39. 44
      src/Squidex/app/framework/angular/forms/tag-editor.component.ts
  40. 5
      src/Squidex/app/framework/angular/forms/toggle.component.ts
  41. 31
      src/Squidex/app/framework/angular/http/loading.interceptor.ts
  42. 23
      src/Squidex/app/framework/angular/image-source.directive.ts
  43. 20
      src/Squidex/app/framework/angular/modals/dialog-renderer.component.ts
  44. 4
      src/Squidex/app/framework/angular/modals/modal-target.directive.ts
  45. 2
      src/Squidex/app/framework/angular/modals/modal-view.directive.ts
  46. 8
      src/Squidex/app/framework/angular/modals/onboarding-tooltip.component.ts
  47. 5
      src/Squidex/app/framework/angular/modals/root-view.component.ts
  48. 5
      src/Squidex/app/framework/angular/modals/tooltip.component.ts
  49. 17
      src/Squidex/app/framework/angular/panel-container.directive.ts
  50. 8
      src/Squidex/app/framework/angular/panel.component.ts
  51. 22
      src/Squidex/app/framework/angular/routers/router-utils.ts
  52. 5
      src/Squidex/app/framework/angular/shortcut.component.ts
  53. 5
      src/Squidex/app/framework/angular/title.component.ts
  54. 5
      src/Squidex/app/framework/angular/user-report.component.ts
  55. 1
      src/Squidex/app/framework/declarations.ts
  56. 1
      src/Squidex/app/framework/internal.ts
  57. 11
      src/Squidex/app/framework/module.ts
  58. 114
      src/Squidex/app/framework/services/loading.service.spec.ts
  59. 68
      src/Squidex/app/framework/services/loading.service.ts
  60. 7
      src/Squidex/app/shared/components/app-form.component.ts
  61. 8
      src/Squidex/app/shared/components/asset.component.html
  62. 26
      src/Squidex/app/shared/components/asset.component.scss
  63. 24
      src/Squidex/app/shared/components/asset.component.ts
  64. 5
      src/Squidex/app/shared/components/assets-list.component.ts
  65. 5
      src/Squidex/app/shared/components/assets-selector.component.ts
  66. 85
      src/Squidex/app/shared/components/geolocation-editor.component.ts
  67. 8
      src/Squidex/app/shared/components/help.component.ts
  68. 4
      src/Squidex/app/shared/components/history-list.component.html
  69. 21
      src/Squidex/app/shared/components/history-list.component.ts
  70. 7
      src/Squidex/app/shared/components/history.component.ts
  71. 5
      src/Squidex/app/shared/components/language-selector.component.ts
  72. 5
      src/Squidex/app/shared/components/markdown-editor.component.ts
  73. 46
      src/Squidex/app/shared/components/pipes.ts
  74. 7
      src/Squidex/app/shared/components/rich-editor.component.ts
  75. 2
      src/Squidex/app/shared/components/schema-category.component.html
  76. 12
      src/Squidex/app/shared/components/schema-category.component.ts
  77. 4
      src/Squidex/app/shared/guards/content-must-exist.guard.spec.ts
  78. 2
      src/Squidex/app/shared/guards/content-must-exist.guard.ts
  79. 4
      src/Squidex/app/shared/guards/must-be-authenticated.guard.spec.ts
  80. 4
      src/Squidex/app/shared/guards/must-be-not-authenticated.guard.spec.ts
  81. 41
      src/Squidex/app/shared/guards/schema-must-exist-published.guard.spec.ts
  82. 10
      src/Squidex/app/shared/guards/schema-must-exist-published.guard.ts
  83. 2
      src/Squidex/app/shared/guards/schema-must-exist.guard.spec.ts
  84. 2
      src/Squidex/app/shared/guards/schema-must-exist.guard.ts
  85. 105
      src/Squidex/app/shared/guards/schema-must-not-be-singleton.guard.spec.ts
  86. 42
      src/Squidex/app/shared/guards/schema-must-not-be-singleton.guard.ts
  87. 1
      src/Squidex/app/shared/internal.ts
  88. 5
      src/Squidex/app/shared/module.ts
  89. 2
      src/Squidex/app/shared/state/contents.forms.ts
  90. 2
      src/Squidex/app/shared/state/contents.state.ts
  91. 4
      src/Squidex/app/shared/state/queries.spec.ts
  92. 5
      src/Squidex/app/shell/pages/app/left-menu.component.ts
  93. 5
      src/Squidex/app/shell/pages/internal/apps-menu.component.ts
  94. 8
      src/Squidex/app/shell/pages/internal/internal-area.component.html
  95. 4
      src/Squidex/app/shell/pages/internal/internal-area.component.scss
  96. 8
      src/Squidex/app/shell/pages/internal/internal-area.component.ts
  97. 10
      src/Squidex/app/shell/pages/internal/profile-menu.component.ts
  98. 14
      src/Squidex/app/theme/_common.scss
  99. 4
      src/Squidex/app/theme/icomoon/demo-files/demo.css
  100. 776
      src/Squidex/app/theme/icomoon/demo.html

7
Nuget.config

@ -1,7 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<configuration>
<packageSources>
<add key="myget.org" value="https://dotnet.myget.org/F/orleans-ci/api/v3/index.json" protocolVersion="3" />
<add key="nuget.org" value="https://api.nuget.org/v3/index.json" protocolVersion="3" />
</packageSources>
</configuration>

2
extensions/Squidex.Extensions/Squidex.Extensions.csproj

@ -10,7 +10,7 @@
<ProjectReference Include="..\..\src\Squidex.Domain.Apps.Core.Operations\Squidex.Domain.Apps.Core.Operations.csproj" /> <ProjectReference Include="..\..\src\Squidex.Domain.Apps.Core.Operations\Squidex.Domain.Apps.Core.Operations.csproj" />
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="Algolia.Search" Version="5.1.0" /> <PackageReference Include="Algolia.Search" Version="5.2.0" />
<PackageReference Include="CoreTweet" Version="0.9.0.415" /> <PackageReference Include="CoreTweet" Version="0.9.0.415" />
<PackageReference Include="Elasticsearch.Net" Version="6.3.1" /> <PackageReference Include="Elasticsearch.Net" Version="6.3.1" />
<PackageReference Include="Microsoft.Extensions.Http" Version="2.1.1" /> <PackageReference Include="Microsoft.Extensions.Http" Version="2.1.1" />

4
src/Squidex.Domain.Apps.Core.Model/Squidex.Domain.Apps.Core.Model.csproj

@ -8,11 +8,11 @@
<DebugSymbols>True</DebugSymbols> <DebugSymbols>True</DebugSymbols>
</PropertyGroup> </PropertyGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="Fody" Version="3.2.4"> <PackageReference Include="Fody" Version="3.2.9">
<PrivateAssets>all</PrivateAssets> <PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers</IncludeAssets> <IncludeAssets>runtime; build; native; contentfiles; analyzers</IncludeAssets>
</PackageReference> </PackageReference>
<PackageReference Include="Freezable.Fody" Version="1.9.1" /> <PackageReference Include="Freezable.Fody" Version="1.9.3" />
<PackageReference Include="System.Collections.Immutable" Version="1.5.0" /> <PackageReference Include="System.Collections.Immutable" Version="1.5.0" />
<PackageReference Include="System.ComponentModel.Annotations" Version="4.5.0" /> <PackageReference Include="System.ComponentModel.Annotations" Version="4.5.0" />
<PackageReference Include="System.ValueTuple" Version="4.5.0" /> <PackageReference Include="System.ValueTuple" Version="4.5.0" />

2
src/Squidex.Domain.Apps.Core.Operations/Squidex.Domain.Apps.Core.Operations.csproj

@ -17,7 +17,7 @@
<PackageReference Include="Jint" Version="2.11.58" /> <PackageReference Include="Jint" Version="2.11.58" />
<PackageReference Include="Microsoft.OData.Core" Version="7.5.1" /> <PackageReference Include="Microsoft.OData.Core" Version="7.5.1" />
<PackageReference Include="Newtonsoft.Json" Version="11.0.2" /> <PackageReference Include="Newtonsoft.Json" Version="11.0.2" />
<PackageReference Include="NJsonSchema" Version="9.10.75" /> <PackageReference Include="NJsonSchema" Version="9.11.0" />
<PackageReference Include="RefactoringEssentials" Version="5.6.0" PrivateAssets="all" /> <PackageReference Include="RefactoringEssentials" Version="5.6.0" PrivateAssets="all" />
<PackageReference Include="StyleCop.Analyzers" Version="1.0.2" PrivateAssets="all" /> <PackageReference Include="StyleCop.Analyzers" Version="1.0.2" PrivateAssets="all" />
<PackageReference Include="System.Collections.Immutable" Version="1.5.0" /> <PackageReference Include="System.Collections.Immutable" Version="1.5.0" />

6
src/Squidex.Domain.Apps.Entities/Squidex.Domain.Apps.Entities.csproj

@ -14,12 +14,12 @@
<ProjectReference Include="..\Squidex.Shared\Squidex.Shared.csproj" /> <ProjectReference Include="..\Squidex.Shared\Squidex.Shared.csproj" />
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="GraphQL" Version="2.0.0" /> <PackageReference Include="GraphQL" Version="2.1.0" />
<PackageReference Include="Microsoft.Orleans.CodeGenerator.MSBuild" Version="2.1.0-rc2"> <PackageReference Include="Microsoft.Orleans.CodeGenerator.MSBuild" Version="2.1.0">
<PrivateAssets>all</PrivateAssets> <PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers</IncludeAssets> <IncludeAssets>runtime; build; native; contentfiles; analyzers</IncludeAssets>
</PackageReference> </PackageReference>
<PackageReference Include="Microsoft.Orleans.Core" Version="2.1.0-rc2" /> <PackageReference Include="Microsoft.Orleans.Core" Version="2.1.0" />
<PackageReference Include="NodaTime" Version="2.4.0" /> <PackageReference Include="NodaTime" Version="2.4.0" />
<PackageReference Include="RefactoringEssentials" Version="5.6.0" PrivateAssets="all" /> <PackageReference Include="RefactoringEssentials" Version="5.6.0" PrivateAssets="all" />
<PackageReference Include="StyleCop.Analyzers" Version="1.0.2" PrivateAssets="all" /> <PackageReference Include="StyleCop.Analyzers" Version="1.0.2" PrivateAssets="all" />

2
src/Squidex.Domain.Users.MongoDb/Squidex.Domain.Users.MongoDb.csproj

@ -19,7 +19,7 @@
<PackageReference Include="MongoDB.Driver" Version="2.7.0" /> <PackageReference Include="MongoDB.Driver" Version="2.7.0" />
<PackageReference Include="RefactoringEssentials" Version="5.6.0" PrivateAssets="all" /> <PackageReference Include="RefactoringEssentials" Version="5.6.0" PrivateAssets="all" />
<PackageReference Include="StyleCop.Analyzers" Version="1.0.2" PrivateAssets="all" /> <PackageReference Include="StyleCop.Analyzers" Version="1.0.2" PrivateAssets="all" />
<PackageReference Include="System.Security.Principal.Windows" Version="4.5.0" /> <PackageReference Include="System.Security.Principal.Windows" Version="4.5.1" />
</ItemGroup> </ItemGroup>
<PropertyGroup> <PropertyGroup>
<CodeAnalysisRuleSet>..\..\Squidex.ruleset</CodeAnalysisRuleSet> <CodeAnalysisRuleSet>..\..\Squidex.ruleset</CodeAnalysisRuleSet>

2
src/Squidex.Domain.Users/Squidex.Domain.Users.csproj

@ -17,7 +17,7 @@
<PackageReference Include="SharpPwned.NET" Version="1.0.8" /> <PackageReference Include="SharpPwned.NET" Version="1.0.8" />
<PackageReference Include="StyleCop.Analyzers" Version="1.0.2" PrivateAssets="all" /> <PackageReference Include="StyleCop.Analyzers" Version="1.0.2" PrivateAssets="all" />
<PackageReference Include="System.Linq.Queryable" Version="4.3.0" /> <PackageReference Include="System.Linq.Queryable" Version="4.3.0" />
<PackageReference Include="System.Security.Principal.Windows" Version="4.5.0" /> <PackageReference Include="System.Security.Principal.Windows" Version="4.5.1" />
</ItemGroup> </ItemGroup>
<PropertyGroup> <PropertyGroup>
<CodeAnalysisRuleSet>..\..\Squidex.ruleset</CodeAnalysisRuleSet> <CodeAnalysisRuleSet>..\..\Squidex.ruleset</CodeAnalysisRuleSet>

8
src/Squidex.Infrastructure/Squidex.Infrastructure.csproj

@ -11,12 +11,12 @@
<PackageReference Include="Microsoft.Extensions.Caching.Abstractions" Version="2.1.2" /> <PackageReference Include="Microsoft.Extensions.Caching.Abstractions" Version="2.1.2" />
<PackageReference Include="Microsoft.Extensions.Logging" Version="2.1.1" /> <PackageReference Include="Microsoft.Extensions.Logging" Version="2.1.1" />
<PackageReference Include="Microsoft.OData.Core" Version="7.5.1" /> <PackageReference Include="Microsoft.OData.Core" Version="7.5.1" />
<PackageReference Include="Microsoft.Orleans.CodeGenerator.MSBuild" Version="2.1.0-rc2"> <PackageReference Include="Microsoft.Orleans.CodeGenerator.MSBuild" Version="2.1.0">
<PrivateAssets>all</PrivateAssets> <PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers</IncludeAssets> <IncludeAssets>runtime; build; native; contentfiles; analyzers</IncludeAssets>
</PackageReference> </PackageReference>
<PackageReference Include="Microsoft.Orleans.Core" Version="2.1.0-rc2" /> <PackageReference Include="Microsoft.Orleans.Core" Version="2.1.0" />
<PackageReference Include="Microsoft.Orleans.OrleansRuntime" Version="2.1.0-rc2" /> <PackageReference Include="Microsoft.Orleans.OrleansRuntime" Version="2.1.0" />
<PackageReference Include="Newtonsoft.Json" Version="11.0.2" /> <PackageReference Include="Newtonsoft.Json" Version="11.0.2" />
<PackageReference Include="NodaTime" Version="2.4.0" /> <PackageReference Include="NodaTime" Version="2.4.0" />
<PackageReference Include="RefactoringEssentials" Version="5.6.0" PrivateAssets="all" /> <PackageReference Include="RefactoringEssentials" Version="5.6.0" PrivateAssets="all" />
@ -25,7 +25,7 @@
<PackageReference Include="System.Collections.Immutable" Version="1.5.0" /> <PackageReference Include="System.Collections.Immutable" Version="1.5.0" />
<PackageReference Include="System.ComponentModel.Annotations" Version="4.5.0" /> <PackageReference Include="System.ComponentModel.Annotations" Version="4.5.0" />
<PackageReference Include="System.Linq" Version="4.3.0" /> <PackageReference Include="System.Linq" Version="4.3.0" />
<PackageReference Include="System.Reactive" Version="4.1.0" /> <PackageReference Include="System.Reactive" Version="4.1.1" />
<PackageReference Include="System.Reflection.TypeExtensions" Version="4.5.1" /> <PackageReference Include="System.Reflection.TypeExtensions" Version="4.5.1" />
<PackageReference Include="System.Security.Claims" Version="4.3.0" /> <PackageReference Include="System.Security.Claims" Version="4.3.0" />
<PackageReference Include="System.Threading.Tasks.Dataflow" Version="4.9.0" /> <PackageReference Include="System.Threading.Tasks.Dataflow" Version="4.9.0" />

20
src/Squidex/Squidex.csproj

@ -54,36 +54,36 @@
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="Algolia.Search" Version="5.1.0" /> <PackageReference Include="Algolia.Search" Version="5.2.0" />
<PackageReference Include="Ben.BlockingDetector" Version="0.0.3" /> <PackageReference Include="Ben.BlockingDetector" Version="0.0.3" />
<PackageReference Include="EventStore.ClientAPI.NetCore" Version="4.1.0.23" /> <PackageReference Include="EventStore.ClientAPI.NetCore" Version="4.1.0.23" />
<PackageReference Include="IdentityServer4" Version="2.2.0" /> <PackageReference Include="IdentityServer4" Version="2.2.0" />
<PackageReference Include="IdentityServer4.AccessTokenValidation" Version="2.6.0" /> <PackageReference Include="IdentityServer4.AccessTokenValidation" Version="2.6.0" />
<PackageReference Include="IdentityServer4.AspNetIdentity" Version="2.1.0" /> <PackageReference Include="IdentityServer4.AspNetIdentity" Version="2.1.0" />
<PackageReference Include="Microsoft.AspNetCore" Version="2.1.3" /> <PackageReference Include="Microsoft.AspNetCore" Version="2.1.4" />
<PackageReference Include="Microsoft.AspNetCore.Authentication.Cookies" Version="2.1.2" /> <PackageReference Include="Microsoft.AspNetCore.Authentication.Cookies" Version="2.1.2" />
<PackageReference Include="Microsoft.AspNetCore.Authentication.Google" Version="2.1.2" /> <PackageReference Include="Microsoft.AspNetCore.Authentication.Google" Version="2.1.2" />
<PackageReference Include="Microsoft.AspNetCore.Authentication.MicrosoftAccount" Version="2.1.2" /> <PackageReference Include="Microsoft.AspNetCore.Authentication.MicrosoftAccount" Version="2.1.2" />
<PackageReference Include="Microsoft.AspNetCore.HttpOverrides" Version="2.1.1" /> <PackageReference Include="Microsoft.AspNetCore.HttpOverrides" Version="2.1.1" />
<PackageReference Include="Microsoft.AspNetCore.Identity" Version="2.1.3" /> <PackageReference Include="Microsoft.AspNetCore.Identity" Version="2.1.3" />
<PackageReference Include="Microsoft.AspNetCore.Mvc" Version="2.1.2" /> <PackageReference Include="Microsoft.AspNetCore.Mvc" Version="2.1.3" />
<PackageReference Include="Microsoft.AspNetCore.Mvc.Razor.ViewCompilation" Version="2.1.1" /> <PackageReference Include="Microsoft.AspNetCore.Mvc.Razor.ViewCompilation" Version="2.1.1" />
<PackageReference Include="Microsoft.AspNetCore.StaticFiles" Version="2.1.1" /> <PackageReference Include="Microsoft.AspNetCore.StaticFiles" Version="2.1.1" />
<PackageReference Include="Microsoft.Extensions.DependencyModel" Version="2.1.0" /> <PackageReference Include="Microsoft.Extensions.DependencyModel" Version="2.1.0" />
<PackageReference Include="Microsoft.Extensions.Options.ConfigurationExtensions" Version="2.1.1" /> <PackageReference Include="Microsoft.Extensions.Options.ConfigurationExtensions" Version="2.1.1" />
<PackageReference Include="Microsoft.Data.Edm" Version="5.8.4" /> <PackageReference Include="Microsoft.Data.Edm" Version="5.8.4" />
<PackageReference Include="Microsoft.OData.Core" Version="7.5.1" /> <PackageReference Include="Microsoft.OData.Core" Version="7.5.1" />
<PackageReference Include="Microsoft.Orleans.Client" Version="2.1.0-rc2" /> <PackageReference Include="Microsoft.Orleans.Client" Version="2.1.0" />
<PackageReference Include="Microsoft.Orleans.Core" Version="2.1.0-rc2" /> <PackageReference Include="Microsoft.Orleans.Core" Version="2.1.0" />
<PackageReference Include="Microsoft.Orleans.Core.Abstractions" Version="2.1.0-rc2" /> <PackageReference Include="Microsoft.Orleans.Core.Abstractions" Version="2.1.0" />
<PackageReference Include="Microsoft.Orleans.OrleansRuntime" Version="2.1.0-rc2" /> <PackageReference Include="Microsoft.Orleans.OrleansRuntime" Version="2.1.0" />
<PackageReference Include="MongoDB.Driver" Version="2.7.0" /> <PackageReference Include="MongoDB.Driver" Version="2.7.0" />
<PackageReference Include="NJsonSchema" Version="9.10.75" /> <PackageReference Include="NJsonSchema" Version="9.11.0" />
<PackageReference Include="NodaTime.Serialization.JsonNet" Version="2.0.0" /> <PackageReference Include="NodaTime.Serialization.JsonNet" Version="2.0.0" />
<PackageReference Include="NSwag.AspNetCore" Version="11.19.2" /> <PackageReference Include="NSwag.AspNetCore" Version="11.20.1" />
<PackageReference Include="OpenCover" Version="4.6.519" /> <PackageReference Include="OpenCover" Version="4.6.519" />
<PackageReference Include="Orleans.Providers.MongoDB" Version="2.0.1" /> <PackageReference Include="Orleans.Providers.MongoDB" Version="2.0.1" />
<PackageReference Include="OrleansDashboard" Version="2.0.9" /> <PackageReference Include="OrleansDashboard" Version="2.1.2" />
<PackageReference Include="RefactoringEssentials" Version="5.6.0" PrivateAssets="all" /> <PackageReference Include="RefactoringEssentials" Version="5.6.0" PrivateAssets="all" />
<PackageReference Include="ReportGenerator" Version="3.1.2" /> <PackageReference Include="ReportGenerator" Version="3.1.2" />
<PackageReference Include="StackExchange.Redis.StrongName" Version="1.2.6" /> <PackageReference Include="StackExchange.Redis.StrongName" Version="1.2.6" />

2
src/Squidex/app/features/administration/guards/user-must-exist.guard.ts

@ -32,7 +32,7 @@ export class UserMustExistGuard implements CanActivate {
this.router.navigate(['/404']); this.router.navigate(['/404']);
} }
}), }),
map(u => u !== null)); map(u => !!u));
return result; return result;
} }

3
src/Squidex/app/features/administration/pages/event-consumers/event-consumers-page.component.ts

@ -38,8 +38,7 @@ export class EventConsumersPageComponent implements OnDestroy, OnInit {
this.eventConsumersState.load(false, true).pipe(onErrorResumeNext()).subscribe(); this.eventConsumersState.load(false, true).pipe(onErrorResumeNext()).subscribe();
this.timerSubscription = this.timerSubscription =
timer(2000, 2000).pipe( timer(2000, 2000).pipe(switchMap(x => this.eventConsumersState.load(true, true)), onErrorResumeNext())
switchMap(x => this.eventConsumersState.load(true, true)), onErrorResumeNext())
.subscribe(); .subscribe();
} }

8
src/Squidex/app/features/administration/pages/restore/restore-page.component.ts

@ -8,7 +8,7 @@
import { Component, OnDestroy, OnInit } from '@angular/core'; import { Component, OnDestroy, OnInit } from '@angular/core';
import { FormBuilder } from '@angular/forms'; import { FormBuilder } from '@angular/forms';
import { Subscription, timer } from 'rxjs'; import { Subscription, timer } from 'rxjs';
import { switchMap } from 'rxjs/operators'; import { filter, switchMap } from 'rxjs/operators';
import { import {
AuthService, AuthService,
@ -43,11 +43,9 @@ export class RestorePageComponent implements OnDestroy, OnInit {
public ngOnInit() { public ngOnInit() {
this.timerSubscription = this.timerSubscription =
timer(0, 2000).pipe(switchMap(() => this.backupsService.getRestore())) timer(0, 2000).pipe(switchMap(() => this.backupsService.getRestore()), filter(x => !!x))
.subscribe(dto => { .subscribe(dto => {
if (dto !== null) { this.restoreJob = dto!;
this.restoreJob = dto;
}
}); });
} }

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

@ -15,6 +15,7 @@ import {
ContentMustExistGuard, ContentMustExistGuard,
LoadLanguagesGuard, LoadLanguagesGuard,
SchemaMustExistPublishedGuard, SchemaMustExistPublishedGuard,
SchemaMustNotBeSingletonGuard,
SqxFrameworkModule, SqxFrameworkModule,
SqxSharedModule, SqxSharedModule,
UnsetContentGuard UnsetContentGuard
@ -52,12 +53,13 @@ const routes: Routes = [
{ {
path: '', path: '',
component: ContentsPageComponent, component: ContentsPageComponent,
canActivate: [SchemaMustNotBeSingletonGuard],
canDeactivate: [CanDeactivateGuard] canDeactivate: [CanDeactivateGuard]
}, },
{ {
path: 'new', path: 'new',
component: ContentPageComponent, component: ContentPageComponent,
canActivate: [UnsetContentGuard], canActivate: [SchemaMustNotBeSingletonGuard, UnsetContentGuard],
canDeactivate: [CanDeactivateGuard] canDeactivate: [CanDeactivateGuard]
}, },
{ {

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

@ -1,4 +1,4 @@
<div class="table-items-row" [class.invalid]="fieldForm.invalid"> <div class="table-items-row" [class.invalid]="isInvalid | async">
<ng-container *ngIf="field.isLocalizable && languages.length > 1"> <ng-container *ngIf="field.isLocalizable && languages.length > 1">
<div class="languages-buttons" #buttonLanguages> <div class="languages-buttons" #buttonLanguages>
<sqx-language-selector size="sm" <sqx-language-selector size="sm"

13
src/Squidex/app/features/content/pages/content/content-field.component.ts

@ -5,8 +5,9 @@
* Copyright (c) Squidex UG (haftungsbeschränkt). All rights reserved. * Copyright (c) Squidex UG (haftungsbeschränkt). All rights reserved.
*/ */
import { Component, EventEmitter, Input, OnChanges, Output, SimpleChanges } from '@angular/core'; import { ChangeDetectionStrategy, Component, EventEmitter, Input, OnChanges, Output, SimpleChanges } from '@angular/core';
import { AbstractControl, FormGroup } from '@angular/forms'; import { AbstractControl, FormGroup } from '@angular/forms';
import { Observable } from 'rxjs';
import { import {
AppLanguageDto, AppLanguageDto,
@ -16,11 +17,13 @@ import {
RootFieldDto, RootFieldDto,
Types Types
} from '@app/shared'; } from '@app/shared';
import { map, startWith } from 'rxjs/operators';
@Component({ @Component({
selector: 'sqx-content-field', selector: 'sqx-content-field',
styleUrls: ['./content-field.component.scss'], styleUrls: ['./content-field.component.scss'],
templateUrl: './content-field.component.html' templateUrl: './content-field.component.html',
changeDetection: ChangeDetectionStrategy.OnPush
}) })
export class ContentFieldComponent implements OnChanges { export class ContentFieldComponent implements OnChanges {
@Input() @Input()
@ -43,6 +46,8 @@ export class ContentFieldComponent implements OnChanges {
public selectedFormControl: AbstractControl; public selectedFormControl: AbstractControl;
public isInvalid: Observable<boolean>;
public ngOnChanges(changes: SimpleChanges) { public ngOnChanges(changes: SimpleChanges) {
if (this.field.isLocalizable) { if (this.field.isLocalizable) {
this.selectedFormControl = this.fieldForm.controls[this.language.iso2Code]; this.selectedFormControl = this.fieldForm.controls[this.language.iso2Code];
@ -55,6 +60,10 @@ export class ContentFieldComponent implements OnChanges {
this.selectedFormControl['_clearChangeFns'](); this.selectedFormControl['_clearChangeFns']();
} }
} }
if (changes['fieldForm']) {
this.isInvalid = this.fieldForm.statusChanges.pipe(startWith(this.fieldForm.invalid), map(x => this.fieldForm.invalid));
}
} }
} }

5
src/Squidex/app/features/content/pages/content/content-history.component.html

@ -4,13 +4,14 @@
</ng-container> </ng-container>
<ng-container content> <ng-container content>
<div *ngFor="let event of events | async" class="event row no-gutters"> <div *ngFor="let event of events | async; trackBy: trackByEvent" class="event row no-gutters">
<div class="col col-auto"> <div class="col col-auto">
<img class="user-picture" [attr.title]="event.actor | sqxUserNameRef:null" [attr.src]="event.actor | sqxUserPictureRef" /> <img class="user-picture" [attr.title]="event.actor | sqxUserNameRef:null" [attr.src]="event.actor | sqxUserPictureRef" />
</div> </div>
<div class="col pl-2"> <div class="col pl-2">
<div class="event-message"> <div class="event-message">
<span class="event-actor user-ref">{{event.actor | sqxUserNameRef:null}}</span> <span [innerHTML]="format(event.message) | async"></span> <span class="event-actor user-ref mr-1">{{event.actor | sqxUserNameRef:null}}</span>
<span [innerHTML]="event | sqxHistoryMessage"></span>
</div> </div>
<div class="event-created">{{event.created | sqxFromNow}}</div> <div class="event-created">{{event.created | sqxFromNow}}</div>

9
src/Squidex/app/features/content/pages/content/content-history.component.ts

@ -13,12 +13,10 @@ import { delay, switchMap } from 'rxjs/operators';
import { import {
allParams, allParams,
AppsState, AppsState,
formatHistoryMessage,
HistoryChannelUpdated, HistoryChannelUpdated,
HistoryEventDto, HistoryEventDto,
HistoryService, HistoryService,
MessageBus, MessageBus,
UsersProviderService,
Version Version
} from '@app/shared'; } from '@app/shared';
@ -59,8 +57,7 @@ export class ContentHistoryComponent {
private readonly appsState: AppsState, private readonly appsState: AppsState,
private readonly historyService: HistoryService, private readonly historyService: HistoryService,
private readonly messageBus: MessageBus, private readonly messageBus: MessageBus,
private readonly route: ActivatedRoute, private readonly route: ActivatedRoute
private readonly users: UsersProviderService
) { ) {
} }
@ -68,7 +65,7 @@ export class ContentHistoryComponent {
this.messageBus.emit(new ContentVersionSelected(new Version(version.toString()))); this.messageBus.emit(new ContentVersionSelected(new Version(version.toString())));
} }
public format(message: string): Observable<string> { public trackByEvent(index: number, event: HistoryEventDto) {
return formatHistoryMessage(message, this.users); return event.eventId;
} }
} }

12
src/Squidex/app/features/content/pages/content/content-page.component.ts

@ -8,7 +8,7 @@
import { Component, OnDestroy, OnInit, ViewChild } from '@angular/core'; import { Component, OnDestroy, OnInit, ViewChild } from '@angular/core';
import { ActivatedRoute, Router } from '@angular/router'; import { ActivatedRoute, Router } from '@angular/router';
import { Observable, of, Subscription } from 'rxjs'; import { Observable, of, Subscription } from 'rxjs';
import { filter, map, onErrorResumeNext, switchMap } from 'rxjs/operators'; import { filter, onErrorResumeNext, switchMap } from 'rxjs/operators';
import { ContentVersionSelected } from './../messages'; import { ContentVersionSelected } from './../messages';
@ -88,19 +88,19 @@ export class ContentPageComponent implements CanComponentDeactivate, OnDestroy,
}); });
this.selectedSchemaSubscription = this.selectedSchemaSubscription =
this.schemasState.selectedSchema.pipe(filter(s => !!s), map(s => s!)) this.schemasState.selectedSchema.pipe(filter(s => !!s))
.subscribe(schema => { .subscribe(schema => {
this.schema = schema; this.schema = schema!;
this.contentForm = new EditContentForm(this.schema, this.languages); this.contentForm = new EditContentForm(this.schema, this.languages);
}); });
this.contentSubscription = this.contentSubscription =
this.contentsState.selectedContent.pipe(filter(c => !!c), map(c => c!)) this.contentsState.selectedContent.pipe(filter(c => !!c))
.subscribe(content => { .subscribe(content => {
this.content = content; this.content = content!;
this.loadContent(content.dataDraft); this.loadContent(this.content.dataDraft);
}); });
this.contentVersionSelectedSubscription = this.contentVersionSelectedSubscription =

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

@ -73,19 +73,19 @@
<div class="selection" *ngIf="selectionCount > 0"> <div class="selection" *ngIf="selectionCount > 0">
{{selectionCount}} items selected:&nbsp;&nbsp; {{selectionCount}} items selected:&nbsp;&nbsp;
<button class="btn btn-secondary" (click)="publishSelected()" *ngIf="canPublish"> <button class="btn btn-secondary mr-1" (click)="publishSelected()" *ngIf="canPublish">
Publish Publish
</button> </button>
<button class="btn btn-secondary" (click)="unpublishSelected()" *ngIf="canUnpublish"> <button class="btn btn-secondary mr-1" (click)="unpublishSelected()" *ngIf="canUnpublish">
Unpublish Unpublish
</button> </button>
<button class="btn btn-secondary" (click)="archiveSelected()" *ngIf="(contentsState.isArchive | async) === false"> <button class="btn btn-secondary mr-1" (click)="archiveSelected()" *ngIf="(contentsState.isArchive | async) === false">
Archive Archive
</button> </button>
<button class="btn btn-secondary" (click)="restoreSelected()" *ngIf="contentsState.isArchive | async"> <button class="btn btn-secondary mr-1" (click)="restoreSelected()" *ngIf="contentsState.isArchive | async">
Restore Restore
</button> </button>

49
src/Squidex/app/features/content/pages/contents/contents-page.component.ts

@ -6,8 +6,9 @@
*/ */
import { Component, OnDestroy, OnInit, ViewChild } from '@angular/core'; import { Component, OnDestroy, OnInit, ViewChild } from '@angular/core';
import { Router } from '@angular/router';
import { Subscription } from 'rxjs'; import { Subscription } from 'rxjs';
import { onErrorResumeNext, switchMap, tap } from 'rxjs/operators'; import { filter, onErrorResumeNext, switchMap, takeUntil, tap } from 'rxjs/operators';
import { import {
AppLanguageDto, AppLanguageDto,
@ -17,6 +18,7 @@ import {
ImmutableArray, ImmutableArray,
LanguagesState, LanguagesState,
ModalModel, ModalModel,
navigatedToOtherComponent,
Queries, Queries,
SchemaDetailsDto, SchemaDetailsDto,
SchemasState, SchemasState,
@ -59,6 +61,7 @@ export class ContentsPageComponent implements OnDestroy, OnInit {
public readonly contentsState: ContentsState, public readonly contentsState: ContentsState,
private readonly languagesState: LanguagesState, private readonly languagesState: LanguagesState,
private readonly schemasState: SchemasState, private readonly schemasState: SchemasState,
private readonly router: Router,
private readonly uiState: UIState private readonly uiState: UIState
) { ) {
} }
@ -70,8 +73,10 @@ export class ContentsPageComponent implements OnDestroy, OnInit {
} }
public ngOnInit() { public ngOnInit() {
const routeChanged = this.router.events.pipe(filter(navigatedToOtherComponent(this.router)));
this.selectedSchemaSubscription = this.selectedSchemaSubscription =
this.schemasState.selectedSchema this.schemasState.selectedSchema.pipe(takeUntil(routeChanged))
.subscribe(schema => { .subscribe(schema => {
this.resetSelection(); this.resetSelection();
@ -82,7 +87,7 @@ export class ContentsPageComponent implements OnDestroy, OnInit {
}); });
this.contentsSubscription = this.contentsSubscription =
this.contentsState.contents this.contentsState.contents.pipe(takeUntil(routeChanged))
.subscribe(() => { .subscribe(() => {
this.updateSelectionSummary(); this.updateSelectionSummary();
}); });
@ -100,7 +105,7 @@ export class ContentsPageComponent implements OnDestroy, OnInit {
} }
public deleteSelected() { public deleteSelected() {
this.contentsState.deleteMany(this.select()).pipe(onErrorResumeNext()).subscribe(); this.contentsState.deleteMany(this.selectItems()).pipe(onErrorResumeNext()).subscribe();
} }
public delete(content: ContentDto) { public delete(content: ContentDto) {
@ -112,7 +117,7 @@ export class ContentsPageComponent implements OnDestroy, OnInit {
} }
public publishSelected() { public publishSelected() {
this.changeContentItems(this.select(c => c.status !== 'Published'), 'Publish'); this.changeContentItems(this.selectItems(c => c.status !== 'Published'), 'Publish');
} }
public unpublish(content: ContentDto) { public unpublish(content: ContentDto) {
@ -120,7 +125,7 @@ export class ContentsPageComponent implements OnDestroy, OnInit {
} }
public unpublishSelected() { public unpublishSelected() {
this.changeContentItems(this.select(c => c.status === 'Published'), 'Unpublish'); this.changeContentItems(this.selectItems(c => c.status === 'Published'), 'Unpublish');
} }
public archive(content: ContentDto) { public archive(content: ContentDto) {
@ -128,15 +133,15 @@ export class ContentsPageComponent implements OnDestroy, OnInit {
} }
public archiveSelected() { public archiveSelected() {
this.changeContentItems(this.select(), 'Archive'); this.changeContentItems(this.selectItems(), 'Archive');
} }
public restore(content: ContentDto) { public restore(content: ContentDto) {
this.changeContentItems([content], 'Restore'); this.changeContentItems([content], 'Restore');
} }
public restoreSelected(scheduled: boolean) { public restoreSelected() {
this.changeContentItems(this.select(), 'Restore'); this.changeContentItems(this.selectItems(), 'Restore');
} }
public clone(content: ContentDto) { public clone(content: ContentDto) {
@ -144,7 +149,7 @@ export class ContentsPageComponent implements OnDestroy, OnInit {
} }
public isSelectedQuery(query: string) { public isSelectedQuery(query: string) {
return query === this.contentsState.snapshot.contentsQuery || (!query && !this.contentsState.contentsQuery); return query === this.contentsState.snapshot.contentsQuery || (!query && !this.contentsState.snapshot.contentsQuery);
} }
private changeContentItems(contents: ContentDto[], action: string) { private changeContentItems(contents: ContentDto[], action: string) {
@ -185,12 +190,16 @@ export class ContentsPageComponent implements OnDestroy, OnInit {
this.contentsState.search(query).pipe(onErrorResumeNext()).subscribe(); this.contentsState.search(query).pipe(onErrorResumeNext()).subscribe();
} }
public selectLanguage(language: AppLanguageDto) {
this.language = language;
}
public isItemSelected(content: ContentDto): boolean { public isItemSelected(content: ContentDto): boolean {
return !!this.selectedItems[content.id]; return !!this.selectedItems[content.id];
} }
public selectLanguage(language: AppLanguageDto) { private selectItems(predicate?: (content: ContentDto) => boolean) {
this.language = language; return this.contentsState.snapshot.contents.values.filter(c => this.selectedItems[c.id] && (!predicate || predicate(c)));
} }
public selectItem(content: ContentDto, isSelected: boolean) { public selectItem(content: ContentDto, isSelected: boolean) {
@ -199,6 +208,12 @@ export class ContentsPageComponent implements OnDestroy, OnInit {
this.updateSelectionSummary(); this.updateSelectionSummary();
} }
private resetSelection() {
this.selectedItems = {};
this.updateSelectionSummary();
}
public selectAll(isSelected: boolean) { public selectAll(isSelected: boolean) {
this.selectedItems = {}; this.selectedItems = {};
@ -215,16 +230,6 @@ export class ContentsPageComponent implements OnDestroy, OnInit {
return content.id; return content.id;
} }
private select(predicate?: (content: ContentDto) => boolean) {
return this.contentsState.snapshot.contents.values.filter(c => this.selectedItems[c.id] && (!predicate || predicate(c)));
}
private resetSelection() {
this.selectedItems = {};
this.updateSelectionSummary();
}
private updateSelectionSummary() { private updateSelectionSummary() {
this.isAllSelected = this.contentsState.snapshot.contents.length > 0; this.isAllSelected = this.contentsState.snapshot.contents.length > 0;

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

@ -21,6 +21,7 @@
[name]="category" [name]="category"
[schemas]="schemas" [schemas]="schemas"
[schemasFilter]="schemasFilter.valueChanges | async" [schemasFilter]="schemasFilter.valueChanges | async"
[routeSingletonToContent]="true"
[isReadonly]="true"> [isReadonly]="true">
</sqx-schema-category> </sqx-schema-category>
</ng-container> </ng-container>

10
src/Squidex/app/features/content/shared/assets-editor.component.ts

@ -7,7 +7,7 @@
// tslint:disable:prefer-for-of // tslint:disable:prefer-for-of
import { Component, forwardRef } from '@angular/core'; import { ChangeDetectionStrategy, ChangeDetectorRef, Component, forwardRef } from '@angular/core';
import { ControlValueAccessor, NG_VALUE_ACCESSOR } from '@angular/forms'; import { ControlValueAccessor, NG_VALUE_ACCESSOR } from '@angular/forms';
import { import {
@ -28,9 +28,8 @@ export const SQX_ASSETS_EDITOR_CONTROL_VALUE_ACCESSOR: any = {
selector: 'sqx-assets-editor', selector: 'sqx-assets-editor',
styleUrls: ['./assets-editor.component.scss'], styleUrls: ['./assets-editor.component.scss'],
templateUrl: './assets-editor.component.html', templateUrl: './assets-editor.component.html',
providers: [ providers: [SQX_ASSETS_EDITOR_CONTROL_VALUE_ACCESSOR],
SQX_ASSETS_EDITOR_CONTROL_VALUE_ACCESSOR changeDetection: ChangeDetectionStrategy.OnPush
]
}) })
export class AssetsEditorComponent implements ControlValueAccessor { export class AssetsEditorComponent implements ControlValueAccessor {
private callChange = (v: any) => { /* NOOP */ }; private callChange = (v: any) => { /* NOOP */ };
@ -47,6 +46,7 @@ export class AssetsEditorComponent implements ControlValueAccessor {
constructor( constructor(
private readonly appsState: AppsState, private readonly appsState: AppsState,
private readonly assetsService: AssetsService, private readonly assetsService: AssetsService,
private readonly changeDetector: ChangeDetectorRef,
private readonly localStore: LocalStoreService private readonly localStore: LocalStoreService
) { ) {
this.isListView = this.localStore.get('assetView') === 'List'; this.isListView = this.localStore.get('assetView') === 'List';
@ -131,6 +131,8 @@ export class AssetsEditorComponent implements ControlValueAccessor {
this.callTouched(); this.callTouched();
this.callChange(ids); this.callChange(ids);
this.changeDetector.detectChanges();
} }
public sort(assets: AssetDto[]) { public sort(assets: AssetDto[]) {

5
src/Squidex/app/features/content/shared/content-item.component.ts

@ -5,7 +5,7 @@
* Copyright (c) Squidex UG (haftungsbeschränkt). All rights reserved. * Copyright (c) Squidex UG (haftungsbeschränkt). All rights reserved.
*/ */
import { Component, EventEmitter, Input, OnChanges, Output, SimpleChanges } from '@angular/core'; import { ChangeDetectionStrategy, Component, EventEmitter, Input, OnChanges, Output, SimpleChanges } from '@angular/core';
import { import {
AppLanguageDto, AppLanguageDto,
@ -29,7 +29,8 @@ import {
templateUrl: './content-item.component.html', templateUrl: './content-item.component.html',
animations: [ animations: [
fadeAnimation fadeAnimation
] ],
changeDetection: ChangeDetectionStrategy.OnPush
}) })
export class ContentItemComponent implements OnChanges { export class ContentItemComponent implements OnChanges {
@Output() @Output()

8
src/Squidex/app/features/content/shared/contents-selector.component.html

@ -16,10 +16,10 @@
(queryChanged)="search($event)" (queryChanged)="search($event)"
expandable="true"> expandable="true">
</sqx-search-form> </sqx-search-form>
</div>
<ng-container *ngIf="languages.length > 1">
<sqx-language-selector class="languages-buttons" (selectedLanguageChange)="selectLanguage($event)" [languages]="languages.values"></sqx-language-selector> <div class="col pl-1" *ngIf="languages.length > 1">
</ng-container> <sqx-language-selector class="languages-buttons" (selectedLanguageChange)="selectLanguage($event)" [languages]="languages.values"></sqx-language-selector>
</div> </div>
</div> </div>
</ng-container> </ng-container>

20
src/Squidex/app/features/content/shared/references-editor.component.ts

@ -7,7 +7,7 @@
// tslint:disable:prefer-for-of // tslint:disable:prefer-for-of
import { Component, forwardRef, Input, OnInit } from '@angular/core'; import { ChangeDetectionStrategy, ChangeDetectorRef, Component, forwardRef, Input, OnInit } from '@angular/core';
import { ControlValueAccessor, NG_VALUE_ACCESSOR } from '@angular/forms'; import { ControlValueAccessor, NG_VALUE_ACCESSOR } from '@angular/forms';
import { import {
@ -31,9 +31,8 @@ export const SQX_REFERENCES_EDITOR_CONTROL_VALUE_ACCESSOR: any = {
selector: 'sqx-references-editor', selector: 'sqx-references-editor',
styleUrls: ['./references-editor.component.scss'], styleUrls: ['./references-editor.component.scss'],
templateUrl: './references-editor.component.html', templateUrl: './references-editor.component.html',
providers: [ providers: [SQX_REFERENCES_EDITOR_CONTROL_VALUE_ACCESSOR],
SQX_REFERENCES_EDITOR_CONTROL_VALUE_ACCESSOR changeDetection: ChangeDetectionStrategy.OnPush
]
}) })
export class ReferencesEditorComponent implements ControlValueAccessor, OnInit { export class ReferencesEditorComponent implements ControlValueAccessor, OnInit {
private callChange = (v: any) => { /* NOOP */ }; private callChange = (v: any) => { /* NOOP */ };
@ -59,6 +58,7 @@ export class ReferencesEditorComponent implements ControlValueAccessor, OnInit {
constructor( constructor(
private readonly appsState: AppsState, private readonly appsState: AppsState,
private readonly changeDetector: ChangeDetectorRef,
private readonly contentsService: ContentsService, private readonly contentsService: ContentsService,
private readonly schemasService: SchemasService private readonly schemasService: SchemasService
) { ) {
@ -73,8 +73,12 @@ export class ReferencesEditorComponent implements ControlValueAccessor, OnInit {
this.schemasService.getSchema(this.appsState.appName, this.schemaId) this.schemasService.getSchema(this.appsState.appName, this.schemaId)
.subscribe(dto => { .subscribe(dto => {
this.schema = dto; this.schema = dto;
}, error => {
this.changeDetector.detectChanges();
}, () => {
this.isInvalidSchema = true; this.isInvalidSchema = true;
this.changeDetector.detectChanges();
}); });
} }
@ -90,8 +94,12 @@ export class ReferencesEditorComponent implements ControlValueAccessor, OnInit {
if (this.contentItems.length !== contentIds.length) { if (this.contentItems.length !== contentIds.length) {
this.updateValue(); this.updateValue();
} }
this.changeDetector.detectChanges();
}, () => { }, () => {
this.contentItems = ImmutableArray.empty(); this.contentItems = ImmutableArray.empty();
this.changeDetector.detectChanges();
}); });
} }
} else { } else {
@ -148,5 +156,7 @@ export class ReferencesEditorComponent implements ControlValueAccessor, OnInit {
this.callTouched(); this.callTouched();
this.callChange(ids); this.callChange(ids);
this.changeDetector.detectChanges();
} }
} }

2
src/Squidex/app/features/rules/pages/rules/triggers/content-changed-trigger.component.ts

@ -82,7 +82,7 @@ export class ContentChangedTriggerComponent implements OnInit {
} else { } else {
return null; return null;
} }
}).filter(s => s !== null).map(s => s!)).sortByStringAsc(s => s.schema.name); }).filter(s => !!s).map(s => s!)).sortByStringAsc(s => s.schema.name);
this.schemasToAdd = this.schemasToAdd =
this.schemas.filter(schema => this.schemas.filter(schema =>

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

@ -10,7 +10,7 @@
import { Component, OnDestroy, OnInit } from '@angular/core'; import { Component, OnDestroy, OnInit } from '@angular/core';
import { ActivatedRoute, Router } from '@angular/router'; import { ActivatedRoute, Router } from '@angular/router';
import { Subscription } from 'rxjs'; import { Subscription } from 'rxjs';
import { filter, map, onErrorResumeNext } from 'rxjs/operators'; import { filter, onErrorResumeNext } from 'rxjs/operators';
import { import {
AppsState, AppsState,
@ -73,9 +73,9 @@ export class SchemaPageComponent implements OnDestroy, OnInit {
this.patternsState.load().pipe(onErrorResumeNext()).subscribe(); this.patternsState.load().pipe(onErrorResumeNext()).subscribe();
this.selectedSchemaSubscription = this.selectedSchemaSubscription =
this.schemasState.selectedSchema.pipe(filter(s => !!s), map(s => s!)) this.schemasState.selectedSchema.pipe(filter(s => !!s))
.subscribe(schema => { .subscribe(schema => {
this.schema = schema; this.schema = schema!;
this.export(); this.export();
}); });

5
src/Squidex/app/framework/angular/forms/autocomplete.component.ts

@ -5,7 +5,7 @@
* Copyright (c) Squidex UG (haftungsbeschränkt). All rights reserved. * Copyright (c) Squidex UG (haftungsbeschränkt). All rights reserved.
*/ */
import { Component, ContentChild, forwardRef, Input, OnDestroy, OnInit, TemplateRef } from '@angular/core'; import { ChangeDetectionStrategy, Component, ContentChild, forwardRef, Input, OnDestroy, OnInit, TemplateRef } from '@angular/core';
import { ControlValueAccessor, FormControl, NG_VALUE_ACCESSOR } from '@angular/forms'; import { ControlValueAccessor, FormControl, NG_VALUE_ACCESSOR } from '@angular/forms';
import { Observable, of, Subscription } from 'rxjs'; import { Observable, of, Subscription } from 'rxjs';
import { catchError, debounceTime, distinctUntilChanged, filter, map, switchMap, tap } from 'rxjs/operators'; import { catchError, debounceTime, distinctUntilChanged, filter, map, switchMap, tap } from 'rxjs/operators';
@ -27,7 +27,8 @@ export const SQX_AUTOCOMPLETE_CONTROL_VALUE_ACCESSOR: any = {
selector: 'sqx-autocomplete', selector: 'sqx-autocomplete',
styleUrls: ['./autocomplete.component.scss'], styleUrls: ['./autocomplete.component.scss'],
templateUrl: './autocomplete.component.html', templateUrl: './autocomplete.component.html',
providers: [SQX_AUTOCOMPLETE_CONTROL_VALUE_ACCESSOR] providers: [SQX_AUTOCOMPLETE_CONTROL_VALUE_ACCESSOR],
changeDetection: ChangeDetectionStrategy.OnPush
}) })
export class AutocompleteComponent implements ControlValueAccessor, OnDestroy, OnInit { export class AutocompleteComponent implements ControlValueAccessor, OnDestroy, OnInit {
private subscription: Subscription; private subscription: Subscription;

16
src/Squidex/app/framework/angular/forms/date-time-editor.component.ts

@ -5,7 +5,7 @@
* Copyright (c) Squidex UG (haftungsbeschränkt). All rights reserved. * Copyright (c) Squidex UG (haftungsbeschränkt). All rights reserved.
*/ */
import { AfterViewInit, Component, ElementRef, forwardRef, Input, OnDestroy, OnInit, ViewChild } from '@angular/core'; import { AfterViewInit, ChangeDetectionStrategy, ChangeDetectorRef, Component, ElementRef, forwardRef, Input, OnDestroy, OnInit, ViewChild } from '@angular/core';
import { ControlValueAccessor, FormControl, NG_VALUE_ACCESSOR } from '@angular/forms'; import { ControlValueAccessor, FormControl, NG_VALUE_ACCESSOR } from '@angular/forms';
import * as moment from 'moment'; import * as moment from 'moment';
import { Subscription } from 'rxjs'; import { Subscription } from 'rxjs';
@ -22,7 +22,8 @@ export const SQX_DATE_TIME_EDITOR_CONTROL_VALUE_ACCESSOR: any = {
selector: 'sqx-date-time-editor', selector: 'sqx-date-time-editor',
styleUrls: ['./date-time-editor.component.scss'], styleUrls: ['./date-time-editor.component.scss'],
templateUrl: './date-time-editor.component.html', templateUrl: './date-time-editor.component.html',
providers: [SQX_DATE_TIME_EDITOR_CONTROL_VALUE_ACCESSOR] providers: [SQX_DATE_TIME_EDITOR_CONTROL_VALUE_ACCESSOR],
changeDetection: ChangeDetectionStrategy.OnPush
}) })
export class DateTimeEditorComponent implements ControlValueAccessor, OnDestroy, OnInit, AfterViewInit { export class DateTimeEditorComponent implements ControlValueAccessor, OnDestroy, OnInit, AfterViewInit {
private timeSubscription: Subscription; private timeSubscription: Subscription;
@ -52,7 +53,7 @@ export class DateTimeEditorComponent implements ControlValueAccessor, OnDestroy,
} }
public get hasValue() { public get hasValue() {
return this.dateValue !== null; return !!this.dateValue;
} }
@ViewChild('dateInput') @ViewChild('dateInput')
@ -60,6 +61,11 @@ export class DateTimeEditorComponent implements ControlValueAccessor, OnDestroy,
public isDisabled = false; public isDisabled = false;
constructor(
private readonly changeDetector: ChangeDetectorRef
) {
}
public ngOnDestroy() { public ngOnDestroy() {
this.dateSubscription.unsubscribe(); this.dateSubscription.unsubscribe();
this.timeSubscription.unsubscribe(); this.timeSubscription.unsubscribe();
@ -136,6 +142,10 @@ export class DateTimeEditorComponent implements ControlValueAccessor, OnDestroy,
this.updateValue(); this.updateValue();
this.touched(); this.touched();
if (false) {
this.changeDetector.detectChanges();
}
} }
}); });

5
src/Squidex/app/framework/angular/forms/dropdown.component.ts

@ -5,7 +5,7 @@
* Copyright (c) Squidex UG (haftungsbeschränkt). All rights reserved. * Copyright (c) Squidex UG (haftungsbeschränkt). All rights reserved.
*/ */
import { AfterContentInit, Component, ContentChildren, forwardRef, Input, QueryList, TemplateRef } from '@angular/core'; import { AfterContentInit, ChangeDetectionStrategy, Component, ContentChildren, forwardRef, Input, QueryList, TemplateRef } from '@angular/core';
import { ControlValueAccessor, NG_VALUE_ACCESSOR } from '@angular/forms'; import { ControlValueAccessor, NG_VALUE_ACCESSOR } from '@angular/forms';
const KEY_ENTER = 13; const KEY_ENTER = 13;
@ -23,7 +23,8 @@ export const SQX_DROPDOWN_CONTROL_VALUE_ACCESSOR: any = {
selector: 'sqx-dropdown', selector: 'sqx-dropdown',
styleUrls: ['./dropdown.component.scss'], styleUrls: ['./dropdown.component.scss'],
templateUrl: './dropdown.component.html', templateUrl: './dropdown.component.html',
providers: [SQX_DROPDOWN_CONTROL_VALUE_ACCESSOR] providers: [SQX_DROPDOWN_CONTROL_VALUE_ACCESSOR],
changeDetection: ChangeDetectionStrategy.OnPush
}) })
export class DropdownComponent implements AfterContentInit, ControlValueAccessor { export class DropdownComponent implements AfterContentInit, ControlValueAccessor {
private callChange = (v: any) => { /* NOOP */ }; private callChange = (v: any) => { /* NOOP */ };

2
src/Squidex/app/framework/angular/forms/iframe-editor.component.html

@ -1 +1 @@
<iframe #iframe scrolling="no" [attr.src]="sanitizedUrl()" width="100%"></iframe> <iframe #iframe scrolling="no" [attr.src]="sanitizedUrl" width="100%"></iframe>

16
src/Squidex/app/framework/angular/forms/iframe-editor.component.ts

@ -7,7 +7,7 @@
import { AfterViewInit, ChangeDetectionStrategy, Component, ElementRef, forwardRef, Input, OnDestroy, OnInit, Renderer2, ViewChild } from '@angular/core'; import { AfterViewInit, ChangeDetectionStrategy, Component, ElementRef, forwardRef, Input, OnDestroy, OnInit, Renderer2, ViewChild } from '@angular/core';
import { ControlValueAccessor, NG_VALUE_ACCESSOR } from '@angular/forms'; import { ControlValueAccessor, NG_VALUE_ACCESSOR } from '@angular/forms';
import { DomSanitizer } from '@angular/platform-browser'; import { DomSanitizer, SafeResourceUrl } from '@angular/platform-browser';
import { Types } from '@app/framework/internal'; import { Types } from '@app/framework/internal';
@ -19,8 +19,8 @@ export const SQX_IFRAME_EDITOR_CONTROL_VALUE_ACCESSOR: any = {
selector: 'sqx-iframe-editor', selector: 'sqx-iframe-editor',
styleUrls: ['./iframe-editor.component.scss'], styleUrls: ['./iframe-editor.component.scss'],
templateUrl: './iframe-editor.component.html', templateUrl: './iframe-editor.component.html',
changeDetection: ChangeDetectionStrategy.OnPush, providers: [SQX_IFRAME_EDITOR_CONTROL_VALUE_ACCESSOR],
providers: [SQX_IFRAME_EDITOR_CONTROL_VALUE_ACCESSOR] changeDetection: ChangeDetectionStrategy.OnPush
}) })
export class IFrameEditorComponent implements ControlValueAccessor, AfterViewInit, OnInit, OnDestroy { export class IFrameEditorComponent implements ControlValueAccessor, AfterViewInit, OnInit, OnDestroy {
private windowMessageListener: Function; private windowMessageListener: Function;
@ -35,7 +35,11 @@ export class IFrameEditorComponent implements ControlValueAccessor, AfterViewIni
public iframe: ElementRef; public iframe: ElementRef;
@Input() @Input()
public url: string; public set url(value: string) {
this.sanitizedUrl = this.sanitizer.bypassSecurityTrustResourceUrl(value);
}
public sanitizedUrl: SafeResourceUrl;
constructor( constructor(
private readonly sanitizer: DomSanitizer, private readonly sanitizer: DomSanitizer,
@ -83,10 +87,6 @@ export class IFrameEditorComponent implements ControlValueAccessor, AfterViewIni
}); });
} }
public sanitizedUrl() {
return this.sanitizer.bypassSecurityTrustResourceUrl(this.url);
}
public writeValue(obj: any) { public writeValue(obj: any) {
this.value = obj; this.value = obj;

5
src/Squidex/app/framework/angular/forms/jscript-editor.component.ts

@ -5,7 +5,7 @@
* Copyright (c) Squidex UG (haftungsbeschränkt). All rights reserved. * Copyright (c) Squidex UG (haftungsbeschränkt). All rights reserved.
*/ */
import { AfterViewInit, Component, ElementRef, forwardRef, ViewChild } from '@angular/core'; import { AfterViewInit, ChangeDetectionStrategy, Component, ElementRef, forwardRef, ViewChild } from '@angular/core';
import { ControlValueAccessor, NG_VALUE_ACCESSOR } from '@angular/forms'; import { ControlValueAccessor, NG_VALUE_ACCESSOR } from '@angular/forms';
import { Subject } from 'rxjs'; import { Subject } from 'rxjs';
import { debounceTime } from 'rxjs/operators'; import { debounceTime } from 'rxjs/operators';
@ -22,7 +22,8 @@ export const SQX_JSCRIPT_EDITOR_CONTROL_VALUE_ACCESSOR: any = {
selector: 'sqx-jscript-editor', selector: 'sqx-jscript-editor',
styleUrls: ['./jscript-editor.component.scss'], styleUrls: ['./jscript-editor.component.scss'],
templateUrl: './jscript-editor.component.html', templateUrl: './jscript-editor.component.html',
providers: [SQX_JSCRIPT_EDITOR_CONTROL_VALUE_ACCESSOR] providers: [SQX_JSCRIPT_EDITOR_CONTROL_VALUE_ACCESSOR],
changeDetection: ChangeDetectionStrategy.OnPush
}) })
export class JscriptEditorComponent implements ControlValueAccessor, AfterViewInit { export class JscriptEditorComponent implements ControlValueAccessor, AfterViewInit {
private callChange = (v: any) => { /* NOOP */ }; private callChange = (v: any) => { /* NOOP */ };

5
src/Squidex/app/framework/angular/forms/json-editor.component.ts

@ -5,7 +5,7 @@
* Copyright (c) Squidex UG (haftungsbeschränkt). All rights reserved. * Copyright (c) Squidex UG (haftungsbeschränkt). All rights reserved.
*/ */
import { AfterViewInit, Component, ElementRef, forwardRef, ViewChild } from '@angular/core'; import { AfterViewInit, ChangeDetectionStrategy, Component, ElementRef, forwardRef, ViewChild } from '@angular/core';
import { ControlValueAccessor, NG_VALUE_ACCESSOR } from '@angular/forms'; import { ControlValueAccessor, NG_VALUE_ACCESSOR } from '@angular/forms';
import { Subject } from 'rxjs'; import { Subject } from 'rxjs';
import { debounceTime } from 'rxjs/operators'; import { debounceTime } from 'rxjs/operators';
@ -22,7 +22,8 @@ export const SQX_JSON_EDITOR_CONTROL_VALUE_ACCESSOR: any = {
selector: 'sqx-json-editor', selector: 'sqx-json-editor',
styleUrls: ['./json-editor.component.scss'], styleUrls: ['./json-editor.component.scss'],
templateUrl: './json-editor.component.html', templateUrl: './json-editor.component.html',
providers: [SQX_JSON_EDITOR_CONTROL_VALUE_ACCESSOR] providers: [SQX_JSON_EDITOR_CONTROL_VALUE_ACCESSOR],
changeDetection: ChangeDetectionStrategy.OnPush
}) })
export class JsonEditorComponent implements ControlValueAccessor, AfterViewInit { export class JsonEditorComponent implements ControlValueAccessor, AfterViewInit {
private callChange = (v: any) => { /* NOOP */ }; private callChange = (v: any) => { /* NOOP */ };

13
src/Squidex/app/framework/angular/forms/progress-bar.component.ts

@ -5,13 +5,14 @@
* Copyright (c) Squidex UG (haftungsbeschränkt). All rights reserved. * Copyright (c) Squidex UG (haftungsbeschränkt). All rights reserved.
*/ */
import { Component, ElementRef, Input, OnChanges, OnInit, Renderer2, SimpleChanges } from '@angular/core'; import { ChangeDetectionStrategy, Component, ElementRef, Input, OnChanges, OnInit, Renderer2, SimpleChanges } from '@angular/core';
const ProgressBar = require('progressbar.js'); const ProgressBar = require('progressbar.js');
@Component({ @Component({
selector: 'sqx-progress-bar', selector: 'sqx-progress-bar',
template: '' template: '',
changeDetection: ChangeDetectionStrategy.OnPush
}) })
export class ProgressBarComponent implements OnChanges, OnInit { export class ProgressBarComponent implements OnChanges, OnInit {
private progressBar: any; private progressBar: any;
@ -31,6 +32,9 @@ export class ProgressBarComponent implements OnChanges, OnInit {
@Input() @Input()
public strokeWidth = 4; public strokeWidth = 4;
@Input()
public showText = true;
@Input() @Input()
public value = 0; public value = 0;
@ -45,7 +49,8 @@ export class ProgressBarComponent implements OnChanges, OnInit {
color: this.color, color: this.color,
trailColor: this.trailColor, trailColor: this.trailColor,
trailWidth: this.trailWidth, trailWidth: this.trailWidth,
strokeWidth: this.strokeWidth strokeWidth: this.strokeWidth,
svgStyle: { width: '100%', height: '100%' }
}; };
this.renderer.setStyle(this.element.nativeElement, 'display', 'block'); this.renderer.setStyle(this.element.nativeElement, 'display', 'block');
@ -70,7 +75,7 @@ export class ProgressBarComponent implements OnChanges, OnInit {
this.progressBar.animate(value / 100); this.progressBar.animate(value / 100);
if (value > 0) { if (value > 0 && this.showText) {
this.progressBar.setText(Math.round(value) + '%'); this.progressBar.setText(Math.round(value) + '%');
} }
} }

5
src/Squidex/app/framework/angular/forms/slider.component.ts

@ -5,7 +5,7 @@
* Copyright (c) Squidex UG (haftungsbeschränkt). All rights reserved. * Copyright (c) Squidex UG (haftungsbeschränkt). All rights reserved.
*/ */
import { Component, ElementRef, forwardRef, Input, Renderer2, ViewChild } from '@angular/core'; import { ChangeDetectionStrategy, Component, ElementRef, forwardRef, Input, Renderer2, ViewChild } from '@angular/core';
import { ControlValueAccessor, NG_VALUE_ACCESSOR } from '@angular/forms'; import { ControlValueAccessor, NG_VALUE_ACCESSOR } from '@angular/forms';
import { Types } from '@app/framework/internal'; import { Types } from '@app/framework/internal';
@ -18,7 +18,8 @@ export const SQX_SLIDER_CONTROL_VALUE_ACCESSOR: any = {
selector: 'sqx-slider', selector: 'sqx-slider',
styleUrls: ['./slider.component.scss'], styleUrls: ['./slider.component.scss'],
templateUrl: './slider.component.html', templateUrl: './slider.component.html',
providers: [SQX_SLIDER_CONTROL_VALUE_ACCESSOR] providers: [SQX_SLIDER_CONTROL_VALUE_ACCESSOR],
changeDetection: ChangeDetectionStrategy.OnPush
}) })
export class SliderComponent implements ControlValueAccessor { export class SliderComponent implements ControlValueAccessor {
private callChange = (v: any) => { /* NOOP */ }; private callChange = (v: any) => { /* NOOP */ };

7
src/Squidex/app/framework/angular/forms/stars.component.ts

@ -5,7 +5,7 @@
* Copyright (c) Squidex UG (haftungsbeschränkt). All rights reserved. * Copyright (c) Squidex UG (haftungsbeschränkt). All rights reserved.
*/ */
import { Component, forwardRef, Input } from '@angular/core'; import { ChangeDetectionStrategy, Component, forwardRef, Input } from '@angular/core';
import { ControlValueAccessor, NG_VALUE_ACCESSOR } from '@angular/forms'; import { ControlValueAccessor, NG_VALUE_ACCESSOR } from '@angular/forms';
import { Types } from '@app/framework/internal'; import { Types } from '@app/framework/internal';
@ -18,7 +18,8 @@ export const SQX_STARS_CONTROL_VALUE_ACCESSOR: any = {
selector: 'sqx-stars', selector: 'sqx-stars',
styleUrls: ['./stars.component.scss'], styleUrls: ['./stars.component.scss'],
templateUrl: './stars.component.html', templateUrl: './stars.component.html',
providers: [SQX_STARS_CONTROL_VALUE_ACCESSOR] providers: [SQX_STARS_CONTROL_VALUE_ACCESSOR],
changeDetection: ChangeDetectionStrategy.OnPush
}) })
export class StarsComponent implements ControlValueAccessor { export class StarsComponent implements ControlValueAccessor {
private callChange = (v: any) => { /* NOOP */ }; private callChange = (v: any) => { /* NOOP */ };
@ -88,7 +89,7 @@ export class StarsComponent implements ControlValueAccessor {
return false; return false;
} }
if (this.value !== null) { if (this.value) {
this.value = null; this.value = null;
this.stars = 0; this.stars = 0;

1
src/Squidex/app/framework/angular/forms/tag-editor.component.html

@ -14,7 +14,6 @@
[formControl]="addInput" [formControl]="addInput"
[attr.name]="inputName" [attr.name]="inputName"
[attr.placeholder]="placeholder" [attr.placeholder]="placeholder"
[disabled]="addInput.disabled"
autocomplete="off" autocomplete="off"
autocorrect="off" autocorrect="off"
autocapitalize="off" autocapitalize="off"

44
src/Squidex/app/framework/angular/forms/tag-editor.component.ts

@ -5,7 +5,7 @@
* Copyright (c) Squidex UG (haftungsbeschränkt). All rights reserved. * Copyright (c) Squidex UG (haftungsbeschränkt). All rights reserved.
*/ */
import { AfterViewInit, Component, ElementRef, forwardRef, Input, OnDestroy, OnInit, ViewChild } from '@angular/core'; import { AfterViewInit, ChangeDetectionStrategy, Component, ElementRef, forwardRef, Input, OnDestroy, OnInit, ViewChild } from '@angular/core';
import { ControlValueAccessor, FormControl, NG_VALUE_ACCESSOR } from '@angular/forms'; import { ControlValueAccessor, FormControl, NG_VALUE_ACCESSOR } from '@angular/forms';
import { Subscription } from 'rxjs'; import { Subscription } from 'rxjs';
import { distinctUntilChanged, map, tap } from 'rxjs/operators'; import { distinctUntilChanged, map, tap } from 'rxjs/operators';
@ -71,11 +71,16 @@ export const SQX_TAG_EDITOR_CONTROL_VALUE_ACCESSOR: any = {
provide: NG_VALUE_ACCESSOR, useExisting: forwardRef(() => TagEditorComponent), multi: true provide: NG_VALUE_ACCESSOR, useExisting: forwardRef(() => TagEditorComponent), multi: true
}; };
const CACHED_SIZES: { [key: string]: number } = {};
let CACHED_FONT: string;
@Component({ @Component({
selector: 'sqx-tag-editor', selector: 'sqx-tag-editor',
styleUrls: ['./tag-editor.component.scss'], styleUrls: ['./tag-editor.component.scss'],
templateUrl: './tag-editor.component.html', templateUrl: './tag-editor.component.html',
providers: [SQX_TAG_EDITOR_CONTROL_VALUE_ACCESSOR] providers: [SQX_TAG_EDITOR_CONTROL_VALUE_ACCESSOR],
changeDetection: ChangeDetectionStrategy.OnPush
}) })
export class TagEditorComponent implements AfterViewInit, ControlValueAccessor, OnDestroy, OnInit { export class TagEditorComponent implements AfterViewInit, ControlValueAccessor, OnDestroy, OnInit {
private subscription: Subscription; private subscription: Subscription;
@ -129,6 +134,12 @@ export class TagEditorComponent implements AfterViewInit, ControlValueAccessor,
} }
public ngAfterViewInit() { public ngAfterViewInit() {
if (!CACHED_FONT) {
const style = window.getComputedStyle(this.inputElement.nativeElement);
CACHED_FONT = `${style.getPropertyValue('font-size')} ${style.getPropertyValue('font-family')}`;
}
this.resetSize(); this.resetSize();
} }
@ -204,7 +215,9 @@ export class TagEditorComponent implements AfterViewInit, ControlValueAccessor,
} }
public resetSize() { public resetSize() {
const style = window.getComputedStyle(this.inputElement.nativeElement); if (!CACHED_FONT) {
return;
}
if (!canvas) { if (!canvas) {
canvas = document.createElement('canvas'); canvas = document.createElement('canvas');
@ -214,20 +227,31 @@ export class TagEditorComponent implements AfterViewInit, ControlValueAccessor,
const ctx = canvas.getContext('2d'); const ctx = canvas.getContext('2d');
if (ctx) { if (ctx) {
ctx.font = `${style.getPropertyValue('font-size')} ${style.getPropertyValue('font-family')}`; ctx.font = CACHED_FONT;
const widthText = ctx.measureText(this.inputElement.nativeElement.value).width; const text = this.inputElement.nativeElement.value;
const widthPlaceholder = ctx.measureText(this.placeholder).width; const textKey = `${text}§${this.placeholder}§${ctx.font}`;
const width = Math.max(widthText, widthPlaceholder); let width = CACHED_SIZES[textKey];
if (!width) {
const widthText = ctx.measureText(text).width;
const widthPlaceholder = ctx.measureText(this.placeholder).width;
width = Math.max(widthText, widthPlaceholder);
CACHED_SIZES[textKey] = width;
}
this.inputElement.nativeElement.style.width = <any>((width + 5) + 'px'); this.inputElement.nativeElement.style.width = <any>((width + 5) + 'px');
} }
} }
setTimeout(() => { if (this.singleLine) {
this.formElement.nativeElement.scrollLeft = this.formElement.nativeElement.scrollWidth; setTimeout(() => {
}, 0); this.formElement.nativeElement.scrollLeft = this.formElement.nativeElement.scrollWidth;
}, 0);
}
} }
public onKeyDown(event: KeyboardEvent) { public onKeyDown(event: KeyboardEvent) {

5
src/Squidex/app/framework/angular/forms/toggle.component.ts

@ -5,7 +5,7 @@
* Copyright (c) Squidex UG (haftungsbeschränkt). All rights reserved. * Copyright (c) Squidex UG (haftungsbeschränkt). All rights reserved.
*/ */
import { Component, forwardRef } from '@angular/core'; import { ChangeDetectionStrategy, Component, forwardRef } from '@angular/core';
import { ControlValueAccessor, NG_VALUE_ACCESSOR } from '@angular/forms'; import { ControlValueAccessor, NG_VALUE_ACCESSOR } from '@angular/forms';
import { Types } from '@app/framework/internal'; import { Types } from '@app/framework/internal';
@ -18,7 +18,8 @@ export const SQX_TOGGLE_CONTROL_VALUE_ACCESSOR: any = {
selector: 'sqx-toggle', selector: 'sqx-toggle',
styleUrls: ['./toggle.component.scss'], styleUrls: ['./toggle.component.scss'],
templateUrl: './toggle.component.html', templateUrl: './toggle.component.html',
providers: [SQX_TOGGLE_CONTROL_VALUE_ACCESSOR] providers: [SQX_TOGGLE_CONTROL_VALUE_ACCESSOR],
changeDetection: ChangeDetectionStrategy.OnPush
}) })
export class ToggleComponent implements ControlValueAccessor { export class ToggleComponent implements ControlValueAccessor {
private callChange = (v: any) => { /* NOOP */ }; private callChange = (v: any) => { /* NOOP */ };

31
src/Squidex/app/framework/angular/http/loading.interceptor.ts

@ -0,0 +1,31 @@
/*
* Squidex Headless CMS
*
* @license
* Copyright (c) Squidex UG (haftungsbeschränkt). All rights reserved.
*/
import { HttpEvent, HttpHandler, HttpInterceptor, HttpRequest } from '@angular/common/http';
import { Injectable} from '@angular/core';
import { Observable } from 'rxjs';
import { finalize } from 'rxjs/operators';
import { LoadingService, MathHelper } from './../../internal';
@Injectable()
export class LoadingInterceptor implements HttpInterceptor {
constructor(
private readonly loadingService: LoadingService
) {
}
public intercept(req: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> {
const id = MathHelper.guid();
this.loadingService.startLoading(id);
return next.handle(req).pipe(finalize(() => {
this.loadingService.completeLoading(id);
}));
}
}

23
src/Squidex/app/framework/angular/image-source.directive.ts

@ -9,6 +9,8 @@ import { AfterViewInit, Directive, ElementRef, HostListener, Input, OnChanges, O
import { MathHelper } from './../utils/math-helper'; import { MathHelper } from './../utils/math-helper';
const LAYOUT_CACHE: { [key: string]: { width: number, height: number } } = {};
@Directive({ @Directive({
selector: '[sqxImageSource]' selector: '[sqxImageSource]'
}) })
@ -26,6 +28,9 @@ export class ImageSourceDirective implements OnChanges, OnDestroy, OnInit, After
@Input() @Input()
public retryCount = 10; public retryCount = 10;
@Input()
public layoutKey: string;
@Input() @Input()
public parent: any = null; public parent: any = null;
@ -76,7 +81,21 @@ export class ImageSourceDirective implements OnChanges, OnDestroy, OnInit, After
} }
private resize() { private resize() {
this.size = this.parent.getBoundingClientRect(); let size: { width: number, height: number } = null!;
if (this.layoutKey) {
size = LAYOUT_CACHE[this.layoutKey];
}
if (!size) {
size = { width: this.parent.offsetWidth, height: this.parent.offsetHeight };
}
this.size = size;
if (this.layoutKey) {
LAYOUT_CACHE[this.layoutKey] = size;
}
this.renderer.setStyle(this.element.nativeElement, 'display', 'inline-block'); this.renderer.setStyle(this.element.nativeElement, 'display', 'inline-block');
this.renderer.setStyle(this.element.nativeElement, 'width', this.size.width + 'px'); this.renderer.setStyle(this.element.nativeElement, 'width', this.size.width + 'px');
@ -96,7 +115,7 @@ export class ImageSourceDirective implements OnChanges, OnDestroy, OnInit, After
if (w > 0 && h > 0) { if (w > 0 && h > 0) {
let source = `${this.imageSource}&width=${w}&height=${h}&mode=Crop`; let source = `${this.imageSource}&width=${w}&height=${h}&mode=Crop`;
if (this.loadQuery !== null) { if (this.loadQuery) {
source += `&q=${this.loadQuery}`; source += `&q=${this.loadQuery}`;
} }

20
src/Squidex/app/framework/angular/modals/dialog-renderer.component.ts

@ -5,7 +5,7 @@
* Copyright (c) Sebastian Stehle. All rights r vbeserved * Copyright (c) Sebastian Stehle. All rights r vbeserved
*/ */
import { Component, Input, OnDestroy, OnInit } from '@angular/core'; import { ChangeDetectionStrategy, ChangeDetectorRef, Component, Input, OnDestroy, OnInit } from '@angular/core';
import { Subscription } from 'rxjs'; import { Subscription } from 'rxjs';
import { import {
@ -22,7 +22,8 @@ import {
templateUrl: './dialog-renderer.component.html', templateUrl: './dialog-renderer.component.html',
animations: [ animations: [
fadeAnimation fadeAnimation
] ],
changeDetection: ChangeDetectionStrategy.OnPush
}) })
export class DialogRendererComponent implements OnDestroy, OnInit { export class DialogRendererComponent implements OnDestroy, OnInit {
private dialogSubscription: Subscription; private dialogSubscription: Subscription;
@ -38,6 +39,7 @@ export class DialogRendererComponent implements OnDestroy, OnInit {
public position = 'bottomright'; public position = 'bottomright';
constructor( constructor(
private readonly changeDetector: ChangeDetectorRef,
private readonly dialogs: DialogService private readonly dialogs: DialogService
) { ) {
} }
@ -53,6 +55,8 @@ export class DialogRendererComponent implements OnDestroy, OnInit {
this.dialogView.isOpen.subscribe(isOpen => { this.dialogView.isOpen.subscribe(isOpen => {
if (!isOpen) { if (!isOpen) {
this.cancel(); this.cancel();
this.changeDetector.detectChanges();
} }
}); });
@ -65,6 +69,8 @@ export class DialogRendererComponent implements OnDestroy, OnInit {
this.close(notification); this.close(notification);
}, notification.displayTime); }, notification.displayTime);
} }
this.changeDetector.detectChanges();
}); });
this.dialogsSubscription = this.dialogsSubscription =
@ -74,6 +80,8 @@ export class DialogRendererComponent implements OnDestroy, OnInit {
this.dialogRequest = request; this.dialogRequest = request;
this.dialogView.show(); this.dialogView.show();
this.changeDetector.detectChanges();
}); });
} }
@ -94,6 +102,12 @@ export class DialogRendererComponent implements OnDestroy, OnInit {
} }
public close(notification: Notification) { public close(notification: Notification) {
this.notifications.splice(this.notifications.indexOf(notification), 1); const index = this.notifications.indexOf(notification);
if (index >= 0) {
this.notifications.splice(index, 1);
this.changeDetector.detectChanges();
}
} }
} }

4
src/Squidex/app/framework/angular/modals/modal-target.directive.ts

@ -91,8 +91,8 @@ export class ModalTargetDirective implements AfterViewInit, OnDestroy, OnInit {
return; return;
} }
const viewportHeight = document.documentElement.clientHeight; const viewportHeight = document.documentElement!.clientHeight;
const viewportWidth = document.documentElement.clientWidth; const viewportWidth = document.documentElement!.clientWidth;
const modalRef = this.element.nativeElement; const modalRef = this.element.nativeElement;
const modalRect = this.element.nativeElement.getBoundingClientRect(); const modalRect = this.element.nativeElement.getBoundingClientRect();

2
src/Squidex/app/framework/angular/modals/modal-view.directive.ts

@ -71,7 +71,7 @@ export class ModalViewDirective implements OnChanges, OnDestroy {
} }
private update(isOpen: boolean) { private update(isOpen: boolean) {
if (isOpen === (this.renderedView !== null)) { if (isOpen === (!!this.renderedView)) {
return; return;
} }

8
src/Squidex/app/framework/angular/modals/onboarding-tooltip.component.ts

@ -5,7 +5,7 @@
* Copyright (c) Squidex UG (haftungsbeschränkt). All rights reserved. * Copyright (c) Squidex UG (haftungsbeschränkt). All rights reserved.
*/ */
import { Component, Input, OnDestroy, OnInit, Renderer2 } from '@angular/core'; import { ChangeDetectionStrategy, ChangeDetectorRef, Component, Input, OnDestroy, OnInit, Renderer2 } from '@angular/core';
import { import {
fadeAnimation, fadeAnimation,
@ -20,7 +20,8 @@ import {
templateUrl: './onboarding-tooltip.component.html', templateUrl: './onboarding-tooltip.component.html',
animations: [ animations: [
fadeAnimation fadeAnimation
] ],
changeDetection: ChangeDetectionStrategy.OnPush
}) })
export class OnboardingTooltipComponent implements OnDestroy, OnInit { export class OnboardingTooltipComponent implements OnDestroy, OnInit {
private showTimer: any; private showTimer: any;
@ -42,6 +43,7 @@ export class OnboardingTooltipComponent implements OnDestroy, OnInit {
public position = 'left'; public position = 'left';
constructor( constructor(
private readonly changeDetector: ChangeDetectorRef,
private readonly onboardingService: OnboardingService, private readonly onboardingService: OnboardingService,
private readonly renderer: Renderer2 private readonly renderer: Renderer2
) { ) {
@ -73,6 +75,8 @@ export class OnboardingTooltipComponent implements OnDestroy, OnInit {
if (this.isSameOrParent(fromPoint)) { if (this.isSameOrParent(fromPoint)) {
this.tooltipModal.show(); this.tooltipModal.show();
this.changeDetector.detectChanges();
this.closeTimer = setTimeout(() => { this.closeTimer = setTimeout(() => {
this.hideThis(); this.hideThis();
}, 10000); }, 10000);

5
src/Squidex/app/framework/angular/modals/root-view.component.ts

@ -5,12 +5,13 @@
* Copyright (c) Squidex UG (haftungsbeschränkt). All rights reserved. * Copyright (c) Squidex UG (haftungsbeschränkt). All rights reserved.
*/ */
import { Component, ViewChild, ViewContainerRef } from '@angular/core'; import { ChangeDetectionStrategy, Component, ViewChild, ViewContainerRef } from '@angular/core';
@Component({ @Component({
selector: 'sqx-root-view', selector: 'sqx-root-view',
styleUrls: ['./root-view.component.scss'], styleUrls: ['./root-view.component.scss'],
templateUrl: './root-view.component.html' templateUrl: './root-view.component.html',
changeDetection: ChangeDetectionStrategy.OnPush
}) })
export class RootViewComponent { export class RootViewComponent {
@ViewChild('element', { read: ViewContainerRef }) @ViewChild('element', { read: ViewContainerRef })

5
src/Squidex/app/framework/angular/modals/tooltip.component.ts

@ -5,7 +5,7 @@
* Copyright (c) Squidex UG (haftungsbeschränkt). All rights reserved. * Copyright (c) Squidex UG (haftungsbeschränkt). All rights reserved.
*/ */
import { ChangeDetectionStrategy, Component, Input, OnDestroy, OnInit, Renderer2 } from '@angular/core'; import { ChangeDetectionStrategy, ChangeDetectorRef, Component, Input, OnDestroy, OnInit, Renderer2 } from '@angular/core';
import { ModalModel } from './../../utils/modal-view'; import { ModalModel } from './../../utils/modal-view';
@ -33,6 +33,7 @@ export class TooltipComponent implements OnDestroy, OnInit {
public modal = new ModalModel(); public modal = new ModalModel();
constructor( constructor(
private readonly changeDetector: ChangeDetectorRef,
private readonly renderer: Renderer2 private readonly renderer: Renderer2
) { ) {
} }
@ -52,6 +53,8 @@ export class TooltipComponent implements OnDestroy, OnInit {
this.targetMouseEnterListener = this.targetMouseEnterListener =
this.renderer.listen(this.target, 'mouseenter', () => { this.renderer.listen(this.target, 'mouseenter', () => {
this.modal.show(); this.modal.show();
this.changeDetector.detectChanges();
}); });
this.targetMouseLeaveListener = this.targetMouseLeaveListener =

17
src/Squidex/app/framework/angular/panel-container.directive.ts

@ -14,6 +14,7 @@ import { PanelComponent } from './panel.component';
}) })
export class PanelContainerDirective implements AfterViewInit { export class PanelContainerDirective implements AfterViewInit {
private readonly panels: PanelComponent[] = []; private readonly panels: PanelComponent[] = [];
private isViewInit = false;
private containerWidth = 0; private containerWidth = 0;
constructor( constructor(
@ -27,14 +28,14 @@ export class PanelContainerDirective implements AfterViewInit {
this.invalidate(true); this.invalidate(true);
} }
public ngAfterViewInit() {
this.invalidate(true);
}
public push(panel: PanelComponent) { public push(panel: PanelComponent) {
this.panels.push(panel); this.panels.push(panel);
}
this.invalidate(); public ngAfterViewInit() {
this.isViewInit = true;
this.invalidate(true);
} }
public pop() { public pop() {
@ -44,8 +45,12 @@ export class PanelContainerDirective implements AfterViewInit {
} }
public invalidate(resize = false) { public invalidate(resize = false) {
if (!this.isViewInit) {
return;
}
if (resize) { if (resize) {
this.containerWidth = this.element.nativeElement.getBoundingClientRect().width; this.containerWidth = this.element.nativeElement.offsetWidth;
} }
const panels = this.panels; const panels = this.panels;

8
src/Squidex/app/framework/angular/panel.component.ts

@ -5,7 +5,7 @@
* Copyright (c) Squidex UG (haftungsbeschränkt). All rights reserved. * Copyright (c) Squidex UG (haftungsbeschränkt). All rights reserved.
*/ */
import { AfterViewInit, Component, ElementRef, Input, OnDestroy, OnInit, Renderer2, ViewChild } from '@angular/core'; import { AfterViewInit, ChangeDetectionStrategy, Component, ElementRef, Input, OnDestroy, OnInit, Renderer2, ViewChild } from '@angular/core';
import { slideRightAnimation } from './animations'; import { slideRightAnimation } from './animations';
@ -17,7 +17,8 @@ import { PanelContainerDirective } from './panel-container.directive';
templateUrl: './panel.component.html', templateUrl: './panel.component.html',
animations: [ animations: [
slideRightAnimation slideRightAnimation
] ],
changeDetection: ChangeDetectionStrategy.OnPush
}) })
export class PanelComponent implements AfterViewInit, OnDestroy, OnInit { export class PanelComponent implements AfterViewInit, OnDestroy, OnInit {
private styleWidth: string; private styleWidth: string;
@ -83,8 +84,7 @@ export class PanelComponent implements AfterViewInit, OnDestroy, OnInit {
this.styleWidth = size; this.styleWidth = size;
this.renderer.setStyle(this.panel.nativeElement, 'width', size); this.renderer.setStyle(this.panel.nativeElement, 'width', size);
this.renderWidth = this.panel.nativeElement.offsetWidth;
this.renderWidth = this.panel.nativeElement.getBoundingClientRect().width;
} }
} }

22
src/Squidex/app/framework/angular/routers/router-utils.ts

@ -5,7 +5,9 @@
* Copyright (c) Squidex UG (haftungsbeschränkt). All rights reserved. * Copyright (c) Squidex UG (haftungsbeschränkt). All rights reserved.
*/ */
import { ActivatedRoute, ActivatedRouteSnapshot, Data, Params } from '@angular/router'; import { ActivatedRoute, ActivatedRouteSnapshot, Data, Params, Router, RouterEvent, RouterStateSnapshot, RoutesRecognized } from '@angular/router';
import { Types } from './../../utils/types';
export function allData(value: ActivatedRouteSnapshot | ActivatedRoute): Data { export function allData(value: ActivatedRouteSnapshot | ActivatedRoute): Data {
let snapshot: ActivatedRouteSnapshot | null = value['snapshot'] || value; let snapshot: ActivatedRouteSnapshot | null = value['snapshot'] || value;
@ -40,4 +42,22 @@ export function allParams(value: ActivatedRouteSnapshot | ActivatedRoute): Param
} }
return result; return result;
}
export function childComponent(value: RouterStateSnapshot) {
let current = value.root;
while (true) {
if (current.firstChild) {
current = current.firstChild;
} else {
break;
}
}
return current.component;
}
export function navigatedToOtherComponent(router: Router) {
return (e: RouterEvent) => Types.is(e, RoutesRecognized) && childComponent(e.state) !== childComponent(router.routerState.snapshot);
} }

5
src/Squidex/app/framework/angular/shortcut.component.ts

@ -5,13 +5,14 @@
* Copyright (c) Squidex UG (haftungsbeschränkt). All rights reserved. * Copyright (c) Squidex UG (haftungsbeschränkt). All rights reserved.
*/ */
import { Component, EventEmitter, Input, NgZone, OnDestroy, OnInit, Output } from '@angular/core'; import { ChangeDetectionStrategy, Component, EventEmitter, Input, NgZone, OnDestroy, OnInit, Output } from '@angular/core';
import { ShortcutService } from './../services/shortcut.service'; import { ShortcutService } from './../services/shortcut.service';
@Component({ @Component({
selector: 'sqx-shortcut', selector: 'sqx-shortcut',
template: '' template: '',
changeDetection: ChangeDetectionStrategy.OnPush
}) })
export class ShortcutComponent implements OnDestroy, OnInit { export class ShortcutComponent implements OnDestroy, OnInit {
@Input() @Input()

5
src/Squidex/app/framework/angular/title.component.ts

@ -5,13 +5,14 @@
* Copyright (c) Squidex UG (haftungsbeschränkt). All rights reserved. * Copyright (c) Squidex UG (haftungsbeschränkt). All rights reserved.
*/ */
import { Component, Input, OnChanges } from '@angular/core'; import { ChangeDetectionStrategy, Component, Input, OnChanges } from '@angular/core';
import { TitleService } from './../services/title.service'; import { TitleService } from './../services/title.service';
@Component({ @Component({
selector: 'sqx-title', selector: 'sqx-title',
template: '' template: '',
changeDetection: ChangeDetectionStrategy.OnPush
}) })
export class TitleComponent implements OnChanges { export class TitleComponent implements OnChanges {
@Input() @Input()

5
src/Squidex/app/framework/angular/user-report.component.ts

@ -5,14 +5,15 @@
* Copyright (c) Squidex UG (haftungsbeschränkt). All rights reserved. * Copyright (c) Squidex UG (haftungsbeschränkt). All rights reserved.
*/ */
import { ChangeDetectorRef, Component, OnDestroy, OnInit } from '@angular/core'; import { ChangeDetectionStrategy, ChangeDetectorRef, Component, OnDestroy, OnInit } from '@angular/core';
import { UserReportConfig } from './../configurations'; import { UserReportConfig } from './../configurations';
import { ResourceLoaderService } from './../services/resource-loader.service'; import { ResourceLoaderService } from './../services/resource-loader.service';
@Component({ @Component({
selector: 'sqx-user-report', selector: 'sqx-user-report',
template: '' template: '',
changeDetection: ChangeDetectionStrategy.OnPush
}) })
export class UserReportComponent implements OnDestroy, OnInit { export class UserReportComponent implements OnDestroy, OnInit {
private loadingTimer: any; private loadingTimer: any;

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

@ -27,6 +27,7 @@ export * from './angular/forms/toggle.component';
export * from './angular/forms/transform-input.directive'; export * from './angular/forms/transform-input.directive';
export * from './angular/forms/validators'; export * from './angular/forms/validators';
export * from './angular/http/loading.interceptor';
export * from './angular/http/http-extensions'; export * from './angular/http/http-extensions';
export * from './angular/modals/dialog-renderer.component'; export * from './angular/modals/dialog-renderer.component';

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

@ -11,6 +11,7 @@ export * from './angular/animations';
export * from './services/analytics.service'; export * from './services/analytics.service';
export * from './services/clipboard.service'; export * from './services/clipboard.service';
export * from './services/dialog.service'; export * from './services/dialog.service';
export * from './services/loading.service';
export * from './services/local-store.service'; export * from './services/local-store.service';
export * from './services/message-bus.service'; export * from './services/message-bus.service';
export * from './services/onboarding.service'; export * from './services/onboarding.service';

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

@ -6,6 +6,7 @@
*/ */
import { CommonModule } from '@angular/common'; import { CommonModule } from '@angular/common';
import { HTTP_INTERCEPTORS } from '@angular/common/http';
import { ModuleWithProviders, NgModule } from '@angular/core'; import { ModuleWithProviders, NgModule } from '@angular/core';
import { FormsModule, ReactiveFormsModule } from '@angular/forms'; import { FormsModule, ReactiveFormsModule } from '@angular/forms';
@ -44,6 +45,8 @@ import {
KeysPipe, KeysPipe,
KNumberPipe, KNumberPipe,
LightenPipe, LightenPipe,
LoadingInterceptor,
LoadingService,
LocalStoreService, LocalStoreService,
MessageBus, MessageBus,
ModalDialogComponent, ModalDialogComponent,
@ -220,11 +223,17 @@ export class SqxFrameworkModule {
ClipboardService, ClipboardService,
DialogService, DialogService,
LocalStoreService, LocalStoreService,
LoadingService,
MessageBus, MessageBus,
OnboardingService, OnboardingService,
ResourceLoaderService, ResourceLoaderService,
ShortcutService, ShortcutService,
TitleService TitleService,
{
provide: HTTP_INTERCEPTORS,
useClass: LoadingInterceptor,
multi: true
}
] ]
}; };
} }

114
src/Squidex/app/framework/services/loading.service.spec.ts

@ -0,0 +1,114 @@
/*
* Squidex Headless CMS
*
* @license
* Copyright (c) Squidex UG (haftungsbeschränkt). All rights reserved.
*/
import { Event, NavigationError, NavigationStart } from '@angular/router';
import { Subject } from 'rxjs';
import { LoadingService, LoadingServiceFactory } from './loading.service';
describe('LoadingService', () => {
let events = new Subject<Event>();
it('should instantiate from factory', () => {
const loadingService = LoadingServiceFactory(<any>{ events });
expect(loadingService).toBeDefined();
});
it('should instantiate', () => {
const loadingService = new LoadingService(<any>{ events });
expect(loadingService).toBeDefined();
loadingService.ngOnDestroy();
});
it('should set to loaded', () => {
const loadingService = new LoadingService(<any>{ events });
let state = false;
loadingService.loading.subscribe(v => state = v);
loadingService.startLoading('1');
expect(state).toBeTruthy();
});
it('should set to loaded on navigation start', () => {
const loadingService = new LoadingService(<any>{ events });
let state = false;
loadingService.loading.subscribe(v => state = v);
events.next(new NavigationStart(0, ''));
expect(state).toBeTruthy();
});
it('should not unset from loaded immediately', () => {
const loadingService = new LoadingService(<any>{ events });
let state = false;
loadingService.loading.subscribe(v => state = v);
loadingService.startLoading('1');
loadingService.completeLoading('1');
expect(state).toBeTruthy();
});
it('should not unset from loaded delayed', (cb) => {
const loadingService = new LoadingService(<any>{ events });
let state = false;
loadingService.loading.subscribe(v => state = v);
loadingService.startLoading('1');
loadingService.completeLoading('1');
setTimeout(() => {
expect(state).toBeFalsy();
cb();
}, 400);
});
it('should not unset from loaded delayed on navigation event', (cb) => {
const loadingService = new LoadingService(<any>{ events });
let state = false;
loadingService.loading.subscribe(v => state = v);
events.next(new NavigationStart(0, ''));
events.next(new NavigationError(0, '', 0));
setTimeout(() => {
expect(state).toBeFalsy();
cb();
}, 400);
});
it('should set back to loaded after several completions', (cb) => {
const loadingService = new LoadingService(<any>{ events });
let state = false;
loadingService.loading.subscribe(v => state = v);
loadingService.startLoading('1');
loadingService.completeLoading('1');
loadingService.completeLoading('1');
loadingService.startLoading('2');
setTimeout(() => {
expect(state).toBeTruthy();
cb();
}, 400);
});
});

68
src/Squidex/app/framework/services/loading.service.ts

@ -0,0 +1,68 @@
/*
* Squidex Headless CMS
*
* @license
* Copyright (c) Squidex UG (haftungsbeschränkt). All rights reserved.
*/
import { Injectable, OnDestroy } from '@angular/core';
import { NavigationCancel, NavigationEnd, NavigationError, NavigationStart, Router } from '@angular/router';
import { BehaviorSubject, Observable, Subscription } from 'rxjs';
import { map } from 'rxjs/operators';
import { Types } from './../utils/types';
export const LoadingServiceFactory = (router: Router) => {
return new LoadingService(router);
};
@Injectable()
export class LoadingService implements OnDestroy {
private readonly routerSubscription: Subscription;
private readonly loading$ = new BehaviorSubject(0);
private readonly loadingOperations: { [key: string]: boolean } = {};
public get loading(): Observable<boolean> {
return this.loading$.pipe(map(x => x > 0));
}
constructor(router: Router) {
this.routerSubscription =
router.events.subscribe(event => {
if (Types.is(event, NavigationStart)) {
this.startLoading(event.id.toString());
} else if (
Types.is(event, NavigationEnd) ||
Types.is(event, NavigationCancel) ||
Types.is(event, NavigationError)) {
this.completeLoading(event.id.toString());
}
});
}
public ngOnDestroy() {
this.routerSubscription.unsubscribe();
}
public startLoading(key: string) {
if (!this.loadingOperations[key]) {
this.loadingOperations[key] = true;
this.loading$.next(this.loading$.value + 1);
}
}
public completeLoading(key: string) {
if (this.loadingOperations[key]) {
delete this.loadingOperations[key];
setTimeout(() => {
const value = this.loading$.value;
if (value > 0) {
this.loading$.next(value - 1);
}
}, 250);
}
}
}

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

@ -5,7 +5,7 @@
* Copyright (c) Squidex UG (haftungsbeschränkt). All rights reserved. * Copyright (c) Squidex UG (haftungsbeschränkt). All rights reserved.
*/ */
import { Component, EventEmitter, Input, Output } from '@angular/core'; import { ChangeDetectionStrategy, Component, EventEmitter, Input, Output } from '@angular/core';
import { FormBuilder } from '@angular/forms'; import { FormBuilder } from '@angular/forms';
import { import {
@ -18,7 +18,8 @@ import {
@Component({ @Component({
selector: 'sqx-app-form', selector: 'sqx-app-form',
styleUrls: ['./app-form.component.scss'], styleUrls: ['./app-form.component.scss'],
templateUrl: './app-form.component.html' templateUrl: './app-form.component.html',
changeDetection: ChangeDetectionStrategy.OnPush
}) })
export class AppFormComponent { export class AppFormComponent {
@Output() @Output()
@ -46,7 +47,7 @@ export class AppFormComponent {
const request = new CreateAppDto(value.name, this.template); const request = new CreateAppDto(value.name, this.template);
this.appsStore.create(request) this.appsStore.create(request)
.subscribe(dto => { .subscribe(() => {
this.complete(); this.complete();
}, error => { }, error => {
this.createForm.submitFailed(error); this.createForm.submitFailed(error);

8
src/Squidex/app/shared/components/asset.component.html

@ -7,7 +7,7 @@
</span> </span>
<div *ngIf="asset.isImage" class="file-image"> <div *ngIf="asset.isImage" class="file-image">
<img [sqxImageSource]="asset | sqxAssetPreviewUrl"> <img [sqxImageSource]="asset | sqxAssetPreviewUrl" class="bg" layoutKey="asset-large">
</div> </div>
<div *ngIf="!asset.isImage" class="file-icon"> <div *ngIf="!asset.isImage" class="file-icon">
<img [attr.src]="asset | sqxFileIcon"> <img [attr.src]="asset | sqxFileIcon">
@ -89,7 +89,7 @@
<div class="left-border" [class.selected]="isSelected" ></div> <div class="left-border" [class.selected]="isSelected" ></div>
<div *ngIf="asset && asset.isImage && progress === 0" class="image" @fade> <div *ngIf="asset && asset.isImage && progress === 0" class="image" @fade>
<img [sqxImageSource]="asset | sqxAssetPreviewUrl"> <img [sqxImageSource]="asset | sqxAssetPreviewUrl" class="bg2" layoutKey="asset-small">
</div> </div>
<div *ngIf="asset && !asset.isImage && progress === 0" class="image image-padded" @fade> <div *ngIf="asset && !asset.isImage && progress === 0" class="image image-padded" @fade>
<img class="icon" [attr.src]="asset | sqxFileIcon"> <img class="icon" [attr.src]="asset | sqxFileIcon">
@ -130,9 +130,7 @@
</div> </div>
<div class="upload-progress" *ngIf="progress > 0"> <div class="upload-progress" *ngIf="progress > 0">
<div class="progress-background"> <sqx-progress-bar [value]="50" [trailWidth]="0.8" [strokeWidth]="0.8" [showText]="false"></sqx-progress-bar>
<div class="progress-bar" [style.width]="(progress + 40) + '%'"></div>
</div>
</div> </div>
<div class="drop-overlay align-items-center justify-content-center" *ngIf="asset && progress === 0"> <div class="drop-overlay align-items-center justify-content-center" *ngIf="asset && progress === 0">

26
src/Squidex/app/shared/components/asset.component.scss

@ -254,6 +254,10 @@ $list-height: 2.375rem;
min-width: 12rem; min-width: 12rem;
} }
} }
.upload-progress {
padding: .25rem 0;
}
} }
.drop-overlay { .drop-overlay {
@ -279,24 +283,16 @@ $list-height: 2.375rem;
} }
} }
.progress { .selectable {
&-background { cursor: pointer;
background: $color-border; }
margin: (($list-height - .25rem) / 2) 0;
}
&-bar {
background: $color-theme-blue;
}
&-background, .bg {
&-bar { background-image: url('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAADwAAAA8CAIAAAC1nk4lAAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8YQUAAAAJcEhZcwAADsMAAA7DAcdvqGQAAAAYdEVYdFNvZnR3YXJlAHBhaW50Lm5ldCA0LjEuMWMqnEsAAACbSURBVGhD7c6hDQAxAMPA33+m7vYlJh7AoFKOBMbfyfyZRRsPgUUbD4FFGw+BRRsPgUUbD4FFGw+BRRsPgUUbD4FFGw+BRRsPgUUbD4FFGw+BRRsPgUUbD4E3o9kA7YFFGw+BRRsPgUUbD4FFGw+BRRsPgUUbD4FFGw+BRRsPgUUbD4FFGw+BRRsPgUUbD4FFGw+BRRsPgQejz7nPYYKl8IqSfgAAAABJRU5ErkJggg==');
height: .25rem;
}
} }
.selectable { .bg2 {
cursor: pointer; background-image: url('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAAUCAIAAAAC64paAAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8YQUAAAAJcEhZcwAADsMAAA7DAcdvqGQAAAAYdEVYdFNvZnR3YXJlAHBhaW50Lm5ldCA0LjEuMWMqnEsAAAAsSURBVDhPY9iDF/zHC0Y1YwCoKhxgRGqG0jgA1AwcYFQzBoCqwgFGnuY9ewCdSg6FRg4gMAAAAABJRU5ErkJggg==');
} }
.tags { .tags {

24
src/Squidex/app/shared/components/asset.component.ts

@ -5,7 +5,7 @@
* Copyright (c) Squidex UG (haftungsbeschränkt). All rights reserved. * Copyright (c) Squidex UG (haftungsbeschränkt). All rights reserved.
*/ */
import { Component, EventEmitter, HostBinding, Input, OnDestroy, OnInit, Output } from '@angular/core'; import { ChangeDetectionStrategy, ChangeDetectorRef, Component, EventEmitter, HostBinding, Input, OnDestroy, OnInit, Output } from '@angular/core';
import { FormBuilder, FormControl } from '@angular/forms'; import { FormBuilder, FormControl } from '@angular/forms';
import { Subscription } from 'rxjs'; import { Subscription } from 'rxjs';
import { debounceTime, distinctUntilChanged } from 'rxjs/operators'; import { debounceTime, distinctUntilChanged } from 'rxjs/operators';
@ -31,7 +31,8 @@ import {
templateUrl: './asset.component.html', templateUrl: './asset.component.html',
animations: [ animations: [
fadeAnimation fadeAnimation
] ],
changeDetection: ChangeDetectionStrategy.OnPush
}) })
export class AssetComponent implements OnDestroy, OnInit { export class AssetComponent implements OnDestroy, OnInit {
private tagSubscription: Subscription; private tagSubscription: Subscription;
@ -91,6 +92,7 @@ export class AssetComponent implements OnDestroy, OnInit {
private readonly appsState: AppsState, private readonly appsState: AppsState,
private readonly assetsService: AssetsService, private readonly assetsService: AssetsService,
private readonly authState: AuthService, private readonly authState: AuthService,
private readonly changeDetector: ChangeDetectorRef,
private readonly dialogs: DialogService, private readonly dialogs: DialogService,
private readonly formBuilder: FormBuilder private readonly formBuilder: FormBuilder
) { ) {
@ -105,7 +107,7 @@ export class AssetComponent implements OnDestroy, OnInit {
if (Types.is(dto, AssetDto)) { if (Types.is(dto, AssetDto)) {
this.emitLoaded(dto); this.emitLoaded(dto);
} else { } else {
this.progress = dto; this.setProgress(dto);
} }
}, error => { }, error => {
this.dialogs.notifyError(error); this.dialogs.notifyError(error);
@ -145,7 +147,7 @@ export class AssetComponent implements OnDestroy, OnInit {
}, error => { }, error => {
this.dialogs.notifyError(error); this.dialogs.notifyError(error);
this.setProgress(); this.setProgress(0);
}); });
} }
} }
@ -159,8 +161,6 @@ export class AssetComponent implements OnDestroy, OnInit {
this.assetsService.putAsset(this.appsState.appName, this.asset.id, requestDto, this.asset.version) this.assetsService.putAsset(this.appsState.appName, this.asset.id, requestDto, this.asset.version)
.subscribe(dto => { .subscribe(dto => {
this.updateAsset(this.asset.rename(requestDto.fileName, this.authState.user!.token, dto.version), true); this.updateAsset(this.asset.rename(requestDto.fileName, this.authState.user!.token, dto.version), true);
this.renameCancel();
}, error => { }, error => {
this.dialogs.notifyError(error); this.dialogs.notifyError(error);
@ -194,10 +194,6 @@ export class AssetComponent implements OnDestroy, OnInit {
this.renaming = false; this.renaming = false;
} }
private setProgress(progress = 0) {
this.progress = progress;
}
private emitFailed(error: any) { private emitFailed(error: any) {
this.failed.emit(error); this.failed.emit(error);
} }
@ -210,6 +206,12 @@ export class AssetComponent implements OnDestroy, OnInit {
this.updated.emit(asset); this.updated.emit(asset);
} }
private setProgress(progress: number) {
this.progress = progress;
this.changeDetector.detectChanges();
}
private updateAsset(asset: AssetDto, emitEvent: boolean) { private updateAsset(asset: AssetDto, emitEvent: boolean) {
this.asset = asset; this.asset = asset;
this.progress = 0; this.progress = 0;
@ -221,5 +223,7 @@ export class AssetComponent implements OnDestroy, OnInit {
} }
this.renameCancel(); this.renameCancel();
this.changeDetector.detectChanges();
} }
} }

5
src/Squidex/app/shared/components/assets-list.component.ts

@ -7,7 +7,7 @@
// tslint:disable:prefer-for-of // tslint:disable:prefer-for-of
import { Component, EventEmitter, Input, Output } from '@angular/core'; import { ChangeDetectionStrategy, Component, EventEmitter, Input, Output } from '@angular/core';
import { onErrorResumeNext } from 'rxjs/operators'; import { onErrorResumeNext } from 'rxjs/operators';
import { import {
@ -19,7 +19,8 @@ import {
@Component({ @Component({
selector: 'sqx-assets-list', selector: 'sqx-assets-list',
styleUrls: ['./assets-list.component.scss'], styleUrls: ['./assets-list.component.scss'],
templateUrl: './assets-list.component.html' templateUrl: './assets-list.component.html',
changeDetection: ChangeDetectionStrategy.OnPush
}) })
export class AssetsListComponent { export class AssetsListComponent {
public newFiles = ImmutableArray.empty<File>(); public newFiles = ImmutableArray.empty<File>();

5
src/Squidex/app/shared/components/assets-selector.component.ts

@ -7,7 +7,7 @@
// tslint:disable:prefer-for-of // tslint:disable:prefer-for-of
import { Component, EventEmitter, OnInit, Output } from '@angular/core'; import { ChangeDetectionStrategy, Component, EventEmitter, OnInit, Output } from '@angular/core';
import { onErrorResumeNext } from 'rxjs/operators'; import { onErrorResumeNext } from 'rxjs/operators';
import { import {
@ -23,7 +23,8 @@ import {
templateUrl: './assets-selector.component.html', templateUrl: './assets-selector.component.html',
animations: [ animations: [
fadeAnimation fadeAnimation
] ],
changeDetection: ChangeDetectionStrategy.OnPush
}) })
export class AssetsSelectorComponent implements OnInit { export class AssetsSelectorComponent implements OnInit {
public selectedAssets: { [id: string]: AssetDto } = {}; public selectedAssets: { [id: string]: AssetDto } = {};

85
src/Squidex/app/shared/components/geolocation-editor.component.ts

@ -5,7 +5,7 @@
* Copyright (c) Squidex UG (haftungsbeschränkt). All rights reserved. * Copyright (c) Squidex UG (haftungsbeschränkt). All rights reserved.
*/ */
import { AfterViewInit, Component, ElementRef, forwardRef, ViewChild } from '@angular/core'; import { AfterViewInit, ChangeDetectionStrategy, Component, ElementRef, forwardRef, ViewChild } from '@angular/core';
import { ControlValueAccessor, FormBuilder, NG_VALUE_ACCESSOR } from '@angular/forms'; import { ControlValueAccessor, FormBuilder, NG_VALUE_ACCESSOR } from '@angular/forms';
import { import {
@ -31,7 +31,8 @@ interface Geolocation {
selector: 'sqx-geolocation-editor', selector: 'sqx-geolocation-editor',
styleUrls: ['./geolocation-editor.component.scss'], styleUrls: ['./geolocation-editor.component.scss'],
templateUrl: './geolocation-editor.component.html', templateUrl: './geolocation-editor.component.html',
providers: [SQX_GEOLOCATION_EDITOR_CONTROL_VALUE_ACCESSOR] providers: [SQX_GEOLOCATION_EDITOR_CONTROL_VALUE_ACCESSOR],
changeDetection: ChangeDetectionStrategy.OnPush
}) })
export class GeolocationEditorComponent implements ControlValueAccessor, AfterViewInit { export class GeolocationEditorComponent implements ControlValueAccessor, AfterViewInit {
private callChange = (v: any) => { /* NOOP */ }; private callChange = (v: any) => { /* NOOP */ };
@ -144,12 +145,12 @@ export class GeolocationEditorComponent implements ControlValueAccessor, AfterVi
} }
public updateValueByInput() { public updateValueByInput() {
let updateMap = this.geolocationForm.controls['latitude'].value !== null && const lat = this.geolocationForm.controls['latitude'].value;
this.geolocationForm.controls['longitude'].value !== null; const lng = this.geolocationForm.controls['longitude'].value;
this.value = this.geolocationForm.value; this.updateValue(lat, lng);
if (updateMap) { if (lat && lng) {
this.updateMarker(true, true); this.updateMarker(true, true);
} else { } else {
this.callChange(this.value); this.callChange(this.value);
@ -188,11 +189,7 @@ export class GeolocationEditorComponent implements ControlValueAccessor, AfterVi
if (!this.marker && !this.isDisabled) { if (!this.marker && !this.isDisabled) {
const latlng = event.latlng.wrap(); const latlng = event.latlng.wrap();
this.value = { this.updateValue(latlng.lat, latlng.lng);
latitude: latlng.lat,
longitude: latlng.lng
};
this.updateMarker(false, true); this.updateMarker(false, true);
} }
}); });
@ -225,11 +222,7 @@ export class GeolocationEditorComponent implements ControlValueAccessor, AfterVi
this.map.addListener('click', this.map.addListener('click',
(event: any) => { (event: any) => {
if (!this.isDisabled) { if (!this.isDisabled) {
this.value = { this.updateValue(event.latLng.lat(), event.latLng.lng());
latitude: event.latLng.lat(),
longitude: event.latLng.lng()
};
this.updateMarker(false, true); this.updateMarker(false, true);
} }
}); });
@ -252,8 +245,7 @@ export class GeolocationEditorComponent implements ControlValueAccessor, AfterVi
let lat = place.geometry.location.lat(); let lat = place.geometry.location.lat();
let lng = place.geometry.location.lng(); let lng = place.geometry.location.lng();
this.value = { latitude: lat, longitude: lng }; this.updateValue(lat, lng);
this.updateMarker(false, true); this.updateMarker(false, true);
} }
} }
@ -274,15 +266,30 @@ export class GeolocationEditorComponent implements ControlValueAccessor, AfterVi
this.updateMarker(true, true); this.updateMarker(true, true);
} }
private updateValue(lat: number, lng: number) {
this.value = { latitude: lat, longitude: lng };
}
private updateMarker(zoom: boolean, fireEvent: boolean) { private updateMarker(zoom: boolean, fireEvent: boolean) {
if (!this.isGoogleMaps) { if (!this.isGoogleMaps) {
this.updateMarkerOSM(zoom, fireEvent); this.updateMarkerOSM(zoom);
} else { } else {
this.updateMarkerGoogle(zoom, fireEvent); this.updateMarkerGoogle(zoom);
}
if (this.value) {
this.geolocationForm.setValue(this.value, { emitEvent: true, onlySelf: false });
} else {
this.geolocationForm.reset(undefined, { emitEvent: true, onlySelf: false });
}
if (fireEvent) {
this.callChange(this.value);
this.callTouched();
} }
} }
private updateMarkerOSM(zoom: boolean, fireEvent: boolean) { private updateMarkerOSM(zoom: boolean) {
if (this.value) { if (this.value) {
if (!this.marker) { if (!this.marker) {
this.marker = L.marker([0, 90], { draggable: true }).addTo(this.map); this.marker = L.marker([0, 90], { draggable: true }).addTo(this.map);
@ -290,10 +297,7 @@ export class GeolocationEditorComponent implements ControlValueAccessor, AfterVi
this.marker.on('drag', (event: any) => { this.marker.on('drag', (event: any) => {
const latlng = event.latlng.wrap(); const latlng = event.latlng.wrap();
this.value = { this.updateValue(latlng.lat, latlng.lng);
latitude: latlng.lat,
longitude: latlng.lng
};
}); });
this.marker.on('dragend', () => { this.marker.on('dragend', () => {
@ -314,8 +318,6 @@ export class GeolocationEditorComponent implements ControlValueAccessor, AfterVi
} }
this.marker.setLatLng(latLng); this.marker.setLatLng(latLng);
this.geolocationForm.setValue(this.value, { emitEvent: false, onlySelf: false });
} else { } else {
if (this.marker) { if (this.marker) {
this.marker.removeFrom(this.map); this.marker.removeFrom(this.map);
@ -323,17 +325,10 @@ export class GeolocationEditorComponent implements ControlValueAccessor, AfterVi
} }
this.map.fitWorld(); this.map.fitWorld();
this.geolocationForm.reset(undefined, { emitEvent: false, onlySelf: false });
}
if (fireEvent) {
this.callChange(this.value);
this.callTouched();
} }
} }
private updateMarkerGoogle(zoom: boolean, fireEvent: boolean) { private updateMarkerGoogle(zoom: boolean) {
if (this.value) { if (this.value) {
if (!this.marker) { if (!this.marker) {
this.marker = new google.maps.Marker({ this.marker = new google.maps.Marker({
@ -347,19 +342,12 @@ export class GeolocationEditorComponent implements ControlValueAccessor, AfterVi
this.marker.addListener('drag', (event: any) => { this.marker.addListener('drag', (event: any) => {
if (!this.isDisabled) { if (!this.isDisabled) {
this.value = { this.updateValue(event.latLng.lat(), event.LatLng.lng());
latitude: event.latLng.lat(),
longitude: event.latLng.lng()
};
} }
}); });
this.marker.addListener('dragend', (event: any) => { this.marker.addListener('dragend', (event: any) => {
if (!this.isDisabled) { if (!this.isDisabled) {
this.value = { this.updateValue(event.latLng.lat(), event.LatLng.lng());
latitude: event.latLng.lat(),
longitude: event.latLng.lng()
};
this.updateMarker(false, true); this.updateMarker(false, true);
} }
}); });
@ -375,8 +363,6 @@ export class GeolocationEditorComponent implements ControlValueAccessor, AfterVi
this.marker.setPosition(latLng); this.marker.setPosition(latLng);
this.map.setZoom(12); this.map.setZoom(12);
this.geolocationForm.setValue(this.value, { emitEvent: false, onlySelf: false });
} else { } else {
if (this.marker) { if (this.marker) {
this.marker.setMap(null); this.marker.setMap(null);
@ -384,13 +370,6 @@ export class GeolocationEditorComponent implements ControlValueAccessor, AfterVi
} }
this.map.setCenter({ lat: 0, lng: 0 }); this.map.setCenter({ lat: 0, lng: 0 });
this.geolocationForm.reset(undefined, { emitEvent: false, onlySelf: false });
}
if (fireEvent) {
this.callChange(this.value);
this.callTouched();
} }
} }
} }

8
src/Squidex/app/shared/components/help.component.ts

@ -5,7 +5,7 @@
* Copyright (c) Squidex UG (haftungsbeschränkt). All rights reserved. * Copyright (c) Squidex UG (haftungsbeschränkt). All rights reserved.
*/ */
import { Component } from '@angular/core'; import { ChangeDetectionStrategy, Component } from '@angular/core';
import { ActivatedRoute } from '@angular/router'; import { ActivatedRoute } from '@angular/router';
import { HelpService } from '@app/shared/internal'; import { HelpService } from '@app/shared/internal';
@ -13,11 +13,11 @@ import { HelpService } from '@app/shared/internal';
@Component({ @Component({
selector: 'sqx-help', selector: 'sqx-help',
styleUrls: ['./help.component.scss'], styleUrls: ['./help.component.scss'],
templateUrl: './help.component.html' templateUrl: './help.component.html',
changeDetection: ChangeDetectionStrategy.OnPush
}) })
export class HelpComponent { export class HelpComponent {
public helpSections = public helpSections = this.helpService.getHelp(this.route.snapshot.data['helpPage']);
this.helpService.getHelp(this.route.snapshot.data['helpPage']);
constructor( constructor(
private readonly helpService: HelpService, private readonly helpService: HelpService,

4
src/Squidex/app/shared/components/history-list.component.html

@ -1,12 +1,12 @@
<ng-container *ngIf="events"> <ng-container *ngIf="events">
<div *ngFor="let event of events" class="event row no-gutters"> <div *ngFor="let event of events; trackBy: trackByEvent" class="event row no-gutters">
<div class="col col-auto"> <div class="col col-auto">
<img class="user-picture" [attr.title]="event.actor | sqxUserNameRef:null" [attr.src]="event.actor | sqxUserPictureRef" /> <img class="user-picture" [attr.title]="event.actor | sqxUserNameRef:null" [attr.src]="event.actor | sqxUserPictureRef" />
</div> </div>
<div class="col pl-2"> <div class="col pl-2">
<div class="event-message"> <div class="event-message">
<span class="user-ref">{{event.actor | sqxUserNameRef:null}}</span> <span class="user-ref">{{event.actor | sqxUserNameRef:null}}</span>
<span [innerHTML]="format(event.message) | async"></span> <span [innerHTML]="event | sqxHistoryMessage"></span>
</div> </div>
<div class="event-created">{{event.created | sqxFromNow}}</div> <div class="event-created">{{event.created | sqxFromNow}}</div>
</div> </div>

21
src/Squidex/app/shared/components/history-list.component.ts

@ -5,33 +5,20 @@
* Copyright (c) Squidex UG (haftungsbeschränkt). All rights reserved. * Copyright (c) Squidex UG (haftungsbeschränkt). All rights reserved.
*/ */
import { Component, Input } from '@angular/core'; import { ChangeDetectionStrategy, Component, Input } from '@angular/core';
import { Observable } from 'rxjs';
import { import { HistoryEventDto } from '@app/shared/internal';
formatHistoryMessage,
HistoryEventDto,
UsersProviderService
} from '@app/shared/internal';
@Component({ @Component({
selector: 'sqx-history-list', selector: 'sqx-history-list',
styleUrls: ['./history-list.component.scss'], styleUrls: ['./history-list.component.scss'],
templateUrl: './history-list.component.html' templateUrl: './history-list.component.html',
changeDetection: ChangeDetectionStrategy.OnPush
}) })
export class HistoryListComponent { export class HistoryListComponent {
@Input() @Input()
public events: HistoryEventDto; public events: HistoryEventDto;
constructor(
private readonly users: UsersProviderService
) {
}
public format(message: string): Observable<string> {
return formatHistoryMessage(message, this.users);
}
public trackByEvent(index: number, event: HistoryEventDto) { public trackByEvent(index: number, event: HistoryEventDto) {
return event.eventId; return event.eventId;
} }

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

@ -5,7 +5,7 @@
* Copyright (c) Squidex UG (haftungsbeschränkt). All rights reserved. * Copyright (c) Squidex UG (haftungsbeschränkt). All rights reserved.
*/ */
import { Component } from '@angular/core'; import { ChangeDetectionStrategy, Component } from '@angular/core';
import { ActivatedRoute } from '@angular/router'; import { ActivatedRoute } from '@angular/router';
import { merge, Observable, timer } from 'rxjs'; import { merge, Observable, timer } from 'rxjs';
import { delay, switchMap } from 'rxjs/operators'; import { delay, switchMap } from 'rxjs/operators';
@ -22,7 +22,8 @@ import {
@Component({ @Component({
selector: 'sqx-history', selector: 'sqx-history',
styleUrls: ['./history.component.scss'], styleUrls: ['./history.component.scss'],
templateUrl: './history.component.html' templateUrl: './history.component.html',
changeDetection: ChangeDetectionStrategy.OnPush
}) })
export class HistoryComponent { export class HistoryComponent {
private readonly channel = this.calculateChannel(); private readonly channel = this.calculateChannel();
@ -32,7 +33,7 @@ export class HistoryComponent {
timer(0, 10000), timer(0, 10000),
this.messageBus.of(HistoryChannelUpdated).pipe(delay(1000)) this.messageBus.of(HistoryChannelUpdated).pipe(delay(1000))
).pipe( ).pipe(
switchMap(app => this.historyService.getHistory(this.appsState.appName, this.channel))); switchMap(() => this.historyService.getHistory(this.appsState.appName, this.channel)));
constructor( constructor(
private readonly appsState: AppsState, private readonly appsState: AppsState,

5
src/Squidex/app/shared/components/language-selector.component.ts

@ -5,7 +5,7 @@
* Copyright (c) Squidex UG (haftungsbeschränkt). All rights reserved. * Copyright (c) Squidex UG (haftungsbeschränkt). All rights reserved.
*/ */
import { Component, EventEmitter, Input, OnChanges, OnInit, Output } from '@angular/core'; import { ChangeDetectionStrategy, Component, EventEmitter, Input, OnChanges, OnInit, Output } from '@angular/core';
import { fadeAnimation, ModalModel } from '@app/shared/internal'; import { fadeAnimation, ModalModel } from '@app/shared/internal';
@ -17,7 +17,8 @@ export interface Language { iso2Code: string; englishName: string; isMasterLangu
templateUrl: './language-selector.component.html', templateUrl: './language-selector.component.html',
animations: [ animations: [
fadeAnimation fadeAnimation
] ],
changeDetection: ChangeDetectionStrategy.OnPush
}) })
export class LanguageSelectorComponent implements OnChanges, OnInit { export class LanguageSelectorComponent implements OnChanges, OnInit {
public dropdown = new ModalModel(); public dropdown = new ModalModel();

5
src/Squidex/app/shared/components/markdown-editor.component.ts

@ -5,7 +5,7 @@
* Copyright (c) Squidex UG (haftungsbeschränkt). All rights reserved. * Copyright (c) Squidex UG (haftungsbeschränkt). All rights reserved.
*/ */
import { AfterViewInit, Component, ElementRef, forwardRef, Renderer2, ViewChild } from '@angular/core'; import { AfterViewInit, ChangeDetectionStrategy, Component, ElementRef, forwardRef, Renderer2, ViewChild } from '@angular/core';
import { ControlValueAccessor, NG_VALUE_ACCESSOR } from '@angular/forms'; import { ControlValueAccessor, NG_VALUE_ACCESSOR } from '@angular/forms';
import { import {
@ -25,7 +25,8 @@ export const SQX_MARKDOWN_EDITOR_CONTROL_VALUE_ACCESSOR: any = {
selector: 'sqx-markdown-editor', selector: 'sqx-markdown-editor',
styleUrls: ['./markdown-editor.component.scss'], styleUrls: ['./markdown-editor.component.scss'],
templateUrl: './markdown-editor.component.html', templateUrl: './markdown-editor.component.html',
providers: [SQX_MARKDOWN_EDITOR_CONTROL_VALUE_ACCESSOR] providers: [SQX_MARKDOWN_EDITOR_CONTROL_VALUE_ACCESSOR],
changeDetection: ChangeDetectionStrategy.OnPush
}) })
export class MarkdownEditorComponent implements ControlValueAccessor, AfterViewInit { export class MarkdownEditorComponent implements ControlValueAccessor, AfterViewInit {
private callChange = (v: any) => { /* NOOP */ }; private callChange = (v: any) => { /* NOOP */ };

46
src/Squidex/app/shared/components/pipes.ts

@ -11,11 +11,57 @@ import { map } from 'rxjs/operators';
import { import {
ApiUrlConfig, ApiUrlConfig,
formatHistoryMessage,
HistoryEventDto,
MathHelper, MathHelper,
UserDto, UserDto,
UsersProviderService UsersProviderService
} from '@app/shared/internal'; } from '@app/shared/internal';
@Pipe({
name: 'sqxHistoryMessage',
pure: false
})
export class HistoryMessagePipe implements OnDestroy, PipeTransform {
private subscription: Subscription;
private lastMessage: string;
private lastValue: string | null = null;
constructor(
private readonly changeDetector: ChangeDetectorRef,
private readonly users: UsersProviderService
) {
}
public ngOnDestroy() {
if (this.subscription) {
this.subscription.unsubscribe();
}
}
public transform(event: HistoryEventDto): string | null {
if (!event) {
return this.lastValue;
}
if (this.lastMessage !== event.message) {
this.lastMessage = event.message;
if (this.subscription) {
this.subscription.unsubscribe();
}
this.subscription = formatHistoryMessage(event.message, this.users).subscribe(value => {
this.lastValue = value;
this.changeDetector.markForCheck();
});
}
return this.lastValue;
}
}
class UserAsyncPipe implements OnDestroy { class UserAsyncPipe implements OnDestroy {
private lastUserId: string; private lastUserId: string;
private lastValue: string | null = null; private lastValue: string | null = null;

7
src/Squidex/app/shared/components/rich-editor.component.ts

@ -5,7 +5,7 @@
* Copyright (c) Squidex UG (haftungsbeschränkt). All rights reserved. * Copyright (c) Squidex UG (haftungsbeschränkt). All rights reserved.
*/ */
import { AfterViewInit, Component, ElementRef, EventEmitter, forwardRef, OnDestroy, Output, ViewChild } from '@angular/core'; import { AfterViewInit, ChangeDetectionStrategy, Component, ElementRef, EventEmitter, forwardRef, OnDestroy, Output, ViewChild } from '@angular/core';
import { ControlValueAccessor, NG_VALUE_ACCESSOR } from '@angular/forms'; import { ControlValueAccessor, NG_VALUE_ACCESSOR } from '@angular/forms';
import { import {
@ -25,7 +25,8 @@ export const SQX_RICH_EDITOR_CONTROL_VALUE_ACCESSOR: any = {
selector: 'sqx-rich-editor', selector: 'sqx-rich-editor',
styleUrls: ['./rich-editor.component.scss'], styleUrls: ['./rich-editor.component.scss'],
templateUrl: './rich-editor.component.html', templateUrl: './rich-editor.component.html',
providers: [SQX_RICH_EDITOR_CONTROL_VALUE_ACCESSOR] providers: [SQX_RICH_EDITOR_CONTROL_VALUE_ACCESSOR],
changeDetection: ChangeDetectionStrategy.OnPush
}) })
export class RichEditorComponent implements ControlValueAccessor, AfterViewInit, OnDestroy { export class RichEditorComponent implements ControlValueAccessor, AfterViewInit, OnDestroy {
private callChange = (v: any) => { /* NOOP */ }; private callChange = (v: any) => { /* NOOP */ };
@ -105,7 +106,7 @@ export class RichEditorComponent implements ControlValueAccessor, AfterViewInit,
self.tinyInitTimer = self.tinyInitTimer =
setTimeout(() => { setTimeout(() => {
self.tinyEditor.setContent(this.value || ''); self.tinyEditor.setContent(this.value || '');
}, 500); }, 1000);
}, },
target: this.editor.nativeElement target: this.editor.nativeElement

2
src/Squidex/app/shared/components/schema-category.component.html

@ -16,7 +16,7 @@
<ul class="nav nav-panel nav-dark nav-dark-bordered flex-column" *ngIf="isOpen" @fade> <ul class="nav nav-panel nav-dark nav-dark-bordered flex-column" *ngIf="isOpen" @fade>
<li class="nav-item" *ngFor="let schema of schemasFiltered; trackBy: trackBySchema" dnd-draggable [dragEnabled]="!isReadonly" [dragData]="schema"> <li class="nav-item" *ngFor="let schema of schemasFiltered; trackBy: trackBySchema" dnd-draggable [dragEnabled]="!isReadonly" [dragData]="schema">
<a class="nav-link" [routerLink]="[schema.name]" routerLinkActive="active"> <a class="nav-link" [routerLink]="schemaRoute(schema)" routerLinkActive="active">
<div class="row" *ngIf="!isReadonly"> <div class="row" *ngIf="!isReadonly">
<div class="col col-4"> <div class="col col-4">
<span class="schema-name schema-name-accent">{{schema.displayName}}</span> <span class="schema-name schema-name-accent">{{schema.displayName}}</span>

12
src/Squidex/app/shared/components/schema-category.component.ts

@ -5,7 +5,7 @@
* Copyright (c) Squidex UG (haftungsbeschränkt). All rights reserved. * Copyright (c) Squidex UG (haftungsbeschränkt). All rights reserved.
*/ */
import { Component, EventEmitter, Input, OnChanges, OnInit, Output, SimpleChanges } from '@angular/core'; import { ChangeDetectionStrategy, Component, EventEmitter, Input, OnChanges, OnInit, Output, SimpleChanges } from '@angular/core';
import { onErrorResumeNext } from 'rxjs/operators'; import { onErrorResumeNext } from 'rxjs/operators';
import { import {
@ -24,7 +24,8 @@ import {
templateUrl: './schema-category.component.html', templateUrl: './schema-category.component.html',
animations: [ animations: [
fadeAnimation fadeAnimation
] ],
changeDetection: ChangeDetectionStrategy.OnPush
}) })
export class SchemaCategoryComponent implements OnInit, OnChanges { export class SchemaCategoryComponent implements OnInit, OnChanges {
@Output() @Output()
@ -36,6 +37,9 @@ export class SchemaCategoryComponent implements OnInit, OnChanges {
@Input() @Input()
public isReadonly: boolean; public isReadonly: boolean;
@Input()
public routeSingletonToContent = false;
@Input() @Input()
public schemasFilter: string; public schemasFilter: string;
@ -100,6 +104,10 @@ export class SchemaCategoryComponent implements OnInit, OnChanges {
this.schemasState.changeCategory(schema, this.name).pipe(onErrorResumeNext()).subscribe(); this.schemasState.changeCategory(schema, this.name).pipe(onErrorResumeNext()).subscribe();
} }
public schemaRoute(schema: SchemaDto) {
return schema.isSingleton && this.routeSingletonToContent ? [schema.name, schema.id] : [schema.name];
}
public trackBySchema(index: number, schema: SchemaDto) { public trackBySchema(index: number, schema: SchemaDto) {
return schema.id; return schema.id;
} }

4
src/Squidex/app/shared/guards/content-must-exist.guard.spec.ts

@ -7,7 +7,7 @@
import { Router } from '@angular/router'; import { Router } from '@angular/router';
import { of } from 'rxjs'; import { of } from 'rxjs';
import { IMock, Mock, Times } from 'typemoq'; import { IMock, It, Mock, Times } from 'typemoq';
import { ContentDto } from './../services/contents.service'; import { ContentDto } from './../services/contents.service';
import { ContentsState } from './../state/contents.state'; import { ContentsState } from './../state/contents.state';
@ -42,7 +42,7 @@ describe('ContentMustExistGuard', () => {
expect(result!).toBeTruthy(); expect(result!).toBeTruthy();
contentsState.verify(x => x.select('123'), Times.once()); router.verify(x => x.navigate(It.isAny()), Times.never());
}); });
it('should load content and return false when not found', () => { it('should load content and return false when not found', () => {

2
src/Squidex/app/shared/guards/content-must-exist.guard.ts

@ -32,7 +32,7 @@ export class ContentMustExistGuard implements CanActivate {
this.router.navigate(['/404']); this.router.navigate(['/404']);
} }
}), }),
map(u => u !== null)); map(u => !!u));
return result; return result;
} }

4
src/Squidex/app/shared/guards/must-be-authenticated.guard.spec.ts

@ -7,7 +7,7 @@
import { Router } from '@angular/router'; import { Router } from '@angular/router';
import { of } from 'rxjs'; import { of } from 'rxjs';
import { IMock, Mock, Times } from 'typemoq'; import { IMock, It, Mock, Times } from 'typemoq';
import { AuthService } from '@app/shared'; import { AuthService } from '@app/shared';
@ -52,5 +52,7 @@ describe('MustBeAuthenticatedGuard', () => {
}); });
expect(result!).toBeTruthy(); expect(result!).toBeTruthy();
router.verify(x => x.navigate(It.isAny()), Times.never());
}); });
}); });

4
src/Squidex/app/shared/guards/must-be-not-authenticated.guard.spec.ts

@ -7,7 +7,7 @@
import { Router } from '@angular/router'; import { Router } from '@angular/router';
import { of } from 'rxjs'; import { of } from 'rxjs';
import { IMock, Mock, Times } from 'typemoq'; import { IMock, It, Mock, Times } from 'typemoq';
import { AuthService } from '@app/shared'; import { AuthService } from '@app/shared';
@ -52,5 +52,7 @@ describe('MustNotBeAuthenticatedGuard', () => {
}); });
expect(result!).toBeTruthy(); expect(result!).toBeTruthy();
router.verify(x => x.navigate(It.isAny()), Times.never());
}); });
}); });

41
src/Squidex/app/shared/guards/schema-must-exist-published.guard.spec.ts

@ -5,9 +5,9 @@
* Copyright (c) Squidex UG (haftungsbeschränkt). All rights reserved. * Copyright (c) Squidex UG (haftungsbeschränkt). All rights reserved.
*/ */
import { Router, RouterStateSnapshot } from '@angular/router'; import { Router } from '@angular/router';
import { of } from 'rxjs'; import { of } from 'rxjs';
import { IMock, Mock, Times } from 'typemoq'; import { IMock, It, Mock, Times } from 'typemoq';
import { SchemaDetailsDto } from './../services/schemas.service'; import { SchemaDetailsDto } from './../services/schemas.service';
import { SchemasState } from './../state/schemas.state'; import { SchemasState } from './../state/schemas.state';
@ -21,7 +21,6 @@ describe('SchemaMustExistPublishedGuard', () => {
}; };
let schemasState: IMock<SchemasState>; let schemasState: IMock<SchemasState>;
let state: RouterStateSnapshot = <any>{ url: 'current-url' };
let router: IMock<Router>; let router: IMock<Router>;
let schemaGuard: SchemaMustExistPublishedGuard; let schemaGuard: SchemaMustExistPublishedGuard;
@ -37,13 +36,13 @@ describe('SchemaMustExistPublishedGuard', () => {
let result: boolean; let result: boolean;
schemaGuard.canActivate(route, state).subscribe(x => { schemaGuard.canActivate(route).subscribe(x => {
result = x; result = x;
}).unsubscribe(); }).unsubscribe();
expect(result!).toBeTruthy(); expect(result!).toBeTruthy();
schemasState.verify(x => x.select('123'), Times.once()); router.verify(x => x.navigate(It.isAny()), Times.never());
}); });
it('should load schema and return false when not found', () => { it('should load schema and return false when not found', () => {
@ -52,7 +51,7 @@ describe('SchemaMustExistPublishedGuard', () => {
let result: boolean; let result: boolean;
schemaGuard.canActivate(route, state).subscribe(x => { schemaGuard.canActivate(route).subscribe(x => {
result = x; result = x;
}).unsubscribe(); }).unsubscribe();
@ -60,34 +59,4 @@ describe('SchemaMustExistPublishedGuard', () => {
router.verify(x => x.navigate(['/404']), Times.once()); router.verify(x => x.navigate(['/404']), Times.once());
}); });
it('should load schema and return false when not found', () => {
schemasState.setup(x => x.select('123'))
.returns(() => of(null));
let result: boolean;
schemaGuard.canActivate(route, state).subscribe(x => {
result = x;
}).unsubscribe();
expect(result!).toBeFalsy();
router.verify(x => x.navigate(['/404']), Times.once());
});
it('should redirect to content when singleton', () => {
schemasState.setup(x => x.select('123'))
.returns(() => of(<SchemaDetailsDto>{ isSingleton: true, id: 'schema-id' }));
let result: boolean;
schemaGuard.canActivate(route, state).subscribe(x => {
result = x;
}).unsubscribe();
expect(result!).toBeFalsy();
router.verify(x => x.navigate([state.url, 'schema-id']), Times.once());
});
}); });

10
src/Squidex/app/shared/guards/schema-must-exist-published.guard.ts

@ -6,7 +6,7 @@
*/ */
import { Injectable } from '@angular/core'; import { Injectable } from '@angular/core';
import { ActivatedRouteSnapshot, CanActivate, Router, RouterStateSnapshot } from '@angular/router'; import { ActivatedRouteSnapshot, CanActivate, Router } from '@angular/router';
import { Observable } from 'rxjs'; import { Observable } from 'rxjs';
import { map, tap } from 'rxjs/operators'; import { map, tap } from 'rxjs/operators';
@ -22,7 +22,7 @@ export class SchemaMustExistPublishedGuard implements CanActivate {
) { ) {
} }
public canActivate(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable<boolean> { public canActivate(route: ActivatedRouteSnapshot): Observable<boolean> {
const schemaName = allParams(route)['schemaName']; const schemaName = allParams(route)['schemaName'];
const result = const result =
@ -31,12 +31,8 @@ export class SchemaMustExistPublishedGuard implements CanActivate {
if (!dto || !dto.isPublished) { if (!dto || !dto.isPublished) {
this.router.navigate(['/404']); this.router.navigate(['/404']);
} }
if (dto && dto.isSingleton && state.url.indexOf(dto.id) < 0) {
this.router.navigate([state.url, dto.id]);
}
}), }),
map(s => s !== null && s.isPublished)); map(s => !!s && s.isPublished));
return result; return result;
} }

2
src/Squidex/app/shared/guards/schema-must-exist.guard.spec.ts

@ -41,8 +41,6 @@ describe('SchemaMustExistGuard', () => {
}).unsubscribe(); }).unsubscribe();
expect(result!).toBeTruthy(); expect(result!).toBeTruthy();
schemasState.verify(x => x.select('123'), Times.once());
}); });
it('should load schema and return false when not found', () => { it('should load schema and return false when not found', () => {

2
src/Squidex/app/shared/guards/schema-must-exist.guard.ts

@ -32,7 +32,7 @@ export class SchemaMustExistGuard implements CanActivate {
this.router.navigate(['/404']); this.router.navigate(['/404']);
} }
}), }),
map(s => s !== null)); map(s => !!s));
return result; return result;
} }

105
src/Squidex/app/shared/guards/schema-must-not-be-singleton.guard.spec.ts

@ -0,0 +1,105 @@
/*
* Squidex Headless CMS
*
* @license
* Copyright (c) Squidex UG (haftungsbeschränkt). All rights reserved.
*/
import { Router, RouterStateSnapshot, UrlSegment } from '@angular/router';
import { of } from 'rxjs';
import { IMock, It, Mock, Times } from 'typemoq';
import { SchemaDetailsDto } from './../services/schemas.service';
import { SchemasState } from './../state/schemas.state';
import { SchemaMustNotBeSingletonGuard } from './schema-must-not-be-singleton.guard';
describe('SchemaMustNotBeSingletonGuard', () => {
const route: any = {
params: {
schemaName: '123'
},
url: [
new UrlSegment('schemas', {}),
new UrlSegment('name', {}),
new UrlSegment('new', {})
]
};
let schemasState: IMock<SchemasState>;
let router: IMock<Router>;
let schemaGuard: SchemaMustNotBeSingletonGuard;
beforeEach(() => {
router = Mock.ofType<Router>();
schemasState = Mock.ofType<SchemasState>();
schemaGuard = new SchemaMustNotBeSingletonGuard(schemasState.object, router.object);
});
it('should subscribe to schema and return true when not singleton', () => {
const state: RouterStateSnapshot = <any>{ url: 'schemas/name/' };
schemasState.setup(x => x.selectedSchema)
.returns(() => of(<SchemaDetailsDto>{ id: '123', isSingleton: false }));
let result: boolean;
schemaGuard.canActivate(route, state).subscribe(x => {
result = x;
}).unsubscribe();
expect(result!).toBeTruthy();
router.verify(x => x.navigate(It.isAny()), Times.never());
});
it('should subscribe to schema and return false when not found', () => {
const state: RouterStateSnapshot = <any>{ url: 'schemas/name/' };
schemasState.setup(x => x.selectedSchema)
.returns(() => of(null));
let result: boolean;
schemaGuard.canActivate(route, state).subscribe(x => {
result = x;
}).unsubscribe();
expect(result!).toBeFalsy();
router.verify(x => x.navigate(It.isAny()), Times.never());
});
it('should redirect to content when singleton', () => {
const state: RouterStateSnapshot = <any>{ url: 'schemas/name/' };
schemasState.setup(x => x.selectedSchema)
.returns(() => of(<SchemaDetailsDto>{ id: '123', isSingleton: true }));
let result: boolean;
schemaGuard.canActivate(route, state).subscribe(x => {
result = x;
}).unsubscribe();
expect(result!).toBeFalsy();
router.verify(x => x.navigate([state.url, '123']), Times.once());
});
it('should redirect to content when singleton on new page', () => {
const state: RouterStateSnapshot = <any>{ url: 'schemas/name/new/' };
schemasState.setup(x => x.selectedSchema)
.returns(() => of(<SchemaDetailsDto>{ id: '123', isSingleton: true }));
let result: boolean;
schemaGuard.canActivate(route, state).subscribe(x => {
result = x;
}).unsubscribe();
expect(result!).toBeFalsy();
router.verify(x => x.navigate(['schemas/name/', '123']), Times.once());
});
});

42
src/Squidex/app/shared/guards/schema-must-not-be-singleton.guard.ts

@ -0,0 +1,42 @@
/*
* Squidex Headless CMS
*
* @license
* Copyright (c) Squidex UG (haftungsbeschränkt). All rights reserved.
*/
import { Injectable } from '@angular/core';
import { ActivatedRouteSnapshot, CanActivate, Router, RouterStateSnapshot } from '@angular/router';
import { Observable } from 'rxjs';
import { map, take, tap } from 'rxjs/operators';
import { SchemasState } from './../state/schemas.state';
@Injectable()
export class SchemaMustNotBeSingletonGuard implements CanActivate {
constructor(
private readonly schemasState: SchemasState,
private readonly router: Router
) {
}
public canActivate(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable<boolean> {
const result =
this.schemasState.selectedSchema.pipe(
take(1),
tap(dto => {
if (dto && dto.isSingleton) {
if (state.url.indexOf('/new') >= 0) {
const parentUrl = state.url.slice(0, state.url.indexOf(route.url[route.url.length - 1].path));
this.router.navigate([parentUrl, dto.id]);
} else {
this.router.navigate([state.url, dto.id]);
}
}
}),
map(s => !!s && !s.isSingleton));
return result;
}
}

1
src/Squidex/app/shared/internal.ts

@ -13,6 +13,7 @@ export * from './guards/must-be-authenticated.guard';
export * from './guards/must-be-not-authenticated.guard'; export * from './guards/must-be-not-authenticated.guard';
export * from './guards/schema-must-exist-published.guard'; export * from './guards/schema-must-exist-published.guard';
export * from './guards/schema-must-exist.guard'; export * from './guards/schema-must-exist.guard';
export * from './guards/schema-must-not-be-singleton.guard';
export * from './guards/unset-app.guard'; export * from './guards/unset-app.guard';
export * from './guards/unset-content.guard'; export * from './guards/unset-content.guard';

5
src/Squidex/app/shared/module.ts

@ -45,6 +45,7 @@ import {
HelpService, HelpService,
HistoryComponent, HistoryComponent,
HistoryListComponent, HistoryListComponent,
HistoryMessagePipe,
HistoryService, HistoryService,
LanguageSelectorComponent, LanguageSelectorComponent,
LanguagesService, LanguagesService,
@ -64,6 +65,7 @@ import {
SchemaCategoryComponent, SchemaCategoryComponent,
SchemaMustExistGuard, SchemaMustExistGuard,
SchemaMustExistPublishedGuard, SchemaMustExistPublishedGuard,
SchemaMustNotBeSingletonGuard,
SchemasService, SchemasService,
SchemasState, SchemasState,
SearchFormComponent, SearchFormComponent,
@ -100,6 +102,7 @@ import {
HelpComponent, HelpComponent,
HistoryComponent, HistoryComponent,
HistoryListComponent, HistoryListComponent,
HistoryMessagePipe,
LanguageSelectorComponent, LanguageSelectorComponent,
MarkdownEditorComponent, MarkdownEditorComponent,
SchemaCategoryComponent, SchemaCategoryComponent,
@ -124,6 +127,7 @@ import {
HelpComponent, HelpComponent,
HistoryComponent, HistoryComponent,
HistoryListComponent, HistoryListComponent,
HistoryMessagePipe,
LanguageSelectorComponent, LanguageSelectorComponent,
MarkdownEditorComponent, MarkdownEditorComponent,
RouterModule, RouterModule,
@ -180,6 +184,7 @@ export class SqxSharedModule {
RulesState, RulesState,
SchemaMustExistGuard, SchemaMustExistGuard,
SchemaMustExistPublishedGuard, SchemaMustExistPublishedGuard,
SchemaMustNotBeSingletonGuard,
SchemasService, SchemasService,
SchemasState, SchemasState,
UIService, UIService,

2
src/Squidex/app/shared/state/contents.forms.ts

@ -379,7 +379,7 @@ export class EditContentForm extends Form<FormGroup> {
private addArrayItem(field: RootFieldDto, language: AppLanguageDto | null, partitionForm: FormArray) { private addArrayItem(field: RootFieldDto, language: AppLanguageDto | null, partitionForm: FormArray) {
const itemForm = new FormGroup({}); const itemForm = new FormGroup({});
let isOptional = field.isLocalizable && language !== null && language.isOptional; let isOptional = field.isLocalizable && !!language && language.isOptional;
for (let nested of field.nested) { for (let nested of field.nested) {
const nestedValidators = FieldValidatorsFactory.createValidators(nested, isOptional); const nestedValidators = FieldValidatorsFactory.createValidators(nested, isOptional);

2
src/Squidex/app/shared/state/contents.state.ts

@ -273,7 +273,7 @@ export abstract class ContentsStateBase extends State<Snapshot> {
} }
public init(): Observable<any> { public init(): Observable<any> {
this.next(s => ({ ...s, contentsPager: new Pager(0), contentsQuery: '', isArchive: false, isLoaded: false })); this.next(s => ({ contents: ImmutableArray.of(), contentsPager: new Pager(0) }));
return this.loadInternal(); return this.loadInternal();
} }

4
src/Squidex/app/shared/state/queries.spec.ts

@ -68,12 +68,16 @@ describe('Queries', () => {
it('should forward add call to state', () => { it('should forward add call to state', () => {
queries.add('key3', 'filter3'); queries.add('key3', 'filter3');
expect(true).toBeTruthy();
uiState.verify(x => x.set('schemas.my-schema.queries.key3', 'filter3'), Times.once()); uiState.verify(x => x.set('schemas.my-schema.queries.key3', 'filter3'), Times.once());
}); });
it('should forward remove call to state', () => { it('should forward remove call to state', () => {
queries.remove('key3'); queries.remove('key3');
expect(true).toBeTruthy();
uiState.verify(x => x.remove('schemas.my-schema.queries.key3'), Times.once()); uiState.verify(x => x.remove('schemas.my-schema.queries.key3'), Times.once());
}); });
}); });

5
src/Squidex/app/shell/pages/app/left-menu.component.ts

@ -5,14 +5,15 @@
* Copyright (c) Squidex UG (haftungsbeschränkt). All rights reserved. * Copyright (c) Squidex UG (haftungsbeschränkt). All rights reserved.
*/ */
import { Component } from '@angular/core'; import { ChangeDetectionStrategy, Component } from '@angular/core';
import { AppsState } from '@app/shared'; import { AppsState } from '@app/shared';
@Component({ @Component({
selector: 'sqx-left-menu', selector: 'sqx-left-menu',
styleUrls: ['./left-menu.component.scss'], styleUrls: ['./left-menu.component.scss'],
templateUrl: './left-menu.component.html' templateUrl: './left-menu.component.html',
changeDetection: ChangeDetectionStrategy.OnPush
}) })
export class LeftMenuComponent { export class LeftMenuComponent {
constructor(public readonly appsState: AppsState constructor(public readonly appsState: AppsState

5
src/Squidex/app/shell/pages/internal/apps-menu.component.ts

@ -5,7 +5,7 @@
* Copyright (c) Squidex UG (haftungsbeschränkt). All rights reserved. * Copyright (c) Squidex UG (haftungsbeschränkt). All rights reserved.
*/ */
import { Component } from '@angular/core'; import { ChangeDetectionStrategy, Component } from '@angular/core';
import { import {
AppsState, AppsState,
@ -20,7 +20,8 @@ import {
templateUrl: './apps-menu.component.html', templateUrl: './apps-menu.component.html',
animations: [ animations: [
fadeAnimation fadeAnimation
] ],
changeDetection: ChangeDetectionStrategy.OnPush
}) })
export class AppsMenuComponent { export class AppsMenuComponent {
public addAppDialog = new DialogModel(); public addAppDialog = new DialogModel();

8
src/Squidex/app/shell/pages/internal/internal-area.component.html

@ -1,6 +1,12 @@
<nav class="navbar fixed-top navbar-dark bg-primary bg-faded"> <nav class="navbar fixed-top navbar-dark bg-primary bg-faded">
<span class="navbar-brand" routerLink="/app"> <span class="navbar-brand" routerLink="/app">
<i class="icon-logo"></i> <ng-container *ngIf="loadingService.loading | async; else noLoading">
<img class="loader" src="/images/loader-white.gif" @fade />
</ng-container>
<ng-template #noLoading>
<i class="icon-logo" @fade></i>
</ng-template>
</span> </span>
<div class="float-left apps-menu"> <div class="float-left apps-menu">

4
src/Squidex/app/shell/pages/internal/internal-area.component.scss

@ -71,4 +71,8 @@ $support-text-size: 200px;
font-weight: normal; font-weight: normal;
width: $support-icon-size; width: $support-icon-size;
} }
}
.loader {
@include absolute(12px, auto, auto, 40px);
} }

8
src/Squidex/app/shell/pages/internal/internal-area.component.ts

@ -11,13 +11,18 @@ import { Subscription } from 'rxjs';
import { import {
DialogService, DialogService,
fadeAnimation,
LoadingService,
Notification Notification
} from '@app/shared'; } from '@app/shared';
@Component({ @Component({
selector: 'sqx-internal-area', selector: 'sqx-internal-area',
styleUrls: ['./internal-area.component.scss'], styleUrls: ['./internal-area.component.scss'],
templateUrl: './internal-area.component.html' templateUrl: './internal-area.component.html',
animations: [
fadeAnimation
]
}) })
export class InternalAreaComponent implements OnDestroy, OnInit { export class InternalAreaComponent implements OnDestroy, OnInit {
private queryParamsSubscription: Subscription; private queryParamsSubscription: Subscription;
@ -25,6 +30,7 @@ export class InternalAreaComponent implements OnDestroy, OnInit {
public notifications: Notification[] = []; public notifications: Notification[] = [];
constructor( constructor(
public readonly loadingService: LoadingService,
private readonly dialogs: DialogService, private readonly dialogs: DialogService,
private readonly route: ActivatedRoute private readonly route: ActivatedRoute
) { ) {

10
src/Squidex/app/shell/pages/internal/profile-menu.component.ts

@ -5,7 +5,7 @@
* Copyright (c) Squidex UG (haftungsbeschränkt). All rights reserved. * Copyright (c) Squidex UG (haftungsbeschränkt). All rights reserved.
*/ */
import { Component, OnDestroy, OnInit } from '@angular/core'; import { ChangeDetectionStrategy, ChangeDetectorRef, Component, OnDestroy, OnInit } from '@angular/core';
import { Subscription } from 'rxjs'; import { Subscription } from 'rxjs';
import { filter } from 'rxjs/operators'; import { filter } from 'rxjs/operators';
@ -22,7 +22,8 @@ import {
templateUrl: './profile-menu.component.html', templateUrl: './profile-menu.component.html',
animations: [ animations: [
fadeAnimation fadeAnimation
] ],
changeDetection: ChangeDetectionStrategy.OnPush
}) })
export class ProfileMenuComponent implements OnDestroy, OnInit { export class ProfileMenuComponent implements OnDestroy, OnInit {
private authenticationSubscription: Subscription; private authenticationSubscription: Subscription;
@ -38,7 +39,8 @@ export class ProfileMenuComponent implements OnDestroy, OnInit {
constructor( constructor(
private readonly authService: AuthService, private readonly authService: AuthService,
private readonly apiUrl: ApiUrlConfig private readonly apiUrl: ApiUrlConfig,
private readonly changeDetector: ChangeDetectorRef
) { ) {
} }
@ -54,6 +56,8 @@ export class ProfileMenuComponent implements OnDestroy, OnInit {
this.profileDisplayName = user!.displayName; this.profileDisplayName = user!.displayName;
this.isAdmin = user!.isAdmin; this.isAdmin = user!.isAdmin;
this.changeDetector.detectChanges();
}); });
} }

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

@ -163,6 +163,20 @@ body {
@include animation(spin 3s infinite linear); @include animation(spin 3s infinite linear);
} }
.spin2 {
@include animation(spin2 5s infinite linear);
}
@include keyframes (spin2) {
50% {
@include rotate(180deg);
}
100% {
@include rotate(360deg);
}
}
@include keyframes (spin) { @include keyframes (spin) {
20% { 20% {
@include rotate(0deg); @include rotate(0deg);

4
src/Squidex/app/theme/icomoon/demo-files/demo.css

@ -147,10 +147,10 @@ p {
font-size: 16px; font-size: 16px;
} }
.fs1 { .fs1 {
font-size: 20px; font-size: 32px;
} }
.fs2 { .fs2 {
font-size: 32px; font-size: 20px;
} }
.fs3 { .fs3 {
font-size: 32px; font-size: 32px;

776
src/Squidex/app/theme/icomoon/demo.html

File diff suppressed because it is too large

Some files were not shown because too many files changed in this diff

Loading…
Cancel
Save