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.scheduledTo": "to",
"contents.scheduledToLabel": "Scheduled to",
"contents.scheduledTooltip": "Will be set to '{status}' at {time}.",
"contents.schemasPageTitle": "Contents",
"contents.searchPlaceholder": "Fulltext search",
"contents.searchSchemasPlaceholder": "Search",

1
backend/i18n/frontend_it.json

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

1
backend/i18n/frontend_nl.json

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

1
backend/i18n/frontend_zh.json

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

1
backend/i18n/source/frontend_en.json

@ -483,6 +483,7 @@
"contents.scheduledBy": "by",
"contents.scheduledTo": "to",
"contents.scheduledToLabel": "Scheduled to",
"contents.scheduledTooltip": "Will be set to '{status}' at {time}.",
"contents.schemasPageTitle": "Contents",
"contents.searchPlaceholder": "Fulltext 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",
"ngx-color-picker": "11.0.0",
"ngx-doc-viewer": "2.0.5",
"ngx-virtual-scroller": "^4.0.3",
"oidc-client": "1.11.5",
"pikaday": "1.8.2",
"progressbar.js": "1.1.0",
@ -3573,6 +3574,11 @@
"integrity": "sha512-eZxlbI8GZscaGS7kkc/trHTT5xgrjH3/1n2JDwusC9iahPKWMRvRjJSAN5mCXviuTGQ/lHnhvv8Q1YTpnfz9gA==",
"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": {
"version": "0.0.108",
"resolved": "https://registry.npmjs.org/@types/codemirror/-/codemirror-0.0.108.tgz",
@ -3885,6 +3891,11 @@
"@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": {
"version": "3.13.1",
"resolved": "https://registry.npmjs.org/@types/uglify-js/-/uglify-js-3.13.1.tgz",
@ -11592,6 +11603,19 @@
"@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": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/nice-napi/-/nice-napi-1.0.2.tgz",
@ -20366,6 +20390,11 @@
"integrity": "sha512-eZxlbI8GZscaGS7kkc/trHTT5xgrjH3/1n2JDwusC9iahPKWMRvRjJSAN5mCXviuTGQ/lHnhvv8Q1YTpnfz9gA==",
"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": {
"version": "0.0.108",
"resolved": "https://registry.npmjs.org/@types/codemirror/-/codemirror-0.0.108.tgz",
@ -20658,6 +20687,11 @@
"@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": {
"version": "3.13.1",
"resolved": "https://registry.npmjs.org/@types/uglify-js/-/uglify-js-3.13.1.tgz",
@ -26509,6 +26543,15 @@
"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": {
"version": "1.0.2",
"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",
"ngx-color-picker": "11.0.0",
"ngx-doc-viewer": "2.0.5",
"ngx-virtual-scroller": "^4.0.3",
"oidc-client": "1.11.5",
"pikaday": "1.8.2",
"progressbar.js": "1.1.0",

30
frontend/src/app/_theme.html

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

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

@ -12,7 +12,7 @@
<span class="truncate">{{tag.name}}</span>
</div>
<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>
<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.
*/
import { ScrollingModule as ScrollingModuleExperimental } from '@angular/cdk-experimental/scrolling';
import { ScrollingModule } from '@angular/cdk/scrolling';
import { NgModule } from '@angular/core';
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 { 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({
imports: [
RouterModule.forChild(routes),
ScrollingModule,
ScrollingModuleExperimental,
SqxFrameworkModule,
SqxSharedModule,
VirtualScrollerModule,
],
declarations: [
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.
*/
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';
@Component({
@ -29,10 +29,12 @@ export class ContentEventComponent implements OnChanges {
public canLoadOrCompare = false;
public ngOnChanges() {
public ngOnChanges(changes: SimpleChanges) {
if (changes['event']) {
this.canLoadOrCompare =
(this.event.eventType === 'ContentUpdatedEvent' ||
this.event.eventType === 'ContentCreatedEventV2') &&
!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()
public languages!: ReadonlyArray<AppLanguageDto>;
public showAllControls = false;
public isDifferent?: Observable<boolean>;
public isInvalid?: Observable<boolean>;
@HostBinding('class')
public get class() {
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;
}
public showAllControls = false;
public isDifferent?: Observable<boolean>;
public isInvalid?: Observable<boolean>;
constructor(
private readonly appsState: AppsState,
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.
*/
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';
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) {
const isCollapsed = this.localStore.getBoolean(this.expandedKey());
this.next({ isCollapsed });
}
}
}
public toggle() {
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 { combineLatest } from 'rxjs';
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({
selector: 'sqx-schemas-page',
@ -43,27 +43,13 @@ export class SchemasPageComponent {
return getCategoryTree(schemas, categories, filter);
});
public isCollapsed = false;
public get width() {
return this.isCollapsed ? '4rem' : '16rem';
}
constructor(uiOptions: UIOptions,
public readonly schemasState: SchemasState,
private readonly appsState: AppsState,
private readonly localStore: LocalStoreService,
) {
this.isCollapsed = localStore.getBoolean(Settings.Local.SCHEMAS_COLLAPSED);
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) {
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">
<div class="array-container" *ngIf="items.length > 0">
<div class="cdk-container" *ngIf="items.length <= 20;"
<div class="array-container" *ngIf="items.length > 0 && items.length <= 20;"
cdkDropList
[cdkDropListDisabled]="false"
[cdkDropListData]="items"
(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
cdkDragLockAxis="y">
cdkDragLockAxis="y"
[class.first]="isFirst"
[class.last]="isLast">
<sqx-array-item
[canUnset]="canUnset"
(clone)="addCopy(itemForm)"
@ -29,8 +30,11 @@
</div>
</div>
<cdk-virtual-scroll-viewport autosize *ngIf="items.length > 20">
<div *cdkVirtualFor="let itemForm of items; index as i; last as isLast; first as isFirst" class="table-drag item">
<div class="array-container" *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
[canUnset]="canUnset"
(clone)="addCopy(itemForm)"
@ -38,19 +42,19 @@
[formContext]="formContext"
[formLevel]="formLevel + 1"
[formModel]="itemForm"
[index]="i"
[index]="scroll.viewPortInfo.startIndexWithBuffer + i"
[isCollapsedInitial]="isCollapsedInitial"
[isDisabled]="isDisabled | async"
[isFirst]="isFirst"
[isLast]="isLast"
(itemExpanded)="onExpanded()"
(itemRemove)="removeItem(i)"
[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)"
[language]="language"
[languages]="languages">
</sqx-array-item>
</div>
</cdk-virtual-scroll-viewport>
</virtual-scroller>
</div>
<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 'vars';
:host ::ng-deep {
.cdk-virtual-scroll-content-wrapper {
right: 0;
/* stylelint-disable no-descending-specificity */
virtual-scroller {
height: 700px;
}
.item {
padding: .25rem 1rem;
&.first {
padding-top: .75rem;
}
&.last {
padding-bottom: .75rem;
}
}
@ -11,17 +23,8 @@
background: $color-border-lighter;
margin: 0;
margin-bottom: 1rem;
padding: .5rem 0;
}
.drag-container {
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 { CdkVirtualScrollViewport } from '@angular/cdk/scrolling';
import { ChangeDetectionStrategy, Component, Input, OnChanges, OnInit, QueryList, SimpleChanges, ViewChildren } from '@angular/core';
import { ChangeDetectionStrategy, Component, Input, OnChanges, QueryList, SimpleChanges, ViewChildren } from '@angular/core';
import { VirtualScrollerComponent } from 'ngx-virtual-scroller';
import { combineLatest, Observable } from 'rxjs';
import { map } from 'rxjs/operators';
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',
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class ArrayEditorComponent implements OnChanges, OnInit {
export class ArrayEditorComponent implements OnChanges {
@Input()
public form!: EditContentForm;
@ -44,8 +44,8 @@ export class ArrayEditorComponent implements OnChanges, OnInit {
@ViewChildren(ArrayItemComponent)
public children!: QueryList<ArrayItemComponent>;
@ViewChildren(CdkVirtualScrollViewport)
public viewport?: QueryList<CdkVirtualScrollViewport>;
@ViewChildren(VirtualScrollerComponent)
public scroller?: QueryList<VirtualScrollerComponent>;
public isArray = false;
@ -66,10 +66,6 @@ export class ArrayEditorComponent implements OnChanges, OnInit {
) {
}
public ngOnInit() {
this.isCollapsedInitial = this.formLevel > 0;
}
public ngOnChanges(changes: SimpleChanges) {
if (changes['formModel']) {
const maxItems = this.formModel.field.properties['maxItems'] || Number.MAX_VALUE;
@ -88,10 +84,10 @@ export class ArrayEditorComponent implements OnChanges, OnInit {
]).pipe(map(([disabled, items]) => {
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();
}
public onExpanded() {
this.viewport?.first?.checkViewportSize();
}
public collapseAll() {
this.children.forEach(child => {
child.collapse();
});
for (const item of this.formModel.items) {
item.collapse();
}
if (this.formLevel === 0) {
this.localStore.setBoolean(this.expandedKey(), true);
}
this.onExpanded();
this.scroller?.first?.invalidateAllCachedMeasurements();
}
public expandAll() {
this.children.forEach(child => {
child.expand();
});
for (const item of this.formModel.items) {
item.expand();
}
if (this.formLevel === 0) {
this.localStore.setBoolean(this.expandedKey(), false);
}
this.onExpanded();
this.scroller?.first?.invalidateAllCachedMeasurements();
}
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">
<i class="icon-caret-bottom"></i>
</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>
</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>
</button>
</div>
@ -42,7 +42,7 @@
</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">
<sqx-component-section
[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.
*/
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 { 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';
interface State {
// True when the item is expanded.
isExpanded: boolean;
// True when the item is expanded at least once.
isExpandedOnce: boolean;
}
@Component({
selector: 'sqx-array-item[form][formContext][formLevel][formModel][index][language][languages]',
styleUrls: ['./array-item.component.scss'],
templateUrl: './array-item.component.html',
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class ArrayItemComponent extends StatefulComponent<State> implements OnChanges, OnInit {
export class ArrayItemComponent implements OnChanges {
@Output()
public itemRemove = new EventEmitter();
@ -77,24 +69,13 @@ export class ArrayItemComponent extends StatefulComponent<State> implements OnCh
@ViewChildren(ComponentSectionComponent)
public sections!: QueryList<ComponentSectionComponent>;
public isCollapsed = false;
public isInvalid?: Observable<boolean>;
public isInvalidComponent?: Observable<boolean>;
public title?: Observable<string>;
constructor(changeDetector: ChangeDetectorRef,
) {
super(changeDetector, {
isExpanded: false,
isExpandedOnce: false,
});
}
public ngOnInit() {
if (!this.isCollapsedInitial) {
this.expand();
}
public get isCollapsed() {
return this.formModel.collapsedChanges;
}
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));
}
if (changes['formModel'] || changes['isCollapsedInitial']) {
if (this.isCollapsedInitial && this.formModel.collapsed === null) {
this.collapse();
}
}
}
public collapse() {
this.next({ isExpanded: false });
this.formModel.collapse();
this.itemExpanded.emit();
}
public expand() {
this.next({ isExpanded: true, isExpandedOnce: true });
this.formModel.expand();
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 get isDirty() {
return this.patchForm && this.patchForm.form.dirty;
return this.patchForm?.form.dirty === true;
}
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);
}
public control = new FormControl('');
public get isValid() {
return !!this.schemaId && !!this.language;
}
public control = new FormControl('');
constructor(changeDetector: ChangeDetectorRef,
private readonly contentsResolver: ResolveContents,
private readonly localizer: LocalizerService,

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

@ -12,8 +12,8 @@
</td>
<td class="cell-valid" *ngIf="validityVisible">
<span class="badge rounded-pill bg-success" *ngIf="valid === true">VALID</span>
<span class="badge rounded-pill bg-danger" *ngIf="valid === false">INVALID</span>
<span class="badge rounded-pill badge-success" *ngIf="isValid === true">VALID</span>
<span class="badge rounded-pill badge-danger" *ngIf="isValid === false">INVALID</span>
</td>
<td sqxContentListCell="meta.status.color">
@ -22,7 +22,7 @@
<td class="cell-label" *ngIf="!isCompact">
<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>
</td>

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

@ -47,12 +47,12 @@ export class ReferenceItemComponent implements OnChanges {
@Input('sqxReferenceItem')
public content!: ContentDto;
public get valid() {
public values: ReadonlyArray<any> = [];
public get isValid() {
return !this.validations ? undefined : this.validations[this.content.id];
}
public values: ReadonlyArray<any> = [];
public ngOnChanges() {
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);
}
public control = new FormControl([]);
public get isValid() {
return !!this.schemaId && !!this.language;
}
public control = new FormControl([]);
constructor(changeDetector: ChangeDetectorRef, uiOptions: UIOptions,
private readonly appsState: AppsState,
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);
}
public control = new FormControl([]);
public get isValid() {
return !!this.schemaId && !!this.language;
}
public control = new FormControl([]);
constructor(changeDetector: ChangeDetectorRef,
private readonly contentsResolver: ResolveContents,
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()
public isStackedChange = new EventEmitter<boolean>();
public chartData: any;
public chartSummary = 0;
public get chartOptions() {
return this.isStacked ? ChartOptions.Stacked : ChartOptions.Default;
}
public chartData: any;
public chartSummary = 0;
public ngOnChanges() {
if (this.usage) {
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()
public isStackedChange = new EventEmitter<boolean>();
public chartData: any;
public chartSummary = 0;
public get chartOptions() {
return this.isStacked ? ChartOptions.Stacked : ChartOptions.Default;
}
public chartData: any;
public chartSummary = 0;
public ngOnChanges(changes: SimpleChanges) {
if (this.usage && changes['usage']) {
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/rule-element.component';
export * from './shared/rule-icon.component';
export * from './shared/pipes';
export * from './shared/triggers/asset-changed-trigger.component';
export * from './shared/triggers/comment-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 { RouterModule, Routes } from '@angular/router';
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 { RulePageComponent } from './pages/rule/rule-page.component';
import { SimulatedRuleEventComponent } from './pages/simulator/simulated-rule-event.component';
@ -71,6 +71,7 @@ const routes: Routes = [
ContentChangedTriggerComponent,
FormattableInputComponent,
GenericActionComponent,
RuleClassPipe,
RuleComponent,
RuleElementComponent,
RuleEventComponent,
@ -82,6 +83,7 @@ const routes: Routes = [
RulesPageComponent,
SchemaChangedTriggerComponent,
SimulatedRuleEventComponent,
SimulatedRuleEventStatusPipe,
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">
<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 class="cell-40">
<span class="truncate">{{event.eventName}}</span>
@ -25,7 +25,7 @@
<div class="row g-0 event-stats align-items-center">
<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 class="col-2">
{{ '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()
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">
{{ '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 class="col-3">
{{ '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 class="col">
{{ '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.
*/
import { ChangeDetectionStrategy, Component, Input } from '@angular/core';
import { ChangeDetectionStrategy, Component, Input, OnChanges, SimpleChanges } from '@angular/core';
import { SimulatedRuleEventDto } from '@app/shared';
@Component({
@ -14,7 +14,7 @@ import { SimulatedRuleEventDto } from '@app/shared';
templateUrl: './rule-transition.component.html',
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class RuleTransitionComponent {
export class RuleTransitionComponent implements OnChanges {
@Input()
public event: SimulatedRuleEventDto | undefined | null;
@ -24,19 +24,25 @@ export class RuleTransitionComponent {
@Input()
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;
if (!errors) {
return null;
this.filteredErrors = null;
return;
}
const result = this.event?.skipReasons.filter(x => errors.includes(x)).map(x => `rules.simulation.error${x}`);
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">
<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 class="cell-40">
<span class="truncate">{{event.eventName}}</span>
</td>
<td class="cell-40">
<small class="truncate">{{event.skipReasons.join(', ')}}</small>
<small class="truncate">{{event.skipReasons | sqxJoin}}</small>
</td>
<td class="cell-actions">
<button type="button" class="btn btn-outline-secondary btn-expand" [class.expanded]="expanded" (click)="expandedChange.emit()">
@ -53,7 +53,7 @@
<div class="history-state">
<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>
<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 errorsAfterEnrichedEvent = ERRORS_AFTER_ENRICHED_EVENT;
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 get valueAccessor(): ControlValueAccessor {
return this.codeEditor || this.inputEditor;
}
public modes = MODES;
public mode: TemplateMode = 'Text';
public aceMode = 'ace/editor/text';
public get valueAccessor(): ControlValueAccessor {
return this.codeEditor || this.inputEditor;
}
public ngAfterViewInit() {
this.valueAccessor.registerOnChange((value: any) => {
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()
public complete = new EventEmitter();
public get isLocalizable() {
return (this.parent && this.parent.isLocalizable) || this.field['isLocalizable'];
}
public fieldTypes = fieldTypes;
public field!: FieldDto;
@ -42,6 +38,10 @@ export class FieldWizardComponent implements OnInit {
public editForm?: EditFieldForm;
public get isLocalizable() {
return (this.parent && this.parent.isLocalizable) || this.field['isLocalizable'];
}
constructor(
private readonly schemasState: SchemasState,
public readonly languagesState: LanguagesState,

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

@ -22,15 +22,15 @@
</div>
<div class="col col-tags flex-nowrap">
<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 }}
</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 }}
</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 }}
</span>
</div>

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

@ -30,10 +30,6 @@ export class FieldComponent implements OnChanges {
@Input()
public settings!: AppSettingsDto;
public get isLocalizable() {
return (this.parent && this.parent.isLocalizable) || this.field['isLocalizable'];
}
public dropdown = new ModalModel();
public trackByFieldFn: (_index: number, field: NestedFieldDto) => any;
@ -45,6 +41,10 @@ export class FieldComponent implements OnChanges {
public addFieldDialog = new DialogModel();
public get isLocalizable() {
return (this.parent && this.parent.isLocalizable) || this.field['isLocalizable'];
}
constructor(
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.
*/
import { ChangeDetectionStrategy, Component, Input } from '@angular/core';
import { ChangeDetectionStrategy, Component, Input, OnChanges, SimpleChanges } from '@angular/core';
import { ApiUrlConfig, BackupDto, BackupsState, Duration } from '@app/shared';
@Component({
@ -14,19 +14,24 @@ import { ApiUrlConfig, BackupDto, BackupsState, Duration } from '@app/shared';
templateUrl: './backup.component.html',
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class BackupComponent {
export class BackupComponent implements OnChanges {
@Input()
public backup!: BackupDto;
public get duration() {
return Duration.create(this.backup.started, this.backup.stopped!).toString();
}
public duration = '';
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() {
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">
{{ '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 class="col text-decent">
{{ '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 class="col-auto">
<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 })
public addPermissionInput!: AutocompleteComponent;
public get halfSchemas() {
return Math.ceil(this.schemas.length / 2);
}
public descriptions = DESCRIPTIONS;
public propertiesList = Settings.AppProperties;
@ -67,6 +63,10 @@ export class RoleComponent implements OnChanges {
public editForm = new EditRoleForm();
public get halfSchemas() {
return Math.ceil(this.schemas.length / 2);
}
constructor(
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 })
public panel!: ElementRef<HTMLElement>;
public isCollapsed = false;
public get desiredWidth() {
return this.isCollapsed ? 3 : this.width;
}
@ -84,8 +86,6 @@ export class LayoutComponent implements OnInit, OnDestroy, AfterViewInit {
return this.widthToRender;
}
public isCollapsed = false;
public firstChild =
this.router.events.pipe(
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/image-source.directive';
export * from './angular/image-url.directive';
export * from './angular/if-once.directive';
export * from './angular/language-selector.component';
export * from './angular/layout-container.directive';
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/name.pipe';
export * from './angular/pipes/numbers.pipes';
export * from './angular/pipes/strings.pipes';
export * from './angular/pipes/translate.pipe';
export * from './angular/resized.directive';
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 { RouterModule } from '@angular/router';
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({
imports: [
@ -55,10 +55,12 @@ import { AnalyticsService, AutocompleteComponent, AvatarComponent, CachingInterc
FullDateTimePipe,
HighlightPipe,
HoverBackgroundDirective,
IfOnceDirective,
ImageSourceDirective,
ImageUrlDirective,
IndeterminateValueDirective,
ISODatePipe,
JoinPipe,
KeysPipe,
KNumberPipe,
LanguageSelectorComponent,
@ -140,10 +142,12 @@ import { AnalyticsService, AutocompleteComponent, AvatarComponent, CachingInterc
FullDateTimePipe,
HighlightPipe,
HoverBackgroundDirective,
IfOnceDirective,
ImageSourceDirective,
ImageUrlDirective,
IndeterminateValueDirective,
ISODatePipe,
JoinPipe,
KeysPipe,
KNumberPipe,
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 selectedTab = 0;
public isEditable = false;
public isEditableAny = false;
public isUploadable = false;
public progress = 0;
public selectedTab = 0;
public annotateForm = new AnnotateAssetForm();
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">
<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()
public selectNode = new EventEmitter<AssetFolderDropdowNode>();
public get style() {
return { paddingLeft: `${this.nodeLevel}rem` };
public get paddingLeft() {
return `${this.nodeLevel}rem`;
}
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 { ResourceOwner } from '@app/framework';
import { RootFieldDto, TableSettings } from '@app/shared';
import { ContentDto, MetaFields, TableField, FieldSizes, Types } from '@app/shared/internal';
import { ContentDto, MetaFields, TableField, FieldSizes, Types, RootFieldDto, TableSettings } from '@app/shared/internal';
export function getCellWidth(field: TableField, sizes: FieldSizes | undefined | null) {
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()
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) {
super(changeDetector, {
formatted: '',
@ -64,16 +76,4 @@ export class ContentListFieldComponent extends StatefulComponent<State> implemen
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.
*/
import { ChangeDetectionStrategy, Component, Input } from '@angular/core';
import { ScheduleDto } from '@app/shared';
import { ChangeDetectionStrategy, Component, Input, OnChanges, SimpleChanges } from '@angular/core';
import { LocalizerService } from '@app/framework';
import { ScheduleDto } from '@app/shared/internal';
@Component({
selector: 'sqx-content-status[status][statusColor]',
@ -14,7 +15,7 @@ import { ScheduleDto } from '@app/shared';
templateUrl: './content-status.component.html',
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class ContentStatusComponent {
export class ContentStatusComponent implements OnChanges {
@Input()
public status!: string;
@ -33,6 +34,8 @@ export class ContentStatusComponent {
@Input()
public small?: boolean | null;
public tooltipText = '';
public get isMultiline() {
return this.layout === 'multiline';
}
@ -41,11 +44,20 @@ export class ContentStatusComponent {
return this.layout === 'text';
}
public get tooltipText() {
constructor(
private readonly localizer: LocalizerService,
) {
}
public ngOnChanges(changes: SimpleChanges) {
if (changes['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 {
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 { ResourceOwner } from '@app/framework';
import { RootFieldDto, TableField } from '@app/shared';
import { FieldWrappings, HtmlValue, TableSettings, Types } from '@app/shared/internal';
import { FieldWrappings, HtmlValue, RootFieldDto, TableField, TableSettings, Types } from '@app/shared/internal';
@Component({
selector: 'sqx-content-value[value]',

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

@ -19,7 +19,7 @@
</div>
<div class="col-auto">
<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-template #noSchemas>
<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"
[language]="language"
[languages]="languages"
[level]="nestedLevel"
[level]="level + 1"
[model]="model"
[statuses]="statuses"
(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);
}
public filters: FilterNode[] = [];
public get filter() {
return this.filterValue;
}
@ -60,12 +62,6 @@ export class FilterLogicalComponent {
return !!this.filterValue.or;
}
public get nestedLevel() {
return this.level + 1;
}
public filters: FilterNode[] = [];
public addComparison() {
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"
[items]="model.schema.fields"
[itemSeparator]="true"
[ngModel]="value"
[ngModel]="field"
(ngModelChange)="pathChange.emit($event?.path)"
[dropdownStyles]="{ maxWidth: '40rem' }">
<ng-template let-field="$implicit">
<div class="row">
<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 class="col text-end">
<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.
*/
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';
@Component({
@ -14,7 +15,7 @@ import { QueryModel } from '@app/shared/internal';
templateUrl: './query-path.component.html',
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class QueryPathComponent {
export class QueryPathComponent implements OnChanges {
@Output()
public pathChange = new EventEmitter<string>();
@ -24,7 +25,9 @@ export class QueryPathComponent {
@Input()
public model!: QueryModel;
public get value() {
return this.model.schema.fields.find(x => x.path === this.path);
public field?: FilterableField;
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 {
private readonly disabled$ = new BehaviorSubject<boolean>(false);
private readonly collapsed$ = new BehaviorSubject<boolean | null>(null);
private readonly ruleSet: CompiledRules;
public get disabled() {
@ -131,6 +132,14 @@ export abstract class AbstractContentForm<T extends FieldDto, TForm extends Abst
return this.disabled$;
}
public get collapsed() {
return this.collapsed$.value;
}
public get collapsedChanges(): Observable<boolean | null> {
return this.collapsed$;
}
protected constructor(
public readonly globals: FormGlobals,
public readonly field: T,
@ -148,6 +157,14 @@ export abstract class AbstractContentForm<T extends FieldDto, TForm extends Abst
return `${this.fieldPath}.${relative}`;
}
public collapse() {
this.collapsed$.next(true);
}
public expand() {
this.collapsed$.next(false);
}
public updateState(context: RuleContext, itemData: any, parentState: AbstractContentFormState) {
const state = {
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>
</div>
<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>
</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()">
<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>
</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 versionReceived = -1;
public unread = 0;
public userToken = '';
public get unread() {
return Math.max(0, this.versionReceived - this.versionRead);
}
public isNotifoConfigured = false;
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.versionReceived = this.versionRead;
this.updateVersion();
const commentsUrl = `users/${authService.user!.id}/notifications`;
this.commentsState =
@ -87,6 +86,8 @@ export class NotificationsMenuComponent extends ResourceOwner implements OnInit
}
private updateVersion() {
this.unread = Math.max(0, this.versionReceived - this.versionRead);
if (this.modalMenu.isOpen) {
this.versionRead = this.versionReceived;

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

@ -20,7 +20,7 @@
{{result.name}}
</div>
<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>
</ng-template>

Loading…
Cancel
Save