Browse Source

Move asset dialog (#966)

* Started to move.

* Fix tests

* More fixes

* Fix tests
pull/967/head
Sebastian Stehle 3 years ago
committed by GitHub
parent
commit
dddb665317
No known key found for this signature in database GPG Key ID: 4AEE18F83AFDEB23
  1. 4
      backend/i18n/frontend_en.json
  2. 4
      backend/i18n/frontend_it.json
  3. 4
      backend/i18n/frontend_nl.json
  4. 4
      backend/i18n/frontend_pt.json
  5. 4
      backend/i18n/frontend_zh.json
  6. 4
      backend/i18n/source/frontend_en.json
  7. 2
      backend/i18n/source/frontend_it.json
  8. 2
      backend/i18n/source/frontend_nl.json
  9. 2
      backend/i18n/source/frontend_pt.json
  10. 2
      backend/i18n/source/frontend_zh.json
  11. 7
      frontend/src/app/features/administration/state/event-consumers.state.spec.ts
  12. 5
      frontend/src/app/features/administration/state/users.state.spec.ts
  13. 19
      frontend/src/app/features/assets/pages/assets-page.component.html
  14. 16
      frontend/src/app/features/assets/pages/assets-page.component.ts
  15. 9
      frontend/src/app/features/content/pages/content/editor/content-editor.component.html
  16. 21
      frontend/src/app/features/content/shared/forms/assets-editor.component.html
  17. 15
      frontend/src/app/features/content/shared/forms/assets-editor.component.ts
  18. 2
      frontend/src/app/features/content/shared/forms/field-editor.component.html
  19. 8
      frontend/src/app/features/content/shared/references/references-tags.component.html
  20. 3
      frontend/src/app/features/schemas/pages/schema/fields/types/component-validation.component.html
  21. 3
      frontend/src/app/features/schemas/pages/schema/fields/types/components-validation.component.html
  22. 2
      frontend/src/app/features/schemas/pages/schema/fields/types/number-ui.component.html
  23. 3
      frontend/src/app/features/schemas/pages/schema/fields/types/references-validation.component.html
  24. 3
      frontend/src/app/features/schemas/pages/schema/fields/types/string-ui.component.html
  25. 4
      frontend/src/app/features/settings/pages/clients/client.component.html
  26. 2
      frontend/src/app/features/settings/pages/clients/client.component.ts
  27. 4
      frontend/src/app/features/settings/pages/contributors/contributor-add-form.component.html
  28. 2
      frontend/src/app/features/settings/pages/languages/language-add-form.component.html
  29. 2
      frontend/src/app/features/settings/pages/roles/role.component.html
  30. 6
      frontend/src/app/features/settings/pages/workflows/workflow-step.component.html
  31. 8
      frontend/src/app/features/settings/pages/workflows/workflow-transition.component.html
  32. 17
      frontend/src/app/features/settings/pages/workflows/workflow.component.html
  33. 2
      frontend/src/app/features/teams/pages/contributors/contributor-add-form.component.html
  34. 6
      frontend/src/app/features/teams/state/team-contributors.state.spec.ts
  35. 9
      frontend/src/app/features/teams/state/team-plans.state.spec.ts
  36. 12
      frontend/src/app/framework/angular/forms/editors/autocomplete.component.html
  37. 11
      frontend/src/app/framework/angular/forms/editors/autocomplete.component.ts
  38. 6
      frontend/src/app/framework/angular/forms/editors/autocomplete.stories.ts
  39. 4
      frontend/src/app/framework/angular/forms/editors/dropdown.component.html
  40. 44
      frontend/src/app/framework/angular/forms/editors/tag-editor.component.html
  41. 109
      frontend/src/app/framework/angular/forms/editors/tag-editor.component.ts
  42. 36
      frontend/src/app/framework/angular/forms/editors/tag-editor.stories.ts
  43. 5
      frontend/src/app/framework/angular/modals/modal.directive.ts
  44. 44
      frontend/src/app/framework/utils/rxjs-extensions.ts
  45. 34
      frontend/src/app/shared/components/assets/asset-dialog.component.html
  46. 4
      frontend/src/app/shared/components/assets/asset-dialog.component.scss
  47. 139
      frontend/src/app/shared/components/assets/asset-dialog.component.ts
  48. 19
      frontend/src/app/shared/components/assets/asset-folder-dropdown-item.component.html
  49. 4
      frontend/src/app/shared/components/assets/asset-folder-dropdown-item.component.scss
  50. 27
      frontend/src/app/shared/components/assets/asset-folder-dropdown-item.component.ts
  51. 2
      frontend/src/app/shared/components/assets/asset-folder-dropdown.component.html
  52. 2
      frontend/src/app/shared/components/assets/asset-folder-dropdown.component.ts
  53. 6
      frontend/src/app/shared/components/assets/asset-folder.component.html
  54. 14
      frontend/src/app/shared/components/assets/asset-folder.component.ts
  55. 30
      frontend/src/app/shared/components/assets/asset-selector.component.html
  56. 4
      frontend/src/app/shared/components/assets/asset-selector.component.ts
  57. 11
      frontend/src/app/shared/components/assets/asset-uploader.component.ts
  58. 18
      frontend/src/app/shared/components/assets/asset.component.html
  59. 40
      frontend/src/app/shared/components/assets/asset.component.ts
  60. 140
      frontend/src/app/shared/components/assets/assets-list.component.html
  61. 13
      frontend/src/app/shared/components/assets/assets-list.component.ts
  62. 17
      frontend/src/app/shared/components/contents/content-list-cell.directive.ts
  63. 2
      frontend/src/app/shared/components/forms/markdown-editor.component.ts
  64. 4
      frontend/src/app/shared/components/forms/rich-editor.component.ts
  65. 9
      frontend/src/app/shared/interceptors/auth.interceptor.spec.ts
  66. 2
      frontend/src/app/shared/module.ts
  67. 40
      frontend/src/app/shared/services/assets.service.spec.ts
  68. 17
      frontend/src/app/shared/services/assets.service.ts
  69. 5
      frontend/src/app/shared/services/users-provider.service.spec.ts
  70. 5
      frontend/src/app/shared/state/asset-scripts.state.spec.ts
  71. 33
      frontend/src/app/shared/state/asset-uploader.state.spec.ts
  72. 32
      frontend/src/app/shared/state/asset-uploader.state.ts
  73. 12
      frontend/src/app/shared/state/assets.forms.ts
  74. 97
      frontend/src/app/shared/state/assets.state.spec.ts
  75. 108
      frontend/src/app/shared/state/assets.state.ts
  76. 9
      frontend/src/app/shared/state/backups.state.spec.ts
  77. 5
      frontend/src/app/shared/state/clients.state.spec.ts
  78. 5
      frontend/src/app/shared/state/contributors.state.spec.ts
  79. 5
      frontend/src/app/shared/state/languages.state.spec.ts
  80. 9
      frontend/src/app/shared/state/plans.state.spec.ts
  81. 5
      frontend/src/app/shared/state/roles.state.spec.ts
  82. 5
      frontend/src/app/shared/state/rule-events.state.spec.ts
  83. 5
      frontend/src/app/shared/state/rule-simulator.state.spec.ts
  84. 5
      frontend/src/app/shared/state/rules.state.spec.ts
  85. 5
      frontend/src/app/shared/state/schemas.state.spec.ts
  86. 5
      frontend/src/app/shared/state/template.state.spec.ts
  87. 5
      frontend/src/app/shared/state/workflows.state.spec.ts
  88. 6
      frontend/src/app/shell/pages/internal/notification-dropdown.component.ts
  89. 3
      frontend/src/app/shell/pages/internal/search-menu.component.html

4
backend/i18n/frontend_en.json

@ -98,6 +98,7 @@
"assets.loadTagsFailed": "Failed to load tags. Please reload.",
"assets.metadata": "Metadata",
"assets.metadataAdd": "Add Metadata",
"assets.move": "Move",
"assets.moveFailed": "Failed to move asset. Please reload.",
"assets.protected": "Protected",
"assets.protectedHint": "Assets are public by default. Everybody with the link can download the file. If you make an asset protected, only authenticated users (usually a client) can download the asset.",
@ -535,7 +536,7 @@
"contributors.import.run": "Add Contributors",
"contributors.import.run2": "Import",
"contributors.importButton": "Add many contributors at once",
"contributors.importHintg": "Big team?",
"contributors.importHint": "Big team?",
"contributors.importTitle": "Import contributors",
"contributors.loadFailed": "Failed to load contributors. Please reload.",
"contributors.planHint": "Your plan allows up to {maxContributors} contributors.",
@ -665,7 +666,6 @@
"roles.loadPermissionsFailed": "Failed to load permissions. Please reload.",
"roles.permissions": "Permissions",
"roles.permissionsDescription": "Permissions restrict the allowed operations and queries at API level and are a security feature.",
"roles.permissionsPlaceholder": "Start typing to search for permissions",
"roles.properties": "Properties",
"roles.properties.hideAPI": "Hide API",
"roles.properties.hideAssets": "Hide Assets",

4
backend/i18n/frontend_it.json

@ -98,6 +98,7 @@
"assets.loadTagsFailed": "Failed to load tags. Please reload.",
"assets.metadata": "Metadati",
"assets.metadataAdd": "Aggiungi un metadato",
"assets.move": "Move",
"assets.moveFailed": "Non è stato possibile spostare la risorsa. Per favore ricarica.",
"assets.protected": "Protetto",
"assets.protectedHint": "Assets are public by default. Everybody with the link can download the file. If you make an asset protected, only authenticated users (usually a client) can download the asset.",
@ -535,7 +536,7 @@
"contributors.import.run": "Aggiungi Collaboratori",
"contributors.import.run2": "Importa",
"contributors.importButton": "Aggiungi più collaboratori contemporaneamente",
"contributors.importHintg": "Team numeroso?",
"contributors.importHint": "Team numeroso?",
"contributors.importTitle": "Importa collaboratori",
"contributors.loadFailed": "Non è stato possibile caricare contributors. Per favore ricarica.",
"contributors.planHint": "Il tuo piano prevede un numero massimo di {maxContributors} collaboratori.",
@ -665,7 +666,6 @@
"roles.loadPermissionsFailed": "Non è stato possibile caricare i permessi. Per favore ricarica.",
"roles.permissions": "Permessi",
"roles.permissionsDescription": "I permessi limitano le operazioni consentite e le interrogazioni (query) a livello di API e sono una funzionalità per garantire la sicurezza.",
"roles.permissionsPlaceholder": "Inizia a digitare per ricercare i permessi",
"roles.properties": "Proprietà",
"roles.properties.hideAPI": "Nascondi le API",
"roles.properties.hideAssets": "Nascondi le Risorse",

4
backend/i18n/frontend_nl.json

@ -98,6 +98,7 @@
"assets.loadTagsFailed": "Laden van tags is mislukt. Laad opnieuw.",
"assets.metadata": "Metadata",
"assets.metadataAdd": "Metadata toevoegen",
"assets.move": "Move",
"assets.moveFailed": "Verplaatsen van item is mislukt. Laad opnieuw.",
"assets.protected": "Beschermd",
"assets.protectedHint": "Assets are public by default. Everybody with the link can download the file. If you make an asset protected, only authenticated users (usually a client) can download the asset.",
@ -535,7 +536,7 @@
"contributors.import.run": "Bijdrager toevoegen",
"contributors.import.run2": "Importeren",
"contributors.importButton": "Voeg veel bijdragers tegelijk toe",
"contributors.importHintg": "Groot team?",
"contributors.importHint": "Groot team?",
"contributors.importTitle": "Bijdragers importeren",
"contributors.loadFailed": "Laden van bijdragers is mislukt. Laad opnieuw.",
"contributors.planHint": "Uw plan staat maximaal {maxContributors} bijdragers toe.",
@ -665,7 +666,6 @@
"roles.loadPermissionsFailed": "Kan machtigingen niet laden. Laad opnieuw.",
"roles.permissions": "Rechten",
"roles.permissionsDescription": "Machtigingen beperken de toegestane bewerkingen en zoekopdrachten op API-niveau en zijn een beveiligingsfunctie.",
"roles.permissionsPlaceholder": "Begin met typen om naar rechten te zoeken",
"roles.properties": "Eigenschappen",
"roles.properties.hideAPI": "API verbergen",
"roles.properties.hideAssets": "Assets verbergen",

4
backend/i18n/frontend_pt.json

@ -98,6 +98,7 @@
"assets.loadTagsFailed": "Falhou em carregar etiquetas. Por favor, recarregue.",
"assets.metadata": "Metadados",
"assets.metadataAdd": "Adicionar metadados",
"assets.move": "Move",
"assets.moveFailed": "Falha ao mover ficheiro. Por favor, recarregue.",
"assets.protected": "Protegido",
"assets.protectedHint": "Os ativos são públicos por defeito. Todos com o link podem descarregar o ficheiro. Se fizer um ficheiro protegido, apenas utilizadores autenticados (normalmente um cliente) podem descarregar o ficheiro.",
@ -535,7 +536,7 @@
"contributors.import.run": "Adicionar Contribuintes",
"contributors.import.run2": "Importação",
"contributors.importButton": "Adicionar muitos contribuintes ao mesmo tempo",
"contributors.importHintg": "Uma grande equipa?",
"contributors.importHint": "Uma grande equipa?",
"contributors.importTitle": "Contribuintes de importação",
"contributors.loadFailed": "Falhou em carregar os contribuintes. Por favor, recarregue.",
"contributors.planHint": "O seu plano permite até contribuintes {maxContributors} .",
@ -665,7 +666,6 @@
"roles.loadPermissionsFailed": "Falhou em carregar permissões. Por favor, recarregue.",
"roles.permissions": "Permissões",
"roles.permissionsDescription": "As permissões restringem as operações e consultas permitidas a nível API e são uma funcionalidade de segurança.",
"roles.permissionsPlaceholder": "Comece a escrever para procurar permissões",
"roles.properties": "Propriedades",
"roles.properties.hideAPI": "Ocultar API",
"roles.properties.hideAssets": "Ocultar ficheiros",

4
backend/i18n/frontend_zh.json

@ -98,6 +98,7 @@
"assets.loadTagsFailed": "Failed to load tags. Please reload.",
"assets.metadata": "元数据",
"assets.metadataAdd": "添加元数据",
"assets.move": "Move",
"assets.moveFailed": "资源移动失败。请重新加载。",
"assets.protected": "受保护",
"assets.protectedHint": "Assets are public by default. Everybody with the link can download the file. If you make an asset protected, only authenticated users (usually a client) can download the asset.",
@ -535,7 +536,7 @@
"contributors.import.run": "添加贡献者",
"contributors.import.run2": "导入",
"contributors.importButton": "一次添加多个贡献者",
"contributors.importHintg": "大团队?",
"contributors.importHint": "大团队?",
"contributors.importTitle": "导入贡献者",
"contributors.loadFailed": "加载贡献者失败。请重新加载。",
"contributors.planHint": "您的计划允许最多 {maxContributors} 个贡献者。",
@ -665,7 +666,6 @@
"roles.loadPermissionsFailed": "加载权限失败。请重新加载。",
"roles.permissions": "权限",
"roles.permissionsDescription": "权限在 API 级别限制允许的操作和查询,是一项安全功能。",
"roles.permissionsPlaceholder": "开始输入以搜索权限",
"roles.properties": "属性",
"roles.properties.hideAPI": "隐藏 API",
"roles.properties.hideAssets": "隐藏资源",

4
backend/i18n/source/frontend_en.json

@ -98,6 +98,7 @@
"assets.loadTagsFailed": "Failed to load tags. Please reload.",
"assets.metadata": "Metadata",
"assets.metadataAdd": "Add Metadata",
"assets.move": "Move",
"assets.moveFailed": "Failed to move asset. Please reload.",
"assets.protected": "Protected",
"assets.protectedHint": "Assets are public by default. Everybody with the link can download the file. If you make an asset protected, only authenticated users (usually a client) can download the asset.",
@ -535,7 +536,7 @@
"contributors.import.run": "Add Contributors",
"contributors.import.run2": "Import",
"contributors.importButton": "Add many contributors at once",
"contributors.importHintg": "Big team?",
"contributors.importHint": "Big team?",
"contributors.importTitle": "Import contributors",
"contributors.loadFailed": "Failed to load contributors. Please reload.",
"contributors.planHint": "Your plan allows up to {maxContributors} contributors.",
@ -665,7 +666,6 @@
"roles.loadPermissionsFailed": "Failed to load permissions. Please reload.",
"roles.permissions": "Permissions",
"roles.permissionsDescription": "Permissions restrict the allowed operations and queries at API level and are a security feature.",
"roles.permissionsPlaceholder": "Start typing to search for permissions",
"roles.properties": "Properties",
"roles.properties.hideAPI": "Hide API",
"roles.properties.hideAssets": "Hide Assets",

2
backend/i18n/source/frontend_it.json

@ -431,7 +431,7 @@
"contributors.import.run": "Aggiungi Collaboratori",
"contributors.import.run2": "Importa",
"contributors.importButton": "Aggiungi più collaboratori contemporaneamente",
"contributors.importHintg": "Team numeroso?",
"contributors.importHint": "Team numeroso?",
"contributors.importTitle": "Importa collaboratori",
"contributors.loadFailed": "Non è stato possibile caricare contributors. Per favore ricarica.",
"contributors.planHint": "Il tuo piano prevede un numero massimo di {maxContributors} collaboratori.",

2
backend/i18n/source/frontend_nl.json

@ -500,7 +500,7 @@
"contributors.import.run": "Bijdrager toevoegen",
"contributors.import.run2": "Importeren",
"contributors.importButton": "Voeg veel bijdragers tegelijk toe",
"contributors.importHintg": "Groot team?",
"contributors.importHint": "Groot team?",
"contributors.importTitle": "Bijdragers importeren",
"contributors.loadFailed": "Laden van bijdragers is mislukt. Laad opnieuw.",
"contributors.planHint": "Uw plan staat maximaal {maxContributors} bijdragers toe.",

2
backend/i18n/source/frontend_pt.json

@ -524,7 +524,7 @@
"contributors.import.run": "Adicionar Contribuintes",
"contributors.import.run2": "Importação",
"contributors.importButton": "Adicionar muitos contribuintes ao mesmo tempo",
"contributors.importHintg": "Uma grande equipa?",
"contributors.importHint": "Uma grande equipa?",
"contributors.importTitle": "Contribuintes de importação",
"contributors.loadFailed": "Falhou em carregar os contribuintes. Por favor, recarregue.",
"contributors.planHint": "O seu plano permite até contribuintes {maxContributors} .",

2
backend/i18n/source/frontend_zh.json

@ -453,7 +453,7 @@
"contributors.import.run": "添加贡献者",
"contributors.import.run2": "导入",
"contributors.importButton": "一次添加多个贡献者",
"contributors.importHintg": "大团队?",
"contributors.importHint": "大团队?",
"contributors.importTitle": "导入贡献者",
"contributors.loadFailed": "加载贡献者失败。请重新加载。",
"contributors.planHint": "您的计划允许最多 {maxContributors} 个贡献者。",

7
frontend/src/app/features/administration/state/event-consumers.state.spec.ts

@ -5,8 +5,7 @@
* Copyright (c) Squidex UG (haftungsbeschränkt). All rights reserved.
*/
import { of, throwError } from 'rxjs';
import { onErrorResumeNext } from 'rxjs/operators';
import { of, onErrorResumeNextWith, throwError } from 'rxjs';
import { IMock, It, Mock, Times } from 'typemoq';
import { EventConsumersService } from '@app/features/administration/internal';
import { DialogService } from '@app/framework';
@ -50,7 +49,7 @@ describe('EventConsumersState', () => {
eventConsumersService.setup(x => x.getEventConsumers())
.returns(() => throwError(() => 'Service Error'));
eventConsumersState.load().pipe(onErrorResumeNext()).subscribe();
eventConsumersState.load().pipe(onErrorResumeNextWith()).subscribe();
expect(eventConsumersState.snapshot.isLoading).toBeFalsy();
});
@ -70,7 +69,7 @@ describe('EventConsumersState', () => {
eventConsumersService.setup(x => x.getEventConsumers())
.returns(() => throwError(() => 'Service Error')).verifiable();
eventConsumersState.load(true, false).pipe(onErrorResumeNext()).subscribe();
eventConsumersState.load(true, false).pipe(onErrorResumeNextWith()).subscribe();
expect().nothing();

5
frontend/src/app/features/administration/state/users.state.spec.ts

@ -5,8 +5,7 @@
* Copyright (c) Squidex UG (haftungsbeschränkt). All rights reserved.
*/
import { firstValueFrom, of, throwError } from 'rxjs';
import { onErrorResumeNext } from 'rxjs/operators';
import { firstValueFrom, of, onErrorResumeNextWith, throwError } from 'rxjs';
import { IMock, It, Mock, Times } from 'typemoq';
import { UpsertUserDto, UsersService } from '@app/features/administration/internal';
import { DialogService } from '@app/shared';
@ -53,7 +52,7 @@ describe('UsersState', () => {
usersService.setup(x => x.getUsers(10, 0, undefined))
.returns(() => throwError(() => 'Service Error'));
usersState.load().pipe(onErrorResumeNext()).subscribe();
usersState.load().pipe(onErrorResumeNextWith()).subscribe();
expect(usersState.snapshot.isLoading).toBeFalsy();
});

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

@ -11,10 +11,11 @@
<div class="col" style="width: 300px;">
<div class="row g-0 search">
<div class="col-6">
<sqx-tag-editor class="tags" [singleLine]="true" placeholder="{{ 'assets.searchByTags' | sqxTranslate }}"
[suggestions]="assetsState.tagsNames | async"
<sqx-tag-editor class="tags" placeholder="{{ 'assets.searchByTags' | sqxTranslate }}"
[itemsSource]="assetsState.tagsNames | async"
[ngModel]="assetsState.selectedTagNames | async"
(ngModelChange)="selectTags($event)"
[styleScrollable]="true"
[undefinedWhenEmpty]="false">
</sqx-tag-editor>
</div>
@ -57,9 +58,10 @@
<div *ngIf="assetsState.path | async; let path">
<sqx-assets-list
[assetsState]="assetsState"
[showPager]="false"
[showFolderIcon]="path.length === 0"
[isListView]="isListView">
(edit)="editStart($event)"
[isDisabled]="false"
[isListView]="isListView"
[showFolderIcon]="path.length === 0">
</sqx-assets-list>
</div>
@ -83,3 +85,10 @@
<ng-container *sqxModal="addAssetFolderDialog">
<sqx-asset-folder-dialog (complete)="addAssetFolderDialog.hide()"></sqx-asset-folder-dialog>
</ng-container>
<sqx-asset-dialog *sqxModal="editAsset;isDialog:true"
[asset]="editAsset!"
(assetUpdated)="replaceAsset($event)"
(assetReplaced)="replaceAsset($event)"
(complete)="editDone()">
</sqx-asset-dialog>

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

@ -6,7 +6,7 @@
*/
import { Component, OnInit } from '@angular/core';
import { AssetsState, DialogModel, LocalStoreService, MathHelper, Queries, Query, QueryFullTextSynchronizer, ResourceOwner, Router2State, UIState } from '@app/shared';
import { AssetDto, AssetsState, DialogModel, LocalStoreService, MathHelper, Queries, Query, QueryFullTextSynchronizer, ResourceOwner, Router2State, UIState } from '@app/shared';
import { Settings } from '@app/shared/state/settings';
@Component({
@ -20,6 +20,8 @@ import { Settings } from '@app/shared/state/settings';
export class AssetsPageComponent extends ResourceOwner implements OnInit {
public queries = new Queries(this.uiState, 'assets');
public editAsset?: AssetDto;
public addAssetFolderDialog = new DialogModel();
public isListView = false;
@ -57,10 +59,22 @@ export class AssetsPageComponent extends ResourceOwner implements OnInit {
this.assetsState.load(true, false);
}
public editStart(asset: AssetDto) {
this.editAsset = asset;
}
public editDone() {
this.editAsset = undefined;
}
public search(query: Query) {
this.assetsState.search(query);
}
public replaceAsset(asset: AssetDto) {
this.assetsState.replaceAsset(asset);
}
public selectTags(tags: ReadonlyArray<string>) {
this.assetsState.selectTags(tags);
}

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

@ -1,14 +1,7 @@
<sqx-form-error [bubble]="true" [closeable]="true" [error]="(contentForm.error | async)"></sqx-form-error>
<sqx-list-view>
<ng-container topHeader>
<div *ngIf="isNew && showIdInput">
<input class="form-control" placeholder="{{ 'contents.idPlaceholder' | sqxTranslate }}"
[ngModel]="contentId"
(ngModelChange)="contentIdChange.emit($event)" />
</div>
<ng-container topHeader>
<div class="alert alert-danger" *ngIf="!contentVersion && isDeleted">
{{ 'contents.deleted' | sqxTranslate }}
</div>

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

@ -31,12 +31,13 @@
[isDisabled]="snapshot.isDisabled"
[isCompact]="snapshot.isCompact"
[folderId]="folderId"
(loadError)="removeLoadingAsset(file)"
(load)="addAsset(file, $event)">
(loadDone)="addAsset(file, $event)"
(loadError)="removeLoadingAsset(file)">
</sqx-asset>
<sqx-asset *ngFor="let asset of snapshot.assets; trackBy: trackByAsset"
[asset]="asset"
(edit)="editStart($event)"
[isDisabled]="snapshot.isDisabled"
[isCompact]="snapshot.isCompact"
[removeMode]="true"
@ -49,13 +50,13 @@
<ng-template #listTemplate>
<div class="list-view">
<sqx-asset *ngFor="let file of snapshot.assetFiles"
(load)="addAsset(file, $event)"
(loadError)="removeLoadingAsset(file)"
[assetFile]="file"
[folderId]="folderId"
[isCompact]="snapshot.isCompact"
[isDisabled]="snapshot.isDisabled"
[isListView]="true">
[isListView]="true"
(loadDone)="addAsset(file, $event)"
(loadError)="removeLoadingAsset(file)">
</sqx-asset>
<div cdkDropList
@ -65,6 +66,7 @@
<div *ngFor="let asset of snapshot.assets; trackBy: trackByAsset" class="table-drag" cdkDrag cdkDragLockAxis="y">
<sqx-asset
[asset]="asset"
(edit)="editStart($event)"
[isListView]="true"
[isDisabled]="snapshot.isDisabled"
[isCompact]="snapshot.isCompact"
@ -81,4 +83,11 @@
<ng-container *sqxModal="assetsDialog">
<sqx-asset-selector (select)="selectAssets($event)"></sqx-asset-selector>
</ng-container>
</ng-container>
<sqx-asset-dialog *sqxModal="snapshot.editAsset;isDialog:true"
[asset]="snapshot.editAsset!"
(assetReplaced)="notifyOthers($event)"
(assetUpdated)="notifyOthers($event)"
(complete)="editDone()">
</sqx-asset-dialog>

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

@ -30,6 +30,9 @@ interface State {
// The assets to render.
assets: ReadonlyArray<AssetDto>;
// The asset to edit.
editAsset?: AssetDto;
// True when showing the assets as list.
isListView: boolean;
@ -107,9 +110,7 @@ export class AssetsEditorComponent extends StatefulControlComponent<State, Reado
this.own(
this.messageBus.of(AssetUpdated)
.subscribe(event => {
if (event.source !== this) {
this.setAssets(this.snapshot.assets.replacedBy('id', event.asset));
}
this.setAssets(this.snapshot.assets.replacedBy('id', event.asset));
}));
}
@ -179,6 +180,14 @@ export class AssetsEditorComponent extends StatefulControlComponent<State, Reado
this.next({ isListView });
}
public editStart(asset: AssetDto) {
this.next({ editAsset: asset });
}
public editDone() {
this.next({ editAsset: undefined });
}
private updateValue() {
const ids = this.snapshot.assets.map(x => x.id);

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

@ -221,7 +221,7 @@
<ng-container *ngSwitchCase="'Tags'">
<ng-container [ngSwitch]="field.rawProperties.editor">
<ng-container *ngSwitchCase="'Tags'">
<sqx-tag-editor [formControl]="$any(fieldForm)" [placeholder]="field.displayPlaceholder" [suggestions]="field.rawProperties.allowedValues"></sqx-tag-editor>
<sqx-tag-editor [formControl]="$any(fieldForm)" [placeholder]="field.displayPlaceholder" [itemsSource]="field.rawProperties.allowedValues"></sqx-tag-editor>
</ng-container>
<ng-container *ngSwitchCase="'Checkboxes'">
<sqx-checkbox-group [formControl]="$any(fieldForm)" [values]="field.rawProperties.allowedValues"></sqx-checkbox-group>

8
frontend/src/app/features/content/shared/references/references-tags.component.html

@ -1,9 +1,9 @@
<sqx-tag-editor placeholder="{{ 'common.tagAddReference' | sqxTranslate }}"
(open)="onOpened()"
[formControl]="control"
[allowDuplicates]="false"
[allowOpen]="true"
[converter]="snapshot.converter"
[suggestions]="snapshot.converter.suggestions"
[suggestionsLoading]="snapshot.isLoading">
[formControl]="control"
[itemConverter]="snapshot.converter"
[itemsSource]="snapshot.converter.suggestions"
[itemsSourceLoading]="snapshot.isLoading">
</sqx-tag-editor>

3
frontend/src/app/features/schemas/pages/schema/fields/types/component-validation.component.html

@ -4,7 +4,8 @@
<div class="col-9">
<sqx-tag-editor placeholder="{{ 'common.tagAddSchema' | sqxTranslate }}" formControlName="schemaIds"
[converter]="(schemasSource.converter | async)!" [suggestions]="(schemasSource.converter | async)?.suggestions">
[itemConverter]="(schemasSource.converter | async)!"
[itemsSource]="(schemasSource.converter | async)?.suggestions">
</sqx-tag-editor>
</div>
</div>

3
frontend/src/app/features/schemas/pages/schema/fields/types/components-validation.component.html

@ -22,7 +22,8 @@
<div class="col-9">
<sqx-tag-editor placeholder="{{ 'common.tagAddSchema' | sqxTranslate }}" formControlName="schemaIds"
[converter]="(schemasSource.converter | async)!" [suggestions]="(schemasSource.converter | async)?.suggestions">
[itemConverter]="(schemasSource.converter | async)!"
[itemsSource]="(schemasSource.converter | async)?.suggestions">
</sqx-tag-editor>
</div>
</div>

2
frontend/src/app/features/schemas/pages/schema/fields/types/number-ui.component.html

@ -27,7 +27,7 @@
<label class="col-3 col-form-label">{{ 'schemas.field.allowedValues' | sqxTranslate }}</label>
<div class="col-9">
<sqx-tag-editor formControlName="allowedValues" [converter]="converter"></sqx-tag-editor>
<sqx-tag-editor formControlName="allowedValues" [itemConverter]="converter"></sqx-tag-editor>
</div>
</div>
<div class="form-group row" [class.hidden]="hideInlineEditable | async">

3
frontend/src/app/features/schemas/pages/schema/fields/types/references-validation.component.html

@ -4,7 +4,8 @@
<div class="col-9">
<sqx-tag-editor placeholder="{{ 'common.tagAddSchema' | sqxTranslate }}" formControlName="schemaIds"
[converter]="(schemasSource.normalConverter | async)!" [suggestions]="(schemasSource.normalConverter | async)?.suggestions">
[itemConverter]="(schemasSource.normalConverter | async)!"
[itemsSource]="(schemasSource.normalConverter | async)?.suggestions">
</sqx-tag-editor>
</div>
</div>

3
frontend/src/app/features/schemas/pages/schema/fields/types/string-ui.component.html

@ -91,7 +91,8 @@
<div class="col-9">
<sqx-tag-editor placeholder="{{ 'common.tagAddSchema' | sqxTranslate }}" formControlName="schemaIds"
[converter]="(schemasSource.normalConverter | async)!" [suggestions]="(schemasSource.normalConverter | async)?.suggestions">
[itemConverter]="(schemasSource.normalConverter | async)!"
[itemsSource]="(schemasSource.normalConverter | async)?.suggestions">
</sqx-tag-editor>
</div>
</div>

4
frontend/src/app/features/settings/pages/clients/client.component.html

@ -33,9 +33,9 @@
</label>
<div class="col">
<div class="input-group">
<input readonly class="form-control" value="{{appsState.appName}}:{{client.id}}" #inputName>
<input readonly class="form-control" value="{{appsState.appName}}:{{client.id}}" #clientId>
<button type="button" class="btn btn-outline-secondary" [sqxCopy]="inputName">
<button type="button" class="btn btn-outline-secondary" [sqxCopy]="clientId">
<i class="icon-copy"></i>
</button>
</div>

2
frontend/src/app/features/settings/pages/clients/client.component.ts

@ -50,7 +50,7 @@ export class ClientComponent implements OnChanges {
}
public updateApiCallsLimit() {
this.clientsState.update(this.client, { apiCallsLimit: this.client.apiCallsLimit });
this.clientsState.update(this.client, { apiCallsLimit: this.apiCallsLimit });
}
public rename(name: string) {

4
frontend/src/app/features/settings/pages/contributors/contributor-add-form.component.html

@ -4,7 +4,7 @@
<form [formGroup]="assignContributorForm.form" (ngSubmit)="assignContributor()">
<div class="row gx-2">
<div class="col">
<sqx-autocomplete [source]="usersDataSource" formControlName="user" icon="search" inputName="contributor" placeholder="{{ 'contributors.emailPlaceholder' | sqxTranslate }}" displayProperty="displayName">
<sqx-autocomplete [itemsSource]="usersDataSource" formControlName="user" icon="search" placeholder="{{ 'contributors.emailPlaceholder' | sqxTranslate }}" displayProperty="displayName">
<ng-template let-user="$implicit">
<span class="autocomplete-user">
<img class="user-picture" [src]="user | sqxUserDtoPicture">
@ -29,7 +29,7 @@
<div class="import-hint">
<sqx-form-hint>
{{ 'contributors.importHintg' | sqxTranslate }} <a class="force" (click)="importDialog.show()">{{ 'contributors.importButton' | sqxTranslate }}</a>
{{ 'contributors.importHint' | sqxTranslate }} <a class="force" (click)="importDialog.show()">{{ 'contributors.importButton' | sqxTranslate }}</a>
</sqx-form-hint>
</div>
</div>

2
frontend/src/app/features/settings/pages/languages/language-add-form.component.html

@ -4,7 +4,7 @@
<form [formGroup]="addLanguageForm.form" (ngSubmit)="addLanguage()">
<div class="row gx-2">
<div class="col">
<sqx-autocomplete formControlName="language" displayProperty="iso2Code" valueProperty="iso2Code" [source]="addLanguagesSource">
<sqx-autocomplete formControlName="language" displayProperty="iso2Code" valueProperty="iso2Code" [itemsSource]="addLanguagesSource">
<ng-template let-language="$implicit">
{{language.iso2Code}} ({{language.englishName}})
</ng-template>

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

@ -56,7 +56,7 @@
<div class="col">
<sqx-control-errors [for]="control" [fieldName]="'Permission'"></sqx-control-errors>
<sqx-autocomplete [formControl]="control" [source]="allPermissions"></sqx-autocomplete>
<sqx-autocomplete [formControl]="control" [itemsSource]="allPermissions"></sqx-autocomplete>
</div>
<div class="col-auto" *ngIf="isEditable">
<button type="button" class="btn btn-text-danger" (click)="editForm.form.removeAt(i)">

6
frontend/src/app/features/settings/pages/workflows/workflow-step.component.html

@ -95,12 +95,12 @@
<sqx-tag-editor
[allowDuplicates]="false"
[disabled]="!!disabled"
[itemsSource]="roles" placeholder="{{ 'common.role' | sqxTranslate }}"
[ngModelOptions]="onBlur"
[ngModel]="step.noUpdateRoles"
(ngModelChange)="changeNoUpdateRoles($event)"
[singleLine]="true"
[suggestions]="roles" placeholder="{{ 'common.role' | sqxTranslate }}"
[styleDashed]="true">
[styleDashed]="true"
[styleScrollable]="true">
</sqx-tag-editor>
</div>
<div class="col col-button"></div>

8
frontend/src/app/features/settings/pages/workflows/workflow-transition.component.html

@ -22,15 +22,15 @@
<span class="text-decent">{{ 'workflows.syntax.for' | sqxTranslate }}</span>
</div>
<div class="col col-roles">
<sqx-tag-editor
<sqx-tag-editor placeholder="{{ 'common.role' | sqxTranslate }}"
[allowDuplicates]="false"
[disabled]="!!disabled"
[itemsSource]="roles"
[ngModelOptions]="onBlur"
[ngModel]="transition.roles"
(ngModelChange)="changeRole($event)"
[singleLine]="true"
[styleDashed]="true"
[suggestions]="roles" placeholder="{{ 'common.role' | sqxTranslate }}">
[styleDashed]="true"
[styleScrollable]="true">
</sqx-tag-editor>
</div>

17
frontend/src/app/features/settings/pages/workflows/workflow.component.html

@ -4,10 +4,12 @@
<span class="workflow-name">{{workflow.displayName}}</span>
</div>
<div class="col col-tags">
<sqx-tag-editor [converter]="(schemasSource.normalConverter | async)!" [ngModel]="workflow.schemaIds"
[styleBlank]="true"
[singleLine]="true"
<sqx-tag-editor
[itemConverter]="(schemasSource.normalConverter | async)!"
[ngModel]="workflow.schemaIds"
[readonly]="true">
[styleBlank]="true"
[styleScrollable]="true"
</sqx-tag-editor>
</div>
<div class="col-options">
@ -77,11 +79,12 @@
<label class="col-form-label" for="{{workflow.id}}_schemas">{{ 'common.schemas' | sqxTranslate }}</label>
<div class="col">
<sqx-tag-editor placeholder="{{ 'common.tagAddSchema' | sqxTranslate }}" [converter]="(schemasSource.converter | async)!"
<sqx-tag-editor placeholder="{{ 'common.tagAddSchema' | sqxTranslate }}"
[disabled]="!isEditable"
[ngModel]="workflow.schemaIds"
(ngModelChange)="changeSchemaIds($event)"
[suggestions]="(schemasSource.normalConverter | async)?.suggestions">
[itemConverter]="(schemasSource.converter | async)!"
[itemsSource]="(schemasSource.normalConverter | async)?.suggestions"
[ngModel]="workflow.schemaIds"
(ngModelChange)="changeSchemaIds($event)">
</sqx-tag-editor>
<sqx-form-hint>

2
frontend/src/app/features/teams/pages/contributors/contributor-add-form.component.html

@ -4,7 +4,7 @@
<form [formGroup]="assignContributorForm.form" (ngSubmit)="assignContributor()">
<div class="row gx-2">
<div class="col">
<sqx-autocomplete [source]="usersDataSource" formControlName="user" icon="search" inputName="contributor" placeholder="{{ 'contributors.emailPlaceholder' | sqxTranslate }}" displayProperty="displayName">
<sqx-autocomplete [itemsSource]="usersDataSource" formControlName="user" icon="search" placeholder="{{ 'contributors.emailPlaceholder' | sqxTranslate }}" displayProperty="displayName">
<ng-template let-user="$implicit">
<span class="autocomplete-user">
<img class="user-picture" [src]="user | sqxUserDtoPicture">

6
frontend/src/app/features/teams/state/team-contributors.state.spec.ts

@ -5,8 +5,8 @@
* Copyright (c) Squidex UG (haftungsbeschränkt). All rights reserved.
*/
import { EMPTY, of, throwError } from 'rxjs';
import { catchError, onErrorResumeNext } from 'rxjs/operators';
import { EMPTY, of, onErrorResumeNextWith, throwError } from 'rxjs';
import { catchError } from 'rxjs/operators';
import { IMock, It, Mock, Times } from 'typemoq';
import { TeamContributorsService, TeamContributorsState } from '@app/features/teams/internal';
import { ContributorDto, ContributorsPayload, DialogService, ErrorDto, versioned } from '@app/shared';
@ -65,7 +65,7 @@ describe('TeamContributorsState', () => {
contributorsService.setup(x => x.getContributors(team))
.returns(() => throwError(() => 'Service Error'));
contributorsState.load().pipe(onErrorResumeNext()).subscribe();
contributorsState.load().pipe(onErrorResumeNextWith()).subscribe();
expect(contributorsState.snapshot.isLoading).toBeFalsy();
});

9
frontend/src/app/features/teams/state/team-plans.state.spec.ts

@ -5,8 +5,7 @@
* Copyright (c) Squidex UG (haftungsbeschränkt). All rights reserved.
*/
import { of, throwError } from 'rxjs';
import { onErrorResumeNext } from 'rxjs/operators';
import { of, onErrorResumeNextWith, throwError } from 'rxjs';
import { IMock, It, Mock, Times } from 'typemoq';
import { TeamPlansService, TeamPlansState } from '@app/features/teams/internal';
import { DialogService, PlanDto, PlanLockedReason, versioned } from '@app/shared';
@ -84,7 +83,7 @@ describe('TeamPlansState', () => {
plansService.setup(x => x.getPlans(team))
.returns(() => throwError(() => 'Service Error'));
plansState.load().pipe(onErrorResumeNext()).subscribe();
plansState.load().pipe(onErrorResumeNextWith()).subscribe();
expect(plansState.snapshot.isLoading).toBeFalsy();
});
@ -119,7 +118,7 @@ describe('TeamPlansState', () => {
plansService.setup(x => x.putPlan(team, It.isAny(), version))
.returns(() => of(versioned(newVersion, result)));
plansState.change('free').pipe(onErrorResumeNext()).subscribe();
plansState.change('free').pipe(onErrorResumeNextWith()).subscribe();
expect(plansState.snapshot.plans).toEqual([
{ isSelected: true, isYearlySelected: false, plan: oldPlans.plans[0] },
@ -133,7 +132,7 @@ describe('TeamPlansState', () => {
plansService.setup(x => x.putPlan(team, It.isAny(), version))
.returns(() => of(versioned(newVersion, { redirectUri: '' })));
plansState.change('id2_yearly').pipe(onErrorResumeNext()).subscribe();
plansState.change('id2_yearly').pipe(onErrorResumeNextWith()).subscribe();
expect(plansState.snapshot.plans).toEqual([
{ isSelected: false, isYearlySelected: false, plan: oldPlans.plans[0] },

12
frontend/src/app/framework/angular/forms/editors/autocomplete.component.html

@ -1,15 +1,15 @@
<div class="control-container">
<input class="form-control" (blur)="blur()" (keydown)="onKeyDown($event)" #input
<input class="form-control" #input
(blur)="blur()" (keydown)="onKeyDown($event)"
[sqxFocusOnInit]="autoFocus"
[name]="inputName"
[placeholder]="placeholder"
autocapitalize="off"
autocomplete="off"
autocorrect="off"
autocapitalize="off"
[class.form-empty]="inputStyle === 'empty'"
[class.form-underlined]="inputStyle === 'underlined'"
[class.form-icon]="!!icon"
[formControl]="queryInput">
[class.form-underlined]="inputStyle === 'underlined'"
[formControl]="queryInput"
[placeholder]="placeholder">
<div class="icon" *ngIf="icon">
<ng-container *ngIf="snapshot.isLoading; else notLoading">

11
frontend/src/app/framework/angular/forms/editors/autocomplete.component.ts

@ -36,7 +36,7 @@ interface State {
const NO_EMIT = { emitEvent: false };
@Component({
selector: 'sqx-autocomplete',
selector: 'sqx-autocomplete[itemsSource]',
styleUrls: ['./autocomplete.component.scss'],
templateUrl: './autocomplete.component.html',
providers: [
@ -49,10 +49,7 @@ export class AutocompleteComponent extends StatefulControlComponent<State, Reado
private timer: any;
@Input()
public source!: AutocompleteSource;
@Input()
public inputName = 'autocompletion';
public itemsSource!: AutocompleteSource;
@Input()
public inputStyle?: 'underlined' | 'empty';
@ -139,12 +136,12 @@ export class AutocompleteComponent extends StatefulControlComponent<State, Reado
this.own(
merge(inputStream, this.modalStream).pipe(
switchMap(query => {
if (!this.source) {
if (!this.itemsSource) {
return of([]);
} else {
this.setLoading(true);
return this.source.find(query).pipe(
return this.itemsSource.find(query).pipe(
finalize(() => {
this.setLoading(false);
}),

6
frontend/src/app/framework/angular/forms/editors/autocomplete.stories.ts

@ -50,7 +50,7 @@ const Template: Story<AutocompleteComponent & { model: any }> = (args: Autocompl
[disabled]="disabled"
[icon]="icon"
[inputStyle]="inputStyle"
[source]="source">
[itemsSource]="itemsSource">
</sqx-autocomplete>
</sqx-root-view>
`,
@ -71,7 +71,7 @@ class Source implements AutocompleteSource {
export const Default = Template.bind({});
Default.args = {
source: new Source(['Lorem', 'ipsum', 'dolor', 'sit', 'amet', 'consectetur', 'adipiscing']),
itemsSource: new Source(['Lorem', 'ipsum', 'dolor', 'sit', 'amet', 'consectetur', 'adipiscing']),
};
export const Disabled = Template.bind({});
@ -101,6 +101,6 @@ StyleUnderlined.args = {
export const IconLoading = Template.bind({});
IconLoading.args = {
source: new Source(['Lorem', 'ipsum', 'dolor', 'sit', 'amet', 'consectetur', 'adipiscing'], 4000),
itemsSource: new Source(['Lorem', 'ipsum', 'dolor', 'sit', 'amet', 'consectetur', 'adipiscing'], 4000),
icon: 'user',
};

4
frontend/src/app/framework/angular/forms/editors/dropdown.component.html

@ -29,9 +29,9 @@
<div class="control-dropdown-items" #container>
<div *ngFor="let item of snapshot.suggestedItems; let i = index;" class="control-dropdown-item control-dropdown-item-selectable"
(mousedown)="selectIndexAndClose(i)"
[class.active]="i === snapshot.suggestedIndex"
[class.separated]="itemSeparator"
(mousedown)="selectIndexAndClose(i)"
[class.separated]="itemSeparator"
[sqxScrollActive]="i === snapshot.suggestedIndex"
[sqxScrollContainer]="container">
<ng-container *ngIf="!templateItem">{{item}}</ng-container>

44
frontend/src/app/framework/angular/forms/editors/tag-editor.component.html

@ -1,35 +1,33 @@
<div class="form-container">
<div class="form-control tags" tabindex="0" #form
(mousedown)="focusInput($event)"
(focus)="focusInput($event)"
(focus)="focusInput($event)" (mousedown)="focusInput($event)"
[class.blank]="styleBlank"
[class.singleline]="singleLine"
[class.readonly]="readonly"
[class.suggested]="suggestionsSorted.length > 0"
[class.multiline]="!singleLine"
[class.focus]="snapshot.hasFocus"
[class.dashed]="styleDashed && !(snapshot.tags.length > 0)"
[class.disabled]="snapshot.isDisabled"
[class.dashed]="styleDashed && !(snapshot.items.length > 0)">
<span class="item" *ngFor="let item of snapshot.items; let i = index" [class.disabled]="addInput.disabled">
{{item}} <i class="icon-close" (click)="remove(i)"></i>
[class.focus]="snapshot.hasFocus"
[class.multiline]="!styleScrollable"
[class.readonly]="readonly"
[class.singleline]="styleScrollable"
[class.suggested]="itemsSorted.length > 0">
<span class="item" *ngFor="let tag of snapshot.tags; let i = index" [class.disabled]="addInput.disabled">
{{tag}} <i class="icon-close" (click)="remove(i)"></i>
</span>
<input class="blank text-input" #input
(blur)="markTouched()" (copy)="onCopy($event)" (cut)="onCut($event)" (focus)="focus()" (keydown)="onKeyDown($event)" (paste)="onPaste($event)"
[name]="inputName"
[placeholder]="placeholder | sqxTranslate"
autocomplete="off"
autocomplete="off"
autocorrect="off"
autocapitalize="off"
spellcheck="false"
[formControl]="addInput">
</div>
<div class="btn btn-sm" (click)="openModal()" sqxStopClick *ngIf="!readonly && (allowOpen || suggestionsSorted.length > 0)">
<div class="btn btn-sm" (click)="openModal()" sqxStopClick *ngIf="!readonly && (allowOpen || itemsSorted.length > 0)">
<i class="icon-caret-down"></i>
</div>
<ng-container *sqxModal="snapshot.suggestedItems.length > 0">
<ng-container *sqxModal="snapshot.itemsList.length > 0">
<sqx-dropdown-menu class="control-dropdown"
[sqxAnchoredTo]="form"
[adjustWidth]="true"
@ -37,27 +35,27 @@
[scrollY]="true"
[style.minWidth]="dropdownWidth"
position="bottom-left" #container>
<div *ngFor="let item of snapshot.suggestedItems; let i = index" class="control-dropdown-item control-dropdown-item-selectable"
[class.active]="i === snapshot.suggestedIndex"
<div *ngFor="let item of snapshot.itemsList; let i = index" class="control-dropdown-item control-dropdown-item-selectable"
[class.active]="i === snapshot.itemsIndex"
[class.separated]="itemSeparator"
(mousedown)="selectValue(item)"
(mouseover)="selectIndex(i)"
[sqxScrollActive]="i === snapshot.suggestedIndex"
[sqxScrollActive]="i === snapshot.itemsIndex"
[sqxScrollContainer]="container.nativeElement">
<ng-container>{{item}}</ng-container>
</div>
</sqx-dropdown-menu>
</ng-container>
<ng-container *sqxModal="suggestionsModal">
<ng-container *sqxModal="itemsModal">
<sqx-dropdown-menu class="control-dropdown suggestions-dropdown"
[sqxAnchoredTo]="form"
[adjustWidth]="false"
[adjustHeight]="false"
[scrollY]="true"
[scrollY]="true"
position="bottom-left">
<div class="row">
<div class=" col-6" *ngFor="let item of suggestionsSorted; let i = index">
<div class=" col-6" *ngFor="let item of itemsSorted; let i = index">
<div class="form-check form-check">
<input class="form-check-input" type="checkbox" id="tag_{{i}}"
[ngModel]="isSelected(item)"
@ -71,13 +69,13 @@
</div>
</div>
<div class="text-decent" *ngIf="suggestionsSorted.length === 0">
<ng-container *ngIf="suggestionsLoading; else notLoading">
<div class="text-decent" *ngIf="itemsSorted.length === 0">
<ng-container *ngIf="itemsSourceLoading; else notLoading">
<sqx-loader color="input"></sqx-loader>
</ng-container>
<ng-template #notLoading>
<small>{{suggestionsEmptyText | sqxTranslate}}</small>
<small>{{itemsSourceEmptyText | sqxTranslate}}</small>
</ng-template>
</div>
</sqx-dropdown-menu>

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

@ -19,13 +19,13 @@ interface State {
hasFocus: boolean;
// The suggested item.
suggestedItems: ReadonlyArray<TagValue>;
itemsList: ReadonlyArray<TagValue>;
// The index of the selected suggested items.
suggestedIndex: number;
itemsIndex: number;
// All available tag values.
items: ReadonlyArray<TagValue>;
tags: ReadonlyArray<TagValue>;
}
@Component({
@ -58,7 +58,7 @@ export class TagEditorComponent extends StatefulControlComponent<State, Readonly
public blur = new EventEmitter();
@Input()
public converter = StringConverter.INSTANCE;
public itemConverter = StringConverter.INSTANCE;
@Input()
public undefinedWhenEmpty?: boolean | null = true;
@ -72,12 +72,6 @@ export class TagEditorComponent extends StatefulControlComponent<State, Readonly
@Input()
public allowDuplicates?: boolean | null = true;
@Input()
public itemSeparator?: boolean | null;
@Input()
public singleLine?: boolean | null;
@Input()
public readonly?: boolean | null;
@ -88,19 +82,22 @@ export class TagEditorComponent extends StatefulControlComponent<State, Readonly
public styleBlank?: boolean | null;
@Input()
public placeholder = 'i18n:common.tagAdd';
public styleScrollable?: boolean | null;
@Input()
public inputName = 'tag-editor';
public placeholder = 'i18n:common.tagAdd';
@Input()
public dropdownWidth = '18rem';
@Input()
public suggestionsLoading?: boolean | null;
public itemSeparator?: boolean | null;
@Input()
public itemsSourceLoading?: boolean | null;
@Input()
public suggestionsEmptyText = 'i18n:common.empty';
public itemsSourceEmptyText = 'i18n:common.empty';
@Input()
public set disabled(value: boolean | undefined | null) {
@ -108,32 +105,32 @@ export class TagEditorComponent extends StatefulControlComponent<State, Readonly
}
@Input()
public set suggestions(value: ReadonlyArray<string | TagValue> | undefined | null) {
this.suggestionsSorted = getTagValues(value);
public set itemsSource(value: ReadonlyArray<string | TagValue> | undefined | null) {
this.itemsSorted = getTagValues(value);
if (this.addInput.value) {
const query = this.addInput.value;
const items = this.suggestionsSorted.filter(s => s.lowerCaseName.includes(query) && !this.snapshot.items.find(x => x.id === s.id));
const items = this.itemsSorted.filter(s => s.lowerCaseName.includes(query) && !this.snapshot.tags.find(x => x.id === s.id));
this.next({
suggestedIndex: -1,
suggestedItems: items || [],
itemsIndex: -1,
itemsList: items || [],
});
}
}
public suggestionsSorted: ReadonlyArray<TagValue> = [];
public suggestionsModal = new ModalModel();
public itemsSorted: ReadonlyArray<TagValue> = [];
public itemsModal = new ModalModel();
public addInput = new UntypedFormControl();
constructor(changeDetector: ChangeDetectorRef) {
super(changeDetector, {
hasFocus: false,
suggestedItems: [],
suggestedIndex: 0,
items: [],
itemsList: [],
itemsIndex: 0,
tags: [],
});
this.textMeasurer = new TextMeasurer(() => this.inputElement);
@ -175,16 +172,16 @@ export class TagEditorComponent extends StatefulControlComponent<State, Readonly
map(query => {
if (!query) {
return [];
} else if (Types.isArray(this.suggestionsSorted)) {
return this.suggestionsSorted.filter(s => s.lowerCaseName.includes(query) && !this.snapshot.items.find(x => x.id === s.id));
} else if (Types.isArray(this.itemsSorted)) {
return this.itemsSorted.filter(s => s.lowerCaseName.includes(query) && !this.snapshot.tags.find(x => x.id === s.id));
} else {
return [];
}
}))
.subscribe(suggestedItems => {
this.next({
suggestedIndex: -1,
suggestedItems,
itemsIndex: -1,
itemsList: suggestedItems,
});
}));
}
@ -199,12 +196,12 @@ export class TagEditorComponent extends StatefulControlComponent<State, Readonly
const items: any[] = [];
if (this.converter && Types.isArray(obj)) {
if (this.itemConverter && Types.isArray(obj)) {
for (const value of obj) {
if (Types.is(value, TagValue)) {
items.push(value);
} else {
const converted = this.converter.convertValue(value);
const converted = this.itemConverter.convertValue(value);
if (converted) {
items.push(converted);
@ -213,7 +210,7 @@ export class TagEditorComponent extends StatefulControlComponent<State, Readonly
}
}
this.next({ items });
this.next({ tags: items });
}
public onDisabled(isDisabled: boolean) {
@ -240,7 +237,7 @@ export class TagEditorComponent extends StatefulControlComponent<State, Readonly
}
public remove(index: number) {
this.updateItems(this.snapshot.items.filter((_, i) => i !== index), true);
this.updateItems(this.snapshot.tags.filter((_, i) => i !== index), true);
}
public resetSize() {
@ -261,7 +258,7 @@ export class TagEditorComponent extends StatefulControlComponent<State, Readonly
this.inputElement.nativeElement.style.width = `${width + 5}px`;
if (this.singleLine) {
if (this.styleScrollable) {
setTimeout(() => {
this.formElement.nativeElement.scrollLeft = this.formElement.nativeElement.scrollWidth;
}, 0);
@ -275,11 +272,11 @@ export class TagEditorComponent extends StatefulControlComponent<State, Readonly
const value = this.addInput.value as string;
if (!value || value.length === 0) {
this.updateItems(this.snapshot.items.slice(0, this.snapshot.items.length - 1), false);
this.updateItems(this.snapshot.tags.slice(0, this.snapshot.tags.length - 1), false);
return false;
}
} else if (Keys.isEscape(event) && this.suggestionsModal.isOpen) {
} else if (Keys.isEscape(event) && this.itemsModal.isOpen) {
this.closeModal();
return false;
} else if (Keys.isUp(event)) {
@ -289,8 +286,8 @@ export class TagEditorComponent extends StatefulControlComponent<State, Readonly
this.selectNextIndex();
return false;
} else if (Keys.isEnter(event)) {
if (this.snapshot.suggestedIndex >= 0) {
if (this.selectValue(this.snapshot.suggestedItems[this.snapshot.suggestedIndex])) {
if (this.snapshot.itemsIndex >= 0) {
if (this.selectValue(this.snapshot.itemsList[this.snapshot.itemsIndex])) {
return false;
}
} else if (this.acceptEnter) {
@ -311,14 +308,14 @@ export class TagEditorComponent extends StatefulControlComponent<State, Readonly
let tagValue: TagValue | null;
if (Types.isString(value)) {
tagValue = this.converter.convertInput(value);
tagValue = this.itemConverter.convertInput(value);
} else {
tagValue = value;
}
if (tagValue) {
if (this.allowDuplicates || !this.isSelected(tagValue)) {
this.updateItems([...this.snapshot.items, tagValue], true);
this.updateItems([...this.snapshot.tags, tagValue], true);
}
this.resetForm();
@ -331,18 +328,18 @@ export class TagEditorComponent extends StatefulControlComponent<State, Readonly
public toggleValue(isSelected: boolean, tagValue: TagValue) {
if (isSelected) {
this.updateItems([...this.snapshot.items, tagValue], true);
this.updateItems([...this.snapshot.tags, tagValue], true);
} else {
this.updateItems(this.snapshot.items.filter(x => x.id !== tagValue.id), true);
this.updateItems(this.snapshot.tags.filter(x => x.id !== tagValue.id), true);
}
}
public selectPrevIndex() {
this.selectIndex(this.snapshot.suggestedIndex - 1);
this.selectIndex(this.snapshot.itemsIndex - 1);
}
public selectNextIndex() {
this.selectIndex(this.snapshot.suggestedIndex + 1);
this.selectIndex(this.snapshot.itemsIndex + 1);
}
public selectIndex(suggestedIndex: number) {
@ -350,11 +347,11 @@ export class TagEditorComponent extends StatefulControlComponent<State, Readonly
suggestedIndex = 0;
}
if (suggestedIndex >= this.snapshot.suggestedItems.length) {
suggestedIndex = this.snapshot.suggestedItems.length - 1;
if (suggestedIndex >= this.snapshot.itemsList.length) {
suggestedIndex = this.snapshot.itemsList.length - 1;
}
this.next({ suggestedIndex });
this.next({ itemsIndex: suggestedIndex });
}
public resetFocus(): any {
@ -362,7 +359,7 @@ export class TagEditorComponent extends StatefulControlComponent<State, Readonly
}
private resetAutocompletion() {
this.next({ suggestedItems: [], suggestedIndex: -1 });
this.next({ itemsList: [], itemsIndex: -1 });
}
private resetForm() {
@ -370,22 +367,22 @@ export class TagEditorComponent extends StatefulControlComponent<State, Readonly
}
public isSelected(tagValue: TagValue) {
return this.snapshot.items.find(x => x.id === tagValue.id);
return this.snapshot.tags.find(x => x.id === tagValue.id);
}
public closeModal() {
if (this.suggestionsModal.isOpen) {
if (this.itemsModal.isOpen) {
this.close.emit();
this.suggestionsModal.hide();
this.itemsModal.hide();
}
}
public openModal() {
if (!this.suggestionsModal.isOpen) {
if (!this.itemsModal.isOpen) {
this.open.emit();
this.suggestionsModal.show();
this.itemsModal.show();
}
}
@ -412,7 +409,7 @@ export class TagEditorComponent extends StatefulControlComponent<State, Readonly
public onCopy(event: ClipboardEvent) {
if (!this.hasSelection()) {
if (event.clipboardData) {
event.clipboardData.setData('text/plain', this.snapshot.items.map(x => x.name).join(','));
event.clipboardData.setData('text/plain', this.snapshot.tags.map(x => x.name).join(','));
}
event.preventDefault();
@ -426,10 +423,10 @@ export class TagEditorComponent extends StatefulControlComponent<State, Readonly
if (value) {
this.resetForm();
const values = [...this.snapshot.items];
const values = [...this.snapshot.tags];
for (const part of value.split(',')) {
const converted = this.converter.convertInput(part);
const converted = this.itemConverter.convertInput(part);
if (converted) {
values.push(converted);
@ -451,7 +448,7 @@ export class TagEditorComponent extends StatefulControlComponent<State, Readonly
}
private updateItems(items: ReadonlyArray<TagValue>, touched: boolean) {
this.next({ items });
this.next({ tags: items });
if (items.length === 0 && this.undefinedWhenEmpty) {
this.callChange(undefined);

36
frontend/src/app/framework/angular/forms/editors/tag-editor.stories.ts

@ -23,24 +23,24 @@ const TRANSLATIONS = {
<sqx-root-view>
<sqx-tag-editor
[allowOpen]="true"
[suggestions]="suggestions"
[suggestionsLoading]="suggestionsLoading"
[itemsSource]="itemsSource"
[itemsSourceLoading]="itemsSourceLoading"
(open)="load()">
</sqx-tag-editor>
</sqx-root-view>
`,
})
class TestComponent {
public suggestions: string[] = [];
public suggestionsLoading = false;
public itemsSource: string[] = [];
public itemsSourceLoading = false;
public load() {
this.suggestions = [];
this.suggestionsLoading = true;
this.itemsSource = [];
this.itemsSourceLoading = true;
setTimeout(() => {
this.suggestions = ['A', 'B'];
this.suggestionsLoading = false;
this.itemsSource = ['A', 'B'];
this.itemsSourceLoading = false;
}, 1000);
}
}
@ -80,12 +80,12 @@ const Template: Story<TagEditorComponent & { ngModel: any }> = (args: TagEditorC
<sqx-tag-editor
[allowOpen]="allowOpen"
[disabled]="disabled"
[itemsSource]="itemsSource"
[itemsSourceLoading]="itemsSourceLoading"
[ngModel]="ngModel"
[singleLine]="singleLine"
[styleScrollable]="styleScrollable"
[styleBlank]="styleBlank"
[styleDashed]="styleDashed"
[suggestions]="suggestions"
[suggestionsLoading]="suggestionsLoading">
[styleDashed]="styleDashed">
</sqx-tag-editor>
</sqx-root-view>
`,
@ -103,28 +103,28 @@ export const Default = Template.bind({});
export const Suggestions = Template.bind({});
Suggestions.args = {
suggestions: ['A', 'B', 'C'],
itemsSource: ['A', 'B', 'C'],
allowOpen: true,
};
export const SuggestionsEmpty = Template.bind({});
SuggestionsEmpty.args = {
suggestions: [],
itemsSource: [],
allowOpen: true,
};
export const SuggestionsLoading = Template.bind({});
SuggestionsLoading.args = {
suggestionsLoading: true,
itemsSourceLoading: true,
allowOpen: true,
};
export const Values = Template.bind({});
Values.args = {
suggestions: [],
itemsSource: [],
ngModel: ['A', 'A', 'B'],
};
@ -159,14 +159,14 @@ StyleBlankValues.args = {
export const Multiline = Template.bind({});
Multiline.args = {
singleLine: false,
styleScrollable: false,
ngModel: ['Lorem', 'ipsum', 'dolor', 'sit', 'amet', 'consectetur', 'adipiscing', 'elit', 'sed', 'do', 'eiusmod', 'tempor', 'incididunt', 'ut', 'labore', 'et', 'dolore', 'magna', 'aliqua'],
};
export const SingleLine = Template.bind({});
SingleLine.args = {
singleLine: true,
styleScrollable: true,
ngModel: ['Lorem', 'ipsum', 'dolor', 'sit', 'amet', 'consectetur', 'adipiscing', 'elit', 'sed', 'do', 'eiusmod', 'tempor', 'incididunt', 'ut', 'labore', 'et', 'dolore', 'magna', 'aliqua'],
};

5
frontend/src/app/framework/angular/modals/modal.directive.ts

@ -43,6 +43,9 @@ export class ModalDirective implements OnDestroy {
@Input('sqxModalCloseAlways')
public closeAlways = false;
@Input('sqxModalIsDialog')
public isDialog = false;
constructor(
private readonly changeDetector: ChangeDetectorRef,
private readonly renderer: Renderer2,
@ -115,7 +118,7 @@ export class ModalDirective implements OnDestroy {
}
private subscribeToView() {
if (Types.is(this.currentModel, DialogModel)) {
if (Types.is(this.currentModel, DialogModel) || this.isDialog) {
return;
}

44
frontend/src/app/framework/utils/rxjs-extensions.ts

@ -5,9 +5,8 @@
* Copyright (c) Squidex UG (haftungsbeschränkt). All rights reserved.
*/
import { EMPTY, Observable, ReplaySubject, throwError } from 'rxjs';
import { catchError, debounceTime, distinctUntilChanged, filter, map, share, switchMap } from 'rxjs/operators';
import { onErrorResumeNext } from 'rxjs/operators';
import { EMPTY, Observable, of, onErrorResumeNextWith, ReplaySubject, throwError } from 'rxjs';
import { catchError, debounceTime, distinctUntilChanged, filter, map, share, switchMap, tap } from 'rxjs/operators';
import { DialogService } from './../services/dialog.service';
import { Version, versioned, Versioned } from './version';
@ -55,9 +54,9 @@ export function shareMapSubscribed<T, R = T>(dialogs: DialogService, project: (v
export function debounceTimeSafe<T>(duration: number) {
return function mapOperation(source: Observable<T>) {
if (duration > 0) {
return source.pipe(debounceTime(duration), onErrorResumeNext());
return source.pipe(debounceTime(duration), onErrorResumeNextWith());
} else {
return source.pipe(onErrorResumeNext());
return source.pipe(onErrorResumeNextWith());
}
};
}
@ -69,22 +68,27 @@ export function defined<T>() {
}
export function switchSafe<T, R>(project: (source: T) => Observable<R>) {
return function mapOperation(source: Observable<T>) {
return source.pipe(
switchMap(x => {
try {
return project(x).pipe(catchError(_ => EMPTY));
} catch {
return EMPTY;
}
}));
};
return switchMap<T, Observable<R>>(x => {
try {
return project(x).pipe(catchError(_ => EMPTY));
} catch {
return EMPTY;
}
});
}
export function ofForever<T>(...values: ReadonlyArray<T>) {
return new Observable<T>(s => {
for (const value of values) {
s.next(value);
export function switchMapCached<R>(project: (source: string) => Observable<R>) {
const cache: { [key: string]: R } = {};
return switchMap<string, Observable<R>>(x => {
const cached = cache[x];
if (cached) {
return of(cached);
}
return project(x).pipe(tap(result => {
cache[x] = result;
}));
});
}
}

34
frontend/src/app/shared/components/assets/asset-dialog.component.html

@ -77,7 +77,35 @@
<label for="id">{{ 'common.folder' | sqxTranslate }}</label>
<div class="path">
<sqx-asset-path [path]="path | async" [all]="true" (navigate)="navigate($event.id)"></sqx-asset-path>
<ng-container *ngIf="isMoving; else pathTemplate">
<form [formGroup]="moveForm.form" (ngSubmit)="moveAsset()">
<div class="row align-items-center g-2">
<div class="col">
<sqx-asset-folder-dropdown formControlName="parentId"></sqx-asset-folder-dropdown>
</div>
<div class="col-auto">
<button type="submit" class="btn btn-primary">
{{ 'assets.move' | sqxTranslate }}
</button>
</div>
</div>
</form>
</ng-container>
<ng-template #pathTemplate>
<div class="row align-items-center g-2">
<div class="col">
<sqx-asset-path [path]="pathItems | async" [all]="true" (navigate)="navigate($event.id)"></sqx-asset-path>
</div>
<div class="col-auto" *ngIf="isMoveable">
<button type="button" class="btn btn-outline-secondary" (click)="startMoving()">
{{ 'assets.move' | sqxTranslate }}
</button>
</div>
</div>
</ng-template>
</div>
</div>
@ -125,12 +153,12 @@
</button>
</div>
<div class="form-group">
<div class="form-group" *ngIf="annotateTags | async; let tags">
<label>{{ 'common.tags' | sqxTranslate }}</label>
<sqx-control-errors for="tags"></sqx-control-errors>
<sqx-tag-editor [suggestions]="allTags" [allowDuplicates]="false" [undefinedWhenEmpty]="false" formControlName="tags"></sqx-tag-editor>
<sqx-tag-editor [itemsSource]="tags" [allowDuplicates]="false" [undefinedWhenEmpty]="false" formControlName="tags"></sqx-tag-editor>
</div>
<div class="form-group">

4
frontend/src/app/shared/components/assets/asset-dialog.component.scss

@ -41,10 +41,6 @@
}
}
.path {
min-height: 2.5rem;
}
.slug {
padding-right: 6rem;
}

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

@ -5,33 +5,33 @@
* Copyright (c) Squidex UG (haftungsbeschränkt). All rights reserved.
*/
import { ChangeDetectorRef, Component, EventEmitter, Input, OnChanges, Output, QueryList, ViewChildren } from '@angular/core';
import { Observable } from 'rxjs';
import { ChangeDetectorRef, Component, EventEmitter, Input, OnInit, Output, QueryList, ViewChildren } from '@angular/core';
import { BehaviorSubject, Observable } from 'rxjs';
import { map } from 'rxjs/operators';
import { AnnotateAssetDto, AnnotateAssetForm, AppsState, AssetDto, AssetsState, AssetUploaderState, AuthService, DialogService, Types, UploadCanceled } from '@app/shared/internal';
import { AssetsService } from '@app/shared/services/assets.service';
import { AnnotateAssetDto, AnnotateAssetForm, AppsState, AssetDto, AssetsState, AssetUploaderState, AuthService, DialogService, MoveAssetForm, switchMapCached, Types, UploadCanceled } from '@app/shared/internal';
import { AssetsService, MoveAssetItemDto } from '@app/shared/services/assets.service';
import { AssetPathItem, ROOT_ITEM } from '@app/shared/state/assets.state';
import { AssetTextEditorComponent } from './asset-text-editor.component';
import { ImageCropperComponent } from './image-cropper.component';
import { ImageFocusPointComponent } from './image-focus-point.component';
@Component({
selector: 'sqx-asset-dialog[allTags][asset]',
selector: 'sqx-asset-dialog[asset]',
styleUrls: ['./asset-dialog.component.scss'],
templateUrl: './asset-dialog.component.html',
})
export class AssetDialogComponent implements OnChanges {
export class AssetDialogComponent implements OnInit {
@Output()
public complete = new EventEmitter();
@Output()
public changed = new EventEmitter<AssetDto>();
public assetReplaced = new EventEmitter<AssetDto>();
@Input()
public asset!: AssetDto;
@Output()
public assetUpdated = new EventEmitter<AssetDto>();
@Input()
public allTags!: ReadonlyArray<string>;
public asset!: AssetDto;
@ViewChildren(ImageCropperComponent)
public imageCropper!: QueryList<ImageCropperComponent>;
@ -42,15 +42,21 @@ export class AssetDialogComponent implements OnChanges {
@ViewChildren(AssetTextEditorComponent)
public textEditor!: QueryList<AssetTextEditorComponent>;
public path!: Observable<ReadonlyArray<AssetPathItem>>;
public pathSource = new BehaviorSubject<string>('');
public pathItems!: Observable<ReadonlyArray<AssetPathItem>>;
public progress = 0;
public selectedTab = 0;
public isEditable = false;
public isEditableAny = false;
public isUploadable = false;
public isMoving = false;
public isMoveable = false;
public progress = 0;
public moveForm = new MoveAssetForm();
public annotateTags!: Observable<string[]>;
public annotateForm = new AnnotateAssetForm();
public get isImage() {
@ -76,32 +82,52 @@ export class AssetDialogComponent implements OnChanges {
) {
}
public ngOnChanges() {
public ngOnInit() {
this.annotateTags =
this.assetsService.getTags(this.appsState.appName).pipe(
map(tags => Object.keys(tags)));
this.pathItems =
this.pathSource.pipe(
switchMapCached(x => this.assetsService.getAssetFolders(this.appsState.appName, x, 'Path')), map(({ path }) => [ROOT_ITEM, ...path]));
this.selectTab(0);
this.isEditable = this.asset.canUpdate;
this.isUploadable = this.asset.canUpload;
this.assetchanged(this.asset);
}
private assetchanged(asset: AssetDto) {
this.pathSource.next(asset.parentId);
this.isEditable = asset.canUpdate;
this.isUploadable = asset.canUpload;
this.isMoveable = asset.canMove;
this.annotateForm.load(this.asset);
this.annotateForm.load(asset);
this.annotateForm.setEnabled(this.isEditable);
this.path =
this.assetsService.getAssetFolders(this.appsState.appName, this.asset.parentId, 'Path').pipe(
map(folders => [ROOT_ITEM, ...folders.path]));
}
this.moveForm.load(asset);
this.moveForm.setEnabled(this.isMoveable);
public navigate(id: string) {
this.assetsState.navigate(id);
this.asset = asset;
}
public selectTab(tab: number) {
this.selectedTab = tab;
}
public navigate(id: string) {
this.assetsState.navigate(id);
}
public generateSlug() {
this.annotateForm.generateSlug(this.asset);
}
public startMoving() {
this.isMoving = true;
}
public emitComplete() {
this.complete.emit();
}
@ -133,7 +159,8 @@ export class AssetDialogComponent implements OnChanges {
if (Types.isNumber(dto)) {
this.setProgress(dto);
} else {
this.changed.emit(dto);
this.assetReplaced.emit(dto);
this.assetchanged(dto);
this.dialogs.notifyInfo('i18n:assets.updated');
}
@ -144,7 +171,7 @@ export class AssetDialogComponent implements OnChanges {
}
},
complete: () => {
this.setProgress(0);
this.setProgress(0);
},
});
} else {
@ -158,7 +185,7 @@ export class AssetDialogComponent implements OnChanges {
return;
}
this.annoateAssetInternal(this.imageFocus.first.submit(this.asset));
this.annotateInternal(this.imageFocus.first.submit(this.asset));
}
public annotateAsset() {
@ -166,25 +193,59 @@ export class AssetDialogComponent implements OnChanges {
return;
}
this.annoateAssetInternal(this.annotateForm.submit(this.asset));
this.annotateInternal(this.annotateForm.submit(this.asset));
}
private annoateAssetInternal(value: AnnotateAssetDto | null) {
if (value) {
this.assetsState.updateAsset(this.asset, value)
.subscribe({
next: () => {
this.annotateForm.submitCompleted({ noReset: true });
public moveAsset() {
if (!this.isMoveable) {
return;
}
this.dialogs.notifyInfo('i18n:assets.updated');
},
error: error => {
this.annotateForm.submitFailed(error);
},
});
} else {
this.moveInternal(this.moveForm.submit());
}
private annotateInternal(value: AnnotateAssetDto | null) {
if (!value) {
this.dialogs.notifyInfo('i18n:common.nothingChanged');
return;
}
this.assetsState.updateAsset(this.asset, value)
.subscribe({
next: dto => {
this.assetUpdated.emit(dto);
this.assetchanged(dto);
this.annotateForm.submitCompleted({ noReset: true });
this.dialogs.notifyInfo('i18n:assets.updated');
},
error: error => {
this.annotateForm.submitFailed(error);
},
});
}
private moveInternal(values: MoveAssetItemDto | null) {
if (!values) {
this.isMoving = false;
return;
}
this.assetsState.moveAsset(this.asset, values.parentId)
.subscribe({
next: (dto) => {
this.assetUpdated.emit(dto);
this.assetchanged(dto);
this.annotateForm.submitCompleted({ noReset: true });
this.dialogs.notifyInfo('i18n:assets.moved');
},
complete: () => {
this.isMoving = false;
},
});
}
public setProgress(progress: number) {

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

@ -1,26 +1,25 @@
<div class="control-dropdown-item d-flex align-items-center" [class.active]="node.isSelected" [style.paddingLeft]="paddingLeft" (click)="selectNode.emit(node)">
<ng-container *ngIf="node.isLoading; else notLoading" class="loader">
<div class="control-dropdown-item d-flex align-items-center" [class.active]="nodeModel.isSelected" [style.paddingLeft]="paddingLeft" (click)="selectNode.emit(nodeModel)">
<ng-container *ngIf="nodeModel.isLoading; else notLoading" class="loader">
<button type="button" class="btn btn-sm btn-decent btn-text-secondary">
<sqx-loader></sqx-loader>>
<sqx-loader [size]="14"></sqx-loader>
</button>
</ng-container>
<ng-template #notLoading>
<button type="button" class="btn btn-sm btn-decent btn-text-secondary" (click)="toggle()" sqxStopClick [class.invisible]="node.isLoaded && node.children.length === 0">
<i [class.icon-caret-right]="!node.isExpanded || !node.isLoaded" [class.icon-caret-down]="node.isExpanded && node.isLoaded"></i>
<button type="button" class="btn btn-sm btn-decent btn-text-secondary" (click)="toggle()" sqxStopClick [class.invisible]="nodeModel.isLoaded && nodeModel.children.length === 0">
<i [class.icon-caret-right]="!nodeModel.isExpanded || !nodeModel.isLoaded" [class.icon-caret-down]="nodeModel.isExpanded && nodeModel.isLoaded"></i>
</button>
</ng-template>
<div class="name truncate">
{{node.item.folderName | sqxTranslate}}
{{nodeModel.item.folderName | sqxTranslate}}
</div>
</div>
<div class="tree-children" *ngIf="node.isExpanded || node.isSelected">
<sqx-asset-folder-dropdown-item *ngFor="let child of node.children; trackBy: trackByNode"
<div class="tree-children" *ngIf="nodeModel.isExpanded || nodeModel.isSelected">
<sqx-asset-folder-dropdown-item *ngFor="let child of nodeModel.children; trackBy: trackByNode"
[appName]="appName"
[node]="child"
[nodeModel]="child"
[nodeLevel]="nodeLevel + 1"
(selectNode)="selectNode.emit($event)">
</sqx-asset-folder-dropdown-item>

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

@ -1,6 +1,10 @@
@import 'mixins';
@import 'vars';
.name {
cursor: default;
}
.loader {
font-size: 60%;
flex-grow: 0;

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

@ -5,12 +5,12 @@
* Copyright (c) Squidex UG (haftungsbeschränkt). All rights reserved.
*/
import { Component, EventEmitter, Input, Output } from '@angular/core';
import { ChangeDetectorRef, Component, EventEmitter, Input, Output } from '@angular/core';
import { AssetsService } from '@app/shared/internal';
import { AssetFolderDropdowNode } from './asset-folder-dropdown.state';
@Component({
selector: 'sqx-asset-folder-dropdown-item[appName][node]',
selector: 'sqx-asset-folder-dropdown-item[appName][nodeModel]',
styleUrls: ['./asset-folder-dropdown-item.component.scss'],
templateUrl: './asset-folder-dropdown-item.component.html',
})
@ -19,7 +19,7 @@ export class AssetFolderDropdownItemComponent {
public appName!: string;
@Input()
public node!: AssetFolderDropdowNode;
public nodeModel!: AssetFolderDropdowNode;
@Input()
public nodeLevel = 0;
@ -33,11 +33,12 @@ export class AssetFolderDropdownItemComponent {
constructor(
private readonly assetsService: AssetsService,
private readonly changeDetector: ChangeDetectorRef,
) {
}
public toggle() {
if (this.node.isExpanded && this.node.isLoaded) {
if (this.nodeModel.isExpanded && this.nodeModel.isLoaded) {
this.collapse();
} else {
this.expand();
@ -45,27 +46,27 @@ export class AssetFolderDropdownItemComponent {
}
public collapse() {
this.node.isExpanded = false;
this.nodeModel.isExpanded = false;
}
public expand() {
this.node.isExpanded = true;
this.nodeModel.isExpanded = true;
this.loadChildren();
}
public loadChildren() {
if (this.node.isLoading || this.node.isLoaded) {
if (this.nodeModel.isLoading || this.nodeModel.isLoaded) {
return;
}
this.node.isLoading = true;
this.nodeModel.isLoading = true;
this.assetsService.getAssetFolders(this.appName, this.node.item.id, 'Items')
this.assetsService.getAssetFolders(this.appName, this.nodeModel.item.id, 'Items')
.subscribe({
next: dto => {
if (dto.items.length > 0) {
const parent = this.node;
const parent = this.nodeModel;
for (const item of dto.items) {
if (!parent.children.find(x => x.item.id === item.id)) {
@ -76,11 +77,13 @@ export class AssetFolderDropdownItemComponent {
parent.children.sortByString(x => x.item.folderName);
}
this.node.isLoaded = true;
this.nodeModel.isLoaded = true;
this.changeDetector.detectChanges();
},
complete: () => {
setTimeout(() => {
this.node.isLoading = false;
this.nodeModel.isLoading = false;
this.changeDetector.detectChanges();
}, 250);
},
});

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

@ -14,7 +14,7 @@
<div class="control-dropdown" [sqxAnchoredTo]="input" [scrollY]="true" position="bottom-left">
<sqx-asset-folder-dropdown-item
[appName]="appName"
[node]="root"
[nodeModel]="root"
[nodeLevel]="0"
(selectNode)="select($event)">
</sqx-asset-folder-dropdown-item>

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

@ -113,6 +113,8 @@ export class AssetFolderDropdownComponent extends StatefulControlComponent<any,
this.dropdown.hide();
}
this.detectChanges();
}
private resetSelected(node: AssetFolderDropdowNode) {

6
frontend/src/app/shared/components/assets/asset-folder.component.html

@ -8,12 +8,12 @@
{{assetFolder.folderName | sqxTranslate}}
</div>
<div class="col-auto">
<ng-container *ngIf="canDelete || canUpdate">
<button type="button" class="btn btn-sm btn-text-secondary" (click)="dropdown.toggle()" #buttonOptions>
<ng-container *ngIf="(canDelete || canUpdate) && !isDisabled">
<button type="button" class="btn btn-sm btn-text-secondary" (click)="editDropdown.toggle()" #buttonOptions>
<i class="icon-dots"></i>
</button>
<ng-container *sqxModal="dropdown;closeAlways:true">
<ng-container *sqxModal="editDropdown;closeAlways:true">
<sqx-dropdown-menu [sqxAnchoredTo]="buttonOptions" [scrollY]="true">
<ng-container *ngIf="canUpdate">
<a class="dropdown-item" (click)="editDialog.show()">

14
frontend/src/app/shared/components/assets/asset-folder.component.ts

@ -22,10 +22,12 @@ export class AssetFolderComponent {
public delete = new EventEmitter<AssetFolderDto>();
@Input()
public assetPathItem!: AssetPathItem;
public isDisabled?: boolean | null;
public dropdown = new ModalModel();
@Input()
public assetPathItem!: AssetPathItem;
public editDropdown = new ModalModel();
public editDialog = new DialogModel();
public get assetFolder(): AssetFolderDto {
@ -47,10 +49,18 @@ export class AssetFolderComponent {
}
public emitDelete() {
if (this.isDisabled) {
return;
}
this.delete.emit(this.assetFolder);
}
public emitNavigate() {
if (this.isDisabled) {
return;
}
this.navigate.emit(this.assetPathItem);
}
}

30
frontend/src/app/shared/components/assets/asset-selector.component.html

@ -1,4 +1,4 @@
<sqx-modal-dialog (close)="emitComplete()" size="lg" [fullHeight]="true" [hasTabs]="false">
<sqx-modal-dialog (close)="emitComplete()" size="lg" [fullHeight]="true" [flexBody]="true" [hasTabs]="false">
<ng-container title>
{{ 'assets.selectMany' | sqxTranslate }}
</ng-container>
@ -13,10 +13,11 @@
<div class="col">
<div class="row g-0 search">
<div class="col-6">
<sqx-tag-editor class="tags" [singleLine]="true" placeholder="{{ 'assets.searchByTags' | sqxTranslate }}"
<sqx-tag-editor class="tags" placeholder="{{ 'assets.searchByTags' | sqxTranslate }}"
[itemsSource]="assetsState.tagsNames | async"
(ngModelChange)="selectTags($event)" [undefinedWhenEmpty]="false"
[ngModel]="assetsState.selectedTagNames | async"
[suggestions]="assetsState.tagsNames | async">
[styleScrollable]="true">
</sqx-tag-editor>
</div>
<div class="col-6">
@ -43,14 +44,21 @@
</ng-container>
<ng-container content>
<sqx-assets-list
[assetsState]="assetsState"
[indicateLoading]="true"
[isDisabled]="true"
[isListView]="snapshot.isListView"
(select)="selectAsset($event)"
[selectedIds]="snapshot.selectedAssets">
</sqx-assets-list>
<sqx-list-view [isLoading]="assetsState.isLoading | async" [overflow]="true">
<ng-container content>
<sqx-assets-list
[assetsState]="assetsState"
[isDisabled]="true"
[isListView]="snapshot.isListView"
(select)="selectAsset($event)"
[selectedIds]="snapshot.selectedAssets">
</sqx-assets-list>
</ng-container>
<ng-container footer>
<sqx-pager (loadTotal)="reloadTotal()" [paging]="assetsState.paging | async" (pagingChange)="assetsState.page($event)"></sqx-pager>
</ng-container>
</sqx-list-view>
</ng-container>
<ng-container footer>

4
frontend/src/app/shared/components/assets/asset-selector.component.ts

@ -55,6 +55,10 @@ export class AssetSelectorComponent extends StatefulComponent<State> implements
this.assetsState.load(true);
}
public reloadTotal() {
this.assetsState.load(true, false);
}
public search(query: Query) {
this.assetsState.search(query);
}

11
frontend/src/app/shared/components/assets/asset-uploader.component.ts

@ -6,7 +6,7 @@
*/
import { ChangeDetectionStrategy, Component } from '@angular/core';
import { AppsState, AssetsState, AssetUploaderState, ModalModel, Upload } from '@app/shared/internal';
import { AppsState, AssetsState, AssetUploaderState, ModalModel, Types, Upload } from '@app/shared/internal';
@Component({
selector: 'sqx-asset-uploader',
@ -26,7 +26,14 @@ export class AssetUploaderComponent {
public addFiles(files: ReadonlyArray<File>) {
for (const file of files) {
this.assetUploader.uploadFile(file, this.assetsState);
this.assetUploader.uploadFile(file)
.subscribe({
next: assetOrProgress => {
if (!Types.isNumber(assetOrProgress)) {
this.assetsState.addAsset(assetOrProgress);
}
},
});
}
this.modalMenu.show();

18
frontend/src/app/shared/components/assets/asset.component.html

@ -26,7 +26,7 @@
<div class="overlay-background"></div>
<div class="overlay-menu">
<a class="file-edit ms-2" (click)="edit()" *ngIf="!isDisabled">
<a class="file-edit ms-2" (click)="emitEdit()" *ngIf="!isDisabled">
<i class="icon-pencil"></i>
</a>
@ -81,10 +81,10 @@
<sqx-progress-bar mode="Circle" [value]="snapshot.progress"></sqx-progress-bar>
</div>
</div>
<div class="card-footer" (dblclick)="edit()">
<div class="card-footer" (dblclick)="emitEdit()">
<ng-container *ngIf="asset">
<div>
<div class="file-name truncate editable" (click)="edit()">
<div class="file-name truncate editable" (click)="emitEdit()">
<i class="icon-lock" *ngIf="asset?.isProtected"></i>
{{asset.fileName}}
@ -124,7 +124,7 @@
<table class="table-fixed">
<tr>
<td class="col-name">
<div class="file-name truncate editable" (click)="edit()">
<div class="file-name truncate editable" (click)="emitEdit()">
<i class="icon-lock" *ngIf="asset?.isProtected"></i>
{{asset.fileName}}
@ -184,12 +184,4 @@
</sqx-progress-bar>
</div>
</div>
</ng-template>
<ng-container *ngIf="asset">
<ng-container *sqxModal="editDialog">
<sqx-asset-dialog [allTags]="allTags"
[asset]="asset" (changed)="setAsset($event)" (complete)="editDialog.hide()">
</sqx-asset-dialog>
</ng-container>
</ng-container>
</ng-template>

40
frontend/src/app/shared/components/assets/asset.component.ts

@ -6,7 +6,7 @@
*/
import { ChangeDetectionStrategy, ChangeDetectorRef, Component, EventEmitter, HostBinding, Input, OnInit, Output } from '@angular/core';
import { AssetDto, AssetsState, AssetUploaderState, DialogModel, DialogService, StatefulComponent, Types, UploadCanceled } from '@app/shared/internal';
import { AssetDto, AssetUploaderState, DialogService, StatefulComponent, Types, UploadCanceled } from '@app/shared/internal';
interface State {
// The download progress.
@ -21,7 +21,7 @@ interface State {
})
export class AssetComponent extends StatefulComponent<State> implements OnInit {
@Output()
public load = new EventEmitter<AssetDto>();
public loadDone = new EventEmitter<AssetDto>();
@Output()
public loadError = new EventEmitter();
@ -32,6 +32,9 @@ export class AssetComponent extends StatefulComponent<State> implements OnInit {
@Output()
public delete = new EventEmitter();
@Output()
public edit = new EventEmitter<AssetDto>();
@Output()
public select = new EventEmitter();
@ -44,9 +47,6 @@ export class AssetComponent extends StatefulComponent<State> implements OnInit {
@Input()
public asset?: AssetDto;
@Input()
public assetsState!: AssetsState;
@Input()
public folderId?: string;
@ -71,11 +71,6 @@ export class AssetComponent extends StatefulComponent<State> implements OnInit {
@Input() @HostBinding('class.isListView')
public isListView?: boolean | null;
@Input()
public allTags!: ReadonlyArray<string>;
public editDialog = new DialogModel();
constructor(changeDetector: ChangeDetectorRef,
private readonly assetUploader: AssetUploaderState,
private readonly dialogs: DialogService,
@ -91,13 +86,13 @@ export class AssetComponent extends StatefulComponent<State> implements OnInit {
if (assetFile) {
this.setProgress(1);
this.assetUploader.uploadFile(assetFile, this.assetsState, this.folderId)
this.assetUploader.uploadFile(assetFile, this.folderId)
.subscribe({
next: dto => {
if (Types.isNumber(dto)) {
this.setProgress(dto);
next: assetOrProgress => {
if (Types.isNumber(assetOrProgress)) {
this.setProgress(assetOrProgress);
} else {
this.emitLoad(dto);
this.emitLoad(assetOrProgress);
}
},
error: error => {
@ -122,12 +117,11 @@ export class AssetComponent extends StatefulComponent<State> implements OnInit {
this.assetUploader.uploadAsset(asset, files[0])
.subscribe({
next: asset => {
if (Types.isNumber(asset)) {
this.setProgress(asset);
next: assetOrProgress => {
if (Types.isNumber(assetOrProgress)) {
this.setProgress(assetOrProgress);
} else {
this.setProgress(0);
this.setAsset(asset);
this.emitLoad(assetOrProgress);
}
},
error: error => {
@ -144,14 +138,14 @@ export class AssetComponent extends StatefulComponent<State> implements OnInit {
}
}
public edit() {
public emitEdit() {
if (!this.isDisabled) {
this.editDialog.show();
this.edit.emit(this.asset);
}
}
public emitLoad(asset: AssetDto) {
this.load.emit(asset);
this.loadDone.emit(asset);
}
public emitLoadError(error: any) {

140
frontend/src/app/shared/components/assets/assets-list.component.html

@ -14,81 +14,75 @@
<div class="file-drop-info">{{ 'assets.uploadHint' | sqxTranslate }}</div>
</div>
<sqx-list-view [isLoading]="(assetsState.isLoading | async) && indicateLoading" [overflow]="true">
<ng-container topHeader>
<div cdkDropListGroup *ngIf="assetsState.path | async; let path">
<div class="folders" *ngIf="path.length > 0">
<ng-container *ngIf="(assetsState.hasFolders | async) || (assetsState.parentFolder | async)">
<h5>{{ 'common.folders' | sqxTranslate }}</h5>
</ng-container>
<div class="row g-0">
<div class="folder-container" *ngIf="assetsState.parentFolder | async; let parent"
cdkDropList
[cdkDropListData]="parent?.id"
(cdkDropListDropped)="move($event)">
<div class="folder-container-over"></div>
<div cdkDropListGroup *ngIf="assetsState.path | async; let path">
<div class="folders" *ngIf="path.length > 0">
<ng-container *ngIf="(assetsState.hasFolders | async) || (assetsState.parentFolder | async)">
<h5>{{ 'common.folders' | sqxTranslate }}</h5>
</ng-container>
<div class="row g-0">
<div class="folder-container" *ngIf="assetsState.parentFolder | async; let parent"
cdkDropList
[cdkDropListData]="parent?.id"
(cdkDropListDropped)="move($event)">
<div class="folder-container-over"></div>
<sqx-asset-folder [assetPathItem]="parent"
(navigate)="assetsState.navigate($event.id)">
</sqx-asset-folder>
</div>
<sqx-asset-folder [assetPathItem]="parent"
(navigate)="assetsState.navigate($event.id)">
</sqx-asset-folder>
</div>
<div class="folder-container" *ngFor="let assetFolder of assetsState.folders | async; trackBy: trackByAssetFolder"
cdkDropList
cdkDropListSortingDisabled
[cdkDropListEnterPredicate]="canEnter"
[cdkDropListData]="assetFolder.id"
(cdkDropListDropped)="move($event)">
<div class="folder-container-over"></div>
<sqx-asset-folder [assetPathItem]="assetFolder"
cdkDrag
[cdkDragData]="assetFolder"
[cdkDragDisabled]="isDisabled || !assetFolder.canMove"
(navigate)="assetsState.navigate($event.id)" (delete)="deleteAssetFolder($event)">
</sqx-asset-folder>
</div>
</div>
<div class="folder-container" *ngFor="let assetFolder of assetsState.folders | async; trackBy: trackByAssetFolder"
cdkDropList
cdkDropListSortingDisabled
[cdkDropListEnterPredicate]="canEnter"
[cdkDropListData]="assetFolder.id"
(cdkDropListDropped)="move($event)">
<div class="folder-container-over"></div>
<sqx-asset-folder [assetPathItem]="assetFolder"
cdkDrag
[cdkDragData]="assetFolder"
[cdkDragDisabled]="isDisabled || !assetFolder.canMove"
[isDisabled]="isDisabled"
(navigate)="assetsState.navigate($event.id)" (delete)="deleteAssetFolder($event)">
</sqx-asset-folder>
</div>
</div>
</div>
<ng-container *ngIf="assetsState.assets | async; let assets">
<ng-container *ngIf="assets.length > 0 || snapshot.newFiles.length > 0">
<h5>{{ 'common.files' | sqxTranslate }}</h5>
</ng-container>
<ng-container *ngIf="assetsState.assets | async; let assets">
<ng-container *ngIf="assets.length > 0 || snapshot.newFiles.length > 0">
<h5>{{ 'common.files' | sqxTranslate }}</h5>
</ng-container>
<div class="row g-0" [class.unrow]="isListView" *ngIf="assetsState.tagsNames | async; let tags"
cdkDropList
cdkDropListSortingDisabled>
<sqx-asset *ngFor="let file of snapshot.newFiles"
[assetFile]="file"
[assetsState]="assetsState"
[isListView]="isListView"
(loadError)="remove(file)"
(load)="add(file, $event)">
</sqx-asset>
<sqx-asset *ngFor="let asset of assets; trackBy: trackByAsset"
cdkDrag
[cdkDragData]="asset"
[cdkDragDisabled]="isDisabled || !asset.canMove"
[allTags]="tags"
[asset]="asset"
[assetsState]="assetsState"
[isListView]="isListView"
[isDisabled]="isDisabled"
[isSelectable]="!!selectedIds"
[isSelected]="isSelected(asset)"
[folderIcon]="showFolderIcon && path.length === 0"
(select)="select.emit(asset)" (delete)="deleteAsset(asset)"
(selectFolder)="selectFolder(asset)">
</sqx-asset>
</div>
</ng-container>
<div class="row g-0" [class.unrow]="isListView"
cdkDropList
cdkDropListSortingDisabled>
<sqx-asset *ngFor="let file of snapshot.newFiles"
[assetFile]="file"
[isDisabled]="isDisabled"
[isListView]="isListView"
(loadDone)="add(file, $event)"
(loadError)="remove(file)">
</sqx-asset>
<sqx-asset *ngFor="let asset of assets; trackBy: trackByAsset"
cdkDrag
[asset]="asset"
[cdkDragData]="asset"
[cdkDragDisabled]="isDisabled || !asset.canMove"
(delete)="deleteAsset(asset)"
(edit)="edit.emit($event)"
[folderIcon]="showFolderIcon && path.length === 0"
[isDisabled]="isDisabled"
[isListView]="isListView"
[isSelectable]="!!selectedIds"
[isSelected]="isSelected(asset)"
(loadDone)="replaceAsset($event)"
(select)="select.emit(asset)"
(selectFolder)="selectFolder(asset)">
</sqx-asset>
</div>
</ng-container>
</sqx-list-view>
<ng-container *ngIf="showPager">
<sqx-pager (loadTotal)="reloadTotal()" [autoHide]="true" [paging]="assetsState.paging | async" (pagingChange)="assetsState.page($event)"></sqx-pager>
</ng-container>
</div>

13
frontend/src/app/shared/components/assets/assets-list.component.ts

@ -21,6 +21,9 @@ interface State {
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class AssetsListComponent extends StatefulComponent<State> {
@Output()
public edit = new EventEmitter<AssetDto>();
@Output()
public select = new EventEmitter<AssetDto>();
@ -33,18 +36,12 @@ export class AssetsListComponent extends StatefulComponent<State> {
@Input()
public isListView?: boolean | null;
@Input()
public indicateLoading?: boolean | null;
@Input()
public selectedIds?: {};
@Input()
public showFolderIcon?: boolean | null = true;
@Input()
public showPager?: boolean | null = true;
constructor(changeDetector: ChangeDetectorRef) {
super(changeDetector, {
newFiles: [],
@ -75,8 +72,8 @@ export class AssetsListComponent extends StatefulComponent<State> {
}
}
public reloadTotal() {
this.assetsState.load(true, false);
public replaceAsset(asset: AssetDto) {
this.assetsState.replaceAsset(asset);
}
public selectFolder(asset: AssetDto) {

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

@ -169,7 +169,7 @@ export class ContentListCellDirective extends ResourceOwner implements OnChanges
}
@Directive({
selector: '[sqxContentListCellResize]',
selector: '[sqxContentListCellResize][field][fields]',
})
export class ContentListCellResizeDirective implements OnInit, OnDestroy {
private mouseMove?: Function;
@ -179,13 +179,10 @@ export class ContentListCellResizeDirective implements OnInit, OnDestroy {
private windowBlur?: Function;
private startOffset = 0;
private startWidth = 0;
private fieldName?: string;
private resizer: any;
@Input()
public set field(value: TableField) {
this.fieldName = value.rootField?.name;
}
public field!: TableField;
@Input('fields')
public tableFields?: TableSettings;
@ -209,7 +206,7 @@ export class ContentListCellResizeDirective implements OnInit, OnDestroy {
}
public ngOnInit() {
if (!this.tableFields || !this.fieldName) {
if (!this.tableFields || !this.field) {
return;
}
@ -223,7 +220,7 @@ export class ContentListCellResizeDirective implements OnInit, OnDestroy {
}
private onMouseDown = (event: MouseEvent) => {
if (!this.tableFields || !this.fieldName) {
if (!this.tableFields || !this.field) {
return;
}
@ -238,7 +235,7 @@ export class ContentListCellResizeDirective implements OnInit, OnDestroy {
};
private onMouseMove = (event: MouseEvent) => {
if (!this.mouseMove || !this.tableFields || !this.fieldName) {
if (!this.mouseMove || !this.tableFields || !this.field) {
return;
}
@ -250,7 +247,7 @@ export class ContentListCellResizeDirective implements OnInit, OnDestroy {
};
private onMouseUp = (event: MouseEvent) => {
if (!this.mouseMove || !this.tableFields || !this.fieldName) {
if (!this.mouseMove || !this.tableFields || !this.field) {
return;
}
@ -272,7 +269,7 @@ export class ContentListCellResizeDirective implements OnInit, OnDestroy {
width = this.minimumWidth;
}
this.tableFields!.updateSize(this.fieldName!, width, save);
this.tableFields!.updateSize(this.field.name!, width, save);
}
private resetMovement() {

2
frontend/src/app/shared/components/forms/markdown-editor.component.ts

@ -292,7 +292,7 @@ export class MarkdownEditorComponent extends StatefulControlComponent<State, str
}
};
this.assetUploader.uploadFile(file, undefined, this.folderId)
this.assetUploader.uploadFile(file, this.folderId)
.subscribe({
next: asset => {
if (Types.is(asset, AssetDto)) {

4
frontend/src/app/shared/components/forms/rich-editor.component.ts

@ -119,7 +119,7 @@ export class RichEditorComponent extends StatefulControlComponent<{}, string> im
images_upload_handler: (blob: any, success: (url: string) => void, failure: (message: string) => void) => {
const file = new File([blob.blob()], blob.filename(), { lastModified: new Date().getTime() });
self.assetUploader.uploadFile(file, undefined, this.folderId)
self.assetUploader.uploadFile(file, this.folderId)
.subscribe({
next: asset => {
if (Types.is(asset, AssetDto)) {
@ -292,7 +292,7 @@ export class RichEditorComponent extends StatefulControlComponent<{}, string> im
this.tinyEditor.setContent(content);
};
this.assetUploader.uploadFile(file, undefined, this.folderId)
this.assetUploader.uploadFile(file, this.folderId)
.subscribe({
next: asset => {
if (Types.is(asset, AssetDto)) {

9
frontend/src/app/shared/interceptors/auth.interceptor.spec.ts

@ -12,8 +12,7 @@ import { HTTP_INTERCEPTORS, HttpClient, HttpHeaders } from '@angular/common/http
import { HttpClientTestingModule, HttpTestingController } from '@angular/common/http/testing';
import { inject, TestBed } from '@angular/core/testing';
import { Router } from '@angular/router';
import { of } from 'rxjs';
import { onErrorResumeNext } from 'rxjs/operators';
import { of, onErrorResumeNextWith } from 'rxjs';
import { IMock, Mock, Times } from 'typemoq';
import { ApiUrlConfig, AuthService } from '@app/shared/internal';
import { AuthInterceptor } from './auth.interceptor';
@ -101,7 +100,7 @@ describe('AuthInterceptor', () => {
authService.setup(x => x.userChanges).returns(() => of(<any>{ authToken: 'letmein' }));
authService.setup(x => x.loginSilent()).returns(() => of(<any>{ authToken: 'letmereallyin' }));
http.get('http://service/p/apps').pipe(onErrorResumeNext()).subscribe();
http.get('http://service/p/apps').pipe(onErrorResumeNextWith()).subscribe();
httpMock.expectOne('http://service/p/apps').error(<any>{}, { status: 401 });
httpMock.expectOne('http://service/p/apps').error(<any>{}, { status: 401 });
@ -118,7 +117,7 @@ describe('AuthInterceptor', () => {
inject([HttpClient, HttpTestingController], (http: HttpClient, httpMock: HttpTestingController) => {
authService.setup(x => x.userChanges).returns(() => of(<any>{ authToken: 'letmein' }));
http.get('http://service/p/apps').pipe(onErrorResumeNext()).subscribe();
http.get('http://service/p/apps').pipe(onErrorResumeNextWith()).subscribe();
const req = httpMock.expectOne('http://service/p/apps');
@ -137,7 +136,7 @@ describe('AuthInterceptor', () => {
inject([HttpClient, HttpTestingController], (http: HttpClient, httpMock: HttpTestingController) => {
authService.setup(x => x.userChanges).returns(() => of(<any>{ authToken: 'letmein' }));
http.get('http://service/p/apps').pipe(onErrorResumeNext()).subscribe();
http.get('http://service/p/apps').pipe(onErrorResumeNextWith()).subscribe();
const req = httpMock.expectOne('http://service/p/apps');

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

@ -13,7 +13,7 @@ import { MentionModule } from 'angular-mentions';
import { NgChartsModule } from 'ng2-charts';
import { NgxDocViewerModule } from 'ngx-doc-viewer';
import { SqxFrameworkModule } from '@app/framework';
import { ApiCallsCardComponent, ApiCallsSummaryCardComponent, ApiPerformanceCardComponent, ApiTrafficCardComponent, ApiTrafficSummaryCardComponent, AppFormComponent, AppLanguagesService, AppMustExistGuard, AppsService, AppsState, AssetComponent, AssetDialogComponent, AssetFolderComponent, AssetFolderDialogComponent, AssetFolderDropdownComponent, AssetFolderDropdownItemComponent, AssetHistoryComponent, AssetPathComponent, AssetPreviewUrlPipe, AssetScriptsState, AssetsListComponent, AssetSelectorComponent, AssetsService, AssetsState, AssetTextEditorComponent, AssetUploaderComponent, AssetUploaderState, AssetUploadsCountCardComponent, AssetUploadsSizeCardComponent, AssetUploadsSizeSummaryCardComponent, 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, HelpComponent, HelpMarkdownPipe, HelpService, HistoryComponent, HistoryListComponent, HistoryMessagePipe, HistoryService, IFrameCardComponent, ImageCropperComponent, ImageFocusPointComponent, LanguagesService, LanguagesState, LoadAppsGuard, LoadLanguagesGuard, LoadSchemasGuard, LoadTeamsGuard, MarkdownEditorComponent, MustBeAuthenticatedGuard, MustBeNotAuthenticatedGuard, NewsService, NotifoComponent, PlansService, PlansState, PreviewableType, QueryComponent, QueryListComponent, QueryPathComponent, RandomCatCardComponent, RandomDogCardComponent, ReferenceInputComponent, RichEditorComponent, RolesService, RolesState, RuleEventsState, RuleMustExistGuard, RuleSimulatorState, RulesService, RulesState, SavedQueriesComponent, SchemaCategoryComponent, SchemaMustExistGuard, SchemaMustExistPublishedGuard, SchemaMustNotBeSingletonGuard, SchemasService, SchemasState, SchemaTagSource, SearchFormComponent, SearchService, SortingComponent, StockPhotoService, SupportCardComponent, TableHeaderComponent, TeamFormComponent, TeamMustExistGuard, TeamsService, TeamsState, TemplatesService, TemplatesState, TranslationsService, TranslationStatusComponent, UIService, UIState, UnsetAppGuard, UnsetTeamGuard, UsagesService, UserDtoPicture, UserIdPicturePipe, UserNamePipe, UserNameRefPipe, UserPicturePipe, UserPictureRefPipe, UsersProviderService, UsersService, WatchingUsersComponent, WorkflowsService, WorkflowsState, ScriptNamePipe } from './declarations';
import { ApiCallsCardComponent, ApiCallsSummaryCardComponent, ApiPerformanceCardComponent, ApiTrafficCardComponent, ApiTrafficSummaryCardComponent, AppFormComponent, AppLanguagesService, AppMustExistGuard, AppsService, AppsState, AssetComponent, AssetDialogComponent, AssetFolderComponent, AssetFolderDialogComponent, AssetFolderDropdownComponent, AssetFolderDropdownItemComponent, AssetHistoryComponent, AssetPathComponent, AssetPreviewUrlPipe, AssetScriptsState, AssetSelectorComponent, AssetsListComponent, AssetsService, AssetsState, AssetTextEditorComponent, AssetUploaderComponent, AssetUploaderState, AssetUploadsCountCardComponent, AssetUploadsSizeCardComponent, AssetUploadsSizeSummaryCardComponent, 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, HelpComponent, HelpMarkdownPipe, HelpService, HistoryComponent, HistoryListComponent, HistoryMessagePipe, HistoryService, IFrameCardComponent, ImageCropperComponent, ImageFocusPointComponent, LanguagesService, LanguagesState, LoadAppsGuard, LoadLanguagesGuard, LoadSchemasGuard, LoadTeamsGuard, MarkdownEditorComponent, MustBeAuthenticatedGuard, MustBeNotAuthenticatedGuard, NewsService, NotifoComponent, PlansService, PlansState, PreviewableType, QueryComponent, QueryListComponent, QueryPathComponent, RandomCatCardComponent, RandomDogCardComponent, ReferenceInputComponent, RichEditorComponent, RolesService, RolesState, RuleEventsState, RuleMustExistGuard, RuleSimulatorState, RulesService, RulesState, SavedQueriesComponent, SchemaCategoryComponent, SchemaMustExistGuard, SchemaMustExistPublishedGuard, SchemaMustNotBeSingletonGuard, SchemasService, SchemasState, SchemaTagSource, ScriptNamePipe, SearchFormComponent, SearchService, SortingComponent, StockPhotoService, SupportCardComponent, TableHeaderComponent, TeamFormComponent, TeamMustExistGuard, TeamsService, TeamsState, TemplatesService, TemplatesState, TranslationsService, TranslationStatusComponent, UIService, UIState, UnsetAppGuard, UnsetTeamGuard, UsagesService, UserDtoPicture, UserIdPicturePipe, UserNamePipe, UserNameRefPipe, UserPicturePipe, UserPictureRefPipe, UsersProviderService, UsersService, WatchingUsersComponent, WorkflowsService, WorkflowsState } from './declarations';
@NgModule({
imports: [

40
frontend/src/app/shared/services/assets.service.spec.ts

@ -358,24 +358,52 @@ describe('AssetsService', () => {
expect(asset!).toEqual(createAsset(123));
}));
it('should make delete request to move asset item',
it('should make put request to move asset',
inject([AssetsService, HttpTestingController], (assetsService: AssetsService, httpMock: HttpTestingController) => {
const resource: Resource = {
_links: {
move: { method: 'DELETE', href: 'api/apps/my-app/assets/123/parent' },
move: { method: 'PUT', href: 'api/apps/my-app/assets/123/parent' },
},
};
const dto = { parentId: 'parent1' };
let asset: AssetDto;
assetsService.putAssetItemParent('my-app', resource, dto, version).subscribe();
assetsService.putAssetParent('my-app', resource, { parentId: 'parent1' }, version).subscribe(result => {
asset = result;
});
const req = httpMock.expectOne('http://service/p/api/apps/my-app/assets/123/parent');
expect(req.request.method).toEqual('DELETE');
expect(req.request.method).toEqual('PUT');
expect(req.request.headers.get('If-Match')).toEqual(version.value);
req.flush({});
req.flush(assetResponse(123));
expect(asset!).toEqual(createAsset(123));
}));
it('should make put request to move asset folder',
inject([AssetsService, HttpTestingController], (assetsService: AssetsService, httpMock: HttpTestingController) => {
const resource: Resource = {
_links: {
move: { method: 'PUT', href: 'api/apps/my-app/assets/folders/123/parent' },
},
};
let assetFolder: AssetFolderDto;
assetsService.putAssetFolderParent('my-app', resource, { parentId: 'parent1' }, version).subscribe(result => {
assetFolder = result;
});
const req = httpMock.expectOne('http://service/p/api/apps/my-app/assets/folders/123/parent');
expect(req.request.method).toEqual('PUT');
expect(req.request.headers.get('If-Match')).toEqual(version.value);
req.flush(assetFolderResponse(123));
expect(assetFolder!).toEqual(createAssetFolder(123));
}));
it('should make delete request to delete asset item',

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

@ -374,12 +374,27 @@ export class AssetsService {
pretifyError('i18n:assets.updateFolderFailed'));
}
public putAssetItemParent(appName: string, resource: Resource, dto: MoveAssetItemDto, version: Version): Observable<Versioned<any>> {
public putAssetParent(appName: string, resource: Resource, dto: MoveAssetItemDto, version: Version): Observable<AssetDto> {
const link = resource._links['move'];
const url = this.apiUrl.buildUrl(link.href);
return HTTP.requestVersioned(this.http, link.method, url, version, dto).pipe(
map(({ payload }) => {
return parseAsset(payload.body);
}),
pretifyError('i18n:assets.moveFailed'));
}
public putAssetFolderParent(appName: string, resource: Resource, dto: MoveAssetItemDto, version: Version): Observable<AssetFolderDto> {
const link = resource._links['move'];
const url = this.apiUrl.buildUrl(link.href);
return HTTP.requestVersioned(this.http, link.method, url, version, dto).pipe(
map(({ payload }) => {
return parseAssetFolder(payload.body);
}),
pretifyError('i18n:assets.moveFailed'));
}

5
frontend/src/app/shared/services/users-provider.service.spec.ts

@ -5,8 +5,7 @@
* Copyright (c) Squidex UG (haftungsbeschränkt). All rights reserved.
*/
import { of, throwError } from 'rxjs';
import { onErrorResumeNext } from 'rxjs/operators';
import { of, onErrorResumeNextWith, throwError } from 'rxjs';
import { IMock, Mock, Times } from 'typemoq';
import { AuthService, Profile, UserDto, UsersProviderService, UsersService } from '@app/shared/internal';
@ -86,7 +85,7 @@ describe('UsersProviderService', () => {
let resultingUser: UserDto;
usersProviderService.getUser('123').pipe(onErrorResumeNext()).subscribe(result => {
usersProviderService.getUser('123').pipe(onErrorResumeNextWith()).subscribe(result => {
resultingUser = result;
}).unsubscribe();

5
frontend/src/app/shared/state/asset-scripts.state.spec.ts

@ -5,8 +5,7 @@
* Copyright (c) Squidex UG (haftungsbeschränkt). All rights reserved.
*/
import { of, throwError } from 'rxjs';
import { onErrorResumeNext } from 'rxjs/operators';
import { of, onErrorResumeNextWith, throwError } from 'rxjs';
import { IMock, It, Mock, Times } from 'typemoq';
import { DialogService, versioned } from '@app/shared/internal';
import { AppsService, AssetScriptsPayload } from './../services/apps.service';
@ -58,7 +57,7 @@ describe('AssetScriptsState', () => {
appsService.setup(x => x.getAssetScripts(app))
.returns(() => throwError(() => 'Service Error'));
assetScriptsState.load().pipe(onErrorResumeNext()).subscribe();
assetScriptsState.load().pipe(onErrorResumeNextWith()).subscribe();
expect(assetScriptsState.snapshot.isLoading).toBeFalsy();
});

33
frontend/src/app/shared/state/asset-uploader.state.spec.ts

@ -5,10 +5,9 @@
* Copyright (c) Squidex UG (haftungsbeschränkt). All rights reserved.
*/
import { lastValueFrom, NEVER, of, throwError } from 'rxjs';
import { onErrorResumeNext } from 'rxjs/operators';
import { lastValueFrom, NEVER, Observable, of, onErrorResumeNextWith, throwError } from 'rxjs';
import { IMock, Mock } from 'typemoq';
import { AssetsService, AssetsState, AssetUploaderState, DialogService, ofForever } from '@app/shared/internal';
import { AssetsService, AssetUploaderState, DialogService } from '@app/shared/internal';
import { createAsset } from './../services/assets.service.spec';
import { TestValues } from './_test-helpers';
@ -49,22 +48,6 @@ describe('AssetUploaderState', () => {
expect(upload.progress).toBe(1);
});
it('should upload file with folder id from asset state', () => {
const assetsState = Mock.ofType<AssetsState>();
assetsState.setup(x => x.parentId)
.returns(() => 'parent1');
const file: File = <any>{ name: 'my-file' };
assetsService.setup(x => x.postAssetFile(app, file, 'parent1'))
.returns(() => NEVER).verifiable();
assetUploader.uploadFile(file, assetsState.object).subscribe();
expect().nothing();
});
it('should update progress if uploading file makes progress', () => {
const file: File = <any>{ name: 'my-file' };
@ -85,7 +68,7 @@ describe('AssetUploaderState', () => {
assetsService.setup(x => x.postAssetFile(app, file, undefined))
.returns(() => throwError(() => 'Service Error')).verifiable();
assetUploader.uploadFile(file).pipe(onErrorResumeNext()).subscribe();
assetUploader.uploadFile(file).pipe(onErrorResumeNextWith()).subscribe();
const upload = assetUploader.snapshot.uploads[0];
@ -142,7 +125,7 @@ describe('AssetUploaderState', () => {
assetsService.setup(x => x.putAssetFile(app, asset, file, asset.version))
.returns(() => throwError(() => 'Service Error')).verifiable();
assetUploader.uploadAsset(asset, file).pipe(onErrorResumeNext()).subscribe();
assetUploader.uploadAsset(asset, file).pipe(onErrorResumeNextWith()).subscribe();
const upload = assetUploader.snapshot.uploads[0];
@ -167,3 +150,11 @@ describe('AssetUploaderState', () => {
expect(uploadedAsset!).toEqual(updated);
});
});
function ofForever<T>(...values: ReadonlyArray<T>) {
return new Observable<T>(s => {
for (const value of values) {
s.next(value);
}
});
}

32
frontend/src/app/shared/state/asset-uploader.state.ts

@ -6,11 +6,10 @@
*/
import { Injectable } from '@angular/core';
import { map, Observable, shareReplay, Subject, takeUntil } from 'rxjs';
import { Observable, shareReplay, Subject, takeUntil } from 'rxjs';
import { DialogService, MathHelper, State, Types } from '@app/framework';
import { AssetDto, AssetsService } from './../services/assets.service';
import { AppsState } from './apps.state';
import { AssetsState } from './assets.state';
export interface Upload {
// Unique id.
@ -67,18 +66,10 @@ export class AssetUploaderState extends State<Snapshot> {
}, 'Stopped');
}
public uploadFile(file: File, target?: AssetsState, parentId?: string): Observable<AssetDto | number> {
const stream = this.assetsService.postAssetFile(this.appName, file, parentId ?? target?.parentId);
public uploadFile(file: File, parentId?: string): Observable<AssetDto | number> {
const stream = this.assetsService.postAssetFile(this.appName, file, parentId);
return this.upload(stream, MathHelper.guid(), file.name, asset => {
if (asset.isDuplicate) {
this.dialogs.notifyError('i18n:assets.duplicateFile');
} else if (target) {
target.addAsset(asset);
}
return asset;
});
return this.upload(stream, MathHelper.guid(), file.name);
}
public uploadAsset(asset: AssetDto, file: Blob): Observable<AssetDto | number> {
@ -87,26 +78,19 @@ export class AssetUploaderState extends State<Snapshot> {
return this.upload(stream, asset.id, file['name'] || asset.fileName);
}
private upload(source: Observable<number | AssetDto>, id: string, name: string, complete?: ((completion: AssetDto) => AssetDto)) {
private upload(source: Observable<number | AssetDto>, id: string, name: string) {
let upload = { id, name, progress: 1, status: 'Running', cancel: new Subject() };
this.addUpload(upload);
const stream = source.pipe(takeUntil(upload.cancel),
map(event => {
if (Types.isNumber(event)) {
return event;
} else if (complete) {
return complete(event);
} else {
return event;
}
}), shareReplay());
const stream = source.pipe(takeUntil(upload.cancel), shareReplay());
stream.subscribe({
next: event => {
if (Types.isNumber(event)) {
upload = this.update(upload, { progress: event });
} else if (event.isDuplicate) {
this.dialogs.notifyError('i18n:assets.duplicateFile');
}
},
error: () => {

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

@ -8,7 +8,7 @@
import { UntypedFormControl, Validators } from '@angular/forms';
import slugify from 'slugify';
import { ExtendedFormGroup, Form, Mutable, TemplatedFormArray, Types } from '@app/framework';
import { AnnotateAssetDto, AssetDto, AssetFolderDto, RenameAssetFolderDto, RenameAssetTagDto } from './../services/assets.service';
import { AnnotateAssetDto, AssetDto, AssetFolderDto, MoveAssetItemDto, RenameAssetFolderDto, RenameAssetTagDto } from './../services/assets.service';
export class AnnotateAssetForm extends Form<ExtendedFormGroup, AnnotateAssetDto, AssetDto> {
public get metadata() {
@ -219,3 +219,13 @@ export class RenameAssetTagForm extends Form<ExtendedFormGroup, RenameAssetTagDt
}));
}
}
export class MoveAssetForm extends Form<ExtendedFormGroup, AssetDto, MoveAssetItemDto> {
constructor() {
super(new ExtendedFormGroup({
parentId: new UntypedFormControl('',
Validators.required,
),
}));
}
}

97
frontend/src/app/shared/state/assets.state.spec.ts

@ -5,8 +5,7 @@
* Copyright (c) Squidex UG (haftungsbeschränkt). All rights reserved.
*/
import { of, throwError } from 'rxjs';
import { onErrorResumeNext } from 'rxjs/operators';
import { of, onErrorResumeNextWith, throwError } from 'rxjs';
import { IMock, It, Mock, Times } from 'typemoq';
import { ErrorDto } from '@app/framework';
import { AssetsService, AssetsState, DialogService, MathHelper, versioned } from '@app/shared/internal';
@ -205,8 +204,8 @@ describe('AssetsState', () => {
assetsState.load(true).subscribe();
});
it('should add asset to snapshot if created', () => {
const newAsset = createAsset(5, ['new']);
it('should add asset to snapshot', () => {
const newAsset = createAsset(3, ['new']);
assetsState.addAsset(newAsset);
@ -214,8 +213,17 @@ describe('AssetsState', () => {
expect(assetsState.snapshot.total).toBe(201);
});
it('should not add asset to snapshot if it already exist', () => {
const newAsset = createAsset(1, ['new']);
assetsState.addAsset(newAsset);
expect(assetsState.snapshot.assets).toEqual([asset1, asset2]);
expect(assetsState.snapshot.total).toBe(200);
});
it('should truncate assets if page size reached', () => {
const newAsset = createAsset(5, ['new']);
const newAsset = createAsset(3, ['new']);
assetsState.page({ page: 0, pageSize: 2 }).subscribe();
assetsState.addAsset(newAsset);
@ -225,7 +233,7 @@ describe('AssetsState', () => {
});
it('should not add asset to snapshot if parent id is not the same', () => {
const newAsset = createAsset(5, ['new'], '_new', 'other-parent');
const newAsset = createAsset(3, ['new'], '_new', 'other-parent');
assetsState.addAsset(newAsset);
@ -240,6 +248,24 @@ describe('AssetsState', () => {
expect(assetsState.snapshot.tagsAvailable).toEqual({ tag1: 1, tag2: 1, shared: 2, new: 2 });
});
it('should replace asset in snapshot', () => {
const newAsset = createAsset(2, ['new']);
assetsState.replaceAsset(newAsset);
expect(assetsState.snapshot.assets).toEqual([asset1, newAsset]);
expect(assetsState.snapshot.total).toBe(200);
});
it('should not replace asset in snapshot if it does not exist', () => {
const newAsset = createAsset(3, ['new']);
assetsState.replaceAsset(newAsset);
expect(assetsState.snapshot.assets).toEqual([asset1, asset2]);
expect(assetsState.snapshot.total).toBe(200);
});
it('should add asset folder if created', () => {
const request = { folderName: 'New Folder', parentId: MathHelper.EMPTY_GUID };
@ -292,21 +318,37 @@ describe('AssetsState', () => {
});
it('should remove asset from snapshot if moved to other folder', () => {
const updated = createAsset(1, ['new'], '_new');
const request = { parentId: 'newParent' };
assetsService.setup(x => x.putAssetItemParent(app, asset1, It.isValue(request), asset1.version))
.returns(() => of(versioned(newVersion)));
assetsService.setup(x => x.putAssetParent(app, asset1, It.isValue(request), asset1.version))
.returns(() => of(updated));
assetsState.moveAsset(asset1, request.parentId).subscribe();
expect(assetsState.snapshot.assets.length).toBe(1);
expect(assetsState.snapshot.total).toBe(200);
expect(assetsState.snapshot.total).toBe(199);
});
it('should add asset to snapshot if moved to current folder', () => {
const asset3 = createAsset(3, undefined, undefined, 'oldParent');
const request = { parentId: assetsState.snapshot.parentId };
assetsService.setup(x => x.putAssetParent(app, asset3, It.isValue(request), asset3.version))
.returns(() => of(asset3));
assetsState.moveAsset(asset3, request.parentId).subscribe();
expect(assetsState.snapshot.assets).toEqual([asset3, asset1, asset2]);
expect(assetsState.snapshot.total).toBe(201);
});
it('should not do anything if moving asset to current parent', () => {
it('should not do anything if moving asset to same parent', () => {
const request = { parentId: MathHelper.EMPTY_GUID };
assetsState.moveAsset(asset1, request.parentId).pipe(onErrorResumeNext()).subscribe();
assetsState.moveAsset(asset1, request.parentId).pipe(onErrorResumeNextWith()).subscribe();
expect(assetsState.snapshot.assets.length).toBe(2);
expect(assetsState.snapshot.total).toBe(200);
@ -315,30 +357,45 @@ describe('AssetsState', () => {
it('should move asset back to snapshot if moving via api failed', () => {
const request = { parentId: 'newParent' };
assetsService.setup(x => x.putAssetItemParent(app, asset1, It.isValue(request), asset1.version))
assetsService.setup(x => x.putAssetParent(app, asset1, It.isValue(request), asset1.version))
.returns(() => throwError(() => 'Service Error'));
assetsState.moveAsset(asset1, request.parentId).pipe(onErrorResumeNext()).subscribe();
assetsState.moveAsset(asset1, request.parentId).pipe(onErrorResumeNextWith()).subscribe();
expect(assetsState.snapshot.assets.length).toBe(2);
expect(assetsState.snapshot.total).toBe(200);
});
it('should remove asset folder from snapshot if moved to other folder', () => {
const updated = createAssetFolder(1, '_new');
const request = { parentId: 'newParent' };
assetsService.setup(x => x.putAssetItemParent(app, assetFolder1, It.isValue(request), assetFolder1.version))
.returns(() => of(versioned(newVersion)));
assetsService.setup(x => x.putAssetFolderParent(app, assetFolder1, It.isValue(request), assetFolder1.version))
.returns(() => of(updated));
assetsState.moveAssetFolder(assetFolder1, request.parentId).subscribe();
expect(assetsState.snapshot.folders).toEqual([assetFolder2]);
});
it('should add asset folder to snapshot if moved to current folder', () => {
const assetFolder3 = createAssetFolder(3, undefined, 'oldParent');
const request = { parentId: assetsState.snapshot.parentId };
assetsService.setup(x => x.putAssetFolderParent(app, assetFolder3, It.isValue(request), assetFolder3.version))
.returns(() => of(assetFolder3));
assetsState.moveAssetFolder(assetFolder3, request.parentId).subscribe();
expect(assetsState.snapshot.folders).toEqual([assetFolder1, assetFolder2, assetFolder3]);
});
it('should not do anything if moving asset folder to itself', () => {
const request = { parentId: assetFolder1.id };
assetsState.moveAssetFolder(assetFolder1, request.parentId).pipe(onErrorResumeNext()).subscribe();
assetsState.moveAssetFolder(assetFolder1, request.parentId).pipe(onErrorResumeNextWith()).subscribe();
expect(assetsState.snapshot.assets.length).toBe(2);
});
@ -346,7 +403,7 @@ describe('AssetsState', () => {
it('should not do anything if moving asset folder to current parent', () => {
const request = { parentId: MathHelper.EMPTY_GUID };
assetsState.moveAssetFolder(assetFolder1, request.parentId).pipe(onErrorResumeNext()).subscribe();
assetsState.moveAssetFolder(assetFolder1, request.parentId).pipe(onErrorResumeNextWith()).subscribe();
expect(assetsState.snapshot.assets.length).toBe(2);
});
@ -354,10 +411,10 @@ describe('AssetsState', () => {
it('should move asset folder back to snapshot if moving via api failed', () => {
const request = { parentId: 'newParent' };
assetsService.setup(x => x.putAssetItemParent(app, assetFolder1, It.isValue(request), assetFolder1.version))
assetsService.setup(x => x.putAssetFolderParent(app, assetFolder1, It.isValue(request), assetFolder1.version))
.returns(() => throwError(() => 'Service Error'));
assetsState.moveAssetFolder(assetFolder1, request.parentId).pipe(onErrorResumeNext()).subscribe();
assetsState.moveAssetFolder(assetFolder1, request.parentId).pipe(onErrorResumeNextWith()).subscribe();
expect(assetsState.snapshot.assets.length).toBe(2);
});
@ -397,7 +454,7 @@ describe('AssetsState', () => {
dialogs.setup(x => x.confirm(It.isAnyString(), It.isAnyString(), It.isAnyString()))
.returns(() => of(false));
assetsState.deleteAsset(asset1).pipe(onErrorResumeNext()).subscribe();
assetsState.deleteAsset(asset1).pipe(onErrorResumeNextWith()).subscribe();
expect(assetsState.snapshot.assets.length).toBe(2);
});

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

@ -200,17 +200,61 @@ export abstract class AssetsStateBase extends State<Snapshot> {
}
public addAsset(asset: AssetDto) {
if (asset.parentId !== this.snapshot.parentId || this.snapshot.assets.find(x => x.id === asset.id)) {
if (asset.parentId !== this.snapshot.parentId) {
return;
}
const existing = this.snapshot.assets.find(x => x.id === asset.id);
if (existing) {
return;
}
this.next(s => {
const assets = [asset, ...s.assets].slice(0, s.pageSize);
const tags = updateTags(s, asset);
const tags = updateTags(s, asset, existing);
return { ...s, assets, total: s.total + 1, ...tags };
}, 'Asset Created');
}, 'Asset Added');
}
public replaceAsset(asset: AssetDto) {
if (asset.parentId !== this.snapshot.parentId) {
return;
}
const existing = this.snapshot.assets.find(x => x.id === asset.id);
if (!existing) {
return;
}
this.next(s => {
const assets = s.assets.replacedBy('id', asset);
const tags = updateTags(s, asset, existing);
return { ...s, assets, ...tags };
}, 'Asset Replaced');
}
public addFolder(folder: AssetFolderDto) {
if (folder.parentId !== this.snapshot.parentId) {
return;
}
const existing = this.snapshot.folders.find(x => x.id === folder.id);
if (existing) {
return;
}
this.next(s => {
const folders = [folder, ...s.folders].sortByString(x => x.folderName);
return { ...s, folders };
}, 'Asset Folder Added');
}
public createAssetFolder(folderName: string) {
@ -260,19 +304,29 @@ export abstract class AssetsStateBase extends State<Snapshot> {
return EMPTY;
}
this.next(s => {
const assets = s.assets.filter(x => x.id !== asset.id);
return { ...s, assets };
}, 'Asset Moving Started');
const moveIn = parentId === this.snapshot.parentId;
return this.assetsService.putAssetItemParent(this.appName, asset, { parentId }, asset.version).pipe(
catchError(error => {
this.next(s => {
function moveOutOrIn(state: AssetsState, asset: AssetDto, moveIn: boolean, name: string) {
if (moveIn) {
state.next(s => {
const assets = [asset, ...s.assets];
return { ...s, assets };
}, 'Asset Moving Failed');
return { ...s, assets, total: s.total + 1 };
}, name);
} else {
state.next(s => {
const assets = s.assets.filter(x => x.id !== asset.id);
return { ...s, assets, total: s.total - 1 };
}, name);
}
}
moveOutOrIn(this, asset, moveIn, 'Asset Moving');
return this.assetsService.putAssetParent(this.appName, asset, { parentId }, asset.version).pipe(
catchError(error => {
moveOutOrIn(this, asset, !moveIn, 'Asset Moving reverted.');
return throwError(() => error);
}),
@ -284,19 +338,29 @@ export abstract class AssetsStateBase extends State<Snapshot> {
return EMPTY;
}
this.next(s => {
const folders = s.folders.filter(x => x.id !== folder.id);
const moveIn = parentId === this.snapshot.parentId;
return { ...s, folders };
}, 'Folder Moving Started');
function moveOutOrIn(state: AssetsState, folder: AssetFolderDto, moveIn: boolean, name: string) {
if (moveIn) {
state.next(s => {
const folders = [folder, ...s.folders].sortByString(x => x.folderName);
return this.assetsService.putAssetItemParent(this.appName, folder, { parentId }, folder.version).pipe(
catchError(error => {
this.next(s => {
const folders = [...s.folders, folder].sortByString(x => x.folderName);
return { ...s, folders };
}, name);
} else {
state.next(s => {
const folders = s.folders.filter(x => x.id !== folder.id);
return { ...s, folders };
}, 'Folder Moving Done');
}, name);
}
}
moveOutOrIn(this, folder, moveIn, 'Asset Folder Moving');
return this.assetsService.putAssetFolderParent(this.appName, folder, { parentId }, folder.version).pipe(
catchError(error => {
moveOutOrIn(this, folder, !moveIn, 'Asset Folder Moving reverted.');
return throwError(() => error);
}),

9
frontend/src/app/shared/state/backups.state.spec.ts

@ -5,8 +5,7 @@
* Copyright (c) Squidex UG (haftungsbeschränkt). All rights reserved.
*/
import { of, throwError } from 'rxjs';
import { onErrorResumeNext } from 'rxjs/operators';
import { of, onErrorResumeNextWith, throwError } from 'rxjs';
import { IMock, It, Mock, Times } from 'typemoq';
import { BackupsService, BackupsState, DialogService } from '@app/shared/internal';
import { createBackup } from './../services/backups.service.spec';
@ -54,7 +53,7 @@ describe('BackupsState', () => {
backupsService.setup(x => x.getBackups(app))
.returns(() => throwError(() => 'Service Error'));
backupsState.load().pipe(onErrorResumeNext()).subscribe();
backupsState.load().pipe(onErrorResumeNextWith()).subscribe();
expect(backupsState.snapshot.isLoading).toBeFalsy();
});
@ -74,7 +73,7 @@ describe('BackupsState', () => {
backupsService.setup(x => x.getBackups(app))
.returns(() => throwError(() => 'Service Error'));
backupsState.load(true, false).pipe(onErrorResumeNext()).subscribe();
backupsState.load(true, false).pipe(onErrorResumeNextWith()).subscribe();
expect().nothing();
@ -85,7 +84,7 @@ describe('BackupsState', () => {
backupsService.setup(x => x.getBackups(app))
.returns(() => throwError(() => 'Service Error'));
backupsState.load(true, true).pipe(onErrorResumeNext()).subscribe();
backupsState.load(true, true).pipe(onErrorResumeNextWith()).subscribe();
expect().nothing();

5
frontend/src/app/shared/state/clients.state.spec.ts

@ -5,8 +5,7 @@
* Copyright (c) Squidex UG (haftungsbeschränkt). All rights reserved.
*/
import { of, throwError } from 'rxjs';
import { onErrorResumeNext } from 'rxjs/operators';
import { of, onErrorResumeNextWith, throwError } from 'rxjs';
import { IMock, It, Mock, Times } from 'typemoq';
import { ClientsPayload, ClientsService, ClientsState, DialogService, versioned } from '@app/shared/internal';
import { createClients } from './../services/clients.service.spec';
@ -56,7 +55,7 @@ describe('ClientsState', () => {
clientsService.setup(x => x.getClients(app))
.returns(() => throwError(() => 'Service Error'));
clientsState.load().pipe(onErrorResumeNext()).subscribe();
clientsState.load().pipe(onErrorResumeNextWith()).subscribe();
expect(clientsState.snapshot.isLoading).toBeFalsy();
});

5
frontend/src/app/shared/state/contributors.state.spec.ts

@ -5,8 +5,7 @@
* Copyright (c) Squidex UG (haftungsbeschränkt). All rights reserved.
*/
import { EMPTY, of, throwError } from 'rxjs';
import { catchError, onErrorResumeNext } from 'rxjs/operators';
import { catchError, EMPTY, of, onErrorResumeNextWith, throwError } from 'rxjs';
import { IMock, It, Mock, Times } from 'typemoq';
import { ErrorDto } from '@app/framework';
import { ContributorDto, ContributorsPayload, ContributorsService, ContributorsState, DialogService, versioned } from '@app/shared/internal';
@ -65,7 +64,7 @@ describe('ContributorsState', () => {
contributorsService.setup(x => x.getContributors(app))
.returns(() => throwError(() => 'Service Error'));
contributorsState.load().pipe(onErrorResumeNext()).subscribe();
contributorsState.load().pipe(onErrorResumeNextWith()).subscribe();
expect(contributorsState.snapshot.isLoading).toBeFalsy();
});

5
frontend/src/app/shared/state/languages.state.spec.ts

@ -5,8 +5,7 @@
* Copyright (c) Squidex UG (haftungsbeschränkt). All rights reserved.
*/
import { of, throwError } from 'rxjs';
import { onErrorResumeNext } from 'rxjs/operators';
import { of, onErrorResumeNextWith, throwError } from 'rxjs';
import { IMock, It, Mock, Times } from 'typemoq';
import { AppLanguagesPayload, AppLanguagesService, DialogService, LanguageDto, LanguagesService, LanguagesState, versioned } from '@app/shared/internal';
import { createLanguages } from './../services/app-languages.service.spec';
@ -79,7 +78,7 @@ describe('LanguagesState', () => {
languagesService.setup(x => x.getLanguages(app))
.returns(() => throwError(() => 'Service Error'));
languagesState.load().pipe(onErrorResumeNext()).subscribe();
languagesState.load().pipe(onErrorResumeNextWith()).subscribe();
expect(languagesState.snapshot.isLoading).toBeFalsy();
});

9
frontend/src/app/shared/state/plans.state.spec.ts

@ -5,8 +5,7 @@
* Copyright (c) Squidex UG (haftungsbeschränkt). All rights reserved.
*/
import { of, throwError } from 'rxjs';
import { onErrorResumeNext } from 'rxjs/operators';
import { of, onErrorResumeNextWith, throwError } from 'rxjs';
import { IMock, It, Mock, Times } from 'typemoq';
import { DialogService, PlanDto, PlanLockedReason, PlansService, PlansState, versioned } from '@app/shared/internal';
import { TestValues } from './_test-helpers';
@ -83,7 +82,7 @@ describe('PlansState', () => {
plansService.setup(x => x.getPlans(app))
.returns(() => throwError(() => 'Service Error'));
plansState.load().pipe(onErrorResumeNext()).subscribe();
plansState.load().pipe(onErrorResumeNextWith()).subscribe();
expect(plansState.snapshot.isLoading).toBeFalsy();
});
@ -118,7 +117,7 @@ describe('PlansState', () => {
plansService.setup(x => x.putPlan(app, It.isAny(), version))
.returns(() => of(versioned(newVersion, result)));
plansState.change('free').pipe(onErrorResumeNext()).subscribe();
plansState.change('free').pipe(onErrorResumeNextWith()).subscribe();
expect(plansState.snapshot.plans).toEqual([
{ isSelected: true, isYearlySelected: false, plan: oldPlans.plans[0] },
@ -132,7 +131,7 @@ describe('PlansState', () => {
plansService.setup(x => x.putPlan(app, It.isAny(), version))
.returns(() => of(versioned(newVersion, { redirectUri: '' })));
plansState.change('id2_yearly').pipe(onErrorResumeNext()).subscribe();
plansState.change('id2_yearly').pipe(onErrorResumeNextWith()).subscribe();
expect(plansState.snapshot.plans).toEqual([
{ isSelected: false, isYearlySelected: false, plan: oldPlans.plans[0] },

5
frontend/src/app/shared/state/roles.state.spec.ts

@ -5,8 +5,7 @@
* Copyright (c) Squidex UG (haftungsbeschränkt). All rights reserved.
*/
import { of, throwError } from 'rxjs';
import { onErrorResumeNext } from 'rxjs/operators';
import { of, onErrorResumeNextWith, throwError } from 'rxjs';
import { IMock, It, Mock, Times } from 'typemoq';
import { DialogService, RolesPayload, RolesService, RolesState, versioned } from '@app/shared/internal';
import { createRoles } from './../services/roles.service.spec';
@ -52,7 +51,7 @@ describe('RolesState', () => {
rolesService.setup(x => x.getRoles(app))
.returns(() => throwError(() => 'Service Error'));
rolesState.load().pipe(onErrorResumeNext()).subscribe();
rolesState.load().pipe(onErrorResumeNextWith()).subscribe();
expect(rolesState.snapshot.isLoading).toBeFalsy();
});

5
frontend/src/app/shared/state/rule-events.state.spec.ts

@ -5,8 +5,7 @@
* Copyright (c) Squidex UG (haftungsbeschränkt). All rights reserved.
*/
import { of, throwError } from 'rxjs';
import { onErrorResumeNext } from 'rxjs/operators';
import { of, onErrorResumeNextWith, throwError } from 'rxjs';
import { IMock, It, Mock, Times } from 'typemoq';
import { DialogService, RuleEventsState, RulesService } from '@app/shared/internal';
import { createRuleEvent } from './../services/rules.service.spec';
@ -51,7 +50,7 @@ describe('RuleEventsState', () => {
rulesService.setup(x => x.getEvents(app, 30, 0, undefined))
.returns(() => throwError(() => 'Service Error'));
ruleEventsState.load().pipe(onErrorResumeNext()).subscribe();
ruleEventsState.load().pipe(onErrorResumeNextWith()).subscribe();
expect(ruleEventsState.snapshot.isLoading).toBeFalsy();
});

5
frontend/src/app/shared/state/rule-simulator.state.spec.ts

@ -5,8 +5,7 @@
* Copyright (c) Squidex UG (haftungsbeschränkt). All rights reserved.
*/
import { of, throwError } from 'rxjs';
import { onErrorResumeNext } from 'rxjs/operators';
import { of, onErrorResumeNextWith, throwError } from 'rxjs';
import { IMock, It, Mock, Times } from 'typemoq';
import { DialogService, RulesService } from '@app/shared/internal';
import { createSimulatedRuleEvent } from './../services/rules.service.spec';
@ -71,7 +70,7 @@ describe('RuleSimulatorState', () => {
.returns(() => throwError(() => 'Service Error'));
ruleSimulatorState.selectRule('12');
ruleSimulatorState.load().pipe(onErrorResumeNext()).subscribe();
ruleSimulatorState.load().pipe(onErrorResumeNextWith()).subscribe();
expect(ruleSimulatorState.snapshot.isLoading).toBeFalsy();
});

5
frontend/src/app/shared/state/rules.state.spec.ts

@ -5,8 +5,7 @@
* Copyright (c) Squidex UG (haftungsbeschränkt). All rights reserved.
*/
import { firstValueFrom, of, throwError } from 'rxjs';
import { onErrorResumeNext } from 'rxjs/operators';
import { firstValueFrom, of, onErrorResumeNextWith, throwError } from 'rxjs';
import { IMock, It, Mock, Times } from 'typemoq';
import { DialogService, RulesService, versioned } from '@app/shared/internal';
import { RuleDto } from './../services/rules.service';
@ -68,7 +67,7 @@ describe('RulesState', () => {
rulesService.setup(x => x.getRules(app))
.returns(() => throwError(() => 'Service Error'));
rulesState.load().pipe(onErrorResumeNext()).subscribe();
rulesState.load().pipe(onErrorResumeNextWith()).subscribe();
expect(rulesState.snapshot.isLoading).toBeFalsy();
});

5
frontend/src/app/shared/state/schemas.state.spec.ts

@ -5,8 +5,7 @@
* Copyright (c) Squidex UG (haftungsbeschränkt). All rights reserved.
*/
import { firstValueFrom, of, throwError } from 'rxjs';
import { onErrorResumeNext } from 'rxjs/operators';
import { firstValueFrom, of, onErrorResumeNextWith, throwError } from 'rxjs';
import { IMock, It, Mock, Times } from 'typemoq';
import { DialogService, SchemaDto, SchemasService, UpdateSchemaCategoryDto, versioned } from '@app/shared/internal';
import { createSchema } from './../services/schemas.service.spec';
@ -79,7 +78,7 @@ describe('SchemasState', () => {
schemasService.setup(x => x.getSchemas(app))
.returns(() => throwError(() => 'Service Error'));
schemasState.load().pipe(onErrorResumeNext()).subscribe();
schemasState.load().pipe(onErrorResumeNextWith()).subscribe();
expect(schemasState.snapshot.isLoading).toBeFalsy();
});

5
frontend/src/app/shared/state/template.state.spec.ts

@ -5,8 +5,7 @@
* Copyright (c) Squidex UG (haftungsbeschränkt). All rights reserved.
*/
import { of, throwError } from 'rxjs';
import { onErrorResumeNext } from 'rxjs/operators';
import { of, onErrorResumeNextWith, throwError } from 'rxjs';
import { IMock, It, Mock, Times } from 'typemoq';
import { DialogService, TemplatesService, TemplatesState } from '@app/shared/internal';
import { createTemplate } from './../services/templates.service.spec';
@ -48,7 +47,7 @@ describe('TemplatesState', () => {
templatesService.setup(x => x.getTemplates())
.returns(() => throwError(() => 'Service Error'));
templatesState.load().pipe(onErrorResumeNext()).subscribe();
templatesState.load().pipe(onErrorResumeNextWith()).subscribe();
expect(templatesState.snapshot.isLoading).toBeFalsy();
});

5
frontend/src/app/shared/state/workflows.state.spec.ts

@ -5,8 +5,7 @@
* Copyright (c) Squidex UG (haftungsbeschränkt). All rights reserved.
*/
import { of, throwError } from 'rxjs';
import { onErrorResumeNext } from 'rxjs/operators';
import { of, onErrorResumeNextWith, throwError } from 'rxjs';
import { IMock, It, Mock, Times } from 'typemoq';
import { DialogService, versioned, WorkflowsPayload, WorkflowsService, WorkflowsState } from '@app/shared/internal';
import { createWorkflows } from './../services/workflows.service.spec';
@ -56,7 +55,7 @@ describe('WorkflowsState', () => {
workflowsService.setup(x => x.getWorkflows(app))
.returns(() => throwError(() => 'Service Error'));
workflowsState.load().pipe(onErrorResumeNext()).subscribe();
workflowsState.load().pipe(onErrorResumeNextWith()).subscribe();
expect(workflowsState.snapshot.isLoading).toBeFalsy();
});

6
frontend/src/app/shell/pages/internal/notification-dropdown.component.ts

@ -6,8 +6,8 @@
*/
import { ChangeDetectionStrategy, ChangeDetectorRef, Component, OnInit } from '@angular/core';
import { timer } from 'rxjs';
import { onErrorResumeNext, switchMap, tap } from 'rxjs/operators';
import { onErrorResumeNextWith, timer } from 'rxjs';
import { switchMap, tap } from 'rxjs/operators';
import { AuthService, CommentDto, CommentsService, CommentsState, DialogService, LocalStoreService, ModalModel, ResourceOwner, Settings } from '@app/shared';
@Component({
@ -69,7 +69,7 @@ export class NotificationDropdownComponent extends ResourceOwner implements OnIn
this.changeDetector.detectChanges();
})));
this.own(timer(0, 4000).pipe(switchMap(() => this.commentsState.load(true).pipe(onErrorResumeNext()))));
this.own(timer(0, 4000).pipe(switchMap(() => this.commentsState.load(true).pipe(onErrorResumeNextWith()))));
}
public trackByComment(_index: number, comment: CommentDto) {

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

@ -4,13 +4,12 @@
[dropdownStyles]="{ width: '30rem' }"
[dropdownPosition]="'bottom-right'"
icon="search"
inputName="searchMenu"
(ngModelChange)="selectResult($event)"
[ngModel]="searchResult"
placeholder="{{ 'search.quickNavPlaceholder' | sqxTranslate }}"
shortcut="q"
shortcutAction="none"
[source]="searchSource">
[itemsSource]="searchSource">
<ng-template let-result="$implicit">
<div class="row g-0">
<div class="col-auto pe-4">

Loading…
Cancel
Save