Browse Source

Feature/resizable columns (#852)

* Started with resizable columns.

* Store settings together.

* Finalized.

* Fix drag preview.

* Some more design fixes.

* Mini improvements.

* Modernization.

* Wrapping

* Fix tests.

* Refactorings.
pull/853/head
Sebastian Stehle 4 years ago
committed by GitHub
parent
commit
baf2cc2f89
No known key found for this signature in database GPG Key ID: 4AEE18F83AFDEB23
  1. 2
      frontend/src/app/features/administration/state/users.forms.ts
  2. 2
      frontend/src/app/features/administration/state/users.state.ts
  3. 2
      frontend/src/app/features/content/pages/calendar/calendar-page.component.ts
  4. 2
      frontend/src/app/features/content/pages/content/editor/field-copy-button.component.ts
  5. 29
      frontend/src/app/features/content/pages/contents/contents-page.component.html
  6. 45
      frontend/src/app/features/content/pages/contents/contents-page.component.scss
  7. 14
      frontend/src/app/features/content/pages/contents/contents-page.component.ts
  8. 28
      frontend/src/app/features/content/pages/contents/custom-view-editor.component.html
  9. 4
      frontend/src/app/features/content/pages/contents/custom-view-editor.component.scss
  10. 19
      frontend/src/app/features/content/pages/contents/custom-view-editor.component.ts
  11. 2
      frontend/src/app/features/content/shared/forms/array-editor.component.ts
  12. 2
      frontend/src/app/features/content/shared/forms/component.component.ts
  13. 13
      frontend/src/app/features/content/shared/list/content.component.html
  14. 25
      frontend/src/app/features/content/shared/list/content.component.scss
  15. 11
      frontend/src/app/features/content/shared/list/content.component.ts
  16. 3
      frontend/src/app/features/content/shared/references/reference-dropdown.component.ts
  17. 4
      frontend/src/app/features/content/shared/references/references-editor.component.ts
  18. 2
      frontend/src/app/features/content/shared/references/references-tag-converter.ts
  19. 2
      frontend/src/app/features/rules/pages/simulator/rule-transition.component.ts
  20. 4
      frontend/src/app/features/schemas/pages/schema/ui/field-list.component.ts
  21. 18
      frontend/src/app/framework/angular/forms/control-errors.component.ts
  22. 4
      frontend/src/app/framework/angular/forms/editors/checkbox-group.component.ts
  23. 4
      frontend/src/app/framework/angular/forms/editors/tag-editor.component.ts
  24. 2
      frontend/src/app/framework/angular/forms/file-drop.directive.ts
  25. 2
      frontend/src/app/framework/angular/forms/validators.ts
  26. 2
      frontend/src/app/framework/angular/language-selector.component.ts
  27. 4
      frontend/src/app/framework/angular/list-view.component.html
  28. 10
      frontend/src/app/framework/angular/list-view.component.scss
  29. 3
      frontend/src/app/framework/angular/list-view.component.ts
  30. 2
      frontend/src/app/framework/angular/modals/modal-placement.directive.ts
  31. 12
      frontend/src/app/framework/angular/modals/onboarding-tooltip.component.html
  32. 12
      frontend/src/app/framework/angular/routers/router-utils.ts
  33. 2
      frontend/src/app/framework/angular/stateful.component.ts
  34. 2
      frontend/src/app/framework/services/localizer.service.ts
  35. 2
      frontend/src/app/framework/services/title.service.spec.ts
  36. 2
      frontend/src/app/framework/services/title.service.ts
  37. 10
      frontend/src/app/framework/state.ts
  38. 21
      frontend/src/app/framework/utils/array-extensions.spec.ts
  39. 22
      frontend/src/app/framework/utils/array-extensions.ts
  40. 4
      frontend/src/app/framework/utils/error.ts
  41. 2
      frontend/src/app/framework/utils/math-helper.ts
  42. 2
      frontend/src/app/framework/utils/string-helper.ts
  43. 16
      frontend/src/app/framework/utils/types.ts
  44. 2
      frontend/src/app/framework/utils/version.ts
  45. 4
      frontend/src/app/shared/components/assets/pipes.ts
  46. 296
      frontend/src/app/shared/components/contents/content-list-cell.directive.ts
  47. 2
      frontend/src/app/shared/components/contents/content-list-field.component.html
  48. 4
      frontend/src/app/shared/components/contents/content-list-field.component.scss
  49. 5
      frontend/src/app/shared/components/contents/content-list-field.component.ts
  50. 10
      frontend/src/app/shared/components/contents/content-value.component.html
  51. 38
      frontend/src/app/shared/components/contents/content-value.component.scss
  52. 53
      frontend/src/app/shared/components/contents/content-value.component.ts
  53. 8
      frontend/src/app/shared/components/history/history.component.ts
  54. 2
      frontend/src/app/shared/components/notifo.component.ts
  55. 4
      frontend/src/app/shared/components/pipes.ts
  56. 2
      frontend/src/app/shared/components/references/content-selector-item.component.html
  57. 6
      frontend/src/app/shared/components/references/content-selector.component.html
  58. 2
      frontend/src/app/shared/components/references/content-selector.component.ts
  59. 2
      frontend/src/app/shared/components/references/reference-input.component.ts
  60. 2
      frontend/src/app/shared/components/search/queries/filter-comparison.component.ts
  61. 2
      frontend/src/app/shared/components/table-header.component.scss
  62. 2
      frontend/src/app/shared/guards/schema-must-not-be-singleton.guard.ts
  63. 2
      frontend/src/app/shared/internal.ts
  64. 8
      frontend/src/app/shared/module.ts
  65. 2
      frontend/src/app/shared/services/assets.service.ts
  66. 8
      frontend/src/app/shared/services/contents.service.ts
  67. 10
      frontend/src/app/shared/services/schemas.service.ts
  68. 22
      frontend/src/app/shared/services/workflows.service.ts
  69. 26
      frontend/src/app/shared/state/assets.forms.ts
  70. 8
      frontend/src/app/shared/state/assets.state.ts
  71. 2
      frontend/src/app/shared/state/contents.form-rules.ts
  72. 4
      frontend/src/app/shared/state/contents.state.ts
  73. 8
      frontend/src/app/shared/state/contributors.state.ts
  74. 7
      frontend/src/app/shared/state/languages.state.ts
  75. 2
      frontend/src/app/shared/state/resolvers.ts
  76. 6
      frontend/src/app/shared/state/schemas.state.ts
  77. 131
      frontend/src/app/shared/state/table-fields.spec.ts
  78. 62
      frontend/src/app/shared/state/table-fields.ts
  79. 329
      frontend/src/app/shared/state/table-settings.spec.ts
  80. 152
      frontend/src/app/shared/state/table-settings.ts
  81. 2
      frontend/src/app/shell/pages/internal/search-menu.component.ts
  82. 1
      frontend/src/app/theme/_common.scss
  83. 16
      frontend/src/app/theme/icomoon/demo.html
  84. BIN
      frontend/src/app/theme/icomoon/fonts/icomoon.eot
  85. 1
      frontend/src/app/theme/icomoon/fonts/icomoon.svg
  86. BIN
      frontend/src/app/theme/icomoon/fonts/icomoon.ttf
  87. BIN
      frontend/src/app/theme/icomoon/fonts/icomoon.woff
  88. 2
      frontend/src/app/theme/icomoon/selection.json
  89. 13
      frontend/src/app/theme/icomoon/style.css
  90. 2
      frontend/src/app/theme/theme.scss

2
frontend/src/app/features/administration/state/users.forms.ts

@ -50,7 +50,7 @@ export class UserForm extends Form<ExtendedFormGroup, UpdateUserDto, UserDto> {
} }
protected transformSubmit(value: any) { protected transformSubmit(value: any) {
const permissions = value['permissions'].split('\n').filter((x: any) => !!x); const permissions = value['permissions'].split('\n').defined();
return { ...value, permissions }; return { ...value, permissions };
} }

2
frontend/src/app/features/administration/state/users.state.ts

@ -171,7 +171,7 @@ export class UsersState extends State<Snapshot> {
return this.usersService.deleteUser(user).pipe( return this.usersService.deleteUser(user).pipe(
tap(() => { tap(() => {
this.next(s => { this.next(s => {
const users = s.users.filter(x => x.id !== user.id); const users = s.users.removedBy('id', user);
const selectedUser = const selectedUser =
s.selectedUser?.id !== user.id ? s.selectedUser?.id !== user.id ?

2
frontend/src/app/features/content/pages/calendar/calendar-page.component.ts

@ -196,7 +196,7 @@ export class CalendarPageComponent implements AfterViewInit, OnDestroy {
content.referenceFields content.referenceFields
.map(f => getContentValue(content, this.language, f, false)) .map(f => getContentValue(content, this.language, f, false))
.map(v => v.formatted) .map(v => v.formatted)
.filter(v => !!v) .defined()
.join(', ') .join(', ')
|| this.localizer.getOrKey('common.noValue'); || this.localizer.getOrKey('common.noValue');

2
frontend/src/app/features/content/pages/content/editor/field-copy-button.component.ts

@ -41,7 +41,7 @@ export class FieldCopyButtonComponent implements OnChanges {
this.copySource = language; this.copySource = language;
this.copyTargets = []; this.copyTargets = [];
this.languageCodes = this.languages.map(x => x.iso2Code).filter(x => x !== language); this.languageCodes = this.languages.map(x => x.iso2Code).removed(language);
} }
public copy() { public copy() {

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

@ -35,8 +35,8 @@
</ng-container> </ng-container>
<ng-container> <ng-container>
<ng-container *ngIf="tableView.listFields | async; let listFields"> <ng-container *ngIf="tableSettings.listFields | async; let listFields">
<sqx-list-view [isLoading]="contentsState.isLoading | async" [syncedHeader]="true" [table]="true"> <sqx-list-view [isLoading]="contentsState.isLoading | async" [syncedHeader]="true" [tableNoPadding]="true">
<ng-container topHeader> <ng-container topHeader>
<div class="selection" *ngIf="selectionCount > 0"> <div class="selection" *ngIf="selectionCount > 0">
{{ 'contents.selectionCount' | sqxTranslate: { count: selectionCount } }}&nbsp;&nbsp; {{ 'contents.selectionCount' | sqxTranslate: { count: selectionCount } }}&nbsp;&nbsp;
@ -65,9 +65,10 @@
<ng-container *sqxModal="tableViewModal"> <ng-container *sqxModal="tableViewModal">
<sqx-dropdown-menu [sqxAnchoredTo]="buttonSettings" [scrollY]="true" position="bottom-right"> <sqx-dropdown-menu [sqxAnchoredTo]="buttonSettings" [scrollY]="true" position="bottom-right">
<sqx-custom-view-editor <sqx-custom-view-editor
[allFields]="tableView.allFields" [allFields]="tableSettings.schemaFields"
(fieldNamesChange)="tableView.updateFields($event)" (listFieldsChange)="tableSettings.updateFields($event)"
[fieldNames]="$any(tableView.listFieldNames | async)"> [listFields]="$any(tableSettings.listFieldNames | async)"
(reset)="tableSettings.reset()">
</sqx-custom-view-editor> </sqx-custom-view-editor>
</sqx-dropdown-menu> </sqx-dropdown-menu>
</ng-container> </ng-container>
@ -75,7 +76,7 @@
</ng-container> </ng-container>
<ng-container header> <ng-container header>
<table class="table table-items table-fixed" [style.minWidth]="listFields | sqxContentListWidth" #header> <table class="table table-items table-fixed" [sqxContentListWidth]="listFields" [fields]="tableSettings" #header>
<thead> <thead>
<tr> <tr>
<th class="cell-select"> <th class="cell-select">
@ -88,9 +89,13 @@
</div> </div>
</th> </th>
<th class="cell-actions cell-actions-left"> <th class="cell-actions cell-actions-left">
{{ 'common.actions' | sqxTranslate }} <span class="truncate">{{ 'common.actions' | sqxTranslate }}</span>
</th> </th>
<th *ngFor="let field of listFields" [sqxContentListCell]="field"> <th *ngFor="let field of listFields"
sqxContentListCell
sqxContentListCellResize
[field]="field"
[fields]="tableSettings">
<sqx-content-list-header <sqx-content-list-header
[field]="field" [field]="field"
(queryChange)="search($event)" (queryChange)="search($event)"
@ -98,6 +103,7 @@
[language]="language"> [language]="language">
</sqx-content-list-header> </sqx-content-list-header>
</th> </th>
<th></th>
</tr> </tr>
</thead> </thead>
</table> </table>
@ -105,18 +111,19 @@
<ng-container> <ng-container>
<div class="table-container"> <div class="table-container">
<table class="table table-items table-fixed" [style.minWidth]="listFields | sqxContentListWidth" [sqxSyncWidth]="header"> <table class="table table-center table-fixed" [sqxContentListWidth]="listFields" [fields]="tableSettings" [sqxSyncWidth]="header">
<tbody *ngFor="let content of contentsState.contents | async; trackBy: trackByContent" <tbody *ngFor="let content of contentsState.contents | async; trackBy: trackByContent"
[sqxContent]="content" [sqxContent]="content"
(clone)="clone(content)" (clone)="clone(content)"
(delete)="delete(content)" (delete)="delete(content)"
(selectedChange)="selectItem(content, $event)" (selectedChange)="selectItem(content, $event)"
[selected]="isItemSelected(content)"
(statusChange)="changeStatus(content, $event)" (statusChange)="changeStatus(content, $event)"
[cloneable]="contentsState.snapshot.canCreate" [cloneable]="contentsState.snapshot.canCreate"
[language]="language" [language]="language"
[link]="[content.id, 'history']" [link]="[content.id, 'history']"
[listFields]="listFields" [tableFields]="listFields"
[selected]="isItemSelected(content)"> [tableSettings]="tableSettings">
</tbody> </tbody>
</table> </table>
</div> </div>

45
frontend/src/app/features/content/pages/contents/contents-page.component.scss

@ -9,11 +9,44 @@
border-radius: 0; border-radius: 0;
} }
.table-items {
background-color: $color-white;
th {
border-left: 1px solid $color-border;
border-right: 0;
position: relative;
&.cell-select {
@include force-width(60px);
border-left: 0;
padding-left: 1.25rem;
padding-right: 1.25rem;
}
.resize-holder {
@include absolute(0, -10px, 0);
cursor: col-resize;
width: 20px;
z-index: 1;
}
&:last-child {
border-right: 0;
}
}
.cell-actions {
@include force-width(90px);
}
}
.table-container { .table-container {
display: inline-block; display: block;
min-width: 100%;
padding: 0; .table {
padding-right: 1.5rem; margin: 0;
}
.table-items { .table-items {
margin: 0; margin: 0;
@ -50,4 +83,8 @@
border-bottom: 1px solid $color-border; border-bottom: 1px solid $color-border;
border-radius: 0; border-radius: 0;
padding: .25 * $panel-padding $panel-padding; padding: .25 * $panel-padding $panel-padding;
}
tbody {
border: 0 !important;
} }

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

@ -10,7 +10,7 @@
import { Component, OnInit, ViewChild } from '@angular/core'; import { Component, OnInit, ViewChild } from '@angular/core';
import { ActivatedRoute, Router } from '@angular/router'; import { ActivatedRoute, Router } from '@angular/router';
import { distinctUntilChanged, map, switchMap, take, tap } from 'rxjs/operators'; import { distinctUntilChanged, map, switchMap, take, tap } from 'rxjs/operators';
import { AppLanguageDto, AppsState, ContentDto, ContentsState, ContributorsState, defined, LanguagesState, ModalModel, Queries, Query, QuerySynchronizer, ResourceOwner, Router2State, SchemaDto, SchemasService, SchemasState, switchSafe, TableFields, TempService, UIState } from '@app/shared'; import { AppLanguageDto, AppsState, ContentDto, ContentsState, ContributorsState, defined, LanguagesState, ModalModel, Queries, Query, QuerySynchronizer, ResourceOwner, Router2State, SchemaDto, SchemasService, SchemasState, switchSafe, TableSettings, TempService, UIState } from '@app/shared';
import { DueTimeSelectorComponent } from './../../shared/due-time-selector.component'; import { DueTimeSelectorComponent } from './../../shared/due-time-selector.component';
@Component({ @Component({
@ -27,7 +27,7 @@ export class ContentsPageComponent extends ResourceOwner implements OnInit {
public schema!: SchemaDto; public schema!: SchemaDto;
public tableView!: TableFields; public tableSettings!: TableSettings;
public tableViewModal = new ModalModel(); public tableViewModal = new ModalModel();
public searchModal = new ModalModel(); public searchModal = new ModalModel();
@ -93,7 +93,7 @@ export class ContentsPageComponent extends ResourceOwner implements OnInit {
this.schema = schema; this.schema = schema;
this.tableView = new TableFields(this.uiState, schema); this.tableSettings = new TableSettings(this.uiState, schema);
const initial = const initial =
this.contentsRoute.mapTo(this.contentsState) this.contentsRoute.mapTo(this.contentsState)
@ -199,11 +199,9 @@ export class ContentsPageComponent extends ResourceOwner implements OnInit {
if (this.selectedItems[content.id]) { if (this.selectedItems[content.id]) {
this.selectionCount++; this.selectionCount++;
for (const action in this.selectionStatuses) { for (const action of Object.keys(this.selectionStatuses)) {
if (this.selectionStatuses.hasOwnProperty(action)) { if (!content.statusUpdates.find(x => x.status === action)) {
if (!content.statusUpdates.find(x => x.status === action)) { delete this.selectionStatuses[action];
delete this.selectionStatuses[action];
}
} }
} }

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

@ -9,9 +9,9 @@
<div <div
cdkDropList cdkDropList
[cdkDropListData]="fieldNames" [cdkDropListData]="listFields"
(cdkDropListDropped)="drop($event)"> (cdkDropListDropped)="drop($event)">
<div *ngFor="let field of fieldNames" cdkDrag> <div *ngFor="let field of listFields" cdkDrag>
<i class="icon-drag2 drag-handle"></i> <i class="icon-drag2 drag-handle"></i>
<div class="form-check"> <div class="form-check">
@ -23,18 +23,20 @@
</div> </div>
</div> </div>
<hr> <ng-container *ngIf="fieldsNotAdded.length > 0">
<hr>
<div >
<div *ngFor="let field of fieldsNotAdded">
<i class="icon-drag2 drag-handle invisible"></i>
<div> <div class="form-check">
<div *ngFor="let field of fieldsNotAdded"> <input class="form-check-input" type="checkbox" (click)="addField(field)" id="field_{{field}}">
<i class="icon-drag2 drag-handle invisible"></i> <label class="form-check-label" for="field_{{field}}">
{{field}}
<div class="form-check"> </label>
<input class="form-check-input" type="checkbox" (click)="addField(field)" id="field_{{field}}"> </div>
<label class="form-check-label" for="field_{{field}}">
{{field}}
</label>
</div> </div>
</div> </div>
</div> </ng-container>
</div> </div>

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

@ -17,4 +17,8 @@
display: inline-block; display: inline-block;
padding-left: 2rem; padding-left: 2rem;
padding-right: .5rem; padding-right: .5rem;
}
.cdk-drag-preview {
z-index: 9000 !important;
} }

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

@ -9,17 +9,20 @@ import { CdkDragDrop, moveItemInArray } from '@angular/cdk/drag-drop';
import { ChangeDetectionStrategy, Component, EventEmitter, Input, OnChanges, Output } from '@angular/core'; import { ChangeDetectionStrategy, Component, EventEmitter, Input, OnChanges, Output } from '@angular/core';
@Component({ @Component({
selector: 'sqx-custom-view-editor[allFields][fieldNames]', selector: 'sqx-custom-view-editor[allFields][listFields]',
styleUrls: ['./custom-view-editor.component.scss'], styleUrls: ['./custom-view-editor.component.scss'],
templateUrl: './custom-view-editor.component.html', templateUrl: './custom-view-editor.component.html',
changeDetection: ChangeDetectionStrategy.OnPush, changeDetection: ChangeDetectionStrategy.OnPush,
}) })
export class CustomViewEditorComponent implements OnChanges { export class CustomViewEditorComponent implements OnChanges {
@Output() @Output()
public fieldNamesChange = new EventEmitter<ReadonlyArray<string>>(); public reset = new EventEmitter();
@Output()
public listFieldsChange = new EventEmitter<ReadonlyArray<string>>();
@Input() @Input()
public fieldNames!: string[]; public listFields!: string[];
@Input() @Input()
public allFields!: ReadonlyArray<string>; public allFields!: ReadonlyArray<string>;
@ -27,7 +30,7 @@ export class CustomViewEditorComponent implements OnChanges {
public fieldsNotAdded!: ReadonlyArray<string>; public fieldsNotAdded!: ReadonlyArray<string>;
public ngOnChanges() { public ngOnChanges() {
this.fieldsNotAdded = this.allFields.filter(n => this.fieldNames.indexOf(n) < 0); this.fieldsNotAdded = this.allFields.filter(n => !this.listFields.includes(n));
} }
public drop(event: CdkDragDrop<string[], any>) { public drop(event: CdkDragDrop<string[], any>) {
@ -37,18 +40,18 @@ export class CustomViewEditorComponent implements OnChanges {
} }
public resetDefault() { public resetDefault() {
this.updateFieldNames([]); this.reset.emit();
} }
public addField(field: string) { public addField(field: string) {
this.updateFieldNames([...this.fieldNames, field]); this.updateFieldNames([...this.listFields, field]);
} }
public removeField(field: string) { public removeField(field: string) {
this.updateFieldNames(this.fieldNames.filter(x => x !== field)); this.updateFieldNames(this.listFields.removed(field));
} }
private updateFieldNames(fieldNames: ReadonlyArray<string>) { private updateFieldNames(fieldNames: ReadonlyArray<string>) {
this.fieldNamesChange.emit(fieldNames); this.listFieldsChange.emit(fieldNames);
} }
} }

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

@ -75,7 +75,7 @@ export class ArrayEditorComponent implements OnChanges, OnInit {
const maxItems = this.formModel.field.properties['maxItems'] || Number.MAX_VALUE; const maxItems = this.formModel.field.properties['maxItems'] || Number.MAX_VALUE;
if (Types.is(this.formModel.field.properties, ComponentsFieldPropertiesDto)) { if (Types.is(this.formModel.field.properties, ComponentsFieldPropertiesDto)) {
this.schemasList = this.formModel.field.properties.schemaIds?.map(x => this.formModel.globals.schemas[x]).filter(x => !!x) || []; this.schemasList = this.formModel.field.properties.schemaIds?.map(x => this.formModel.globals.schemas[x]).defined() || [];
} else { } else {
this.isArray = true; this.isArray = true;
} }

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

@ -60,7 +60,7 @@ export class ComponentComponent extends ResourceOwner implements OnChanges {
})); }));
if (Types.is(this.formModel.field.properties, ComponentFieldPropertiesDto)) { if (Types.is(this.formModel.field.properties, ComponentFieldPropertiesDto)) {
this.schemasList = this.formModel.field.properties.schemaIds?.map(x => this.formModel.globals.schemas[x]).filter(x => !!x) || []; this.schemasList = this.formModel.field.properties.schemaIds?.map(x => this.formModel.globals.schemas[x]).defined() || [];
} }
} }
} }

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

@ -54,14 +54,21 @@
</ng-container> </ng-container>
</td> </td>
<td *ngFor="let field of listFields" [sqxContentListCell]="field" [sqxStopClick]="shouldStop(field)"> <td *ngFor="let field of tableFields"
sqxContentListCell
sqxContentListCellResize
[field]="field"
[fields]="tableSettings"
[sqxStopClick]="shouldStop(field)">
<sqx-content-list-field <sqx-content-list-field
[field]="field" [field]="field"
[fields]="tableSettings"
[patchForm]="patchForm?.form" [patchForm]="patchForm?.form"
[patchAllowed]="patchAllowed" [patchAllowed]="patchAllowed"
[content]="content" [content]="content"
[language]="language"> [language]="language">
</sqx-content-list-field> </sqx-content-list-field>
</td> </td>
</tr>
<tr class="spacer"></tr> <td></td>
</tr>

25
frontend/src/app/features/content/shared/list/content.component.scss

@ -5,11 +5,34 @@
position: relative; position: relative;
.edit-menu { .edit-menu {
@include absolute(.75rem, auto, auto, .25rem); @include absolute(.5rem, auto, auto, .5rem);
background: $color-white; background: $color-white;
border-right: 1px solid $color-border; border-right: 1px solid $color-border;
padding-right: 1rem; padding-right: 1rem;
white-space: nowrap; white-space: nowrap;
z-index: 100; z-index: 100;
} }
}
tr {
background-color: $color-white;
}
td {
border-bottom: 1px solid $color-border;
border-left: 1px solid $color-white;
border-top: 0 !important;
position: relative;
vertical-align: middle;
&.cell-select {
@include force-width(60px);
border-left: 0;
padding-left: 1.25rem;
padding-right: 1.25rem;
}
&.cell-actions {
@include force-width(90px);
}
} }

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

@ -6,12 +6,12 @@
*/ */
import { ChangeDetectionStrategy, ChangeDetectorRef, Component, EventEmitter, Input, OnChanges, Output, QueryList, SimpleChanges, ViewChildren } from '@angular/core'; import { ChangeDetectionStrategy, ChangeDetectorRef, Component, EventEmitter, Input, OnChanges, Output, QueryList, SimpleChanges, ViewChildren } from '@angular/core';
import { AppLanguageDto, ContentDto, ContentListFieldComponent, ContentsState, ModalModel, PatchContentForm, RootFieldDto, TableField, Types } from '@app/shared'; import { AppLanguageDto, ContentDto, ContentListFieldComponent, ContentsState, ModalModel, PatchContentForm, RootFieldDto, TableField, TableSettings, Types } from '@app/shared';
/* tslint:disable: component-selector */ /* tslint:disable: component-selector */
@Component({ @Component({
selector: '[sqxContent][language][listFields]', selector: '[sqxContent][language][tableFields][tableSettings]',
styleUrls: ['./content.component.scss'], styleUrls: ['./content.component.scss'],
templateUrl: './content.component.html', templateUrl: './content.component.html',
changeDetection: ChangeDetectionStrategy.OnPush, changeDetection: ChangeDetectionStrategy.OnPush,
@ -36,7 +36,10 @@ export class ContentComponent implements OnChanges {
public language!: AppLanguageDto; public language!: AppLanguageDto;
@Input() @Input()
public listFields!: ReadonlyArray<TableField>; public tableFields!: ReadonlyArray<TableField>;
@Input()
public tableSettings!: TableSettings;
@Input() @Input()
public cloneable?: boolean | null; public cloneable?: boolean | null;
@ -71,7 +74,7 @@ export class ContentComponent implements OnChanges {
} }
if (this.patchAllowed && (changes['listFields'] || changes['language'])) { if (this.patchAllowed && (changes['listFields'] || changes['language'])) {
this.patchForm = new PatchContentForm(this.listFields, this.language); this.patchForm = new PatchContentForm(this.tableFields, this.language);
} }
} }

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

@ -186,7 +186,8 @@ export class ReferenceDropdownComponent extends StatefulControlComponent<State,
const name = const name =
content.referenceFields content.referenceFields
.map(f => getContentValue(content, this.language, f, false)) .map(f => getContentValue(content, this.language, f, false))
.map(v => v.formatted).filter(v => !!v) .map(v => v.formatted)
.defined()
.join(', ') .join(', ')
|| this.localizer.getOrKey('common.noValue'); || this.localizer.getOrKey('common.noValue');

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

@ -71,7 +71,7 @@ export class ReferencesEditorComponent extends StatefulControlComponent<State, R
this.contentsResolver.resolveMany(contentIds) this.contentsResolver.resolveMany(contentIds)
.subscribe({ .subscribe({
next: ({ items }) => { next: ({ items }) => {
this.setContentItems(contentIds.map(id => items.find(c => c.id === id)!).filter(r => !!r)); this.setContentItems(contentIds.map(id => items.find(c => c.id === id)!).defined());
if (this.snapshot.contentItems.length !== contentIds.length) { if (this.snapshot.contentItems.length !== contentIds.length) {
this.updateValue(); this.updateValue();
@ -104,7 +104,7 @@ export class ReferencesEditorComponent extends StatefulControlComponent<State, R
public remove(content: ContentDto) { public remove(content: ContentDto) {
if (content && !this.snapshot.isDisabled) { if (content && !this.snapshot.isDisabled) {
this.setContentItems(this.snapshot.contentItems.filter(x => x.id !== content.id)); this.setContentItems(this.snapshot.contentItems.removedBy('id', content));
this.updateValue(); this.updateValue();
} }

2
frontend/src/app/features/content/shared/references/references-tag-converter.ts

@ -38,7 +38,7 @@ export class ReferencesTagsConverter implements TagConverter {
content.referenceFields content.referenceFields
.map(f => getContentValue(content, language, f, false)) .map(f => getContentValue(content, language, f, false))
.map(v => v.formatted) .map(v => v.formatted)
.filter(v => !!v) .defined()
.join(', ') .join(', ')
|| this.localizer.getOrKey('common.noValue'); || this.localizer.getOrKey('common.noValue');

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

@ -31,7 +31,7 @@ export class RuleTransitionComponent {
return null; return null;
} }
const result = this.event?.skipReasons.filter(x => errors.indexOf(x) >= 0).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; return null;

4
frontend/src/app/features/schemas/pages/schema/ui/field-list.component.ts

@ -43,8 +43,8 @@ export class FieldListComponent implements OnChanges {
allFields = [...allFields, ...META_FIELD_NAMES]; allFields = [...allFields, ...META_FIELD_NAMES];
} }
this.fieldsAdded = this.fieldNames.filter(n => allFields.indexOf(n) >= 0); this.fieldsAdded = this.fieldNames.filter(n => allFields.includes(n));
this.fieldsNotAdded = allFields.filter(n => this.fieldNames.indexOf(n) < 0); this.fieldsNotAdded = allFields.filter(n => !this.fieldNames.includes(n));
} }
public drop(event: CdkDragDrop<string[]>) { public drop(event: CdkDragDrop<string[]>) {

18
frontend/src/app/framework/angular/forms/control-errors.component.ts

@ -97,16 +97,14 @@ export class ControlErrorsComponent extends StatefulComponent<State> implements
const errorMessages: string[] = []; const errorMessages: string[] = [];
if (this.control && this.control.invalid && this.isTouched && this.control.errors) { if (this.control && this.control.invalid && this.isTouched && this.control.errors) {
for (const key in this.control.errors) { for (const [key, error] of Object.entries(this.control.errors)) {
if (this.control.errors.hasOwnProperty(key)) { const message = formatError(this.localizer, this.controlDisplayName, key, error, this.control.value);
const message = formatError(this.localizer, this.controlDisplayName, key, this.control.errors[key], this.control.value);
if (Types.isString(message)) {
if (Types.isString(message)) { errorMessages.push(message);
errorMessages.push(message); } else if (Types.isArray(message)) {
} else if (Types.isArray(message)) { for (const error of message) {
for (const error of message) { errorMessages.push(error);
errorMessages.push(error);
}
} }
} }
} }

4
frontend/src/app/framework/angular/forms/editors/checkbox-group.component.ts

@ -163,7 +163,7 @@ export class CheckboxGroupComponent extends StatefulControlComponent<State, stri
let checkedValues: TagValue[] = []; let checkedValues: TagValue[] = [];
if (Types.isArray(obj) && obj.length > 0) { if (Types.isArray(obj) && obj.length > 0) {
checkedValues = this.valuesSorted.filter(x => obj.indexOf(x.value) >= 0); checkedValues = this.valuesSorted.filter(x => obj.includes(x.value));
} }
this.next({ checkedValues }); this.next({ checkedValues });
@ -184,7 +184,7 @@ export class CheckboxGroupComponent extends StatefulControlComponent<State, stri
} }
public isChecked(value: TagValue) { public isChecked(value: TagValue) {
return this.snapshot.checkedValues.indexOf(value) >= 0; return this.snapshot.checkedValues.includes(value);
} }
public trackByValue(_index: number, tag: TagValue) { public trackByValue(_index: number, tag: TagValue) {

4
frontend/src/app/framework/angular/forms/editors/tag-editor.component.ts

@ -109,7 +109,7 @@ export class TagEditorComponent extends StatefulControlComponent<State, Readonly
if (this.addInput.value) { if (this.addInput.value) {
const query = this.addInput.value; const query = this.addInput.value;
const items = this.suggestionsSorted.filter(s => s.lowerCaseName.indexOf(query) >= 0 && !this.snapshot.items.find(x => x.id === s.id)); const items = this.suggestionsSorted.filter(s => s.lowerCaseName.includes(query) && !this.snapshot.items.find(x => x.id === s.id));
this.next({ this.next({
suggestedIndex: -1, suggestedIndex: -1,
@ -169,7 +169,7 @@ export class TagEditorComponent extends StatefulControlComponent<State, Readonly
if (!query) { if (!query) {
return []; return [];
} else if (Types.isArray(this.suggestionsSorted)) { } else if (Types.isArray(this.suggestionsSorted)) {
return this.suggestionsSorted.filter(s => s.lowerCaseName.indexOf(query) >= 0 && !this.snapshot.items.find(x => x.id === s.id)); return this.suggestionsSorted.filter(s => s.lowerCaseName.includes(query) && !this.snapshot.items.find(x => x.id === s.id));
} else { } else {
return []; return [];
} }

2
frontend/src/app/framework/angular/forms/file-drop.directive.ts

@ -193,7 +193,7 @@ export class FileDropDirective {
} }
private isAllowed(file: { type: string }) { private isAllowed(file: { type: string }) {
return !this.allowedFiles || this.allowedFiles.indexOf(file.type) >= 0; return !this.allowedFiles || this.allowedFiles.includes(file.type);
} }
private isImage(file: { type: string }) { private isImage(file: { type: string }) {

2
frontend/src/app/framework/angular/forms/validators.ts

@ -143,7 +143,7 @@ export module ValidatorsEx {
return (control: AbstractControl) => { return (control: AbstractControl) => {
const value: T = control.value; const value: T = control.value;
if (allowed.indexOf(value) < 0) { if (!allowed.includes(value)) {
return { validvalues: false }; return { validvalues: false };
} }

2
frontend/src/app/framework/angular/language-selector.component.ts

@ -48,7 +48,7 @@ export class LanguageSelectorComponent implements OnChanges, OnInit {
} }
private update() { private update() {
if (this.languages && this.languages.length > 0 && (!this.language || this.languages.indexOf(this.language) < 0)) { if (this.languages?.length > 0 && (!this.language || !this.languages.includes(this.language))) {
const selectedLanguage = const selectedLanguage =
this.languages.find(l => l.isMasterLanguage) || this.languages.find(l => l.isMasterLanguage) ||
this.languages[0]; this.languages[0];

4
frontend/src/app/framework/angular/list-view.component.html

@ -18,13 +18,13 @@
<ng-container *ngIf="syncedHeader"> <ng-container *ngIf="syncedHeader">
<div class="list-header synced" [class.loading-indicator]="isLoading" #headerElement> <div class="list-header synced" [class.loading-indicator]="isLoading" #headerElement>
<div class="sync-inner inner" #header> <div class="sync-inner inner" [class.tabled2]="tableNoPadding" #header>
<ng-container *ngTemplateOutlet="headerTemplate"></ng-container> <ng-container *ngTemplateOutlet="headerTemplate"></ng-container>
</div> </div>
</div> </div>
<div class="list-content synced" [class.loading-indicator]="isLoading" [sqxSyncScrolling]="header"> <div class="list-content synced" [class.loading-indicator]="isLoading" [sqxSyncScrolling]="header">
<div class="inner" [class.tabled]="table"> <div class="inner" [class.tabled]="table" [class.tabled2]="tableNoPadding">
<ng-container *ngTemplateOutlet="contentTemplate"></ng-container> <ng-container *ngTemplateOutlet="contentTemplate"></ng-container>
</div> </div>
</div> </div>

10
frontend/src/app/framework/angular/list-view.component.scss

@ -23,9 +23,13 @@
padding: 0 $panel-padding; padding: 0 $panel-padding;
&.tabled { &.tabled {
padding-bottom: .5rem !important; padding-bottom: .25rem !important;
padding-top: .5rem !important; padding-top: .5rem !important;
} }
&.tabled2 {
padding: 0 !important;
}
} }
.list { .list {
@ -63,6 +67,10 @@
overflow-x: hidden; overflow-x: hidden;
overflow-y: hidden; overflow-y: hidden;
&.tabled2 {
background-color: $color-white;
}
.sync-inner { .sync-inner {
display: table-cell; display: table-cell;
} }

3
frontend/src/app/framework/angular/list-view.component.ts

@ -43,6 +43,9 @@ export class ListViewComponent extends StatefulComponent<State> implements After
@Input() @Input()
public table?: boolean | null; public table?: boolean | null;
@Input()
public tableNoPadding?: boolean | null;
@Input() @Input()
public isLoaded: boolean | undefined | null = true; public isLoaded: boolean | undefined | null = true;

2
frontend/src/app/framework/angular/modals/modal-placement.directive.ts

@ -148,6 +148,8 @@ export class ModalPlacementDirective extends ResourceOwner implements AfterViewI
modalRect, modalRect,
offsetX: this.offsetX, offsetX: this.offsetX,
offsetY: this.offsetY, offsetY: this.offsetY,
spaceX: this.spaceX,
spaceY: this.spaceY,
targetRect, targetRect,
}; };

12
frontend/src/app/framework/angular/modals/onboarding-tooltip.component.html

@ -1,8 +1,10 @@
<ng-container *sqxModal="tooltipModal"> <ng-container *sqxModal="tooltipModal">
<div class="onboarding-rect" <div class="onboarding-rect"
[sqxAnchoredTo]="for" [sqxAnchoredTo]="for"
[offsetX]="4" [offsetX]="-4"
[offsetY]="4" [offsetY]="-4"
[spaceX]="4"
[spaceY]="4"
[adjustWidth]="true" [adjustWidth]="true"
[adjustHeight]="true" [adjustHeight]="true"
anchorX="left-to-left" anchorX="left-to-left"
@ -10,8 +12,10 @@
<div class="onboarding-help" <div class="onboarding-help"
[sqxAnchoredTo]="for" [sqxAnchoredTo]="for"
[offsetX]="4" [offsetX]="-4"
[offsetY]="4" [offsetY]="-4"
[spaceX]="4"
[spaceY]="4"
[position]="position" @fade> [position]="position" @fade>
<small class="onboarding-text"> <small class="onboarding-text">
<ng-content></ng-content> <ng-content></ng-content>

12
frontend/src/app/framework/angular/routers/router-utils.ts

@ -13,9 +13,9 @@ export function allData(value: ActivatedRouteSnapshot | ActivatedRoute): Data {
const result: { [key: string]: any } = {}; const result: { [key: string]: any } = {};
while (snapshot) { while (snapshot) {
for (const key in snapshot.data) { for (const [key, value] of Object.entries(snapshot.data)) {
if (snapshot.data.hasOwnProperty(key) && !result[key]) { if (!result[key]) {
result[key] = snapshot.data[key]; result[key] = value;
} }
} }
@ -30,9 +30,9 @@ export function allParams(value: ActivatedRouteSnapshot | ActivatedRoute): Param
const result: { [key: string]: any } = {}; const result: { [key: string]: any } = {};
while (snapshot) { while (snapshot) {
for (const key in snapshot.params) { for (const [key, value] of Object.entries(snapshot.params)) {
if (snapshot.params.hasOwnProperty(key) && !result[key]) { if (!result[key]) {
result[key] = snapshot.params[key]; result[key] = value;
} }
} }

2
frontend/src/app/framework/angular/stateful.component.ts

@ -18,7 +18,7 @@ declare type UnsubscribeFunction = () => void;
export class ResourceOwner implements OnDestroy { export class ResourceOwner implements OnDestroy {
private subscriptions: (Subscription | UnsubscribeFunction)[] = []; private subscriptions: (Subscription | UnsubscribeFunction)[] = [];
public own<T>(subscription: Subscription | UnsubscribeFunction | Observable<T>) { public own<T>(subscription: Subscription | UnsubscribeFunction | Observable<T> | null | undefined) {
if (subscription) { if (subscription) {
if (Types.isFunction(subscription['subscribe'])) { if (Types.isFunction(subscription['subscribe'])) {
const observable = <Observable<T>>subscription; const observable = <Observable<T>>subscription;

2
frontend/src/app/framework/services/localizer.service.ts

@ -39,7 +39,7 @@ export class LocalizerService {
let text = this.translations[key]; let text = this.translations[key];
if (!text) { if (!text) {
if (this.shouldLog && !key.indexOf(' ')) { if (this.shouldLog && !key.includes(' ')) {
// eslint-disable-next-line no-console // eslint-disable-next-line no-console
console.warn(`Missing i18n key: ${key}`); console.warn(`Missing i18n key: ${key}`);
} }

2
frontend/src/app/framework/services/title.service.spec.ts

@ -18,7 +18,7 @@ describe('TitleService', () => {
localizer = Mock.ofType<LocalizerService>(); localizer = Mock.ofType<LocalizerService>();
localizer.setup(x => x.getOrKey(It.isAnyString())) localizer.setup(x => x.getOrKey(It.isAnyString()))
.returns((key: string) => key.substr(5)); .returns((key: string) => key.substring(5));
}); });
it('should instantiate', () => { it('should instantiate', () => {

2
frontend/src/app/framework/services/title.service.ts

@ -84,7 +84,7 @@ export class TitleService {
let title = ''; let title = '';
const cleaned = path.map(x => x.localized).filter(x => !!x); const cleaned = path.map(x => x.localized).defined();
if (cleaned.length > 0) { if (cleaned.length > 0) {
title = cleaned.join(separator || ' | '); title = cleaned.join(separator || ' | ');

10
frontend/src/app/framework/state.ts

@ -30,13 +30,9 @@ export class Model<T> {
const clone = Object.assign(Object.create(Object.getPrototypeOf(this)), this); const clone = Object.assign(Object.create(Object.getPrototypeOf(this)), this);
for (const key in values) { for (const [key, value] of Object.entries(values)) {
if (values.hasOwnProperty(key)) { if (value || !validOnly) {
const value = values[key]; clone[key] = value;
if (value || !validOnly) {
clone[key] = value;
}
} }
} }

21
frontend/src/app/framework/utils/array-extensions.spec.ts

@ -129,6 +129,27 @@ describe('ArrayExtensions', () => {
expect(array_1).toEqual([]); expect(array_1).toEqual([]);
}); });
it('should returned defined only', () => {
const array_0 = [0, 1, 2, false, '', null, undefined];
const array_1 = array_0.defined();
expect(array_1).toEqual([1, 2]);
});
it('should return true if array include item', () => {
const array_0 = [0, 1, 2];
expect(array_0.includes(0)).toBeTrue();
expect(array_0.includes(1)).toBeTrue();
});
it('should return false if array does not include item', () => {
const array_0 = [0, 1, 2];
expect(array_0.includes(3)).toBeFalse();
expect(array_0.includes(4)).toBeFalse();
});
it('should convert to map', () => { it('should convert to map', () => {
const array_0 = [{ id: 'A', value: 1 }, { id: 'B', value: 2 }, { id: 'B', value: 3 }]; const array_0 = [{ id: 'A', value: 1 }, { id: 'B', value: 2 }, { id: 'B', value: 3 }];
const map = array_0.toMap(x => x.id); const map = array_0.toMap(x => x.id);

22
frontend/src/app/framework/utils/array-extensions.ts

@ -14,10 +14,14 @@ interface ReadonlyArray<T> {
removed(value?: T): ReadonlyArray<T>; removed(value?: T): ReadonlyArray<T>;
defined(): ReadonlyArray<NonNullable<T>>;
sorted(): ReadonlyArray<T>; sorted(): ReadonlyArray<T>;
sortedByString(selector: (value: T) => string): ReadonlyArray<T>; sortedByString(selector: (value: T) => string): ReadonlyArray<T>;
includes(value: T): boolean;
toMap(selector: (value: T) => string): { [key: string]: T }; toMap(selector: (value: T) => string): { [key: string]: T };
} }
@ -32,16 +36,20 @@ interface Array<T> {
removeBy(field: keyof T, value: T): Array<T>; removeBy(field: keyof T, value: T): Array<T>;
removed(value: T): ReadonlyArray<T>; removed(value: T): Array<T>;
remove(value: T): Array<T>; remove(value: T): Array<T>;
defined(): ReadonlyArray<NonNullable<T>>;
sorted(): ReadonlyArray<T>; sorted(): ReadonlyArray<T>;
sortedByString(selector: (value: T) => string): ReadonlyArray<T>; sortedByString(selector: (value: T) => string): ReadonlyArray<T>;
sortByString(selector: (value: T) => string): Array<T>; sortByString(selector: (value: T) => string): Array<T>;
includes(value: T): boolean;
toMap(selector: (value: T) => string): { [key: string]: T }; toMap(selector: (value: T) => string): { [key: string]: T };
} }
@ -149,6 +157,18 @@ Array.prototype.sorted = function() {
return copy; return copy;
}; };
Array.prototype.defined = function() {
const self: any[] = this;
return self.filter(x => !!x);
}
Array.prototype.includes = function<T>(value: T) {
const self: any[] = this;
return self.indexOf(value) >= 0;
}
Array.prototype.sortedByString = function<T>(selector: (value: T) => string) { Array.prototype.sortedByString = function<T>(selector: (value: T) => string) {
const self: ReadonlyArray<any> = this; const self: ReadonlyArray<any> = this;

4
frontend/src/app/framework/utils/error.ts

@ -21,11 +21,11 @@ export class ErrorDetailsDto {
if (propertySeparator > 0 && propertySeparator < originalMessage.length - 1) { if (propertySeparator > 0 && propertySeparator < originalMessage.length - 1) {
this.properties = this.properties =
originalMessage originalMessage
.substr(0, propertySeparator) .substring(0, propertySeparator)
.split(', ') .split(', ')
.map(x => x.trim()).filter(x => x.length > 0); .map(x => x.trim()).filter(x => x.length > 0);
this.message = originalMessage.substr(propertySeparator + 2); this.message = originalMessage.substring(propertySeparator + 2);
} else { } else {
this.message = originalMessage; this.message = originalMessage;
} }

2
frontend/src/app/framework/utils/math-helper.ts

@ -146,7 +146,7 @@ export module MathHelper {
} }
if (value.charAt(0) === '#') { if (value.charAt(0) === '#') {
value = value.substr(1, 7); value = value.substring(1, 7);
} }
value = value.replace(/ /g, '').toLowerCase(); value = value.replace(/ /g, '').toLowerCase();

2
frontend/src/app/framework/utils/string-helper.ts

@ -21,7 +21,7 @@ export module StringHelper {
} }
export function appendToUrl(url: string, key: string, value?: any, ambersand = false) { export function appendToUrl(url: string, key: string, value?: any, ambersand = false) {
if (url.indexOf('?') >= 0 || ambersand) { if (url.includes('?') || ambersand) {
url += '&'; url += '&';
} else { } else {
url += '?'; url += '?';

16
frontend/src/app/framework/utils/types.ts

@ -88,11 +88,9 @@ export module Types {
} }
if (Types.isObject(value)) { if (Types.isObject(value)) {
for (const key in value) { for (const item of Object.values(value)) {
if (value.hasOwnProperty(key)) { if (!isEmpty(item)) {
if (!isEmpty(value[key])) { return false;
return false;
}
} }
} }
@ -116,8 +114,6 @@ export module Types {
} }
export function clone<T>(lhs: T): T { export function clone<T>(lhs: T): T {
const any: any = lhs;
if (Types.isArray(lhs)) { if (Types.isArray(lhs)) {
const result = []; const result = [];
@ -129,10 +125,8 @@ export module Types {
} else if (Types.isObject(lhs)) { } else if (Types.isObject(lhs)) {
const result = {}; const result = {};
for (const key in any) { for (const [key, value] of Object.entries(lhs)) {
if (any.hasOwnProperty(key)) { result[key] = clone(value);
result[key] = clone(lhs[key]);
}
} }
return result as any; return result as any;

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

@ -23,7 +23,7 @@ export class Version {
private trimmed(): string { private trimmed(): string {
if (this.value.startsWith('W/')) { if (this.value.startsWith('W/')) {
return this.value.substr(2); return this.value.substring(2);
} else { } else {
return this.value; return this.value;
} }

4
frontend/src/app/shared/components/assets/pipes.ts

@ -69,7 +69,7 @@ export class FileIconPipe implements PipeTransform {
if (mimeParts.length === 2 && mimeParts[0].toLowerCase() === 'video') { if (mimeParts.length === 2 && mimeParts[0].toLowerCase() === 'video') {
mimeIcon = 'video'; mimeIcon = 'video';
} else { } else {
mimeIcon = KNOWN_TYPES.indexOf(asset.fileType) >= 0 ? asset.fileType : 'generic'; mimeIcon = KNOWN_TYPES.includes(asset.fileType) ? asset.fileType : 'generic';
} }
return `./images/asset_${mimeIcon}.svg`; return `./images/asset_${mimeIcon}.svg`;
@ -82,7 +82,7 @@ export class FileIconPipe implements PipeTransform {
}) })
export class PreviewableType implements PipeTransform { export class PreviewableType implements PipeTransform {
public transform(asset: { fileSize: number; fileType: string }): boolean { public transform(asset: { fileSize: number; fileType: string }): boolean {
return PREVIEW_TYPES.indexOf(asset.fileType) >= 0 && asset.fileSize < 25_000_000; return PREVIEW_TYPES.includes(asset.fileType) && asset.fileSize < 25_000_000;
} }
} }

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

@ -5,49 +5,47 @@
* Copyright (c) Squidex UG (haftungsbeschränkt). All rights reserved. * Copyright (c) Squidex UG (haftungsbeschränkt). All rights reserved.
*/ */
import { Directive, ElementRef, Input, OnChanges, Pipe, PipeTransform, Renderer2 } from '@angular/core'; import { Directive, ElementRef, Input, OnChanges, OnDestroy, OnInit, Pipe, PipeTransform, Renderer2 } from '@angular/core';
import { ContentDto, MetaFields, RootFieldDto, TableField, Types } from '@app/shared/internal'; import { ResourceOwner } from '@app/framework';
import { RootFieldDto, TableSettings } from '@app/shared';
import { ContentDto, MetaFields, TableField, FieldSizes, Types } from '@app/shared/internal';
export function getTableWidth(fields: ReadonlyArray<TableField>) { export function getCellWidth(field: TableField, sizes: FieldSizes | undefined | null) {
let result = 0; if (Types.is(field, RootFieldDto)) {
field = field.name;
for (const field of fields) {
result += getCellWidth(field);
} }
return result; const size = sizes?.[field] || 0;
}
export function getCellWidth(field: TableField) { if (size > 0) {
if (Types.is(field, RootFieldDto)) { return size;
return 220; }
} else {
switch (field) {
case MetaFields.id:
return 280;
case MetaFields.created:
return 150;
case MetaFields.createdByAvatar:
return 55;
case MetaFields.createdByName:
return 150;
case MetaFields.lastModified:
return 150;
case MetaFields.lastModifiedByAvatar:
return 55;
case MetaFields.lastModifiedByName:
return 150;
case MetaFields.status:
return 200;
case MetaFields.statusNext:
return 240;
case MetaFields.statusColor:
return 50;
case MetaFields.version:
return 80;
}
return 0; switch (field) {
case MetaFields.id:
return 280;
case MetaFields.created:
return 150;
case MetaFields.createdByAvatar:
return 55;
case MetaFields.createdByName:
return 150;
case MetaFields.lastModified:
return 150;
case MetaFields.lastModifiedByAvatar:
return 55;
case MetaFields.lastModifiedByName:
return 150;
case MetaFields.status:
return 200;
case MetaFields.statusNext:
return 240;
case MetaFields.statusColor:
return 50;
case MetaFields.version:
return 80;
default:
return 200;
} }
} }
@ -67,40 +65,230 @@ export class ContentsColumnsPipe implements PipeTransform {
} }
} }
@Pipe({ @Directive({
name: 'sqxContentListWidth', selector: '[sqxContentListWidth]',
pure: true,
}) })
export class ContentListWidthPipe implements PipeTransform { export class ContentListWidthDirective extends ResourceOwner implements OnChanges {
public transform(value: ReadonlyArray<TableField>) { private sizes?: FieldSizes;
if (!value) { private size = -1;
return 0;
@Input('sqxContentListWidth')
public fields!: ReadonlyArray<TableField>;
@Input('fields')
public set tableSettings(value: TableSettings | undefined | null) {
this.unsubscribeAll();
this.own(value?.fieldSizes.subscribe(sizes => {
this.sizes = sizes;
this.updateSize();
}));
}
constructor(
private readonly element: ElementRef,
private readonly renderer: Renderer2,
) {
super();
}
public ngOnChanges() {
this.updateSize();
}
private updateSize() {
if (!this.fields) {
return;
} }
let size = 100;
for (const field of this.fields) {
size += getCellWidth(field, this.sizes);
}
if (size === this.size) {
return;
}
const width = `${size}px`;
this.renderer.setStyle(this.element.nativeElement, 'min-width', width);
return `${getTableWidth(value) + 100}px`; this.size === size;
} }
} }
@Directive({ @Directive({
selector: '[sqxContentListCell]', selector: '[sqxContentListCell]',
}) })
export class ContentListCellDirective implements OnChanges { export class ContentListCellDirective extends ResourceOwner implements OnChanges {
@Input('sqxContentListCell') private sizes?: FieldSizes;
public field!: TableField; private size = -1;
private fieldName?: string;
@Input()
public set field(value: TableField) {
this.fieldName = Types.is(value, RootFieldDto) ? value.name : value;
}
@Input('fields')
public set tableFields(value: TableSettings | undefined | null) {
this.unsubscribeAll();
this.own(value?.fieldSizes.subscribe(sizes => {
this.sizes = sizes;
this.updateSize();
}));
}
constructor( constructor(
private readonly element: ElementRef, private readonly element: ElementRef,
private readonly renderer: Renderer2, private readonly renderer: Renderer2,
) { ) {
super();
} }
public ngOnChanges() { public ngOnChanges() {
if (Types.isString(this.field) && this.field) { this.updateSize();
const width = `${getCellWidth(this.field)}px`; }
private updateSize() {
if (!this.fieldName) {
return;
}
const size = getCellWidth(this.fieldName, this.sizes);
this.renderer.setStyle(this.element.nativeElement, 'min-width', width); if (size === this.size) {
this.renderer.setStyle(this.element.nativeElement, 'max-width', width); return;
this.renderer.setStyle(this.element.nativeElement, 'width', width);
} }
const width = `${size}px`;
this.renderer.setStyle(this.element.nativeElement, 'min-width', width);
this.renderer.setStyle(this.element.nativeElement, 'max-width', width);
this.renderer.setStyle(this.element.nativeElement, 'width', width);
this.size === size;
} }
} }
@Directive({
selector: '[sqxContentListCellResize]',
})
export class ContentListCellResizeDirective implements OnInit, OnDestroy {
private mouseMove?: Function;
private mouseUp?: Function;
private mouseDown?: Function;
private mouseBlur?: Function;
private documentBlur?: Function;
private startOffset = 0;
private startWidth = 0;
private fieldName?: string;
private resizer: any;
@Input()
public set field(value: TableField) {
this.fieldName = Types.is(value, RootFieldDto) ? value.name : undefined;
}
@Input('fields')
public tableFields?: TableSettings;
@Input()
public minimumWidth = 50;
constructor(
private readonly element: ElementRef<HTMLTableCellElement>,
private readonly renderer: Renderer2,
) {
}
public ngOnDestroy() {
this.mouseDown?.();
this.mouseDown = undefined;
this.mouseBlur?.();
this.mouseBlur = undefined;
this.resetMovement();
}
public ngOnInit() {
if (!this.tableFields || !this.fieldName) {
return;
}
this.resizer = this.renderer.createElement('span');
this.renderer.addClass(this.resizer, 'resize-holder');
this.renderer.appendChild(this.element.nativeElement, this.resizer);
this.mouseDown = this.renderer.listen(this.resizer, 'mousedown', this.onMouseDown);
this.mouseBlur = this.renderer.listen(this.resizer, 'blur', this.onMouseUp);
}
private onMouseDown = (event: MouseEvent) => {
if (!this.tableFields || !this.fieldName) {
return;
}
this.resizer.focus();
this.mouseMove = this.renderer.listen('document', 'mousemove', this.onMouseMove);
this.mouseUp = this.renderer.listen('document', 'mouseup', this.onMouseUp);
this.documentBlur = this.renderer.listen('document', 'blur', this.onBlur);
this.startOffset = event.pageX;
this.startWidth = this.element.nativeElement.offsetWidth;
};
private onMouseMove = (event: MouseEvent) => {
if (!this.mouseMove || !this.tableFields || !this.fieldName) {
return;
}
try {
this.updateWidth(event, false);
} catch {
this.resetMovement();
}
};
private onMouseUp = (event: MouseEvent) => {
if (!this.mouseMove || !this.tableFields || !this.fieldName) {
return;
}
try {
this.updateWidth(event, true);
} finally {
this.resetMovement();
}
};
private onBlur = () => {
this.resetMovement();
};
private updateWidth(event: MouseEvent, save: boolean) {
let width = this.startWidth + (event.pageX - this.startOffset);
if (width < this.minimumWidth) {
width = this.minimumWidth;
}
this.tableFields!.updateSize(this.fieldName!, width, save);
}
private resetMovement() {
this.mouseMove?.();
this.mouseMove = undefined;
this.mouseUp?.();
this.mouseUp = undefined;
this.documentBlur?.();
this.documentBlur = undefined;
}
}

2
frontend/src/app/shared/components/contents/content-list-field.component.html

@ -99,7 +99,7 @@
</ng-container> </ng-container>
<ng-template #displayTemplate> <ng-template #displayTemplate>
<sqx-content-value [value]="snapshot.formatted"></sqx-content-value> <sqx-content-value [value]="snapshot.formatted" [field]="field" [fields]="fields"></sqx-content-value>
</ng-template> </ng-template>
</ng-container> </ng-container>
</ng-container> </ng-container>

4
frontend/src/app/shared/components/contents/content-list-field.component.scss

@ -1,6 +1,10 @@
@import 'mixins'; @import 'mixins';
@import 'vars'; @import 'vars';
:host {
display: block;
}
.icon-caret-right { .icon-caret-right {
color: $color-text-decent; color: $color-text-decent;
margin-left: 5px; margin-left: 5px;

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

@ -7,7 +7,7 @@
import { ChangeDetectionStrategy, ChangeDetectorRef, Component, Input, OnChanges } from '@angular/core'; import { ChangeDetectionStrategy, ChangeDetectorRef, Component, Input, OnChanges } from '@angular/core';
import { FormGroup } from '@angular/forms'; import { FormGroup } from '@angular/forms';
import { ContentDto, FieldValue, getContentValue, LanguageDto, MetaFields, RootFieldDto, StatefulComponent, TableField, Types } from '@app/shared/internal'; import { ContentDto, FieldValue, getContentValue, LanguageDto, MetaFields, RootFieldDto, StatefulComponent, TableField, TableSettings, Types } from '@app/shared/internal';
interface State { interface State {
// The formatted value. // The formatted value.
@ -24,6 +24,9 @@ export class ContentListFieldComponent extends StatefulComponent<State> implemen
@Input() @Input()
public field!: TableField; public field!: TableField;
@Input()
public fields?: TableSettings;
@Input() @Input()
public content!: ContentDto; public content!: ContentDto;

10
frontend/src/app/shared/components/contents/content-value.component.html

@ -1,7 +1,11 @@
<ng-container *ngIf="isPlain; else html"> <ng-container *ngIf="isPlain; else html">
<span> <div class="value-container">
<span class="truncate">{{value}}</span> <div #valueElement [class.truncate]="!wrapping">{{value}}</div>
</span> </div>
<button class="btn btn-icon" *ngIf="isString && fields" (click)="toggle()" sqxStopClick>
<i class="icon-wrap_text"></i>
</button>
</ng-container> </ng-container>
<ng-template #html> <ng-template #html>
<div class="html-value" [innerHTML]="value.html"></div> <div class="html-value" [innerHTML]="value.html"></div>

38
frontend/src/app/shared/components/contents/content-value.component.scss

@ -1,10 +1,30 @@
@import 'mixins'; @import 'mixins';
@import 'vars'; @import 'vars';
/* stylelint-disable */
.html-value { .html-value {
position: relative; position: relative;
} }
:host {
display: block;
.btn {
@include absolute(.5rem, .25rem);
background-color: $color-white;
border: 1px solid $color-border;
border-radius: $border-radius;
display: none;
}
&:hover {
.btn {
display: block;
}
}
}
:host ::ng-deep { :host ::ng-deep {
img { img {
margin-top: -25px; margin-top: -25px;
@ -14,11 +34,21 @@
top: 50%; top: 50%;
} }
span {
@include truncate;
}
img + span { img + span {
padding-left: 60px; padding-left: 60px;
} }
.html-value {
span {
@include truncate;
}
}
}
.value-container {
max-height: 10rem;
overflow-x: hidden;
overflow-y: auto;
position: relative;
word-wrap: break-word;
} }

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

@ -5,8 +5,10 @@
* 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, ChangeDetectorRef, Component, Input, OnChanges, SimpleChanges } from '@angular/core';
import { HtmlValue, Types } from '@app/shared/internal'; import { ResourceOwner } from '@app/framework';
import { RootFieldDto, TableField } from '@app/shared';
import { FieldWrappings, HtmlValue, TableSettings, Types } from '@app/shared/internal';
@Component({ @Component({
selector: 'sqx-content-value[value]', selector: 'sqx-content-value[value]',
@ -14,11 +16,56 @@ import { HtmlValue, Types } from '@app/shared/internal';
templateUrl: './content-value.component.html', templateUrl: './content-value.component.html',
changeDetection: ChangeDetectionStrategy.OnPush, changeDetection: ChangeDetectionStrategy.OnPush,
}) })
export class ContentValueComponent { export class ContentValueComponent extends ResourceOwner implements OnChanges {
@Input() @Input()
public value!: any; public value!: any;
@Input()
public field!: TableField;
@Input()
public fields?: TableSettings;
public wrapping = false;
public get isString() {
return Types.is(this.field, RootFieldDto) && this.field.properties.fieldType === 'String';
}
public get isPlain() { public get isPlain() {
return !Types.is(this.value, HtmlValue); return !Types.is(this.value, HtmlValue);
} }
constructor(
private readonly changeDetector: ChangeDetectorRef,
) {
super();
}
public ngOnChanges(changes: SimpleChanges) {
if (changes['fields']) {
this.unsubscribeAll();
this.own(this.fields?.fieldWrappings
.subscribe(wrappings => {
this.updateWrapping(wrappings);
}));
}
}
public toggle() {
this.fields?.toggleWrapping(this.field?.['name']);
}
private updateWrapping(wrappings: FieldWrappings) {
const wrapping = wrappings[this.field?.['name']];
if (wrapping === this.wrapping) {
return;
}
this.wrapping = wrapping;
this.changeDetector.detectChanges();
}
} }

8
frontend/src/app/shared/components/history/history.component.ts

@ -41,12 +41,8 @@ export class HistoryComponent {
if (channel) { if (channel) {
const params = allParams(this.route); const params = allParams(this.route);
for (const key in params) { for (const [key, value] of Object.entries(params)) {
if (params.hasOwnProperty(key)) { channel = channel.replace(`{${key}}`, value);
const value = params[key];
channel = channel.replace(`{${key}}`, value);
}
} }
} }

2
frontend/src/app/shared/components/notifo.component.ts

@ -57,7 +57,7 @@ export class NotifoComponent implements AfterViewInit, OnChanges, OnDestroy {
const options: any = { apiUrl: this.notifoApiUrl, userToken: this.notifoApiKey }; const options: any = { apiUrl: this.notifoApiUrl, userToken: this.notifoApiKey };
if (this.notifoApiUrl.indexOf('localhost:5002') >= 0) { if (this.notifoApiUrl.includes('localhost:5002')) {
options.styleUrl = 'https://localhost:3002/notifo-sdk.css'; options.styleUrl = 'https://localhost:3002/notifo-sdk.css';
} }

4
frontend/src/app/shared/components/pipes.ts

@ -182,8 +182,8 @@ function split(token: string) {
const index = token.indexOf(':'); const index = token.indexOf(':');
if (index > 0 && index < token.length - 1) { if (index > 0 && index < token.length - 1) {
const type = token.substr(0, index); const type = token.substring(0, index);
const name = token.substr(index + 1); const name = token.substring(index + 1);
return { type, id: name }; return { type, id: name };
} }

2
frontend/src/app/shared/components/references/content-selector-item.component.html

@ -11,7 +11,7 @@
<sqx-content-list-field [language]="language" field="meta.lastModifiedBy.avatar" [content]="content"></sqx-content-list-field> <sqx-content-list-field [language]="language" field="meta.lastModifiedBy.avatar" [content]="content"></sqx-content-list-field>
</td> </td>
<td *ngFor="let field of schema.defaultReferenceFields" [sqxContentListCell]="field"> <td *ngFor="let field of schema.defaultReferenceFields" sqxContentListCell [field]="field">
<sqx-content-list-field [field]="field" [content]="content" [language]="language"></sqx-content-list-field> <sqx-content-list-field [field]="field" [content]="content" [language]="language"></sqx-content-list-field>
</td> </td>

6
frontend/src/app/shared/components/references/content-selector.component.html

@ -41,7 +41,7 @@
<ng-container *ngIf="schema"> <ng-container *ngIf="schema">
<sqx-list-view [isLoading]="contentsState.isLoading | async" [syncedHeader]="true" [table]="true"> <sqx-list-view [isLoading]="contentsState.isLoading | async" [syncedHeader]="true" [table]="true">
<ng-container header> <ng-container header>
<table class="table table-items table-fixed" [style.minWidth]="schema.defaultReferenceFields | sqxContentListWidth" #header> <table class="table table-items table-fixed" [sqxContentListWidth]="schema.defaultReferenceFields" #header>
<thead> <thead>
<tr> <tr>
<th class="cell-select"> <th class="cell-select">
@ -52,7 +52,7 @@
<th sqxContentListCell="meta.lastModifiedBy.avatar"> <th sqxContentListCell="meta.lastModifiedBy.avatar">
<sqx-content-list-header [language]="language" field="meta.lastModifiedBy.avatar"></sqx-content-list-header> <sqx-content-list-header [language]="language" field="meta.lastModifiedBy.avatar"></sqx-content-list-header>
</th> </th>
<th *ngFor="let field of schema.defaultReferenceFields" [sqxContentListCell]="field"> <th *ngFor="let field of schema.defaultReferenceFields" sqxContentListCell [field]="field">
<sqx-content-list-header <sqx-content-list-header
[field]="field" [field]="field"
[query]="(contentsState.query | async)!" [query]="(contentsState.query | async)!"
@ -70,7 +70,7 @@
<ng-container content> <ng-container content>
<div class="table-container"> <div class="table-container">
<table class="table table-items table-fixed" [style.minWidth]="schema.defaultReferenceFields | sqxContentListWidth" *ngIf="contentsState.contents | async; let contents" [sqxSyncWidth]="header"> <table class="table table-items table-fixed" [sqxContentListWidth]="schema.defaultReferenceFields" *ngIf="contentsState.contents | async; let contents" [sqxSyncWidth]="header">
<tbody *ngFor="let content of contents; trackBy: trackByContent" <tbody *ngFor="let content of contents; trackBy: trackByContent"
[sqxContentSelectorItem]="content" [sqxContentSelectorItem]="content"
[language]="language" [language]="language"

2
frontend/src/app/shared/components/references/content-selector.component.ts

@ -81,7 +81,7 @@ export class ContentSelectorComponent extends ResourceOwner implements OnInit {
this.schemas = this.schemasState.snapshot.schemas.filter(x => x.canReadContents); this.schemas = this.schemasState.snapshot.schemas.filter(x => x.canReadContents);
if (this.schemaIds && this.schemaIds.length > 0) { if (this.schemaIds && this.schemaIds.length > 0) {
this.schemas = this.schemas.filter(x => x.canReadContents && this.schemaIds!.indexOf(x.id) >= 0); this.schemas = this.schemas.filter(x => x.canReadContents && this.schemaIds!.includes(x.id));
} }
this.selectSchema(this.schemas[0]); this.selectSchema(this.schemas[0]);

2
frontend/src/app/shared/components/references/reference-input.component.ts

@ -131,7 +131,7 @@ export class ReferenceInputComponent extends StatefulControlComponent<State, Rea
content.referenceFields content.referenceFields
.map(f => getContentValue(content, this.language, f, false)) .map(f => getContentValue(content, this.language, f, false))
.map(v => v.formatted) .map(v => v.formatted)
.filter(v => !!v) .defined()
.join(', ') .join(', ')
|| this.localizer.getOrKey('common.noValue'); || this.localizer.getOrKey('common.noValue');

2
frontend/src/app/shared/components/search/queries/filter-comparison.component.ts

@ -77,7 +77,7 @@ export class FilterComparisonComponent implements OnChanges {
this.operators = this.model.operators[this.field?.schema.type!] || []; this.operators = this.model.operators[this.field?.schema.type!] || [];
if (this.operators.indexOf(this.filter.op) < 0) { if (!this.operators.includes(this.filter.op)) {
this.filter.op = this.operators[0]; this.filter.op = this.operators[0];
} }

2
frontend/src/app/shared/components/table-header.component.scss

@ -2,5 +2,7 @@
@import 'vars'; @import 'vars';
a { a {
text-align: left;
text-decoration: none; text-decoration: none;
color: $color-text;
} }

2
frontend/src/app/shared/guards/schema-must-not-be-singleton.guard.ts

@ -27,7 +27,7 @@ export class SchemaMustNotBeSingletonGuard implements CanActivate {
take(1), take(1),
tap(schema => { tap(schema => {
if (schema.type === 'Singleton') { if (schema.type === 'Singleton') {
if (state.url.indexOf('/new') >= 0) { if (state.url.includes('/new')) {
const parentUrl = state.url.slice(0, state.url.indexOf(route.url[route.url.length - 1].path)); const parentUrl = state.url.slice(0, state.url.indexOf(route.url[route.url.length - 1].path));
this.router.navigate([parentUrl, schema.id]); this.router.navigate([parentUrl, schema.id]);

2
frontend/src/app/shared/internal.ts

@ -69,7 +69,7 @@ export * from './state/schema-tag-source';
export * from './state/schemas.forms'; export * from './state/schemas.forms';
export * from './state/schemas.state'; export * from './state/schemas.state';
export * from './state/settings'; export * from './state/settings';
export * from './state/table-fields'; export * from './state/table-settings';
export * from './state/ui-languages'; export * from './state/ui-languages';
export * from './state/ui.state'; export * from './state/ui.state';
export * from './state/workflows.forms'; export * from './state/workflows.forms';

8
frontend/src/app/shared/module.ts

@ -12,7 +12,7 @@ import { RouterModule } from '@angular/router';
import { MentionModule } from 'angular-mentions'; import { MentionModule } from 'angular-mentions';
import { NgxDocViewerModule } from 'ngx-doc-viewer'; import { NgxDocViewerModule } from 'ngx-doc-viewer';
import { SqxFrameworkModule } from '@app/framework'; import { SqxFrameworkModule } from '@app/framework';
import { AppFormComponent, AppLanguagesService, AppMustExistGuard, AppsService, AppsState, AssetComponent, AssetDialogComponent, AssetFolderComponent, AssetFolderDialogComponent, AssetFolderDropdownComponent, AssetFolderDropdownItemComponent, AssetHistoryComponent, AssetPathComponent, AssetPreviewUrlPipe, AssetScriptsState, AssetsListComponent, AssetsSelectorComponent, AssetsService, AssetsState, AssetTextEditorComponent, AssetUploaderComponent, AssetUploaderState, AssetUrlPipe, AuthInterceptor, AuthService, AutoSaveService, BackupsService, BackupsState, ClientsService, ClientsState, CommentComponent, CommentsComponent, CommentsService, ContentListCellDirective, ContentListFieldComponent, ContentListHeaderComponent, ContentListWidthPipe, ContentMustExistGuard, ContentsColumnsPipe, ContentSelectorComponent, ContentSelectorItemComponent, ContentsService, ContentsState, ContentStatusComponent, ContentValueComponent, ContentValueEditorComponent, ContributorsService, ContributorsState, FileIconPipe, FilterComparisonComponent, FilterLogicalComponent, FilterNodeComponent, FilterOperatorPipe, GeolocationEditorComponent, GraphQlService, HelpComponent, HelpMarkdownPipe, HelpService, HistoryComponent, HistoryListComponent, HistoryMessagePipe, HistoryService, ImageCropperComponent, ImageFocusPointComponent, LanguagesService, LanguagesState, LoadAppsGuard, LoadLanguagesGuard, LoadSchemasGuard, MarkdownEditorComponent, MustBeAuthenticatedGuard, MustBeNotAuthenticatedGuard, NewsService, NotifoComponent, PlansService, PlansState, PreviewableType, QueryComponent, QueryListComponent, QueryPathComponent, ReferenceInputComponent, RichEditorComponent, RolesService, RolesState, RuleEventsState, RuleMustExistGuard, RuleSimulatorState, RulesService, RulesState, SavedQueriesComponent, SchemaCategoryComponent, SchemaMustExistGuard, SchemaMustExistPublishedGuard, SchemaMustNotBeSingletonGuard, SchemasService, SchemasState, SchemaTagSource, SearchFormComponent, SearchService, SortingComponent, StockPhotoService, TableHeaderComponent, TranslationsService, UIService, UIState, UnsetAppGuard, UsagesService, UserDtoPicture, UserIdPicturePipe, UserNamePipe, UserNameRefPipe, UserPicturePipe, UserPictureRefPipe, UsersProviderService, UsersService, WatchingUsersComponent, WorkflowsService, WorkflowsState } from './declarations'; import { AppFormComponent, AppLanguagesService, AppMustExistGuard, AppsService, AppsState, AssetComponent, AssetDialogComponent, AssetFolderComponent, AssetFolderDialogComponent, AssetFolderDropdownComponent, AssetFolderDropdownItemComponent, AssetHistoryComponent, AssetPathComponent, AssetPreviewUrlPipe, AssetScriptsState, AssetsListComponent, AssetsSelectorComponent, AssetsService, AssetsState, AssetTextEditorComponent, AssetUploaderComponent, AssetUploaderState, AssetUrlPipe, AuthInterceptor, AuthService, AutoSaveService, BackupsService, BackupsState, ClientsService, ClientsState, CommentComponent, CommentsComponent, CommentsService, ContentListCellDirective, ContentListCellResizeDirective, ContentListFieldComponent, ContentListHeaderComponent, ContentListWidthDirective, ContentMustExistGuard, ContentsColumnsPipe, ContentSelectorComponent, ContentSelectorItemComponent, ContentsService, ContentsState, ContentStatusComponent, ContentValueComponent, ContentValueEditorComponent, ContributorsService, ContributorsState, FileIconPipe, FilterComparisonComponent, FilterLogicalComponent, FilterNodeComponent, FilterOperatorPipe, GeolocationEditorComponent, GraphQlService, HelpComponent, HelpMarkdownPipe, HelpService, HistoryComponent, HistoryListComponent, HistoryMessagePipe, HistoryService, ImageCropperComponent, ImageFocusPointComponent, LanguagesService, LanguagesState, LoadAppsGuard, LoadLanguagesGuard, LoadSchemasGuard, MarkdownEditorComponent, MustBeAuthenticatedGuard, MustBeNotAuthenticatedGuard, NewsService, NotifoComponent, PlansService, PlansState, PreviewableType, QueryComponent, QueryListComponent, QueryPathComponent, ReferenceInputComponent, RichEditorComponent, RolesService, RolesState, RuleEventsState, RuleMustExistGuard, RuleSimulatorState, RulesService, RulesState, SavedQueriesComponent, SchemaCategoryComponent, SchemaMustExistGuard, SchemaMustExistPublishedGuard, SchemaMustNotBeSingletonGuard, SchemasService, SchemasState, SchemaTagSource, SearchFormComponent, SearchService, SortingComponent, StockPhotoService, TableHeaderComponent, TranslationsService, UIService, UIState, UnsetAppGuard, UsagesService, UserDtoPicture, UserIdPicturePipe, UserNamePipe, UserNameRefPipe, UserPicturePipe, UserPictureRefPipe, UsersProviderService, UsersService, WatchingUsersComponent, WorkflowsService, WorkflowsState } from './declarations';
@NgModule({ @NgModule({
imports: [ imports: [
@ -41,9 +41,10 @@ import { AppFormComponent, AppLanguagesService, AppMustExistGuard, AppsService,
CommentComponent, CommentComponent,
CommentsComponent, CommentsComponent,
ContentListCellDirective, ContentListCellDirective,
ContentListCellResizeDirective,
ContentListFieldComponent, ContentListFieldComponent,
ContentListHeaderComponent, ContentListHeaderComponent,
ContentListWidthPipe, ContentListWidthDirective,
ContentsColumnsPipe, ContentsColumnsPipe,
ContentSelectorComponent, ContentSelectorComponent,
ContentSelectorItemComponent, ContentSelectorItemComponent,
@ -100,9 +101,10 @@ import { AppFormComponent, AppLanguagesService, AppMustExistGuard, AppsService,
CommentComponent, CommentComponent,
CommentsComponent, CommentsComponent,
ContentListCellDirective, ContentListCellDirective,
ContentListCellResizeDirective,
ContentListFieldComponent, ContentListFieldComponent,
ContentListHeaderComponent, ContentListHeaderComponent,
ContentListWidthPipe, ContentListWidthDirective,
ContentsColumnsPipe, ContentsColumnsPipe,
ContentSelectorComponent, ContentSelectorComponent,
ContentSelectorItemComponent, ContentSelectorItemComponent,

2
frontend/src/app/shared/services/assets.service.ts

@ -51,7 +51,7 @@ export class AssetDto {
const index = this.fileName.lastIndexOf('.'); const index = this.fileName.lastIndexOf('.');
if (index > 0) { if (index > 0) {
return this.fileName.substr(0, index); return this.fileName.substring(0, index);
} else { } else {
return this.fileName; return this.fileName;
} }

8
frontend/src/app/shared/services/contents.service.ts

@ -86,11 +86,9 @@ export class ContentDto {
const updates: StatusInfo[] = []; const updates: StatusInfo[] = [];
for (const link in links) { for (const [key, link] of Object.entries(links)) {
if (links.hasOwnProperty(link) && link.startsWith('status/')) { if (key.startsWith('status/')) {
const status = link.substr(7); updates.push({ status: key.substring(7), color: link.metadata! });
updates.push({ status, color: links[link].metadata! });
} }
} }

10
frontend/src/app/shared/services/schemas.service.ts

@ -141,13 +141,9 @@ export class SchemaDto {
const cleanup = (source: any, ...exclude: string[]): any => { const cleanup = (source: any, ...exclude: string[]): any => {
const clone = {}; const clone = {};
for (const key in source) { for (const [key, value] of Object.entries(source)) {
if (source.hasOwnProperty(key) && exclude.indexOf(key) < 0 && key.indexOf('can') !== 0) { if (!exclude.includes(key) && key.indexOf('can') !== 0 && !Types.isUndefined(value) && !Types.isNull(value)) {
const value = source[key]; clone[key] = value;
if (!Types.isUndefined(value) && !Types.isNull(value)) {
clone[key] = value;
}
} }
} }

22
frontend/src/app/shared/services/workflows.service.ts

@ -286,24 +286,18 @@ function parseWorkflows(response: any) {
function parseWorkflow(workflow: any) { function parseWorkflow(workflow: any) {
const { id, name, initial, schemaIds, _links } = workflow; const { id, name, initial, schemaIds, _links } = workflow;
const steps: WorkflowStep[] = []; const resultSteps: WorkflowStep[] = [];
const transitions: WorkflowTransition[] = []; const resultTransitions: WorkflowTransition[] = [];
for (const stepName in workflow.steps) { for (const [stepName, stepValue] of Object.entries(workflow.steps)) {
if (workflow.steps.hasOwnProperty(stepName)) { const { transitions, ...step } = stepValue as any;
const { transitions: srcTransitions, ...step } = workflow.steps[stepName];
steps.push({ name: stepName, isLocked: stepName === 'Published', ...step }); resultSteps.push({ name: stepName, isLocked: stepName === 'Published', ...step });
for (const to in srcTransitions) { for (const [to, transition] of Object.entries(transitions)) {
if (srcTransitions.hasOwnProperty(to)) { resultTransitions.push({ from: stepName, to, ...transition as any });
const transition = srcTransitions[to];
transitions.push({ from: stepName, to, ...transition });
}
}
} }
} }
return new WorkflowDto(_links, id, name, initial, schemaIds, steps, transitions); return new WorkflowDto(_links, id, name, initial, schemaIds, resultSteps, resultTransitions);
} }

26
frontend/src/app/shared/state/assets.forms.ts

@ -72,7 +72,7 @@ export class AnnotateAssetForm extends Form<ExtendedFormGroup, AnnotateAssetDto,
const index = asset.fileName.lastIndexOf('.'); const index = asset.fileName.lastIndexOf('.');
if (index > 0) { if (index > 0) {
result.fileName += asset.fileName.substr(index); result.fileName += asset.fileName.substring(index);
} }
if (result.fileName === asset.fileName) { if (result.fileName === asset.fileName) {
@ -113,7 +113,7 @@ export class AnnotateAssetForm extends Form<ExtendedFormGroup, AnnotateAssetDto,
const index = fileName.lastIndexOf('.'); const index = fileName.lastIndexOf('.');
if (index > 0) { if (index > 0) {
fileName = fileName.substr(0, index); fileName = fileName.substring(0, index);
} }
result.fileName = fileName; result.fileName = fileName;
@ -122,20 +122,16 @@ export class AnnotateAssetForm extends Form<ExtendedFormGroup, AnnotateAssetDto,
if (Types.isObject(value.metadata)) { if (Types.isObject(value.metadata)) {
result.metadata = []; result.metadata = [];
for (const name in value.metadata) { for (const [name, raw] of Object.entries(value.metadata)) {
if (value.metadata.hasOwnProperty(name)) { let converted = '';
const raw = value.metadata[name];
let converted = ''; if (Types.isString(raw)) {
converted = raw;
if (Types.isString(raw)) { } else if (!Types.isUndefined(raw) && !Types.isNull(raw)) {
converted = raw; converted = JSON.stringify(raw);
} else if (!Types.isUndefined(raw) && !Types.isNull(raw)) {
converted = JSON.stringify(raw);
}
result.metadata.push({ name, value: converted });
} }
result.metadata.push({ name, value: converted });
} }
} }
@ -152,7 +148,7 @@ export class AnnotateAssetForm extends Form<ExtendedFormGroup, AnnotateAssetDto,
const index = asset.fileName.lastIndexOf('.'); const index = asset.fileName.lastIndexOf('.');
if (index > 0) { if (index > 0) {
slug += asset.fileName.substr(index); slug += asset.fileName.substring(index);
} }
} }

8
frontend/src/app/shared/state/assets.state.ts

@ -69,10 +69,10 @@ export abstract class AssetsStateBase extends State<Snapshot> {
this.projectFrom(this.tagsUnsorted, getSortedTags); this.projectFrom(this.tagsUnsorted, getSortedTags);
public tagsNames = public tagsNames =
this.projectFrom(this.tagsUnsorted, x => Object.keys(x)); this.projectFrom(this.tagsUnsorted, getTagNames);
public selectedTagNames = public selectedTagNames =
this.projectFrom(this.tagsSelected, x => Object.keys(x)); this.projectFrom(this.tagsSelected, getTagNames);
public assets = public assets =
this.project(x => x.assets); this.project(x => x.assets);
@ -424,6 +424,10 @@ export abstract class AssetsStateBase extends State<Snapshot> {
} }
} }
function getTagNames(tags: object): ReadonlyArray<string> {
return Object.keys(tags);
}
function isReferrerError(error?: ErrorDto) { function isReferrerError(error?: ErrorDto) {
return error?.errorCode === 'OBJECT_REFERENCED'; return error?.errorCode === 'OBJECT_REFERENCED';
} }

2
frontend/src/app/shared/state/contents.form-rules.ts

@ -92,7 +92,7 @@ class ComponentRules implements ComponentRules {
return EMPTY_RULES; return EMPTY_RULES;
} }
const pathField = form.fieldPath.substr(this.parentPath.length + 1); const pathField = form.fieldPath.substring(this.parentPath.length + 1);
const pathSimplified = pathField.replace('.iv.', '.'); const pathSimplified = pathField.replace('.iv.', '.');
return rules.filter(x => x.field === pathField || x.field === pathSimplified); return rules.filter(x => x.field === pathField || x.field === pathSimplified);

4
frontend/src/app/shared/state/contents.state.ts

@ -79,7 +79,7 @@ export abstract class ContentsStateBase extends State<Snapshot> {
this.project(x => x.statuses); this.project(x => x.statuses);
public statusQueries = public statusQueries =
this.projectFrom(this.statuses, buildStatusQueries); this.projectFrom(this.statuses, getStatusQueries);
public get appName() { public get appName() {
return this.appsState.appName; return this.appsState.appName;
@ -448,7 +448,7 @@ export class ComponentContentsState extends ContentsStateBase {
} }
} }
function buildStatusQueries(statuses: ReadonlyArray<StatusInfo> | undefined): ReadonlyArray<SavedQuery> { function getStatusQueries(statuses: ReadonlyArray<StatusInfo> | undefined): ReadonlyArray<SavedQuery> {
return statuses?.map(buildStatusQuery) || []; return statuses?.map(buildStatusQuery) || [];
} }

8
frontend/src/app/shared/state/contributors.state.ts

@ -12,7 +12,7 @@ import { DialogService, ErrorDto, getPagingInfo, ListState, shareMapSubscribed,
import { AssignContributorDto, ContributorDto, ContributorsPayload, ContributorsService } from './../services/contributors.service'; import { AssignContributorDto, ContributorDto, ContributorsPayload, ContributorsService } from './../services/contributors.service';
import { AppsState } from './apps.state'; import { AppsState } from './apps.state';
interface Snapshot extends ListState { interface Snapshot extends ListState<string> {
// The current contributors. // The current contributors.
contributors: ReadonlyArray<ContributorDto>; contributors: ReadonlyArray<ContributorDto>;
@ -38,7 +38,7 @@ export class ContributorsState extends State<Snapshot> {
this.project(x => x.query); this.project(x => x.query);
public queryRegex = public queryRegex =
this.projectFrom(this.query, x => (x ? new RegExp(x, 'i') : undefined)); this.projectFrom(this.query, x => getRegex(x));
public maxContributors = public maxContributors =
this.project(x => x.maxContributors); this.project(x => x.maxContributors);
@ -159,6 +159,10 @@ export class ContributorsState extends State<Snapshot> {
} }
} }
function getRegex(query?: string): RegExp | undefined {
return query ? new RegExp(query, 'i') : undefined;
}
function getFilteredContributors(snapshot: Snapshot) { function getFilteredContributors(snapshot: Snapshot) {
const { contributors, query, page, pageSize } = snapshot; const { contributors, query, page, pageSize } = snapshot;

7
frontend/src/app/shared/state/languages.state.ts

@ -192,13 +192,10 @@ export class LanguagesState extends State<Snapshot> {
language, language,
fallbackLanguages: fallbackLanguages:
language.fallback language.fallback
.map(l => languages.find(x => x.iso2Code === l)).filter(x => !!x) .map(l => languages.find(x => x.iso2Code === l)).defined(),
.map(l => l!),
fallbackLanguagesNew: fallbackLanguagesNew:
languages languages
.filter(l => .filter(l => language.iso2Code !== l.iso2Code && !language.fallback.includes(l.iso2Code))
language.iso2Code !== l.iso2Code &&
language.fallback.indexOf(l.iso2Code) < 0)
.sortByString(x => x.englishName), .sortByString(x => x.englishName),
}; };
} }

2
frontend/src/app/shared/state/resolvers.ts

@ -58,7 +58,7 @@ abstract class ResolverBase<T extends { id: string }, TResult extends { items: R
private async buildPromise(promises: Promise<T | undefined>[]) { private async buildPromise(promises: Promise<T | undefined>[]) {
const promise = await Promise.all(promises); const promise = await Promise.all(promises);
return this.createResult(promise.filter(x => !!x) as any); return this.createResult(promise.defined() as any);
} }
private resolvePending() { private resolvePending() {

6
frontend/src/app/shared/state/schemas.state.ts

@ -157,7 +157,7 @@ export class SchemasState extends State<Snapshot> {
return this.schemasService.deleteSchema(this.appName, schema, schema.version).pipe( return this.schemasService.deleteSchema(this.appName, schema, schema.version).pipe(
tap(() => { tap(() => {
this.next(s => { this.next(s => {
const schemas = s.schemas.filter(x => x.id !== schema.id); const schemas = s.schemas.removedBy('id', schema);
const selectedSchema = const selectedSchema =
s.selectedSchema?.id !== schema.id ? s.selectedSchema?.id !== schema.id ?
@ -462,7 +462,7 @@ export function getCategoryTree(allSchemas: ReadonlyArray<SchemaDto>, categories
const lastSeparatorIndex = name.lastIndexOf(NESTED_CATEGORY_SEPARATOR); const lastSeparatorIndex = name.lastIndexOf(NESTED_CATEGORY_SEPARATOR);
if (lastSeparatorIndex >= 0) { if (lastSeparatorIndex >= 0) {
displayName = displayName.substr(lastSeparatorIndex + 1); displayName = displayName.substring(lastSeparatorIndex + 1);
} }
category = { category = {
@ -479,7 +479,7 @@ export function getCategoryTree(allSchemas: ReadonlyArray<SchemaDto>, categories
if (lastSeparatorIndex >= 0) { if (lastSeparatorIndex >= 0) {
// Recurse back creating all the parents of this category // Recurse back creating all the parents of this category
const parentName = name.substr(0, lastSeparatorIndex); const parentName = name.substring(0, lastSeparatorIndex);
const parentCategory = getOrCreateCategory(parentName); const parentCategory = getOrCreateCategory(parentName);
parentCategory.categories.push(category); parentCategory.categories.push(category);

131
frontend/src/app/shared/state/table-fields.spec.ts

@ -1,131 +0,0 @@
/*
* Squidex Headless CMS
*
* @license
* Copyright (c) Squidex UG (haftungsbeschränkt). All rights reserved.
*/
import { of } from 'rxjs';
import { IMock, Mock, Times } from 'typemoq';
import { DateTime, Version } from '@app/framework';
import { createProperties, MetaFields, RootFieldDto, SchemaDto, TableField, TableFields, UIState } from '@app/shared/internal';
describe('TableFields', () => {
let uiState: IMock<UIState>;
const schema =
new SchemaDto({},
'1',
DateTime.now(), 'me',
DateTime.now(), 'me',
new Version('1'),
'my-schema',
'my-category',
'Default',
false,
{},
[
new RootFieldDto({}, 1, 'string', createProperties('String'), 'invariant'),
]);
beforeEach(() => {
uiState = Mock.ofType<UIState>();
});
const INVALID_CONFIGS = [
{ case: 'empty', fields: [] },
{ case: 'invalid', fields: ['invalid'] },
];
INVALID_CONFIGS.forEach(test => {
it(`should provide default fields if config is ${test.case}`, async () => {
let fields: ReadonlyArray<TableField>;
let fieldNames: ReadonlyArray<string>;
uiState.setup(x => x.getUser<string[]>('schemas.my-schema.view', []))
.returns(() => of(test.fields));
const tableFields = new TableFields(uiState.object, schema);
tableFields.listFields.subscribe(result => {
fields = result;
});
tableFields.listFieldNames.subscribe(result => {
fieldNames = result;
});
expect(fields!).toEqual([
MetaFields.lastModifiedByAvatar,
schema.fields[0],
MetaFields.statusColor,
MetaFields.lastModified,
]);
expect(fieldNames!).toEqual([
MetaFields.lastModifiedByAvatar,
schema.fields[0].name,
MetaFields.statusColor,
MetaFields.lastModified,
]);
});
});
INVALID_CONFIGS.forEach(test => {
it(`should remove ui state if config is ${test.case}`, () => {
uiState.setup(x => x.getUser<string[]>('schemas.my-schema.view', []))
.returns(() => of([]));
const tableFields = new TableFields(uiState.object, schema);
tableFields.updateFields(test.fields, true);
uiState.verify(x => x.removeUser('schemas.my-schema.view'), Times.once());
expect().nothing();
});
});
it('should eliminate invalid fields from the config', () => {
let fields: ReadonlyArray<TableField>;
let fieldNames: ReadonlyArray<string>;
const config = ['invalid', MetaFields.version];
uiState.setup(x => x.getUser<string[]>('schemas.my-schema.view', []))
.returns(() => of(config));
const tableFields = new TableFields(uiState.object, schema);
tableFields.listFields.subscribe(result => {
fields = result;
});
tableFields.listFieldNames.subscribe(result => {
fieldNames = result;
});
expect(fields!).toEqual([
MetaFields.version,
]);
expect(fieldNames!).toEqual([
MetaFields.version,
]);
});
it('should update config if fields are saved', () => {
uiState.setup(x => x.getUser<string[]>('schemas.my-schema.view', []))
.returns(() => of([]));
const tableFields = new TableFields(uiState.object, schema);
const config = ['invalid', MetaFields.version];
tableFields.updateFields(config, true);
uiState.verify(x => x.set('schemas.my-schema.view', [MetaFields.version], true), Times.once());
expect().nothing();
});
});

62
frontend/src/app/shared/state/table-fields.ts

@ -1,62 +0,0 @@
/*
* Squidex Headless CMS
*
* @license
* Copyright (c) Squidex UG (haftungsbeschränkt). All rights reserved.
*/
import { BehaviorSubject, Observable } from 'rxjs';
import { take } from 'rxjs/operators';
import { MetaFields, SchemaDto, TableField } from './../services/schemas.service';
import { UIState } from './ui.state';
const META_FIELD_NAMES = Object.values(MetaFields);
export class TableFields {
private readonly listField$ = new BehaviorSubject<ReadonlyArray<TableField>>([]);
private readonly listFieldName$ = new BehaviorSubject<ReadonlyArray<string>>([]);
private readonly settingsKey: string;
public readonly allFields: ReadonlyArray<string>;
public get listFields(): Observable<ReadonlyArray<TableField>> {
return this.listField$;
}
public get listFieldNames(): Observable<ReadonlyArray<string>> {
return this.listFieldName$;
}
constructor(
private readonly uiState: UIState,
private readonly schema: SchemaDto,
) {
this.allFields = [...this.schema.contentFields.map(x => x.name), ...META_FIELD_NAMES].sort();
this.settingsKey = `schemas.${this.schema.name}.view`;
this.uiState.getUser<string[]>(this.settingsKey, []).pipe(take(1))
.subscribe(fieldNames => {
this.updateFields(fieldNames, false);
});
}
public updateFields(fieldNames: ReadonlyArray<string>, save = true) {
fieldNames = fieldNames.filter(x => this.allFields.indexOf(x) >= 0);
if (fieldNames.length === 0) {
fieldNames = this.schema.defaultListFields.map(x => x['name'] || x);
if (save) {
this.uiState.removeUser(this.settingsKey);
}
} else if (save) {
this.uiState.set(this.settingsKey, fieldNames, true);
}
const fields: ReadonlyArray<TableField> = fieldNames.map(n => this.schema.fields.find(f => f.name === n) || n);
this.listField$.next(fields);
this.listFieldName$.next(fieldNames);
}
}

329
frontend/src/app/shared/state/table-settings.spec.ts

@ -0,0 +1,329 @@
/*
* Squidex Headless CMS
*
* @license
* Copyright (c) Squidex UG (haftungsbeschränkt). All rights reserved.
*/
import { of } from 'rxjs';
import { IMock, It, Mock, Times } from 'typemoq';
import { DateTime, Version } from '@app/framework';
import { createProperties, MetaFields, RootFieldDto, SchemaDto, TableField, TableSettings, FieldSizes, UIState } from '@app/shared/internal';
import { FieldWrappings } from '..';
describe('TableSettings', () => {
let uiState: IMock<UIState>;
const schema =
new SchemaDto({},
'1',
DateTime.now(), 'me',
DateTime.now(), 'me',
new Version('1'),
'my-schema',
'my-category',
'Default',
false,
{},
[
new RootFieldDto({}, 1, 'string', createProperties('String'), 'invariant'),
]);
beforeEach(() => {
uiState = Mock.ofType<UIState>();
});
const INVALID_CONFIGS = [
{ case: 'blank', fields: [] },
{ case: 'broken', fields: ['invalid'] },
];
const EMPTY = { fields: [], sizes: {}, wrappings: {} };
INVALID_CONFIGS.forEach(test => {
it(`should provide default fields if config is ${test.case}`, () => {
let listFields: ReadonlyArray<TableField>;
let listFieldNames: ReadonlyArray<string>;
let fieldSizes: FieldSizes;
let fieldWrappings: FieldWrappings;
const config = {
fields: test.fields,
sizes: {
field1: 100,
field2: 200,
},
wrappings: {
field3: true,
field4: false,
},
};
uiState.setup(x => x.getUser<any>('schemas.my-schema.config', {}))
.returns(() => of(config));
const tableSettings = new TableSettings(uiState.object, schema);
tableSettings.listFields.subscribe(result => {
listFields = result;
});
tableSettings.listFieldNames.subscribe(result => {
listFieldNames = result;
});
tableSettings.fieldSizes.subscribe(result => {
fieldSizes = result;
});
tableSettings.fieldWrappings.subscribe(result => {
fieldWrappings = result;
});
expect(listFields!).toEqual([
MetaFields.lastModifiedByAvatar,
schema.fields[0],
MetaFields.statusColor,
MetaFields.lastModified,
]);
expect(listFieldNames!).toEqual([
MetaFields.lastModifiedByAvatar,
schema.fields[0].name,
MetaFields.statusColor,
MetaFields.lastModified,
]);
expect(fieldSizes!).toEqual({
field1: 100,
field2: 200,
});
expect(fieldWrappings!).toEqual({
field3: true,
field4: false,
});
});
});
INVALID_CONFIGS.forEach(test => {
it(`should remove ui state if config is ${test.case}`, () => {
uiState.setup(x => x.getUser<any>('schemas.my-schema.config', {}))
.returns(() => of(({})));
const tableSettings = new TableSettings(uiState.object, schema);
tableSettings.updateFields(test.fields, true);
uiState.verify(x => x.removeUser('schemas.my-schema.config'), Times.once());
expect().nothing();
});
});
it('should eliminate invalid fields from the config', () => {
let listFields: ReadonlyArray<TableField>;
let listFieldNames: ReadonlyArray<string>;
uiState.setup(x => x.getUser<any>('schemas.my-schema.config', {}))
.returns(() => of(({ fields: ['invalid', MetaFields.version] })));
const tableSettings = new TableSettings(uiState.object, schema);
tableSettings.listFields.subscribe(result => {
listFields = result;
});
tableSettings.listFieldNames.subscribe(result => {
listFieldNames = result;
});
expect(listFields!).toEqual([
MetaFields.version,
]);
expect(listFieldNames!).toEqual([
MetaFields.version,
]);
});
it('should update config if fields are saved', () => {
uiState.setup(x => x.getUser<any>('schemas.my-schema.config', {}))
.returns(() => of(({})));
const tableSettings = new TableSettings(uiState.object, schema);
const config = ['invalid', MetaFields.version];
tableSettings.updateFields(config, true);
uiState.verify(x => x.set('schemas.my-schema.config', { ...EMPTY, fields: [MetaFields.version] }, true), Times.once());
expect().nothing();
});
it('should remove config if fields are saved', () => {
uiState.setup(x => x.getUser<any>('schemas.my-schema.config', {}))
.returns(() => of(({})));
const tableSettings = new TableSettings(uiState.object, schema);
tableSettings.updateFields([], true);
uiState.verify(x => x.removeUser('schemas.my-schema.config'), Times.once());
expect().nothing();
});
it('should update config if fields are only updated', () => {
uiState.setup(x => x.getUser<any>('schemas.my-schema.config', {}))
.returns(() => of(({})));
const tableSettings = new TableSettings(uiState.object, schema);
const config = ['invalid', MetaFields.version];
tableSettings.updateFields(config, false);
uiState.verify(x => x.set('schemas.my-schema.config', It.isAny(), true), Times.never());
expect().nothing();
});
it('should update config if sizes are saved', () => {
let fieldSizes: FieldSizes;
uiState.setup(x => x.getUser<any>('schemas.my-schema.config', {}))
.returns(() => of(({})));
const tableSettings = new TableSettings(uiState.object, schema);
tableSettings.fieldSizes.subscribe(result => {
fieldSizes = result;
});
tableSettings.updateSize(MetaFields.version, 100, true);
uiState.verify(x => x.set('schemas.my-schema.config', { ...EMPTY, sizes: { [MetaFields.version]: 100 } }, true), Times.once());
expect(fieldSizes!).toEqual({ [MetaFields.version]: 100 });
});
it('should update config if sizes are only updated', () => {
let fieldSizes: FieldSizes;
uiState.setup(x => x.getUser<any>('schemas.my-schema.config', {}))
.returns(() => of(({})));
const tableSettings = new TableSettings(uiState.object, schema);
tableSettings.fieldSizes.subscribe(result => {
fieldSizes = result;
});
tableSettings.updateSize(MetaFields.version, 100, false);
uiState.verify(x => x.set('schemas.my-schema.config', It.isAny(), true), Times.never());
expect(fieldSizes!).toEqual({ [MetaFields.version]: 100 });
});
it('should update config if wrapping is toggled', () => {
let fieldWrappings: FieldWrappings;
uiState.setup(x => x.getUser<any>('schemas.my-schema.config', {}))
.returns(() => of(({})));
const tableSettings = new TableSettings(uiState.object, schema);
tableSettings.fieldWrappings.subscribe(result => {
fieldWrappings = result;
});
tableSettings.toggleWrapping(MetaFields.version, true);
uiState.verify(x => x.set('schemas.my-schema.config', { ...EMPTY, wrappings: { [MetaFields.version]: true } }, true), Times.once());
expect(fieldWrappings!).toEqual({ [MetaFields.version]: true });
});
it('should update config if wrapping is toggled and only updated', () => {
let fieldWrappings: FieldWrappings;
uiState.setup(x => x.getUser<any>('schemas.my-schema.config', {}))
.returns(() => of(({})));
const tableSettings = new TableSettings(uiState.object, schema);
tableSettings.fieldWrappings.subscribe(result => {
fieldWrappings = result;
});
tableSettings.toggleWrapping(MetaFields.version, false);
uiState.verify(x => x.set('schemas.my-schema.config', It.isAny(), true), Times.never());
expect(fieldWrappings!).toEqual({ [MetaFields.version]: true });
});
it('should provide default fields if reset', () => {
let listFields: ReadonlyArray<TableField>;
let listFieldNames: ReadonlyArray<string>;
let fieldSizes: FieldSizes;
let fieldWrappings: FieldWrappings;
const config = {
fields: [
MetaFields.version,
],
sizes: {
field1: 100,
field2: 200,
},
wrappings: {
field3: true,
field4: false,
},
};
uiState.setup(x => x.getUser<any>('schemas.my-schema.config', {}))
.returns(() => of(config));
const tableSettings = new TableSettings(uiState.object, schema);
tableSettings.listFields.subscribe(result => {
listFields = result;
});
tableSettings.listFieldNames.subscribe(result => {
listFieldNames = result;
});
tableSettings.fieldSizes.subscribe(result => {
fieldSizes = result;
});
tableSettings.fieldWrappings.subscribe(result => {
fieldWrappings = result;
});
tableSettings.reset();
expect(listFields!).toEqual([
MetaFields.lastModifiedByAvatar,
schema.fields[0],
MetaFields.statusColor,
MetaFields.lastModified,
]);
expect(listFieldNames!).toEqual([
MetaFields.lastModifiedByAvatar,
schema.fields[0].name,
MetaFields.statusColor,
MetaFields.lastModified,
]);
expect(fieldSizes!).toEqual({});
expect(fieldWrappings!).toEqual({});
});
});

152
frontend/src/app/shared/state/table-settings.ts

@ -0,0 +1,152 @@
/*
* Squidex Headless CMS
*
* @license
* Copyright (c) Squidex UG (haftungsbeschränkt). All rights reserved.
*/
import { take } from 'rxjs/operators';
import { State, Types } from '@app/framework';
import { MetaFields, SchemaDto, TableField } from './../services/schemas.service';
import { UIState } from './ui.state';
const META_FIELD_NAMES = Object.values(MetaFields);
export type FieldSizes = { [name: string]: number };
export type FieldWrappings = { [name: string]: boolean };
interface Snapshot {
// The table fields in the right order.
fields: ReadonlyArray<string>;
// The sizes of the columns if overriden.
sizes: FieldSizes;
// True to enable wrapping.
wrappings: FieldWrappings;
}
export class TableSettings extends State<Snapshot> {
private readonly settingsKey: string;
public readonly schemaFields: ReadonlyArray<string>;
public readonly schemaDefaults: ReadonlyArray<string>;
public fieldSizes =
this.project(x => x.sizes);
public fieldWrappings =
this.project(x => x.wrappings);
public fields =
this.project(x => x.fields);
public listFieldNames =
this.projectFrom(this.fields, x => this.getListFieldNames(x));
public listFields =
this.projectFrom(this.listFieldNames, x => this.getListFields(x));
constructor(
private readonly uiState: UIState,
private readonly schema: SchemaDto,
) {
super({ fields: [], sizes: {}, wrappings: {} });
this.schemaFields = [...schema.contentFields.map(x => x.name), ...META_FIELD_NAMES].sort();
this.schemaDefaults = schema.defaultListFields.map(x => x['name'] || x);
this.settingsKey = `schemas.${this.schema.name}.config`;
this.uiState.getUser<any>(this.settingsKey, {}).pipe(take(1))
.subscribe(settings => {
if (!Types.isArrayOfString(settings.fields)) {
settings.fields = [];
}
if (!Types.isObject(settings.sizes)) {
settings.sizes = {};
}
if (!Types.isObject(settings.wrappings)) {
settings.wrappings = {};
}
this.publishSizes(settings.sizes);
this.publishFields(settings.fields);
this.publishWrappings(settings.wrappings);
});
}
public reset() {
super.resetState();
this.saveConfig();
}
public updateSize(field: string, size: number, save = true) {
this.next(s => ({
...s,
sizes: {
...s.sizes,
[field]: size,
},
}));
if (save) {
this.saveConfig();
}
}
public toggleWrapping(field: string, save = true) {
this.next(s => ({
...s,
wrappings: {
...s.wrappings,
[field]: !s.wrappings[field],
},
}));
if (save) {
this.saveConfig();
}
}
public updateFields(fields: ReadonlyArray<string>, save = true) {
this.publishFields(fields);
if (save) {
this.saveConfig();
}
}
private publishSizes(sizes: FieldSizes) {
this.next({ sizes });
}
private publishWrappings(wrappings: FieldWrappings) {
this.next({ wrappings });
}
private publishFields(fields: ReadonlyArray<string>) {
this.next({ fields: fields.filter(x => this.schemaFields.includes(x)) });
}
private saveConfig() {
const { sizes, fields, wrappings } = this.snapshot;
if (Object.keys(sizes).length === 0 && Object.keys(wrappings).length === 0 && fields.length === 0) {
this.uiState.removeUser(this.settingsKey);
} else {
this.uiState.set(this.settingsKey, this.snapshot, true);
}
}
private getListFields(names: ReadonlyArray<string>): ReadonlyArray<TableField> {
return names.map(n => this.schema.fields.find(f => f.name === n) || n);
}
private getListFieldNames(names: ReadonlyArray<string>): ReadonlyArray<string> {
return names.length === 0 ? this.schemaDefaults : names;
}
}

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

@ -53,7 +53,7 @@ export class SearchMenuComponent {
public selectResult(result: SearchResultDto) { public selectResult(result: SearchResultDto) {
if (Types.is(result, SearchResultDto)) { if (Types.is(result, SearchResultDto)) {
const relativeUrl = result.url.substr(this.apiUrl.value.length); const relativeUrl = result.url.substring(this.apiUrl.value.length);
this.router.navigateByUrl(relativeUrl); this.router.navigateByUrl(relativeUrl);

1
frontend/src/app/theme/_common.scss

@ -316,7 +316,6 @@ hr {
// //
// ACE // ACE
// //
// sass-lint:disable class-name-format
.ace_tooltip { .ace_tooltip {
background: $color-white; background: $color-white;
border: 1px solid $color-input; border: 1px solid $color-input;

16
frontend/src/app/theme/icomoon/demo.html

@ -9,10 +9,24 @@
<link rel="stylesheet" href="style.css"></head> <link rel="stylesheet" href="style.css"></head>
<body> <body>
<div class="bgc1 clearfix"> <div class="bgc1 clearfix">
<h1 class="mhmm mvm"><span class="fgc1">Font Name:</span> icomoon <small class="fgc1">(Glyphs:&nbsp;148)</small></h1> <h1 class="mhmm mvm"><span class="fgc1">Font Name:</span> icomoon <small class="fgc1">(Glyphs:&nbsp;149)</small></h1>
</div> </div>
<div class="clearfix mhl ptl"> <div class="clearfix mhl ptl">
<h1 class="mvm mtn fgc1">Grid Size: 24</h1> <h1 class="mvm mtn fgc1">Grid Size: 24</h1>
<div class="glyph fs1">
<div class="clearfix bshadow0 pbs">
<span class="icon-wrap_text"></span>
<span class="mls"> icon-wrap_text</span>
</div>
<fieldset class="fs0 size1of1 clearfix hidden-false">
<input type="text" readonly value="e991" class="unit size1of2" />
<input type="text" maxlength="1" readonly value="&#xe991;" class="unitRight size1of2 talign-right" />
</fieldset>
<div class="fs0 bshadow0 clearfix hidden-true">
<span class="unit pvs fgc1">liga: </span>
<input type="text" readonly value="" class="liga unitRight" />
</div>
</div>
<div class="glyph fs1"> <div class="glyph fs1">
<div class="clearfix bshadow0 pbs"> <div class="clearfix bshadow0 pbs">
<span class="icon-fullscreen_exit"></span> <span class="icon-fullscreen_exit"></span>

BIN
frontend/src/app/theme/icomoon/fonts/icomoon.eot

Binary file not shown.

1
frontend/src/app/theme/icomoon/fonts/icomoon.svg

@ -152,6 +152,7 @@
<glyph unicode="&#xe98e;" glyph-name="workflows" d="M823.125 919.232c-17.62-0.072-31.877-14.372-31.877-32.003 0-9.085 3.786-17.286 9.865-23.111l0.011-0.011 132.375-130.875-132.375-130.875c-6.134-5.842-9.949-14.071-9.949-23.19 0-17.675 14.328-32.003 32.003-32.003 8.996 0 17.124 3.711 22.938 9.686l0.007 0.007 155.375 153.625c5.864 5.803 9.494 13.853 9.494 22.75s-3.631 16.947-9.492 22.747l-155.378 153.628c-5.815 5.942-13.917 9.625-22.879 9.625-0.043 0-0.085 0-0.128 0h0.007zM576 767.107c-0.135 0.002-0.293 0.003-0.453 0.003-17.675 0-32.003-14.328-32.003-32.003s14.328-32.003 32.003-32.003c0.159 0 0.318 0.001 0.477 0.003h402.976c0.135-0.002 0.293-0.003 0.453-0.003 17.675 0 32.003 14.328 32.003 32.003s-14.328 32.003-32.003 32.003c-0.159 0-0.318-0.001-0.477-0.003h0.024zM218.875 343.107c-8.624-0.212-16.371-3.805-21.997-9.497l-0.003-0.003-147.625-146c-10.536-5.383-17.624-16.16-17.624-28.591 0-2.992 0.41-5.887 1.178-8.634l-0.054 0.225c0.167-0.686 0.296-1.146 0.434-1.602l-0.059 0.227c1.502-5.803 4.423-10.792 8.375-14.75v0l155.375-153.625c5.821-5.982 13.95-9.693 22.945-9.693 17.675 0 32.003 14.328 32.003 32.003 0 9.12-3.815 17.348-9.935 23.178l-0.013 0.012-101.875 100.75h327c0.135-0.002 0.293-0.003 0.453-0.003 17.675 0 32.003 14.328 32.003 32.003s-14.328 32.003-32.003 32.003c-0.159 0-0.318-0.001-0.477-0.003h-323.226l98.125 97c6.019 5.826 9.756 13.98 9.756 23.006 0 17.675-14.328 32.003-32.003 32.003-0.265 0-0.529-0.003-0.792-0.010l0.039 0.001zM128 950.857c-70.358 0-128-57.642-128-128v-192c0-70.358 57.642-128 128-128h192c70.358 0 128 57.642 128 128v192c0 70.358-57.642 128-128 128zM128 886.857h192c36.010 0 64-27.99 64-64v-192c0-36.010-27.99-64-64-64h-192c-36.010 0-64 27.99-64 64v192c0 36.010 27.99 64 64 64zM704 374.857c-70.358 0-128-57.642-128-128v-192c0-70.358 57.642-128 128-128h192c70.358 0 128 57.642 128 128v192c0 70.358-57.642 128-128 128zM704 310.857h192c36.010 0 64-27.99 64-64v-192c0-36.010-27.99-64-64-64h-192c-36.010 0-64 27.99-64 64v192c0 36.010 27.99 64 64 64z" /> <glyph unicode="&#xe98e;" glyph-name="workflows" d="M823.125 919.232c-17.62-0.072-31.877-14.372-31.877-32.003 0-9.085 3.786-17.286 9.865-23.111l0.011-0.011 132.375-130.875-132.375-130.875c-6.134-5.842-9.949-14.071-9.949-23.19 0-17.675 14.328-32.003 32.003-32.003 8.996 0 17.124 3.711 22.938 9.686l0.007 0.007 155.375 153.625c5.864 5.803 9.494 13.853 9.494 22.75s-3.631 16.947-9.492 22.747l-155.378 153.628c-5.815 5.942-13.917 9.625-22.879 9.625-0.043 0-0.085 0-0.128 0h0.007zM576 767.107c-0.135 0.002-0.293 0.003-0.453 0.003-17.675 0-32.003-14.328-32.003-32.003s14.328-32.003 32.003-32.003c0.159 0 0.318 0.001 0.477 0.003h402.976c0.135-0.002 0.293-0.003 0.453-0.003 17.675 0 32.003 14.328 32.003 32.003s-14.328 32.003-32.003 32.003c-0.159 0-0.318-0.001-0.477-0.003h0.024zM218.875 343.107c-8.624-0.212-16.371-3.805-21.997-9.497l-0.003-0.003-147.625-146c-10.536-5.383-17.624-16.16-17.624-28.591 0-2.992 0.41-5.887 1.178-8.634l-0.054 0.225c0.167-0.686 0.296-1.146 0.434-1.602l-0.059 0.227c1.502-5.803 4.423-10.792 8.375-14.75v0l155.375-153.625c5.821-5.982 13.95-9.693 22.945-9.693 17.675 0 32.003 14.328 32.003 32.003 0 9.12-3.815 17.348-9.935 23.178l-0.013 0.012-101.875 100.75h327c0.135-0.002 0.293-0.003 0.453-0.003 17.675 0 32.003 14.328 32.003 32.003s-14.328 32.003-32.003 32.003c-0.159 0-0.318-0.001-0.477-0.003h-323.226l98.125 97c6.019 5.826 9.756 13.98 9.756 23.006 0 17.675-14.328 32.003-32.003 32.003-0.265 0-0.529-0.003-0.792-0.010l0.039 0.001zM128 950.857c-70.358 0-128-57.642-128-128v-192c0-70.358 57.642-128 128-128h192c70.358 0 128 57.642 128 128v192c0 70.358-57.642 128-128 128zM128 886.857h192c36.010 0 64-27.99 64-64v-192c0-36.010-27.99-64-64-64h-192c-36.010 0-64 27.99-64 64v192c0 36.010 27.99 64 64 64zM704 374.857c-70.358 0-128-57.642-128-128v-192c0-70.358 57.642-128 128-128h192c70.358 0 128 57.642 128 128v192c0 70.358-57.642 128-128 128zM704 310.857h192c36.010 0 64-27.99 64-64v-192c0-36.010-27.99-64-64-64h-192c-36.010 0-64 27.99-64 64v192c0 36.010 27.99 64 64 64z" />
<glyph unicode="&#xe98f;" glyph-name="fullscreen_exit" d="M682 596.667h128v-84h-212v212h84v-128zM598 128.667v212h212v-84h-128v-128h-84zM342 596.667v128h84v-212h-212v84h128zM214 256.667v84h212v-212h-84v128h-128z" /> <glyph unicode="&#xe98f;" glyph-name="fullscreen_exit" d="M682 596.667h128v-84h-212v212h84v-128zM598 128.667v212h212v-84h-128v-128h-84zM342 596.667v128h84v-212h-212v84h128zM214 256.667v84h212v-212h-84v128h-128z" />
<glyph unicode="&#xe990;" glyph-name="fullscreen" d="M598 724.667h212v-212h-84v128h-128v84zM726 212.667v128h84v-212h-212v84h128zM214 512.667v212h212v-84h-128v-128h-84zM298 340.667v-128h128v-84h-212v212h84z" /> <glyph unicode="&#xe990;" glyph-name="fullscreen" d="M598 724.667h212v-212h-84v128h-128v84zM726 212.667v128h84v-212h-212v84h128zM214 512.667v212h212v-84h-128v-128h-84zM298 340.667v-128h128v-84h-212v212h84z" />
<glyph unicode="&#xe991;" glyph-name="wrap_text" d="M726 468.667q70 0 120-50t50-120-50-120-120-50h-86v-86l-128 128 128 128v-86h96q34 0 60 26t26 60-26 60-60 26h-566v84h556zM854 724.667v-84h-684v84h684zM170 128.667v84h256v-84h-256z" />
<glyph unicode="&#xe9ca;" glyph-name="earth" d="M512 960c-282.77 0-512-229.23-512-512s229.23-512 512-512 512 229.23 512 512-229.23 512-512 512zM512-0.002c-62.958 0-122.872 13.012-177.23 36.452l233.148 262.29c5.206 5.858 8.082 13.422 8.082 21.26v96c0 17.674-14.326 32-32 32-112.99 0-232.204 117.462-233.374 118.626-6 6.002-14.14 9.374-22.626 9.374h-128c-17.672 0-32-14.328-32-32v-192c0-12.122 6.848-23.202 17.69-28.622l110.31-55.156v-187.886c-116.052 80.956-192 215.432-192 367.664 0 68.714 15.49 133.806 43.138 192h116.862c8.488 0 16.626 3.372 22.628 9.372l128 128c6 6.002 9.372 14.14 9.372 22.628v77.412c40.562 12.074 83.518 18.588 128 18.588 70.406 0 137.004-16.26 196.282-45.2-4.144-3.502-8.176-7.164-12.046-11.036-36.266-36.264-56.236-84.478-56.236-135.764s19.97-99.5 56.236-135.764c36.434-36.432 85.218-56.264 135.634-56.26 3.166 0 6.342 0.080 9.518 0.236 13.814-51.802 38.752-186.656-8.404-372.334-0.444-1.744-0.696-3.488-0.842-5.224-81.324-83.080-194.7-134.656-320.142-134.656z" /> <glyph unicode="&#xe9ca;" glyph-name="earth" d="M512 960c-282.77 0-512-229.23-512-512s229.23-512 512-512 512 229.23 512 512-229.23 512-512 512zM512-0.002c-62.958 0-122.872 13.012-177.23 36.452l233.148 262.29c5.206 5.858 8.082 13.422 8.082 21.26v96c0 17.674-14.326 32-32 32-112.99 0-232.204 117.462-233.374 118.626-6 6.002-14.14 9.374-22.626 9.374h-128c-17.672 0-32-14.328-32-32v-192c0-12.122 6.848-23.202 17.69-28.622l110.31-55.156v-187.886c-116.052 80.956-192 215.432-192 367.664 0 68.714 15.49 133.806 43.138 192h116.862c8.488 0 16.626 3.372 22.628 9.372l128 128c6 6.002 9.372 14.14 9.372 22.628v77.412c40.562 12.074 83.518 18.588 128 18.588 70.406 0 137.004-16.26 196.282-45.2-4.144-3.502-8.176-7.164-12.046-11.036-36.266-36.264-56.236-84.478-56.236-135.764s19.97-99.5 56.236-135.764c36.434-36.432 85.218-56.264 135.634-56.26 3.166 0 6.342 0.080 9.518 0.236 13.814-51.802 38.752-186.656-8.404-372.334-0.444-1.744-0.696-3.488-0.842-5.224-81.324-83.080-194.7-134.656-320.142-134.656z" />
<glyph unicode="&#xf00a;" glyph-name="grid" d="M292.571 237.714v-109.714c0-30.286-24.571-54.857-54.857-54.857h-182.857c-30.286 0-54.857 24.571-54.857 54.857v109.714c0 30.286 24.571 54.857 54.857 54.857h182.857c30.286 0 54.857-24.571 54.857-54.857zM292.571 530.286v-109.714c0-30.286-24.571-54.857-54.857-54.857h-182.857c-30.286 0-54.857 24.571-54.857 54.857v109.714c0 30.286 24.571 54.857 54.857 54.857h182.857c30.286 0 54.857-24.571 54.857-54.857zM658.286 237.714v-109.714c0-30.286-24.571-54.857-54.857-54.857h-182.857c-30.286 0-54.857 24.571-54.857 54.857v109.714c0 30.286 24.571 54.857 54.857 54.857h182.857c30.286 0 54.857-24.571 54.857-54.857zM292.571 822.857v-109.714c0-30.286-24.571-54.857-54.857-54.857h-182.857c-30.286 0-54.857 24.571-54.857 54.857v109.714c0 30.286 24.571 54.857 54.857 54.857h182.857c30.286 0 54.857-24.571 54.857-54.857zM658.286 530.286v-109.714c0-30.286-24.571-54.857-54.857-54.857h-182.857c-30.286 0-54.857 24.571-54.857 54.857v109.714c0 30.286 24.571 54.857 54.857 54.857h182.857c30.286 0 54.857-24.571 54.857-54.857zM1024 237.714v-109.714c0-30.286-24.571-54.857-54.857-54.857h-182.857c-30.286 0-54.857 24.571-54.857 54.857v109.714c0 30.286 24.571 54.857 54.857 54.857h182.857c30.286 0 54.857-24.571 54.857-54.857zM658.286 822.857v-109.714c0-30.286-24.571-54.857-54.857-54.857h-182.857c-30.286 0-54.857 24.571-54.857 54.857v109.714c0 30.286 24.571 54.857 54.857 54.857h182.857c30.286 0 54.857-24.571 54.857-54.857zM1024 530.286v-109.714c0-30.286-24.571-54.857-54.857-54.857h-182.857c-30.286 0-54.857 24.571-54.857 54.857v109.714c0 30.286 24.571 54.857 54.857 54.857h182.857c30.286 0 54.857-24.571 54.857-54.857zM1024 822.857v-109.714c0-30.286-24.571-54.857-54.857-54.857h-182.857c-30.286 0-54.857 24.571-54.857 54.857v109.714c0 30.286 24.571 54.857 54.857 54.857h182.857c30.286 0 54.857-24.571 54.857-54.857z" /> <glyph unicode="&#xf00a;" glyph-name="grid" d="M292.571 237.714v-109.714c0-30.286-24.571-54.857-54.857-54.857h-182.857c-30.286 0-54.857 24.571-54.857 54.857v109.714c0 30.286 24.571 54.857 54.857 54.857h182.857c30.286 0 54.857-24.571 54.857-54.857zM292.571 530.286v-109.714c0-30.286-24.571-54.857-54.857-54.857h-182.857c-30.286 0-54.857 24.571-54.857 54.857v109.714c0 30.286 24.571 54.857 54.857 54.857h182.857c30.286 0 54.857-24.571 54.857-54.857zM658.286 237.714v-109.714c0-30.286-24.571-54.857-54.857-54.857h-182.857c-30.286 0-54.857 24.571-54.857 54.857v109.714c0 30.286 24.571 54.857 54.857 54.857h182.857c30.286 0 54.857-24.571 54.857-54.857zM292.571 822.857v-109.714c0-30.286-24.571-54.857-54.857-54.857h-182.857c-30.286 0-54.857 24.571-54.857 54.857v109.714c0 30.286 24.571 54.857 54.857 54.857h182.857c30.286 0 54.857-24.571 54.857-54.857zM658.286 530.286v-109.714c0-30.286-24.571-54.857-54.857-54.857h-182.857c-30.286 0-54.857 24.571-54.857 54.857v109.714c0 30.286 24.571 54.857 54.857 54.857h182.857c30.286 0 54.857-24.571 54.857-54.857zM1024 237.714v-109.714c0-30.286-24.571-54.857-54.857-54.857h-182.857c-30.286 0-54.857 24.571-54.857 54.857v109.714c0 30.286 24.571 54.857 54.857 54.857h182.857c30.286 0 54.857-24.571 54.857-54.857zM658.286 822.857v-109.714c0-30.286-24.571-54.857-54.857-54.857h-182.857c-30.286 0-54.857 24.571-54.857 54.857v109.714c0 30.286 24.571 54.857 54.857 54.857h182.857c30.286 0 54.857-24.571 54.857-54.857zM1024 530.286v-109.714c0-30.286-24.571-54.857-54.857-54.857h-182.857c-30.286 0-54.857 24.571-54.857 54.857v109.714c0 30.286 24.571 54.857 54.857 54.857h182.857c30.286 0 54.857-24.571 54.857-54.857zM1024 822.857v-109.714c0-30.286-24.571-54.857-54.857-54.857h-182.857c-30.286 0-54.857 24.571-54.857 54.857v109.714c0 30.286 24.571 54.857 54.857 54.857h182.857c30.286 0 54.857-24.571 54.857-54.857z" />
<glyph unicode="&#xf0c9;" glyph-name="list1" horiz-adv-x="878" d="M877.714 182.857v-73.143c0-20-16.571-36.571-36.571-36.571h-804.571c-20 0-36.571 16.571-36.571 36.571v73.143c0 20 16.571 36.571 36.571 36.571h804.571c20 0 36.571-16.571 36.571-36.571zM877.714 475.428v-73.143c0-20-16.571-36.571-36.571-36.571h-804.571c-20 0-36.571 16.571-36.571 36.571v73.143c0 20 16.571 36.571 36.571 36.571h804.571c20 0 36.571-16.571 36.571-36.571zM877.714 768v-73.143c0-20-16.571-36.571-36.571-36.571h-804.571c-20 0-36.571 16.571-36.571 36.571v73.143c0 20 16.571 36.571 36.571 36.571h804.571c20 0 36.571-16.571 36.571-36.571z" /> <glyph unicode="&#xf0c9;" glyph-name="list1" horiz-adv-x="878" d="M877.714 182.857v-73.143c0-20-16.571-36.571-36.571-36.571h-804.571c-20 0-36.571 16.571-36.571 36.571v73.143c0 20 16.571 36.571 36.571 36.571h804.571c20 0 36.571-16.571 36.571-36.571zM877.714 475.428v-73.143c0-20-16.571-36.571-36.571-36.571h-804.571c-20 0-36.571 16.571-36.571 36.571v73.143c0 20 16.571 36.571 36.571 36.571h804.571c20 0 36.571-16.571 36.571-36.571zM877.714 768v-73.143c0-20-16.571-36.571-36.571-36.571h-804.571c-20 0-36.571 16.571-36.571 36.571v73.143c0 20 16.571 36.571 36.571 36.571h804.571c20 0 36.571-16.571 36.571-36.571z" />

Before

Width:  |  Height:  |  Size: 119 KiB

After

Width:  |  Height:  |  Size: 119 KiB

BIN
frontend/src/app/theme/icomoon/fonts/icomoon.ttf

Binary file not shown.

BIN
frontend/src/app/theme/icomoon/fonts/icomoon.woff

Binary file not shown.

2
frontend/src/app/theme/icomoon/selection.json

File diff suppressed because one or more lines are too long

13
frontend/src/app/theme/icomoon/styles.css → frontend/src/app/theme/icomoon/style.css

@ -1,10 +1,10 @@
@font-face { @font-face {
font-family: 'icomoon'; font-family: 'icomoon';
src: url('fonts/icomoon.eot?ywommh'); src: url('fonts/icomoon.eot?t85fog');
src: url('fonts/icomoon.eot?ywommh#iefix') format('embedded-opentype'), src: url('fonts/icomoon.eot?t85fog#iefix') format('embedded-opentype'),
url('fonts/icomoon.ttf?ywommh') format('truetype'), url('fonts/icomoon.ttf?t85fog') format('truetype'),
url('fonts/icomoon.woff?ywommh') format('woff'), url('fonts/icomoon.woff?t85fog') format('woff'),
url('fonts/icomoon.svg?ywommh#icomoon') format('svg'); url('fonts/icomoon.svg?t85fog#icomoon') format('svg');
font-weight: normal; font-weight: normal;
font-style: normal; font-style: normal;
font-display: block; font-display: block;
@ -25,6 +25,9 @@
-moz-osx-font-smoothing: grayscale; -moz-osx-font-smoothing: grayscale;
} }
.icon-wrap_text:before {
content: "\e991";
}
.icon-fullscreen_exit:before { .icon-fullscreen_exit:before {
content: "\e98f"; content: "\e98f";
} }

2
frontend/src/app/theme/theme.scss

@ -10,7 +10,7 @@
@import 'bootstrap'; @import 'bootstrap';
// icomoon // icomoon
@import 'icomoon/styles'; @import 'icomoon/style';
// Custom files // Custom files
@import 'common'; @import 'common';

Loading…
Cancel
Save