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" />
</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="Elasticsearch.Net" Version="6.3.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>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Fody" Version="3.2.4">
<PackageReference Include="Fody" Version="3.2.9">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers</IncludeAssets>
</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.ComponentModel.Annotations" 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="Microsoft.OData.Core" Version="7.5.1" />
<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="StyleCop.Analyzers" Version="1.0.2" PrivateAssets="all" />
<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" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="GraphQL" Version="2.0.0" />
<PackageReference Include="Microsoft.Orleans.CodeGenerator.MSBuild" Version="2.1.0-rc2">
<PackageReference Include="GraphQL" Version="2.1.0" />
<PackageReference Include="Microsoft.Orleans.CodeGenerator.MSBuild" Version="2.1.0">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers</IncludeAssets>
</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="RefactoringEssentials" Version="5.6.0" 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="RefactoringEssentials" Version="5.6.0" 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>
<PropertyGroup>
<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="StyleCop.Analyzers" Version="1.0.2" PrivateAssets="all" />
<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>
<PropertyGroup>
<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.Logging" Version="2.1.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>
<IncludeAssets>runtime; build; native; contentfiles; analyzers</IncludeAssets>
</PackageReference>
<PackageReference Include="Microsoft.Orleans.Core" Version="2.1.0-rc2" />
<PackageReference Include="Microsoft.Orleans.OrleansRuntime" Version="2.1.0-rc2" />
<PackageReference Include="Microsoft.Orleans.Core" Version="2.1.0" />
<PackageReference Include="Microsoft.Orleans.OrleansRuntime" Version="2.1.0" />
<PackageReference Include="Newtonsoft.Json" Version="11.0.2" />
<PackageReference Include="NodaTime" Version="2.4.0" />
<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.ComponentModel.Annotations" Version="4.5.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.Security.Claims" Version="4.3.0" />
<PackageReference Include="System.Threading.Tasks.Dataflow" Version="4.9.0" />

20
src/Squidex/Squidex.csproj

@ -54,36 +54,36 @@
</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="EventStore.ClientAPI.NetCore" Version="4.1.0.23" />
<PackageReference Include="IdentityServer4" Version="2.2.0" />
<PackageReference Include="IdentityServer4.AccessTokenValidation" Version="2.6.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.Google" 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.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.StaticFiles" Version="2.1.1" />
<PackageReference Include="Microsoft.Extensions.DependencyModel" Version="2.1.0" />
<PackageReference Include="Microsoft.Extensions.Options.ConfigurationExtensions" Version="2.1.1" />
<PackageReference Include="Microsoft.Data.Edm" Version="5.8.4" />
<PackageReference Include="Microsoft.OData.Core" Version="7.5.1" />
<PackageReference Include="Microsoft.Orleans.Client" Version="2.1.0-rc2" />
<PackageReference Include="Microsoft.Orleans.Core" Version="2.1.0-rc2" />
<PackageReference Include="Microsoft.Orleans.Core.Abstractions" Version="2.1.0-rc2" />
<PackageReference Include="Microsoft.Orleans.OrleansRuntime" Version="2.1.0-rc2" />
<PackageReference Include="Microsoft.Orleans.Client" Version="2.1.0" />
<PackageReference Include="Microsoft.Orleans.Core" Version="2.1.0" />
<PackageReference Include="Microsoft.Orleans.Core.Abstractions" Version="2.1.0" />
<PackageReference Include="Microsoft.Orleans.OrleansRuntime" Version="2.1.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="NSwag.AspNetCore" Version="11.19.2" />
<PackageReference Include="NSwag.AspNetCore" Version="11.20.1" />
<PackageReference Include="OpenCover" Version="4.6.519" />
<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="ReportGenerator" Version="3.1.2" />
<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']);
}
}),
map(u => u !== null));
map(u => !!u));
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.timerSubscription =
timer(2000, 2000).pipe(
switchMap(x => this.eventConsumersState.load(true, true)), onErrorResumeNext())
timer(2000, 2000).pipe(switchMap(x => this.eventConsumersState.load(true, true)), onErrorResumeNext())
.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 { FormBuilder } from '@angular/forms';
import { Subscription, timer } from 'rxjs';
import { switchMap } from 'rxjs/operators';
import { filter, switchMap } from 'rxjs/operators';
import {
AuthService,
@ -43,11 +43,9 @@ export class RestorePageComponent implements OnDestroy, OnInit {
public ngOnInit() {
this.timerSubscription =
timer(0, 2000).pipe(switchMap(() => this.backupsService.getRestore()))
timer(0, 2000).pipe(switchMap(() => this.backupsService.getRestore()), filter(x => !!x))
.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,
LoadLanguagesGuard,
SchemaMustExistPublishedGuard,
SchemaMustNotBeSingletonGuard,
SqxFrameworkModule,
SqxSharedModule,
UnsetContentGuard
@ -52,12 +53,13 @@ const routes: Routes = [
{
path: '',
component: ContentsPageComponent,
canActivate: [SchemaMustNotBeSingletonGuard],
canDeactivate: [CanDeactivateGuard]
},
{
path: 'new',
component: ContentPageComponent,
canActivate: [UnsetContentGuard],
canActivate: [SchemaMustNotBeSingletonGuard, UnsetContentGuard],
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">
<div class="languages-buttons" #buttonLanguages>
<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.
*/
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 { Observable } from 'rxjs';
import {
AppLanguageDto,
@ -16,11 +17,13 @@ import {
RootFieldDto,
Types
} from '@app/shared';
import { map, startWith } from 'rxjs/operators';
@Component({
selector: 'sqx-content-field',
styleUrls: ['./content-field.component.scss'],
templateUrl: './content-field.component.html'
templateUrl: './content-field.component.html',
changeDetection: ChangeDetectionStrategy.OnPush
})
export class ContentFieldComponent implements OnChanges {
@Input()
@ -43,6 +46,8 @@ export class ContentFieldComponent implements OnChanges {
public selectedFormControl: AbstractControl;
public isInvalid: Observable<boolean>;
public ngOnChanges(changes: SimpleChanges) {
if (this.field.isLocalizable) {
this.selectedFormControl = this.fieldForm.controls[this.language.iso2Code];
@ -55,6 +60,10 @@ export class ContentFieldComponent implements OnChanges {
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 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">
<img class="user-picture" [attr.title]="event.actor | sqxUserNameRef:null" [attr.src]="event.actor | sqxUserPictureRef" />
</div>
<div class="col pl-2">
<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 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 {
allParams,
AppsState,
formatHistoryMessage,
HistoryChannelUpdated,
HistoryEventDto,
HistoryService,
MessageBus,
UsersProviderService,
Version
} from '@app/shared';
@ -59,8 +57,7 @@ export class ContentHistoryComponent {
private readonly appsState: AppsState,
private readonly historyService: HistoryService,
private readonly messageBus: MessageBus,
private readonly route: ActivatedRoute,
private readonly users: UsersProviderService
private readonly route: ActivatedRoute
) {
}
@ -68,7 +65,7 @@ export class ContentHistoryComponent {
this.messageBus.emit(new ContentVersionSelected(new Version(version.toString())));
}
public format(message: string): Observable<string> {
return formatHistoryMessage(message, this.users);
public trackByEvent(index: number, event: HistoryEventDto) {
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 { ActivatedRoute, Router } from '@angular/router';
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';
@ -88,19 +88,19 @@ export class ContentPageComponent implements CanComponentDeactivate, OnDestroy,
});
this.selectedSchemaSubscription =
this.schemasState.selectedSchema.pipe(filter(s => !!s), map(s => s!))
this.schemasState.selectedSchema.pipe(filter(s => !!s))
.subscribe(schema => {
this.schema = schema;
this.schema = schema!;
this.contentForm = new EditContentForm(this.schema, this.languages);
});
this.contentSubscription =
this.contentsState.selectedContent.pipe(filter(c => !!c), map(c => c!))
this.contentsState.selectedContent.pipe(filter(c => !!c))
.subscribe(content => {
this.content = content;
this.content = content!;
this.loadContent(content.dataDraft);
this.loadContent(this.content.dataDraft);
});
this.contentVersionSelectedSubscription =

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

@ -73,19 +73,19 @@
<div class="selection" *ngIf="selectionCount > 0">
{{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
</button>
<button class="btn btn-secondary" (click)="unpublishSelected()" *ngIf="canUnpublish">
<button class="btn btn-secondary mr-1" (click)="unpublishSelected()" *ngIf="canUnpublish">
Unpublish
</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
</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
</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 { Router } from '@angular/router';
import { Subscription } from 'rxjs';
import { onErrorResumeNext, switchMap, tap } from 'rxjs/operators';
import { filter, onErrorResumeNext, switchMap, takeUntil, tap } from 'rxjs/operators';
import {
AppLanguageDto,
@ -17,6 +18,7 @@ import {
ImmutableArray,
LanguagesState,
ModalModel,
navigatedToOtherComponent,
Queries,
SchemaDetailsDto,
SchemasState,
@ -59,6 +61,7 @@ export class ContentsPageComponent implements OnDestroy, OnInit {
public readonly contentsState: ContentsState,
private readonly languagesState: LanguagesState,
private readonly schemasState: SchemasState,
private readonly router: Router,
private readonly uiState: UIState
) {
}
@ -70,8 +73,10 @@ export class ContentsPageComponent implements OnDestroy, OnInit {
}
public ngOnInit() {
const routeChanged = this.router.events.pipe(filter(navigatedToOtherComponent(this.router)));
this.selectedSchemaSubscription =
this.schemasState.selectedSchema
this.schemasState.selectedSchema.pipe(takeUntil(routeChanged))
.subscribe(schema => {
this.resetSelection();
@ -82,7 +87,7 @@ export class ContentsPageComponent implements OnDestroy, OnInit {
});
this.contentsSubscription =
this.contentsState.contents
this.contentsState.contents.pipe(takeUntil(routeChanged))
.subscribe(() => {
this.updateSelectionSummary();
});
@ -100,7 +105,7 @@ export class ContentsPageComponent implements OnDestroy, OnInit {
}
public deleteSelected() {
this.contentsState.deleteMany(this.select()).pipe(onErrorResumeNext()).subscribe();
this.contentsState.deleteMany(this.selectItems()).pipe(onErrorResumeNext()).subscribe();
}
public delete(content: ContentDto) {
@ -112,7 +117,7 @@ export class ContentsPageComponent implements OnDestroy, OnInit {
}
public publishSelected() {
this.changeContentItems(this.select(c => c.status !== 'Published'), 'Publish');
this.changeContentItems(this.selectItems(c => c.status !== 'Published'), 'Publish');
}
public unpublish(content: ContentDto) {
@ -120,7 +125,7 @@ export class ContentsPageComponent implements OnDestroy, OnInit {
}
public unpublishSelected() {
this.changeContentItems(this.select(c => c.status === 'Published'), 'Unpublish');
this.changeContentItems(this.selectItems(c => c.status === 'Published'), 'Unpublish');
}
public archive(content: ContentDto) {
@ -128,15 +133,15 @@ export class ContentsPageComponent implements OnDestroy, OnInit {
}
public archiveSelected() {
this.changeContentItems(this.select(), 'Archive');
this.changeContentItems(this.selectItems(), 'Archive');
}
public restore(content: ContentDto) {
this.changeContentItems([content], 'Restore');
}
public restoreSelected(scheduled: boolean) {
this.changeContentItems(this.select(), 'Restore');
public restoreSelected() {
this.changeContentItems(this.selectItems(), 'Restore');
}
public clone(content: ContentDto) {
@ -144,7 +149,7 @@ export class ContentsPageComponent implements OnDestroy, OnInit {
}
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) {
@ -185,12 +190,16 @@ export class ContentsPageComponent implements OnDestroy, OnInit {
this.contentsState.search(query).pipe(onErrorResumeNext()).subscribe();
}
public selectLanguage(language: AppLanguageDto) {
this.language = language;
}
public isItemSelected(content: ContentDto): boolean {
return !!this.selectedItems[content.id];
}
public selectLanguage(language: AppLanguageDto) {
this.language = language;
private selectItems(predicate?: (content: ContentDto) => boolean) {
return this.contentsState.snapshot.contents.values.filter(c => this.selectedItems[c.id] && (!predicate || predicate(c)));
}
public selectItem(content: ContentDto, isSelected: boolean) {
@ -199,6 +208,12 @@ export class ContentsPageComponent implements OnDestroy, OnInit {
this.updateSelectionSummary();
}
private resetSelection() {
this.selectedItems = {};
this.updateSelectionSummary();
}
public selectAll(isSelected: boolean) {
this.selectedItems = {};
@ -215,16 +230,6 @@ export class ContentsPageComponent implements OnDestroy, OnInit {
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() {
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"
[schemas]="schemas"
[schemasFilter]="schemasFilter.valueChanges | async"
[routeSingletonToContent]="true"
[isReadonly]="true">
</sqx-schema-category>
</ng-container>

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

@ -7,7 +7,7 @@
// 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 {
@ -28,9 +28,8 @@ export const SQX_ASSETS_EDITOR_CONTROL_VALUE_ACCESSOR: any = {
selector: 'sqx-assets-editor',
styleUrls: ['./assets-editor.component.scss'],
templateUrl: './assets-editor.component.html',
providers: [
SQX_ASSETS_EDITOR_CONTROL_VALUE_ACCESSOR
]
providers: [SQX_ASSETS_EDITOR_CONTROL_VALUE_ACCESSOR],
changeDetection: ChangeDetectionStrategy.OnPush
})
export class AssetsEditorComponent implements ControlValueAccessor {
private callChange = (v: any) => { /* NOOP */ };
@ -47,6 +46,7 @@ export class AssetsEditorComponent implements ControlValueAccessor {
constructor(
private readonly appsState: AppsState,
private readonly assetsService: AssetsService,
private readonly changeDetector: ChangeDetectorRef,
private readonly localStore: LocalStoreService
) {
this.isListView = this.localStore.get('assetView') === 'List';
@ -131,6 +131,8 @@ export class AssetsEditorComponent implements ControlValueAccessor {
this.callTouched();
this.callChange(ids);
this.changeDetector.detectChanges();
}
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.
*/
import { Component, EventEmitter, Input, OnChanges, Output, SimpleChanges } from '@angular/core';
import { ChangeDetectionStrategy, Component, EventEmitter, Input, OnChanges, Output, SimpleChanges } from '@angular/core';
import {
AppLanguageDto,
@ -29,7 +29,8 @@ import {
templateUrl: './content-item.component.html',
animations: [
fadeAnimation
]
],
changeDetection: ChangeDetectionStrategy.OnPush
})
export class ContentItemComponent implements OnChanges {
@Output()

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

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

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

@ -7,7 +7,7 @@
// 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 {
@ -31,9 +31,8 @@ export const SQX_REFERENCES_EDITOR_CONTROL_VALUE_ACCESSOR: any = {
selector: 'sqx-references-editor',
styleUrls: ['./references-editor.component.scss'],
templateUrl: './references-editor.component.html',
providers: [
SQX_REFERENCES_EDITOR_CONTROL_VALUE_ACCESSOR
]
providers: [SQX_REFERENCES_EDITOR_CONTROL_VALUE_ACCESSOR],
changeDetection: ChangeDetectionStrategy.OnPush
})
export class ReferencesEditorComponent implements ControlValueAccessor, OnInit {
private callChange = (v: any) => { /* NOOP */ };
@ -59,6 +58,7 @@ export class ReferencesEditorComponent implements ControlValueAccessor, OnInit {
constructor(
private readonly appsState: AppsState,
private readonly changeDetector: ChangeDetectorRef,
private readonly contentsService: ContentsService,
private readonly schemasService: SchemasService
) {
@ -73,8 +73,12 @@ export class ReferencesEditorComponent implements ControlValueAccessor, OnInit {
this.schemasService.getSchema(this.appsState.appName, this.schemaId)
.subscribe(dto => {
this.schema = dto;
}, error => {
this.changeDetector.detectChanges();
}, () => {
this.isInvalidSchema = true;
this.changeDetector.detectChanges();
});
}
@ -90,8 +94,12 @@ export class ReferencesEditorComponent implements ControlValueAccessor, OnInit {
if (this.contentItems.length !== contentIds.length) {
this.updateValue();
}
this.changeDetector.detectChanges();
}, () => {
this.contentItems = ImmutableArray.empty();
this.changeDetector.detectChanges();
});
}
} else {
@ -148,5 +156,7 @@ export class ReferencesEditorComponent implements ControlValueAccessor, OnInit {
this.callTouched();
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 {
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.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 { ActivatedRoute, Router } from '@angular/router';
import { Subscription } from 'rxjs';
import { filter, map, onErrorResumeNext } from 'rxjs/operators';
import { filter, onErrorResumeNext } from 'rxjs/operators';
import {
AppsState,
@ -73,9 +73,9 @@ export class SchemaPageComponent implements OnDestroy, OnInit {
this.patternsState.load().pipe(onErrorResumeNext()).subscribe();
this.selectedSchemaSubscription =
this.schemasState.selectedSchema.pipe(filter(s => !!s), map(s => s!))
this.schemasState.selectedSchema.pipe(filter(s => !!s))
.subscribe(schema => {
this.schema = schema;
this.schema = schema!;
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.
*/
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 { Observable, of, Subscription } from 'rxjs';
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',
styleUrls: ['./autocomplete.component.scss'],
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 {
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.
*/
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 * as moment from 'moment';
import { Subscription } from 'rxjs';
@ -22,7 +22,8 @@ export const SQX_DATE_TIME_EDITOR_CONTROL_VALUE_ACCESSOR: any = {
selector: 'sqx-date-time-editor',
styleUrls: ['./date-time-editor.component.scss'],
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 {
private timeSubscription: Subscription;
@ -52,7 +53,7 @@ export class DateTimeEditorComponent implements ControlValueAccessor, OnDestroy,
}
public get hasValue() {
return this.dateValue !== null;
return !!this.dateValue;
}
@ViewChild('dateInput')
@ -60,6 +61,11 @@ export class DateTimeEditorComponent implements ControlValueAccessor, OnDestroy,
public isDisabled = false;
constructor(
private readonly changeDetector: ChangeDetectorRef
) {
}
public ngOnDestroy() {
this.dateSubscription.unsubscribe();
this.timeSubscription.unsubscribe();
@ -136,6 +142,10 @@ export class DateTimeEditorComponent implements ControlValueAccessor, OnDestroy,
this.updateValue();
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.
*/
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';
const KEY_ENTER = 13;
@ -23,7 +23,8 @@ export const SQX_DROPDOWN_CONTROL_VALUE_ACCESSOR: any = {
selector: 'sqx-dropdown',
styleUrls: ['./dropdown.component.scss'],
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 {
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 { 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';
@ -19,8 +19,8 @@ export const SQX_IFRAME_EDITOR_CONTROL_VALUE_ACCESSOR: any = {
selector: 'sqx-iframe-editor',
styleUrls: ['./iframe-editor.component.scss'],
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 {
private windowMessageListener: Function;
@ -35,7 +35,11 @@ export class IFrameEditorComponent implements ControlValueAccessor, AfterViewIni
public iframe: ElementRef;
@Input()
public url: string;
public set url(value: string) {
this.sanitizedUrl = this.sanitizer.bypassSecurityTrustResourceUrl(value);
}
public sanitizedUrl: SafeResourceUrl;
constructor(
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) {
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.
*/
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 { Subject } from 'rxjs';
import { debounceTime } from 'rxjs/operators';
@ -22,7 +22,8 @@ export const SQX_JSCRIPT_EDITOR_CONTROL_VALUE_ACCESSOR: any = {
selector: 'sqx-jscript-editor',
styleUrls: ['./jscript-editor.component.scss'],
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 {
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.
*/
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 { Subject } from 'rxjs';
import { debounceTime } from 'rxjs/operators';
@ -22,7 +22,8 @@ export const SQX_JSON_EDITOR_CONTROL_VALUE_ACCESSOR: any = {
selector: 'sqx-json-editor',
styleUrls: ['./json-editor.component.scss'],
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 {
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.
*/
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');
@Component({
selector: 'sqx-progress-bar',
template: ''
template: '',
changeDetection: ChangeDetectionStrategy.OnPush
})
export class ProgressBarComponent implements OnChanges, OnInit {
private progressBar: any;
@ -31,6 +32,9 @@ export class ProgressBarComponent implements OnChanges, OnInit {
@Input()
public strokeWidth = 4;
@Input()
public showText = true;
@Input()
public value = 0;
@ -45,7 +49,8 @@ export class ProgressBarComponent implements OnChanges, OnInit {
color: this.color,
trailColor: this.trailColor,
trailWidth: this.trailWidth,
strokeWidth: this.strokeWidth
strokeWidth: this.strokeWidth,
svgStyle: { width: '100%', height: '100%' }
};
this.renderer.setStyle(this.element.nativeElement, 'display', 'block');
@ -70,7 +75,7 @@ export class ProgressBarComponent implements OnChanges, OnInit {
this.progressBar.animate(value / 100);
if (value > 0) {
if (value > 0 && this.showText) {
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.
*/
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 { Types } from '@app/framework/internal';
@ -18,7 +18,8 @@ export const SQX_SLIDER_CONTROL_VALUE_ACCESSOR: any = {
selector: 'sqx-slider',
styleUrls: ['./slider.component.scss'],
templateUrl: './slider.component.html',
providers: [SQX_SLIDER_CONTROL_VALUE_ACCESSOR]
providers: [SQX_SLIDER_CONTROL_VALUE_ACCESSOR],
changeDetection: ChangeDetectionStrategy.OnPush
})
export class SliderComponent implements ControlValueAccessor {
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.
*/
import { Component, forwardRef, Input } from '@angular/core';
import { ChangeDetectionStrategy, Component, forwardRef, Input } from '@angular/core';
import { ControlValueAccessor, NG_VALUE_ACCESSOR } from '@angular/forms';
import { Types } from '@app/framework/internal';
@ -18,7 +18,8 @@ export const SQX_STARS_CONTROL_VALUE_ACCESSOR: any = {
selector: 'sqx-stars',
styleUrls: ['./stars.component.scss'],
templateUrl: './stars.component.html',
providers: [SQX_STARS_CONTROL_VALUE_ACCESSOR]
providers: [SQX_STARS_CONTROL_VALUE_ACCESSOR],
changeDetection: ChangeDetectionStrategy.OnPush
})
export class StarsComponent implements ControlValueAccessor {
private callChange = (v: any) => { /* NOOP */ };
@ -88,7 +89,7 @@ export class StarsComponent implements ControlValueAccessor {
return false;
}
if (this.value !== null) {
if (this.value) {
this.value = null;
this.stars = 0;

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

@ -14,7 +14,6 @@
[formControl]="addInput"
[attr.name]="inputName"
[attr.placeholder]="placeholder"
[disabled]="addInput.disabled"
autocomplete="off"
autocorrect="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.
*/
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 { Subscription } from 'rxjs';
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
};
const CACHED_SIZES: { [key: string]: number } = {};
let CACHED_FONT: string;
@Component({
selector: 'sqx-tag-editor',
styleUrls: ['./tag-editor.component.scss'],
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 {
private subscription: Subscription;
@ -129,6 +134,12 @@ export class TagEditorComponent implements AfterViewInit, ControlValueAccessor,
}
public ngAfterViewInit() {
if (!CACHED_FONT) {
const style = window.getComputedStyle(this.inputElement.nativeElement);
CACHED_FONT = `${style.getPropertyValue('font-size')} ${style.getPropertyValue('font-family')}`;
}
this.resetSize();
}
@ -204,7 +215,9 @@ export class TagEditorComponent implements AfterViewInit, ControlValueAccessor,
}
public resetSize() {
const style = window.getComputedStyle(this.inputElement.nativeElement);
if (!CACHED_FONT) {
return;
}
if (!canvas) {
canvas = document.createElement('canvas');
@ -214,20 +227,31 @@ export class TagEditorComponent implements AfterViewInit, ControlValueAccessor,
const ctx = canvas.getContext('2d');
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 widthPlaceholder = ctx.measureText(this.placeholder).width;
const text = this.inputElement.nativeElement.value;
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');
}
}
setTimeout(() => {
this.formElement.nativeElement.scrollLeft = this.formElement.nativeElement.scrollWidth;
}, 0);
if (this.singleLine) {
setTimeout(() => {
this.formElement.nativeElement.scrollLeft = this.formElement.nativeElement.scrollWidth;
}, 0);
}
}
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.
*/
import { Component, forwardRef } from '@angular/core';
import { ChangeDetectionStrategy, Component, forwardRef } from '@angular/core';
import { ControlValueAccessor, NG_VALUE_ACCESSOR } from '@angular/forms';
import { Types } from '@app/framework/internal';
@ -18,7 +18,8 @@ export const SQX_TOGGLE_CONTROL_VALUE_ACCESSOR: any = {
selector: 'sqx-toggle',
styleUrls: ['./toggle.component.scss'],
templateUrl: './toggle.component.html',
providers: [SQX_TOGGLE_CONTROL_VALUE_ACCESSOR]
providers: [SQX_TOGGLE_CONTROL_VALUE_ACCESSOR],
changeDetection: ChangeDetectionStrategy.OnPush
})
export class ToggleComponent implements ControlValueAccessor {
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';
const LAYOUT_CACHE: { [key: string]: { width: number, height: number } } = {};
@Directive({
selector: '[sqxImageSource]'
})
@ -26,6 +28,9 @@ export class ImageSourceDirective implements OnChanges, OnDestroy, OnInit, After
@Input()
public retryCount = 10;
@Input()
public layoutKey: string;
@Input()
public parent: any = null;
@ -76,7 +81,21 @@ export class ImageSourceDirective implements OnChanges, OnDestroy, OnInit, After
}
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, 'width', this.size.width + 'px');
@ -96,7 +115,7 @@ export class ImageSourceDirective implements OnChanges, OnDestroy, OnInit, After
if (w > 0 && h > 0) {
let source = `${this.imageSource}&width=${w}&height=${h}&mode=Crop`;
if (this.loadQuery !== null) {
if (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
*/
import { Component, Input, OnDestroy, OnInit } from '@angular/core';
import { ChangeDetectionStrategy, ChangeDetectorRef, Component, Input, OnDestroy, OnInit } from '@angular/core';
import { Subscription } from 'rxjs';
import {
@ -22,7 +22,8 @@ import {
templateUrl: './dialog-renderer.component.html',
animations: [
fadeAnimation
]
],
changeDetection: ChangeDetectionStrategy.OnPush
})
export class DialogRendererComponent implements OnDestroy, OnInit {
private dialogSubscription: Subscription;
@ -38,6 +39,7 @@ export class DialogRendererComponent implements OnDestroy, OnInit {
public position = 'bottomright';
constructor(
private readonly changeDetector: ChangeDetectorRef,
private readonly dialogs: DialogService
) {
}
@ -53,6 +55,8 @@ export class DialogRendererComponent implements OnDestroy, OnInit {
this.dialogView.isOpen.subscribe(isOpen => {
if (!isOpen) {
this.cancel();
this.changeDetector.detectChanges();
}
});
@ -65,6 +69,8 @@ export class DialogRendererComponent implements OnDestroy, OnInit {
this.close(notification);
}, notification.displayTime);
}
this.changeDetector.detectChanges();
});
this.dialogsSubscription =
@ -74,6 +80,8 @@ export class DialogRendererComponent implements OnDestroy, OnInit {
this.dialogRequest = request;
this.dialogView.show();
this.changeDetector.detectChanges();
});
}
@ -94,6 +102,12 @@ export class DialogRendererComponent implements OnDestroy, OnInit {
}
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;
}
const viewportHeight = document.documentElement.clientHeight;
const viewportWidth = document.documentElement.clientWidth;
const viewportHeight = document.documentElement!.clientHeight;
const viewportWidth = document.documentElement!.clientWidth;
const modalRef = this.element.nativeElement;
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) {
if (isOpen === (this.renderedView !== null)) {
if (isOpen === (!!this.renderedView)) {
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.
*/
import { Component, Input, OnDestroy, OnInit, Renderer2 } from '@angular/core';
import { ChangeDetectionStrategy, ChangeDetectorRef, Component, Input, OnDestroy, OnInit, Renderer2 } from '@angular/core';
import {
fadeAnimation,
@ -20,7 +20,8 @@ import {
templateUrl: './onboarding-tooltip.component.html',
animations: [
fadeAnimation
]
],
changeDetection: ChangeDetectionStrategy.OnPush
})
export class OnboardingTooltipComponent implements OnDestroy, OnInit {
private showTimer: any;
@ -42,6 +43,7 @@ export class OnboardingTooltipComponent implements OnDestroy, OnInit {
public position = 'left';
constructor(
private readonly changeDetector: ChangeDetectorRef,
private readonly onboardingService: OnboardingService,
private readonly renderer: Renderer2
) {
@ -73,6 +75,8 @@ export class OnboardingTooltipComponent implements OnDestroy, OnInit {
if (this.isSameOrParent(fromPoint)) {
this.tooltipModal.show();
this.changeDetector.detectChanges();
this.closeTimer = setTimeout(() => {
this.hideThis();
}, 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.
*/
import { Component, ViewChild, ViewContainerRef } from '@angular/core';
import { ChangeDetectionStrategy, Component, ViewChild, ViewContainerRef } from '@angular/core';
@Component({
selector: 'sqx-root-view',
styleUrls: ['./root-view.component.scss'],
templateUrl: './root-view.component.html'
templateUrl: './root-view.component.html',
changeDetection: ChangeDetectionStrategy.OnPush
})
export class RootViewComponent {
@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.
*/
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';
@ -33,6 +33,7 @@ export class TooltipComponent implements OnDestroy, OnInit {
public modal = new ModalModel();
constructor(
private readonly changeDetector: ChangeDetectorRef,
private readonly renderer: Renderer2
) {
}
@ -52,6 +53,8 @@ export class TooltipComponent implements OnDestroy, OnInit {
this.targetMouseEnterListener =
this.renderer.listen(this.target, 'mouseenter', () => {
this.modal.show();
this.changeDetector.detectChanges();
});
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 {
private readonly panels: PanelComponent[] = [];
private isViewInit = false;
private containerWidth = 0;
constructor(
@ -27,14 +28,14 @@ export class PanelContainerDirective implements AfterViewInit {
this.invalidate(true);
}
public ngAfterViewInit() {
this.invalidate(true);
}
public push(panel: PanelComponent) {
this.panels.push(panel);
}
this.invalidate();
public ngAfterViewInit() {
this.isViewInit = true;
this.invalidate(true);
}
public pop() {
@ -44,8 +45,12 @@ export class PanelContainerDirective implements AfterViewInit {
}
public invalidate(resize = false) {
if (!this.isViewInit) {
return;
}
if (resize) {
this.containerWidth = this.element.nativeElement.getBoundingClientRect().width;
this.containerWidth = this.element.nativeElement.offsetWidth;
}
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.
*/
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';
@ -17,7 +17,8 @@ import { PanelContainerDirective } from './panel-container.directive';
templateUrl: './panel.component.html',
animations: [
slideRightAnimation
]
],
changeDetection: ChangeDetectionStrategy.OnPush
})
export class PanelComponent implements AfterViewInit, OnDestroy, OnInit {
private styleWidth: string;
@ -83,8 +84,7 @@ export class PanelComponent implements AfterViewInit, OnDestroy, OnInit {
this.styleWidth = size;
this.renderer.setStyle(this.panel.nativeElement, 'width', size);
this.renderWidth = this.panel.nativeElement.getBoundingClientRect().width;
this.renderWidth = this.panel.nativeElement.offsetWidth;
}
}

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

@ -5,7 +5,9 @@
* 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 {
let snapshot: ActivatedRouteSnapshot | null = value['snapshot'] || value;
@ -40,4 +42,22 @@ export function allParams(value: ActivatedRouteSnapshot | ActivatedRoute): Param
}
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.
*/
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';
@Component({
selector: 'sqx-shortcut',
template: ''
template: '',
changeDetection: ChangeDetectionStrategy.OnPush
})
export class ShortcutComponent implements OnDestroy, OnInit {
@Input()

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

@ -5,13 +5,14 @@
* 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';
@Component({
selector: 'sqx-title',
template: ''
template: '',
changeDetection: ChangeDetectionStrategy.OnPush
})
export class TitleComponent implements OnChanges {
@Input()

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

@ -5,14 +5,15 @@
* 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 { ResourceLoaderService } from './../services/resource-loader.service';
@Component({
selector: 'sqx-user-report',
template: ''
template: '',
changeDetection: ChangeDetectionStrategy.OnPush
})
export class UserReportComponent implements OnDestroy, OnInit {
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/validators';
export * from './angular/http/loading.interceptor';
export * from './angular/http/http-extensions';
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/clipboard.service';
export * from './services/dialog.service';
export * from './services/loading.service';
export * from './services/local-store.service';
export * from './services/message-bus.service';
export * from './services/onboarding.service';

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

@ -6,6 +6,7 @@
*/
import { CommonModule } from '@angular/common';
import { HTTP_INTERCEPTORS } from '@angular/common/http';
import { ModuleWithProviders, NgModule } from '@angular/core';
import { FormsModule, ReactiveFormsModule } from '@angular/forms';
@ -44,6 +45,8 @@ import {
KeysPipe,
KNumberPipe,
LightenPipe,
LoadingInterceptor,
LoadingService,
LocalStoreService,
MessageBus,
ModalDialogComponent,
@ -220,11 +223,17 @@ export class SqxFrameworkModule {
ClipboardService,
DialogService,
LocalStoreService,
LoadingService,
MessageBus,
OnboardingService,
ResourceLoaderService,
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.
*/
import { Component, EventEmitter, Input, Output } from '@angular/core';
import { ChangeDetectionStrategy, Component, EventEmitter, Input, Output } from '@angular/core';
import { FormBuilder } from '@angular/forms';
import {
@ -18,7 +18,8 @@ import {
@Component({
selector: 'sqx-app-form',
styleUrls: ['./app-form.component.scss'],
templateUrl: './app-form.component.html'
templateUrl: './app-form.component.html',
changeDetection: ChangeDetectionStrategy.OnPush
})
export class AppFormComponent {
@Output()
@ -46,7 +47,7 @@ export class AppFormComponent {
const request = new CreateAppDto(value.name, this.template);
this.appsStore.create(request)
.subscribe(dto => {
.subscribe(() => {
this.complete();
}, error => {
this.createForm.submitFailed(error);

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

@ -7,7 +7,7 @@
</span>
<div *ngIf="asset.isImage" class="file-image">
<img [sqxImageSource]="asset | sqxAssetPreviewUrl">
<img [sqxImageSource]="asset | sqxAssetPreviewUrl" class="bg" layoutKey="asset-large">
</div>
<div *ngIf="!asset.isImage" class="file-icon">
<img [attr.src]="asset | sqxFileIcon">
@ -89,7 +89,7 @@
<div class="left-border" [class.selected]="isSelected" ></div>
<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 *ngIf="asset && !asset.isImage && progress === 0" class="image image-padded" @fade>
<img class="icon" [attr.src]="asset | sqxFileIcon">
@ -130,9 +130,7 @@
</div>
<div class="upload-progress" *ngIf="progress > 0">
<div class="progress-background">
<div class="progress-bar" [style.width]="(progress + 40) + '%'"></div>
</div>
<sqx-progress-bar [value]="50" [trailWidth]="0.8" [strokeWidth]="0.8" [showText]="false"></sqx-progress-bar>
</div>
<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;
}
}
.upload-progress {
padding: .25rem 0;
}
}
.drop-overlay {
@ -279,24 +283,16 @@ $list-height: 2.375rem;
}
}
.progress {
&-background {
background: $color-border;
margin: (($list-height - .25rem) / 2) 0;
}
&-bar {
background: $color-theme-blue;
}
.selectable {
cursor: pointer;
}
&-background,
&-bar {
height: .25rem;
}
.bg {
background-image: url('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAADwAAAA8CAIAAAC1nk4lAAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8YQUAAAAJcEhZcwAADsMAAA7DAcdvqGQAAAAYdEVYdFNvZnR3YXJlAHBhaW50Lm5ldCA0LjEuMWMqnEsAAACbSURBVGhD7c6hDQAxAMPA33+m7vYlJh7AoFKOBMbfyfyZRRsPgUUbD4FFGw+BRRsPgUUbD4FFGw+BRRsPgUUbD4FFGw+BRRsPgUUbD4FFGw+BRRsPgUUbD4E3o9kA7YFFGw+BRRsPgUUbD4FFGw+BRRsPgUUbD4FFGw+BRRsPgUUbD4FFGw+BRRsPgUUbD4FFGw+BRRsPgQejz7nPYYKl8IqSfgAAAABJRU5ErkJggg==');
}
.selectable {
cursor: pointer;
.bg2 {
background-image: url('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAAUCAIAAAAC64paAAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8YQUAAAAJcEhZcwAADsMAAA7DAcdvqGQAAAAYdEVYdFNvZnR3YXJlAHBhaW50Lm5ldCA0LjEuMWMqnEsAAAAsSURBVDhPY9iDF/zHC0Y1YwCoKhxgRGqG0jgA1AwcYFQzBoCqwgFGnuY9ewCdSg6FRg4gMAAAAABJRU5ErkJggg==');
}
.tags {

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

@ -5,7 +5,7 @@
* 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 { Subscription } from 'rxjs';
import { debounceTime, distinctUntilChanged } from 'rxjs/operators';
@ -31,7 +31,8 @@ import {
templateUrl: './asset.component.html',
animations: [
fadeAnimation
]
],
changeDetection: ChangeDetectionStrategy.OnPush
})
export class AssetComponent implements OnDestroy, OnInit {
private tagSubscription: Subscription;
@ -91,6 +92,7 @@ export class AssetComponent implements OnDestroy, OnInit {
private readonly appsState: AppsState,
private readonly assetsService: AssetsService,
private readonly authState: AuthService,
private readonly changeDetector: ChangeDetectorRef,
private readonly dialogs: DialogService,
private readonly formBuilder: FormBuilder
) {
@ -105,7 +107,7 @@ export class AssetComponent implements OnDestroy, OnInit {
if (Types.is(dto, AssetDto)) {
this.emitLoaded(dto);
} else {
this.progress = dto;
this.setProgress(dto);
}
}, error => {
this.dialogs.notifyError(error);
@ -145,7 +147,7 @@ export class AssetComponent implements OnDestroy, OnInit {
}, 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)
.subscribe(dto => {
this.updateAsset(this.asset.rename(requestDto.fileName, this.authState.user!.token, dto.version), true);
this.renameCancel();
}, error => {
this.dialogs.notifyError(error);
@ -194,10 +194,6 @@ export class AssetComponent implements OnDestroy, OnInit {
this.renaming = false;
}
private setProgress(progress = 0) {
this.progress = progress;
}
private emitFailed(error: any) {
this.failed.emit(error);
}
@ -210,6 +206,12 @@ export class AssetComponent implements OnDestroy, OnInit {
this.updated.emit(asset);
}
private setProgress(progress: number) {
this.progress = progress;
this.changeDetector.detectChanges();
}
private updateAsset(asset: AssetDto, emitEvent: boolean) {
this.asset = asset;
this.progress = 0;
@ -221,5 +223,7 @@ export class AssetComponent implements OnDestroy, OnInit {
}
this.renameCancel();
this.changeDetector.detectChanges();
}
}

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

@ -7,7 +7,7 @@
// 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 {
@ -19,7 +19,8 @@ import {
@Component({
selector: 'sqx-assets-list',
styleUrls: ['./assets-list.component.scss'],
templateUrl: './assets-list.component.html'
templateUrl: './assets-list.component.html',
changeDetection: ChangeDetectionStrategy.OnPush
})
export class AssetsListComponent {
public newFiles = ImmutableArray.empty<File>();

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

@ -7,7 +7,7 @@
// 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 {
@ -23,7 +23,8 @@ import {
templateUrl: './assets-selector.component.html',
animations: [
fadeAnimation
]
],
changeDetection: ChangeDetectionStrategy.OnPush
})
export class AssetsSelectorComponent implements OnInit {
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.
*/
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 {
@ -31,7 +31,8 @@ interface Geolocation {
selector: 'sqx-geolocation-editor',
styleUrls: ['./geolocation-editor.component.scss'],
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 {
private callChange = (v: any) => { /* NOOP */ };
@ -144,12 +145,12 @@ export class GeolocationEditorComponent implements ControlValueAccessor, AfterVi
}
public updateValueByInput() {
let updateMap = this.geolocationForm.controls['latitude'].value !== null &&
this.geolocationForm.controls['longitude'].value !== null;
const lat = this.geolocationForm.controls['latitude'].value;
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);
} else {
this.callChange(this.value);
@ -188,11 +189,7 @@ export class GeolocationEditorComponent implements ControlValueAccessor, AfterVi
if (!this.marker && !this.isDisabled) {
const latlng = event.latlng.wrap();
this.value = {
latitude: latlng.lat,
longitude: latlng.lng
};
this.updateValue(latlng.lat, latlng.lng);
this.updateMarker(false, true);
}
});
@ -225,11 +222,7 @@ export class GeolocationEditorComponent implements ControlValueAccessor, AfterVi
this.map.addListener('click',
(event: any) => {
if (!this.isDisabled) {
this.value = {
latitude: event.latLng.lat(),
longitude: event.latLng.lng()
};
this.updateValue(event.latLng.lat(), event.latLng.lng());
this.updateMarker(false, true);
}
});
@ -252,8 +245,7 @@ export class GeolocationEditorComponent implements ControlValueAccessor, AfterVi
let lat = place.geometry.location.lat();
let lng = place.geometry.location.lng();
this.value = { latitude: lat, longitude: lng };
this.updateValue(lat, lng);
this.updateMarker(false, true);
}
}
@ -274,15 +266,30 @@ export class GeolocationEditorComponent implements ControlValueAccessor, AfterVi
this.updateMarker(true, true);
}
private updateValue(lat: number, lng: number) {
this.value = { latitude: lat, longitude: lng };
}
private updateMarker(zoom: boolean, fireEvent: boolean) {
if (!this.isGoogleMaps) {
this.updateMarkerOSM(zoom, fireEvent);
this.updateMarkerOSM(zoom);
} 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.marker) {
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) => {
const latlng = event.latlng.wrap();
this.value = {
latitude: latlng.lat,
longitude: latlng.lng
};
this.updateValue(latlng.lat, latlng.lng);
});
this.marker.on('dragend', () => {
@ -314,8 +318,6 @@ export class GeolocationEditorComponent implements ControlValueAccessor, AfterVi
}
this.marker.setLatLng(latLng);
this.geolocationForm.setValue(this.value, { emitEvent: false, onlySelf: false });
} else {
if (this.marker) {
this.marker.removeFrom(this.map);
@ -323,17 +325,10 @@ export class GeolocationEditorComponent implements ControlValueAccessor, AfterVi
}
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.marker) {
this.marker = new google.maps.Marker({
@ -347,19 +342,12 @@ export class GeolocationEditorComponent implements ControlValueAccessor, AfterVi
this.marker.addListener('drag', (event: any) => {
if (!this.isDisabled) {
this.value = {
latitude: event.latLng.lat(),
longitude: event.latLng.lng()
};
this.updateValue(event.latLng.lat(), event.LatLng.lng());
}
});
this.marker.addListener('dragend', (event: any) => {
if (!this.isDisabled) {
this.value = {
latitude: event.latLng.lat(),
longitude: event.latLng.lng()
};
this.updateValue(event.latLng.lat(), event.LatLng.lng());
this.updateMarker(false, true);
}
});
@ -375,8 +363,6 @@ export class GeolocationEditorComponent implements ControlValueAccessor, AfterVi
this.marker.setPosition(latLng);
this.map.setZoom(12);
this.geolocationForm.setValue(this.value, { emitEvent: false, onlySelf: false });
} else {
if (this.marker) {
this.marker.setMap(null);
@ -384,13 +370,6 @@ export class GeolocationEditorComponent implements ControlValueAccessor, AfterVi
}
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.
*/
import { Component } from '@angular/core';
import { ChangeDetectionStrategy, Component } from '@angular/core';
import { ActivatedRoute } from '@angular/router';
import { HelpService } from '@app/shared/internal';
@ -13,11 +13,11 @@ import { HelpService } from '@app/shared/internal';
@Component({
selector: 'sqx-help',
styleUrls: ['./help.component.scss'],
templateUrl: './help.component.html'
templateUrl: './help.component.html',
changeDetection: ChangeDetectionStrategy.OnPush
})
export class HelpComponent {
public helpSections =
this.helpService.getHelp(this.route.snapshot.data['helpPage']);
public helpSections = this.helpService.getHelp(this.route.snapshot.data['helpPage']);
constructor(
private readonly helpService: HelpService,

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

@ -1,12 +1,12 @@
<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">
<img class="user-picture" [attr.title]="event.actor | sqxUserNameRef:null" [attr.src]="event.actor | sqxUserPictureRef" />
</div>
<div class="col pl-2">
<div class="event-message">
<span class="user-ref">{{event.actor | sqxUserNameRef:null}}</span>
<span [innerHTML]="format(event.message) | async"></span>
<span [innerHTML]="event | sqxHistoryMessage"></span>
</div>
<div class="event-created">{{event.created | sqxFromNow}}</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.
*/
import { Component, Input } from '@angular/core';
import { Observable } from 'rxjs';
import { ChangeDetectionStrategy, Component, Input } from '@angular/core';
import {
formatHistoryMessage,
HistoryEventDto,
UsersProviderService
} from '@app/shared/internal';
import { HistoryEventDto } from '@app/shared/internal';
@Component({
selector: 'sqx-history-list',
styleUrls: ['./history-list.component.scss'],
templateUrl: './history-list.component.html'
templateUrl: './history-list.component.html',
changeDetection: ChangeDetectionStrategy.OnPush
})
export class HistoryListComponent {
@Input()
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) {
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.
*/
import { Component } from '@angular/core';
import { ChangeDetectionStrategy, Component } from '@angular/core';
import { ActivatedRoute } from '@angular/router';
import { merge, Observable, timer } from 'rxjs';
import { delay, switchMap } from 'rxjs/operators';
@ -22,7 +22,8 @@ import {
@Component({
selector: 'sqx-history',
styleUrls: ['./history.component.scss'],
templateUrl: './history.component.html'
templateUrl: './history.component.html',
changeDetection: ChangeDetectionStrategy.OnPush
})
export class HistoryComponent {
private readonly channel = this.calculateChannel();
@ -32,7 +33,7 @@ export class HistoryComponent {
timer(0, 10000),
this.messageBus.of(HistoryChannelUpdated).pipe(delay(1000))
).pipe(
switchMap(app => this.historyService.getHistory(this.appsState.appName, this.channel)));
switchMap(() => this.historyService.getHistory(this.appsState.appName, this.channel)));
constructor(
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.
*/
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';
@ -17,7 +17,8 @@ export interface Language { iso2Code: string; englishName: string; isMasterLangu
templateUrl: './language-selector.component.html',
animations: [
fadeAnimation
]
],
changeDetection: ChangeDetectionStrategy.OnPush
})
export class LanguageSelectorComponent implements OnChanges, OnInit {
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.
*/
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 {
@ -25,7 +25,8 @@ export const SQX_MARKDOWN_EDITOR_CONTROL_VALUE_ACCESSOR: any = {
selector: 'sqx-markdown-editor',
styleUrls: ['./markdown-editor.component.scss'],
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 {
private callChange = (v: any) => { /* NOOP */ };

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

@ -11,11 +11,57 @@ import { map } from 'rxjs/operators';
import {
ApiUrlConfig,
formatHistoryMessage,
HistoryEventDto,
MathHelper,
UserDto,
UsersProviderService
} 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 {
private lastUserId: string;
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.
*/
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 {
@ -25,7 +25,8 @@ export const SQX_RICH_EDITOR_CONTROL_VALUE_ACCESSOR: any = {
selector: 'sqx-rich-editor',
styleUrls: ['./rich-editor.component.scss'],
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 {
private callChange = (v: any) => { /* NOOP */ };
@ -105,7 +106,7 @@ export class RichEditorComponent implements ControlValueAccessor, AfterViewInit,
self.tinyInitTimer =
setTimeout(() => {
self.tinyEditor.setContent(this.value || '');
}, 500);
}, 1000);
},
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>
<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="col col-4">
<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.
*/
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 {
@ -24,7 +24,8 @@ import {
templateUrl: './schema-category.component.html',
animations: [
fadeAnimation
]
],
changeDetection: ChangeDetectionStrategy.OnPush
})
export class SchemaCategoryComponent implements OnInit, OnChanges {
@Output()
@ -36,6 +37,9 @@ export class SchemaCategoryComponent implements OnInit, OnChanges {
@Input()
public isReadonly: boolean;
@Input()
public routeSingletonToContent = false;
@Input()
public schemasFilter: string;
@ -100,6 +104,10 @@ export class SchemaCategoryComponent implements OnInit, OnChanges {
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) {
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 { of } from 'rxjs';
import { IMock, Mock, Times } from 'typemoq';
import { IMock, It, Mock, Times } from 'typemoq';
import { ContentDto } from './../services/contents.service';
import { ContentsState } from './../state/contents.state';
@ -42,7 +42,7 @@ describe('ContentMustExistGuard', () => {
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', () => {

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

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

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

@ -7,7 +7,7 @@
import { Router } from '@angular/router';
import { of } from 'rxjs';
import { IMock, Mock, Times } from 'typemoq';
import { IMock, It, Mock, Times } from 'typemoq';
import { AuthService } from '@app/shared';
@ -52,5 +52,7 @@ describe('MustBeAuthenticatedGuard', () => {
});
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 { of } from 'rxjs';
import { IMock, Mock, Times } from 'typemoq';
import { IMock, It, Mock, Times } from 'typemoq';
import { AuthService } from '@app/shared';
@ -52,5 +52,7 @@ describe('MustNotBeAuthenticatedGuard', () => {
});
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.
*/
import { Router, RouterStateSnapshot } from '@angular/router';
import { Router } from '@angular/router';
import { of } from 'rxjs';
import { IMock, Mock, Times } from 'typemoq';
import { IMock, It, Mock, Times } from 'typemoq';
import { SchemaDetailsDto } from './../services/schemas.service';
import { SchemasState } from './../state/schemas.state';
@ -21,7 +21,6 @@ describe('SchemaMustExistPublishedGuard', () => {
};
let schemasState: IMock<SchemasState>;
let state: RouterStateSnapshot = <any>{ url: 'current-url' };
let router: IMock<Router>;
let schemaGuard: SchemaMustExistPublishedGuard;
@ -37,13 +36,13 @@ describe('SchemaMustExistPublishedGuard', () => {
let result: boolean;
schemaGuard.canActivate(route, state).subscribe(x => {
schemaGuard.canActivate(route).subscribe(x => {
result = x;
}).unsubscribe();
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', () => {
@ -52,7 +51,7 @@ describe('SchemaMustExistPublishedGuard', () => {
let result: boolean;
schemaGuard.canActivate(route, state).subscribe(x => {
schemaGuard.canActivate(route).subscribe(x => {
result = x;
}).unsubscribe();
@ -60,34 +59,4 @@ describe('SchemaMustExistPublishedGuard', () => {
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 { ActivatedRouteSnapshot, CanActivate, Router, RouterStateSnapshot } from '@angular/router';
import { ActivatedRouteSnapshot, CanActivate, Router } from '@angular/router';
import { Observable } from 'rxjs';
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 result =
@ -31,12 +31,8 @@ export class SchemaMustExistPublishedGuard implements CanActivate {
if (!dto || !dto.isPublished) {
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;
}

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

@ -41,8 +41,6 @@ describe('SchemaMustExistGuard', () => {
}).unsubscribe();
expect(result!).toBeTruthy();
schemasState.verify(x => x.select('123'), Times.once());
});
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']);
}
}),
map(s => s !== null));
map(s => !!s));
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/schema-must-exist-published.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-content.guard';

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

@ -45,6 +45,7 @@ import {
HelpService,
HistoryComponent,
HistoryListComponent,
HistoryMessagePipe,
HistoryService,
LanguageSelectorComponent,
LanguagesService,
@ -64,6 +65,7 @@ import {
SchemaCategoryComponent,
SchemaMustExistGuard,
SchemaMustExistPublishedGuard,
SchemaMustNotBeSingletonGuard,
SchemasService,
SchemasState,
SearchFormComponent,
@ -100,6 +102,7 @@ import {
HelpComponent,
HistoryComponent,
HistoryListComponent,
HistoryMessagePipe,
LanguageSelectorComponent,
MarkdownEditorComponent,
SchemaCategoryComponent,
@ -124,6 +127,7 @@ import {
HelpComponent,
HistoryComponent,
HistoryListComponent,
HistoryMessagePipe,
LanguageSelectorComponent,
MarkdownEditorComponent,
RouterModule,
@ -180,6 +184,7 @@ export class SqxSharedModule {
RulesState,
SchemaMustExistGuard,
SchemaMustExistPublishedGuard,
SchemaMustNotBeSingletonGuard,
SchemasService,
SchemasState,
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) {
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) {
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> {
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();
}

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

@ -68,12 +68,16 @@ describe('Queries', () => {
it('should forward add call to state', () => {
queries.add('key3', 'filter3');
expect(true).toBeTruthy();
uiState.verify(x => x.set('schemas.my-schema.queries.key3', 'filter3'), Times.once());
});
it('should forward remove call to state', () => {
queries.remove('key3');
expect(true).toBeTruthy();
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.
*/
import { Component } from '@angular/core';
import { ChangeDetectionStrategy, Component } from '@angular/core';
import { AppsState } from '@app/shared';
@Component({
selector: 'sqx-left-menu',
styleUrls: ['./left-menu.component.scss'],
templateUrl: './left-menu.component.html'
templateUrl: './left-menu.component.html',
changeDetection: ChangeDetectionStrategy.OnPush
})
export class LeftMenuComponent {
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.
*/
import { Component } from '@angular/core';
import { ChangeDetectionStrategy, Component } from '@angular/core';
import {
AppsState,
@ -20,7 +20,8 @@ import {
templateUrl: './apps-menu.component.html',
animations: [
fadeAnimation
]
],
changeDetection: ChangeDetectionStrategy.OnPush
})
export class AppsMenuComponent {
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">
<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>
<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;
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 {
DialogService,
fadeAnimation,
LoadingService,
Notification
} from '@app/shared';
@Component({
selector: 'sqx-internal-area',
styleUrls: ['./internal-area.component.scss'],
templateUrl: './internal-area.component.html'
templateUrl: './internal-area.component.html',
animations: [
fadeAnimation
]
})
export class InternalAreaComponent implements OnDestroy, OnInit {
private queryParamsSubscription: Subscription;
@ -25,6 +30,7 @@ export class InternalAreaComponent implements OnDestroy, OnInit {
public notifications: Notification[] = [];
constructor(
public readonly loadingService: LoadingService,
private readonly dialogs: DialogService,
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.
*/
import { Component, OnDestroy, OnInit } from '@angular/core';
import { ChangeDetectionStrategy, ChangeDetectorRef, Component, OnDestroy, OnInit } from '@angular/core';
import { Subscription } from 'rxjs';
import { filter } from 'rxjs/operators';
@ -22,7 +22,8 @@ import {
templateUrl: './profile-menu.component.html',
animations: [
fadeAnimation
]
],
changeDetection: ChangeDetectionStrategy.OnPush
})
export class ProfileMenuComponent implements OnDestroy, OnInit {
private authenticationSubscription: Subscription;
@ -38,7 +39,8 @@ export class ProfileMenuComponent implements OnDestroy, OnInit {
constructor(
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.isAdmin = user!.isAdmin;
this.changeDetector.detectChanges();
});
}

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

@ -163,6 +163,20 @@ body {
@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) {
20% {
@include rotate(0deg);

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

@ -147,10 +147,10 @@ p {
font-size: 16px;
}
.fs1 {
font-size: 20px;
font-size: 32px;
}
.fs2 {
font-size: 32px;
font-size: 20px;
}
.fs3 {
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