Browse Source

Autocompletion fixes (#972)

* Several fixes.

* Improved autocompletion.

* Fix tests.
pull/973/head
Sebastian Stehle 4 years ago
committed by GitHub
parent
commit
fd51ab9f63
No known key found for this signature in database GPG Key ID: 4AEE18F83AFDEB23
  1. 1
      backend/extensions/Squidex.Extensions/Actions/Webhook/WebhookAction.cs
  2. 1784
      backend/src/Squidex.Domain.Apps.Core.Model/FieldDescriptions.Designer.cs
  3. 11
      backend/src/Squidex.Domain.Apps.Core.Model/FieldDescriptions.resx
  4. 2
      backend/src/Squidex.Domain.Apps.Core.Operations/Scripting/ContentWrapper/ContentDataObject.cs
  5. 2
      backend/src/Squidex.Domain.Apps.Core.Operations/Scripting/IScriptDescriptor.cs
  6. 2
      backend/src/Squidex.Domain.Apps.Core.Operations/Scripting/ScriptExecutionContext.cs
  7. 62
      backend/src/Squidex.Domain.Apps.Core.Operations/Scripting/ScriptingCompleter.cs
  8. 5
      backend/src/Squidex.Domain.Apps.Core.Operations/Scripting/ScriptingValue.cs
  9. 17
      backend/src/Squidex/Areas/Api/Controllers/Schemas/SchemasController.cs
  10. 21
      backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/Scripting/ScriptingCompleterTests.cs
  11. 18
      frontend/.storybook/main.js
  12. 10
      frontend/src/app/features/content/pages/content/content-event.component.ts
  13. 12
      frontend/src/app/features/content/pages/content/editor/content-field.component.ts
  14. 10
      frontend/src/app/features/content/pages/content/editor/content-section.component.ts
  15. 10
      frontend/src/app/features/content/pages/content/editor/field-copy-button.component.ts
  16. 12
      frontend/src/app/features/content/pages/content/inspecting/content-inspection.component.ts
  17. 10
      frontend/src/app/features/content/pages/content/references/content-references.component.ts
  18. 4
      frontend/src/app/features/content/pages/contents/custom-view-editor.component.ts
  19. 12
      frontend/src/app/features/content/shared/content-extension.component.ts
  20. 12
      frontend/src/app/features/content/shared/forms/array-editor.component.ts
  21. 12
      frontend/src/app/features/content/shared/forms/array-item.component.ts
  22. 10
      frontend/src/app/features/content/shared/forms/component.component.ts
  23. 10
      frontend/src/app/features/content/shared/forms/field-editor.component.ts
  24. 18
      frontend/src/app/features/content/shared/forms/iframe-editor.component.ts
  25. 12
      frontend/src/app/features/content/shared/list/content.component.ts
  26. 12
      frontend/src/app/features/content/shared/references/reference-dropdown.component.ts
  27. 4
      frontend/src/app/features/content/shared/references/reference-item.component.ts
  28. 2
      frontend/src/app/features/content/shared/references/references-checkboxes.component.html
  29. 10
      frontend/src/app/features/content/shared/references/references-checkboxes.component.ts
  30. 8
      frontend/src/app/features/content/shared/references/references-tag-converter.ts
  31. 2
      frontend/src/app/features/content/shared/references/references-tags.component.html
  32. 12
      frontend/src/app/features/content/shared/references/references-tags.component.ts
  33. 4
      frontend/src/app/features/dashboard/pages/cards/history-card.component.ts
  34. 12
      frontend/src/app/features/dashboard/pages/dashboard-config.component.ts
  35. 1
      frontend/src/app/features/rules/declarations.ts
  36. 3
      frontend/src/app/features/rules/module.ts
  37. 10
      frontend/src/app/features/rules/pages/simulator/rule-transition.component.ts
  38. 2
      frontend/src/app/features/rules/shared/actions/formattable-input.component.html
  39. 4
      frontend/src/app/features/rules/shared/actions/formattable-input.component.scss
  40. 2
      frontend/src/app/features/rules/shared/actions/formattable-input.component.ts
  41. 10
      frontend/src/app/features/rules/shared/actions/generic-action.component.ts
  42. 40
      frontend/src/app/features/rules/shared/triggers/completions-cache.ts
  43. 29
      frontend/src/app/features/rules/shared/triggers/content-changed-schema.component.html
  44. 8
      frontend/src/app/features/rules/shared/triggers/content-changed-schema.component.scss
  45. 47
      frontend/src/app/features/rules/shared/triggers/content-changed-schema.component.ts
  46. 63
      frontend/src/app/features/rules/shared/triggers/content-changed-trigger.component.html
  47. 6
      frontend/src/app/features/rules/shared/triggers/content-changed-trigger.component.scss
  48. 82
      frontend/src/app/features/rules/shared/triggers/content-changed-trigger.component.ts
  49. 4
      frontend/src/app/features/schemas/pages/schema/common/schema-edit-form.component.ts
  50. 4
      frontend/src/app/features/schemas/pages/schema/export/schema-export-form.component.ts
  51. 2
      frontend/src/app/features/schemas/pages/schema/fields/field-group.component.html
  52. 10
      frontend/src/app/features/schemas/pages/schema/fields/field.component.ts
  53. 9
      frontend/src/app/features/schemas/pages/schema/fields/sortable-field-list.component.html
  54. 10
      frontend/src/app/features/schemas/pages/schema/fields/types/boolean-validation.component.ts
  55. 10
      frontend/src/app/features/schemas/pages/schema/fields/types/date-time-validation.component.ts
  56. 10
      frontend/src/app/features/schemas/pages/schema/fields/types/number-ui.component.ts
  57. 10
      frontend/src/app/features/schemas/pages/schema/fields/types/string-ui.component.ts
  58. 12
      frontend/src/app/features/schemas/pages/schema/fields/types/string-validation.component.ts
  59. 2
      frontend/src/app/features/schemas/pages/schema/preview/schema-preview-urls-form.component.html
  60. 4
      frontend/src/app/features/schemas/pages/schema/preview/schema-preview-urls-form.component.ts
  61. 4
      frontend/src/app/features/schemas/pages/schema/rules/schema-field-rules-form.component.html
  62. 13
      frontend/src/app/features/schemas/pages/schema/rules/schema-field-rules-form.component.ts
  63. 6
      frontend/src/app/features/schemas/pages/schema/scripts/schema-scripts-form.component.ts
  64. 4
      frontend/src/app/features/schemas/pages/schema/ui/field-list.component.ts
  65. 4
      frontend/src/app/features/schemas/pages/schema/ui/schema-ui-form.component.ts
  66. 10
      frontend/src/app/features/settings/pages/backups/backup.component.ts
  67. 10
      frontend/src/app/features/settings/pages/clients/client.component.ts
  68. 4
      frontend/src/app/features/settings/pages/contributors/contributor-add-form.component.ts
  69. 4
      frontend/src/app/features/settings/pages/languages/language.component.ts
  70. 10
      frontend/src/app/features/settings/pages/roles/role.component.ts
  71. 4
      frontend/src/app/features/settings/pages/templates/template.component.ts
  72. 4
      frontend/src/app/features/settings/pages/workflows/workflow-diagram.component.ts
  73. 10
      frontend/src/app/features/settings/pages/workflows/workflow-step.component.ts
  74. 4
      frontend/src/app/features/settings/pages/workflows/workflow.component.ts
  75. 13
      frontend/src/app/framework/angular/avatar.component.ts
  76. 4
      frontend/src/app/framework/angular/forms/control-errors.component.ts
  77. 4
      frontend/src/app/framework/angular/forms/editors/checkbox-group.component.ts
  78. 5
      frontend/src/app/framework/angular/forms/editors/code-editor.component.html
  79. 23
      frontend/src/app/framework/angular/forms/editors/code-editor.component.scss
  80. 136
      frontend/src/app/framework/angular/forms/editors/code-editor.component.ts
  81. 99
      frontend/src/app/framework/angular/forms/editors/code-editor.stories.ts
  82. 9
      frontend/src/app/framework/angular/forms/editors/dropdown.component.ts
  83. 4
      frontend/src/app/framework/angular/forms/editors/radio-group.component.ts
  84. 8
      frontend/src/app/framework/angular/forms/editors/tag-editor.component.html
  85. 61
      frontend/src/app/framework/angular/forms/editors/tag-editor.component.ts
  86. 9
      frontend/src/app/framework/angular/forms/form-error.component.ts
  87. 7
      frontend/src/app/framework/angular/forms/progress-bar.component.ts
  88. 15
      frontend/src/app/framework/angular/helpers.ts
  89. 4
      frontend/src/app/framework/angular/image-source.directive.ts
  90. 4
      frontend/src/app/framework/angular/image-url.directive.ts
  91. 4
      frontend/src/app/framework/angular/language-selector.component.ts
  92. 12
      frontend/src/app/framework/angular/markdown.directive.spec.ts
  93. 6
      frontend/src/app/framework/angular/markdown.directive.ts
  94. 4
      frontend/src/app/framework/angular/pager.component.ts
  95. 4
      frontend/src/app/framework/angular/resized.directive.ts
  96. 4
      frontend/src/app/framework/angular/scroll-active.directive.ts
  97. 7
      frontend/src/app/framework/angular/template-wrapper.directive.ts
  98. 4
      frontend/src/app/framework/angular/title.component.ts
  99. 4
      frontend/src/app/framework/angular/video-player.component.ts
  100. 1
      frontend/src/app/framework/declarations.ts

1
backend/extensions/Squidex.Extensions/Actions/Webhook/WebhookAction.cs

@ -44,6 +44,7 @@ public sealed record WebhookAction : RuleAction
[Display(Name = "Headers (Optional)", Description = "The message headers in the format '[Key]=[Value]', one entry per line.")] [Display(Name = "Headers (Optional)", Description = "The message headers in the format '[Key]=[Value]', one entry per line.")]
[Editor(RuleFieldEditor.TextArea)] [Editor(RuleFieldEditor.TextArea)]
[Formattable]
public string Headers { get; set; } public string Headers { get; set; }
[Display(Name = "Shared Secret", Description = "The shared secret that is used to calculate the payload signature.")] [Display(Name = "Shared Secret", Description = "The shared secret that is used to calculate the payload signature.")]

1784
backend/src/Squidex.Domain.Apps.Core.Model/FieldDescriptions.Designer.cs

File diff suppressed because it is too large

11
backend/src/Squidex.Domain.Apps.Core.Model/FieldDescriptions.resx

@ -343,7 +343,13 @@
<value>The event timestamp.</value> <value>The event timestamp.</value>
</data> </data>
<data name="EventType" xml:space="preserve"> <data name="EventType" xml:space="preserve">
<value>The type of the event, e.g. 'Created' or 'Updated'.</value> <value>The type of the event.</value>
</data>
<data name="ItemData" xml:space="preserve">
<value>The current item, if the field is part of an array.</value>
</data>
<data name="ItemDataField" xml:space="preserve">
<value>The current item, if the field is part of an array.</value>
</data> </data>
<data name="JsonPath" xml:space="preserve"> <data name="JsonPath" xml:space="preserve">
<value>The path to the json value.</value> <value>The path to the json value.</value>
@ -408,6 +414,9 @@
<data name="User" xml:space="preserve"> <data name="User" xml:space="preserve">
<value>Information about the current user.</value> <value>Information about the current user.</value>
</data> </data>
<data name="UserAppRole" xml:space="preserve">
<value>The role of the user within the current app.</value>
</data>
<data name="UserClaims" xml:space="preserve"> <data name="UserClaims" xml:space="preserve">
<value>The additional properties of this user.</value> <value>The additional properties of this user.</value>
</data> </data>

2
backend/src/Squidex.Domain.Apps.Core.Operations/Scripting/ContentWrapper/ContentDataObject.cs

@ -139,7 +139,7 @@ public sealed class ContentDataObject : ObjectInstance
foreach (var (key, value) in contentData) foreach (var (key, value) in contentData)
{ {
fieldProperties.Add(key, new ContentDataProperty(this, new ContentFieldObject(this, value, false))); fieldProperties[key] = new ContentDataProperty(this, new ContentFieldObject(this, value, false));
} }
} }
} }

2
backend/src/Squidex.Domain.Apps.Core.Operations/Scripting/IScriptDescriptor.cs

@ -9,7 +9,7 @@
namespace Squidex.Domain.Apps.Core.Scripting; namespace Squidex.Domain.Apps.Core.Scripting;
public delegate void AddDescription(JsonType type, string name, string description); public delegate void AddDescription(JsonType type, string name, string description, string[]? allowedValues = null);
public interface IScriptDescriptor public interface IScriptDescriptor
{ {

2
backend/src/Squidex.Domain.Apps.Core.Operations/Scripting/ScriptExecutionContext.cs

@ -5,9 +5,9 @@
// All rights reserved. Licensed under the MIT license. // All rights reserved. Licensed under the MIT license.
// ========================================================================== // ==========================================================================
using System.Diagnostics;
using Jint; using Jint;
using Squidex.Infrastructure.Tasks; using Squidex.Infrastructure.Tasks;
using System.Diagnostics;
namespace Squidex.Domain.Apps.Core.Scripting; namespace Squidex.Domain.Apps.Core.Scripting;

62
backend/src/Squidex.Domain.Apps.Core.Operations/Scripting/ScriptingCompleter.cs

@ -73,6 +73,13 @@ public sealed class ScriptingCompleter
return new Process(descriptors, dataSchema.Flatten()).ContentTrigger(); return new Process(descriptors, dataSchema.Flatten()).ContentTrigger();
} }
public IReadOnlyList<ScriptingValue> FieldRule(FilterSchema dataSchema)
{
Guard.NotNull(dataSchema);
return new Process(descriptors, dataSchema.Flatten()).FieldRule();
}
public IReadOnlyList<ScriptingValue> AssetScript() public IReadOnlyList<ScriptingValue> AssetScript()
{ {
return new Process(descriptors).AssetScript(); return new Process(descriptors).AssetScript();
@ -112,11 +119,6 @@ public sealed class ScriptingCompleter
this.dataSchema = dataSchema; this.dataSchema = dataSchema;
} }
private IReadOnlyList<ScriptingValue> Build()
{
return result.Values.OrderBy(x => x.Path).ToList();
}
public IReadOnlyList<ScriptingValue> SchemaTrigger() public IReadOnlyList<ScriptingValue> SchemaTrigger()
{ {
AddHelpers(ScriptScope.SchemaTrigger | ScriptScope.Async); AddHelpers(ScriptScope.SchemaTrigger | ScriptScope.Async);
@ -205,6 +207,41 @@ public sealed class ScriptingCompleter
return Build(); return Build();
} }
public IReadOnlyList<ScriptingValue> FieldRule()
{
AddObject("user", FieldDescriptions.User, () =>
{
AddString("id",
FieldDescriptions.UserId);
AddString("email",
FieldDescriptions.UserEmail);
AddString("displayName",
FieldDescriptions.UserDisplayName);
AddString("role",
FieldDescriptions.UserAppRole);
});
AddObject("data", FieldDescriptions.ContentData, () =>
{
AddData();
});
AddObject("itemData", FieldDescriptions.ItemData, () =>
{
AddAny("my-field", FieldDescriptions.ItemDataField);
});
return Build();
}
private IReadOnlyList<ScriptingValue> Build()
{
return result.Values.OrderBy(x => x.Path).ToList();
}
private void AddHelpers(ScriptScope scope) private void AddHelpers(ScriptScope scope)
{ {
foreach (var descriptor in descriptors) foreach (var descriptor in descriptors)
@ -219,7 +256,13 @@ public sealed class ScriptingCompleter
{ {
var propertyType = Nullable.GetUnderlyingType(propertyTypeOrNullable) ?? propertyTypeOrNullable; var propertyType = Nullable.GetUnderlyingType(propertyTypeOrNullable) ?? propertyTypeOrNullable;
if (propertyType.IsEnum || if (propertyType.IsEnum)
{
var allowedValues = Enum.GetValues(propertyType).OfType<object>().Select(x => x.ToString()!).Order().ToArray();
Add(JsonType.String, name, description, allowedValues);
}
else if (
propertyType == typeof(string) || propertyType == typeof(string) ||
propertyType == typeof(DomainId) || propertyType == typeof(DomainId) ||
propertyType == typeof(Instant) || propertyType == typeof(Instant) ||
@ -410,7 +453,7 @@ public sealed class ScriptingCompleter
Add(JsonType.String, name, description); Add(JsonType.String, name, description);
} }
private void Add(JsonType type, string? name, string? description) private void Add(JsonType type, string? name, string? description, string[]? allowedValues = null)
{ {
var parts = name?.Split('.') ?? Array.Empty<string>(); var parts = name?.Split('.') ?? Array.Empty<string>();
@ -426,7 +469,10 @@ public sealed class ScriptingCompleter
var path = string.Concat(prefixes.Reverse()); var path = string.Concat(prefixes.Reverse());
result[path] = new ScriptingValue(path, type, description); result[path] = new ScriptingValue(path, type, description)
{
AllowedValues = allowedValues
};
for (int i = 0; i < parts.Length; i++) for (int i = 0; i < parts.Length; i++)
{ {

5
backend/src/Squidex.Domain.Apps.Core.Operations/Scripting/ScriptingValue.cs

@ -9,4 +9,7 @@
namespace Squidex.Domain.Apps.Core.Scripting; namespace Squidex.Domain.Apps.Core.Scripting;
public sealed record ScriptingValue(string Path, JsonType Type, string? Description); public sealed record ScriptingValue(string Path, JsonType Type, string? Description)
{
public string[]? AllowedValues { get; init; }
}

17
backend/src/Squidex/Areas/Api/Controllers/Schemas/SchemasController.cs

@ -309,7 +309,7 @@ public sealed class SchemasController : ApiController
} }
[HttpGet] [HttpGet]
[Route("apps/{app}/schemas/{schema}/completion")] [Route("apps/{app}/schemas/{schema}/completion/content-scripts")]
[ApiPermissionOrAnonymous] [ApiPermissionOrAnonymous]
[ApiCosts(1)] [ApiCosts(1)]
[ApiExplorerSettings(IgnoreApi = true)] [ApiExplorerSettings(IgnoreApi = true)]
@ -322,7 +322,7 @@ public sealed class SchemasController : ApiController
} }
[HttpGet] [HttpGet]
[Route("apps/{app}/schemas/{schema}/completion/triggers")] [Route("apps/{app}/schemas/{schema}/completion/content-triggers")]
[ApiPermissionOrAnonymous] [ApiPermissionOrAnonymous]
[ApiCosts(1)] [ApiCosts(1)]
[ApiExplorerSettings(IgnoreApi = true)] [ApiExplorerSettings(IgnoreApi = true)]
@ -334,6 +334,19 @@ public sealed class SchemasController : ApiController
return Ok(completion); return Ok(completion);
} }
[HttpGet]
[Route("apps/{app}/schemas/{schema}/completion/field-rules")]
[ApiPermissionOrAnonymous]
[ApiCosts(1)]
[ApiExplorerSettings(IgnoreApi = true)]
public async Task<IActionResult> GetScriptFieldRulesCompletion(string app, string schema,
[FromServices] ScriptingCompleter completer)
{
var completion = completer.FieldRule(await BuildModel());
return Ok(completion);
}
[HttpGet] [HttpGet]
[Route("apps/{app}/schemas/{schema}/filters")] [Route("apps/{app}/schemas/{schema}/filters")]
[ApiPermissionOrAnonymous] [ApiPermissionOrAnonymous]

21
backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/Scripting/ScriptingCompleterTests.cs

@ -173,6 +173,27 @@ public class ScriptingCompleterTests
}); });
} }
[Fact]
public void Should_describe_field_rule()
{
var actual = sut.FieldRule(dataSchema);
AssertCompletion(actual,
new[]
{
"data",
"data['my-field']",
"data['my-field'].iv",
"itemData",
"itemData['my-field']",
"user",
"user.displayName",
"user.email",
"user.id",
"user.role",
});
}
[Fact] [Fact]
public void Should_describe_asset_trigger() public void Should_describe_asset_trigger()
{ {

18
frontend/.storybook/main.js

@ -5,18 +5,24 @@
* Copyright (c) Squidex UG (haftungsbeschränkt). All rights reserved. * Copyright (c) Squidex UG (haftungsbeschränkt). All rights reserved.
*/ */
const customConfig = require('./../src/config/webpack.config');
module.exports = { module.exports = {
"stories": [ stories: [
"../src/**/*.stories.mdx", "../src/**/*.stories.mdx",
"../src/**/*.stories.@(js|jsx|ts|tsx)" "../src/**/*.stories.@(js|jsx|ts|tsx)"
], ],
"addons": [ addons: [
"@storybook/addon-links", "@storybook/addon-links",
"@storybook/addon-essentials", "@storybook/addon-essentials",
"@storybook/addon-interactions" "@storybook/addon-interactions"
], ],
"framework": "@storybook/angular", framework: "@storybook/angular",
"core": { core: {
"builder": "@storybook/builder-webpack5" builder: "@storybook/builder-webpack5"
} },
webpackFinal: async (config) => {
customConfig(config, {}, {});
return config;
},
} }

10
frontend/src/app/features/content/pages/content/content-event.component.ts

@ -5,8 +5,8 @@
* Copyright (c) Squidex UG (haftungsbeschränkt). All rights reserved. * Copyright (c) Squidex UG (haftungsbeschränkt). All rights reserved.
*/ */
import { ChangeDetectionStrategy, Component, EventEmitter, Input, OnChanges, Output, SimpleChanges } from '@angular/core'; import { ChangeDetectionStrategy, Component, EventEmitter, Input, Output } from '@angular/core';
import { ContentDto, HistoryEventDto } from '@app/shared'; import { ContentDto, HistoryEventDto, TypedSimpleChanges } from '@app/shared';
@Component({ @Component({
selector: 'sqx-content-event[content][event]', selector: 'sqx-content-event[content][event]',
@ -14,7 +14,7 @@ import { ContentDto, HistoryEventDto } from '@app/shared';
templateUrl: './content-event.component.html', templateUrl: './content-event.component.html',
changeDetection: ChangeDetectionStrategy.OnPush, changeDetection: ChangeDetectionStrategy.OnPush,
}) })
export class ContentEventComponent implements OnChanges { export class ContentEventComponent {
@Output() @Output()
public dataLoad = new EventEmitter(); public dataLoad = new EventEmitter();
@ -29,8 +29,8 @@ export class ContentEventComponent implements OnChanges {
public canLoadOrCompare = false; public canLoadOrCompare = false;
public ngOnChanges(changes: SimpleChanges) { public ngOnChanges(changes: TypedSimpleChanges<this>) {
if (changes['event']) { if (changes.event) {
this.canLoadOrCompare = this.canLoadOrCompare =
(this.event.eventType === 'ContentUpdatedEvent' || (this.event.eventType === 'ContentUpdatedEvent' ||
this.event.eventType === 'ContentCreatedEventV2') && this.event.eventType === 'ContentCreatedEventV2') &&

12
frontend/src/app/features/content/pages/content/editor/content-field.component.ts

@ -5,16 +5,16 @@
* Copyright (c) Squidex UG (haftungsbeschränkt). All rights reserved. * Copyright (c) Squidex UG (haftungsbeschränkt). All rights reserved.
*/ */
import { Component, EventEmitter, HostBinding, Input, OnChanges, Output, SimpleChanges } from '@angular/core'; import { Component, EventEmitter, HostBinding, Input, Output } from '@angular/core';
import { Observable } from 'rxjs'; import { Observable } from 'rxjs';
import { AppLanguageDto, AppsState, changed$, disabled$, EditContentForm, FieldForm, invalid$, LocalStoreService, SchemaDto, Settings, TranslationsService } from '@app/shared'; import { AppLanguageDto, AppsState, changed$, disabled$, EditContentForm, FieldForm, invalid$, LocalStoreService, SchemaDto, Settings, TranslationsService, TypedSimpleChanges } from '@app/shared';
@Component({ @Component({
selector: 'sqx-content-field[form][formContext][formLevel][formModel][language][languages][schema]', selector: 'sqx-content-field[form][formContext][formLevel][formModel][language][languages][schema]',
styleUrls: ['./content-field.component.scss'], styleUrls: ['./content-field.component.scss'],
templateUrl: './content-field.component.html', templateUrl: './content-field.component.html',
}) })
export class ContentFieldComponent implements OnChanges { export class ContentFieldComponent {
@Output() @Output()
public languageChange = new EventEmitter<AppLanguageDto>(); public languageChange = new EventEmitter<AppLanguageDto>();
@ -74,15 +74,15 @@ export class ContentFieldComponent implements OnChanges {
) { ) {
} }
public ngOnChanges(changes: SimpleChanges) { public ngOnChanges(changes: TypedSimpleChanges<this>) {
this.showAllControls = this.localStore.getBoolean(this.showAllControlsKey()); this.showAllControls = this.localStore.getBoolean(this.showAllControlsKey());
if (changes['formModel'] && this.formModel) { if (changes.formModel && this.formModel) {
this.isInvalid = invalid$(this.formModel.form); this.isInvalid = invalid$(this.formModel.form);
this.isDisabled = disabled$(this.formModel.form); this.isDisabled = disabled$(this.formModel.form);
} }
if ((changes['formModel'] || changes['formModelCompare']) && this.formModelCompare) { if ((changes.formModel || changes.formModelCompare) && this.formModelCompare) {
this.isDifferent = changed$(this.formModel.form, this.formModelCompare.form); this.isDifferent = changed$(this.formModel.form, this.formModelCompare.form);
} }
} }

10
frontend/src/app/features/content/pages/content/editor/content-section.component.ts

@ -5,8 +5,8 @@
* Copyright (c) Squidex UG (haftungsbeschränkt). All rights reserved. * Copyright (c) Squidex UG (haftungsbeschränkt). All rights reserved.
*/ */
import { ChangeDetectionStrategy, ChangeDetectorRef, Component, EventEmitter, Input, OnChanges, Output, SimpleChanges } from '@angular/core'; import { ChangeDetectionStrategy, ChangeDetectorRef, Component, EventEmitter, Input, Output } from '@angular/core';
import { AppLanguageDto, EditContentForm, FieldForm, FieldSection, LocalStoreService, RootFieldDto, SchemaDto, Settings, StatefulComponent } from '@app/shared'; import { AppLanguageDto, EditContentForm, FieldForm, FieldSection, LocalStoreService, RootFieldDto, SchemaDto, Settings, StatefulComponent, TypedSimpleChanges } from '@app/shared';
interface State { interface State {
// The when the section is collapsed. // The when the section is collapsed.
@ -19,7 +19,7 @@ interface State {
templateUrl: './content-section.component.html', templateUrl: './content-section.component.html',
changeDetection: ChangeDetectionStrategy.OnPush, changeDetection: ChangeDetectionStrategy.OnPush,
}) })
export class ContentSectionComponent extends StatefulComponent<State> implements OnChanges { export class ContentSectionComponent extends StatefulComponent<State> {
@Output() @Output()
public languageChange = new EventEmitter<AppLanguageDto>(); public languageChange = new EventEmitter<AppLanguageDto>();
@ -64,8 +64,8 @@ export class ContentSectionComponent extends StatefulComponent<State> implements
}); });
} }
public ngOnChanges(changes: SimpleChanges) { public ngOnChanges(changes: TypedSimpleChanges<this>) {
if (changes['formSection' || changes['schema']]) { if (changes.formSection || changes.schema) {
if (this.formSection?.separator && this.schema) { if (this.formSection?.separator && this.schema) {
const isCollapsed = this.localStore.getBoolean(this.isCollapsedKey()); const isCollapsed = this.localStore.getBoolean(this.isCollapsedKey());

10
frontend/src/app/features/content/pages/content/editor/field-copy-button.component.ts

@ -5,15 +5,15 @@
* Copyright (c) Squidex UG (haftungsbeschränkt). All rights reserved. * Copyright (c) Squidex UG (haftungsbeschränkt). All rights reserved.
*/ */
import { Component, Input, OnChanges, SimpleChanges } from '@angular/core'; import { Component, Input } from '@angular/core';
import { AppLanguageDto, FieldForm, ModalModel } from '@app/shared'; import { AppLanguageDto, FieldForm, ModalModel, TypedSimpleChanges } from '@app/shared';
@Component({ @Component({
selector: 'sqx-field-copy-button[formModel][languages]', selector: 'sqx-field-copy-button[formModel][languages]',
styleUrls: ['./field-copy-button.component.scss'], styleUrls: ['./field-copy-button.component.scss'],
templateUrl: './field-copy-button.component.html', templateUrl: './field-copy-button.component.html',
}) })
export class FieldCopyButtonComponent implements OnChanges { export class FieldCopyButtonComponent {
@Input() @Input()
public formModel!: FieldForm; public formModel!: FieldForm;
@ -31,8 +31,8 @@ export class FieldCopyButtonComponent implements OnChanges {
return this.formModel.field.isLocalizable && this.languages.length > 1; return this.formModel.field.isLocalizable && this.languages.length > 1;
} }
public ngOnChanges(changes: SimpleChanges) { public ngOnChanges(changes: TypedSimpleChanges<this>) {
if (changes['languages']) { if (changes.languages) {
this.setCopySource(this.languages[0]?.iso2Code); this.setCopySource(this.languages[0]?.iso2Code);
} }
} }

12
frontend/src/app/features/content/pages/content/inspecting/content-inspection.component.ts

@ -5,10 +5,10 @@
* Copyright (c) Squidex UG (haftungsbeschränkt). All rights reserved. * Copyright (c) Squidex UG (haftungsbeschränkt). All rights reserved.
*/ */
import { ChangeDetectorRef, Component, Input, OnChanges, OnDestroy, SimpleChanges } from '@angular/core'; import { ChangeDetectorRef, Component, Input, OnDestroy } from '@angular/core';
import { BehaviorSubject, combineLatest, of } from 'rxjs'; import { BehaviorSubject, combineLatest, of } from 'rxjs';
import { filter, map, switchMap } from 'rxjs/operators'; import { filter, map, switchMap } from 'rxjs/operators';
import { AppLanguageDto, ContentDto, ContentsService, ContentsState, ErrorDto, ToolbarService } from '@app/shared'; import { AppLanguageDto, ContentDto, ContentsService, ContentsState, ErrorDto, ToolbarService, TypedSimpleChanges } from '@app/shared';
type Mode = 'Content' | 'Data' | 'FlatData'; type Mode = 'Content' | 'Data' | 'FlatData';
@ -17,7 +17,7 @@ type Mode = 'Content' | 'Data' | 'FlatData';
styleUrls: ['./content-inspection.component.scss'], styleUrls: ['./content-inspection.component.scss'],
templateUrl: './content-inspection.component.html', templateUrl: './content-inspection.component.html',
}) })
export class ContentInspectionComponent implements OnChanges, OnDestroy { export class ContentInspectionComponent implements OnDestroy {
private languageChanges$ = new BehaviorSubject<AppLanguageDto | null>(null); private languageChanges$ = new BehaviorSubject<AppLanguageDto | null>(null);
@Input() @Input()
@ -69,12 +69,12 @@ export class ContentInspectionComponent implements OnChanges, OnDestroy {
this.toolbar.remove(this); this.toolbar.remove(this);
} }
public ngOnChanges(changes: SimpleChanges) { public ngOnChanges(changes: TypedSimpleChanges<this>) {
if (changes['language']) { if (changes.language) {
this.languageChanges$.next(this.language); this.languageChanges$.next(this.language);
} }
if (changes['content']) { if (changes.content) {
this.updateActions(); this.updateActions();
} }
} }

10
frontend/src/app/features/content/pages/content/references/content-references.component.ts

@ -5,8 +5,8 @@
* Copyright (c) Squidex UG (haftungsbeschränkt). All rights reserved. * Copyright (c) Squidex UG (haftungsbeschränkt). All rights reserved.
*/ */
import { ChangeDetectionStrategy, ChangeDetectorRef, Component, Input, OnChanges, OnDestroy, OnInit, SimpleChanges } from '@angular/core'; import { ChangeDetectionStrategy, ChangeDetectorRef, Component, Input, OnDestroy, OnInit } from '@angular/core';
import { AppLanguageDto, ComponentContentsState, ContentDto, QuerySynchronizer, Router2State, ToolbarService } from '@app/shared'; import { AppLanguageDto, ComponentContentsState, ContentDto, QuerySynchronizer, Router2State, ToolbarService, TypedSimpleChanges } from '@app/shared';
@Component({ @Component({
selector: 'sqx-content-references[content][language][languages]', selector: 'sqx-content-references[content][language][languages]',
@ -17,7 +17,7 @@ import { AppLanguageDto, ComponentContentsState, ContentDto, QuerySynchronizer,
Router2State, ComponentContentsState, Router2State, ComponentContentsState,
], ],
}) })
export class ContentReferencesComponent implements OnChanges, OnInit, OnDestroy { export class ContentReferencesComponent implements OnInit, OnDestroy {
@Input() @Input()
public content!: ContentDto; public content!: ContentDto;
@ -56,8 +56,8 @@ export class ContentReferencesComponent implements OnChanges, OnInit, OnDestroy
}); });
} }
public ngOnChanges(changes: SimpleChanges) { public ngOnChanges(changes: TypedSimpleChanges<this>) {
if (changes['content'] || changes['mode']) { if (changes.content || changes.mode) {
this.contentsState.schema = { name: this.content.schemaName }; this.contentsState.schema = { name: this.content.schemaName };
const initial = const initial =

4
frontend/src/app/features/content/pages/contents/custom-view-editor.component.ts

@ -6,7 +6,7 @@
*/ */
import { CdkDragDrop, moveItemInArray } from '@angular/cdk/drag-drop'; import { CdkDragDrop, moveItemInArray } from '@angular/cdk/drag-drop';
import { ChangeDetectionStrategy, Component, EventEmitter, Input, OnChanges, Output } from '@angular/core'; import { ChangeDetectionStrategy, Component, EventEmitter, Input, Output } from '@angular/core';
import { TableField } from '@app/shared'; import { TableField } from '@app/shared';
@Component({ @Component({
@ -15,7 +15,7 @@ import { TableField } from '@app/shared';
templateUrl: './custom-view-editor.component.html', templateUrl: './custom-view-editor.component.html',
changeDetection: ChangeDetectionStrategy.OnPush, changeDetection: ChangeDetectionStrategy.OnPush,
}) })
export class CustomViewEditorComponent implements OnChanges { export class CustomViewEditorComponent {
@Output() @Output()
public reset = new EventEmitter(); public reset = new EventEmitter();

12
frontend/src/app/features/content/shared/content-extension.component.ts

@ -5,10 +5,10 @@
* Copyright (c) Squidex UG (haftungsbeschränkt). All rights reserved. * Copyright (c) Squidex UG (haftungsbeschränkt). All rights reserved.
*/ */
import { ChangeDetectionStrategy, Component, ElementRef, HostListener, Input, OnChanges, SimpleChanges, ViewChild } from '@angular/core'; import { ChangeDetectionStrategy, Component, ElementRef, HostListener, Input, ViewChild } from '@angular/core';
import { Router } from '@angular/router'; import { Router } from '@angular/router';
import { ApiUrlConfig, ResourceOwner, Types } from '@app/framework/internal'; import { ApiUrlConfig, ResourceOwner, Types } from '@app/framework/internal';
import { AppsState, AuthService, computeEditorUrl, ContentDto, SchemaDto } from '@app/shared'; import { AppsState, AuthService, computeEditorUrl, ContentDto, SchemaDto, TypedSimpleChanges } from '@app/shared';
@Component({ @Component({
selector: 'sqx-content-extension[content][contentSchema]', selector: 'sqx-content-extension[content][contentSchema]',
@ -16,7 +16,7 @@ import { AppsState, AuthService, computeEditorUrl, ContentDto, SchemaDto } from
templateUrl: './content-extension.component.html', templateUrl: './content-extension.component.html',
changeDetection: ChangeDetectionStrategy.OnPush, changeDetection: ChangeDetectionStrategy.OnPush,
}) })
export class ContentExtensionComponent extends ResourceOwner implements OnChanges { export class ContentExtensionComponent extends ResourceOwner {
private readonly context: any; private readonly context: any;
private isInitialized = false; private isInitialized = false;
@ -50,13 +50,13 @@ export class ContentExtensionComponent extends ResourceOwner implements OnChange
}; };
} }
public ngOnChanges(changes: SimpleChanges) { public ngOnChanges(changes: TypedSimpleChanges<this>) {
if (changes['contentSchema']) { if (changes.contentSchema) {
this.context['schemaName'] = this.contentSchema?.name; this.context['schemaName'] = this.contentSchema?.name;
this.context['schemaId'] = this.contentSchema?.id; this.context['schemaId'] = this.contentSchema?.id;
} }
if (changes['content']) { if (changes.content) {
this.sendContent(); this.sendContent();
} }
} }

12
frontend/src/app/features/content/shared/forms/array-editor.component.ts

@ -6,11 +6,11 @@
*/ */
import { CdkDragDrop } from '@angular/cdk/drag-drop'; import { CdkDragDrop } from '@angular/cdk/drag-drop';
import { ChangeDetectionStrategy, Component, Input, OnChanges, QueryList, SimpleChanges, ViewChildren } from '@angular/core'; import { ChangeDetectionStrategy, Component, Input, QueryList, ViewChildren } from '@angular/core';
import { VirtualScrollerComponent } from 'ngx-virtual-scroller'; import { VirtualScrollerComponent } from 'ngx-virtual-scroller';
import { combineLatest, Observable } from 'rxjs'; import { combineLatest, Observable } from 'rxjs';
import { map } from 'rxjs/operators'; import { map } from 'rxjs/operators';
import { AppLanguageDto, ComponentsFieldPropertiesDto, disabled$, EditContentForm, FieldArrayForm, LocalStoreService, ModalModel, ObjectFormBase, SchemaDto, Settings, sorted, Types } from '@app/shared'; import { AppLanguageDto, ComponentsFieldPropertiesDto, disabled$, EditContentForm, FieldArrayForm, LocalStoreService, ModalModel, ObjectFormBase, SchemaDto, Settings, sorted, TypedSimpleChanges, Types } from '@app/shared';
import { ArrayItemComponent } from './array-item.component'; import { ArrayItemComponent } from './array-item.component';
@Component({ @Component({
@ -19,7 +19,7 @@ import { ArrayItemComponent } from './array-item.component';
templateUrl: './array-editor.component.html', templateUrl: './array-editor.component.html',
changeDetection: ChangeDetectionStrategy.OnPush, changeDetection: ChangeDetectionStrategy.OnPush,
}) })
export class ArrayEditorComponent implements OnChanges { export class ArrayEditorComponent {
@Input() @Input()
public form!: EditContentForm; public form!: EditContentForm;
@ -71,8 +71,8 @@ export class ArrayEditorComponent implements OnChanges {
) { ) {
} }
public ngOnChanges(changes: SimpleChanges) { public ngOnChanges(changes: TypedSimpleChanges<this>) {
if (changes['formModel']) { if (changes.formModel) {
const maxItems = this.formModel.field.properties['maxItems'] || Number.MAX_VALUE; const maxItems = this.formModel.field.properties['maxItems'] || Number.MAX_VALUE;
if (Types.is(this.formModel.field.properties, ComponentsFieldPropertiesDto)) { if (Types.is(this.formModel.field.properties, ComponentsFieldPropertiesDto)) {
@ -90,7 +90,7 @@ export class ArrayEditorComponent implements OnChanges {
})); }));
} }
if (changes['formModel'] || changes['formLevel']) { if (changes.formModel || changes.formLevel) {
this.isCollapsedInitial = this.localStore.getBoolean(this.isCollapsedKey()) || this.formLevel > 0; this.isCollapsedInitial = this.localStore.getBoolean(this.isCollapsedKey()) || this.formLevel > 0;
} }
} }

12
frontend/src/app/features/content/shared/forms/array-item.component.ts

@ -5,10 +5,10 @@
* Copyright (c) Squidex UG (haftungsbeschränkt). All rights reserved. * Copyright (c) Squidex UG (haftungsbeschränkt). All rights reserved.
*/ */
import { ChangeDetectionStrategy, Component, EventEmitter, Input, OnChanges, Output, QueryList, SimpleChanges, ViewChildren } from '@angular/core'; import { ChangeDetectionStrategy, Component, EventEmitter, Input, Output, QueryList, ViewChildren } from '@angular/core';
import { Observable, of } from 'rxjs'; import { Observable, of } from 'rxjs';
import { map } from 'rxjs/operators'; import { map } from 'rxjs/operators';
import { AppLanguageDto, ComponentForm, EditContentForm, FieldDto, FieldFormatter, FieldSection, invalid$, ObjectFormBase, RootFieldDto, Types, valueProjection$ } from '@app/shared'; import { AppLanguageDto, ComponentForm, EditContentForm, FieldDto, FieldFormatter, FieldSection, invalid$, ObjectFormBase, RootFieldDto, TypedSimpleChanges, Types, valueProjection$ } from '@app/shared';
import { ComponentSectionComponent } from './component-section.component'; import { ComponentSectionComponent } from './component-section.component';
@Component({ @Component({
@ -17,7 +17,7 @@ import { ComponentSectionComponent } from './component-section.component';
templateUrl: './array-item.component.html', templateUrl: './array-item.component.html',
changeDetection: ChangeDetectionStrategy.OnPush, changeDetection: ChangeDetectionStrategy.OnPush,
}) })
export class ArrayItemComponent implements OnChanges { export class ArrayItemComponent {
@Output() @Output()
public itemRemove = new EventEmitter(); public itemRemove = new EventEmitter();
@ -81,8 +81,8 @@ export class ArrayItemComponent implements OnChanges {
return this.formModel.collapsedChanges; return this.formModel.collapsedChanges;
} }
public ngOnChanges(changes: SimpleChanges) { public ngOnChanges(changes: TypedSimpleChanges<this>) {
if (changes['formModel']) { if (changes.formModel) {
this.isInvalidForm = invalid$(this.formModel.form); this.isInvalidForm = invalid$(this.formModel.form);
if (Types.is(this.formModel, ComponentForm)) { if (Types.is(this.formModel, ComponentForm)) {
@ -94,7 +94,7 @@ export class ArrayItemComponent implements OnChanges {
this.title = valueProjection$(this.formModel.form, () => getTitle(this.formModel)); this.title = valueProjection$(this.formModel.form, () => getTitle(this.formModel));
} }
if (changes['formModel'] || changes['isCollapsedInitial']) { if (changes.formModel || changes.isCollapsedInitial) {
if (this.isCollapsedInitial && this.formModel.collapsed === null) { if (this.isCollapsedInitial && this.formModel.collapsed === null) {
this.collapse(); this.collapse();
} }

10
frontend/src/app/features/content/shared/forms/component.component.ts

@ -5,9 +5,9 @@
* Copyright (c) Squidex UG (haftungsbeschränkt). All rights reserved. * Copyright (c) Squidex UG (haftungsbeschränkt). All rights reserved.
*/ */
import { ChangeDetectionStrategy, ChangeDetectorRef, Component, Input, OnChanges, QueryList, SimpleChanges, ViewChildren } from '@angular/core'; import { ChangeDetectionStrategy, ChangeDetectorRef, Component, Input, QueryList, ViewChildren } from '@angular/core';
import { Observable } from 'rxjs'; import { Observable } from 'rxjs';
import { AppLanguageDto, ComponentFieldPropertiesDto, ComponentForm, disabled$, EditContentForm, FieldDto, FieldSection, ModalModel, ResourceOwner, SchemaDto, Types } from '@app/shared'; import { AppLanguageDto, ComponentFieldPropertiesDto, ComponentForm, disabled$, EditContentForm, FieldDto, FieldSection, ModalModel, ResourceOwner, SchemaDto, TypedSimpleChanges, Types } from '@app/shared';
import { ComponentSectionComponent } from './component-section.component'; import { ComponentSectionComponent } from './component-section.component';
@Component({ @Component({
@ -16,7 +16,7 @@ import { ComponentSectionComponent } from './component-section.component';
templateUrl: './component.component.html', templateUrl: './component.component.html',
changeDetection: ChangeDetectionStrategy.OnPush, changeDetection: ChangeDetectionStrategy.OnPush,
}) })
export class ComponentComponent extends ResourceOwner implements OnChanges { export class ComponentComponent extends ResourceOwner {
@Input() @Input()
public canUnset?: boolean | null; public canUnset?: boolean | null;
@ -55,8 +55,8 @@ export class ComponentComponent extends ResourceOwner implements OnChanges {
super(); super();
} }
public ngOnChanges(changes: SimpleChanges) { public ngOnChanges(changes: TypedSimpleChanges<this>) {
if (changes['formModel']) { if (changes.formModel) {
this.unsubscribeAll(); this.unsubscribeAll();
this.isDisabled = disabled$(this.formModel.form); this.isDisabled = disabled$(this.formModel.form);

10
frontend/src/app/features/content/shared/forms/field-editor.component.ts

@ -5,17 +5,17 @@
* Copyright (c) Squidex UG (haftungsbeschränkt). All rights reserved. * Copyright (c) Squidex UG (haftungsbeschränkt). All rights reserved.
*/ */
import { Component, ElementRef, EventEmitter, Input, OnChanges, Output, SimpleChanges, ViewChild } from '@angular/core'; import { Component, ElementRef, EventEmitter, Input, Output, ViewChild } from '@angular/core';
import { AbstractControl } from '@angular/forms'; import { AbstractControl } from '@angular/forms';
import { Observable } from 'rxjs'; import { Observable } from 'rxjs';
import { AbstractContentForm, AppLanguageDto, EditContentForm, FieldDto, hasNoValue$, MathHelper, Types } from '@app/shared'; import { AbstractContentForm, AppLanguageDto, EditContentForm, FieldDto, hasNoValue$, MathHelper, TypedSimpleChanges, Types } from '@app/shared';
@Component({ @Component({
selector: 'sqx-field-editor[form][formContext][formLevel][formModel][isComparing][language][languages]', selector: 'sqx-field-editor[form][formContext][formLevel][formModel][isComparing][language][languages]',
styleUrls: ['./field-editor.component.scss'], styleUrls: ['./field-editor.component.scss'],
templateUrl: './field-editor.component.html', templateUrl: './field-editor.component.html',
}) })
export class FieldEditorComponent implements OnChanges { export class FieldEditorComponent {
public readonly uniqueId = MathHelper.guid(); public readonly uniqueId = MathHelper.guid();
@Output() @Output()
@ -65,8 +65,8 @@ export class FieldEditorComponent implements OnChanges {
return this.formModel.form; return this.formModel.form;
} }
public ngOnChanges(changes: SimpleChanges) { public ngOnChanges(changes: TypedSimpleChanges<this>) {
if (changes['formModel']) { if (changes.formModel) {
this.isEmpty = hasNoValue$(this.formModel.form); this.isEmpty = hasNoValue$(this.formModel.form);
} }
} }

18
frontend/src/app/features/content/shared/forms/iframe-editor.component.ts

@ -5,10 +5,10 @@
* Copyright (c) Squidex UG (haftungsbeschränkt). All rights reserved. * Copyright (c) Squidex UG (haftungsbeschränkt). All rights reserved.
*/ */
import { ChangeDetectionStrategy, ChangeDetectorRef, Component, ElementRef, EventEmitter, HostListener, Input, OnChanges, OnDestroy, Output, Renderer2, SimpleChanges, ViewChild } from '@angular/core'; import { ChangeDetectionStrategy, ChangeDetectorRef, Component, ElementRef, EventEmitter, HostListener, Input, OnDestroy, Output, Renderer2, ViewChild } from '@angular/core';
import { AbstractControl } from '@angular/forms'; import { AbstractControl } from '@angular/forms';
import { Router } from '@angular/router'; import { Router } from '@angular/router';
import { DialogModel, DialogService, disabled$, StatefulComponent, Types, value$ } from '@app/framework'; import { DialogModel, DialogService, disabled$, StatefulComponent, TypedSimpleChanges, Types, value$ } from '@app/framework';
import { AppLanguageDto, AppsState, AssetDto, computeEditorUrl, ContentDto } from '@app/shared'; import { AppLanguageDto, AppsState, AssetDto, computeEditorUrl, ContentDto } from '@app/shared';
interface State { interface State {
@ -22,7 +22,7 @@ interface State {
templateUrl: './iframe-editor.component.html', templateUrl: './iframe-editor.component.html',
changeDetection: ChangeDetectionStrategy.OnPush, changeDetection: ChangeDetectionStrategy.OnPush,
}) })
export class IFrameEditorComponent extends StatefulComponent<State> implements OnChanges, OnDestroy { export class IFrameEditorComponent extends StatefulComponent<State> implements OnDestroy {
private value: any; private value: any;
private isInitialized = false; private isInitialized = false;
private isDisabled = false; private isDisabled = false;
@ -99,24 +99,24 @@ export class IFrameEditorComponent extends StatefulComponent<State> implements O
this.toggleFullscreen(false); this.toggleFullscreen(false);
} }
public ngOnChanges(changes: SimpleChanges) { public ngOnChanges(changes: TypedSimpleChanges<this>) {
if (changes['formValue']) { if (changes.formValue) {
this.sendFormValue(); this.sendFormValue();
} }
if (changes['formIndex']) { if (changes.formIndex) {
this.sendMoved(); this.sendMoved();
} }
if (changes['expanded']) { if (changes.isExpanded) {
this.sendExpanded(); this.sendExpanded();
} }
if (changes['language']) { if (changes.language) {
this.sendLanguage(); this.sendLanguage();
} }
if (changes['formControlBinding']) { if (changes.formControlBinding) {
this.unsubscribeAll(); this.unsubscribeAll();
const control = this.formControlBinding; const control = this.formControlBinding;

12
frontend/src/app/features/content/shared/list/content.component.ts

@ -5,8 +5,8 @@
* Copyright (c) Squidex UG (haftungsbeschränkt). All rights reserved. * Copyright (c) Squidex UG (haftungsbeschränkt). All rights reserved.
*/ */
import { ChangeDetectionStrategy, ChangeDetectorRef, Component, EventEmitter, Input, OnChanges, Output, QueryList, SimpleChanges, ViewChildren } from '@angular/core'; import { ChangeDetectionStrategy, ChangeDetectorRef, Component, EventEmitter, Input, Output, QueryList, ViewChildren } from '@angular/core';
import { AppLanguageDto, ContentDto, ContentListFieldComponent, ContentsState, ModalModel, PatchContentForm, SchemaDto, TableField, TableSettings } from '@app/shared'; import { AppLanguageDto, ContentDto, ContentListFieldComponent, ContentsState, ModalModel, PatchContentForm, SchemaDto, TableField, TableSettings, TypedSimpleChanges } from '@app/shared';
/* tslint:disable: component-selector */ /* tslint:disable: component-selector */
@ -16,7 +16,7 @@ import { AppLanguageDto, ContentDto, ContentListFieldComponent, ContentsState, M
templateUrl: './content.component.html', templateUrl: './content.component.html',
changeDetection: ChangeDetectionStrategy.OnPush, changeDetection: ChangeDetectionStrategy.OnPush,
}) })
export class ContentComponent implements OnChanges { export class ContentComponent {
@Output() @Output()
public clone = new EventEmitter(); public clone = new EventEmitter();
@ -74,12 +74,12 @@ export class ContentComponent implements OnChanges {
) { ) {
} }
public ngOnChanges(changes: SimpleChanges) { public ngOnChanges(changes: TypedSimpleChanges<this>) {
if (changes['content']) { if (changes.content) {
this.patchAllowed = this.content.canUpdate; this.patchAllowed = this.content.canUpdate;
} }
if (this.patchAllowed && (changes['listFields'] || changes['language'])) { if (this.patchAllowed && (changes.tableFields || changes.language)) {
this.patchForm = new PatchContentForm(this.tableFields, this.language); this.patchForm = new PatchContentForm(this.tableFields, this.language);
} }
} }

12
frontend/src/app/features/content/shared/references/reference-dropdown.component.ts

@ -5,11 +5,11 @@
* Copyright (c) Squidex UG (haftungsbeschränkt). All rights reserved. * Copyright (c) Squidex UG (haftungsbeschränkt). All rights reserved.
*/ */
import { ChangeDetectionStrategy, ChangeDetectorRef, Component, forwardRef, Input, OnChanges, SimpleChanges } from '@angular/core'; import { ChangeDetectionStrategy, ChangeDetectorRef, Component, forwardRef, Input } from '@angular/core';
import { NG_VALUE_ACCESSOR, UntypedFormControl } from '@angular/forms'; import { NG_VALUE_ACCESSOR, UntypedFormControl } from '@angular/forms';
import { Observable } from 'rxjs'; import { Observable } from 'rxjs';
import { ContentsDto } from '@app/shared'; import { ContentsDto } from '@app/shared';
import { ContentDto, getContentValue, LanguageDto, LocalizerService, ResolveContents, StatefulControlComponent, Types, value$ } from '@app/shared/internal'; import { ContentDto, getContentValue, LanguageDto, LocalizerService, ResolveContents, StatefulControlComponent, TypedSimpleChanges, Types, value$ } from '@app/shared/internal';
export const SQX_REFERENCE_DROPDOWN_CONTROL_VALUE_ACCESSOR: any = { export const SQX_REFERENCE_DROPDOWN_CONTROL_VALUE_ACCESSOR: any = {
provide: NG_VALUE_ACCESSOR, useExisting: forwardRef(() => ReferenceDropdownComponent), multi: true, provide: NG_VALUE_ACCESSOR, useExisting: forwardRef(() => ReferenceDropdownComponent), multi: true,
@ -39,7 +39,7 @@ const NO_EMIT = { emitEvent: false };
], ],
changeDetection: ChangeDetectionStrategy.OnPush, changeDetection: ChangeDetectionStrategy.OnPush,
}) })
export class ReferenceDropdownComponent extends StatefulControlComponent<State, ReadonlyArray<string> | string> implements OnChanges { export class ReferenceDropdownComponent extends StatefulControlComponent<State, ReadonlyArray<string> | string> {
private readonly contents: ContentDto[] = []; private readonly contents: ContentDto[] = [];
private isOpenedBefore = false; private isOpenedBefore = false;
private isLoadingFailed = false; private isLoadingFailed = false;
@ -96,14 +96,14 @@ export class ReferenceDropdownComponent extends StatefulControlComponent<State,
})); }));
} }
public ngOnChanges(changes: SimpleChanges) { public ngOnChanges(changes: TypedSimpleChanges<this>) {
if (changes['schemaId']) { if (changes.schemaId) {
this.contents.clear(); this.contents.clear();
this.resetState(); this.resetState();
} }
if (changes['language'] || changes['schemaId']) { if (changes.language || changes.schemaId) {
this.resetContentNames(true); this.resetContentNames(true);
} }
} }

4
frontend/src/app/features/content/shared/references/reference-item.component.ts

@ -7,7 +7,7 @@
/* tslint:disable: component-selector */ /* tslint:disable: component-selector */
import { ChangeDetectionStrategy, Component, EventEmitter, Input, OnChanges, Output } from '@angular/core'; import { ChangeDetectionStrategy, Component, EventEmitter, Input, Output } from '@angular/core';
import { ContentDto, getContentValue, LanguageDto, META_FIELDS } from '@app/shared'; import { ContentDto, getContentValue, LanguageDto, META_FIELDS } from '@app/shared';
@Component({ @Component({
@ -16,7 +16,7 @@ import { ContentDto, getContentValue, LanguageDto, META_FIELDS } from '@app/shar
templateUrl: './reference-item.component.html', templateUrl: './reference-item.component.html',
changeDetection: ChangeDetectionStrategy.OnPush, changeDetection: ChangeDetectionStrategy.OnPush,
}) })
export class ReferenceItemComponent implements OnChanges { export class ReferenceItemComponent {
public readonly metaFields = META_FIELDS; public readonly metaFields = META_FIELDS;
@Output() @Output()

2
frontend/src/app/features/content/shared/references/references-checkboxes.component.html

@ -1,3 +1,3 @@
<sqx-checkbox-group [formControl]="control" <sqx-checkbox-group [formControl]="control"
[values]="snapshot.converter.suggestions"> [values]="snapshot.converter.tags">
</sqx-checkbox-group> </sqx-checkbox-group>

10
frontend/src/app/features/content/shared/references/references-checkboxes.component.ts

@ -5,9 +5,9 @@
* Copyright (c) Squidex UG (haftungsbeschränkt). All rights reserved. * Copyright (c) Squidex UG (haftungsbeschränkt). All rights reserved.
*/ */
import { ChangeDetectionStrategy, ChangeDetectorRef, Component, forwardRef, Input, OnChanges, SimpleChanges } from '@angular/core'; import { ChangeDetectionStrategy, ChangeDetectorRef, Component, forwardRef, Input } from '@angular/core';
import { NG_VALUE_ACCESSOR, UntypedFormControl } from '@angular/forms'; import { NG_VALUE_ACCESSOR, UntypedFormControl } from '@angular/forms';
import { AppsState, ContentDto, ContentsService, LanguageDto, LocalizerService, StatefulControlComponent, UIOptions } from '@app/shared/internal'; import { AppsState, ContentDto, ContentsService, LanguageDto, LocalizerService, StatefulControlComponent, TypedSimpleChanges, UIOptions } from '@app/shared/internal';
import { ReferencesTagsConverter } from './references-tag-converter'; import { ReferencesTagsConverter } from './references-tag-converter';
export const SQX_REFERENCES_CHECKBOXES_CONTROL_VALUE_ACCESSOR: any = { export const SQX_REFERENCES_CHECKBOXES_CONTROL_VALUE_ACCESSOR: any = {
@ -30,7 +30,7 @@ const NO_EMIT = { emitEvent: false };
], ],
changeDetection: ChangeDetectionStrategy.OnPush, changeDetection: ChangeDetectionStrategy.OnPush,
}) })
export class ReferencesCheckboxesComponent extends StatefulControlComponent<State, ReadonlyArray<string>> implements OnChanges { export class ReferencesCheckboxesComponent extends StatefulControlComponent<State, ReadonlyArray<string>> {
private readonly itemCount: number; private readonly itemCount: number;
private contentItems: ReadonlyArray<ContentDto> | null = null; private contentItems: ReadonlyArray<ContentDto> | null = null;
@ -75,8 +75,8 @@ export class ReferencesCheckboxesComponent extends StatefulControlComponent<Stat
})); }));
} }
public ngOnChanges(changes: SimpleChanges) { public ngOnChanges(changes: TypedSimpleChanges<this>) {
if (changes['schemaId']) { if (changes.schemaId) {
this.resetState(); this.resetState();
if (this.isValid) { if (this.isValid) {

8
frontend/src/app/features/content/shared/references/references-tag-converter.ts

@ -8,22 +8,22 @@
import { ContentDto, getContentValue, LanguageDto, LocalizerService, TagConverter, TagValue } from '@app/shared/internal'; import { ContentDto, getContentValue, LanguageDto, LocalizerService, TagConverter, TagValue } from '@app/shared/internal';
export class ReferencesTagsConverter implements TagConverter { export class ReferencesTagsConverter implements TagConverter {
public suggestions: ReadonlyArray<TagValue> = []; public tags: ReadonlyArray<TagValue> = [];
constructor(language: LanguageDto, contents: ReadonlyArray<ContentDto>, constructor(language: LanguageDto, contents: ReadonlyArray<ContentDto>,
private readonly localizer: LocalizerService, private readonly localizer: LocalizerService,
) { ) {
this.suggestions = this.createTags(language, contents); this.tags = this.createTags(language, contents);
} }
public convertInput(input: string) { public convertInput(input: string) {
const result = this.suggestions.find(x => x.name === input); const result = this.tags.find(x => x.name === input);
return result || null; return result || null;
} }
public convertValue(value: any) { public convertValue(value: any) {
const result = this.suggestions.find(x => x.id === value); const result = this.tags.find(x => x.id === value);
return result || null; return result || null;
} }

2
frontend/src/app/features/content/shared/references/references-tags.component.html

@ -4,6 +4,6 @@
[allowOpen]="true" [allowOpen]="true"
[formControl]="control" [formControl]="control"
[itemConverter]="snapshot.converter" [itemConverter]="snapshot.converter"
[itemsSource]="snapshot.converter.suggestions" [itemsSource]="snapshot.converter.tags"
[itemsSourceLoading]="snapshot.isLoading"> [itemsSourceLoading]="snapshot.isLoading">
</sqx-tag-editor> </sqx-tag-editor>

12
frontend/src/app/features/content/shared/references/references-tags.component.ts

@ -5,10 +5,10 @@
* Copyright (c) Squidex UG (haftungsbeschränkt). All rights reserved. * Copyright (c) Squidex UG (haftungsbeschränkt). All rights reserved.
*/ */
import { ChangeDetectionStrategy, ChangeDetectorRef, Component, forwardRef, Input, OnChanges, SimpleChanges } from '@angular/core'; import { ChangeDetectionStrategy, ChangeDetectorRef, Component, forwardRef, Input } from '@angular/core';
import { NG_VALUE_ACCESSOR, UntypedFormControl } from '@angular/forms'; import { NG_VALUE_ACCESSOR, UntypedFormControl } from '@angular/forms';
import { Observable } from 'rxjs'; import { Observable } from 'rxjs';
import { Types } from '@app/framework'; import { TypedSimpleChanges, Types } from '@app/framework';
import { ContentDto, ContentsDto, LanguageDto, LocalizerService, ResolveContents, StatefulControlComponent } from '@app/shared/internal'; import { ContentDto, ContentsDto, LanguageDto, LocalizerService, ResolveContents, StatefulControlComponent } from '@app/shared/internal';
import { ReferencesTagsConverter } from './references-tag-converter'; import { ReferencesTagsConverter } from './references-tag-converter';
@ -35,7 +35,7 @@ const NO_EMIT = { emitEvent: false };
], ],
changeDetection: ChangeDetectionStrategy.OnPush, changeDetection: ChangeDetectionStrategy.OnPush,
}) })
export class ReferencesTagsComponent extends StatefulControlComponent<State, ReadonlyArray<string>> implements OnChanges { export class ReferencesTagsComponent extends StatefulControlComponent<State, ReadonlyArray<string>> {
private readonly contents: ContentDto[] = []; private readonly contents: ContentDto[] = [];
private isOpenedBefore = false; private isOpenedBefore = false;
private isLoadingFailed = false; private isLoadingFailed = false;
@ -81,14 +81,14 @@ export class ReferencesTagsComponent extends StatefulControlComponent<State, Rea
})); }));
} }
public ngOnChanges(changes: SimpleChanges) { public ngOnChanges(changes: TypedSimpleChanges<this>) {
if (changes['schemaId']) { if (changes.schemaId) {
this.contents.clear(); this.contents.clear();
this.resetState(); this.resetState();
} }
if (changes['language'] || changes['schemaId']) { if (changes.language || changes.schemaId) {
this.resetConverterState(true); this.resetConverterState(true);
} }
} }

4
frontend/src/app/features/dashboard/pages/cards/history-card.component.ts

@ -5,7 +5,7 @@
* Copyright (c) Squidex UG (haftungsbeschränkt). All rights reserved. * Copyright (c) Squidex UG (haftungsbeschränkt). All rights reserved.
*/ */
import { ChangeDetectionStrategy, Component, Input, OnChanges } from '@angular/core'; import { ChangeDetectionStrategy, Component, Input } from '@angular/core';
import { Observable } from 'rxjs'; import { Observable } from 'rxjs';
import { AppDto, HistoryEventDto, HistoryService } from '@app/shared'; import { AppDto, HistoryEventDto, HistoryService } from '@app/shared';
@ -15,7 +15,7 @@ import { AppDto, HistoryEventDto, HistoryService } from '@app/shared';
templateUrl: './history-card.component.html', templateUrl: './history-card.component.html',
changeDetection: ChangeDetectionStrategy.OnPush, changeDetection: ChangeDetectionStrategy.OnPush,
}) })
export class HistoryCardComponent implements OnChanges { export class HistoryCardComponent {
@Input() @Input()
public app!: AppDto; public app!: AppDto;

12
frontend/src/app/features/dashboard/pages/dashboard-config.component.ts

@ -5,17 +5,17 @@
* Copyright (c) Squidex UG (haftungsbeschränkt). All rights reserved. * Copyright (c) Squidex UG (haftungsbeschränkt). All rights reserved.
*/ */
import { Component, EventEmitter, Input, OnChanges, Output, SimpleChanges } from '@angular/core'; import { Component, EventEmitter, Input, Output } from '@angular/core';
import { GridsterItem } from 'angular-gridster2'; import { GridsterItem } from 'angular-gridster2';
import { take } from 'rxjs/operators'; import { take } from 'rxjs/operators';
import { AppDto, AppsState, AuthService, DialogModel, DialogService, LocalizerService, ModalModel, Types, UIState } from '@app/shared'; import { AppDto, AppsState, AuthService, DialogModel, DialogService, LocalizerService, ModalModel, TypedSimpleChanges, Types, UIState } from '@app/shared';
@Component({ @Component({
selector: 'sqx-dashboard-config[app][config]', selector: 'sqx-dashboard-config[app][config]',
styleUrls: ['./dashboard-config.component.scss'], styleUrls: ['./dashboard-config.component.scss'],
templateUrl: './dashboard-config.component.html', templateUrl: './dashboard-config.component.html',
}) })
export class DashboardConfigComponent implements OnChanges { export class DashboardConfigComponent {
@Input() @Input()
public app!: AppDto; public app!: AppDto;
@ -50,8 +50,8 @@ export class DashboardConfigComponent implements OnChanges {
) { ) {
} }
public ngOnChanges(changes: SimpleChanges) { public ngOnChanges(changes: TypedSimpleChanges<this>) {
if (changes['configAvailable']) { if (changes.configAvailable) {
this.configOptions = this.configAvailable.map(item => { this.configOptions = this.configAvailable.map(item => {
const name = this.localizer.getOrKey(item.name); const name = this.localizer.getOrKey(item.name);
@ -59,7 +59,7 @@ export class DashboardConfigComponent implements OnChanges {
}).sortByString(x => x.name); }).sortByString(x => x.name);
} }
if (changes['app']) { if (changes.app) {
this.uiState.getUser('dashboard.grid', this.configDefaults).pipe(take(1)) this.uiState.getUser('dashboard.grid', this.configDefaults).pipe(take(1))
.subscribe(dto => { .subscribe(dto => {
this.setConfig(dto); this.setConfig(dto);

1
frontend/src/app/features/rules/declarations.ts

@ -20,6 +20,7 @@ export * from './shared/rule-icon.component';
export * from './shared/pipes'; export * from './shared/pipes';
export * from './shared/triggers/asset-changed-trigger.component'; export * from './shared/triggers/asset-changed-trigger.component';
export * from './shared/triggers/comment-trigger.component'; export * from './shared/triggers/comment-trigger.component';
export * from './shared/triggers/content-changed-schema.component';
export * from './shared/triggers/content-changed-trigger.component'; export * from './shared/triggers/content-changed-trigger.component';
export * from './shared/triggers/schema-changed-trigger.component'; export * from './shared/triggers/schema-changed-trigger.component';
export * from './shared/triggers/usage-trigger.component'; export * from './shared/triggers/usage-trigger.component';

3
frontend/src/app/features/rules/module.ts

@ -8,7 +8,7 @@
import { NgModule } from '@angular/core'; import { NgModule } from '@angular/core';
import { RouterModule, Routes } from '@angular/router'; import { RouterModule, Routes } from '@angular/router';
import { HelpComponent, RuleMustExistGuard, SqxFrameworkModule, SqxSharedModule } from '@app/shared'; import { HelpComponent, RuleMustExistGuard, SqxFrameworkModule, SqxSharedModule } from '@app/shared';
import { AssetChangedTriggerComponent, CommentTriggerComponent, ContentChangedTriggerComponent, GenericActionComponent, RuleClassPipe, RuleComponent, RuleElementComponent, RuleEventsPageComponent, RuleIconComponent, RuleSimulatorPageComponent, RulesPageComponent, RuleTransitionComponent, SchemaChangedTriggerComponent, SimulatedRuleEventStatusPipe, UsageTriggerComponent } from './declarations'; import { AssetChangedTriggerComponent, CommentTriggerComponent, ContentChangedSchemaComponent, ContentChangedTriggerComponent, GenericActionComponent, RuleClassPipe, RuleComponent, RuleElementComponent, RuleEventsPageComponent, RuleIconComponent, RuleSimulatorPageComponent, RulesPageComponent, RuleTransitionComponent, SchemaChangedTriggerComponent, SimulatedRuleEventStatusPipe, UsageTriggerComponent } from './declarations';
import { RuleEventComponent } from './pages/events/rule-event.component'; import { RuleEventComponent } from './pages/events/rule-event.component';
import { RulePageComponent } from './pages/rule/rule-page.component'; import { RulePageComponent } from './pages/rule/rule-page.component';
import { SimulatedRuleEventComponent } from './pages/simulator/simulated-rule-event.component'; import { SimulatedRuleEventComponent } from './pages/simulator/simulated-rule-event.component';
@ -68,6 +68,7 @@ const routes: Routes = [
declarations: [ declarations: [
AssetChangedTriggerComponent, AssetChangedTriggerComponent,
CommentTriggerComponent, CommentTriggerComponent,
ContentChangedSchemaComponent,
ContentChangedTriggerComponent, ContentChangedTriggerComponent,
FormattableInputComponent, FormattableInputComponent,
GenericActionComponent, GenericActionComponent,

10
frontend/src/app/features/rules/pages/simulator/rule-transition.component.ts

@ -5,8 +5,8 @@
* Copyright (c) Squidex UG (haftungsbeschränkt). All rights reserved. * Copyright (c) Squidex UG (haftungsbeschränkt). All rights reserved.
*/ */
import { ChangeDetectionStrategy, Component, Input, OnChanges, SimpleChanges } from '@angular/core'; import { ChangeDetectionStrategy, Component, Input } from '@angular/core';
import { SimulatedRuleEventDto } from '@app/shared'; import { SimulatedRuleEventDto, TypedSimpleChanges } from '@app/shared';
@Component({ @Component({
selector: 'sqx-rule-transition', selector: 'sqx-rule-transition',
@ -14,7 +14,7 @@ import { SimulatedRuleEventDto } from '@app/shared';
templateUrl: './rule-transition.component.html', templateUrl: './rule-transition.component.html',
changeDetection: ChangeDetectionStrategy.OnPush, changeDetection: ChangeDetectionStrategy.OnPush,
}) })
export class RuleTransitionComponent implements OnChanges { export class RuleTransitionComponent {
@Input() @Input()
public event: SimulatedRuleEventDto | undefined | null; public event: SimulatedRuleEventDto | undefined | null;
@ -26,8 +26,8 @@ export class RuleTransitionComponent implements OnChanges {
public filteredErrors?: string[] | null; public filteredErrors?: string[] | null;
public ngOnChanges(changes: SimpleChanges) { public ngOnChanges(changes: TypedSimpleChanges<this>) {
if (changes['event'] || changes['errors']) { if (changes.event || changes.errors) {
const errors = this.errors; const errors = this.errors;
if (!errors) { if (!errors) {

2
frontend/src/app/features/rules/shared/actions/formattable-input.component.html

@ -1,6 +1,6 @@
<ng-container *ngIf="type === 'Text'; else code"> <ng-container *ngIf="type === 'Text'; else code">
<div class="input-group"> <div class="input-group">
<input class="form-control" ngDefaultControl /> <sqx-code-editor [completion]="actualCompletion" [singleLine]="true" [mode]="aceMode"></sqx-code-editor>
<div class="input-group-apppend"> <div class="input-group-apppend">
<select class="form-select" [ngModel]="mode" (ngModelChange)="setMode($event)" [disabled]="disabled" tabindex="-1"> <select class="form-select" [ngModel]="mode" (ngModelChange)="setMode($event)" [disabled]="disabled" tabindex="-1">

4
frontend/src/app/features/rules/shared/actions/formattable-input.component.scss

@ -1,6 +1,10 @@
@import 'mixins'; @import 'mixins';
@import 'vars'; @import 'vars';
sqx-code-editor {
flex-grow: 1;
}
.input-group-apppend { .input-group-apppend {
z-index: 1; z-index: 1;

2
frontend/src/app/features/rules/shared/actions/formattable-input.component.ts

@ -35,7 +35,7 @@ export class FormattableInputComponent implements ControlValueAccessor, AfterVie
public type: 'Text' | 'Code' = 'Text'; public type: 'Text' | 'Code' = 'Text';
@Input() @Input()
public completion: ReadonlyArray<{ path: string; description: string }> | undefined | null; public completion: ReadonlyArray<{ path: string; description: string; type: string }> | undefined | null;
@ViewChild(DefaultValueAccessor) @ViewChild(DefaultValueAccessor)
public inputEditor!: DefaultValueAccessor; public inputEditor!: DefaultValueAccessor;

10
frontend/src/app/features/rules/shared/actions/generic-action.component.ts

@ -5,9 +5,9 @@
* Copyright (c) Squidex UG (haftungsbeschränkt). All rights reserved. * Copyright (c) Squidex UG (haftungsbeschränkt). All rights reserved.
*/ */
import { ChangeDetectionStrategy, Component, Input, OnChanges, SimpleChanges } from '@angular/core'; import { ChangeDetectionStrategy, Component, Input } from '@angular/core';
import { EMPTY, Observable, shareReplay } from 'rxjs'; import { EMPTY, Observable, shareReplay } from 'rxjs';
import { ActionForm, RuleCompletions, RulesService } from '@app/shared'; import { ActionForm, RuleCompletions, RulesService, TypedSimpleChanges } from '@app/shared';
@Component({ @Component({
selector: 'sqx-generic-action[actionForm][appName][trigger][triggerType]', selector: 'sqx-generic-action[actionForm][appName][trigger][triggerType]',
@ -15,7 +15,7 @@ import { ActionForm, RuleCompletions, RulesService } from '@app/shared';
templateUrl: './generic-action.component.html', templateUrl: './generic-action.component.html',
changeDetection: ChangeDetectionStrategy.OnPush, changeDetection: ChangeDetectionStrategy.OnPush,
}) })
export class GenericActionComponent implements OnChanges { export class GenericActionComponent {
@Input() @Input()
public actionForm!: ActionForm; public actionForm!: ActionForm;
@ -35,8 +35,8 @@ export class GenericActionComponent implements OnChanges {
) { ) {
} }
public ngOnChanges(changes: SimpleChanges) { public ngOnChanges(changes: TypedSimpleChanges<this>) {
if (changes['appName'] || changes['triggerType']) { if (changes.appName || changes.triggerType) {
if (this.triggerType) { if (this.triggerType) {
this.ruleCompletions = this.rulesService.getCompletions(this.appName, this.triggerType).pipe(shareReplay(1)); this.ruleCompletions = this.rulesService.getCompletions(this.appName, this.triggerType).pipe(shareReplay(1));
} else { } else {

40
frontend/src/app/features/rules/shared/triggers/completions-cache.ts

@ -0,0 +1,40 @@
/*
* Squidex Headless CMS
*
* @license
* Copyright (c) Squidex UG (haftungsbeschränkt). All rights reserved.
*/
import { Injectable } from '@angular/core';
import { EMPTY, map, Observable, shareReplay } from 'rxjs';
import { SchemaCompletions, SchemasService, SchemasState } from '@app/shared';
@Injectable()
export class CompletionsCache {
private readonly cache: { [schema: string]: Observable<SchemaCompletions> } = {};
constructor(
private readonly schemasService: SchemasService,
private readonly schemasState: SchemasState,
) {
}
public getCompletions(schema: string, skipFunctions: boolean) {
if (!schema) {
return EMPTY;
}
let result = this.cache[schema];
if (!result) {
result = this.schemasService.getContentTriggerCompletion(this.schemasState.appName, schema).pipe(shareReplay(1));
this.cache[schema] = result;
}
if (skipFunctions) {
result = result.pipe(map(x => x.filter(c => c.type !== 'Function')));
}
return result;
}
}

29
frontend/src/app/features/rules/shared/triggers/content-changed-schema.component.html

@ -0,0 +1,29 @@
<div class="row gx-2" [formGroup]="form">
<div class="col col-3">
<sqx-control-errors for="action"></sqx-control-errors>
<select class="form-select schemas-control" formControlName="schemaId">
<option *ngFor="let schema of schemas; trackBy: trackBySchema" [ngValue]="schema.id">{{schema.displayName}}</option>
</select>
</div>
<div class="col-auto">
<div class="label text-muted">{{ 'schemas.rules.when' | sqxTranslate }}</div>
</div>
<div class="col">
<sqx-control-errors for="condition"></sqx-control-errors>
<sqx-code-editor [singleLine]="true" formControlName="condition" [completion]="completions | async"></sqx-code-editor>
</div>
<div class="col-auto">
<button type="button" class="btn btn-text-danger" [disabled]="form.disabled"
(sqxConfirmClick)="remove.emit()"
confirmTitle="i18n:rules.deleteContentChangedSchemaTitle"
confirmText="i18n:rules.deleteContentChangedSchemaText"
confirmRememberKey="deleteContentChangedSchema">
<i class="icon-bin2"></i>
</button>
</div>
</div>

8
frontend/src/app/features/rules/shared/triggers/content-changed-schema.component.scss

@ -0,0 +1,8 @@
@import 'mixins';
@import 'vars';
.label {
font-size: $font-small;
font-weight: normal;
line-height: 2.4rem;
}

47
frontend/src/app/features/rules/shared/triggers/content-changed-schema.component.ts

@ -0,0 +1,47 @@
/*
* Squidex Headless CMS
*
* @license
* Copyright (c) Squidex UG (haftungsbeschränkt). All rights reserved.
*/
import { Component, EventEmitter, Input, Output } from '@angular/core';
import { FormGroup } from '@angular/forms';
import { EMPTY, Observable, switchMap } from 'rxjs';
import { SchemaCompletions, SchemaDto, TypedSimpleChanges, value$ } from '@app/shared';
import { CompletionsCache } from './completions-cache';
@Component({
selector: 'sqx-content-changed-schema[form][schemas]',
styleUrls: ['./content-changed-schema.component.scss'],
templateUrl: './content-changed-schema.component.html',
})
export class ContentChangedSchemaComponent {
@Input()
public schemas?: ReadonlyArray<SchemaDto> | null;
@Input()
public form!: FormGroup;
@Output()
public remove = new EventEmitter<any>();
public completions: Observable<SchemaCompletions> = EMPTY;
constructor(
private readonly completionsCache: CompletionsCache,
) {
}
public ngOnChanges(changes: TypedSimpleChanges<this>) {
if (changes.form) {
this.completions =
value$(this.form.controls['schemaId']).pipe(
switchMap(x => this.completionsCache.getCompletions(x, true)));
}
}
public trackBySchema(_index: number, schema: SchemaDto) {
return schema.id;
}
}

63
frontend/src/app/features/rules/shared/triggers/content-changed-trigger.component.html

@ -1,55 +1,28 @@
<ng-container *ngIf="!triggerForm.form.controls.handleAll.value"> <ng-container *ngIf="!triggerForm.form.controls.handleAll.value">
<table class="table table-middle table-sm table-fixed table-borderless"> <div class="mb-2" *ngFor="let form of schemasForm.controls; let i = index">
<colgroup> <sqx-content-changed-schema [schemas]="schemas" [form]="$any(form)" (remove)="remove(i)"></sqx-content-changed-schema>
<col style="width: 40%;"> </div>
<col style="width: 60%;">
<col style="width: 40px;">
</colgroup>
<tr> <div class="form-group row gx-2 align-items-center">
<td> <div class="col-3">
<label>{{ 'common.schema' | sqxTranslate }}</label> <div class="form-control preview">{{ 'common.schema' | sqxTranslate }}</div>
</td> </div>
<td>
<label>{{ 'common.condition' | sqxTranslate }}</label>
</td>
<th></th>
</tr>
<tr *ngFor="let triggerSchema of triggerSchemas"> <div class="col-auto">
<td> <div class="label text-muted">{{ 'schemas.rules.when' | sqxTranslate }}</div>
<span class="truncate">{{triggerSchema.schema.displayName}}</span> </div>
</td>
<td class="text-center">
<input class="form-control code" placeholder="{{ 'rules.conditionHint' | sqxTranslate }}"
[disabled]="triggerForm.form.disabled"
[ngModelOptions]="{ updateOn: 'blur' }"
[ngModel]="triggerSchema.condition"
(ngModelChange)="updateCondition(triggerSchema.schema, $event)">
</td>
<td class="text-center">
<button type="button" class="btn btn-text-secondary" (click)="removeSchema(triggerSchema)" [disabled]="triggerForm.form.disabled">
<i class="icon-close"></i>
</button>
</td>
</tr>
</table>
<div class="section" *ngIf="schemasToAdd.length > 0"> <div class="col">
<form class="row g-2" (ngSubmit)="addSchema()"> <div class="form-control preview">{{ 'schemas.rules.condition' | sqxTranslate }}</div>
<div class="col"> </div>
<select class="form-select schemas-control" [disabled]="triggerForm.form.disabled" [(ngModel)]="schemaToAdd" name="schema">
<option *ngFor="let schema of schemasToAdd; trackBy: trackBySchema" [ngValue]="schema">{{schema.displayName}}</option>
</select>
</div>
<div class="col-auto"> <div class="col-auto">
<button type="submit" class="btn btn-success" [disabled]="triggerForm.form.disabled">{{ 'rules.addSchema' | sqxTranslate }}</button> <button type="button" class="btn btn-success" (click)="add()">
</div> <i class="icon-add"></i>
</form> </button>
</div>
</div> </div>
<div class="help"> <div class="help">
<h4>{{ 'common.conditions' | sqxTranslate }}</h4> <h4>{{ 'common.conditions' | sqxTranslate }}</h4>

6
frontend/src/app/features/rules/shared/triggers/content-changed-trigger.component.scss

@ -9,4 +9,10 @@
.form-check { .form-check {
margin-top: 1rem; margin-top: 1rem;
}
.label {
font-size: $font-small;
font-weight: normal;
line-height: 2.4rem;
} }

82
frontend/src/app/features/rules/shared/triggers/content-changed-trigger.component.ts

@ -5,21 +5,19 @@
* Copyright (c) Squidex UG (haftungsbeschränkt). All rights reserved. * Copyright (c) Squidex UG (haftungsbeschränkt). All rights reserved.
*/ */
import { Component, Input, OnChanges } from '@angular/core'; import { Component, Input } from '@angular/core';
import { SchemaDto, TriggerForm } from '@app/shared'; import { SchemaDto, TemplatedFormArray, TriggerForm } from '@app/shared';
import { CompletionsCache } from './completions-cache';
export interface TriggerSchemaForm {
schema: SchemaDto;
condition?: string;
}
@Component({ @Component({
selector: 'sqx-content-changed-trigger[trigger][triggerForm]', selector: 'sqx-content-changed-trigger[trigger][triggerForm]',
styleUrls: ['./content-changed-trigger.component.scss'], styleUrls: ['./content-changed-trigger.component.scss'],
templateUrl: './content-changed-trigger.component.html', templateUrl: './content-changed-trigger.component.html',
providers: [
CompletionsCache,
],
}) })
export class ContentChangedTriggerComponent implements OnChanges { export class ContentChangedTriggerComponent {
@Input() @Input()
public schemas?: ReadonlyArray<SchemaDto> | null; public schemas?: ReadonlyArray<SchemaDto> | null;
@ -29,69 +27,15 @@ export class ContentChangedTriggerComponent implements OnChanges {
@Input() @Input()
public triggerForm!: TriggerForm; public triggerForm!: TriggerForm;
public triggerSchemas: TriggerSchemaForm[] = []; public get schemasForm() {
return this.triggerForm.form.get('schemas') as TemplatedFormArray;
public schemaToAdd!: SchemaDto;
public schemasToAdd!: ReadonlyArray<SchemaDto>;
public get hasSchema() {
return !!this.schemaToAdd;
}
public ngOnChanges() {
const schemas: TriggerSchemaForm[] = [];
if (this.trigger.schemas && this.schemas) {
for (const triggerSchema of this.trigger.schemas) {
const schema = this.schemas.find(s => s.id === triggerSchema.schemaId);
if (schema) {
const condition = triggerSchema.condition;
schemas.push({ schema, condition });
}
}
}
this.triggerSchemas = schemas;
this.triggerSchemas.sortByString(x => x.schema.name);
this.updateSchemaToAdd();
}
public removeSchema(schemaForm: TriggerSchemaForm) {
this.triggerSchemas.remove(schemaForm);
this.updateValue();
this.updateSchemaToAdd();
}
public addSchema() {
this.triggerSchemas.push({ schema: this.schemaToAdd });
this.triggerSchemas.sortByString(x => x.schema.name);
this.updateValue();
this.updateSchemaToAdd();
}
public updateCondition(schema: SchemaDto, condition: string) {
this.triggerSchemas.replaceBy('schema', { schema, condition });
this.updateValue();
}
public updateValue() {
const schemas = this.triggerSchemas.map(({ schema, condition }) => ({ schemaId: schema.id, condition }));
this.triggerForm.form.patchValue({ schemas });
} }
private updateSchemaToAdd() { public add() {
this.schemasToAdd = this.schemas?.filter(schema => !this.triggerSchemas.find(s => s.schema.id === schema.id)).sortByString(x => x.name) || []; this.schemasForm.add();
this.schemaToAdd = this.schemasToAdd[0];
} }
public trackBySchema(_index: number, schema: SchemaDto) { public remove(index: number) {
return schema.id; this.schemasForm.removeAt(index);
} }
} }

4
frontend/src/app/features/schemas/pages/schema/common/schema-edit-form.component.ts

@ -5,7 +5,7 @@
* Copyright (c) Squidex UG (haftungsbeschränkt). All rights reserved. * Copyright (c) Squidex UG (haftungsbeschränkt). All rights reserved.
*/ */
import { Component, Input, OnChanges } from '@angular/core'; import { Component, Input } from '@angular/core';
import { EditSchemaForm, SchemaDto, SchemasState } from '@app/shared'; import { EditSchemaForm, SchemaDto, SchemasState } from '@app/shared';
@Component({ @Component({
@ -13,7 +13,7 @@ import { EditSchemaForm, SchemaDto, SchemasState } from '@app/shared';
styleUrls: ['./schema-edit-form.component.scss'], styleUrls: ['./schema-edit-form.component.scss'],
templateUrl: './schema-edit-form.component.html', templateUrl: './schema-edit-form.component.html',
}) })
export class SchemaEditFormComponent implements OnChanges { export class SchemaEditFormComponent {
@Input() @Input()
public schema!: SchemaDto; public schema!: SchemaDto;

4
frontend/src/app/features/schemas/pages/schema/export/schema-export-form.component.ts

@ -5,7 +5,7 @@
* Copyright (c) Squidex UG (haftungsbeschränkt). All rights reserved. * Copyright (c) Squidex UG (haftungsbeschränkt). All rights reserved.
*/ */
import { Component, Input, OnChanges } from '@angular/core'; import { Component, Input } from '@angular/core';
import { SchemaDto, SchemasState, SynchronizeSchemaForm } from '@app/shared'; import { SchemaDto, SchemasState, SynchronizeSchemaForm } from '@app/shared';
@Component({ @Component({
@ -13,7 +13,7 @@ import { SchemaDto, SchemasState, SynchronizeSchemaForm } from '@app/shared';
styleUrls: ['./schema-export-form.component.scss'], styleUrls: ['./schema-export-form.component.scss'],
templateUrl: './schema-export-form.component.html', templateUrl: './schema-export-form.component.html',
}) })
export class SchemaExportFormComponent implements OnChanges { export class SchemaExportFormComponent {
@Input() @Input()
public schema!: SchemaDto; public schema!: SchemaDto;

2
frontend/src/app/features/schemas/pages/schema/fields/field-group.component.html

@ -3,7 +3,7 @@
<sqx-field [field]="$any(fieldGroup.separator)" [languages]="languages" [parent]="parent" [plain]="true" [schema]="schema" [settings]="settings"> <sqx-field [field]="$any(fieldGroup.separator)" [languages]="languages" [parent]="parent" [plain]="true" [schema]="schema" [settings]="settings">
<div class="d-flex align-items-center"> <div class="d-flex align-items-center">
<i cdkDragHandle class="icon-drag2 drag-handle"></i> <ng-content></ng-content>
<button type="button" class="btn btn-sm btn-text-secondary ms-2" (click)="toggle()"> <button type="button" class="btn btn-sm btn-text-secondary ms-2" (click)="toggle()">
<i [class.icon-caret-right]="snapshot.isCollapsed" [class.icon-caret-down]="!snapshot.isCollapsed"></i> <i [class.icon-caret-right]="snapshot.isCollapsed" [class.icon-caret-down]="!snapshot.isCollapsed"></i>

10
frontend/src/app/features/schemas/pages/schema/fields/field.component.ts

@ -5,15 +5,15 @@
* Copyright (c) Squidex UG (haftungsbeschränkt). All rights reserved. * Copyright (c) Squidex UG (haftungsbeschränkt). All rights reserved.
*/ */
import { Component, Input, OnChanges, SimpleChanges } from '@angular/core'; import { Component, Input } from '@angular/core';
import { AppSettingsDto, createProperties, DialogModel, EditFieldForm, FieldDto, LanguageDto, ModalModel, NestedFieldDto, RootFieldDto, SchemaDto, SchemasState } from '@app/shared'; import { AppSettingsDto, createProperties, DialogModel, EditFieldForm, FieldDto, LanguageDto, ModalModel, NestedFieldDto, RootFieldDto, SchemaDto, SchemasState, TypedSimpleChanges } from '@app/shared';
@Component({ @Component({
selector: 'sqx-field[field][languages][schema][settings]', selector: 'sqx-field[field][languages][schema][settings]',
styleUrls: ['./field.component.scss'], styleUrls: ['./field.component.scss'],
templateUrl: './field.component.html', templateUrl: './field.component.html',
}) })
export class FieldComponent implements OnChanges { export class FieldComponent {
@Input() @Input()
public field!: NestedFieldDto | RootFieldDto; public field!: NestedFieldDto | RootFieldDto;
@ -50,8 +50,8 @@ export class FieldComponent implements OnChanges {
) { ) {
} }
public ngOnChanges(changes: SimpleChanges) { public ngOnChanges(changes: TypedSimpleChanges<this>) {
if (changes['field']) { if (changes.field) {
this.isEditable = this.field.canUpdate; this.isEditable = this.field.canUpdate;
this.editForm = new EditFieldForm(this.field.properties); this.editForm = new EditFieldForm(this.field.properties);

9
frontend/src/app/features/schemas/pages/schema/fields/sortable-field-list.component.html

@ -1,9 +1,13 @@
<div cdkDropListGroup <div
cdkDropListGroup
cdkDropList cdkDropList
[cdkDropListDisabled]="!sortable" [cdkDropListDisabled]="!sortable"
[cdkDropListData]="fieldGroups" [cdkDropListData]="fieldGroups"
(cdkDropListDropped)="sortGroups($event)"> (cdkDropListDropped)="sortGroups($event)">
<div *ngFor="let group of fieldGroups;" class="table-drag" cdkDrag cdkDragLockAxis="y"> <div *ngFor="let group of fieldGroups;" class="table-drag"
cdkDrag
cdkDragLockAxis="y"
[cdkDragDisabled]="!group.separator">
<sqx-field-group <sqx-field-group
[languages]="languages" [languages]="languages"
[fieldsEmpty]="fieldsEmpty" [fieldsEmpty]="fieldsEmpty"
@ -13,6 +17,7 @@
[settings]="settings" [settings]="settings"
[sortable]="sortable" [sortable]="sortable"
(sorted)="sortFields($event)"> (sorted)="sortFields($event)">
<i cdkDragHandle class="icon-drag2 drag-handle"></i>
</sqx-field-group> </sqx-field-group>
</div> </div>
</div> </div>

10
frontend/src/app/features/schemas/pages/schema/fields/types/boolean-validation.component.ts

@ -5,17 +5,17 @@
* Copyright (c) Squidex UG (haftungsbeschränkt). All rights reserved. * Copyright (c) Squidex UG (haftungsbeschränkt). All rights reserved.
*/ */
import { Component, Input, OnChanges, SimpleChanges } from '@angular/core'; import { Component, Input } from '@angular/core';
import { UntypedFormGroup } from '@angular/forms'; import { UntypedFormGroup } from '@angular/forms';
import { Observable } from 'rxjs'; import { Observable } from 'rxjs';
import { BooleanFieldPropertiesDto, FieldDto, hasNoValue$, LanguageDto } from '@app/shared'; import { BooleanFieldPropertiesDto, FieldDto, hasNoValue$, LanguageDto, TypedSimpleChanges } from '@app/shared';
@Component({ @Component({
selector: 'sqx-boolean-validation[field][fieldForm][languages][properties]', selector: 'sqx-boolean-validation[field][fieldForm][languages][properties]',
styleUrls: ['boolean-validation.component.scss'], styleUrls: ['boolean-validation.component.scss'],
templateUrl: 'boolean-validation.component.html', templateUrl: 'boolean-validation.component.html',
}) })
export class BooleanValidationComponent implements OnChanges { export class BooleanValidationComponent {
@Input() @Input()
public fieldForm!: UntypedFormGroup; public fieldForm!: UntypedFormGroup;
@ -33,8 +33,8 @@ export class BooleanValidationComponent implements OnChanges {
public showDefaultValue?: Observable<boolean>; public showDefaultValue?: Observable<boolean>;
public ngOnChanges(changes: SimpleChanges) { public ngOnChanges(changes: TypedSimpleChanges<this>) {
if (changes['fieldForm']) { if (changes.fieldForm) {
this.showDefaultValue = this.showDefaultValue =
hasNoValue$(this.fieldForm.controls['isRequired']); hasNoValue$(this.fieldForm.controls['isRequired']);
} }

10
frontend/src/app/features/schemas/pages/schema/fields/types/date-time-validation.component.ts

@ -5,10 +5,10 @@
* Copyright (c) Squidex UG (haftungsbeschränkt). All rights reserved. * Copyright (c) Squidex UG (haftungsbeschränkt). All rights reserved.
*/ */
import { Component, Input, OnChanges, SimpleChanges } from '@angular/core'; import { Component, Input } from '@angular/core';
import { UntypedFormGroup } from '@angular/forms'; import { UntypedFormGroup } from '@angular/forms';
import { Observable } from 'rxjs'; import { Observable } from 'rxjs';
import { DateTimeFieldPropertiesDto, FieldDto, hasNoValue$, LanguageDto } from '@app/shared'; import { DateTimeFieldPropertiesDto, FieldDto, hasNoValue$, LanguageDto, TypedSimpleChanges } from '@app/shared';
const CALCULATED_DEFAULT_VALUES: ReadonlyArray<string> = ['Now', 'Today']; const CALCULATED_DEFAULT_VALUES: ReadonlyArray<string> = ['Now', 'Today'];
@ -17,7 +17,7 @@ const CALCULATED_DEFAULT_VALUES: ReadonlyArray<string> = ['Now', 'Today'];
styleUrls: ['date-time-validation.component.scss'], styleUrls: ['date-time-validation.component.scss'],
templateUrl: 'date-time-validation.component.html', templateUrl: 'date-time-validation.component.html',
}) })
export class DateTimeValidationComponent implements OnChanges { export class DateTimeValidationComponent {
@Input() @Input()
public fieldForm!: UntypedFormGroup; public fieldForm!: UntypedFormGroup;
@ -38,8 +38,8 @@ export class DateTimeValidationComponent implements OnChanges {
public calculatedDefaultValues = CALCULATED_DEFAULT_VALUES; public calculatedDefaultValues = CALCULATED_DEFAULT_VALUES;
public ngOnChanges(changes: SimpleChanges) { public ngOnChanges(changes: TypedSimpleChanges<this>) {
if (changes['fieldForm']) { if (changes.fieldForm) {
this.showDefaultValues = this.showDefaultValues =
hasNoValue$(this.fieldForm.controls['isRequired']); hasNoValue$(this.fieldForm.controls['isRequired']);

10
frontend/src/app/features/schemas/pages/schema/fields/types/number-ui.component.ts

@ -5,17 +5,17 @@
* Copyright (c) Squidex UG (haftungsbeschränkt). All rights reserved. * Copyright (c) Squidex UG (haftungsbeschränkt). All rights reserved.
*/ */
import { Component, Input, OnChanges, SimpleChanges } from '@angular/core'; import { Component, Input } from '@angular/core';
import { UntypedFormGroup } from '@angular/forms'; import { UntypedFormGroup } from '@angular/forms';
import { Observable } from 'rxjs'; import { Observable } from 'rxjs';
import { FieldDto, FloatConverter, NUMBER_FIELD_EDITORS, NumberFieldPropertiesDto, ResourceOwner, valueProjection$ } from '@app/shared'; import { FieldDto, FloatConverter, NUMBER_FIELD_EDITORS, NumberFieldPropertiesDto, ResourceOwner, valueProjection$, TypedSimpleChanges } from '@app/shared';
@Component({ @Component({
selector: 'sqx-number-ui[field][fieldForm][properties]', selector: 'sqx-number-ui[field][fieldForm][properties]',
styleUrls: ['number-ui.component.scss'], styleUrls: ['number-ui.component.scss'],
templateUrl: 'number-ui.component.html', templateUrl: 'number-ui.component.html',
}) })
export class NumberUIComponent extends ResourceOwner implements OnChanges { export class NumberUIComponent extends ResourceOwner {
public readonly converter = FloatConverter.INSTANCE; public readonly converter = FloatConverter.INSTANCE;
public readonly editors = NUMBER_FIELD_EDITORS; public readonly editors = NUMBER_FIELD_EDITORS;
@ -31,8 +31,8 @@ export class NumberUIComponent extends ResourceOwner implements OnChanges {
public hideAllowedValues?: Observable<boolean>; public hideAllowedValues?: Observable<boolean>;
public hideInlineEditable?: Observable<boolean>; public hideInlineEditable?: Observable<boolean>;
public ngOnChanges(changes: SimpleChanges) { public ngOnChanges(changes: TypedSimpleChanges<this>) {
if (changes['fieldForm']) { if (changes.fieldForm) {
this.unsubscribeAll(); this.unsubscribeAll();
const editor = this.fieldForm.controls['editor']; const editor = this.fieldForm.controls['editor'];

10
frontend/src/app/features/schemas/pages/schema/fields/types/string-ui.component.ts

@ -5,17 +5,17 @@
* Copyright (c) Squidex UG (haftungsbeschränkt). All rights reserved. * Copyright (c) Squidex UG (haftungsbeschränkt). All rights reserved.
*/ */
import { Component, Input, OnChanges, SimpleChanges } from '@angular/core'; import { Component, Input } from '@angular/core';
import { UntypedFormGroup } from '@angular/forms'; import { UntypedFormGroup } from '@angular/forms';
import { Observable } from 'rxjs'; import { Observable } from 'rxjs';
import { FieldDto, ResourceOwner, SchemaTagSource, STRING_FIELD_EDITORS, StringFieldPropertiesDto, valueProjection$ } from '@app/shared'; import { FieldDto, ResourceOwner, SchemaTagSource, STRING_FIELD_EDITORS, StringFieldPropertiesDto, valueProjection$, TypedSimpleChanges } from '@app/shared';
@Component({ @Component({
selector: 'sqx-string-ui[field][fieldForm][properties]', selector: 'sqx-string-ui[field][fieldForm][properties]',
styleUrls: ['string-ui.component.scss'], styleUrls: ['string-ui.component.scss'],
templateUrl: 'string-ui.component.html', templateUrl: 'string-ui.component.html',
}) })
export class StringUIComponent extends ResourceOwner implements OnChanges { export class StringUIComponent extends ResourceOwner {
public readonly editors = STRING_FIELD_EDITORS; public readonly editors = STRING_FIELD_EDITORS;
@Input() @Input()
@ -37,8 +37,8 @@ export class StringUIComponent extends ResourceOwner implements OnChanges {
super(); super();
} }
public ngOnChanges(changes: SimpleChanges) { public ngOnChanges(changes: TypedSimpleChanges<this>) {
if (changes['fieldForm']) { if (changes.fieldForm) {
this.unsubscribeAll(); this.unsubscribeAll();
const editor = this.fieldForm.controls['editor']; const editor = this.fieldForm.controls['editor'];

12
frontend/src/app/features/schemas/pages/schema/fields/types/string-validation.component.ts

@ -5,17 +5,17 @@
* Copyright (c) Squidex UG (haftungsbeschränkt). All rights reserved. * Copyright (c) Squidex UG (haftungsbeschränkt). All rights reserved.
*/ */
import { Component, Input, OnChanges, SimpleChanges } from '@angular/core'; import { Component, Input } from '@angular/core';
import { UntypedFormGroup } from '@angular/forms'; import { UntypedFormGroup } from '@angular/forms';
import { Observable } from 'rxjs'; import { Observable } from 'rxjs';
import { AppSettingsDto, FieldDto, hasNoValue$, hasValue$, LanguageDto, ModalModel, PatternDto, ResourceOwner, RootFieldDto, SchemaDto, STRING_CONTENT_TYPES, StringFieldPropertiesDto, Types, value$ } from '@app/shared'; import { AppSettingsDto, FieldDto, hasNoValue$, hasValue$, LanguageDto, ModalModel, PatternDto, ResourceOwner, RootFieldDto, SchemaDto, STRING_CONTENT_TYPES, StringFieldPropertiesDto, Types, value$, TypedSimpleChanges } from '@app/shared';
@Component({ @Component({
selector: 'sqx-string-validation[field][fieldForm][languages][properties][schema][settings]', selector: 'sqx-string-validation[field][fieldForm][languages][properties][schema][settings]',
styleUrls: ['string-validation.component.scss'], styleUrls: ['string-validation.component.scss'],
templateUrl: 'string-validation.component.html', templateUrl: 'string-validation.component.html',
}) })
export class StringValidationComponent extends ResourceOwner implements OnChanges { export class StringValidationComponent extends ResourceOwner {
public readonly contentTypes = STRING_CONTENT_TYPES; public readonly contentTypes = STRING_CONTENT_TYPES;
@Input() @Input()
@ -49,12 +49,12 @@ export class StringValidationComponent extends ResourceOwner implements OnChange
return Types.is(this.field, RootFieldDto) && !this.field.isLocalizable && this.schema.type !== 'Component'; return Types.is(this.field, RootFieldDto) && !this.field.isLocalizable && this.schema.type !== 'Component';
} }
public ngOnChanges(changes: SimpleChanges) { public ngOnChanges(changes: TypedSimpleChanges<this>) {
if (changes['settings']) { if (changes.settings) {
this.setPatternName(); this.setPatternName();
} }
if (changes['fieldForm']) { if (changes.fieldForm) {
this.unsubscribeAll(); this.unsubscribeAll();
this.showPatternSuggestions = this.showPatternSuggestions =

2
frontend/src/app/features/schemas/pages/schema/preview/schema-preview-urls-form.component.html

@ -12,7 +12,7 @@
{{ 'schemas.previewUrls.empty' | sqxTranslate }} {{ 'schemas.previewUrls.empty' | sqxTranslate }}
</div> </div>
<div class="form-group row gx-2" *ngFor="let form of editForm.previewControls; let i = index" [formGroup]="form"> <div class="mb-2 row gx-2" *ngFor="let form of editForm.previewControls; let i = index" [formGroup]="form">
<div class="col col-name"> <div class="col col-name">
<sqx-control-errors for="name"></sqx-control-errors> <sqx-control-errors for="name"></sqx-control-errors>

4
frontend/src/app/features/schemas/pages/schema/preview/schema-preview-urls-form.component.ts

@ -5,7 +5,7 @@
* Copyright (c) Squidex UG (haftungsbeschränkt). All rights reserved. * Copyright (c) Squidex UG (haftungsbeschränkt). All rights reserved.
*/ */
import { Component, Input, OnChanges } from '@angular/core'; import { Component, Input } from '@angular/core';
import { ConfigurePreviewUrlsForm, SchemaDto, SchemasState } from '@app/shared'; import { ConfigurePreviewUrlsForm, SchemaDto, SchemasState } from '@app/shared';
@Component({ @Component({
@ -13,7 +13,7 @@ import { ConfigurePreviewUrlsForm, SchemaDto, SchemasState } from '@app/shared';
styleUrls: ['./schema-preview-urls-form.component.scss'], styleUrls: ['./schema-preview-urls-form.component.scss'],
templateUrl: './schema-preview-urls-form.component.html', templateUrl: './schema-preview-urls-form.component.html',
}) })
export class SchemaPreviewUrlsFormComponent implements OnChanges { export class SchemaPreviewUrlsFormComponent {
@Input() @Input()
public schema!: SchemaDto; public schema!: SchemaDto;

4
frontend/src/app/features/schemas/pages/schema/rules/schema-field-rules-form.component.html

@ -8,7 +8,7 @@
{{ 'schemas.rules.empty' | sqxTranslate }} {{ 'schemas.rules.empty' | sqxTranslate }}
</div> </div>
<div class="form-group row gx-2" *ngFor="let form of editForm.rulesControls; let i = index" [formGroup]="form"> <div class="mb-2 row gx-2" *ngFor="let form of editForm.rulesControls; let i = index" [formGroup]="form">
<div class="col col-action"> <div class="col col-action">
<sqx-control-errors for="action"></sqx-control-errors> <sqx-control-errors for="action"></sqx-control-errors>
@ -32,7 +32,7 @@
<div class="col"> <div class="col">
<sqx-control-errors for="condition"></sqx-control-errors> <sqx-control-errors for="condition"></sqx-control-errors>
<input class="form-control" formControlName="condition" placeholder="{{ 'schemas.rules.condition' | sqxTranslate }}"> <sqx-code-editor [singleLine]="true" formControlName="condition" [completion]="fieldCompletions | async"></sqx-code-editor>
</div> </div>
<div class="col-auto"> <div class="col-auto">

13
frontend/src/app/features/schemas/pages/schema/rules/schema-field-rules-form.component.ts

@ -5,15 +5,16 @@
* Copyright (c) Squidex UG (haftungsbeschränkt). All rights reserved. * Copyright (c) Squidex UG (haftungsbeschränkt). All rights reserved.
*/ */
import { Component, Input, OnChanges } from '@angular/core'; import { Component, Input, OnInit } from '@angular/core';
import { ConfigureFieldRulesForm, FIELD_RULE_ACTIONS, SchemaDto, SchemasState } from '@app/shared'; import { EMPTY, Observable, shareReplay } from 'rxjs';
import { ConfigureFieldRulesForm, FIELD_RULE_ACTIONS, SchemaCompletions, SchemaDto, SchemasService, SchemasState } from '@app/shared';
@Component({ @Component({
selector: 'sqx-schema-field-rules-form', selector: 'sqx-schema-field-rules-form',
styleUrls: ['./schema-field-rules-form.component.scss'], styleUrls: ['./schema-field-rules-form.component.scss'],
templateUrl: './schema-field-rules-form.component.html', templateUrl: './schema-field-rules-form.component.html',
}) })
export class SchemaFieldRulesFormComponent implements OnChanges { export class SchemaFieldRulesFormComponent implements OnInit {
@Input() @Input()
public schema!: SchemaDto; public schema!: SchemaDto;
@ -21,14 +22,20 @@ export class SchemaFieldRulesFormComponent implements OnChanges {
public fieldNames!: ReadonlyArray<string>; public fieldNames!: ReadonlyArray<string>;
public fieldActions = FIELD_RULE_ACTIONS; public fieldActions = FIELD_RULE_ACTIONS;
public fieldCompletions: Observable<SchemaCompletions> = EMPTY;
public isEditable = false; public isEditable = false;
constructor( constructor(
private readonly schemasState: SchemasState, private readonly schemasState: SchemasState,
private readonly schemasService: SchemasService,
) { ) {
} }
public ngOnInit() {
this.fieldCompletions = this.schemasService.getFieldRulesCompletion(this.schemasState.appName, this.schema.name).pipe(shareReplay(1));
}
public ngOnChanges() { public ngOnChanges() {
const fieldNames: string[] = []; const fieldNames: string[] = [];

6
frontend/src/app/features/schemas/pages/schema/scripts/schema-scripts-form.component.ts

@ -5,7 +5,7 @@
* Copyright (c) Squidex UG (haftungsbeschränkt). All rights reserved. * Copyright (c) Squidex UG (haftungsbeschränkt). All rights reserved.
*/ */
import { Component, Input, OnChanges } from '@angular/core'; import { Component, Input } from '@angular/core';
import { EMPTY, Observable, shareReplay } from 'rxjs'; import { EMPTY, Observable, shareReplay } from 'rxjs';
import { AppsState, EditSchemaScriptsForm, SchemaCompletions, SchemaDto, SchemasService, SchemasState } from '@app/shared'; import { AppsState, EditSchemaScriptsForm, SchemaCompletions, SchemaDto, SchemasService, SchemasState } from '@app/shared';
@ -14,7 +14,7 @@ import { AppsState, EditSchemaScriptsForm, SchemaCompletions, SchemaDto, Schemas
styleUrls: ['./schema-scripts-form.component.scss'], styleUrls: ['./schema-scripts-form.component.scss'],
templateUrl: './schema-scripts-form.component.html', templateUrl: './schema-scripts-form.component.html',
}) })
export class SchemaScriptsFormComponent implements OnChanges { export class SchemaScriptsFormComponent {
@Input() @Input()
public schema!: SchemaDto; public schema!: SchemaDto;
@ -38,7 +38,7 @@ export class SchemaScriptsFormComponent implements OnChanges {
this.editForm.load(this.schema.scripts); this.editForm.load(this.schema.scripts);
this.editForm.setEnabled(this.isEditable); this.editForm.setEnabled(this.isEditable);
this.schemaCompletions = this.schemasService.getCompletions(this.appsState.appName, this.schema.name).pipe(shareReplay(1)); this.schemaCompletions = this.schemasService.getContentScriptsCompletion(this.appsState.appName, this.schema.name).pipe(shareReplay(1));
} }
public selectField(field: string) { public selectField(field: string) {

4
frontend/src/app/features/schemas/pages/schema/ui/field-list.component.ts

@ -6,7 +6,7 @@
*/ */
import { CdkDragDrop, moveItemInArray, transferArrayItem } from '@angular/cdk/drag-drop'; import { CdkDragDrop, moveItemInArray, transferArrayItem } from '@angular/cdk/drag-drop';
import { ChangeDetectionStrategy, Component, EventEmitter, Input, OnChanges, Output } from '@angular/core'; import { ChangeDetectionStrategy, Component, EventEmitter, Input, Output } from '@angular/core';
import { META_FIELDS, SchemaDto, TableField } from '@app/shared'; import { META_FIELDS, SchemaDto, TableField } from '@app/shared';
const META_FIELD_NAMES = Object.values(META_FIELDS).filter(x => x !== META_FIELDS.empty); const META_FIELD_NAMES = Object.values(META_FIELDS).filter(x => x !== META_FIELDS.empty);
@ -17,7 +17,7 @@ const META_FIELD_NAMES = Object.values(META_FIELDS).filter(x => x !== META_FIELD
templateUrl: './field-list.component.html', templateUrl: './field-list.component.html',
changeDetection: ChangeDetectionStrategy.OnPush, changeDetection: ChangeDetectionStrategy.OnPush,
}) })
export class FieldListComponent implements OnChanges { export class FieldListComponent {
@Input() @Input()
public emptyText = ''; public emptyText = '';

4
frontend/src/app/features/schemas/pages/schema/ui/schema-ui-form.component.ts

@ -5,7 +5,7 @@
* Copyright (c) Squidex UG (haftungsbeschränkt). All rights reserved. * Copyright (c) Squidex UG (haftungsbeschränkt). All rights reserved.
*/ */
import { Component, Input, OnChanges } from '@angular/core'; import { Component, Input } from '@angular/core';
import { SchemaDto, SchemasState } from '@app/shared'; import { SchemaDto, SchemasState } from '@app/shared';
@Component({ @Component({
@ -13,7 +13,7 @@ import { SchemaDto, SchemasState } from '@app/shared';
styleUrls: ['./schema-ui-form.component.scss'], styleUrls: ['./schema-ui-form.component.scss'],
templateUrl: './schema-ui-form.component.html', templateUrl: './schema-ui-form.component.html',
}) })
export class SchemaUIFormComponent implements OnChanges { export class SchemaUIFormComponent {
@Input() @Input()
public schema!: SchemaDto; public schema!: SchemaDto;

10
frontend/src/app/features/settings/pages/backups/backup.component.ts

@ -5,8 +5,8 @@
* Copyright (c) Squidex UG (haftungsbeschränkt). All rights reserved. * Copyright (c) Squidex UG (haftungsbeschränkt). All rights reserved.
*/ */
import { ChangeDetectionStrategy, Component, Input, OnChanges, SimpleChanges } from '@angular/core'; import { ChangeDetectionStrategy, Component, Input } from '@angular/core';
import { ApiUrlConfig, BackupDto, BackupsState, Duration } from '@app/shared'; import { ApiUrlConfig, BackupDto, BackupsState, Duration, TypedSimpleChanges } from '@app/shared';
@Component({ @Component({
selector: 'sqx-backup[backup]', selector: 'sqx-backup[backup]',
@ -14,7 +14,7 @@ import { ApiUrlConfig, BackupDto, BackupsState, Duration } from '@app/shared';
templateUrl: './backup.component.html', templateUrl: './backup.component.html',
changeDetection: ChangeDetectionStrategy.OnPush, changeDetection: ChangeDetectionStrategy.OnPush,
}) })
export class BackupComponent implements OnChanges { export class BackupComponent {
@Input() @Input()
public backup!: BackupDto; public backup!: BackupDto;
@ -26,8 +26,8 @@ export class BackupComponent implements OnChanges {
) { ) {
} }
public ngOnChanges(changes: SimpleChanges) { public ngOnChanges(changes: TypedSimpleChanges<this>) {
if (changes['backup']) { if (changes.backup) {
this.duration = Duration.create(this.backup.started, this.backup.stopped!).toString(); this.duration = Duration.create(this.backup.started, this.backup.stopped!).toString();
} }
} }

10
frontend/src/app/features/settings/pages/clients/client.component.ts

@ -5,8 +5,8 @@
* Copyright (c) Squidex UG (haftungsbeschränkt). All rights reserved. * Copyright (c) Squidex UG (haftungsbeschränkt). All rights reserved.
*/ */
import { ChangeDetectionStrategy, Component, Input, OnChanges, SimpleChanges } from '@angular/core'; import { ChangeDetectionStrategy, Component, Input } from '@angular/core';
import { AppsState, ClientDto, ClientsState, DialogModel, RoleDto } from '@app/shared'; import { AppsState, ClientDto, ClientsState, DialogModel, RoleDto, TypedSimpleChanges } from '@app/shared';
@Component({ @Component({
selector: 'sqx-client[client][clientRoles]', selector: 'sqx-client[client][clientRoles]',
@ -14,7 +14,7 @@ import { AppsState, ClientDto, ClientsState, DialogModel, RoleDto } from '@app/s
templateUrl: './client.component.html', templateUrl: './client.component.html',
changeDetection: ChangeDetectionStrategy.OnPush, changeDetection: ChangeDetectionStrategy.OnPush,
}) })
export class ClientComponent implements OnChanges { export class ClientComponent {
@Input() @Input()
public client!: ClientDto; public client!: ClientDto;
@ -31,8 +31,8 @@ export class ClientComponent implements OnChanges {
) { ) {
} }
public ngOnChanges(changes: SimpleChanges) { public ngOnChanges(changes: TypedSimpleChanges<this>) {
if (changes['client']) { if (changes.client) {
this.apiCallsLimit = this.client.apiCallsLimit; this.apiCallsLimit = this.client.apiCallsLimit;
} }
} }

4
frontend/src/app/features/settings/pages/contributors/contributor-add-form.component.ts

@ -5,7 +5,7 @@
* Copyright (c) Squidex UG (haftungsbeschränkt). All rights reserved. * Copyright (c) Squidex UG (haftungsbeschränkt). All rights reserved.
*/ */
import { Component, Injectable, Input, OnChanges } from '@angular/core'; import { Component, Injectable, Input } from '@angular/core';
import { Observable, of } from 'rxjs'; import { Observable, of } from 'rxjs';
import { withLatestFrom } from 'rxjs/operators'; import { withLatestFrom } from 'rxjs/operators';
import { AssignContributorForm, AutocompleteSource, ContributorsState, DialogModel, DialogService, RoleDto, UsersService } from '@app/shared'; import { AssignContributorForm, AutocompleteSource, ContributorsState, DialogModel, DialogService, RoleDto, UsersService } from '@app/shared';
@ -45,7 +45,7 @@ export class UsersDataSource implements AutocompleteSource {
UsersDataSource, UsersDataSource,
], ],
}) })
export class ContributorAddFormComponent implements OnChanges { export class ContributorAddFormComponent {
private defaultValue: any; private defaultValue: any;
@Input() @Input()

4
frontend/src/app/features/settings/pages/languages/language.component.ts

@ -6,7 +6,7 @@
*/ */
import { CdkDragDrop } from '@angular/cdk/drag-drop'; import { CdkDragDrop } from '@angular/cdk/drag-drop';
import { Component, Input, OnChanges } from '@angular/core'; import { Component, Input } from '@angular/core';
import { AppLanguageDto, EditLanguageForm, LanguageDto, LanguagesState, sorted } from '@app/shared'; import { AppLanguageDto, EditLanguageForm, LanguageDto, LanguagesState, sorted } from '@app/shared';
@Component({ @Component({
@ -14,7 +14,7 @@ import { AppLanguageDto, EditLanguageForm, LanguageDto, LanguagesState, sorted }
styleUrls: ['./language.component.scss'], styleUrls: ['./language.component.scss'],
templateUrl: './language.component.html', templateUrl: './language.component.html',
}) })
export class LanguageComponent implements OnChanges { export class LanguageComponent {
@Input() @Input()
public language!: AppLanguageDto; public language!: AppLanguageDto;

10
frontend/src/app/features/settings/pages/roles/role.component.ts

@ -5,8 +5,8 @@
* Copyright (c) Squidex UG (haftungsbeschränkt). All rights reserved. * Copyright (c) Squidex UG (haftungsbeschränkt). All rights reserved.
*/ */
import { ChangeDetectionStrategy, Component, Input, OnChanges, SimpleChanges, ViewChild } from '@angular/core'; import { ChangeDetectionStrategy, Component, Input, ViewChild } from '@angular/core';
import { AutocompleteComponent, AutocompleteSource, EditRoleForm, RoleDto, RolesState, SchemaDto, Settings } from '@app/shared'; import { AutocompleteComponent, AutocompleteSource, EditRoleForm, RoleDto, RolesState, SchemaDto, Settings, TypedSimpleChanges } from '@app/shared';
const DESCRIPTIONS = { const DESCRIPTIONS = {
Developer: 'i18n:roles.defaults.developer', Developer: 'i18n:roles.defaults.developer',
@ -37,7 +37,7 @@ const SIMPLE_PROPERTIES: ReadonlyArray<Property> = [{
templateUrl: './role.component.html', templateUrl: './role.component.html',
changeDetection: ChangeDetectionStrategy.OnPush, changeDetection: ChangeDetectionStrategy.OnPush,
}) })
export class RoleComponent implements OnChanges { export class RoleComponent {
@Input() @Input()
public role!: RoleDto; public role!: RoleDto;
@ -70,8 +70,8 @@ export class RoleComponent implements OnChanges {
) { ) {
} }
public ngOnChanges(changes: SimpleChanges) { public ngOnChanges(changes: TypedSimpleChanges<this>) {
if (changes['role']) { if (changes.role) {
this.isEditable = this.role.canUpdate; this.isEditable = this.role.canUpdate;
this.properties = this.role.properties; this.properties = this.role.properties;

4
frontend/src/app/features/settings/pages/templates/template.component.ts

@ -5,7 +5,7 @@
* Copyright (c) Squidex UG (haftungsbeschränkt). All rights reserved. * Copyright (c) Squidex UG (haftungsbeschränkt). All rights reserved.
*/ */
import { ChangeDetectionStrategy, Component, Input, OnChanges } from '@angular/core'; import { ChangeDetectionStrategy, Component, Input } from '@angular/core';
import { map, Observable, shareReplay } from 'rxjs'; import { map, Observable, shareReplay } from 'rxjs';
import { AppsState, ClientsState, TemplateDetailsDto, TemplateDto, TemplatesService } from '@app/shared'; import { AppsState, ClientsState, TemplateDetailsDto, TemplateDto, TemplatesService } from '@app/shared';
@ -15,7 +15,7 @@ import { AppsState, ClientsState, TemplateDetailsDto, TemplateDto, TemplatesServ
templateUrl: './template.component.html', templateUrl: './template.component.html',
changeDetection: ChangeDetectionStrategy.OnPush, changeDetection: ChangeDetectionStrategy.OnPush,
}) })
export class TemplateComponent implements OnChanges { export class TemplateComponent {
@Input() @Input()
public template!: TemplateDto; public template!: TemplateDto;

4
frontend/src/app/features/settings/pages/workflows/workflow-diagram.component.ts

@ -5,7 +5,7 @@
* Copyright (c) Squidex UG (haftungsbeschränkt). All rights reserved. * Copyright (c) Squidex UG (haftungsbeschränkt). All rights reserved.
*/ */
import { AfterViewInit, Component, ElementRef, Input, OnChanges, OnDestroy, ViewChild } from '@angular/core'; import { AfterViewInit, Component, ElementRef, Input, OnDestroy, ViewChild } from '@angular/core';
import { ResourceLoaderService, WorkflowDto } from '@app/shared'; import { ResourceLoaderService, WorkflowDto } from '@app/shared';
declare const vis: any; declare const vis: any;
@ -15,7 +15,7 @@ declare const vis: any;
styleUrls: ['./workflow-diagram.component.scss'], styleUrls: ['./workflow-diagram.component.scss'],
templateUrl: './workflow-diagram.component.html', templateUrl: './workflow-diagram.component.html',
}) })
export class WorkflowDiagramComponent implements AfterViewInit, OnDestroy, OnChanges { export class WorkflowDiagramComponent implements AfterViewInit, OnDestroy {
private network: any; private network: any;
@ViewChild('chartContainer', { static: false }) @ViewChild('chartContainer', { static: false })

10
frontend/src/app/features/settings/pages/workflows/workflow-step.component.ts

@ -5,15 +5,15 @@
* Copyright (c) Squidex UG (haftungsbeschränkt). All rights reserved. * Copyright (c) Squidex UG (haftungsbeschränkt). All rights reserved.
*/ */
import { Component, EventEmitter, Input, OnChanges, Output, SimpleChanges } from '@angular/core'; import { Component, EventEmitter, Input, Output } from '@angular/core';
import { WorkflowDto, WorkflowStep, WorkflowStepValues, WorkflowTransition, WorkflowTransitionValues, WorkflowTransitionView } from '@app/shared'; import { TypedSimpleChanges, WorkflowDto, WorkflowStep, WorkflowStepValues, WorkflowTransition, WorkflowTransitionValues, WorkflowTransitionView } from '@app/shared';
@Component({ @Component({
selector: 'sqx-workflow-step[roles][step][workflow]', selector: 'sqx-workflow-step[roles][step][workflow]',
styleUrls: ['./workflow-step.component.scss'], styleUrls: ['./workflow-step.component.scss'],
templateUrl: './workflow-step.component.html', templateUrl: './workflow-step.component.html',
}) })
export class WorkflowStepComponent implements OnChanges { export class WorkflowStepComponent {
public readonly onBlur: { updateOn: 'blur' } = { updateOn: 'blur' }; public readonly onBlur: { updateOn: 'blur' } = { updateOn: 'blur' };
@Output() @Output()
@ -54,8 +54,8 @@ export class WorkflowStepComponent implements OnChanges {
public transitions!: ReadonlyArray<WorkflowTransitionView>; public transitions!: ReadonlyArray<WorkflowTransitionView>;
public ngOnChanges(changes: SimpleChanges) { public ngOnChanges(changes: TypedSimpleChanges<this>) {
if (changes['workflow'] || changes['step']) { if (changes.workflow || changes.step) {
this.openSteps = this.workflow.getOpenSteps(this.step); this.openSteps = this.workflow.getOpenSteps(this.step);
this.openStep = this.openSteps[0]; this.openStep = this.openSteps[0];

4
frontend/src/app/features/settings/pages/workflows/workflow.component.ts

@ -5,7 +5,7 @@
* Copyright (c) Squidex UG (haftungsbeschränkt). All rights reserved. * Copyright (c) Squidex UG (haftungsbeschränkt). All rights reserved.
*/ */
import { Component, Input, OnChanges } from '@angular/core'; import { Component, Input } from '@angular/core';
import { ErrorDto, MathHelper, SchemaTagSource, WorkflowDto, WorkflowsState, WorkflowStep, WorkflowStepValues, WorkflowTransition, WorkflowTransitionValues } from '@app/shared'; import { ErrorDto, MathHelper, SchemaTagSource, WorkflowDto, WorkflowsState, WorkflowStep, WorkflowStepValues, WorkflowTransition, WorkflowTransitionValues } from '@app/shared';
@Component({ @Component({
@ -13,7 +13,7 @@ import { ErrorDto, MathHelper, SchemaTagSource, WorkflowDto, WorkflowsState, Wor
styleUrls: ['./workflow.component.scss'], styleUrls: ['./workflow.component.scss'],
templateUrl: './workflow.component.html', templateUrl: './workflow.component.html',
}) })
export class WorkflowComponent implements OnChanges { export class WorkflowComponent {
public readonly onBlur: { updateOn: 'blur' } = { updateOn: 'blur' }; public readonly onBlur: { updateOn: 'blur' } = { updateOn: 'blur' };
@Input() @Input()

13
frontend/src/app/framework/angular/avatar.component.ts

@ -5,8 +5,9 @@
* Copyright (c) Squidex UG (haftungsbeschränkt). All rights reserved. * Copyright (c) Squidex UG (haftungsbeschränkt). All rights reserved.
*/ */
import { ChangeDetectionStrategy, Component, Input, OnChanges, SimpleChanges } from '@angular/core'; import { ChangeDetectionStrategy, Component, Input } from '@angular/core';
import { picasso } from '@app/framework/internal'; import { picasso } from '@app/framework/internal';
import { TypedSimpleChanges } from './helpers';
@Component({ @Component({
selector: 'sqx-avatar', selector: 'sqx-avatar',
@ -14,7 +15,7 @@ import { picasso } from '@app/framework/internal';
templateUrl: './avatar.component.html', templateUrl: './avatar.component.html',
changeDetection: ChangeDetectionStrategy.OnPush, changeDetection: ChangeDetectionStrategy.OnPush,
}) })
export class AvatarComponent implements OnChanges { export class AvatarComponent {
@Input() @Input()
public identifier: string | undefined | null; public identifier: string | undefined | null;
@ -29,16 +30,16 @@ export class AvatarComponent implements OnChanges {
public actualImage: string | undefined | null; public actualImage: string | undefined | null;
public ngOnChanges(changes: SimpleChanges) { public ngOnChanges(changes: TypedSimpleChanges<this>) {
if (changes['image']) { if (changes.image) {
this.actualImage = this.image; this.actualImage = this.image;
} }
if (changes['image'] || changes['identifier']) { if (changes.image || changes.identifier) {
this.updateSource(); this.updateSource();
} }
if (changes['size']) { if (changes.size) {
this.imageSize = `${this.size}px`; this.imageSize = `${this.size}px`;
} }
} }

4
frontend/src/app/framework/angular/forms/control-errors.component.ts

@ -5,7 +5,7 @@
* Copyright (c) Sebastian Stehle. All rights r vbeserved * Copyright (c) Sebastian Stehle. All rights r vbeserved
*/ */
import { ChangeDetectionStrategy, ChangeDetectorRef, Component, Host, Input, OnChanges, OnDestroy, Optional } from '@angular/core'; import { ChangeDetectionStrategy, ChangeDetectorRef, Component, Host, Input, OnDestroy, Optional } from '@angular/core';
import { AbstractControl, FormGroupDirective, UntypedFormArray } from '@angular/forms'; import { AbstractControl, FormGroupDirective, UntypedFormArray } from '@angular/forms';
import { merge } from 'rxjs'; import { merge } from 'rxjs';
import { LocalizerService, StatefulComponent, Types } from '@app/framework/internal'; import { LocalizerService, StatefulComponent, Types } from '@app/framework/internal';
@ -23,7 +23,7 @@ interface State {
templateUrl: './control-errors.component.html', templateUrl: './control-errors.component.html',
changeDetection: ChangeDetectionStrategy.OnPush, changeDetection: ChangeDetectionStrategy.OnPush,
}) })
export class ControlErrorsComponent extends StatefulComponent<State> implements OnChanges, OnDestroy { export class ControlErrorsComponent extends StatefulComponent<State> implements OnDestroy {
private controlDisplayName = ''; private controlDisplayName = '';
private control: AbstractControl | null = null; private control: AbstractControl | null = null;

4
frontend/src/app/framework/angular/forms/editors/checkbox-group.component.ts

@ -5,7 +5,7 @@
* Copyright (c) Squidex UG (haftungsbeschränkt). All rights reserved. * Copyright (c) Squidex UG (haftungsbeschränkt). All rights reserved.
*/ */
import { AfterViewChecked, AfterViewInit, ChangeDetectionStrategy, ChangeDetectorRef, Component, ElementRef, forwardRef, Input, OnChanges, ViewChild } from '@angular/core'; import { AfterViewChecked, AfterViewInit, ChangeDetectionStrategy, ChangeDetectorRef, Component, ElementRef, forwardRef, Input, ViewChild } from '@angular/core';
import { NG_VALUE_ACCESSOR } from '@angular/forms'; import { NG_VALUE_ACCESSOR } from '@angular/forms';
import { getTagValues, MathHelper, StatefulControlComponent, TagValue, TextMeasurer, Types } from '@app/framework/internal'; import { getTagValues, MathHelper, StatefulControlComponent, TagValue, TextMeasurer, Types } from '@app/framework/internal';
@ -30,7 +30,7 @@ interface State {
], ],
changeDetection: ChangeDetectionStrategy.OnPush, changeDetection: ChangeDetectionStrategy.OnPush,
}) })
export class CheckboxGroupComponent extends StatefulControlComponent<State, string[]> implements AfterViewInit, AfterViewChecked, OnChanges { export class CheckboxGroupComponent extends StatefulControlComponent<State, string[]> implements AfterViewInit, AfterViewChecked {
private readonly textMeasurer: TextMeasurer; private readonly textMeasurer: TextMeasurer;
private childrenWidth = 0; private childrenWidth = 0;
private checkedValuesRaw: any; private checkedValuesRaw: any;

5
frontend/src/app/framework/angular/forms/editors/code-editor.component.html

@ -1 +1,4 @@
<div class="editor" [class.no-border]="borderless" #editor></div> <div [class.form-control]="singleLine">
<span></span>
<div class="editor" [class.no-border]="borderless || singleLine" #editor></div>
</div>

23
frontend/src/app/framework/angular/forms/editors/code-editor.component.scss

@ -32,4 +32,27 @@
.ace_gutter { .ace_gutter {
background: $color-border !important; background: $color-border !important;
} }
.ace_scroller {
box-shadow: none !important;
}
.form-control {
display: flex;
flex-direction: row;
flex-grow: 1;
align-items: center;
.editor {
flex-grow: 1;
}
span {
height: 1.5rem;
}
.ace_hidden-cursors {
display: none;
}
}
} }

136
frontend/src/app/framework/angular/forms/editors/code-editor.component.ts

@ -5,10 +5,11 @@
* Copyright (c) Squidex UG (haftungsbeschränkt). All rights reserved. * Copyright (c) Squidex UG (haftungsbeschränkt). All rights reserved.
*/ */
import { AfterViewInit, ChangeDetectionStrategy, ChangeDetectorRef, Component, ElementRef, forwardRef, Input, OnChanges, SimpleChanges, ViewChild } from '@angular/core'; import { AfterViewInit, ChangeDetectionStrategy, ChangeDetectorRef, Component, ElementRef, forwardRef, Input, ViewChild } from '@angular/core';
import { NG_VALUE_ACCESSOR } from '@angular/forms'; import { NG_VALUE_ACCESSOR } from '@angular/forms';
import { debounceTime, Subject } from 'rxjs'; import { debounceTime, Subject } from 'rxjs';
import { ResourceLoaderService, StatefulControlComponent, Types } from '@app/framework/internal'; import { ResourceLoaderService, StatefulControlComponent, Types } from '@app/framework/internal';
import { TypedSimpleChanges } from './../../helpers';
import { FocusComponent } from './../forms-helper'; import { FocusComponent } from './../forms-helper';
declare const ace: any; declare const ace: any;
@ -26,8 +27,9 @@ export const SQX_CODE_EDITOR_CONTROL_VALUE_ACCESSOR: any = {
], ],
changeDetection: ChangeDetectionStrategy.OnPush, changeDetection: ChangeDetectionStrategy.OnPush,
}) })
export class CodeEditorComponent extends StatefulControlComponent<{}, string> implements AfterViewInit, FocusComponent, OnChanges { export class CodeEditorComponent extends StatefulControlComponent<{}, string> implements AfterViewInit, FocusComponent {
private aceEditor: any; private aceEditor: any;
private aceTools: any;
private valueChanged = new Subject(); private valueChanged = new Subject();
private value = ''; private value = '';
private modelist: any; private modelist: any;
@ -51,6 +53,9 @@ export class CodeEditorComponent extends StatefulControlComponent<{}, string> im
@Input() @Input()
public maxLines: number | undefined; public maxLines: number | undefined;
@Input()
public singleLine = false;
@Input() @Input()
public wordWrap = false; public wordWrap = false;
@ -63,9 +68,9 @@ export class CodeEditorComponent extends StatefulControlComponent<{}, string> im
} }
@Input() @Input()
public set completion(value: ReadonlyArray<{ path: string; description: string }> | undefined | null) { public set completion(value: ReadonlyArray<{ path: string; description: string; type: string; allowedValues?: string[] }> | undefined | null) {
if (value) { if (value) {
this.completions = value.map(({ path, description }) => ({ value: path, name: path, meta: 'context', description })); this.completions = value.map(({ path, description, type, allowedValues }) => ({ value: path, name: path, description, meta: type?.toLowerCase(), path, allowedValues }));
} else { } else {
this.completions = []; this.completions = [];
} }
@ -77,16 +82,16 @@ export class CodeEditorComponent extends StatefulControlComponent<{}, string> im
super(changeDetector, {}); super(changeDetector, {});
} }
public ngOnChanges(changes: SimpleChanges) { public ngOnChanges(changes: TypedSimpleChanges<this>) {
if (changes['valueFile'] || changes['mode']) { if (changes.valueFile || changes.mode) {
this.setMode(); this.setMode();
} }
if (changes['height'] || changes['maxLines']) { if (changes.height || changes.maxLines || changes.singleLine) {
this.setHeight(); this.setOptions();
} }
if (changes['wordWrap']) { if (changes.wordWrap) {
this.setWordWrap(); this.setWordWrap();
} }
} }
@ -114,15 +119,19 @@ export class CodeEditorComponent extends StatefulControlComponent<{}, string> im
} }
public onDisabled(isDisabled: boolean) { public onDisabled(isDisabled: boolean) {
if (this.aceEditor) { if (!this.aceEditor) {
this.aceEditor.setReadOnly(isDisabled); return;
} }
this.aceEditor.setReadOnly(isDisabled);
} }
public focus() { public focus() {
if (this.aceEditor) { if (!this.aceEditor) {
this.aceEditor.focus(); return;
} }
this.aceEditor.focus();
} }
public ngAfterViewInit() { public ngAfterViewInit() {
@ -136,39 +145,39 @@ export class CodeEditorComponent extends StatefulControlComponent<{}, string> im
this.resourceLoader.loadLocalScript('dependencies/ace/ext/modelist.js'), this.resourceLoader.loadLocalScript('dependencies/ace/ext/modelist.js'),
this.resourceLoader.loadLocalScript('dependencies/ace/ext/language_tools.js'), this.resourceLoader.loadLocalScript('dependencies/ace/ext/language_tools.js'),
]).then(() => { ]).then(() => {
this.aceEditor = ace.edit(this.editor.nativeElement);
this.modelist = ace.require('ace/ext/modelist'); this.modelist = ace.require('ace/ext/modelist');
this.aceEditor.setReadOnly(this.snapshot.isDisabled); this.aceEditor = ace.edit(this.editor.nativeElement);
this.aceEditor.setFontSize(14); this.aceEditor.setFontSize(14);
this.aceTools = ace.require('ace/ext/language_tools');
this.onDisabled(this.snapshot.isDisabled);
this.setValue(this.value); this.setValue(this.value);
this.setMode(); this.setMode();
this.setHeight(); this.setOptions();
this.setWordWrap(); this.setWordWrap();
this.onDisabled(this.snapshot.isDisabled);
const langTools = ace.require('ace/ext/language_tools'); if (this.aceTools) {
if (langTools) {
this.aceEditor.setOptions({
enableBasicAutocompletion: true,
enableSnippets: true,
enableLiveAutocompletion: true,
});
const previous = this.aceEditor.completers; const previous = this.aceEditor.completers;
this.aceEditor.completers = [ this.aceEditor.completers = [
previous[0], { previous?.[0], {
getCompletions: (editor: any, session: any, pos: any, prefix: any, callback: any) => { getCompletions: (editor: any, session: any, pos: any, prefix: any, callback: any) => {
callback(null, this.completions); callback(null, this.completions);
}, },
getDocTooltip: (item: any) => { getDocTooltip: (item: any) => {
if (item.meta === 'context' && item.description) { if (item.path && item.description) {
item.docHTML = `<b>${item.value}</b><hr></hr>${item.description}`; item.docHTML = `<b>${item.value}</b><hr></hr>${item.description}`;
if (item.allowedValues) {
item.docHTML += '<div class="mt-2 mb-2">Allowed Values:<ul>';
for (const value of item.allowedValues) {
item.docHTML += `<li><code>${value}</code></li>`;
}
item.docHTML += '</ul></div>';
}
} }
}, },
// eslint-disable-next-line no-useless-escape // eslint-disable-next-line no-useless-escape
@ -176,7 +185,6 @@ export class CodeEditorComponent extends StatefulControlComponent<{}, string> im
}, },
]; ];
} }
this.aceEditor.on('blur', () => { this.aceEditor.on('blur', () => {
this.changeValue(); this.changeValue();
@ -187,6 +195,12 @@ export class CodeEditorComponent extends StatefulControlComponent<{}, string> im
this.valueChanged.next(true); this.valueChanged.next(true);
}); });
this.aceEditor.on('paste', (event: any) => {
if (this.singleLine) {
event.text = event.text.replace(/[\r\n]+/g, ' ');
}
});
this.detach(); this.detach();
}); });
} }
@ -219,39 +233,61 @@ export class CodeEditorComponent extends StatefulControlComponent<{}, string> im
} }
private setWordWrap() { private setWordWrap() {
if (this.aceEditor) { if (!this.aceEditor || this.singleLine) {
this.aceEditor.getSession().setUseWrapMode(this.wordWrap); return;
} }
this.aceEditor.getSession().setUseWrapMode(this.wordWrap);
} }
private setMode() { private setMode() {
if (this.aceEditor) { if (!this.aceEditor) {
if (this.valueFile && this.modelist) { return;
const mode = this.modelist.getModeForPath(this.valueFile).mode; }
this.aceEditor.getSession().setMode(mode); if (this.valueFile && this.modelist) {
} else { const mode = this.modelist.getModeForPath(this.valueFile).mode;
this.aceEditor.getSession().setMode(this.mode);
} this.aceEditor.getSession().setMode(mode);
} else {
this.aceEditor.getSession().setMode(this.mode);
} }
} }
private setHeight() { private setOptions() {
if (this.aceEditor && this.editor?.nativeElement) { if (!this.aceEditor) {
return;
}
let maxLines = undefined;
let minLines = undefined;
if (!this.singleLine) {
if (Types.isNumber(this.height)) { if (Types.isNumber(this.height)) {
const lines = this.height / 15; const lines = this.height / 15;
this.aceEditor.setOptions({ maxLines = lines;
minLines: lines, minLines = lines;
maxLines: lines,
});
} else if (this.height === 'auto') { } else if (this.height === 'auto') {
this.aceEditor.setOptions({ maxLines = this.maxLines || 500;
minLines: 3, minLines = Math.min(3, maxLines);
maxLines: this.maxLines || 500,
});
} }
} else {
maxLines = 1;
minLines = 1;
} }
this.aceEditor.setOptions({
autoScrollEditorIntoView: this.singleLine,
enableBasicAutocompletion: !!this.aceTools,
enableLiveAutocompletion: !!this.aceTools,
enableSnippets: !!this.aceTools && !this.singleLine,
highlightActiveLine: !this.singleLine,
maxLines,
minLines,
printMargin: !this.singleLine,
showGutter: !this.singleLine,
});
} }
private setValue(value: string) { private setValue(value: string) {

99
frontend/src/app/framework/angular/forms/editors/code-editor.stories.ts

@ -0,0 +1,99 @@
/*
* Squidex Headless CMS
*
* @license
* Copyright (c) Squidex UG (haftungsbeschränkt). All rights reserved.
*/
import { BrowserAnimationsModule } from '@angular/platform-browser/animations';
import { moduleMetadata } from '@storybook/angular';
import { Meta, Story } from '@storybook/angular/types-6-0';
import { CodeEditorComponent, SqxFrameworkModule } from '@app/framework';
export default {
title: 'Framework/CodeEditor',
component: CodeEditorComponent,
argTypes: {
disabled: {
control: 'boolean',
},
dropdownFullWidth: {
control: 'boolean',
},
},
decorators: [
moduleMetadata({
imports: [
BrowserAnimationsModule,
SqxFrameworkModule,
SqxFrameworkModule.forRoot(),
],
}),
],
} as Meta;
const Template: Story<CodeEditorComponent & { model: any }> = (args: CodeEditorComponent) => ({
props: args,
template: `
<div>
<sqx-code-editor
[borderless]="borderless"
[disabled]="disabled"
[height]="height"
[maxLines]="maxLines"
[singleLine]="singleLine"
[valueFile]="valueFile"
[valueMode]="valueMode"
[wordWrap]="wordWrap">
</sqx-code-editor>
</div>
`,
});
const SingleLineTemplate: Story<CodeEditorComponent & { model: any }> = (args: CodeEditorComponent) => ({
props: args,
template: `
<div class="row">
<div class="col">
<sqx-code-editor
[borderless]="borderless"
[completion]="completion"
[disabled]="disabled"
[height]="height"
[maxLines]="maxLines"
[singleLine]="singleLine"
[valueFile]="valueFile"
[valueMode]="valueMode"
[wordWrap]="wordWrap">
</sqx-code-editor>
</div>
<div class="col">
<input class="form-control">
</div>
</div>
`,
});
const COMPLETIONS = [{
path: 'path1',
description: 'Test1 Path',
}, {
path: 'path2',
description: 'Test2 Path',
}, {
path: 'path3',
description: 'Test3 Path',
}];
export const Default = Template.bind({});
Default.args = {
height: 'auto',
};
export const SingleLine = SingleLineTemplate.bind({});
SingleLine.args = {
completion: COMPLETIONS,
singleLine: true,
};

9
frontend/src/app/framework/angular/forms/editors/dropdown.component.ts

@ -5,10 +5,11 @@
* Copyright (c) Squidex UG (haftungsbeschränkt). All rights reserved. * Copyright (c) Squidex UG (haftungsbeschränkt). All rights reserved.
*/ */
import { AfterContentInit, ChangeDetectionStrategy, ChangeDetectorRef, Component, ContentChildren, EventEmitter, forwardRef, Input, OnChanges, OnInit, Output, QueryList, SimpleChanges, TemplateRef } from '@angular/core'; import { AfterContentInit, ChangeDetectionStrategy, ChangeDetectorRef, Component, ContentChildren, EventEmitter, forwardRef, Input, OnInit, Output, QueryList, TemplateRef } from '@angular/core';
import { NG_VALUE_ACCESSOR, UntypedFormControl } from '@angular/forms'; import { NG_VALUE_ACCESSOR, UntypedFormControl } from '@angular/forms';
import { map } from 'rxjs/operators'; import { map } from 'rxjs/operators';
import { Keys, ModalModel, RelativePosition, StatefulControlComponent, Types } from '@app/framework/internal'; import { Keys, ModalModel, RelativePosition, StatefulControlComponent, Types } from '@app/framework/internal';
import { TypedSimpleChanges } from './../../helpers';
export const SQX_DROPDOWN_CONTROL_VALUE_ACCESSOR: any = { export const SQX_DROPDOWN_CONTROL_VALUE_ACCESSOR: any = {
provide: NG_VALUE_ACCESSOR, useExisting: forwardRef(() => DropdownComponent), multi: true, provide: NG_VALUE_ACCESSOR, useExisting: forwardRef(() => DropdownComponent), multi: true,
@ -37,7 +38,7 @@ interface State {
], ],
changeDetection: ChangeDetectionStrategy.OnPush, changeDetection: ChangeDetectionStrategy.OnPush,
}) })
export class DropdownComponent extends StatefulControlComponent<State, ReadonlyArray<any>> implements AfterContentInit, OnChanges, OnInit { export class DropdownComponent extends StatefulControlComponent<State, ReadonlyArray<any>> implements AfterContentInit, OnInit {
private value: any; private value: any;
@Output() @Output()
@ -127,8 +128,8 @@ export class DropdownComponent extends StatefulControlComponent<State, ReadonlyA
})); }));
} }
public ngOnChanges(changes: SimpleChanges) { public ngOnChanges(changes: TypedSimpleChanges<this>) {
if (changes['items']) { if (changes.items) {
this.items = this.items || []; this.items = this.items || [];
this.next({ suggestedItems: this.items }); this.next({ suggestedItems: this.items });

4
frontend/src/app/framework/angular/forms/editors/radio-group.component.ts

@ -5,7 +5,7 @@
* Copyright (c) Squidex UG (haftungsbeschränkt). All rights reserved. * Copyright (c) Squidex UG (haftungsbeschränkt). All rights reserved.
*/ */
import { AfterViewChecked, AfterViewInit, ChangeDetectionStrategy, ChangeDetectorRef, Component, ElementRef, forwardRef, Input, OnChanges, ViewChild } from '@angular/core'; import { AfterViewChecked, AfterViewInit, ChangeDetectionStrategy, ChangeDetectorRef, Component, ElementRef, forwardRef, Input, ViewChild } from '@angular/core';
import { NG_VALUE_ACCESSOR } from '@angular/forms'; import { NG_VALUE_ACCESSOR } from '@angular/forms';
import { getTagValues, MathHelper, StatefulControlComponent, TagValue, TextMeasurer } from '@app/framework/internal'; import { getTagValues, MathHelper, StatefulControlComponent, TagValue, TextMeasurer } from '@app/framework/internal';
@ -27,7 +27,7 @@ interface State {
], ],
changeDetection: ChangeDetectionStrategy.OnPush, changeDetection: ChangeDetectionStrategy.OnPush,
}) })
export class RadioGroupComponent extends StatefulControlComponent<State, string> implements AfterViewInit, AfterViewChecked, OnChanges { export class RadioGroupComponent extends StatefulControlComponent<State, string> implements AfterViewInit, AfterViewChecked {
private readonly textMeasurer: TextMeasurer; private readonly textMeasurer: TextMeasurer;
private childrenWidth = 0; private childrenWidth = 0;
private containerWidth = 0; private containerWidth = 0;

8
frontend/src/app/framework/angular/forms/editors/tag-editor.component.html

@ -27,7 +27,7 @@
<i class="icon-caret-down"></i> <i class="icon-caret-down"></i>
</div> </div>
<ng-container *sqxModal="snapshot.itemsList.length > 0"> <ng-container *sqxModal="snapshot.suggestedItems.length > 0">
<sqx-dropdown-menu class="control-dropdown" <sqx-dropdown-menu class="control-dropdown"
[sqxAnchoredTo]="form" [sqxAnchoredTo]="form"
[adjustWidth]="true" [adjustWidth]="true"
@ -35,12 +35,12 @@
[scrollY]="true" [scrollY]="true"
[style.minWidth]="dropdownWidth" [style.minWidth]="dropdownWidth"
position="bottom-left" #container> position="bottom-left" #container>
<div *ngFor="let item of snapshot.itemsList; let i = index" class="control-dropdown-item control-dropdown-item-selectable" <div *ngFor="let item of snapshot.suggestedItems; let i = index" class="control-dropdown-item control-dropdown-item-selectable"
[class.active]="i === snapshot.itemsIndex" [class.active]="i === snapshot.suggestedIndex"
[class.separated]="itemSeparator" [class.separated]="itemSeparator"
(mousedown)="selectValue(item)" (mousedown)="selectValue(item)"
(mouseover)="selectIndex(i)" (mouseover)="selectIndex(i)"
[sqxScrollActive]="i === snapshot.itemsIndex" [sqxScrollActive]="i === snapshot.suggestedIndex"
[sqxScrollContainer]="container.nativeElement"> [sqxScrollContainer]="container.nativeElement">
<ng-container>{{item}}</ng-container> <ng-container>{{item}}</ng-container>
</div> </div>

61
frontend/src/app/framework/angular/forms/editors/tag-editor.component.ts

@ -5,10 +5,11 @@
* Copyright (c) Squidex UG (haftungsbeschränkt). All rights reserved. * Copyright (c) Squidex UG (haftungsbeschränkt). All rights reserved.
*/ */
import { AfterViewInit, ChangeDetectionStrategy, ChangeDetectorRef, Component, ElementRef, EventEmitter, forwardRef, Input, OnChanges, OnInit, Output, SimpleChanges, ViewChild } from '@angular/core'; import { AfterViewInit, ChangeDetectionStrategy, ChangeDetectorRef, Component, ElementRef, EventEmitter, forwardRef, Input, OnInit, Output, ViewChild } from '@angular/core';
import { NG_VALUE_ACCESSOR, UntypedFormControl } from '@angular/forms'; import { NG_VALUE_ACCESSOR, UntypedFormControl } from '@angular/forms';
import { distinctUntilChanged, map, tap } from 'rxjs/operators'; import { distinctUntilChanged, map, tap } from 'rxjs/operators';
import { getTagValues, Keys, ModalModel, StatefulControlComponent, StringConverter, TagValue, TextMeasurer, Types } from '@app/framework/internal'; import { getTagValues, Keys, ModalModel, StatefulControlComponent, StringConverter, TagValue, TextMeasurer, Types } from '@app/framework/internal';
import { TypedSimpleChanges } from './../../helpers';
export const SQX_TAG_EDITOR_CONTROL_VALUE_ACCESSOR: any = { export const SQX_TAG_EDITOR_CONTROL_VALUE_ACCESSOR: any = {
provide: NG_VALUE_ACCESSOR, useExisting: forwardRef(() => TagEditorComponent), multi: true, provide: NG_VALUE_ACCESSOR, useExisting: forwardRef(() => TagEditorComponent), multi: true,
@ -18,11 +19,11 @@ interface State {
// True, when the item has the focus. // True, when the item has the focus.
hasFocus: boolean; hasFocus: boolean;
// The suggested item. // The suggested items.
itemsList: ReadonlyArray<TagValue>; suggestedItems: ReadonlyArray<any>;
// The index of the selected suggested items. // The selected suggest item index.
itemsIndex: number; suggestedIndex: number;
// All available tag values. // All available tag values.
tags: ReadonlyArray<TagValue>; tags: ReadonlyArray<TagValue>;
@ -37,7 +38,7 @@ interface State {
], ],
changeDetection: ChangeDetectionStrategy.OnPush, changeDetection: ChangeDetectionStrategy.OnPush,
}) })
export class TagEditorComponent extends StatefulControlComponent<State, ReadonlyArray<any>> implements AfterViewInit, OnChanges, OnInit { export class TagEditorComponent extends StatefulControlComponent<State, ReadonlyArray<any>> implements AfterViewInit, OnInit {
private readonly textMeasurer: TextMeasurer; private readonly textMeasurer: TextMeasurer;
private latestValue: any; private latestValue: any;
private latestInput?: string; private latestInput?: string;
@ -114,8 +115,8 @@ export class TagEditorComponent extends StatefulControlComponent<State, Readonly
const items = this.itemsSorted.filter(s => s.lowerCaseName.includes(query) && !this.snapshot.tags.find(x => x.id === s.id)); const items = this.itemsSorted.filter(s => s.lowerCaseName.includes(query) && !this.snapshot.tags.find(x => x.id === s.id));
this.next({ this.next({
itemsIndex: -1, suggestedIndex: -1,
itemsList: items || [], suggestedItems: items || [],
}); });
} }
} }
@ -128,8 +129,8 @@ export class TagEditorComponent extends StatefulControlComponent<State, Readonly
constructor(changeDetector: ChangeDetectorRef) { constructor(changeDetector: ChangeDetectorRef) {
super(changeDetector, { super(changeDetector, {
hasFocus: false, hasFocus: false,
itemsList: [], suggestedIndex: 0,
itemsIndex: 0, suggestedItems: [],
tags: [], tags: [],
}); });
@ -140,8 +141,8 @@ export class TagEditorComponent extends StatefulControlComponent<State, Readonly
this.resetSize(); this.resetSize();
} }
public ngOnChanges(changes: SimpleChanges) { public ngOnChanges(changes: TypedSimpleChanges<this>) {
if (changes['converter']) { if (changes.itemConverter) {
this.writeValue(this.latestValue, true); this.writeValue(this.latestValue, true);
} }
} }
@ -180,8 +181,8 @@ export class TagEditorComponent extends StatefulControlComponent<State, Readonly
})) }))
.subscribe(suggestedItems => { .subscribe(suggestedItems => {
this.next({ this.next({
itemsIndex: -1, suggestedIndex: -1,
itemsList: suggestedItems, suggestedItems,
}); });
})); }));
} }
@ -194,23 +195,23 @@ export class TagEditorComponent extends StatefulControlComponent<State, Readonly
this.resetSize(); this.resetSize();
} }
const items: any[] = []; const tags: any[] = [];
if (this.itemConverter && Types.isArray(obj)) { if (this.itemConverter && Types.isArray(obj)) {
for (const value of obj) { for (const value of obj) {
if (Types.is(value, TagValue)) { if (Types.is(value, TagValue)) {
items.push(value); tags.push(value);
} else { } else {
const converted = this.itemConverter.convertValue(value); const converted = this.itemConverter.convertValue(value);
if (converted) { if (converted) {
items.push(converted); tags.push(converted);
} }
} }
} }
} }
this.next({ tags: items }); this.next({ tags });
} }
public onDisabled(isDisabled: boolean) { public onDisabled(isDisabled: boolean) {
@ -286,8 +287,8 @@ export class TagEditorComponent extends StatefulControlComponent<State, Readonly
this.selectNextIndex(); this.selectNextIndex();
return false; return false;
} else if (Keys.isEnter(event)) { } else if (Keys.isEnter(event)) {
if (this.snapshot.itemsIndex >= 0) { if (this.snapshot.suggestedIndex >= 0) {
if (this.selectValue(this.snapshot.itemsList[this.snapshot.itemsIndex])) { if (this.selectValue(this.snapshot.suggestedItems[this.snapshot.suggestedIndex])) {
return false; return false;
} }
} else if (this.acceptEnter) { } else if (this.acceptEnter) {
@ -335,11 +336,11 @@ export class TagEditorComponent extends StatefulControlComponent<State, Readonly
} }
public selectPrevIndex() { public selectPrevIndex() {
this.selectIndex(this.snapshot.itemsIndex - 1); this.selectIndex(this.snapshot.suggestedIndex - 1);
} }
public selectNextIndex() { public selectNextIndex() {
this.selectIndex(this.snapshot.itemsIndex + 1); this.selectIndex(this.snapshot.suggestedIndex + 1);
} }
public selectIndex(suggestedIndex: number) { public selectIndex(suggestedIndex: number) {
@ -347,11 +348,11 @@ export class TagEditorComponent extends StatefulControlComponent<State, Readonly
suggestedIndex = 0; suggestedIndex = 0;
} }
if (suggestedIndex >= this.snapshot.itemsList.length) { if (suggestedIndex >= this.snapshot.suggestedItems.length) {
suggestedIndex = this.snapshot.itemsList.length - 1; suggestedIndex = this.snapshot.suggestedItems.length - 1;
} }
this.next({ itemsIndex: suggestedIndex }); this.next({ suggestedIndex });
} }
public resetFocus(): any { public resetFocus(): any {
@ -359,7 +360,7 @@ export class TagEditorComponent extends StatefulControlComponent<State, Readonly
} }
private resetAutocompletion() { private resetAutocompletion() {
this.next({ itemsList: [], itemsIndex: -1 }); this.next({ suggestedItems: [], suggestedIndex: -1 });
} }
private resetForm() { private resetForm() {
@ -447,13 +448,13 @@ export class TagEditorComponent extends StatefulControlComponent<State, Readonly
return s && e && (e - s) > 0; return s && e && (e - s) > 0;
} }
private updateItems(items: ReadonlyArray<TagValue>, touched: boolean) { private updateItems(tags: ReadonlyArray<TagValue>, touched: boolean) {
this.next({ tags: items }); this.next({ tags });
if (items.length === 0 && this.undefinedWhenEmpty) { if (tags.length === 0 && this.undefinedWhenEmpty) {
this.callChange(undefined); this.callChange(undefined);
} else { } else {
this.callChange(items.map(x => x.value)); this.callChange(tags.map(x => x.value));
} }
if (touched) { if (touched) {

9
frontend/src/app/framework/angular/forms/form-error.component.ts

@ -5,8 +5,9 @@
* Copyright (c) Squidex UG (haftungsbeschränkt). All rights reserved. * Copyright (c) Squidex UG (haftungsbeschränkt). All rights reserved.
*/ */
import { ChangeDetectionStrategy, Component, Input, OnChanges, SimpleChanges } from '@angular/core'; import { ChangeDetectionStrategy, Component, Input } from '@angular/core';
import { ErrorDto } from '@app/framework/internal'; import { ErrorDto } from '@app/framework/internal';
import { TypedSimpleChanges } from './../helpers';
@Component({ @Component({
selector: 'sqx-form-error[error]', selector: 'sqx-form-error[error]',
@ -14,7 +15,7 @@ import { ErrorDto } from '@app/framework/internal';
templateUrl: './form-error.component.html', templateUrl: './form-error.component.html',
changeDetection: ChangeDetectionStrategy.OnPush, changeDetection: ChangeDetectionStrategy.OnPush,
}) })
export class FormErrorComponent implements OnChanges { export class FormErrorComponent {
@Input() @Input()
public error?: ErrorDto | null; public error?: ErrorDto | null;
@ -26,8 +27,8 @@ export class FormErrorComponent implements OnChanges {
public show = false; public show = false;
public ngOnChanges(changes: SimpleChanges) { public ngOnChanges(changes: TypedSimpleChanges<this>) {
if (changes['error']) { if (changes.error) {
this.show = !!this.error; this.show = !!this.error;
} }
} }

7
frontend/src/app/framework/angular/forms/progress-bar.component.ts

@ -5,8 +5,9 @@
* Copyright (c) Squidex UG (haftungsbeschränkt). All rights reserved. * Copyright (c) Squidex UG (haftungsbeschränkt). All rights reserved.
*/ */
import { ChangeDetectionStrategy, ChangeDetectorRef, Component, ElementRef, Input, OnChanges, OnInit, Renderer2, SimpleChanges } from '@angular/core'; import { ChangeDetectionStrategy, ChangeDetectorRef, Component, ElementRef, Input, OnInit, Renderer2 } from '@angular/core';
import * as ProgressBar from 'progressbar.js'; import * as ProgressBar from 'progressbar.js';
import { TypedSimpleChanges } from './../helpers';
@Component({ @Component({
selector: 'sqx-progress-bar', selector: 'sqx-progress-bar',
@ -18,7 +19,7 @@ import * as ProgressBar from 'progressbar.js';
template: '', template: '',
changeDetection: ChangeDetectionStrategy.OnPush, changeDetection: ChangeDetectionStrategy.OnPush,
}) })
export class ProgressBarComponent implements OnChanges, OnInit { export class ProgressBarComponent implements OnInit {
private progressBar: any; private progressBar: any;
@Input() @Input()
@ -72,7 +73,7 @@ export class ProgressBarComponent implements OnChanges, OnInit {
this.updateValue(); this.updateValue();
} }
public ngOnChanges(changes: SimpleChanges) { public ngOnChanges(changes: TypedSimpleChanges<this>) {
if (this.progressBar && changes.value) { if (this.progressBar && changes.value) {
this.updateValue(); this.updateValue();
} }

15
frontend/src/app/framework/angular/helpers.ts

@ -0,0 +1,15 @@
/*
* Squidex Headless CMS
*
* @license
* Copyright (c) Squidex UG (haftungsbeschränkt). All rights reserved.
*/
import { SimpleChange } from '@angular/core';
export type TypedSimpleChange<T> = Omit<SimpleChange, SimpleChange['previousValue'] | SimpleChange['currentValue']>
& { previousValue: T; currentValue: T };
export type TypedSimpleChanges<T> = {
[K in keyof T]: TypedSimpleChange<T[K]>;
};

4
frontend/src/app/framework/angular/image-source.directive.ts

@ -5,7 +5,7 @@
* Copyright (c) Squidex UG (haftungsbeschränkt). All rights reserved. * Copyright (c) Squidex UG (haftungsbeschränkt). All rights reserved.
*/ */
import { AfterViewInit, Directive, ElementRef, Input, NgZone, OnChanges, OnDestroy, OnInit, Renderer2 } from '@angular/core'; import { AfterViewInit, Directive, ElementRef, Input, NgZone, OnDestroy, OnInit, Renderer2 } from '@angular/core';
import { MathHelper, ResourceOwner, StringHelper } from '@app/framework/internal'; import { MathHelper, ResourceOwner, StringHelper } from '@app/framework/internal';
const LAYOUT_CACHE: { [key: string]: { width: number; height: number } } = {}; const LAYOUT_CACHE: { [key: string]: { width: number; height: number } } = {};
@ -13,7 +13,7 @@ const LAYOUT_CACHE: { [key: string]: { width: number; height: number } } = {};
@Directive({ @Directive({
selector: '[sqxImageSource]', selector: '[sqxImageSource]',
}) })
export class ImageSourceDirective extends ResourceOwner implements OnChanges, OnDestroy, OnInit, AfterViewInit { export class ImageSourceDirective extends ResourceOwner implements OnDestroy, OnInit, AfterViewInit {
private size: any; private size: any;
private loadTimer: any; private loadTimer: any;
private loadRetries = 0; private loadRetries = 0;

4
frontend/src/app/framework/angular/image-url.directive.ts

@ -5,13 +5,13 @@
* Copyright (c) Squidex UG (haftungsbeschränkt). All rights reserved. * Copyright (c) Squidex UG (haftungsbeschränkt). All rights reserved.
*/ */
import { Directive, ElementRef, HostBinding, Input, NgZone, OnChanges, OnInit, Renderer2 } from '@angular/core'; import { Directive, ElementRef, HostBinding, Input, NgZone, OnInit, Renderer2 } from '@angular/core';
import { ResourceOwner } from '@app/framework/internal'; import { ResourceOwner } from '@app/framework/internal';
@Directive({ @Directive({
selector: '[sqxImageUrl]', selector: '[sqxImageUrl]',
}) })
export class ImageUrlDirective extends ResourceOwner implements OnChanges, OnInit { export class ImageUrlDirective extends ResourceOwner implements OnInit {
@Input('sqxImageUrl') @HostBinding('attr.src') @Input('sqxImageUrl') @HostBinding('attr.src')
public imageUrl!: string; public imageUrl!: string;

4
frontend/src/app/framework/angular/language-selector.component.ts

@ -5,7 +5,7 @@
* Copyright (c) Squidex UG (haftungsbeschränkt). All rights reserved. * Copyright (c) Squidex UG (haftungsbeschränkt). All rights reserved.
*/ */
import { ChangeDetectionStrategy, Component, EventEmitter, Input, OnChanges, OnInit, Output } from '@angular/core'; import { ChangeDetectionStrategy, Component, EventEmitter, Input, OnInit, Output } from '@angular/core';
import { ModalModel, RelativePosition } from '@app/framework/internal'; import { ModalModel, RelativePosition } from '@app/framework/internal';
export interface Language { iso2Code: string; englishName: string; isMasterLanguage?: boolean } export interface Language { iso2Code: string; englishName: string; isMasterLanguage?: boolean }
@ -16,7 +16,7 @@ export interface Language { iso2Code: string; englishName: string; isMasterLangu
templateUrl: './language-selector.component.html', templateUrl: './language-selector.component.html',
changeDetection: ChangeDetectionStrategy.OnPush, changeDetection: ChangeDetectionStrategy.OnPush,
}) })
export class LanguageSelectorComponent implements OnChanges, OnInit { export class LanguageSelectorComponent implements OnInit {
@Output() @Output()
public languageChange = new EventEmitter<any>(); public languageChange = new EventEmitter<any>();

12
frontend/src/app/framework/angular/markdown.directive.spec.ts

@ -52,7 +52,15 @@ describe('MarkdownDirective', () => {
verifyHtmlRender('<strong>bold</strong>'); verifyHtmlRender('<strong>bold</strong>');
}); });
it('should render as HTML if allowed', () => { it('should render as HTML if it has encoded characters', () => {
markdownDirective.inline = false;
markdownDirective.markdown = '\'Example\'';
markdownDirective.ngOnChanges();
verifyHtmlRender('<p>&#39;Example&#39;</p>\n');
});
it('should render as HTML if it has tags', () => {
markdownDirective.inline = false; markdownDirective.inline = false;
markdownDirective.markdown = '**bold**'; markdownDirective.markdown = '**bold**';
markdownDirective.ngOnChanges(); markdownDirective.ngOnChanges();
@ -60,7 +68,7 @@ describe('MarkdownDirective', () => {
verifyHtmlRender('<p><strong>bold</strong></p>\n'); verifyHtmlRender('<p><strong>bold</strong></p>\n');
}); });
it('should render as inline HTML if allowed', () => { it('should render as inline HTML if it has tags', () => {
markdownDirective.markdown = '!**bold**'; markdownDirective.markdown = '!**bold**';
markdownDirective.ngOnChanges(); markdownDirective.ngOnChanges();

6
frontend/src/app/framework/angular/markdown.directive.ts

@ -5,13 +5,13 @@
* Copyright (c) Squidex UG (haftungsbeschränkt). All rights reserved. * Copyright (c) Squidex UG (haftungsbeschränkt). All rights reserved.
*/ */
import { Directive, ElementRef, Input, OnChanges, Renderer2 } from '@angular/core'; import { Directive, ElementRef, Input, Renderer2 } from '@angular/core';
import { renderMarkdown } from '@app/framework/internal'; import { renderMarkdown } from '@app/framework/internal';
@Directive({ @Directive({
selector: '[sqxMarkdown]', selector: '[sqxMarkdown]',
}) })
export class MarkdownDirective implements OnChanges { export class MarkdownDirective {
@Input('sqxMarkdown') @Input('sqxMarkdown')
public markdown!: string; public markdown!: string;
@ -46,7 +46,7 @@ export class MarkdownDirective implements OnChanges {
html = renderMarkdown(markdown, this.inline); html = renderMarkdown(markdown, this.inline);
} }
const hasHtml = html.indexOf('<') >= 0; const hasHtml = html.indexOf('<') >= 0 || html.indexOf('&') >= 0;
if (hasHtml) { if (hasHtml) {
this.renderer.setProperty(this.element.nativeElement, 'innerHTML', html); this.renderer.setProperty(this.element.nativeElement, 'innerHTML', html);

4
frontend/src/app/framework/angular/pager.component.ts

@ -5,7 +5,7 @@
* Copyright (c) Squidex UG (haftungsbeschränkt). All rights reserved. * Copyright (c) Squidex UG (haftungsbeschränkt). All rights reserved.
*/ */
import { ChangeDetectionStrategy, Component, EventEmitter, Input, OnChanges, Output } from '@angular/core'; import { ChangeDetectionStrategy, Component, EventEmitter, Input, Output } from '@angular/core';
import { PagingInfo } from './../state'; import { PagingInfo } from './../state';
export const PAGE_SIZES: ReadonlyArray<number> = [10, 20, 30, 50]; export const PAGE_SIZES: ReadonlyArray<number> = [10, 20, 30, 50];
@ -16,7 +16,7 @@ export const PAGE_SIZES: ReadonlyArray<number> = [10, 20, 30, 50];
templateUrl: './pager.component.html', templateUrl: './pager.component.html',
changeDetection: ChangeDetectionStrategy.OnPush, changeDetection: ChangeDetectionStrategy.OnPush,
}) })
export class PagerComponent implements OnChanges { export class PagerComponent {
@Output() @Output()
public loadTotal = new EventEmitter(); public loadTotal = new EventEmitter();

4
frontend/src/app/framework/angular/resized.directive.ts

@ -5,13 +5,13 @@
* Copyright (c) Squidex UG (haftungsbeschränkt). All rights reserved. * Copyright (c) Squidex UG (haftungsbeschränkt). All rights reserved.
*/ */
import { Directive, ElementRef, EventEmitter, Input, NgZone, OnChanges, OnDestroy, Output } from '@angular/core'; import { Directive, ElementRef, EventEmitter, Input, NgZone, OnDestroy, Output } from '@angular/core';
import { ResizeListener, ResizeService, ResourceOwner } from '@app/framework/internal'; import { ResizeListener, ResizeService, ResourceOwner } from '@app/framework/internal';
@Directive({ @Directive({
selector: '[sqxResized], [sqxResizeCondition]', selector: '[sqxResized], [sqxResizeCondition]',
}) })
export class ResizedDirective extends ResourceOwner implements OnDestroy, OnChanges, ResizeListener { export class ResizedDirective extends ResourceOwner implements OnDestroy, ResizeListener {
private condition: ((rect: DOMRect) => boolean) | undefined; private condition: ((rect: DOMRect) => boolean) | undefined;
private conditionValue = false; private conditionValue = false;

4
frontend/src/app/framework/angular/scroll-active.directive.ts

@ -5,12 +5,12 @@
* Copyright (c) Squidex UG (haftungsbeschränkt). All rights reserved. * Copyright (c) Squidex UG (haftungsbeschränkt). All rights reserved.
*/ */
import { AfterViewInit, Directive, ElementRef, Input, OnChanges, Renderer2 } from '@angular/core'; import { AfterViewInit, Directive, ElementRef, Input, Renderer2 } from '@angular/core';
@Directive({ @Directive({
selector: '[sqxScrollActive]', selector: '[sqxScrollActive]',
}) })
export class ScrollActiveDirective implements AfterViewInit, OnChanges { export class ScrollActiveDirective implements AfterViewInit {
@Input('sqxScrollActive') @Input('sqxScrollActive')
public isActive = false; public isActive = false;

7
frontend/src/app/framework/angular/template-wrapper.directive.ts

@ -5,12 +5,13 @@
* Copyright (c) Squidex UG (haftungsbeschränkt). All rights reserved. * Copyright (c) Squidex UG (haftungsbeschränkt). All rights reserved.
*/ */
import { Directive, EmbeddedViewRef, Input, OnChanges, OnDestroy, OnInit, SimpleChanges, TemplateRef, ViewContainerRef } from '@angular/core'; import { Directive, EmbeddedViewRef, Input, OnDestroy, OnInit, TemplateRef, ViewContainerRef } from '@angular/core';
import { TypedSimpleChanges } from './helpers';
@Directive({ @Directive({
selector: '[sqxTemplateWrapper]', selector: '[sqxTemplateWrapper]',
}) })
export class TemplateWrapperDirective implements OnDestroy, OnInit, OnChanges { export class TemplateWrapperDirective implements OnDestroy, OnInit {
@Input() @Input()
public item: any; public item: any;
@ -48,7 +49,7 @@ export class TemplateWrapperDirective implements OnDestroy, OnInit, OnChanges {
this.view = this.viewContainer.createEmbeddedView(this.templateRef, data); this.view = this.viewContainer.createEmbeddedView(this.templateRef, data);
} }
public ngOnChanges(changes: SimpleChanges) { public ngOnChanges(changes: TypedSimpleChanges<this>) {
if (this.view) { if (this.view) {
if (changes.item) { if (changes.item) {
this.view.context.$implicit = this.item; this.view.context.$implicit = this.item;

4
frontend/src/app/framework/angular/title.component.ts

@ -7,7 +7,7 @@
// tslint:disable: readonly-array // tslint:disable: readonly-array
import { ChangeDetectionStrategy, Component, Input, OnChanges, OnDestroy } from '@angular/core'; import { ChangeDetectionStrategy, Component, Input, OnDestroy } from '@angular/core';
import { ActivatedRoute, Router } from '@angular/router'; import { ActivatedRoute, Router } from '@angular/router';
import { TitleService, Types } from '@app/framework/internal'; import { TitleService, Types } from '@app/framework/internal';
@ -16,7 +16,7 @@ import { TitleService, Types } from '@app/framework/internal';
template: '', template: '',
changeDetection: ChangeDetectionStrategy.OnPush, changeDetection: ChangeDetectionStrategy.OnPush,
}) })
export class TitleComponent implements OnDestroy, OnChanges { export class TitleComponent implements OnDestroy {
private previousIndex: undefined | number; private previousIndex: undefined | number;
@Input() @Input()

4
frontend/src/app/framework/angular/video-player.component.ts

@ -5,7 +5,7 @@
* Copyright (c) Squidex UG (haftungsbeschränkt). All rights reserved. * Copyright (c) Squidex UG (haftungsbeschränkt). All rights reserved.
*/ */
import { AfterViewInit, ChangeDetectionStrategy, Component, ElementRef, Input, OnChanges, OnDestroy, Renderer2, ViewChild } from '@angular/core'; import { AfterViewInit, ChangeDetectionStrategy, Component, ElementRef, Input, OnDestroy, Renderer2, ViewChild } from '@angular/core';
import { ResourceLoaderService } from '@app/framework/internal'; import { ResourceLoaderService } from '@app/framework/internal';
declare const videojs: any; declare const videojs: any;
@ -16,7 +16,7 @@ declare const videojs: any;
templateUrl: './video-player.component.html', templateUrl: './video-player.component.html',
changeDetection: ChangeDetectionStrategy.OnPush, changeDetection: ChangeDetectionStrategy.OnPush,
}) })
export class VideoPlayerComponent implements AfterViewInit, OnDestroy, OnChanges { export class VideoPlayerComponent implements AfterViewInit, OnDestroy {
private player: any; private player: any;
@Input() @Input()

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

@ -41,6 +41,7 @@ export * from './angular/forms/templated-form-array';
export * from './angular/forms/transform-input.directive'; export * from './angular/forms/transform-input.directive';
export * from './angular/forms/validators'; export * from './angular/forms/validators';
export * from './angular/global-error-handler'; export * from './angular/global-error-handler';
export * from './angular/helpers';
export * from './angular/hover-background.directive'; export * from './angular/hover-background.directive';
export * from './angular/http/caching.interceptor'; export * from './angular/http/caching.interceptor';
export * from './angular/http/http-extensions'; export * from './angular/http/http-extensions';

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

Loading…
Cancel
Save