Browse Source

Ng update (#1097)

* Temp

* Temp

* Update

* Temp

* Fix track-by

* Build fixes

* Migration to new flow.

* More manual fixes.

* Reformat templates.

* Fix tests

* Improve tests

* Revert change.
pull/1098/head
Sebastian Stehle 2 years ago
committed by GitHub
parent
commit
bee338113c
No known key found for this signature in database GPG Key ID: B5690EEEBB952194
  1. 18
      backend/src/Squidex.Web/BuilderExtensions.cs
  2. 2
      backend/src/Squidex.Web/Constants.cs
  3. 10
      backend/src/Squidex/Startup.cs
  4. 20
      frontend/.prettierrc
  5. 10
      frontend/angular.json
  6. 33249
      frontend/package-lock.json
  7. 150
      frontend/package.json
  8. 17
      frontend/src/app/_theme.html
  9. 11
      frontend/src/app/app.component.html
  10. 3
      frontend/src/app/app.component.ts
  11. 43
      frontend/src/app/features/administration/administration-area.component.html
  12. 3
      frontend/src/app/features/administration/administration-area.component.ts
  13. 37
      frontend/src/app/features/administration/pages/event-consumers/event-consumer.component.html
  14. 3
      frontend/src/app/features/administration/pages/event-consumers/event-consumer.component.ts
  15. 45
      frontend/src/app/features/administration/pages/event-consumers/event-consumers-page.component.html
  16. 7
      frontend/src/app/features/administration/pages/event-consumers/event-consumers-page.component.ts
  17. 96
      frontend/src/app/features/administration/pages/restore/restore-page.component.html
  18. 4
      frontend/src/app/features/administration/pages/restore/restore-page.component.ts
  19. 93
      frontend/src/app/features/administration/pages/users/user-page.component.html
  20. 3
      frontend/src/app/features/administration/pages/users/user-page.component.ts
  21. 39
      frontend/src/app/features/administration/pages/users/user.component.html
  22. 3
      frontend/src/app/features/administration/pages/users/user.component.ts
  23. 60
      frontend/src/app/features/administration/pages/users/users-page.component.html
  24. 10
      frontend/src/app/features/administration/pages/users/users-page.component.ts
  25. 19
      frontend/src/app/features/administration/services/event-consumers.service.spec.ts
  26. 19
      frontend/src/app/features/administration/services/users.service.spec.ts
  27. 14
      frontend/src/app/features/api/api-area.component.html
  28. 40
      frontend/src/app/features/api/pages/graphql/graphql-page.component.html
  29. 4
      frontend/src/app/features/api/pages/graphql/graphql-page.component.ts
  30. 46
      frontend/src/app/features/apps/pages/app.component.html
  31. 3
      frontend/src/app/features/apps/pages/app.component.ts
  32. 134
      frontend/src/app/features/apps/pages/apps-page.component.html
  33. 8
      frontend/src/app/features/apps/pages/apps-page.component.ts
  34. 20
      frontend/src/app/features/apps/pages/news-dialog.component.html
  35. 7
      frontend/src/app/features/apps/pages/news-dialog.component.ts
  36. 161
      frontend/src/app/features/apps/pages/onboarding-dialog.component.html
  37. 3
      frontend/src/app/features/apps/pages/onboarding-dialog.component.ts
  38. 25
      frontend/src/app/features/apps/pages/team.component.html
  39. 27
      frontend/src/app/features/assets/pages/asset-tag-dialog.component.html
  40. 44
      frontend/src/app/features/assets/pages/asset-tags.component.html
  41. 8
      frontend/src/app/features/assets/pages/asset-tags.component.ts
  42. 20
      frontend/src/app/features/assets/pages/assets-filters-page.component.html
  43. 4
      frontend/src/app/features/assets/pages/assets-filters-page.component.ts
  44. 106
      frontend/src/app/features/assets/pages/assets-page.component.html
  45. 3
      frontend/src/app/features/assets/pages/assets-page.component.ts
  46. 181
      frontend/src/app/features/content/pages/calendar/calendar-page.component.html
  47. 3
      frontend/src/app/features/content/pages/calendar/calendar-page.component.ts
  48. 4
      frontend/src/app/features/content/pages/comments/comments-page.component.html
  49. 22
      frontend/src/app/features/content/pages/content/content-event.component.html
  50. 5
      frontend/src/app/features/content/pages/content/content-event.component.ts
  51. 237
      frontend/src/app/features/content/pages/content/content-history-page.component.html
  52. 8
      frontend/src/app/features/content/pages/content/content-history-page.component.ts
  53. 308
      frontend/src/app/features/content/pages/content/content-page.component.html
  54. 5
      frontend/src/app/features/content/pages/content/content-page.component.ts
  55. 79
      frontend/src/app/features/content/pages/content/editor/content-editor.component.html
  56. 10
      frontend/src/app/features/content/pages/content/editor/content-editor.component.ts
  57. 43
      frontend/src/app/features/content/pages/content/inspecting/content-inspection.component.html
  58. 3
      frontend/src/app/features/content/pages/content/inspecting/content-inspection.component.ts
  59. 58
      frontend/src/app/features/content/pages/content/references/content-references.component.html
  60. 8
      frontend/src/app/features/content/pages/content/references/content-references.component.ts
  61. 22
      frontend/src/app/features/content/pages/contents-plugin/contents-plugin.component.html
  62. 3
      frontend/src/app/features/content/pages/contents-plugin/contents-plugin.component.ts
  63. 40
      frontend/src/app/features/content/pages/contents/contents-filters-page.component.html
  64. 3
      frontend/src/app/features/content/pages/contents/contents-filters-page.component.ts
  65. 240
      frontend/src/app/features/content/pages/contents/contents-page.component.html
  66. 8
      frontend/src/app/features/content/pages/contents/contents-page.component.ts
  67. 94
      frontend/src/app/features/content/pages/contents/custom-view-editor.component.html
  68. 4
      frontend/src/app/features/content/pages/contents/custom-view-editor.component.ts
  69. 78
      frontend/src/app/features/content/pages/references/references-page.component.html
  70. 10
      frontend/src/app/features/content/pages/references/references-page.component.ts
  71. 49
      frontend/src/app/features/content/pages/schemas/schemas-page.component.html
  72. 12
      frontend/src/app/features/content/pages/schemas/schemas-page.component.ts
  73. 11
      frontend/src/app/features/content/pages/sidebar/sidebar-page.component.html
  74. 2
      frontend/src/app/features/content/shared/content-extension.component.html
  75. 38
      frontend/src/app/features/content/shared/due-time-selector.component.html
  76. 233
      frontend/src/app/features/content/shared/forms/array-editor.component.html
  77. 4
      frontend/src/app/features/content/shared/forms/array-editor.component.ts
  78. 98
      frontend/src/app/features/content/shared/forms/array-item.component.html
  79. 10
      frontend/src/app/features/content/shared/forms/array-item.component.ts
  80. 154
      frontend/src/app/features/content/shared/forms/assets-editor.component.html
  81. 8
      frontend/src/app/features/content/shared/forms/assets-editor.component.ts
  82. 58
      frontend/src/app/features/content/shared/forms/component-section.component.html
  83. 10
      frontend/src/app/features/content/shared/forms/component-section.component.ts
  84. 80
      frontend/src/app/features/content/shared/forms/component.component.html
  85. 10
      frontend/src/app/features/content/shared/forms/component.component.ts
  86. 197
      frontend/src/app/features/content/shared/forms/content-field.component.html
  87. 8
      frontend/src/app/features/content/shared/forms/content-field.component.ts
  88. 64
      frontend/src/app/features/content/shared/forms/content-section.component.html
  89. 8
      frontend/src/app/features/content/shared/forms/content-section.component.ts
  90. 39
      frontend/src/app/features/content/shared/forms/field-copy-button.component.html
  91. 4
      frontend/src/app/features/content/shared/forms/field-copy-button.component.ts
  92. 636
      frontend/src/app/features/content/shared/forms/field-editor.component.html
  93. 6
      frontend/src/app/features/content/shared/forms/field-editor.component.ts
  94. 40
      frontend/src/app/features/content/shared/forms/field-languages.component.html
  95. 3
      frontend/src/app/features/content/shared/forms/field-languages.component.ts
  96. 23
      frontend/src/app/features/content/shared/forms/iframe-editor.component.html
  97. 74
      frontend/src/app/features/content/shared/forms/stock-photo-editor.component.html
  98. 8
      frontend/src/app/features/content/shared/forms/stock-photo-editor.component.ts
  99. 101
      frontend/src/app/features/content/shared/list/content.component.html
  100. 4
      frontend/src/app/features/content/shared/list/content.component.ts

18
backend/src/Squidex.Web/BuilderExtensions.cs

@ -1,18 +0,0 @@
// ==========================================================================
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex UG (haftungsbeschraenkt)
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
using Microsoft.AspNetCore.Builder;
namespace Squidex.Web;
public static class BuilderExtensions
{
public static void UseWhenPath(this IApplicationBuilder builder, string path, Action<IApplicationBuilder> configurator)
{
builder.UseWhen(c => c.Request.Path.StartsWithSegments(path, StringComparison.OrdinalIgnoreCase), configurator);
}
}

2
backend/src/Squidex.Web/Constants.cs

@ -20,6 +20,8 @@ public static class Constants
public const string PrefixIdentityServer = "/identity-server"; public const string PrefixIdentityServer = "/identity-server";
public const string PrefixSignin = "/signin";
public const string ScopePermissions = "permissions"; public const string ScopePermissions = "permissions";
public const string ScopeProfile = "squidex-profile"; public const string ScopeProfile = "squidex-profile";

10
backend/src/Squidex/Startup.cs

@ -94,15 +94,15 @@ public sealed class Startup
options.Path = "/api/swagger/v1/swagger.json"; options.Path = "/api/swagger/v1/swagger.json";
}); });
if (app.ApplicationServices.GetRequiredService<IWebHostEnvironment>().IsDevelopment()) app.UseWhen(c =>
{ c.Request.Path.Value?.StartsWith(Constants.PrefixIdentityServer, StringComparison.OrdinalIgnoreCase) == true ||
app.UseWhenPath(Constants.PrefixIdentityServer, builder => c.Request.Path.Value?.StartsWith(Constants.PrefixSignin, StringComparison.OrdinalIgnoreCase) == true,
builder =>
{ {
builder.UseExceptionHandler("/identity-server/error"); builder.UseExceptionHandler("/identity-server/error");
}); });
}
app.UseWhenPath(Constants.PrefixApi, builder => app.UseWhen(c => c.Request.Path.StartsWithSegments(Constants.PrefixApi, StringComparison.OrdinalIgnoreCase), builder =>
{ {
builder.UseSquidexCacheKeys(); builder.UseSquidexCacheKeys();
builder.UseSquidexExceptionHandling(); builder.UseSquidexExceptionHandling();

20
frontend/.prettierrc

@ -0,0 +1,20 @@
{
"plugins": ["squidex-prettier-plugin-organize-attributes"],
"overrides": [
{
"files": "*.html",
"options": {
"attributeGroups": ["$ANGULAR"],
"attributeIgnoreChars": "[]()*",
"attributeSort": "ASC",
"parser": "angular",
"printWidth": 140,
"bracketSameLine": true,
"bracketSpacing": true,
"singleAttributePerLine": false,
"tabWidth": 4
}
}
],
"tabWidth": 4
}

10
frontend/angular.json

@ -119,7 +119,15 @@
"with": "src/environments/environment.prod.ts" "with": "src/environments/environment.prod.ts"
} }
], ],
"outputHashing": "all" "outputHashing": "all",
"optimization": {
"scripts": true,
"styles": {
"minify": true,
"inlineCritical": false
},
"fonts": true
},
}, },
"development": { "development": {
"extractLicenses": false, "extractLicenses": false,

33249
frontend/package-lock.json

File diff suppressed because it is too large

150
frontend/package.json

@ -15,119 +15,121 @@
}, },
"private": true, "private": true,
"dependencies": { "dependencies": {
"@angular/animations": "17.0.4", "@angular/animations": "18.0.1",
"@angular/cdk": "17.0.1", "@angular/cdk": "18.0.1",
"@angular/cdk-experimental": "17.0.1", "@angular/cdk-experimental": "18.0.1",
"@angular/common": "17.0.4", "@angular/common": "18.0.1",
"@angular/core": "17.0.4", "@angular/core": "18.0.1",
"@angular/forms": "17.0.4", "@angular/forms": "18.0.1",
"@angular/localize": "17.0.4", "@angular/localize": "18.0.1",
"@angular/platform-browser": "17.0.4", "@angular/platform-browser": "18.0.1",
"@angular/platform-browser-dynamic": "17.0.4", "@angular/platform-browser-dynamic": "18.0.1",
"@angular/platform-server": "17.0.4", "@angular/platform-server": "18.0.1",
"@angular/router": "17.0.4", "@angular/router": "18.0.1",
"@egjs/hammerjs": "2.0.17", "@egjs/hammerjs": "2.0.17",
"@floating-ui/dom": "^1.5.3", "@floating-ui/dom": "^1.6.5",
"@graphiql/toolkit": "^0.9.1", "@graphiql/toolkit": "^0.9.1",
"@iharbeck/ngx-virtual-scroller": "^17.0.0", "@iharbeck/ngx-virtual-scroller": "^17.0.2",
"@lithiumjs/angular": "^7.3.0", "@lithiumjs/angular": "^7.3.1",
"@lithiumjs/ngx-virtual-scroll": "^0.3.0", "@lithiumjs/ngx-virtual-scroll": "^0.3.1",
"@marker.io/browser": "^0.19.0", "@marker.io/browser": "^0.19.0",
"@types/ace": "^0.0.51", "@types/ace": "^0.0.52",
"ace-builds": "^1.31.2", "ace-builds": "^1.34.2",
"angular-gridster2": "17.0.0", "angular-gridster2": "18.0.1",
"angular-mentions": "1.5.0", "angular-mentions": "1.5.0",
"bootstrap": "5.2.3", "bootstrap": "5.3.3",
"copy-webpack-plugin": "^11.0.0", "copy-webpack-plugin": "^12.0.2",
"core-js": "3.33.3", "core-js": "3.37.1",
"cropperjs": "2.0.0-alpha.1", "cropperjs": "2.0.0-alpha.1",
"date-fns": "2.30.0", "date-fns": "3.6.0",
"graphiql": "3.0.10", "graphiql": "3.2.2",
"graphql": "16.8.1", "graphql": "16.8.1",
"graphql-ws": "^5.14.2", "graphql-ws": "^5.16.0",
"image-focus": "1.2.1", "image-focus": "1.2.1",
"keycharm": "0.4.0", "keycharm": "0.4.0",
"leaflet": "1.9.4", "leaflet": "1.9.4",
"leaflet-control-geocoder": "2.4.0", "leaflet-control-geocoder": "2.4.0",
"marked": "10.0.0", "marked": "12.0.2",
"mersenne-twister": "1.1.0", "mersenne-twister": "1.1.0",
"moment": "^2.29.4", "moment": "^2.30.1",
"mousetrap": "1.6.5", "mousetrap": "1.6.5",
"ng2-charts": "^5.0.3", "ng2-charts": "^6.0.1",
"ngx-color-picker": "15.0.0", "ngx-color-picker": "16.0.0",
"ngx-doc-viewer": "15.0.1", "ngx-doc-viewer": "15.0.1",
"ngx-ui-tour-core": "^11.0.6", "ngx-ui-tour-core": "^12.0.2",
"oidc-client-ts": "^2.4.0", "oidc-client-ts": "^3.0.1",
"pikaday": "1.8.2", "pikaday": "1.8.2",
"progressbar.js": "1.1.1", "progressbar.js": "1.1.1",
"react": "18.2.0", "react": "18.3.1",
"react-dom": "18.2.0", "react-dom": "18.3.1",
"rxjs": "7.8.1", "rxjs": "7.8.1",
"simplemde": "1.11.2", "simplemde": "1.11.2",
"slugify": "1.6.6", "slugify": "1.6.6",
"tslib": "2.6.2", "tslib": "2.6.2",
"tui-calendar": "^1.15.3", "tui-calendar": "^1.15.3",
"typemoq": "^2.1.0", "typemoq": "^2.1.0",
"video.js": "8.6.1", "video.js": "8.12.0",
"vis-data": "7.1.8", "vis-data": "7.1.9",
"vis-network": "9.1.9", "vis-network": "9.1.9",
"vis-util": "5.0.7", "vis-util": "5.0.7",
"y-protocols": "^1.0.6", "y-protocols": "^1.0.6",
"y-websocket": "^1.5.0", "y-websocket": "^2.0.3",
"zone.js": "0.14.2" "zone.js": "0.14.6"
}, },
"devDependencies": { "devDependencies": {
"@angular-devkit/architect": "^0.1700.2", "@angular-devkit/architect": "^0.1800.2",
"@angular-devkit/build-angular": "^17.0.2", "@angular-devkit/build-angular": "^18.0.2",
"@angular-eslint/builder": "17.1.0", "@angular-eslint/builder": "18.0.0",
"@angular-eslint/eslint-plugin": "17.1.0", "@angular-eslint/eslint-plugin": "18.0.0",
"@angular-eslint/eslint-plugin-template": "17.1.0", "@angular-eslint/eslint-plugin-template": "18.0.0",
"@angular-eslint/schematics": "17.1.0", "@angular-eslint/schematics": "18.0.0",
"@angular-eslint/template-parser": "17.1.0", "@angular-eslint/template-parser": "18.0.0",
"@angular/cli": "^17.0.2", "@angular/cli": "^18.0.2",
"@angular/compiler": "^17.0.4", "@angular/compiler": "^18.0.1",
"@angular/compiler-cli": "^17.0.4", "@angular/compiler-cli": "^18.0.1",
"@angular/elements": "^17.0.4", "@angular/elements": "^18.0.1",
"@compodoc/compodoc": "^1.1.22", "@compodoc/compodoc": "^1.1.25",
"@storybook/addon-actions": "^7.5.3", "@storybook/addon-actions": "^8.1.5",
"@storybook/addon-essentials": "^7.5.3", "@storybook/addon-essentials": "^8.1.5",
"@storybook/addon-interactions": "^7.5.3", "@storybook/addon-interactions": "^8.1.5",
"@storybook/addon-links": "^7.5.3", "@storybook/addon-links": "^8.1.5",
"@storybook/angular": "^7.5.3", "@storybook/angular": "^8.1.5",
"@storybook/testing-library": "^0.2.2", "@storybook/testing-library": "^0.2.2",
"@types/codemirror": "5.60.14", "@types/codemirror": "5.60.15",
"@types/core-js": "2.5.8", "@types/core-js": "2.5.8",
"@types/jasmine": "5.1.3", "@types/jasmine": "5.1.4",
"@types/marked": "5.0.1", "@types/marked": "5.0.2",
"@types/mersenne-twister": "1.1.7", "@types/mersenne-twister": "1.1.7",
"@types/mousetrap": "1.6.14", "@types/mousetrap": "1.6.15",
"@types/node": "20.9.3", "@types/node": "20.12.13",
"@types/react": "18.2.38", "@types/react": "18.3.3",
"@types/react-dom": "18.2.16", "@types/react-dom": "18.3.0",
"@types/simplemde": "1.11.11", "@types/simplemde": "1.11.11",
"@types/tapable": "2.2.7", "@types/tapable": "2.2.7",
"@types/ws": "8.5.10", "@types/ws": "8.5.10",
"@typescript-eslint/eslint-plugin": "^6.12.0", "@typescript-eslint/eslint-plugin": "^7.11.0",
"@typescript-eslint/parser": "^6.12.0", "@typescript-eslint/parser": "^7.11.0",
"@webcomponents/custom-elements": "^1.6.0", "@webcomponents/custom-elements": "^1.6.0",
"eslint": "^8.54.0", "eslint": "^9.3.0",
"eslint-config-airbnb-typescript": "17.1.0", "eslint-config-airbnb-typescript": "18.0.0",
"eslint-plugin-deprecation": "^2.0.0", "eslint-plugin-deprecation": "^2.0.0",
"eslint-plugin-import": "2.29.0", "eslint-plugin-import": "2.29.1",
"eslint-plugin-jsx-a11y": "6.8.0", "eslint-plugin-jsx-a11y": "6.8.0",
"eslint-plugin-storybook": "^0.6.15", "eslint-plugin-storybook": "^0.8.0",
"jasmine-core": "~5.1.1", "jasmine-core": "~5.1.2",
"karma": "~6.4.2", "karma": "~6.4.3",
"karma-chrome-launcher": "~3.2.0", "karma-chrome-launcher": "~3.2.0",
"karma-coverage": "~2.2.1", "karma-coverage": "~2.2.1",
"karma-jasmine": "~5.1.0", "karma-jasmine": "~5.1.0",
"karma-jasmine-html-reporter": "~2.1.0", "karma-jasmine-html-reporter": "~2.1.0",
"storybook": "^7.5.3", "prettier": "^3.2.5",
"stylelint": "15.11.0", "squidex-prettier-plugin-organize-attributes": "^1.0.1",
"stylelint-config-standard": "34.0.0", "storybook": "^8.1.5",
"stylelint-config-standard-scss": "^11.1.0", "stylelint": "16.6.1",
"stylelint-scss": "5.3.1", "stylelint-config-standard": "36.0.0",
"typescript": "5.2.2" "stylelint-config-standard-scss": "^13.1.0",
"stylelint-scss": "6.3.0",
"typescript": "5.4.5"
}, },
"overrides": { "overrides": {
"ng2-charts": { "ng2-charts": {

17
frontend/src/app/_theme.html

@ -1092,11 +1092,14 @@
<i class="nav-icon icon-help2"></i> <i class="nav-icon icon-help2"></i>
</a> </a>
</li> </li>
<li class="nav-item" *ngIf="!isSettingsHidden(app)">
<a class="nav-link"> @if (!isSettingsHidden(app)) {
<i class="nav-icon icon-settings"></i> <li class="nav-item">
</a> <a class="nav-link">
</li> <i class="nav-icon icon-settings"></i>
</a>
</li>
}
</ul> </ul>
</div> </div>
</div> </div>
@ -1488,7 +1491,7 @@
<div class="modal-dialog modal-lg" style="position: absolute; top: 50px; left: 50px; right: 50px; bottom: 50px; display: block;"> <div class="modal-dialog modal-lg" style="position: absolute; top: 50px; left: 50px; right: 50px; bottom: 50px; display: block;">
<div class="modal-content"> <div class="modal-content">
<div class="modal-header with-tabs" *ngIf="showHeader"> <div class="modal-header with-tabs">
<h4 class="modal-title"> <h4 class="modal-title">
Title Title
</h4> </h4>
@ -1537,7 +1540,7 @@
<div class="modal-dialog modal-lg" style="position: absolute; top: 50px; left: 50px; right: 50px; bottom: 50px; display: block;"> <div class="modal-dialog modal-lg" style="position: absolute; top: 50px; left: 50px; right: 50px; bottom: 50px; display: block;">
<div class="modal-content"> <div class="modal-content">
<div class="modal-header" *ngIf="showHeader"> <div class="modal-header">
<h4 class="modal-title"> <h4 class="modal-title">
Title Title
</h4> </h4>

11
frontend/src/app/app.component.html

@ -1,11 +1,12 @@
<main sqxCopyGlobal> <main sqxCopyGlobal>
<sqx-root-view> <sqx-root-view>
<router-outlet (activate)="isLoaded = true"> <router-outlet (activate)="isLoaded = true">
<div class="loading" *ngIf="!isLoaded"> @if (!isLoaded) {
<img alt="Loading" src="./images/loader.svg"> <div class="loading">
<img alt="Loading" src="./images/loader.svg" />
<div>{{ 'common.loading' | sqxTranslate }}</div> <div>{{ "common.loading" | sqxTranslate }}</div>
</div> </div>
}
</router-outlet> </router-outlet>
<sqx-tour-guide></sqx-tour-guide> <sqx-tour-guide></sqx-tour-guide>

3
frontend/src/app/app.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 { NgIf } from '@angular/common';
import { Component, Injector } from '@angular/core'; import { Component, Injector } from '@angular/core';
import { RouterOutlet } from '@angular/router'; import { RouterOutlet } from '@angular/router';
import { AnalyticsService, CopyGlobalDirective, DialogRendererComponent, RootViewComponent, TourGuideComponent, TourTemplateComponent, TranslatePipe } from '@app/shared'; import { AnalyticsService, CopyGlobalDirective, DialogRendererComponent, RootViewComponent, TourGuideComponent, TourTemplateComponent, TranslatePipe } from '@app/shared';
@ -18,7 +18,6 @@ import { AnalyticsService, CopyGlobalDirective, DialogRendererComponent, RootVie
imports: [ imports: [
CopyGlobalDirective, CopyGlobalDirective,
DialogRendererComponent, DialogRendererComponent,
NgIf,
RootViewComponent, RootViewComponent,
RouterOutlet, RouterOutlet,
TourGuideComponent, TourGuideComponent,

43
frontend/src/app/features/administration/administration-area.component.html

@ -2,24 +2,33 @@
<div class="sidebar"> <div class="sidebar">
<ul class="nav nav-panel flex-column"> <ul class="nav nav-panel flex-column">
<li class="nav-item" *ngIf="uiState.canReadUsers | async"> @if (uiState.canReadUsers | async) {
<a class="nav-link" routerLink="users" routerLinkActive="active"> <li class="nav-item">
<i class="nav-icon icon-user-o"></i> <div class="nav-text">{{ 'common.users' | sqxTranslate }}</div> <a class="nav-link" routerLink="users" routerLinkActive="active">
</a> <i class="nav-icon icon-user-o"></i>
</li> <div class="nav-text">{{ "common.users" | sqxTranslate }}</div>
<li class="nav-item" *ngIf="uiState.canReadEvents | async"> </a>
<a class="nav-link" routerLink="event-consumers" routerLinkActive="active"> </li>
<i class="nav-icon icon-time"></i> <div class="nav-text">{{ 'common.consumers' | sqxTranslate }}</div> }
</a> @if (uiState.canReadEvents | async) {
</li> <li class="nav-item">
<li class="nav-item" *ngIf="uiState.canRestore | async"> <a class="nav-link" routerLink="event-consumers" routerLinkActive="active">
<a class="nav-link" routerLink="restore" routerLinkActive="active"> <i class="nav-icon icon-time"></i>
<i class="nav-icon icon-backup"></i> <div class="nav-text">{{ 'common.restore' | sqxTranslate }}</div> <div class="nav-text">{{ "common.consumers" | sqxTranslate }}</div>
</a> </a>
</li> </li>
}
@if (uiState.canRestore | async) {
<li class="nav-item">
<a class="nav-link" routerLink="restore" routerLinkActive="active">
<i class="nav-icon icon-backup"></i>
<div class="nav-text">{{ "common.restore" | sqxTranslate }}</div>
</a>
</li>
}
</ul> </ul>
</div> </div>
<div sqxLayoutContainer class="panel-container"> <div class="panel-container" sqxLayoutContainer>
<router-outlet></router-outlet> <router-outlet></router-outlet>
</div> </div>

3
frontend/src/app/features/administration/administration-area.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 { AsyncPipe, NgIf } from '@angular/common'; import { AsyncPipe } from '@angular/common';
import { Component } from '@angular/core'; import { Component } from '@angular/core';
import { RouterLink, RouterLinkActive, RouterOutlet } from '@angular/router'; import { RouterLink, RouterLinkActive, RouterOutlet } from '@angular/router';
import { LayoutContainerDirective, TitleComponent, TranslatePipe, UIState } from '@app/shared'; import { LayoutContainerDirective, TitleComponent, TranslatePipe, UIState } from '@app/shared';
@ -18,7 +18,6 @@ import { LayoutContainerDirective, TitleComponent, TranslatePipe, UIState } from
imports: [ imports: [
AsyncPipe, AsyncPipe,
LayoutContainerDirective, LayoutContainerDirective,
NgIf,
RouterLink, RouterLink,
RouterLinkActive, RouterLinkActive,
RouterOutlet, RouterOutlet,

37
frontend/src/app/features/administration/pages/event-consumers/event-consumer.component.html

@ -1,27 +1,36 @@
<tr [class.faulted]="eventConsumer.error && eventConsumer.error && eventConsumer.error.length > 0"> <tr [class.faulted]="eventConsumer.error && eventConsumer.error && eventConsumer.error.length > 0">
<td class="cell-auto"> <td class="cell-auto">
<span class="truncate"> <span class="truncate">
<i class="faulted-icon icon icon-bug" (click)="failure.emit()" [class.hidden]="!eventConsumer.error || eventConsumer.error.length === 0"></i> <i
class="faulted-icon icon icon-bug"
[class.hidden]="!eventConsumer.error || eventConsumer.error.length === 0"
(click)="failure.emit()"></i>
{{eventConsumer.name}} {{ eventConsumer.name }}
</span> </span>
</td> </td>
<td class="cell-auto-right"> <td class="cell-auto-right">
<span>{{eventConsumer.count}}</span> <span>{{ eventConsumer.count }}</span>
</td> </td>
<td class="cell-auto-right"> <td class="cell-auto-right">
<span>{{eventConsumer.position}}</span> <span>{{ eventConsumer.position }}</span>
</td> </td>
<td class="cell-actions-lg"> <td class="cell-actions-lg">
<button type="button" class="btn btn-text-secondary" (click)="reset()" *ngIf="eventConsumer.canReset" title="i18n:eventConsumers.resetTooltip"> @if (eventConsumer.canReset) {
<i class="icon icon-reset"></i> <button class="btn btn-text-secondary" (click)="reset()" title="i18n:eventConsumers.resetTooltip" type="button">
</button> <i class="icon icon-reset"></i>
<button type="button" class="btn btn-text-secondary" (click)="start()" *ngIf="eventConsumer.canStart" title="i18n:eventConsumers.startTooltip"> </button>
<i class="icon icon-play"></i> }
</button> @if (eventConsumer.canStart) {
<button type="button" class="btn btn-text-secondary" (click)="stop()" *ngIf="eventConsumer.canStop" title="i18n:eventConsumers.stopTooltip"> <button class="btn btn-text-secondary" (click)="start()" title="i18n:eventConsumers.startTooltip" type="button">
<i class="icon icon-pause"></i> <i class="icon icon-play"></i>
</button> </button>
}
@if (eventConsumer.canStop) {
<button class="btn btn-text-secondary" (click)="stop()" title="i18n:eventConsumers.stopTooltip" type="button">
<i class="icon icon-pause"></i>
</button>
}
</td> </td>
</tr> </tr>
<tr class="spacer"></tr> <tr class="spacer"></tr>

3
frontend/src/app/features/administration/pages/event-consumers/event-consumer.component.ts

@ -7,7 +7,7 @@
/* eslint-disable @angular-eslint/component-selector */ /* eslint-disable @angular-eslint/component-selector */
import { NgIf } from '@angular/common';
import { ChangeDetectionStrategy, Component, EventEmitter, Input, Output } from '@angular/core'; import { ChangeDetectionStrategy, Component, EventEmitter, Input, Output } from '@angular/core';
import { TooltipDirective } from '@app/shared'; import { TooltipDirective } from '@app/shared';
import { EventConsumerDto, EventConsumersState } from '../../internal'; import { EventConsumerDto, EventConsumersState } from '../../internal';
@ -19,7 +19,6 @@ import { EventConsumerDto, EventConsumersState } from '../../internal';
templateUrl: './event-consumer.component.html', templateUrl: './event-consumer.component.html',
changeDetection: ChangeDetectionStrategy.OnPush, changeDetection: ChangeDetectionStrategy.OnPush,
imports: [ imports: [
NgIf,
TooltipDirective, TooltipDirective,
], ],
}) })

45
frontend/src/app/features/administration/pages/event-consumers/event-consumers-page.component.html

@ -1,9 +1,15 @@
<sqx-title message="i18n:eventConsumers.pageTitle"></sqx-title> <sqx-title message="i18n:eventConsumers.pageTitle"></sqx-title>
<sqx-layout layout="main" titleText="i18n:common.consumers" titleIcon="time" innerWidth="50"> <sqx-layout innerWidth="50" layout="main" titleIcon="time" titleText="i18n:common.consumers">
<ng-container menu> <ng-container menu>
<button type="button" class="btn btn-text-secondary" (click)="reload()" title="i18n:eventConsumers.refreshTooltip" shortcut="CTRL + B"> <button
<i class="icon-reset"></i> {{ 'common.refresh' | sqxTranslate }} class="btn btn-text-secondary"
(click)="reload()"
shortcut="CTRL + B"
title="i18n:eventConsumers.refreshTooltip"
type="button">
<i class="icon-reset"></i>
{{ "common.refresh" | sqxTranslate }}
</button> </button>
</ng-container> </ng-container>
@ -14,16 +20,16 @@
<thead> <thead>
<tr> <tr>
<th class="cell-auto"> <th class="cell-auto">
{{ 'common.name' | sqxTranslate }} {{ "common.name" | sqxTranslate }}
</th> </th>
<th class="cell-auto-right"> <th class="cell-auto-right">
{{ 'eventConsumers.count' | sqxTranslate }} {{ "eventConsumers.count" | sqxTranslate }}
</th> </th>
<th class="cell-auto-right"> <th class="cell-auto-right">
{{ 'eventConsumers.position' | sqxTranslate }} {{ "eventConsumers.position" | sqxTranslate }}
</th> </th>
<th class="cell-actions-lg"> <th class="cell-actions-lg">
{{ 'common.actions' | sqxTranslate }} {{ "common.actions" | sqxTranslate }}
</th> </th>
</tr> </tr>
</thead> </thead>
@ -31,10 +37,10 @@
</ng-container> </ng-container>
<ng-container> <ng-container>
<table class="table table-items table-fixed" [sqxSyncWidth]="header"> <table class="table table-items table-fixed" [sqxSyncWidth]="header">
<tbody *ngFor="let eventConsumer of eventConsumersState.eventConsumers | async; trackBy: trackByEventConsumer" @for (eventConsumer of eventConsumersState.eventConsumers | async; track eventConsumer.name) {
[sqxEventConsumer]="eventConsumer" (failure)="showError(eventConsumer)"> <tbody (failure)="showError(eventConsumer)" [sqxEventConsumer]="eventConsumer"></tbody>
</tbody> }
</table> </table>
</ng-container> </ng-container>
</sqx-list-view> </sqx-list-view>
@ -42,14 +48,15 @@
<ng-template sidebarMenu> <ng-template sidebarMenu>
<div class="panel-nav"> <div class="panel-nav">
<a class="panel-link" <a
class="panel-link"
queryParamsHandling="preserve"
replaceUrl="true" replaceUrl="true"
routerLink="help" routerLink="help"
routerLinkActive="active" routerLinkActive="active"
queryParamsHandling="preserve" sqxTourStep="help"
title="i18n:common.help" title="i18n:common.help"
titlePosition="left" titlePosition="left">
sqxTourStep="help">
<i class="icon-help2"></i> <i class="icon-help2"></i>
</a> </a>
</div> </div>
@ -58,12 +65,12 @@
<router-outlet></router-outlet> <router-outlet></router-outlet>
<sqx-modal-dialog *sqxModal="eventConsumerErrorDialog" (dialogClose)="eventConsumerErrorDialog.hide()"> <sqx-modal-dialog (dialogClose)="eventConsumerErrorDialog.hide()" *sqxModal="eventConsumerErrorDialog">
<ng-container title> <ng-container title>
{{ 'common.error' | sqxTranslate }} {{ "common.error" | sqxTranslate }}
</ng-container> </ng-container>
<ng-container content> <ng-container content>
<textarea readonly class="form-control error-message small">{{eventConsumerError}}</textarea> <textarea class="form-control error-message small" readonly>{{ eventConsumerError }}</textarea>
</ng-container> </ng-container>
</sqx-modal-dialog> </sqx-modal-dialog>

7
frontend/src/app/features/administration/pages/event-consumers/event-consumers-page.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 { AsyncPipe, NgFor } from '@angular/common'; import { AsyncPipe } from '@angular/common';
import { Component, OnInit } from '@angular/core'; import { Component, OnInit } from '@angular/core';
import { RouterLink, RouterLinkActive, RouterOutlet } from '@angular/router'; import { RouterLink, RouterLinkActive, RouterOutlet } from '@angular/router';
import { timer } from 'rxjs'; import { timer } from 'rxjs';
@ -26,7 +26,6 @@ import { EventConsumerComponent } from './event-consumer.component';
ListViewComponent, ListViewComponent,
ModalDialogComponent, ModalDialogComponent,
ModalDirective, ModalDirective,
NgFor,
RouterLink, RouterLink,
RouterLinkActive, RouterLinkActive,
RouterOutlet, RouterOutlet,
@ -62,10 +61,6 @@ export class EventConsumersPageComponent implements OnInit {
this.eventConsumersState.load(true, false); this.eventConsumersState.load(true, false);
} }
public trackByEventConsumer(_index: number, es: EventConsumerDto) {
return es.name;
}
public showError(eventConsumer: EventConsumerDto) { public showError(eventConsumer: EventConsumerDto) {
this.eventConsumerError = eventConsumer.error; this.eventConsumerError = eventConsumer.error;
this.eventConsumerErrorDialog.show(); this.eventConsumerErrorDialog.show();

96
frontend/src/app/features/administration/pages/restore/restore-page.component.html

@ -1,65 +1,76 @@
<sqx-title message="i18n:jobs.restorePageTitle"></sqx-title> <sqx-title message="i18n:jobs.restorePageTitle"></sqx-title>
<sqx-layout layout="main" titleText="i18n:jobs.restoreTitle" titleIcon="backup" innerWidth="70"> <sqx-layout innerWidth="70" layout="main" titleIcon="backup" titleText="i18n:jobs.restoreTitle">
<ng-container> <ng-container>
<sqx-list-view innerWidth="70rem"> <sqx-list-view innerWidth="70rem">
<div class="card section" *ngIf="restoreJob | async; let job"> @if (restoreJob | async; as job) {
<div class="card-header"> <div class="card section">
<div class="row gx-2 align-items-center"> <div class="card-header">
<div class="col-auto"> <div class="row gx-2 align-items-center">
<div *ngIf="job.status === 'Started'" class="restore-status restore-status-pending spin"> <div class="col-auto">
<i class="icon-hour-glass"></i> @if (job.status === "Started") {
<div class="restore-status restore-status-pending spin">
<i class="icon-hour-glass"></i>
</div>
}
@if (job.status === "Failed") {
<div class="restore-status restore-status-failed">
<i class="icon-exclamation"></i>
</div>
}
@if (job.status === "Completed") {
<div class="restore-status restore-status-success">
<i class="icon-checkmark"></i>
</div>
}
</div> </div>
<div *ngIf="job.status === 'Failed'" class="restore-status restore-status-failed"> <div class="col">
<i class="icon-exclamation"></i> <h3>{{ "jobs.restoreLastStatus" | sqxTranslate }}</h3>
</div> </div>
<div *ngIf="job.status === 'Completed'" class="restore-status restore-status-success"> <div class="col text-end restore-url">
<i class="icon-checkmark"></i> {{ job.url }}
</div> </div>
</div> </div>
<div class="col">
<h3>{{ 'jobs.restoreLastStatus' | sqxTranslate }}</h3>
</div>
<div class="col text-end restore-url">
{{job.url}}
</div>
</div> </div>
</div> <div class="card-body">
<div class="card-body"> @for (row of job.log; track row) {
<div *ngFor="let row of job.log"> <div>
{{row}} {{ row }}
</div>
}
</div> </div>
</div> <div class="card-footer small text-muted">
<div class="card-footer small text-muted"> <div class="row">
<div class="row"> <div class="col">{{ "jobs.restoreStartedLabel" | sqxTranslate }}: {{ job.started | sqxISODate }}</div>
<div class="col"> @if (job.stopped) {
{{ 'jobs.restoreStartedLabel' | sqxTranslate }}: {{job.started | sqxISODate}} <div class="col text-end">
</div> {{ "jobs.restoreStoppedLabel" | sqxTranslate }}: {{ job.stopped | sqxISODate }}
<div class="col text-end" *ngIf="job.stopped"> </div>
{{ 'jobs.restoreStoppedLabel' | sqxTranslate }}: {{job.stopped | sqxISODate}} }
</div> </div>
</div> </div>
</div> </div>
</div> }
<div class="table-items-row table-items-row-summary"> <div class="table-items-row table-items-row-summary">
<form [formGroup]="restoreForm.form" (ngSubmit)="restore()"> <form [formGroup]="restoreForm.form" (ngSubmit)="restore()">
<div class="row gx-2"> <div class="row gx-2">
<div class="col"> <div class="col">
<sqx-control-errors for="url"></sqx-control-errors> <sqx-control-errors for="url"></sqx-control-errors>
<input class="form-control" formControlName="url" placeholder="{{ 'jobs.restoreLastUrl' | sqxTranslate }}"> <input class="form-control" formControlName="url" placeholder="{{ 'jobs.restoreLastUrl' | sqxTranslate }}" />
</div> </div>
<div class="col"> <div class="col">
<sqx-control-errors for="name"></sqx-control-errors> <sqx-control-errors for="name"></sqx-control-errors>
<input class="form-control" formControlName="name" placeholder="{{ 'jobs.restoreNewAppName' | sqxTranslate }}"> <input
class="form-control"
formControlName="name"
placeholder="{{ 'jobs.restoreNewAppName' | sqxTranslate }}" />
</div> </div>
<div class="col-auto"> <div class="col-auto">
<button type="submit" class="btn btn-success" [disabled]="restoreForm.hasNoUrl | async"> <button class="btn btn-success" [disabled]="restoreForm.hasNoUrl | async" type="submit">
{{ 'jobs.restore' | sqxTranslate }} {{ "jobs.restore" | sqxTranslate }}
</button> </button>
</div> </div>
</div> </div>
@ -70,18 +81,19 @@
<ng-template sidebarMenu> <ng-template sidebarMenu>
<div class="panel-nav"> <div class="panel-nav">
<a class="panel-link" <a
class="panel-link"
queryParamsHandling="preserve"
replaceUrl="true" replaceUrl="true"
routerLink="help" routerLink="help"
routerLinkActive="active" routerLinkActive="active"
queryParamsHandling="preserve" sqxTourStep="help"
title="i18n:common.help" title="i18n:common.help"
titlePosition="left" titlePosition="left">
sqxTourStep="help">
<i class="icon-help2"></i> <i class="icon-help2"></i>
</a> </a>
</div> </div>
</ng-template> </ng-template>
</sqx-layout> </sqx-layout>
<router-outlet></router-outlet> <router-outlet></router-outlet>

4
frontend/src/app/features/administration/pages/restore/restore-page.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 { AsyncPipe, NgFor, NgIf } from '@angular/common'; import { AsyncPipe } from '@angular/common';
import { Component } from '@angular/core'; import { Component } from '@angular/core';
import { FormsModule, ReactiveFormsModule } from '@angular/forms'; import { FormsModule, ReactiveFormsModule } from '@angular/forms';
import { RouterLink, RouterLinkActive, RouterOutlet } from '@angular/router'; import { RouterLink, RouterLinkActive, RouterOutlet } from '@angular/router';
@ -24,8 +24,6 @@ import { AuthService, ControlErrorsComponent, DialogService, ISODatePipe, JobsSe
ISODatePipe, ISODatePipe,
LayoutComponent, LayoutComponent,
ListViewComponent, ListViewComponent,
NgFor,
NgIf,
ReactiveFormsModule, ReactiveFormsModule,
RouterLink, RouterLink,
RouterLinkActive, RouterLinkActive,

93
frontend/src/app/features/administration/pages/users/user-page.component.html

@ -1,79 +1,104 @@
<form [formGroup]="userForm.form" (ngSubmit)="save()"> <form [formGroup]="userForm.form" (ngSubmit)="save()">
<input style="display: none;" type="password" name="foilautofill"> <input name="foilautofill" style="display: none" type="password" />
<sqx-layout layout="right" width="30" white="true" padding="true" overflow="true"> <sqx-layout layout="right" overflow="true" padding="true" white="true" width="30">
<ng-container title> <ng-container title>
<ng-container *ngIf="usersState.selectedUser | async; else noUserTitle"> @if (usersState.selectedUser | async) {
<sqx-title message="i18n:users.editPageTitle"></sqx-title> <sqx-title message="i18n:users.editPageTitle"></sqx-title>
<h3>{{ "users.editTitle" | sqxTranslate }}</h3>
<h3>{{ 'users.editTitle' | sqxTranslate }}</h3> } @else {
</ng-container>
<ng-template #noUserTitle>
<sqx-title message="i18n:users.createPageTitle"></sqx-title> <sqx-title message="i18n:users.createPageTitle"></sqx-title>
<h3>{{ "users.createTitle" | sqxTranslate }}</h3>
<h3>{{ 'users.createTitle' | sqxTranslate }}</h3> }
</ng-template>
</ng-container> </ng-container>
<ng-container menu> <ng-container menu>
<ng-container *ngIf="usersState.selectedUser | async; let user; else noUserMenu"> @if (usersState.selectedUser | async; as user) {
<button type="submit" class="btn btn-primary" shortcut="CTRL + SHIFT + S" *ngIf="isEditable"> @if (isEditable) {
{{ 'common.save' | sqxTranslate }} <button class="btn btn-primary" shortcut="CTRL + SHIFT + S" type="submit">
</button> {{ "common.save" | sqxTranslate }}
</ng-container> </button>
}
<ng-template #noUserMenu> } @else {
<button type="submit" class="btn btn-primary" shortcut="CTRL + SHIFT + S"> <button class="btn btn-primary" shortcut="CTRL + SHIFT + S" type="submit">
{{ 'common.save' | sqxTranslate }} {{ "common.save" | sqxTranslate }}
</button> </button>
</ng-template> }
</ng-container> </ng-container>
<ng-container > <ng-container>
<sqx-form-error [error]="userForm.error | async"></sqx-form-error> <sqx-form-error [error]="userForm.error | async"></sqx-form-error>
<div class="form-group"> <div class="form-group">
<label for="email">{{ 'common.email' | sqxTranslate }} <small class="hint">({{ 'common.requiredHint' | sqxTranslate }})</small></label> <label for="email">
{{ "common.email" | sqxTranslate }}
<small class="hint">({{ "common.requiredHint" | sqxTranslate }})</small>
</label>
<sqx-control-errors for="email"></sqx-control-errors> <sqx-control-errors for="email"></sqx-control-errors>
<input type="email" class="form-control" id="email" maxlength="100" formControlName="email" autocomplete="off"> <input class="form-control" id="email" autocomplete="off" formControlName="email" maxlength="100" type="email" />
</div> </div>
<div class="form-group"> <div class="form-group">
<label for="displayName">{{ 'common.displayName' | sqxTranslate }} <small class="hint">({{ 'common.requiredHint' | sqxTranslate }})</small></label> <label for="displayName">
{{ "common.displayName" | sqxTranslate }}
<small class="hint">({{ "common.requiredHint" | sqxTranslate }})</small>
</label>
<sqx-control-errors for="displayName"></sqx-control-errors> <sqx-control-errors for="displayName"></sqx-control-errors>
<input class="form-control" id="displayName" maxlength="100" formControlName="displayName" autocomplete="off" spellcheck="false"> <input
class="form-control"
id="displayName"
autocomplete="off"
formControlName="displayName"
maxlength="100"
spellcheck="false" />
</div> </div>
<div class="form-group form-group-section"> <div class="form-group form-group-section">
<div class="form-group"> <div class="form-group">
<label for="password">{{ 'common.password' | sqxTranslate }}</label> <label for="password">{{ "common.password" | sqxTranslate }}</label>
<sqx-control-errors for="password"></sqx-control-errors> <sqx-control-errors for="password"></sqx-control-errors>
<input type="password" class="form-control" id="password" maxlength="100" formControlName="password" autocomplete="off"> <input
class="form-control"
id="password"
autocomplete="off"
formControlName="password"
maxlength="100"
type="password" />
</div> </div>
<div class="form-group"> <div class="form-group">
<label for="password">{{ 'common.passwordConfirm' | sqxTranslate }}</label> <label for="password">{{ "common.passwordConfirm" | sqxTranslate }}</label>
<sqx-control-errors for="passwordConfirm"></sqx-control-errors> <sqx-control-errors for="passwordConfirm"></sqx-control-errors>
<input type="password" class="form-control" id="passwordConfirm" maxlength="100" formControlName="passwordConfirm" autocomplete="off"> <input
class="form-control"
id="passwordConfirm"
autocomplete="off"
formControlName="passwordConfirm"
maxlength="100"
type="password" />
</div> </div>
</div> </div>
<div class="form-group form-group-section"> <div class="form-group form-group-section">
<label for="permissions">{{ 'common.permissions' | sqxTranslate }}</label> <label for="permissions">{{ "common.permissions" | sqxTranslate }}</label>
<sqx-control-errors for="permissions"></sqx-control-errors> <sqx-control-errors for="permissions"></sqx-control-errors>
<textarea class="form-control" id="permissions" formControlName="permissions" placeholder="{{ 'common.separateByLine' | sqxTranslate }}" autocomplete="off" spellcheck="false"></textarea> <textarea
class="form-control"
id="permissions"
autocomplete="off"
formControlName="permissions"
placeholder="{{ 'common.separateByLine' | sqxTranslate }}"
spellcheck="false"></textarea>
</div> </div>
</ng-container> </ng-container>
</sqx-layout> </sqx-layout>
</form> </form>

3
frontend/src/app/features/administration/pages/users/user-page.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 { AsyncPipe, NgIf } from '@angular/common'; import { AsyncPipe } from '@angular/common';
import { Component, OnInit } from '@angular/core'; import { Component, OnInit } from '@angular/core';
import { FormsModule, ReactiveFormsModule } from '@angular/forms'; import { FormsModule, ReactiveFormsModule } from '@angular/forms';
import { ActivatedRoute, Router } from '@angular/router'; import { ActivatedRoute, Router } from '@angular/router';
@ -23,7 +23,6 @@ import { UpsertUserDto, UserDto, UserForm, UsersState } from '../../internal';
FormErrorComponent, FormErrorComponent,
FormsModule, FormsModule,
LayoutComponent, LayoutComponent,
NgIf,
ReactiveFormsModule, ReactiveFormsModule,
ShortcutDirective, ShortcutDirective,
TitleComponent, TitleComponent,

39
frontend/src/app/features/administration/pages/users/user.component.html

@ -1,29 +1,36 @@
<tr [routerLink]="user.id" queryParamsHandling="preserve" routerLinkActive="active"> <tr queryParamsHandling="preserve" [routerLink]="user.id" routerLinkActive="active">
<td class="cell-user"> <td class="cell-user">
<img class="user-picture" title="{{user.displayName}}" [src]="user | sqxUserDtoPicture"> <img class="user-picture" [src]="user | sqxUserDtoPicture" title="{{ user.displayName }}" />
</td> </td>
<td class="cell-auto"> <td class="cell-auto">
<span class="user-name table-cell">{{user.displayName}}</span> <span class="user-name table-cell">{{ user.displayName }}</span>
</td> </td>
<td class="cell-auto"> <td class="cell-auto">
<span class="user-email table-cell">{{user.email}}</span> <span class="user-email table-cell">{{ user.email }}</span>
</td> </td>
<td class="cell-actions-lg"> <td class="cell-actions-lg">
<button type="button" class="btn btn-text-secondary" (click)="lock()" sqxStopClick *ngIf="user.canLock" title="i18n:users.lockTooltip"> @if (user.canLock) {
<i class="icon icon-unlocked"></i> <button class="btn btn-text-secondary" (click)="lock()" sqxStopClick title="i18n:users.lockTooltip" type="button">
</button> <i class="icon icon-unlocked"></i>
<button type="button" class="btn btn-text-secondary" (click)="unlock()" sqxStopClick *ngIf="user.canUnlock" title="i18n:users.unlockTooltip"> </button>
<i class="icon icon-lock"></i> }
</button> @if (user.canUnlock) {
<button class="btn btn-text-secondary" (click)="unlock()" sqxStopClick title="i18n:users.unlockTooltip" type="button">
<i class="icon icon-lock"></i>
</button>
}
<button type="button" class="btn btn-text-danger" [disabled]="!user.canDelete" <button
(sqxConfirmClick)="delete()" class="btn btn-text-danger"
confirmTitle="i18n:users.deleteConfirmTitle"
confirmText="i18n:users.deleteConfirmText"
confirmRememberKey="deleteUser" confirmRememberKey="deleteUser"
sqxStopClick> confirmText="i18n:users.deleteConfirmText"
confirmTitle="i18n:users.deleteConfirmTitle"
[disabled]="!user.canDelete"
(sqxConfirmClick)="delete()"
sqxStopClick
type="button">
<i class="icon-bin2"></i> <i class="icon-bin2"></i>
</button> </button>
</td> </td>
</tr> </tr>
<tr class="spacer"></tr> <tr class="spacer"></tr>

3
frontend/src/app/features/administration/pages/users/user.component.ts

@ -7,7 +7,7 @@
/* eslint-disable @angular-eslint/component-selector */ /* eslint-disable @angular-eslint/component-selector */
import { NgIf } from '@angular/common';
import { ChangeDetectionStrategy, Component, Input } from '@angular/core'; import { ChangeDetectionStrategy, Component, Input } from '@angular/core';
import { RouterLink, RouterLinkActive } from '@angular/router'; import { RouterLink, RouterLinkActive } from '@angular/router';
import { ConfirmClickDirective, StopClickDirective, TooltipDirective, UserDtoPicture } from '@app/shared'; import { ConfirmClickDirective, StopClickDirective, TooltipDirective, UserDtoPicture } from '@app/shared';
@ -21,7 +21,6 @@ import { UserDto, UsersState } from '../../internal';
changeDetection: ChangeDetectionStrategy.OnPush, changeDetection: ChangeDetectionStrategy.OnPush,
imports: [ imports: [
ConfirmClickDirective, ConfirmClickDirective,
NgIf,
RouterLink, RouterLink,
RouterLinkActive, RouterLinkActive,
StopClickDirective, StopClickDirective,

60
frontend/src/app/features/administration/pages/users/users-page.component.html

@ -1,23 +1,28 @@
<sqx-title message="i18n:users.listPageTitle"></sqx-title> <sqx-title message="i18n:users.listPageTitle"></sqx-title>
<sqx-layout layout="main" titleText="i18n:users.listTitle" titleIcon="user-o" innerWidth="50"> <sqx-layout innerWidth="50" layout="main" titleIcon="user-o" titleText="i18n:users.listTitle">
<ng-container menu> <ng-container menu>
<div class="d-flex justify-content-end"> <div class="d-flex justify-content-end">
<button type="button" class="btn btn-text-secondary" (click)="reload()" title="i18n:users.refreshTooltip" shortcut="CTRL + B"> <button class="btn btn-text-secondary" (click)="reload()" shortcut="CTRL + B" title="i18n:users.refreshTooltip" type="button">
<i class="icon-reset"></i> {{ 'common.refresh' | sqxTranslate }} <i class="icon-reset"></i>
{{ "common.refresh" | sqxTranslate }}
</button> </button>
<form class="form-inline ms-2" (ngSubmit)="search()"> <form class="form-inline ms-2" (ngSubmit)="search()">
<input class="form-control" [formControl]="usersFilter" placeholder="{{ 'users.search' | sqxTranslate }}" <input
class="form-control"
[formControl]="usersFilter"
placeholder="{{ 'users.search' | sqxTranslate }}"
shortcut="CTRL + SHIFT + S" shortcut="CTRL + SHIFT + S"
shortcutAction="focus"> shortcutAction="focus" />
</form> </form>
<ng-container *ngIf="usersState.canCreate | async"> @if (usersState.canCreate | async) {
<button type="button" class="btn btn-success ms-2" routerLink="new" title="i18n:users.createTooltip" shortcut="CTRL + U"> <button class="btn btn-success ms-2" routerLink="new" shortcut="CTRL + U" title="i18n:users.createTooltip" type="button">
<i class="icon-plus"></i> {{ 'users.create' | sqxTranslate }} <i class="icon-plus"></i>
{{ "users.create" | sqxTranslate }}
</button> </button>
</ng-container> }
</div> </div>
</ng-container> </ng-container>
@ -27,29 +32,31 @@
<table class="table table-items table-fixed" #header> <table class="table table-items table-fixed" #header>
<thead> <thead>
<tr> <tr>
<th class="cell-user"> <th class="cell-user">&nbsp;</th>
&nbsp;
</th>
<th class="cell-auto"> <th class="cell-auto">
<span class="truncate">{{ 'common.name' | sqxTranslate }}</span> <span class="truncate">{{ "common.name" | sqxTranslate }}</span>
</th> </th>
<th class="cell-auto"> <th class="cell-auto">
<span class="truncate">{{ 'common.email' | sqxTranslate }}</span> <span class="truncate">{{ "common.email" | sqxTranslate }}</span>
</th> </th>
<th class="cell-actions-lg"> <th class="cell-actions-lg">
<span class="truncate">{{ 'common.actions' | sqxTranslate }}</span> <span class="truncate">{{ "common.actions" | sqxTranslate }}</span>
</th> </th>
</tr> </tr>
</thead> </thead>
</table> </table>
</ng-container> </ng-container>
<ng-container> <ng-container>
<table class="table table-items table-fixed" *ngIf="usersState.users | async; let users" [sqxSyncWidth]="header"> @if (usersState.users | async; as users) {
<tbody *ngFor="let user of users; trackBy: trackByUser" [sqxUser]="user"></tbody> <table class="table table-items table-fixed" [sqxSyncWidth]="header">
</table> @for (user of users; track user.id) {
<tbody [sqxUser]="user"></tbody>
}
</table>
}
</ng-container> </ng-container>
<ng-container footer> <ng-container footer>
<sqx-pager [paging]="usersState.paging | async" (pagingChange)="usersState.page($event)"></sqx-pager> <sqx-pager [paging]="usersState.paging | async" (pagingChange)="usersState.page($event)"></sqx-pager>
</ng-container> </ng-container>
@ -58,18 +65,19 @@
<ng-template sidebarMenu> <ng-template sidebarMenu>
<div class="panel-nav"> <div class="panel-nav">
<a class="panel-link" <a
class="panel-link"
queryParamsHandling="preserve"
replaceUrl="true" replaceUrl="true"
routerLink="help" routerLink="help"
routerLinkActive="active" routerLinkActive="active"
queryParamsHandling="preserve" sqxTourStep="help"
title="i18n:common.help" title="i18n:common.help"
titlePosition="left" titlePosition="left">
sqxTourStep="help">
<i class="icon-help2"></i> <i class="icon-help2"></i>
</a> </a>
</div> </div>
</ng-template> </ng-template>
</sqx-layout> </sqx-layout>
<router-outlet></router-outlet> <router-outlet></router-outlet>

10
frontend/src/app/features/administration/pages/users/users-page.component.ts

@ -5,12 +5,12 @@
* Copyright (c) Squidex UG (haftungsbeschränkt). All rights reserved. * Copyright (c) Squidex UG (haftungsbeschränkt). All rights reserved.
*/ */
import { AsyncPipe, NgFor, NgIf } from '@angular/common'; import { AsyncPipe } from '@angular/common';
import { Component, OnInit } from '@angular/core'; import { Component, OnInit } from '@angular/core';
import { FormsModule, ReactiveFormsModule, UntypedFormControl } from '@angular/forms'; import { FormsModule, ReactiveFormsModule, UntypedFormControl } from '@angular/forms';
import { RouterLink, RouterLinkActive, RouterOutlet } from '@angular/router'; import { RouterLink, RouterLinkActive, RouterOutlet } from '@angular/router';
import { LayoutComponent, ListViewComponent, PagerComponent, Router2State, ShortcutDirective, SidebarMenuDirective, Subscriptions, SyncWidthDirective, TitleComponent, TooltipDirective, TourStepDirective, TranslatePipe } from '@app/shared'; import { LayoutComponent, ListViewComponent, PagerComponent, Router2State, ShortcutDirective, SidebarMenuDirective, Subscriptions, SyncWidthDirective, TitleComponent, TooltipDirective, TourStepDirective, TranslatePipe } from '@app/shared';
import { UserDto, UsersState } from '../../internal'; import { UsersState } from '../../internal';
import { UserComponent } from './user.component'; import { UserComponent } from './user.component';
@Component({ @Component({
@ -26,8 +26,6 @@ import { UserComponent } from './user.component';
FormsModule, FormsModule,
LayoutComponent, LayoutComponent,
ListViewComponent, ListViewComponent,
NgFor,
NgIf,
PagerComponent, PagerComponent,
ReactiveFormsModule, ReactiveFormsModule,
RouterLink, RouterLink,
@ -75,8 +73,4 @@ export class UsersPageComponent implements OnInit {
public search() { public search() {
this.usersState.search(this.usersFilter.value); this.usersState.search(this.usersFilter.value);
} }
public trackByUser(_index: number, user: UserDto) {
return user.id;
}
} }

19
frontend/src/app/features/administration/services/event-consumers.service.spec.ts

@ -5,7 +5,8 @@
* Copyright (c) Squidex UG (haftungsbeschränkt). All rights reserved. * Copyright (c) Squidex UG (haftungsbeschränkt). All rights reserved.
*/ */
import { HttpClientTestingModule, HttpTestingController } from '@angular/common/http/testing'; import { provideHttpClient, withInterceptorsFromDi } from '@angular/common/http';
import { HttpTestingController, provideHttpClientTesting } from '@angular/common/http/testing';
import { inject, TestBed } from '@angular/core/testing'; import { inject, TestBed } from '@angular/core/testing';
import { ApiUrlConfig, Resource, ResourceLinks } from '@app/shared'; import { ApiUrlConfig, Resource, ResourceLinks } from '@app/shared';
import { EventConsumerDto, EventConsumersDto, EventConsumersService } from './event-consumers.service'; import { EventConsumerDto, EventConsumersDto, EventConsumersService } from './event-consumers.service';
@ -13,14 +14,14 @@ import { EventConsumerDto, EventConsumersDto, EventConsumersService } from './ev
describe('EventConsumersService', () => { describe('EventConsumersService', () => {
beforeEach(() => { beforeEach(() => {
TestBed.configureTestingModule({ TestBed.configureTestingModule({
imports: [ imports: [],
HttpClientTestingModule, providers: [
], provideHttpClient(withInterceptorsFromDi()),
providers: [ provideHttpClientTesting(),
EventConsumersService, EventConsumersService,
{ provide: ApiUrlConfig, useValue: new ApiUrlConfig('http://service/p/') }, { provide: ApiUrlConfig, useValue: new ApiUrlConfig('http://service/p/') },
], ],
}); });
}); });
afterEach(inject([HttpTestingController], (httpMock: HttpTestingController) => { afterEach(inject([HttpTestingController], (httpMock: HttpTestingController) => {

19
frontend/src/app/features/administration/services/users.service.spec.ts

@ -5,7 +5,8 @@
* Copyright (c) Squidex UG (haftungsbeschränkt). All rights reserved. * Copyright (c) Squidex UG (haftungsbeschränkt). All rights reserved.
*/ */
import { HttpClientTestingModule, HttpTestingController } from '@angular/common/http/testing'; import { provideHttpClient, withInterceptorsFromDi } from '@angular/common/http';
import { HttpTestingController, provideHttpClientTesting } from '@angular/common/http/testing';
import { inject, TestBed } from '@angular/core/testing'; import { inject, TestBed } from '@angular/core/testing';
import { ApiUrlConfig, Resource, ResourceLinks } from '@app/shared'; import { ApiUrlConfig, Resource, ResourceLinks } from '@app/shared';
import { UserDto, UsersDto, UsersService } from './users.service'; import { UserDto, UsersDto, UsersService } from './users.service';
@ -13,14 +14,14 @@ import { UserDto, UsersDto, UsersService } from './users.service';
describe('UsersService', () => { describe('UsersService', () => {
beforeEach(() => { beforeEach(() => {
TestBed.configureTestingModule({ TestBed.configureTestingModule({
imports: [ imports: [],
HttpClientTestingModule, providers: [
], provideHttpClient(withInterceptorsFromDi()),
providers: [ provideHttpClientTesting(),
UsersService, UsersService,
{ provide: ApiUrlConfig, useValue: new ApiUrlConfig('http://service/p/') }, { provide: ApiUrlConfig, useValue: new ApiUrlConfig('http://service/p/') },
], ],
}); });
}); });
afterEach(inject([HttpTestingController], (httpMock: HttpTestingController) => { afterEach(inject([HttpTestingController], (httpMock: HttpTestingController) => {

14
frontend/src/app/features/api/api-area.component.html

@ -1,29 +1,29 @@
<sqx-title message="i18n:api.pageTitle"></sqx-title> <sqx-title message="i18n:api.pageTitle"></sqx-title>
<sqx-layout layout="left" titleText="i18n:api.title" titleIcon="api" width="16" white="true" overflow="true" padding="true"> <sqx-layout layout="left" overflow="true" padding="true" titleIcon="api" titleText="i18n:api.title" white="true" width="16">
<ng-container> <ng-container>
<ul class="nav nav-light flex-column"> <ul class="nav nav-light flex-column">
<li class="nav-item" sqxTourStep="graphql"> <li class="nav-item" sqxTourStep="graphql">
<a class="nav-link" routerLink="graphql" routerLinkActive="active"> <a class="nav-link" routerLink="graphql" routerLinkActive="active">
{{ 'api.graphql' | sqxTranslate }} {{ "api.graphql" | sqxTranslate }}
</a> </a>
</li> </li>
<li class="nav-item nav-heading"> <li class="nav-item nav-heading">
{{ 'common.openAPI' | sqxTranslate }} {{ "common.openAPI" | sqxTranslate }}
</li> </li>
<li class="nav-item" sqxTourStep="contentApi"> <li class="nav-item" sqxTourStep="contentApi">
<a class="nav-link" href="/api/content/{{appsState.appName}}/docs" sqxExternalLink> <a class="nav-link" href="/api/content/{{ appsState.appName }}/docs" sqxExternalLink>
{{ 'api.contentApi' | sqxTranslate }} {{ "api.contentApi" | sqxTranslate }}
</a> </a>
</li> </li>
<li class="nav-item" sqxTourStep="generalAPi"> <li class="nav-item" sqxTourStep="generalAPi">
<a class="nav-link" href="/api/docs" sqxExternalLink> <a class="nav-link" href="/api/docs" sqxExternalLink>
{{ 'api.generalApi' | sqxTranslate }} {{ "api.generalApi" | sqxTranslate }}
</a> </a>
</li> </li>
</ul> </ul>
</ng-container> </ng-container>
</sqx-layout> </sqx-layout>
<router-outlet></router-outlet> <router-outlet></router-outlet>

40
frontend/src/app/features/api/pages/graphql/graphql-page.component.html

@ -1,38 +1,40 @@
<sqx-title message="i18n:api.graphqlPageTitle"></sqx-title> <sqx-title message="i18n:api.graphqlPageTitle"></sqx-title>
<sqx-layout layout="main" hideHeader="true" hideSidebar="true"> <sqx-layout hideHeader="true" hideSidebar="true" layout="main">
<div inner #graphiQLContainer sqxTourStep="graphQLExplorer"></div> <div #graphiQLContainer inner sqxTourStep="graphQLExplorer"></div>
<button class="btn btn-simple btn-options" *ngIf="clientsReadable" (click)="clientsDialog.show()"> @if (clientsReadable) {
<i class="icon-clients"></i> <button class="btn btn-simple btn-options" (click)="clientsDialog.show()">
</button> <i class="icon-clients"></i>
</button>
}
</sqx-layout> </sqx-layout>
<sqx-modal-dialog *sqxModal="clientsDialog" (dialogClose)="clientsDialog.hide()"> <sqx-modal-dialog (dialogClose)="clientsDialog.hide()" *sqxModal="clientsDialog">
<ng-container title> <ng-container title>
{{ 'api.selectClient' | sqxTranslate }} {{ "api.selectClient" | sqxTranslate }}
</ng-container> </ng-container>
<ng-container content> <ng-container content>
<sqx-form-hint> <sqx-form-hint>
{{ 'api.selectClientDescription' | sqxTranslate }} {{ "api.selectClientDescription" | sqxTranslate }}
</sqx-form-hint> </sqx-form-hint>
<div class="form-group"> <div class="form-group">
<label for="client">{{ 'common.client' | sqxTranslate }}</label> <label for="client">{{ "common.client" | sqxTranslate }}</label>
<select class="form-control" id="client" <select class="form-control" id="client" [ngModel]="clientSelected" (ngModelChange)="selectClient($event)">
[ngModel]="clientSelected" <option [ngValue]="null">{{ "api.noClient" | sqxTranslate }}</option>
(ngModelChange)="selectClient($event)"> @for (client of clientsState.clients | async; track client) {
<option [ngValue]="null">{{ 'api.noClient' | sqxTranslate }}</option> <option [ngValue]="client">{{ client.id }}</option>
<option *ngFor="let client of clientsState.clients | async" [ngValue]="client">{{client.id}}</option> }
</select> </select>
</div> </div>
</ng-container> </ng-container>
<ng-container footer> <ng-container footer>
<button type="button" class="btn btn-text-secondary" (click)="clientsDialog.hide()"> <button class="btn btn-text-secondary" (click)="clientsDialog.hide()" type="button">
{{ 'common.close' | sqxTranslate }} {{ "common.close" | sqxTranslate }}
</button> </button>
</ng-container> </ng-container>
</sqx-modal-dialog> </sqx-modal-dialog>

4
frontend/src/app/features/api/pages/graphql/graphql-page.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 { AsyncPipe, NgFor, NgIf } from '@angular/common'; import { AsyncPipe } from '@angular/common';
import { AfterViewInit, Component, ElementRef, OnInit, ViewChild } from '@angular/core'; import { AfterViewInit, Component, ElementRef, OnInit, ViewChild } from '@angular/core';
import { FormsModule } from '@angular/forms'; import { FormsModule } from '@angular/forms';
import { createGraphiQLFetcher } from '@graphiql/toolkit'; import { createGraphiQLFetcher } from '@graphiql/toolkit';
@ -26,8 +26,6 @@ import { ApiUrlConfig, AppsState, AuthService, ClientDto, ClientsService, Client
LayoutComponent, LayoutComponent,
ModalDialogComponent, ModalDialogComponent,
ModalDirective, ModalDirective,
NgFor,
NgIf,
TitleComponent, TitleComponent,
TooltipDirective, TooltipDirective,
TourStepDirective, TourStepDirective,

46
frontend/src/app/features/apps/pages/app.component.html

@ -2,43 +2,47 @@
<div class="card-body" sqxTourStep="app"> <div class="card-body" sqxTourStep="app">
<div class="row g-0"> <div class="row g-0">
<div class="col-auto card-left"> <div class="col-auto card-left">
<sqx-avatar [image]="app.image" [identifier]="app.name"></sqx-avatar> <sqx-avatar [identifier]="app.name" [image]="app.image"></sqx-avatar>
</div> </div>
<div class="col card-right"> <div class="col card-right">
<h3 class="card-title">{{app.displayName}}</h3> <h3 class="card-title">{{ app.displayName }}</h3>
<div class="card-text card-links truncate"> <div class="card-text card-links truncate">
<a [routerLink]="['/app', app.name]" sqxStopClick>{{ 'common.edit' | sqxTranslate }}</a> <a [routerLink]="['/app', app.name]" sqxStopClick>{{ "common.edit" | sqxTranslate }}</a>
<span class="deeplinks"> <span class="deeplinks">
&nbsp;| &nbsp;|
<a [routerLink]="['/app', app.name, 'content']" sqxStopClick>{{ 'common.content' | sqxTranslate }}</a> &middot; <a [routerLink]="['/app', app.name, 'content']" sqxStopClick>{{ "common.content" | sqxTranslate }}</a>
<a [routerLink]="['/app', app.name, 'assets']" sqxStopClick>{{ 'common.assets' | sqxTranslate }}</a> &middot; &middot;
<a [routerLink]="['/app', app.name, 'settings']" sqxStopClick>{{ 'common.settings' | sqxTranslate }}</a> <a [routerLink]="['/app', app.name, 'assets']" sqxStopClick>{{ "common.assets" | sqxTranslate }}</a>
&middot;
<a [routerLink]="['/app', app.name, 'settings']" sqxStopClick>{{ "common.settings" | sqxTranslate }}</a>
</span> </span>
</div> </div>
<div class="card-text" *ngIf="app.description"> @if (app.description) {
{{app.description}} <div class="card-text">
</div> {{ app.description }}
</div>
}
</div> </div>
</div> </div>
<ng-container *ngIf="app.canLeave"> @if (app.canLeave) {
<button type="button" class="btn btn-sm btn-text-secondary" (click)="dropdown.toggle()" sqxStopClick #buttonOptions> <button class="btn btn-sm btn-text-secondary" #buttonOptions (click)="dropdown.toggle()" sqxStopClick type="button">
<span class="hidden">{{ 'common.options' | sqxTranslate }}</span> <span class="hidden">{{ "common.options" | sqxTranslate }}</span>
<i class="icon-dots"></i> <i class="icon-dots"></i>
</button> </button>
<sqx-dropdown-menu scrollY="true" [sqxAnchoredTo]="buttonOptions" *sqxModal="dropdown; closeAlways: true">
<sqx-dropdown-menu *sqxModal="dropdown;closeAlways:true" [sqxAnchoredTo]="buttonOptions" scrollY="true"> <a
<a class="dropdown-item dropdown-item-delete" class="dropdown-item dropdown-item-delete"
(sqxConfirmClick)="leave.emit(app)" confirmRememberKey="leaveApp"
confirmTitle="i18n:apps.leaveConfirmTitle"
confirmText="i18n:apps.leaveConfirmText" confirmText="i18n:apps.leaveConfirmText"
confirmRememberKey="leaveApp"> confirmTitle="i18n:apps.leaveConfirmTitle"
{{ 'apps.leave' | sqxTranslate }} (sqxConfirmClick)="leave.emit(app)">
{{ "apps.leave" | sqxTranslate }}
</a> </a>
</sqx-dropdown-menu> </sqx-dropdown-menu>
</ng-container> }
</div> </div>
</div> </div>

3
frontend/src/app/features/apps/pages/app.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 { NgIf } from '@angular/common';
import { ChangeDetectionStrategy, Component, EventEmitter, Input, Output } from '@angular/core'; import { ChangeDetectionStrategy, Component, EventEmitter, Input, Output } from '@angular/core';
import { RouterLink } from '@angular/router'; import { RouterLink } from '@angular/router';
import { AppDto, AvatarComponent, ConfirmClickDirective, DropdownMenuComponent, ModalDirective, ModalModel, ModalPlacementDirective, StopClickDirective, TourStepDirective, TranslatePipe } from '@app/shared'; import { AppDto, AvatarComponent, ConfirmClickDirective, DropdownMenuComponent, ModalDirective, ModalModel, ModalPlacementDirective, StopClickDirective, TourStepDirective, TranslatePipe } from '@app/shared';
@ -22,7 +22,6 @@ import { AppDto, AvatarComponent, ConfirmClickDirective, DropdownMenuComponent,
DropdownMenuComponent, DropdownMenuComponent,
ModalDirective, ModalDirective,
ModalPlacementDirective, ModalPlacementDirective,
NgIf,
RouterLink, RouterLink,
StopClickDirective, StopClickDirective,
TourStepDirective, TourStepDirective,

134
frontend/src/app/features/apps/pages/apps-page.component.html

@ -1,79 +1,77 @@
<sqx-title message="i18n:apps.listPageTitle"></sqx-title> <sqx-title message="i18n:apps.listPageTitle"></sqx-title>
<div class="panel-container page" *ngIf="authState.userChanges | async; let user"> @if (authState.userChanges | async; as user) {
<div class="apps-section"> <div class="panel-container page">
<h1 class="apps-title">{{ 'apps.welcomeTitle' | sqxTranslate: { user: user.displayName } }}</h1> <div class="apps-section">
<h1 class="apps-title">{{ "apps.welcomeTitle" | sqxTranslate: { user: user.displayName } }}</h1>
<div class="subtext"> <div class="subtext">
{{ 'apps.welcomeSubtitle' | sqxTranslate }} {{ "apps.welcomeSubtitle" | sqxTranslate }}
</div>
</div>
<ng-container *ngIf="groupedApps | async; let groups">
<div class="apps-section" sqxTourStep="allApps">
<div class="empty" *ngIf="groups.length === 0">
<h3 class="empty-headline">{{ 'apps.empty' | sqxTranslate }}</h3>
</div>
<div class="team" *ngFor="let group of groups; trackBy: trackByGroup">
<div class="team-header" *ngIf="group.team">
<sqx-team [team]="group.team" (leave)="leaveTeam($event)"></sqx-team>
</div>
<div class="team-body" [class.padded]="group.team">
<sqx-app *ngFor="let app of group.apps; trackBy: trackByApp" [app]="app" (leave)="leaveApp($event)"></sqx-app>
<small class="team-empty" *ngIf="group.apps.length === 0">
{{ 'teams.empty' | sqxTranslate }}
</small>
</div>
</div> </div>
</div> </div>
</ng-container> @if (groupedApps | async; as groups) {
<div class="apps-section" sqxTourStep="allApps">
<div class="apps-section" *ngIf="(uiState.settings | async)?.canCreateApps"> @for (group of groups; track trackByGroup($index, group)) {
<div class="card card-template card-href" data-testid="new-app" (click)="createNewApp()" sqxTourStep="addApp"> <div class="team">
<div class="card-body"> @if (group.team) {
<div class="card-image"> <div class="team-header">
<img src="./images/add-app.svg"> <sqx-team (leave)="leaveTeam($event)" [team]="group.team"></sqx-team>
</div> </div>
}
<h3 class="card-title">{{ 'apps.createBlankApp' | sqxTranslate }}</h3> <div class="team-body" [class.padded]="group.team">
@for (app of group.apps; track app.id) {
<sqx-form-hint> <sqx-app [app]="app" (leave)="leaveApp($event)"></sqx-app>
{{ 'apps.createBlankAppDescription' | sqxTranslate }} } @empty {
</sqx-form-hint> <small class="team-empty">
{{ "teams.empty" | sqxTranslate }}
</small>
}
</div>
</div>
} @empty {
<div class="empty">
<h3 class="empty-headline">{{ "apps.empty" | sqxTranslate }}</h3>
</div>
}
</div> </div>
</div> }
@if ((uiState.settings | async)?.canCreateApps) {
<div class="card card-template card-href" *ngFor="let template of templates | async" (click)="createNewApp(template)"> <div class="apps-section">
<div class="card-body"> <div class="card card-template card-href" (click)="createNewApp()" data-testid="new-app" sqxTourStep="addApp">
<div class="card-image"> <div class="card-body">
<img src="./images/add-template.svg"> <div class="card-image">
<img src="./images/add-app.svg" />
</div>
<h3 class="card-title">{{ "apps.createBlankApp" | sqxTranslate }}</h3>
<sqx-form-hint>
{{ "apps.createBlankAppDescription" | sqxTranslate }}
</sqx-form-hint>
</div>
</div> </div>
@for (template of templates | async; track template) {
<h3 class="card-title">{{template.title}}</h3> <div class="card card-template card-href" (click)="createNewApp(template)">
<div class="card-body">
<sqx-form-hint> <div class="card-image">
{{template.description}} <img src="./images/add-template.svg" />
</sqx-form-hint> </div>
<h3 class="card-title">{{ template.title }}</h3>
<sqx-form-hint>
{{ template.description }}
</sqx-form-hint>
</div>
</div>
}
</div> </div>
</div> }
</div> @if (info) {
<div class="apps-section">
<div *ngIf="info" class="apps-section"> <small class="info">{{ info }}</small>
<small class="info">{{info}}</small> </div>
}
</div> </div>
</div> }
<sqx-app-form *sqxModal="addAppDialog" <sqx-app-form (dialogClose)="addAppDialog.hide()" *sqxModal="addAppDialog" [template]="addAppTemplate"></sqx-app-form>
(dialogClose)="addAppDialog.hide()" [template]="addAppTemplate">
</sqx-app-form>
<sqx-onboarding-dialog *sqxModal="onboardingDialog" <sqx-onboarding-dialog (dialogClose)="onboardingDialog.hide()" *sqxModal="onboardingDialog"></sqx-onboarding-dialog>
(dialogClose)="onboardingDialog.hide()">
</sqx-onboarding-dialog>
<sqx-news-dialog *sqxModal="newsDialog" [features]="newsFeatures!" <sqx-news-dialog (dialogClose)="newsDialog.hide()" [features]="newsFeatures!" *sqxModal="newsDialog"></sqx-news-dialog>
(dialogClose)="newsDialog.hide()">
</sqx-news-dialog>

8
frontend/src/app/features/apps/pages/apps-page.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 { AsyncPipe, NgFor, NgIf } from '@angular/common'; import { AsyncPipe } from '@angular/common';
import { Component, OnInit } from '@angular/core'; import { Component, OnInit } from '@angular/core';
import { combineLatest } from 'rxjs'; import { combineLatest } from 'rxjs';
import { map, take } from 'rxjs/operators'; import { map, take } from 'rxjs/operators';
@ -29,8 +29,6 @@ type GroupedApps = { team?: TeamDto; apps: AppDto[] };
FormHintComponent, FormHintComponent,
ModalDirective, ModalDirective,
NewsDialogComponent, NewsDialogComponent,
NgFor,
NgIf,
OnboardingDialogComponent, OnboardingDialogComponent,
TeamComponent, TeamComponent,
TitleComponent, TitleComponent,
@ -141,10 +139,6 @@ export class AppsPageComponent implements OnInit {
this.teamsState.leave(team); this.teamsState.leave(team);
} }
public trackByApp(_index: number, app: AppDto) {
return app.id;
}
public trackByGroup(_index: number, group: GroupedApps) { public trackByGroup(_index: number, group: GroupedApps) {
return group.team?.id || '0'; return group.team?.id || '0';
} }

20
frontend/src/app/features/apps/pages/news-dialog.component.html

@ -1,19 +1,19 @@
<sqx-modal-dialog size="lg" (dialogClose)="dialogClose.emit()"> <sqx-modal-dialog (dialogClose)="dialogClose.emit()" size="lg">
<ng-container title> <ng-container title>
{{ 'news.title' | sqxTranslate }} {{ "news.title" | sqxTranslate }}
</ng-container> </ng-container>
<ng-container content> <ng-container content>
<div class="help"> <div class="help">
<h1>{{ 'news.headline' | sqxTranslate }}</h1> <h1>{{ "news.headline" | sqxTranslate }}</h1>
<div *ngFor="let feature of features; trackBy: trackByFeature; let last = last"> @for (feature of features; track feature; let last = $last) {
<h4>{{feature.name}}</h4> <div>
<h4>{{ feature.name }}</h4>
<div [innerHTML]="feature.text | sqxHelpMarkdown"></div> <div [innerHTML]="feature.text | sqxHelpMarkdown"></div>
<hr [class.hidden]="last" />
<hr [class.hidden]="last" /> </div>
</div> }
</div> </div>
</ng-container> </ng-container>
</sqx-modal-dialog> </sqx-modal-dialog>

7
frontend/src/app/features/apps/pages/news-dialog.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 { NgFor } from '@angular/common';
import { Component, EventEmitter, Input, Output } from '@angular/core'; import { Component, EventEmitter, Input, Output } from '@angular/core';
import { FeatureDto, HelpMarkdownPipe, ModalDialogComponent, TooltipDirective, TranslatePipe } from '@app/shared'; import { FeatureDto, HelpMarkdownPipe, ModalDialogComponent, TooltipDirective, TranslatePipe } from '@app/shared';
@ -17,7 +17,6 @@ import { FeatureDto, HelpMarkdownPipe, ModalDialogComponent, TooltipDirective, T
imports: [ imports: [
HelpMarkdownPipe, HelpMarkdownPipe,
ModalDialogComponent, ModalDialogComponent,
NgFor,
TooltipDirective, TooltipDirective,
TranslatePipe, TranslatePipe,
], ],
@ -28,8 +27,4 @@ export class NewsDialogComponent {
@Input({ required: true }) @Input({ required: true })
public features!: ReadonlyArray<FeatureDto>; public features!: ReadonlyArray<FeatureDto>;
public trackByFeature(_index: number, feature: FeatureDto) {
return feature;
}
} }

161
frontend/src/app/features/apps/pages/onboarding-dialog.component.html

@ -1,100 +1,95 @@
<sqx-modal-dialog showHeader="false"> <sqx-modal-dialog showHeader="false">
<ng-container tabs> <ng-container tabs>
<div class="squid d-flex align-items-center justify-content-center"> <div class="squid d-flex align-items-center justify-content-center">
<img src="./images/squid.svg"> <img src="./images/squid.svg" />
</div> </div>
</ng-container> </ng-container>
<ng-container content> <ng-container content>
<a class="header-right modal-close force" (click)="cancel()">{{ 'tour.skip' | sqxTranslate }}</a> <a class="header-right modal-close force" (click)="cancel()">{{ "tour.skip" | sqxTranslate }}</a>
<div class="onboarding-step " *ngIf="step === 0"> @if (step === 0) {
<img @fade class="header-left" src="./images/logo-white-small.png"> <div class="onboarding-step">
<img class="header-left" @fade src="./images/logo-white-small.png" />
<div @slide class="onboarding-enter-leave text-center"> <div class="onboarding-enter-leave text-center" @slide>
<h1>{{ 'tour.welcome' | sqxTranslate }} <span class="header-focus">{{ 'tour.welcomeProduct' | sqxTranslate }}</span></h1> <h1>
{{ "tour.welcome" | sqxTranslate }}
<div [innerHTML]="'tour.stepIntroText' | sqxTranslate | sqxMarkdown | sqxSafeHtml"></div> <span class="header-focus">{{ "tour.welcomeProduct" | sqxTranslate }}</span>
</h1>
<div class="mt-4"> <div [innerHTML]="'tour.stepIntroText' | sqxTranslate | sqxMarkdown | sqxSafeHtml"></div>
<button (click)="next()" class="btn btn-success"> <div class="mt-4">
{{ 'tour.stepIntroNext' | sqxTranslate }} <button class="btn btn-success" (click)="next()">
</button> {{ "tour.stepIntroNext" | sqxTranslate }}
</button>
</div>
</div> </div>
</div> </div>
</div> }
<div *ngIf="step === 1"> @if (step === 1) {
<img @fade class="header-left" src="./images/logo-white-small.png"> <div>
<img class="header-left" @fade src="./images/logo-white-small.png" />
<div @slide class="onboarding-enter-leave"> <div class="onboarding-enter-leave" @slide>
<form [formGroup]="answersForm" (ngSubmit)="submitAnswers()"> <form [formGroup]="answersForm" (ngSubmit)="submitAnswers()">
<h2>{{ 'tour.stepDataTitle' | sqxTranslate }}</h2> <h2>{{ "tour.stepDataTitle" | sqxTranslate }}</h2>
<div [innerHTML]="'tour.stepDataText' | sqxTranslate | sqxMarkdown | sqxSafeHtml"></div>
<div [innerHTML]="'tour.stepDataText' | sqxTranslate | sqxMarkdown | sqxSafeHtml"></div> <div class="form-group mt-4">
<label for="role">{{ "tour.stepDataCompanyRole" | sqxTranslate }}</label>
<div class="form-group mt-4"> <select class="form-select" id="companyRole" formControlName="companyRole">
<label for="role">{{ 'tour.stepDataCompanyRole' | sqxTranslate }}</label> <option [ngValue]="'RoleEmployee'">{{ "tour.roleEmployee" | sqxTranslate }}</option>
<option [ngValue]="'RoleBusinessOwner'">{{ "tour.roleBusinessOwner" | sqxTranslate }}</option>
<select class="form-select" id="companyRole" formControlName="companyRole"> <option [ngValue]="'RoleProductManager'">{{ "tour.roleProductManager" | sqxTranslate }}</option>
<option [ngValue]="'RoleEmployee'">{{ 'tour.roleEmployee' | sqxTranslate }}</option> <option [ngValue]="'RoleContentCreator'">{{ "tour.roleContentCreator" | sqxTranslate }}</option>
<option [ngValue]="'RoleBusinessOwner'">{{ 'tour.roleBusinessOwner' | sqxTranslate }}</option> <option [ngValue]="'RoleSoftwareDeveloper'">{{ "tour.roleSoftwareDeveloper" | sqxTranslate }}</option>
<option [ngValue]="'RoleProductManager'">{{ 'tour.roleProductManager' | sqxTranslate }}</option> <option [ngValue]="'RoleBusinessAnalyst'">{{ "tour.roleBusinessAnalyst" | sqxTranslate }}</option>
<option [ngValue]="'RoleContentCreator'">{{ 'tour.roleContentCreator' | sqxTranslate }}</option> </select>
<option [ngValue]="'RoleSoftwareDeveloper'">{{ 'tour.roleSoftwareDeveloper' | sqxTranslate }}</option> </div>
<option [ngValue]="'RoleBusinessAnalyst'">{{ 'tour.roleBusinessAnalyst' | sqxTranslate }}</option> <div class="form-group">
</select> <label for="companySize">{{ "tour.stepDataCompanySize" | sqxTranslate }}</label>
</div> <select class="form-select" id="companySize" formControlName="companySize">
<option [ngValue]="'SizeSingle'">{{ "tour.sizeSingle" | sqxTranslate }}</option>
<div class="form-group"> <option [ngValue]="'SizeSmall'">{{ "tour.sizeSmall" | sqxTranslate }}</option>
<label for="companySize">{{ 'tour.stepDataCompanySize' | sqxTranslate }}</label> <option [ngValue]="'SizeMedium'">{{ "tour.sizeMedium" | sqxTranslate }}</option>
<option [ngValue]="'SizeLarge'">{{ "tour.sizeLarge" | sqxTranslate }}</option>
<select class="form-select" id="companySize" formControlName="companySize"> <option [ngValue]="'SizeVeryLarge'">{{ "tour.sizeVeryLarge" | sqxTranslate }}</option>
<option [ngValue]="'SizeSingle'">{{ 'tour.sizeSingle' | sqxTranslate }}</option> </select>
<option [ngValue]="'SizeSmall'">{{ 'tour.sizeSmall' | sqxTranslate }}</option> </div>
<option [ngValue]="'SizeMedium'">{{ 'tour.sizeMedium' | sqxTranslate }}</option> <div class="form-group">
<option [ngValue]="'SizeLarge'">{{ 'tour.sizeLarge' | sqxTranslate }}</option> <label for="project">{{ "tour.stepDataProject" | sqxTranslate }}</label>
<option [ngValue]="'SizeVeryLarge'">{{ 'tour.sizeVeryLarge' | sqxTranslate }}</option> <select class="form-select" id="project" formControlName="project">
</select> <option [ngValue]="'ProjectNewsMagazine'">{{ "tour.projectNewsMagazine" | sqxTranslate }}</option>
</div> <option [ngValue]="'ProjectPersonalBlog'">{{ "tour.projectPersonalBlog" | sqxTranslate }}</option>
<option [ngValue]="'ProjectSmallBusiness'">{{ "tour.projectSmallBusiness" | sqxTranslate }}</option>
<div class="form-group"> <option [ngValue]="'ProjectCommerce'">{{ "tour.projectCommerce" | sqxTranslate }}</option>
<label for="project">{{ 'tour.stepDataProject' | sqxTranslate }}</label> <option [ngValue]="'ProjectMobileApp'">{{ "tour.projectMobileApp" | sqxTranslate }}</option>
<option [ngValue]="'ProjectBackend'">{{ "tour.projectBackend" | sqxTranslate }}</option>
<select class="form-select" id="project" formControlName="project"> <option [ngValue]="'ProjectLearning'">{{ "tour.projectLearning" | sqxTranslate }}</option>
<option [ngValue]="'ProjectNewsMagazine'">{{ 'tour.projectNewsMagazine' | sqxTranslate }}</option> </select>
<option [ngValue]="'ProjectPersonalBlog'">{{ 'tour.projectPersonalBlog' | sqxTranslate }}</option> </div>
<option [ngValue]="'ProjectSmallBusiness'">{{ 'tour.projectSmallBusiness' | sqxTranslate }}</option> <button class="btn btn-success" type="submit">
<option [ngValue]="'ProjectCommerce'">{{ 'tour.projectCommerce' | sqxTranslate }}</option> {{ "tour.stepDataNext" | sqxTranslate }}
<option [ngValue]="'ProjectMobileApp'">{{ 'tour.projectMobileApp' | sqxTranslate }}</option> </button>
<option [ngValue]="'ProjectBackend'">{{ 'tour.projectBackend' | sqxTranslate }}</option> </form>
<option [ngValue]="'ProjectLearning'">{{ 'tour.projectLearning' | sqxTranslate }}</option> </div>
</select>
</div>
<button type="submit" class="btn btn-success">{{ 'tour.stepDataNext' | sqxTranslate }}</button>
</form>
</div> </div>
</div> }
<div class="onboarding-step" *ngIf="step === 2">
<img @fade class="header-left" src="./images/logo-white-small.png">
<div @slide class="onboarding-enter-leave text-center">
<h2>{{ 'tour.stepTourTitle' | sqxTranslate }}</h2>
<div [innerHTML]="'tour.stepTourText' | sqxTranslate | sqxMarkdown | sqxSafeHtml"></div> @if (step === 2) {
<div class="onboarding-step">
<div class="mt-4"> <img class="header-left" @fade src="./images/logo-white-small.png" />
<button (click)="start()" class="btn btn-success"> <div class="onboarding-enter-leave text-center" @slide>
{{ 'tour.startYes' | sqxTranslate }} <h2>{{ "tour.stepTourTitle" | sqxTranslate }}</h2>
</button> <div [innerHTML]="'tour.stepTourText' | sqxTranslate | sqxMarkdown | sqxSafeHtml"></div>
<div class="mt-4">
<button (click)="cancel()" class="btn btn-outline-secondary ms-2"> <button class="btn btn-success" (click)="start()">
{{ 'tour.startNo' | sqxTranslate }} {{ "tour.startYes" | sqxTranslate }}
</button> </button>
<button class="btn btn-outline-secondary ms-2" (click)="cancel()">
{{ "tour.startNo" | sqxTranslate }}
</button>
</div>
</div> </div>
</div> </div>
</div> }
</ng-container> </ng-container>
</sqx-modal-dialog> </sqx-modal-dialog>

3
frontend/src/app/features/apps/pages/onboarding-dialog.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 { NgIf } from '@angular/common';
import { Component, EventEmitter, Output } from '@angular/core'; import { Component, EventEmitter, Output } from '@angular/core';
import { FormBuilder, FormsModule, ReactiveFormsModule } from '@angular/forms'; import { FormBuilder, FormsModule, ReactiveFormsModule } from '@angular/forms';
import { fadeAnimation, MarkdownPipe, ModalDialogComponent, SafeHtmlPipe, slideAnimation, TourState, TranslatePipe, UsersService } from '@app/shared'; import { fadeAnimation, MarkdownPipe, ModalDialogComponent, SafeHtmlPipe, slideAnimation, TourState, TranslatePipe, UsersService } from '@app/shared';
@ -22,7 +22,6 @@ import { fadeAnimation, MarkdownPipe, ModalDialogComponent, SafeHtmlPipe, slideA
FormsModule, FormsModule,
MarkdownPipe, MarkdownPipe,
ModalDialogComponent, ModalDialogComponent,
NgIf,
ReactiveFormsModule, ReactiveFormsModule,
SafeHtmlPipe, SafeHtmlPipe,
TranslatePipe, TranslatePipe,

25
frontend/src/app/features/apps/pages/team.component.html

@ -1,26 +1,27 @@
<div class="team-header"> <div class="team-header">
<div class="row align-items-center"> <div class="row align-items-center">
<div class="col"> <div class="col">
<h3>{{team.name}}</h3> <h3>{{ team.name }}</h3>
</div> </div>
<div class="col-auto"> <div class="col-auto">
<a class="link" [routerLink]="['/app/teams', team.id]" sqxStopClick>{{ 'common.edit' | sqxTranslate }}</a> <a class="link" [routerLink]="['/app/teams', team.id]" sqxStopClick>{{ "common.edit" | sqxTranslate }}</a>
</div> </div>
<div class="col-auto"> <div class="col-auto">
<button type="button" class="btn btn-sm btn-text-secondary" (click)="dropdown.toggle()" sqxStopClick #buttonOptions> <button class="btn btn-sm btn-text-secondary" #buttonOptions (click)="dropdown.toggle()" sqxStopClick type="button">
<span class="hidden">{{ 'common.options' | sqxTranslate }}</span> <span class="hidden">{{ "common.options" | sqxTranslate }}</span>
<i class="icon-dots"></i> <i class="icon-dots"></i>
</button> </button>
<sqx-dropdown-menu *sqxModal="dropdown;closeAlways:true" [sqxAnchoredTo]="buttonOptions" scrollY="true"> <sqx-dropdown-menu scrollY="true" [sqxAnchoredTo]="buttonOptions" *sqxModal="dropdown; closeAlways: true">
<a class="dropdown-item dropdown-item-delete" <a
(sqxConfirmClick)="leave.emit(team)" class="dropdown-item dropdown-item-delete"
confirmTitle="i18n:teams.leaveConfirmTitle" confirmRememberKey="leaveApp"
confirmText="i18n:teams.leaveConfirmText" confirmText="i18n:teams.leaveConfirmText"
confirmRememberKey="leaveApp"> confirmTitle="i18n:teams.leaveConfirmTitle"
{{ 'teams.leave' | sqxTranslate }} (sqxConfirmClick)="leave.emit(team)">
{{ "teams.leave" | sqxTranslate }}
</a> </a>
</sqx-dropdown-menu> </sqx-dropdown-menu>
</div> </div>
</div> </div>
</div> </div>

27
frontend/src/app/features/assets/pages/asset-tag-dialog.component.html

@ -1,29 +1,32 @@
<form [formGroup]="editForm.form" (ngSubmit)="renameAssetTag()"> <form [formGroup]="editForm.form" (ngSubmit)="renameAssetTag()">
<sqx-modal-dialog (dialogClose)="emitClose()"> <sqx-modal-dialog (dialogClose)="emitClose()">
<ng-container title> <ng-container title>
{{ 'common.renameTag' | sqxTranslate }} {{ "common.renameTag" | sqxTranslate }}
</ng-container> </ng-container>
<ng-container content> <ng-container content>
<sqx-form-error [error]="editForm.error | async"></sqx-form-error> <sqx-form-error [error]="editForm.error | async"></sqx-form-error>
<div class="form-group"> <div class="form-group">
<label for="tagName">{{ 'common.name' | sqxTranslate }} <small class="hint">({{ 'common.requiredHint' | sqxTranslate }})</small></label> <label for="tagName">
{{ "common.name" | sqxTranslate }}
<small class="hint">({{ "common.requiredHint" | sqxTranslate }})</small>
</label>
<sqx-control-errors for="tagName"></sqx-control-errors> <sqx-control-errors for="tagName"></sqx-control-errors>
<input class="form-control" id="tagName" formControlName="tagName" autocomplete="off" sqxFocusOnInit> <input class="form-control" id="tagName" autocomplete="off" formControlName="tagName" sqxFocusOnInit />
</div> </div>
</ng-container> </ng-container>
<ng-container footer> <ng-container footer>
<button type="button" class="btn btn-text-secondary" (click)="emitClose()"> <button class="btn btn-text-secondary" (click)="emitClose()" type="button">
{{ 'common.cancel' | sqxTranslate }} {{ "common.cancel" | sqxTranslate }}
</button> </button>
<button type="submit" class="btn btn-success"> <button class="btn btn-success" type="submit">
{{ 'common.rename' | sqxTranslate }} {{ "common.rename" | sqxTranslate }}
</button> </button>
</ng-container> </ng-container>
</sqx-modal-dialog> </sqx-modal-dialog>
</form> </form>

44
frontend/src/app/features/assets/pages/asset-tags.component.html

@ -1,28 +1,32 @@
<div class="nav nav-light flex-column"> <div class="nav nav-light flex-column">
<div class="nav-item"> <div class="nav-item">
<a class="nav-link" (click)="tagsReset.emit()" [class.active]="isEmpty()"> <a class="nav-link" [class.active]="isEmpty()" (click)="tagsReset.emit()">
{{ 'common.tagsAll' | sqxTranslate }} {{ "common.tagsAll" | sqxTranslate }}
</a> </a>
</div> </div>
<div class="nav-item" *ngFor="let tag of tags; trackBy: trackByTag"> @for (tag of tags; track tag.name) {
<a class="nav-link" (click)="toggle.emit(tag.name)" [class.active]="isSelected(tag)"> <div class="nav-item">
<div class="row g-0"> <a class="nav-link" [class.active]="isSelected(tag)" (click)="toggle.emit(tag.name)">
<div class="col"> <div class="row g-0">
<span class="truncate">{{tag.name}}</span> <div class="col">
<span class="truncate">{{ tag.name }}</span>
</div>
<div class="col-auto">
<div class="badge badge-secondary rounded-pill">{{ tag.count }}</div>
@if (canRename) {
<a class="btn-sm btn-text-secondary btn-rename" (click)="renameTag(tag)" sqxStopClick>
<i class="icon-pencil"></i>
</a>
}
</div>
</div> </div>
<div class="col-auto"> </a>
<div class="badge badge-secondary rounded-pill">{{tag.count}}</div> </div>
}
<a class="btn-sm btn-text-secondary btn-rename" (click)="renameTag(tag)" *ngIf="canRename" sqxStopClick>
<i class="icon-pencil"></i>
</a>
</div>
</div>
</a>
</div>
</div> </div>
<sqx-asset-tag-dialog *sqxModal="tagRenameDialog" <sqx-asset-tag-dialog
(dialogClose)="tagRenameDialog.hide()" [tagName]="tagRenaming!.name"> (dialogClose)="tagRenameDialog.hide()"
</sqx-asset-tag-dialog> *sqxModal="tagRenameDialog"
[tagName]="tagRenaming!.name"></sqx-asset-tag-dialog>

8
frontend/src/app/features/assets/pages/asset-tags.component.ts

@ -7,7 +7,7 @@
/* eslint-disable @typescript-eslint/no-unnecessary-boolean-literal-compare */ /* eslint-disable @typescript-eslint/no-unnecessary-boolean-literal-compare */
import { NgFor, NgIf } from '@angular/common';
import { booleanAttribute, ChangeDetectionStrategy, Component, EventEmitter, Input, Output } from '@angular/core'; import { booleanAttribute, ChangeDetectionStrategy, Component, EventEmitter, Input, Output } from '@angular/core';
import { DialogModel, ModalDirective, StopClickDirective, TagItem, TagsSelected, TranslatePipe } from '@app/shared'; import { DialogModel, ModalDirective, StopClickDirective, TagItem, TagsSelected, TranslatePipe } from '@app/shared';
import { AssetTagDialogComponent } from './asset-tag-dialog.component'; import { AssetTagDialogComponent } from './asset-tag-dialog.component';
@ -21,8 +21,6 @@ import { AssetTagDialogComponent } from './asset-tag-dialog.component';
imports: [ imports: [
AssetTagDialogComponent, AssetTagDialogComponent,
ModalDirective, ModalDirective,
NgFor,
NgIf,
StopClickDirective, StopClickDirective,
TranslatePipe, TranslatePipe,
], ],
@ -58,8 +56,4 @@ export class AssetTagsComponent {
this.tagRenaming = tag; this.tagRenaming = tag;
this.tagRenameDialog.show(); this.tagRenameDialog.show();
} }
public trackByTag(_index: number, tag: TagItem) {
return tag.name;
}
} }

20
frontend/src/app/features/assets/pages/assets-filters-page.component.html

@ -1,20 +1,18 @@
<sqx-layout layout="right" titleText="i18n:common.filters" width="20" white="true" padding="true" overflow="true"> <sqx-layout layout="right" overflow="true" padding="true" titleText="i18n:common.filters" white="true" width="20">
<h3>{{ 'common.tags' | sqxTranslate }}</h3> <h3>{{ "common.tags" | sqxTranslate }}</h3>
<sqx-asset-tags <sqx-asset-tags
[canRename]="(assetsState.canRenameTag | async)!" [canRename]="(assetsState.canRenameTag | async)!"
[tags]="(assetsState.tags | async)!" [tags]="(assetsState.tags | async)!"
(tagsReset)="resetTags()" (tagsReset)="resetTags()"
[tagsSelected]="(assetsState.tagsSelected | async)!" [tagsSelected]="(assetsState.tagsSelected | async)!"
(toggle)="toggleTag($event)"> (toggle)="toggleTag($event)"></sqx-asset-tags>
</sqx-asset-tags>
<hr> <hr />
<sqx-shared-queries <sqx-shared-queries
[types]="'common.assets' | sqxTranslate"
[queryUsed]="assetsState.query | async"
[queries]="assetsQueries" [queries]="assetsQueries"
(search)="search($event)"> [queryUsed]="assetsState.query | async"
</sqx-shared-queries> (search)="search($event)"
</sqx-layout> [types]="'common.assets' | sqxTranslate"></sqx-shared-queries>
</sqx-layout>

4
frontend/src/app/features/assets/pages/assets-filters-page.component.ts

@ -47,8 +47,4 @@ export class AssetsFiltersPageComponent {
public resetTags() { public resetTags() {
this.assetsState.resetTags(); this.assetsState.resetTags();
} }
public trackByTag(_index: number, tag: { name: string }) {
return tag.name;
}
} }

106
frontend/src/app/features/assets/pages/assets-page.component.html

@ -1,48 +1,71 @@
<sqx-title message="i18n:assets.listPageTitle"></sqx-title> <sqx-title message="i18n:assets.listPageTitle"></sqx-title>
<sqx-layout layout="main" titleText="i18n:common.assets" titleIcon="assets"> <sqx-layout layout="main" titleIcon="assets" titleText="i18n:common.assets">
<ng-container menu> <ng-container menu>
<div class="row flex-nowrap flex-grow-1 gx-2"> <div class="row flex-nowrap flex-grow-1 gx-2">
<div class="col-auto offset-xl-2"> <div class="col-auto offset-xl-2">
<button type="button" class="btn btn-text-secondary" (click)="reload()" title="i18n:assets.refreshTooltip" shortcut="CTRL + B"> <button
<i class="icon-reset"></i> {{ 'common.refresh' | sqxTranslate }} class="btn btn-text-secondary"
(click)="reload()"
shortcut="CTRL + B"
title="i18n:assets.refreshTooltip"
type="button">
<i class="icon-reset"></i>
{{ "common.refresh" | sqxTranslate }}
</button> </button>
</div> </div>
<div class="col" style="width: 300px;"> <div class="col" style="width: 300px">
<div class="row g-0 search"> <div class="row g-0 search">
<div class="col-6"> <div class="col-6">
<sqx-tag-editor class="tags" placeholder="{{ 'assets.searchByTags' | sqxTranslate }}" <sqx-tag-editor
class="tags"
[itemsSource]="assetsState.tagsNames | async" [itemsSource]="assetsState.tagsNames | async"
[ngModel]="assetsState.selectedTagNames | async" [ngModel]="assetsState.selectedTagNames | async"
(ngModelChange)="selectTags($event)" (ngModelChange)="selectTags($event)"
placeholder="{{ 'assets.searchByTags' | sqxTranslate }}"
styleScrollable="true" styleScrollable="true"
undefinedWhenEmpty="false"> undefinedWhenEmpty="false"></sqx-tag-editor>
</sqx-tag-editor>
</div> </div>
<div class="col-6"> <div class="col-6">
<sqx-search-form formClass="form" placeholder="{{ 'assets.searchByName' | sqxTranslate }}" fieldExample="fileSize" <sqx-search-form
enableShortcut="true" enableShortcut="true"
fieldExample="fileSize"
formClass="form"
placeholder="{{ 'assets.searchByName' | sqxTranslate }}"
[queries]="listQueries" [queries]="listQueries"
[queriesTypes]="'common.assets' | sqxTranslate" [queriesTypes]="'common.assets' | sqxTranslate"
[query]="assetsState.query | async" [query]="assetsState.query | async"
(queryChange)="search($event)"> (queryChange)="search($event)"></sqx-search-form>
</sqx-search-form>
</div> </div>
</div> </div>
</div> </div>
<div class="col-auto"> <div class="col-auto">
<div class="btn-group"> <div class="btn-group">
<button type="button" class="btn btn-secondary btn-toggle" [class.btn-primary]="listMode" [disabled]="listMode" (click)="changeView(true)"> <button
class="btn btn-secondary btn-toggle"
[class.btn-primary]="listMode"
(click)="changeView(true)"
[disabled]="listMode"
type="button">
<i class="icon-list"></i> <i class="icon-list"></i>
</button> </button>
<button type="button" class="btn btn-secondary btn-toggle" [class.btn-primary]="!listMode" [disabled]="!listMode" (click)="changeView(false)"> <button
class="btn btn-secondary btn-toggle"
[class.btn-primary]="!listMode"
(click)="changeView(false)"
[disabled]="!listMode"
type="button">
<i class="icon-grid"></i> <i class="icon-grid"></i>
</button> </button>
</div> </div>
</div> </div>
<div class="col-auto"> <div class="col-auto">
<button type="button" class="btn btn-success" (click)="addAssetFolderDialog.show()" title="i18n:assets.createFolderTooltip" shortcut="CTRL + U"> <button
class="btn btn-success"
(click)="addAssetFolderDialog.show()"
shortcut="CTRL + U"
title="i18n:assets.createFolderTooltip"
type="button">
<i class="icon-create_new_folder"></i> <i class="icon-create_new_folder"></i>
</button> </button>
</div> </div>
@ -52,50 +75,53 @@
<ng-container> <ng-container>
<sqx-list-view [isLoading]="assetsState.isLoading | async"> <sqx-list-view [isLoading]="assetsState.isLoading | async">
<ng-container header> <ng-container header>
<sqx-asset-path [path]="assetsState.path | async" (navigate)="assetsState.navigate($event.id)"></sqx-asset-path> <sqx-asset-path (navigate)="assetsState.navigate($event.id)" [path]="assetsState.path | async"></sqx-asset-path>
</ng-container> </ng-container>
<div *ngIf="assetsState.path | async; let path"> @if (assetsState.path | async; as path) {
<sqx-assets-list <div>
[assetsState]="assetsState" <sqx-assets-list
(edit)="editStart($event)" [assetsState]="assetsState"
[isDisabled]="false" (edit)="editStart($event)"
[isListView]="listMode" [isDisabled]="false"
[showFolderIcon]="path.length === 0"> [isListView]="listMode"
</sqx-assets-list> [showFolderIcon]="path.length === 0"></sqx-assets-list>
</div> </div>
}
<ng-container footer> <ng-container footer>
<sqx-pager (loadTotal)="reloadTotal()" [paging]="assetsState.paging | async" (pagingChange)="assetsState.page($event)"></sqx-pager> <sqx-pager
(loadTotal)="reloadTotal()"
[paging]="assetsState.paging | async"
(pagingChange)="assetsState.page($event)"></sqx-pager>
</ng-container> </ng-container>
</sqx-list-view> </sqx-list-view>
</ng-container> </ng-container>
<ng-template sidebarMenu> <ng-template sidebarMenu>
<div class="panel-nav"> <div class="panel-nav">
<a class="panel-link" <a
class="panel-link"
queryParamsHandling="preserve"
replaceUrl="true" replaceUrl="true"
routerLink="filters" routerLink="filters"
routerLinkActive="active" routerLinkActive="active"
queryParamsHandling="preserve" sqxTourStep="filter"
title="i18n:common.filters" title="i18n:common.filters"
titlePosition="left" titlePosition="left">
sqxTourStep="filter">
<i class="icon-filter"></i> <i class="icon-filter"></i>
</a> </a>
</div> </div>
</ng-template> </ng-template>
</sqx-layout> </sqx-layout>
<router-outlet></router-outlet> <router-outlet></router-outlet>
<sqx-asset-folder-dialog *sqxModal="addAssetFolderDialog" <sqx-asset-folder-dialog (dialogClose)="addAssetFolderDialog.hide()" *sqxModal="addAssetFolderDialog"></sqx-asset-folder-dialog>
(dialogClose)="addAssetFolderDialog.hide()">
</sqx-asset-folder-dialog>
<sqx-asset-dialog *sqxModal="editAsset;isDialog:true" <sqx-asset-dialog
[asset]="editAsset!" [asset]="editAsset!"
(assetUpdated)="replaceAsset($event)"
(assetReplaced)="replaceAsset($event)" (assetReplaced)="replaceAsset($event)"
(dialogClose)="editDone()"> (assetUpdated)="replaceAsset($event)"
</sqx-asset-dialog> (dialogClose)="editDone()"
*sqxModal="editAsset; isDialog: true"></sqx-asset-dialog>

3
frontend/src/app/features/assets/pages/assets-page.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 { AsyncPipe, NgIf } from '@angular/common'; import { AsyncPipe } from '@angular/common';
import { Component, OnInit } from '@angular/core'; import { Component, OnInit } from '@angular/core';
import { FormsModule } from '@angular/forms'; import { FormsModule } from '@angular/forms';
import { RouterLink, RouterLinkActive, RouterOutlet } from '@angular/router'; import { RouterLink, RouterLinkActive, RouterOutlet } from '@angular/router';
@ -29,7 +29,6 @@ import { AssetDialogComponent, AssetDto, AssetFolderDialogComponent, AssetPathCo
LayoutComponent, LayoutComponent,
ListViewComponent, ListViewComponent,
ModalDirective, ModalDirective,
NgIf,
PagerComponent, PagerComponent,
RouterLink, RouterLink,
RouterLinkActive, RouterLinkActive,

181
frontend/src/app/features/content/pages/calendar/calendar-page.component.html

@ -1,19 +1,19 @@
<sqx-title message="i18n:contents.calendar"></sqx-title> <sqx-title message="i18n:contents.calendar"></sqx-title>
<sqx-layout layout="main" titleText="i18n:contents.calendar" hideSidebar="true"> <sqx-layout hideSidebar="true" layout="main" titleText="i18n:contents.calendar">
<ng-container menu> <ng-container menu>
{{title}} {{ title }}
<select class="form-select ms-4" [ngModel]="view" (ngModelChange)="changeView($event)" [disabled]="isLoading"> <select class="form-select ms-4" [disabled]="isLoading" [ngModel]="view" (ngModelChange)="changeView($event)">
<option ngValue="day">{{ 'common.daily' | sqxTranslate }}</option> <option ngValue="day">{{ "common.daily" | sqxTranslate }}</option>
<option ngValue="week">{{ 'common.weekly' | sqxTranslate }}</option> <option ngValue="week">{{ "common.weekly" | sqxTranslate }}</option>
<option ngValue="month">{{ 'common.monthly' | sqxTranslate }}</option> <option ngValue="month">{{ "common.monthly" | sqxTranslate }}</option>
</select> </select>
<button type="button" class="btn btn-text-secondary btn-navigate ms-2" (click)="goPrev()" [disabled]="isLoading"> <button class="btn btn-text-secondary btn-navigate ms-2" (click)="goPrev()" [disabled]="isLoading" type="button">
<i class="icon-caret-left"></i> <i class="icon-caret-left"></i>
</button> </button>
<button type="button" class="btn btn-text-secondary btn-navigate ms-2" (click)="goNext()" [disabled]="isLoading"> <button class="btn btn-text-secondary btn-navigate ms-2" (click)="goNext()" [disabled]="isLoading" type="button">
<i class="icon-caret-right"></i> <i class="icon-caret-right"></i>
</button> </button>
</ng-container> </ng-container>
@ -23,106 +23,93 @@
</ng-container> </ng-container>
</sqx-layout> </sqx-layout>
<sqx-modal-dialog *sqxModal="contentDialog" (dialogClose)="contentDialog.hide()"> <sqx-modal-dialog (dialogClose)="contentDialog.hide()" *sqxModal="contentDialog">
<ng-container title> <ng-container title>
{{ 'common.content' | sqxTranslate }} {{ "common.content" | sqxTranslate }}
</ng-container> </ng-container>
<ng-container content> <ng-container content>
<div *ngIf="contentSelected && contentSelected.scheduleJob"> @if (contentSelected && contentSelected.scheduleJob) {
<div class="form-group row"> <div>
<label class="col-4 col-form-label">{{ 'common.id' | sqxTranslate }}</label> <div class="form-group row">
<label class="col-4 col-form-label">{{ "common.id" | sqxTranslate }}</label>
<div class="col-8"> <div class="col-8">
<div class="input-group"> <div class="input-group">
<input readonly class="form-control" name="id" id="id" value="{{contentSelected.id}}" #inputId> <input class="form-control" id="id" #inputId name="id" readonly value="{{ contentSelected.id }}" />
<button class="btn btn-outline-secondary" [sqxCopy]="inputId" type="button">
<button type="button" class="btn btn-outline-secondary" [sqxCopy]="inputId"> <i class="icon-copy"></i>
<i class="icon-copy"></i> </button>
</button> </div>
</div> </div>
</div> </div>
</div> <div class="form-group form-group-aligned row">
<label class="col-4 col-form-label">{{ "common.content" | sqxTranslate }}</label>
<div class="form-group form-group-aligned row"> <div class="col-8">
<label class="col-4 col-form-label">{{ 'common.content' | sqxTranslate }}</label> <a class="truncate" [routerLink]="['../', contentSelected.schemaName, contentSelected.id]">
{{ createContentName(contentSelected) }}
<div class="col-8"> </a>
<a class="truncate" [routerLink]="['../', contentSelected.schemaName, contentSelected.id]"> </div>
{{createContentName(contentSelected)}}
</a>
</div>
</div>
<div class="form-group form-group-aligned row">
<label class="col-4 col-form-label">{{ 'common.schema' | sqxTranslate }}</label>
<div class="col-8">
<a class="truncate" [routerLink]="['../', contentSelected.schemaName]">
{{contentSelected.schemaDisplayName}}
</a>
</div> </div>
</div> <div class="form-group form-group-aligned row">
<label class="col-4 col-form-label">{{ "common.schema" | sqxTranslate }}</label>
<div class="form-group form-group-aligned row"> <div class="col-8">
<label class="col-4 col-form-label">{{ 'common.status' | sqxTranslate }}</label> <a class="truncate" [routerLink]="['../', contentSelected.schemaName]">
{{ contentSelected.schemaDisplayName }}
<div class="col-8"> </a>
<sqx-content-status </div>
layout="text"
[status]="contentSelected.status"
[statusColor]="contentSelected.statusColor"
small="true">
</sqx-content-status>
</div> </div>
</div> <div class="form-group form-group-aligned row">
<label class="col-4 col-form-label">{{ "common.status" | sqxTranslate }}</label>
<hr /> <div class="col-8">
<sqx-content-status
<div class="form-group form-group-aligned row"> layout="text"
<label class="col-4 col-form-label">{{ 'contents.scheduledToLabel' | sqxTranslate }}</label> small="true"
[status]="contentSelected.status"
<div class="col-8"> [statusColor]="contentSelected.statusColor"></sqx-content-status>
<sqx-content-status </div>
layout="text"
[status]="contentSelected.scheduleJob.status"
[statusColor]="contentSelected.scheduleJob.color"
small="true">
</sqx-content-status>
</div> </div>
</div> <hr />
<div class="form-group form-group-aligned row">
<div class="form-group form-group-aligned row"> <label class="col-4 col-form-label">{{ "contents.scheduledToLabel" | sqxTranslate }}</label>
<label class="col-4 col-form-label">{{ 'contents.scheduledAt' | sqxTranslate }}</label> <div class="col-8">
<sqx-content-status
<div class="col-8"> layout="text"
{{contentSelected.scheduleJob.dueTime | sqxFullDateTime}} small="true"
[status]="contentSelected.scheduleJob.status"
[statusColor]="contentSelected.scheduleJob.color"></sqx-content-status>
</div>
</div> </div>
</div> <div class="form-group form-group-aligned row">
<label class="col-4 col-form-label">{{ "contents.scheduledAt" | sqxTranslate }}</label>
<div class="form-group form-group-aligned row"> <div class="col-8">
<label class="col-4 col-form-label">{{ 'contents.scheduledBy' | sqxTranslate }}</label> {{ contentSelected.scheduleJob.dueTime | sqxFullDateTime }}
</div>
<div class="col-8">
<img class="user-picture" [src]="contentSelected.scheduleJob.scheduledBy | sqxUserPictureRef"> {{contentSelected.scheduleJob.scheduledBy | sqxUserNameRef}}
</div> </div>
</div> <div class="form-group form-group-aligned row">
<label class="col-4 col-form-label">{{ "contents.scheduledBy" | sqxTranslate }}</label>
<ng-container *ngIf="contentSelected.canCancelStatus"> <div class="col-8">
<hr /> <img class="user-picture" [src]="contentSelected.scheduleJob.scheduledBy | sqxUserPictureRef" />
{{ contentSelected.scheduleJob.scheduledBy | sqxUserNameRef }}
<div class="row">
<div class="col-8 offset-4">
<button type="button" class="btn btn-outline-danger" [class.disabled]="!contentSelected.canCancelStatus"
(sqxConfirmClick)="cancelStatus()"
confirmTitle="i18n:contents.cancelStatusConfirmTitle"
confirmText="i18n:contents.cancelStatusConfirmText"
confirmRememberKey="cancelStatus">
{{ 'contents.cancelStatus' | sqxTranslate }}
</button>
</div> </div>
</div> </div>
</ng-container> @if (contentSelected.canCancelStatus) {
</div> <hr />
<div class="row">
<div class="col-8 offset-4">
<button
class="btn btn-outline-danger"
[class.disabled]="!contentSelected.canCancelStatus"
confirmRememberKey="cancelStatus"
confirmText="i18n:contents.cancelStatusConfirmText"
confirmTitle="i18n:contents.cancelStatusConfirmTitle"
(sqxConfirmClick)="cancelStatus()"
type="button">
{{ "contents.cancelStatus" | sqxTranslate }}
</button>
</div>
</div>
}
</div>
}
</ng-container> </ng-container>
</sqx-modal-dialog> </sqx-modal-dialog>

3
frontend/src/app/features/content/pages/calendar/calendar-page.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 { NgIf } from '@angular/common';
import { AfterViewInit, ChangeDetectorRef, Component, ElementRef, HostListener, OnDestroy, OnInit, ViewChild } from '@angular/core'; import { AfterViewInit, ChangeDetectorRef, Component, ElementRef, HostListener, OnDestroy, OnInit, ViewChild } from '@angular/core';
import { FormsModule } from '@angular/forms'; import { FormsModule } from '@angular/forms';
import { RouterLink } from '@angular/router'; import { RouterLink } from '@angular/router';
@ -29,7 +29,6 @@ type ViewMode = 'day' | 'week' | 'month';
LayoutComponent, LayoutComponent,
ModalDialogComponent, ModalDialogComponent,
ModalDirective, ModalDirective,
NgIf,
RouterLink, RouterLink,
TitleComponent, TitleComponent,
TooltipDirective, TooltipDirective,

4
frontend/src/app/features/content/pages/comments/comments-page.component.html

@ -1,3 +1,3 @@
<sqx-layout layout="right" titleText="i18n:comments.title" width="20" white="true"> <sqx-layout layout="right" titleText="i18n:comments.title" white="true" width="20">
<sqx-comments [commentsId]="commentsId | async"></sqx-comments> <sqx-comments [commentsId]="commentsId | async"></sqx-comments>
</sqx-layout> </sqx-layout>

22
frontend/src/app/features/content/pages/content/content-event.component.html

@ -1,21 +1,21 @@
<div class="event row g-0"> <div class="event row g-0">
<div class="col-auto"> <div class="col-auto">
<img class="user-picture" title="{{event.actor | sqxUserNameRef}}" [src]="event.actor | sqxUserPictureRef"> <img class="user-picture" [src]="event.actor | sqxUserPictureRef" title="{{ event.actor | sqxUserNameRef }}" />
</div> </div>
<div class="col ps-2 event-right"> <div class="col ps-2 event-right">
<div class="event-message"> <div class="event-message">
<span class="event-actor user-ref me-1" [title]="event.actor | sqxUserNameRef:null">{{event.actor | sqxUserNameRef:null}}</span> <span class="event-actor user-ref me-1" [title]="event.actor | sqxUserNameRef: null">
{{ event.actor | sqxUserNameRef: null }}
</span>
<span [innerHTML]="event | sqxHistoryMessage"></span> <span [innerHTML]="event | sqxHistoryMessage"></span>
</div> </div>
<div class="event-created">{{event.created | sqxFromNow}}</div> <div class="event-created">{{ event.created | sqxFromNow }}</div>
<ng-container *ngIf="canLoadOrCompare"> @if (canLoadOrCompare) {
<a class="event-load force" (click)="dataLoad.emit()">{{ 'contents.loadContent' | sqxTranslate }}</a> <a class="event-load force" (click)="dataLoad.emit()">{{ "contents.loadContent" | sqxTranslate }}</a>
&middot;
&middot; <a class="event-load force" (click)="dataCompare.emit()">{{ "contents.versionCompare" | sqxTranslate }}</a>
}
<a class="event-load force" (click)="dataCompare.emit()">{{ 'contents.versionCompare' | sqxTranslate }}</a>
</ng-container>
</div> </div>
</div> </div>

5
frontend/src/app/features/content/pages/content/content-event.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 { NgIf } from '@angular/common';
import { ChangeDetectionStrategy, Component, EventEmitter, Input, Output } from '@angular/core'; import { ChangeDetectionStrategy, Component, EventEmitter, Input, Output } from '@angular/core';
import { ContentDto, FromNowPipe, HistoryEventDto, HistoryMessagePipe, TooltipDirective, TranslatePipe, TypedSimpleChanges, UserNameRefPipe, UserPictureRefPipe } from '@app/shared'; import { ContentDto, FromNowPipe, HistoryEventDto, HistoryMessagePipe, TooltipDirective, TranslatePipe, TypedSimpleChanges, UserNameRefPipe, UserPictureRefPipe } from '@app/shared';
@ -18,7 +18,6 @@ import { ContentDto, FromNowPipe, HistoryEventDto, HistoryMessagePipe, TooltipDi
imports: [ imports: [
FromNowPipe, FromNowPipe,
HistoryMessagePipe, HistoryMessagePipe,
NgIf,
TooltipDirective, TooltipDirective,
TranslatePipe, TranslatePipe,
UserNameRefPipe, UserNameRefPipe,
@ -44,7 +43,7 @@ export class ContentEventComponent {
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') &&
!this.event.version.eq(this.content.version); !this.event.version.eq(this.content.version);
} }
} }

237
frontend/src/app/features/content/pages/content/content-history-page.component.html

@ -1,141 +1,154 @@
<sqx-layout layout="right" titleText="i18n:common.workflow" width="20" white="true" overflow="true" padding="true"> <sqx-layout layout="right" overflow="true" padding="true" titleText="i18n:common.workflow" white="true" width="20">
<ng-container> <ng-container>
<div class="section mb-2"> <div class="section mb-2">
<label for="id">{{ 'common.id' | sqxTranslate }}</label> <label for="id">{{ "common.id" | sqxTranslate }}</label>
<div class="input-group"> <div class="input-group">
<input readonly class="form-control" name="id" id="id" value="{{content.id}}" #inputId> <input class="form-control" id="id" #inputId name="id" readonly value="{{ content.id }}" />
<button type="button" class="btn btn-outline-secondary" [sqxCopy]="inputId"> <button class="btn btn-outline-secondary" [sqxCopy]="inputId" type="button">
<i class="icon-copy"></i> <i class="icon-copy"></i>
</button> </button>
</div> </div>
</div> </div>
<div class="section mb-4"> <div class="section mb-4">
<label for="version">{{ 'common.version' | sqxTranslate }}</label>: <span id="version">{{content.version}}</span> <label for="version">{{ "common.version" | sqxTranslate }}</label>: <span id="version">{{ content.version }}</span>
</div> </div>
<div class="section mb-4" *ngIf="content.canDraftCreate || content.canDraftDelete"> @if (content.canDraftCreate || content.canDraftDelete) {
<ng-container *ngIf="!content.newStatus; else newVersion"> <div class="section mb-4">
<button class="btn btn-success btn-block" (click)="createDraft()"> @if (!content.newStatus) {
{{ 'contents.draftNew' | sqxTranslate }} <button class="btn btn-success btn-block" (click)="createDraft()">
</button> {{ "contents.draftNew" | sqxTranslate }}
</ng-container> </button>
} @else {
<ng-template #newVersion> <label>{{ "contents.draftStatus" | sqxTranslate }}</label>
<label>{{ 'contents.draftStatus' | sqxTranslate }}</label> <button
class="btn btn-outline-secondary btn-block btn-status"
<button type="button" class="btn btn-outline-secondary btn-block btn-status" (click)="dropdownNew.toggle()" #buttonOptions sqxTourStep="status"> #buttonOptions
<sqx-content-status (click)="dropdownNew.toggle()"
layout="multiline" sqxTourStep="status"
[status]="content.newStatus!" type="button">
[statusColor]="content.newStatusColor!" <sqx-content-status
[scheduled]="content.scheduleJob"> layout="multiline"
</sqx-content-status> [scheduled]="content.scheduleJob"
</button> [status]="content.newStatus!"
[statusColor]="content.newStatusColor!"></sqx-content-status>
<sqx-dropdown-menu *sqxModal="dropdownNew;closeAlways:true" [sqxAnchoredTo]="buttonOptions" scrollY="true"> </button>
<ng-container *ngIf="content.statusUpdates.length > 0"> <sqx-dropdown-menu scrollY="true" [sqxAnchoredTo]="buttonOptions" *sqxModal="dropdownNew; closeAlways: true">
<a class="dropdown-item" *ngFor="let info of content.statusUpdates" (click)="changeStatus(info.status)"> @if (content.statusUpdates.length > 0) {
{{ 'common.statusChangeTo' | sqxTranslate }} <i class="icon-circle icon-sm" [style.color]="info.color"></i> {{info.status}} @for (info of content.statusUpdates; track info) {
<a class="dropdown-item" (click)="changeStatus(info.status)">
{{ "common.statusChangeTo" | sqxTranslate }}
<i class="icon-circle icon-sm" [style.color]="info.color"></i>
{{ info.status }}
</a>
}
<div class="dropdown-divider"></div>
}
<a
class="dropdown-item dropdown-item-delete"
[class.disabled]="!content.canDraftDelete"
confirmRememberKey="deleteDraft"
confirmText="i18n:contents.deleteVersionConfirmText"
confirmTitle="i18n:contents.deleteConfirmTitle"
(sqxConfirmClick)="deleteDraft()">
{{ "contents.versionDelete" | sqxTranslate }}
</a> </a>
<div class="dropdown-divider"></div> <div class="dropdown-divider"></div>
</ng-container> <a
class="dropdown-item dropdown-item-delete"
<a class="dropdown-item dropdown-item-delete" [class.disabled]="!content.canDraftDelete" [class.disabled]="!content.canDelete"
(sqxConfirmClick)="deleteDraft()" confirmRememberKey="deleteContent"
confirmTitle="i18n:contents.deleteConfirmTitle" confirmText="i18n:contents.deleteConfirmText"
confirmText="i18n:contents.deleteVersionConfirmText" confirmTitle="i18n:contents.deleteConfirmTitle"
confirmRememberKey="deleteDraft"> (sqxConfirmClick)="delete()">
{{ 'contents.versionDelete' | sqxTranslate }} {{ "common.delete" | sqxTranslate }}
</a> </a>
</sqx-dropdown-menu>
<div class="dropdown-divider"></div> }
</div>
<a class="dropdown-item dropdown-item-delete" [class.disabled]="!content.canDelete" }
(sqxConfirmClick)="delete()"
confirmTitle="i18n:contents.deleteConfirmTitle"
confirmText="i18n:contents.deleteConfirmText"
confirmRememberKey="deleteContent">
{{ 'common.delete' | sqxTranslate }}
</a>
</sqx-dropdown-menu>
</ng-template>
</div>
<div class="section"> <div class="section">
<label>{{ 'contents.currentStatusLabel' | sqxTranslate }}</label> <label>{{ "contents.currentStatusLabel" | sqxTranslate }}</label>
<div *ngIf="!content.newStatus; else newStatusOld"> @if (!content.newStatus) {
<button type="button" class="btn btn-outline-secondary btn-block btn-status" (click)="dropdown.toggle()" #buttonOptions sqxTourStep="status"> <div>
<button
class="btn btn-outline-secondary btn-block btn-status"
#buttonOptions
(click)="dropdown.toggle()"
sqxTourStep="status"
type="button">
<sqx-content-status
layout="multiline"
[scheduled]="content.scheduleJob"
small="true"
[status]="content.status"
[statusColor]="content.statusColor"></sqx-content-status>
</button>
<sqx-dropdown-menu scrollY="true" [sqxAnchoredTo]="buttonOptions" *sqxModal="dropdown; closeAlways: true">
@if (content.statusUpdates.length > 0) {
@for (info of content.statusUpdates; track info) {
<a class="dropdown-item" (click)="changeStatus(info.status)">
{{ "common.statusChangeTo" | sqxTranslate }}
<sqx-content-status
layout="text"
small="true"
[status]="info.status"
[statusColor]="info.color"></sqx-content-status>
</a>
}
<div class="dropdown-divider"></div>
}
<a
class="dropdown-item dropdown-item-delete"
[class.disabled]="!content.canCancelStatus"
confirmRememberKey="cancelStatus"
confirmText="i18n:contents.cancelStatusConfirmText"
confirmTitle="i18n:contents.cancelStatusConfirmTitle"
(sqxConfirmClick)="cancelStatus()">
{{ "contents.cancelStatus" | sqxTranslate }}
</a>
<div class="dropdown-divider"></div>
<a
class="dropdown-item dropdown-item-delete"
[class.disabled]="!content.canDelete"
confirmRememberKey="deleteContent"
confirmText="i18n:contents.deleteConfirmText"
confirmTitle="i18n:contents.deleteConfirmTitle"
(sqxConfirmClick)="delete()">
{{ "common.delete" | sqxTranslate }}
</a>
</sqx-dropdown-menu>
</div>
} @else {
<button class="btn btn-outline-secondary btn-block btn-status" type="button">
<sqx-content-status <sqx-content-status
layout="multiline" layout="multiline"
[status]="content.status" [status]="content.status"
[statusColor]="content.statusColor" [statusColor]="content.statusColor"></sqx-content-status>
[scheduled]="content.scheduleJob"
small="true">
</sqx-content-status>
</button>
<sqx-dropdown-menu *sqxModal="dropdown;closeAlways:true" [sqxAnchoredTo]="buttonOptions" scrollY="true">
<ng-container *ngIf="content.statusUpdates.length > 0">
<a class="dropdown-item" *ngFor="let info of content.statusUpdates" (click)="changeStatus(info.status)">
{{ 'common.statusChangeTo' | sqxTranslate }}
<sqx-content-status
layout="text"
[status]="info.status"
[statusColor]="info.color"
small="true">
</sqx-content-status>
</a>
<div class="dropdown-divider"></div>
</ng-container>
<a class="dropdown-item dropdown-item-delete" [class.disabled]="!content.canCancelStatus"
(sqxConfirmClick)="cancelStatus()"
confirmTitle="i18n:contents.cancelStatusConfirmTitle"
confirmText="i18n:contents.cancelStatusConfirmText"
confirmRememberKey="cancelStatus">
{{ 'contents.cancelStatus' | sqxTranslate }}
</a>
<div class="dropdown-divider"></div>
<a class="dropdown-item dropdown-item-delete" [class.disabled]="!content.canDelete"
(sqxConfirmClick)="delete()"
confirmTitle="i18n:contents.deleteConfirmTitle"
confirmText="i18n:contents.deleteConfirmText"
confirmRememberKey="deleteContent">
{{ 'common.delete' | sqxTranslate }}
</a>
</sqx-dropdown-menu>
</div>
<ng-template #newStatusOld>
<button type="button" class="btn btn-outline-secondary btn-block btn-status">
<sqx-content-status [status]="content.status" [statusColor]="content.statusColor" layout="multiline"></sqx-content-status>
</button> </button>
</ng-template> }
<sqx-form-hint marginTop="1"> <sqx-form-hint marginTop="1">
{{ 'contents.lastUpdatedLabel' | sqxTranslate }}: {{content.lastModified | sqxFromNow}} {{ "contents.lastUpdatedLabel" | sqxTranslate }}: {{ content.lastModified | sqxFromNow }}
</sqx-form-hint> </sqx-form-hint>
</div> </div>
<div class="section"> <div class="section">
<h3 class="bordered">{{ 'common.history' | sqxTranslate }}</h3> <h3 class="bordered">{{ "common.history" | sqxTranslate }}</h3>
<sqx-content-event *ngFor="let event of contentEvents | async; trackBy: trackByEvent" @for (event of contentEvents | async; track event.eventId) {
[content]="content" <sqx-content-event
[event]="event" [content]="content"
(dataLoad)="loadVersion(event)" (dataCompare)="compareVersion(event)"
(dataCompare)="compareVersion(event)"> (dataLoad)="loadVersion(event)"
</sqx-content-event> [event]="event"></sqx-content-event>
}
</div> </div>
</ng-container> </ng-container>
</sqx-layout> </sqx-layout>
<sqx-due-time-selector [disabled]="disableScheduler" #dueTimeSelector></sqx-due-time-selector> <sqx-due-time-selector #dueTimeSelector [disabled]="disableScheduler"></sqx-due-time-selector>

8
frontend/src/app/features/content/pages/content/content-history-page.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 { AsyncPipe, NgFor, NgIf } from '@angular/common'; import { AsyncPipe } from '@angular/common';
import { Component, OnInit, ViewChild } from '@angular/core'; import { Component, OnInit, ViewChild } from '@angular/core';
import { Observable, timer } from 'rxjs'; import { Observable, timer } from 'rxjs';
import { map } from 'rxjs/operators'; import { map } from 'rxjs/operators';
@ -32,8 +32,6 @@ import { ContentPageComponent } from './content-page.component';
LayoutComponent, LayoutComponent,
ModalDirective, ModalDirective,
ModalPlacementDirective, ModalPlacementDirective,
NgFor,
NgIf,
TourStepDirective, TourStepDirective,
TranslatePipe, TranslatePipe,
], ],
@ -112,8 +110,4 @@ export class ContentHistoryPageComponent implements OnInit {
public compareVersion(event: HistoryEventDto) { public compareVersion(event: HistoryEventDto) {
this.contentPage.loadVersion(event.version, true); this.contentPage.loadVersion(event.version, true);
} }
public trackByEvent(_index: number, event: HistoryEventDto) {
return event.eventId;
}
} }

308
frontend/src/app/features/content/pages/content/content-page.component.html

@ -1,218 +1,230 @@
<sqx-title [message]="schema.displayName" [url]="['..']"></sqx-title> <sqx-title [message]="schema.displayName" [url]="['..']"></sqx-title>
<form [formGroup]="contentForm.form" (ngSubmit)="saveAndPublish()"> <form [formGroup]="contentForm.form" (ngSubmit)="saveAndPublish()">
<sqx-layout layout="main" [hideSidebar]="!content"> <sqx-layout [hideSidebar]="!content" layout="main">
<ng-container title> <ng-container title>
<div class="d-flex align-items-center"> <div class="d-flex align-items-center">
<a class="btn btn-text-secondary" aria-labelledby="content-back" (click)="back()" *ngIf="schema.type !== 'Singleton'"> @if (schema.type !== "Singleton") {
<span id="content-back" hidden>{{ 'common.back' | sqxTranslate }}</span> <a class="btn btn-text-secondary" aria-labelledby="content-back" (click)="back()">
<i class="icon-angle-left"></i> <span id="content-back" hidden>{{ "common.back" | sqxTranslate }}</span>
</a> <i class="icon-angle-left"></i>
</a>
<ng-container *ngIf="content; else noContentTitle"> }
@if (content) {
<sqx-title message="i18n:contents.editPageTitle"></sqx-title> <sqx-title message="i18n:contents.editPageTitle"></sqx-title>
</ng-container> } @else {
<h3>{{ "contents.createTitle" | sqxTranslate }}</h3>
<ng-template #noContentTitle>
<h3>{{ 'contents.createTitle' | sqxTranslate }}</h3>
<sqx-title message="i18n:contents.createPageTitle"></sqx-title> <sqx-title message="i18n:contents.createPageTitle"></sqx-title>
</ng-template> }
<ul class="nav nav-tabs2" *ngIf="content && contentTab | async; let tab"> @if (content && contentTab | async; as tab) {
<li class="nav-item"> <ul class="nav nav-tabs2">
<a class="nav-link" [routerLink]="[]" [queryParams]="{ tab: 'editor' }" [class.active]="tab === 'editor'"> <li class="nav-item">
{{ 'contents.contentTab.editor' | sqxTranslate }} <a class="nav-link" [class.active]="tab === 'editor'" [queryParams]="{ tab: 'editor' }" [routerLink]="[]">
</a> {{ "contents.contentTab.editor" | sqxTranslate }}
</li> </a>
<li> </li>
<a class="nav-link" [routerLink]="[]" [queryParams]="{ tab: 'references' }" [class.active]="tab === 'references'"> <li>
{{ 'contents.contentTab.references' | sqxTranslate }} <a
</a> class="nav-link"
</li> [class.active]="tab === 'references'"
<li> [queryParams]="{ tab: 'references' }"
<a class="nav-link" [routerLink]="[]" [queryParams]="{ tab: 'referencing' }" [class.active]="tab === 'referencing'"> [routerLink]="[]">
{{ 'contents.contentTab.referencing' | sqxTranslate }} {{ "contents.contentTab.references" | sqxTranslate }}
</a> </a>
</li> </li>
<li> <li>
<a class="nav-link" [routerLink]="[]" [queryParams]="{ tab: 'inspect' }" [class.active]="tab === 'inspect'"> <a
{{ 'contents.contentTab.inspect' | sqxTranslate }} class="nav-link"
</a> [class.active]="tab === 'referencing'"
</li> [queryParams]="{ tab: 'referencing' }"
<li *ngIf="schema.properties.contentEditorUrl"> [routerLink]="[]">
<a class="nav-link" [routerLink]="[]" [queryParams]="{ tab: 'extension' }" [class.active]="tab === 'extension'"> {{ "contents.contentTab.referencing" | sqxTranslate }}
{{ 'common.extension' | sqxTranslate }} </a>
</a> </li>
</li> <li>
</ul> <a class="nav-link" [class.active]="tab === 'inspect'" [queryParams]="{ tab: 'inspect' }" [routerLink]="[]">
{{ "contents.contentTab.inspect" | sqxTranslate }}
</a>
</li>
@if (schema.properties.contentEditorUrl) {
<li>
<a
class="nav-link"
[class.active]="tab === 'extension'"
[queryParams]="{ tab: 'extension' }"
[routerLink]="[]">
{{ "common.extension" | sqxTranslate }}
</a>
</li>
}
</ul>
}
</div> </div>
</ng-container> </ng-container>
<ng-container menu> <ng-container menu>
<div class="menu"> <div class="menu">
<ng-container *ngIf="content; else noContentMenu"> @if (content) {
<sqx-watching-users></sqx-watching-users> <sqx-watching-users></sqx-watching-users>
<sqx-notifo topic="apps/{{ contentsState.appId }}/schemas/{{ schema.id }}/contents/{{ content.id }}"></sqx-notifo>
<sqx-notifo topic="apps/{{contentsState.appId}}/schemas/{{schema.id}}/contents/{{content.id}}"></sqx-notifo> <sqx-language-selector
class="languages-buttons"
<sqx-language-selector class="languages-buttons" [language]="language"
(languageChange)="changeLanguage($event)" (languageChange)="changeLanguage($event)"
[language]="language"
[languages]="languages" [languages]="languages"
[percents]="contentForm.translationStatus | async"> [percents]="contentForm.translationStatus | async"></sqx-language-selector>
</sqx-language-selector> @if (content.canDelete) {
<button class="btn btn-outline-secondary ms-2" #buttonOptions (click)="dropdown.toggle()" type="button">
<ng-container *ngIf="content?.canDelete"> <span class="hidden">{{ "common.options" | sqxTranslate }}</span>
<button type="button" class="btn btn-outline-secondary ms-2" (click)="dropdown.toggle()" #buttonOptions>
<span class="hidden">{{ 'common.options' | sqxTranslate }}</span>
<i class="icon-dots"></i> <i class="icon-dots"></i>
</button> </button>
<sqx-dropdown-menu scrollY="true" [sqxAnchoredTo]="buttonOptions" *sqxModal="dropdown; closeAlways: true">
<sqx-dropdown-menu *sqxModal="dropdown;closeAlways:true" [sqxAnchoredTo]="buttonOptions" scrollY="true"> <a
<a class="dropdown-item dropdown-item-delete" class="dropdown-item dropdown-item-delete"
(sqxConfirmClick)="delete()" confirmRememberKey="deleteContent"
confirmTitle="i18n:contents.deleteConfirmTitle"
confirmText="i18n:contents.deleteConfirmText" confirmText="i18n:contents.deleteConfirmText"
confirmRememberKey="deleteContent"> confirmTitle="i18n:contents.deleteConfirmTitle"
{{ 'common.delete' | sqxTranslate }} (sqxConfirmClick)="delete()">
{{ "common.delete" | sqxTranslate }}
</a> </a>
</sqx-dropdown-menu> </sqx-dropdown-menu>
</ng-container> }
@if (contentTab | async; as tab) {
<ng-container *ngIf="contentTab | async; let tab">
<sqx-toolbar></sqx-toolbar> <sqx-toolbar></sqx-toolbar>
@if (tab === "editor") {
<ng-container *ngIf="tab === 'editor'"> <sqx-preview-button [confirm]="confirmPreview" [content]="content" [schema]="schema"></sqx-preview-button>
<sqx-preview-button [schema]="schema" [content]="content" [confirm]="confirmPreview"></sqx-preview-button> @if (content.canUpdate) {
<button class="btn btn-primary ms-2" shortcut="CTRL + SHIFT + S" sqxTourStep="saveContent" type="submit">
<ng-container *ngIf="content?.canUpdate"> {{ "common.save" | sqxTranslate }}
<button type="submit" class="btn btn-primary ms-2" shortcut="CTRL + SHIFT + S" sqxTourStep="saveContent">
{{ 'common.save' | sqxTranslate }}
</button> </button>
</ng-container> }
</ng-container> }
</ng-container> }
</ng-container> } @else {
<button class="btn btn-more btn-outline-secondary btn-sm me-2" (click)="changeShowIdInput(!showIdInput)" type="button">
<ng-template #noContentMenu>
<button type="button" class="btn btn-more btn-outline-secondary btn-sm me-2" (click)="changeShowIdInput(!showIdInput)">
<span [class.hidden]="showIdInput">+</span> <span [class.hidden]="showIdInput">+</span>
<span [class.hidden]="!showIdInput">-</span> <span [class.hidden]="!showIdInput">-</span>
</button> </button>
<sqx-language-selector
<sqx-language-selector class="languages-buttons" class="languages-buttons"
[language]="language"
(languageChange)="changeLanguage($event)" (languageChange)="changeLanguage($event)"
[language]="language"
[languages]="languages" [languages]="languages"
[percents]="contentForm.translationStatus | async"> [percents]="contentForm.translationStatus | async"></sqx-language-selector>
</sqx-language-selector>
<div sqxTourStep="saveContent"> <div sqxTourStep="saveContent">
<button type="button" class="btn btn-primary ms-2" (click)="save()" *ngIf="contentsState.canCreate | async"> @if (contentsState.canCreate | async) {
{{ 'common.save' | sqxTranslate }} <button class="btn btn-primary ms-2" (click)="save()" type="button">
</button> {{ "common.save" | sqxTranslate }}
</button>
<button type="submit" class="btn btn-success ms-2" shortcut="CTRL + SHIFT + S" *ngIf="contentsState.canCreateAndPublish | async"> }
{{ 'contents.saveAndPublish' | sqxTranslate }} @if (contentsState.canCreateAndPublish | async) {
</button> <button class="btn btn-success ms-2" shortcut="CTRL + SHIFT + S" type="submit">
{{ "contents.saveAndPublish" | sqxTranslate }}
</button>
}
</div> </div>
</ng-template> }
</div> </div>
</ng-container> </ng-container>
<ng-container> <ng-container>
<ng-container *ngIf="content"> @if (content) {
<ng-container [ngSwitch]="contentTab | async"> @switch (contentTab | async) {
<ng-container *ngSwitchCase="'references'"> @case ("references") {
<sqx-content-references mode="references" <sqx-content-references
[content]="content" [content]="content"
[language]="language" [language]="language"
[languages]="languages"> [languages]="languages"
</sqx-content-references> mode="references"></sqx-content-references>
</ng-container> }
<ng-container *ngSwitchCase="'referencing'"> @case ("referencing") {
<sqx-content-references mode="referencing" <sqx-content-references
[content]="content" [content]="content"
[language]="language" [language]="language"
[languages]="languages"> [languages]="languages"
</sqx-content-references> mode="referencing"></sqx-content-references>
</ng-container> }
<ng-container *ngSwitchCase="'inspect'"> @case ("inspect") {
<sqx-content-inspection <sqx-content-inspection
[appName]="contentsState.appName"
[content]="content" [content]="content"
[language]="language" [language]="language"
[languages]="languages" [languages]="languages"></sqx-content-inspection>
[appName]="contentsState.appName"> }
</sqx-content-inspection> @case ("extension") {
</ng-container> @if (schema.properties.contentEditorUrl && content) {
<ng-container *ngSwitchCase="'extension'"> <sqx-content-extension
<sqx-content-extension mode="referencing" *ngIf="schema.properties.contentEditorUrl && content" [contentItem]="content"
[editorUrl]="schema.properties.contentEditorUrl" [contentSchema]="schema"
[contentItem]="content" [editorUrl]="schema.properties.contentEditorUrl"
[contentSchema]="schema"> mode="referencing"></sqx-content-extension>
</sqx-content-extension> }
</ng-container> }
</ng-container> }
</ng-container> }
<ng-container *ngIf="!content || (contentTab | async) === 'editor'"> @if (!content || (contentTab | async) === "editor") {
<sqx-content-editor <sqx-content-editor
[(contentId)]="contentId"
[isNew]="!content"
[isDeleted]="content?.isDeleted"
[contentForm]="contentForm" [contentForm]="contentForm"
[contentFormCompare]="contentFormCompare" [contentFormCompare]="contentFormCompare"
[(contentId)]="contentId"
[contentVersion]="contentVersion" [contentVersion]="contentVersion"
[formContext]="formContext" [formContext]="formContext"
[isDeleted]="content?.isDeleted"
[isNew]="!content"
[language]="language" [language]="language"
(languageChange)="language = $event" (languageChange)="language = $event"
[languages]="languages" [languages]="languages"
(loadLatest)="loadLatest()" (loadLatest)="loadLatest()"
[schema]="schema" [schema]="schema"
[showIdInput]="showIdInput"> [showIdInput]="showIdInput"></sqx-content-editor>
</sqx-content-editor> }
</ng-container>
</ng-container> </ng-container>
<ng-template sidebarMenu> <ng-template sidebarMenu>
<div class="panel-nav"> <div class="panel-nav">
<a class="panel-link" <a
class="panel-link"
#linkHistory
queryParamsHandling="preserve"
replaceUrl="true" replaceUrl="true"
routerLink="history" routerLink="history"
routerLinkActive="active" routerLinkActive="active"
queryParamsHandling="preserve"
title="i18n:common.workflow"
titlePosition="left"
sqxTourStep="history" sqxTourStep="history"
#linkHistory> title="i18n:common.workflow"
titlePosition="left">
<i class="icon-time"></i> <i class="icon-time"></i>
</a> </a>
<a class="panel-link" <a
class="panel-link"
hintAfter="120000"
hintText="i18n:common.sidebarTour"
queryParamsHandling="preserve"
replaceUrl="true" replaceUrl="true"
routerLink="comments" routerLink="comments"
routerLinkActive="active" routerLinkActive="active"
queryParamsHandling="preserve" sqxTourStep="comments"
title="i18n:common.comments" title="i18n:common.comments"
titlePosition="left" titlePosition="left">
hintText="i18n:common.sidebarTour"
hintAfter="120000"
sqxTourStep="comments">
<i class="icon-comments"></i> <i class="icon-comments"></i>
</a> </a>
<a class="panel-link" @if (schema.properties.contentSidebarUrl) {
replaceUrl="true" <a
routerLink="sidebar" class="panel-link"
routerLinkActive="active" queryParamsHandling="preserve"
queryParamsHandling="preserve" replaceUrl="true"
title="i18n:common.sidebar" routerLink="sidebar"
titlePosition="left" routerLinkActive="active"
sqxTourStep="plugin" sqxTourStep="plugin"
*ngIf="schema.properties.contentSidebarUrl"> title="i18n:common.sidebar"
<i class="icon-plugin"></i> titlePosition="left">
</a> <i class="icon-plugin"></i>
</a>
}
</div> </div>
</ng-template> </ng-template>
</sqx-layout> </sqx-layout>

5
frontend/src/app/features/content/pages/content/content-page.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 { AsyncPipe, Location, NgIf, NgSwitch, NgSwitchCase } from '@angular/common'; import { AsyncPipe, Location } from '@angular/common';
import { Component, OnInit } from '@angular/core'; import { Component, OnInit } from '@angular/core';
import { FormsModule, ReactiveFormsModule } from '@angular/forms'; import { FormsModule, ReactiveFormsModule } from '@angular/forms';
import { ActivatedRoute, Router, RouterLink, RouterLinkActive, RouterOutlet } from '@angular/router'; import { ActivatedRoute, Router, RouterLink, RouterLinkActive, RouterOutlet } from '@angular/router';
@ -43,9 +43,6 @@ import { ContentReferencesComponent } from './references/content-references.comp
LayoutComponent, LayoutComponent,
ModalDirective, ModalDirective,
ModalPlacementDirective, ModalPlacementDirective,
NgIf,
NgSwitch,
NgSwitchCase,
NotifoComponent, NotifoComponent,
PreviewButtonComponent, PreviewButtonComponent,
ReactiveFormsModule, ReactiveFormsModule,

79
frontend/src/app/features/content/pages/content/editor/content-editor.component.html

@ -1,47 +1,60 @@
<sqx-form-error bubble="true" closeable="true" [error]="(contentForm.error | async)"></sqx-form-error> <sqx-form-error bubble="true" closeable="true" [error]="contentForm.error | async"></sqx-form-error>
<sqx-list-view noPadding="true"> <sqx-list-view noPadding="true">
<ng-container topHeader> <ng-container topHeader>
<div class="alert alert-danger" *ngIf="!contentVersion && isDeleted"> @if (!contentVersion && isDeleted) {
{{ 'contents.deleted' | sqxTranslate }} <div class="alert alert-danger">
</div> {{ "contents.deleted" | sqxTranslate }}
<div class="alert alert-danger" *ngIf="contentVersion">
<div class="float-end">
<a (click)="loadLatest.emit()">{{ 'contents.viewLatest' | sqxTranslate }}</a>
</div> </div>
}
<div *ngIf="isDeleted" @if (contentVersion) {
[innerHTML]="'contents.versionViewingDeleted' | sqxTranslate: { version: contentVersion } | sqxMarkdownInline | sqxSafeHtml"> <div class="alert alert-danger">
<div class="float-end">
<a (click)="loadLatest.emit()">{{ "contents.viewLatest" | sqxTranslate }}</a>
</div>
@if (isDeleted) {
<div
[innerHTML]="
'contents.versionViewingDeleted' | sqxTranslate: { version: contentVersion } | sqxMarkdownInline | sqxSafeHtml
"></div>
}
@if (!isDeleted) {
<div
[innerHTML]="
'contents.versionViewing' | sqxTranslate: { version: contentVersion } | sqxMarkdownInline | sqxSafeHtml
"></div>
}
</div> </div>
}
<div *ngIf="!isDeleted"
[innerHTML]="'contents.versionViewing' | sqxTranslate: { version: contentVersion } | sqxMarkdownInline | sqxSafeHtml"> @if (isNew && showIdInput) {
<div>
<input
class="form-control"
[ngModel]="contentId"
(ngModelChange)="contentIdChange.emit($event)"
placeholder="{{ 'contents.idPlaceholder' | sqxTranslate }}" />
</div> </div>
</div> }
<div *ngIf="isNew && showIdInput">
<input class="form-control" placeholder="{{ 'contents.idPlaceholder' | sqxTranslate }}"
[ngModel]="contentId"
(ngModelChange)="contentIdChange.emit($event)" />
</div>
</ng-container> </ng-container>
<ng-container> <ng-container>
<div class="cursors" sqxCursors> <div class="cursors" sqxCursors>
<sqx-cursors></sqx-cursors> <sqx-cursors></sqx-cursors>
<sqx-content-section *ngFor="let section of contentForm.sections; trackBy: trackBySection" @for (section of contentForm.sections; track section.separator?.fieldId) {
[form]="contentForm" <sqx-content-section
[formCompare]="contentFormCompare" [form]="contentForm"
[formContext]="formContext" [formCompare]="contentFormCompare"
[formLevel]="0" [formContext]="formContext"
[formSection]="section" [formLevel]="0"
[language]="language" [formSection]="section"
(languageChange)="languageChange.emit($event)" [language]="language"
[languages]="languages" (languageChange)="languageChange.emit($event)"
[schema]="schema"> [languages]="languages"
</sqx-content-section> [schema]="schema"></sqx-content-section>
}
</div> </div>
</ng-container> </ng-container>
</sqx-list-view> </sqx-list-view>

10
frontend/src/app/features/content/pages/content/editor/content-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 { AsyncPipe, NgFor, NgIf } from '@angular/common'; import { AsyncPipe } from '@angular/common';
import { booleanAttribute, Component, EventEmitter, Input, Output } from '@angular/core'; import { booleanAttribute, Component, EventEmitter, Input, Output } from '@angular/core';
import { FormsModule } from '@angular/forms'; import { FormsModule } from '@angular/forms';
import { AppLanguageDto, CursorsComponent, CursorsDirective, EditContentForm, FieldForm, FieldSection, FormErrorComponent, ListViewComponent, MarkdownInlinePipe, RootFieldDto, SafeHtmlPipe, SchemaDto, TranslatePipe, Version } from '@app/shared'; import { AppLanguageDto, CursorsComponent, CursorsDirective, EditContentForm, FormErrorComponent, ListViewComponent, MarkdownInlinePipe, SafeHtmlPipe, SchemaDto, TranslatePipe, Version } from '@app/shared';
import { ContentSectionComponent } from '../../../shared/forms/content-section.component'; import { ContentSectionComponent } from '../../../shared/forms/content-section.component';
@Component({ @Component({
@ -25,8 +25,6 @@ import { ContentSectionComponent } from '../../../shared/forms/content-section.c
FormsModule, FormsModule,
ListViewComponent, ListViewComponent,
MarkdownInlinePipe, MarkdownInlinePipe,
NgFor,
NgIf,
SafeHtmlPipe, SafeHtmlPipe,
TranslatePipe, TranslatePipe,
], ],
@ -73,8 +71,4 @@ export class ContentEditorComponent {
@Input({ required: true }) @Input({ required: true })
public languages!: ReadonlyArray<AppLanguageDto>; public languages!: ReadonlyArray<AppLanguageDto>;
public trackBySection(_index: number, section: FieldSection<RootFieldDto, FieldForm>) {
return section.separator?.fieldId;
}
} }

43
frontend/src/app/features/content/pages/content/inspecting/content-inspection.component.html

@ -1,30 +1,27 @@
<sqx-form-error bubble="true" closeable="true" [error]="contentError"></sqx-form-error> <sqx-form-error bubble="true" closeable="true" [error]="contentError"></sqx-form-error>
<div class="inner-menu"> <div class="inner-menu">
<ul class="nav nav-tabs2" *ngIf="mode | async; let currentMode"> @if (mode | async; as currentMode) {
<li class="nav-item"> <ul class="nav nav-tabs2">
<a class="nav-link" [class.active]="currentMode === 'Content'" (click)="setMode('Content')"> <li class="nav-item">
{{ 'contents.inspectContent' | sqxTranslate }} <a class="nav-link" [class.active]="currentMode === 'Content'" (click)="setMode('Content')">
</a> {{ "contents.inspectContent" | sqxTranslate }}
</li> </a>
<li class="nav-item"> </li>
<a class="nav-link" [class.active]="currentMode === 'Data'" (click)="setMode('Data')"> <li class="nav-item">
{{ 'contents.inspectData' | sqxTranslate }} <a class="nav-link" [class.active]="currentMode === 'Data'" (click)="setMode('Data')">
</a> {{ "contents.inspectData" | sqxTranslate }}
</li> </a>
<li class="nav-item"> </li>
<a class="nav-link" [class.active]="currentMode === 'FlatData'" (click)="setMode('FlatData')"> <li class="nav-item">
{{ 'contents.inspectFlatData' | sqxTranslate }} <a class="nav-link" [class.active]="currentMode === 'FlatData'" (click)="setMode('FlatData')">
</a> {{ "contents.inspectFlatData" | sqxTranslate }}
</li> </a>
</ul> </li>
</ul>
}
</div> </div>
<div class="inner-main"> <div class="inner-main">
<sqx-code-editor <sqx-code-editor borderless="true" [ngModel]="actualData | async" (ngModelChange)="setData($event)" valueMode="Json"></sqx-code-editor>
borderless="true"
[ngModel]="actualData | async"
(ngModelChange)="setData($event)"
valueMode="Json">
</sqx-code-editor>
</div> </div>

3
frontend/src/app/features/content/pages/content/inspecting/content-inspection.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 { AsyncPipe, NgIf } from '@angular/common'; import { AsyncPipe } from '@angular/common';
import { ChangeDetectorRef, Component, Input, OnDestroy } from '@angular/core'; import { ChangeDetectorRef, Component, Input, OnDestroy } from '@angular/core';
import { FormsModule } from '@angular/forms'; import { FormsModule } from '@angular/forms';
import { BehaviorSubject, combineLatest, of } from 'rxjs'; import { BehaviorSubject, combineLatest, of } from 'rxjs';
@ -24,7 +24,6 @@ type Mode = 'Content' | 'Data' | 'FlatData';
CodeEditorComponent, CodeEditorComponent,
FormErrorComponent, FormErrorComponent,
FormsModule, FormsModule,
NgIf,
TranslatePipe, TranslatePipe,
], ],
}) })

58
frontend/src/app/features/content/pages/content/references/content-references.component.html

@ -1,32 +1,40 @@
<sqx-list-view [isLoading]="contentsState.isLoading | async" table="true"> <sqx-list-view [isLoading]="contentsState.isLoading | async" table="true">
<ng-container> <ng-container>
<table class="table table-items table-fixed" *ngIf="contentsState.contents | async; let contents"> @if (contentsState.contents | async; as contents) {
<tbody *ngFor="let content of contents; trackBy: trackByContent" <table class="table table-items table-fixed">
[sqxReferenceItem]="content" @for (content of contents; track content.id) {
[canRemove]="false" <tbody
[columns]="contents | sqxContentsColumns" [canRemove]="false"
[isCompact]="false" [columns]="contents | sqxContentsColumns"
[isDisabled]="false" [isCompact]="false"
[language]="language" [isDisabled]="false"
[languages]="languages" [language]="language"
[validations]="(contentsState.validationResults | async)!" [languages]="languages"
[validityVisible]="true"> [sqxReferenceItem]="content"
</tbody> [validations]="(contentsState.validationResults | async)!"
[validityVisible]="true"></tbody>
<tbody *ngIf="(contentsState.isLoaded | async) && contents.length === 0"> }
<tr> @if ((contentsState.isLoaded | async) && contents.length === 0) {
<td class="table-items-row-empty" *ngIf="mode === 'references'"> <tbody>
{{ 'contents.noReferences' | sqxTranslate }} <tr>
</td> @if (mode === "references") {
<td class="table-items-row-empty" *ngIf="mode === 'referencing'"> <td class="table-items-row-empty">
{{ 'contents.noReferencing' | sqxTranslate }} {{ "contents.noReferences" | sqxTranslate }}
</td> </td>
</tr> }
</tbody> @if (mode === "referencing") {
</table> <td class="table-items-row-empty">
{{ "contents.noReferencing" | sqxTranslate }}
</td>
}
</tr>
</tbody>
}
</table>
}
</ng-container> </ng-container>
<ng-container footer> <ng-container footer>
<sqx-pager [paging]="contentsState.paging | async" (pagingChange)="contentsState.page($event)"></sqx-pager> <sqx-pager [paging]="contentsState.paging | async" (pagingChange)="contentsState.page($event)"></sqx-pager>
</ng-container> </ng-container>
</sqx-list-view> </sqx-list-view>

8
frontend/src/app/features/content/pages/content/references/content-references.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 { AsyncPipe, NgFor, NgIf } from '@angular/common'; import { AsyncPipe } from '@angular/common';
import { ChangeDetectionStrategy, ChangeDetectorRef, Component, Input, OnDestroy, OnInit } from '@angular/core'; import { ChangeDetectionStrategy, ChangeDetectorRef, Component, Input, OnDestroy, OnInit } from '@angular/core';
import { AppLanguageDto, ComponentContentsState, ContentDto, ContentsColumnsPipe, ListViewComponent, PagerComponent, QuerySynchronizer, Router2State, ToolbarService, TranslatePipe, TypedSimpleChanges } from '@app/shared'; import { AppLanguageDto, ComponentContentsState, ContentDto, ContentsColumnsPipe, ListViewComponent, PagerComponent, QuerySynchronizer, Router2State, ToolbarService, TranslatePipe, TypedSimpleChanges } from '@app/shared';
import { ReferenceItemComponent } from '../../../shared/references/reference-item.component'; import { ReferenceItemComponent } from '../../../shared/references/reference-item.component';
@ -24,8 +24,6 @@ import { ReferenceItemComponent } from '../../../shared/references/reference-ite
AsyncPipe, AsyncPipe,
ContentsColumnsPipe, ContentsColumnsPipe,
ListViewComponent, ListViewComponent,
NgFor,
NgIf,
PagerComponent, PagerComponent,
ReferenceItemComponent, ReferenceItemComponent,
TranslatePipe, TranslatePipe,
@ -97,8 +95,4 @@ export class ContentReferencesComponent implements OnInit, OnDestroy {
private publishAll() { private publishAll() {
this.contentsState.changeManyStatus(this.contentsState.snapshot.contents.filter(x => x.canPublish), 'Published'); this.contentsState.changeManyStatus(this.contentsState.snapshot.contents.filter(x => x.canPublish), 'Published');
} }
public trackByContent(_index: number, content: ContentDto) {
return content.id;
}
} }

22
frontend/src/app/features/content/pages/contents-plugin/contents-plugin.component.html

@ -1,10 +1,16 @@
<sqx-layout layout="main" titleText="i18n:common.contents" titleIcon="contents" hideHeader="true" hideSidebar="true" white="true" overflow="true"> <sqx-layout
<ng-container *ngIf="schema | async; let schema"> hideHeader="true"
<sqx-content-extension hideSidebar="true"
[editorUrl]="schema.properties.contentsListUrl" layout="main"
overflow="true"
titleIcon="contents"
titleText="i18n:common.contents"
white="true">
@if (schema | async; as schema) {
<sqx-content-extension
[contentItem]="undefined" [contentItem]="undefined"
[contentSchema]="schema" [contentSchema]="schema"
scrollable="true"> [editorUrl]="schema.properties.contentsListUrl"
</sqx-content-extension> scrollable="true"></sqx-content-extension>
</ng-container> }
</sqx-layout> </sqx-layout>

3
frontend/src/app/features/content/pages/contents-plugin/contents-plugin.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 { AsyncPipe, NgIf } from '@angular/common'; import { AsyncPipe } from '@angular/common';
import { ChangeDetectionStrategy, Component } from '@angular/core'; import { ChangeDetectionStrategy, Component } from '@angular/core';
import { LayoutComponent, SchemasState } from '@app/shared'; import { LayoutComponent, SchemasState } from '@app/shared';
import { ContentExtensionComponent } from '../../shared/content-extension.component'; import { ContentExtensionComponent } from '../../shared/content-extension.component';
@ -20,7 +20,6 @@ import { ContentExtensionComponent } from '../../shared/content-extension.compon
AsyncPipe, AsyncPipe,
ContentExtensionComponent, ContentExtensionComponent,
LayoutComponent, LayoutComponent,
NgIf,
], ],
}) })
export class ContentsPluginComponent { export class ContentsPluginComponent {

40
frontend/src/app/features/content/pages/contents/contents-filters-page.component.html

@ -1,32 +1,24 @@
<sqx-layout layout="right" titleText="i18n:common.filters" width="20" white="true" padding="true" overflow="true"> <sqx-layout layout="right" overflow="true" padding="true" titleText="i18n:common.filters" white="true" width="20">
<ng-container *ngIf="schemaQueries | async; let queries"> @if (schemaQueries | async; as queries) {
<sqx-query-list <sqx-query-list
[types]="'common.contents' | sqxTranslate"
[queryUsed]="contentsState.query | async"
[queries]="queries.defaultQueries" [queries]="queries.defaultQueries"
(search)="search($event)"> [queryUsed]="contentsState.query | async"
</sqx-query-list> (search)="search($event)"
[types]="'common.contents' | sqxTranslate"></sqx-query-list>
<hr> <hr />
<div class="sidebar-section"> <div class="sidebar-section">
<h3>{{ 'contents.statusQueries' | sqxTranslate }}</h3> <h3>{{ "contents.statusQueries" | sqxTranslate }}</h3>
<sqx-query-list <sqx-query-list
[types]="'common.contents' | sqxTranslate"
[queryUsed]="contentsState.query | async"
[queries]="contentsState.statusQueries | async" [queries]="contentsState.statusQueries | async"
(search)="search($event)"> [queryUsed]="contentsState.query | async"
</sqx-query-list> (search)="search($event)"
[types]="'common.contents' | sqxTranslate"></sqx-query-list>
</div> </div>
<hr />
<hr>
<sqx-shared-queries <sqx-shared-queries
[types]="'common.contents' | sqxTranslate"
[queryUsed]="contentsState.query | async"
[queries]="queries" [queries]="queries"
(search)="search($event)"> [queryUsed]="contentsState.query | async"
</sqx-shared-queries> (search)="search($event)"
</ng-container> [types]="'common.contents' | sqxTranslate"></sqx-shared-queries>
</sqx-layout> }
</sqx-layout>

3
frontend/src/app/features/content/pages/contents/contents-filters-page.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 { AsyncPipe, NgIf } from '@angular/common'; import { AsyncPipe } from '@angular/common';
import { Component } from '@angular/core'; import { Component } from '@angular/core';
import { map } from 'rxjs/operators'; import { map } from 'rxjs/operators';
import { ContentsState, defined, LayoutComponent, Queries, Query, QueryListComponent, SavedQueriesComponent, SchemasState, TranslatePipe, UIState } from '@app/shared'; import { ContentsState, defined, LayoutComponent, Queries, Query, QueryListComponent, SavedQueriesComponent, SchemasState, TranslatePipe, UIState } from '@app/shared';
@ -18,7 +18,6 @@ import { ContentsState, defined, LayoutComponent, Queries, Query, QueryListCompo
imports: [ imports: [
AsyncPipe, AsyncPipe,
LayoutComponent, LayoutComponent,
NgIf,
QueryListComponent, QueryListComponent,
SavedQueriesComponent, SavedQueriesComponent,
TranslatePipe, TranslatePipe,

240
frontend/src/app/features/content/pages/contents/contents-page.component.html

@ -1,177 +1,211 @@
<sqx-title [message]="schema.displayName"></sqx-title> <sqx-title [message]="schema.displayName"></sqx-title>
<sqx-layout layout="main" titleText="i18n:common.contents" titleIcon="contents"> <sqx-layout layout="main" titleIcon="contents" titleText="i18n:common.contents">
<ng-container menu> <ng-container menu>
<div class="row flex-nowrap flex-grow-1 gx-2"> <div class="row flex-nowrap flex-grow-1 gx-2">
<div class="col-auto ms-8"> <div class="col-auto ms-8">
<sqx-notifo topic="apps/{{contentsState.appId}}/schemas/{{schema.id}}/contents" position="bottom-left"></sqx-notifo> <sqx-notifo position="bottom-left" topic="apps/{{ contentsState.appId }}/schemas/{{ schema.id }}/contents"></sqx-notifo>
<button type="button" class="btn btn-text-secondary" (click)="reload()" title="i18n:contents.refreshTooltip" shortcut="CTRL + B"> <button
<i class="icon-reset"></i> {{ 'common.refresh' | sqxTranslate }} class="btn btn-text-secondary"
(click)="reload()"
shortcut="CTRL + B"
title="i18n:contents.refreshTooltip"
type="button">
<i class="icon-reset"></i>
{{ "common.refresh" | sqxTranslate }}
</button> </button>
</div> </div>
<div class="col"> <div class="col">
<sqx-search-form formClass="form" placeholder="{{ 'contents.searchPlaceholder' | sqxTranslate }}" <sqx-search-form
enableShortcut="true" enableShortcut="true"
formClass="form"
[language]="(languagesState.isoMasterLanguage | async)!" [language]="(languagesState.isoMasterLanguage | async)!"
[languages]="languages" [languages]="languages"
placeholder="{{ 'contents.searchPlaceholder' | sqxTranslate }}"
[queries]="queries | async" [queries]="queries | async"
[queriesTypes]="'common.contents' | sqxTranslate" [queriesTypes]="'common.contents' | sqxTranslate"
(queryChange)="search($event)"
[query]="contentsState.query | async" [query]="contentsState.query | async"
(queryChange)="search($event)"
[queryModel]="queryModel | async" [queryModel]="queryModel | async"
[statuses]="contentsState.statuses | async"> [statuses]="contentsState.statuses | async"></sqx-search-form>
</sqx-search-form>
</div>
<div class="col-auto" *ngIf="languages.length > 1">
<sqx-language-selector class="languages-buttons"
(languageChange)="changeLanguage($event)"
[language]="language"
[languages]="languages"
[percents]="translationStatus">
</sqx-language-selector>
</div> </div>
@if (languages.length > 1) {
<div class="col-auto">
<sqx-language-selector
class="languages-buttons"
[language]="language"
(languageChange)="changeLanguage($event)"
[languages]="languages"
[percents]="translationStatus"></sqx-language-selector>
</div>
}
<div class="col-auto"> <div class="col-auto">
<button type="button" class="btn btn-success" routerLink="new" title="i18n:contents.createContentTooltip" shortcut="CTRL + U" [disabled]="(contentsState.canCreateAny | async) === false" sqxTourStep="addContent"> <button
<i class="icon-plus"></i> {{ 'contents.create' | sqxTranslate }} class="btn btn-success"
[disabled]="(contentsState.canCreateAny | async) === false"
routerLink="new"
shortcut="CTRL + U"
sqxTourStep="addContent"
title="i18n:contents.createContentTooltip"
type="button">
<i class="icon-plus"></i>
{{ "contents.create" | sqxTranslate }}
</button> </button>
</div> </div>
</div> </div>
</ng-container> </ng-container>
<ng-container> <ng-container>
<ng-container *ngIf="tableSettings | async; let tableSettings"> @if (tableSettings | async; as tableSettings) {
<ng-container *ngIf="tableSettings.listFields | async; let tableFields"> @if (tableSettings.listFields | async; as tableFields) {
<sqx-list-view [isLoading]="contentsState.isLoading | async" syncedHeader="true" tableNoPadding="true"> <sqx-list-view [isLoading]="contentsState.isLoading | async" syncedHeader="true" tableNoPadding="true">
<ng-container topHeader> <ng-container topHeader>
<div class="selection" *ngIf="selectionCount > 0"> @if (selectionCount > 0) {
{{ 'contents.selectionCount' | sqxTranslate: { count: selectionCount } }}&nbsp;&nbsp; <div class="selection">
{{ "contents.selectionCount" | sqxTranslate: { count: selectionCount } }}&nbsp;&nbsp;
<button type="button" class="btn btn-outline-secondary btn-status me-2" *ngFor="let status of selectionStatuses | sqxKeys" (click)="changeSelectedStatus(status)"> @for (status of selectionStatuses | sqxKeys; track status) {
<sqx-content-status layout="text" <button
[status]="status" class="btn btn-outline-secondary btn-status me-2"
[statusColor]="selectionStatuses[status]"> (click)="changeSelectedStatus(status)"
</sqx-content-status> type="button">
</button> <sqx-content-status
layout="text"
<button type="button" class="btn btn-danger" *ngIf="selectionCanDelete" [status]="status"
(sqxConfirmClick)="deleteSelected()" [statusColor]="selectionStatuses[status]"></sqx-content-status>
confirmTitle="i18n:contents.deleteConfirmTitle" </button>
confirmText="i18n:contents.deleteManyConfirmText" }
confirmRememberKey="deleteContents"> @if (selectionCanDelete) {
{{ 'common.delete' | sqxTranslate }} <button
</button> class="btn btn-danger"
</div> confirmRememberKey="deleteContents"
confirmText="i18n:contents.deleteManyConfirmText"
confirmTitle="i18n:contents.deleteConfirmTitle"
(sqxConfirmClick)="deleteSelected()"
type="button">
{{ "common.delete" | sqxTranslate }}
</button>
}
</div>
}
<div class="settings-container"> <div class="settings-container">
<button type="button" class="btn btn-sm settings-button" (click)="tableViewModal.toggle()" #buttonSettings> <button class="btn btn-sm settings-button" #buttonSettings (click)="tableViewModal.toggle()" type="button">
<span class="hidden">{{ 'common.settings' | sqxTranslate }}</span> <span class="hidden">{{ "common.settings" | sqxTranslate }}</span>
<i class="icon-settings"></i> <i class="icon-settings"></i>
</button> </button>
<sqx-dropdown-menu
<sqx-dropdown-menu *sqxModal="tableViewModal" [sqxAnchoredTo]="buttonSettings" scrollY="true" position="bottom-end"> position="bottom-end"
scrollY="true"
[sqxAnchoredTo]="buttonSettings"
*sqxModal="tableViewModal">
<sqx-custom-view-editor <sqx-custom-view-editor
[allFields]="tableSettings.schemaFields" [allFields]="tableSettings.schemaFields"
[listFields]="$any(tableFields)"
(listFieldsChange)="tableSettings.updateFields($event)" (listFieldsChange)="tableSettings.updateFields($event)"
(listFieldsReset)="tableSettings.reset()" (listFieldsReset)="tableSettings.reset()"></sqx-custom-view-editor>
[listFields]="$any(tableFields)">
</sqx-custom-view-editor>
</sqx-dropdown-menu> </sqx-dropdown-menu>
</div> </div>
</ng-container> </ng-container>
<ng-container header> <ng-container header>
<table class="table table-items table-fixed" [sqxContentListWidth]="tableFields" [fields]="tableSettings" #header> <table class="table table-items table-fixed" #header [fields]="tableSettings" [sqxContentListWidth]="tableFields">
<thead> <thead>
<tr> <tr>
<th class="cell-select"> <th class="cell-select">
<div class="form-check"> <div class="form-check">
<input class="form-check-input" type="checkbox" id="all_selected" <input
[ngModel]="selectedAll" class="form-check-input"
(ngModelChange)="selectAll($event)"> id="all_selected"
[ngModel]="selectedAll"
(ngModelChange)="selectAll($event)"
type="checkbox" />
<label class="form-check-label" for="all_selected"></label> <label class="form-check-label" for="all_selected"></label>
</div> </div>
</th> </th>
<th class="cell-actions cell-actions-left"> <th class="cell-actions cell-actions-left">
<span class="truncate">{{ 'common.actions' | sqxTranslate }}</span> <span class="truncate">{{ "common.actions" | sqxTranslate }}</span>
</th>
<th *ngFor="let field of tableFields"
sqxContentListCell
sqxContentListCellResize
[field]="field"
[fields]="tableSettings">
<sqx-content-list-header
[field]="field"
(queryChange)="search($event)"
[query]="(contentsState.query | async)!"
[language]="language">
</sqx-content-list-header>
</th> </th>
@for (field of tableFields; track field) {
<th [field]="field" [fields]="tableSettings" sqxContentListCell sqxContentListCellResize>
<sqx-content-list-header
[field]="field"
[language]="language"
[query]="(contentsState.query | async)!"
(queryChange)="search($event)"></sqx-content-list-header>
</th>
}
<th></th> <th></th>
</tr> </tr>
</thead> </thead>
</table> </table>
</ng-container> </ng-container>
<ng-container> <ng-container>
<div class="table-container"> <div class="table-container">
<table class="table table-center table-fixed" [sqxContentListWidth]="tableFields" [fields]="tableSettings" [sqxSyncWidth]="header"> <table
<tbody *ngFor="let content of contentsState.contents | async; trackBy: trackByContent" class="table table-center table-fixed"
[sqxContent]="content" [fields]="tableSettings"
(clone)="clone(content)" [sqxContentListWidth]="tableFields"
[cloneable]="contentsState.snapshot.canCreate" [sqxSyncWidth]="header">
(delete)="delete(content)" @for (content of contentsState.contents | async; track content.id) {
[language]="language" <tbody
[languages]="languages" (clone)="clone(content)"
[link]="[content.id, 'history']" [cloneable]="contentsState.snapshot.canCreate"
[schema]="schema" (delete)="delete(content)"
[selected]="isItemSelected(content)" [language]="language"
(selectedChange)="selectItem(content, $event)" [languages]="languages"
(statusChange)="changeStatus(content, $event)" [link]="[content.id, 'history']"
[tableFields]="tableFields" [schema]="schema"
[tableSettings]="tableSettings"> [selected]="isItemSelected(content)"
</tbody> (selectedChange)="selectItem(content, $event)"
[sqxContent]="content"
(statusChange)="changeStatus(content, $event)"
[tableFields]="tableFields"
[tableSettings]="tableSettings"></tbody>
}
</table> </table>
</div> </div>
</ng-container> </ng-container>
<ng-container footer> <ng-container footer>
<sqx-pager (loadTotal)="reloadTotal()" [paging]="contentsState.paging | async" (pagingChange)="contentsState.page($event)"></sqx-pager> <sqx-pager
(loadTotal)="reloadTotal()"
[paging]="contentsState.paging | async"
(pagingChange)="contentsState.page($event)"></sqx-pager>
</ng-container> </ng-container>
</sqx-list-view> </sqx-list-view>
</ng-container> }
</ng-container> }
</ng-container> </ng-container>
<ng-template sidebarMenu> <ng-template sidebarMenu>
<div class="panel-nav"> <div class="panel-nav">
<a class="panel-link" <a
class="panel-link"
queryParamsHandling="preserve"
replaceUrl="true" replaceUrl="true"
routerLink="filters" routerLink="filters"
routerLinkActive="active" routerLinkActive="active"
queryParamsHandling="preserve" sqxTourStep="filters"
title="i18n:common.filters" title="i18n:common.filters"
titlePosition="left" titlePosition="left">
sqxTourStep="filters">
<i class="icon-filter"></i> <i class="icon-filter"></i>
</a> </a>
<a class="panel-link" @if (schema.properties.contentsSidebarUrl) {
replaceUrl="true" <a
routerLink="sidebar" class="panel-link"
routerLinkActive="active" queryParamsHandling="preserve"
queryParamsHandling="preserve" replaceUrl="true"
title="i18n:common.sidebar" routerLink="sidebar"
titlePosition="left" routerLinkActive="active"
sqxTourStep="plugin" sqxTourStep="plugin"
*ngIf="schema.properties.contentsSidebarUrl"> title="i18n:common.sidebar"
<i class="icon-plugin"></i> titlePosition="left">
</a> <i class="icon-plugin"></i>
</a>
}
</div> </div>
</ng-template> </ng-template>
</sqx-layout> </sqx-layout>
<router-outlet></router-outlet> <router-outlet></router-outlet>
<sqx-due-time-selector [disabled]="disableScheduler" #dueTimeSelector></sqx-due-time-selector> <sqx-due-time-selector #dueTimeSelector [disabled]="disableScheduler"></sqx-due-time-selector>

8
frontend/src/app/features/content/pages/contents/contents-page.component.ts

@ -7,7 +7,7 @@
/* eslint-disable @typescript-eslint/no-unnecessary-boolean-literal-compare */ /* eslint-disable @typescript-eslint/no-unnecessary-boolean-literal-compare */
import { AsyncPipe, NgFor, NgIf } from '@angular/common'; import { AsyncPipe } from '@angular/common';
import { Component, OnInit, ViewChild } from '@angular/core'; import { Component, OnInit, ViewChild } from '@angular/core';
import { FormsModule } from '@angular/forms'; import { FormsModule } from '@angular/forms';
import { ActivatedRoute, Router, RouterLink, RouterLinkActive, RouterOutlet } from '@angular/router'; import { ActivatedRoute, Router, RouterLink, RouterLinkActive, RouterOutlet } from '@angular/router';
@ -45,8 +45,6 @@ import { CustomViewEditorComponent } from './custom-view-editor.component';
ListViewComponent, ListViewComponent,
ModalDirective, ModalDirective,
ModalPlacementDirective, ModalPlacementDirective,
NgFor,
NgIf,
NotifoComponent, NotifoComponent,
PagerComponent, PagerComponent,
RouterLink, RouterLink,
@ -300,10 +298,6 @@ export class ContentsPageComponent implements OnInit {
} }
} }
public trackByContent(_index: number, content: ContentDto): string {
return content.id;
}
private selectItems(predicate?: (content: ContentDto) => boolean) { private selectItems(predicate?: (content: ContentDto) => boolean) {
return this.contentsState.snapshot.contents.filter(c => this.selectedItems[c.id] && (!predicate || predicate(c))); return this.contentsState.snapshot.contents.filter(c => this.selectedItems[c.id] && (!predicate || predicate(c)));
} }

94
frontend/src/app/features/content/pages/contents/custom-view-editor.component.html

@ -1,52 +1,62 @@
<div class="container"> <div class="container">
<div class="header"> <div class="header">
<button type="button" class="btn btn-secondary btn-sm" (click)="resetDefault()"> <button class="btn btn-secondary btn-sm" (click)="resetDefault()" type="button">
{{ 'contents.viewReset' | sqxTranslate }} {{ "contents.viewReset" | sqxTranslate }}
</button> </button>
</div> </div>
<hr>
<div <hr />
cdkDropList
[cdkDropListData]="listFields" <div cdkDropList [cdkDropListData]="listFields" (cdkDropListDropped)="drop($event)">
(cdkDropListDropped)="drop($event)"> @for (field of listFields; track field) {
<div *ngFor="let field of listFields" cdkDrag> <div cdkDrag>
<i class="icon-drag2 drag-handle"></i> <i class="icon-drag2 drag-handle"></i>
<div class="form-check">
<input class="form-check-input" type="checkbox" checked (click)="removeField(field)" id="field_{{field}}" [disabled]="!field">
<label class="form-check-label" for="field_{{field}}">
<span *ngIf="field.name">
{{(field.title || field.label) | sqxTranslate}}: <code>{{field.name}}</code>
</span>
<span class="text-muted" *ngIf="!field.name">
- Placeholder -
</span>
</label>
</div>
</div>
</div>
<ng-container *ngIf="fieldsNotAdded.length > 0">
<hr>
<div >
<div *ngFor="let field of fieldsNotAdded">
<i class="icon-drag2 drag-handle invisible"></i>
<div class="form-check"> <div class="form-check">
<input class="form-check-input" type="checkbox" (click)="addField(field)" id="field_{{field}}"> <input
<label class="form-check-label" for="field_{{field}}"> class="form-check-input"
<span *ngIf="field.name"> id="field_{{ field }}"
{{(field.title || field.label) | sqxTranslate}}: <code>{{field.name}}</code> checked
</span> (click)="removeField(field)"
<span class="text-muted" *ngIf="!field.name"> [disabled]="!field"
- Placeholder - type="checkbox" />
</span> <label class="form-check-label" for="field_{{ field }}">
@if (field.name) {
<span>
{{ field.title || field.label | sqxTranslate }}:
<code>{{ field.name }}</code>
</span>
}
@if (!field.name) {
<span class="text-muted">- Placeholder -</span>
}
</label> </label>
</div> </div>
</div> </div>
}
</div>
@if (fieldsNotAdded.length > 0) {
<hr />
<div>
@for (field of fieldsNotAdded; track field) {
<div>
<i class="icon-drag2 drag-handle invisible"></i>
<div class="form-check">
<input class="form-check-input" id="field_{{ field }}" (click)="addField(field)" type="checkbox" />
<label class="form-check-label" for="field_{{ field }}">
@if (field.name) {
<span>
{{ field.title || field.label | sqxTranslate }}:
<code>{{ field.name }}</code>
</span>
}
@if (!field.name) {
<span class="text-muted">- Placeholder -</span>
}
</label>
</div>
</div>
}
</div> </div>
</ng-container> }
</div> </div>

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

@ -6,7 +6,7 @@
*/ */
import { CdkDrag, CdkDragDrop, CdkDropList, moveItemInArray } from '@angular/cdk/drag-drop'; import { CdkDrag, CdkDragDrop, CdkDropList, moveItemInArray } from '@angular/cdk/drag-drop';
import { NgFor, NgIf } from '@angular/common';
import { ChangeDetectionStrategy, Component, EventEmitter, Input, Output } from '@angular/core'; import { ChangeDetectionStrategy, Component, EventEmitter, Input, Output } from '@angular/core';
import { TableField, TranslatePipe } from '@app/shared'; import { TableField, TranslatePipe } from '@app/shared';
@ -19,8 +19,6 @@ import { TableField, TranslatePipe } from '@app/shared';
imports: [ imports: [
CdkDrag, CdkDrag,
CdkDropList, CdkDropList,
NgFor,
NgIf,
TranslatePipe, TranslatePipe,
], ],
}) })

78
frontend/src/app/features/content/pages/references/references-page.component.html

@ -1,52 +1,64 @@
<sqx-title message="i18n:common.references"></sqx-title> <sqx-title message="i18n:common.references"></sqx-title>
<sqx-layout layout="main" titleText="i18n:common.references" titleIcon="contents"> <sqx-layout layout="main" titleIcon="contents" titleText="i18n:common.references">
<ng-container menu> <ng-container menu>
<div class="row flex-nowrap flex-grow-1 gx-2"> <div class="row flex-nowrap flex-grow-1 gx-2">
<div class="col-auto ms-8"> <div class="col-auto ms-8">
<button type="button" class="btn btn-text-secondary" (click)="reload()" title="i18n:contents.refreshTooltip" shortcut="CTRL + B"> <button
<i class="icon-reset"></i> {{ 'common.refresh' | sqxTranslate }} class="btn btn-text-secondary"
(click)="reload()"
shortcut="CTRL + B"
title="i18n:contents.refreshTooltip"
type="button">
<i class="icon-reset"></i>
{{ "common.refresh" | sqxTranslate }}
</button> </button>
</div> </div>
<div class="col-auto" *ngIf="languages.length > 1"> @if (languages.length > 1) {
<sqx-language-selector class="languages-buttons" <div class="col-auto">
(languageChange)="changeLanguage($event)" <sqx-language-selector
[language]="language" class="languages-buttons"
[languages]="languages"> [language]="language"
</sqx-language-selector> (languageChange)="changeLanguage($event)"
</div> [languages]="languages"></sqx-language-selector>
</div>
}
</div> </div>
</ng-container> </ng-container>
<ng-container> <ng-container>
<sqx-list-view [isLoading]="contentsState.isLoading | async" table="true"> <sqx-list-view [isLoading]="contentsState.isLoading | async" table="true">
<ng-container> <ng-container>
<table class="table table-items table-fixed" *ngIf="contentsState.contents | async; let contents"> @if (contentsState.contents | async; as contents) {
<tbody *ngFor="let content of contents; trackBy: trackByContent" <table class="table table-items table-fixed">
[sqxReferenceItem]="content" @for (content of contents; track content.id) {
[canRemove]="false" <tbody
[columns]="contents | sqxContentsColumns" [canRemove]="false"
[isCompact]="false" [columns]="contents | sqxContentsColumns"
[isDisabled]="false" [isCompact]="false"
[language]="language" [isDisabled]="false"
[languages]="languages" [language]="language"
[validations]="(contentsState.validationResults | async)!" [languages]="languages"
[validityVisible]="true"> [sqxReferenceItem]="content"
</tbody> [validations]="(contentsState.validationResults | async)!"
[validityVisible]="true"></tbody>
<tbody *ngIf="(contentsState.isLoaded | async) && contents.length === 0"> }
<tr> @if ((contentsState.isLoaded | async) && contents.length === 0) {
<td class="table-items-row-empty"> <tbody>
{{ 'contents.noReferencing' | sqxTranslate }} <tr>
</td> <td class="table-items-row-empty">
</tr> {{ "contents.noReferencing" | sqxTranslate }}
</tbody> </td>
</table> </tr>
</tbody>
}
</table>
}
</ng-container> </ng-container>
<ng-container footer> <ng-container footer>
<sqx-pager [paging]="contentsState.paging | async" (pagingChange)="contentsState.page($event)"></sqx-pager> <sqx-pager [paging]="contentsState.paging | async" (pagingChange)="contentsState.page($event)"></sqx-pager>
</ng-container> </ng-container>
</sqx-list-view> </sqx-list-view>
</ng-container> </ng-container>
</sqx-layout> </sqx-layout>

10
frontend/src/app/features/content/pages/references/references-page.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 { AsyncPipe, NgFor, NgIf } from '@angular/common'; import { AsyncPipe } from '@angular/common';
import { ChangeDetectionStrategy, Component, OnInit } from '@angular/core'; import { ChangeDetectionStrategy, Component, OnInit } from '@angular/core';
import { ActivatedRoute } from '@angular/router'; import { ActivatedRoute } from '@angular/router';
import { distinctUntilChanged, map } from 'rxjs/operators'; import { distinctUntilChanged, map } from 'rxjs/operators';
import { AppLanguageDto, ComponentContentsState, ContentDto, ContentsColumnsPipe, LanguageSelectorComponent, LanguagesState, LayoutComponent, ListViewComponent, PagerComponent, QuerySynchronizer, Router2State, ShortcutDirective, Subscriptions, TitleComponent, TooltipDirective, TranslatePipe } from '@app/shared'; import { AppLanguageDto, ComponentContentsState, ContentsColumnsPipe, LanguageSelectorComponent, LanguagesState, LayoutComponent, ListViewComponent, PagerComponent, QuerySynchronizer, Router2State, ShortcutDirective, Subscriptions, TitleComponent, TooltipDirective, TranslatePipe } from '@app/shared';
import { ReferenceItemComponent } from '../../shared/references/reference-item.component'; import { ReferenceItemComponent } from '../../shared/references/reference-item.component';
@Component({ @Component({
@ -28,8 +28,6 @@ import { ReferenceItemComponent } from '../../shared/references/reference-item.c
LanguageSelectorComponent, LanguageSelectorComponent,
LayoutComponent, LayoutComponent,
ListViewComponent, ListViewComponent,
NgFor,
NgIf,
PagerComponent, PagerComponent,
ReferenceItemComponent, ReferenceItemComponent,
ShortcutDirective, ShortcutDirective,
@ -87,10 +85,6 @@ export class ReferencesPageComponent implements OnInit {
public changeLanguage(language: AppLanguageDto) { public changeLanguage(language: AppLanguageDto) {
this.language = language; this.language = language;
} }
public trackByContent(_index: number, content: ContentDto) {
return content.id;
}
} }
function getReferenceId(route: ActivatedRoute) { function getReferenceId(route: ActivatedRoute) {

49
frontend/src/app/features/content/pages/schemas/schemas-page.component.html

@ -1,30 +1,31 @@
<sqx-title message="i18n:contents.schemasPageTitle"></sqx-title> <sqx-title message="i18n:contents.schemasPageTitle"></sqx-title>
<sqx-layout layout="left" titleCollapsed="i18n:common.schemas" width="18" white="true" padding="true" overflow="true" *ngIf="!isEmbedded"> @if (!isEmbedded) {
<ng-container menu> <sqx-layout layout="left" overflow="true" padding="true" titleCollapsed="i18n:common.schemas" white="true" width="18">
<div class="search-form"> <ng-container menu>
<input class="form-control" [formControl]="schemasFilter" placeholder="{{ 'contents.searchSchemasPlaceholder' | sqxTranslate }}"> <div class="search-form">
<input
<i class="icon-search"></i> class="form-control"
</div> [formControl]="schemasFilter"
</ng-container> placeholder="{{ 'contents.searchSchemasPlaceholder' | sqxTranslate }}" />
<i class="icon-search"></i>
<ng-container> </div>
<ul class="nav nav-light mb-2 flex-column"> </ng-container>
<li class="nav-item">
<a class="nav-link" routerLink="__calendar" routerLinkActive="active">
{{ 'contents.calendar' | sqxTranslate }}
</a>
</li>
</ul>
<ng-container> <ng-container>
<sqx-schema-category *ngFor="let category of categories | async; trackBy: trackByCategory" <ul class="nav nav-light mb-2 flex-column">
[schemaCategory]="category" <li class="nav-item">
[schemaTarget]="'Contents'"> <a class="nav-link" routerLink="__calendar" routerLinkActive="active">
</sqx-schema-category> {{ "contents.calendar" | sqxTranslate }}
</a>
</li>
</ul>
<ng-container>
@for (category of categories | async; track category.name) {
<sqx-schema-category [schemaCategory]="category" [schemaTarget]="'Contents'"></sqx-schema-category>
}
</ng-container>
</ng-container> </ng-container>
</ng-container> </sqx-layout>
</sqx-layout> }
<router-outlet></router-outlet> <router-outlet></router-outlet>

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

@ -5,13 +5,13 @@
* Copyright (c) Squidex UG (haftungsbeschränkt). All rights reserved. * Copyright (c) Squidex UG (haftungsbeschränkt). All rights reserved.
*/ */
import { AsyncPipe, NgFor, NgIf } from '@angular/common'; import { AsyncPipe } from '@angular/common';
import { Component, inject } from '@angular/core'; import { Component, inject } from '@angular/core';
import { FormsModule, ReactiveFormsModule, UntypedFormControl } from '@angular/forms'; import { FormsModule, ReactiveFormsModule, UntypedFormControl } from '@angular/forms';
import { RouterLink, RouterLinkActive, RouterOutlet } from '@angular/router'; import { RouterLink, RouterLinkActive, RouterOutlet } from '@angular/router';
import { combineLatest } from 'rxjs'; import { combineLatest } from 'rxjs';
import { map } from 'rxjs/operators'; import { map } from 'rxjs/operators';
import { AppsState, getCategoryTree, LayoutComponent, SchemaCategory, SchemaCategoryComponent, SchemasState, Settings, TitleComponent, TranslatePipe, UIOptions, value$ } from '@app/shared'; import { AppsState, getCategoryTree, LayoutComponent, SchemaCategoryComponent, SchemasState, Settings, TitleComponent, TranslatePipe, UIOptions, value$ } from '@app/shared';
@Component({ @Component({
standalone: true, standalone: true,
@ -22,8 +22,6 @@ import { AppsState, getCategoryTree, LayoutComponent, SchemaCategory, SchemaCate
AsyncPipe, AsyncPipe,
FormsModule, FormsModule,
LayoutComponent, LayoutComponent,
NgFor,
NgIf,
ReactiveFormsModule, ReactiveFormsModule,
RouterLink, RouterLink,
RouterLinkActive, RouterLinkActive,
@ -32,7 +30,7 @@ import { AppsState, getCategoryTree, LayoutComponent, SchemaCategory, SchemaCate
TitleComponent, TitleComponent,
TranslatePipe, TranslatePipe,
], ],
}) })
export class SchemasPageComponent { export class SchemasPageComponent {
public schemasFilter = new UntypedFormControl(); public schemasFilter = new UntypedFormControl();
@ -63,8 +61,4 @@ export class SchemasPageComponent {
private readonly appsState: AppsState, private readonly appsState: AppsState,
) { ) {
} }
public trackByCategory(_index: number, category: SchemaCategory) {
return category.name;
}
} }

11
frontend/src/app/features/content/pages/sidebar/sidebar-page.component.html

@ -1,12 +1,7 @@
<sqx-layout layout="right" titleText="i18n:common.sidebar" width="20" white="true"> <sqx-layout layout="right" titleText="i18n:common.sidebar" white="true" width="20">
<sqx-content-extension <sqx-content-extension
[editorUrl]="url | async"
[contentItem]="contentsState.selectedContent | async" [contentItem]="contentsState.selectedContent | async"
[contentSchema]="(schemasState.selectedSchema | async)!" [contentSchema]="(schemasState.selectedSchema | async)!"
scrollable="true"> [editorUrl]="url | async"
</sqx-content-extension> scrollable="true"></sqx-content-extension>
</sqx-layout> </sqx-layout>

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

@ -1 +1 @@
<iframe #iframe [attr.scrollable]="scrollable ? 'yes' : 'no'" width="100%" [attr.src]="computedUrl | sqxSafeResourceUrl"></iframe> <iframe #iframe [attr.scrollable]="scrollable ? 'yes' : 'no'" [attr.src]="computedUrl | sqxSafeResourceUrl" width="100%"></iframe>

38
frontend/src/app/features/content/shared/due-time-selector.component.html

@ -1,33 +1,49 @@
<sqx-modal-dialog *sqxModal="dueTimeDialog" (dialogClose)="cancelStatusChange()"> <sqx-modal-dialog (dialogClose)="cancelStatusChange()" *sqxModal="dueTimeDialog">
<ng-container title> <ng-container title>
{{ 'contents.changeStatusTo' | sqxTranslate: { action: dueTimeAction } }} {{ "contents.changeStatusTo" | sqxTranslate: { action: dueTimeAction } }}
</ng-container> </ng-container>
<ng-container content> <ng-container content>
<div class="form-check"> <div class="form-check">
<input class="form-check-input" type="radio" [(ngModel)]="dueTimeMode" value="Immediately" id="immediately" name="dueTimeMode"> <input
class="form-check-input"
id="immediately"
name="dueTimeMode"
[(ngModel)]="dueTimeMode"
type="radio"
value="Immediately" />
<label class="form-check-label" for="immediately"> <label class="form-check-label" for="immediately">
{{ 'contents.changeStatusToImmediately' | sqxTranslate: { action: dueTimeAction } }} {{ "contents.changeStatusToImmediately" | sqxTranslate: { action: dueTimeAction } }}
</label> </label>
</div> </div>
<div class="form-check"> <div class="form-check">
<input class="form-check-input" type="radio" [(ngModel)]="dueTimeMode" value="Scheduled" id="scheduled" name="dueTimeMode"> <input class="form-check-input" id="scheduled" name="dueTimeMode" [(ngModel)]="dueTimeMode" type="radio" value="Scheduled" />
<label class="form-check-label" for="scheduled"> <label class="form-check-label" for="scheduled">
{{ 'contents.changeStatusToLater' | sqxTranslate: { action: dueTimeAction } }} {{ "contents.changeStatusToLater" | sqxTranslate: { action: dueTimeAction } }}
</label> </label>
</div> </div>
<sqx-date-time-editor [disabled]="dueTimeMode === 'Immediately'" enforceTime="true" mode="DateTime" hideClear="true" [(ngModel)]="dueTime"></sqx-date-time-editor> <sqx-date-time-editor
[disabled]="dueTimeMode === 'Immediately'"
enforceTime="true"
hideClear="true"
mode="DateTime"
[(ngModel)]="dueTime"></sqx-date-time-editor>
</ng-container> </ng-container>
<ng-container footer> <ng-container footer>
<button type="button" class="btn btn-text-secondary" (click)="cancelStatusChange()"> <button class="btn btn-text-secondary" (click)="cancelStatusChange()" type="button">
{{ 'common.cancel' | sqxTranslate }} {{ "common.cancel" | sqxTranslate }}
</button> </button>
<button type="button" class="btn btn-primary" [disabled]="dueTimeMode === 'Scheduled' && !dueTime" (click)="confirmStatusChange()" sqxFocusOnInit> <button
{{ 'common.confirm' | sqxTranslate }} class="btn btn-primary"
(click)="confirmStatusChange()"
[disabled]="dueTimeMode === 'Scheduled' && !dueTime"
sqxFocusOnInit
type="button">
{{ "common.confirm" | sqxTranslate }}
</button> </button>
</ng-container> </ng-container>
</sqx-modal-dialog> </sqx-modal-dialog>

233
frontend/src/app/features/content/shared/forms/array-editor.component.html

@ -1,122 +1,139 @@
<ng-container *ngIf="formModel.itemChanges | async; let items"> @if (formModel.itemChanges | async; as items) {
<div class="array-container static" [class.expanded]="isExpanded" *ngIf="items.length > 0 && items.length <= 20;" @if (items.length > 0 && items.length <= 20) {
cdkDropList <div
[cdkDropListSortingDisabled]="isDisabled | async" class="array-container static"
[cdkDropListDisabled]="isDisabled | async" cdkDropList
[cdkDropListData]="items" [cdkDropListData]="items"
(cdkDropListDropped)="sort($event)"> [cdkDropListDisabled]="isDisabled | async"
<div *ngFor="let itemForm of items; index as i; last as isLast; first as isFirst;" class="table-drag item" (cdkDropListDropped)="sort($event)"
cdkDrag [cdkDropListSortingDisabled]="isDisabled | async"
cdkDragLockAxis="y" [class.expanded]="isExpanded">
[class.first]="isFirst" @for (itemForm of items; track itemForm; let i = $index; let isLast = $last; let isFirst = $first) {
[class.last]="isLast"> <div class="table-drag item" cdkDrag cdkDragLockAxis="y" [class.first]="isFirst" [class.last]="isLast">
<sqx-array-item <sqx-array-item
(clone)="addCopy(itemForm)" (clone)="addCopy(itemForm)"
[form]="form" [form]="form"
[formContext]="formContext" [formContext]="formContext"
[formLevel]="formLevel + 1" [formLevel]="formLevel + 1"
[formModel]="itemForm" [formModel]="itemForm"
[index]="i" [hasChatBot]="hasChatBot"
[isComparing]="isComparing" [index]="i"
[isCollapsedInitial]="isCollapsedInitial" [isCollapsedInitial]="isCollapsedInitial"
[isDisabled]="isDisabled | async" [isComparing]="isComparing"
[isFirst]="isFirst" [isDisabled]="isDisabled | async"
[isLast]="isLast" [isFirst]="isFirst"
(itemRemove)="removeItem(i)" [isLast]="isLast"
(itemMove)="move(itemForm, $event)" (itemMove)="move(itemForm, $event)"
[hasChatBot]="hasChatBot" (itemRemove)="removeItem(i)"
[language]="language" [language]="language"
[languages]="languages"> [languages]="languages">
<i cdkDragHandle class="icon-drag2" [class.disabled]="isDisabled | async"></i> <i class="icon-drag2" cdkDragHandle [class.disabled]="isDisabled | async"></i>
</sqx-array-item> </sqx-array-item>
</div>
}
</div> </div>
</div> }
@if (items.length > 20) {
<div class="array-container" [class.expanded]="isExpanded" *ngIf="items.length > 20"> <div class="array-container" [class.expanded]="isExpanded">
<virtual-scroller #scroll [items]="$any(items)" [enableUnequalChildrenSizes]="true"> <virtual-scroller #scroll [enableUnequalChildrenSizes]="true" [items]="$any(items)">
<div *ngFor="let itemForm of scroll.viewPortItems; index as i" class="item" @for (itemForm of scroll.viewPortItems; track itemForm; let i = $index) {
[class.first]="scroll.viewPortInfo.startIndexWithBuffer + i === 0" <div
[class.last]="scroll.viewPortInfo.startIndexWithBuffer + i === items.length - 1"> class="item"
<sqx-array-item [class.first]="scroll.viewPortInfo.startIndexWithBuffer + i === 0"
(clone)="addCopy(itemForm)" [class.last]="scroll.viewPortInfo.startIndexWithBuffer + i === items.length - 1">
[form]="form" <sqx-array-item
[formContext]="formContext" (clone)="addCopy(itemForm)"
[formLevel]="formLevel + 1" [form]="form"
[formModel]="itemForm" [formContext]="formContext"
[index]="scroll.viewPortInfo.startIndexWithBuffer + i" [formLevel]="formLevel + 1"
[isCollapsedInitial]="isCollapsedInitial" [formModel]="itemForm"
[isComparing]="isComparing" [hasChatBot]="hasChatBot"
[isDisabled]="isDisabled | async" [index]="scroll.viewPortInfo.startIndexWithBuffer + i"
[isFirst]="scroll.viewPortInfo.startIndexWithBuffer + i === 0" [isCollapsedInitial]="isCollapsedInitial"
[isLast]="scroll.viewPortInfo.startIndexWithBuffer + i === items.length - 1" [isComparing]="isComparing"
(itemExpanded)="scroll.invalidateCachedMeasurementAtIndex(scroll.viewPortInfo.startIndexWithBuffer + i)" [isDisabled]="isDisabled | async"
(itemRemove)="removeItem(scroll.viewPortInfo.startIndexWithBuffer + i)" [isFirst]="scroll.viewPortInfo.startIndexWithBuffer + i === 0"
(itemMove)="move(itemForm, $event)" [isLast]="scroll.viewPortInfo.startIndexWithBuffer + i === items.length - 1"
[hasChatBot]="hasChatBot" (itemExpanded)="scroll.invalidateCachedMeasurementAtIndex(scroll.viewPortInfo.startIndexWithBuffer + i)"
[language]="language" (itemMove)="move(itemForm, $event)"
[languages]="languages"> (itemRemove)="removeItem(scroll.viewPortInfo.startIndexWithBuffer + i)"
</sqx-array-item> [language]="language"
</div> [languages]="languages"></sqx-array-item>
</virtual-scroller> </div>
</div> }
</virtual-scroller>
</div>
}
<div class="array-buttons row g-0 align-items-center" [class.expanded]="isExpanded"> <div class="array-buttons row g-0 align-items-center" [class.expanded]="isExpanded">
<div class="col-auto"> <div class="col-auto">
<ng-container *ngIf="isArray; else component"> @if (isArray) {
<ng-container *ngIf="hasField"> @if (hasField) {
<button type="button" class="btn btn-outline-success" [disabled]="isDisabledOrFull | async" (click)="addItem()"> <button class="btn btn-outline-success" (click)="addItem()" [disabled]="isDisabledOrFull | async" type="button">
{{ 'contents.arrayAddItem' | sqxTranslate }} {{ "contents.arrayAddItem" | sqxTranslate }}
</button> </button>
</ng-container> }
<ng-container *ngIf="!hasField"> @if (!hasField) {
<sqx-form-hint> <sqx-form-hint>
{{ 'contents.arrayNoFields' | sqxTranslate }} {{ "contents.arrayNoFields" | sqxTranslate }}
</sqx-form-hint> </sqx-form-hint>
</ng-container> }
</ng-container> } @else {
@if (schemasList.length > 1) {
<ng-template #component> <button
<ng-container *ngIf="schemasList.length > 1"> class="btn btn-outline-success dropdown-toggle"
<button type="button" class="btn btn-outline-success dropdown-toggle" [disabled]="isDisabledOrFull | async" (click)="schemasDropdown.show()" #buttonSelect> #buttonSelect
{{ 'contents.addComponent' | sqxTranslate}} (click)="schemasDropdown.show()"
[disabled]="isDisabledOrFull | async"
type="button">
{{ "contents.addComponent" | sqxTranslate }}
</button> </button>
<sqx-dropdown-menu scrollY="true" [sqxAnchoredTo]="buttonSelect" *sqxModal="schemasDropdown; closeAlways: true">
<sqx-dropdown-menu *sqxModal="schemasDropdown;closeAlways:true" [sqxAnchoredTo]="buttonSelect" scrollY="true"> @for (schema of schemasList; track schema) {
<a class="dropdown-item" *ngFor="let schema of schemasList" (click)="addComponent(schema)"> <a class="dropdown-item" (click)="addComponent(schema)">
{{schema.displayName}} {{ schema.displayName }}
</a> </a>
}
</sqx-dropdown-menu> </sqx-dropdown-menu>
</ng-container> }
<ng-container *ngIf="schemasList.length === 1"> @if (schemasList.length === 1) {
<button type="button" class="btn btn-outline-success" [disabled]="isDisabledOrFull | async" (click)="addComponent(schemasList[0])"> <button
{{ 'contents.addComponent' | sqxTranslate}} class="btn btn-outline-success"
(click)="addComponent(schemasList[0])"
[disabled]="isDisabledOrFull | async"
type="button">
{{ "contents.addComponent" | sqxTranslate }}
</button> </button>
</ng-container> }
<ng-container *ngIf="schemasList.length === 0"> @if (schemasList.length === 0) {
<sqx-form-hint> <sqx-form-hint>
{{ 'contents.componentsNoSchema' | sqxTranslate }} {{ "contents.componentsNoSchema" | sqxTranslate }}
</sqx-form-hint> </sqx-form-hint>
</ng-container> }
</ng-template> }
</div>
<div class="col">
<button type="button" class="btn btn-text-danger ms-2" *ngIf="items.length > 0" [disabled]="isDisabled | async"
(sqxConfirmClick)="clear()"
confirmTitle="i18n:contents.arrayClearConfirmTitle"
confirmText="i18n:contents.arrayClearConfirmText"
confirmRememberKey="leaveApp">
{{ 'contents.arrayClear' | sqxTranslate }}
</button>
</div> </div>
<div class="col">
<div class="col-auto" *ngIf="items.length > 0"> @if (items.length > 0) {
<button type="button" class="btn btn-text-secondary" (click)="expandAll()" title="i18n:contents.arrayExpandAll"> <button
<i class="icon-plus-square"></i> class="btn btn-text-danger ms-2"
</button> confirmRememberKey="leaveApp"
<button type="button" class="btn btn-text-secondary" (click)="collapseAll()" title="i18n:contents.arrayCollapseAll"> confirmText="i18n:contents.arrayClearConfirmText"
<i class="icon-minus-square"></i> confirmTitle="i18n:contents.arrayClearConfirmTitle"
</button> [disabled]="isDisabled | async"
(sqxConfirmClick)="clear()"
type="button">
{{ "contents.arrayClear" | sqxTranslate }}
</button>
}
</div> </div>
@if (items.length > 0) {
<div class="col-auto">
<button class="btn btn-text-secondary" (click)="expandAll()" title="i18n:contents.arrayExpandAll" type="button">
<i class="icon-plus-square"></i>
</button>
<button class="btn btn-text-secondary" (click)="collapseAll()" title="i18n:contents.arrayCollapseAll" type="button">
<i class="icon-minus-square"></i>
</button>
</div>
}
</div> </div>
</ng-container> }

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

@ -6,7 +6,7 @@
*/ */
import { CdkDrag, CdkDragDrop, CdkDragHandle, CdkDropList } from '@angular/cdk/drag-drop'; import { CdkDrag, CdkDragDrop, CdkDragHandle, CdkDropList } from '@angular/cdk/drag-drop';
import { AsyncPipe, NgFor, NgIf } from '@angular/common'; import { AsyncPipe } from '@angular/common';
import { booleanAttribute, ChangeDetectionStrategy, Component, Input, numberAttribute, QueryList, ViewChildren } from '@angular/core'; import { booleanAttribute, ChangeDetectionStrategy, Component, Input, numberAttribute, QueryList, ViewChildren } from '@angular/core';
import { VirtualScrollerComponent, VirtualScrollerModule } from '@iharbeck/ngx-virtual-scroller'; import { VirtualScrollerComponent, VirtualScrollerModule } from '@iharbeck/ngx-virtual-scroller';
import { combineLatest, Observable } from 'rxjs'; import { combineLatest, Observable } from 'rxjs';
@ -30,8 +30,6 @@ import { ArrayItemComponent } from './array-item.component';
FormHintComponent, FormHintComponent,
ModalDirective, ModalDirective,
ModalPlacementDirective, ModalPlacementDirective,
NgFor,
NgIf,
TooltipDirective, TooltipDirective,
TranslatePipe, TranslatePipe,
VirtualScrollerModule, VirtualScrollerModule,

98
frontend/src/app/features/content/shared/forms/array-item.component.html

@ -6,59 +6,97 @@
</div> </div>
<div class="col"> <div class="col">
<div class="truncate"> <div class="truncate">
<span class="header-index">#{{index + 1}}</span> <span class="header-index">#{{ index + 1 }}</span>
<span class="header-title">{{title | async }}</span> <span class="header-title">{{ title | async }}</span>
</div> </div>
</div> </div>
<div class="col-auto pe-4"> <div class="col-auto pe-4">
<button type="button" class="btn btn-text-secondary" [disabled]="isDisabled || isFirst" (click)="moveTop()" title="i18n:contents.arrayMoveTop"> <button
class="btn btn-text-secondary"
(click)="moveTop()"
[disabled]="isDisabled || isFirst"
title="i18n:contents.arrayMoveTop"
type="button">
<i class="icon-caret-top"></i> <i class="icon-caret-top"></i>
</button> </button>
<button type="button" class="btn btn-text-secondary" [disabled]="isDisabled || isFirst" (click)="moveUp()" title="i18n:contents.arrayMoveUp"> <button
class="btn btn-text-secondary"
(click)="moveUp()"
[disabled]="isDisabled || isFirst"
title="i18n:contents.arrayMoveUp"
type="button">
<i class="icon-caret-up"></i> <i class="icon-caret-up"></i>
</button> </button>
<button type="button" class="btn btn-text-secondary" [disabled]="isDisabled || isLast" (click)="moveDown()" title="i18n:contents.arrayMoveDown"> <button
class="btn btn-text-secondary"
(click)="moveDown()"
[disabled]="isDisabled || isLast"
title="i18n:contents.arrayMoveDown"
type="button">
<i class="icon-caret-down"></i> <i class="icon-caret-down"></i>
</button> </button>
<button type="button" class="btn btn-text-secondary" [disabled]="isDisabled || isLast" (click)="moveBottom()" title="i18n:contents.arrayMoveBottom"> <button
class="btn btn-text-secondary"
(click)="moveBottom()"
[disabled]="isDisabled || isLast"
title="i18n:contents.arrayMoveBottom"
type="button">
<i class="icon-caret-bottom"></i> <i class="icon-caret-bottom"></i>
</button> </button>
<button type="button" class="btn btn-text-secondary" [class.hidden]="!(isCollapsed | async)" (click)="expand()" title="i18n:contents.arrayExpandItem"> <button
class="btn btn-text-secondary"
[class.hidden]="!(isCollapsed | async)"
(click)="expand()"
title="i18n:contents.arrayExpandItem"
type="button">
<i class="icon-plus-square"></i> <i class="icon-plus-square"></i>
</button> </button>
<button type="button" class="btn btn-text-secondary" [class.hidden]="isCollapsed | async" (click)="collapse()" title="i18n:contents.arrayCollapseItem"> <button
class="btn btn-text-secondary"
[class.hidden]="isCollapsed | async"
(click)="collapse()"
title="i18n:contents.arrayCollapseItem"
type="button">
<i class="icon-minus-square"></i> <i class="icon-minus-square"></i>
</button> </button>
</div> </div>
<div class="col-auto"> <div class="col-auto">
<button type="button" class="btn btn-text-secondary" [disabled]="isDisabled" (click)="clone.emit()" title="i18n:contents.arrayCloneItem"> <button
class="btn btn-text-secondary"
(click)="clone.emit()"
[disabled]="isDisabled"
title="i18n:contents.arrayCloneItem"
type="button">
<i class="icon-clone"></i> <i class="icon-clone"></i>
</button> </button>
<button type="button" class="btn btn-text-danger" [disabled]="isDisabled" (click)="itemRemove.emit()"> <button class="btn btn-text-danger" (click)="itemRemove.emit()" [disabled]="isDisabled" type="button">
<i class="icon-bin2"></i> <i class="icon-bin2"></i>
</button> </button>
</div> </div>
</div> </div>
</div> </div>
<div class="card-body" *sqxIfOnce="!(isCollapsed | async)" [class.hidden]="isCollapsed | async">
<div class="form-group" *ngFor="let section of formModel.sectionsChanges | async">
<sqx-component-section
[form]="form"
[formContext]="formContext"
[formLevel]="formLevel"
[formSection]="$any(section)"
[index]="index"
[isComparing]="isComparing"
[hasChatBot]="hasChatBot"
[language]="language"
[languages]="languages">
</sqx-component-section>
</div>
<sqx-form-hint *ngIf="isInvalidComponent | async"> <div class="card-body" [class.hidden]="isCollapsed | async" *sqxIfOnce="!(isCollapsed | async)">
{{ 'contents.componentInvalid' | sqxTranslate }} @for (section of formModel.sectionsChanges | async; track section) {
</sqx-form-hint> <div class="form-group">
<sqx-component-section
[form]="form"
[formContext]="formContext"
[formLevel]="formLevel"
[formSection]="$any(section)"
[hasChatBot]="hasChatBot"
[index]="index"
[isComparing]="isComparing"
[language]="language"
[languages]="languages"></sqx-component-section>
</div>
}
@if (isInvalidComponent | async) {
<sqx-form-hint>
{{ "contents.componentInvalid" | sqxTranslate }}
</sqx-form-hint>
}
</div> </div>
</div> </div>

10
frontend/src/app/features/content/shared/forms/array-item.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 { AsyncPipe, NgFor, NgIf } from '@angular/common'; import { AsyncPipe } from '@angular/common';
import { booleanAttribute, ChangeDetectionStrategy, Component, EventEmitter, Input, numberAttribute, Output, QueryList, ViewChildren } from '@angular/core'; import { booleanAttribute, ChangeDetectionStrategy, Component, EventEmitter, Input, numberAttribute, 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, FormHintComponent, IfOnceDirective, invalid$, ObjectFormBase, RootFieldDto, TooltipDirective, TranslatePipe, TypedSimpleChanges, Types, valueProjection$ } from '@app/shared'; import { AppLanguageDto, ComponentForm, EditContentForm, FieldDto, FieldFormatter, FormHintComponent, IfOnceDirective, invalid$, ObjectFormBase, RootFieldDto, TooltipDirective, TranslatePipe, TypedSimpleChanges, Types, valueProjection$ } from '@app/shared';
import { ComponentSectionComponent } from './component-section.component'; import { ComponentSectionComponent } from './component-section.component';
@Component({ @Component({
@ -23,8 +23,6 @@ import { ComponentSectionComponent } from './component-section.component';
ComponentSectionComponent, ComponentSectionComponent,
FormHintComponent, FormHintComponent,
IfOnceDirective, IfOnceDirective,
NgFor,
NgIf,
TooltipDirective, TooltipDirective,
TranslatePipe, TranslatePipe,
], ],
@ -146,10 +144,6 @@ export class ArrayItemComponent {
section.reset(); section.reset();
}); });
} }
public trackBySection(_index: number, section: FieldSection<FieldDto, any>) {
return section.separator?.fieldId;
}
} }
function getTitle(formModel: ObjectFormBase) { function getTitle(formModel: ObjectFormBase) {

154
frontend/src/app/features/content/shared/forms/assets-editor.component.html

@ -1,26 +1,43 @@
<div class="assets-container" <div
class="assets-container"
[class.expanded]="isExpanded" [class.expanded]="isExpanded"
(sqxDropFile)="addFiles($event)" [sqxDropDisabled]="snapshot.isDisabled"
[sqxDropDisabled]="snapshot.isDisabled" (sqxDropFile)="addFiles($event)"
tabindex="1000"> tabindex="1000">
<div class="header list"> <div class="header list">
<div class="row gx-2"> <div class="row gx-2">
<div class="col" [class.disabled]="snapshot.isDisabled"> <div class="col" [class.disabled]="snapshot.isDisabled">
<div class="drop-area align-items-center" (click)="assetsDialog.show()" (sqxDropFile)="addFiles($event)" [sqxDropDisabled]="snapshot.isDisabled"> <div
{{ 'contents.assetsUpload' | sqxTranslate }} class="drop-area align-items-center"
(click)="assetsDialog.show()"
[sqxDropDisabled]="snapshot.isDisabled"
(sqxDropFile)="addFiles($event)">
{{ "contents.assetsUpload" | sqxTranslate }}
</div> </div>
</div> </div>
<div class="col-auto" *ngIf="hasChatBot"> @if (hasChatBot) {
<button type="button" class="btn btn-outline-secondary force no-focus-shadow" (click)="chatDialog.show()" tabindex="-1"> <div class="col-auto">
AI <button class="btn btn-outline-secondary force no-focus-shadow" (click)="chatDialog.show()" tabindex="-1" type="button">
</button> AI
</div> </button>
</div>
}
<div class="col-auto"> <div class="col-auto">
<div class="btn-group"> <div class="btn-group">
<button type="button" class="btn btn-secondary btn-toggle" [class.btn-primary]="snapshot.isListView" [disabled]="snapshot.isListView" (click)="changeView(true)"> <button
class="btn btn-secondary btn-toggle"
[class.btn-primary]="snapshot.isListView"
(click)="changeView(true)"
[disabled]="snapshot.isListView"
type="button">
<i class="icon-list"></i> <i class="icon-list"></i>
</button> </button>
<button type="button" class="btn btn-secondary btn-toggle" [class.btn-primary]="!snapshot.isListView" [disabled]="!snapshot.isListView" (click)="changeView(false)"> <button
class="btn btn-secondary btn-toggle"
[class.btn-primary]="!snapshot.isListView"
(click)="changeView(false)"
[disabled]="!snapshot.isListView"
type="button">
<i class="icon-grid"></i> <i class="icon-grid"></i>
</button> </button>
</div> </div>
@ -28,75 +45,72 @@
</div> </div>
</div> </div>
<div class="body" (sqxResizeCondition)="setCompact($event)" sqxResizeMinWidth="600" sqxResizeMaxWidth="0"> <div class="body" (sqxResizeCondition)="setCompact($event)" sqxResizeMaxWidth="0" sqxResizeMinWidth="600">
<ng-container *ngIf="!snapshot.isListView; else listTemplate"> @if (!snapshot.isListView) {
<div class="row g-0"> <div class="row g-0">
<sqx-asset *ngFor="let file of snapshot.assetFiles" @for (file of snapshot.assetFiles; track file) {
[assetFile]="file" <sqx-asset
[isCompact]="snapshot.isCompact" [assetFile]="file"
[isDisabled]="snapshot.isDisabled" [folderId]="folderId"
[folderId]="folderId" [isCompact]="snapshot.isCompact"
(loadDone)="addAsset(file, $event)" [isDisabled]="snapshot.isDisabled"
(loadError)="removeLoadingAsset(file)"> (loadDone)="addAsset(file, $event)"
</sqx-asset> (loadError)="removeLoadingAsset(file)"></sqx-asset>
}
<sqx-asset *ngFor="let asset of snapshot.assetItems; trackBy: trackByAsset" @for (asset of snapshot.assetItems; track asset.id) {
[asset]="asset" <sqx-asset
(edit)="editStart($event)" [asset]="asset"
[isCompact]="snapshot.isCompact" (edit)="editStart($event)"
[isDisabled]="snapshot.isDisabled" [isCompact]="snapshot.isCompact"
removeMode="true" [isDisabled]="snapshot.isDisabled"
(remove)="removeLoadedAsset(asset)" (remove)="removeLoadedAsset(asset)"
(update)="notifyOthers(asset)"> removeMode="true"
</sqx-asset> (update)="notifyOthers(asset)"></sqx-asset>
}
</div> </div>
</ng-container> } @else {
<ng-template #listTemplate>
<div class="list-view"> <div class="list-view">
<sqx-asset *ngFor="let file of snapshot.assetFiles" @for (file of snapshot.assetFiles; track file) {
[assetFile]="file" <sqx-asset
[folderId]="folderId" [assetFile]="file"
[isCompact]="snapshot.isCompact" [folderId]="folderId"
[isDisabled]="snapshot.isDisabled" [isCompact]="snapshot.isCompact"
isListView="true" [isDisabled]="snapshot.isDisabled"
(loadDone)="addAsset(file, $event)" isListView="true"
(loadError)="removeLoadingAsset(file)"> (loadDone)="addAsset(file, $event)"
</sqx-asset> (loadError)="removeLoadingAsset(file)"></sqx-asset>
}
<div cdkDropList <div
[cdkDropListDisabled]="snapshot.isDisabled" cdkDropList
[cdkDropListData]="snapshot.assetItems" [cdkDropListData]="snapshot.assetItems"
[cdkDropListDisabled]="snapshot.isDisabled"
(cdkDropListDropped)="sortAssets($event)"> (cdkDropListDropped)="sortAssets($event)">
<div *ngFor="let asset of snapshot.assetItems; trackBy: trackByAsset" class="table-drag" cdkDrag cdkDragLockAxis="y"> @for (asset of snapshot.assetItems; track asset.id) {
<sqx-asset <div class="table-drag" cdkDrag cdkDragLockAxis="y">
[asset]="asset" <sqx-asset
(edit)="editStart($event)" [asset]="asset"
[isCompact]="snapshot.isCompact" (edit)="editStart($event)"
[isDisabled]="snapshot.isDisabled" [isCompact]="snapshot.isCompact"
isListView="true" [isDisabled]="snapshot.isDisabled"
[removeMode]="true" isListView="true"
(remove)="removeLoadedAsset(asset)" (remove)="removeLoadedAsset(asset)"
(update)="notifyOthers(asset)"> [removeMode]="true"
</sqx-asset> (update)="notifyOthers(asset)"></sqx-asset>
</div> </div>
}
</div> </div>
</div> </div>
</ng-template> }
</div> </div>
</div> </div>
<sqx-asset-selector *sqxModal="assetsDialog" <sqx-asset-selector (assetSelect)="selectAssets($event)" *sqxModal="assetsDialog"></sqx-asset-selector>
(assetSelect)="selectAssets($event)">
</sqx-asset-selector>
<sqx-asset-dialog *sqxModal="snapshot.editAsset;isDialog:true" <sqx-asset-dialog
[asset]="snapshot.editAsset!" [asset]="snapshot.editAsset!"
(assetReplaced)="notifyOthers($event)" (assetReplaced)="notifyOthers($event)"
(assetUpdated)="notifyOthers($event)" (assetUpdated)="notifyOthers($event)"
(dialogClose)="editDone()"> (dialogClose)="editDone()"
</sqx-asset-dialog> *sqxModal="snapshot.editAsset; isDialog: true"></sqx-asset-dialog>
<sqx-chat-dialog *sqxModal="chatDialog" configuration="image" copyMode="Image" <sqx-chat-dialog configuration="image" (contentSelect)="addAssetFromAI($event)" copyMode="Image" *sqxModal="chatDialog"></sqx-chat-dialog>
(contentSelect)="addAssetFromAI($event)">
</sqx-chat-dialog>

8
frontend/src/app/features/content/shared/forms/assets-editor.component.ts

@ -6,7 +6,7 @@
*/ */
import { CdkDrag, CdkDragDrop, CdkDropList } from '@angular/cdk/drag-drop'; import { CdkDrag, CdkDragDrop, CdkDropList } from '@angular/cdk/drag-drop';
import { NgFor, NgIf } from '@angular/common';
import { booleanAttribute, ChangeDetectionStrategy, Component, forwardRef, Input, OnInit } from '@angular/core'; import { booleanAttribute, ChangeDetectionStrategy, Component, forwardRef, Input, OnInit } from '@angular/core';
import { NG_VALUE_ACCESSOR } from '@angular/forms'; import { NG_VALUE_ACCESSOR } from '@angular/forms';
import { AssetComponent, AssetDialogComponent, AssetDto, AssetSelectorComponent, ChatDialogComponent, DialogModel, FileDropDirective, HTTP, LocalStoreService, MessageBus, ModalDirective, ResizedDirective, ResolveAssets, Settings, sorted, StatefulControlComponent, Subscriptions, TranslatePipe, Types } from '@app/shared'; import { AssetComponent, AssetDialogComponent, AssetDto, AssetSelectorComponent, ChatDialogComponent, DialogModel, FileDropDirective, HTTP, LocalStoreService, MessageBus, ModalDirective, ResizedDirective, ResolveAssets, Settings, sorted, StatefulControlComponent, Subscriptions, TranslatePipe, Types } from '@app/shared';
@ -58,8 +58,6 @@ interface State {
ChatDialogComponent, ChatDialogComponent,
FileDropDirective, FileDropDirective,
ModalDirective, ModalDirective,
NgFor,
NgIf,
ResizedDirective, ResizedDirective,
TranslatePipe, TranslatePipe,
], ],
@ -237,8 +235,4 @@ export class AssetsEditorComponent extends StatefulControlComponent<State, Reado
this.callTouched(); this.callTouched();
} }
public trackByAsset(_index: number, asset: AssetDto) {
return asset.id;
}
} }

58
frontend/src/app/features/content/shared/forms/component-section.component.html

@ -1,28 +1,34 @@
<ng-container *ngIf="!(formSection.hiddenChanges | async)"> @if (!(formSection.hiddenChanges | async)) {
<div class="header" *ngIf="formSection.separator; let separator"> @if (formSection.separator; as separator) {
<h3>{{separator!.displayName}}</h3> <div class="header">
<h3>{{ separator!.displayName }}</h3>
<sqx-form-hint *ngIf="separator.properties.hints && separator.properties.hints.length > 0"> @if (separator.properties.hints && separator.properties.hints.length > 0) {
<span [sqxMarkdown]="separator.properties.hints" optional="true" inline="true"></span> <sqx-form-hint>
</sqx-form-hint> <span inline="true" optional="true" [sqxMarkdown]="separator.properties.hints"></span>
</div> </sqx-form-hint>
}
<div class="row">
<div class="form-group" *ngFor="let child of formSection.fields; trackBy: trackByField"
[class.col-12]="isComparing || !child.field.properties.isHalfWidth"
[class.col-6]="!isComparing && child.field.properties.isHalfWidth">
<sqx-field-editor *ngIf="!(child.hiddenChanges | async)"
[comments]="null"
[form]="form"
[formContext]="formContext"
[formLevel]="formLevel"
[formModel]="child"
[index]="index"
[isComparing]="isComparing"
[hasChatBot]="hasChatBot"
[language]="language"
[languages]="languages">
</sqx-field-editor>
</div> </div>
}
<div class="row">
@for (child of formSection.fields; track child.field.fieldId) {
<div
class="form-group"
[class.col-12]="isComparing || !child.field.properties.isHalfWidth"
[class.col-6]="!isComparing && child.field.properties.isHalfWidth">
@if (!(child.hiddenChanges | async)) {
<sqx-field-editor
[comments]="null"
[form]="form"
[formContext]="formContext"
[formLevel]="formLevel"
[formModel]="child"
[hasChatBot]="hasChatBot"
[index]="index"
[isComparing]="isComparing"
[language]="language"
[languages]="languages"></sqx-field-editor>
}
</div>
}
</div> </div>
</ng-container> }

10
frontend/src/app/features/content/shared/forms/component-section.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 { AsyncPipe, NgFor, NgIf } from '@angular/common'; import { AsyncPipe } from '@angular/common';
import { booleanAttribute, ChangeDetectionStrategy, Component, forwardRef, Input, numberAttribute, QueryList, ViewChildren } from '@angular/core'; import { booleanAttribute, ChangeDetectionStrategy, Component, forwardRef, Input, numberAttribute, QueryList, ViewChildren } from '@angular/core';
import { AbstractContentForm, AppLanguageDto, EditContentForm, FieldDto, FieldSection, FormHintComponent, MarkdownDirective } from '@app/shared'; import { AppLanguageDto, EditContentForm, FieldDto, FieldSection, FormHintComponent, MarkdownDirective } from '@app/shared';
import { FieldEditorComponent } from './field-editor.component'; import { FieldEditorComponent } from './field-editor.component';
@Component({ @Component({
@ -20,8 +20,6 @@ import { FieldEditorComponent } from './field-editor.component';
AsyncPipe, AsyncPipe,
FormHintComponent, FormHintComponent,
MarkdownDirective, MarkdownDirective,
NgFor,
NgIf,
forwardRef(() => FieldEditorComponent), forwardRef(() => FieldEditorComponent),
], ],
}) })
@ -61,8 +59,4 @@ export class ComponentSectionComponent {
editor.reset(); editor.reset();
}); });
} }
public trackByField(_index: number, field: AbstractContentForm<any, any>) {
return field.field.fieldId;
}
} }

80
frontend/src/app/features/content/shared/forms/component.component.html

@ -1,44 +1,50 @@
<div class="component"> <div class="component">
<div *ngIf="formModel.schemaChanges | async; let schema; else noSchema"> @if (formModel.schemaChanges | async; as schema) {
<sqx-form-hint> <div>
{{schema.displayName}} <sqx-form-hint>
</sqx-form-hint> {{ schema.displayName }}
</sqx-form-hint>
<div class="form-group" *ngFor="let section of formModel.sectionsChanges | async"> @for (section of formModel.sectionsChanges | async; track section) {
<sqx-component-section <div class="form-group">
[form]="form" <sqx-component-section
[formContext]="formContext" [form]="form"
[formLevel]="formLevel + 1" [formContext]="formContext"
[formSection]="$any(section)" [formLevel]="formLevel + 1"
[isComparing]="isComparing" [formSection]="$any(section)"
[hasChatBot]="hasChatBot" [hasChatBot]="hasChatBot"
[language]="language" [isComparing]="isComparing"
[languages]="languages"> [language]="language"
</sqx-component-section> [languages]="languages"></sqx-component-section>
</div>
}
</div> </div>
</div> } @else {
@if (schemasList.length > 1) {
<ng-template #noSchema> <button
<ng-container *ngIf="schemasList.length > 1"> class="btn btn-outline-success dropdown-toggle"
<button type="button" class="btn btn-outline-success dropdown-toggle" [disabled]="isDisabled | async" (click)="schemasDropdown.show()" #buttonSelect> #buttonSelect
{{ 'contents.addComponent' | sqxTranslate}} (click)="schemasDropdown.show()"
[disabled]="isDisabled | async"
type="button">
{{ "contents.addComponent" | sqxTranslate }}
</button> </button>
<sqx-dropdown-menu scrollY="true" [sqxAnchoredTo]="buttonSelect" *sqxModal="schemasDropdown; closeAlways: true">
<sqx-dropdown-menu *sqxModal="schemasDropdown;closeAlways:true" [sqxAnchoredTo]="buttonSelect" scrollY="true"> @for (schema of schemasList; track schema) {
<a class="dropdown-item" *ngFor="let schema of schemasList" (click)="setSchema(schema)"> <a class="dropdown-item" (click)="setSchema(schema)">
{{schema.displayName}} {{ schema.displayName }}
</a> </a>
}
</sqx-dropdown-menu> </sqx-dropdown-menu>
</ng-container> }
<ng-container *ngIf="schemasList.length === 1"> @if (schemasList.length === 1) {
<button type="button" class="btn btn-outline-success" [disabled]="isDisabled | async" (click)="setSchema(schemasList[0])"> <button class="btn btn-outline-success" (click)="setSchema(schemasList[0])" [disabled]="isDisabled | async" type="button">
{{ 'contents.addComponent' | sqxTranslate}} {{ "contents.addComponent" | sqxTranslate }}
</button> </button>
</ng-container> }
<ng-container *ngIf="schemasList.length === 0"> @if (schemasList.length === 0) {
<sqx-form-hint> <sqx-form-hint>
{{ 'contents.componentNoSchema' | sqxTranslate }} {{ "contents.componentNoSchema" | sqxTranslate }}
</sqx-form-hint> </sqx-form-hint>
</ng-container> }
</ng-template> }
</div> </div>

10
frontend/src/app/features/content/shared/forms/component.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 { AsyncPipe, NgFor, NgIf } from '@angular/common'; import { AsyncPipe } from '@angular/common';
import { booleanAttribute, ChangeDetectionStrategy, ChangeDetectorRef, Component, Input, numberAttribute, QueryList, ViewChildren } from '@angular/core'; import { booleanAttribute, ChangeDetectionStrategy, ChangeDetectorRef, Component, Input, numberAttribute, QueryList, ViewChildren } from '@angular/core';
import { Observable } from 'rxjs'; import { Observable } from 'rxjs';
import { AppLanguageDto, ComponentFieldPropertiesDto, ComponentForm, disabled$, DropdownMenuComponent, EditContentForm, FieldDto, FieldSection, FormHintComponent, ModalDirective, ModalModel, ModalPlacementDirective, SchemaDto, Subscriptions, TranslatePipe, TypedSimpleChanges, Types } from '@app/shared'; import { AppLanguageDto, ComponentFieldPropertiesDto, ComponentForm, disabled$, DropdownMenuComponent, EditContentForm, FormHintComponent, ModalDirective, ModalModel, ModalPlacementDirective, SchemaDto, Subscriptions, TranslatePipe, TypedSimpleChanges, Types } from '@app/shared';
import { ComponentSectionComponent } from './component-section.component'; import { ComponentSectionComponent } from './component-section.component';
@Component({ @Component({
@ -24,8 +24,6 @@ import { ComponentSectionComponent } from './component-section.component';
FormHintComponent, FormHintComponent,
ModalDirective, ModalDirective,
ModalPlacementDirective, ModalPlacementDirective,
NgFor,
NgIf,
TranslatePipe, TranslatePipe,
], ],
}) })
@ -96,8 +94,4 @@ export class ComponentComponent {
public setSchema(schema: SchemaDto) { public setSchema(schema: SchemaDto) {
this.formModel.selectSchema(schema.id); this.formModel.selectSchema(schema.id);
} }
public trackBySection(_index: number, section: FieldSection<FieldDto, any>) {
return section.separator?.fieldId;
}
} }

197
frontend/src/app/features/content/shared/forms/content-field.component.html

@ -1,113 +1,120 @@
<div class="row g-0" [class.compare]="formModelCompare"> <div class="row g-0" [class.compare]="formModelCompare">
<div [class.col-12]="!formModelCompare" [class.col-6]="formModelCompare"> <div [class.col-12]="!formModelCompare" [class.col-6]="formModelCompare">
<sqx-focus-marker [controlId]="formModel.fieldPath"> <sqx-focus-marker [controlId]="formModel.fieldPath">
<div class="table-items-row table-items-row-summary" [class.field-invalid]="isInvalid | async" *ngIf="!(formModel.hiddenChanges | async)"> @if (!(formModel.hiddenChanges | async)) {
<div class="languages-container"> <div class="table-items-row table-items-row-summary" [class.field-invalid]="isInvalid | async">
<div class="languages-buttons"> <div class="languages-container">
<div class="languages-inner"> <div class="languages-buttons">
<sqx-field-languages <div class="languages-inner">
[formModel]="formModel" <sqx-field-languages
(languageChange)="languageChange.emit($event)" [formModel]="formModel"
[language]="language" [language]="language"
[languages]="languages" (languageChange)="languageChange.emit($event)"
[showAllControls]="showAllControls" [languages]="languages"
(showAllControlsChange)="changeShowAllControls($event)"> [showAllControls]="showAllControls"
</sqx-field-languages> (showAllControlsChange)="changeShowAllControls($event)"></sqx-field-languages>
<sqx-field-copy-button [formModel]="formModel" [languages]="languages"></sqx-field-copy-button>
<sqx-field-copy-button [formModel]="formModel" [languages]="languages"></sqx-field-copy-button> @if (isTranslatable) {
<button
<button *ngIf="isTranslatable" type="button" [disabled]="formModel.field.isDisabled" class="btn btn-sm btn-outline-secondary force no-focus-shadow ms-1" title="i18n:contents.autotranslate" (click)="translate()" tabindex="-1"> class="btn btn-sm btn-outline-secondary force no-focus-shadow ms-1"
<i class="icon-translate"></i> (click)="translate()"
</button> [disabled]="formModel.field.isDisabled"
tabindex="-1"
title="i18n:contents.autotranslate"
type="button">
<i class="icon-translate"></i>
</button>
}
</div>
</div> </div>
</div> </div>
</div> @if (showAllControls) {
@for (language of languages; track language) {
<ng-container *ngIf="showAllControls; else singleControl"> <div class="form-group">
<div class="form-group" *ngFor="let language of languages"> <sqx-field-editor
[comments]="commentsState"
[displaySuffix]="prefix(language)"
[form]="form"
[formContext]="formContext"
[formLevel]="formLevel"
[formModel]="formModel.get(language)"
[hasChatBot]="hasChatBot"
[isComparing]="!!formModelCompare"
[language]="language"
[languages]="languages"></sqx-field-editor>
</div>
}
} @else {
<sqx-field-editor <sqx-field-editor
[comments]="commentsState" [comments]="commentsState"
[displaySuffix]="prefix(language)"
[form]="form" [form]="form"
[formContext]="formContext" [formContext]="formContext"
[formLevel]="formLevel" [formLevel]="formLevel"
[formModel]="formModel.get(language)" [formModel]="getControl()"
[isComparing]="!!formModelCompare"
[hasChatBot]="hasChatBot" [hasChatBot]="hasChatBot"
[isComparing]="!!formModelCompare"
[language]="language" [language]="language"
[languages]="languages"> [languages]="languages"></sqx-field-editor>
</sqx-field-editor> }
</div> </div>
</ng-container> }
<ng-template #singleControl>
<sqx-field-editor
[comments]="commentsState"
[form]="form"
[formContext]="formContext"
[formLevel]="formLevel"
[formModel]="getControl()"
[isComparing]="!!formModelCompare"
[hasChatBot]="hasChatBot"
[language]="language"
[languages]="languages">
</sqx-field-editor>
</ng-template>
</div>
</sqx-focus-marker> </sqx-focus-marker>
</div> </div>
<div class="col-6 col-right" *ngIf="formModelCompare && formCompare"> @if (formModelCompare && formCompare) {
<div class="copy-button-container" *ngIf="!(isDisabled | async)"> <div class="col-6 col-right">
<button type="button" class="btn btn-primary btn-sm field-copy" (click)="copy()" *ngIf="isDifferent | async"> @if (!(isDisabled | async)) {
<i class="icon-arrow_back"></i> <div class="copy-button-container">
</button> @if (isDifferent | async) {
</div> <button class="btn btn-primary btn-sm field-copy" (click)="copy()" type="button">
<i class="icon-arrow_back"></i>
<div class="table-items-row table-items-row-summary" *ngIf="!(formModelCompare!.hiddenChanges | async)"> </button>
<div class="languages-container"> }
<div class="languages-buttons-compare">
<div class="languages-inner">
<sqx-field-languages
[formModel]="formModelCompare!"
(languageChange)="languageChange.emit($event)"
[language]="language"
[languages]="languages"
[showAllControls]="showAllControls"
(showAllControlsChange)="changeShowAllControls($event)">
</sqx-field-languages>
</div>
</div> </div>
</div> }
@if (!(formModelCompare!.hiddenChanges | async)) {
<ng-container *ngIf="showAllControls; else singleControlCompare"> <div class="table-items-row table-items-row-summary">
<div class="form-group" *ngFor="let language of languages"> <div class="languages-container">
<sqx-field-editor <div class="languages-buttons-compare">
[displaySuffix]="prefix(language)" <div class="languages-inner">
[form]="formCompare" <sqx-field-languages
[formContext]="formContext" [formModel]="formModelCompare!"
[formLevel]="formLevel" [language]="language"
[formModel]="formModelCompare.get(language)" (languageChange)="languageChange.emit($event)"
[isComparing]="!!formModelCompare" [languages]="languages"
[hasChatBot]="hasChatBot" [showAllControls]="showAllControls"
[language]="language" (showAllControlsChange)="changeShowAllControls($event)"></sqx-field-languages>
[languages]="languages"> </div>
</sqx-field-editor> </div>
</div>
@if (showAllControls) {
@for (language of languages; track language) {
<div class="form-group">
<sqx-field-editor
[displaySuffix]="prefix(language)"
[form]="formCompare"
[formContext]="formContext"
[formLevel]="formLevel"
[formModel]="formModelCompare.get(language)"
[hasChatBot]="hasChatBot"
[isComparing]="!!formModelCompare"
[language]="language"
[languages]="languages"></sqx-field-editor>
</div>
}
} @else {
<sqx-field-editor
[form]="formCompare"
[formContext]="formContext"
[formLevel]="formLevel"
[formModel]="getControlCompare()!"
[hasChatBot]="hasChatBot"
[isComparing]="!!formModelCompare"
[language]="language"
[languages]="languages"></sqx-field-editor>
}
</div> </div>
</ng-container> }
<ng-template #singleControlCompare>
<sqx-field-editor
[form]="formCompare"
[formContext]="formContext"
[formLevel]="formLevel"
[formModel]="getControlCompare()!"
[language]="language"
[languages]="languages"
[isComparing]="!!formModelCompare"
[hasChatBot]="hasChatBot">
</sqx-field-editor>
</ng-template>
</div> </div>
</div> }
</div> </div>

8
frontend/src/app/features/content/shared/forms/content-field.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 { AsyncPipe, NgFor, NgIf } from '@angular/common'; import { AsyncPipe } from '@angular/common';
import { booleanAttribute, Component, EventEmitter, HostBinding, inject, Input, numberAttribute, Optional, Output } from '@angular/core'; import { booleanAttribute, Component, EventEmitter, HostBinding, inject, Input, numberAttribute, Optional, Output } from '@angular/core';
import { Observable } from 'rxjs'; import { Observable } from 'rxjs';
import { AppLanguageDto, AppsState, changed$, CommentsState, disabled$, EditContentForm, FieldForm, FocusMarkerComponent, invalid$, LocalStoreService, SchemaDto, Settings, TooltipDirective, TranslationsService, TypedSimpleChanges, UIOptions } from '@app/shared'; import { AppLanguageDto, AppsState, changed$, CommentsState, disabled$, EditContentForm, FieldForm, FocusMarkerComponent, invalid$, LocalStoreService, SchemaDto, Settings, TooltipDirective, TranslationsService, TypedSimpleChanges, UIOptions } from '@app/shared';
@ -24,8 +24,6 @@ import { FieldLanguagesComponent } from './field-languages.component';
FieldEditorComponent, FieldEditorComponent,
FieldLanguagesComponent, FieldLanguagesComponent,
FocusMarkerComponent, FocusMarkerComponent,
NgFor,
NgIf,
TooltipDirective, TooltipDirective,
], ],
}) })
@ -182,10 +180,6 @@ export class ContentFieldComponent {
return this.formModelCompare?.get(this.language.iso2Code); return this.formModelCompare?.get(this.language.iso2Code);
} }
public trackByLanguage(_index: number, language: AppLanguageDto) {
return language.iso2Code;
}
private showAllControlsKey() { private showAllControlsKey() {
return Settings.Local.FIELD_ALL(this.schema?.id, this.formModel.field.fieldId); return Settings.Local.FIELD_ALL(this.schema?.id, this.formModel.field.fieldId);
} }

64
frontend/src/app/features/content/shared/forms/content-section.component.html

@ -1,34 +1,38 @@
<ng-container *ngIf="(formSection.visibleChanges | async) || formCompare"> @if ((formSection.visibleChanges | async) || formCompare) {
<div class="header" *ngIf="formSection.separator; let separator"> @if (formSection.separator; as separator) {
<div class="row g-0 align"> <div class="header">
<div class="col-auto"> <div class="row g-0 align">
<button type="button" class="btn btn-sm btn-text-secondary" (click)="toggle()"> <div class="col-auto">
<i [class.icon-caret-right]="snapshot.isCollapsed" [class.icon-caret-down]="!snapshot.isCollapsed"></i> <button class="btn btn-sm btn-text-secondary" (click)="toggle()" type="button">
</button> <i [class.icon-caret-down]="!snapshot.isCollapsed" [class.icon-caret-right]="snapshot.isCollapsed"></i>
</div> </button>
<div class="col"> </div>
<h3>{{separator.displayName}}</h3> <div class="col">
<h3>{{ separator.displayName }}</h3>
<sqx-form-hint *ngIf="separator.properties.hints && separator.properties.hints.length > 0"> @if (separator.properties.hints && separator.properties.hints.length > 0) {
<span [sqxMarkdown]="separator.properties.hints" optional="true" inline="true"></span> <sqx-form-hint>
</sqx-form-hint> <span inline="true" optional="true" [sqxMarkdown]="separator.properties.hints"></span>
</sqx-form-hint>
}
</div>
</div> </div>
</div> </div>
</div> }
</ng-container> }
<div class="row gx-1" [class.hidden]="snapshot.isCollapsed && !formCompare"> <div class="row gx-1" [class.hidden]="snapshot.isCollapsed && !formCompare">
<sqx-content-field *ngFor="let field of formSection.fields; trackBy: trackByField" @for (field of formSection.fields; track field.field.fieldId) {
(languageChange)="languageChange.emit($event)" <sqx-content-field
[form]="form" [form]="form"
[formCompare]="formCompare" [formCompare]="formCompare"
[formContext]="formContext" [formContext]="formContext"
[formLevel]="formLevel" [formLevel]="formLevel"
[formModel]="field" [formModel]="field"
[formModelCompare]="getFieldFormCompare(field)" [formModelCompare]="getFieldFormCompare(field)"
[isCompact]="isCompact" [isCompact]="isCompact"
[language]="language" [language]="language"
[languages]="languages" (languageChange)="languageChange.emit($event)"
[schema]="schema"> [languages]="languages"
</sqx-content-field> [schema]="schema"></sqx-content-field>
</div> }
</div>

8
frontend/src/app/features/content/shared/forms/content-section.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 { AsyncPipe, NgFor, NgIf } from '@angular/common'; import { AsyncPipe } from '@angular/common';
import { booleanAttribute, ChangeDetectionStrategy, Component, EventEmitter, forwardRef, Input, numberAttribute, Output } from '@angular/core'; import { booleanAttribute, ChangeDetectionStrategy, Component, EventEmitter, forwardRef, Input, numberAttribute, Output } from '@angular/core';
import { AppLanguageDto, EditContentForm, FieldForm, FieldSection, FormHintComponent, LocalStoreService, MarkdownDirective, RootFieldDto, SchemaDto, Settings, StatefulComponent, TypedSimpleChanges } from '@app/shared'; import { AppLanguageDto, EditContentForm, FieldForm, FieldSection, FormHintComponent, LocalStoreService, MarkdownDirective, RootFieldDto, SchemaDto, Settings, StatefulComponent, TypedSimpleChanges } from '@app/shared';
import { ContentFieldComponent } from './content-field.component'; import { ContentFieldComponent } from './content-field.component';
@ -25,8 +25,6 @@ interface State {
AsyncPipe, AsyncPipe,
FormHintComponent, FormHintComponent,
MarkdownDirective, MarkdownDirective,
NgFor,
NgIf,
forwardRef(() => ContentFieldComponent), forwardRef(() => ContentFieldComponent),
], ],
}) })
@ -94,10 +92,6 @@ export class ContentSectionComponent extends StatefulComponent<State> {
return this.formCompare?.get(formState.field.name); return this.formCompare?.get(formState.field.name);
} }
public trackByField(_index: number, formState: FieldForm) {
return formState.field.fieldId;
}
private isCollapsedKey(): string { private isCollapsedKey(): string {
return Settings.Local.FIELD_COLLAPSED(this.schema?.id, this.formSection?.separator?.fieldId); return Settings.Local.FIELD_COLLAPSED(this.schema?.id, this.formSection?.separator?.fieldId);
} }

39
frontend/src/app/features/content/shared/forms/field-copy-button.component.html

@ -1,37 +1,36 @@
<ng-container *ngIf="isLocalized"> @if (isLocalized) {
<button type="button" class="btn btn-outline-secondary btn-sm ms-1 dropdown-toggle" title="{{ 'common.copy' | sqxTranslate }}" (click)="dropdown.toggle()" #button tabindex="-1"> <button
class="btn btn-outline-secondary btn-sm ms-1 dropdown-toggle"
#button
(click)="dropdown.toggle()"
tabindex="-1"
title="{{ 'common.copy' | sqxTranslate }}"
type="button">
<i class="icon-copy"></i> <i class="icon-copy"></i>
</button> </button>
<sqx-dropdown-menu scrollY="true" [sqxAnchoredTo]="button" *sqxModal="dropdown">
<sqx-dropdown-menu *sqxModal="dropdown" [sqxAnchoredTo]="button" scrollY="true">
<div class="section d-flex justify-content-end"> <div class="section d-flex justify-content-end">
<button type="button" class="btn btn-primary" (click)="copy()" tabindex="-1"> <button class="btn btn-primary" (click)="copy()" tabindex="-1" type="button">
{{ 'common.copy' | sqxTranslate }} {{ "common.copy" | sqxTranslate }}
</button> </button>
</div> </div>
<div class="dropdown-divider"></div> <div class="dropdown-divider"></div>
<div class="section"> <div class="section">
<div class="row"> <div class="row">
<label class="col-auto col-form-label" for="languageSource">{{ 'common.from' | sqxTranslate }}</label> <label class="col-auto col-form-label" for="languageSource">{{ "common.from" | sqxTranslate }}</label>
<div class="col"> <div class="col">
<select class="form-select" id="languagesSource" <select class="form-select" id="languagesSource" [ngModel]="copySource" (ngModelChange)="setCopySource($event)">
[ngModel]="copySource" @for (language of languages; track language) {
(ngModelChange)="setCopySource($event)"> <option [ngValue]="language.iso2Code">{{ language.iso2Code }}</option>
<option *ngFor="let language of languages" [ngValue]="language.iso2Code">{{language.iso2Code}}</option> }
</select> </select>
</div> </div>
</div> </div>
</div> </div>
<div class="dropdown-divider"></div> <div class="dropdown-divider"></div>
<div class="section"> <div class="section">
<label>{{ 'common.to' | sqxTranslate }}</label> <label>{{ "common.to" | sqxTranslate }}</label>
<sqx-checkbox-group layout="Multiline" [(ngModel)]="copyTargets" [values]="languageCodes"></sqx-checkbox-group>
<sqx-checkbox-group [(ngModel)]="copyTargets" [values]="languageCodes" layout="Multiline"></sqx-checkbox-group>
</div> </div>
</sqx-dropdown-menu> </sqx-dropdown-menu>
</ng-container> }

4
frontend/src/app/features/content/shared/forms/field-copy-button.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 { NgFor, NgIf } from '@angular/common';
import { Component, Input } from '@angular/core'; import { Component, Input } from '@angular/core';
import { FormsModule } from '@angular/forms'; import { FormsModule } from '@angular/forms';
import { AppLanguageDto, CheckboxGroupComponent, DropdownMenuComponent, FieldForm, ModalDirective, ModalModel, ModalPlacementDirective, TooltipDirective, TranslatePipe, TypedSimpleChanges } from '@app/shared'; import { AppLanguageDto, CheckboxGroupComponent, DropdownMenuComponent, FieldForm, ModalDirective, ModalModel, ModalPlacementDirective, TooltipDirective, TranslatePipe, TypedSimpleChanges } from '@app/shared';
@ -21,8 +21,6 @@ import { AppLanguageDto, CheckboxGroupComponent, DropdownMenuComponent, FieldFor
FormsModule, FormsModule,
ModalDirective, ModalDirective,
ModalPlacementDirective, ModalPlacementDirective,
NgFor,
NgIf,
TooltipDirective, TooltipDirective,
TranslatePipe, TranslatePipe,
], ],

636
frontend/src/app/features/content/shared/forms/field-editor.component.html

@ -1,297 +1,345 @@
<div class="field" [class.expanded]="isExpanded" *ngIf="formModel"> @if (formModel) {
<fieldset class="buttons-container" [disabled]="isDisabled | async"> <div class="field" [class.expanded]="isExpanded">
<div class="buttons"> <fieldset class="buttons-container" [disabled]="isDisabled | async">
<button type="button" class="btn btn-sm btn-outline-secondary force no-focus-shadow" (click)="chatDialog.show()" [disabled]="!hasChatBot || !isString" tabindex="-1"> <div class="buttons">
AI <button
</button> class="btn btn-sm btn-outline-secondary force no-focus-shadow"
(click)="chatDialog.show()"
<button type="button" class="btn btn-sm btn-outline-secondary force no-focus-shadow ms-1" title="i18n:contents.fieldFullscreen" (click)="toggleExpanded()" tabindex="-1"> [disabled]="!hasChatBot || !isString"
<i class="icon-fullscreen"></i> tabindex="-1"
</button> type="button">
AI
<button type="button" class="btn btn-sm btn-outline-secondary btn-clear force no-focus-shadow ms-1" [disabled]="isEmpty | async" tabindex="-1" </button>
(sqxConfirmClick)="unset()" <button
confirmTitle="i18n:contents.unsetValueConfirmTitle" class="btn btn-sm btn-outline-secondary force no-focus-shadow ms-1"
confirmText="i18n:contents.unsetValueConfirmText" (click)="toggleExpanded()"
confirmRememberKey="unsetValue" tabindex="-1"
title="i18n:contents.unsetValue"> title="i18n:contents.fieldFullscreen"
<i class="icon-close"></i> type="button">
</button> <i class="icon-fullscreen"></i>
</button>
<button
class="btn btn-sm btn-outline-secondary btn-clear force no-focus-shadow ms-1"
confirmRememberKey="unsetValue"
confirmText="i18n:contents.unsetValueConfirmText"
confirmTitle="i18n:contents.unsetValueConfirmTitle"
[disabled]="isEmpty | async"
(sqxConfirmClick)="unset()"
tabindex="-1"
title="i18n:contents.unsetValue"
type="button">
<i class="icon-close"></i>
</button>
</div>
</fieldset>
<label>
{{ field.displayName }} {{ displaySuffix }}
<span class="field-required" [class.hidden]="!field.properties.isRequired">*</span>
</label>
@if (field.isDisabled) {
<small class="field-disabled ps-1">Disabled</small>
}
@if (form) {
<sqx-control-errors [fieldName]="field.displayName" [for]="$any(fieldForm)"></sqx-control-errors>
}
<div>
@if (field.properties.editorUrl) {
<sqx-iframe-editor
#editor
[context]="formContext"
[formControlBinding]="$any(fieldForm)"
[formField]="formModel.field.name"
[formIndex]="index"
[formValue]="form.valueChanges | async"
[isExpanded]="isExpanded"
(isExpandedChange)="toggleExpanded()"
[language]="language"
[languages]="languages"
[schemaIds]="field.rawProperties.schemaIds"
[url]="field.properties.editorUrl"></sqx-iframe-editor>
} @else {
@switch (field.properties.fieldType) {
@case ("Array") {
<sqx-array-editor
[form]="form"
[formContext]="formContext"
[formLevel]="formLevel"
[formModel]="$any(formModel)"
[hasChatBot]="hasChatBot"
[isComparing]="isComparing"
[isExpanded]="isExpanded"
[language]="language"
[languages]="languages"></sqx-array-editor>
}
@case ("Assets") {
<sqx-assets-editor
[folderId]="field.rawProperties.folderId"
[formControl]="$any(fieldForm)"
[hasChatBot]="hasChatBot"
[isExpanded]="isExpanded"></sqx-assets-editor>
}
@case ("Boolean") {
@switch (field.rawProperties.editor) {
@case ("Toggle") {
<sqx-toggle [formControl]="$any(fieldForm)" [threeStates]="!field.properties.isRequired"></sqx-toggle>
}
@case ("Checkbox") {
<div class="form-check">
<input
class="form-check-input"
id="{{ uniqueId }}"
[formControl]="$any(fieldForm)"
sqxIndeterminateValue
[threeStates]="!field.properties.isRequired"
type="checkbox" />
<label class="form-check-label" for="{{ uniqueId }}"></label>
</div>
}
}
}
@case ("Component") {
<sqx-component
[form]="form"
[formContext]="formContext"
[formLevel]="formLevel"
[formModel]="$any(formModel)"
[hasChatBot]="hasChatBot"
[isComparing]="isComparing"
[language]="language"
[languages]="languages"></sqx-component>
}
@case ("Components") {
<sqx-array-editor
[form]="form"
[formContext]="formContext"
[formLevel]="formLevel"
[formModel]="$any(formModel)"
[hasChatBot]="hasChatBot"
[isComparing]="isComparing"
[isExpanded]="isExpanded"
[language]="language"
[languages]="languages"></sqx-array-editor>
}
@case ("DateTime") {
<sqx-date-time-editor
enforceTime="true"
[formControl]="$any(fieldForm)"
[mode]="field.rawProperties.editor"></sqx-date-time-editor>
}
@case ("Geolocation") {
<sqx-geolocation-editor [formControl]="$any(fieldForm)"></sqx-geolocation-editor>
}
@case ("Json") {
<sqx-code-editor [formControl]="$any(fieldForm)" [height]="350" valueMode="Json"></sqx-code-editor>
}
@case ("Number") {
@switch (field.rawProperties.editor) {
@case ("Input") {
<input
class="form-control"
[formControl]="$any(fieldForm)"
[placeholder]="field.displayPlaceholder"
type="number" />
}
@case ("Stars") {
<sqx-stars [formControl]="$any(fieldForm)" [maximumStars]="field.rawProperties.maxValue"></sqx-stars>
}
@case ("Radio") {
<sqx-radio-group
[formControl]="$any(fieldForm)"
unsorted="true"
[values]="field.rawProperties.allowedValues"></sqx-radio-group>
}
@case ("Dropdown") {
<select class="form-select" [formControl]="$any(fieldForm)">
<option [ngValue]="null"></option>
@for (value of field.rawProperties.allowedValues; track value) {
<option [ngValue]="value">{{ value }}</option>
}
</select>
}
}
}
@case ("References") {
@switch (field.rawProperties.editor) {
@case ("List") {
<sqx-references-editor
[allowDuplicates]="field.rawProperties.allowDuplicated"
[formContext]="formContext"
[formControl]="$any(fieldForm)"
[isExpanded]="isExpanded"
[language]="language"
[languages]="languages"
[query]="field.rawProperties.query"
[schemaIds]="field.rawProperties.schemaIds"></sqx-references-editor>
}
@case ("Dropdown") {
<sqx-reference-dropdown
[formControl]="$any(fieldForm)"
[language]="language"
[languages]="languages"
mode="Array"
[schemaId]="field.rawProperties.singleId"></sqx-reference-dropdown>
}
@case ("Input") {
<sqx-reference-input
[formControl]="$any(fieldForm)"
[language]="language"
[languages]="languages"
mode="Array"
[query]="field.rawProperties.query"
[schemaIds]="field.rawProperties.schemaIds"></sqx-reference-input>
}
@case ("Tags") {
<sqx-references-tags
[formControl]="$any(fieldForm)"
[language]="language"
[languages]="languages"
[schemaId]="field.rawProperties.singleId"></sqx-references-tags>
}
@case ("Checkboxes") {
<sqx-references-checkboxes
[formControl]="$any(fieldForm)"
[language]="language"
[schemaId]="field.rawProperties.singleId"></sqx-references-checkboxes>
}
}
}
@case ("String") {
@switch (field.rawProperties.editor) {
@case ("Input") {
<input class="form-control" [formControl]="$any(fieldForm)" [placeholder]="field.displayPlaceholder" />
}
@case ("Slug") {
<input
class="form-control"
[formControl]="$any(fieldForm)"
[placeholder]="field.displayPlaceholder"
sqxTransformInput="Slugify" />
}
@case ("TextArea") {
<textarea
class="form-control"
[formControl]="$any(fieldForm)"
[placeholder]="field.displayPlaceholder"
rows="5"></textarea>
}
@case ("RichText") {
<sqx-rich-editor
#editor
[annotations]="annotations | async"
(annotationsCreate)="annotationCreate($event)"
(annotationsSelect)="annotationsSelect($event)"
(annotationsUpdate)="annotationsUpdate($event)"
[classNames]="field.rawProperties.classNames"
[folderId]="field.rawProperties.folderId"
[formControl]="$any(fieldForm)"
[hasAnnotations]="!!comments"
[hasChatBot]="hasChatBot"
[language]="language"
[languages]="languages"
mode="Html"
[schemaIds]="field.rawProperties.schemaIds"></sqx-rich-editor>
}
@case ("Html") {
<sqx-code-editor
#editor
[formControl]="$any(fieldForm)"
[height]="350"
mode="ace/mode/html"></sqx-code-editor>
}
@case ("Markdown") {
<sqx-rich-editor
#editor
[annotations]="annotations | async"
(annotationsCreate)="annotationCreate($event)"
(annotationsSelect)="annotationsSelect($event)"
(annotationsUpdate)="annotationsUpdate($event)"
[classNames]="undefined"
[folderId]="field.rawProperties.folderId"
[formControl]="$any(fieldForm)"
[hasAnnotations]="!!comments"
[hasChatBot]="hasChatBot"
[language]="language"
[languages]="languages"
mode="Markdown"
[schemaIds]="field.rawProperties.schemaIds"></sqx-rich-editor>
}
@case ("StockPhoto") {
<sqx-stock-photo-editor [formControl]="$any(fieldForm)"></sqx-stock-photo-editor>
}
@case ("Dropdown") {
<select class="form-select" [formControl]="$any(fieldForm)">
<option [ngValue]="null"></option>
@for (value of field.rawProperties.allowedValues; track value) {
<option [ngValue]="value">{{ value }}</option>
}
</select>
}
@case ("Radio") {
<sqx-radio-group
[formControl]="$any(fieldForm)"
unsorted="true"
[values]="field.rawProperties.allowedValues"></sqx-radio-group>
}
@case ("Color") {
<sqx-color-picker
[formControl]="$any(fieldForm)"
[placeholder]="field.displayPlaceholder"></sqx-color-picker>
}
}
}
@case ("RichText") {
<sqx-rich-editor
#editor
[annotations]="annotations | async"
(annotationsCreate)="annotationCreate($event)"
(annotationsSelect)="annotationsSelect($event)"
(annotationsUpdate)="annotationsUpdate($event)"
[classNames]="field.rawProperties.classNames"
[folderId]="field.rawProperties.folderId"
[formControl]="$any(fieldForm)"
[hasAnnotations]="!!comments"
[hasChatBot]="hasChatBot"
[language]="language"
[languages]="languages"
mode="State"
[schemaIds]="field.rawProperties.schemaIds"></sqx-rich-editor>
}
@case ("Tags") {
@switch (field.rawProperties.editor) {
@case ("Tags") {
<sqx-tag-editor
[formControl]="$any(fieldForm)"
[itemsSource]="field.rawProperties.allowedValues"
[placeholder]="field.displayPlaceholder"></sqx-tag-editor>
}
@case ("Checkboxes") {
<sqx-checkbox-group
[formControl]="$any(fieldForm)"
[values]="field.rawProperties.allowedValues"></sqx-checkbox-group>
}
@case ("Dropdown") {
<select class="form-select" [formControl]="$any(fieldForm)" multiple>
@for (value of field.rawProperties.allowedValues; track value) {
<option [ngValue]="value">{{ value }}</option>
}
</select>
}
}
}
@case ("UI") {
<h4 class="ui-separator">{{ field.displayName }}</h4>
}
}
}
</div> </div>
</fieldset> @if (field.properties.hints && field.properties.hints.length > 0) {
<sqx-form-hint>
<label> <span inline="true" optional="true" [sqxMarkdown]="field.properties.hints"></span>
{{field.displayName}} {{displaySuffix}} <span class="field-required" [class.hidden]="!field.properties.isRequired">*</span> </sqx-form-hint>
</label> }
<small class="field-disabled ps-1" *ngIf="field.isDisabled">Disabled</small>
<sqx-control-errors *ngIf="form" [for]="$any(fieldForm)" [fieldName]="field.displayName"></sqx-control-errors>
<div>
<ng-container *ngIf="field.properties.editorUrl; else noEditor">
<sqx-iframe-editor [url]="field.properties.editorUrl" #editor
[context]="formContext"
[isExpanded]="isExpanded"
(isExpandedChange)="toggleExpanded()"
[formControlBinding]="$any(fieldForm)"
[formValue]="form.valueChanges | async"
[formIndex]="index"
[formField]="formModel.field.name"
[language]="language"
[languages]="languages"
[schemaIds]="field.rawProperties.schemaIds">
</sqx-iframe-editor>
</ng-container>
<ng-template #noEditor>
<ng-container [ngSwitch]="field.properties.fieldType">
<ng-container *ngSwitchCase="'Array'">
<sqx-array-editor
[form]="form"
[formLevel]="formLevel"
[formModel]="$any(formModel)"
[formContext]="formContext"
[isComparing]="isComparing"
[isExpanded]="isExpanded"
[hasChatBot]="hasChatBot"
[language]="language"
[languages]="languages">
</sqx-array-editor>
</ng-container>
<ng-container *ngSwitchCase="'Assets'">
<sqx-assets-editor
[formControl]="$any(fieldForm)"
[folderId]="field.rawProperties.folderId"
[hasChatBot]="hasChatBot"
[isExpanded]="isExpanded">
</sqx-assets-editor>
</ng-container>
<ng-container *ngSwitchCase="'Boolean'">
<ng-container [ngSwitch]="field.rawProperties.editor">
<ng-container *ngSwitchCase="'Toggle'">
<sqx-toggle [formControl]="$any(fieldForm)" [threeStates]="!field.properties.isRequired"></sqx-toggle>
</ng-container>
<ng-container *ngSwitchCase="'Checkbox'">
<div class="form-check">
<input class="form-check-input" type="checkbox" [formControl]="$any(fieldForm)" id="{{uniqueId}}" sqxIndeterminateValue [threeStates]="!field.properties.isRequired">
<label class="form-check-label" for="{{uniqueId}}"></label>
</div>
</ng-container>
</ng-container>
</ng-container>
<ng-container *ngSwitchCase="'Component'">
<sqx-component
[form]="form"
[formContext]="formContext"
[formLevel]="formLevel"
[formModel]="$any(formModel)"
[isComparing]="isComparing"
[hasChatBot]="hasChatBot"
[language]="language"
[languages]="languages">
</sqx-component>
</ng-container>
<ng-container *ngSwitchCase="'Components'">
<sqx-array-editor
[form]="form"
[formLevel]="formLevel"
[formModel]="$any(formModel)"
[formContext]="formContext"
[isComparing]="isComparing"
[isExpanded]="isExpanded"
[hasChatBot]="hasChatBot"
[language]="language"
[languages]="languages">
</sqx-array-editor>
</ng-container>
<ng-container *ngSwitchCase="'DateTime'">
<sqx-date-time-editor [formControl]="$any(fieldForm)" [mode]="field.rawProperties.editor" enforceTime="true"></sqx-date-time-editor>
</ng-container>
<ng-container *ngSwitchCase="'Geolocation'">
<sqx-geolocation-editor [formControl]="$any(fieldForm)"></sqx-geolocation-editor>
</ng-container>
<ng-container *ngSwitchCase="'Json'">
<sqx-code-editor [formControl]="$any(fieldForm)" valueMode="Json" [height]="350"></sqx-code-editor>
</ng-container>
<ng-container *ngSwitchCase="'Number'">
<ng-container [ngSwitch]="field.rawProperties.editor">
<ng-container *ngSwitchCase="'Input'">
<input class="form-control" type="number" [formControl]="$any(fieldForm)" [placeholder]="field.displayPlaceholder">
</ng-container>
<ng-container *ngSwitchCase="'Stars'">
<sqx-stars [formControl]="$any(fieldForm)" [maximumStars]="field.rawProperties.maxValue"></sqx-stars>
</ng-container>
<ng-container *ngSwitchCase="'Radio'">
<sqx-radio-group [formControl]="$any(fieldForm)" [values]="field.rawProperties.allowedValues" unsorted="true"></sqx-radio-group>
</ng-container>
<ng-container *ngSwitchCase="'Dropdown'">
<select class="form-select" [formControl]="$any(fieldForm)">
<option [ngValue]="null"></option>
<option *ngFor="let value of field.rawProperties.allowedValues" [ngValue]="value">{{value}}</option>
</select>
</ng-container>
</ng-container>
</ng-container>
<ng-container *ngSwitchCase="'References'">
<ng-container [ngSwitch]="field.rawProperties.editor">
<ng-container *ngSwitchCase="'List'">
<sqx-references-editor
[allowDuplicates]="field.rawProperties.allowDuplicated"
[formContext]="formContext"
[formControl]="$any(fieldForm)"
[isExpanded]="isExpanded"
[language]="language"
[languages]="languages"
[query]="field.rawProperties.query"
[schemaIds]="field.rawProperties.schemaIds">
</sqx-references-editor>
</ng-container>
<ng-container *ngSwitchCase="'Dropdown'">
<sqx-reference-dropdown
mode="Array"
[formControl]="$any(fieldForm)"
[language]="language"
[languages]="languages"
[schemaId]="field.rawProperties.singleId">
</sqx-reference-dropdown>
</ng-container>
<ng-container *ngSwitchCase="'Input'">
<sqx-reference-input
mode="Array"
[formControl]="$any(fieldForm)"
[language]="language"
[languages]="languages"
[query]="field.rawProperties.query"
[schemaIds]="field.rawProperties.schemaIds">
</sqx-reference-input>
</ng-container>
<ng-container *ngSwitchCase="'Tags'">
<sqx-references-tags
[formControl]="$any(fieldForm)"
[language]="language"
[languages]="languages"
[schemaId]="field.rawProperties.singleId">
</sqx-references-tags>
</ng-container>
<ng-container *ngSwitchCase="'Checkboxes'">
<sqx-references-checkboxes
[formControl]="$any(fieldForm)"
[language]="language"
[schemaId]="field.rawProperties.singleId">
</sqx-references-checkboxes>
</ng-container>
</ng-container>
</ng-container>
<ng-container *ngSwitchCase="'String'">
<ng-container [ngSwitch]="field.rawProperties.editor">
<ng-container *ngSwitchCase="'Input'">
<input class="form-control" [formControl]="$any(fieldForm)" [placeholder]="field.displayPlaceholder">
</ng-container>
<ng-container *ngSwitchCase="'Slug'">
<input class="form-control" [formControl]="$any(fieldForm)" [placeholder]="field.displayPlaceholder" sqxTransformInput="Slugify">
</ng-container>
<ng-container *ngSwitchCase="'TextArea'">
<textarea class="form-control" [formControl]="$any(fieldForm)" [placeholder]="field.displayPlaceholder" rows="5"></textarea>
</ng-container>
<ng-container *ngSwitchCase="'RichText'">
<sqx-rich-editor #editor
mode="Html"
[annotations]="annotations | async"
(annotationsCreate)="annotationCreate($event)"
(annotationsSelect)="annotationsSelect($event)"
(annotationsUpdate)="annotationsUpdate($event)"
[classNames]="field.rawProperties.classNames"
[formControl]="$any(fieldForm)"
[folderId]="field.rawProperties.folderId"
[hasAnnotations]="!!comments"
[hasChatBot]="hasChatBot"
[language]="language"
[languages]="languages"
[schemaIds]="field.rawProperties.schemaIds">
</sqx-rich-editor>
</ng-container>
<ng-container *ngSwitchCase="'Html'">
<sqx-code-editor [formControl]="$any(fieldForm)" #editor mode="ace/mode/html" [height]="350" ></sqx-code-editor>
</ng-container>
<ng-container *ngSwitchCase="'Markdown'">
<sqx-rich-editor #editor
mode="Markdown"
[annotations]="annotations | async"
(annotationsCreate)="annotationCreate($event)"
(annotationsSelect)="annotationsSelect($event)"
(annotationsUpdate)="annotationsUpdate($event)"
[classNames]="undefined"
[formControl]="$any(fieldForm)"
[folderId]="field.rawProperties.folderId"
[hasAnnotations]="!!comments"
[hasChatBot]="hasChatBot"
[language]="language"
[languages]="languages"
[schemaIds]="field.rawProperties.schemaIds">
</sqx-rich-editor>
</ng-container>
<ng-container *ngSwitchCase="'StockPhoto'">
<sqx-stock-photo-editor [formControl]="$any(fieldForm)"></sqx-stock-photo-editor>
</ng-container>
<ng-container *ngSwitchCase="'Dropdown'">
<select class="form-select" [formControl]="$any(fieldForm)">
<option [ngValue]="null"></option>
<option *ngFor="let value of field.rawProperties.allowedValues" [ngValue]="value">{{value}}</option>
</select>
</ng-container>
<ng-container *ngSwitchCase="'Radio'">
<sqx-radio-group [formControl]="$any(fieldForm)" [values]="field.rawProperties.allowedValues" unsorted="true"></sqx-radio-group>
</ng-container>
<ng-container *ngSwitchCase="'Color'">
<sqx-color-picker [formControl]="$any(fieldForm)" [placeholder]="field.displayPlaceholder"></sqx-color-picker>
</ng-container>
</ng-container>
</ng-container>
<ng-container *ngSwitchCase="'RichText'">
<sqx-rich-editor #editor
mode="State"
[annotations]="annotations | async"
(annotationsCreate)="annotationCreate($event)"
(annotationsSelect)="annotationsSelect($event)"
(annotationsUpdate)="annotationsUpdate($event)"
[classNames]="field.rawProperties.classNames"
[formControl]="$any(fieldForm)"
[folderId]="field.rawProperties.folderId"
[hasAnnotations]="!!comments"
[hasChatBot]="hasChatBot"
[language]="language"
[languages]="languages"
[schemaIds]="field.rawProperties.schemaIds">
</sqx-rich-editor>
</ng-container>
<ng-container *ngSwitchCase="'Tags'">
<ng-container [ngSwitch]="field.rawProperties.editor">
<ng-container *ngSwitchCase="'Tags'">
<sqx-tag-editor [formControl]="$any(fieldForm)" [placeholder]="field.displayPlaceholder" [itemsSource]="field.rawProperties.allowedValues"></sqx-tag-editor>
</ng-container>
<ng-container *ngSwitchCase="'Checkboxes'">
<sqx-checkbox-group [formControl]="$any(fieldForm)" [values]="field.rawProperties.allowedValues"></sqx-checkbox-group>
</ng-container>
<ng-container *ngSwitchCase="'Dropdown'">
<select multiple class="form-select" [formControl]="$any(fieldForm)">
<option *ngFor="let value of field.rawProperties.allowedValues" [ngValue]="value">{{value}}</option>
</select>
</ng-container>
</ng-container>
</ng-container>
<ng-container *ngSwitchCase="'UI'">
<h4 class="ui-separator">{{field.displayName}}</h4>
</ng-container>
</ng-container>
</ng-template>
</div> </div>
}
<sqx-form-hint *ngIf="field.properties.hints && field.properties.hints.length > 0"> <sqx-chat-dialog (contentSelect)="setValue($event)" *sqxModal="chatDialog"></sqx-chat-dialog>
<span [sqxMarkdown]="field.properties.hints" optional="true" inline="true"></span>
</sqx-form-hint>
</div>
<sqx-chat-dialog *sqxModal="chatDialog"
(contentSelect)="setValue($event)">
</sqx-chat-dialog>

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

@ -5,7 +5,7 @@
* Copyright (c) Squidex UG (haftungsbeschränkt). All rights reserved. * Copyright (c) Squidex UG (haftungsbeschränkt). All rights reserved.
*/ */
import { AsyncPipe, NgFor, NgIf, NgSwitch, NgSwitchCase } from '@angular/common'; import { AsyncPipe } from '@angular/common';
import { booleanAttribute, Component, ElementRef, EventEmitter, Input, numberAttribute, Output, ViewChild } from '@angular/core'; import { booleanAttribute, Component, ElementRef, EventEmitter, Input, numberAttribute, Output, ViewChild } from '@angular/core';
import { AbstractControl, FormsModule, ReactiveFormsModule } from '@angular/forms'; import { AbstractControl, FormsModule, ReactiveFormsModule } from '@angular/forms';
import { Observable } from 'rxjs'; import { Observable } from 'rxjs';
@ -44,10 +44,6 @@ import { StockPhotoEditorComponent } from './stock-photo-editor.component';
IndeterminateValueDirective, IndeterminateValueDirective,
MarkdownDirective, MarkdownDirective,
ModalDirective, ModalDirective,
NgFor,
NgIf,
NgSwitch,
NgSwitchCase,
RadioGroupComponent, RadioGroupComponent,
ReactiveFormsModule, ReactiveFormsModule,
ReferenceDropdownComponent, ReferenceDropdownComponent,

40
frontend/src/app/features/content/shared/forms/field-languages.component.html

@ -1,27 +1,25 @@
<ng-container *ngIf="formModel.field.isLocalizable && languages.length > 1"> @if (formModel.field.isLocalizable && languages.length > 1) {
<button *ngIf="!formModel.field.properties.isComplexUI" type="button" class="btn btn-sm btn-outline-secondary force no-focus-shadow" (click)="toggleShowAllControls()"> @if (!formModel.field.properties.isComplexUI) {
<ng-container *ngIf="showAllControls; else singleLanguage"> <button class="btn btn-sm btn-outline-secondary force no-focus-shadow" (click)="toggleShowAllControls()" type="button">
<span>{{ 'contents.languageModeSingle' | sqxTranslate }}</span> @if (showAllControls) {
</ng-container> <span>{{ "contents.languageModeSingle" | sqxTranslate }}</span>
} @else {
<ng-template #singleLanguage> <span>{{ "contents.languageModeAll" | sqxTranslate }}</span>
<span>{{ 'contents.languageModeAll' | sqxTranslate }}</span> }
</ng-template> </button>
</button> }
@if (formModel.field.properties.isComplexUI || !showAllControls) {
<ng-container *ngIf="formModel.field.properties.isComplexUI || !showAllControls">
<div class="button-container ms-1"> <div class="button-container ms-1">
<sqx-language-selector <sqx-language-selector
size="sm"
[exists]="formModel.translationStatus | async" [exists]="formModel.translationStatus | async"
(languageChange)="languageChange.emit($event)"
[language]="language"
[languages]="languages"
hintText="i18n:contents.validationHint"
hintAfter="120000" hintAfter="120000"
hintPosition="top-end" hintPosition="top-end"
sqxTourStep="languages"> hintText="i18n:contents.validationHint"
</sqx-language-selector> [language]="language"
(languageChange)="languageChange.emit($event)"
[languages]="languages"
size="sm"
sqxTourStep="languages"></sqx-language-selector>
</div> </div>
</ng-container> }
</ng-container> }

3
frontend/src/app/features/content/shared/forms/field-languages.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 { AsyncPipe, NgIf } from '@angular/common'; import { AsyncPipe } from '@angular/common';
import { booleanAttribute, ChangeDetectionStrategy, Component, EventEmitter, Input, Output } from '@angular/core'; import { booleanAttribute, ChangeDetectionStrategy, Component, EventEmitter, Input, Output } from '@angular/core';
import { AppLanguageDto, FieldForm, LanguageSelectorComponent, TourHintDirective, TourStepDirective, TranslatePipe } from '@app/shared'; import { AppLanguageDto, FieldForm, LanguageSelectorComponent, TourHintDirective, TourStepDirective, TranslatePipe } from '@app/shared';
@ -18,7 +18,6 @@ import { AppLanguageDto, FieldForm, LanguageSelectorComponent, TourHintDirective
imports: [ imports: [
AsyncPipe, AsyncPipe,
LanguageSelectorComponent, LanguageSelectorComponent,
NgIf,
TourHintDirective, TourHintDirective,
TourStepDirective, TourStepDirective,
TranslatePipe, TranslatePipe,

23
frontend/src/app/features/content/shared/forms/iframe-editor.component.html

@ -1,18 +1,21 @@
<div #container> <div #container>
<div #inner [class.fullscreen]="snapshot.isFullscreen" [class.expanded]="isExpanded"> <div #inner [class.expanded]="isExpanded" [class.fullscreen]="snapshot.isFullscreen">
<iframe #iframe [scrolling]="!isExpanded ? 'no' : 'yes'" width="100%" [style.height]="0" [attr.src]="computedUrl | sqxSafeResourceUrl"></iframe> <iframe
#iframe
[attr.src]="computedUrl | sqxSafeResourceUrl"
[scrolling]="!isExpanded ? 'no' : 'yes'"
[style.height]="0"
width="100%"></iframe>
</div> </div>
</div> </div>
<sqx-asset-selector *sqxModal="assetsDialog" <sqx-asset-selector (assetSelect)="pickAssets($event)" *sqxModal="assetsDialog"></sqx-asset-selector>
(assetSelect)="pickAssets($event)">
</sqx-asset-selector>
<sqx-content-selector *sqxModal="contentsDialog" <sqx-content-selector
(contentSelect)="pickContents($event)"
[alreadySelectedIds]="contentsSelectedIds" [alreadySelectedIds]="contentsSelectedIds"
[query]="contentsQuery" (contentSelect)="pickContents($event)"
[language]="language" [language]="language"
[languages]="languages" [languages]="languages"
[schemaIdentifiers]="contentsSchemas"> [query]="contentsQuery"
</sqx-content-selector> [schemaIdentifiers]="contentsSchemas"
*sqxModal="contentsDialog"></sqx-content-selector>

74
frontend/src/app/features/content/shared/forms/stock-photo-editor.component.html

@ -1,50 +1,64 @@
<div class="input-group"> <div class="input-group">
<button type="button" class="btn btn-outline-secondary" (click)="reset()" [disabled]="!valueControl.value"> <button class="btn btn-outline-secondary" (click)="reset()" [disabled]="!valueControl.value" type="button">
<i class="icon-close"></i> <i class="icon-close"></i>
</button> </button>
<button type="button" class="btn btn-outline-secondary" (click)="searchDialog.show()"> <button class="btn btn-outline-secondary" (click)="searchDialog.show()" type="button">
<i class="icon-search"></i> <i class="icon-search"></i>
</button> </button>
<input readonly [disabled]="true" class="form-control" [formControl]="valueControl"> <input class="form-control" [disabled]="true" [formControl]="valueControl" readonly />
</div> </div>
<div *ngIf="stockPhotoThumbnail | async; let url;" class="preview mt-1" [class.hidden-important]="snapshot.thumbnailStatus === 'Failed'"> @if (stockPhotoThumbnail | async; as url) {
<img [src]="url" (error)="onThumbnailFailed()" (load)="onThumbnailLoaded()"> <div class="preview mt-1" [class.hidden-important]="snapshot.thumbnailStatus === 'Failed'">
<img (error)="onThumbnailFailed()" (load)="onThumbnailLoaded()" [src]="url" />
<sqx-loader color="white" *ngIf="snapshot.thumbnailStatus !== 'Loaded'"></sqx-loader> @if (snapshot.thumbnailStatus !== "Loaded") {
</div> <sqx-loader color="white"></sqx-loader>
}
</div>
}
<sqx-modal-dialog *sqxModal="searchDialog" size="lg" fullHeight="true" (dialogClose)="searchDialog.hide()"> <sqx-modal-dialog (dialogClose)="searchDialog.hide()" fullHeight="true" size="lg" *sqxModal="searchDialog">
<ng-container title> <ng-container title>
<input class="form-control search" [formControl]="stockPhotoSearch" sqxFocusOnInit placeholder="{{ 'contents.stockPhotoSearch' | sqxTranslate }}"> <input
class="form-control search"
<sqx-loader *ngIf="snapshot.isLoading"></sqx-loader> [formControl]="stockPhotoSearch"
placeholder="{{ 'contents.stockPhotoSearch' | sqxTranslate }}"
sqxFocusOnInit />
@if (snapshot.isLoading) {
<sqx-loader></sqx-loader>
}
</ng-container> </ng-container>
<ng-container content> <ng-container content>
<div class="photos"> <div class="photos">
<div *ngFor="let photo of snapshot.stockPhotos; trackBy: trackByPhoto" class="photo" [class.selected]="isSelected(photo)" (click)="selectPhoto(photo)"> @for (photo of snapshot.stockPhotos; track photo.thumbUrl) {
<img [src]="photo.thumbUrl"> <div class="photo" [class.selected]="isSelected(photo)" (click)="selectPhoto(photo)">
<img [src]="photo.thumbUrl" />
<div class="photo-user"> <div class="photo-user">
<a class="photo-user-link" [href]="photo.userProfileUrl" sqxExternalLink sqxStopClick> <a class="photo-user-link" [href]="photo.userProfileUrl" sqxExternalLink sqxStopClick>
{{photo.user}} {{ photo.user }}
</a> </a>
</div>
</div> </div>
</div> } @empty {
</div> <div class="empty small text-muted text-center">
{{ "contents.stockPhotoSearchEmpty" | sqxTranslate }}
<div class="empty small text-muted text-center" *ngIf="snapshot.stockPhotos.length === 0"> </div>
{{ 'contents.stockPhotoSearchEmpty' | sqxTranslate }} }
</div> </div>
<div class="mt-4 text-center" *ngIf="snapshot.hasMore"> @if (snapshot.hasMore) {
<button class="btn btn-outline-secondary" type="button" (click)="loadMore()" [disabled]="snapshot.isLoading"> <div class="mt-4 text-center">
{{ 'common.loadMore' | sqxTranslate }} <sqx-loader *ngIf="snapshot.isLoading"></sqx-loader> <button class="btn btn-outline-secondary" (click)="loadMore()" [disabled]="snapshot.isLoading" type="button">
</button> {{ "common.loadMore" | sqxTranslate }}
</div> @if (snapshot.isLoading) {
<sqx-loader></sqx-loader>
}
</button>
</div>
}
</ng-container> </ng-container>
</sqx-modal-dialog> </sqx-modal-dialog>

8
frontend/src/app/features/content/shared/forms/stock-photo-editor.component.ts

@ -5,7 +5,7 @@
* Copyright (c) Squidex UG (haftungsbeschränkt). All rights reserved. * Copyright (c) Squidex UG (haftungsbeschränkt). All rights reserved.
*/ */
import { AsyncPipe, NgFor, NgIf } from '@angular/common'; import { AsyncPipe } from '@angular/common';
import { booleanAttribute, ChangeDetectionStrategy, Component, forwardRef, Input, OnInit } from '@angular/core'; import { booleanAttribute, ChangeDetectionStrategy, Component, forwardRef, Input, OnInit } from '@angular/core';
import { FormsModule, NG_VALUE_ACCESSOR, ReactiveFormsModule, UntypedFormControl } from '@angular/forms'; import { FormsModule, NG_VALUE_ACCESSOR, ReactiveFormsModule, UntypedFormControl } from '@angular/forms';
import { BehaviorSubject, of } from 'rxjs'; import { BehaviorSubject, of } from 'rxjs';
@ -49,8 +49,6 @@ type Request = { search?: string; page: number };
LoaderComponent, LoaderComponent,
ModalDialogComponent, ModalDialogComponent,
ModalDirective, ModalDirective,
NgFor,
NgIf,
ReactiveFormsModule, ReactiveFormsModule,
StopClickDirective, StopClickDirective,
TooltipDirective, TooltipDirective,
@ -172,8 +170,4 @@ export class StockPhotoEditorComponent extends StatefulControlComponent<State, s
public isSelected(photo: StockPhotoDto) { public isSelected(photo: StockPhotoDto) {
return photo.url === this.valueControl.value; return photo.url === this.valueControl.value;
} }
public trackByPhoto(_index: number, photo: StockPhotoDto) {
return photo.thumbUrl;
}
} }

101
frontend/src/app/features/content/shared/list/content.component.html

@ -1,79 +1,82 @@
<tr [sqxTabRouterLink]="link"> <tr [sqxTabRouterLink]="link">
<td class="cell-select inline-edit" sqxStopClick> <td class="cell-select inline-edit" sqxStopClick>
<div class="form-check"> <div class="form-check">
<input class="form-check-input" type="checkbox" id="{{content.id}}_selected" <input
class="form-check-input"
id="{{ content.id }}_selected"
[ngModel]="selected" [ngModel]="selected"
(ngModelChange)="selectedChange.emit($event)" /> (ngModelChange)="selectedChange.emit($event)"
type="checkbox" />
<label class="form-check-label" for="{{content.id}}_selected" ></label> <label class="form-check-label" for="{{ content.id }}_selected"></label>
</div> </div>
<ng-container *ngIf="isDirty"> @if (isDirty) {
<div class="edit-menu"> <div class="edit-menu">
<button type="button" class="btn btn-text-secondary btn-cancel me-2" (click)="cancel()" sqxStopClick> <button class="btn btn-text-secondary btn-cancel me-2" (click)="cancel()" sqxStopClick type="button">
<i class="icon-close"></i> <i class="icon-close"></i>
</button> </button>
<button class="btn btn-success" (click)="save()" sqxStopClick type="button">
<button type="button" class="btn btn-success" (click)="save()" sqxStopClick>
<i class="icon-checkmark"></i> <i class="icon-checkmark"></i>
</button> </button>
</div> </div>
</ng-container> }
</td> </td>
<td class="cell-actions cell-actions-left" sqxStopClick> <td class="cell-actions cell-actions-left" sqxStopClick>
<button type="button" class="btn btn-text-secondary" attr.aria-label="{{ 'common.options' | sqxTranslate }}" (click)="dropdown.toggle()" #buttonOptions> <button
class="btn btn-text-secondary"
#buttonOptions
attr.aria-label="{{ 'common.options' | sqxTranslate }}"
(click)="dropdown.toggle()"
type="button">
<i class="icon-dots"></i> <i class="icon-dots"></i>
</button> </button>
<sqx-dropdown-menu *sqxModal="dropdown;closeAlways:true" [sqxAnchoredTo]="buttonOptions" scrollY="true" position="bottom-start"> <sqx-dropdown-menu position="bottom-start" scrollY="true" [sqxAnchoredTo]="buttonOptions" *sqxModal="dropdown; closeAlways: true">
<a class="dropdown-item" [routerLink]="link" target="_blank" sqxExternalLink> <a class="dropdown-item" [routerLink]="link" sqxExternalLink target="_blank">
{{ 'common.editInNewTab' | sqxTranslate }} {{ "common.editInNewTab" | sqxTranslate }}
</a> </a>
<a class="dropdown-item" *ngFor="let info of content.statusUpdates" (click)="statusChange.emit(info.status)"> @for (info of content.statusUpdates; track info) {
{{ 'common.statusChangeTo' | sqxTranslate }} <a class="dropdown-item" (click)="statusChange.emit(info.status)">
{{ "common.statusChangeTo" | sqxTranslate }}
<sqx-content-status layout="text" small="true" [status]="info.status" [statusColor]="info.color"></sqx-content-status>
</a>
}
@if (cloneable) {
<a class="dropdown-item" (click)="clone.emit(); dropdown.hide()">
{{ "common.clone" | sqxTranslate }}
</a>
}
<sqx-content-status
layout="text"
[status]="info.status"
[statusColor]="info.color"
small="true">
</sqx-content-status>
</a>
<a class="dropdown-item" (click)="clone.emit(); dropdown.hide()" *ngIf="cloneable">
{{ 'common.clone' | sqxTranslate }}
</a>
<div class="dropdown-divider"></div> <div class="dropdown-divider"></div>
<a class="dropdown-item dropdown-item-delete" [class.disabled]="!content.canDelete" <a
(sqxConfirmClick)="delete.emit()" class="dropdown-item dropdown-item-delete"
confirmTitle="i18n:contents.deleteConfirmTitle" [class.disabled]="!content.canDelete"
confirmRememberKey="deleteContent"
confirmText="i18n:contents.deleteConfirmText" confirmText="i18n:contents.deleteConfirmText"
confirmRememberKey="deleteContent"> confirmTitle="i18n:contents.deleteConfirmTitle"
{{ 'common.delete' | sqxTranslate }} (sqxConfirmClick)="delete.emit()">
{{ "common.delete" | sqxTranslate }}
</a> </a>
</sqx-dropdown-menu> </sqx-dropdown-menu>
</td> </td>
<td *ngFor="let field of tableFields" @for (field of tableFields; track field) {
sqxContentListCell <td [field]="field" [fields]="tableSettings" sqxContentListCell sqxContentListCellResize [sqxStopClick]="shouldStop(field)">
sqxContentListCellResize <sqx-content-list-field
[field]="field" [content]="content"
[fields]="tableSettings" [field]="field"
[sqxStopClick]="shouldStop(field)"> [fields]="tableSettings"
<sqx-content-list-field [language]="language"
[content]="content" [languages]="languages"
[field]="field" [patchAllowed]="patchAllowed"
[fields]="tableSettings" [patchForm]="patchForm?.form"
[language]="language" [schema]="schema"></sqx-content-list-field>
[languages]="languages" </td>
[patchAllowed]="patchAllowed" }
[patchForm]="patchForm?.form"
[schema]="schema">
</sqx-content-list-field>
</td>
<td></td> <td></td>
</tr> </tr>

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

@ -8,7 +8,7 @@
/* eslint-disable @angular-eslint/component-selector */ /* eslint-disable @angular-eslint/component-selector */
import { NgFor, NgIf } from '@angular/common';
import { booleanAttribute, ChangeDetectionStrategy, ChangeDetectorRef, Component, EventEmitter, Input, Output, QueryList, ViewChildren } from '@angular/core'; import { booleanAttribute, ChangeDetectionStrategy, ChangeDetectorRef, Component, EventEmitter, Input, Output, QueryList, ViewChildren } from '@angular/core';
import { FormsModule } from '@angular/forms'; import { FormsModule } from '@angular/forms';
import { RouterLink } from '@angular/router'; import { RouterLink } from '@angular/router';
@ -31,8 +31,6 @@ import { AppLanguageDto, ConfirmClickDirective, ContentDto, ContentListCellDirec
FormsModule, FormsModule,
ModalDirective, ModalDirective,
ModalPlacementDirective, ModalPlacementDirective,
NgFor,
NgIf,
RouterLink, RouterLink,
StopClickDirective, StopClickDirective,
TabRouterlinkDirective, TabRouterlinkDirective,

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

Loading…
Cancel
Save