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

17
frontend/src/app/_theme.html

@ -1092,11 +1092,14 @@
<i class="nav-icon icon-help2"></i>
</a>
</li>
<li class="nav-item" *ngIf="!isSettingsHidden(app)">
<a class="nav-link">
<i class="nav-icon icon-settings"></i>
</a>
</li>
@if (!isSettingsHidden(app)) {
<li class="nav-item">
<a class="nav-link">
<i class="nav-icon icon-settings"></i>
</a>
</li>
}
</ul>
</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-content">
<div class="modal-header with-tabs" *ngIf="showHeader">
<div class="modal-header with-tabs">
<h4 class="modal-title">
Title
</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-content">
<div class="modal-header" *ngIf="showHeader">
<div class="modal-header">
<h4 class="modal-title">
Title
</h4>

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

@ -1,11 +1,12 @@
<main sqxCopyGlobal>
<sqx-root-view>
<router-outlet (activate)="isLoaded = true">
<div class="loading" *ngIf="!isLoaded">
<img alt="Loading" src="./images/loader.svg">
<div>{{ 'common.loading' | sqxTranslate }}</div>
</div>
@if (!isLoaded) {
<div class="loading">
<img alt="Loading" src="./images/loader.svg" />
<div>{{ "common.loading" | sqxTranslate }}</div>
</div>
}
</router-outlet>
<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.
*/
import { NgIf } from '@angular/common';
import { Component, Injector } from '@angular/core';
import { RouterOutlet } from '@angular/router';
import { AnalyticsService, CopyGlobalDirective, DialogRendererComponent, RootViewComponent, TourGuideComponent, TourTemplateComponent, TranslatePipe } from '@app/shared';
@ -18,7 +18,6 @@ import { AnalyticsService, CopyGlobalDirective, DialogRendererComponent, RootVie
imports: [
CopyGlobalDirective,
DialogRendererComponent,
NgIf,
RootViewComponent,
RouterOutlet,
TourGuideComponent,

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

@ -2,24 +2,33 @@
<div class="sidebar">
<ul class="nav nav-panel flex-column">
<li class="nav-item" *ngIf="uiState.canReadUsers | async">
<a class="nav-link" routerLink="users" routerLinkActive="active">
<i class="nav-icon icon-user-o"></i> <div class="nav-text">{{ 'common.users' | sqxTranslate }}</div>
</a>
</li>
<li class="nav-item" *ngIf="uiState.canReadEvents | async">
<a class="nav-link" routerLink="event-consumers" routerLinkActive="active">
<i class="nav-icon icon-time"></i> <div class="nav-text">{{ 'common.consumers' | sqxTranslate }}</div>
</a>
</li>
<li class="nav-item" *ngIf="uiState.canRestore | async">
<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>
@if (uiState.canReadUsers | async) {
<li class="nav-item">
<a class="nav-link" routerLink="users" routerLinkActive="active">
<i class="nav-icon icon-user-o"></i>
<div class="nav-text">{{ "common.users" | sqxTranslate }}</div>
</a>
</li>
}
@if (uiState.canReadEvents | async) {
<li class="nav-item">
<a class="nav-link" routerLink="event-consumers" routerLinkActive="active">
<i class="nav-icon icon-time"></i>
<div class="nav-text">{{ "common.consumers" | sqxTranslate }}</div>
</a>
</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>
</div>
<div sqxLayoutContainer class="panel-container">
<div class="panel-container" sqxLayoutContainer>
<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.
*/
import { AsyncPipe, NgIf } from '@angular/common';
import { AsyncPipe } from '@angular/common';
import { Component } from '@angular/core';
import { RouterLink, RouterLinkActive, RouterOutlet } from '@angular/router';
import { LayoutContainerDirective, TitleComponent, TranslatePipe, UIState } from '@app/shared';
@ -18,7 +18,6 @@ import { LayoutContainerDirective, TitleComponent, TranslatePipe, UIState } from
imports: [
AsyncPipe,
LayoutContainerDirective,
NgIf,
RouterLink,
RouterLinkActive,
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">
<td class="cell-auto">
<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>
</td>
<td class="cell-auto-right">
<span>{{eventConsumer.count}}</span>
<span>{{ eventConsumer.count }}</span>
</td>
<td class="cell-auto-right">
<span>{{eventConsumer.position}}</span>
<span>{{ eventConsumer.position }}</span>
</td>
<td class="cell-actions-lg">
<button type="button" class="btn btn-text-secondary" (click)="reset()" *ngIf="eventConsumer.canReset" title="i18n:eventConsumers.resetTooltip">
<i class="icon icon-reset"></i>
</button>
<button type="button" class="btn btn-text-secondary" (click)="start()" *ngIf="eventConsumer.canStart" title="i18n:eventConsumers.startTooltip">
<i class="icon icon-play"></i>
</button>
<button type="button" class="btn btn-text-secondary" (click)="stop()" *ngIf="eventConsumer.canStop" title="i18n:eventConsumers.stopTooltip">
<i class="icon icon-pause"></i>
</button>
@if (eventConsumer.canReset) {
<button class="btn btn-text-secondary" (click)="reset()" title="i18n:eventConsumers.resetTooltip" type="button">
<i class="icon icon-reset"></i>
</button>
}
@if (eventConsumer.canStart) {
<button class="btn btn-text-secondary" (click)="start()" title="i18n:eventConsumers.startTooltip" type="button">
<i class="icon icon-play"></i>
</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>
</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 */
import { NgIf } from '@angular/common';
import { ChangeDetectionStrategy, Component, EventEmitter, Input, Output } from '@angular/core';
import { TooltipDirective } from '@app/shared';
import { EventConsumerDto, EventConsumersState } from '../../internal';
@ -19,7 +19,6 @@ import { EventConsumerDto, EventConsumersState } from '../../internal';
templateUrl: './event-consumer.component.html',
changeDetection: ChangeDetectionStrategy.OnPush,
imports: [
NgIf,
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-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>
<button type="button" class="btn btn-text-secondary" (click)="reload()" title="i18n:eventConsumers.refreshTooltip" shortcut="CTRL + B">
<i class="icon-reset"></i> {{ 'common.refresh' | sqxTranslate }}
<button
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>
</ng-container>
@ -14,16 +20,16 @@
<thead>
<tr>
<th class="cell-auto">
{{ 'common.name' | sqxTranslate }}
{{ "common.name" | sqxTranslate }}
</th>
<th class="cell-auto-right">
{{ 'eventConsumers.count' | sqxTranslate }}
{{ "eventConsumers.count" | sqxTranslate }}
</th>
<th class="cell-auto-right">
{{ 'eventConsumers.position' | sqxTranslate }}
{{ "eventConsumers.position" | sqxTranslate }}
</th>
<th class="cell-actions-lg">
{{ 'common.actions' | sqxTranslate }}
{{ "common.actions" | sqxTranslate }}
</th>
</tr>
</thead>
@ -31,10 +37,10 @@
</ng-container>
<ng-container>
<table class="table table-items table-fixed" [sqxSyncWidth]="header">
<tbody *ngFor="let eventConsumer of eventConsumersState.eventConsumers | async; trackBy: trackByEventConsumer"
[sqxEventConsumer]="eventConsumer" (failure)="showError(eventConsumer)">
</tbody>
<table class="table table-items table-fixed" [sqxSyncWidth]="header">
@for (eventConsumer of eventConsumersState.eventConsumers | async; track eventConsumer.name) {
<tbody (failure)="showError(eventConsumer)" [sqxEventConsumer]="eventConsumer"></tbody>
}
</table>
</ng-container>
</sqx-list-view>
@ -42,14 +48,15 @@
<ng-template sidebarMenu>
<div class="panel-nav">
<a class="panel-link"
<a
class="panel-link"
queryParamsHandling="preserve"
replaceUrl="true"
routerLink="help"
routerLinkActive="active"
queryParamsHandling="preserve"
sqxTourStep="help"
title="i18n:common.help"
titlePosition="left"
sqxTourStep="help">
titlePosition="left">
<i class="icon-help2"></i>
</a>
</div>
@ -58,12 +65,12 @@
<router-outlet></router-outlet>
<sqx-modal-dialog *sqxModal="eventConsumerErrorDialog" (dialogClose)="eventConsumerErrorDialog.hide()">
<sqx-modal-dialog (dialogClose)="eventConsumerErrorDialog.hide()" *sqxModal="eventConsumerErrorDialog">
<ng-container title>
{{ 'common.error' | sqxTranslate }}
{{ "common.error" | sqxTranslate }}
</ng-container>
<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>
</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.
*/
import { AsyncPipe, NgFor } from '@angular/common';
import { AsyncPipe } from '@angular/common';
import { Component, OnInit } from '@angular/core';
import { RouterLink, RouterLinkActive, RouterOutlet } from '@angular/router';
import { timer } from 'rxjs';
@ -26,7 +26,6 @@ import { EventConsumerComponent } from './event-consumer.component';
ListViewComponent,
ModalDialogComponent,
ModalDirective,
NgFor,
RouterLink,
RouterLinkActive,
RouterOutlet,
@ -62,10 +61,6 @@ export class EventConsumersPageComponent implements OnInit {
this.eventConsumersState.load(true, false);
}
public trackByEventConsumer(_index: number, es: EventConsumerDto) {
return es.name;
}
public showError(eventConsumer: EventConsumerDto) {
this.eventConsumerError = eventConsumer.error;
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-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>
<sqx-list-view innerWidth="70rem">
<div class="card section" *ngIf="restoreJob | async; let job">
<div class="card-header">
<div class="row gx-2 align-items-center">
<div class="col-auto">
<div *ngIf="job.status === 'Started'" class="restore-status restore-status-pending spin">
<i class="icon-hour-glass"></i>
@if (restoreJob | async; as job) {
<div class="card section">
<div class="card-header">
<div class="row gx-2 align-items-center">
<div class="col-auto">
@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 *ngIf="job.status === 'Failed'" class="restore-status restore-status-failed">
<i class="icon-exclamation"></i>
<div class="col">
<h3>{{ "jobs.restoreLastStatus" | sqxTranslate }}</h3>
</div>
<div *ngIf="job.status === 'Completed'" class="restore-status restore-status-success">
<i class="icon-checkmark"></i>
<div class="col text-end restore-url">
{{ job.url }}
</div>
</div>
<div class="col">
<h3>{{ 'jobs.restoreLastStatus' | sqxTranslate }}</h3>
</div>
<div class="col text-end restore-url">
{{job.url}}
</div>
</div>
</div>
<div class="card-body">
<div *ngFor="let row of job.log">
{{row}}
<div class="card-body">
@for (row of job.log; track row) {
<div>
{{ row }}
</div>
}
</div>
</div>
<div class="card-footer small text-muted">
<div class="row">
<div class="col">
{{ 'jobs.restoreStartedLabel' | sqxTranslate }}: {{job.started | sqxISODate}}
</div>
<div class="col text-end" *ngIf="job.stopped">
{{ 'jobs.restoreStoppedLabel' | sqxTranslate }}: {{job.stopped | sqxISODate}}
<div class="card-footer small text-muted">
<div class="row">
<div class="col">{{ "jobs.restoreStartedLabel" | sqxTranslate }}: {{ job.started | sqxISODate }}</div>
@if (job.stopped) {
<div class="col text-end">
{{ "jobs.restoreStoppedLabel" | sqxTranslate }}: {{ job.stopped | sqxISODate }}
</div>
}
</div>
</div>
</div>
</div>
}
<div class="table-items-row table-items-row-summary">
<form [formGroup]="restoreForm.form" (ngSubmit)="restore()">
<div class="row gx-2">
<div class="col">
<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 class="col">
<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 class="col-auto">
<button type="submit" class="btn btn-success" [disabled]="restoreForm.hasNoUrl | async">
{{ 'jobs.restore' | sqxTranslate }}
<button class="btn btn-success" [disabled]="restoreForm.hasNoUrl | async" type="submit">
{{ "jobs.restore" | sqxTranslate }}
</button>
</div>
</div>
@ -70,18 +81,19 @@
<ng-template sidebarMenu>
<div class="panel-nav">
<a class="panel-link"
<a
class="panel-link"
queryParamsHandling="preserve"
replaceUrl="true"
routerLink="help"
routerLinkActive="active"
queryParamsHandling="preserve"
sqxTourStep="help"
title="i18n:common.help"
titlePosition="left"
sqxTourStep="help">
titlePosition="left">
<i class="icon-help2"></i>
</a>
</div>
</ng-template>
</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.
*/
import { AsyncPipe, NgFor, NgIf } from '@angular/common';
import { AsyncPipe } from '@angular/common';
import { Component } from '@angular/core';
import { FormsModule, ReactiveFormsModule } from '@angular/forms';
import { RouterLink, RouterLinkActive, RouterOutlet } from '@angular/router';
@ -24,8 +24,6 @@ import { AuthService, ControlErrorsComponent, DialogService, ISODatePipe, JobsSe
ISODatePipe,
LayoutComponent,
ListViewComponent,
NgFor,
NgIf,
ReactiveFormsModule,
RouterLink,
RouterLinkActive,

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

@ -1,79 +1,104 @@
<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 *ngIf="usersState.selectedUser | async; else noUserTitle">
@if (usersState.selectedUser | async) {
<sqx-title message="i18n:users.editPageTitle"></sqx-title>
<h3>{{ 'users.editTitle' | sqxTranslate }}</h3>
</ng-container>
<ng-template #noUserTitle>
<h3>{{ "users.editTitle" | sqxTranslate }}</h3>
} @else {
<sqx-title message="i18n:users.createPageTitle"></sqx-title>
<h3>{{ 'users.createTitle' | sqxTranslate }}</h3>
</ng-template>
<h3>{{ "users.createTitle" | sqxTranslate }}</h3>
}
</ng-container>
<ng-container menu>
<ng-container *ngIf="usersState.selectedUser | async; let user; else noUserMenu">
<button type="submit" class="btn btn-primary" shortcut="CTRL + SHIFT + S" *ngIf="isEditable">
{{ 'common.save' | sqxTranslate }}
</button>
</ng-container>
<ng-template #noUserMenu>
<button type="submit" class="btn btn-primary" shortcut="CTRL + SHIFT + S">
{{ 'common.save' | sqxTranslate }}
@if (usersState.selectedUser | async; as user) {
@if (isEditable) {
<button class="btn btn-primary" shortcut="CTRL + SHIFT + S" type="submit">
{{ "common.save" | sqxTranslate }}
</button>
}
} @else {
<button class="btn btn-primary" shortcut="CTRL + SHIFT + S" type="submit">
{{ "common.save" | sqxTranslate }}
</button>
</ng-template>
}
</ng-container>
<ng-container >
<ng-container>
<sqx-form-error [error]="userForm.error | async"></sqx-form-error>
<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>
<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 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>
<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 class="form-group form-group-section">
<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>
<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 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>
<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 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>
<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>
</ng-container>
</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.
*/
import { AsyncPipe, NgIf } from '@angular/common';
import { AsyncPipe } from '@angular/common';
import { Component, OnInit } from '@angular/core';
import { FormsModule, ReactiveFormsModule } from '@angular/forms';
import { ActivatedRoute, Router } from '@angular/router';
@ -23,7 +23,6 @@ import { UpsertUserDto, UserDto, UserForm, UsersState } from '../../internal';
FormErrorComponent,
FormsModule,
LayoutComponent,
NgIf,
ReactiveFormsModule,
ShortcutDirective,
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">
<img class="user-picture" title="{{user.displayName}}" [src]="user | sqxUserDtoPicture">
<img class="user-picture" [src]="user | sqxUserDtoPicture" title="{{ user.displayName }}" />
</td>
<td class="cell-auto">
<span class="user-name table-cell">{{user.displayName}}</span>
<span class="user-name table-cell">{{ user.displayName }}</span>
</td>
<td class="cell-auto">
<span class="user-email table-cell">{{user.email}}</span>
<span class="user-email table-cell">{{ user.email }}</span>
</td>
<td class="cell-actions-lg">
<button type="button" class="btn btn-text-secondary" (click)="lock()" sqxStopClick *ngIf="user.canLock" title="i18n:users.lockTooltip">
<i class="icon icon-unlocked"></i>
</button>
<button type="button" class="btn btn-text-secondary" (click)="unlock()" sqxStopClick *ngIf="user.canUnlock" title="i18n:users.unlockTooltip">
<i class="icon icon-lock"></i>
</button>
@if (user.canLock) {
<button class="btn btn-text-secondary" (click)="lock()" sqxStopClick title="i18n:users.lockTooltip" type="button">
<i class="icon icon-unlocked"></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"
(sqxConfirmClick)="delete()"
confirmTitle="i18n:users.deleteConfirmTitle"
confirmText="i18n:users.deleteConfirmText"
<button
class="btn btn-text-danger"
confirmRememberKey="deleteUser"
sqxStopClick>
confirmText="i18n:users.deleteConfirmText"
confirmTitle="i18n:users.deleteConfirmTitle"
[disabled]="!user.canDelete"
(sqxConfirmClick)="delete()"
sqxStopClick
type="button">
<i class="icon-bin2"></i>
</button>
</td>
</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 */
import { NgIf } from '@angular/common';
import { ChangeDetectionStrategy, Component, Input } from '@angular/core';
import { RouterLink, RouterLinkActive } from '@angular/router';
import { ConfirmClickDirective, StopClickDirective, TooltipDirective, UserDtoPicture } from '@app/shared';
@ -21,7 +21,6 @@ import { UserDto, UsersState } from '../../internal';
changeDetection: ChangeDetectionStrategy.OnPush,
imports: [
ConfirmClickDirective,
NgIf,
RouterLink,
RouterLinkActive,
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-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>
<div class="d-flex justify-content-end">
<button type="button" class="btn btn-text-secondary" (click)="reload()" title="i18n:users.refreshTooltip" shortcut="CTRL + B">
<i class="icon-reset"></i> {{ 'common.refresh' | sqxTranslate }}
<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 }}
</button>
<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"
shortcutAction="focus">
shortcutAction="focus" />
</form>
<ng-container *ngIf="usersState.canCreate | async">
<button type="button" class="btn btn-success ms-2" routerLink="new" title="i18n:users.createTooltip" shortcut="CTRL + U">
<i class="icon-plus"></i> {{ 'users.create' | sqxTranslate }}
@if (usersState.canCreate | async) {
<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 }}
</button>
</ng-container>
}
</div>
</ng-container>
@ -27,29 +32,31 @@
<table class="table table-items table-fixed" #header>
<thead>
<tr>
<th class="cell-user">
&nbsp;
</th>
<th class="cell-user">&nbsp;</th>
<th class="cell-auto">
<span class="truncate">{{ 'common.name' | sqxTranslate }}</span>
<span class="truncate">{{ "common.name" | sqxTranslate }}</span>
</th>
<th class="cell-auto">
<span class="truncate">{{ 'common.email' | sqxTranslate }}</span>
<span class="truncate">{{ "common.email" | sqxTranslate }}</span>
</th>
<th class="cell-actions-lg">
<span class="truncate">{{ 'common.actions' | sqxTranslate }}</span>
<span class="truncate">{{ "common.actions" | sqxTranslate }}</span>
</th>
</tr>
</thead>
</table>
</ng-container>
<ng-container>
<table class="table table-items table-fixed" *ngIf="usersState.users | async; let users" [sqxSyncWidth]="header">
<tbody *ngFor="let user of users; trackBy: trackByUser" [sqxUser]="user"></tbody>
</table>
@if (usersState.users | async; as users) {
<table class="table table-items table-fixed" [sqxSyncWidth]="header">
@for (user of users; track user.id) {
<tbody [sqxUser]="user"></tbody>
}
</table>
}
</ng-container>
<ng-container footer>
<sqx-pager [paging]="usersState.paging | async" (pagingChange)="usersState.page($event)"></sqx-pager>
</ng-container>
@ -58,18 +65,19 @@
<ng-template sidebarMenu>
<div class="panel-nav">
<a class="panel-link"
<a
class="panel-link"
queryParamsHandling="preserve"
replaceUrl="true"
routerLink="help"
routerLinkActive="active"
queryParamsHandling="preserve"
sqxTourStep="help"
title="i18n:common.help"
titlePosition="left"
sqxTourStep="help">
titlePosition="left">
<i class="icon-help2"></i>
</a>
</div>
</ng-template>
</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.
*/
import { AsyncPipe, NgFor, NgIf } from '@angular/common';
import { AsyncPipe } from '@angular/common';
import { Component, OnInit } from '@angular/core';
import { FormsModule, ReactiveFormsModule, UntypedFormControl } from '@angular/forms';
import { RouterLink, RouterLinkActive, RouterOutlet } from '@angular/router';
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';
@Component({
@ -26,8 +26,6 @@ import { UserComponent } from './user.component';
FormsModule,
LayoutComponent,
ListViewComponent,
NgFor,
NgIf,
PagerComponent,
ReactiveFormsModule,
RouterLink,
@ -75,8 +73,4 @@ export class UsersPageComponent implements OnInit {
public search() {
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.
*/
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 { ApiUrlConfig, Resource, ResourceLinks } from '@app/shared';
import { EventConsumerDto, EventConsumersDto, EventConsumersService } from './event-consumers.service';
@ -13,14 +14,14 @@ import { EventConsumerDto, EventConsumersDto, EventConsumersService } from './ev
describe('EventConsumersService', () => {
beforeEach(() => {
TestBed.configureTestingModule({
imports: [
HttpClientTestingModule,
],
providers: [
EventConsumersService,
{ provide: ApiUrlConfig, useValue: new ApiUrlConfig('http://service/p/') },
],
});
imports: [],
providers: [
provideHttpClient(withInterceptorsFromDi()),
provideHttpClientTesting(),
EventConsumersService,
{ provide: ApiUrlConfig, useValue: new ApiUrlConfig('http://service/p/') },
],
});
});
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.
*/
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 { ApiUrlConfig, Resource, ResourceLinks } from '@app/shared';
import { UserDto, UsersDto, UsersService } from './users.service';
@ -13,14 +14,14 @@ import { UserDto, UsersDto, UsersService } from './users.service';
describe('UsersService', () => {
beforeEach(() => {
TestBed.configureTestingModule({
imports: [
HttpClientTestingModule,
],
providers: [
UsersService,
{ provide: ApiUrlConfig, useValue: new ApiUrlConfig('http://service/p/') },
],
});
imports: [],
providers: [
provideHttpClient(withInterceptorsFromDi()),
provideHttpClientTesting(),
UsersService,
{ provide: ApiUrlConfig, useValue: new ApiUrlConfig('http://service/p/') },
],
});
});
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-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>
<ul class="nav nav-light flex-column">
<li class="nav-item" sqxTourStep="graphql">
<a class="nav-link" routerLink="graphql" routerLinkActive="active">
{{ 'api.graphql' | sqxTranslate }}
{{ "api.graphql" | sqxTranslate }}
</a>
</li>
<li class="nav-item nav-heading">
{{ 'common.openAPI' | sqxTranslate }}
{{ "common.openAPI" | sqxTranslate }}
</li>
<li class="nav-item" sqxTourStep="contentApi">
<a class="nav-link" href="/api/content/{{appsState.appName}}/docs" sqxExternalLink>
{{ 'api.contentApi' | sqxTranslate }}
<a class="nav-link" href="/api/content/{{ appsState.appName }}/docs" sqxExternalLink>
{{ "api.contentApi" | sqxTranslate }}
</a>
</li>
<li class="nav-item" sqxTourStep="generalAPi">
<a class="nav-link" href="/api/docs" sqxExternalLink>
{{ 'api.generalApi' | sqxTranslate }}
{{ "api.generalApi" | sqxTranslate }}
</a>
</li>
</ul>
</ng-container>
</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-layout layout="main" hideHeader="true" hideSidebar="true">
<div inner #graphiQLContainer sqxTourStep="graphQLExplorer"></div>
<button class="btn btn-simple btn-options" *ngIf="clientsReadable" (click)="clientsDialog.show()">
<i class="icon-clients"></i>
</button>
<sqx-layout hideHeader="true" hideSidebar="true" layout="main">
<div #graphiQLContainer inner sqxTourStep="graphQLExplorer"></div>
@if (clientsReadable) {
<button class="btn btn-simple btn-options" (click)="clientsDialog.show()">
<i class="icon-clients"></i>
</button>
}
</sqx-layout>
<sqx-modal-dialog *sqxModal="clientsDialog" (dialogClose)="clientsDialog.hide()">
<sqx-modal-dialog (dialogClose)="clientsDialog.hide()" *sqxModal="clientsDialog">
<ng-container title>
{{ 'api.selectClient' | sqxTranslate }}
{{ "api.selectClient" | sqxTranslate }}
</ng-container>
<ng-container content>
<sqx-form-hint>
{{ 'api.selectClientDescription' | sqxTranslate }}
{{ "api.selectClientDescription" | sqxTranslate }}
</sqx-form-hint>
<div class="form-group">
<label for="client">{{ 'common.client' | sqxTranslate }}</label>
<select class="form-control" id="client"
[ngModel]="clientSelected"
(ngModelChange)="selectClient($event)">
<option [ngValue]="null">{{ 'api.noClient' | sqxTranslate }}</option>
<option *ngFor="let client of clientsState.clients | async" [ngValue]="client">{{client.id}}</option>
<label for="client">{{ "common.client" | sqxTranslate }}</label>
<select class="form-control" id="client" [ngModel]="clientSelected" (ngModelChange)="selectClient($event)">
<option [ngValue]="null">{{ "api.noClient" | sqxTranslate }}</option>
@for (client of clientsState.clients | async; track client) {
<option [ngValue]="client">{{ client.id }}</option>
}
</select>
</div>
</ng-container>
<ng-container footer>
<button type="button" class="btn btn-text-secondary" (click)="clientsDialog.hide()">
{{ 'common.close' | sqxTranslate }}
<button class="btn btn-text-secondary" (click)="clientsDialog.hide()" type="button">
{{ "common.close" | sqxTranslate }}
</button>
</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.
*/
import { AsyncPipe, NgFor, NgIf } from '@angular/common';
import { AsyncPipe } from '@angular/common';
import { AfterViewInit, Component, ElementRef, OnInit, ViewChild } from '@angular/core';
import { FormsModule } from '@angular/forms';
import { createGraphiQLFetcher } from '@graphiql/toolkit';
@ -26,8 +26,6 @@ import { ApiUrlConfig, AppsState, AuthService, ClientDto, ClientsService, Client
LayoutComponent,
ModalDialogComponent,
ModalDirective,
NgFor,
NgIf,
TitleComponent,
TooltipDirective,
TourStepDirective,

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

@ -2,43 +2,47 @@
<div class="card-body" sqxTourStep="app">
<div class="row g-0">
<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 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">
<a [routerLink]="['/app', app.name]" sqxStopClick>{{ 'common.edit' | sqxTranslate }}</a>
<a [routerLink]="['/app', app.name]" sqxStopClick>{{ "common.edit" | sqxTranslate }}</a>
<span class="deeplinks">
&nbsp;|
<a [routerLink]="['/app', app.name, 'content']" sqxStopClick>{{ 'common.content' | sqxTranslate }}</a> &middot;
<a [routerLink]="['/app', app.name, 'assets']" sqxStopClick>{{ 'common.assets' | sqxTranslate }}</a> &middot;
<a [routerLink]="['/app', app.name, 'settings']" sqxStopClick>{{ 'common.settings' | sqxTranslate }}</a>
<a [routerLink]="['/app', app.name, 'content']" sqxStopClick>{{ "common.content" | sqxTranslate }}</a>
&middot;
<a [routerLink]="['/app', app.name, 'assets']" sqxStopClick>{{ "common.assets" | sqxTranslate }}</a>
&middot;
<a [routerLink]="['/app', app.name, 'settings']" sqxStopClick>{{ "common.settings" | sqxTranslate }}</a>
</span>
</div>
<div class="card-text" *ngIf="app.description">
{{app.description}}
</div>
@if (app.description) {
<div class="card-text">
{{ app.description }}
</div>
}
</div>
</div>
<ng-container *ngIf="app.canLeave">
<button type="button" class="btn btn-sm btn-text-secondary" (click)="dropdown.toggle()" sqxStopClick #buttonOptions>
<span class="hidden">{{ 'common.options' | sqxTranslate }}</span>
@if (app.canLeave) {
<button class="btn btn-sm btn-text-secondary" #buttonOptions (click)="dropdown.toggle()" sqxStopClick type="button">
<span class="hidden">{{ "common.options" | sqxTranslate }}</span>
<i class="icon-dots"></i>
</button>
<sqx-dropdown-menu *sqxModal="dropdown;closeAlways:true" [sqxAnchoredTo]="buttonOptions" scrollY="true">
<a class="dropdown-item dropdown-item-delete"
(sqxConfirmClick)="leave.emit(app)"
confirmTitle="i18n:apps.leaveConfirmTitle"
<sqx-dropdown-menu scrollY="true" [sqxAnchoredTo]="buttonOptions" *sqxModal="dropdown; closeAlways: true">
<a
class="dropdown-item dropdown-item-delete"
confirmRememberKey="leaveApp"
confirmText="i18n:apps.leaveConfirmText"
confirmRememberKey="leaveApp">
{{ 'apps.leave' | sqxTranslate }}
confirmTitle="i18n:apps.leaveConfirmTitle"
(sqxConfirmClick)="leave.emit(app)">
{{ "apps.leave" | sqxTranslate }}
</a>
</sqx-dropdown-menu>
</ng-container>
}
</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.
*/
import { NgIf } from '@angular/common';
import { ChangeDetectionStrategy, Component, EventEmitter, Input, Output } from '@angular/core';
import { RouterLink } from '@angular/router';
import { AppDto, AvatarComponent, ConfirmClickDirective, DropdownMenuComponent, ModalDirective, ModalModel, ModalPlacementDirective, StopClickDirective, TourStepDirective, TranslatePipe } from '@app/shared';
@ -22,7 +22,6 @@ import { AppDto, AvatarComponent, ConfirmClickDirective, DropdownMenuComponent,
DropdownMenuComponent,
ModalDirective,
ModalPlacementDirective,
NgIf,
RouterLink,
StopClickDirective,
TourStepDirective,

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

@ -1,79 +1,77 @@
<sqx-title message="i18n:apps.listPageTitle"></sqx-title>
<div class="panel-container page" *ngIf="authState.userChanges | async; let user">
<div class="apps-section">
<h1 class="apps-title">{{ 'apps.welcomeTitle' | sqxTranslate: { user: user.displayName } }}</h1>
<div class="subtext">
{{ '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>
@if (authState.userChanges | async; as user) {
<div class="panel-container page">
<div class="apps-section">
<h1 class="apps-title">{{ "apps.welcomeTitle" | sqxTranslate: { user: user.displayName } }}</h1>
<div class="subtext">
{{ "apps.welcomeSubtitle" | sqxTranslate }}
</div>
</div>
</ng-container>
<div class="apps-section" *ngIf="(uiState.settings | async)?.canCreateApps">
<div class="card card-template card-href" data-testid="new-app" (click)="createNewApp()" sqxTourStep="addApp">
<div class="card-body">
<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>
@if (groupedApps | async; as groups) {
<div class="apps-section" sqxTourStep="allApps">
@for (group of groups; track trackByGroup($index, group)) {
<div class="team">
@if (group.team) {
<div class="team-header">
<sqx-team (leave)="leaveTeam($event)" [team]="group.team"></sqx-team>
</div>
}
<div class="team-body" [class.padded]="group.team">
@for (app of group.apps; track app.id) {
<sqx-app [app]="app" (leave)="leaveApp($event)"></sqx-app>
} @empty {
<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 class="card card-template card-href" *ngFor="let template of templates | async" (click)="createNewApp(template)">
<div class="card-body">
<div class="card-image">
<img src="./images/add-template.svg">
}
@if ((uiState.settings | async)?.canCreateApps) {
<div class="apps-section">
<div class="card card-template card-href" (click)="createNewApp()" data-testid="new-app" sqxTourStep="addApp">
<div class="card-body">
<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>
<h3 class="card-title">{{template.title}}</h3>
<sqx-form-hint>
{{template.description}}
</sqx-form-hint>
@for (template of templates | async; track template) {
<div class="card card-template card-href" (click)="createNewApp(template)">
<div class="card-body">
<div class="card-image">
<img src="./images/add-template.svg" />
</div>
<h3 class="card-title">{{ template.title }}</h3>
<sqx-form-hint>
{{ template.description }}
</sqx-form-hint>
</div>
</div>
}
</div>
</div>
</div>
<div *ngIf="info" class="apps-section">
<small class="info">{{info}}</small>
}
@if (info) {
<div class="apps-section">
<small class="info">{{ info }}</small>
</div>
}
</div>
</div>
}
<sqx-app-form *sqxModal="addAppDialog"
(dialogClose)="addAppDialog.hide()" [template]="addAppTemplate">
</sqx-app-form>
<sqx-app-form (dialogClose)="addAppDialog.hide()" *sqxModal="addAppDialog" [template]="addAppTemplate"></sqx-app-form>
<sqx-onboarding-dialog *sqxModal="onboardingDialog"
(dialogClose)="onboardingDialog.hide()">
</sqx-onboarding-dialog>
<sqx-onboarding-dialog (dialogClose)="onboardingDialog.hide()" *sqxModal="onboardingDialog"></sqx-onboarding-dialog>
<sqx-news-dialog *sqxModal="newsDialog" [features]="newsFeatures!"
(dialogClose)="newsDialog.hide()">
</sqx-news-dialog>
<sqx-news-dialog (dialogClose)="newsDialog.hide()" [features]="newsFeatures!" *sqxModal="newsDialog"></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.
*/
import { AsyncPipe, NgFor, NgIf } from '@angular/common';
import { AsyncPipe } from '@angular/common';
import { Component, OnInit } from '@angular/core';
import { combineLatest } from 'rxjs';
import { map, take } from 'rxjs/operators';
@ -29,8 +29,6 @@ type GroupedApps = { team?: TeamDto; apps: AppDto[] };
FormHintComponent,
ModalDirective,
NewsDialogComponent,
NgFor,
NgIf,
OnboardingDialogComponent,
TeamComponent,
TitleComponent,
@ -141,10 +139,6 @@ export class AppsPageComponent implements OnInit {
this.teamsState.leave(team);
}
public trackByApp(_index: number, app: AppDto) {
return app.id;
}
public trackByGroup(_index: number, group: GroupedApps) {
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>
{{ 'news.title' | sqxTranslate }}
{{ "news.title" | sqxTranslate }}
</ng-container>
<ng-container content>
<div class="help">
<h1>{{ 'news.headline' | sqxTranslate }}</h1>
<h1>{{ "news.headline" | sqxTranslate }}</h1>
<div *ngFor="let feature of features; trackBy: trackByFeature; let last = last">
<h4>{{feature.name}}</h4>
<div [innerHTML]="feature.text | sqxHelpMarkdown"></div>
<hr [class.hidden]="last" />
</div>
@for (feature of features; track feature; let last = $last) {
<div>
<h4>{{ feature.name }}</h4>
<div [innerHTML]="feature.text | sqxHelpMarkdown"></div>
<hr [class.hidden]="last" />
</div>
}
</div>
</ng-container>
</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.
*/
import { NgFor } from '@angular/common';
import { Component, EventEmitter, Input, Output } from '@angular/core';
import { FeatureDto, HelpMarkdownPipe, ModalDialogComponent, TooltipDirective, TranslatePipe } from '@app/shared';
@ -17,7 +17,6 @@ import { FeatureDto, HelpMarkdownPipe, ModalDialogComponent, TooltipDirective, T
imports: [
HelpMarkdownPipe,
ModalDialogComponent,
NgFor,
TooltipDirective,
TranslatePipe,
],
@ -28,8 +27,4 @@ export class NewsDialogComponent {
@Input({ required: true })
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">
<ng-container tabs>
<div class="squid d-flex align-items-center justify-content-center">
<img src="./images/squid.svg">
<img src="./images/squid.svg" />
</div>
</ng-container>
<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">
<img @fade class="header-left" src="./images/logo-white-small.png">
<div @slide class="onboarding-enter-leave text-center">
<h1>{{ 'tour.welcome' | sqxTranslate }} <span class="header-focus">{{ 'tour.welcomeProduct' | sqxTranslate }}</span></h1>
<div [innerHTML]="'tour.stepIntroText' | sqxTranslate | sqxMarkdown | sqxSafeHtml"></div>
<div class="mt-4">
<button (click)="next()" class="btn btn-success">
{{ 'tour.stepIntroNext' | sqxTranslate }}
</button>
@if (step === 0) {
<div class="onboarding-step">
<img class="header-left" @fade src="./images/logo-white-small.png" />
<div class="onboarding-enter-leave text-center" @slide>
<h1>
{{ "tour.welcome" | sqxTranslate }}
<span class="header-focus">{{ "tour.welcomeProduct" | sqxTranslate }}</span>
</h1>
<div [innerHTML]="'tour.stepIntroText' | sqxTranslate | sqxMarkdown | sqxSafeHtml"></div>
<div class="mt-4">
<button class="btn btn-success" (click)="next()">
{{ "tour.stepIntroNext" | sqxTranslate }}
</button>
</div>
</div>
</div>
</div>
}
<div *ngIf="step === 1">
<img @fade class="header-left" src="./images/logo-white-small.png">
<div @slide class="onboarding-enter-leave">
<form [formGroup]="answersForm" (ngSubmit)="submitAnswers()">
<h2>{{ 'tour.stepDataTitle' | sqxTranslate }}</h2>
<div [innerHTML]="'tour.stepDataText' | sqxTranslate | sqxMarkdown | sqxSafeHtml"></div>
<div class="form-group mt-4">
<label for="role">{{ 'tour.stepDataCompanyRole' | sqxTranslate }}</label>
<select class="form-select" id="companyRole" formControlName="companyRole">
<option [ngValue]="'RoleEmployee'">{{ 'tour.roleEmployee' | sqxTranslate }}</option>
<option [ngValue]="'RoleBusinessOwner'">{{ 'tour.roleBusinessOwner' | sqxTranslate }}</option>
<option [ngValue]="'RoleProductManager'">{{ 'tour.roleProductManager' | sqxTranslate }}</option>
<option [ngValue]="'RoleContentCreator'">{{ 'tour.roleContentCreator' | sqxTranslate }}</option>
<option [ngValue]="'RoleSoftwareDeveloper'">{{ 'tour.roleSoftwareDeveloper' | sqxTranslate }}</option>
<option [ngValue]="'RoleBusinessAnalyst'">{{ 'tour.roleBusinessAnalyst' | sqxTranslate }}</option>
</select>
</div>
<div class="form-group">
<label for="companySize">{{ 'tour.stepDataCompanySize' | sqxTranslate }}</label>
<select class="form-select" id="companySize" formControlName="companySize">
<option [ngValue]="'SizeSingle'">{{ 'tour.sizeSingle' | sqxTranslate }}</option>
<option [ngValue]="'SizeSmall'">{{ 'tour.sizeSmall' | sqxTranslate }}</option>
<option [ngValue]="'SizeMedium'">{{ 'tour.sizeMedium' | sqxTranslate }}</option>
<option [ngValue]="'SizeLarge'">{{ 'tour.sizeLarge' | sqxTranslate }}</option>
<option [ngValue]="'SizeVeryLarge'">{{ 'tour.sizeVeryLarge' | sqxTranslate }}</option>
</select>
</div>
<div class="form-group">
<label for="project">{{ 'tour.stepDataProject' | sqxTranslate }}</label>
<select class="form-select" id="project" formControlName="project">
<option [ngValue]="'ProjectNewsMagazine'">{{ 'tour.projectNewsMagazine' | sqxTranslate }}</option>
<option [ngValue]="'ProjectPersonalBlog'">{{ 'tour.projectPersonalBlog' | sqxTranslate }}</option>
<option [ngValue]="'ProjectSmallBusiness'">{{ 'tour.projectSmallBusiness' | sqxTranslate }}</option>
<option [ngValue]="'ProjectCommerce'">{{ 'tour.projectCommerce' | sqxTranslate }}</option>
<option [ngValue]="'ProjectMobileApp'">{{ 'tour.projectMobileApp' | sqxTranslate }}</option>
<option [ngValue]="'ProjectBackend'">{{ 'tour.projectBackend' | sqxTranslate }}</option>
<option [ngValue]="'ProjectLearning'">{{ 'tour.projectLearning' | sqxTranslate }}</option>
</select>
</div>
<button type="submit" class="btn btn-success">{{ 'tour.stepDataNext' | sqxTranslate }}</button>
</form>
@if (step === 1) {
<div>
<img class="header-left" @fade src="./images/logo-white-small.png" />
<div class="onboarding-enter-leave" @slide>
<form [formGroup]="answersForm" (ngSubmit)="submitAnswers()">
<h2>{{ "tour.stepDataTitle" | sqxTranslate }}</h2>
<div [innerHTML]="'tour.stepDataText' | sqxTranslate | sqxMarkdown | sqxSafeHtml"></div>
<div class="form-group mt-4">
<label for="role">{{ "tour.stepDataCompanyRole" | sqxTranslate }}</label>
<select class="form-select" id="companyRole" formControlName="companyRole">
<option [ngValue]="'RoleEmployee'">{{ "tour.roleEmployee" | sqxTranslate }}</option>
<option [ngValue]="'RoleBusinessOwner'">{{ "tour.roleBusinessOwner" | sqxTranslate }}</option>
<option [ngValue]="'RoleProductManager'">{{ "tour.roleProductManager" | sqxTranslate }}</option>
<option [ngValue]="'RoleContentCreator'">{{ "tour.roleContentCreator" | sqxTranslate }}</option>
<option [ngValue]="'RoleSoftwareDeveloper'">{{ "tour.roleSoftwareDeveloper" | sqxTranslate }}</option>
<option [ngValue]="'RoleBusinessAnalyst'">{{ "tour.roleBusinessAnalyst" | sqxTranslate }}</option>
</select>
</div>
<div class="form-group">
<label for="companySize">{{ "tour.stepDataCompanySize" | sqxTranslate }}</label>
<select class="form-select" id="companySize" formControlName="companySize">
<option [ngValue]="'SizeSingle'">{{ "tour.sizeSingle" | sqxTranslate }}</option>
<option [ngValue]="'SizeSmall'">{{ "tour.sizeSmall" | sqxTranslate }}</option>
<option [ngValue]="'SizeMedium'">{{ "tour.sizeMedium" | sqxTranslate }}</option>
<option [ngValue]="'SizeLarge'">{{ "tour.sizeLarge" | sqxTranslate }}</option>
<option [ngValue]="'SizeVeryLarge'">{{ "tour.sizeVeryLarge" | sqxTranslate }}</option>
</select>
</div>
<div class="form-group">
<label for="project">{{ "tour.stepDataProject" | sqxTranslate }}</label>
<select class="form-select" id="project" formControlName="project">
<option [ngValue]="'ProjectNewsMagazine'">{{ "tour.projectNewsMagazine" | sqxTranslate }}</option>
<option [ngValue]="'ProjectPersonalBlog'">{{ "tour.projectPersonalBlog" | sqxTranslate }}</option>
<option [ngValue]="'ProjectSmallBusiness'">{{ "tour.projectSmallBusiness" | sqxTranslate }}</option>
<option [ngValue]="'ProjectCommerce'">{{ "tour.projectCommerce" | sqxTranslate }}</option>
<option [ngValue]="'ProjectMobileApp'">{{ "tour.projectMobileApp" | sqxTranslate }}</option>
<option [ngValue]="'ProjectBackend'">{{ "tour.projectBackend" | sqxTranslate }}</option>
<option [ngValue]="'ProjectLearning'">{{ "tour.projectLearning" | sqxTranslate }}</option>
</select>
</div>
<button class="btn btn-success" type="submit">
{{ "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>
<div class="mt-4">
<button (click)="start()" class="btn btn-success">
{{ 'tour.startYes' | sqxTranslate }}
</button>
<button (click)="cancel()" class="btn btn-outline-secondary ms-2">
{{ 'tour.startNo' | sqxTranslate }}
</button>
@if (step === 2) {
<div class="onboarding-step">
<img class="header-left" @fade src="./images/logo-white-small.png" />
<div class="onboarding-enter-leave text-center" @slide>
<h2>{{ "tour.stepTourTitle" | sqxTranslate }}</h2>
<div [innerHTML]="'tour.stepTourText' | sqxTranslate | sqxMarkdown | sqxSafeHtml"></div>
<div class="mt-4">
<button class="btn btn-success" (click)="start()">
{{ "tour.startYes" | sqxTranslate }}
</button>
<button class="btn btn-outline-secondary ms-2" (click)="cancel()">
{{ "tour.startNo" | sqxTranslate }}
</button>
</div>
</div>
</div>
</div>
}
</ng-container>
</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.
*/
import { NgIf } from '@angular/common';
import { Component, EventEmitter, Output } from '@angular/core';
import { FormBuilder, FormsModule, ReactiveFormsModule } from '@angular/forms';
import { fadeAnimation, MarkdownPipe, ModalDialogComponent, SafeHtmlPipe, slideAnimation, TourState, TranslatePipe, UsersService } from '@app/shared';
@ -22,7 +22,6 @@ import { fadeAnimation, MarkdownPipe, ModalDialogComponent, SafeHtmlPipe, slideA
FormsModule,
MarkdownPipe,
ModalDialogComponent,
NgIf,
ReactiveFormsModule,
SafeHtmlPipe,
TranslatePipe,

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

@ -1,26 +1,27 @@
<div class="team-header">
<div class="row align-items-center">
<div class="col">
<h3>{{team.name}}</h3>
<h3>{{ team.name }}</h3>
</div>
<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 class="col-auto">
<button type="button" class="btn btn-sm btn-text-secondary" (click)="dropdown.toggle()" sqxStopClick #buttonOptions>
<span class="hidden">{{ 'common.options' | sqxTranslate }}</span>
<button class="btn btn-sm btn-text-secondary" #buttonOptions (click)="dropdown.toggle()" sqxStopClick type="button">
<span class="hidden">{{ "common.options" | sqxTranslate }}</span>
<i class="icon-dots"></i>
</button>
<sqx-dropdown-menu *sqxModal="dropdown;closeAlways:true" [sqxAnchoredTo]="buttonOptions" scrollY="true">
<a class="dropdown-item dropdown-item-delete"
(sqxConfirmClick)="leave.emit(team)"
confirmTitle="i18n:teams.leaveConfirmTitle"
<sqx-dropdown-menu scrollY="true" [sqxAnchoredTo]="buttonOptions" *sqxModal="dropdown; closeAlways: true">
<a
class="dropdown-item dropdown-item-delete"
confirmRememberKey="leaveApp"
confirmText="i18n:teams.leaveConfirmText"
confirmRememberKey="leaveApp">
{{ 'teams.leave' | sqxTranslate }}
confirmTitle="i18n:teams.leaveConfirmTitle"
(sqxConfirmClick)="leave.emit(team)">
{{ "teams.leave" | sqxTranslate }}
</a>
</sqx-dropdown-menu>
</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()">
<sqx-modal-dialog (dialogClose)="emitClose()">
<ng-container title>
{{ 'common.renameTag' | sqxTranslate }}
{{ "common.renameTag" | sqxTranslate }}
</ng-container>
<ng-container content>
<sqx-form-error [error]="editForm.error | async"></sqx-form-error>
<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>
<input class="form-control" id="tagName" formControlName="tagName" autocomplete="off" sqxFocusOnInit>
<input class="form-control" id="tagName" autocomplete="off" formControlName="tagName" sqxFocusOnInit />
</div>
</ng-container>
<ng-container footer>
<button type="button" class="btn btn-text-secondary" (click)="emitClose()">
{{ 'common.cancel' | sqxTranslate }}
<button class="btn btn-text-secondary" (click)="emitClose()" type="button">
{{ "common.cancel" | sqxTranslate }}
</button>
<button type="submit" class="btn btn-success">
{{ 'common.rename' | sqxTranslate }}
<button class="btn btn-success" type="submit">
{{ "common.rename" | sqxTranslate }}
</button>
</ng-container>
</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-item">
<a class="nav-link" (click)="tagsReset.emit()" [class.active]="isEmpty()">
{{ 'common.tagsAll' | sqxTranslate }}
<a class="nav-link" [class.active]="isEmpty()" (click)="tagsReset.emit()">
{{ "common.tagsAll" | sqxTranslate }}
</a>
</div>
<div class="nav-item" *ngFor="let tag of tags; trackBy: trackByTag">
<a class="nav-link" (click)="toggle.emit(tag.name)" [class.active]="isSelected(tag)">
<div class="row g-0">
<div class="col">
<span class="truncate">{{tag.name}}</span>
@for (tag of tags; track tag.name) {
<div class="nav-item">
<a class="nav-link" [class.active]="isSelected(tag)" (click)="toggle.emit(tag.name)">
<div class="row g-0">
<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 class="col-auto">
<div class="badge badge-secondary rounded-pill">{{tag.count}}</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>
</a>
</div>
}
</div>
<sqx-asset-tag-dialog *sqxModal="tagRenameDialog"
(dialogClose)="tagRenameDialog.hide()" [tagName]="tagRenaming!.name">
</sqx-asset-tag-dialog>
<sqx-asset-tag-dialog
(dialogClose)="tagRenameDialog.hide()"
*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 */
import { NgFor, NgIf } from '@angular/common';
import { booleanAttribute, ChangeDetectionStrategy, Component, EventEmitter, Input, Output } from '@angular/core';
import { DialogModel, ModalDirective, StopClickDirective, TagItem, TagsSelected, TranslatePipe } from '@app/shared';
import { AssetTagDialogComponent } from './asset-tag-dialog.component';
@ -21,8 +21,6 @@ import { AssetTagDialogComponent } from './asset-tag-dialog.component';
imports: [
AssetTagDialogComponent,
ModalDirective,
NgFor,
NgIf,
StopClickDirective,
TranslatePipe,
],
@ -58,8 +56,4 @@ export class AssetTagsComponent {
this.tagRenaming = tag;
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">
<h3>{{ 'common.tags' | sqxTranslate }}</h3>
<sqx-layout layout="right" overflow="true" padding="true" titleText="i18n:common.filters" white="true" width="20">
<h3>{{ "common.tags" | sqxTranslate }}</h3>
<sqx-asset-tags
<sqx-asset-tags
[canRename]="(assetsState.canRenameTag | async)!"
[tags]="(assetsState.tags | async)!"
(tagsReset)="resetTags()"
[tagsSelected]="(assetsState.tagsSelected | async)!"
(toggle)="toggleTag($event)">
</sqx-asset-tags>
(toggle)="toggleTag($event)"></sqx-asset-tags>
<hr>
<hr />
<sqx-shared-queries
[types]="'common.assets' | sqxTranslate"
[queryUsed]="assetsState.query | async"
[queries]="assetsQueries"
(search)="search($event)">
</sqx-shared-queries>
</sqx-layout>
[queryUsed]="assetsState.query | async"
(search)="search($event)"
[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() {
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-layout layout="main" titleText="i18n:common.assets" titleIcon="assets">
<sqx-layout layout="main" titleIcon="assets" titleText="i18n:common.assets">
<ng-container menu>
<div class="row flex-nowrap flex-grow-1 gx-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">
<i class="icon-reset"></i> {{ 'common.refresh' | sqxTranslate }}
<button
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>
</div>
<div class="col" style="width: 300px;">
<div class="col" style="width: 300px">
<div class="row g-0 search">
<div class="col-6">
<sqx-tag-editor class="tags" placeholder="{{ 'assets.searchByTags' | sqxTranslate }}"
<sqx-tag-editor
class="tags"
[itemsSource]="assetsState.tagsNames | async"
[ngModel]="assetsState.selectedTagNames | async"
(ngModelChange)="selectTags($event)"
placeholder="{{ 'assets.searchByTags' | sqxTranslate }}"
styleScrollable="true"
undefinedWhenEmpty="false">
</sqx-tag-editor>
undefinedWhenEmpty="false"></sqx-tag-editor>
</div>
<div class="col-6">
<sqx-search-form formClass="form" placeholder="{{ 'assets.searchByName' | sqxTranslate }}" fieldExample="fileSize"
<sqx-search-form
enableShortcut="true"
fieldExample="fileSize"
formClass="form"
placeholder="{{ 'assets.searchByName' | sqxTranslate }}"
[queries]="listQueries"
[queriesTypes]="'common.assets' | sqxTranslate"
[query]="assetsState.query | async"
(queryChange)="search($event)">
</sqx-search-form>
(queryChange)="search($event)"></sqx-search-form>
</div>
</div>
</div>
<div class="col-auto">
<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>
</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>
</button>
</div>
</div>
<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>
</button>
</div>
@ -52,50 +75,53 @@
<ng-container>
<sqx-list-view [isLoading]="assetsState.isLoading | async">
<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>
<div *ngIf="assetsState.path | async; let path">
<sqx-assets-list
[assetsState]="assetsState"
(edit)="editStart($event)"
[isDisabled]="false"
[isListView]="listMode"
[showFolderIcon]="path.length === 0">
</sqx-assets-list>
</div>
@if (assetsState.path | async; as path) {
<div>
<sqx-assets-list
[assetsState]="assetsState"
(edit)="editStart($event)"
[isDisabled]="false"
[isListView]="listMode"
[showFolderIcon]="path.length === 0"></sqx-assets-list>
</div>
}
<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>
</sqx-list-view>
</ng-container>
<ng-template sidebarMenu>
<ng-template sidebarMenu>
<div class="panel-nav">
<a class="panel-link"
<a
class="panel-link"
queryParamsHandling="preserve"
replaceUrl="true"
routerLink="filters"
routerLinkActive="active"
queryParamsHandling="preserve"
sqxTourStep="filter"
title="i18n:common.filters"
titlePosition="left"
sqxTourStep="filter">
titlePosition="left">
<i class="icon-filter"></i>
</a>
</div>
</a>
</div>
</ng-template>
</sqx-layout>
<router-outlet></router-outlet>
<sqx-asset-folder-dialog *sqxModal="addAssetFolderDialog"
(dialogClose)="addAssetFolderDialog.hide()">
</sqx-asset-folder-dialog>
<sqx-asset-folder-dialog (dialogClose)="addAssetFolderDialog.hide()" *sqxModal="addAssetFolderDialog"></sqx-asset-folder-dialog>
<sqx-asset-dialog *sqxModal="editAsset;isDialog:true"
[asset]="editAsset!"
(assetUpdated)="replaceAsset($event)"
<sqx-asset-dialog
[asset]="editAsset!"
(assetReplaced)="replaceAsset($event)"
(dialogClose)="editDone()">
</sqx-asset-dialog>
(assetUpdated)="replaceAsset($event)"
(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.
*/
import { AsyncPipe, NgIf } from '@angular/common';
import { AsyncPipe } from '@angular/common';
import { Component, OnInit } from '@angular/core';
import { FormsModule } from '@angular/forms';
import { RouterLink, RouterLinkActive, RouterOutlet } from '@angular/router';
@ -29,7 +29,6 @@ import { AssetDialogComponent, AssetDto, AssetFolderDialogComponent, AssetPathCo
LayoutComponent,
ListViewComponent,
ModalDirective,
NgIf,
PagerComponent,
RouterLink,
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-layout layout="main" titleText="i18n:contents.calendar" hideSidebar="true">
<sqx-layout hideSidebar="true" layout="main" titleText="i18n:contents.calendar">
<ng-container menu>
{{title}}
{{ title }}
<select class="form-select ms-4" [ngModel]="view" (ngModelChange)="changeView($event)" [disabled]="isLoading">
<option ngValue="day">{{ 'common.daily' | sqxTranslate }}</option>
<option ngValue="week">{{ 'common.weekly' | sqxTranslate }}</option>
<option ngValue="month">{{ 'common.monthly' | sqxTranslate }}</option>
<select class="form-select ms-4" [disabled]="isLoading" [ngModel]="view" (ngModelChange)="changeView($event)">
<option ngValue="day">{{ "common.daily" | sqxTranslate }}</option>
<option ngValue="week">{{ "common.weekly" | sqxTranslate }}</option>
<option ngValue="month">{{ "common.monthly" | sqxTranslate }}</option>
</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>
</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>
</button>
</ng-container>
@ -23,106 +23,93 @@
</ng-container>
</sqx-layout>
<sqx-modal-dialog *sqxModal="contentDialog" (dialogClose)="contentDialog.hide()">
<sqx-modal-dialog (dialogClose)="contentDialog.hide()" *sqxModal="contentDialog">
<ng-container title>
{{ 'common.content' | sqxTranslate }}
{{ "common.content" | sqxTranslate }}
</ng-container>
<ng-container content>
<div *ngIf="contentSelected && contentSelected.scheduleJob">
<div class="form-group row">
<label class="col-4 col-form-label">{{ 'common.id' | sqxTranslate }}</label>
<div class="col-8">
<div class="input-group">
<input readonly class="form-control" name="id" id="id" value="{{contentSelected.id}}" #inputId>
<button type="button" class="btn btn-outline-secondary" [sqxCopy]="inputId">
<i class="icon-copy"></i>
</button>
@if (contentSelected && contentSelected.scheduleJob) {
<div>
<div class="form-group row">
<label class="col-4 col-form-label">{{ "common.id" | sqxTranslate }}</label>
<div class="col-8">
<div class="input-group">
<input class="form-control" id="id" #inputId name="id" readonly value="{{ contentSelected.id }}" />
<button class="btn btn-outline-secondary" [sqxCopy]="inputId" type="button">
<i class="icon-copy"></i>
</button>
</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="col-8">
<a class="truncate" [routerLink]="['../', contentSelected.schemaName, contentSelected.id]">
{{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 class="form-group form-group-aligned row">
<label class="col-4 col-form-label">{{ "common.content" | sqxTranslate }}</label>
<div class="col-8">
<a class="truncate" [routerLink]="['../', contentSelected.schemaName, contentSelected.id]">
{{ createContentName(contentSelected) }}
</a>
</div>
</div>
</div>
<div class="form-group form-group-aligned row">
<label class="col-4 col-form-label">{{ 'common.status' | sqxTranslate }}</label>
<div class="col-8">
<sqx-content-status
layout="text"
[status]="contentSelected.status"
[statusColor]="contentSelected.statusColor"
small="true">
</sqx-content-status>
<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>
<hr />
<div class="form-group form-group-aligned row">
<label class="col-4 col-form-label">{{ 'contents.scheduledToLabel' | sqxTranslate }}</label>
<div class="col-8">
<sqx-content-status
layout="text"
[status]="contentSelected.scheduleJob.status"
[statusColor]="contentSelected.scheduleJob.color"
small="true">
</sqx-content-status>
<div class="form-group form-group-aligned row">
<label class="col-4 col-form-label">{{ "common.status" | sqxTranslate }}</label>
<div class="col-8">
<sqx-content-status
layout="text"
small="true"
[status]="contentSelected.status"
[statusColor]="contentSelected.statusColor"></sqx-content-status>
</div>
</div>
</div>
<div class="form-group form-group-aligned row">
<label class="col-4 col-form-label">{{ 'contents.scheduledAt' | sqxTranslate }}</label>
<div class="col-8">
{{contentSelected.scheduleJob.dueTime | sqxFullDateTime}}
<hr />
<div class="form-group form-group-aligned row">
<label class="col-4 col-form-label">{{ "contents.scheduledToLabel" | sqxTranslate }}</label>
<div class="col-8">
<sqx-content-status
layout="text"
small="true"
[status]="contentSelected.scheduleJob.status"
[statusColor]="contentSelected.scheduleJob.color"></sqx-content-status>
</div>
</div>
</div>
<div class="form-group form-group-aligned row">
<label class="col-4 col-form-label">{{ 'contents.scheduledBy' | sqxTranslate }}</label>
<div class="col-8">
<img class="user-picture" [src]="contentSelected.scheduleJob.scheduledBy | sqxUserPictureRef"> {{contentSelected.scheduleJob.scheduledBy | sqxUserNameRef}}
<div class="form-group form-group-aligned row">
<label class="col-4 col-form-label">{{ "contents.scheduledAt" | sqxTranslate }}</label>
<div class="col-8">
{{ contentSelected.scheduleJob.dueTime | sqxFullDateTime }}
</div>
</div>
</div>
<ng-container *ngIf="contentSelected.canCancelStatus">
<hr />
<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 class="form-group form-group-aligned row">
<label class="col-4 col-form-label">{{ "contents.scheduledBy" | sqxTranslate }}</label>
<div class="col-8">
<img class="user-picture" [src]="contentSelected.scheduleJob.scheduledBy | sqxUserPictureRef" />
{{ contentSelected.scheduleJob.scheduledBy | sqxUserNameRef }}
</div>
</div>
</ng-container>
</div>
@if (contentSelected.canCancelStatus) {
<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>
</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.
*/
import { NgIf } from '@angular/common';
import { AfterViewInit, ChangeDetectorRef, Component, ElementRef, HostListener, OnDestroy, OnInit, ViewChild } from '@angular/core';
import { FormsModule } from '@angular/forms';
import { RouterLink } from '@angular/router';
@ -29,7 +29,6 @@ type ViewMode = 'day' | 'week' | 'month';
LayoutComponent,
ModalDialogComponent,
ModalDirective,
NgIf,
RouterLink,
TitleComponent,
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-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="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 class="col ps-2 event-right">
<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>
</div>
<div class="event-created">{{event.created | sqxFromNow}}</div>
<div class="event-created">{{ event.created | sqxFromNow }}</div>
<ng-container *ngIf="canLoadOrCompare">
<a class="event-load force" (click)="dataLoad.emit()">{{ 'contents.loadContent' | sqxTranslate }}</a>
&middot;
<a class="event-load force" (click)="dataCompare.emit()">{{ 'contents.versionCompare' | sqxTranslate }}</a>
</ng-container>
@if (canLoadOrCompare) {
<a class="event-load force" (click)="dataLoad.emit()">{{ "contents.loadContent" | sqxTranslate }}</a>
&middot;
<a class="event-load force" (click)="dataCompare.emit()">{{ "contents.versionCompare" | sqxTranslate }}</a>
}
</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.
*/
import { NgIf } from '@angular/common';
import { ChangeDetectionStrategy, Component, EventEmitter, Input, Output } from '@angular/core';
import { ContentDto, FromNowPipe, HistoryEventDto, HistoryMessagePipe, TooltipDirective, TranslatePipe, TypedSimpleChanges, UserNameRefPipe, UserPictureRefPipe } from '@app/shared';
@ -18,7 +18,6 @@ import { ContentDto, FromNowPipe, HistoryEventDto, HistoryMessagePipe, TooltipDi
imports: [
FromNowPipe,
HistoryMessagePipe,
NgIf,
TooltipDirective,
TranslatePipe,
UserNameRefPipe,
@ -44,7 +43,7 @@ export class ContentEventComponent {
if (changes.event) {
this.canLoadOrCompare =
(this.event.eventType === 'ContentUpdatedEvent' ||
this.event.eventType === 'ContentCreatedEventV2') &&
this.event.eventType === 'ContentCreatedEventV2') &&
!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>
<div class="section mb-2">
<label for="id">{{ 'common.id' | sqxTranslate }}</label>
<label for="id">{{ "common.id" | sqxTranslate }}</label>
<div class="input-group">
<input readonly class="form-control" name="id" id="id" value="{{content.id}}" #inputId>
<button type="button" class="btn btn-outline-secondary" [sqxCopy]="inputId">
<input class="form-control" id="id" #inputId name="id" readonly value="{{ content.id }}" />
<button class="btn btn-outline-secondary" [sqxCopy]="inputId" type="button">
<i class="icon-copy"></i>
</button>
</div>
</div>
<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 class="section mb-4" *ngIf="content.canDraftCreate || content.canDraftDelete">
<ng-container *ngIf="!content.newStatus; else newVersion">
<button class="btn btn-success btn-block" (click)="createDraft()">
{{ 'contents.draftNew' | sqxTranslate }}
</button>
</ng-container>
<ng-template #newVersion>
<label>{{ 'contents.draftStatus' | sqxTranslate }}</label>
<button type="button" class="btn btn-outline-secondary btn-block btn-status" (click)="dropdownNew.toggle()" #buttonOptions sqxTourStep="status">
<sqx-content-status
layout="multiline"
[status]="content.newStatus!"
[statusColor]="content.newStatusColor!"
[scheduled]="content.scheduleJob">
</sqx-content-status>
</button>
<sqx-dropdown-menu *sqxModal="dropdownNew;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 }} <i class="icon-circle icon-sm" [style.color]="info.color"></i> {{info.status}}
@if (content.canDraftCreate || content.canDraftDelete) {
<div class="section mb-4">
@if (!content.newStatus) {
<button class="btn btn-success btn-block" (click)="createDraft()">
{{ "contents.draftNew" | sqxTranslate }}
</button>
} @else {
<label>{{ "contents.draftStatus" | sqxTranslate }}</label>
<button
class="btn btn-outline-secondary btn-block btn-status"
#buttonOptions
(click)="dropdownNew.toggle()"
sqxTourStep="status"
type="button">
<sqx-content-status
layout="multiline"
[scheduled]="content.scheduleJob"
[status]="content.newStatus!"
[statusColor]="content.newStatusColor!"></sqx-content-status>
</button>
<sqx-dropdown-menu scrollY="true" [sqxAnchoredTo]="buttonOptions" *sqxModal="dropdownNew; 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 }}
<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>
<div class="dropdown-divider"></div>
</ng-container>
<a class="dropdown-item dropdown-item-delete" [class.disabled]="!content.canDraftDelete"
(sqxConfirmClick)="deleteDraft()"
confirmTitle="i18n:contents.deleteConfirmTitle"
confirmText="i18n:contents.deleteVersionConfirmText"
confirmRememberKey="deleteDraft">
{{ 'contents.versionDelete' | 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>
</ng-template>
</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>
}
<div class="section">
<label>{{ 'contents.currentStatusLabel' | sqxTranslate }}</label>
<div *ngIf="!content.newStatus; else newStatusOld">
<button type="button" class="btn btn-outline-secondary btn-block btn-status" (click)="dropdown.toggle()" #buttonOptions sqxTourStep="status">
<label>{{ "contents.currentStatusLabel" | sqxTranslate }}</label>
@if (!content.newStatus) {
<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
layout="multiline"
[status]="content.status"
[statusColor]="content.statusColor"
[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>
[statusColor]="content.statusColor"></sqx-content-status>
</button>
</ng-template>
}
<sqx-form-hint marginTop="1">
{{ 'contents.lastUpdatedLabel' | sqxTranslate }}: {{content.lastModified | sqxFromNow}}
{{ "contents.lastUpdatedLabel" | sqxTranslate }}: {{ content.lastModified | sqxFromNow }}
</sqx-form-hint>
</div>
<div class="section">
<h3 class="bordered">{{ 'common.history' | sqxTranslate }}</h3>
<sqx-content-event *ngFor="let event of contentEvents | async; trackBy: trackByEvent"
[content]="content"
[event]="event"
(dataLoad)="loadVersion(event)"
(dataCompare)="compareVersion(event)">
</sqx-content-event>
<h3 class="bordered">{{ "common.history" | sqxTranslate }}</h3>
@for (event of contentEvents | async; track event.eventId) {
<sqx-content-event
[content]="content"
(dataCompare)="compareVersion(event)"
(dataLoad)="loadVersion(event)"
[event]="event"></sqx-content-event>
}
</div>
</ng-container>
</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.
*/
import { AsyncPipe, NgFor, NgIf } from '@angular/common';
import { AsyncPipe } from '@angular/common';
import { Component, OnInit, ViewChild } from '@angular/core';
import { Observable, timer } from 'rxjs';
import { map } from 'rxjs/operators';
@ -32,8 +32,6 @@ import { ContentPageComponent } from './content-page.component';
LayoutComponent,
ModalDirective,
ModalPlacementDirective,
NgFor,
NgIf,
TourStepDirective,
TranslatePipe,
],
@ -112,8 +110,4 @@ export class ContentHistoryPageComponent implements OnInit {
public compareVersion(event: HistoryEventDto) {
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>
<form [formGroup]="contentForm.form" (ngSubmit)="saveAndPublish()">
<sqx-layout layout="main" [hideSidebar]="!content">
<sqx-layout [hideSidebar]="!content" layout="main">
<ng-container title>
<div class="d-flex align-items-center">
<a class="btn btn-text-secondary" aria-labelledby="content-back" (click)="back()" *ngIf="schema.type !== 'Singleton'">
<span id="content-back" hidden>{{ 'common.back' | sqxTranslate }}</span>
<i class="icon-angle-left"></i>
</a>
<ng-container *ngIf="content; else noContentTitle">
@if (schema.type !== "Singleton") {
<a class="btn btn-text-secondary" aria-labelledby="content-back" (click)="back()">
<span id="content-back" hidden>{{ "common.back" | sqxTranslate }}</span>
<i class="icon-angle-left"></i>
</a>
}
@if (content) {
<sqx-title message="i18n:contents.editPageTitle"></sqx-title>
</ng-container>
<ng-template #noContentTitle>
<h3>{{ 'contents.createTitle' | sqxTranslate }}</h3>
} @else {
<h3>{{ "contents.createTitle" | sqxTranslate }}</h3>
<sqx-title message="i18n:contents.createPageTitle"></sqx-title>
</ng-template>
}
<ul class="nav nav-tabs2" *ngIf="content && contentTab | async; let tab">
<li class="nav-item">
<a class="nav-link" [routerLink]="[]" [queryParams]="{ tab: 'editor' }" [class.active]="tab === 'editor'">
{{ 'contents.contentTab.editor' | sqxTranslate }}
</a>
</li>
<li>
<a class="nav-link" [routerLink]="[]" [queryParams]="{ tab: 'references' }" [class.active]="tab === 'references'">
{{ 'contents.contentTab.references' | sqxTranslate }}
</a>
</li>
<li>
<a class="nav-link" [routerLink]="[]" [queryParams]="{ tab: 'referencing' }" [class.active]="tab === 'referencing'">
{{ 'contents.contentTab.referencing' | sqxTranslate }}
</a>
</li>
<li>
<a class="nav-link" [routerLink]="[]" [queryParams]="{ tab: 'inspect' }" [class.active]="tab === 'inspect'">
{{ 'contents.contentTab.inspect' | sqxTranslate }}
</a>
</li>
<li *ngIf="schema.properties.contentEditorUrl">
<a class="nav-link" [routerLink]="[]" [queryParams]="{ tab: 'extension' }" [class.active]="tab === 'extension'">
{{ 'common.extension' | sqxTranslate }}
</a>
</li>
</ul>
@if (content && contentTab | async; as tab) {
<ul class="nav nav-tabs2">
<li class="nav-item">
<a class="nav-link" [class.active]="tab === 'editor'" [queryParams]="{ tab: 'editor' }" [routerLink]="[]">
{{ "contents.contentTab.editor" | sqxTranslate }}
</a>
</li>
<li>
<a
class="nav-link"
[class.active]="tab === 'references'"
[queryParams]="{ tab: 'references' }"
[routerLink]="[]">
{{ "contents.contentTab.references" | sqxTranslate }}
</a>
</li>
<li>
<a
class="nav-link"
[class.active]="tab === 'referencing'"
[queryParams]="{ tab: 'referencing' }"
[routerLink]="[]">
{{ "contents.contentTab.referencing" | sqxTranslate }}
</a>
</li>
<li>
<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>
</ng-container>
<ng-container menu>
<div class="menu">
<ng-container *ngIf="content; else noContentMenu">
@if (content) {
<sqx-watching-users></sqx-watching-users>
<sqx-notifo topic="apps/{{contentsState.appId}}/schemas/{{schema.id}}/contents/{{content.id}}"></sqx-notifo>
<sqx-language-selector class="languages-buttons"
<sqx-notifo topic="apps/{{ contentsState.appId }}/schemas/{{ schema.id }}/contents/{{ content.id }}"></sqx-notifo>
<sqx-language-selector
class="languages-buttons"
[language]="language"
(languageChange)="changeLanguage($event)"
[language]="language"
[languages]="languages"
[percents]="contentForm.translationStatus | async">
</sqx-language-selector>
<ng-container *ngIf="content?.canDelete">
<button type="button" class="btn btn-outline-secondary ms-2" (click)="dropdown.toggle()" #buttonOptions>
<span class="hidden">{{ 'common.options' | sqxTranslate }}</span>
[percents]="contentForm.translationStatus | async"></sqx-language-selector>
@if (content.canDelete) {
<button class="btn btn-outline-secondary ms-2" #buttonOptions (click)="dropdown.toggle()" type="button">
<span class="hidden">{{ "common.options" | sqxTranslate }}</span>
<i class="icon-dots"></i>
</button>
<sqx-dropdown-menu *sqxModal="dropdown;closeAlways:true" [sqxAnchoredTo]="buttonOptions" scrollY="true">
<a class="dropdown-item dropdown-item-delete"
(sqxConfirmClick)="delete()"
confirmTitle="i18n:contents.deleteConfirmTitle"
<sqx-dropdown-menu scrollY="true" [sqxAnchoredTo]="buttonOptions" *sqxModal="dropdown; closeAlways: true">
<a
class="dropdown-item dropdown-item-delete"
confirmRememberKey="deleteContent"
confirmText="i18n:contents.deleteConfirmText"
confirmRememberKey="deleteContent">
{{ 'common.delete' | sqxTranslate }}
confirmTitle="i18n:contents.deleteConfirmTitle"
(sqxConfirmClick)="delete()">
{{ "common.delete" | sqxTranslate }}
</a>
</sqx-dropdown-menu>
</ng-container>
<ng-container *ngIf="contentTab | async; let tab">
}
@if (contentTab | async; as tab) {
<sqx-toolbar></sqx-toolbar>
<ng-container *ngIf="tab === 'editor'">
<sqx-preview-button [schema]="schema" [content]="content" [confirm]="confirmPreview"></sqx-preview-button>
<ng-container *ngIf="content?.canUpdate">
<button type="submit" class="btn btn-primary ms-2" shortcut="CTRL + SHIFT + S" sqxTourStep="saveContent">
{{ 'common.save' | sqxTranslate }}
@if (tab === "editor") {
<sqx-preview-button [confirm]="confirmPreview" [content]="content" [schema]="schema"></sqx-preview-button>
@if (content.canUpdate) {
<button class="btn btn-primary ms-2" shortcut="CTRL + SHIFT + S" sqxTourStep="saveContent" type="submit">
{{ "common.save" | sqxTranslate }}
</button>
</ng-container>
</ng-container>
</ng-container>
</ng-container>
<ng-template #noContentMenu>
<button type="button" class="btn btn-more btn-outline-secondary btn-sm me-2" (click)="changeShowIdInput(!showIdInput)">
}
}
}
} @else {
<button class="btn btn-more btn-outline-secondary btn-sm me-2" (click)="changeShowIdInput(!showIdInput)" type="button">
<span [class.hidden]="showIdInput">+</span>
<span [class.hidden]="!showIdInput">-</span>
</button>
<sqx-language-selector class="languages-buttons"
<sqx-language-selector
class="languages-buttons"
[language]="language"
(languageChange)="changeLanguage($event)"
[language]="language"
[languages]="languages"
[percents]="contentForm.translationStatus | async">
</sqx-language-selector>
[percents]="contentForm.translationStatus | async"></sqx-language-selector>
<div sqxTourStep="saveContent">
<button type="button" class="btn btn-primary ms-2" (click)="save()" *ngIf="contentsState.canCreate | async">
{{ 'common.save' | sqxTranslate }}
</button>
<button type="submit" class="btn btn-success ms-2" shortcut="CTRL + SHIFT + S" *ngIf="contentsState.canCreateAndPublish | async">
{{ 'contents.saveAndPublish' | sqxTranslate }}
</button>
@if (contentsState.canCreate | async) {
<button class="btn btn-primary ms-2" (click)="save()" type="button">
{{ "common.save" | sqxTranslate }}
</button>
}
@if (contentsState.canCreateAndPublish | async) {
<button class="btn btn-success ms-2" shortcut="CTRL + SHIFT + S" type="submit">
{{ "contents.saveAndPublish" | sqxTranslate }}
</button>
}
</div>
</ng-template>
}
</div>
</ng-container>
<ng-container>
<ng-container *ngIf="content">
<ng-container [ngSwitch]="contentTab | async">
<ng-container *ngSwitchCase="'references'">
<sqx-content-references mode="references"
@if (content) {
@switch (contentTab | async) {
@case ("references") {
<sqx-content-references
[content]="content"
[language]="language"
[languages]="languages">
</sqx-content-references>
</ng-container>
<ng-container *ngSwitchCase="'referencing'">
<sqx-content-references mode="referencing"
[languages]="languages"
mode="references"></sqx-content-references>
}
@case ("referencing") {
<sqx-content-references
[content]="content"
[language]="language"
[languages]="languages">
</sqx-content-references>
</ng-container>
<ng-container *ngSwitchCase="'inspect'">
[languages]="languages"
mode="referencing"></sqx-content-references>
}
@case ("inspect") {
<sqx-content-inspection
[appName]="contentsState.appName"
[content]="content"
[language]="language"
[languages]="languages"
[appName]="contentsState.appName">
</sqx-content-inspection>
</ng-container>
<ng-container *ngSwitchCase="'extension'">
<sqx-content-extension mode="referencing" *ngIf="schema.properties.contentEditorUrl && content"
[editorUrl]="schema.properties.contentEditorUrl"
[contentItem]="content"
[contentSchema]="schema">
</sqx-content-extension>
</ng-container>
</ng-container>
</ng-container>
<ng-container *ngIf="!content || (contentTab | async) === 'editor'">
[languages]="languages"></sqx-content-inspection>
}
@case ("extension") {
@if (schema.properties.contentEditorUrl && content) {
<sqx-content-extension
[contentItem]="content"
[contentSchema]="schema"
[editorUrl]="schema.properties.contentEditorUrl"
mode="referencing"></sqx-content-extension>
}
}
}
}
@if (!content || (contentTab | async) === "editor") {
<sqx-content-editor
[(contentId)]="contentId"
[isNew]="!content"
[isDeleted]="content?.isDeleted"
[contentForm]="contentForm"
[contentFormCompare]="contentFormCompare"
[(contentId)]="contentId"
[contentVersion]="contentVersion"
[formContext]="formContext"
[isDeleted]="content?.isDeleted"
[isNew]="!content"
[language]="language"
(languageChange)="language = $event"
[languages]="languages"
(loadLatest)="loadLatest()"
[schema]="schema"
[showIdInput]="showIdInput">
</sqx-content-editor>
</ng-container>
[showIdInput]="showIdInput"></sqx-content-editor>
}
</ng-container>
<ng-template sidebarMenu>
<div class="panel-nav">
<a class="panel-link"
<a
class="panel-link"
#linkHistory
queryParamsHandling="preserve"
replaceUrl="true"
routerLink="history"
routerLinkActive="active"
queryParamsHandling="preserve"
title="i18n:common.workflow"
titlePosition="left"
sqxTourStep="history"
#linkHistory>
title="i18n:common.workflow"
titlePosition="left">
<i class="icon-time"></i>
</a>
<a class="panel-link"
<a
class="panel-link"
hintAfter="120000"
hintText="i18n:common.sidebarTour"
queryParamsHandling="preserve"
replaceUrl="true"
routerLink="comments"
routerLinkActive="active"
queryParamsHandling="preserve"
sqxTourStep="comments"
title="i18n:common.comments"
titlePosition="left"
hintText="i18n:common.sidebarTour"
hintAfter="120000"
sqxTourStep="comments">
titlePosition="left">
<i class="icon-comments"></i>
</a>
<a class="panel-link"
replaceUrl="true"
routerLink="sidebar"
routerLinkActive="active"
queryParamsHandling="preserve"
title="i18n:common.sidebar"
titlePosition="left"
sqxTourStep="plugin"
*ngIf="schema.properties.contentSidebarUrl">
<i class="icon-plugin"></i>
</a>
@if (schema.properties.contentSidebarUrl) {
<a
class="panel-link"
queryParamsHandling="preserve"
replaceUrl="true"
routerLink="sidebar"
routerLinkActive="active"
sqxTourStep="plugin"
title="i18n:common.sidebar"
titlePosition="left">
<i class="icon-plugin"></i>
</a>
}
</div>
</ng-template>
</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.
*/
import { AsyncPipe, Location, NgIf, NgSwitch, NgSwitchCase } from '@angular/common';
import { AsyncPipe, Location } from '@angular/common';
import { Component, OnInit } from '@angular/core';
import { FormsModule, ReactiveFormsModule } from '@angular/forms';
import { ActivatedRoute, Router, RouterLink, RouterLinkActive, RouterOutlet } from '@angular/router';
@ -43,9 +43,6 @@ import { ContentReferencesComponent } from './references/content-references.comp
LayoutComponent,
ModalDirective,
ModalPlacementDirective,
NgIf,
NgSwitch,
NgSwitchCase,
NotifoComponent,
PreviewButtonComponent,
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">
<ng-container topHeader>
<div class="alert alert-danger" *ngIf="!contentVersion && isDeleted">
{{ 'contents.deleted' | sqxTranslate }}
</div>
<div class="alert alert-danger" *ngIf="contentVersion">
<div class="float-end">
<a (click)="loadLatest.emit()">{{ 'contents.viewLatest' | sqxTranslate }}</a>
<ng-container topHeader>
@if (!contentVersion && isDeleted) {
<div class="alert alert-danger">
{{ "contents.deleted" | sqxTranslate }}
</div>
}
<div *ngIf="isDeleted"
[innerHTML]="'contents.versionViewingDeleted' | sqxTranslate: { version: contentVersion } | sqxMarkdownInline | sqxSafeHtml">
@if (contentVersion) {
<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 *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 *ngIf="isNew && showIdInput">
<input class="form-control" placeholder="{{ 'contents.idPlaceholder' | sqxTranslate }}"
[ngModel]="contentId"
(ngModelChange)="contentIdChange.emit($event)" />
</div>
}
</ng-container>
<ng-container>
<div class="cursors" sqxCursors>
<sqx-cursors></sqx-cursors>
<sqx-content-section *ngFor="let section of contentForm.sections; trackBy: trackBySection"
[form]="contentForm"
[formCompare]="contentFormCompare"
[formContext]="formContext"
[formLevel]="0"
[formSection]="section"
[language]="language"
(languageChange)="languageChange.emit($event)"
[languages]="languages"
[schema]="schema">
</sqx-content-section>
@for (section of contentForm.sections; track section.separator?.fieldId) {
<sqx-content-section
[form]="contentForm"
[formCompare]="contentFormCompare"
[formContext]="formContext"
[formLevel]="0"
[formSection]="section"
[language]="language"
(languageChange)="languageChange.emit($event)"
[languages]="languages"
[schema]="schema"></sqx-content-section>
}
</div>
</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.
*/
import { AsyncPipe, NgFor, NgIf } from '@angular/common';
import { AsyncPipe } from '@angular/common';
import { booleanAttribute, Component, EventEmitter, Input, Output } from '@angular/core';
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';
@Component({
@ -25,8 +25,6 @@ import { ContentSectionComponent } from '../../../shared/forms/content-section.c
FormsModule,
ListViewComponent,
MarkdownInlinePipe,
NgFor,
NgIf,
SafeHtmlPipe,
TranslatePipe,
],
@ -73,8 +71,4 @@ export class ContentEditorComponent {
@Input({ required: true })
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>
<div class="inner-menu">
<ul class="nav nav-tabs2" *ngIf="mode | async; let currentMode">
<li class="nav-item">
<a class="nav-link" [class.active]="currentMode === 'Content'" (click)="setMode('Content')">
{{ 'contents.inspectContent' | sqxTranslate }}
</a>
</li>
<li class="nav-item">
<a class="nav-link" [class.active]="currentMode === 'Data'" (click)="setMode('Data')">
{{ 'contents.inspectData' | sqxTranslate }}
</a>
</li>
<li class="nav-item">
<a class="nav-link" [class.active]="currentMode === 'FlatData'" (click)="setMode('FlatData')">
{{ 'contents.inspectFlatData' | sqxTranslate }}
</a>
</li>
</ul>
@if (mode | async; as currentMode) {
<ul class="nav nav-tabs2">
<li class="nav-item">
<a class="nav-link" [class.active]="currentMode === 'Content'" (click)="setMode('Content')">
{{ "contents.inspectContent" | sqxTranslate }}
</a>
</li>
<li class="nav-item">
<a class="nav-link" [class.active]="currentMode === 'Data'" (click)="setMode('Data')">
{{ "contents.inspectData" | sqxTranslate }}
</a>
</li>
<li class="nav-item">
<a class="nav-link" [class.active]="currentMode === 'FlatData'" (click)="setMode('FlatData')">
{{ "contents.inspectFlatData" | sqxTranslate }}
</a>
</li>
</ul>
}
</div>
<div class="inner-main">
<sqx-code-editor
borderless="true"
[ngModel]="actualData | async"
(ngModelChange)="setData($event)"
valueMode="Json">
</sqx-code-editor>
<sqx-code-editor borderless="true" [ngModel]="actualData | async" (ngModelChange)="setData($event)" valueMode="Json"></sqx-code-editor>
</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.
*/
import { AsyncPipe, NgIf } from '@angular/common';
import { AsyncPipe } from '@angular/common';
import { ChangeDetectorRef, Component, Input, OnDestroy } from '@angular/core';
import { FormsModule } from '@angular/forms';
import { BehaviorSubject, combineLatest, of } from 'rxjs';
@ -24,7 +24,6 @@ type Mode = 'Content' | 'Data' | 'FlatData';
CodeEditorComponent,
FormErrorComponent,
FormsModule,
NgIf,
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">
<ng-container>
<table class="table table-items table-fixed" *ngIf="contentsState.contents | async; let contents">
<tbody *ngFor="let content of contents; trackBy: trackByContent"
[sqxReferenceItem]="content"
[canRemove]="false"
[columns]="contents | sqxContentsColumns"
[isCompact]="false"
[isDisabled]="false"
[language]="language"
[languages]="languages"
[validations]="(contentsState.validationResults | async)!"
[validityVisible]="true">
</tbody>
<tbody *ngIf="(contentsState.isLoaded | async) && contents.length === 0">
<tr>
<td class="table-items-row-empty" *ngIf="mode === 'references'">
{{ 'contents.noReferences' | sqxTranslate }}
</td>
<td class="table-items-row-empty" *ngIf="mode === 'referencing'">
{{ 'contents.noReferencing' | sqxTranslate }}
</td>
</tr>
</tbody>
</table>
@if (contentsState.contents | async; as contents) {
<table class="table table-items table-fixed">
@for (content of contents; track content.id) {
<tbody
[canRemove]="false"
[columns]="contents | sqxContentsColumns"
[isCompact]="false"
[isDisabled]="false"
[language]="language"
[languages]="languages"
[sqxReferenceItem]="content"
[validations]="(contentsState.validationResults | async)!"
[validityVisible]="true"></tbody>
}
@if ((contentsState.isLoaded | async) && contents.length === 0) {
<tbody>
<tr>
@if (mode === "references") {
<td class="table-items-row-empty">
{{ "contents.noReferences" | sqxTranslate }}
</td>
}
@if (mode === "referencing") {
<td class="table-items-row-empty">
{{ "contents.noReferencing" | sqxTranslate }}
</td>
}
</tr>
</tbody>
}
</table>
}
</ng-container>
<ng-container footer>
<sqx-pager [paging]="contentsState.paging | async" (pagingChange)="contentsState.page($event)"></sqx-pager>
</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.
*/
import { AsyncPipe, NgFor, NgIf } from '@angular/common';
import { AsyncPipe } from '@angular/common';
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 { ReferenceItemComponent } from '../../../shared/references/reference-item.component';
@ -24,8 +24,6 @@ import { ReferenceItemComponent } from '../../../shared/references/reference-ite
AsyncPipe,
ContentsColumnsPipe,
ListViewComponent,
NgFor,
NgIf,
PagerComponent,
ReferenceItemComponent,
TranslatePipe,
@ -97,8 +95,4 @@ export class ContentReferencesComponent implements OnInit, OnDestroy {
private publishAll() {
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">
<ng-container *ngIf="schema | async; let schema">
<sqx-content-extension
[editorUrl]="schema.properties.contentsListUrl"
<sqx-layout
hideHeader="true"
hideSidebar="true"
layout="main"
overflow="true"
titleIcon="contents"
titleText="i18n:common.contents"
white="true">
@if (schema | async; as schema) {
<sqx-content-extension
[contentItem]="undefined"
[contentSchema]="schema"
scrollable="true">
</sqx-content-extension>
</ng-container>
</sqx-layout>
[editorUrl]="schema.properties.contentsListUrl"
scrollable="true"></sqx-content-extension>
}
</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.
*/
import { AsyncPipe, NgIf } from '@angular/common';
import { AsyncPipe } from '@angular/common';
import { ChangeDetectionStrategy, Component } from '@angular/core';
import { LayoutComponent, SchemasState } from '@app/shared';
import { ContentExtensionComponent } from '../../shared/content-extension.component';
@ -20,7 +20,6 @@ import { ContentExtensionComponent } from '../../shared/content-extension.compon
AsyncPipe,
ContentExtensionComponent,
LayoutComponent,
NgIf,
],
})
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">
<ng-container *ngIf="schemaQueries | async; let queries">
<sqx-layout layout="right" overflow="true" padding="true" titleText="i18n:common.filters" white="true" width="20">
@if (schemaQueries | async; as queries) {
<sqx-query-list
[types]="'common.contents' | sqxTranslate"
[queryUsed]="contentsState.query | async"
[queries]="queries.defaultQueries"
(search)="search($event)">
</sqx-query-list>
<hr>
[queryUsed]="contentsState.query | async"
(search)="search($event)"
[types]="'common.contents' | sqxTranslate"></sqx-query-list>
<hr />
<div class="sidebar-section">
<h3>{{ 'contents.statusQueries' | sqxTranslate }}</h3>
<h3>{{ "contents.statusQueries" | sqxTranslate }}</h3>
<sqx-query-list
[types]="'common.contents' | sqxTranslate"
[queryUsed]="contentsState.query | async"
[queries]="contentsState.statusQueries | async"
(search)="search($event)">
</sqx-query-list>
[queryUsed]="contentsState.query | async"
(search)="search($event)"
[types]="'common.contents' | sqxTranslate"></sqx-query-list>
</div>
<hr>
<hr />
<sqx-shared-queries
[types]="'common.contents' | sqxTranslate"
[queryUsed]="contentsState.query | async"
[queries]="queries"
(search)="search($event)">
</sqx-shared-queries>
</ng-container>
</sqx-layout>
[queryUsed]="contentsState.query | async"
(search)="search($event)"
[types]="'common.contents' | sqxTranslate"></sqx-shared-queries>
}
</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.
*/
import { AsyncPipe, NgIf } from '@angular/common';
import { AsyncPipe } from '@angular/common';
import { Component } from '@angular/core';
import { map } from 'rxjs/operators';
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: [
AsyncPipe,
LayoutComponent,
NgIf,
QueryListComponent,
SavedQueriesComponent,
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-layout layout="main" titleText="i18n:common.contents" titleIcon="contents">
<sqx-layout layout="main" titleIcon="contents" titleText="i18n:common.contents">
<ng-container menu>
<div class="row flex-nowrap flex-grow-1 gx-2">
<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">
<i class="icon-reset"></i> {{ 'common.refresh' | sqxTranslate }}
<button
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>
</div>
<div class="col">
<sqx-search-form formClass="form" placeholder="{{ 'contents.searchPlaceholder' | sqxTranslate }}"
<sqx-search-form
enableShortcut="true"
formClass="form"
[language]="(languagesState.isoMasterLanguage | async)!"
[languages]="languages"
placeholder="{{ 'contents.searchPlaceholder' | sqxTranslate }}"
[queries]="queries | async"
[queriesTypes]="'common.contents' | sqxTranslate"
(queryChange)="search($event)"
[query]="contentsState.query | async"
(queryChange)="search($event)"
[queryModel]="queryModel | async"
[statuses]="contentsState.statuses | async">
</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>
[statuses]="contentsState.statuses | async"></sqx-search-form>
</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">
<button type="button" class="btn btn-success" routerLink="new" title="i18n:contents.createContentTooltip" shortcut="CTRL + U" [disabled]="(contentsState.canCreateAny | async) === false" sqxTourStep="addContent">
<i class="icon-plus"></i> {{ 'contents.create' | sqxTranslate }}
<button
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>
</div>
</div>
</ng-container>
<ng-container>
<ng-container *ngIf="tableSettings | async; let tableSettings">
<ng-container *ngIf="tableSettings.listFields | async; let tableFields">
@if (tableSettings | async; as tableSettings) {
@if (tableSettings.listFields | async; as tableFields) {
<sqx-list-view [isLoading]="contentsState.isLoading | async" syncedHeader="true" tableNoPadding="true">
<ng-container topHeader>
<div class="selection" *ngIf="selectionCount > 0">
{{ '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)">
<sqx-content-status layout="text"
[status]="status"
[statusColor]="selectionStatuses[status]">
</sqx-content-status>
</button>
<button type="button" class="btn btn-danger" *ngIf="selectionCanDelete"
(sqxConfirmClick)="deleteSelected()"
confirmTitle="i18n:contents.deleteConfirmTitle"
confirmText="i18n:contents.deleteManyConfirmText"
confirmRememberKey="deleteContents">
{{ 'common.delete' | sqxTranslate }}
</button>
</div>
@if (selectionCount > 0) {
<div class="selection">
{{ "contents.selectionCount" | sqxTranslate: { count: selectionCount } }}&nbsp;&nbsp;
@for (status of selectionStatuses | sqxKeys; track status) {
<button
class="btn btn-outline-secondary btn-status me-2"
(click)="changeSelectedStatus(status)"
type="button">
<sqx-content-status
layout="text"
[status]="status"
[statusColor]="selectionStatuses[status]"></sqx-content-status>
</button>
}
@if (selectionCanDelete) {
<button
class="btn btn-danger"
confirmRememberKey="deleteContents"
confirmText="i18n:contents.deleteManyConfirmText"
confirmTitle="i18n:contents.deleteConfirmTitle"
(sqxConfirmClick)="deleteSelected()"
type="button">
{{ "common.delete" | sqxTranslate }}
</button>
}
</div>
}
<div class="settings-container">
<button type="button" class="btn btn-sm settings-button" (click)="tableViewModal.toggle()" #buttonSettings>
<span class="hidden">{{ 'common.settings' | sqxTranslate }}</span>
<button class="btn btn-sm settings-button" #buttonSettings (click)="tableViewModal.toggle()" type="button">
<span class="hidden">{{ "common.settings" | sqxTranslate }}</span>
<i class="icon-settings"></i>
</button>
<sqx-dropdown-menu *sqxModal="tableViewModal" [sqxAnchoredTo]="buttonSettings" scrollY="true" position="bottom-end">
<sqx-dropdown-menu
position="bottom-end"
scrollY="true"
[sqxAnchoredTo]="buttonSettings"
*sqxModal="tableViewModal">
<sqx-custom-view-editor
[allFields]="tableSettings.schemaFields"
[allFields]="tableSettings.schemaFields"
[listFields]="$any(tableFields)"
(listFieldsChange)="tableSettings.updateFields($event)"
(listFieldsReset)="tableSettings.reset()"
[listFields]="$any(tableFields)">
</sqx-custom-view-editor>
(listFieldsReset)="tableSettings.reset()"></sqx-custom-view-editor>
</sqx-dropdown-menu>
</div>
</ng-container>
<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>
<tr>
<th class="cell-select">
<div class="form-check">
<input class="form-check-input" type="checkbox" id="all_selected"
[ngModel]="selectedAll"
(ngModelChange)="selectAll($event)">
<input
class="form-check-input"
id="all_selected"
[ngModel]="selectedAll"
(ngModelChange)="selectAll($event)"
type="checkbox" />
<label class="form-check-label" for="all_selected"></label>
</div>
</th>
<th class="cell-actions cell-actions-left">
<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>
<span class="truncate">{{ "common.actions" | sqxTranslate }}</span>
</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>
</tr>
</thead>
</table>
</ng-container>
<ng-container>
<div class="table-container">
<table class="table table-center table-fixed" [sqxContentListWidth]="tableFields" [fields]="tableSettings" [sqxSyncWidth]="header">
<tbody *ngFor="let content of contentsState.contents | async; trackBy: trackByContent"
[sqxContent]="content"
(clone)="clone(content)"
[cloneable]="contentsState.snapshot.canCreate"
(delete)="delete(content)"
[language]="language"
[languages]="languages"
[link]="[content.id, 'history']"
[schema]="schema"
[selected]="isItemSelected(content)"
(selectedChange)="selectItem(content, $event)"
(statusChange)="changeStatus(content, $event)"
[tableFields]="tableFields"
[tableSettings]="tableSettings">
</tbody>
<table
class="table table-center table-fixed"
[fields]="tableSettings"
[sqxContentListWidth]="tableFields"
[sqxSyncWidth]="header">
@for (content of contentsState.contents | async; track content.id) {
<tbody
(clone)="clone(content)"
[cloneable]="contentsState.snapshot.canCreate"
(delete)="delete(content)"
[language]="language"
[languages]="languages"
[link]="[content.id, 'history']"
[schema]="schema"
[selected]="isItemSelected(content)"
(selectedChange)="selectItem(content, $event)"
[sqxContent]="content"
(statusChange)="changeStatus(content, $event)"
[tableFields]="tableFields"
[tableSettings]="tableSettings"></tbody>
}
</table>
</div>
</ng-container>
<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>
</sqx-list-view>
</ng-container>
</ng-container>
}
}
</ng-container>
<ng-template sidebarMenu>
<div class="panel-nav">
<a class="panel-link"
<a
class="panel-link"
queryParamsHandling="preserve"
replaceUrl="true"
routerLink="filters"
routerLinkActive="active"
queryParamsHandling="preserve"
sqxTourStep="filters"
title="i18n:common.filters"
titlePosition="left"
sqxTourStep="filters">
titlePosition="left">
<i class="icon-filter"></i>
</a>
<a class="panel-link"
replaceUrl="true"
routerLink="sidebar"
routerLinkActive="active"
queryParamsHandling="preserve"
title="i18n:common.sidebar"
titlePosition="left"
sqxTourStep="plugin"
*ngIf="schema.properties.contentsSidebarUrl">
<i class="icon-plugin"></i>
</a>
@if (schema.properties.contentsSidebarUrl) {
<a
class="panel-link"
queryParamsHandling="preserve"
replaceUrl="true"
routerLink="sidebar"
routerLinkActive="active"
sqxTourStep="plugin"
title="i18n:common.sidebar"
titlePosition="left">
<i class="icon-plugin"></i>
</a>
}
</div>
</ng-template>
</sqx-layout>
<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 */
import { AsyncPipe, NgFor, NgIf } from '@angular/common';
import { AsyncPipe } from '@angular/common';
import { Component, OnInit, ViewChild } from '@angular/core';
import { FormsModule } from '@angular/forms';
import { ActivatedRoute, Router, RouterLink, RouterLinkActive, RouterOutlet } from '@angular/router';
@ -45,8 +45,6 @@ import { CustomViewEditorComponent } from './custom-view-editor.component';
ListViewComponent,
ModalDirective,
ModalPlacementDirective,
NgFor,
NgIf,
NotifoComponent,
PagerComponent,
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) {
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="header">
<button type="button" class="btn btn-secondary btn-sm" (click)="resetDefault()">
{{ 'contents.viewReset' | sqxTranslate }}
<button class="btn btn-secondary btn-sm" (click)="resetDefault()" type="button">
{{ "contents.viewReset" | sqxTranslate }}
</button>
</div>
<hr>
<div
cdkDropList
[cdkDropListData]="listFields"
(cdkDropListDropped)="drop($event)">
<div *ngFor="let field of listFields" cdkDrag>
<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>
<hr />
<div cdkDropList [cdkDropListData]="listFields" (cdkDropListDropped)="drop($event)">
@for (field of listFields; track field) {
<div cdkDrag>
<i class="icon-drag2 drag-handle"></i>
<div class="form-check">
<input class="form-check-input" type="checkbox" (click)="addField(field)" id="field_{{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>
<input
class="form-check-input"
id="field_{{ field }}"
checked
(click)="removeField(field)"
[disabled]="!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>
@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>
</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 { NgFor, NgIf } from '@angular/common';
import { ChangeDetectionStrategy, Component, EventEmitter, Input, Output } from '@angular/core';
import { TableField, TranslatePipe } from '@app/shared';
@ -19,8 +19,6 @@ import { TableField, TranslatePipe } from '@app/shared';
imports: [
CdkDrag,
CdkDropList,
NgFor,
NgIf,
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-layout layout="main" titleText="i18n:common.references" titleIcon="contents">
<sqx-layout layout="main" titleIcon="contents" titleText="i18n:common.references">
<ng-container menu>
<div class="row flex-nowrap flex-grow-1 gx-2">
<div class="col-auto ms-8">
<button type="button" class="btn btn-text-secondary" (click)="reload()" title="i18n:contents.refreshTooltip" shortcut="CTRL + B">
<i class="icon-reset"></i> {{ 'common.refresh' | sqxTranslate }}
<button
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>
</div>
<div class="col-auto" *ngIf="languages.length > 1">
<sqx-language-selector class="languages-buttons"
(languageChange)="changeLanguage($event)"
[language]="language"
[languages]="languages">
</sqx-language-selector>
</div>
@if (languages.length > 1) {
<div class="col-auto">
<sqx-language-selector
class="languages-buttons"
[language]="language"
(languageChange)="changeLanguage($event)"
[languages]="languages"></sqx-language-selector>
</div>
}
</div>
</ng-container>
<ng-container>
<sqx-list-view [isLoading]="contentsState.isLoading | async" table="true">
<ng-container>
<table class="table table-items table-fixed" *ngIf="contentsState.contents | async; let contents">
<tbody *ngFor="let content of contents; trackBy: trackByContent"
[sqxReferenceItem]="content"
[canRemove]="false"
[columns]="contents | sqxContentsColumns"
[isCompact]="false"
[isDisabled]="false"
[language]="language"
[languages]="languages"
[validations]="(contentsState.validationResults | async)!"
[validityVisible]="true">
</tbody>
<tbody *ngIf="(contentsState.isLoaded | async) && contents.length === 0">
<tr>
<td class="table-items-row-empty">
{{ 'contents.noReferencing' | sqxTranslate }}
</td>
</tr>
</tbody>
</table>
@if (contentsState.contents | async; as contents) {
<table class="table table-items table-fixed">
@for (content of contents; track content.id) {
<tbody
[canRemove]="false"
[columns]="contents | sqxContentsColumns"
[isCompact]="false"
[isDisabled]="false"
[language]="language"
[languages]="languages"
[sqxReferenceItem]="content"
[validations]="(contentsState.validationResults | async)!"
[validityVisible]="true"></tbody>
}
@if ((contentsState.isLoaded | async) && contents.length === 0) {
<tbody>
<tr>
<td class="table-items-row-empty">
{{ "contents.noReferencing" | sqxTranslate }}
</td>
</tr>
</tbody>
}
</table>
}
</ng-container>
<ng-container footer>
<sqx-pager [paging]="contentsState.paging | async" (pagingChange)="contentsState.page($event)"></sqx-pager>
</ng-container>
</sqx-list-view>
</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.
*/
import { AsyncPipe, NgFor, NgIf } from '@angular/common';
import { AsyncPipe } from '@angular/common';
import { ChangeDetectionStrategy, Component, OnInit } from '@angular/core';
import { ActivatedRoute } from '@angular/router';
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';
@Component({
@ -28,8 +28,6 @@ import { ReferenceItemComponent } from '../../shared/references/reference-item.c
LanguageSelectorComponent,
LayoutComponent,
ListViewComponent,
NgFor,
NgIf,
PagerComponent,
ReferenceItemComponent,
ShortcutDirective,
@ -87,10 +85,6 @@ export class ReferencesPageComponent implements OnInit {
public changeLanguage(language: AppLanguageDto) {
this.language = language;
}
public trackByContent(_index: number, content: ContentDto) {
return content.id;
}
}
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-layout layout="left" titleCollapsed="i18n:common.schemas" width="18" white="true" padding="true" overflow="true" *ngIf="!isEmbedded">
<ng-container menu>
<div class="search-form">
<input class="form-control" [formControl]="schemasFilter" placeholder="{{ 'contents.searchSchemasPlaceholder' | sqxTranslate }}">
<i class="icon-search"></i>
</div>
</ng-container>
<ng-container>
<ul class="nav nav-light mb-2 flex-column">
<li class="nav-item">
<a class="nav-link" routerLink="__calendar" routerLinkActive="active">
{{ 'contents.calendar' | sqxTranslate }}
</a>
</li>
</ul>
@if (!isEmbedded) {
<sqx-layout layout="left" overflow="true" padding="true" titleCollapsed="i18n:common.schemas" white="true" width="18">
<ng-container menu>
<div class="search-form">
<input
class="form-control"
[formControl]="schemasFilter"
placeholder="{{ 'contents.searchSchemasPlaceholder' | sqxTranslate }}" />
<i class="icon-search"></i>
</div>
</ng-container>
<ng-container>
<sqx-schema-category *ngFor="let category of categories | async; trackBy: trackByCategory"
[schemaCategory]="category"
[schemaTarget]="'Contents'">
</sqx-schema-category>
<ul class="nav nav-light mb-2 flex-column">
<li class="nav-item">
<a class="nav-link" routerLink="__calendar" routerLinkActive="active">
{{ "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>
</sqx-layout>
</sqx-layout>
}
<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.
*/
import { AsyncPipe, NgFor, NgIf } from '@angular/common';
import { AsyncPipe } from '@angular/common';
import { Component, inject } from '@angular/core';
import { FormsModule, ReactiveFormsModule, UntypedFormControl } from '@angular/forms';
import { RouterLink, RouterLinkActive, RouterOutlet } from '@angular/router';
import { combineLatest } from 'rxjs';
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({
standalone: true,
@ -22,8 +22,6 @@ import { AppsState, getCategoryTree, LayoutComponent, SchemaCategory, SchemaCate
AsyncPipe,
FormsModule,
LayoutComponent,
NgFor,
NgIf,
ReactiveFormsModule,
RouterLink,
RouterLinkActive,
@ -32,7 +30,7 @@ import { AppsState, getCategoryTree, LayoutComponent, SchemaCategory, SchemaCate
TitleComponent,
TranslatePipe,
],
})
})
export class SchemasPageComponent {
public schemasFilter = new UntypedFormControl();
@ -63,8 +61,4 @@ export class SchemasPageComponent {
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
[editorUrl]="url | async"
[contentItem]="contentsState.selectedContent | async"
[contentSchema]="(schemasState.selectedSchema | async)!"
scrollable="true">
</sqx-content-extension>
[editorUrl]="url | async"
scrollable="true"></sqx-content-extension>
</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>
{{ 'contents.changeStatusTo' | sqxTranslate: { action: dueTimeAction } }}
{{ "contents.changeStatusTo" | sqxTranslate: { action: dueTimeAction } }}
</ng-container>
<ng-container content>
<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">
{{ 'contents.changeStatusToImmediately' | sqxTranslate: { action: dueTimeAction } }}
{{ "contents.changeStatusToImmediately" | sqxTranslate: { action: dueTimeAction } }}
</label>
</div>
<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">
{{ 'contents.changeStatusToLater' | sqxTranslate: { action: dueTimeAction } }}
{{ "contents.changeStatusToLater" | sqxTranslate: { action: dueTimeAction } }}
</label>
</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 footer>
<button type="button" class="btn btn-text-secondary" (click)="cancelStatusChange()">
{{ 'common.cancel' | sqxTranslate }}
<button class="btn btn-text-secondary" (click)="cancelStatusChange()" type="button">
{{ "common.cancel" | sqxTranslate }}
</button>
<button type="button" class="btn btn-primary" [disabled]="dueTimeMode === 'Scheduled' && !dueTime" (click)="confirmStatusChange()" sqxFocusOnInit>
{{ 'common.confirm' | sqxTranslate }}
<button
class="btn btn-primary"
(click)="confirmStatusChange()"
[disabled]="dueTimeMode === 'Scheduled' && !dueTime"
sqxFocusOnInit
type="button">
{{ "common.confirm" | sqxTranslate }}
</button>
</ng-container>
</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">
<div class="array-container static" [class.expanded]="isExpanded" *ngIf="items.length > 0 && items.length <= 20;"
cdkDropList
[cdkDropListSortingDisabled]="isDisabled | async"
[cdkDropListDisabled]="isDisabled | async"
[cdkDropListData]="items"
(cdkDropListDropped)="sort($event)">
<div *ngFor="let itemForm of items; index as i; last as isLast; first as isFirst;" class="table-drag item"
cdkDrag
cdkDragLockAxis="y"
[class.first]="isFirst"
[class.last]="isLast">
<sqx-array-item
(clone)="addCopy(itemForm)"
[form]="form"
[formContext]="formContext"
[formLevel]="formLevel + 1"
[formModel]="itemForm"
[index]="i"
[isComparing]="isComparing"
[isCollapsedInitial]="isCollapsedInitial"
[isDisabled]="isDisabled | async"
[isFirst]="isFirst"
[isLast]="isLast"
(itemRemove)="removeItem(i)"
(itemMove)="move(itemForm, $event)"
[hasChatBot]="hasChatBot"
[language]="language"
[languages]="languages">
<i cdkDragHandle class="icon-drag2" [class.disabled]="isDisabled | async"></i>
</sqx-array-item>
@if (formModel.itemChanges | async; as items) {
@if (items.length > 0 && items.length <= 20) {
<div
class="array-container static"
cdkDropList
[cdkDropListData]="items"
[cdkDropListDisabled]="isDisabled | async"
(cdkDropListDropped)="sort($event)"
[cdkDropListSortingDisabled]="isDisabled | async"
[class.expanded]="isExpanded">
@for (itemForm of items; track itemForm; let i = $index; let isLast = $last; let isFirst = $first) {
<div class="table-drag item" cdkDrag cdkDragLockAxis="y" [class.first]="isFirst" [class.last]="isLast">
<sqx-array-item
(clone)="addCopy(itemForm)"
[form]="form"
[formContext]="formContext"
[formLevel]="formLevel + 1"
[formModel]="itemForm"
[hasChatBot]="hasChatBot"
[index]="i"
[isCollapsedInitial]="isCollapsedInitial"
[isComparing]="isComparing"
[isDisabled]="isDisabled | async"
[isFirst]="isFirst"
[isLast]="isLast"
(itemMove)="move(itemForm, $event)"
(itemRemove)="removeItem(i)"
[language]="language"
[languages]="languages">
<i class="icon-drag2" cdkDragHandle [class.disabled]="isDisabled | async"></i>
</sqx-array-item>
</div>
}
</div>
</div>
<div class="array-container" [class.expanded]="isExpanded" *ngIf="items.length > 20">
<virtual-scroller #scroll [items]="$any(items)" [enableUnequalChildrenSizes]="true">
<div *ngFor="let itemForm of scroll.viewPortItems; index as i" class="item"
[class.first]="scroll.viewPortInfo.startIndexWithBuffer + i === 0"
[class.last]="scroll.viewPortInfo.startIndexWithBuffer + i === items.length - 1">
<sqx-array-item
(clone)="addCopy(itemForm)"
[form]="form"
[formContext]="formContext"
[formLevel]="formLevel + 1"
[formModel]="itemForm"
[index]="scroll.viewPortInfo.startIndexWithBuffer + i"
[isCollapsedInitial]="isCollapsedInitial"
[isComparing]="isComparing"
[isDisabled]="isDisabled | async"
[isFirst]="scroll.viewPortInfo.startIndexWithBuffer + i === 0"
[isLast]="scroll.viewPortInfo.startIndexWithBuffer + i === items.length - 1"
(itemExpanded)="scroll.invalidateCachedMeasurementAtIndex(scroll.viewPortInfo.startIndexWithBuffer + i)"
(itemRemove)="removeItem(scroll.viewPortInfo.startIndexWithBuffer + i)"
(itemMove)="move(itemForm, $event)"
[hasChatBot]="hasChatBot"
[language]="language"
[languages]="languages">
</sqx-array-item>
</div>
</virtual-scroller>
</div>
}
@if (items.length > 20) {
<div class="array-container" [class.expanded]="isExpanded">
<virtual-scroller #scroll [enableUnequalChildrenSizes]="true" [items]="$any(items)">
@for (itemForm of scroll.viewPortItems; track itemForm; let i = $index) {
<div
class="item"
[class.first]="scroll.viewPortInfo.startIndexWithBuffer + i === 0"
[class.last]="scroll.viewPortInfo.startIndexWithBuffer + i === items.length - 1">
<sqx-array-item
(clone)="addCopy(itemForm)"
[form]="form"
[formContext]="formContext"
[formLevel]="formLevel + 1"
[formModel]="itemForm"
[hasChatBot]="hasChatBot"
[index]="scroll.viewPortInfo.startIndexWithBuffer + i"
[isCollapsedInitial]="isCollapsedInitial"
[isComparing]="isComparing"
[isDisabled]="isDisabled | async"
[isFirst]="scroll.viewPortInfo.startIndexWithBuffer + i === 0"
[isLast]="scroll.viewPortInfo.startIndexWithBuffer + i === items.length - 1"
(itemExpanded)="scroll.invalidateCachedMeasurementAtIndex(scroll.viewPortInfo.startIndexWithBuffer + i)"
(itemMove)="move(itemForm, $event)"
(itemRemove)="removeItem(scroll.viewPortInfo.startIndexWithBuffer + i)"
[language]="language"
[languages]="languages"></sqx-array-item>
</div>
}
</virtual-scroller>
</div>
}
<div class="array-buttons row g-0 align-items-center" [class.expanded]="isExpanded">
<div class="col-auto">
<ng-container *ngIf="isArray; else component">
<ng-container *ngIf="hasField">
<button type="button" class="btn btn-outline-success" [disabled]="isDisabledOrFull | async" (click)="addItem()">
{{ 'contents.arrayAddItem' | sqxTranslate }}
@if (isArray) {
@if (hasField) {
<button class="btn btn-outline-success" (click)="addItem()" [disabled]="isDisabledOrFull | async" type="button">
{{ "contents.arrayAddItem" | sqxTranslate }}
</button>
</ng-container>
<ng-container *ngIf="!hasField">
}
@if (!hasField) {
<sqx-form-hint>
{{ 'contents.arrayNoFields' | sqxTranslate }}
{{ "contents.arrayNoFields" | sqxTranslate }}
</sqx-form-hint>
</ng-container>
</ng-container>
<ng-template #component>
<ng-container *ngIf="schemasList.length > 1">
<button type="button" class="btn btn-outline-success dropdown-toggle" [disabled]="isDisabledOrFull | async" (click)="schemasDropdown.show()" #buttonSelect>
{{ 'contents.addComponent' | sqxTranslate}}
}
} @else {
@if (schemasList.length > 1) {
<button
class="btn btn-outline-success dropdown-toggle"
#buttonSelect
(click)="schemasDropdown.show()"
[disabled]="isDisabledOrFull | async"
type="button">
{{ "contents.addComponent" | sqxTranslate }}
</button>
<sqx-dropdown-menu *sqxModal="schemasDropdown;closeAlways:true" [sqxAnchoredTo]="buttonSelect" scrollY="true">
<a class="dropdown-item" *ngFor="let schema of schemasList" (click)="addComponent(schema)">
{{schema.displayName}}
</a>
<sqx-dropdown-menu scrollY="true" [sqxAnchoredTo]="buttonSelect" *sqxModal="schemasDropdown; closeAlways: true">
@for (schema of schemasList; track schema) {
<a class="dropdown-item" (click)="addComponent(schema)">
{{ schema.displayName }}
</a>
}
</sqx-dropdown-menu>
</ng-container>
<ng-container *ngIf="schemasList.length === 1">
<button type="button" class="btn btn-outline-success" [disabled]="isDisabledOrFull | async" (click)="addComponent(schemasList[0])">
{{ 'contents.addComponent' | sqxTranslate}}
}
@if (schemasList.length === 1) {
<button
class="btn btn-outline-success"
(click)="addComponent(schemasList[0])"
[disabled]="isDisabledOrFull | async"
type="button">
{{ "contents.addComponent" | sqxTranslate }}
</button>
</ng-container>
<ng-container *ngIf="schemasList.length === 0">
}
@if (schemasList.length === 0) {
<sqx-form-hint>
{{ 'contents.componentsNoSchema' | sqxTranslate }}
{{ "contents.componentsNoSchema" | sqxTranslate }}
</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 class="col-auto" *ngIf="items.length > 0">
<button type="button" class="btn btn-text-secondary" (click)="expandAll()" title="i18n:contents.arrayExpandAll">
<i class="icon-plus-square"></i>
</button>
<button type="button" class="btn btn-text-secondary" (click)="collapseAll()" title="i18n:contents.arrayCollapseAll">
<i class="icon-minus-square"></i>
</button>
<div class="col">
@if (items.length > 0) {
<button
class="btn btn-text-danger ms-2"
confirmRememberKey="leaveApp"
confirmText="i18n:contents.arrayClearConfirmText"
confirmTitle="i18n:contents.arrayClearConfirmTitle"
[disabled]="isDisabled | async"
(sqxConfirmClick)="clear()"
type="button">
{{ "contents.arrayClear" | sqxTranslate }}
</button>
}
</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>
</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 { AsyncPipe, NgFor, NgIf } from '@angular/common';
import { AsyncPipe } from '@angular/common';
import { booleanAttribute, ChangeDetectionStrategy, Component, Input, numberAttribute, QueryList, ViewChildren } from '@angular/core';
import { VirtualScrollerComponent, VirtualScrollerModule } from '@iharbeck/ngx-virtual-scroller';
import { combineLatest, Observable } from 'rxjs';
@ -30,8 +30,6 @@ import { ArrayItemComponent } from './array-item.component';
FormHintComponent,
ModalDirective,
ModalPlacementDirective,
NgFor,
NgIf,
TooltipDirective,
TranslatePipe,
VirtualScrollerModule,

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

@ -6,59 +6,97 @@
</div>
<div class="col">
<div class="truncate">
<span class="header-index">#{{index + 1}}</span>
<span class="header-title">{{title | async }}</span>
<span class="header-index">#{{ index + 1 }}</span>
<span class="header-title">{{ title | async }}</span>
</div>
</div>
<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>
</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>
</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>
</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>
</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>
</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>
</button>
</div>
<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>
</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>
</button>
</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">
{{ 'contents.componentInvalid' | sqxTranslate }}
</sqx-form-hint>
<div class="card-body" [class.hidden]="isCollapsed | async" *sqxIfOnce="!(isCollapsed | async)">
@for (section of formModel.sectionsChanges | async; track section) {
<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>

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.
*/
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 { Observable, of } from 'rxjs';
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';
@Component({
@ -23,8 +23,6 @@ import { ComponentSectionComponent } from './component-section.component';
ComponentSectionComponent,
FormHintComponent,
IfOnceDirective,
NgFor,
NgIf,
TooltipDirective,
TranslatePipe,
],
@ -146,10 +144,6 @@ export class ArrayItemComponent {
section.reset();
});
}
public trackBySection(_index: number, section: FieldSection<FieldDto, any>) {
return section.separator?.fieldId;
}
}
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"
(sqxDropFile)="addFiles($event)"
[sqxDropDisabled]="snapshot.isDisabled"
[sqxDropDisabled]="snapshot.isDisabled"
(sqxDropFile)="addFiles($event)"
tabindex="1000">
<div class="header list">
<div class="row gx-2">
<div class="col" [class.disabled]="snapshot.isDisabled">
<div class="drop-area align-items-center" (click)="assetsDialog.show()" (sqxDropFile)="addFiles($event)" [sqxDropDisabled]="snapshot.isDisabled">
{{ 'contents.assetsUpload' | sqxTranslate }}
<div
class="drop-area align-items-center"
(click)="assetsDialog.show()"
[sqxDropDisabled]="snapshot.isDisabled"
(sqxDropFile)="addFiles($event)">
{{ "contents.assetsUpload" | sqxTranslate }}
</div>
</div>
<div class="col-auto" *ngIf="hasChatBot">
<button type="button" class="btn btn-outline-secondary force no-focus-shadow" (click)="chatDialog.show()" tabindex="-1">
AI
</button>
</div>
@if (hasChatBot) {
<div class="col-auto">
<button class="btn btn-outline-secondary force no-focus-shadow" (click)="chatDialog.show()" tabindex="-1" type="button">
AI
</button>
</div>
}
<div class="col-auto">
<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>
</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>
</button>
</div>
@ -28,75 +45,72 @@
</div>
</div>
<div class="body" (sqxResizeCondition)="setCompact($event)" sqxResizeMinWidth="600" sqxResizeMaxWidth="0">
<ng-container *ngIf="!snapshot.isListView; else listTemplate">
<div class="body" (sqxResizeCondition)="setCompact($event)" sqxResizeMaxWidth="0" sqxResizeMinWidth="600">
@if (!snapshot.isListView) {
<div class="row g-0">
<sqx-asset *ngFor="let file of snapshot.assetFiles"
[assetFile]="file"
[isCompact]="snapshot.isCompact"
[isDisabled]="snapshot.isDisabled"
[folderId]="folderId"
(loadDone)="addAsset(file, $event)"
(loadError)="removeLoadingAsset(file)">
</sqx-asset>
<sqx-asset *ngFor="let asset of snapshot.assetItems; trackBy: trackByAsset"
[asset]="asset"
(edit)="editStart($event)"
[isCompact]="snapshot.isCompact"
[isDisabled]="snapshot.isDisabled"
removeMode="true"
(remove)="removeLoadedAsset(asset)"
(update)="notifyOthers(asset)">
</sqx-asset>
@for (file of snapshot.assetFiles; track file) {
<sqx-asset
[assetFile]="file"
[folderId]="folderId"
[isCompact]="snapshot.isCompact"
[isDisabled]="snapshot.isDisabled"
(loadDone)="addAsset(file, $event)"
(loadError)="removeLoadingAsset(file)"></sqx-asset>
}
@for (asset of snapshot.assetItems; track asset.id) {
<sqx-asset
[asset]="asset"
(edit)="editStart($event)"
[isCompact]="snapshot.isCompact"
[isDisabled]="snapshot.isDisabled"
(remove)="removeLoadedAsset(asset)"
removeMode="true"
(update)="notifyOthers(asset)"></sqx-asset>
}
</div>
</ng-container>
<ng-template #listTemplate>
} @else {
<div class="list-view">
<sqx-asset *ngFor="let file of snapshot.assetFiles"
[assetFile]="file"
[folderId]="folderId"
[isCompact]="snapshot.isCompact"
[isDisabled]="snapshot.isDisabled"
isListView="true"
(loadDone)="addAsset(file, $event)"
(loadError)="removeLoadingAsset(file)">
</sqx-asset>
<div cdkDropList
[cdkDropListDisabled]="snapshot.isDisabled"
@for (file of snapshot.assetFiles; track file) {
<sqx-asset
[assetFile]="file"
[folderId]="folderId"
[isCompact]="snapshot.isCompact"
[isDisabled]="snapshot.isDisabled"
isListView="true"
(loadDone)="addAsset(file, $event)"
(loadError)="removeLoadingAsset(file)"></sqx-asset>
}
<div
cdkDropList
[cdkDropListData]="snapshot.assetItems"
[cdkDropListDisabled]="snapshot.isDisabled"
(cdkDropListDropped)="sortAssets($event)">
<div *ngFor="let asset of snapshot.assetItems; trackBy: trackByAsset" class="table-drag" cdkDrag cdkDragLockAxis="y">
<sqx-asset
[asset]="asset"
(edit)="editStart($event)"
[isCompact]="snapshot.isCompact"
[isDisabled]="snapshot.isDisabled"
isListView="true"
[removeMode]="true"
(remove)="removeLoadedAsset(asset)"
(update)="notifyOthers(asset)">
</sqx-asset>
</div>
@for (asset of snapshot.assetItems; track asset.id) {
<div class="table-drag" cdkDrag cdkDragLockAxis="y">
<sqx-asset
[asset]="asset"
(edit)="editStart($event)"
[isCompact]="snapshot.isCompact"
[isDisabled]="snapshot.isDisabled"
isListView="true"
(remove)="removeLoadedAsset(asset)"
[removeMode]="true"
(update)="notifyOthers(asset)"></sqx-asset>
</div>
}
</div>
</div>
</ng-template>
}
</div>
</div>
<sqx-asset-selector *sqxModal="assetsDialog"
(assetSelect)="selectAssets($event)">
</sqx-asset-selector>
<sqx-asset-selector (assetSelect)="selectAssets($event)" *sqxModal="assetsDialog"></sqx-asset-selector>
<sqx-asset-dialog *sqxModal="snapshot.editAsset;isDialog:true"
[asset]="snapshot.editAsset!"
<sqx-asset-dialog
[asset]="snapshot.editAsset!"
(assetReplaced)="notifyOthers($event)"
(assetUpdated)="notifyOthers($event)"
(dialogClose)="editDone()">
</sqx-asset-dialog>
(dialogClose)="editDone()"
*sqxModal="snapshot.editAsset; isDialog: true"></sqx-asset-dialog>
<sqx-chat-dialog *sqxModal="chatDialog" configuration="image" copyMode="Image"
(contentSelect)="addAssetFromAI($event)">
</sqx-chat-dialog>
<sqx-chat-dialog configuration="image" (contentSelect)="addAssetFromAI($event)" copyMode="Image" *sqxModal="chatDialog"></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 { NgFor, NgIf } from '@angular/common';
import { booleanAttribute, ChangeDetectionStrategy, Component, forwardRef, Input, OnInit } from '@angular/core';
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';
@ -58,8 +58,6 @@ interface State {
ChatDialogComponent,
FileDropDirective,
ModalDirective,
NgFor,
NgIf,
ResizedDirective,
TranslatePipe,
],
@ -237,8 +235,4 @@ export class AssetsEditorComponent extends StatefulControlComponent<State, Reado
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)">
<div class="header" *ngIf="formSection.separator; let separator">
<h3>{{separator!.displayName}}</h3>
<sqx-form-hint *ngIf="separator.properties.hints && separator.properties.hints.length > 0">
<span [sqxMarkdown]="separator.properties.hints" optional="true" inline="true"></span>
</sqx-form-hint>
</div>
<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>
@if (!(formSection.hiddenChanges | async)) {
@if (formSection.separator; as separator) {
<div class="header">
<h3>{{ separator!.displayName }}</h3>
@if (separator.properties.hints && separator.properties.hints.length > 0) {
<sqx-form-hint>
<span inline="true" optional="true" [sqxMarkdown]="separator.properties.hints"></span>
</sqx-form-hint>
}
</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>
</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.
*/
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 { 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';
@Component({
@ -20,8 +20,6 @@ import { FieldEditorComponent } from './field-editor.component';
AsyncPipe,
FormHintComponent,
MarkdownDirective,
NgFor,
NgIf,
forwardRef(() => FieldEditorComponent),
],
})
@ -61,8 +59,4 @@ export class ComponentSectionComponent {
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 *ngIf="formModel.schemaChanges | async; let schema; else noSchema">
<sqx-form-hint>
{{schema.displayName}}
</sqx-form-hint>
<div class="form-group" *ngFor="let section of formModel.sectionsChanges | async">
<sqx-component-section
[form]="form"
[formContext]="formContext"
[formLevel]="formLevel + 1"
[formSection]="$any(section)"
[isComparing]="isComparing"
[hasChatBot]="hasChatBot"
[language]="language"
[languages]="languages">
</sqx-component-section>
@if (formModel.schemaChanges | async; as schema) {
<div>
<sqx-form-hint>
{{ schema.displayName }}
</sqx-form-hint>
@for (section of formModel.sectionsChanges | async; track section) {
<div class="form-group">
<sqx-component-section
[form]="form"
[formContext]="formContext"
[formLevel]="formLevel + 1"
[formSection]="$any(section)"
[hasChatBot]="hasChatBot"
[isComparing]="isComparing"
[language]="language"
[languages]="languages"></sqx-component-section>
</div>
}
</div>
</div>
<ng-template #noSchema>
<ng-container *ngIf="schemasList.length > 1">
<button type="button" class="btn btn-outline-success dropdown-toggle" [disabled]="isDisabled | async" (click)="schemasDropdown.show()" #buttonSelect>
{{ 'contents.addComponent' | sqxTranslate}}
} @else {
@if (schemasList.length > 1) {
<button
class="btn btn-outline-success dropdown-toggle"
#buttonSelect
(click)="schemasDropdown.show()"
[disabled]="isDisabled | async"
type="button">
{{ "contents.addComponent" | sqxTranslate }}
</button>
<sqx-dropdown-menu *sqxModal="schemasDropdown;closeAlways:true" [sqxAnchoredTo]="buttonSelect" scrollY="true">
<a class="dropdown-item" *ngFor="let schema of schemasList" (click)="setSchema(schema)">
{{schema.displayName}}
</a>
<sqx-dropdown-menu scrollY="true" [sqxAnchoredTo]="buttonSelect" *sqxModal="schemasDropdown; closeAlways: true">
@for (schema of schemasList; track schema) {
<a class="dropdown-item" (click)="setSchema(schema)">
{{ schema.displayName }}
</a>
}
</sqx-dropdown-menu>
</ng-container>
<ng-container *ngIf="schemasList.length === 1">
<button type="button" class="btn btn-outline-success" [disabled]="isDisabled | async" (click)="setSchema(schemasList[0])">
{{ 'contents.addComponent' | sqxTranslate}}
}
@if (schemasList.length === 1) {
<button class="btn btn-outline-success" (click)="setSchema(schemasList[0])" [disabled]="isDisabled | async" type="button">
{{ "contents.addComponent" | sqxTranslate }}
</button>
</ng-container>
<ng-container *ngIf="schemasList.length === 0">
}
@if (schemasList.length === 0) {
<sqx-form-hint>
{{ 'contents.componentNoSchema' | sqxTranslate }}
{{ "contents.componentNoSchema" | sqxTranslate }}
</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.
*/
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 { 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';
@Component({
@ -24,8 +24,6 @@ import { ComponentSectionComponent } from './component-section.component';
FormHintComponent,
ModalDirective,
ModalPlacementDirective,
NgFor,
NgIf,
TranslatePipe,
],
})
@ -96,8 +94,4 @@ export class ComponentComponent {
public setSchema(schema: SchemaDto) {
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.col-12]="!formModelCompare" [class.col-6]="formModelCompare">
<sqx-focus-marker [controlId]="formModel.fieldPath">
<div class="table-items-row table-items-row-summary" [class.field-invalid]="isInvalid | async" *ngIf="!(formModel.hiddenChanges | async)">
<div class="languages-container">
<div class="languages-buttons">
<div class="languages-inner">
<sqx-field-languages
[formModel]="formModel"
(languageChange)="languageChange.emit($event)"
[language]="language"
[languages]="languages"
[showAllControls]="showAllControls"
(showAllControlsChange)="changeShowAllControls($event)">
</sqx-field-languages>
<sqx-field-copy-button [formModel]="formModel" [languages]="languages"></sqx-field-copy-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">
<i class="icon-translate"></i>
</button>
@if (!(formModel.hiddenChanges | async)) {
<div class="table-items-row table-items-row-summary" [class.field-invalid]="isInvalid | async">
<div class="languages-container">
<div class="languages-buttons">
<div class="languages-inner">
<sqx-field-languages
[formModel]="formModel"
[language]="language"
(languageChange)="languageChange.emit($event)"
[languages]="languages"
[showAllControls]="showAllControls"
(showAllControlsChange)="changeShowAllControls($event)"></sqx-field-languages>
<sqx-field-copy-button [formModel]="formModel" [languages]="languages"></sqx-field-copy-button>
@if (isTranslatable) {
<button
class="btn btn-sm btn-outline-secondary force no-focus-shadow ms-1"
(click)="translate()"
[disabled]="formModel.field.isDisabled"
tabindex="-1"
title="i18n:contents.autotranslate"
type="button">
<i class="icon-translate"></i>
</button>
}
</div>
</div>
</div>
</div>
<ng-container *ngIf="showAllControls; else singleControl">
<div class="form-group" *ngFor="let language of languages">
@if (showAllControls) {
@for (language of languages; track language) {
<div class="form-group">
<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
[comments]="commentsState"
[displaySuffix]="prefix(language)"
[form]="form"
[formContext]="formContext"
[formLevel]="formLevel"
[formModel]="formModel.get(language)"
[isComparing]="!!formModelCompare"
[formModel]="getControl()"
[hasChatBot]="hasChatBot"
[isComparing]="!!formModelCompare"
[language]="language"
[languages]="languages">
</sqx-field-editor>
</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>
[languages]="languages"></sqx-field-editor>
}
</div>
}
</sqx-focus-marker>
</div>
<div class="col-6 col-right" *ngIf="formModelCompare && formCompare">
<div class="copy-button-container" *ngIf="!(isDisabled | async)">
<button type="button" class="btn btn-primary btn-sm field-copy" (click)="copy()" *ngIf="isDifferent | async">
<i class="icon-arrow_back"></i>
</button>
</div>
<div class="table-items-row table-items-row-summary" *ngIf="!(formModelCompare!.hiddenChanges | async)">
<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>
@if (formModelCompare && formCompare) {
<div class="col-6 col-right">
@if (!(isDisabled | async)) {
<div class="copy-button-container">
@if (isDifferent | async) {
<button class="btn btn-primary btn-sm field-copy" (click)="copy()" type="button">
<i class="icon-arrow_back"></i>
</button>
}
</div>
</div>
<ng-container *ngIf="showAllControls; else singleControlCompare">
<div class="form-group" *ngFor="let language of languages">
<sqx-field-editor
[displaySuffix]="prefix(language)"
[form]="formCompare"
[formContext]="formContext"
[formLevel]="formLevel"
[formModel]="formModelCompare.get(language)"
[isComparing]="!!formModelCompare"
[hasChatBot]="hasChatBot"
[language]="language"
[languages]="languages">
</sqx-field-editor>
}
@if (!(formModelCompare!.hiddenChanges | async)) {
<div class="table-items-row table-items-row-summary">
<div class="languages-container">
<div class="languages-buttons-compare">
<div class="languages-inner">
<sqx-field-languages
[formModel]="formModelCompare!"
[language]="language"
(languageChange)="languageChange.emit($event)"
[languages]="languages"
[showAllControls]="showAllControls"
(showAllControlsChange)="changeShowAllControls($event)"></sqx-field-languages>
</div>
</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>
</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>

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.
*/
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 { Observable } from 'rxjs';
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,
FieldLanguagesComponent,
FocusMarkerComponent,
NgFor,
NgIf,
TooltipDirective,
],
})
@ -182,10 +180,6 @@ export class ContentFieldComponent {
return this.formModelCompare?.get(this.language.iso2Code);
}
public trackByLanguage(_index: number, language: AppLanguageDto) {
return language.iso2Code;
}
private showAllControlsKey() {
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">
<div class="header" *ngIf="formSection.separator; let separator">
<div class="row g-0 align">
<div class="col-auto">
<button type="button" class="btn btn-sm btn-text-secondary" (click)="toggle()">
<i [class.icon-caret-right]="snapshot.isCollapsed" [class.icon-caret-down]="!snapshot.isCollapsed"></i>
</button>
</div>
<div class="col">
<h3>{{separator.displayName}}</h3>
<sqx-form-hint *ngIf="separator.properties.hints && separator.properties.hints.length > 0">
<span [sqxMarkdown]="separator.properties.hints" optional="true" inline="true"></span>
</sqx-form-hint>
@if ((formSection.visibleChanges | async) || formCompare) {
@if (formSection.separator; as separator) {
<div class="header">
<div class="row g-0 align">
<div class="col-auto">
<button class="btn btn-sm btn-text-secondary" (click)="toggle()" type="button">
<i [class.icon-caret-down]="!snapshot.isCollapsed" [class.icon-caret-right]="snapshot.isCollapsed"></i>
</button>
</div>
<div class="col">
<h3>{{ separator.displayName }}</h3>
@if (separator.properties.hints && separator.properties.hints.length > 0) {
<sqx-form-hint>
<span inline="true" optional="true" [sqxMarkdown]="separator.properties.hints"></span>
</sqx-form-hint>
}
</div>
</div>
</div>
</div>
</ng-container>
}
}
<div class="row gx-1" [class.hidden]="snapshot.isCollapsed && !formCompare">
<sqx-content-field *ngFor="let field of formSection.fields; trackBy: trackByField"
(languageChange)="languageChange.emit($event)"
[form]="form"
[formCompare]="formCompare"
[formContext]="formContext"
[formLevel]="formLevel"
[formModel]="field"
[formModelCompare]="getFieldFormCompare(field)"
[isCompact]="isCompact"
[language]="language"
[languages]="languages"
[schema]="schema">
</sqx-content-field>
</div>
@for (field of formSection.fields; track field.field.fieldId) {
<sqx-content-field
[form]="form"
[formCompare]="formCompare"
[formContext]="formContext"
[formLevel]="formLevel"
[formModel]="field"
[formModelCompare]="getFieldFormCompare(field)"
[isCompact]="isCompact"
[language]="language"
(languageChange)="languageChange.emit($event)"
[languages]="languages"
[schema]="schema"></sqx-content-field>
}
</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.
*/
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 { AppLanguageDto, EditContentForm, FieldForm, FieldSection, FormHintComponent, LocalStoreService, MarkdownDirective, RootFieldDto, SchemaDto, Settings, StatefulComponent, TypedSimpleChanges } from '@app/shared';
import { ContentFieldComponent } from './content-field.component';
@ -25,8 +25,6 @@ interface State {
AsyncPipe,
FormHintComponent,
MarkdownDirective,
NgFor,
NgIf,
forwardRef(() => ContentFieldComponent),
],
})
@ -94,10 +92,6 @@ export class ContentSectionComponent extends StatefulComponent<State> {
return this.formCompare?.get(formState.field.name);
}
public trackByField(_index: number, formState: FieldForm) {
return formState.field.fieldId;
}
private isCollapsedKey(): string {
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">
<button type="button" class="btn btn-outline-secondary btn-sm ms-1 dropdown-toggle" title="{{ 'common.copy' | sqxTranslate }}" (click)="dropdown.toggle()" #button tabindex="-1">
@if (isLocalized) {
<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>
</button>
<sqx-dropdown-menu *sqxModal="dropdown" [sqxAnchoredTo]="button" scrollY="true">
<sqx-dropdown-menu scrollY="true" [sqxAnchoredTo]="button" *sqxModal="dropdown">
<div class="section d-flex justify-content-end">
<button type="button" class="btn btn-primary" (click)="copy()" tabindex="-1">
{{ 'common.copy' | sqxTranslate }}
<button class="btn btn-primary" (click)="copy()" tabindex="-1" type="button">
{{ "common.copy" | sqxTranslate }}
</button>
</div>
<div class="dropdown-divider"></div>
<div class="section">
<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">
<select class="form-select" id="languagesSource"
[ngModel]="copySource"
(ngModelChange)="setCopySource($event)">
<option *ngFor="let language of languages" [ngValue]="language.iso2Code">{{language.iso2Code}}</option>
<select class="form-select" id="languagesSource" [ngModel]="copySource" (ngModelChange)="setCopySource($event)">
@for (language of languages; track language) {
<option [ngValue]="language.iso2Code">{{ language.iso2Code }}</option>
}
</select>
</div>
</div>
</div>
<div class="dropdown-divider"></div>
<div class="section">
<label>{{ 'common.to' | sqxTranslate }}</label>
<sqx-checkbox-group [(ngModel)]="copyTargets" [values]="languageCodes" layout="Multiline"></sqx-checkbox-group>
<label>{{ "common.to" | sqxTranslate }}</label>
<sqx-checkbox-group layout="Multiline" [(ngModel)]="copyTargets" [values]="languageCodes"></sqx-checkbox-group>
</div>
</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.
*/
import { NgFor, NgIf } from '@angular/common';
import { Component, Input } from '@angular/core';
import { FormsModule } from '@angular/forms';
import { AppLanguageDto, CheckboxGroupComponent, DropdownMenuComponent, FieldForm, ModalDirective, ModalModel, ModalPlacementDirective, TooltipDirective, TranslatePipe, TypedSimpleChanges } from '@app/shared';
@ -21,8 +21,6 @@ import { AppLanguageDto, CheckboxGroupComponent, DropdownMenuComponent, FieldFor
FormsModule,
ModalDirective,
ModalPlacementDirective,
NgFor,
NgIf,
TooltipDirective,
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">
<fieldset class="buttons-container" [disabled]="isDisabled | async">
<div class="buttons">
<button type="button" class="btn btn-sm btn-outline-secondary force no-focus-shadow" (click)="chatDialog.show()" [disabled]="!hasChatBot || !isString" tabindex="-1">
AI
</button>
<button type="button" class="btn btn-sm btn-outline-secondary force no-focus-shadow ms-1" title="i18n:contents.fieldFullscreen" (click)="toggleExpanded()" tabindex="-1">
<i class="icon-fullscreen"></i>
</button>
<button type="button" class="btn btn-sm btn-outline-secondary btn-clear force no-focus-shadow ms-1" [disabled]="isEmpty | async" tabindex="-1"
(sqxConfirmClick)="unset()"
confirmTitle="i18n:contents.unsetValueConfirmTitle"
confirmText="i18n:contents.unsetValueConfirmText"
confirmRememberKey="unsetValue"
title="i18n:contents.unsetValue">
<i class="icon-close"></i>
</button>
@if (formModel) {
<div class="field" [class.expanded]="isExpanded">
<fieldset class="buttons-container" [disabled]="isDisabled | async">
<div class="buttons">
<button
class="btn btn-sm btn-outline-secondary force no-focus-shadow"
(click)="chatDialog.show()"
[disabled]="!hasChatBot || !isString"
tabindex="-1"
type="button">
AI
</button>
<button
class="btn btn-sm btn-outline-secondary force no-focus-shadow ms-1"
(click)="toggleExpanded()"
tabindex="-1"
title="i18n:contents.fieldFullscreen"
type="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>
</fieldset>
<label>
{{field.displayName}} {{displaySuffix}} <span class="field-required" [class.hidden]="!field.properties.isRequired">*</span>
</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>
@if (field.properties.hints && field.properties.hints.length > 0) {
<sqx-form-hint>
<span inline="true" optional="true" [sqxMarkdown]="field.properties.hints"></span>
</sqx-form-hint>
}
</div>
}
<sqx-form-hint *ngIf="field.properties.hints && field.properties.hints.length > 0">
<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>
<sqx-chat-dialog (contentSelect)="setValue($event)" *sqxModal="chatDialog"></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.
*/
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 { AbstractControl, FormsModule, ReactiveFormsModule } from '@angular/forms';
import { Observable } from 'rxjs';
@ -44,10 +44,6 @@ import { StockPhotoEditorComponent } from './stock-photo-editor.component';
IndeterminateValueDirective,
MarkdownDirective,
ModalDirective,
NgFor,
NgIf,
NgSwitch,
NgSwitchCase,
RadioGroupComponent,
ReactiveFormsModule,
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">
<button *ngIf="!formModel.field.properties.isComplexUI" type="button" class="btn btn-sm btn-outline-secondary force no-focus-shadow" (click)="toggleShowAllControls()">
<ng-container *ngIf="showAllControls; else singleLanguage">
<span>{{ 'contents.languageModeSingle' | sqxTranslate }}</span>
</ng-container>
<ng-template #singleLanguage>
<span>{{ 'contents.languageModeAll' | sqxTranslate }}</span>
</ng-template>
</button>
<ng-container *ngIf="formModel.field.properties.isComplexUI || !showAllControls">
@if (formModel.field.isLocalizable && languages.length > 1) {
@if (!formModel.field.properties.isComplexUI) {
<button class="btn btn-sm btn-outline-secondary force no-focus-shadow" (click)="toggleShowAllControls()" type="button">
@if (showAllControls) {
<span>{{ "contents.languageModeSingle" | sqxTranslate }}</span>
} @else {
<span>{{ "contents.languageModeAll" | sqxTranslate }}</span>
}
</button>
}
@if (formModel.field.properties.isComplexUI || !showAllControls) {
<div class="button-container ms-1">
<sqx-language-selector
size="sm"
[exists]="formModel.translationStatus | async"
(languageChange)="languageChange.emit($event)"
[language]="language"
[languages]="languages"
hintText="i18n:contents.validationHint"
hintAfter="120000"
hintPosition="top-end"
sqxTourStep="languages">
</sqx-language-selector>
hintText="i18n:contents.validationHint"
[language]="language"
(languageChange)="languageChange.emit($event)"
[languages]="languages"
size="sm"
sqxTourStep="languages"></sqx-language-selector>
</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.
*/
import { AsyncPipe, NgIf } from '@angular/common';
import { AsyncPipe } from '@angular/common';
import { booleanAttribute, ChangeDetectionStrategy, Component, EventEmitter, Input, Output } from '@angular/core';
import { AppLanguageDto, FieldForm, LanguageSelectorComponent, TourHintDirective, TourStepDirective, TranslatePipe } from '@app/shared';
@ -18,7 +18,6 @@ import { AppLanguageDto, FieldForm, LanguageSelectorComponent, TourHintDirective
imports: [
AsyncPipe,
LanguageSelectorComponent,
NgIf,
TourHintDirective,
TourStepDirective,
TranslatePipe,

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

@ -1,18 +1,21 @@
<div #container>
<div #inner [class.fullscreen]="snapshot.isFullscreen" [class.expanded]="isExpanded">
<iframe #iframe [scrolling]="!isExpanded ? 'no' : 'yes'" width="100%" [style.height]="0" [attr.src]="computedUrl | sqxSafeResourceUrl"></iframe>
<div #inner [class.expanded]="isExpanded" [class.fullscreen]="snapshot.isFullscreen">
<iframe
#iframe
[attr.src]="computedUrl | sqxSafeResourceUrl"
[scrolling]="!isExpanded ? 'no' : 'yes'"
[style.height]="0"
width="100%"></iframe>
</div>
</div>
<sqx-asset-selector *sqxModal="assetsDialog"
(assetSelect)="pickAssets($event)">
</sqx-asset-selector>
<sqx-asset-selector (assetSelect)="pickAssets($event)" *sqxModal="assetsDialog"></sqx-asset-selector>
<sqx-content-selector *sqxModal="contentsDialog"
(contentSelect)="pickContents($event)"
<sqx-content-selector
[alreadySelectedIds]="contentsSelectedIds"
[query]="contentsQuery"
(contentSelect)="pickContents($event)"
[language]="language"
[languages]="languages"
[schemaIdentifiers]="contentsSchemas">
</sqx-content-selector>
[query]="contentsQuery"
[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">
<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>
</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>
</button>
<input readonly [disabled]="true" class="form-control" [formControl]="valueControl">
<input class="form-control" [disabled]="true" [formControl]="valueControl" readonly />
</div>
<div *ngIf="stockPhotoThumbnail | async; let url;" class="preview mt-1" [class.hidden-important]="snapshot.thumbnailStatus === 'Failed'">
<img [src]="url" (error)="onThumbnailFailed()" (load)="onThumbnailLoaded()">
<sqx-loader color="white" *ngIf="snapshot.thumbnailStatus !== 'Loaded'"></sqx-loader>
</div>
@if (stockPhotoThumbnail | async; as url) {
<div class="preview mt-1" [class.hidden-important]="snapshot.thumbnailStatus === 'Failed'">
<img (error)="onThumbnailFailed()" (load)="onThumbnailLoaded()" [src]="url" />
@if (snapshot.thumbnailStatus !== "Loaded") {
<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>
<input class="form-control search" [formControl]="stockPhotoSearch" sqxFocusOnInit placeholder="{{ 'contents.stockPhotoSearch' | sqxTranslate }}">
<sqx-loader *ngIf="snapshot.isLoading"></sqx-loader>
<input
class="form-control search"
[formControl]="stockPhotoSearch"
placeholder="{{ 'contents.stockPhotoSearch' | sqxTranslate }}"
sqxFocusOnInit />
@if (snapshot.isLoading) {
<sqx-loader></sqx-loader>
}
</ng-container>
<ng-container content>
<div class="photos">
<div *ngFor="let photo of snapshot.stockPhotos; trackBy: trackByPhoto" class="photo" [class.selected]="isSelected(photo)" (click)="selectPhoto(photo)">
<img [src]="photo.thumbUrl">
<div class="photo-user">
<a class="photo-user-link" [href]="photo.userProfileUrl" sqxExternalLink sqxStopClick>
{{photo.user}}
</a>
@for (photo of snapshot.stockPhotos; track photo.thumbUrl) {
<div class="photo" [class.selected]="isSelected(photo)" (click)="selectPhoto(photo)">
<img [src]="photo.thumbUrl" />
<div class="photo-user">
<a class="photo-user-link" [href]="photo.userProfileUrl" sqxExternalLink sqxStopClick>
{{ photo.user }}
</a>
</div>
</div>
</div>
</div>
<div class="empty small text-muted text-center" *ngIf="snapshot.stockPhotos.length === 0">
{{ 'contents.stockPhotoSearchEmpty' | sqxTranslate }}
} @empty {
<div class="empty small text-muted text-center">
{{ "contents.stockPhotoSearchEmpty" | sqxTranslate }}
</div>
}
</div>
<div class="mt-4 text-center" *ngIf="snapshot.hasMore">
<button class="btn btn-outline-secondary" type="button" (click)="loadMore()" [disabled]="snapshot.isLoading">
{{ 'common.loadMore' | sqxTranslate }} <sqx-loader *ngIf="snapshot.isLoading"></sqx-loader>
</button>
</div>
@if (snapshot.hasMore) {
<div class="mt-4 text-center">
<button class="btn btn-outline-secondary" (click)="loadMore()" [disabled]="snapshot.isLoading" type="button">
{{ "common.loadMore" | sqxTranslate }}
@if (snapshot.isLoading) {
<sqx-loader></sqx-loader>
}
</button>
</div>
}
</ng-container>
</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.
*/
import { AsyncPipe, NgFor, NgIf } from '@angular/common';
import { AsyncPipe } from '@angular/common';
import { booleanAttribute, ChangeDetectionStrategy, Component, forwardRef, Input, OnInit } from '@angular/core';
import { FormsModule, NG_VALUE_ACCESSOR, ReactiveFormsModule, UntypedFormControl } from '@angular/forms';
import { BehaviorSubject, of } from 'rxjs';
@ -49,8 +49,6 @@ type Request = { search?: string; page: number };
LoaderComponent,
ModalDialogComponent,
ModalDirective,
NgFor,
NgIf,
ReactiveFormsModule,
StopClickDirective,
TooltipDirective,
@ -172,8 +170,4 @@ export class StockPhotoEditorComponent extends StatefulControlComponent<State, s
public isSelected(photo: StockPhotoDto) {
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">
<td class="cell-select inline-edit" sqxStopClick>
<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"
(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>
<ng-container *ngIf="isDirty">
@if (isDirty) {
<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>
</button>
<button type="button" class="btn btn-success" (click)="save()" sqxStopClick>
<button class="btn btn-success" (click)="save()" sqxStopClick type="button">
<i class="icon-checkmark"></i>
</button>
</div>
</ng-container>
}
</td>
<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>
</button>
<sqx-dropdown-menu *sqxModal="dropdown;closeAlways:true" [sqxAnchoredTo]="buttonOptions" scrollY="true" position="bottom-start">
<a class="dropdown-item" [routerLink]="link" target="_blank" sqxExternalLink>
{{ 'common.editInNewTab' | sqxTranslate }}
<sqx-dropdown-menu position="bottom-start" scrollY="true" [sqxAnchoredTo]="buttonOptions" *sqxModal="dropdown; closeAlways: true">
<a class="dropdown-item" [routerLink]="link" sqxExternalLink target="_blank">
{{ "common.editInNewTab" | sqxTranslate }}
</a>
<a class="dropdown-item" *ngFor="let info of content.statusUpdates" (click)="statusChange.emit(info.status)">
{{ 'common.statusChangeTo' | sqxTranslate }}
@for (info of content.statusUpdates; track info) {
<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>
<a class="dropdown-item dropdown-item-delete" [class.disabled]="!content.canDelete"
(sqxConfirmClick)="delete.emit()"
confirmTitle="i18n:contents.deleteConfirmTitle"
<a
class="dropdown-item dropdown-item-delete"
[class.disabled]="!content.canDelete"
confirmRememberKey="deleteContent"
confirmText="i18n:contents.deleteConfirmText"
confirmRememberKey="deleteContent">
{{ 'common.delete' | sqxTranslate }}
confirmTitle="i18n:contents.deleteConfirmTitle"
(sqxConfirmClick)="delete.emit()">
{{ "common.delete" | sqxTranslate }}
</a>
</sqx-dropdown-menu>
</td>
<td *ngFor="let field of tableFields"
sqxContentListCell
sqxContentListCellResize
[field]="field"
[fields]="tableSettings"
[sqxStopClick]="shouldStop(field)">
<sqx-content-list-field
[content]="content"
[field]="field"
[fields]="tableSettings"
[language]="language"
[languages]="languages"
[patchAllowed]="patchAllowed"
[patchForm]="patchForm?.form"
[schema]="schema">
</sqx-content-list-field>
</td>
@for (field of tableFields; track field) {
<td [field]="field" [fields]="tableSettings" sqxContentListCell sqxContentListCellResize [sqxStopClick]="shouldStop(field)">
<sqx-content-list-field
[content]="content"
[field]="field"
[fields]="tableSettings"
[language]="language"
[languages]="languages"
[patchAllowed]="patchAllowed"
[patchForm]="patchForm?.form"
[schema]="schema"></sqx-content-list-field>
</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 */
import { NgFor, NgIf } from '@angular/common';
import { booleanAttribute, ChangeDetectionStrategy, ChangeDetectorRef, Component, EventEmitter, Input, Output, QueryList, ViewChildren } from '@angular/core';
import { FormsModule } from '@angular/forms';
import { RouterLink } from '@angular/router';
@ -31,8 +31,6 @@ import { AppLanguageDto, ConfirmClickDirective, ContentDto, ContentListCellDirec
FormsModule,
ModalDirective,
ModalPlacementDirective,
NgFor,
NgIf,
RouterLink,
StopClickDirective,
TabRouterlinkDirective,

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

Loading…
Cancel
Save