Browse Source

Comment fixes (#1143)

* Simplified editors.

* Primary ctor.

* Move inheritance to new line

* Formatting fixes.

* Fix registration.

* Textarea improvements.

* Comment fixes.

* Improve positioning.

* Formatting fixes.
pull/1144/head
Sebastian Stehle 2 years ago
committed by GitHub
parent
commit
2b8c93dc76
No known key found for this signature in database GPG Key ID: B5690EEEBB952194
  1. 2
      backend/src/Squidex.Domain.Apps.Entities.MongoDb/Text/LuceneSearchDefinitionExtensions.cs
  2. 4
      backend/src/Squidex.Domain.Apps.Entities/Collaboration/CommentCollaborationHandler.cs
  3. 5
      backend/src/Squidex.Domain.Apps.Entities/Squidex.Domain.Apps.Entities.csproj
  4. 1
      backend/src/Squidex/Areas/Api/Controllers/Assets/AssetsController.cs
  5. 4
      backend/src/Squidex/Pipeline/Squid/SquidMiddleware.cs
  6. 2
      backend/tests/Squidex.Domain.Apps.Entities.Tests/Squidex.Domain.Apps.Entities.Tests.csproj
  7. 2
      backend/tests/Squidex.Infrastructure.Tests/EventSourcing/Consume/EventConsumerProcessorTests.cs
  8. 202
      frontend/package-lock.json
  9. 12
      frontend/package.json
  10. 5
      frontend/src/app/declarations.d.ts
  11. 1
      frontend/src/app/features/teams/pages/contributors/contributor-add-form.component.ts
  12. 51
      frontend/src/app/framework/angular/autosize.directive.ts
  13. 63
      frontend/src/app/framework/angular/forms/editors/autocomplete.component.html
  14. 25
      frontend/src/app/framework/angular/forms/editors/autocomplete.component.scss
  15. 170
      frontend/src/app/framework/angular/forms/editors/autocomplete.component.ts
  16. 48
      frontend/src/app/framework/angular/forms/editors/autocomplete.stories.ts
  17. 2
      frontend/src/app/framework/utils/markdown.ts
  18. 51
      frontend/src/app/shared/components/comments/comment.component.html
  19. 44
      frontend/src/app/shared/components/comments/comment.component.ts
  20. 70
      frontend/src/app/shared/components/comments/comments.component.html
  21. 30
      frontend/src/app/shared/components/comments/comments.component.scss
  22. 37
      frontend/src/app/shared/components/comments/comments.component.ts
  23. 33
      frontend/src/app/shared/components/comments/data-source.ts
  24. 1
      frontend/src/app/shell/index.ts
  25. 7
      frontend/src/app/shell/pages/internal/feedback-menu.component.html
  26. 2
      frontend/src/app/shell/pages/internal/feedback-menu.component.scss
  27. 41
      frontend/src/app/shell/pages/internal/feedback-menu.component.ts
  28. 1
      frontend/src/app/shell/pages/internal/internal-area.component.html
  29. 2
      frontend/src/app/shell/pages/internal/internal-area.component.ts
  30. 2
      frontend/src/app/shell/pages/internal/notification-dropdown.component.html

2
backend/src/Squidex.Domain.Apps.Entities.MongoDb/Text/LuceneSearchDefinitionExtensions.cs

@ -35,7 +35,7 @@ public static class LuceneSearchDefinitionExtensions
string operatorName,
Func<IBsonSerializer<TInput>,
RenderedPipelineStageDefinition<TOutput>> renderer)
: PipelineStageDefinition<TInput, TOutput>
: PipelineStageDefinition<TInput, TOutput>
{
public override string OperatorName { get; } = operatorName;

4
backend/src/Squidex.Domain.Apps.Entities/Collaboration/CommentCollaborationHandler.cs

@ -74,7 +74,7 @@ public sealed partial class CommentCollaborationHandler(
var commentValue = new Comment(clock.GetCurrentInstant(), actor, text, url, skipHandlers);
var commentJson = jsonSerializer.Serialize(commentValue);
stream.InsertRange(transaction, stream.Length, InputFactory.FromJson(commentJson));
stream.InsertRange(transaction, stream.Length(transaction), InputFactory.FromJson(commentJson));
}
}, ct);
}
@ -157,7 +157,7 @@ public sealed partial class CommentCollaborationHandler(
var eventBody = Envelope.Create<IEvent>(commentEvent);
var eventData = eventFormatter.ToEventData(eventBody, Guid.NewGuid());
await eventStore.AppendAsync(Guid.NewGuid(), streamName, EtagVersion.Any, new List<EventData> { eventData });
await eventStore.AppendAsync(Guid.NewGuid(), streamName, EtagVersion.Any, [eventData]);
foreach (var mentionedUser in commentEvent.Mentions.OrEmpty())
{

5
backend/src/Squidex.Domain.Apps.Entities/Squidex.Domain.Apps.Entities.csproj

@ -35,8 +35,9 @@
<PackageReference Include="Notifo.SDK" Version="1.7.5" />
<PackageReference Include="RefactoringEssentials" Version="5.6.0" PrivateAssets="all" />
<PackageReference Include="Squidex.CLI.Core" Version="13.6.0" />
<PackageReference Include="Squidex.YDotNet.Extensions" Version="0.2.9" />
<PackageReference Include="Squidex.YDotNet.Server" Version="0.2.9" />
<PackageReference Include="YDotNet" Version="0.4.1" />
<PackageReference Include="YDotNet.Extensions" Version="0.4.1" />
<PackageReference Include="YDotNet.Server" Version="0.4.1" />
<PackageReference Include="StyleCop.Analyzers" Version="1.1.118" PrivateAssets="all" />
<PackageReference Include="System.Collections.Immutable" Version="8.0.0" />
<PackageReference Include="System.ValueTuple" Version="4.5.0" />

1
backend/src/Squidex/Areas/Api/Controllers/Assets/AssetsController.cs

@ -29,7 +29,6 @@ namespace Squidex.Areas.Api.Controllers.Assets;
public sealed class AssetsController(
ICommandBus commandBus,
IAssetQueryService assetQuery,
IAssetUsageTracker assetUsageTracker,
ITagService tagService,
AssetTusRunner assetTusRunner)
: ApiController(commandBus)

4
backend/src/Squidex/Pipeline/Squid/SquidMiddleware.cs

@ -11,7 +11,9 @@ using Squidex.Infrastructure;
namespace Squidex.Pipeline.Squid;
#pragma warning disable CS9113 // Parameter is unread.
public sealed class SquidMiddleware(RequestDelegate next)
#pragma warning restore CS9113 // Parameter is unread.
{
private readonly string squidHappyLG = LoadSvg("happy");
private readonly string squidHappySM = LoadSvg("happy-sm");
@ -97,7 +99,7 @@ public sealed class SquidMiddleware(RequestDelegate next)
context.Response.StatusCode = 200;
context.Response.ContentType = "image/svg+xml";
context.Response.Headers["Cache-Control"] = "public, max-age=604800";
context.Response.Headers.CacheControl = "public, max-age=604800";
await context.Response.WriteAsync(svg, context.RequestAborted);
}

2
backend/tests/Squidex.Domain.Apps.Entities.Tests/Squidex.Domain.Apps.Entities.Tests.csproj

@ -35,7 +35,7 @@
<PackageReference Include="Microsoft.Extensions.Options" Version="8.0.2" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.11.1" />
<PackageReference Include="NodaTime.Serialization.JsonNet" Version="3.1.0" />
<PackageReference Include="Squidex.YDotNet.Native" Version="0.2.9" />
<PackageReference Include="YDotNet.Native" Version="0.4.1" />
<PackageReference Include="StyleCop.Analyzers" Version="1.1.118" PrivateAssets="all" />
<PackageReference Include="System.Reactive.Linq" Version="6.0.1" />
<PackageReference Include="System.ValueTuple" Version="4.5.0" />

2
backend/tests/Squidex.Infrastructure.Tests/EventSourcing/Consume/EventConsumerProcessorTests.cs

@ -19,7 +19,7 @@ public class EventConsumerProcessorTests
IEventFormatter eventFormatter,
IEventStore eventStore,
ILogger<EventConsumerProcessor> log)
: EventConsumerProcessor(persistenceFactory, eventConsumer, eventFormatter, eventStore, log)
: EventConsumerProcessor(persistenceFactory, eventConsumer, eventFormatter, eventStore, log)
{
public IEventSubscriber<StoredEvent>? Subscriber { get; set; }

202
frontend/package-lock.json

@ -24,14 +24,9 @@
"@graphiql/toolkit": "^0.9.1",
"@iharbeck/ngx-virtual-scroller": "^17.0.2",
"@lithiumjs/angular": "^8.0.0",
"@lithiumjs/ngx-virtual-scroll": "^0.3.2",
"@marker.io/browser": "^0.19.0",
"@types/ace": "^0.0.52",
"ace-builds": "^1.34.2",
"angular-gridster2": "18.0.1",
"angular-mentions": "1.5.0",
"bootstrap": "5.2.3",
"copy-webpack-plugin": "^12.0.2",
"core-js": "3.37.1",
"cropperjs": "2.0.0-alpha.1",
"date-fns": "3.6.0",
@ -39,12 +34,10 @@
"graphql": "16.8.1",
"graphql-ws": "^5.16.0",
"image-focus": "1.2.1",
"keycharm": "0.4.0",
"leaflet": "1.9.4",
"leaflet-control-geocoder": "2.4.0",
"marked": "12.0.2",
"mersenne-twister": "1.1.0",
"moment": "^2.30.1",
"mousetrap": "1.6.5",
"ng2-charts": "^6.0.1",
"ngx-color-picker": "16.0.0",
@ -56,8 +49,8 @@
"react": "18.3.1",
"react-dom": "18.3.1",
"rxjs": "7.8.1",
"simplemde": "1.11.2",
"slugify": "1.6.6",
"textarea-caret": "github:component/textarea-caret-position",
"tslib": "2.6.2",
"tui-calendar": "^1.15.3",
"typemoq": "^2.1.0",
@ -88,6 +81,7 @@
"@storybook/addon-links": "^8.1.5",
"@storybook/angular": "^8.1.5",
"@storybook/testing-library": "^0.2.2",
"@types/ace": "^0.0.52",
"@types/codemirror": "5.60.15",
"@types/core-js": "2.5.8",
"@types/jasmine": "5.1.4",
@ -97,12 +91,10 @@
"@types/node": "20.12.13",
"@types/react": "18.3.3",
"@types/react-dom": "18.3.0",
"@types/simplemde": "1.11.11",
"@types/tapable": "2.2.7",
"@types/ws": "8.5.10",
"@typescript-eslint/eslint-plugin": "^7.11.0",
"@typescript-eslint/parser": "^7.11.0",
"@webcomponents/custom-elements": "^1.6.0",
"copy-webpack-plugin": "^12.0.2",
"eslint": "^8.57.0",
"eslint-config-airbnb-typescript": "18.0.0",
"eslint-plugin-deprecation": "^2.0.0",
@ -4930,6 +4922,7 @@
"version": "0.3.5",
"resolved": "https://registry.npmjs.org/@jridgewell/source-map/-/source-map-0.3.5.tgz",
"integrity": "sha512-UTYAUj/wviwdsMfzoSJspJxbkH5o1snzwX0//0ENX1u/55kkZZkcTZP6u9bwKGkv+dkk9at4m1Cpt0uY80kcpQ==",
"dev": true,
"dependencies": {
"@jridgewell/gen-mapping": "^0.3.0",
"@jridgewell/trace-mapping": "^0.3.9"
@ -5052,20 +5045,6 @@
"typescript": ">=4.8.0"
}
},
"node_modules/@lithiumjs/ngx-virtual-scroll": {
"version": "0.3.2",
"resolved": "https://registry.npmjs.org/@lithiumjs/ngx-virtual-scroll/-/ngx-virtual-scroll-0.3.2.tgz",
"integrity": "sha512-4cGuG8n5rdsgziaorKrKQ5Rew/+BNyBfmMEffgxi+2NLRvIZusuDgEEJBIk9WK09gPTg+sEwpiWW1SmKL2iWnw==",
"dependencies": {
"tslib": "^2.3.0"
},
"peerDependencies": {
"@angular/common": "8.x.x - 18.x.x",
"@angular/core": "8.x.x - 18.x.x",
"@lithiumjs/angular": ">=7.0.0",
"rxjs": "6.x.x - 7.x.x"
}
},
"node_modules/@ljharb/through": {
"version": "2.3.13",
"resolved": "https://registry.npmjs.org/@ljharb/through/-/through-2.3.13.tgz",
@ -5156,11 +5135,6 @@
"win32"
]
},
"node_modules/@marker.io/browser": {
"version": "0.19.0",
"resolved": "https://registry.npmjs.org/@marker.io/browser/-/browser-0.19.0.tgz",
"integrity": "sha512-tK0FN5uMKw3cyUYECWstaMW/5qoz9AhMp21GWR1ppJcIFaAu8k0lyHvsWFzqnHDfaoTs3dBF52DjarQQB74ipw=="
},
"node_modules/@mdx-js/react": {
"version": "3.0.1",
"resolved": "https://registry.npmjs.org/@mdx-js/react/-/react-3.0.1.tgz",
@ -6870,6 +6844,7 @@
"version": "2.3.0",
"resolved": "https://registry.npmjs.org/@sindresorhus/merge-streams/-/merge-streams-2.3.0.tgz",
"integrity": "sha512-LtoMMhxAlorcGhmFYI+LhPgbPZCkgP6ra1YL604EeF6U98pLlQ3iWIGMdWSC+vWmPBWBNgmDBAhnAobLROJmwg==",
"dev": true,
"engines": {
"node": ">=18"
},
@ -10190,7 +10165,8 @@
"node_modules/@types/ace": {
"version": "0.0.52",
"resolved": "https://registry.npmjs.org/@types/ace/-/ace-0.0.52.tgz",
"integrity": "sha512-YPF9S7fzpuyrxru+sG/rrTpZkC6gpHBPF14W3x70kqVOD+ks6jkYLapk4yceh36xej7K4HYxcyz9ZDQ2lTvwgQ=="
"integrity": "sha512-YPF9S7fzpuyrxru+sG/rrTpZkC6gpHBPF14W3x70kqVOD+ks6jkYLapk4yceh36xej7K4HYxcyz9ZDQ2lTvwgQ==",
"dev": true
},
"node_modules/@types/aria-query": {
"version": "5.0.1",
@ -10345,6 +10321,7 @@
"version": "8.44.1",
"resolved": "https://registry.npmjs.org/@types/eslint/-/eslint-8.44.1.tgz",
"integrity": "sha512-XpNDc4Z5Tb4x+SW1MriMVeIsMoONHCkWFMkR/aPJbzEsxqHy+4Glu/BqTdPrApfDeMaXbtNh6bseNgl5KaWrSg==",
"dev": true,
"dependencies": {
"@types/estree": "*",
"@types/json-schema": "*"
@ -10352,6 +10329,7 @@
},
"node_modules/@types/eslint-scope": {
"version": "3.7.4",
"dev": true,
"license": "MIT",
"dependencies": {
"@types/eslint": "*",
@ -10429,7 +10407,8 @@
"node_modules/@types/json-schema": {
"version": "7.0.12",
"resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.12.tgz",
"integrity": "sha512-Hr5Jfhc9eYOQNPYO5WLDq/n4jqijdHNlDXjuAQkkt+mWdQR+XJToOHrsD4cPaMXpn6KO7y2+wM8AZEs8VpBLVA=="
"integrity": "sha512-Hr5Jfhc9eYOQNPYO5WLDq/n4jqijdHNlDXjuAQkkt+mWdQR+XJToOHrsD4cPaMXpn6KO7y2+wM8AZEs8VpBLVA==",
"dev": true
},
"node_modules/@types/json5": {
"version": "0.0.29",
@ -10477,6 +10456,7 @@
"version": "20.12.13",
"resolved": "https://registry.npmjs.org/@types/node/-/node-20.12.13.tgz",
"integrity": "sha512-gBGeanV41c1L171rR7wjbMiEpEI/l5XFQdLLfhr/REwpgDy/4U8y89+i8kRiLzDyZdOkXh+cRaTetUnCYutoXA==",
"devOptional": true,
"dependencies": {
"undici-types": "~5.26.4"
}
@ -10585,12 +10565,6 @@
"@types/send": "*"
}
},
"node_modules/@types/simplemde": {
"version": "1.11.11",
"resolved": "https://registry.npmjs.org/@types/simplemde/-/simplemde-1.11.11.tgz",
"integrity": "sha512-H3ml0YJtx9XMs2pGHwQy/UHzkg9W3Ke3QikQ/jHhEZj1dfmPbWqIvaNyNrt3dtiVlztWhMgK02+Rmoem+Jot/Q==",
"dev": true
},
"node_modules/@types/sockjs": {
"version": "0.3.36",
"resolved": "https://registry.npmjs.org/@types/sockjs/-/sockjs-0.3.36.tgz",
@ -10600,15 +10574,6 @@
"@types/node": "*"
}
},
"node_modules/@types/tapable": {
"version": "2.2.7",
"resolved": "https://registry.npmjs.org/@types/tapable/-/tapable-2.2.7.tgz",
"integrity": "sha512-D6QzACV9vNX3r8HQQNTOnpG+Bv1rko+yEA82wKs3O9CQ5+XW7HI7TED17/UE7+5dIxyxZIWTxKbsBeF6uKFCwA==",
"dev": true,
"dependencies": {
"tapable": "^2.2.0"
}
},
"node_modules/@types/tern": {
"version": "0.23.4",
"license": "MIT",
@ -11160,6 +11125,7 @@
"version": "1.12.1",
"resolved": "https://registry.npmjs.org/@webassemblyjs/ast/-/ast-1.12.1.tgz",
"integrity": "sha512-EKfMUOPRRUTy5UII4qJDGPpqfwjOmZ5jeGFwid9mnoqIFK+e0vqoi1qH56JpmZSzEL53jKnNzScdmftJyG5xWg==",
"dev": true,
"dependencies": {
"@webassemblyjs/helper-numbers": "1.11.6",
"@webassemblyjs/helper-wasm-bytecode": "1.11.6"
@ -11168,22 +11134,26 @@
"node_modules/@webassemblyjs/floating-point-hex-parser": {
"version": "1.11.6",
"resolved": "https://registry.npmjs.org/@webassemblyjs/floating-point-hex-parser/-/floating-point-hex-parser-1.11.6.tgz",
"integrity": "sha512-ejAj9hfRJ2XMsNHk/v6Fu2dGS+i4UaXBXGemOfQ/JfQ6mdQg/WXtwleQRLLS4OvfDhv8rYnVwH27YJLMyYsxhw=="
"integrity": "sha512-ejAj9hfRJ2XMsNHk/v6Fu2dGS+i4UaXBXGemOfQ/JfQ6mdQg/WXtwleQRLLS4OvfDhv8rYnVwH27YJLMyYsxhw==",
"dev": true
},
"node_modules/@webassemblyjs/helper-api-error": {
"version": "1.11.6",
"resolved": "https://registry.npmjs.org/@webassemblyjs/helper-api-error/-/helper-api-error-1.11.6.tgz",
"integrity": "sha512-o0YkoP4pVu4rN8aTJgAyj9hC2Sv5UlkzCHhxqWj8butaLvnpdc2jOwh4ewE6CX0txSfLn/UYaV/pheS2Txg//Q=="
"integrity": "sha512-o0YkoP4pVu4rN8aTJgAyj9hC2Sv5UlkzCHhxqWj8butaLvnpdc2jOwh4ewE6CX0txSfLn/UYaV/pheS2Txg//Q==",
"dev": true
},
"node_modules/@webassemblyjs/helper-buffer": {
"version": "1.12.1",
"resolved": "https://registry.npmjs.org/@webassemblyjs/helper-buffer/-/helper-buffer-1.12.1.tgz",
"integrity": "sha512-nzJwQw99DNDKr9BVCOZcLuJJUlqkJh+kVzVl6Fmq/tI5ZtEyWT1KZMyOXltXLZJmDtvLCDgwsyrkohEtopTXCw=="
"integrity": "sha512-nzJwQw99DNDKr9BVCOZcLuJJUlqkJh+kVzVl6Fmq/tI5ZtEyWT1KZMyOXltXLZJmDtvLCDgwsyrkohEtopTXCw==",
"dev": true
},
"node_modules/@webassemblyjs/helper-numbers": {
"version": "1.11.6",
"resolved": "https://registry.npmjs.org/@webassemblyjs/helper-numbers/-/helper-numbers-1.11.6.tgz",
"integrity": "sha512-vUIhZ8LZoIWHBohiEObxVm6hwP034jwmc9kuq5GdHZH0wiLVLIPcMCdpJzG4C11cHoQ25TFIQj9kaVADVX7N3g==",
"dev": true,
"dependencies": {
"@webassemblyjs/floating-point-hex-parser": "1.11.6",
"@webassemblyjs/helper-api-error": "1.11.6",
@ -11193,12 +11163,14 @@
"node_modules/@webassemblyjs/helper-wasm-bytecode": {
"version": "1.11.6",
"resolved": "https://registry.npmjs.org/@webassemblyjs/helper-wasm-bytecode/-/helper-wasm-bytecode-1.11.6.tgz",
"integrity": "sha512-sFFHKwcmBprO9e7Icf0+gddyWYDViL8bpPjJJl0WHxCdETktXdmtWLGVzoHbqUcY4Be1LkNfwTmXOJUFZYSJdA=="
"integrity": "sha512-sFFHKwcmBprO9e7Icf0+gddyWYDViL8bpPjJJl0WHxCdETktXdmtWLGVzoHbqUcY4Be1LkNfwTmXOJUFZYSJdA==",
"dev": true
},
"node_modules/@webassemblyjs/helper-wasm-section": {
"version": "1.12.1",
"resolved": "https://registry.npmjs.org/@webassemblyjs/helper-wasm-section/-/helper-wasm-section-1.12.1.tgz",
"integrity": "sha512-Jif4vfB6FJlUlSbgEMHUyk1j234GTNG9dBJ4XJdOySoj518Xj0oGsNi59cUQF4RRMS9ouBUxDDdyBVfPTypa5g==",
"dev": true,
"dependencies": {
"@webassemblyjs/ast": "1.12.1",
"@webassemblyjs/helper-buffer": "1.12.1",
@ -11210,6 +11182,7 @@
"version": "1.11.6",
"resolved": "https://registry.npmjs.org/@webassemblyjs/ieee754/-/ieee754-1.11.6.tgz",
"integrity": "sha512-LM4p2csPNvbij6U1f19v6WR56QZ8JcHg3QIJTlSwzFcmx6WSORicYj6I63f9yU1kEUtrpG+kjkiIAkevHpDXrg==",
"dev": true,
"dependencies": {
"@xtuc/ieee754": "^1.2.0"
}
@ -11218,6 +11191,7 @@
"version": "1.11.6",
"resolved": "https://registry.npmjs.org/@webassemblyjs/leb128/-/leb128-1.11.6.tgz",
"integrity": "sha512-m7a0FhE67DQXgouf1tbN5XQcdWoNgaAuoULHIfGFIEVKA6tu/edls6XnIlkmS6FrXAquJRPni3ZZKjw6FSPjPQ==",
"dev": true,
"dependencies": {
"@xtuc/long": "4.2.2"
}
@ -11225,12 +11199,14 @@
"node_modules/@webassemblyjs/utf8": {
"version": "1.11.6",
"resolved": "https://registry.npmjs.org/@webassemblyjs/utf8/-/utf8-1.11.6.tgz",
"integrity": "sha512-vtXf2wTQ3+up9Zsg8sa2yWiQpzSsMyXj0qViVP6xKGCUT8p8YJ6HqI7l5eCnWx1T/FYdsv07HQs2wTFbbof/RA=="
"integrity": "sha512-vtXf2wTQ3+up9Zsg8sa2yWiQpzSsMyXj0qViVP6xKGCUT8p8YJ6HqI7l5eCnWx1T/FYdsv07HQs2wTFbbof/RA==",
"dev": true
},
"node_modules/@webassemblyjs/wasm-edit": {
"version": "1.12.1",
"resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-edit/-/wasm-edit-1.12.1.tgz",
"integrity": "sha512-1DuwbVvADvS5mGnXbE+c9NfA8QRcZ6iKquqjjmR10k6o+zzsRVesil54DKexiowcFCPdr/Q0qaMgB01+SQ1u6g==",
"dev": true,
"dependencies": {
"@webassemblyjs/ast": "1.12.1",
"@webassemblyjs/helper-buffer": "1.12.1",
@ -11246,6 +11222,7 @@
"version": "1.12.1",
"resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-gen/-/wasm-gen-1.12.1.tgz",
"integrity": "sha512-TDq4Ojh9fcohAw6OIMXqiIcTq5KUXTGRkVxbSo1hQnSy6lAM5GSdfwWeSxpAo0YzgsgF182E/U0mDNhuA0tW7w==",
"dev": true,
"dependencies": {
"@webassemblyjs/ast": "1.12.1",
"@webassemblyjs/helper-wasm-bytecode": "1.11.6",
@ -11258,6 +11235,7 @@
"version": "1.12.1",
"resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-opt/-/wasm-opt-1.12.1.tgz",
"integrity": "sha512-Jg99j/2gG2iaz3hijw857AVYekZe2SAskcqlWIZXjji5WStnOpVoat3gQfT/Q5tb2djnCjBtMocY/Su1GfxPBg==",
"dev": true,
"dependencies": {
"@webassemblyjs/ast": "1.12.1",
"@webassemblyjs/helper-buffer": "1.12.1",
@ -11269,6 +11247,7 @@
"version": "1.12.1",
"resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-parser/-/wasm-parser-1.12.1.tgz",
"integrity": "sha512-xikIi7c2FHXysxXe3COrVUPSheuBtpcfhbpFj4gmu7KRLYOzANztwUU0IbsqvMqzuNK2+glRGWCEqZo1WCLyAQ==",
"dev": true,
"dependencies": {
"@webassemblyjs/ast": "1.12.1",
"@webassemblyjs/helper-api-error": "1.11.6",
@ -11282,17 +11261,12 @@
"version": "1.12.1",
"resolved": "https://registry.npmjs.org/@webassemblyjs/wast-printer/-/wast-printer-1.12.1.tgz",
"integrity": "sha512-+X4WAlOisVWQMikjbcvY2e0rwPsKQ9F688lksZhBcPycBBuii3O7m8FACbDMWDojpAqvjIncrG8J0XHKyQfVeA==",
"dev": true,
"dependencies": {
"@webassemblyjs/ast": "1.12.1",
"@xtuc/long": "4.2.2"
}
},
"node_modules/@webcomponents/custom-elements": {
"version": "1.6.0",
"resolved": "https://registry.npmjs.org/@webcomponents/custom-elements/-/custom-elements-1.6.0.tgz",
"integrity": "sha512-CqTpxOlUCPWRNUPZDxT5v2NnHXA4oox612iUGnmTUGQFhZ1Gkj8kirtl/2wcF6MqX7+PqqicZzOCBKKfIn0dww==",
"dev": true
},
"node_modules/@xmldom/xmldom": {
"version": "0.8.10",
"resolved": "https://registry.npmjs.org/@xmldom/xmldom/-/xmldom-0.8.10.tgz",
@ -11304,12 +11278,14 @@
"node_modules/@xtuc/ieee754": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/@xtuc/ieee754/-/ieee754-1.2.0.tgz",
"integrity": "sha512-DX8nKgqcGwsc0eJSqYt5lwP4DH5FlHnmuWWBRy7X0NcaGR0ZtuyeESgMwTYVEtxmsNGY+qit4QYT/MIYTOTPeA=="
"integrity": "sha512-DX8nKgqcGwsc0eJSqYt5lwP4DH5FlHnmuWWBRy7X0NcaGR0ZtuyeESgMwTYVEtxmsNGY+qit4QYT/MIYTOTPeA==",
"dev": true
},
"node_modules/@xtuc/long": {
"version": "4.2.2",
"resolved": "https://registry.npmjs.org/@xtuc/long/-/long-4.2.2.tgz",
"integrity": "sha512-NuHqBY1PB/D8xU6s/thBgOAiAP7HOYDQ32+BFZILJ8ivkUkAHQnWfn6WhL79Owj1qmUnoN/YPhktdIoucipkAQ=="
"integrity": "sha512-NuHqBY1PB/D8xU6s/thBgOAiAP7HOYDQ32+BFZILJ8ivkUkAHQnWfn6WhL79Owj1qmUnoN/YPhktdIoucipkAQ==",
"dev": true
},
"node_modules/@yarnpkg/esbuild-plugin-pnp": {
"version": "3.0.0-rc.15",
@ -11446,6 +11422,7 @@
"version": "8.11.3",
"resolved": "https://registry.npmjs.org/acorn/-/acorn-8.11.3.tgz",
"integrity": "sha512-Y9rRfJG5jcKOE0CLisYbojUjIrIEE7AGMzA/Sm4BslANhbS+cDMpgBdcPT91oJ7OuJ9hYJBx59RjbhxVnrF8Xg==",
"dev": true,
"bin": {
"acorn": "bin/acorn"
},
@ -11457,6 +11434,7 @@
"version": "1.9.0",
"resolved": "https://registry.npmjs.org/acorn-import-assertions/-/acorn-import-assertions-1.9.0.tgz",
"integrity": "sha512-cmMwop9x+8KFhxvKrKfPYmN6/pKTYYHBqLa0DfvVZcKMJWNyWLnaqND7dx/qn66R7ewM1UX5XMaDVP5wlVTaVA==",
"dev": true,
"peerDependencies": {
"acorn": "^8"
}
@ -11557,6 +11535,7 @@
"version": "8.13.0",
"resolved": "https://registry.npmjs.org/ajv/-/ajv-8.13.0.tgz",
"integrity": "sha512-PRA911Blj99jR5RMeTunVbNXMF6Lp4vZXnk5GQjcnUWUTsrXtekg/pnmFFI2u/I36Y/2bITGS30GZCXei6uNkA==",
"dev": true,
"dependencies": {
"fast-deep-equal": "^3.1.3",
"json-schema-traverse": "^1.0.0",
@ -11570,6 +11549,7 @@
},
"node_modules/ajv-formats": {
"version": "2.1.1",
"dev": true,
"license": "MIT",
"dependencies": {
"ajv": "^8.0.0"
@ -11585,6 +11565,7 @@
},
"node_modules/ajv-keywords": {
"version": "5.1.0",
"dev": true,
"license": "MIT",
"dependencies": {
"fast-deep-equal": "^3.1.3"
@ -11606,17 +11587,6 @@
"rxjs": "^7.0.0"
}
},
"node_modules/angular-mentions": {
"version": "1.5.0",
"license": "MIT",
"dependencies": {
"tslib": "^2.0.0"
},
"peerDependencies": {
"@angular/common": ">=7.2.0",
"@angular/core": ">=7.2.0"
}
},
"node_modules/ansi-colors": {
"version": "4.1.3",
"resolved": "https://registry.npmjs.org/ansi-colors/-/ansi-colors-4.1.3.tgz",
@ -12525,6 +12495,7 @@
},
"node_modules/buffer-from": {
"version": "1.1.2",
"dev": true,
"license": "MIT"
},
"node_modules/bundle-name": {
@ -13008,6 +12979,7 @@
},
"node_modules/chrome-trace-event": {
"version": "1.0.3",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=6.0"
@ -13173,13 +13145,6 @@
"@types/tern": "*"
}
},
"node_modules/codemirror-spell-checker": {
"version": "1.1.2",
"license": "MIT",
"dependencies": {
"typo-js": "*"
}
},
"node_modules/color-convert": {
"version": "1.9.3",
"resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz",
@ -13234,6 +13199,7 @@
},
"node_modules/commander": {
"version": "2.20.3",
"dev": true,
"license": "MIT"
},
"node_modules/common-path-prefix": {
@ -13456,6 +13422,7 @@
"version": "12.0.2",
"resolved": "https://registry.npmjs.org/copy-webpack-plugin/-/copy-webpack-plugin-12.0.2.tgz",
"integrity": "sha512-SNwdBeHyII+rWvee/bTnAYyO8vfVdcSTud4EIb6jcZ8inLeWucJE0DnxXQBjlQ5zlteuuvooGQy3LIyGxhvlOA==",
"dev": true,
"dependencies": {
"fast-glob": "^3.3.2",
"glob-parent": "^6.0.1",
@ -13477,6 +13444,7 @@
},
"node_modules/copy-webpack-plugin/node_modules/glob-parent": {
"version": "6.0.2",
"dev": true,
"license": "ISC",
"dependencies": {
"is-glob": "^4.0.3"
@ -13489,6 +13457,7 @@
"version": "14.0.1",
"resolved": "https://registry.npmjs.org/globby/-/globby-14.0.1.tgz",
"integrity": "sha512-jOMLD2Z7MAhyG8aJpNOpmziMOP4rPLcc95oQPKXBazW82z+CEgPFBQvEpRUa1KeIMUJo4Wsm+q6uzO/Q/4BksQ==",
"dev": true,
"dependencies": {
"@sindresorhus/merge-streams": "^2.1.0",
"fast-glob": "^3.3.2",
@ -13508,6 +13477,7 @@
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/path-type/-/path-type-5.0.0.tgz",
"integrity": "sha512-5HviZNaZcfqP95rwpv+1HDgUamezbqdSYTyzjTvwtJSnIH+3vnbmWsItli8OFEndS984VT55M3jduxZbX351gg==",
"dev": true,
"engines": {
"node": ">=12"
},
@ -13519,6 +13489,7 @@
"version": "5.1.0",
"resolved": "https://registry.npmjs.org/slash/-/slash-5.1.0.tgz",
"integrity": "sha512-ZA6oR3T/pEyuqwMgAKT0/hAv8oAXckzbkmR0UkUosQ+Mc4RxGoJkRmwHgHufaenlyAgE1Mxgpdcrf75y6XcnDg==",
"dev": true,
"engines": {
"node": ">=14.16"
},
@ -14726,6 +14697,7 @@
"version": "5.16.1",
"resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.16.1.tgz",
"integrity": "sha512-4U5pNsuDl0EhuZpq46M5xPslstkviJuhrdobaRDBk2Jy2KO37FDAJl4lb2KlNabxT0m4MTK2UHNrsAcphE8nyw==",
"dev": true,
"dependencies": {
"graceful-fs": "^4.2.4",
"tapable": "^2.2.0"
@ -14937,7 +14909,8 @@
"node_modules/es-module-lexer": {
"version": "1.5.3",
"resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-1.5.3.tgz",
"integrity": "sha512-i1gCgmR9dCl6Vil6UKPI/trA69s08g/syhiDK9TG0Nf1RJjjFI+AzoWW7sPufzkgYAn861skuCwJa0pIIHYxvg=="
"integrity": "sha512-i1gCgmR9dCl6Vil6UKPI/trA69s08g/syhiDK9TG0Nf1RJjjFI+AzoWW7sPufzkgYAn861skuCwJa0pIIHYxvg==",
"dev": true
},
"node_modules/es-object-atoms": {
"version": "1.0.0",
@ -15565,6 +15538,7 @@
},
"node_modules/eslint-scope": {
"version": "5.1.1",
"dev": true,
"license": "BSD-2-Clause",
"dependencies": {
"esrecurse": "^4.3.0",
@ -15871,6 +15845,7 @@
},
"node_modules/esrecurse": {
"version": "4.3.0",
"dev": true,
"license": "BSD-2-Clause",
"dependencies": {
"estraverse": "^5.2.0"
@ -15881,6 +15856,7 @@
},
"node_modules/esrecurse/node_modules/estraverse": {
"version": "5.3.0",
"dev": true,
"license": "BSD-2-Clause",
"engines": {
"node": ">=4.0"
@ -15888,6 +15864,7 @@
},
"node_modules/estraverse": {
"version": "4.3.0",
"dev": true,
"license": "BSD-2-Clause",
"engines": {
"node": ">=4.0"
@ -15939,6 +15916,7 @@
},
"node_modules/events": {
"version": "3.3.0",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=0.8.x"
@ -16151,6 +16129,7 @@
},
"node_modules/fast-deep-equal": {
"version": "3.1.3",
"dev": true,
"license": "MIT"
},
"node_modules/fast-glob": {
@ -16170,6 +16149,7 @@
},
"node_modules/fast-json-stable-stringify": {
"version": "2.1.0",
"dev": true,
"license": "MIT"
},
"node_modules/fast-levenshtein": {
@ -16949,6 +16929,7 @@
},
"node_modules/glob-to-regexp": {
"version": "0.4.1",
"dev": true,
"license": "BSD-2-Clause"
},
"node_modules/global": {
@ -17049,7 +17030,8 @@
"node_modules/graceful-fs": {
"version": "4.2.11",
"resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz",
"integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ=="
"integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==",
"dev": true
},
"node_modules/graphemer": {
"version": "1.4.0",
@ -17639,6 +17621,7 @@
"version": "5.3.1",
"resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.1.tgz",
"integrity": "sha512-5Fytz/IraMjqpwfd34ke28PTVMjZjJG2MPn5t7OE4eUCUNf8BAa7b5WUS9/Qvr6mwOQS7Mk6vdsMno5he+T8Xw==",
"dev": true,
"engines": {
"node": ">= 4"
}
@ -18837,6 +18820,7 @@
},
"node_modules/jest-worker": {
"version": "27.4.5",
"dev": true,
"license": "MIT",
"dependencies": {
"@types/node": "*",
@ -18849,6 +18833,7 @@
},
"node_modules/jest-worker/node_modules/has-flag": {
"version": "4.0.0",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=8"
@ -18856,6 +18841,7 @@
},
"node_modules/jest-worker/node_modules/supports-color": {
"version": "8.1.1",
"dev": true,
"license": "MIT",
"dependencies": {
"has-flag": "^4.0.0"
@ -19025,10 +19011,12 @@
},
"node_modules/json-parse-even-better-errors": {
"version": "2.3.1",
"dev": true,
"license": "MIT"
},
"node_modules/json-schema-traverse": {
"version": "1.0.0",
"dev": true,
"license": "MIT"
},
"node_modules/json-stable-stringify-without-jsonify": {
@ -19238,7 +19226,8 @@
},
"node_modules/keycharm": {
"version": "0.4.0",
"license": "(Apache-2.0 OR MIT)"
"license": "(Apache-2.0 OR MIT)",
"peer": true
},
"node_modules/keycode": {
"version": "2.2.0",
@ -19653,6 +19642,7 @@
},
"node_modules/loader-runner": {
"version": "4.2.0",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=6.11.5"
@ -20094,6 +20084,7 @@
},
"node_modules/merge-stream": {
"version": "2.0.0",
"dev": true,
"license": "MIT"
},
"node_modules/merge2": {
@ -20156,6 +20147,7 @@
},
"node_modules/mime-db": {
"version": "1.51.0",
"dev": true,
"license": "MIT",
"engines": {
"node": ">= 0.6"
@ -20163,6 +20155,7 @@
},
"node_modules/mime-types": {
"version": "2.1.34",
"dev": true,
"license": "MIT",
"dependencies": {
"mime-db": "1.51.0"
@ -20377,6 +20370,7 @@
"version": "2.30.1",
"resolved": "https://registry.npmjs.org/moment/-/moment-2.30.1.tgz",
"integrity": "sha512-uEmtNhbDOrWPFS+hdjFCBfy9f2YoyzRpwcl+DqpC6taX21FzsTLQVbMV/W7PzNSX6x/bhC1zA3c2UQ5NzH6how==",
"dev": true,
"engines": {
"node": "*"
}
@ -20598,6 +20592,7 @@
},
"node_modules/neo-async": {
"version": "2.6.2",
"dev": true,
"license": "MIT"
},
"node_modules/ng2-charts": {
@ -22913,6 +22908,7 @@
},
"node_modules/punycode": {
"version": "2.1.1",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=6"
@ -22978,6 +22974,7 @@
},
"node_modules/randombytes": {
"version": "2.1.0",
"dev": true,
"license": "MIT",
"dependencies": {
"safe-buffer": "^5.1.0"
@ -23392,6 +23389,7 @@
},
"node_modules/require-from-string": {
"version": "2.0.2",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=0.10.0"
@ -23734,6 +23732,7 @@
"version": "4.2.0",
"resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-4.2.0.tgz",
"integrity": "sha512-L0jRsrPpjdckP3oPug3/VxNKt2trR8TcabrM6FOAAlvC/9Phcmm+cuAgTlxBqdBR1WJx7Naj9WHw+aOmheSVbw==",
"dev": true,
"dependencies": {
"@types/json-schema": "^7.0.9",
"ajv": "^8.9.0",
@ -23862,6 +23861,7 @@
"version": "6.0.2",
"resolved": "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-6.0.2.tgz",
"integrity": "sha512-Saa1xPByTTq2gdeFZYLLo+RFE35NHZkAbqZeWNd3BpzppeVisAqpDjcp8dyf6uIvEqJRd46jemmyA4iFIeVk8g==",
"dev": true,
"dependencies": {
"randombytes": "^2.1.0"
}
@ -24074,15 +24074,6 @@
"node": "^16.14.0 || >=18.0.0"
}
},
"node_modules/simplemde": {
"version": "1.11.2",
"license": "MIT",
"dependencies": {
"codemirror": "*",
"codemirror-spell-checker": "*",
"marked": "*"
}
},
"node_modules/sisteransi": {
"version": "1.0.5",
"resolved": "https://registry.npmjs.org/sisteransi/-/sisteransi-1.0.5.tgz",
@ -24289,6 +24280,7 @@
"version": "0.6.1",
"resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz",
"integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==",
"dev": true,
"engines": {
"node": ">=0.10.0"
}
@ -24336,6 +24328,7 @@
},
"node_modules/source-map-support": {
"version": "0.5.21",
"dev": true,
"license": "MIT",
"dependencies": {
"buffer-from": "^1.0.0",
@ -25121,6 +25114,7 @@
},
"node_modules/tapable": {
"version": "2.2.1",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=6"
@ -25275,6 +25269,7 @@
"version": "5.31.0",
"resolved": "https://registry.npmjs.org/terser/-/terser-5.31.0.tgz",
"integrity": "sha512-Q1JFAoUKE5IMfI4Z/lkE/E6+SwgzO+x4tq4v1AyBLRj8VSYvRO6A/rQrPg1yud4g0En9EKI1TvFRF2tQFcoUkg==",
"dev": true,
"dependencies": {
"@jridgewell/source-map": "^0.3.3",
"acorn": "^8.8.2",
@ -25292,6 +25287,7 @@
"version": "5.3.10",
"resolved": "https://registry.npmjs.org/terser-webpack-plugin/-/terser-webpack-plugin-5.3.10.tgz",
"integrity": "sha512-BKFPWlPDndPs+NGGCr1U59t0XScL5317Y0UReNrHaw9/FwhPENlq6bfgs+4yPfyP51vqC1bQ4rp1EfXW5ZSH9w==",
"dev": true,
"dependencies": {
"@jridgewell/trace-mapping": "^0.3.20",
"jest-worker": "^27.4.5",
@ -25323,6 +25319,7 @@
},
"node_modules/terser-webpack-plugin/node_modules/ajv": {
"version": "6.12.6",
"dev": true,
"license": "MIT",
"dependencies": {
"fast-deep-equal": "^3.1.1",
@ -25337,6 +25334,7 @@
},
"node_modules/terser-webpack-plugin/node_modules/ajv-keywords": {
"version": "3.5.2",
"dev": true,
"license": "MIT",
"peerDependencies": {
"ajv": "^6.9.1"
@ -25344,10 +25342,12 @@
},
"node_modules/terser-webpack-plugin/node_modules/json-schema-traverse": {
"version": "0.4.1",
"dev": true,
"license": "MIT"
},
"node_modules/terser-webpack-plugin/node_modules/schema-utils": {
"version": "3.1.1",
"dev": true,
"license": "MIT",
"dependencies": {
"@types/json-schema": "^7.0.8",
@ -25380,6 +25380,10 @@
"dev": true,
"license": "MIT"
},
"node_modules/textarea-caret": {
"version": "3.1.0",
"resolved": "git+ssh://git@github.com/component/textarea-caret-position.git#b5845a4c39cf094b56925183c086f92c8f8fec68"
},
"node_modules/thingies": {
"version": "1.21.0",
"resolved": "https://registry.npmjs.org/thingies/-/thingies-1.21.0.tgz",
@ -25936,10 +25940,6 @@
"node": ">=14.17"
}
},
"node_modules/typo-js": {
"version": "1.2.1",
"license": "BSD-3-Clause"
},
"node_modules/ua-parser-js": {
"version": "0.7.31",
"dev": true,
@ -26008,7 +26008,8 @@
"node_modules/undici-types": {
"version": "5.26.5",
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-5.26.5.tgz",
"integrity": "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA=="
"integrity": "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==",
"devOptional": true
},
"node_modules/unicode-canonical-property-names-ecmascript": {
"version": "2.0.0",
@ -26074,6 +26075,7 @@
"version": "0.1.0",
"resolved": "https://registry.npmjs.org/unicorn-magic/-/unicorn-magic-0.1.0.tgz",
"integrity": "sha512-lRfVq8fE8gz6QMBuDM6a+LO3IAzTi05H6gCVaUpir2E1Rwpo4ZUog45KpNXKC/Mn3Yb9UDuHumeFTo9iV/D9FQ==",
"dev": true,
"engines": {
"node": ">=18"
},
@ -26245,6 +26247,7 @@
},
"node_modules/uri-js": {
"version": "4.4.1",
"dev": true,
"license": "BSD-2-Clause",
"dependencies": {
"punycode": "^2.1.0"
@ -26990,6 +26993,7 @@
"version": "2.4.1",
"resolved": "https://registry.npmjs.org/watchpack/-/watchpack-2.4.1.tgz",
"integrity": "sha512-8wrBCMtVhqcXP2Sup1ctSkga6uc2Bx0IIvKyT7yTFier5AXHooSI+QyQQAtTb7+E0IUCCKyTFmXqdqgum2XWGg==",
"dev": true,
"dependencies": {
"glob-to-regexp": "^0.4.1",
"graceful-fs": "^4.1.2"
@ -27031,6 +27035,7 @@
"version": "5.91.0",
"resolved": "https://registry.npmjs.org/webpack/-/webpack-5.91.0.tgz",
"integrity": "sha512-rzVwlLeBWHJbmgTC/8TvAcu5vpJNII+MelQpylD4jNERPwpBJOE2lEcko1zJX3QJeLjTTAnQxn/OJ8bjDzVQaw==",
"dev": true,
"dependencies": {
"@types/eslint-scope": "^3.7.3",
"@types/estree": "^1.0.5",
@ -27384,6 +27389,7 @@
},
"node_modules/webpack-sources": {
"version": "3.2.3",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=10.13.0"
@ -27420,6 +27426,7 @@
"version": "6.12.6",
"resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz",
"integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==",
"dev": true,
"dependencies": {
"fast-deep-equal": "^3.1.1",
"fast-json-stable-stringify": "^2.0.0",
@ -27435,6 +27442,7 @@
"version": "3.5.2",
"resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-3.5.2.tgz",
"integrity": "sha512-5p6WTN0DdTGVQk6VjcEju19IgaHudalcfabD7yhDGeA6bcQnmL+CpveLJq/3hvfwd1aof6L386Ougkx6RfyMIQ==",
"dev": true,
"peerDependencies": {
"ajv": "^6.9.1"
}
@ -27442,12 +27450,14 @@
"node_modules/webpack/node_modules/json-schema-traverse": {
"version": "0.4.1",
"resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz",
"integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg=="
"integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==",
"dev": true
},
"node_modules/webpack/node_modules/schema-utils": {
"version": "3.3.0",
"resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-3.3.0.tgz",
"integrity": "sha512-pN/yOAvcC+5rQ5nERGuwrjLlYvLTbCibnZ1I7B1LaiAz9BRBlE9GMgE/eqV30P7aJQUf7Ddimy/RsbYO/GrVGg==",
"dev": true,
"dependencies": {
"@types/json-schema": "^7.0.8",
"ajv": "^6.12.5",

12
frontend/package.json

@ -31,13 +31,9 @@
"@graphiql/toolkit": "^0.9.1",
"@iharbeck/ngx-virtual-scroller": "^17.0.2",
"@lithiumjs/angular": "^8.0.0",
"@lithiumjs/ngx-virtual-scroll": "^0.3.2",
"@marker.io/browser": "^0.19.0",
"ace-builds": "^1.34.2",
"angular-gridster2": "18.0.1",
"angular-mentions": "1.5.0",
"bootstrap": "5.2.3",
"copy-webpack-plugin": "^12.0.2",
"core-js": "3.37.1",
"cropperjs": "2.0.0-alpha.1",
"date-fns": "3.6.0",
@ -45,12 +41,10 @@
"graphql": "16.8.1",
"graphql-ws": "^5.16.0",
"image-focus": "1.2.1",
"keycharm": "0.4.0",
"leaflet": "1.9.4",
"leaflet-control-geocoder": "2.4.0",
"marked": "12.0.2",
"mersenne-twister": "1.1.0",
"moment": "^2.30.1",
"mousetrap": "1.6.5",
"ng2-charts": "^6.0.1",
"ngx-color-picker": "16.0.0",
@ -62,8 +56,8 @@
"react": "18.3.1",
"react-dom": "18.3.1",
"rxjs": "7.8.1",
"simplemde": "1.11.2",
"slugify": "1.6.6",
"textarea-caret": "github:component/textarea-caret-position",
"tslib": "2.6.2",
"tui-calendar": "^1.15.3",
"typemoq": "^2.1.0",
@ -104,12 +98,10 @@
"@types/node": "20.12.13",
"@types/react": "18.3.3",
"@types/react-dom": "18.3.0",
"@types/simplemde": "1.11.11",
"@types/tapable": "2.2.7",
"@types/ws": "8.5.10",
"@typescript-eslint/eslint-plugin": "^7.11.0",
"@typescript-eslint/parser": "^7.11.0",
"@webcomponents/custom-elements": "^1.6.0",
"copy-webpack-plugin": "^12.0.2",
"eslint": "^8.57.0",
"eslint-config-airbnb-typescript": "18.0.0",
"eslint-plugin-deprecation": "^2.0.0",

5
frontend/src/app/declarations.d.ts

@ -8,6 +8,11 @@
declare module 'pikaday/pikaday';
declare module 'progressbar.js';
declare module 'textarea-caret' {
function getCaretCoordinates(element: any, position: number | null): { top: number; left: number };
export default getCaretCoordinates;
}
declare class SquidexEditorWrapper {
constructor(element: HTMLElement, props: EditorProps);

1
frontend/src/app/features/teams/pages/contributors/contributor-add-form.component.ts

@ -38,6 +38,7 @@ export class UsersDataSource implements AutocompleteSource {
results.push(user);
}
}
return results;
}));
}

51
frontend/src/app/framework/angular/autosize.directive.ts

@ -0,0 +1,51 @@
/*
* Squidex Headless CMS
*
* @license
* Copyright (c) Squidex UG (haftungsbeschränkt). All rights reserved.
*/
import { AfterViewInit, Directive, DoCheck, ElementRef, HostBinding, HostListener, Input, numberAttribute, Renderer2 } from '@angular/core';
@Directive({
standalone: true,
selector: 'textarea[sqxAutosize]',
})
export class AutosizeDirective implements AfterViewInit, DoCheck {
@HostBinding('style.overflow')
public overflow = 'hidden';
@Input({ transform: numberAttribute })
@HostBinding('rows')
public rows = 1;
constructor(
private readonly element: ElementRef<HTMLTextAreaElement>,
private readonly renderer: Renderer2,
) {
}
public ngAfterViewInit() {
this.resize();
}
public ngDoCheck() {
this.resize();
}
@HostListener('input')
private resize() {
const textarea = this.element.nativeElement as HTMLTextAreaElement;
// Calculate border height which is not included in scrollHeight
const borderHeight = textarea.offsetHeight - textarea.clientHeight;
this.setHeight(textarea, 'auto');
this.setHeight(textarea, `${textarea.scrollHeight + borderHeight}px`);
}
private setHeight(element: HTMLTextAreaElement, value: string) {
this.renderer.setStyle(element, 'height', value);
}
}

63
frontend/src/app/framework/angular/forms/editors/autocomplete.component.html

@ -1,20 +1,47 @@
<div class="control-container">
<input
class="form-control"
[id]="formId"
#input
autocapitalize="off"
autocomplete="off"
autocorrect="off"
(blur)="blur()"
[class.form-empty]="inputStyle === 'empty'"
[class.form-icon]="!!icon"
[class.form-underlined]="inputStyle === 'underlined'"
[formControl]="queryInput"
(keydown)="onKeyDown($event)"
[name]="formName"
[placeholder]="placeholder"
[sqxFocusOnInit]="autoFocus" />
<div class="anchor invisible2" #anchor [class.cursor-size]="startCharacter" [class.full-size]="!startCharacter">
<span class="form-control w-100">&nbsp;</span>
</div>
@if (textArea) {
<textarea
class="form-control"
[id]="formId"
#input
autocapitalize="off"
autocomplete="off"
autocorrect="off"
(blur)="blur()"
[class.form-empty]="inputStyle === 'empty'"
[class.form-icon]="!!icon"
[class.form-underlined]="inputStyle === 'underlined'"
[formControl]="queryInput"
(keydown)="onKeyDown($event)"
(keypress)="editorKeyPress.emit($event)"
[name]="formName"
[placeholder]="placeholder"
rows="2"
sqxAutosize
[sqxFocusOnInit]="autoFocus"></textarea>
} @else {
<input
class="form-control"
[id]="formId"
#input
autocapitalize="off"
autocomplete="off"
autocorrect="off"
(blur)="blur()"
[class.form-empty]="inputStyle === 'empty'"
[class.form-icon]="!!icon"
[class.form-underlined]="inputStyle === 'underlined'"
[formControl]="queryInput"
(keydown)="onKeyDown($event)"
(keypress)="editorKeyPress.emit($event)"
[name]="formName"
[placeholder]="placeholder"
[sqxFocusOnInit]="autoFocus" />
}
@if (icon) {
<div class="icon">
@ -36,11 +63,11 @@
class="control-dropdown"
#container
adjustHeight="false"
[adjustWidth]="dropdownFullWidth"
[adjustWidth]="dropdownFullWidth && !startCharacter"
[position]="dropdownPosition"
scrollX="false"
scrollY="true"
[sqxAnchoredTo]="input"
[sqxAnchoredTo]="anchor"
*sqxModal="suggestionsModal"
[style]="dropdownStyles">
@for (item of snapshot.suggestedItems; track item; let i = $index) {

25
frontend/src/app/framework/angular/forms/editors/autocomplete.component.scss

@ -5,6 +5,25 @@
position: relative;
}
.anchor {
@include absolute(0, null, null, 0);
background-color: red !important;
z-index: 1000;
opacity: .1;
& * {
background-color: red !important;
}
&.full-size {
@include absolute(0, 0, 0, 0);
}
&.cursor-size {
margin-top: -.375rem !important;
}
}
.form-icon {
padding-left: 2.25rem;
}
@ -24,4 +43,10 @@
font-weight: normal;
padding-left: 5px;
padding-right: 5px;
}
textarea {
overflow-x: hidden;
overflow-y: hidden;
resize: none;
}

170
frontend/src/app/framework/angular/forms/editors/autocomplete.component.ts

@ -5,12 +5,13 @@
* Copyright (c) Squidex UG (haftungsbeschränkt). All rights reserved.
*/
import { booleanAttribute, ChangeDetectionStrategy, Component, ContentChild, ElementRef, forwardRef, Input, numberAttribute, OnDestroy, OnInit, TemplateRef, ViewChild } from '@angular/core';
import { booleanAttribute, ChangeDetectionStrategy, Component, ContentChild, ElementRef, EventEmitter, forwardRef, Input, numberAttribute, OnDestroy, OnInit, Output, TemplateRef, ViewChild } from '@angular/core';
import { FormsModule, NG_VALUE_ACCESSOR, ReactiveFormsModule, UntypedFormControl } from '@angular/forms';
import { merge, Observable, of, Subject } from 'rxjs';
import { catchError, debounceTime, finalize, map, switchMap, tap } from 'rxjs/operators';
import getCaretCoordinates from 'textarea-caret';
import { FloatingPlacement, Keys, ModalModel, StatefulControlComponent, Subscriptions, Types } from '@app/framework/internal';
import { AutosizeDirective } from '../../autosize.directive';
import { DropdownMenuComponent } from '../../dropdown-menu.component';
import { LoaderComponent } from '../../loader.component';
import { ModalPlacementDirective } from '../../modals/modal-placement.directive';
@ -24,10 +25,24 @@ export interface AutocompleteSource {
find(query: string): Observable<ReadonlyArray<any>>;
}
export const NOOP_DATASOURCE = {
find() {
return of([]);
},
};
export const SQX_AUTOCOMPLETE_CONTROL_VALUE_ACCESSOR: any = {
provide: NG_VALUE_ACCESSOR, useExisting: forwardRef(() => AutocompleteComponent), multi: true,
};
interface Query {
// The query text.
text: string;
// The range.
range?: { from: number; to: number };
}
interface State {
// The suggested items.
suggestedItems: ReadonlyArray<any>;
@ -38,11 +53,16 @@ interface State {
// True, when the searching is in progress.
isSearching?: boolean;
// The last query.
lastQuery?: Query;
// Indicates whether the loading is in progress.
isLoading?: boolean;
}
const NO_EMIT = { emitEvent: false };
const NO_QUERY = { text: '' };
const RANGE_LIMIT = 60;
@Component({
standalone: true,
@ -54,6 +74,7 @@ const NO_EMIT = { emitEvent: false };
],
changeDetection: ChangeDetectionStrategy.OnPush,
imports: [
AutosizeDirective,
DropdownMenuComponent,
FocusOnInitDirective,
FormsModule,
@ -68,9 +89,24 @@ const NO_EMIT = { emitEvent: false };
})
export class AutocompleteComponent extends StatefulControlComponent<State, ReadonlyArray<any>> implements OnInit, OnDestroy {
private readonly subscriptions = new Subscriptions();
private readonly modalStream = new Subject<string>();
private readonly modalStream = new Subject<Query>();
private lastCursor = 0;
private timer: any;
@Output()
public editorBlur = new EventEmitter();
@Output()
public editorKeyDown = new EventEmitter<KeyboardEvent>();
@Output()
public editorKeyPress = new EventEmitter<KeyboardEvent>();
@Input({ transform: booleanAttribute })
public set disabled(value: boolean | undefined | null) {
this.setDisabledState(value === true);
}
@Input({ required: true })
public itemsSource!: AutocompleteSource;
@ -86,6 +122,9 @@ export class AutocompleteComponent extends StatefulControlComponent<State, Reado
@Input()
public formName?: string;
@Input({ transform: booleanAttribute })
public textArea?: boolean;
@Input()
public displayProperty = '';
@ -95,12 +134,18 @@ export class AutocompleteComponent extends StatefulControlComponent<State, Reado
@Input()
public placeholder = '';
@Input()
public startCharacter = '';
@Input()
public icon = '';
@Input({ transform: booleanAttribute })
public autoFocus?: boolean | null;
@Input({ transform: booleanAttribute })
public autoSelectFirst = true;
@Input({ transform: numberAttribute })
public debounceTime = 300;
@ -113,14 +158,12 @@ export class AutocompleteComponent extends StatefulControlComponent<State, Reado
@Input()
public dropdownStyles: any = {};
@Input({ transform: booleanAttribute })
public set disabled(value: boolean | undefined | null) {
this.setDisabledState(value === true);
}
@ContentChild(TemplateRef, { static: false })
public itemTemplate!: TemplateRef<any>;
@ViewChild('anchor', { static: false })
public anchor!: ElementRef<HTMLDivElement>;
@ViewChild('input', { static: false })
public inputControl!: ElementRef<HTMLInputElement>;
@ -148,29 +191,55 @@ export class AutocompleteComponent extends StatefulControlComponent<State, Reado
}
});
const inputStream =
const queryStream: Observable<Query> =
this.queryInput.valueChanges.pipe(
tap(query => {
this.callChange(query);
this.next({ lastQuery: undefined });
}),
map(query => {
if (Types.isString(query)) {
return query.trim();
} else {
return '';
map((text: string) => {
if (!Types.isString(text)) {
return NO_QUERY;
}
if (!this.startCharacter) {
return { text: text.trim() };
}
let rangeTo = Math.min(this.lastCursor, text.length);
let rangeFrom = -1;
for (let j = rangeTo - 1; j >= Math.max(0, rangeTo - RANGE_LIMIT); j--) {
const char = text[j];
if (char === this.startCharacter) {
rangeFrom = j;
}
if (/[\s]/.test(char)) {
break;
}
}
if (rangeFrom >= 0 && rangeFrom < rangeTo) {
text = text.substring(rangeFrom + 1, rangeTo + 1);
return { text, range: { from: rangeFrom, to: rangeTo } };
}
return { text: '' };
}),
debounceTime(this.debounceTime));
this.subscriptions.add(
merge(inputStream, this.modalStream).pipe(
merge(queryStream, this.modalStream).pipe(
switchMap(query => {
if (!this.itemsSource) {
this.next({ lastQuery: query });
if (!this.itemsSource || query.text === '') {
return of([]);
} else {
this.setLoading(true);
return this.itemsSource.find(query).pipe(
return this.itemsSource.find(query.text ).pipe(
finalize(() => {
this.setLoading(false);
}),
@ -179,6 +248,7 @@ export class AutocompleteComponent extends StatefulControlComponent<State, Reado
}
}))
.subscribe(items => {
this.updateAnchor();
this.next({
suggestedIndex: -1,
suggestedItems: items || [],
@ -188,17 +258,26 @@ export class AutocompleteComponent extends StatefulControlComponent<State, Reado
}
public onKeyDown(event: KeyboardEvent) {
this.lastCursor = this.inputControl.nativeElement.selectionEnd || 0;
this.editorKeyDown.emit(event);
if (Keys.isEscape(event)) {
this.resetForm();
this.reset();
} else if (Keys.isUp(event)) {
}
if (this.snapshot.suggestedItems.length === 0) {
return true;
}
if (Keys.isUp(event)) {
this.selectPrevIndex();
return false;
} else if (Keys.isDown(event)) {
this.selectNextIndex();
return false;
} else if (Keys.isEnter(event)) {
return !(this.snapshot.suggestedItems.length > 0 && this.selectItem());
return !this.selectItem();
}
return true;
@ -227,25 +306,23 @@ export class AutocompleteComponent extends StatefulControlComponent<State, Reado
}
public openModal() {
this.modalStream.next('');
this.modalStream.next({ text: '' });
}
public reset() {
this.resetState();
this.queryInput.setValue('', NO_EMIT);
}
public focus() {
this.resetState();
this.inputControl.nativeElement.focus();
}
public blur() {
this.resetState();
this.callTouched();
this.editorBlur.emit();
}
public selectItem(selection: any | null = null): boolean {
@ -253,7 +330,7 @@ export class AutocompleteComponent extends StatefulControlComponent<State, Reado
selection = this.snapshot.suggestedItems[this.snapshot.suggestedIndex];
}
if (!selection && this.snapshot.suggestedItems.length === 1) {
if (!selection && this.snapshot.suggestedItems.length === 1 && this.autoSelectFirst) {
selection = this.snapshot.suggestedItems[0];
}
@ -262,14 +339,25 @@ export class AutocompleteComponent extends StatefulControlComponent<State, Reado
}
try {
let displayString: string;
if (this.displayProperty && this.displayProperty.length > 0) {
this.queryInput.setValue(selection[this.displayProperty], NO_EMIT);
displayString = selection[this.displayProperty];
} else {
this.queryInput.setValue(selection.toString(), NO_EMIT);
displayString = selection.toString();
}
let value = selection;
const query = this.snapshot.lastQuery;
if (query?.range) {
const input = this.queryInput.value;
const textBefore = input.substring(0, query.range.from);
const textAfter = input.substring(query.range.to + 1);
displayString = `${textBefore}${this.startCharacter}${displayString}${textAfter}`;
}
this.queryInput.setValue(displayString, NO_EMIT);
let value = selection;
if (this.valueProperty) {
value = selection[this.valueProperty];
}
@ -318,4 +406,32 @@ export class AutocompleteComponent extends StatefulControlComponent<State, Reado
private resetForm() {
this.queryInput.setValue('', NO_EMIT);
}
private updateAnchor() {
if (!this.startCharacter) {
return;
}
const query = this.snapshot.lastQuery;
if (!query?.range) {
return;
}
const inputWidth = this.inputControl.nativeElement.getBoundingClientRect().width;
const coordsStart = getCaretCoordinates(this.inputControl.nativeElement, query.range.from);
const coordsEnd = getCaretCoordinates(this.inputControl.nativeElement, query.range.to + 1);
let w = coordsEnd.left - coordsStart.left;
let x = coordsStart.left;
if (coordsEnd.left > inputWidth) {
x = inputWidth - w;
}
x = Math.max(0, x);
w = Math.min(w, inputWidth);
this.anchor.nativeElement.style.top = `${coordsStart.top - 2}px`;
this.anchor.nativeElement.style.left = `${x}px`;
this.anchor.nativeElement.style.width = `${w}px`;
}
}

48
frontend/src/app/framework/angular/forms/editors/autocomplete.stories.ts

@ -5,6 +5,7 @@
* Copyright (c) Squidex UG (haftungsbeschränkt). All rights reserved.
*/
import { BrowserAnimationsModule } from '@angular/platform-browser/animations';
import { Meta, moduleMetadata, StoryObj } from '@storybook/angular';
import { map, Observable, timer } from 'rxjs';
import { AutocompleteComponent, AutocompleteSource, LocalizerService, RootViewComponent } from '@app/framework';
@ -29,18 +30,25 @@ export default {
props: args,
template: `
<sqx-root-view>
<sqx-autocomplete
[disabled]="disabled"
[icon]="icon"
[inputStyle]="inputStyle"
[itemsSource]="itemsSource">
</sqx-autocomplete>
<div style="margin-top: 100px">
<sqx-autocomplete
[debounceTime]="debounceTime"
[disabled]="disabled"
[dropdownFullWidth]="dropdownFullWidth"
[icon]="icon"
[inputStyle]="inputStyle"
[itemsSource]="itemsSource"
[startCharacter]="startCharacter"
[textArea]="textArea">
</sqx-autocomplete>
</div>
</sqx-root-view>
`,
}),
decorators: [
moduleMetadata({
imports: [
BrowserAnimationsModule,
RootViewComponent,
],
providers: [
@ -61,7 +69,7 @@ class Source implements AutocompleteSource {
}
public find(query: string): Observable<readonly any[]> {
return timer(this.delay).pipe(map(() => this.values.filter(x => x.indexOf(query) >= 0)));
return timer(this.delay).pipe(map(() => this.values.filter(x => query.length > 0 && x.indexOf(query) >= 0)));
}
}
@ -98,9 +106,33 @@ export const StyleUnderlined: Story = {
},
};
export const StyleFullWidth: Story = {
args: {
itemsSource: new Source(['Lorem', 'ipsum', 'dolor', 'sit', 'amet', 'consectetur', 'adipiscing']),
dropdownFullWidth: true,
},
};
export const IconLoading: Story = {
args: {
itemsSource: new Source(['Lorem', 'ipsum', 'dolor', 'sit', 'amet', 'consectetur', 'adipiscing'], 4000),
itemsSource: new Source(['Lorem', 'ipsum', 'dolor', 'sit', 'amet', 'consectetur', 'adipiscing'], 2000),
icon: 'user',
},
};
export const StartCharacter: Story = {
args: {
debounceTime: 0,
itemsSource: new Source(['donald@duck.com', 'scrooge@mcduck.com']),
startCharacter: '@',
},
};
export const TextArea: Story = {
args: {
debounceTime: 0,
itemsSource: new Source(['Lorem', 'ipsum', 'dolor', 'sit', 'amet', 'consectetur', 'adipiscing']),
startCharacter: '@',
textArea: true,
},
};

2
frontend/src/app/framework/utils/markdown.ts

@ -55,7 +55,7 @@ export function markdownRender(input: string | undefined | null, inline: boolean
if (inline) {
return marked(input, { renderer: RENDERER_INLINE }) as string;
} else {
return marked(input, { renderer: RENDERER_DEFAULT }) as string;
return marked(input, { renderer: RENDERER_DEFAULT, breaks: true }) as string;
}
}

51
frontend/src/app/shared/components/comments/comment.component.html

@ -64,24 +64,22 @@
}
@if (snapshot.mode === "Edit") {
<form (ngSubmit)="update()">
<textarea
class="form-control mb-1"
autocapitalize="off"
autocomplete="off"
autocorrect="off"
(keydown)="updateOnEnter($event)"
[mention]="$any(mentionUsers)"
[mentionConfig]="mentionConfig"
name="{{ commentItem.comment }}"
[(ngModel)]="editingText"
sqxFocusOnInit></textarea>
<div>
<form [formGroup]="updateForm.form" (ngSubmit)="update()">
<sqx-autocomplete
autoFocus="true"
autoSelectFirst="false"
(editorKeyPress)="onKeyPressUpdate($event)"
[itemsSource]="contributors"
formControlName="text"
startCharacter="@"
textArea="true">
</sqx-autocomplete>
<div class="mt-2">
<button class="btn btn-sm btn-primary" type="submit">
<i class="icon-enter"></i>
{{ "common.save" | sqxTranslate }}
</button>
<button class="btn btn-sm btn-text-secondary me-1" (click)="cancelEditOrReply()" type="button">
<button class="btn btn-sm btn-text-secondary me-1" (click)="cancelUpdateOrReply()" type="button">
{{ "common.cancel" | sqxTranslate }}
</button>
</div>
@ -96,29 +94,26 @@
canFollow="false"
[commentItem]="item"
[comments]="comments"
[mentionConfig]="mentionConfig"
[mentionUsers]="mentionUsers"
[userToken]="userToken"></sqx-comment>
}
@if (snapshot.mode === "Reply") {
<form [formGroup]="replyForm.form" (ngSubmit)="reply()">
<textarea
class="form-control mb-1"
autocapitalize="off"
autocomplete="off"
autocorrect="off"
<sqx-autocomplete
autoFocus="true"
autoSelectFirst="false"
(editorKeyPress)="onKeyPressReply($event)"
[itemsSource]="contributors"
formControlName="text"
(keydown)="replayOnEnter($event)"
[mention]="$any(mentionUsers)"
[mentionConfig]="mentionConfig"
name="text"
sqxFocusOnInit></textarea>
<div>
sqxFocusOnInit
startCharacter="@"
textArea="true">
</sqx-autocomplete>
<div class="mt-2">
<button class="btn btn-sm btn-primary" type="submit">
<i class="icon-enter"></i>
{{ "common.reply" | sqxTranslate }}
</button>
<button class="btn btn-sm btn-text-secondary me-1" (click)="cancelEditOrReply()" type="button">
<button class="btn btn-sm btn-text-secondary me-1" (click)="cancelUpdateOrReply()" type="button">
{{ "common.cancel" | sqxTranslate }}
</button>
</div>

44
frontend/src/app/shared/components/comments/comment.component.ts

@ -7,11 +7,10 @@
import { booleanAttribute, ChangeDetectionStrategy, Component, Input } from '@angular/core';
import { FormsModule, ReactiveFormsModule } from '@angular/forms';
import { ReactiveFormsModule } from '@angular/forms';
import { RouterLink } from '@angular/router';
import { MentionConfig, MentionModule } from 'angular-mentions';
import { bounceAnimation, ConfirmClickDirective, FocusOnInitDirective, FromNowPipe, MarkdownPipe, SafeHtmlPipe, ScrollActiveDirective, TooltipDirective, TranslatePipe } from '@app/framework';
import { CommentItem, CommentsState, ContributorDto, DialogService, Keys, StatefulComponent, UpsertCommentForm } from '@app/shared/internal';
import { AutocompleteComponent, AutocompleteSource, bounceAnimation, ConfirmClickDirective, FromNowPipe, MarkdownPipe, NOOP_DATASOURCE, SafeHtmlPipe, ScrollActiveDirective, TooltipDirective, TranslatePipe } from '@app/framework';
import { CommentItem, CommentsState, DialogService, Keys, StatefulComponent, UpsertCommentForm } from '@app/shared/internal';
import { UserNameRefPipe, UserPictureRefPipe } from '../pipes';
interface State {
@ -28,12 +27,10 @@ interface State {
bounceAnimation,
],
imports: [
AutocompleteComponent,
ConfirmClickDirective,
FocusOnInitDirective,
FormsModule,
FromNowPipe,
MarkdownPipe,
MentionModule,
ReactiveFormsModule,
RouterLink,
SafeHtmlPipe,
@ -66,28 +63,25 @@ export class CommentComponent extends StatefulComponent<State> {
@Input({ required: true })
public comments!: CommentsState;
@Input()
public contributors: AutocompleteSource = NOOP_DATASOURCE;
@Input()
public userToken = '';
@Input()
public currentUrl = '';
@Input({ required: true })
public mentionUsers?: ReadonlyArray<ContributorDto>;
@Input({ required: true })
public mentionConfig!: MentionConfig;
@Input()
public scrollContainer?: string;
public replyForm = new UpsertCommentForm();
public updateForm = new UpsertCommentForm();
public isDeletable = false;
public isEditable = false;
public editingText = '';
constructor(
private readonly dialogs: DialogService,
) {
@ -102,8 +96,7 @@ export class CommentComponent extends StatefulComponent<State> {
}
public startEdit() {
this.editingText = this.commentItem.comment.text;
this.updateForm.load({ text: this.commentItem.comment.text });
this.next({ mode: 'Edit' });
}
@ -111,7 +104,7 @@ export class CommentComponent extends StatefulComponent<State> {
this.next({ mode: 'Reply' });
}
public cancelEditOrReply() {
public cancelUpdateOrReply() {
this.next({ mode: 'Normal' });
}
@ -137,7 +130,7 @@ export class CommentComponent extends StatefulComponent<State> {
}
this.replyForm.submitCompleted();
this.cancelEditOrReply();
this.cancelUpdateOrReply();
}
public update() {
@ -145,7 +138,7 @@ export class CommentComponent extends StatefulComponent<State> {
return;
}
const text = this.editingText;
const { text } = this.updateForm.submit() || {};
if (!text || text.length === 0) {
this.dialogs.confirm('i18n:comments.deleteConfirmTitle', 'i18n:comments.deleteConfirmText')
@ -158,20 +151,21 @@ export class CommentComponent extends StatefulComponent<State> {
this.comments.update(this.commentItem.index, { text });
}
this.cancelEditOrReply();
this.updateForm.submitCompleted();
this.cancelUpdateOrReply();
}
public replayOnEnter(event: KeyboardEvent) {
public onKeyPressReply(event: KeyboardEvent) {
if (Keys.isEnter(event) && !event.altKey && !event.shiftKey) {
event.preventDefault();
this.reply();
event.preventDefault();
}
}
public updateOnEnter(event: KeyboardEvent) {
public onKeyPressUpdate(event: KeyboardEvent) {
if (Keys.isEnter(event) && !event.altKey && !event.shiftKey) {
event.preventDefault();
this.update();
event.preventDefault();
}
}
}

70
frontend/src/app/shared/components/comments/comments.component.html

@ -1,33 +1,37 @@
@if (mentionUsers | async; as users) {
<div class="comments-header">
<form [formGroup]="commentForm.form" (ngSubmit)="comment()">
<input
class="form-control"
#input
autocapitalize="off"
autocomplete="off"
autocorrect="off"
(blur)="blurComment()"
formControlName="text"
[mention]="$any(users)"
[mentionConfig]="mentionConfig"
name="text"
placeholder="{{ 'comments.create' | sqxTranslate }}" />
</form>
</div>
<div class="comments-list" #scrollContainer>
@for (comment of commentsItems | async; track comment.index) {
<sqx-comment
canAnswer="true"
canEdit="true"
canFollow="false"
[commentItem]="comment"
[comments]="commentsState"
[currentUrl]="router.url"
[mentionConfig]="mentionConfig"
[mentionUsers]="users"
[scrollContainer]="'.comments-list'"
[userToken]="commentUser"></sqx-comment>
}
</div>
}
<div class="comments-header">
<form [formGroup]="commentForm.form" (ngSubmit)="comment()">
<sqx-autocomplete
autoFocus="true"
autoSelectFirst="false"
displayProperty="contributorEmail"
(editorBlur)="onBlur()"
(editorKeyPress)="onKeyPress($event)"
formControlName="text"
[itemsSource]="contributorsDataSource"
placeholder="{{ 'comments.create' | sqxTranslate }}"
startCharacter="@"
textArea="true">
<ng-template let-contributor="$implicit">
<span class="autocomplete-user">
<img class="user-picture" [src]="contributor.contributorId | sqxUserPicture" />
<span class="user-name">{{ contributor.contributorName }}</span>
</span>
</ng-template>
</sqx-autocomplete>
</form>
</div>
<div class="comments-list" #scrollContainer>
@for (comment of commentsItems | async; track comment.index) {
<sqx-comment
canAnswer="true"
canEdit="true"
canFollow="false"
[commentItem]="comment"
[comments]="commentsState"
[contributors]="contributorsDataSource"
[currentUrl]="router.url"
[scrollContainer]="'.comments-list'"
[userToken]="commentUser"></sqx-comment>
}
</div>

30
frontend/src/app/shared/components/comments/comments.component.scss

@ -10,6 +10,25 @@
.mention-active > a {
background: $color-theme-brand !important;
}
.comments-header {
.form-control {
border-color: $color-white;
border-bottom-color: $color-border;
border-radius: 0;
&:focus {
box-shadow: none;
border-color: $color-theme-brand;
}
}
}
}
.autocomplete-user {
.user-name {
margin-left: .25rem;
}
}
.comments {
@ -24,15 +43,4 @@
&-header {
flex-shrink: 0;
}
}
.form-control {
border-color: $color-white;
border-bottom-color: $color-border;
border-radius: 0;
&:focus {
box-shadow: none;
border-color: $color-theme-brand;
}
}

37
frontend/src/app/shared/components/comments/comments.component.ts

@ -6,27 +6,32 @@
*/
import { AsyncPipe } from '@angular/common';
import { Component, ElementRef, Input, OnInit, ViewChild } from '@angular/core';
import { Component, Input, OnInit, ViewChild } from '@angular/core';
import { FormsModule, ReactiveFormsModule } from '@angular/forms';
import { Router } from '@angular/router';
import { MentionConfig, MentionModule } from 'angular-mentions';
import { BehaviorSubject } from 'rxjs';
import { MessageBus, Subscriptions, TranslatePipe } from '@app/framework';
import { AnnotationCreateAfterNavigate, AnnotationsSelectAfterNavigate, AuthService, CommentsState, ContributorsState, UpsertCommentForm } from '@app/shared/internal';
import { AutocompleteComponent, Keys, MessageBus, Subscriptions, TranslatePipe } from '@app/framework';
import { AnnotationCreateAfterNavigate, AnnotationsSelectAfterNavigate, AuthService, CommentsState, UpsertCommentForm } from '@app/shared/internal';
import { UserPicturePipe } from '../pipes';
import { CommentComponent } from './comment.component';
import { ContributorsDataSource } from './data-source';
@Component({
standalone: true,
selector: 'sqx-comments',
styleUrls: ['./comments.component.scss'],
templateUrl: './comments.component.html',
providers: [
ContributorsDataSource,
],
imports: [
AsyncPipe,
AutocompleteComponent,
CommentComponent,
FormsModule,
MentionModule,
ReactiveFormsModule,
TranslatePipe,
UserPicturePipe,
],
})
export class CommentsComponent implements OnInit {
@ -35,14 +40,11 @@ export class CommentsComponent implements OnInit {
private reference?: AnnotationCreateAfterNavigate;
@ViewChild('input', { static: false })
public input!: ElementRef<HTMLInputElement>;
public input!: AutocompleteComponent;
@Input()
public commentsId = '';
public mentionUsers = this.contributorsState.contributors;
public mentionConfig: MentionConfig = { dropUp: false, labelKey: 'contributorEmail' };
public commentForm = new UpsertCommentForm();
public commentsItems = this.commentsState.getGroupedComments(this.selection);
public commentUser: string;
@ -50,14 +52,14 @@ export class CommentsComponent implements OnInit {
constructor(authService: AuthService,
public readonly commentsState: CommentsState,
public readonly router: Router,
private readonly contributorsState: ContributorsState,
public readonly contributorsDataSource: ContributorsDataSource,
private readonly messageBus: MessageBus,
) {
this.commentUser = authService.user!.token;
}
public ngOnInit() {
this.contributorsState.loadIfNotLoaded();
this.contributorsDataSource.loadIfNotLoaded();
this.subscriptions.add(
this.messageBus.of(AnnotationsSelectAfterNavigate)
@ -70,7 +72,7 @@ export class CommentsComponent implements OnInit {
.subscribe(message => {
this.reference = message;
this.input.nativeElement.focus();
this.input.focus();
}));
}
@ -86,9 +88,16 @@ export class CommentsComponent implements OnInit {
this.commentForm.submitCompleted();
}
public blurComment() {
public onKeyPress(event: KeyboardEvent) {
if (Keys.isEnter(event) && !event.altKey && !event.shiftKey) {
this.comment();
event.preventDefault();
}
}
public onBlur() {
setTimeout(() => {
this.reference = undefined;
}, 100);
}
}
}

33
frontend/src/app/shared/components/comments/data-source.ts

@ -0,0 +1,33 @@
/*
* Squidex Headless CMS
*
* @license
* Copyright (c) Squidex UG (haftungsbeschränkt). All rights reserved.
*/
import { Injectable } from '@angular/core';
import { map, Observable, of } from 'rxjs';
import { AutocompleteSource } from '@app/framework';
import { ContributorsState } from '@app/shared/internal';
@Injectable()
export class ContributorsDataSource implements AutocompleteSource {
constructor(
private readonly contributorsState: ContributorsState,
) {
}
public loadIfNotLoaded() {
this.contributorsState.loadIfNotLoaded();
}
public find(query: string): Observable<ReadonlyArray<any>> {
if (!query) {
return of([]);
}
return this.contributorsState.contributors.pipe(
map(contributors => contributors.filter(c => c.contributorEmail.indexOf(query) >= 0)),
);
}
}

1
frontend/src/app/shell/index.ts

@ -11,7 +11,6 @@ export * from './pages/forbidden/forbidden-page.component';
export * from './pages/home/home-page.component';
export * from './pages/internal/apps-menu.component';
export * from './pages/internal/internal-area.component';
export * from './pages/internal/feedback-menu.component';
export * from './pages/internal/logo.component';
export * from './pages/internal/notification-dropdown.component';
export * from './pages/internal/notifications-menu.component';

7
frontend/src/app/shell/pages/internal/feedback-menu.component.html

@ -1,7 +0,0 @@
@if (markerProject) {
<li class="nav-item nav-icon">
<span class="nav-link" (click)="capture()">
<i class="icon-lightbulb_outline"></i>
</span>
</li>
}

2
frontend/src/app/shell/pages/internal/feedback-menu.component.scss

@ -1,2 +0,0 @@
@import 'mixins';
@import 'vars';

41
frontend/src/app/shell/pages/internal/feedback-menu.component.ts

@ -1,41 +0,0 @@
/*
* Squidex Headless CMS
*
* @license
* Copyright (c) Squidex UG (haftungsbeschränkt). All rights reserved.
*/
import { ChangeDetectionStrategy, Component, inject, OnDestroy, OnInit } from '@angular/core';
import markerSDK, { MarkerSdk } from '@marker.io/browser';
import { UIOptions } from '@app/shared';
@Component({
standalone: true,
selector: 'sqx-feedback-menu',
styleUrls: ['./feedback-menu.component.scss'],
templateUrl: './feedback-menu.component.html',
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class FeedbackMenuComponent implements OnInit, OnDestroy {
private widget?: MarkerSdk;
public readonly markerProject = inject(UIOptions).value.markerProject;
public ngOnDestroy() {
this.widget?.unload();
}
public async ngOnInit() {
if (!this.markerProject) {
return;
}
this.widget = await markerSDK.loadWidget({ project: this.markerProject });
this.widget.hide();
}
public capture() {
this.widget?.capture('fullscreen');
}
}

1
frontend/src/app/shell/pages/internal/internal-area.component.html

@ -10,7 +10,6 @@
<sqx-search-menu></sqx-search-menu>
<sqx-chat-menu></sqx-chat-menu>
<sqx-asset-uploader></sqx-asset-uploader>
<sqx-feedback-menu></sqx-feedback-menu>
<sqx-notifications-menu></sqx-notifications-menu>
<sqx-profile-menu></sqx-profile-menu>
</div>

2
frontend/src/app/shell/pages/internal/internal-area.component.ts

@ -12,7 +12,6 @@ import { DialogService, LoadingService, Notification, Subscriptions, UIOptions }
import { AssetUploaderComponent } from '@app/shared/components/assets/asset-uploader.component';
import { AppsMenuComponent } from './apps-menu.component';
import { ChatMenuComponent } from './chat-menu.component';
import { FeedbackMenuComponent } from './feedback-menu.component';
import { LogoComponent } from './logo.component';
import { NotificationsMenuComponent } from './notifications-menu.component';
import { ProfileMenuComponent } from './profile-menu.component';
@ -28,7 +27,6 @@ import { SearchMenuComponent } from './search-menu.component';
AssetUploaderComponent,
AsyncPipe,
ChatMenuComponent,
FeedbackMenuComponent,
LogoComponent,
NotificationsMenuComponent,
ProfileMenuComponent,

2
frontend/src/app/shell/pages/internal/notification-dropdown.component.html

@ -24,8 +24,6 @@
[commentItem]="item"
[comments]="commentsState"
confirmDelete="false"
[mentionConfig]="{}"
[mentionUsers]="undefined"
[userToken]="commentUser"></sqx-comment>
} @empty {
<small class="text-muted">

Loading…
Cancel
Save