Browse Source

Array virtualization v2 (#854)

* Array editor virtualization.

* Mini improvement.

* Some refactorings.

* Fix imports.
pull/855/head
Sebastian Stehle 4 years ago
committed by GitHub
parent
commit
98f7c46ff1
No known key found for this signature in database GPG Key ID: 4AEE18F83AFDEB23
  1. 1
      backend/i18n/frontend_en.json
  2. 1
      backend/i18n/frontend_it.json
  3. 1
      backend/i18n/frontend_nl.json
  4. 1
      backend/i18n/frontend_zh.json
  5. 1
      backend/i18n/source/frontend_en.json
  6. 1176
      backend/src/Squidex.Shared/Texts.nl.resx
  7. 43
      frontend/package-lock.json
  8. 1
      frontend/package.json
  9. 30
      frontend/src/app/_theme.html
  10. 2
      frontend/src/app/features/assets/pages/asset-tags.component.html
  11. 6
      frontend/src/app/features/content/module.ts
  12. 6
      frontend/src/app/features/content/pages/content/content-event.component.ts
  13. 10
      frontend/src/app/features/content/pages/content/editor/content-field.component.ts
  14. 6
      frontend/src/app/features/content/pages/content/editor/content-section.component.ts
  15. 16
      frontend/src/app/features/content/pages/schemas/schemas-page.component.ts
  16. 28
      frontend/src/app/features/content/shared/forms/array-editor.component.html
  17. 27
      frontend/src/app/features/content/shared/forms/array-editor.component.scss
  18. 40
      frontend/src/app/features/content/shared/forms/array-editor.component.ts
  19. 6
      frontend/src/app/features/content/shared/forms/array-item.component.html
  20. 39
      frontend/src/app/features/content/shared/forms/array-item.component.ts
  21. 2
      frontend/src/app/features/content/shared/list/content.component.ts
  22. 4
      frontend/src/app/features/content/shared/references/reference-dropdown.component.ts
  23. 6
      frontend/src/app/features/content/shared/references/reference-item.component.html
  24. 6
      frontend/src/app/features/content/shared/references/reference-item.component.ts
  25. 4
      frontend/src/app/features/content/shared/references/references-checkboxes.component.ts
  26. 4
      frontend/src/app/features/content/shared/references/references-tags.component.ts
  27. 6
      frontend/src/app/features/dashboard/pages/cards/api-performance-card.component.ts
  28. 6
      frontend/src/app/features/dashboard/pages/cards/api-traffic-card.component.ts
  29. 1
      frontend/src/app/features/rules/declarations.ts
  30. 4
      frontend/src/app/features/rules/module.ts
  31. 4
      frontend/src/app/features/rules/pages/events/rule-event.component.html
  32. 20
      frontend/src/app/features/rules/pages/events/rule-event.component.ts
  33. 4
      frontend/src/app/features/rules/pages/rules/rule.component.html
  34. 18
      frontend/src/app/features/rules/pages/simulator/rule-transition.component.ts
  35. 6
      frontend/src/app/features/rules/pages/simulator/simulated-rule-event.component.html
  36. 34
      frontend/src/app/features/rules/pages/simulator/simulated-rule-event.component.ts
  37. 8
      frontend/src/app/features/rules/shared/actions/formattable-input.component.ts
  38. 43
      frontend/src/app/features/rules/shared/pipes.ts
  39. 8
      frontend/src/app/features/schemas/pages/schema/fields/field-wizard.component.ts
  40. 6
      frontend/src/app/features/schemas/pages/schema/fields/field.component.html
  41. 8
      frontend/src/app/features/schemas/pages/schema/fields/field.component.ts
  42. 17
      frontend/src/app/features/settings/pages/backups/backup.component.ts
  43. 4
      frontend/src/app/features/settings/pages/roles/role.component.html
  44. 8
      frontend/src/app/features/settings/pages/roles/role.component.ts
  45. 30
      frontend/src/app/framework/angular/if-once.directive.ts
  46. 4
      frontend/src/app/framework/angular/layout.component.ts
  47. 18
      frontend/src/app/framework/angular/pipes/strings.pipes.ts
  48. 2
      frontend/src/app/framework/declarations.ts
  49. 6
      frontend/src/app/framework/module.ts
  50. 3
      frontend/src/app/shared/components/assets/asset-dialog.component.ts
  51. 2
      frontend/src/app/shared/components/assets/asset-folder-dropdown-item.component.html
  52. 4
      frontend/src/app/shared/components/assets/asset-folder-dropdown-item.component.ts
  53. 3
      frontend/src/app/shared/components/contents/content-list-cell.directive.ts
  54. 24
      frontend/src/app/shared/components/contents/content-list-field.component.ts
  55. 24
      frontend/src/app/shared/components/contents/content-status.component.ts
  56. 3
      frontend/src/app/shared/components/contents/content-value.component.ts
  57. 2
      frontend/src/app/shared/components/schema-category.component.html
  58. 2
      frontend/src/app/shared/components/search/queries/filter-logical.component.html
  59. 8
      frontend/src/app/shared/components/search/queries/filter-logical.component.ts
  60. 4
      frontend/src/app/shared/components/search/queries/query-path.component.html
  61. 11
      frontend/src/app/shared/components/search/queries/query-path.component.ts
  62. 17
      frontend/src/app/shared/state/contents.forms-helpers.ts
  63. 2
      frontend/src/app/shell/pages/internal/apps-menu.component.html
  64. 2
      frontend/src/app/shell/pages/internal/notifications-menu.component.html
  65. 9
      frontend/src/app/shell/pages/internal/notifications-menu.component.ts
  66. 2
      frontend/src/app/shell/pages/internal/search-menu.component.html

1
backend/i18n/frontend_en.json

@ -483,6 +483,7 @@
"contents.scheduledBy": "by", "contents.scheduledBy": "by",
"contents.scheduledTo": "to", "contents.scheduledTo": "to",
"contents.scheduledToLabel": "Scheduled to", "contents.scheduledToLabel": "Scheduled to",
"contents.scheduledTooltip": "Will be set to '{status}' at {time}.",
"contents.schemasPageTitle": "Contents", "contents.schemasPageTitle": "Contents",
"contents.searchPlaceholder": "Fulltext search", "contents.searchPlaceholder": "Fulltext search",
"contents.searchSchemasPlaceholder": "Search", "contents.searchSchemasPlaceholder": "Search",

1
backend/i18n/frontend_it.json

@ -483,6 +483,7 @@
"contents.scheduledBy": "by", "contents.scheduledBy": "by",
"contents.scheduledTo": "a", "contents.scheduledTo": "a",
"contents.scheduledToLabel": "Scheduled to", "contents.scheduledToLabel": "Scheduled to",
"contents.scheduledTooltip": "Will be set to '{status}' at {time}.",
"contents.schemasPageTitle": "Contenuti", "contents.schemasPageTitle": "Contenuti",
"contents.searchPlaceholder": "Ricerca testuale", "contents.searchPlaceholder": "Ricerca testuale",
"contents.searchSchemasPlaceholder": "Cerca schemi...", "contents.searchSchemasPlaceholder": "Cerca schemi...",

1
backend/i18n/frontend_nl.json

@ -483,6 +483,7 @@
"contents.scheduledBy": "door", "contents.scheduledBy": "door",
"contents.scheduledTo": "naar", "contents.scheduledTo": "naar",
"contents.scheduledToLabel": "Ingepland tot", "contents.scheduledToLabel": "Ingepland tot",
"contents.scheduledTooltip": "Will be set to '{status}' at {time}.",
"contents.schemasPageTitle": "Inhoud", "contents.schemasPageTitle": "Inhoud",
"contents.searchPlaceholder": "Zoeken in volledige tekst", "contents.searchPlaceholder": "Zoeken in volledige tekst",
"contents.searchSchemasPlaceholder": "Zoek schema's ...", "contents.searchSchemasPlaceholder": "Zoek schema's ...",

1
backend/i18n/frontend_zh.json

@ -483,6 +483,7 @@
"contents.scheduledBy": "by", "contents.scheduledBy": "by",
"contents.scheduledTo": "to", "contents.scheduledTo": "to",
"contents.scheduledToLabel": "Scheduled to", "contents.scheduledToLabel": "Scheduled to",
"contents.scheduledTooltip": "Will be set to '{status}' at {time}.",
"contents.schemasPageTitle": "内容", "contents.schemasPageTitle": "内容",
"contents.searchPlaceholder": "全文搜索", "contents.searchPlaceholder": "全文搜索",
"contents.searchSchemasPlaceholder": "搜索Schemas...", "contents.searchSchemasPlaceholder": "搜索Schemas...",

1
backend/i18n/source/frontend_en.json

@ -483,6 +483,7 @@
"contents.scheduledBy": "by", "contents.scheduledBy": "by",
"contents.scheduledTo": "to", "contents.scheduledTo": "to",
"contents.scheduledToLabel": "Scheduled to", "contents.scheduledToLabel": "Scheduled to",
"contents.scheduledTooltip": "Will be set to '{status}' at {time}.",
"contents.schemasPageTitle": "Contents", "contents.schemasPageTitle": "Contents",
"contents.searchPlaceholder": "Fulltext search", "contents.searchPlaceholder": "Fulltext search",
"contents.searchSchemasPlaceholder": "Search", "contents.searchSchemasPlaceholder": "Search",

1176
backend/src/Squidex.Shared/Texts.nl.resx

File diff suppressed because it is too large

43
frontend/package-lock.json

@ -44,6 +44,7 @@
"mousetrap": "1.6.5", "mousetrap": "1.6.5",
"ngx-color-picker": "11.0.0", "ngx-color-picker": "11.0.0",
"ngx-doc-viewer": "2.0.5", "ngx-doc-viewer": "2.0.5",
"ngx-virtual-scroller": "^4.0.3",
"oidc-client": "1.11.5", "oidc-client": "1.11.5",
"pikaday": "1.8.2", "pikaday": "1.8.2",
"progressbar.js": "1.1.0", "progressbar.js": "1.1.0",
@ -3573,6 +3574,11 @@
"integrity": "sha512-eZxlbI8GZscaGS7kkc/trHTT5xgrjH3/1n2JDwusC9iahPKWMRvRjJSAN5mCXviuTGQ/lHnhvv8Q1YTpnfz9gA==", "integrity": "sha512-eZxlbI8GZscaGS7kkc/trHTT5xgrjH3/1n2JDwusC9iahPKWMRvRjJSAN5mCXviuTGQ/lHnhvv8Q1YTpnfz9gA==",
"dev": true "dev": true
}, },
"node_modules/@tweenjs/tween.js": {
"version": "17.4.0",
"resolved": "https://registry.npmjs.org/@tweenjs/tween.js/-/tween.js-17.4.0.tgz",
"integrity": "sha512-J3fzl1F6wvh8KXVVcIuHN12xi1ZDcPA/0Vix+ZcJYwZWVHUwfIqfvzYXXEw7ybeev6477KCTt9fKydU+ajUqcg=="
},
"node_modules/@types/codemirror": { "node_modules/@types/codemirror": {
"version": "0.0.108", "version": "0.0.108",
"resolved": "https://registry.npmjs.org/@types/codemirror/-/codemirror-0.0.108.tgz", "resolved": "https://registry.npmjs.org/@types/codemirror/-/codemirror-0.0.108.tgz",
@ -3885,6 +3891,11 @@
"@types/jquery": "*" "@types/jquery": "*"
} }
}, },
"node_modules/@types/tween.js": {
"version": "17.2.0",
"resolved": "https://registry.npmjs.org/@types/tween.js/-/tween.js-17.2.0.tgz",
"integrity": "sha512-mOsqurEtFEzwgkVc/jDVE2XrjZBYTbrmDUyCr9GXmnfc6q5otokxFtKvSY/B21zgz9LVRIvRTawKczjKi57wrA=="
},
"node_modules/@types/uglify-js": { "node_modules/@types/uglify-js": {
"version": "3.13.1", "version": "3.13.1",
"resolved": "https://registry.npmjs.org/@types/uglify-js/-/uglify-js-3.13.1.tgz", "resolved": "https://registry.npmjs.org/@types/uglify-js/-/uglify-js-3.13.1.tgz",
@ -11592,6 +11603,19 @@
"@angular/platform-browser": ">= 10.0.0" "@angular/platform-browser": ">= 10.0.0"
} }
}, },
"node_modules/ngx-virtual-scroller": {
"version": "4.0.3",
"resolved": "https://registry.npmjs.org/ngx-virtual-scroller/-/ngx-virtual-scroller-4.0.3.tgz",
"integrity": "sha512-JBqUJ/f7GRCZDnI/JeiFoTmYR8rC/Hyv8L5I7ImePM6f/hwiFNRsrK8Abdd0E3TwklwgmZAK875te9XQJrgsyQ==",
"dependencies": {
"@tweenjs/tween.js": "17.4.0",
"@types/tween.js": "17.2.0"
},
"peerDependencies": {
"@angular/core": ">=6.0.0",
"tslib": "^1.10.0"
}
},
"node_modules/nice-napi": { "node_modules/nice-napi": {
"version": "1.0.2", "version": "1.0.2",
"resolved": "https://registry.npmjs.org/nice-napi/-/nice-napi-1.0.2.tgz", "resolved": "https://registry.npmjs.org/nice-napi/-/nice-napi-1.0.2.tgz",
@ -20366,6 +20390,11 @@
"integrity": "sha512-eZxlbI8GZscaGS7kkc/trHTT5xgrjH3/1n2JDwusC9iahPKWMRvRjJSAN5mCXviuTGQ/lHnhvv8Q1YTpnfz9gA==", "integrity": "sha512-eZxlbI8GZscaGS7kkc/trHTT5xgrjH3/1n2JDwusC9iahPKWMRvRjJSAN5mCXviuTGQ/lHnhvv8Q1YTpnfz9gA==",
"dev": true "dev": true
}, },
"@tweenjs/tween.js": {
"version": "17.4.0",
"resolved": "https://registry.npmjs.org/@tweenjs/tween.js/-/tween.js-17.4.0.tgz",
"integrity": "sha512-J3fzl1F6wvh8KXVVcIuHN12xi1ZDcPA/0Vix+ZcJYwZWVHUwfIqfvzYXXEw7ybeev6477KCTt9fKydU+ajUqcg=="
},
"@types/codemirror": { "@types/codemirror": {
"version": "0.0.108", "version": "0.0.108",
"resolved": "https://registry.npmjs.org/@types/codemirror/-/codemirror-0.0.108.tgz", "resolved": "https://registry.npmjs.org/@types/codemirror/-/codemirror-0.0.108.tgz",
@ -20658,6 +20687,11 @@
"@types/jquery": "*" "@types/jquery": "*"
} }
}, },
"@types/tween.js": {
"version": "17.2.0",
"resolved": "https://registry.npmjs.org/@types/tween.js/-/tween.js-17.2.0.tgz",
"integrity": "sha512-mOsqurEtFEzwgkVc/jDVE2XrjZBYTbrmDUyCr9GXmnfc6q5otokxFtKvSY/B21zgz9LVRIvRTawKczjKi57wrA=="
},
"@types/uglify-js": { "@types/uglify-js": {
"version": "3.13.1", "version": "3.13.1",
"resolved": "https://registry.npmjs.org/@types/uglify-js/-/uglify-js-3.13.1.tgz", "resolved": "https://registry.npmjs.org/@types/uglify-js/-/uglify-js-3.13.1.tgz",
@ -26509,6 +26543,15 @@
"tslib": "^2.1.0" "tslib": "^2.1.0"
} }
}, },
"ngx-virtual-scroller": {
"version": "4.0.3",
"resolved": "https://registry.npmjs.org/ngx-virtual-scroller/-/ngx-virtual-scroller-4.0.3.tgz",
"integrity": "sha512-JBqUJ/f7GRCZDnI/JeiFoTmYR8rC/Hyv8L5I7ImePM6f/hwiFNRsrK8Abdd0E3TwklwgmZAK875te9XQJrgsyQ==",
"requires": {
"@tweenjs/tween.js": "17.4.0",
"@types/tween.js": "17.2.0"
}
},
"nice-napi": { "nice-napi": {
"version": "1.0.2", "version": "1.0.2",
"resolved": "https://registry.npmjs.org/nice-napi/-/nice-napi-1.0.2.tgz", "resolved": "https://registry.npmjs.org/nice-napi/-/nice-napi-1.0.2.tgz",

1
frontend/package.json

@ -49,6 +49,7 @@
"mousetrap": "1.6.5", "mousetrap": "1.6.5",
"ngx-color-picker": "11.0.0", "ngx-color-picker": "11.0.0",
"ngx-doc-viewer": "2.0.5", "ngx-doc-viewer": "2.0.5",
"ngx-virtual-scroller": "^4.0.3",
"oidc-client": "1.11.5", "oidc-client": "1.11.5",
"pikaday": "1.8.2", "pikaday": "1.8.2",
"progressbar.js": "1.1.0", "progressbar.js": "1.1.0",

30
frontend/src/app/_theme.html

@ -1161,24 +1161,24 @@
<h2>Badges</h2> <h2>Badges</h2>
<div class="bs-component" style="margin-bottom: 40px;"> <div class="bs-component" style="margin-bottom: 40px;">
<span class="badge bg-primary">Primary</span> <span class="badge badge-primary">Primary</span>
<span class="badge bg-secondary">Secondary</span> <span class="badge badge-secondary">Secondary</span>
<span class="badge bg-success">Success</span> <span class="badge badge-success">Success</span>
<span class="badge bg-danger">Danger</span> <span class="badge badge-danger">Danger</span>
<span class="badge bg-warning">Warning</span> <span class="badge badge-warning">Warning</span>
<span class="badge bg-info">Info</span> <span class="badge badge-info">Info</span>
<span class="badge bg-light">Light</span> <span class="badge badge-light">Light</span>
<span class="badge bg-dark">Dark</span> <span class="badge badge-dark">Dark</span>
</div> </div>
<div class="bs-component"> <div class="bs-component">
<span class="badge rounded-pill bg-primary">Primary</span> <span class="badge rounded-pill badge-primary">Primary</span>
<span class="badge rounded-pill bg-secondary">Secondary</span> <span class="badge rounded-pill badge-secondary">Secondary</span>
<span class="badge rounded-pill bg-success">Success</span> <span class="badge rounded-pill badge-success">Success</span>
<span class="badge rounded-pill bg-danger">Danger</span> <span class="badge rounded-pill badge-danger">Danger</span>
<span class="badge rounded-pill bg-warning">Warning</span> <span class="badge rounded-pill badge-warning">Warning</span>
<span class="badge rounded-pill bg-info">Info</span> <span class="badge rounded-pill badge-info">Info</span>
<span class="badge rounded-pill bg-light">Light</span> <span class="badge rounded-pill badge-light">Light</span>
<span class="badge rounded-pill bg-dark">Dark</span> <span class="badge rounded-pill bg-dark">Dark</span>
</div> </div>
</div> </div>

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

@ -12,7 +12,7 @@
<span class="truncate">{{tag.name}}</span> <span class="truncate">{{tag.name}}</span>
</div> </div>
<div class="col-auto"> <div class="col-auto">
<div class="badge bg-secondary rounded-pill">{{tag.count}}</div> <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> <a class="btn-sm btn-text-secondary btn-rename" (click)="renameTag(tag)" *ngIf="canRename" sqxStopClick>
<i class="icon-pencil"></i> <i class="icon-pencil"></i>

6
frontend/src/app/features/content/module.ts

@ -5,10 +5,9 @@
* Copyright (c) Squidex UG (haftungsbeschränkt). All rights reserved. * Copyright (c) Squidex UG (haftungsbeschränkt). All rights reserved.
*/ */
import { ScrollingModule as ScrollingModuleExperimental } from '@angular/cdk-experimental/scrolling';
import { ScrollingModule } from '@angular/cdk/scrolling';
import { NgModule } from '@angular/core'; import { NgModule } from '@angular/core';
import { RouterModule, Routes } from '@angular/router'; import { RouterModule, Routes } from '@angular/router';
import { VirtualScrollerModule } from 'ngx-virtual-scroller';
import { CanDeactivateGuard, ContentMustExistGuard, LoadLanguagesGuard, LoadSchemasGuard, SchemaMustExistPublishedGuard, SchemaMustNotBeSingletonGuard, SqxFrameworkModule, SqxSharedModule } from '@app/shared'; import { CanDeactivateGuard, ContentMustExistGuard, LoadLanguagesGuard, LoadSchemasGuard, SchemaMustExistPublishedGuard, SchemaMustNotBeSingletonGuard, SqxFrameworkModule, SqxSharedModule } from '@app/shared';
import { ArrayEditorComponent, ArrayItemComponent, AssetsEditorComponent, CalendarPageComponent, CommentsPageComponent, ComponentComponent, ComponentSectionComponent, ContentComponent, ContentCreatorComponent, ContentEditorComponent, ContentEventComponent, ContentExtensionComponent, ContentFieldComponent, ContentHistoryPageComponent, ContentInspectionComponent, ContentPageComponent, ContentReferencesComponent, ContentSectionComponent, ContentsFiltersPageComponent, ContentsPageComponent, CustomViewEditorComponent, DueTimeSelectorComponent, FieldCopyButtonComponent, FieldEditorComponent, FieldLanguagesComponent, IFrameEditorComponent, PreviewButtonComponent, ReferenceDropdownComponent, ReferenceItemComponent, ReferencesCheckboxesComponent, ReferencesEditorComponent, ReferencesTagsComponent, SchemasPageComponent, SidebarPageComponent, StockPhotoEditorComponent } from './declarations'; import { ArrayEditorComponent, ArrayItemComponent, AssetsEditorComponent, CalendarPageComponent, CommentsPageComponent, ComponentComponent, ComponentSectionComponent, ContentComponent, ContentCreatorComponent, ContentEditorComponent, ContentEventComponent, ContentExtensionComponent, ContentFieldComponent, ContentHistoryPageComponent, ContentInspectionComponent, ContentPageComponent, ContentReferencesComponent, ContentSectionComponent, ContentsFiltersPageComponent, ContentsPageComponent, CustomViewEditorComponent, DueTimeSelectorComponent, FieldCopyButtonComponent, FieldEditorComponent, FieldLanguagesComponent, IFrameEditorComponent, PreviewButtonComponent, ReferenceDropdownComponent, ReferenceItemComponent, ReferencesCheckboxesComponent, ReferencesEditorComponent, ReferencesTagsComponent, SchemasPageComponent, SidebarPageComponent, StockPhotoEditorComponent } from './declarations';
@ -88,10 +87,9 @@ const routes: Routes = [
@NgModule({ @NgModule({
imports: [ imports: [
RouterModule.forChild(routes), RouterModule.forChild(routes),
ScrollingModule,
ScrollingModuleExperimental,
SqxFrameworkModule, SqxFrameworkModule,
SqxSharedModule, SqxSharedModule,
VirtualScrollerModule,
], ],
declarations: [ declarations: [
ArrayEditorComponent, ArrayEditorComponent,

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

@ -5,7 +5,7 @@
* Copyright (c) Squidex UG (haftungsbeschränkt). All rights reserved. * Copyright (c) Squidex UG (haftungsbeschränkt). All rights reserved.
*/ */
import { ChangeDetectionStrategy, Component, EventEmitter, Input, OnChanges, Output } from '@angular/core'; import { ChangeDetectionStrategy, Component, EventEmitter, Input, OnChanges, Output, SimpleChanges } from '@angular/core';
import { ContentDto, HistoryEventDto } from '@app/shared'; import { ContentDto, HistoryEventDto } from '@app/shared';
@Component({ @Component({
@ -29,10 +29,12 @@ export class ContentEventComponent implements OnChanges {
public canLoadOrCompare = false; public canLoadOrCompare = false;
public ngOnChanges() { public ngOnChanges(changes: SimpleChanges) {
if (changes['event']) {
this.canLoadOrCompare = this.canLoadOrCompare =
(this.event.eventType === 'ContentUpdatedEvent' || (this.event.eventType === 'ContentUpdatedEvent' ||
this.event.eventType === 'ContentCreatedEventV2') && this.event.eventType === 'ContentCreatedEventV2') &&
!this.event.version.eq(this.content.version); !this.event.version.eq(this.content.version);
} }
} }
}

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

@ -48,6 +48,11 @@ export class ContentFieldComponent implements OnChanges {
@Input() @Input()
public languages!: ReadonlyArray<AppLanguageDto>; public languages!: ReadonlyArray<AppLanguageDto>;
public showAllControls = false;
public isDifferent?: Observable<boolean>;
public isInvalid?: Observable<boolean>;
@HostBinding('class') @HostBinding('class')
public get class() { public get class() {
return this.isHalfWidth ? 'col-6 half-field' : 'col-12'; return this.isHalfWidth ? 'col-6 half-field' : 'col-12';
@ -61,11 +66,6 @@ export class ContentFieldComponent implements OnChanges {
return this.formModel.field.properties.fieldType === 'String' && this.formModel.field.isLocalizable && this.languages.length > 1; return this.formModel.field.properties.fieldType === 'String' && this.formModel.field.isLocalizable && this.languages.length > 1;
} }
public showAllControls = false;
public isDifferent?: Observable<boolean>;
public isInvalid?: Observable<boolean>;
constructor( constructor(
private readonly appsState: AppsState, private readonly appsState: AppsState,
private readonly localStore: LocalStoreService, private readonly localStore: LocalStoreService,

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

@ -5,7 +5,7 @@
* Copyright (c) Squidex UG (haftungsbeschränkt). All rights reserved. * Copyright (c) Squidex UG (haftungsbeschränkt). All rights reserved.
*/ */
import { ChangeDetectionStrategy, ChangeDetectorRef, Component, EventEmitter, Input, OnChanges, Output } from '@angular/core'; import { ChangeDetectionStrategy, ChangeDetectorRef, Component, EventEmitter, Input, OnChanges, Output, SimpleChanges } from '@angular/core';
import { AppLanguageDto, EditContentForm, FieldForm, FieldSection, LocalStoreService, RootFieldDto, SchemaDto, Settings, StatefulComponent } from '@app/shared'; import { AppLanguageDto, EditContentForm, FieldForm, FieldSection, LocalStoreService, RootFieldDto, SchemaDto, Settings, StatefulComponent } from '@app/shared';
interface State { interface State {
@ -64,13 +64,15 @@ export class ContentSectionComponent extends StatefulComponent<State> implements
}); });
} }
public ngOnChanges() { public ngOnChanges(changes: SimpleChanges) {
if (changes['formSection' || changes['schema']]) {
if (this.formSection?.separator && this.schema) { if (this.formSection?.separator && this.schema) {
const isCollapsed = this.localStore.getBoolean(this.expandedKey()); const isCollapsed = this.localStore.getBoolean(this.expandedKey());
this.next({ isCollapsed }); this.next({ isCollapsed });
} }
} }
}
public toggle() { public toggle() {
this.next(s => ({ this.next(s => ({

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

@ -9,7 +9,7 @@ import { Component } from '@angular/core';
import { FormControl } from '@angular/forms'; import { FormControl } from '@angular/forms';
import { combineLatest } from 'rxjs'; import { combineLatest } from 'rxjs';
import { map } from 'rxjs/operators'; import { map } from 'rxjs/operators';
import { AppsState, getCategoryTree, LocalStoreService, SchemaCategory, SchemasState, Settings, UIOptions, value$ } from '@app/shared'; import { AppsState, getCategoryTree, SchemaCategory, SchemasState, Settings, UIOptions, value$ } from '@app/shared';
@Component({ @Component({
selector: 'sqx-schemas-page', selector: 'sqx-schemas-page',
@ -43,27 +43,13 @@ export class SchemasPageComponent {
return getCategoryTree(schemas, categories, filter); return getCategoryTree(schemas, categories, filter);
}); });
public isCollapsed = false;
public get width() {
return this.isCollapsed ? '4rem' : '16rem';
}
constructor(uiOptions: UIOptions, constructor(uiOptions: UIOptions,
public readonly schemasState: SchemasState, public readonly schemasState: SchemasState,
private readonly appsState: AppsState, private readonly appsState: AppsState,
private readonly localStore: LocalStoreService,
) { ) {
this.isCollapsed = localStore.getBoolean(Settings.Local.SCHEMAS_COLLAPSED);
this.isEmbedded = uiOptions.get('embedded'); this.isEmbedded = uiOptions.get('embedded');
} }
public toggle() {
this.isCollapsed = !this.isCollapsed;
this.localStore.setBoolean(Settings.Local.SCHEMAS_COLLAPSED, this.isCollapsed);
}
public trackByCategory(_index: number, category: SchemaCategory) { public trackByCategory(_index: number, category: SchemaCategory) {
return category.name; return category.name;
} }

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

@ -1,13 +1,14 @@
<ng-container *ngIf="formModel.itemChanges | async; let items"> <ng-container *ngIf="formModel.itemChanges | async; let items">
<div class="array-container" *ngIf="items.length > 0"> <div class="array-container" *ngIf="items.length > 0 && items.length <= 20;"
<div class="cdk-container" *ngIf="items.length <= 20;"
cdkDropList cdkDropList
[cdkDropListDisabled]="false" [cdkDropListDisabled]="false"
[cdkDropListData]="items" [cdkDropListData]="items"
(cdkDropListDropped)="sort($event)"> (cdkDropListDropped)="sort($event)">
<div *ngFor="let itemForm of items; index as i; last as isLast; first as isFirst" class="table-drag item" <div *ngFor="let itemForm of items; index as i; last as isLast; first as isFirst;" class="table-drag item"
cdkDrag cdkDrag
cdkDragLockAxis="y"> cdkDragLockAxis="y"
[class.first]="isFirst"
[class.last]="isLast">
<sqx-array-item <sqx-array-item
[canUnset]="canUnset" [canUnset]="canUnset"
(clone)="addCopy(itemForm)" (clone)="addCopy(itemForm)"
@ -29,8 +30,11 @@
</div> </div>
</div> </div>
<cdk-virtual-scroll-viewport autosize *ngIf="items.length > 20"> <div class="array-container" *ngIf="items.length > 20">
<div *cdkVirtualFor="let itemForm of items; index as i; last as isLast; first as isFirst" class="table-drag item"> <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 <sqx-array-item
[canUnset]="canUnset" [canUnset]="canUnset"
(clone)="addCopy(itemForm)" (clone)="addCopy(itemForm)"
@ -38,19 +42,19 @@
[formContext]="formContext" [formContext]="formContext"
[formLevel]="formLevel + 1" [formLevel]="formLevel + 1"
[formModel]="itemForm" [formModel]="itemForm"
[index]="i" [index]="scroll.viewPortInfo.startIndexWithBuffer + i"
[isCollapsedInitial]="isCollapsedInitial" [isCollapsedInitial]="isCollapsedInitial"
[isDisabled]="isDisabled | async" [isDisabled]="isDisabled | async"
[isFirst]="isFirst" [isFirst]="scroll.viewPortInfo.startIndexWithBuffer + i === 0"
[isLast]="isLast" [isLast]="scroll.viewPortInfo.startIndexWithBuffer + i === items.length - 1"
(itemExpanded)="onExpanded()" (itemExpanded)="scroll.invalidateCachedMeasurementAtIndex(scroll.viewPortInfo.startIndexWithBuffer + i)"
(itemRemove)="removeItem(i)" (itemRemove)="removeItem(scroll.viewPortInfo.startIndexWithBuffer + i)"
(itemMove)="move(itemForm, $event)" (itemMove)="move(itemForm, $event)"
[language]="language" [language]="language"
[languages]="languages"> [languages]="languages">
</sqx-array-item> </sqx-array-item>
</div> </div>
</cdk-virtual-scroll-viewport> </virtual-scroller>
</div> </div>
<div class="row g-0 align-items-center"> <div class="row g-0 align-items-center">

27
frontend/src/app/features/content/shared/forms/array-editor.component.scss

@ -1,9 +1,21 @@
@import 'mixins'; @import 'mixins';
@import 'vars'; @import 'vars';
:host ::ng-deep { /* stylelint-disable no-descending-specificity */
.cdk-virtual-scroll-content-wrapper {
right: 0; virtual-scroller {
height: 700px;
}
.item {
padding: .25rem 1rem;
&.first {
padding-top: .75rem;
}
&.last {
padding-bottom: .75rem;
} }
} }
@ -11,17 +23,8 @@
background: $color-border-lighter; background: $color-border-lighter;
margin: 0; margin: 0;
margin-bottom: 1rem; margin-bottom: 1rem;
padding: .5rem 0;
} }
.drag-container { .drag-container {
position: relative; position: relative;
} }
cdk-virtual-scroll-viewport {
height: 1000px;
}
.item {
margin: .25rem 1rem;
}

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

@ -6,8 +6,8 @@
*/ */
import { CdkDragDrop } from '@angular/cdk/drag-drop'; import { CdkDragDrop } from '@angular/cdk/drag-drop';
import { CdkVirtualScrollViewport } from '@angular/cdk/scrolling'; import { ChangeDetectionStrategy, Component, Input, OnChanges, QueryList, SimpleChanges, ViewChildren } from '@angular/core';
import { ChangeDetectionStrategy, Component, Input, OnChanges, OnInit, QueryList, SimpleChanges, ViewChildren } from '@angular/core'; import { VirtualScrollerComponent } from 'ngx-virtual-scroller';
import { combineLatest, Observable } from 'rxjs'; import { combineLatest, Observable } from 'rxjs';
import { map } from 'rxjs/operators'; import { map } from 'rxjs/operators';
import { AppLanguageDto, ComponentsFieldPropertiesDto, disabled$, EditContentForm, FieldArrayForm, LocalStoreService, ModalModel, ObjectFormBase, SchemaDto, Settings, sorted, Types } from '@app/shared'; import { AppLanguageDto, ComponentsFieldPropertiesDto, disabled$, EditContentForm, FieldArrayForm, LocalStoreService, ModalModel, ObjectFormBase, SchemaDto, Settings, sorted, Types } from '@app/shared';
@ -19,7 +19,7 @@ import { ArrayItemComponent } from './array-item.component';
templateUrl: './array-editor.component.html', templateUrl: './array-editor.component.html',
changeDetection: ChangeDetectionStrategy.OnPush, changeDetection: ChangeDetectionStrategy.OnPush,
}) })
export class ArrayEditorComponent implements OnChanges, OnInit { export class ArrayEditorComponent implements OnChanges {
@Input() @Input()
public form!: EditContentForm; public form!: EditContentForm;
@ -44,8 +44,8 @@ export class ArrayEditorComponent implements OnChanges, OnInit {
@ViewChildren(ArrayItemComponent) @ViewChildren(ArrayItemComponent)
public children!: QueryList<ArrayItemComponent>; public children!: QueryList<ArrayItemComponent>;
@ViewChildren(CdkVirtualScrollViewport) @ViewChildren(VirtualScrollerComponent)
public viewport?: QueryList<CdkVirtualScrollViewport>; public scroller?: QueryList<VirtualScrollerComponent>;
public isArray = false; public isArray = false;
@ -66,10 +66,6 @@ export class ArrayEditorComponent implements OnChanges, OnInit {
) { ) {
} }
public ngOnInit() {
this.isCollapsedInitial = this.formLevel > 0;
}
public ngOnChanges(changes: SimpleChanges) { public ngOnChanges(changes: SimpleChanges) {
if (changes['formModel']) { if (changes['formModel']) {
const maxItems = this.formModel.field.properties['maxItems'] || Number.MAX_VALUE; const maxItems = this.formModel.field.properties['maxItems'] || Number.MAX_VALUE;
@ -88,10 +84,10 @@ export class ArrayEditorComponent implements OnChanges, OnInit {
]).pipe(map(([disabled, items]) => { ]).pipe(map(([disabled, items]) => {
return disabled || items.length >= maxItems; return disabled || items.length >= maxItems;
})); }));
if (this.formLevel === 0) {
this.isCollapsedInitial = this.localStore.getBoolean(this.expandedKey());
} }
if (changes['formModel'] || changes['formLevel']) {
this.isCollapsedInitial = this.localStore.getBoolean(this.expandedKey()) || this.formLevel > 0;
} }
} }
@ -127,32 +123,28 @@ export class ArrayEditorComponent implements OnChanges, OnInit {
this.reset(); this.reset();
} }
public onExpanded() {
this.viewport?.first?.checkViewportSize();
}
public collapseAll() { public collapseAll() {
this.children.forEach(child => { for (const item of this.formModel.items) {
child.collapse(); item.collapse();
}); }
if (this.formLevel === 0) { if (this.formLevel === 0) {
this.localStore.setBoolean(this.expandedKey(), true); this.localStore.setBoolean(this.expandedKey(), true);
} }
this.onExpanded(); this.scroller?.first?.invalidateAllCachedMeasurements();
} }
public expandAll() { public expandAll() {
this.children.forEach(child => { for (const item of this.formModel.items) {
child.expand(); item.expand();
}); }
if (this.formLevel === 0) { if (this.formLevel === 0) {
this.localStore.setBoolean(this.expandedKey(), false); this.localStore.setBoolean(this.expandedKey(), false);
} }
this.onExpanded(); this.scroller?.first?.invalidateAllCachedMeasurements();
} }
private reset() { private reset() {

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

@ -23,10 +23,10 @@
<button type="button" class="btn btn-text-secondary" [disabled]="isDisabled || isLast" (click)="moveBottom()" title="i18n:contents.arrayMoveBottom"> <button type="button" class="btn btn-text-secondary" [disabled]="isDisabled || isLast" (click)="moveBottom()" title="i18n:contents.arrayMoveBottom">
<i class="icon-caret-bottom"></i> <i class="icon-caret-bottom"></i>
</button> </button>
<button type="button" class="btn btn-text-secondary" [class.hidden]="snapshot.isExpanded" (click)="expand()" title="i18n:contents.arrayExpandItem"> <button type="button" class="btn btn-text-secondary" [class.hidden]="!(isCollapsed | async)" (click)="expand()" title="i18n:contents.arrayExpandItem">
<i class="icon-plus-square"></i> <i class="icon-plus-square"></i>
</button> </button>
<button type="button" class="btn btn-text-secondary" [class.hidden]="!snapshot.isExpanded" (click)="collapse()" title="i18n:contents.arrayCollapseItem"> <button type="button" class="btn btn-text-secondary" [class.hidden]="isCollapsed | async" (click)="collapse()" title="i18n:contents.arrayCollapseItem">
<i class="icon-minus-square"></i> <i class="icon-minus-square"></i>
</button> </button>
</div> </div>
@ -42,7 +42,7 @@
</div> </div>
</div> </div>
<div class="card-body" *ngIf="snapshot.isExpandedOnce" [class.hidden]="!snapshot.isExpanded"> <div class="card-body" *sqxIfOnce="!(isCollapsed | async)" [class.hidden]="isCollapsed | async">
<div class="form-group" *ngFor="let section of formModel.sectionsChanges | async"> <div class="form-group" *ngFor="let section of formModel.sectionsChanges | async">
<sqx-component-section <sqx-component-section
[canUnset]="canUnset" [canUnset]="canUnset"

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

@ -5,27 +5,19 @@
* Copyright (c) Squidex UG (haftungsbeschränkt). All rights reserved. * Copyright (c) Squidex UG (haftungsbeschränkt). All rights reserved.
*/ */
import { ChangeDetectionStrategy, ChangeDetectorRef, Component, EventEmitter, Input, OnChanges, OnInit, Output, QueryList, SimpleChanges, ViewChildren } from '@angular/core'; import { ChangeDetectionStrategy, Component, EventEmitter, Input, OnChanges, Output, QueryList, SimpleChanges, ViewChildren } from '@angular/core';
import { Observable, of } from 'rxjs'; import { Observable, of } from 'rxjs';
import { map } from 'rxjs/operators'; import { map } from 'rxjs/operators';
import { AppLanguageDto, ComponentForm, EditContentForm, FieldDto, FieldFormatter, FieldSection, invalid$, ObjectFormBase, RootFieldDto, StatefulComponent, Types, valueProjection$ } from '@app/shared'; import { AppLanguageDto, ComponentForm, EditContentForm, FieldDto, FieldFormatter, FieldSection, invalid$, ObjectFormBase, RootFieldDto, Types, valueProjection$ } from '@app/shared';
import { ComponentSectionComponent } from './component-section.component'; import { ComponentSectionComponent } from './component-section.component';
interface State {
// True when the item is expanded.
isExpanded: boolean;
// True when the item is expanded at least once.
isExpandedOnce: boolean;
}
@Component({ @Component({
selector: 'sqx-array-item[form][formContext][formLevel][formModel][index][language][languages]', selector: 'sqx-array-item[form][formContext][formLevel][formModel][index][language][languages]',
styleUrls: ['./array-item.component.scss'], styleUrls: ['./array-item.component.scss'],
templateUrl: './array-item.component.html', templateUrl: './array-item.component.html',
changeDetection: ChangeDetectionStrategy.OnPush, changeDetection: ChangeDetectionStrategy.OnPush,
}) })
export class ArrayItemComponent extends StatefulComponent<State> implements OnChanges, OnInit { export class ArrayItemComponent implements OnChanges {
@Output() @Output()
public itemRemove = new EventEmitter(); public itemRemove = new EventEmitter();
@ -77,24 +69,13 @@ export class ArrayItemComponent extends StatefulComponent<State> implements OnCh
@ViewChildren(ComponentSectionComponent) @ViewChildren(ComponentSectionComponent)
public sections!: QueryList<ComponentSectionComponent>; public sections!: QueryList<ComponentSectionComponent>;
public isCollapsed = false;
public isInvalid?: Observable<boolean>; public isInvalid?: Observable<boolean>;
public isInvalidComponent?: Observable<boolean>; public isInvalidComponent?: Observable<boolean>;
public title?: Observable<string>; public title?: Observable<string>;
constructor(changeDetector: ChangeDetectorRef, public get isCollapsed() {
) { return this.formModel.collapsedChanges;
super(changeDetector, {
isExpanded: false,
isExpandedOnce: false,
});
}
public ngOnInit() {
if (!this.isCollapsedInitial) {
this.expand();
}
} }
public ngOnChanges(changes: SimpleChanges) { public ngOnChanges(changes: SimpleChanges) {
@ -109,16 +90,22 @@ export class ArrayItemComponent extends StatefulComponent<State> implements OnCh
this.title = valueProjection$(this.formModel.form, () => getTitle(this.formModel)); this.title = valueProjection$(this.formModel.form, () => getTitle(this.formModel));
} }
if (changes['formModel'] || changes['isCollapsedInitial']) {
if (this.isCollapsedInitial && this.formModel.collapsed === null) {
this.collapse();
}
}
} }
public collapse() { public collapse() {
this.next({ isExpanded: false }); this.formModel.collapse();
this.itemExpanded.emit(); this.itemExpanded.emit();
} }
public expand() { public expand() {
this.next({ isExpanded: true, isExpandedOnce: true }); this.formModel.expand();
this.itemExpanded.emit(); this.itemExpanded.emit();
} }

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

@ -59,7 +59,7 @@ export class ContentComponent implements OnChanges {
public dropdown = new ModalModel(); public dropdown = new ModalModel();
public get isDirty() { public get isDirty() {
return this.patchForm && this.patchForm.form.dirty; return this.patchForm?.form.dirty === true;
} }
constructor( constructor(

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

@ -58,12 +58,12 @@ export class ReferenceDropdownComponent extends StatefulControlComponent<State,
this.setDisabledState(value === true); this.setDisabledState(value === true);
} }
public control = new FormControl('');
public get isValid() { public get isValid() {
return !!this.schemaId && !!this.language; return !!this.schemaId && !!this.language;
} }
public control = new FormControl('');
constructor(changeDetector: ChangeDetectorRef, constructor(changeDetector: ChangeDetectorRef,
private readonly contentsResolver: ResolveContents, private readonly contentsResolver: ResolveContents,
private readonly localizer: LocalizerService, private readonly localizer: LocalizerService,

6
frontend/src/app/features/content/shared/references/reference-item.component.html

@ -12,8 +12,8 @@
</td> </td>
<td class="cell-valid" *ngIf="validityVisible"> <td class="cell-valid" *ngIf="validityVisible">
<span class="badge rounded-pill bg-success" *ngIf="valid === true">VALID</span> <span class="badge rounded-pill badge-success" *ngIf="isValid === true">VALID</span>
<span class="badge rounded-pill bg-danger" *ngIf="valid === false">INVALID</span> <span class="badge rounded-pill badge-danger" *ngIf="isValid === false">INVALID</span>
</td> </td>
<td sqxContentListCell="meta.status.color"> <td sqxContentListCell="meta.status.color">
@ -22,7 +22,7 @@
<td class="cell-label" *ngIf="!isCompact"> <td class="cell-label" *ngIf="!isCompact">
<div class="bg-container d-flex align-items-center"> <div class="bg-container d-flex align-items-center">
<span class="badge bg-primary rounded-pill truncate-inline">{{content.schemaDisplayName}}</span> <span class="badge badge-primary rounded-pill truncate-inline">{{content.schemaDisplayName}}</span>
</div> </div>
</td> </td>

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

@ -47,12 +47,12 @@ export class ReferenceItemComponent implements OnChanges {
@Input('sqxReferenceItem') @Input('sqxReferenceItem')
public content!: ContentDto; public content!: ContentDto;
public get valid() { public values: ReadonlyArray<any> = [];
public get isValid() {
return !this.validations ? undefined : this.validations[this.content.id]; return !this.validations ? undefined : this.validations[this.content.id];
} }
public values: ReadonlyArray<any> = [];
public ngOnChanges() { public ngOnChanges() {
const values = []; const values = [];

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

@ -45,12 +45,12 @@ export class ReferencesCheckboxesComponent extends StatefulControlComponent<Stat
this.setDisabledState(value === true); this.setDisabledState(value === true);
} }
public control = new FormControl([]);
public get isValid() { public get isValid() {
return !!this.schemaId && !!this.language; return !!this.schemaId && !!this.language;
} }
public control = new FormControl([]);
constructor(changeDetector: ChangeDetectorRef, uiOptions: UIOptions, constructor(changeDetector: ChangeDetectorRef, uiOptions: UIOptions,
private readonly appsState: AppsState, private readonly appsState: AppsState,
private readonly contentsService: ContentsService, private readonly contentsService: ContentsService,

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

@ -51,12 +51,12 @@ export class ReferencesTagsComponent extends StatefulControlComponent<State, Rea
this.setDisabledState(value === true); this.setDisabledState(value === true);
} }
public control = new FormControl([]);
public get isValid() { public get isValid() {
return !!this.schemaId && !!this.language; return !!this.schemaId && !!this.language;
} }
public control = new FormControl([]);
constructor(changeDetector: ChangeDetectorRef, constructor(changeDetector: ChangeDetectorRef,
private readonly contentsResolver: ResolveContents, private readonly contentsResolver: ResolveContents,
private readonly localizer: LocalizerService, private readonly localizer: LocalizerService,

6
frontend/src/app/features/dashboard/pages/cards/api-performance-card.component.ts

@ -28,13 +28,13 @@ export class ApiPerformanceCardComponent implements OnChanges {
@Output() @Output()
public isStackedChange = new EventEmitter<boolean>(); public isStackedChange = new EventEmitter<boolean>();
public chartData: any;
public chartSummary = 0;
public get chartOptions() { public get chartOptions() {
return this.isStacked ? ChartOptions.Stacked : ChartOptions.Default; return this.isStacked ? ChartOptions.Stacked : ChartOptions.Default;
} }
public chartData: any;
public chartSummary = 0;
public ngOnChanges() { public ngOnChanges() {
if (this.usage) { if (this.usage) {
const labels = ChartHelpers.createLabelsFromSet(this.usage.details); const labels = ChartHelpers.createLabelsFromSet(this.usage.details);

6
frontend/src/app/features/dashboard/pages/cards/api-traffic-card.component.ts

@ -28,13 +28,13 @@ export class ApiTrafficCardComponent implements OnChanges {
@Output() @Output()
public isStackedChange = new EventEmitter<boolean>(); public isStackedChange = new EventEmitter<boolean>();
public chartData: any;
public chartSummary = 0;
public get chartOptions() { public get chartOptions() {
return this.isStacked ? ChartOptions.Stacked : ChartOptions.Default; return this.isStacked ? ChartOptions.Stacked : ChartOptions.Default;
} }
public chartData: any;
public chartSummary = 0;
public ngOnChanges(changes: SimpleChanges) { public ngOnChanges(changes: SimpleChanges) {
if (this.usage && changes['usage']) { if (this.usage && changes['usage']) {
const labels = ChartHelpers.createLabelsFromSet(this.usage.details); const labels = ChartHelpers.createLabelsFromSet(this.usage.details);

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

@ -17,6 +17,7 @@ export * from './shared/actions/formattable-input.component';
export * from './shared/actions/generic-action.component'; export * from './shared/actions/generic-action.component';
export * from './shared/rule-element.component'; export * from './shared/rule-element.component';
export * from './shared/rule-icon.component'; export * from './shared/rule-icon.component';
export * from './shared/pipes';
export * from './shared/triggers/asset-changed-trigger.component'; export * from './shared/triggers/asset-changed-trigger.component';
export * from './shared/triggers/comment-trigger.component'; export * from './shared/triggers/comment-trigger.component';
export * from './shared/triggers/content-changed-trigger.component'; export * from './shared/triggers/content-changed-trigger.component';

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

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

4
frontend/src/app/features/rules/pages/events/rule-event.component.html

@ -1,6 +1,6 @@
<tr [class.expanded]="expanded" class="table-items-row"> <tr [class.expanded]="expanded" class="table-items-row">
<td class="cell-label"> <td class="cell-label">
<span class="badge rounded-pill bg-{{jobResultClass}}">{{event.jobResult}}</span> <span class="badge rounded-pill badge-{{event.jobResult | sqxRuleClass}}">{{event.jobResult}}</span>
</td> </td>
<td class="cell-40"> <td class="cell-40">
<span class="truncate">{{event.eventName}}</span> <span class="truncate">{{event.eventName}}</span>
@ -25,7 +25,7 @@
<div class="row g-0 event-stats align-items-center"> <div class="row g-0 event-stats align-items-center">
<div class="col-3"> <div class="col-3">
<span class="badge rounded-pill bg-{{resultClass}}">{{event.result}}</span> <span class="badge rounded-pill badge-{{event.result | sqxRuleClass}}">{{event.result}}</span>
</div> </div>
<div class="col-2"> <div class="col-2">
{{ 'rules.ruleEvents.numAttemptsLabel' | sqxTranslate }}: {{event.numCalls}} {{ 'rules.ruleEvents.numAttemptsLabel' | sqxTranslate }}: {{event.numCalls}}

20
frontend/src/app/features/rules/pages/events/rule-event.component.ts

@ -29,24 +29,4 @@ export class RuleEventComponent {
@Output() @Output()
public cancel = new EventEmitter<any>(); public cancel = new EventEmitter<any>();
public get jobResultClass() {
return getClass(this.event.jobResult);
}
public get resultClass() {
return getClass(this.event.result);
}
}
function getClass(result: string) {
if (result === 'Retry') {
return 'warning';
} else if (result === 'Failed' || result === 'Cancelled') {
return 'danger';
} else if (result === 'Pending') {
return 'secondary';
} else {
return result.toLowerCase();
}
} }

4
frontend/src/app/features/rules/pages/rules/rule.component.html

@ -99,12 +99,12 @@
<div class="col-3"> <div class="col-3">
{{ 'common.succeeded' | sqxTranslate }} {{ 'common.succeeded' | sqxTranslate }}
<span class="badge bg-success rounded-pill" *ngIf="rule.numSucceeded > 0">{{rule.numSucceeded}}</span> <span class="badge badge-success rounded-pill" *ngIf="rule.numSucceeded > 0">{{rule.numSucceeded}}</span>
</div> </div>
<div class="col-3"> <div class="col-3">
{{ 'common.failed' | sqxTranslate }} {{ 'common.failed' | sqxTranslate }}
<span class="badge bg-danger rounded-pill" *ngIf="rule.numFailed > 0">{{rule.numFailed}}</span> <span class="badge badge-danger rounded-pill" *ngIf="rule.numFailed > 0">{{rule.numFailed}}</span>
</div> </div>
<div class="col"> <div class="col">
{{ 'common.lastExecuted' | sqxTranslate }}: <span>{{rule.lastExecuted | sqxFromNow:'-'}}</span> {{ 'common.lastExecuted' | sqxTranslate }}: <span>{{rule.lastExecuted | sqxFromNow:'-'}}</span>

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

@ -5,7 +5,7 @@
* Copyright (c) Squidex UG (haftungsbeschränkt). All rights reserved. * Copyright (c) Squidex UG (haftungsbeschränkt). All rights reserved.
*/ */
import { ChangeDetectionStrategy, Component, Input } from '@angular/core'; import { ChangeDetectionStrategy, Component, Input, OnChanges, SimpleChanges } from '@angular/core';
import { SimulatedRuleEventDto } from '@app/shared'; import { SimulatedRuleEventDto } from '@app/shared';
@Component({ @Component({
@ -14,7 +14,7 @@ import { SimulatedRuleEventDto } from '@app/shared';
templateUrl: './rule-transition.component.html', templateUrl: './rule-transition.component.html',
changeDetection: ChangeDetectionStrategy.OnPush, changeDetection: ChangeDetectionStrategy.OnPush,
}) })
export class RuleTransitionComponent { export class RuleTransitionComponent implements OnChanges {
@Input() @Input()
public event: SimulatedRuleEventDto | undefined | null; public event: SimulatedRuleEventDto | undefined | null;
@ -24,19 +24,25 @@ export class RuleTransitionComponent {
@Input() @Input()
public text: string | undefined | null; public text: string | undefined | null;
public get filteredErrors() { public filteredErrors?: string[] | null;
public ngOnChanges(changes: SimpleChanges) {
if (changes['event'] || changes['errors']) {
const errors = this.errors; const errors = this.errors;
if (!errors) { if (!errors) {
return null; this.filteredErrors = null;
return;
} }
const result = this.event?.skipReasons.filter(x => errors.includes(x)).map(x => `rules.simulation.error${x}`); const result = this.event?.skipReasons.filter(x => errors.includes(x)).map(x => `rules.simulation.error${x}`);
if (result?.length === 0) { if (result?.length === 0) {
return null; this.filteredErrors = null;
return;
} }
return result; this.filteredErrors = result;
}
} }
} }

6
frontend/src/app/features/rules/pages/simulator/simulated-rule-event.component.html

@ -1,12 +1,12 @@
<tr [class.expanded]="expanded" class="table-items-row"> <tr [class.expanded]="expanded" class="table-items-row">
<td class="cell-label"> <td class="cell-label">
<span class="badge rounded-pill bg-{{statusClass}}">{{status}}</span> <span class="badge rounded-pill badge-{{event | sqxSimulatedRuleEventStatus | sqxRuleClass}}">{{event | sqxSimulatedRuleEventStatus}}</span>
</td> </td>
<td class="cell-40"> <td class="cell-40">
<span class="truncate">{{event.eventName}}</span> <span class="truncate">{{event.eventName}}</span>
</td> </td>
<td class="cell-40"> <td class="cell-40">
<small class="truncate">{{event.skipReasons.join(', ')}}</small> <small class="truncate">{{event.skipReasons | sqxJoin}}</small>
</td> </td>
<td class="cell-actions"> <td class="cell-actions">
<button type="button" class="btn btn-outline-secondary btn-expand" [class.expanded]="expanded" (click)="expandedChange.emit()"> <button type="button" class="btn btn-outline-secondary btn-expand" [class.expanded]="expanded" (click)="expandedChange.emit()">
@ -53,7 +53,7 @@
<div class="history-state"> <div class="history-state">
<label>{{ 'rules.actionData' | sqxTranslate }}</label> <label>{{ 'rules.actionData' | sqxTranslate }}</label>
<sqx-code-editor [ngModel]="data" [disabled]="true" [wordWrap]="true" height="auto" [maxLines]="20"></sqx-code-editor> <sqx-code-editor [ngModel]="event.actionData" valueMode="Json" [disabled]="true" [wordWrap]="true" height="auto" [maxLines]="20"></sqx-code-editor>
</div> </div>
<sqx-rule-transition text='rules.simulation.actionExecuted'></sqx-rule-transition> <sqx-rule-transition text='rules.simulation.actionExecuted'></sqx-rule-transition>

34
frontend/src/app/features/rules/pages/simulator/simulated-rule-event.component.ts

@ -46,38 +46,4 @@ export class SimulatedRuleEventComponent {
public errorsAfterEvent = ERRORS_AFTER_EVENT; public errorsAfterEvent = ERRORS_AFTER_EVENT;
public errorsAfterEnrichedEvent = ERRORS_AFTER_ENRICHED_EVENT; public errorsAfterEnrichedEvent = ERRORS_AFTER_ENRICHED_EVENT;
public errorsFailed = ERRORS_FAILED; public errorsFailed = ERRORS_FAILED;
public get data() {
let result = this.event.actionData;
if (result) {
try {
result = JSON.stringify(JSON.parse(result), null, 2);
} catch {
result = this.event.actionData;
}
}
return result;
}
public get status() {
if (this.event.error) {
return 'Failed';
} else if (this.event.skipReasons.length > 0) {
return 'Skipped';
} else {
return 'Success';
}
}
public get statusClass() {
if (this.event.error) {
return 'danger';
} else if (this.event.skipReasons.length > 0) {
return 'warning';
} else {
return 'success';
}
}
} }

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

@ -45,15 +45,15 @@ export class FormattableInputComponent implements ControlValueAccessor, AfterVie
public disabled = false; public disabled = false;
public get valueAccessor(): ControlValueAccessor {
return this.codeEditor || this.inputEditor;
}
public modes = MODES; public modes = MODES;
public mode: TemplateMode = 'Text'; public mode: TemplateMode = 'Text';
public aceMode = 'ace/editor/text'; public aceMode = 'ace/editor/text';
public get valueAccessor(): ControlValueAccessor {
return this.codeEditor || this.inputEditor;
}
public ngAfterViewInit() { public ngAfterViewInit() {
this.valueAccessor.registerOnChange((value: any) => { this.valueAccessor.registerOnChange((value: any) => {
this.value = value; this.value = value;

43
frontend/src/app/features/rules/shared/pipes.ts

@ -0,0 +1,43 @@
/*
* Squidex Headless CMS
*
* @license
* Copyright (c) Squidex UG (haftungsbeschränkt). All rights reserved.
*/
import { Pipe, PipeTransform } from '@angular/core';
import { SimulatedRuleEventDto } from '@app/shared';
@Pipe({
name: 'sqxRuleClass',
pure: true,
})
export class RuleClassPipe implements PipeTransform {
public transform(value: string) {
if (value === 'Retry' || value === 'Skipped') {
return 'warning';
} else if (value === 'Failed' || value === 'Cancelled') {
return 'danger';
} else if (value === 'Pending') {
return 'secondary';
} else {
return value.toLowerCase();
}
}
}
@Pipe({
name: 'sqxSimulatedRuleEventStatus',
pure: true,
})
export class SimulatedRuleEventStatusPipe implements PipeTransform {
public transform(value: SimulatedRuleEventDto) {
if (value.error) {
return 'Failed';
} else if (value.skipReasons.length > 0) {
return 'Skipped';
} else {
return 'Success';
}
}
}

8
frontend/src/app/features/schemas/pages/schema/fields/field-wizard.component.ts

@ -31,10 +31,6 @@ export class FieldWizardComponent implements OnInit {
@Output() @Output()
public complete = new EventEmitter(); public complete = new EventEmitter();
public get isLocalizable() {
return (this.parent && this.parent.isLocalizable) || this.field['isLocalizable'];
}
public fieldTypes = fieldTypes; public fieldTypes = fieldTypes;
public field!: FieldDto; public field!: FieldDto;
@ -42,6 +38,10 @@ export class FieldWizardComponent implements OnInit {
public editForm?: EditFieldForm; public editForm?: EditFieldForm;
public get isLocalizable() {
return (this.parent && this.parent.isLocalizable) || this.field['isLocalizable'];
}
constructor( constructor(
private readonly schemasState: SchemasState, private readonly schemasState: SchemasState,
public readonly languagesState: LanguagesState, public readonly languagesState: LanguagesState,

6
frontend/src/app/features/schemas/pages/schema/fields/field.component.html

@ -22,15 +22,15 @@
</div> </div>
<div class="col col-tags flex-nowrap"> <div class="col col-tags flex-nowrap">
<div class="float-end"> <div class="float-end">
<span class="ms-1 badge rounded-pill bg-danger" *ngIf="field.isLocked"> <span class="ms-1 badge rounded-pill badge-danger" *ngIf="field.isLocked">
{{ 'schemas.field.lockedMarker' | sqxTranslate }} {{ 'schemas.field.lockedMarker' | sqxTranslate }}
</span> </span>
<span class="ms-1 badge rounded-pill bg-success" *ngIf="!field.isDisabled"> <span class="ms-1 badge rounded-pill badge-success" *ngIf="!field.isDisabled">
{{ 'schemas.field.enabledMarker' | sqxTranslate }} {{ 'schemas.field.enabledMarker' | sqxTranslate }}
</span> </span>
<span class="ms-1 badge rounded-pill bg-danger" *ngIf="field.isDisabled"> <span class="ms-1 badge rounded-pill badge-danger" *ngIf="field.isDisabled">
{{ 'schemas.field.disabledMarker' | sqxTranslate }} {{ 'schemas.field.disabledMarker' | sqxTranslate }}
</span> </span>
</div> </div>

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

@ -30,10 +30,6 @@ export class FieldComponent implements OnChanges {
@Input() @Input()
public settings!: AppSettingsDto; public settings!: AppSettingsDto;
public get isLocalizable() {
return (this.parent && this.parent.isLocalizable) || this.field['isLocalizable'];
}
public dropdown = new ModalModel(); public dropdown = new ModalModel();
public trackByFieldFn: (_index: number, field: NestedFieldDto) => any; public trackByFieldFn: (_index: number, field: NestedFieldDto) => any;
@ -45,6 +41,10 @@ export class FieldComponent implements OnChanges {
public addFieldDialog = new DialogModel(); public addFieldDialog = new DialogModel();
public get isLocalizable() {
return (this.parent && this.parent.isLocalizable) || this.field['isLocalizable'];
}
constructor( constructor(
private readonly schemasState: SchemasState, private readonly schemasState: SchemasState,
) { ) {

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

@ -5,7 +5,7 @@
* Copyright (c) Squidex UG (haftungsbeschränkt). All rights reserved. * Copyright (c) Squidex UG (haftungsbeschränkt). All rights reserved.
*/ */
import { ChangeDetectionStrategy, Component, Input } from '@angular/core'; import { ChangeDetectionStrategy, Component, Input, OnChanges, SimpleChanges } from '@angular/core';
import { ApiUrlConfig, BackupDto, BackupsState, Duration } from '@app/shared'; import { ApiUrlConfig, BackupDto, BackupsState, Duration } from '@app/shared';
@Component({ @Component({
@ -14,19 +14,24 @@ import { ApiUrlConfig, BackupDto, BackupsState, Duration } from '@app/shared';
templateUrl: './backup.component.html', templateUrl: './backup.component.html',
changeDetection: ChangeDetectionStrategy.OnPush, changeDetection: ChangeDetectionStrategy.OnPush,
}) })
export class BackupComponent { export class BackupComponent implements OnChanges {
@Input() @Input()
public backup!: BackupDto; public backup!: BackupDto;
public get duration() { public duration = '';
return Duration.create(this.backup.started, this.backup.stopped!).toString();
}
constructor( constructor(
public readonly apiUrl: ApiUrlConfig, private readonly backupsState: BackupsState, public readonly apiUrl: ApiUrlConfig,
private readonly backupsState: BackupsState,
) { ) {
} }
public ngOnChanges(changes: SimpleChanges) {
if (changes['backup']) {
this.duration = Duration.create(this.backup.started, this.backup.stopped!).toString();
}
}
public delete() { public delete() {
this.backupsState.delete(this.backup); this.backupsState.delete(this.backup);
} }

4
frontend/src/app/features/settings/pages/roles/role.component.html

@ -6,12 +6,12 @@
<div class="col text-decent"> <div class="col text-decent">
{{ 'common.clients' | sqxTranslate }} {{ 'common.clients' | sqxTranslate }}
<span class="badge bg-secondary rounded-pill" [class.text-muted]="role.numClients <= 0">{{role.numClients}}</span> <span class="badge badge-secondary rounded-pill" [class.text-muted]="role.numClients <= 0">{{role.numClients}}</span>
</div> </div>
<div class="col text-decent"> <div class="col text-decent">
{{ 'common.users' | sqxTranslate }} {{ 'common.users' | sqxTranslate }}
<span class="badge bg-secondary rounded-pill" [class.text-muted]="role.numContributors <= 0">{{role.numContributors}}</span> <span class="badge badge-secondary rounded-pill" [class.text-muted]="role.numContributors <= 0">{{role.numContributors}}</span>
</div> </div>
<div class="col-auto"> <div class="col-auto">
<div class="float-end"> <div class="float-end">

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

@ -50,10 +50,6 @@ export class RoleComponent implements OnChanges {
@ViewChild('addInput', { static: false }) @ViewChild('addInput', { static: false })
public addPermissionInput!: AutocompleteComponent; public addPermissionInput!: AutocompleteComponent;
public get halfSchemas() {
return Math.ceil(this.schemas.length / 2);
}
public descriptions = DESCRIPTIONS; public descriptions = DESCRIPTIONS;
public propertiesList = Settings.AppProperties; public propertiesList = Settings.AppProperties;
@ -67,6 +63,10 @@ export class RoleComponent implements OnChanges {
public editForm = new EditRoleForm(); public editForm = new EditRoleForm();
public get halfSchemas() {
return Math.ceil(this.schemas.length / 2);
}
constructor( constructor(
private readonly rolesState: RolesState, private readonly rolesState: RolesState,
) { ) {

30
frontend/src/app/framework/angular/if-once.directive.ts

@ -0,0 +1,30 @@
/*
* Squidex Headless CMS
*
* @license
* Copyright (c) Squidex UG (haftungsbeschränkt). All rights reserved.
*/
import { Directive, Input, TemplateRef, ViewContainerRef } from '@angular/core';
@Directive({
selector: '[sqxIfOnce]',
})
export class IfOnceDirective {
private hasView = false;
@Input('sqxIfOnce')
public set condition(value: boolean) {
if (value && !this.hasView) {
this.viewContainer.createEmbeddedView(this.templateRef);
this.hasView = true;
}
}
constructor(
private templateRef: TemplateRef<any>,
private viewContainer: ViewContainerRef,
) {
}
}

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

@ -68,6 +68,8 @@ export class LayoutComponent implements OnInit, OnDestroy, AfterViewInit {
@ViewChild('panel', { static: false }) @ViewChild('panel', { static: false })
public panel!: ElementRef<HTMLElement>; public panel!: ElementRef<HTMLElement>;
public isCollapsed = false;
public get desiredWidth() { public get desiredWidth() {
return this.isCollapsed ? 3 : this.width; return this.isCollapsed ? 3 : this.width;
} }
@ -84,8 +86,6 @@ export class LayoutComponent implements OnInit, OnDestroy, AfterViewInit {
return this.widthToRender; return this.widthToRender;
} }
public isCollapsed = false;
public firstChild = public firstChild =
this.router.events.pipe( this.router.events.pipe(
filter(event => event instanceof NavigationEnd), filter(event => event instanceof NavigationEnd),

18
frontend/src/app/framework/angular/pipes/strings.pipes.ts

@ -0,0 +1,18 @@
/*
* Squidex Headless CMS
*
* @license
* Copyright (c) Squidex UG (haftungsbeschränkt). All rights reserved.
*/
import { Pipe, PipeTransform } from '@angular/core';
@Pipe({
name: 'sqxJoin',
pure: true,
})
export class JoinPipe implements PipeTransform {
public transform(value: ReadonlyArray<string>) {
return value?.join(', ') || '';
}
}

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

@ -45,6 +45,7 @@ export * from './angular/http/http-extensions';
export * from './angular/http/loading.interceptor'; export * from './angular/http/loading.interceptor';
export * from './angular/image-source.directive'; export * from './angular/image-source.directive';
export * from './angular/image-url.directive'; export * from './angular/image-url.directive';
export * from './angular/if-once.directive';
export * from './angular/language-selector.component'; export * from './angular/language-selector.component';
export * from './angular/layout-container.directive'; export * from './angular/layout-container.directive';
export * from './angular/layout.component'; export * from './angular/layout.component';
@ -65,6 +66,7 @@ export * from './angular/pipes/keys.pipe';
export * from './angular/pipes/markdown.pipe'; export * from './angular/pipes/markdown.pipe';
export * from './angular/pipes/name.pipe'; export * from './angular/pipes/name.pipe';
export * from './angular/pipes/numbers.pipes'; export * from './angular/pipes/numbers.pipes';
export * from './angular/pipes/strings.pipes';
export * from './angular/pipes/translate.pipe'; export * from './angular/pipes/translate.pipe';
export * from './angular/resized.directive'; export * from './angular/resized.directive';
export * from './angular/routers/can-deactivate.guard'; export * from './angular/routers/can-deactivate.guard';

6
frontend/src/app/framework/module.ts

@ -11,7 +11,7 @@ import { ModuleWithProviders, NgModule } from '@angular/core';
import { FormsModule, ReactiveFormsModule } from '@angular/forms'; import { FormsModule, ReactiveFormsModule } from '@angular/forms';
import { RouterModule } from '@angular/router'; import { RouterModule } from '@angular/router';
import { ColorPickerModule } from 'ngx-color-picker'; import { ColorPickerModule } from 'ngx-color-picker';
import { AnalyticsService, AutocompleteComponent, AvatarComponent, CachingInterceptor, CanDeactivateGuard, CheckboxGroupComponent, ClipboardService, CodeComponent, CodeEditorComponent, ColorPickerComponent, CompensateScrollbarDirective, ConfirmClickDirective, ControlErrorsComponent, ControlErrorsMessagesComponent, CopyDirective, DarkenPipe, DatePipe, DateTimeEditorComponent, DayOfWeekPipe, DayPipe, DialogRendererComponent, DialogService, DisplayNamePipe, DropdownComponent, DropdownMenuComponent, DurationPipe, EditableTitleComponent, ExternalLinkDirective, FileDropDirective, FileSizePipe, FocusOnInitDirective, FormAlertComponent, FormErrorComponent, FormHintComponent, FromNowPipe, FullDateTimePipe, HighlightPipe, HoverBackgroundDirective, ImageSourceDirective, ImageUrlDirective, IndeterminateValueDirective, ISODatePipe, KeysPipe, KNumberPipe, LanguageSelectorComponent, LayoutComponent, LayoutContainerDirective, LightenPipe, ListViewComponent, LoadingInterceptor, LoadingService, LocalizedInputComponent, LocalStoreService, MarkdownDirective, MarkdownInlinePipe, MarkdownPipe, MessageBus, ModalDialogComponent, ModalDirective, ModalPlacementDirective, MonthPipe, OnboardingService, OnboardingTooltipComponent, PagerComponent, ParentLinkDirective, ProgressBarComponent, ResizedDirective, ResizeService, ResourceLoaderService, RootViewComponent, SafeHtmlPipe, SafeResourceUrlPipe, SafeUrlPipe, ScrollActiveDirective, ShortcutComponent, ShortcutDirective, ShortcutService, ShortDatePipe, ShortTimePipe, StarsComponent, StatusIconComponent, StopClickDirective, StopDragDirective, SyncScollingDirective, SyncWidthDirective, TabRouterlinkDirective, TagEditorComponent, TemplateWrapperDirective, TempService, TitleComponent, TitleService, ToggleComponent, ToolbarComponent, TooltipDirective, TransformInputDirective, TranslatePipe, VideoPlayerComponent } from './declarations'; import { AnalyticsService, AutocompleteComponent, AvatarComponent, CachingInterceptor, CanDeactivateGuard, CheckboxGroupComponent, ClipboardService, CodeComponent, CodeEditorComponent, ColorPickerComponent, CompensateScrollbarDirective, ConfirmClickDirective, ControlErrorsComponent, ControlErrorsMessagesComponent, CopyDirective, DarkenPipe, DatePipe, DateTimeEditorComponent, DayOfWeekPipe, DayPipe, DialogRendererComponent, DialogService, DisplayNamePipe, DropdownComponent, DropdownMenuComponent, DurationPipe, EditableTitleComponent, ExternalLinkDirective, FileDropDirective, FileSizePipe, FocusOnInitDirective, FormAlertComponent, FormErrorComponent, FormHintComponent, FromNowPipe, FullDateTimePipe, HighlightPipe, HoverBackgroundDirective, IfOnceDirective, ImageSourceDirective, ImageUrlDirective, IndeterminateValueDirective, ISODatePipe, JoinPipe, KeysPipe, KNumberPipe, LanguageSelectorComponent, LayoutComponent, LayoutContainerDirective, LightenPipe, ListViewComponent, LoadingInterceptor, LoadingService, LocalizedInputComponent, LocalStoreService, MarkdownDirective, MarkdownInlinePipe, MarkdownPipe, MessageBus, ModalDialogComponent, ModalDirective, ModalPlacementDirective, MonthPipe, OnboardingService, OnboardingTooltipComponent, PagerComponent, ParentLinkDirective, ProgressBarComponent, ResizedDirective, ResizeService, ResourceLoaderService, RootViewComponent, SafeHtmlPipe, SafeResourceUrlPipe, SafeUrlPipe, ScrollActiveDirective, ShortcutComponent, ShortcutDirective, ShortcutService, ShortDatePipe, ShortTimePipe, StarsComponent, StatusIconComponent, StopClickDirective, StopDragDirective, SyncScollingDirective, SyncWidthDirective, TabRouterlinkDirective, TagEditorComponent, TemplateWrapperDirective, TempService, TitleComponent, TitleService, ToggleComponent, ToolbarComponent, TooltipDirective, TransformInputDirective, TranslatePipe, VideoPlayerComponent } from './declarations';
@NgModule({ @NgModule({
imports: [ imports: [
@ -55,10 +55,12 @@ import { AnalyticsService, AutocompleteComponent, AvatarComponent, CachingInterc
FullDateTimePipe, FullDateTimePipe,
HighlightPipe, HighlightPipe,
HoverBackgroundDirective, HoverBackgroundDirective,
IfOnceDirective,
ImageSourceDirective, ImageSourceDirective,
ImageUrlDirective, ImageUrlDirective,
IndeterminateValueDirective, IndeterminateValueDirective,
ISODatePipe, ISODatePipe,
JoinPipe,
KeysPipe, KeysPipe,
KNumberPipe, KNumberPipe,
LanguageSelectorComponent, LanguageSelectorComponent,
@ -140,10 +142,12 @@ import { AnalyticsService, AutocompleteComponent, AvatarComponent, CachingInterc
FullDateTimePipe, FullDateTimePipe,
HighlightPipe, HighlightPipe,
HoverBackgroundDirective, HoverBackgroundDirective,
IfOnceDirective,
ImageSourceDirective, ImageSourceDirective,
ImageUrlDirective, ImageUrlDirective,
IndeterminateValueDirective, IndeterminateValueDirective,
ISODatePipe, ISODatePipe,
JoinPipe,
KeysPipe, KeysPipe,
KNumberPipe, KNumberPipe,
LanguageSelectorComponent, LanguageSelectorComponent,

3
frontend/src/app/shared/components/assets/asset-dialog.component.ts

@ -44,14 +44,13 @@ export class AssetDialogComponent implements OnChanges {
public path!: Observable<ReadonlyArray<AssetPathItem>>; public path!: Observable<ReadonlyArray<AssetPathItem>>;
public selectedTab = 0;
public isEditable = false; public isEditable = false;
public isEditableAny = false; public isEditableAny = false;
public isUploadable = false; public isUploadable = false;
public progress = 0; public progress = 0;
public selectedTab = 0;
public annotateForm = new AnnotateAssetForm(); public annotateForm = new AnnotateAssetForm();
public get isImage() { public get isImage() {

2
frontend/src/app/shared/components/assets/asset-folder-dropdown-item.component.html

@ -1,4 +1,4 @@
<div class="control-dropdown-item d-flex align-items-center" [class.active]="node.isSelected" [style]="style" (click)="selectNode.emit(node)"> <div class="control-dropdown-item d-flex align-items-center" [class.active]="node.isSelected" [style.paddingLeft]="paddingLeft" (click)="selectNode.emit(node)">
<ng-container *ngIf="node.isLoading; else notLoading" class="loader"> <ng-container *ngIf="node.isLoading; else notLoading" class="loader">
<button type="button" class="btn btn-sm btn-decent btn-text-secondary"> <button type="button" class="btn btn-sm btn-decent btn-text-secondary">

4
frontend/src/app/shared/components/assets/asset-folder-dropdown-item.component.ts

@ -27,8 +27,8 @@ export class AssetFolderDropdownItemComponent {
@Output() @Output()
public selectNode = new EventEmitter<AssetFolderDropdowNode>(); public selectNode = new EventEmitter<AssetFolderDropdowNode>();
public get style() { public get paddingLeft() {
return { paddingLeft: `${this.nodeLevel}rem` }; return `${this.nodeLevel}rem`;
} }
constructor( constructor(

3
frontend/src/app/shared/components/contents/content-list-cell.directive.ts

@ -7,8 +7,7 @@
import { Directive, ElementRef, Input, OnChanges, OnDestroy, OnInit, Pipe, PipeTransform, Renderer2 } from '@angular/core'; import { Directive, ElementRef, Input, OnChanges, OnDestroy, OnInit, Pipe, PipeTransform, Renderer2 } from '@angular/core';
import { ResourceOwner } from '@app/framework'; import { ResourceOwner } from '@app/framework';
import { RootFieldDto, TableSettings } from '@app/shared'; import { ContentDto, MetaFields, TableField, FieldSizes, Types, RootFieldDto, TableSettings } from '@app/shared/internal';
import { ContentDto, MetaFields, TableField, FieldSizes, Types } from '@app/shared/internal';
export function getCellWidth(field: TableField, sizes: FieldSizes | undefined | null) { export function getCellWidth(field: TableField, sizes: FieldSizes | undefined | null) {
if (Types.is(field, RootFieldDto)) { if (Types.is(field, RootFieldDto)) {

24
frontend/src/app/shared/components/contents/content-list-field.component.ts

@ -39,6 +39,18 @@ export class ContentListFieldComponent extends StatefulComponent<State> implemen
@Input() @Input()
public language!: LanguageDto; public language!: LanguageDto;
public get metaFields() {
return MetaFields;
}
public get isInlineEditable() {
return Types.is(this.field, RootFieldDto) ? this.field.isInlineEditable : false;
}
public get fieldName() {
return Types.is(this.field, RootFieldDto) ? this.field.name : this.field;
}
constructor(changeDetector: ChangeDetectorRef) { constructor(changeDetector: ChangeDetectorRef) {
super(changeDetector, { super(changeDetector, {
formatted: '', formatted: '',
@ -64,16 +76,4 @@ export class ContentListFieldComponent extends StatefulComponent<State> implemen
this.next({ formatted }); this.next({ formatted });
} }
} }
public get metaFields() {
return MetaFields;
}
public get isInlineEditable() {
return Types.is(this.field, RootFieldDto) ? this.field.isInlineEditable : false;
}
public get fieldName() {
return Types.is(this.field, RootFieldDto) ? this.field.name : this.field;
}
} }

24
frontend/src/app/shared/components/contents/content-status.component.ts

@ -5,8 +5,9 @@
* Copyright (c) Squidex UG (haftungsbeschränkt). All rights reserved. * Copyright (c) Squidex UG (haftungsbeschränkt). All rights reserved.
*/ */
import { ChangeDetectionStrategy, Component, Input } from '@angular/core'; import { ChangeDetectionStrategy, Component, Input, OnChanges, SimpleChanges } from '@angular/core';
import { ScheduleDto } from '@app/shared'; import { LocalizerService } from '@app/framework';
import { ScheduleDto } from '@app/shared/internal';
@Component({ @Component({
selector: 'sqx-content-status[status][statusColor]', selector: 'sqx-content-status[status][statusColor]',
@ -14,7 +15,7 @@ import { ScheduleDto } from '@app/shared';
templateUrl: './content-status.component.html', templateUrl: './content-status.component.html',
changeDetection: ChangeDetectionStrategy.OnPush, changeDetection: ChangeDetectionStrategy.OnPush,
}) })
export class ContentStatusComponent { export class ContentStatusComponent implements OnChanges {
@Input() @Input()
public status!: string; public status!: string;
@ -33,6 +34,8 @@ export class ContentStatusComponent {
@Input() @Input()
public small?: boolean | null; public small?: boolean | null;
public tooltipText = '';
public get isMultiline() { public get isMultiline() {
return this.layout === 'multiline'; return this.layout === 'multiline';
} }
@ -41,11 +44,20 @@ export class ContentStatusComponent {
return this.layout === 'text'; return this.layout === 'text';
} }
public get tooltipText() { constructor(
private readonly localizer: LocalizerService,
) {
}
public ngOnChanges(changes: SimpleChanges) {
if (changes['scheduled']) {
if (this.scheduled) { if (this.scheduled) {
return `Will be set to '${this.scheduled.status}' at ${this.scheduled.dueTime.toStringFormat('PPpp')}`; const args = { status: this.scheduled.status, time: this.scheduled.dueTime.toStringFormat('PPpp') };
this.tooltipText = this.localizer.getOrKey('contents.scheduledTooltip', args);
} else { } else {
return this.status; this.tooltipText = this.status;
}
} }
} }
} }

3
frontend/src/app/shared/components/contents/content-value.component.ts

@ -7,8 +7,7 @@
import { ChangeDetectionStrategy, ChangeDetectorRef, Component, Input, OnChanges, SimpleChanges } from '@angular/core'; import { ChangeDetectionStrategy, ChangeDetectorRef, Component, Input, OnChanges, SimpleChanges } from '@angular/core';
import { ResourceOwner } from '@app/framework'; import { ResourceOwner } from '@app/framework';
import { RootFieldDto, TableField } from '@app/shared'; import { FieldWrappings, HtmlValue, RootFieldDto, TableField, TableSettings, Types } from '@app/shared/internal';
import { FieldWrappings, HtmlValue, TableSettings, Types } from '@app/shared/internal';
@Component({ @Component({
selector: 'sqx-content-value[value]', selector: 'sqx-content-value[value]',

2
frontend/src/app/shared/components/schema-category.component.html

@ -19,7 +19,7 @@
</div> </div>
<div class="col-auto"> <div class="col-auto">
<ng-container *ngIf="schemaCategory.countSchemasInSubtree > 0; else noSchemas"> <ng-container *ngIf="schemaCategory.countSchemasInSubtree > 0; else noSchemas">
<span class="badge rounded-pill bg-secondary">{{schemaCategory.countSchemasInSubtreeFiltered}}</span> <span class="badge rounded-pill badge-secondary">{{schemaCategory.countSchemasInSubtreeFiltered}}</span>
</ng-container> </ng-container>
<ng-template #noSchemas> <ng-template #noSchemas>
<button type="button" class="btn btn-sm btn-text-secondary btn-remove" (click)="remove.emit(schemaCategory.name)" *ngIf="schemaCategory.name"> <button type="button" class="btn btn-sm btn-text-secondary btn-remove" (click)="remove.emit(schemaCategory.name)" *ngIf="schemaCategory.name">

2
frontend/src/app/shared/components/search/queries/filter-logical.component.html

@ -30,7 +30,7 @@
[filter]="filter" [filter]="filter"
[language]="language" [language]="language"
[languages]="languages" [languages]="languages"
[level]="nestedLevel" [level]="level + 1"
[model]="model" [model]="model"
[statuses]="statuses" [statuses]="statuses"
(remove)="removeFilter(i)"> (remove)="removeFilter(i)">

8
frontend/src/app/shared/components/search/queries/filter-logical.component.ts

@ -48,6 +48,8 @@ export class FilterLogicalComponent {
this.updateFilters(this.filterValue); this.updateFilters(this.filterValue);
} }
public filters: FilterNode[] = [];
public get filter() { public get filter() {
return this.filterValue; return this.filterValue;
} }
@ -60,12 +62,6 @@ export class FilterLogicalComponent {
return !!this.filterValue.or; return !!this.filterValue.or;
} }
public get nestedLevel() {
return this.level + 1;
}
public filters: FilterNode[] = [];
public addComparison() { public addComparison() {
this.filters.push(<any>{ path: this.model.schema.fields[0].path }); this.filters.push(<any>{ path: this.model.schema.fields[0].path });

4
frontend/src/app/shared/components/search/queries/query-path.component.html

@ -3,13 +3,13 @@
[disabled]="model.schema.fields.length === 0" [disabled]="model.schema.fields.length === 0"
[items]="model.schema.fields" [items]="model.schema.fields"
[itemSeparator]="true" [itemSeparator]="true"
[ngModel]="value" [ngModel]="field"
(ngModelChange)="pathChange.emit($event?.path)" (ngModelChange)="pathChange.emit($event?.path)"
[dropdownStyles]="{ maxWidth: '40rem' }"> [dropdownStyles]="{ maxWidth: '40rem' }">
<ng-template let-field="$implicit"> <ng-template let-field="$implicit">
<div class="row"> <div class="row">
<div class="col-auto"> <div class="col-auto">
<div class="badge rounded-pill bg-primary">{{field.path}}</div> <div class="badge rounded-pill badge-primary">{{field.path}}</div>
</div> </div>
<div class="col text-end"> <div class="col text-end">
<sqx-form-hint>{{field.description}}</sqx-form-hint> <sqx-form-hint>{{field.description}}</sqx-form-hint>

11
frontend/src/app/shared/components/search/queries/query-path.component.ts

@ -5,7 +5,8 @@
* Copyright (c) Squidex UG (haftungsbeschränkt). All rights reserved. * Copyright (c) Squidex UG (haftungsbeschränkt). All rights reserved.
*/ */
import { ChangeDetectionStrategy, Component, EventEmitter, Input, Output } from '@angular/core'; import { ChangeDetectionStrategy, Component, EventEmitter, Input, OnChanges, Output } from '@angular/core';
import { FilterableField } from '@app/shared';
import { QueryModel } from '@app/shared/internal'; import { QueryModel } from '@app/shared/internal';
@Component({ @Component({
@ -14,7 +15,7 @@ import { QueryModel } from '@app/shared/internal';
templateUrl: './query-path.component.html', templateUrl: './query-path.component.html',
changeDetection: ChangeDetectionStrategy.OnPush, changeDetection: ChangeDetectionStrategy.OnPush,
}) })
export class QueryPathComponent { export class QueryPathComponent implements OnChanges {
@Output() @Output()
public pathChange = new EventEmitter<string>(); public pathChange = new EventEmitter<string>();
@ -24,7 +25,9 @@ export class QueryPathComponent {
@Input() @Input()
public model!: QueryModel; public model!: QueryModel;
public get value() { public field?: FilterableField;
return this.model.schema.fields.find(x => x.path === this.path);
public ngOnChanges() {
this.field = this.model.schema.fields.find(x => x.path === this.path);
} }
} }

17
frontend/src/app/shared/state/contents.forms-helpers.ts

@ -121,6 +121,7 @@ export interface FormGlobals {
export abstract class AbstractContentForm<T extends FieldDto, TForm extends AbstractControl> extends Hidden { export abstract class AbstractContentForm<T extends FieldDto, TForm extends AbstractControl> extends Hidden {
private readonly disabled$ = new BehaviorSubject<boolean>(false); private readonly disabled$ = new BehaviorSubject<boolean>(false);
private readonly collapsed$ = new BehaviorSubject<boolean | null>(null);
private readonly ruleSet: CompiledRules; private readonly ruleSet: CompiledRules;
public get disabled() { public get disabled() {
@ -131,6 +132,14 @@ export abstract class AbstractContentForm<T extends FieldDto, TForm extends Abst
return this.disabled$; return this.disabled$;
} }
public get collapsed() {
return this.collapsed$.value;
}
public get collapsedChanges(): Observable<boolean | null> {
return this.collapsed$;
}
protected constructor( protected constructor(
public readonly globals: FormGlobals, public readonly globals: FormGlobals,
public readonly field: T, public readonly field: T,
@ -148,6 +157,14 @@ export abstract class AbstractContentForm<T extends FieldDto, TForm extends Abst
return `${this.fieldPath}.${relative}`; return `${this.fieldPath}.${relative}`;
} }
public collapse() {
this.collapsed$.next(true);
}
public expand() {
this.collapsed$.next(false);
}
public updateState(context: RuleContext, itemData: any, parentState: AbstractContentFormState) { public updateState(context: RuleContext, itemData: any, parentState: AbstractContentFormState) {
const state = { const state = {
isDisabled: this.field.isDisabled || parentState.isDisabled === true, isDisabled: this.field.isDisabled || parentState.isDisabled === true,

2
frontend/src/app/shell/pages/internal/apps-menu.component.html

@ -19,7 +19,7 @@
<h3>{{ 'apps.allApps' | sqxTranslate }}</h3> <h3>{{ 'apps.allApps' | sqxTranslate }}</h3>
</div> </div>
<div class="col-auto"> <div class="col-auto">
<span class="badge bg-primary rounded-pill">{{apps.length}}</span> <span class="badge badge-primary rounded-pill">{{apps.length}}</span>
</div> </div>
</div> </div>
</a> </a>

2
frontend/src/app/shell/pages/internal/notifications-menu.component.html

@ -6,7 +6,7 @@
<span class="nav-link dropdown-toggle" (click)="modalMenu.show()"> <span class="nav-link dropdown-toggle" (click)="modalMenu.show()">
<i class="icon-comments"></i> <i class="icon-comments"></i>
<span class="badge rounded-pill bg-danger" *ngIf="unread">{{unread}}</span> <span class="badge rounded-pill badge-danger" *ngIf="unread">{{unread}}</span>
</span> </span>
</li> </li>

9
frontend/src/app/shell/pages/internal/notifications-menu.component.ts

@ -25,13 +25,10 @@ export class NotificationsMenuComponent extends ResourceOwner implements OnInit
public versionRead = -1; public versionRead = -1;
public versionReceived = -1; public versionReceived = -1;
public unread = 0;
public userToken = ''; public userToken = '';
public get unread() {
return Math.max(0, this.versionReceived - this.versionRead);
}
public isNotifoConfigured = false; public isNotifoConfigured = false;
constructor(authService: AuthService, commentsService: CommentsService, dialogs: DialogService, uiOptions: UIOptions, constructor(authService: AuthService, commentsService: CommentsService, dialogs: DialogService, uiOptions: UIOptions,
@ -50,6 +47,8 @@ export class NotificationsMenuComponent extends ResourceOwner implements OnInit
this.versionRead = localStore.getInt(CONFIG_KEY, -1); this.versionRead = localStore.getInt(CONFIG_KEY, -1);
this.versionReceived = this.versionRead; this.versionReceived = this.versionRead;
this.updateVersion();
const commentsUrl = `users/${authService.user!.id}/notifications`; const commentsUrl = `users/${authService.user!.id}/notifications`;
this.commentsState = this.commentsState =
@ -87,6 +86,8 @@ export class NotificationsMenuComponent extends ResourceOwner implements OnInit
} }
private updateVersion() { private updateVersion() {
this.unread = Math.max(0, this.versionReceived - this.versionRead);
if (this.modalMenu.isOpen) { if (this.modalMenu.isOpen) {
this.versionRead = this.versionReceived; this.versionRead = this.versionReceived;

2
frontend/src/app/shell/pages/internal/search-menu.component.html

@ -20,7 +20,7 @@
{{result.name}} {{result.name}}
</div> </div>
<div class="col-auto text-end" *ngIf="result.label"> <div class="col-auto text-end" *ngIf="result.label">
<div class="badge rounded-pill bg-primary ms-2">{{result.label}}</div> <div class="badge rounded-pill badge-primary ms-2">{{result.label}}</div>
</div> </div>
</div> </div>
</ng-template> </ng-template>

Loading…
Cancel
Save