From 4c02d6cbccfa5139c3daf0985a6ba1d318618d12 Mon Sep 17 00:00:00 2001 From: Sebastian Stehle Date: Mon, 25 Nov 2019 16:44:34 +0100 Subject: [PATCH] Feature/asset folders (#451) Asset folders. --- .../Assets/MongoAssetEntity.cs | 13 +- .../Assets/MongoAssetFolderEntity.cs | 65 +++ .../Assets/MongoAssetFolderRepository.cs | 74 +++ ...ongoAssetFolderRepository_SnapshotStore.cs | 75 +++ .../Assets/MongoAssetRepository.cs | 27 +- .../Assets/Visitors/FindExtensions.cs | 18 +- .../Assets/AssetChangedTriggerHandler.cs | 5 + .../Assets/AssetEntity.cs | 11 +- .../Assets/AssetFolderGrain.cs | 130 +++++ .../Assets/AssetGrain.cs | 24 +- .../Assets/BackupAssets.cs | 5 + .../Assets/Commands/AssetFolderCommand.cs | 22 + .../Assets/Commands/CreateAsset.cs | 2 + .../Assets/Commands/CreateAssetFolder.cs | 26 + .../Assets/Commands/DeleteAssetFolder.cs | 13 + .../Assets/Commands/MoveAsset.cs | 16 + .../Assets/Commands/MoveAssetFolder.cs | 16 + .../Assets/Commands/RenameAssetFolder.cs | 14 + .../Assets/Guards/GuardAsset.cs | 39 +- .../Assets/Guards/GuardAssetFolder.cs | 95 +++ .../Assets/IAssetEntity.cs | 2 + .../Assets/IAssetFolderEntity.cs | 21 + .../Assets/IAssetFolderGrain.cs | 15 + .../Assets/IAssetQueryService.cs | 6 +- .../Assets/Queries/AssetQueryService.cs | 42 +- .../Repositories/IAssetFolderRepository.cs | 20 + .../Assets/Repositories/IAssetRepository.cs | 6 +- .../Assets/State/AssetFolderState.cs | 72 +++ .../Assets/State/AssetState.cs | 13 +- .../Contents/GraphQL/Types/AssetGraphType.cs | 2 +- .../Contents/Queries/ContentEnricher.cs | 2 +- .../Contents/Queries/QueryExecutionContext.cs | 4 +- .../Squidex.Domain.Apps.Entities/IEntity.cs | 2 +- .../Assets/AssetCreated.cs | 3 + .../Assets/AssetFolderCreated.cs | 20 + .../Assets/AssetFolderDeleted.cs | 16 + .../Assets/AssetFolderEvent.cs | 16 + .../Assets/AssetFolderMoved.cs | 18 + .../Assets/AssetFolderRenamed.cs | 17 + .../Assets/AssetMoved.cs | 18 + .../CollectionExtensions.cs | 17 + .../Api/Controllers/Apps/AppsController.cs | 2 +- .../Assets/AssetFoldersController.cs | 168 ++++++ .../Controllers/Assets/AssetsController.cs | 35 +- .../Api/Controllers/Assets/Models/AssetDto.cs | 7 + .../Assets/Models/AssetFolderDto.cs | 69 +++ .../Assets/Models/AssetFoldersDto.cs | 55 ++ .../Assets/Models/CreateAssetFolderDto.cs | 33 ++ .../Assets/Models/MoveAssetItemDto.cs | 30 + .../Assets/Models/RenameAssetFolderDto.cs | 28 + .../Areas/IdentityServer/Views/_Layout.cshtml | 2 +- .../Squidex/Config/Domain/CommandsServices.cs | 4 + .../Squidex/Config/Domain/StoreServices.cs | 4 + .../src/Squidex/wwwroot/images/add-app.png | Bin 5096 -> 0 bytes .../src/Squidex/wwwroot/images/add-app.svg | 59 ++ .../src/Squidex/wwwroot/images/add-blog.png | Bin 5117 -> 0 bytes .../src/Squidex/wwwroot/images/add-blog.svg | 38 ++ .../Squidex/wwwroot/images/add-identity.png | Bin 5128 -> 0 bytes .../Squidex/wwwroot/images/add-identity.svg | 27 + .../Squidex/wwwroot/images/add-profile.png | Bin 4929 -> 0 bytes .../Squidex/wwwroot/images/add-profile.svg | 26 + .../src/Squidex/wwwroot/images/asset_doc.png | Bin 911 -> 0 bytes .../src/Squidex/wwwroot/images/asset_doc.svg | 7 + .../src/Squidex/wwwroot/images/asset_docx.png | Bin 958 -> 0 bytes .../src/Squidex/wwwroot/images/asset_docx.svg | 7 + .../Squidex/wwwroot/images/asset_generic.png | Bin 514 -> 0 bytes .../Squidex/wwwroot/images/asset_generic.svg | 7 + .../src/Squidex/wwwroot/images/asset_pdf.png | Bin 1156 -> 0 bytes .../src/Squidex/wwwroot/images/asset_pdf.svg | 5 + .../src/Squidex/wwwroot/images/asset_ppt.png | Bin 1034 -> 0 bytes .../src/Squidex/wwwroot/images/asset_ppt.svg | 6 + .../src/Squidex/wwwroot/images/asset_pptx.png | Bin 1058 -> 0 bytes .../src/Squidex/wwwroot/images/asset_pptx.svg | 6 + .../Squidex/wwwroot/images/asset_video.png | Bin 723 -> 0 bytes .../Squidex/wwwroot/images/asset_video.svg | 5 + .../src/Squidex/wwwroot/images/asset_xls.png | Bin 862 -> 0 bytes .../src/Squidex/wwwroot/images/asset_xls.svg | 10 + .../src/Squidex/wwwroot/images/asset_xlsx.png | Bin 905 -> 0 bytes .../src/Squidex/wwwroot/images/asset_xlsx.svg | 10 + .../Squidex/wwwroot/images/dashboard-api.png | Bin 2335 -> 0 bytes .../Squidex/wwwroot/images/dashboard-api.svg | 12 + .../wwwroot/images/dashboard-feedback.png | Bin 4188 -> 0 bytes .../wwwroot/images/dashboard-github.png | Bin 6577 -> 0 bytes .../wwwroot/images/dashboard-github.svg | 6 + .../wwwroot/images/dashboard-schema.png | Bin 3268 -> 0 bytes .../wwwroot/images/dashboard-schema.svg | 10 + .../wwwroot/images/dashboard_feedback.svg | 16 + .../wwwroot/images/dashboard_schema.svg | 10 + backend/src/Squidex/wwwroot/images/folder.svg | 5 + backend/src/Squidex/wwwroot/images/logo.svg | 8 + .../Assets/AssetChangedTriggerHandlerTests.cs | 10 + .../Assets/AssetCommandMiddlewareTests.cs | 2 +- .../Assets/AssetFolderGrainTests.cs | 171 ++++++ .../Assets/AssetGrainTests.cs | 53 +- .../Assets/Guards/GuardAssetFolderTests.cs | 167 ++++++ .../Assets/Guards/GuardAssetTests.cs | 94 ++- .../Assets/Queries/AssetQueryServiceTests.cs | 130 ++++- .../Contents/GraphQL/GraphQLQueriesTests.cs | 14 +- .../Queries/ContentEnricherAssetsTests.cs | 12 +- .../Rules/RuleGrainTests.cs | 25 +- .../CollectionExtensionsTests.cs | 28 + frontend/app/app.module.ts | 2 +- .../event-consumers-page.component.html | 46 +- .../event-consumers-page.component.ts | 4 +- .../pages/users/users-page.component.html | 60 +- .../state/event-consumers.state.spec.ts | 10 + .../state/event-consumers.state.ts | 27 +- .../administration/state/users.state.spec.ts | 13 +- .../administration/state/users.state.ts | 26 +- .../apps/pages/apps-page.component.html | 8 +- .../apps/pages/apps-page.component.scss | 6 + .../assets/pages/assets-page.component.html | 31 +- .../assets/pages/assets-page.component.scss | 9 + .../assets/pages/assets-page.component.ts | 3 + .../contents/contents-page.component.html | 130 +++-- .../contents/contents-page.component.scss | 6 - .../shared/assets-editor.component.html | 4 +- .../shared/content-list-cell.directive.ts | 2 +- .../shared/contents-selector.component.html | 84 +-- .../pages/dashboard-page.component.html | 8 +- .../pages/dashboard-page.component.scss | 6 + .../events/rule-events-page.component.html | 152 ++--- .../pages/rules/rules-page.component.html | 68 +-- .../rules/pages/rules/rules-page.component.ts | 2 +- .../schema/schema-edit-form.component.ts | 6 +- .../pages/schema/schema-page.component.html | 2 +- .../pages/backups/backups-page.component.html | 42 +- .../pages/backups/backups-page.component.ts | 6 +- .../pages/clients/clients-page.component.html | 32 +- .../contributors-page.component.html | 36 +- .../contributors-page.component.scss | 6 +- .../languages/languages-page.component.html | 28 +- .../patterns/patterns-page.component.html | 26 +- .../pages/plans/plans-page.component.html | 44 +- .../pages/roles/roles-page.component.html | 16 +- .../workflows/workflows-page.component.html | 50 +- .../angular/ignore-scrollbar.directive.ts | 58 -- .../angular/list-view.component.html | 33 ++ .../angular/list-view.component.scss | 95 +++ .../framework/angular/list-view.component.ts | 85 +++ .../modals/modal-dialog.component.html | 4 +- .../modals/modal-dialog.component.scss | 7 + .../angular/modals/modal-dialog.component.ts | 23 +- .../framework/angular/pager.component.html | 20 +- .../framework/angular/pager.component.scss | 9 +- .../framework/angular/panel.component.html | 8 +- .../framework/angular/panel.component.scss | 6 +- .../app/framework/angular/panel.component.ts | 6 +- .../framework/angular/sync-width.directive.ts | 44 ++ frontend/app/framework/declarations.ts | 3 +- frontend/app/framework/module.ts | 9 +- frontend/app/framework/state.ts | 22 +- .../shared/components/app-form.component.html | 2 +- .../components/asset-dialog.component.html | 4 +- .../components/asset-dialog.component.ts | 37 +- .../asset-folder-form.component.html | 43 ++ .../asset-folder-form.component.scss | 2 + .../components/asset-folder-form.component.ts | 69 +++ .../components/asset-folder.component.html | 46 ++ .../components/asset-folder.component.scss | 36 ++ .../components/asset-folder.component.ts | 63 ++ .../shared/components/asset-path.component.ts | 45 ++ .../shared/components/asset.component.html | 11 +- .../shared/components/asset.component.scss | 37 +- .../app/shared/components/asset.component.ts | 45 +- .../components/assets-list.component.html | 96 +++- .../components/assets-list.component.scss | 46 +- .../components/assets-list.component.ts | 39 +- .../components/assets-selector.component.html | 1 + .../shared/components/comments.component.html | 2 +- .../shared/components/comments.component.ts | 6 +- frontend/app/shared/components/pipes.ts | 4 +- .../components/search-form.component.ts | 8 +- frontend/app/shared/declarations.ts | 11 +- frontend/app/shared/module.ts | 31 +- .../shared/services/assets.service.spec.ts | 153 ++++- .../app/shared/services/assets.service.ts | 146 ++++- .../shared/state/asset-uploader.state.spec.ts | 25 +- .../app/shared/state/asset-uploader.state.ts | 6 +- frontend/app/shared/state/assets.forms.ts | 12 + .../app/shared/state/assets.state.spec.ts | 284 +++++++-- frontend/app/shared/state/assets.state.ts | 374 +++++++++--- .../app/shared/state/backups.state.spec.ts | 10 + frontend/app/shared/state/backups.state.ts | 28 +- .../app/shared/state/clients.state.spec.ts | 15 +- frontend/app/shared/state/clients.state.ts | 31 +- frontend/app/shared/state/contents.state.ts | 42 +- .../shared/state/contributors.state.spec.ts | 56 +- .../app/shared/state/contributors.state.ts | 24 +- .../app/shared/state/languages.state.spec.ts | 15 +- frontend/app/shared/state/languages.state.ts | 24 +- .../app/shared/state/patterns.state.spec.ts | 14 +- frontend/app/shared/state/patterns.state.ts | 24 +- frontend/app/shared/state/plans.state.spec.ts | 16 +- frontend/app/shared/state/plans.state.ts | 39 +- frontend/app/shared/state/roles.state.spec.ts | 15 +- frontend/app/shared/state/roles.state.ts | 27 +- .../shared/state/rule-events.state.spec.ts | 27 +- .../app/shared/state/rule-events.state.ts | 27 +- frontend/app/shared/state/rules.state.spec.ts | 15 +- frontend/app/shared/state/rules.state.ts | 30 +- .../app/shared/state/schemas.state.spec.ts | 13 +- frontend/app/shared/state/schemas.state.ts | 34 +- .../app/shared/state/workflows.state.spec.ts | 15 +- frontend/app/shared/state/workflows.state.ts | 26 +- .../shell/pages/home/home-page.component.html | 2 +- .../shell/pages/home/home-page.component.scss | 8 +- frontend/app/theme/_bootstrap.scss | 1 - frontend/app/theme/_lists.scss | 5 +- frontend/app/theme/_panels.scss | 60 +- frontend/app/theme/_static.scss | 1 + frontend/app/theme/_vars.scss | 3 + frontend/app/theme/icomoon/demo.html | 30 +- frontend/app/theme/icomoon/fonts/icomoon.eot | Bin 30456 -> 30648 bytes frontend/app/theme/icomoon/fonts/icomoon.svg | 2 + frontend/app/theme/icomoon/fonts/icomoon.ttf | Bin 30292 -> 30484 bytes frontend/app/theme/icomoon/fonts/icomoon.woff | Bin 30368 -> 30560 bytes frontend/app/theme/icomoon/icons/activity.svg | 57 +- frontend/app/theme/icomoon/icons/add-app.svg | 144 ----- frontend/app/theme/icomoon/icons/add.svg | 53 +- .../app/theme/icomoon/icons/angle-down.svg | 5 + .../app/theme/icomoon/icons/angle-left.svg | 5 + .../app/theme/icomoon/icons/angle-right.svg | 5 + frontend/app/theme/icomoon/icons/angle-up.svg | 5 + frontend/app/theme/icomoon/icons/api.svg | 96 +--- .../app/theme/icomoon/icons/arrow-right.svg | 5 + .../app/theme/icomoon/icons/arrow_back.svg | 5 + frontend/app/theme/icomoon/icons/assets.svg | 82 +-- frontend/app/theme/icomoon/icons/backup.svg | 5 + frontend/app/theme/icomoon/icons/bin2.svg | 6 + frontend/app/theme/icomoon/icons/browser.svg | 5 + frontend/app/theme/icomoon/icons/bug.svg | 5 + .../app/theme/icomoon/icons/caret-bottom.svg | 68 +-- .../app/theme/icomoon/icons/caret-down.svg | 5 + .../app/theme/icomoon/icons/caret-left.svg | 5 + .../app/theme/icomoon/icons/caret-right.svg | 5 + .../app/theme/icomoon/icons/caret-top.svg | 68 +-- frontend/app/theme/icomoon/icons/caret-up.svg | 5 + .../icomoon/icons/check-circle-filled.svg | 56 +- .../app/theme/icomoon/icons/check-circle.svg | 53 +- .../app/theme/icomoon/icons/checkmark.svg | 5 + frontend/app/theme/icomoon/icons/circle.svg | 5 + frontend/app/theme/icomoon/icons/clock.svg | 5 + frontend/app/theme/icomoon/icons/clone.svg | 5 + frontend/app/theme/icomoon/icons/close.svg | 56 +- frontend/app/theme/icomoon/icons/comments.svg | 5 + frontend/app/theme/icomoon/icons/contents.svg | 89 +-- .../theme/icomoon/icons/control-Checkbox.svg | 58 +- .../icomoon/icons/control-Checkboxes.svg | 77 +-- .../app/theme/icomoon/icons/control-Color.svg | 5 + .../app/theme/icomoon/icons/control-Date.svg | 58 +- .../theme/icomoon/icons/control-DateTime.svg | 60 +- .../theme/icomoon/icons/control-Dropdown.svg | 53 +- .../app/theme/icomoon/icons/control-Html.svg | 75 +-- .../app/theme/icomoon/icons/control-Input.svg | 52 +- .../theme/icomoon/icons/control-Markdown.svg | 56 +- .../app/theme/icomoon/icons/control-Radio.svg | 54 +- .../theme/icomoon/icons/control-RichText.svg | 56 +- .../app/theme/icomoon/icons/control-Slug.svg | 22 +- .../app/theme/icomoon/icons/control-Stars.svg | 5 + .../app/theme/icomoon/icons/control-Tags.svg | 82 +-- .../theme/icomoon/icons/control-TextArea.svg | 53 +- .../theme/icomoon/icons/control-Toggle.svg | 53 +- frontend/app/theme/icomoon/icons/copy.svg | 53 +- .../theme/icomoon/icons/corner-down-right.svg | 5 + .../theme/icomoon/icons/create_new_folder.svg | 5 + .../app/theme/icomoon/icons/dashboard-api.svg | 116 ---- .../icomoon/icons/dashboard-feedback.svg | 130 ----- .../theme/icomoon/icons/dashboard-github.svg | 109 ---- .../theme/icomoon/icons/dashboard-schema.svg | 121 ---- .../app/theme/icomoon/icons/dashboard.svg | 82 +-- .../app/theme/icomoon/icons/delete-filled.svg | 54 +- frontend/app/theme/icomoon/icons/delete.svg | 56 +- .../theme/icomoon/icons/document-delete.svg | 53 +- .../theme/icomoon/icons/document-disable.svg | 53 +- .../app/theme/icomoon/icons/document-lock.svg | 58 +- .../theme/icomoon/icons/document-publish.svg | 53 +- .../icomoon/icons/document-unpublish.svg | 62 +- frontend/app/theme/icomoon/icons/download.svg | 5 + frontend/app/theme/icomoon/icons/drag.svg | 60 +- frontend/app/theme/icomoon/icons/drag2.svg | 5 + frontend/app/theme/icomoon/icons/earth.svg | 5 + frontend/app/theme/icomoon/icons/elapsed.svg | 5 + .../app/theme/icomoon/icons/exclamation.svg | 5 + .../app/theme/icomoon/icons/external-link.svg | 6 + .../app/theme/icomoon/icons/filter-filled.svg | 5 + frontend/app/theme/icomoon/icons/filter.svg | 54 +- frontend/app/theme/icomoon/icons/folder.svg | 5 + frontend/app/theme/icomoon/icons/github.svg | 5 + frontend/app/theme/icomoon/icons/google.svg | 5 + frontend/app/theme/icomoon/icons/grid.svg | 5 + frontend/app/theme/icomoon/icons/grid1.svg | 5 + frontend/app/theme/icomoon/icons/help.svg | 54 +- frontend/app/theme/icomoon/icons/help2.svg | 5 + frontend/app/theme/icomoon/icons/hide-all.svg | 78 +-- frontend/app/theme/icomoon/icons/hide.svg | 69 +-- .../app/theme/icomoon/icons/hour-glass.svg | 5 + .../app/theme/icomoon/icons/info-outline.svg | 5 + frontend/app/theme/icomoon/icons/info.svg | 5 + frontend/app/theme/icomoon/icons/list.svg | 5 + frontend/app/theme/icomoon/icons/list1.svg | 5 + frontend/app/theme/icomoon/icons/location.svg | 58 +- frontend/app/theme/icomoon/icons/lock.svg | 5 + frontend/app/theme/icomoon/icons/logo.svg | 53 +- frontend/app/theme/icomoon/icons/media.svg | 54 +- .../app/theme/icomoon/icons/microsoft.svg | 5 + .../app/theme/icomoon/icons/minus-square.svg | 6 + frontend/app/theme/icomoon/icons/more.svg | 54 +- .../theme/icomoon/icons/multiple-content.svg | 104 +--- frontend/app/theme/icomoon/icons/orleans.svg | 72 +-- frontend/app/theme/icomoon/icons/pause.svg | 5 + frontend/app/theme/icomoon/icons/pencil.svg | 56 +- frontend/app/theme/icomoon/icons/play.svg | 5 + .../app/theme/icomoon/icons/plus-square.svg | 6 + .../app/theme/icomoon/icons/prerender.svg | 7 + .../app/theme/icomoon/icons/reference.svg | 54 +- frontend/app/theme/icomoon/icons/reset.svg | 6 + frontend/app/theme/icomoon/icons/rules.svg | 12 + frontend/app/theme/icomoon/icons/schemas.svg | 89 +-- frontend/app/theme/icomoon/icons/search.svg | 56 +- frontend/app/theme/icomoon/icons/settings.svg | 54 +- .../app/theme/icomoon/icons/settings2.svg | 5 + frontend/app/theme/icomoon/icons/show-all.svg | 78 +-- frontend/app/theme/icomoon/icons/show.svg | 69 +-- .../theme/icomoon/icons/single-content.svg | 77 +-- frontend/app/theme/icomoon/icons/spinner.svg | 5 + frontend/app/theme/icomoon/icons/spinner2.svg | 5 + .../app/theme/icomoon/icons/star-empty.svg | 5 + .../app/theme/icomoon/icons/star-full.svg | 5 + frontend/app/theme/icomoon/icons/support.svg | 5 + frontend/app/theme/icomoon/icons/timeout.svg | 6 + .../app/theme/icomoon/icons/translate.svg | 5 + .../theme/icomoon/icons/trigger-Manual.svg | 5 + frontend/app/theme/icomoon/icons/twitter.svg | 5 + .../app/theme/icomoon/icons/type-Array.svg | 27 +- .../app/theme/icomoon/icons/type-Boolean.svg | 58 +- .../app/theme/icomoon/icons/type-DateTime.svg | 58 +- .../app/theme/icomoon/icons/type-Json.svg | 5 + .../app/theme/icomoon/icons/type-Number.svg | 54 +- .../theme/icomoon/icons/type-References.svg | 5 + .../app/theme/icomoon/icons/type-String.svg | 54 +- .../app/theme/icomoon/icons/type-Tags.svg | 82 +-- frontend/app/theme/icomoon/icons/type-UI.svg | 5 + frontend/app/theme/icomoon/icons/unlocked.svg | 5 + frontend/app/theme/icomoon/icons/upload-2.svg | 5 + frontend/app/theme/icomoon/icons/upload-3.svg | 5 + frontend/app/theme/icomoon/icons/upload-4.svg | 5 + frontend/app/theme/icomoon/icons/upload.svg | 5 + frontend/app/theme/icomoon/icons/user-o.svg | 55 +- frontend/app/theme/icomoon/icons/user.svg | 54 +- frontend/app/theme/icomoon/selection.json | 2 +- frontend/app/theme/icomoon/style.css | 16 +- media/icons.svg | 540 ++++++++++++++++++ 353 files changed, 6372 insertions(+), 5098 deletions(-) create mode 100644 backend/src/Squidex.Domain.Apps.Entities.MongoDb/Assets/MongoAssetFolderEntity.cs create mode 100644 backend/src/Squidex.Domain.Apps.Entities.MongoDb/Assets/MongoAssetFolderRepository.cs create mode 100644 backend/src/Squidex.Domain.Apps.Entities.MongoDb/Assets/MongoAssetFolderRepository_SnapshotStore.cs create mode 100644 backend/src/Squidex.Domain.Apps.Entities/Assets/AssetFolderGrain.cs create mode 100644 backend/src/Squidex.Domain.Apps.Entities/Assets/Commands/AssetFolderCommand.cs create mode 100644 backend/src/Squidex.Domain.Apps.Entities/Assets/Commands/CreateAssetFolder.cs create mode 100644 backend/src/Squidex.Domain.Apps.Entities/Assets/Commands/DeleteAssetFolder.cs create mode 100644 backend/src/Squidex.Domain.Apps.Entities/Assets/Commands/MoveAsset.cs create mode 100644 backend/src/Squidex.Domain.Apps.Entities/Assets/Commands/MoveAssetFolder.cs create mode 100644 backend/src/Squidex.Domain.Apps.Entities/Assets/Commands/RenameAssetFolder.cs create mode 100644 backend/src/Squidex.Domain.Apps.Entities/Assets/Guards/GuardAssetFolder.cs create mode 100644 backend/src/Squidex.Domain.Apps.Entities/Assets/IAssetFolderEntity.cs create mode 100644 backend/src/Squidex.Domain.Apps.Entities/Assets/IAssetFolderGrain.cs create mode 100644 backend/src/Squidex.Domain.Apps.Entities/Assets/Repositories/IAssetFolderRepository.cs create mode 100644 backend/src/Squidex.Domain.Apps.Entities/Assets/State/AssetFolderState.cs create mode 100644 backend/src/Squidex.Domain.Apps.Events/Assets/AssetFolderCreated.cs create mode 100644 backend/src/Squidex.Domain.Apps.Events/Assets/AssetFolderDeleted.cs create mode 100644 backend/src/Squidex.Domain.Apps.Events/Assets/AssetFolderEvent.cs create mode 100644 backend/src/Squidex.Domain.Apps.Events/Assets/AssetFolderMoved.cs create mode 100644 backend/src/Squidex.Domain.Apps.Events/Assets/AssetFolderRenamed.cs create mode 100644 backend/src/Squidex.Domain.Apps.Events/Assets/AssetMoved.cs create mode 100644 backend/src/Squidex/Areas/Api/Controllers/Assets/AssetFoldersController.cs create mode 100644 backend/src/Squidex/Areas/Api/Controllers/Assets/Models/AssetFolderDto.cs create mode 100644 backend/src/Squidex/Areas/Api/Controllers/Assets/Models/AssetFoldersDto.cs create mode 100644 backend/src/Squidex/Areas/Api/Controllers/Assets/Models/CreateAssetFolderDto.cs create mode 100644 backend/src/Squidex/Areas/Api/Controllers/Assets/Models/MoveAssetItemDto.cs create mode 100644 backend/src/Squidex/Areas/Api/Controllers/Assets/Models/RenameAssetFolderDto.cs delete mode 100644 backend/src/Squidex/wwwroot/images/add-app.png create mode 100644 backend/src/Squidex/wwwroot/images/add-app.svg delete mode 100644 backend/src/Squidex/wwwroot/images/add-blog.png create mode 100644 backend/src/Squidex/wwwroot/images/add-blog.svg delete mode 100644 backend/src/Squidex/wwwroot/images/add-identity.png create mode 100644 backend/src/Squidex/wwwroot/images/add-identity.svg delete mode 100644 backend/src/Squidex/wwwroot/images/add-profile.png create mode 100644 backend/src/Squidex/wwwroot/images/add-profile.svg delete mode 100644 backend/src/Squidex/wwwroot/images/asset_doc.png create mode 100644 backend/src/Squidex/wwwroot/images/asset_doc.svg delete mode 100644 backend/src/Squidex/wwwroot/images/asset_docx.png create mode 100644 backend/src/Squidex/wwwroot/images/asset_docx.svg delete mode 100644 backend/src/Squidex/wwwroot/images/asset_generic.png create mode 100644 backend/src/Squidex/wwwroot/images/asset_generic.svg delete mode 100644 backend/src/Squidex/wwwroot/images/asset_pdf.png create mode 100644 backend/src/Squidex/wwwroot/images/asset_pdf.svg delete mode 100644 backend/src/Squidex/wwwroot/images/asset_ppt.png create mode 100644 backend/src/Squidex/wwwroot/images/asset_ppt.svg delete mode 100644 backend/src/Squidex/wwwroot/images/asset_pptx.png create mode 100644 backend/src/Squidex/wwwroot/images/asset_pptx.svg delete mode 100644 backend/src/Squidex/wwwroot/images/asset_video.png create mode 100644 backend/src/Squidex/wwwroot/images/asset_video.svg delete mode 100644 backend/src/Squidex/wwwroot/images/asset_xls.png create mode 100644 backend/src/Squidex/wwwroot/images/asset_xls.svg delete mode 100644 backend/src/Squidex/wwwroot/images/asset_xlsx.png create mode 100644 backend/src/Squidex/wwwroot/images/asset_xlsx.svg delete mode 100644 backend/src/Squidex/wwwroot/images/dashboard-api.png create mode 100644 backend/src/Squidex/wwwroot/images/dashboard-api.svg delete mode 100644 backend/src/Squidex/wwwroot/images/dashboard-feedback.png delete mode 100644 backend/src/Squidex/wwwroot/images/dashboard-github.png create mode 100644 backend/src/Squidex/wwwroot/images/dashboard-github.svg delete mode 100644 backend/src/Squidex/wwwroot/images/dashboard-schema.png create mode 100644 backend/src/Squidex/wwwroot/images/dashboard-schema.svg create mode 100644 backend/src/Squidex/wwwroot/images/dashboard_feedback.svg create mode 100644 backend/src/Squidex/wwwroot/images/dashboard_schema.svg create mode 100644 backend/src/Squidex/wwwroot/images/folder.svg create mode 100644 backend/src/Squidex/wwwroot/images/logo.svg create mode 100644 backend/tests/Squidex.Domain.Apps.Entities.Tests/Assets/AssetFolderGrainTests.cs create mode 100644 backend/tests/Squidex.Domain.Apps.Entities.Tests/Assets/Guards/GuardAssetFolderTests.cs delete mode 100644 frontend/app/framework/angular/ignore-scrollbar.directive.ts create mode 100644 frontend/app/framework/angular/list-view.component.html create mode 100644 frontend/app/framework/angular/list-view.component.scss create mode 100644 frontend/app/framework/angular/list-view.component.ts create mode 100644 frontend/app/framework/angular/sync-width.directive.ts create mode 100644 frontend/app/shared/components/asset-folder-form.component.html create mode 100644 frontend/app/shared/components/asset-folder-form.component.scss create mode 100644 frontend/app/shared/components/asset-folder-form.component.ts create mode 100644 frontend/app/shared/components/asset-folder.component.html create mode 100644 frontend/app/shared/components/asset-folder.component.scss create mode 100644 frontend/app/shared/components/asset-folder.component.ts create mode 100644 frontend/app/shared/components/asset-path.component.ts delete mode 100644 frontend/app/theme/icomoon/icons/add-app.svg create mode 100644 frontend/app/theme/icomoon/icons/angle-down.svg create mode 100644 frontend/app/theme/icomoon/icons/angle-left.svg create mode 100644 frontend/app/theme/icomoon/icons/angle-right.svg create mode 100644 frontend/app/theme/icomoon/icons/angle-up.svg create mode 100644 frontend/app/theme/icomoon/icons/arrow-right.svg create mode 100644 frontend/app/theme/icomoon/icons/arrow_back.svg create mode 100644 frontend/app/theme/icomoon/icons/backup.svg create mode 100644 frontend/app/theme/icomoon/icons/bin2.svg create mode 100644 frontend/app/theme/icomoon/icons/browser.svg create mode 100644 frontend/app/theme/icomoon/icons/bug.svg create mode 100644 frontend/app/theme/icomoon/icons/caret-down.svg create mode 100644 frontend/app/theme/icomoon/icons/caret-left.svg create mode 100644 frontend/app/theme/icomoon/icons/caret-right.svg create mode 100644 frontend/app/theme/icomoon/icons/caret-up.svg create mode 100644 frontend/app/theme/icomoon/icons/checkmark.svg create mode 100644 frontend/app/theme/icomoon/icons/circle.svg create mode 100644 frontend/app/theme/icomoon/icons/clock.svg create mode 100644 frontend/app/theme/icomoon/icons/clone.svg create mode 100644 frontend/app/theme/icomoon/icons/comments.svg create mode 100644 frontend/app/theme/icomoon/icons/control-Color.svg create mode 100644 frontend/app/theme/icomoon/icons/control-Stars.svg create mode 100644 frontend/app/theme/icomoon/icons/corner-down-right.svg create mode 100644 frontend/app/theme/icomoon/icons/create_new_folder.svg delete mode 100644 frontend/app/theme/icomoon/icons/dashboard-api.svg delete mode 100644 frontend/app/theme/icomoon/icons/dashboard-feedback.svg delete mode 100644 frontend/app/theme/icomoon/icons/dashboard-github.svg delete mode 100644 frontend/app/theme/icomoon/icons/dashboard-schema.svg create mode 100644 frontend/app/theme/icomoon/icons/download.svg create mode 100644 frontend/app/theme/icomoon/icons/drag2.svg create mode 100644 frontend/app/theme/icomoon/icons/earth.svg create mode 100644 frontend/app/theme/icomoon/icons/elapsed.svg create mode 100644 frontend/app/theme/icomoon/icons/exclamation.svg create mode 100644 frontend/app/theme/icomoon/icons/external-link.svg create mode 100644 frontend/app/theme/icomoon/icons/filter-filled.svg create mode 100644 frontend/app/theme/icomoon/icons/folder.svg create mode 100644 frontend/app/theme/icomoon/icons/github.svg create mode 100644 frontend/app/theme/icomoon/icons/google.svg create mode 100644 frontend/app/theme/icomoon/icons/grid.svg create mode 100644 frontend/app/theme/icomoon/icons/grid1.svg create mode 100644 frontend/app/theme/icomoon/icons/help2.svg create mode 100644 frontend/app/theme/icomoon/icons/hour-glass.svg create mode 100644 frontend/app/theme/icomoon/icons/info-outline.svg create mode 100644 frontend/app/theme/icomoon/icons/info.svg create mode 100644 frontend/app/theme/icomoon/icons/list.svg create mode 100644 frontend/app/theme/icomoon/icons/list1.svg create mode 100644 frontend/app/theme/icomoon/icons/lock.svg create mode 100644 frontend/app/theme/icomoon/icons/microsoft.svg create mode 100644 frontend/app/theme/icomoon/icons/minus-square.svg create mode 100644 frontend/app/theme/icomoon/icons/pause.svg create mode 100644 frontend/app/theme/icomoon/icons/play.svg create mode 100644 frontend/app/theme/icomoon/icons/plus-square.svg create mode 100644 frontend/app/theme/icomoon/icons/prerender.svg create mode 100644 frontend/app/theme/icomoon/icons/reset.svg create mode 100644 frontend/app/theme/icomoon/icons/rules.svg create mode 100644 frontend/app/theme/icomoon/icons/settings2.svg create mode 100644 frontend/app/theme/icomoon/icons/spinner.svg create mode 100644 frontend/app/theme/icomoon/icons/spinner2.svg create mode 100644 frontend/app/theme/icomoon/icons/star-empty.svg create mode 100644 frontend/app/theme/icomoon/icons/star-full.svg create mode 100644 frontend/app/theme/icomoon/icons/support.svg create mode 100644 frontend/app/theme/icomoon/icons/timeout.svg create mode 100644 frontend/app/theme/icomoon/icons/translate.svg create mode 100644 frontend/app/theme/icomoon/icons/trigger-Manual.svg create mode 100644 frontend/app/theme/icomoon/icons/twitter.svg create mode 100644 frontend/app/theme/icomoon/icons/type-Json.svg create mode 100644 frontend/app/theme/icomoon/icons/type-References.svg create mode 100644 frontend/app/theme/icomoon/icons/type-UI.svg create mode 100644 frontend/app/theme/icomoon/icons/unlocked.svg create mode 100644 frontend/app/theme/icomoon/icons/upload-2.svg create mode 100644 frontend/app/theme/icomoon/icons/upload-3.svg create mode 100644 frontend/app/theme/icomoon/icons/upload-4.svg create mode 100644 frontend/app/theme/icomoon/icons/upload.svg create mode 100644 media/icons.svg diff --git a/backend/src/Squidex.Domain.Apps.Entities.MongoDb/Assets/MongoAssetEntity.cs b/backend/src/Squidex.Domain.Apps.Entities.MongoDb/Assets/MongoAssetEntity.cs index abbc6be88..f0d9eaa82 100644 --- a/backend/src/Squidex.Domain.Apps.Entities.MongoDb/Assets/MongoAssetEntity.cs +++ b/backend/src/Squidex.Domain.Apps.Entities.MongoDb/Assets/MongoAssetEntity.cs @@ -10,17 +10,12 @@ using System.Collections.Generic; using MongoDB.Bson; using MongoDB.Bson.Serialization.Attributes; using NodaTime; -using Squidex.Domain.Apps.Core.ValidateContent; using Squidex.Domain.Apps.Entities.Assets; using Squidex.Infrastructure; namespace Squidex.Domain.Apps.Entities.MongoDb.Assets { - public sealed class MongoAssetEntity : - IAssetEntity, - IUpdateableEntityWithVersion, - IUpdateableEntityWithCreatedBy, - IUpdateableEntityWithLastModifiedBy + public sealed class MongoAssetEntity : IAssetEntity { [BsonId] [BsonElement("_id")] @@ -32,6 +27,10 @@ namespace Squidex.Domain.Apps.Entities.MongoDb.Assets [BsonRepresentation(BsonType.String)] public Guid IndexedAppId { get; set; } + [BsonIgnoreIfDefault] + [BsonElement("pi")] + public Guid ParentId { get; set; } + [BsonRequired] [BsonElement("ct")] public Instant Created { get; set; } @@ -100,7 +99,7 @@ namespace Squidex.Domain.Apps.Entities.MongoDb.Assets [BsonElement("dl")] public bool IsDeleted { get; set; } - Guid IAssetInfo.AssetId + public Guid AssetId { get { return Id; } } diff --git a/backend/src/Squidex.Domain.Apps.Entities.MongoDb/Assets/MongoAssetFolderEntity.cs b/backend/src/Squidex.Domain.Apps.Entities.MongoDb/Assets/MongoAssetFolderEntity.cs new file mode 100644 index 000000000..74d5d8ed3 --- /dev/null +++ b/backend/src/Squidex.Domain.Apps.Entities.MongoDb/Assets/MongoAssetFolderEntity.cs @@ -0,0 +1,65 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschränkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System; +using MongoDB.Bson; +using MongoDB.Bson.Serialization.Attributes; +using NodaTime; +using Squidex.Domain.Apps.Entities.Assets; +using Squidex.Infrastructure; + +namespace Squidex.Domain.Apps.Entities.MongoDb.Assets +{ + public sealed class MongoAssetFolderEntity : IAssetFolderEntity + { + [BsonId] + [BsonElement("_id")] + [BsonRepresentation(BsonType.String)] + public Guid Id { get; set; } + + [BsonRequired] + [BsonElement("_ai")] + [BsonRepresentation(BsonType.String)] + public Guid IndexedAppId { get; set; } + + [BsonRequired] + [BsonElement("pi")] + public Guid ParentId { get; set; } + + [BsonRequired] + [BsonElement("ct")] + public Instant Created { get; set; } + + [BsonRequired] + [BsonElement("mt")] + public Instant LastModified { get; set; } + + [BsonRequired] + [BsonElement("ai")] + public NamedId AppId { get; set; } + + [BsonRequired] + [BsonElement("fn")] + public string FolderName { get; set; } + + [BsonRequired] + [BsonElement("vs")] + public long Version { get; set; } + + [BsonRequired] + [BsonElement("cb")] + public RefToken CreatedBy { get; set; } + + [BsonRequired] + [BsonElement("mb")] + public RefToken LastModifiedBy { get; set; } + + [BsonRequired] + [BsonElement("dl")] + public bool IsDeleted { get; set; } + } +} diff --git a/backend/src/Squidex.Domain.Apps.Entities.MongoDb/Assets/MongoAssetFolderRepository.cs b/backend/src/Squidex.Domain.Apps.Entities.MongoDb/Assets/MongoAssetFolderRepository.cs new file mode 100644 index 000000000..b34286e93 --- /dev/null +++ b/backend/src/Squidex.Domain.Apps.Entities.MongoDb/Assets/MongoAssetFolderRepository.cs @@ -0,0 +1,74 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschränkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System; +using System.Threading; +using System.Threading.Tasks; +using MongoDB.Driver; +using Squidex.Domain.Apps.Entities.Assets; +using Squidex.Domain.Apps.Entities.Assets.Repositories; +using Squidex.Infrastructure; +using Squidex.Infrastructure.Log; +using Squidex.Infrastructure.MongoDb; + +namespace Squidex.Domain.Apps.Entities.MongoDb.Assets +{ + public sealed partial class MongoAssetFolderRepository : MongoRepositoryBase, IAssetFolderRepository + { + public MongoAssetFolderRepository(IMongoDatabase database) + : base(database) + { + } + + protected override string CollectionName() + { + return "States_AssetFolders"; + } + + protected override Task SetupCollectionAsync(IMongoCollection collection, CancellationToken ct = default) + { + return collection.Indexes.CreateManyAsync(new[] + { + new CreateIndexModel( + Index + .Ascending(x => x.IndexedAppId) + .Ascending(x => x.IsDeleted) + .Ascending(x => x.ParentId)) + }, ct); + } + + public async Task> QueryAsync(Guid appId, Guid parentId) + { + using (Profiler.TraceMethod("QueryAsyncByQuery")) + { + var assetFolders = + await Collection + .Find(x => x.IndexedAppId == appId && !x.IsDeleted && x.ParentId == parentId).SortBy(x => x.FolderName) + .ToListAsync(); + + return ResultList.Create(assetFolders.Count, assetFolders); + } + } + + public async Task FindAssetFolderAsync(Guid id) + { + using (Profiler.TraceMethod()) + { + var assetFolderEntity = + await Collection.Find(x => x.Id == id) + .FirstOrDefaultAsync(); + + if (assetFolderEntity?.IsDeleted == true) + { + return null; + } + + return assetFolderEntity; + } + } + } +} diff --git a/backend/src/Squidex.Domain.Apps.Entities.MongoDb/Assets/MongoAssetFolderRepository_SnapshotStore.cs b/backend/src/Squidex.Domain.Apps.Entities.MongoDb/Assets/MongoAssetFolderRepository_SnapshotStore.cs new file mode 100644 index 000000000..629203307 --- /dev/null +++ b/backend/src/Squidex.Domain.Apps.Entities.MongoDb/Assets/MongoAssetFolderRepository_SnapshotStore.cs @@ -0,0 +1,75 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschränkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System; +using System.Threading; +using System.Threading.Tasks; +using MongoDB.Bson; +using MongoDB.Driver; +using Squidex.Domain.Apps.Entities.Assets.State; +using Squidex.Infrastructure; +using Squidex.Infrastructure.Log; +using Squidex.Infrastructure.MongoDb; +using Squidex.Infrastructure.Reflection; +using Squidex.Infrastructure.States; + +namespace Squidex.Domain.Apps.Entities.MongoDb.Assets +{ + public sealed partial class MongoAssetFolderRepository : ISnapshotStore + { + async Task<(AssetFolderState Value, long Version)> ISnapshotStore.ReadAsync(Guid key) + { + using (Profiler.TraceMethod()) + { + var existing = + await Collection.Find(x => x.Id == key) + .FirstOrDefaultAsync(); + + if (existing != null) + { + return (Map(existing), existing.Version); + } + + return (null!, EtagVersion.NotFound); + } + } + + async Task ISnapshotStore.WriteAsync(Guid key, AssetFolderState value, long oldVersion, long newVersion) + { + using (Profiler.TraceMethod()) + { + var entity = SimpleMapper.Map(value, new MongoAssetFolderEntity()); + + entity.Version = newVersion; + entity.IndexedAppId = value.AppId.Id; + + await Collection.ReplaceOneAsync(x => x.Id == key && x.Version == oldVersion, entity, Upsert); + } + } + + async Task ISnapshotStore.ReadAllAsync(Func callback, CancellationToken ct) + { + using (Profiler.TraceMethod()) + { + await Collection.Find(new BsonDocument(), options: Batching.Options).ForEachPipelineAsync(x => callback(Map(x), x.Version), ct); + } + } + + async Task ISnapshotStore.RemoveAsync(Guid key) + { + using (Profiler.TraceMethod()) + { + await Collection.DeleteOneAsync(x => x.Id == key); + } + } + + private static AssetFolderState Map(MongoAssetFolderEntity existing) + { + return SimpleMapper.Map(existing, new AssetFolderState()); + } + } +} diff --git a/backend/src/Squidex.Domain.Apps.Entities.MongoDb/Assets/MongoAssetRepository.cs b/backend/src/Squidex.Domain.Apps.Entities.MongoDb/Assets/MongoAssetRepository.cs index 536eaee23..6017edfc7 100644 --- a/backend/src/Squidex.Domain.Apps.Entities.MongoDb/Assets/MongoAssetRepository.cs +++ b/backend/src/Squidex.Domain.Apps.Entities.MongoDb/Assets/MongoAssetRepository.cs @@ -41,6 +41,7 @@ namespace Squidex.Domain.Apps.Entities.MongoDb.Assets Index .Ascending(x => x.IndexedAppId) .Ascending(x => x.IsDeleted) + .Ascending(x => x.ParentId) .Ascending(x => x.Tags) .Descending(x => x.LastModified)), new CreateIndexModel( @@ -51,7 +52,7 @@ namespace Squidex.Domain.Apps.Entities.MongoDb.Assets }, ct); } - public async Task> QueryAsync(Guid appId, ClrQuery query) + public async Task> QueryAsync(Guid appId, Guid? parentId, ClrQuery query) { using (Profiler.TraceMethod("QueryAsyncByQuery")) { @@ -59,19 +60,19 @@ namespace Squidex.Domain.Apps.Entities.MongoDb.Assets { query = query.AdjustToModel(); - var filter = query.BuildFilter(appId); + var filter = query.BuildFilter(appId, parentId); - var contentCount = Collection.Find(filter).CountDocumentsAsync(); - var contentItems = + var assetCount = Collection.Find(filter).CountDocumentsAsync(); + var assetItems = Collection.Find(filter) .AssetTake(query) .AssetSkip(query) .AssetSort(query) .ToListAsync(); - await Task.WhenAll(contentItems, contentCount); + await Task.WhenAll(assetItems, assetCount); - return ResultList.Create(contentCount.Result, contentItems.Result); + return ResultList.Create(assetCount.Result, assetItems.Result); } catch (MongoQueryException ex) { @@ -123,26 +124,16 @@ namespace Squidex.Domain.Apps.Entities.MongoDb.Assets } } - public async Task FindAssetAsync(Guid id, bool allowDeleted = false) + public async Task FindAssetAsync(Guid id) { using (Profiler.TraceMethod()) { var assetEntity = - await Collection.Find(x => x.Id == id) + await Collection.Find(x => x.Id == id && !x.IsDeleted) .FirstOrDefaultAsync(); - if (assetEntity?.IsDeleted == true && !allowDeleted) - { - return null; - } - return assetEntity; } } - - public Task RemoveAsync(Guid key) - { - return Collection.DeleteOneAsync(x => x.Id == key); - } } } diff --git a/backend/src/Squidex.Domain.Apps.Entities.MongoDb/Assets/Visitors/FindExtensions.cs b/backend/src/Squidex.Domain.Apps.Entities.MongoDb/Assets/Visitors/FindExtensions.cs index c5f057d86..26eca0f75 100644 --- a/backend/src/Squidex.Domain.Apps.Entities.MongoDb/Assets/Visitors/FindExtensions.cs +++ b/backend/src/Squidex.Domain.Apps.Entities.MongoDb/Assets/Visitors/FindExtensions.cs @@ -19,6 +19,7 @@ namespace Squidex.Domain.Apps.Entities.MongoDb.Assets.Visitors public static class FindExtensions { private static readonly FilterDefinitionBuilder Filter = Builders.Filter; + private static readonly SortDefinitionBuilder Sorting = Builders.Sort; public static ClrQuery AdjustToModel(this ClrQuery query) { @@ -52,7 +53,7 @@ namespace Squidex.Domain.Apps.Entities.MongoDb.Assets.Visitors return cursor.Skip(query); } - public static FilterDefinition BuildFilter(this ClrQuery query, Guid appId) + public static FilterDefinition BuildFilter(this ClrQuery query, Guid appId, Guid? parentId) { var filters = new List> { @@ -60,6 +61,21 @@ namespace Squidex.Domain.Apps.Entities.MongoDb.Assets.Visitors Filter.Eq(x => x.IsDeleted, false) }; + if (parentId.HasValue) + { + if (parentId == Guid.Empty) + { + filters.Add( + Filter.Or( + Filter.Exists(x => x.ParentId, false), + Filter.Eq(x => x.ParentId, Guid.Empty))); + } + else + { + filters.Add(Filter.Eq(x => x.ParentId, parentId.Value)); + } + } + var (filter, last) = query.BuildFilter(false); if (filter != null) diff --git a/backend/src/Squidex.Domain.Apps.Entities/Assets/AssetChangedTriggerHandler.cs b/backend/src/Squidex.Domain.Apps.Entities/Assets/AssetChangedTriggerHandler.cs index 7af666c55..c3fec9472 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Assets/AssetChangedTriggerHandler.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Assets/AssetChangedTriggerHandler.cs @@ -34,6 +34,11 @@ namespace Squidex.Domain.Apps.Entities.Assets protected override async Task CreateEnrichedEventAsync(Envelope @event) { + if (@event.Payload is AssetMoved) + { + return null; + } + var result = new EnrichedAssetEvent(); var asset = await assetLoader.GetAsync(@event.Payload.AssetId, @event.Headers.EventStreamNumber()); diff --git a/backend/src/Squidex.Domain.Apps.Entities/Assets/AssetEntity.cs b/backend/src/Squidex.Domain.Apps.Entities/Assets/AssetEntity.cs index 150e53b78..9424d8c57 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Assets/AssetEntity.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Assets/AssetEntity.cs @@ -18,7 +18,7 @@ namespace Squidex.Domain.Apps.Entities.Assets public Guid Id { get; set; } - public Guid AssetId { get; set; } + public Guid ParentId { get; set; } public Instant Created { get; set; } @@ -34,12 +34,12 @@ namespace Squidex.Domain.Apps.Entities.Assets public long Version { get; set; } - public string MimeType { get; set; } - public string FileName { get; set; } public string FileHash { get; set; } + public string MimeType { get; set; } + public string Slug { get; set; } public long FileSize { get; set; } @@ -53,5 +53,10 @@ namespace Squidex.Domain.Apps.Entities.Assets public int? PixelWidth { get; set; } public int? PixelHeight { get; set; } + + public Guid AssetId + { + get { return Id; } + } } } diff --git a/backend/src/Squidex.Domain.Apps.Entities/Assets/AssetFolderGrain.cs b/backend/src/Squidex.Domain.Apps.Entities/Assets/AssetFolderGrain.cs new file mode 100644 index 000000000..49525def9 --- /dev/null +++ b/backend/src/Squidex.Domain.Apps.Entities/Assets/AssetFolderGrain.cs @@ -0,0 +1,130 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschränkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System; +using System.Threading.Tasks; +using Squidex.Domain.Apps.Entities.Assets.Commands; +using Squidex.Domain.Apps.Entities.Assets.Guards; +using Squidex.Domain.Apps.Entities.Assets.State; +using Squidex.Domain.Apps.Events; +using Squidex.Domain.Apps.Events.Assets; +using Squidex.Infrastructure; +using Squidex.Infrastructure.Commands; +using Squidex.Infrastructure.EventSourcing; +using Squidex.Infrastructure.Log; +using Squidex.Infrastructure.Orleans; +using Squidex.Infrastructure.Reflection; +using Squidex.Infrastructure.States; + +namespace Squidex.Domain.Apps.Entities.Assets +{ + public sealed class AssetFolderGrain : DomainObjectGrain, IAssetFolderGrain + { + private static readonly TimeSpan Lifetime = TimeSpan.FromMinutes(5); + private readonly IAssetQueryService assetQuery; + + public AssetFolderGrain(IStore store, IAssetQueryService assetQuery, IActivationLimit limit, ISemanticLog log) + : base(store, log) + { + Guard.NotNull(assetQuery); + + this.assetQuery = assetQuery; + + limit?.SetLimit(5000, Lifetime); + } + + protected override Task OnActivateAsync(Guid key) + { + TryDelayDeactivation(Lifetime); + + return base.OnActivateAsync(key); + } + + protected override Task ExecuteAsync(IAggregateCommand command) + { + VerifyNotDeleted(); + + switch (command) + { + case CreateAssetFolder createAssetFolder: + return CreateReturnAsync(createAssetFolder, async c => + { + await GuardAssetFolder.CanCreate(c, assetQuery); + + Create(c); + + return Snapshot; + }); + case MoveAssetFolder moveAssetFolder: + return UpdateReturnAsync(moveAssetFolder, async c => + { + await GuardAssetFolder.CanMove(c, assetQuery, Snapshot.Id, Snapshot.ParentId); + + Move(c); + + return Snapshot; + }); + case RenameAssetFolder renameAssetFolder: + return UpdateReturn(renameAssetFolder, c => + { + GuardAssetFolder.CanRename(c, Snapshot.FolderName); + + Rename(c); + + return Snapshot; + }); + case DeleteAssetFolder deleteAssetFolder: + return Update(deleteAssetFolder, c => + { + GuardAssetFolder.CanDelete(c); + + Delete(c); + }); + default: + throw new NotSupportedException(); + } + } + + public void Create(CreateAssetFolder command) + { + RaiseEvent(SimpleMapper.Map(command, new AssetFolderCreated())); + } + + public void Move(MoveAssetFolder command) + { + RaiseEvent(SimpleMapper.Map(command, new AssetFolderMoved())); + } + + public void Rename(RenameAssetFolder command) + { + RaiseEvent(SimpleMapper.Map(command, new AssetFolderRenamed())); + } + + public void Delete(DeleteAssetFolder command) + { + RaiseEvent(SimpleMapper.Map(command, new AssetFolderDeleted())); + } + + private void RaiseEvent(AppEvent @event) + { + if (@event.AppId == null) + { + @event.AppId = Snapshot.AppId; + } + + RaiseEvent(Envelope.Create(@event)); + } + + private void VerifyNotDeleted() + { + if (Snapshot.IsDeleted) + { + throw new DomainException("Asset folder has already been deleted"); + } + } + } +} diff --git a/backend/src/Squidex.Domain.Apps.Entities/Assets/AssetGrain.cs b/backend/src/Squidex.Domain.Apps.Entities/Assets/AssetGrain.cs index 7b31dbfcc..6a625423e 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Assets/AssetGrain.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Assets/AssetGrain.cs @@ -28,14 +28,18 @@ namespace Squidex.Domain.Apps.Entities.Assets { private static readonly TimeSpan Lifetime = TimeSpan.FromMinutes(5); private readonly ITagService tagService; + private readonly IAssetQueryService assetQuery; - public AssetGrain(IStore store, ITagService tagService, IActivationLimit limit, ISemanticLog log) + public AssetGrain(IStore store, ITagService tagService, IAssetQueryService assetQuery, IActivationLimit limit, ISemanticLog log) : base(store, log) { Guard.NotNull(tagService); + Guard.NotNull(assetQuery); this.tagService = tagService; + this.assetQuery = assetQuery; + limit?.SetLimit(5000, Lifetime); } @@ -55,7 +59,7 @@ namespace Squidex.Domain.Apps.Entities.Assets case CreateAsset createAsset: return CreateReturnAsync(createAsset, async c => { - GuardAsset.CanCreate(c); + await GuardAsset.CanCreate(c, assetQuery); var tagIds = await NormalizeTagsAsync(c.AppId.Id, c.Tags); @@ -75,12 +79,21 @@ namespace Squidex.Domain.Apps.Entities.Assets case AnnotateAsset annotateAsset: return UpdateReturnAsync(annotateAsset, async c => { - GuardAsset.CanAnnotate(c, Snapshot.FileName, Snapshot.Slug); + GuardAsset.CanAnnotate(c, Snapshot.FileName!, Snapshot.Slug); var tagIds = await NormalizeTagsAsync(Snapshot.AppId.Id, c.Tags); Annotate(c, tagIds); + return Snapshot; + }); + case MoveAsset moveAsset: + return UpdateReturnAsync(moveAsset, async c => + { + await GuardAsset.CanMove(c, assetQuery, Snapshot.ParentId); + + Move(c); + return Snapshot; }); case DeleteAsset deleteAsset: @@ -152,6 +165,11 @@ namespace Squidex.Domain.Apps.Entities.Assets RaiseEvent(@event); } + public void Move(MoveAsset command) + { + RaiseEvent(SimpleMapper.Map(command, new AssetMoved())); + } + public void Delete(DeleteAsset command) { RaiseEvent(SimpleMapper.Map(command, new AssetDeleted { DeletedSize = Snapshot.TotalSize })); diff --git a/backend/src/Squidex.Domain.Apps.Entities/Assets/BackupAssets.cs b/backend/src/Squidex.Domain.Apps.Entities/Assets/BackupAssets.cs index aa572fa9c..85971681c 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Assets/BackupAssets.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Assets/BackupAssets.cs @@ -24,6 +24,7 @@ namespace Squidex.Domain.Apps.Entities.Assets { private const string TagsFile = "AssetTags.json"; private readonly HashSet assetIds = new HashSet(); + private readonly HashSet assetFolderIds = new HashSet(); private readonly IAssetStore assetStore; private readonly ITagService tagService; @@ -62,6 +63,9 @@ namespace Squidex.Domain.Apps.Entities.Assets { switch (@event.Payload) { + case AssetFolderCreated assetFolderCreated: + assetFolderIds.Add(assetFolderCreated.AssetFolderId); + break; case AssetCreated assetCreated: await ReadAssetAsync(assetCreated.AssetId, assetCreated.FileVersion, reader); break; @@ -78,6 +82,7 @@ namespace Squidex.Domain.Apps.Entities.Assets await RestoreTagsAsync(appId, reader); await RebuildManyAsync(assetIds, RebuildAsync); + await RebuildManyAsync(assetFolderIds, RebuildAsync); } private async Task RestoreTagsAsync(Guid appId, BackupReader reader) diff --git a/backend/src/Squidex.Domain.Apps.Entities/Assets/Commands/AssetFolderCommand.cs b/backend/src/Squidex.Domain.Apps.Entities/Assets/Commands/AssetFolderCommand.cs new file mode 100644 index 000000000..8d3999c25 --- /dev/null +++ b/backend/src/Squidex.Domain.Apps.Entities/Assets/Commands/AssetFolderCommand.cs @@ -0,0 +1,22 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschränkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System; +using Squidex.Infrastructure.Commands; + +namespace Squidex.Domain.Apps.Entities.Assets.Commands +{ + public abstract class AssetFolderCommand : SquidexCommand, IAggregateCommand + { + public Guid AssetFolderId { get; set; } + + Guid IAggregateCommand.AggregateId + { + get { return AssetFolderId; } + } + } +} diff --git a/backend/src/Squidex.Domain.Apps.Entities/Assets/Commands/CreateAsset.cs b/backend/src/Squidex.Domain.Apps.Entities/Assets/Commands/CreateAsset.cs index 8e869ba40..2dcab921d 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Assets/Commands/CreateAsset.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Assets/Commands/CreateAsset.cs @@ -15,6 +15,8 @@ namespace Squidex.Domain.Apps.Entities.Assets.Commands { public NamedId AppId { get; set; } + public Guid ParentId { get; set; } + public HashSet Tags { get; set; } public CreateAsset() diff --git a/backend/src/Squidex.Domain.Apps.Entities/Assets/Commands/CreateAssetFolder.cs b/backend/src/Squidex.Domain.Apps.Entities/Assets/Commands/CreateAssetFolder.cs new file mode 100644 index 000000000..8ca8d412c --- /dev/null +++ b/backend/src/Squidex.Domain.Apps.Entities/Assets/Commands/CreateAssetFolder.cs @@ -0,0 +1,26 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System; +using Squidex.Infrastructure; + +namespace Squidex.Domain.Apps.Entities.Assets.Commands +{ + public sealed class CreateAssetFolder : AssetFolderCommand, IAppCommand + { + public NamedId AppId { get; set; } + + public string FolderName { get; set; } + + public Guid ParentId { get; set; } + + public CreateAssetFolder() + { + AssetFolderId = Guid.NewGuid(); + } + } +} diff --git a/backend/src/Squidex.Domain.Apps.Entities/Assets/Commands/DeleteAssetFolder.cs b/backend/src/Squidex.Domain.Apps.Entities/Assets/Commands/DeleteAssetFolder.cs new file mode 100644 index 000000000..9d5077c6d --- /dev/null +++ b/backend/src/Squidex.Domain.Apps.Entities/Assets/Commands/DeleteAssetFolder.cs @@ -0,0 +1,13 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschränkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +namespace Squidex.Domain.Apps.Entities.Assets.Commands +{ + public sealed class DeleteAssetFolder : AssetFolderCommand + { + } +} diff --git a/backend/src/Squidex.Domain.Apps.Entities/Assets/Commands/MoveAsset.cs b/backend/src/Squidex.Domain.Apps.Entities/Assets/Commands/MoveAsset.cs new file mode 100644 index 000000000..05a16b5fb --- /dev/null +++ b/backend/src/Squidex.Domain.Apps.Entities/Assets/Commands/MoveAsset.cs @@ -0,0 +1,16 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System; + +namespace Squidex.Domain.Apps.Entities.Assets.Commands +{ + public sealed class MoveAsset : AssetCommand + { + public Guid ParentId { get; set; } + } +} diff --git a/backend/src/Squidex.Domain.Apps.Entities/Assets/Commands/MoveAssetFolder.cs b/backend/src/Squidex.Domain.Apps.Entities/Assets/Commands/MoveAssetFolder.cs new file mode 100644 index 000000000..45d16672d --- /dev/null +++ b/backend/src/Squidex.Domain.Apps.Entities/Assets/Commands/MoveAssetFolder.cs @@ -0,0 +1,16 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System; + +namespace Squidex.Domain.Apps.Entities.Assets.Commands +{ + public sealed class MoveAssetFolder : AssetFolderCommand + { + public Guid ParentId { get; set; } + } +} diff --git a/backend/src/Squidex.Domain.Apps.Entities/Assets/Commands/RenameAssetFolder.cs b/backend/src/Squidex.Domain.Apps.Entities/Assets/Commands/RenameAssetFolder.cs new file mode 100644 index 000000000..86e289692 --- /dev/null +++ b/backend/src/Squidex.Domain.Apps.Entities/Assets/Commands/RenameAssetFolder.cs @@ -0,0 +1,14 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +namespace Squidex.Domain.Apps.Entities.Assets.Commands +{ + public sealed class RenameAssetFolder : AssetFolderCommand + { + public string FolderName { get; set; } + } +} diff --git a/backend/src/Squidex.Domain.Apps.Entities/Assets/Guards/GuardAsset.cs b/backend/src/Squidex.Domain.Apps.Entities/Assets/Guards/GuardAsset.cs index 0a0a7188a..edc0ba2c5 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Assets/Guards/GuardAsset.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Assets/Guards/GuardAsset.cs @@ -5,6 +5,8 @@ // All rights reserved. Licensed under the MIT license. // ========================================================================== +using System; +using System.Threading.Tasks; using Squidex.Domain.Apps.Entities.Assets.Commands; using Squidex.Infrastructure; using Squidex.Infrastructure.Validation; @@ -28,9 +30,31 @@ namespace Squidex.Domain.Apps.Entities.Assets.Guards }); } - public static void CanCreate(CreateAsset command) + public static Task CanCreate(CreateAsset command, IAssetQueryService assetQuery) { Guard.NotNull(command); + + return Validate.It(() => "Cannot upload asset.", async e => + { + await CheckPathAsync(command.ParentId, assetQuery, e); + }); + } + + public static Task CanMove(MoveAsset command, IAssetQueryService assetQuery, Guid oldParentId) + { + Guard.NotNull(command); + + return Validate.It(() => "Cannot move asset.", async e => + { + if (command.ParentId == oldParentId) + { + e("Asset is already part of this folder.", nameof(command.ParentId)); + } + else + { + await CheckPathAsync(command.ParentId, assetQuery, e); + } + }); } public static void CanUpdate(UpdateAsset command) @@ -42,5 +66,18 @@ namespace Squidex.Domain.Apps.Entities.Assets.Guards { Guard.NotNull(command); } + + private static async Task CheckPathAsync(Guid parentId, IAssetQueryService assetQuery, AddValidation e) + { + if (parentId != default) + { + var path = await assetQuery.FindAssetFolderAsync(parentId); + + if (path.Count == 0) + { + e("Asset folder does not exist.", nameof(MoveAsset.ParentId)); + } + } + } } } diff --git a/backend/src/Squidex.Domain.Apps.Entities/Assets/Guards/GuardAssetFolder.cs b/backend/src/Squidex.Domain.Apps.Entities/Assets/Guards/GuardAssetFolder.cs new file mode 100644 index 000000000..111eb1b85 --- /dev/null +++ b/backend/src/Squidex.Domain.Apps.Entities/Assets/Guards/GuardAssetFolder.cs @@ -0,0 +1,95 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschränkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System; +using System.Threading.Tasks; +using Squidex.Domain.Apps.Entities.Assets.Commands; +using Squidex.Infrastructure; +using Squidex.Infrastructure.Validation; + +namespace Squidex.Domain.Apps.Entities.Assets.Guards +{ + public static class GuardAssetFolder + { + public static Task CanCreate(CreateAssetFolder command, IAssetQueryService assetQuery) + { + Guard.NotNull(command); + + return Validate.It(() => "Cannot upload asset.", async e => + { + if (string.IsNullOrWhiteSpace(command.FolderName)) + { + e(Not.Defined("Folder name"), nameof(command.FolderName)); + } + + await CheckPathAsync(command.ParentId, assetQuery, Guid.Empty, e); + }); + } + + public static void CanRename(RenameAssetFolder command, string olderFolderName) + { + Guard.NotNull(command); + + Validate.It(() => "Cannot rename asset.", e => + { + if (string.IsNullOrWhiteSpace(command.FolderName)) + { + e(Not.Defined("Folder name"), nameof(command.FolderName)); + } + else if (string.Equals(command.FolderName, olderFolderName)) + { + e(Not.New("Asset folder", "name"), nameof(command.FolderName)); + } + }); + } + + public static Task CanMove(MoveAssetFolder command, IAssetQueryService assetQuery, Guid id, Guid oldParentId) + { + Guard.NotNull(command); + + return Validate.It(() => "Cannot move asset.", async e => + { + if (command.ParentId == oldParentId) + { + e("Asset folder is already part of this folder.", nameof(command.ParentId)); + } + else + { + await CheckPathAsync(command.ParentId, assetQuery, id, e); + } + }); + } + + public static void CanDelete(DeleteAssetFolder command) + { + Guard.NotNull(command); + } + + private static async Task CheckPathAsync(Guid parentId, IAssetQueryService assetQuery, Guid id, AddValidation e) + { + if (parentId != default) + { + var path = await assetQuery.FindAssetFolderAsync(parentId); + + if (path.Count == 0) + { + e("Asset folder does not exist.", nameof(MoveAssetFolder.ParentId)); + } + else if (id != default) + { + var indexOfThis = path.IndexOf(x => x.Id == id); + var indexOfParent = path.IndexOf(x => x.Id == parentId); + + if (indexOfThis >= 0 && indexOfParent > indexOfThis) + { + e("Cannot add folder to its own child.", nameof(MoveAssetFolder.ParentId)); + } + } + } + } + } +} diff --git a/backend/src/Squidex.Domain.Apps.Entities/Assets/IAssetEntity.cs b/backend/src/Squidex.Domain.Apps.Entities/Assets/IAssetEntity.cs index 02890b87e..df6da0cc5 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Assets/IAssetEntity.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Assets/IAssetEntity.cs @@ -21,6 +21,8 @@ namespace Squidex.Domain.Apps.Entities.Assets { NamedId AppId { get; } + Guid ParentId { get; } + string MimeType { get; } long FileVersion { get; } diff --git a/backend/src/Squidex.Domain.Apps.Entities/Assets/IAssetFolderEntity.cs b/backend/src/Squidex.Domain.Apps.Entities/Assets/IAssetFolderEntity.cs new file mode 100644 index 000000000..83f9cf768 --- /dev/null +++ b/backend/src/Squidex.Domain.Apps.Entities/Assets/IAssetFolderEntity.cs @@ -0,0 +1,21 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System; +using Squidex.Infrastructure; + +namespace Squidex.Domain.Apps.Entities.Assets +{ + public interface IAssetFolderEntity : IEntity, IEntityWithVersion + { + NamedId AppId { get; set; } + + string FolderName { get; set; } + + Guid ParentId { get; set; } + } +} diff --git a/backend/src/Squidex.Domain.Apps.Entities/Assets/IAssetFolderGrain.cs b/backend/src/Squidex.Domain.Apps.Entities/Assets/IAssetFolderGrain.cs new file mode 100644 index 000000000..924c52ae2 --- /dev/null +++ b/backend/src/Squidex.Domain.Apps.Entities/Assets/IAssetFolderGrain.cs @@ -0,0 +1,15 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using Squidex.Infrastructure.Commands; + +namespace Squidex.Domain.Apps.Entities.Assets +{ + public interface IAssetFolderGrain : IDomainObjectGrain + { + } +} diff --git a/backend/src/Squidex.Domain.Apps.Entities/Assets/IAssetQueryService.cs b/backend/src/Squidex.Domain.Apps.Entities/Assets/IAssetQueryService.cs index dc94e0293..6db84de4d 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Assets/IAssetQueryService.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Assets/IAssetQueryService.cs @@ -16,7 +16,11 @@ namespace Squidex.Domain.Apps.Entities.Assets { Task> QueryByHashAsync(Context context, Guid appId, string hash); - Task> QueryAsync(Context context, Q query); + Task> QueryAsync(Context context, Guid? parentId, Q query); + + Task> QueryAssetFoldersAsync(Context context, Guid parentId); + + Task> FindAssetFolderAsync(Guid id); Task FindAssetAsync(Context context, Guid id); } diff --git a/backend/src/Squidex.Domain.Apps.Entities/Assets/Queries/AssetQueryService.cs b/backend/src/Squidex.Domain.Apps.Entities/Assets/Queries/AssetQueryService.cs index 098b5a516..09fec1034 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Assets/Queries/AssetQueryService.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Assets/Queries/AssetQueryService.cs @@ -7,6 +7,7 @@ using System; using System.Collections.Generic; +using System.Linq; using System.Threading.Tasks; using Squidex.Domain.Apps.Entities.Assets.Repositories; using Squidex.Infrastructure; @@ -17,19 +18,23 @@ namespace Squidex.Domain.Apps.Entities.Assets.Queries { private readonly IAssetEnricher assetEnricher; private readonly IAssetRepository assetRepository; + private readonly IAssetFolderRepository assetFolderRepository; private readonly AssetQueryParser queryParser; public AssetQueryService( IAssetEnricher assetEnricher, IAssetRepository assetRepository, + IAssetFolderRepository assetFolderRepository, AssetQueryParser queryParser) { Guard.NotNull(assetEnricher); Guard.NotNull(assetRepository); + Guard.NotNull(assetFolderRepository); Guard.NotNull(queryParser); this.assetEnricher = assetEnricher; this.assetRepository = assetRepository; + this.assetFolderRepository = assetFolderRepository; this.queryParser = queryParser; } @@ -45,6 +50,35 @@ namespace Squidex.Domain.Apps.Entities.Assets.Queries return null; } + public async Task> FindAssetFolderAsync(Guid id) + { + var result = new List(); + + while (id != default) + { + var folder = await assetFolderRepository.FindAssetFolderAsync(id); + + if (folder == null || result.Any(x => x.Id == folder.Id)) + { + result.Clear(); + break; + } + + result.Insert(0, folder); + + id = folder.ParentId; + } + + return result; + } + + public async Task> QueryAssetFoldersAsync(Context context, Guid parentId) + { + var assetFolders = await assetFolderRepository.QueryAsync(context.App.Id, parentId); + + return assetFolders; + } + public async Task> QueryByHashAsync(Context context, Guid appId, string hash) { Guard.NotNull(hash); @@ -54,7 +88,7 @@ namespace Squidex.Domain.Apps.Entities.Assets.Queries return await assetEnricher.EnrichAsync(assets, context); } - public async Task> QueryAsync(Context context, Q query) + public async Task> QueryAsync(Context context, Guid? parentId, Q query) { Guard.NotNull(context); Guard.NotNull(query); @@ -67,7 +101,7 @@ namespace Squidex.Domain.Apps.Entities.Assets.Queries } else { - assets = await QueryByQueryAsync(context, query); + assets = await QueryByQueryAsync(context, parentId, query); } var enriched = await assetEnricher.EnrichAsync(assets, context); @@ -75,11 +109,11 @@ namespace Squidex.Domain.Apps.Entities.Assets.Queries return ResultList.Create(assets.Total, enriched); } - private async Task> QueryByQueryAsync(Context context, Q query) + private async Task> QueryByQueryAsync(Context context, Guid? parentId, Q query) { var parsedQuery = queryParser.ParseQuery(context, query); - return await assetRepository.QueryAsync(context.App.Id, parsedQuery); + return await assetRepository.QueryAsync(context.App.Id, parentId, parsedQuery); } private async Task> QueryByIdsAsync(Context context, Q query) diff --git a/backend/src/Squidex.Domain.Apps.Entities/Assets/Repositories/IAssetFolderRepository.cs b/backend/src/Squidex.Domain.Apps.Entities/Assets/Repositories/IAssetFolderRepository.cs new file mode 100644 index 000000000..f1bdac3cc --- /dev/null +++ b/backend/src/Squidex.Domain.Apps.Entities/Assets/Repositories/IAssetFolderRepository.cs @@ -0,0 +1,20 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschränkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System; +using System.Threading.Tasks; +using Squidex.Infrastructure; + +namespace Squidex.Domain.Apps.Entities.Assets.Repositories +{ + public interface IAssetFolderRepository + { + Task> QueryAsync(Guid appId, Guid parentId); + + Task FindAssetFolderAsync(Guid id); + } +} diff --git a/backend/src/Squidex.Domain.Apps.Entities/Assets/Repositories/IAssetRepository.cs b/backend/src/Squidex.Domain.Apps.Entities/Assets/Repositories/IAssetRepository.cs index 11a45e228..cc6f19bf7 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Assets/Repositories/IAssetRepository.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Assets/Repositories/IAssetRepository.cs @@ -17,14 +17,12 @@ namespace Squidex.Domain.Apps.Entities.Assets.Repositories { Task> QueryByHashAsync(Guid appId, string hash); - Task> QueryAsync(Guid appId, ClrQuery query); + Task> QueryAsync(Guid appId, Guid? parentId, ClrQuery query); Task> QueryAsync(Guid appId, HashSet ids); - Task FindAssetAsync(Guid id, bool allowDeleted = false); + Task FindAssetAsync(Guid id); Task FindAssetBySlugAsync(Guid appId, string slug); - - Task RemoveAsync(Guid appId); } } diff --git a/backend/src/Squidex.Domain.Apps.Entities/Assets/State/AssetFolderState.cs b/backend/src/Squidex.Domain.Apps.Entities/Assets/State/AssetFolderState.cs new file mode 100644 index 000000000..6d6ce90cc --- /dev/null +++ b/backend/src/Squidex.Domain.Apps.Entities/Assets/State/AssetFolderState.cs @@ -0,0 +1,72 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschränkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System; +using System.Runtime.Serialization; +using Squidex.Domain.Apps.Events.Assets; +using Squidex.Infrastructure; +using Squidex.Infrastructure.EventSourcing; +using Squidex.Infrastructure.Reflection; + +#pragma warning disable IDE0060 // Remove unused parameter + +namespace Squidex.Domain.Apps.Entities.Assets.State +{ + public class AssetFolderState : DomainObjectState, IAssetFolderEntity + { + [DataMember] + public NamedId AppId { get; set; } + + [DataMember] + public string FolderName { get; set; } + + [DataMember] + public bool IsDeleted { get; set; } + + [DataMember] + public Guid ParentId { get; set; } + + public void ApplyEvent(IEvent @event) + { + switch (@event) + { + case AssetFolderCreated e: + { + SimpleMapper.Map(e, this); + + break; + } + + case AssetFolderRenamed e: + { + SimpleMapper.Map(e, this); + + break; + } + + case AssetFolderMoved e: + { + ParentId = e.ParentId; + + break; + } + + case AssetFolderDeleted _: + { + IsDeleted = true; + + break; + } + } + } + + public override AssetFolderState Apply(Envelope @event) + { + return Clone().Update(@event, (e, s) => s.ApplyEvent(e)); + } + } +} diff --git a/backend/src/Squidex.Domain.Apps.Entities/Assets/State/AssetState.cs b/backend/src/Squidex.Domain.Apps.Entities/Assets/State/AssetState.cs index f3c9834f4..e271c75fc 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Assets/State/AssetState.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Assets/State/AssetState.cs @@ -8,7 +8,6 @@ using System; using System.Collections.Generic; using System.Runtime.Serialization; -using Squidex.Domain.Apps.Core.ValidateContent; using Squidex.Domain.Apps.Events.Assets; using Squidex.Infrastructure; using Squidex.Infrastructure.EventSourcing; @@ -23,6 +22,9 @@ namespace Squidex.Domain.Apps.Entities.Assets.State [DataMember] public NamedId AppId { get; set; } + [DataMember] + public Guid ParentId { get; set; } + [DataMember] public string FileName { get; set; } @@ -59,7 +61,7 @@ namespace Squidex.Domain.Apps.Entities.Assets.State [DataMember] public HashSet Tags { get; set; } - Guid IAssetInfo.AssetId + public Guid AssetId { get { return Id; } } @@ -117,6 +119,13 @@ namespace Squidex.Domain.Apps.Entities.Assets.State break; } + case AssetMoved e: + { + ParentId = e.ParentId; + + break; + } + case AssetDeleted _: { IsDeleted = true; diff --git a/backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/AssetGraphType.cs b/backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/AssetGraphType.cs index 6dde0c175..0ef994ec0 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/AssetGraphType.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/AssetGraphType.cs @@ -111,7 +111,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL.Types { Name = "fileType", ResolvedType = AllTypes.NonNullString, - Resolver = Resolve(x => x.FileName.FileType()), + Resolver = Resolve(x => x.FileName!.FileType()), Description = "The file type." }); diff --git a/backend/src/Squidex.Domain.Apps.Entities/Contents/Queries/ContentEnricher.cs b/backend/src/Squidex.Domain.Apps.Entities/Contents/Queries/ContentEnricher.cs index 356bd8bbc..74d69825c 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Contents/Queries/ContentEnricher.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Contents/Queries/ContentEnricher.cs @@ -322,7 +322,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.Queries return EmptyAssets; } - var assets = await assetQuery.QueryAsync(context.Clone().WithNoAssetEnrichment(true), Q.Empty.WithIds(ids)); + var assets = await assetQuery.QueryAsync(context.Clone().WithNoAssetEnrichment(true), null, Q.Empty.WithIds(ids)); return assets.ToLookup(x => x.Id); } diff --git a/backend/src/Squidex.Domain.Apps.Entities/Contents/Queries/QueryExecutionContext.cs b/backend/src/Squidex.Domain.Apps.Entities/Contents/Queries/QueryExecutionContext.cs index 68aa7ec73..22e9398dd 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Contents/Queries/QueryExecutionContext.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Contents/Queries/QueryExecutionContext.cs @@ -75,7 +75,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.Queries public virtual async Task> QueryAssetsAsync(string query) { - var assets = await assetQuery.QueryAsync(context, Q.Empty.WithODataQuery(query)); + var assets = await assetQuery.QueryAsync(context, null, Q.Empty.WithODataQuery(query)); foreach (var asset in assets) { @@ -105,7 +105,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.Queries if (notLoadedAssets.Count > 0) { - var assets = await assetQuery.QueryAsync(context, Q.Empty.WithIds(notLoadedAssets)); + var assets = await assetQuery.QueryAsync(context, null, Q.Empty.WithIds(notLoadedAssets)); foreach (var asset in assets) { diff --git a/backend/src/Squidex.Domain.Apps.Entities/IEntity.cs b/backend/src/Squidex.Domain.Apps.Entities/IEntity.cs index 45ec90994..f58de4d60 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/IEntity.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/IEntity.cs @@ -12,7 +12,7 @@ namespace Squidex.Domain.Apps.Entities { public interface IEntity { - Guid Id { get; } + Guid Id { get; } Instant Created { get; } diff --git a/backend/src/Squidex.Domain.Apps.Events/Assets/AssetCreated.cs b/backend/src/Squidex.Domain.Apps.Events/Assets/AssetCreated.cs index ef7173832..7698c12b5 100644 --- a/backend/src/Squidex.Domain.Apps.Events/Assets/AssetCreated.cs +++ b/backend/src/Squidex.Domain.Apps.Events/Assets/AssetCreated.cs @@ -5,6 +5,7 @@ // All rights reserved. Licensed under the MIT license. // ========================================================================== +using System; using System.Collections.Generic; using Squidex.Infrastructure.EventSourcing; @@ -13,6 +14,8 @@ namespace Squidex.Domain.Apps.Events.Assets [EventType(nameof(AssetCreated))] public sealed class AssetCreated : AssetEvent { + public Guid ParentId { get; set; } + public string FileName { get; set; } public string FileHash { get; set; } diff --git a/backend/src/Squidex.Domain.Apps.Events/Assets/AssetFolderCreated.cs b/backend/src/Squidex.Domain.Apps.Events/Assets/AssetFolderCreated.cs new file mode 100644 index 000000000..228dc54dd --- /dev/null +++ b/backend/src/Squidex.Domain.Apps.Events/Assets/AssetFolderCreated.cs @@ -0,0 +1,20 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System; +using Squidex.Infrastructure.EventSourcing; + +namespace Squidex.Domain.Apps.Events.Assets +{ + [EventType(nameof(AssetFolderCreated))] + public sealed class AssetFolderCreated : AssetFolderEvent + { + public Guid ParentId { get; set; } + + public string FolderName { get; set; } + } +} diff --git a/backend/src/Squidex.Domain.Apps.Events/Assets/AssetFolderDeleted.cs b/backend/src/Squidex.Domain.Apps.Events/Assets/AssetFolderDeleted.cs new file mode 100644 index 000000000..ff6014106 --- /dev/null +++ b/backend/src/Squidex.Domain.Apps.Events/Assets/AssetFolderDeleted.cs @@ -0,0 +1,16 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschränkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using Squidex.Infrastructure.EventSourcing; + +namespace Squidex.Domain.Apps.Events.Assets +{ + [EventType(nameof(AssetFolderDeleted))] + public sealed class AssetFolderDeleted : AssetFolderEvent + { + } +} diff --git a/backend/src/Squidex.Domain.Apps.Events/Assets/AssetFolderEvent.cs b/backend/src/Squidex.Domain.Apps.Events/Assets/AssetFolderEvent.cs new file mode 100644 index 000000000..7fc7c5fa6 --- /dev/null +++ b/backend/src/Squidex.Domain.Apps.Events/Assets/AssetFolderEvent.cs @@ -0,0 +1,16 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschränkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System; + +namespace Squidex.Domain.Apps.Events.Assets +{ + public abstract class AssetFolderEvent : AppEvent + { + public Guid AssetFolderId { get; set; } + } +} diff --git a/backend/src/Squidex.Domain.Apps.Events/Assets/AssetFolderMoved.cs b/backend/src/Squidex.Domain.Apps.Events/Assets/AssetFolderMoved.cs new file mode 100644 index 000000000..c225ba6e5 --- /dev/null +++ b/backend/src/Squidex.Domain.Apps.Events/Assets/AssetFolderMoved.cs @@ -0,0 +1,18 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschränkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System; +using Squidex.Infrastructure.EventSourcing; + +namespace Squidex.Domain.Apps.Events.Assets +{ + [EventType(nameof(AssetFolderMoved))] + public sealed class AssetFolderMoved : AssetFolderEvent + { + public Guid ParentId { get; set; } + } +} diff --git a/backend/src/Squidex.Domain.Apps.Events/Assets/AssetFolderRenamed.cs b/backend/src/Squidex.Domain.Apps.Events/Assets/AssetFolderRenamed.cs new file mode 100644 index 000000000..8016098b1 --- /dev/null +++ b/backend/src/Squidex.Domain.Apps.Events/Assets/AssetFolderRenamed.cs @@ -0,0 +1,17 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using Squidex.Infrastructure.EventSourcing; + +namespace Squidex.Domain.Apps.Events.Assets +{ + [EventType(nameof(AssetFolderRenamed))] + public sealed class AssetFolderRenamed : AssetFolderEvent + { + public string FolderName { get; set; } + } +} diff --git a/backend/src/Squidex.Domain.Apps.Events/Assets/AssetMoved.cs b/backend/src/Squidex.Domain.Apps.Events/Assets/AssetMoved.cs new file mode 100644 index 000000000..9a1ab2cc9 --- /dev/null +++ b/backend/src/Squidex.Domain.Apps.Events/Assets/AssetMoved.cs @@ -0,0 +1,18 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschränkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System; +using Squidex.Infrastructure.EventSourcing; + +namespace Squidex.Domain.Apps.Events.Assets +{ + [EventType(nameof(AssetMoved))] + public sealed class AssetMoved : AssetEvent + { + public Guid ParentId { get; set; } + } +} diff --git a/backend/src/Squidex.Infrastructure/CollectionExtensions.cs b/backend/src/Squidex.Infrastructure/CollectionExtensions.cs index a714fdb38..5a459b8c5 100644 --- a/backend/src/Squidex.Infrastructure/CollectionExtensions.cs +++ b/backend/src/Squidex.Infrastructure/CollectionExtensions.cs @@ -33,6 +33,23 @@ namespace Squidex.Infrastructure return input.GroupBy(x => x).Where(x => x.Count() > 1).Select(x => x.Key); } + public static int IndexOf(this IEnumerable input, Func predicate) + { + var i = 0; + + foreach (var item in input) + { + if (predicate(item)) + { + return i; + } + + i++; + } + + return -1; + } + public static IEnumerable Duplicates(this IEnumerable input, Func selector) { return input.GroupBy(selector).Where(x => x.Count() > 1).Select(x => x.Key); diff --git a/backend/src/Squidex/Areas/Api/Controllers/Apps/AppsController.cs b/backend/src/Squidex/Areas/Api/Controllers/Apps/AppsController.cs index 4ce50f255..51e24437e 100644 --- a/backend/src/Squidex/Areas/Api/Controllers/Apps/AppsController.cs +++ b/backend/src/Squidex/Areas/Api/Controllers/Apps/AppsController.cs @@ -89,7 +89,7 @@ namespace Squidex.Areas.Api.Controllers.Apps /// /// Create a new app. /// - /// The app object that needs to be added to squidex. + /// The app object that needs to be added to Squidex. /// /// 201 => App created. /// 400 => App request not valid. diff --git a/backend/src/Squidex/Areas/Api/Controllers/Assets/AssetFoldersController.cs b/backend/src/Squidex/Areas/Api/Controllers/Assets/AssetFoldersController.cs new file mode 100644 index 000000000..5a2cf28cb --- /dev/null +++ b/backend/src/Squidex/Areas/Api/Controllers/Assets/AssetFoldersController.cs @@ -0,0 +1,168 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschränkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Mvc; +using Microsoft.Net.Http.Headers; +using Squidex.Areas.Api.Controllers.Assets.Models; +using Squidex.Domain.Apps.Entities.Assets; +using Squidex.Domain.Apps.Entities.Assets.Commands; +using Squidex.Infrastructure.Commands; +using Squidex.Shared; +using Squidex.Web; + +namespace Squidex.Areas.Api.Controllers.Assets +{ + /// + /// Uploads and retrieves assets. + /// + [ApiExplorerSettings(GroupName = nameof(Assets))] + public sealed class AssetFoldersController : ApiController + { + private readonly IAssetQueryService assetQuery; + + public AssetFoldersController(ICommandBus commandBus, IAssetQueryService assetQuery) + : base(commandBus) + { + this.assetQuery = assetQuery; + } + + /// + /// Get asset folders. + /// + /// The name of the app. + /// The optional parent folder id. + /// + /// 200 => Asset folders returned. + /// 404 => App not found. + /// + /// + /// Get all asset folders for the app. + /// + [HttpGet] + [Route("apps/{app}/assets/folders", Order = -1)] + [ProducesResponseType(typeof(AssetsDto), 200)] + [ApiPermission(Permissions.AppAssetsRead)] + [ApiCosts(1)] + public async Task GetAssetFolders(string app, [FromQuery] Guid parentId) + { + var assetFolders = await assetQuery.QueryAssetFoldersAsync(Context, parentId); + + var response = Deferred.Response(() => + { + return AssetFoldersDto.FromAssets(assetFolders, this, app); + }); + + Response.Headers[HeaderNames.ETag] = assetFolders.ToEtag(); + + return Ok(response); + } + + /// + /// Upload a new asset. + /// + /// The name of the app. + /// The asset folder object that needs to be added to the App. + /// + /// 201 => Asset folder created. + /// 404 => App not found. + /// + [HttpPost] + [Route("apps/{app}/assets/folders", Order = -1)] + [ProducesResponseType(typeof(AssetDto), 201)] + [AssetRequestSizeLimit] + [ApiPermission(Permissions.AppAssetsUpdate)] + [ApiCosts(1)] + public async Task PostAssetFolder(string app, [FromBody] CreateAssetFolderDto request) + { + var command = request.ToCommand(); + + var response = await InvokeCommandAsync(app, command); + + return Ok(response); + } + + /// + /// Updates the asset folder. + /// + /// The name of the app. + /// The id of the asset folder. + /// The asset folder object that needs to updated. + /// + /// 200 => Asset folder updated. + /// 400 => Asset folder name not valid. + /// 404 => Asset or app not found. + /// + [HttpPut] + [Route("apps/{app}/assets/folders/{id}/", Order = -1)] + [ProducesResponseType(typeof(AssetDto), 200)] + [AssetRequestSizeLimit] + [ApiPermission(Permissions.AppAssetsUpdate)] + [ApiCosts(1)] + public async Task PutAssetFolder(string app, Guid id, [FromBody] RenameAssetFolderDto request) + { + var command = request.ToCommand(id); + + var response = await InvokeCommandAsync(app, command); + + return Ok(response); + } + + /// + /// Moves the asset folder. + /// + /// The name of the app. + /// The id of the asset folder. + /// The asset folder object that needs to updated. + /// + /// 200 => Asset folder moved. + /// 404 => Asset or app not found. + /// + [HttpPut] + [Route("apps/{app}/assets/folders/{id}/parent", Order = -1)] + [ProducesResponseType(typeof(AssetDto), 200)] + [AssetRequestSizeLimit] + [ApiPermission(Permissions.AppAssetsUpdate)] + [ApiCosts(1)] + public async Task PutAssetFolderParent(string app, Guid id, [FromBody] MoveAssetItemDto request) + { + var command = request.ToFolderCommand(id); + + var response = await InvokeCommandAsync(app, command); + + return Ok(response); + } + + /// + /// Delete an asset folder. + /// + /// The name of the app. + /// The id of the asset folder to delete. + /// + /// 204 => Asset folder deleted. + /// 404 => Asset or app not found. + /// + [HttpDelete] + [Route("apps/{app}/assets/folders/{id}/", Order = -1)] + [ApiPermission(Permissions.AppAssetsUpdate)] + [ApiCosts(1)] + public async Task DeleteAssetFolder(string app, Guid id) + { + await CommandBus.PublishAsync(new DeleteAssetFolder { AssetFolderId = id }); + + return NoContent(); + } + + private async Task InvokeCommandAsync(string app, ICommand command) + { + var context = await CommandBus.PublishAsync(command); + + return AssetFolderDto.FromAssetFolder(context.Result(), this, app); + } + } +} diff --git a/backend/src/Squidex/Areas/Api/Controllers/Assets/AssetsController.cs b/backend/src/Squidex/Areas/Api/Controllers/Assets/AssetsController.cs index 7a7feca6e..4e4254bf9 100644 --- a/backend/src/Squidex/Areas/Api/Controllers/Assets/AssetsController.cs +++ b/backend/src/Squidex/Areas/Api/Controllers/Assets/AssetsController.cs @@ -89,6 +89,7 @@ namespace Squidex.Areas.Api.Controllers.Assets /// Get assets. /// /// The name of the app. + /// The optional parent folder id. /// The optional asset ids. /// The optional json query. /// @@ -103,9 +104,9 @@ namespace Squidex.Areas.Api.Controllers.Assets [ProducesResponseType(typeof(AssetsDto), 200)] [ApiPermission(Permissions.AppAssetsRead)] [ApiCosts(1)] - public async Task GetAssets(string app, [FromQuery] string? ids = null, [FromQuery] string? q = null) + public async Task GetAssets(string app, [FromQuery] Guid? parentId, [FromQuery] string? ids = null, [FromQuery] string? q = null) { - var assets = await assetQuery.QueryAsync(Context, + var assets = await assetQuery.QueryAsync(Context, parentId, Q.Empty .WithIds(ids) .WithJsonQuery(q) @@ -168,6 +169,7 @@ namespace Squidex.Areas.Api.Controllers.Assets /// Upload a new asset. /// /// The name of the app. + /// The optional parent folder id. /// The file to upload. /// /// 201 => Asset created. @@ -183,11 +185,11 @@ namespace Squidex.Areas.Api.Controllers.Assets [AssetRequestSizeLimit] [ApiPermission(Permissions.AppAssetsCreate)] [ApiCosts(1)] - public async Task PostAsset(string app, [OpenApiIgnore] List file) + public async Task PostAsset(string app, [FromQuery] Guid parentId, [OpenApiIgnore] List file) { var assetFile = await CheckAssetFileAsync(file); - var command = new CreateAsset { File = assetFile }; + var command = new CreateAsset { File = assetFile, ParentId = parentId }; var response = await InvokeCommandAsync(app, command); @@ -250,6 +252,31 @@ namespace Squidex.Areas.Api.Controllers.Assets return Ok(response); } + /// + /// Moves the asset. + /// + /// The name of the app. + /// The id of the asset. + /// The asset object that needs to updated. + /// + /// 200 => Asset moved. + /// 404 => Asset or app not found. + /// + [HttpPut] + [Route("apps/{app}/assets/{id}/parent")] + [ProducesResponseType(typeof(AssetDto), 200)] + [AssetRequestSizeLimit] + [ApiPermission(Permissions.AppAssetsUpdate)] + [ApiCosts(1)] + public async Task PutAssetParent(string app, Guid id, [FromBody] MoveAssetItemDto request) + { + var command = request.ToCommand(id); + + var response = await InvokeCommandAsync(app, command); + + return Ok(response); + } + /// /// Delete an asset. /// diff --git a/backend/src/Squidex/Areas/Api/Controllers/Assets/Models/AssetDto.cs b/backend/src/Squidex/Areas/Api/Controllers/Assets/Models/AssetDto.cs index cb3494226..b93767441 100644 --- a/backend/src/Squidex/Areas/Api/Controllers/Assets/Models/AssetDto.cs +++ b/backend/src/Squidex/Areas/Api/Controllers/Assets/Models/AssetDto.cs @@ -25,6 +25,11 @@ namespace Squidex.Areas.Api.Controllers.Assets.Models /// public Guid Id { get; set; } + /// + /// The id of the parent folder. Empty for files without parent. + /// + public Guid ParentId { get; set; } + /// /// The file name. /// @@ -141,6 +146,8 @@ namespace Squidex.Areas.Api.Controllers.Assets.Models { response.AddPutLink("update", controller.Url(x => nameof(x.PutAsset), values)); response.AddPutLink("upload", controller.Url(x => nameof(x.PutAssetContent), values)); + + response.AddPutLink("move", controller.Url(x => nameof(x.PutAssetParent), values)); } if (controller.HasPermission(Permissions.AppAssetsDelete)) diff --git a/backend/src/Squidex/Areas/Api/Controllers/Assets/Models/AssetFolderDto.cs b/backend/src/Squidex/Areas/Api/Controllers/Assets/Models/AssetFolderDto.cs new file mode 100644 index 000000000..810824973 --- /dev/null +++ b/backend/src/Squidex/Areas/Api/Controllers/Assets/Models/AssetFolderDto.cs @@ -0,0 +1,69 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschränkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System; +using System.ComponentModel.DataAnnotations; +using Newtonsoft.Json; +using Squidex.Domain.Apps.Entities.Assets; +using Squidex.Infrastructure.Reflection; +using Squidex.Shared; +using Squidex.Web; + +namespace Squidex.Areas.Api.Controllers.Assets.Models +{ + public sealed class AssetFolderDto : Resource + { + /// + /// The id of the asset. + /// + public Guid Id { get; set; } + + /// + /// The id of the parent folder. Empty for files without parent. + /// + public Guid ParentId { get; set; } + + /// + /// The folder name. + /// + [Required] + public string FolderName { get; set; } + + /// + /// The version of the asset folder. + /// + public long Version { get; set; } + + public static AssetFolderDto FromAssetFolder(IAssetFolderEntity asset, ApiController controller, string app) + { + var response = SimpleMapper.Map(asset, new AssetFolderDto()); + + return CreateLinks(response, controller, app); + } + + private static AssetFolderDto CreateLinks(AssetFolderDto response, ApiController controller, string app) + { + var values = new { app, id = response.Id }; + + response.AddSelfLink(controller.Url(x => nameof(x.GetAsset), values)); + + if (controller.HasPermission(Permissions.AppAssetsUpdate)) + { + response.AddPutLink("update", controller.Url(x => nameof(x.PutAssetFolder), values)); + + response.AddPutLink("move", controller.Url(x => nameof(x.PutAssetFolderParent), values)); + } + + if (controller.HasPermission(Permissions.AppAssetsUpdate)) + { + response.AddDeleteLink("delete", controller.Url(x => nameof(x.DeleteAssetFolder), values)); + } + + return response; + } + } +} diff --git a/backend/src/Squidex/Areas/Api/Controllers/Assets/Models/AssetFoldersDto.cs b/backend/src/Squidex/Areas/Api/Controllers/Assets/Models/AssetFoldersDto.cs new file mode 100644 index 000000000..264296dd4 --- /dev/null +++ b/backend/src/Squidex/Areas/Api/Controllers/Assets/Models/AssetFoldersDto.cs @@ -0,0 +1,55 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschränkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System.ComponentModel.DataAnnotations; +using System.Linq; +using Squidex.Domain.Apps.Entities.Assets; +using Squidex.Infrastructure; +using Squidex.Shared; +using Squidex.Web; + +namespace Squidex.Areas.Api.Controllers.Assets.Models +{ + public sealed class AssetFoldersDto : Resource + { + /// + /// The total number of assets. + /// + public long Total { get; set; } + + /// + /// The assets folders. + /// + [Required] + public AssetFolderDto[] Items { get; set; } + + public static AssetFoldersDto FromAssets(IResultList assetFolders, ApiController controller, string app) + { + var response = new AssetFoldersDto + { + Total = assetFolders.Total, + Items = assetFolders.Select(x => AssetFolderDto.FromAssetFolder(x, controller, app)).ToArray() + }; + + return CreateLinks(response, controller, app); + } + + private static AssetFoldersDto CreateLinks(AssetFoldersDto response, ApiController controller, string app) + { + var values = new { app }; + + response.AddSelfLink(controller.Url(x => nameof(x.GetAssetFolders), values)); + + if (controller.HasPermission(Permissions.AppAssetsUpdate)) + { + response.AddPostLink("create", controller.Url(x => nameof(x.PostAssetFolder), values)); + } + + return response; + } + } +} diff --git a/backend/src/Squidex/Areas/Api/Controllers/Assets/Models/CreateAssetFolderDto.cs b/backend/src/Squidex/Areas/Api/Controllers/Assets/Models/CreateAssetFolderDto.cs new file mode 100644 index 000000000..1b2154b43 --- /dev/null +++ b/backend/src/Squidex/Areas/Api/Controllers/Assets/Models/CreateAssetFolderDto.cs @@ -0,0 +1,33 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System; +using System.ComponentModel.DataAnnotations; +using Squidex.Domain.Apps.Entities.Assets.Commands; +using Squidex.Infrastructure.Reflection; + +namespace Squidex.Areas.Api.Controllers.Assets.Models +{ + public sealed class CreateAssetFolderDto + { + /// + /// The name of the folder. + /// + [Required] + public string FolderName { get; set; } + + /// + /// The id of the parent folder. + /// + public Guid ParentId { get; set; } + + public CreateAssetFolder ToCommand() + { + return SimpleMapper.Map(this, new CreateAssetFolder()); + } + } +} diff --git a/backend/src/Squidex/Areas/Api/Controllers/Assets/Models/MoveAssetItemDto.cs b/backend/src/Squidex/Areas/Api/Controllers/Assets/Models/MoveAssetItemDto.cs new file mode 100644 index 000000000..7053d9d53 --- /dev/null +++ b/backend/src/Squidex/Areas/Api/Controllers/Assets/Models/MoveAssetItemDto.cs @@ -0,0 +1,30 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System; +using Squidex.Domain.Apps.Entities.Assets.Commands; + +namespace Squidex.Areas.Api.Controllers.Assets.Models +{ + public sealed class MoveAssetItemDto + { + /// + /// The parent folder id. + /// + public Guid ParentId { get; set; } + + public MoveAsset ToCommand(Guid id) + { + return new MoveAsset { AssetId = id, ParentId = ParentId }; + } + + public MoveAssetFolder ToFolderCommand(Guid id) + { + return new MoveAssetFolder { AssetFolderId = id, ParentId = ParentId }; + } + } +} diff --git a/backend/src/Squidex/Areas/Api/Controllers/Assets/Models/RenameAssetFolderDto.cs b/backend/src/Squidex/Areas/Api/Controllers/Assets/Models/RenameAssetFolderDto.cs new file mode 100644 index 000000000..c0b4568e9 --- /dev/null +++ b/backend/src/Squidex/Areas/Api/Controllers/Assets/Models/RenameAssetFolderDto.cs @@ -0,0 +1,28 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System; +using System.ComponentModel.DataAnnotations; +using Squidex.Domain.Apps.Entities.Assets.Commands; +using Squidex.Infrastructure.Reflection; + +namespace Squidex.Areas.Api.Controllers.Assets.Models +{ + public sealed class RenameAssetFolderDto + { + /// + /// The name of the folder. + /// + [Required] + public string FolderName { get; set; } + + public RenameAssetFolder ToCommand(Guid id) + { + return SimpleMapper.Map(this, new RenameAssetFolder { AssetFolderId = id }); + } + } +} diff --git a/backend/src/Squidex/Areas/IdentityServer/Views/_Layout.cshtml b/backend/src/Squidex/Areas/IdentityServer/Views/_Layout.cshtml index 2760c2247..d28c046c3 100644 --- a/backend/src/Squidex/Areas/IdentityServer/Views/_Layout.cshtml +++ b/backend/src/Squidex/Areas/IdentityServer/Views/_Layout.cshtml @@ -19,7 +19,7 @@
- + @RenderBody()
diff --git a/backend/src/Squidex/Config/Domain/CommandsServices.cs b/backend/src/Squidex/Config/Domain/CommandsServices.cs index 36fc54e74..e93168e9a 100644 --- a/backend/src/Squidex/Config/Domain/CommandsServices.cs +++ b/backend/src/Squidex/Config/Domain/CommandsServices.cs @@ -13,6 +13,7 @@ using Squidex.Domain.Apps.Entities.Apps.Indexes; using Squidex.Domain.Apps.Entities.Apps.Invitation; using Squidex.Domain.Apps.Entities.Apps.Templates; using Squidex.Domain.Apps.Entities.Assets; +using Squidex.Domain.Apps.Entities.Assets.Commands; using Squidex.Domain.Apps.Entities.Comments; using Squidex.Domain.Apps.Entities.Comments.Commands; using Squidex.Domain.Apps.Entities.Contents; @@ -83,6 +84,9 @@ namespace Squidex.Config.Domain services.AddSingletonAs() .As(); + services.AddSingletonAs>() + .As(); + services.AddSingletonAs>() .As(); diff --git a/backend/src/Squidex/Config/Domain/StoreServices.cs b/backend/src/Squidex/Config/Domain/StoreServices.cs index c216fd390..783788626 100644 --- a/backend/src/Squidex/Config/Domain/StoreServices.cs +++ b/backend/src/Squidex/Config/Domain/StoreServices.cs @@ -100,6 +100,10 @@ namespace Squidex.Config.Domain .As() .As>(); + services.AddSingletonAs() + .As() + .As>(); + services.AddSingletonAs(c => new MongoContentRepository( c.GetRequiredService().GetDatabase(mongoContentDatabaseName), c.GetRequiredService(), diff --git a/backend/src/Squidex/wwwroot/images/add-app.png b/backend/src/Squidex/wwwroot/images/add-app.png deleted file mode 100644 index 605f0a4c489ab5e7ae700ac9c5f86286be1c89d8..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 5096 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 diff --git a/backend/src/Squidex/wwwroot/images/add-app.svg b/backend/src/Squidex/wwwroot/images/add-app.svg new file mode 100644 index 000000000..44b50c081 --- /dev/null +++ b/backend/src/Squidex/wwwroot/images/add-app.svg @@ -0,0 +1,59 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/backend/src/Squidex/wwwroot/images/add-blog.png b/backend/src/Squidex/wwwroot/images/add-blog.png deleted file mode 100644 index 764e65cf1accd1538f5fd880b957f8d91e3a74bb..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 5117 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 diff --git a/backend/src/Squidex/wwwroot/images/add-blog.svg b/backend/src/Squidex/wwwroot/images/add-blog.svg new file mode 100644 index 000000000..cecf13fa4 --- /dev/null +++ b/backend/src/Squidex/wwwroot/images/add-blog.svg @@ -0,0 +1,38 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/backend/src/Squidex/wwwroot/images/add-identity.png b/backend/src/Squidex/wwwroot/images/add-identity.png deleted file mode 100644 index 910ed1c77dd9e5d651432af9e609ae3b808ad323..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 5128 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 q3n@=x|En7}9__sK$M_f@X8b><<=_ + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/backend/src/Squidex/wwwroot/images/add-profile.png b/backend/src/Squidex/wwwroot/images/add-profile.png deleted file mode 100644 index 9aec8b7e1aade04804400c89b7d32de5287f56f0..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 4929 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 + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/backend/src/Squidex/wwwroot/images/asset_doc.png b/backend/src/Squidex/wwwroot/images/asset_doc.png deleted file mode 100644 index 08fb7de74e84392acd22a22827263480cf586e23..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 911 zcmeAS@N?(olHy`uVBq!ia0vp^DImII>x7lo!=05bR|ofDaMp5f4cARCAjm)rqLffP(RFF5(U(4-SU_W%F?fqEI5 z4=3$-z}kM?YWaRR%Z<#H+ta zmV-!ZVtUf1xj##oCT*A~Flz#X@%aau@Bh06P7f{>fADI>RBy}9e`h1>LKmE7n=2~% zcU4iG?!)iDK1B8J(3rg7&0T5Rr93GP`!Y4B2AR2dTe<^@m1!-h%yUD)nWnUV5N1xM)Me{7=F5TL8cUtz1jm)f0 zT!)X!Ys7G9&Vivf9Z`RHIt@5Teo`4bH9JtPwrRl;(GE_@#}NrD<1!>cedpi z=AD|^FKqd>a5nSJ?sapv-Z*vh4xfd<7axXu7p61UANqXsKBsuW5uZNAD4P>|XE3i# zsJzUc^zehi+}U~?wh1gt=Cze$(H6dU;n+ErKfE(0tSped=%y6od?e;X^Na0FKR14T zd?jhukArW&o$@k&6~%b|=;>q64E)m5*`n=I)LJI`DY?w&FKqe$MTh%4>;EhN-+H_h z@er+F;{VG2@8|E4KYuy>%MxEDZuqG@^5h4v&b0UY=V-33S@WzwFZ=2GbXV6+^CO!0 VxDuyNeG5!|44$rjF6*2UngATqz<&S$ diff --git a/backend/src/Squidex/wwwroot/images/asset_doc.svg b/backend/src/Squidex/wwwroot/images/asset_doc.svg new file mode 100644 index 000000000..3bbd3a67b --- /dev/null +++ b/backend/src/Squidex/wwwroot/images/asset_doc.svg @@ -0,0 +1,7 @@ + + + + + + + \ No newline at end of file diff --git a/backend/src/Squidex/wwwroot/images/asset_docx.png b/backend/src/Squidex/wwwroot/images/asset_docx.png deleted file mode 100644 index 5a31c92da66d505e4cd2242dcdfea3a75ebb2dce..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 958 zcmeAS@N?(olHy`uVBq!ia0vp^DImM$_1e*7lfx?5T15HXd0L? zwtFAe-G0CH`rFXWckjIak+tVh)VBMMt8V`N_y6LnZ%3bgDcJuwci*Fg9ru0K-*H)e zOMAi9_doueefjmk<4>FKepqnj&4jZrYmYt++;~@I?&a6ten0#2YsdYM(=NRJ{`>EZ zci&gvdKbFp`jX3Qe*!%zS`y?J%&_X1-IvF|bneft`%?YuYx%x`VpvSqoR-e$=^vH9mkd-)e6O)P(&!=`9IbxOC7k&EX}7a(y`&1K3IA14>j zMkXgNVJ9wQr!K2U7Q0_somW`0LV^E#opC%1m+pQ3o(#6Sr8_UEITWS{ELtQ`6n(7d zcG2B5(Tx6pnClFC+ZJ@KaVdbqPN~Q&8a&6Zn3_-9$U*BZ5iqP>kE{Y zAR`&UohScR?fZV*!Q-I|P88_0C8bneUQ}S>j>!r;gB8qos&xE%S&V9wL5__p*W-B& zB5o}}^h>5+=H6@(PrNLtL2 z(?rhl8^^A(?BSd-VWz>WMGdnIN)n3|xi8MF{*=D@)}jMjKc0(yX~uFv zr!{+C@ATg5&e8|2eUbYi;LRcH;M>6`e@T4x-OV~Z;(x;buUDQKtPm-)bp1#Dzx?l` z-+XO7{9B}2@bQOrdgqdM+5OmE^Jm_JqMo4e_;vMjmM%S6$1EHwe8TTif)p@~GI+ZB KxvX + + + + + + \ No newline at end of file diff --git a/backend/src/Squidex/wwwroot/images/asset_generic.png b/backend/src/Squidex/wwwroot/images/asset_generic.png deleted file mode 100644 index 576117903d0a9f521d9890f074099ddcd6b0349b..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 514 zcmeAS@N?(olHy`uVBq!ia0vp^DImHq)#1KI0$?1BjHKYD80i4zTAL7>#Dx9@=3 z7q42o_vp!$8@D`q^7Q`Wr&n&>5hz_W7ifoANswPK!>VI;UmpLcovnTU-Z=-x=NFcA z8t^f#+h;PFfq~K5)5S3))h=H%I@;Y zXiVL=By!r@*J7uGj+a(17wrG+l|HfB_n<^`jh9z=a>o*#CLnI^STaRHNi)8iOc=hT~&qJ9bpp{XBg-`{W2HXfQqmg0X@? + + + + + + \ No newline at end of file diff --git a/backend/src/Squidex/wwwroot/images/asset_pdf.png b/backend/src/Squidex/wwwroot/images/asset_pdf.png deleted file mode 100644 index 1aa38c9f151a00ea945a086d7ef14d2534f48bc5..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1156 zcmV-~1bh35P)%yB!_aAtBfyA=n`y*&!j=AtBizA=n@w-yk5^AtA;eAJ-uv*dQR=BqZ4! z9M~Nl(jFezAtCBQLjV8&w;UY99v=Vw{p^pA;7v{X;oMF)`XJE7>O}{O<1h<>mO;*!IfG^uE6Hxw-9_nCEtO{`&g; z@$vcF+w!uq?wp+Ki;L=ri0OcU=zM(UZ*S#lYT;8;-$X>-KtTTY_xtPX`{?NCd3on_ zbmV1a^~A*DT3Ya|tnjO=?xCUXpP%cDjN@Kjmn%zq0000LbW%=J0My%D`1V%Mi|n&- z5bo);ZyXyM5ovOR5C8xJE=fc|RA}Dq+Sd-#Fcg4c_TF&#Ax&Dk_n@2ZT{iFkHaJjP zArObg@m>F$sJZ#%xV9q2S1w)p+0#2$Z_595?RrGx)=AKjm)D~iBIh8Hlm~auLn0~n z??)wU&C@RuQdaM-;^dDT9}3!5dK&8zckoWgH@kVuMuSqjcb_D2fdtb()e0=R{Ggkhgfb zCVUe`u|l>Gc#s!Gu|hH)rk=9HSR#_d)D%UrL?)Qpq9~R~r2x!CQ7jQD5zf1!*dkkC zCyHW=41ju7Q7n;E4bxZ@#SWPhwytQ36_S8@7lxwr$Og$mq77b7L9SXLc@J`x4P;a9 zkTe7$Q()o_N&ehZ-a=tvgJ_%ZJiPXxQFVljVIyHXVCV?R!sp|{_hwFz9N-^z;{aB^ z3#1K=BUpe83$pK9*)Eh~L6#ou#UE}sRBvJ`RgIg3&#@pwSS9+ufYLNCGBPgN-Q+#+Ar7R{ zJ3gW0SO3@SA8)oK2Bg~NXT@}0PG#O6?w0M5)Gi=w{bupU3N00D#ZgtPk$r=(;#PTs zut1jYG5!4ix + + + + \ No newline at end of file diff --git a/backend/src/Squidex/wwwroot/images/asset_ppt.png b/backend/src/Squidex/wwwroot/images/asset_ppt.png deleted file mode 100644 index c2c7b7e627c3804dbddd4e570b9792594ac23a0c..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1034 zcmeAS@N?(olHy`uVBq!ia0vp^DImZCqR@8* zK|e1X`?_|{*EO@=*ChVBasJ2QZJ#FB{{Qp)_{_O)fZh-;3GxeOxNG(3rH&5 zUpr;9vhfiwOGOZ2=CWo=vGWnHl@dw;GBA`g!6d7V|4RR?ID^hbjv~FM1vSJxcKi1> z&i|)XC+@PhElo(HjpJ(`cgvogY>i1hG3SEU&+~hJIxe=^!O-ncrv0)jv-ONGDF&RG zzWPPxPLFyTBjj)2C+ zrZ^Fi$DzAxzUNNZ@qbB-sMqp?JyL?ppcfv`Rm@#fBwoa zy}B!=DJHxzLF~-OJIOi@OacK33uZEShI9r=yx!`R%V-podMb_2L`-34lGnvFzA4-j zmT3hVuzzCdI6mQ(44eCt(?`$i?W*40dw-7hHID|BUzShYEi~pgi*w{_8|->*S#fpR z2ey;XAGE4CW&!24|H?V>`~k1ApcCh_hJ)v)@tm}J^JbIPK7oatvztv7HJy^*R91dh zaeuVo+bfyGoc;yBR=d5Qb&sjCbghj*&zJ{-4q~7K+xstzlnVH?#g|v_E?fJH_mt>^+aD)7y8&|pgQu&X%Q~loCIB@;_DBE# diff --git a/backend/src/Squidex/wwwroot/images/asset_ppt.svg b/backend/src/Squidex/wwwroot/images/asset_ppt.svg new file mode 100644 index 000000000..53ab946db --- /dev/null +++ b/backend/src/Squidex/wwwroot/images/asset_ppt.svg @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/backend/src/Squidex/wwwroot/images/asset_pptx.png b/backend/src/Squidex/wwwroot/images/asset_pptx.png deleted file mode 100644 index 1d48321115359ea7708725c3e7a541808d8ec33b..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1058 zcmV+-1l{|IP)|#OfV?pg=LG5Be@MAvhVnXa-L)lR=?PEagWI*g< zLI3~%^?6yxLL=>GM*HE~^rMjNY);foE%KIt?{`}Ew5087OZ?;A?QT!|+|~Kc#Q**L z``OX=x~%=}>HOv3^{=1xsFnTe=JcS9{_^nq-q`ue!1b(~_{Y2Vzp?hUrt_MG?r>86 z_x1VE#{KZ^@{)S-jdkvFRrk58@Pumbd|~j0Z1kdz+1{H*0000IbW%=J0My%D{P^}( z?a#D}5O5mlZydt}%qIW<0{=-wK~!ko?b_>Z+CUJ7;nK7@H*IHHgKKPX;sh{f%sC{v z|GTO=Wda|H>z%CBAG}g5<=3OxT|fqZI(BT`&#S^I{ac?;42?4pvXL(*W(H9X0h zeVz|7Qod0>#7Mc!i5MvtIT0hJV1S5X2@%C2B8o*s6ceN#SK3o*q*pJA<5nE^e4nV? z4ge7)H{v&_+;Z#P^g1W1m5H3%f}$BrB8m}0wWk!J_F|3DqSC!5KnxT?9-r$)s`sC} zHD`lRzvTd8q6iZ98v)hZ%j*VlE1?GnC^?raZMj^uMS4j&9s&Z23h~B7r7<9&6qKBZ z+u}V144$~O*Y|wLfEkosBdtz*6>=fLh(Q5^TaF58Fr%_(q%~DY$h*pTscc^#)e3Kl zG{}(O^thcx=Gb-@C=gEwSuruBnHeHy3uh^9)=KgfNQWhv<#i6Jqd+2NoP}QJkRklLw|(5!*gJs@@++GZs)+N$~Q=y0}Dxm zlwgj7y(mAla1Lw~s1O>+9Ae + + + + + \ No newline at end of file diff --git a/backend/src/Squidex/wwwroot/images/asset_video.png b/backend/src/Squidex/wwwroot/images/asset_video.png deleted file mode 100644 index 0889275a0f64cdd96885fc139793a73c8ec661bf..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 723 zcmeAS@N?(olHy`uVBq!ia0vp^DIm^~xuVi<>V}EO zt{!>#;@-P&pMET^+w<=E=f^i*_b08}yXgFx?YGwU9$D3SXnOAE2`THlV^)89`|ZQa zFSpM=y|n+{@r~E#SL~dUxv@84&Fe=W?w)^kV$=0w>#rVIesSxxle3Gr-M{=|=bSUY zfBq>d_}2h*u}DdfUogWeyDyKA{eD=hGn?_txeJFnU9I_c*Ut@SU|>Ap>EalYaqsP| z<9UZ1L|Pw)Sglk~wK>*s(Cla{U*&)C?ui}e)Pjn1^Oksg)=yhb9)PR`k6+5 zm8!e`H`H{|-31?#w_m?Hg-yXBPD}IbES{B1wkSAjX_hJmd9FMuxRXiI$yIRSqK*Jd z^B{qRL5zmQ3=?~QO&0l_ka(F zycBSG)_!@1nq|Y=IUNy-O$8E9T*@Z@X{k`{eLAfpMsd + + + + \ No newline at end of file diff --git a/backend/src/Squidex/wwwroot/images/asset_xls.png b/backend/src/Squidex/wwwroot/images/asset_xls.png deleted file mode 100644 index 3e7849d3e32e87bcfc92625cccccfeff895b9940..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 862 zcmV-k1EKthP)Kb2-blx9AXV?UKDt}Num1aMd zW9mR*Rt~{}0006xNklrUH15Qbq=S_qWV)41JjZ6`DdiA~N& z&b|M;VzajL3B&@k&Ilp){Sn1~{Ae_?WXZb|3KjkQQrn}K^mARgkSd}ktFN}Z4Jl^PR)B8WhdM4(hhG8Y1ckLas5ks8tBhD@tLez9;M zQ23L~n%R6Xr^HxvGt{L2I}j**gvN`r78OX_Xvl#;VIVrcq7u=|^L#mTB2d`K?Qqbs zh!tli=b00M!bGxsHoepZ($hrL=(!OnJY+d08jUH5udi2=eASId^) z0;MwYuNg(%jd5m-#;HmT0;K{1qflV1x1SJ{3J8o+fnneMp;SaV4RhPe)cj2BKCDsb zL{NMY%n|F>TIpgA1jPrz9EmO;z~S2H5IGe-f;q@!XTcl@iXQ^QDwxBe_#iM!1%^Xm zBQQ2fvoPEW7s1XEE*#1?E;3E6!K%hX><(g0(g^i6$sq_{Z0n06Dzd471mw*lJ!R_& zBKQ}O*FdmjK?FApNI(J-kbp3H*GPRkNc~CVXgkOe|D!*@1NQL+eqRsS2Qm2VbB~np o+k@fk7>8qggxZheolwYj2UMKw9}b!8!2kdN07*qoM6N<$f_S@@AOHXW diff --git a/backend/src/Squidex/wwwroot/images/asset_xls.svg b/backend/src/Squidex/wwwroot/images/asset_xls.svg new file mode 100644 index 000000000..9a08f88b1 --- /dev/null +++ b/backend/src/Squidex/wwwroot/images/asset_xls.svg @@ -0,0 +1,10 @@ + + + + + + + + + + \ No newline at end of file diff --git a/backend/src/Squidex/wwwroot/images/asset_xlsx.png b/backend/src/Squidex/wwwroot/images/asset_xlsx.png deleted file mode 100644 index d28ad0789f465cfacb036118301aebd71562b274..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 905 zcmV;419tq0P)Kb2-bm1aMbWDt}Num1aMd zWayV}aP*v6mAvz5iGbhD9Q zr+`tPbn^K1#^zY5%*Rj;WtSV}_U+Gb5VMRL_vy5X9RHyNT|9 z8VrOGU*(2y=IZzVP+i-okEr9eJI<(zvi}#BU-_qDWC_W8V6$a?KCir|f9uU5(>SUU z`Wy20kY^AXAYWL@>9HBYQclPWVJROC5tdRjL|9725P>3yK#@eCltY;f-K8iHjsk?H31IRuJ1;)N^PA>P9xyr1ivhee!27z&VJuxzAAk~?oLB8tM{V-7J_fW+avJ@hif z_w=Rd(|F@w`TGb*0dhYj8cj*5^?K6lPkL%Z&WdQW+D8})5V!3SMI8zp$I;hKZ%Z^w z*58f_5GVp~d`^)>phzN6BoQc*2oy;KN_hl*`}%b6${=|~5`iL#K#@eCNFq=q5h!9v zJZI03)yP5X?ckWEL0*3KM~`Q(zz{ECj|*fl;6sBQSm`OBhcprU-V9 z02cJY6zOzR!>EcOlI%daB#qG4BS9ozpwa?S#Hpt_7O}{SM|>yE7esJpk^g~U$$|(T zXc3E8#3B|k=v5=N=RsQ*6lrXEpDD2|AQY|4= fK3yExY-v9MTlDUH3492T00000NkvXXu0mjf^ggH+ diff --git a/backend/src/Squidex/wwwroot/images/asset_xlsx.svg b/backend/src/Squidex/wwwroot/images/asset_xlsx.svg new file mode 100644 index 000000000..9a08f88b1 --- /dev/null +++ b/backend/src/Squidex/wwwroot/images/asset_xlsx.svg @@ -0,0 +1,10 @@ + + + + + + + + + + \ No newline at end of file diff --git a/backend/src/Squidex/wwwroot/images/dashboard-api.png b/backend/src/Squidex/wwwroot/images/dashboard-api.png deleted file mode 100644 index a269ef52bac5b5180e27824eea50bf37397a97c4..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 2335 zcmai$dpHvg7sr>-CPkLq&9%1+xm7ONZ)|R}5V>TAFlIx!j8PJ`Ar+(`x9;BU> z%Qn$}5-7f1O&(f`Y(q2(j&ucXM?4Uly4{1q?L4CZ05IuK?qCsoY1^U#(fSY{9p}%i*pbDqXO-agIaGk6G$| zI9Wt@Ad{?UZ`b)WNli>fJXb`5mLhTWjCj~hN}q{}vaPKG&xBq58CLyhD8fwlNJctK zT6OT+hqM{zd+0alhe-#u)$@5lB*FL;WTahG1o^p(eO_xYfh9= z+n^Tnh?<=I{C8Y6q}9&CC{p2$hX^%a;j8o47tUY~S8pfO755+I(1! zFT`9_AKtL&yCXp%`&+E3r@J#0C9$DDd3C(c!50@ZBUvhoIy^W!ItWz-Qq~N!F=UIL zw}|(>A`dnQM}IVh?zE7In0U8@I5C*(wx;(1HZwy%c6cQRbo2LgPL9EPhXex4nM1S@ zLG6!p>DJnbH&)i$hT!^5=dmX5yMrCx=&T%XKIM_3%lQk`ROc1;8vAush7cx}XHShyT=_nt znm*D!znbP5+>WZR5vOsi_lC}B7`Uv=$cx- z*81Zqm%CS=|2W?8l@p-~Z&zm=BS@Au0I4iIb=c*a==PDn=xti07u1!kXaLe35Zoy1 zl1(>U)-Apa9_l`0X9`dDTB`q!khTz%*40KwPdX*>@87%cc*!yezuKwQXcZx&YSo5$ zylA{8&E}4s9+XRq-@~%ig#;%8^xbPA5QKoQg|Kh zf>$fs193$A+&9?B)ghPUn&k&G1^|cbpB^}R#|ATNrnA;1!`9L%c6j+8toWo14<-qE zWw8>sB)ut=_F*G1Lp|?@`BG19|{~uOP zRUMD$xYoErJHxbS=EghOL5;I65L(lWSy0W&E5mGos;rxvd|n~NusfZWN*k4c(*617 zV)x5F`cl$LVTi@&q`AsT6P3M8{Wy&b2F{>ZZ6R#<`ZXVTPEO9~^}h>>uK%_vIVuzr zVHGB1XwE5)Fp26?T$p@@x1xMIdGF__nBC_ft;f0{J$I49iF#q0vcVvKDV+a%SFD-Y z09>89wUz*9V4dSUF^fpq3(}0T5`v!zHJ3A8 zmU&E=bA&+D1{-*=jERjz)FFURPGluqT0EfY>2gP(y2B82tO>d)j|!0kOh^%)o#STK zua#(*;W;2nRwY9oF4xA%dw~Fy+HyLx?ui*NjKi69{7!|*jg)oA&FcYz6ZhPO7A-MX z-bS-dIo}@e`%zt5uHlwgC3J8MMltio#*|OiZXJQeZFmecOn|4`cReIP>?uG@F+~G@ASU?pus-;k`C)ju$2kufMQrPt&WKxos>J!H?H0 zEUdcnBJI=M)Yvbn;INRL7B6ROysJgbhZ`{VAQg4ZSDnk{bRA!GeyE4I)uH`gNOc=j zT+!(53Vh<2E83Gjx3csqMI+GVAW<^N)h%$fH!yxZKmV5bjo50O0&3pmssD(-jqLPJ zH7&R7?Cdu*y=|@J(6y{DhpDJGr+(#tF;qGVA&2YN%>VdcDgDXz5zE6v4yk5Fm!GY~ zC|wn9aYygpcr!mxse#`&0Jb(MPXzT)+BWiru&>@ck*A_iHts zmV(euKNr_+P_2D28U9;H=L#-+0@o|4@d0Swi~N*Iq_wG-evs>qK%s+uauI?8hNmhq zhyAwQ>d7ZTsNjF2^i%O?9y4>i9a}0@Aq6Ru$0@<4?W0g5n~YL9SGS_J2h|Ij^MfT& zjh`T5-EtFMYFwqd$I3oGZ8FbBP#M{ZcVb-!__Ss*x~!&^2Z66p=_-dvzAsC zQK7ExXuP)KY=03~p_Me$oPF1uI-Mun>g=p|I~!{v+VYv0*Kix+)1S#(h + + + + + + + + + + + \ No newline at end of file diff --git a/backend/src/Squidex/wwwroot/images/dashboard-feedback.png b/backend/src/Squidex/wwwroot/images/dashboard-feedback.png deleted file mode 100644 index 5665d765d30d43920abb376e26f850c9ff1b6927..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 4188 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 diff --git a/backend/src/Squidex/wwwroot/images/dashboard-github.svg b/backend/src/Squidex/wwwroot/images/dashboard-github.svg new file mode 100644 index 000000000..8e0c9e334 --- /dev/null +++ b/backend/src/Squidex/wwwroot/images/dashboard-github.svg @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/backend/src/Squidex/wwwroot/images/dashboard-schema.png b/backend/src/Squidex/wwwroot/images/dashboard-schema.png deleted file mode 100644 index 77162cbb472ea83d6b432821bd3d28d7d89530ae..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 3268 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 + + + + + + + + + \ No newline at end of file diff --git a/backend/src/Squidex/wwwroot/images/dashboard_feedback.svg b/backend/src/Squidex/wwwroot/images/dashboard_feedback.svg new file mode 100644 index 000000000..920b7fb2d --- /dev/null +++ b/backend/src/Squidex/wwwroot/images/dashboard_feedback.svg @@ -0,0 +1,16 @@ + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/backend/src/Squidex/wwwroot/images/dashboard_schema.svg b/backend/src/Squidex/wwwroot/images/dashboard_schema.svg new file mode 100644 index 000000000..3bd7aaff7 --- /dev/null +++ b/backend/src/Squidex/wwwroot/images/dashboard_schema.svg @@ -0,0 +1,10 @@ + + + + + + + + + + \ No newline at end of file diff --git a/backend/src/Squidex/wwwroot/images/folder.svg b/backend/src/Squidex/wwwroot/images/folder.svg new file mode 100644 index 000000000..3ad7e240d --- /dev/null +++ b/backend/src/Squidex/wwwroot/images/folder.svg @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/backend/src/Squidex/wwwroot/images/logo.svg b/backend/src/Squidex/wwwroot/images/logo.svg new file mode 100644 index 000000000..c5f97d149 --- /dev/null +++ b/backend/src/Squidex/wwwroot/images/logo.svg @@ -0,0 +1,8 @@ + + + + + + + + \ No newline at end of file diff --git a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Assets/AssetChangedTriggerHandlerTests.cs b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Assets/AssetChangedTriggerHandlerTests.cs index ba32701fc..17a119beb 100644 --- a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Assets/AssetChangedTriggerHandlerTests.cs +++ b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Assets/AssetChangedTriggerHandlerTests.cs @@ -62,6 +62,16 @@ namespace Squidex.Domain.Apps.Entities.Assets Assert.Equal(type, result!.Type); } + [Fact] + public async Task Should_skip_moved_event() + { + var envelope = Envelope.Create(new AssetMoved()); + + var result = await sut.CreateEnrichedEventAsync(envelope); + + Assert.Null(result); + } + [Fact] public void Should_not_trigger_precheck_when_event_type_not_correct() { diff --git a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Assets/AssetCommandMiddlewareTests.cs b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Assets/AssetCommandMiddlewareTests.cs index efa873db6..38caa4e0e 100644 --- a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Assets/AssetCommandMiddlewareTests.cs +++ b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Assets/AssetCommandMiddlewareTests.cs @@ -58,7 +58,7 @@ namespace Squidex.Domain.Apps.Entities.Assets { file = new AssetFile("my-image.png", "image/png", 1024, () => stream); - asset = new AssetGrain(Store, tagService, A.Fake(), A.Dummy()); + asset = new AssetGrain(Store, tagService, assetQuery, A.Fake(), A.Dummy()); asset.ActivateAsync(Id).Wait(); A.CallTo(() => contextProvider.Context) diff --git a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Assets/AssetFolderGrainTests.cs b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Assets/AssetFolderGrainTests.cs new file mode 100644 index 000000000..0d44399d7 --- /dev/null +++ b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Assets/AssetFolderGrainTests.cs @@ -0,0 +1,171 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschränkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System; +using System.Collections.Generic; +using System.Threading.Tasks; +using FakeItEasy; +using Squidex.Domain.Apps.Entities.Assets.Commands; +using Squidex.Domain.Apps.Entities.Assets.State; +using Squidex.Domain.Apps.Entities.TestHelpers; +using Squidex.Domain.Apps.Events.Assets; +using Squidex.Infrastructure; +using Squidex.Infrastructure.Commands; +using Squidex.Infrastructure.Log; +using Squidex.Infrastructure.Orleans; +using Xunit; + +namespace Squidex.Domain.Apps.Entities.Assets +{ + public class AssetFolderGrainTests : HandlerTestBase + { + private readonly IAssetQueryService assetQuery = A.Fake(); + private readonly IActivationLimit limit = A.Fake(); + private readonly Guid parentId = Guid.NewGuid(); + private readonly Guid assetFolderId = Guid.NewGuid(); + private readonly AssetFolderGrain sut; + + protected override Guid Id + { + get { return assetFolderId; } + } + + public AssetFolderGrainTests() + { + A.CallTo(() => assetQuery.FindAssetFolderAsync(parentId)) + .Returns(new List { A.Fake() }); + + sut = new AssetFolderGrain(Store, assetQuery, limit, A.Dummy()); + sut.ActivateAsync(Id).Wait(); + } + + [Fact] + public void Should_set_limit() + { + A.CallTo(() => limit.SetLimit(5000, TimeSpan.FromMinutes(5))) + .MustHaveHappened(); + } + + [Fact] + public async Task Command_should_throw_exception_if_rule_is_deleted() + { + await ExecuteCreateAsync(); + await ExecuteDeleteAsync(); + + await Assert.ThrowsAsync(ExecuteUpdateAsync); + } + + [Fact] + public async Task Create_should_create_events_and_update_state() + { + var command = new CreateAssetFolder { FolderName = "New Name" }; + + var result = await sut.ExecuteAsync(CreateAssetFolderCommand(command)); + + result.ShouldBeEquivalent(sut.Snapshot); + + Assert.Equal(command.FolderName, sut.Snapshot.FolderName); + + LastEvents + .ShouldHaveSameEvents( + CreateAssetFolderEvent(new AssetFolderCreated + { + FolderName = command.FolderName + }) + ); + } + + [Fact] + public async Task Update_should_create_events_and_update_state() + { + var command = new RenameAssetFolder { FolderName = "New Name" }; + + await ExecuteCreateAsync(); + + var result = await sut.ExecuteAsync(CreateAssetFolderCommand(command)); + + result.ShouldBeEquivalent(sut.Snapshot); + + Assert.Equal(command.FolderName, sut.Snapshot.FolderName); + + LastEvents + .ShouldHaveSameEvents( + CreateAssetFolderEvent(new AssetFolderRenamed + { + FolderName = command.FolderName + }) + ); + } + + [Fact] + public async Task Move_should_create_events_and_update_state() + { + var command = new MoveAssetFolder { ParentId = parentId }; + + await ExecuteCreateAsync(); + + var result = await sut.ExecuteAsync(CreateAssetFolderCommand(command)); + + result.ShouldBeEquivalent(sut.Snapshot); + + Assert.Equal(parentId, sut.Snapshot.ParentId); + + LastEvents + .ShouldHaveSameEvents( + CreateAssetFolderEvent(new AssetFolderMoved { ParentId = parentId }) + ); + } + + [Fact] + public async Task Delete_should_create_events_with_total_file_size() + { + var command = new DeleteAssetFolder(); + + await ExecuteCreateAsync(); + + var result = await sut.ExecuteAsync(CreateAssetFolderCommand(command)); + + result.ShouldBeEquivalent(new EntitySavedResult(1)); + + Assert.True(sut.Snapshot.IsDeleted); + + LastEvents + .ShouldHaveSameEvents( + CreateAssetFolderEvent(new AssetFolderDeleted()) + ); + } + + private Task ExecuteCreateAsync() + { + return sut.ExecuteAsync(CreateAssetFolderCommand(new CreateAssetFolder { FolderName = "My Folder" })); + } + + private Task ExecuteUpdateAsync() + { + return sut.ExecuteAsync(CreateAssetFolderCommand(new RenameAssetFolder { FolderName = "My Folder" })); + } + + private Task ExecuteDeleteAsync() + { + return sut.ExecuteAsync(CreateAssetFolderCommand(new DeleteAssetFolder())); + } + + protected T CreateAssetFolderEvent(T @event) where T : AssetFolderEvent + { + @event.AssetFolderId = assetFolderId; + + return CreateEvent(@event); + } + + protected T CreateAssetFolderCommand(T command) where T : AssetFolderCommand + { + command.AssetFolderId = assetFolderId; + + return CreateCommand(command); + } + } +} diff --git a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Assets/AssetGrainTests.cs b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Assets/AssetGrainTests.cs index 301aada09..23dc31f52 100644 --- a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Assets/AssetGrainTests.cs +++ b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Assets/AssetGrainTests.cs @@ -27,11 +27,12 @@ namespace Squidex.Domain.Apps.Entities.Assets public class AssetGrainTests : HandlerTestBase { private readonly ITagService tagService = A.Fake(); + private readonly IAssetQueryService assetQuery = A.Fake(); private readonly IActivationLimit limit = A.Fake(); private readonly ImageInfo image = new ImageInfo(2048, 2048); - private readonly AssetFile file = new AssetFile("my-image.png", "image/png", 1024, () => new MemoryStream()); + private readonly Guid parentId = Guid.NewGuid(); private readonly Guid assetId = Guid.NewGuid(); - private readonly string fileHash = Guid.NewGuid().ToString(); + private readonly AssetFile file = new AssetFile("my-image.png", "image/png", 1024, () => new MemoryStream()); private readonly AssetGrain sut; protected override Guid Id @@ -41,10 +42,13 @@ namespace Squidex.Domain.Apps.Entities.Assets public AssetGrainTests() { + A.CallTo(() => assetQuery.FindAssetFolderAsync(parentId)) + .Returns(new List { A.Fake() }); + A.CallTo(() => tagService.NormalizeTagsAsync(AppId, TagGroups.Assets, A>.Ignored, A>.Ignored)) .Returns(new Dictionary()); - sut = new AssetGrain(Store, tagService, limit, A.Dummy()); + sut = new AssetGrain(Store, tagService, assetQuery, limit, A.Dummy()); sut.ActivateAsync(Id).Wait(); } @@ -56,7 +60,7 @@ namespace Squidex.Domain.Apps.Entities.Assets } [Fact] - public async Task Command_should_throw_exception_if_rule_is_deleted() + public async Task Command_should_throw_exception_if_asset_is_deleted() { await ExecuteCreateAsync(); await ExecuteDeleteAsync(); @@ -65,16 +69,16 @@ namespace Squidex.Domain.Apps.Entities.Assets } [Fact] - public async Task Create_should_create_events() + public async Task Create_should_create_events_and_update_state() { - var command = new CreateAsset { File = file, ImageInfo = image, FileHash = fileHash, Tags = new HashSet() }; + var command = new CreateAsset { File = file, ImageInfo = image, FileHash = "NewHash", Tags = new HashSet() }; var result = await sut.ExecuteAsync(CreateAssetCommand(command)); result.ShouldBeEquivalent(sut.Snapshot); Assert.Equal(0, sut.Snapshot.FileVersion); - Assert.Equal(fileHash, sut.Snapshot.FileHash); + Assert.Equal(command.FileHash, sut.Snapshot.FileHash); LastEvents .ShouldHaveSameEvents( @@ -82,8 +86,8 @@ namespace Squidex.Domain.Apps.Entities.Assets { IsImage = true, FileName = file.FileName, - FileHash = fileHash, FileSize = file.FileSize, + FileHash = command.FileHash, FileVersion = 0, MimeType = file.MimeType, PixelWidth = image.PixelWidth, @@ -95,9 +99,9 @@ namespace Squidex.Domain.Apps.Entities.Assets } [Fact] - public async Task Update_should_create_events() + public async Task Update_should_create_events_and_update_state() { - var command = new UpdateAsset { File = file, ImageInfo = image, FileHash = fileHash }; + var command = new UpdateAsset { File = file, ImageInfo = image, FileHash = "NewHash" }; await ExecuteCreateAsync(); @@ -106,7 +110,7 @@ namespace Squidex.Domain.Apps.Entities.Assets result.ShouldBeEquivalent(sut.Snapshot); Assert.Equal(1, sut.Snapshot.FileVersion); - Assert.Equal(fileHash, sut.Snapshot.FileHash); + Assert.Equal(command.FileHash, sut.Snapshot.FileHash); LastEvents .ShouldHaveSameEvents( @@ -114,7 +118,7 @@ namespace Squidex.Domain.Apps.Entities.Assets { IsImage = true, FileSize = file.FileSize, - FileHash = fileHash, + FileHash = command.FileHash, FileVersion = 1, MimeType = file.MimeType, PixelWidth = image.PixelWidth, @@ -124,7 +128,7 @@ namespace Squidex.Domain.Apps.Entities.Assets } [Fact] - public async Task AnnotateName_should_create_events() + public async Task AnnotateName_should_create_events_and_update_state() { var command = new AnnotateAsset { FileName = "My New Image.png" }; @@ -143,7 +147,7 @@ namespace Squidex.Domain.Apps.Entities.Assets } [Fact] - public async Task AnnotateSlug_should_create_events() + public async Task AnnotateSlug_should_create_events_and_update_state() { var command = new AnnotateAsset { Slug = "my-new-image.png" }; @@ -162,7 +166,7 @@ namespace Squidex.Domain.Apps.Entities.Assets } [Fact] - public async Task AnnotateTag_should_create_events() + public async Task AnnotateTag_should_create_events_and_update_state() { var command = new AnnotateAsset { Tags = new HashSet() }; @@ -178,6 +182,25 @@ namespace Squidex.Domain.Apps.Entities.Assets ); } + [Fact] + public async Task Move_should_create_events_and_update_state() + { + var command = new MoveAsset { ParentId = parentId }; + + await ExecuteCreateAsync(); + + var result = await sut.ExecuteAsync(CreateAssetCommand(command)); + + result.ShouldBeEquivalent(sut.Snapshot); + + Assert.Equal(parentId, sut.Snapshot.ParentId); + + LastEvents + .ShouldHaveSameEvents( + CreateAssetEvent(new AssetMoved { ParentId = parentId }) + ); + } + [Fact] public async Task Delete_should_create_events_with_total_file_size() { diff --git a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Assets/Guards/GuardAssetFolderTests.cs b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Assets/Guards/GuardAssetFolderTests.cs new file mode 100644 index 000000000..37f08f6f5 --- /dev/null +++ b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Assets/Guards/GuardAssetFolderTests.cs @@ -0,0 +1,167 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschränkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System; +using System.Collections.Generic; +using System.Threading.Tasks; +using FakeItEasy; +using Squidex.Domain.Apps.Entities.Assets.Commands; +using Squidex.Domain.Apps.Entities.TestHelpers; +using Squidex.Infrastructure.Validation; +using Xunit; + +namespace Squidex.Domain.Apps.Entities.Assets.Guards +{ + public class GuardAssetFolderTests + { + private readonly IAssetQueryService assetQuery = A.Fake(); + + [Fact] + public async Task CanCreate_should_throw_exception_when_folder_name_not_defined() + { + var command = new CreateAssetFolder(); + + A.CallTo(() => assetQuery.FindAssetFolderAsync(command.ParentId)) + .Returns(new List()); + + await ValidationAssert.ThrowsAsync(() => GuardAssetFolder.CanCreate(command, assetQuery), + new ValidationError("Folder name is required.", "FolderName")); + } + + [Fact] + public async Task CanCreate_should_throw_exception_when_folder_not_found() + { + var command = new CreateAssetFolder { FolderName = "My Folder", ParentId = Guid.NewGuid() }; + + A.CallTo(() => assetQuery.FindAssetFolderAsync(command.ParentId)) + .Returns(new List()); + + await ValidationAssert.ThrowsAsync(() => GuardAssetFolder.CanCreate(command, assetQuery), + new ValidationError("Asset folder does not exist.", "ParentId")); + } + + [Fact] + public async Task CanCreate_should_not_throw_exception_when_folder_found() + { + var command = new CreateAssetFolder { FolderName = "My Folder", ParentId = Guid.NewGuid() }; + + A.CallTo(() => assetQuery.FindAssetFolderAsync(command.ParentId)) + .Returns(new List { CreateFolder() }); + + await GuardAssetFolder.CanCreate(command, assetQuery); + } + + [Fact] + public async Task CanCreate_should_not_throw_exception_when_added_to_root() + { + var command = new CreateAssetFolder { FolderName = "My Folder" }; + + await GuardAssetFolder.CanCreate(command, assetQuery); + } + + [Fact] + public async Task CanMove_should_throw_exception_when_adding_to_its_own_child() + { + var id = Guid.NewGuid(); + + var command = new MoveAssetFolder { ParentId = Guid.NewGuid() }; + + A.CallTo(() => assetQuery.FindAssetFolderAsync(command.ParentId)) + .Returns(new List + { + CreateFolder(id), + CreateFolder(command.ParentId) + }); + + await ValidationAssert.ThrowsAsync(() => GuardAssetFolder.CanMove(command, assetQuery, id, Guid.NewGuid()), + new ValidationError("Cannot add folder to its own child.", "ParentId")); + } + + [Fact] + public async Task CanMove_should_throw_exception_when_folder_not_found() + { + var command = new MoveAssetFolder { ParentId = Guid.NewGuid() }; + + A.CallTo(() => assetQuery.FindAssetFolderAsync(command.ParentId)) + .Returns(new List()); + + await ValidationAssert.ThrowsAsync(() => GuardAssetFolder.CanMove(command, assetQuery, Guid.NewGuid(), Guid.NewGuid()), + new ValidationError("Asset folder does not exist.", "ParentId")); + } + + [Fact] + public async Task CanMove_should_throw_exception_when_folder_has_not_changed() + { + var command = new MoveAssetFolder { ParentId = Guid.NewGuid() }; + + await ValidationAssert.ThrowsAsync(() => GuardAssetFolder.CanMove(command, assetQuery, Guid.NewGuid(), command.ParentId), + new ValidationError("Asset folder is already part of this folder.", "ParentId")); + } + + [Fact] + public async Task CanMove_should_not_throw_exception_when_folder_found() + { + var command = new MoveAssetFolder { ParentId = Guid.NewGuid() }; + + A.CallTo(() => assetQuery.FindAssetFolderAsync(command.ParentId)) + .Returns(new List { CreateFolder() }); + + await GuardAssetFolder.CanMove(command, assetQuery, Guid.NewGuid(), Guid.NewGuid()); + } + + [Fact] + public async Task CanMove_should_not_throw_exception_when_added_to_root() + { + var command = new MoveAssetFolder(); + + await GuardAssetFolder.CanMove(command, assetQuery, Guid.NewGuid(), Guid.NewGuid()); + } + + [Fact] + public void CanRename_should_throw_exception_if_folder_name_is_empty() + { + var command = new RenameAssetFolder(); + + ValidationAssert.Throws(() => GuardAssetFolder.CanRename(command, "My Folder"), + new ValidationError("Folder name is required.", "FolderName")); + } + + [Fact] + public void CanRename_should_throw_exception_if_names_are_the_same() + { + var command = new RenameAssetFolder { FolderName = "My Folder" }; + + ValidationAssert.Throws(() => GuardAssetFolder.CanRename(command, "My Folder"), + new ValidationError("Asset folder has already this name.", "FolderName")); + } + + [Fact] + public void CanRename_should_not_throw_exception_if_names_are_different() + { + var command = new RenameAssetFolder { FolderName = "New Folder Name" }; + + GuardAssetFolder.CanRename(command, "My Folder"); + } + + [Fact] + public void CanDelete_should_not_throw_exception() + { + var command = new DeleteAssetFolder(); + + GuardAssetFolder.CanDelete(command); + } + + private IAssetFolderEntity CreateFolder(Guid id = default) + { + var assetFolder = A.Fake(); + + A.CallTo(() => assetFolder.Id).Returns(id); + + return assetFolder; + } + } +} diff --git a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Assets/Guards/GuardAssetTests.cs b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Assets/Guards/GuardAssetTests.cs index 94e23930a..a343212ab 100644 --- a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Assets/Guards/GuardAssetTests.cs +++ b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Assets/Guards/GuardAssetTests.cs @@ -5,6 +5,10 @@ // All rights reserved. Licensed under the MIT license. // ========================================================================== +using System; +using System.Collections.Generic; +using System.Threading.Tasks; +using FakeItEasy; using Squidex.Domain.Apps.Entities.Assets.Commands; using Squidex.Domain.Apps.Entities.TestHelpers; using Squidex.Infrastructure.Validation; @@ -14,6 +18,79 @@ namespace Squidex.Domain.Apps.Entities.Assets.Guards { public class GuardAssetTests { + private readonly IAssetQueryService assetQuery = A.Fake(); + + [Fact] + public async Task CanCreate_should_not_throw_exception_when_folder_found() + { + var command = new CreateAsset { ParentId = Guid.NewGuid() }; + + A.CallTo(() => assetQuery.FindAssetFolderAsync(command.ParentId)) + .Returns(new List { CreateFolder() }); + + await GuardAsset.CanCreate(command, assetQuery); + } + + [Fact] + public async Task CanCreate_should_throw_exception_when_folder_not_found() + { + var command = new CreateAsset { ParentId = Guid.NewGuid() }; + + A.CallTo(() => assetQuery.FindAssetFolderAsync(command.ParentId)) + .Returns(new List()); + + await ValidationAssert.ThrowsAsync(() => GuardAsset.CanCreate(command, assetQuery), + new ValidationError("Asset folder does not exist.", "ParentId")); + } + + [Fact] + public async Task CanCreate_should_not_throw_exception_when_added_to_root() + { + var command = new CreateAsset(); + + await GuardAsset.CanCreate(command, assetQuery); + } + + [Fact] + public async Task CanMove_should_throw_exception_when_folder_not_found() + { + var command = new MoveAsset { ParentId = Guid.NewGuid() }; + + A.CallTo(() => assetQuery.FindAssetFolderAsync(command.ParentId)) + .Returns(new List()); + + await ValidationAssert.ThrowsAsync(() => GuardAsset.CanMove(command, assetQuery, Guid.NewGuid()), + new ValidationError("Asset folder does not exist.", "ParentId")); + } + + [Fact] + public async Task CanMove_should_throw_exception_when_folder_has_not_changed() + { + var command = new MoveAsset { ParentId = Guid.NewGuid() }; + + await ValidationAssert.ThrowsAsync(() => GuardAsset.CanMove(command, assetQuery, command.ParentId), + new ValidationError("Asset is already part of this folder.", "ParentId")); + } + + [Fact] + public async Task CanMove_should_not_throw_exception_when_folder_found() + { + var command = new MoveAsset { ParentId = Guid.NewGuid() }; + + A.CallTo(() => assetQuery.FindAssetFolderAsync(command.ParentId)) + .Returns(new List { CreateFolder() }); + + await GuardAsset.CanMove(command, assetQuery, Guid.NewGuid()); + } + + [Fact] + public async Task CanMove_should_not_throw_exception_when_added_to_root() + { + var command = new MoveAsset(); + + await GuardAsset.CanMove(command, assetQuery, Guid.NewGuid()); + } + [Fact] public void CanAnnotate_should_throw_exception_if_nothing_defined() { @@ -31,14 +108,6 @@ namespace Squidex.Domain.Apps.Entities.Assets.Guards GuardAsset.CanAnnotate(command, "asset-name", "asset-slug"); } - [Fact] - public void CanCreate_should_not_throw_exception() - { - var command = new CreateAsset(); - - GuardAsset.CanCreate(command); - } - [Fact] public void CanUpdate_should_not_throw_exception() { @@ -54,5 +123,14 @@ namespace Squidex.Domain.Apps.Entities.Assets.Guards GuardAsset.CanDelete(command); } + + private IAssetFolderEntity CreateFolder(Guid id = default) + { + var assetFolder = A.Fake(); + + A.CallTo(() => assetFolder.Id).Returns(id); + + return assetFolder; + } } } diff --git a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Assets/Queries/AssetQueryServiceTests.cs b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Assets/Queries/AssetQueryServiceTests.cs index f804851c7..a210ec60b 100644 --- a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Assets/Queries/AssetQueryServiceTests.cs +++ b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Assets/Queries/AssetQueryServiceTests.cs @@ -22,6 +22,7 @@ namespace Squidex.Domain.Apps.Entities.Assets.Queries { private readonly IAssetEnricher assetEnricher = A.Fake(); private readonly IAssetRepository assetRepository = A.Fake(); + private readonly IAssetFolderRepository assetFolderRepository = A.Fake(); private readonly NamedId appId = NamedId.Of(Guid.NewGuid(), "my-app"); private readonly Context requestContext; private readonly AssetQueryParser queryParser = A.Fake(); @@ -34,7 +35,7 @@ namespace Squidex.Domain.Apps.Entities.Assets.Queries A.CallTo(() => queryParser.ParseQuery(requestContext, A.Ignored)) .Returns(new ClrQuery()); - sut = new AssetQueryService(assetEnricher, assetRepository, queryParser); + sut = new AssetQueryService(assetEnricher, assetRepository, assetFolderRepository, queryParser); } [Fact] @@ -44,7 +45,7 @@ namespace Squidex.Domain.Apps.Entities.Assets.Queries var enriched = new AssetEntity(); - A.CallTo(() => assetRepository.FindAssetAsync(found.Id, false)) + A.CallTo(() => assetRepository.FindAssetAsync(found.Id)) .Returns(found); A.CallTo(() => assetEnricher.EnrichAsync(found, requestContext)) @@ -90,7 +91,7 @@ namespace Squidex.Domain.Apps.Entities.Assets.Queries A.CallTo(() => assetEnricher.EnrichAsync(A>.That.IsSameSequenceAs(found1, found2), requestContext)) .Returns(new List { enriched1, enriched2 }); - var result = await sut.QueryAsync(requestContext, Q.Empty.WithIds(ids)); + var result = await sut.QueryAsync(requestContext, null, Q.Empty.WithIds(ids)); Assert.Equal(8, result.Total); @@ -106,17 +107,136 @@ namespace Squidex.Domain.Apps.Entities.Assets.Queries var enriched1 = new AssetEntity(); var enriched2 = new AssetEntity(); - A.CallTo(() => assetRepository.QueryAsync(appId.Id, A.Ignored)) + var parentId = Guid.NewGuid(); + + A.CallTo(() => assetRepository.QueryAsync(appId.Id, parentId, A.Ignored)) .Returns(ResultList.CreateFrom(8, found1, found2)); A.CallTo(() => assetEnricher.EnrichAsync(A>.That.IsSameSequenceAs(found1, found2), requestContext)) .Returns(new List { enriched1, enriched2 }); - var result = await sut.QueryAsync(requestContext, Q.Empty); + var result = await sut.QueryAsync(requestContext, parentId, Q.Empty); Assert.Equal(8, result.Total); Assert.Equal(new[] { enriched1, enriched2 }, result.ToArray()); } + + [Fact] + public async Task Should_load_assets_folders_from_repository() + { + var parentId = Guid.NewGuid(); + + var assetFolders = ResultList.CreateFrom(10); + + A.CallTo(() => assetFolderRepository.QueryAsync(appId.Id, parentId)) + .Returns(assetFolders); + + var result = await sut.QueryAssetFoldersAsync(requestContext, parentId); + + Assert.Same(assetFolders, result); + } + + [Fact] + public async Task Should_resolve_folder_path_from_root() + { + var folderId1 = Guid.NewGuid(); + var folder1 = CreateFolder(folderId1); + + A.CallTo(() => assetFolderRepository.FindAssetFolderAsync(folderId1)) + .Returns(folder1); + + var result = await sut.FindAssetFolderAsync(folderId1); + + Assert.Equal(result, new[] { folder1 }); + } + + [Fact] + public async Task Should_resolve_folder_path_from_child() + { + var folderId1 = Guid.NewGuid(); + var folderId2 = Guid.NewGuid(); + var folderId3 = Guid.NewGuid(); + + var folder1 = CreateFolder(folderId1); + var folder2 = CreateFolder(folderId2, folderId1); + var folder3 = CreateFolder(folderId3, folderId2); + + A.CallTo(() => assetFolderRepository.FindAssetFolderAsync(folderId1)) + .Returns(folder1); + + A.CallTo(() => assetFolderRepository.FindAssetFolderAsync(folderId2)) + .Returns(folder2); + + A.CallTo(() => assetFolderRepository.FindAssetFolderAsync(folderId3)) + .Returns(folder3); + + var result = await sut.FindAssetFolderAsync(folderId3); + + Assert.Equal(result, new[] { folder1, folder2, folder3 }); + } + + [Fact] + public async Task Should_not_resolve_folder_path_if_root_not_found() + { + var folderId1 = Guid.NewGuid(); + + A.CallTo(() => assetFolderRepository.FindAssetFolderAsync(folderId1)) + .Returns(Task.FromResult(null)); + + var result = await sut.FindAssetFolderAsync(folderId1); + + Assert.Empty(result); + } + + [Fact] + public async Task Should_not_resolve_folder_path_if_parent_of_child_not_found() + { + var folderId1 = Guid.NewGuid(); + var folderId2 = Guid.NewGuid(); + + var folder1 = CreateFolder(folderId1); + var folder2 = CreateFolder(folderId2, folderId1); + + A.CallTo(() => assetFolderRepository.FindAssetFolderAsync(folderId1)) + .Returns(Task.FromResult(null)); + + A.CallTo(() => assetFolderRepository.FindAssetFolderAsync(folderId2)) + .Returns(folder2); + + var result = await sut.FindAssetFolderAsync(folderId2); + + Assert.Empty(result); + } + + [Fact] + public async Task Should_not_resolve_folder_path_if_recursion_detected() + { + var folderId1 = Guid.NewGuid(); + var folderId2 = Guid.NewGuid(); + + var folder1 = CreateFolder(folderId1, folderId2); + var folder2 = CreateFolder(folderId2, folderId1); + + A.CallTo(() => assetFolderRepository.FindAssetFolderAsync(folderId1)) + .Returns(Task.FromResult(null)); + + A.CallTo(() => assetFolderRepository.FindAssetFolderAsync(folderId2)) + .Returns(folder2); + + var result = await sut.FindAssetFolderAsync(folderId2); + + Assert.Empty(result); + } + + private IAssetFolderEntity CreateFolder(Guid id, Guid parentId = default) + { + var assetFolder = A.Fake(); + + A.CallTo(() => assetFolder.Id).Returns(id); + A.CallTo(() => assetFolder.ParentId).Returns(parentId); + + return assetFolder; + } } } \ No newline at end of file diff --git a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/GraphQL/GraphQLQueriesTests.cs b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/GraphQL/GraphQLQueriesTests.cs index 1fba7f791..73efc9fc5 100644 --- a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/GraphQL/GraphQLQueriesTests.cs +++ b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/GraphQL/GraphQLQueriesTests.cs @@ -155,7 +155,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL var asset = CreateAsset(Guid.NewGuid()); - A.CallTo(() => assetQuery.QueryAsync(MatchsAssetContext(), A.That.Matches(x => x.ODataQuery == "?$top=30&$skip=5&$filter=my-query"))) + A.CallTo(() => assetQuery.QueryAsync(MatchsAssetContext(), null, A.That.Matches(x => x.ODataQuery == "?$top=30&$skip=5&$filter=my-query"))) .Returns(ResultList.CreateFrom(0, asset)); var result = await sut.QueryAsync(requestContext, new GraphQLQuery { Query = query }); @@ -228,7 +228,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL var asset = CreateAsset(Guid.NewGuid()); - A.CallTo(() => assetQuery.QueryAsync(MatchsAssetContext(), A.That.Matches(x => x.ODataQuery == "?$top=30&$skip=5&$filter=my-query"))) + A.CallTo(() => assetQuery.QueryAsync(MatchsAssetContext(), null, A.That.Matches(x => x.ODataQuery == "?$top=30&$skip=5&$filter=my-query"))) .Returns(ResultList.CreateFrom(10, asset)); var result = await sut.QueryAsync(requestContext, new GraphQLQuery { Query = query }); @@ -284,7 +284,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL } }".Replace("", assetId.ToString()); - A.CallTo(() => assetQuery.QueryAsync(MatchsAssetContext(), MatchIdQuery(assetId))) + A.CallTo(() => assetQuery.QueryAsync(MatchsAssetContext(), null, MatchIdQuery(assetId))) .Returns(ResultList.CreateFrom(1)); var result = await sut.QueryAsync(requestContext, new GraphQLQuery { Query = query }); @@ -331,7 +331,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL } }".Replace("", assetId.ToString()); - A.CallTo(() => assetQuery.QueryAsync(MatchsAssetContext(), MatchIdQuery(assetId))) + A.CallTo(() => assetQuery.QueryAsync(MatchsAssetContext(), null, MatchIdQuery(assetId))) .Returns(ResultList.CreateFrom(1, asset)); var result = await sut.QueryAsync(requestContext, new GraphQLQuery { Query = query }); @@ -1131,7 +1131,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL A.CallTo(() => contentQuery.QueryAsync(MatchsContentContext(), MatchId(contentId))) .Returns(ResultList.CreateFrom(1, content)); - A.CallTo(() => assetQuery.QueryAsync(MatchsAssetContext(), A.Ignored)) + A.CallTo(() => assetQuery.QueryAsync(MatchsAssetContext(), null, A.Ignored)) .Returns(ResultList.CreateFrom(0, assetRef)); var result = await sut.QueryAsync(requestContext, new GraphQLQuery { Query = query }); @@ -1184,10 +1184,10 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL } }".Replace("", assetId2.ToString()); - A.CallTo(() => assetQuery.QueryAsync(MatchsAssetContext(), MatchIdQuery(assetId1))) + A.CallTo(() => assetQuery.QueryAsync(MatchsAssetContext(), null, MatchIdQuery(assetId1))) .Returns(ResultList.CreateFrom(0, asset1)); - A.CallTo(() => assetQuery.QueryAsync(MatchsAssetContext(), MatchIdQuery(assetId2))) + A.CallTo(() => assetQuery.QueryAsync(MatchsAssetContext(), null, MatchIdQuery(assetId2))) .Returns(ResultList.CreateFrom(0, asset2)); var result = await sut.QueryAsync(requestContext, new GraphQLQuery { Query = query1 }, new GraphQLQuery { Query = query2 }); diff --git a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/Queries/ContentEnricherAssetsTests.cs b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/Queries/ContentEnricherAssetsTests.cs index f51336474..7333491dc 100644 --- a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/Queries/ContentEnricherAssetsTests.cs +++ b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/Queries/ContentEnricherAssetsTests.cs @@ -87,7 +87,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.Queries new[] { document2.Id, image2.Id }) }; - A.CallTo(() => assetQuery.QueryAsync(A.That.Matches(x => x.IsNoAssetEnrichment()), A.That.Matches(x => x.Ids.Count == 4))) + A.CallTo(() => assetQuery.QueryAsync(A.That.Matches(x => x.IsNoAssetEnrichment()), null, A.That.Matches(x => x.Ids.Count == 4))) .Returns(ResultList.CreateFrom(4, image1, image2, document1, document2)); var enriched = await sut.EnrichAsync(source, requestContext); @@ -122,7 +122,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.Queries new[] { document2.Id, image2.Id }) }; - A.CallTo(() => assetQuery.QueryAsync(A.That.Matches(x => x.IsNoAssetEnrichment()), A.That.Matches(x => x.Ids.Count == 4))) + A.CallTo(() => assetQuery.QueryAsync(A.That.Matches(x => x.IsNoAssetEnrichment()), null, A.That.Matches(x => x.Ids.Count == 4))) .Returns(ResultList.CreateFrom(4, image1, image2, document1, document2)); var enriched = await sut.EnrichAsync(source, requestContext); @@ -160,7 +160,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.Queries Assert.Null(enriched.ElementAt(0).ReferenceData); - A.CallTo(() => assetQuery.QueryAsync(A.Ignored, A.Ignored)) + A.CallTo(() => assetQuery.QueryAsync(A.Ignored, null, A.Ignored)) .MustNotHaveHappened(); } @@ -176,7 +176,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.Queries Assert.Null(enriched.ElementAt(0).ReferenceData); - A.CallTo(() => assetQuery.QueryAsync(A.Ignored, A.Ignored)) + A.CallTo(() => assetQuery.QueryAsync(A.Ignored, null, A.Ignored)) .MustNotHaveHappened(); } @@ -192,7 +192,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.Queries Assert.NotNull(enriched.ElementAt(0).ReferenceData); - A.CallTo(() => assetQuery.QueryAsync(A.Ignored, A.Ignored)) + A.CallTo(() => assetQuery.QueryAsync(A.Ignored, null, A.Ignored)) .MustNotHaveHappened(); } @@ -203,7 +203,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.Queries await sut.EnrichAsync(source, requestContext); - A.CallTo(() => assetQuery.QueryAsync(A.Ignored, A.Ignored)) + A.CallTo(() => assetQuery.QueryAsync(A.Ignored, null, A.Ignored)) .MustNotHaveHappened(); } diff --git a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Rules/RuleGrainTests.cs b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Rules/RuleGrainTests.cs index 8a1fb7211..9c8096d98 100644 --- a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Rules/RuleGrainTests.cs +++ b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Rules/RuleGrainTests.cs @@ -15,7 +15,6 @@ using Squidex.Domain.Apps.Entities.Rules.State; using Squidex.Domain.Apps.Entities.TestHelpers; using Squidex.Domain.Apps.Events.Rules; using Squidex.Infrastructure; -using Squidex.Infrastructure.Collections; using Squidex.Infrastructure.Commands; using Squidex.Infrastructure.EventSourcing; using Squidex.Infrastructure.Log; @@ -37,7 +36,7 @@ namespace Squidex.Domain.Apps.Entities.Rules public sealed class TestAction : RuleAction { - public Uri Url { get; set; } + public int Value { get; set; } } public RuleGrainTests() @@ -209,30 +208,16 @@ namespace Squidex.Domain.Apps.Entities.Rules private static CreateRule MakeCreateCommand() { - var newTrigger = new ContentChangedTriggerV2 - { - Schemas = ReadOnlyCollection.Empty() - }; - - var newAction = new TestAction - { - Url = new Uri("https://squidex.io/v2") - }; + var newTrigger = new ManualTrigger(); + var newAction = new TestAction { Value = 123 }; return new CreateRule { Trigger = newTrigger, Action = newAction }; } private static UpdateRule MakeUpdateCommand() { - var newTrigger = new ContentChangedTriggerV2 - { - Schemas = ReadOnlyCollection.Empty() - }; - - var newAction = new TestAction - { - Url = new Uri("https://squidex.io/v2") - }; + var newTrigger = new ManualTrigger(); + var newAction = new TestAction { Value = 123 }; return new UpdateRule { Trigger = newTrigger, Action = newAction, Name = "NewName" }; } diff --git a/backend/tests/Squidex.Infrastructure.Tests/CollectionExtensionsTests.cs b/backend/tests/Squidex.Infrastructure.Tests/CollectionExtensionsTests.cs index 223bb4ffb..7e074b05c 100644 --- a/backend/tests/Squidex.Infrastructure.Tests/CollectionExtensionsTests.cs +++ b/backend/tests/Squidex.Infrastructure.Tests/CollectionExtensionsTests.cs @@ -15,6 +15,34 @@ namespace Squidex.Infrastructure private readonly Dictionary valueDictionary = new Dictionary(); private readonly Dictionary> listDictionary = new Dictionary>(); + [Fact] + public void IndexOf_should_return_index_when_found() + { + var source = new List<(int Value, int Other)> + { + (5, 5), + (4, 4) + }; + + var index = source.IndexOf(x => x.Other == 4); + + Assert.Equal(1, index); + } + + [Fact] + public void IndexOf_should_return_negative_value_when_not_found() + { + var source = new List<(int Value, int Other)> + { + (5, 5), + (4, 4) + }; + + var index = source.IndexOf(x => x.Other == 2); + + Assert.Equal(-1, index); + } + [Fact] public void GetOrDefault_should_return_value_if_key_exists() { diff --git a/frontend/app/app.module.ts b/frontend/app/app.module.ts index 63c98476b..a38224668 100644 --- a/frontend/app/app.module.ts +++ b/frontend/app/app.module.ts @@ -63,7 +63,7 @@ export function configAnalyticsId() { } export function configDecimalSeparator() { - return new DecimalSeparatorConfig('.'); + return new DecimalSeparatorConfig('.'); } export function configCurrency() { diff --git a/frontend/app/features/administration/pages/event-consumers/event-consumers-page.component.html b/frontend/app/features/administration/pages/event-consumers/event-consumers-page.component.html index f690b1919..2b7033039 100644 --- a/frontend/app/features/administration/pages/event-consumers/event-consumers-page.component.html +++ b/frontend/app/features/administration/pages/event-consumers/event-consumers-page.component.html @@ -1,6 +1,6 @@ - + Consumers @@ -14,25 +14,33 @@ - - - - - - - - + + +
- Name - - Position - - Actions -
+ + + + + + + +
+ Name + + Position + + Actions +
+
- - - +
+ + + +
+
+
diff --git a/frontend/app/features/administration/pages/event-consumers/event-consumers-page.component.ts b/frontend/app/features/administration/pages/event-consumers/event-consumers-page.component.ts index cfee6abc9..0de18d12f 100644 --- a/frontend/app/features/administration/pages/event-consumers/event-consumers-page.component.ts +++ b/frontend/app/features/administration/pages/event-consumers/event-consumers-page.component.ts @@ -7,7 +7,7 @@ import { Component, OnInit } from '@angular/core'; import { timer } from 'rxjs'; -import { onErrorResumeNext, switchMap } from 'rxjs/operators'; +import { switchMap } from 'rxjs/operators'; import { DialogModel, ResourceOwner } from '@app/shared'; @@ -31,7 +31,7 @@ export class EventConsumersPageComponent extends ResourceOwner implements OnInit public ngOnInit() { this.eventConsumersState.load(); - this.own(timer(5000, 5000).pipe(switchMap(() => this.eventConsumersState.load(true, true)), onErrorResumeNext())); + this.own(timer(5000, 5000).pipe(switchMap(() => this.eventConsumersState.load(false, true)))); } public reload() { diff --git a/frontend/app/features/administration/pages/users/users-page.component.html b/frontend/app/features/administration/pages/users/users-page.component.html index 137d2a271..72feb75c1 100644 --- a/frontend/app/features/administration/pages/users/users-page.component.html +++ b/frontend/app/features/administration/pages/users/users-page.component.html @@ -1,6 +1,6 @@ - + Users @@ -27,40 +27,40 @@ -
- - - - - - - - - -
-   - - Name - - Email - - Actions -
-
- -
-
- + + +
+ + + + + + + + +
+   + + Name + + Email + + Actions +
+ + +
+
-
- - + + + + + diff --git a/frontend/app/features/administration/state/event-consumers.state.spec.ts b/frontend/app/features/administration/state/event-consumers.state.spec.ts index 9849635be..82123f410 100644 --- a/frontend/app/features/administration/state/event-consumers.state.spec.ts +++ b/frontend/app/features/administration/state/event-consumers.state.spec.ts @@ -44,10 +44,20 @@ describe('EventConsumersState', () => { expect(eventConsumersState.snapshot.eventConsumers).toEqual([eventConsumer1, eventConsumer2]); expect(eventConsumersState.snapshot.isLoaded).toBeTruthy(); + expect(eventConsumersState.snapshot.isLoading).toBeFalsy(); dialogs.verify(x => x.notifyInfo(It.isAnyString()), Times.never()); }); + it('should reset loading when loading failed', () => { + eventConsumersService.setup(x => x.getEventConsumers()) + .returns(() => throwError('error')); + + eventConsumersState.load().pipe(onErrorResumeNext()).subscribe(); + + expect(eventConsumersState.snapshot.isLoading).toBeFalsy(); + }); + it('should show notification on load when reload is true', () => { eventConsumersService.setup(x => x.getEventConsumers()) .returns(() => of(new EventConsumersDto([eventConsumer1, eventConsumer2]))).verifiable(); diff --git a/frontend/app/features/administration/state/event-consumers.state.ts b/frontend/app/features/administration/state/event-consumers.state.ts index 4d5668fa8..ceefabeb5 100644 --- a/frontend/app/features/administration/state/event-consumers.state.ts +++ b/frontend/app/features/administration/state/event-consumers.state.ts @@ -7,7 +7,7 @@ import { Injectable } from '@angular/core'; import { Observable } from 'rxjs'; -import { tap } from 'rxjs/operators'; +import { finalize, tap } from 'rxjs/operators'; import { DialogService, @@ -23,6 +23,9 @@ interface Snapshot { // Indicates if event consumers are loaded. isLoaded?: boolean; + + // Indicates if event consumers are loading. + isLoading?: boolean; } type EventConsumersList = ReadonlyArray; @@ -35,6 +38,9 @@ export class EventConsumersState extends State { public isLoaded = this.project(x => x.isLoaded === true); + public isLoading = + this.project(x => x.isLoading === true); + constructor( private readonly dialogs: DialogService, private readonly eventConsumersService: EventConsumersService @@ -43,20 +49,33 @@ export class EventConsumersState extends State { } public load(isReload = false, silent = false): Observable { - if (!isReload) { + if (isReload && !silent) { this.resetState(); } + return this.loadInternal(isReload, silent); + } + + private loadInternal(isReload: boolean, silent: boolean): Observable { + if (!silent) { + this.next({ isLoading: true }); + } + return this.eventConsumersService.getEventConsumers().pipe( tap(({ items: eventConsumers }) => { if (isReload && !silent) { this.dialogs.notifyInfo('Event Consumers reloaded.'); } - this.next(s => { - return { ...s, eventConsumers, isLoaded: true }; + this.next({ + eventConsumers, + isLoaded: true, + isLoading: false }); }), + finalize(() => { + this.next({ isLoading: false }); + }), shareSubscribed(this.dialogs, { silent })); } diff --git a/frontend/app/features/administration/state/users.state.spec.ts b/frontend/app/features/administration/state/users.state.spec.ts index 4ded26b20..c66ef5e87 100644 --- a/frontend/app/features/administration/state/users.state.spec.ts +++ b/frontend/app/features/administration/state/users.state.spec.ts @@ -6,6 +6,7 @@ */ import { of, throwError } from 'rxjs'; +import { onErrorResumeNext } from 'rxjs/operators'; import { IMock, It, Mock, Times } from 'typemoq'; import { @@ -57,13 +58,23 @@ describe('UsersState', () => { usersState.load().subscribe(); + expect(usersState.snapshot.isLoaded).toBeTruthy(); + expect(usersState.snapshot.isLoading).toBeFalsy(); expect(usersState.snapshot.users).toEqual([user1, user2]); expect(usersState.snapshot.usersPager.numberOfItems).toEqual(200); - expect(usersState.snapshot.isLoaded).toBeTruthy(); dialogs.verify(x => x.notifyInfo(It.isAnyString()), Times.never()); }); + it('should reset loading when loading failed', () => { + usersService.setup(x => x.getUsers(10, 0, undefined)) + .returns(() => throwError('error')); + + usersState.load().pipe(onErrorResumeNext()).subscribe(); + + expect(usersState.snapshot.isLoading).toBeFalsy(); + }); + it('should load page size from local store', () => { localStore.setup(x => x.getInt('users.pageSize', 10)) .returns(() => 25); diff --git a/frontend/app/features/administration/state/users.state.ts b/frontend/app/features/administration/state/users.state.ts index 077a39851..224ced34f 100644 --- a/frontend/app/features/administration/state/users.state.ts +++ b/frontend/app/features/administration/state/users.state.ts @@ -7,7 +7,7 @@ import { Injectable } from '@angular/core'; import { Observable, of } from 'rxjs'; -import { catchError, tap } from 'rxjs/operators'; +import { catchError, finalize, tap } from 'rxjs/operators'; import '@app/framework/utils/rxjs-extensions'; @@ -39,6 +39,9 @@ interface Snapshot { // Indicates if the users are loaded. isLoaded?: boolean; + // Indicates if the users are loading. + isLoading?: boolean; + // The selected user. selectedUser?: UserDto | null; @@ -63,6 +66,9 @@ export class UsersState extends State { public isLoaded = this.project(x => x.isLoaded === true); + public isLoading = + this.project(x => x.isLoading === true); + public canCreate = this.project(x => x.canCreate === true); @@ -105,15 +111,17 @@ export class UsersState extends State { public load(isReload = false): Observable { if (!isReload) { - const selectedUser = this.snapshot.selectedUser; + const usersPager = this.snapshot.usersPager.reset(); - this.resetState({ selectedUser }); + this.resetState({ usersPager, selectedUser: this.snapshot.selectedUser }); } return this.loadInternal(isReload); } - private loadInternal(isReload = false): Observable { + private loadInternal(isReload: boolean): Observable { + this.next({ isLoading: true }); + return this.usersService.getUsers( this.snapshot.usersPager.pageSize, this.snapshot.usersPager.skip, @@ -135,12 +143,16 @@ export class UsersState extends State { return { ...s, canCreate, isLoaded: true, + isLoading: false, selectedUser, users, usersPager }; }); }), + finalize(() => { + this.next({ isLoading: false }); + }), shareSubscribed(this.dialogs)); } @@ -184,13 +196,13 @@ export class UsersState extends State { public search(query: string): Observable { this.next(s => ({ ...s, usersPager: s.usersPager.reset(), usersQuery: query })); - return this.loadInternal(); + return this.loadInternal(false); } public setPager(usersPager: Pager) { - this.next(s => ({ ...s, usersPager })); + this.next({ usersPager }); - return this.loadInternal(); + return this.loadInternal(false); } private replaceUser(user: UserDto) { diff --git a/frontend/app/features/apps/pages/apps-page.component.html b/frontend/app/features/apps/pages/apps-page.component.html index a8f1e9631..2a267d86e 100644 --- a/frontend/app/features/apps/pages/apps-page.component.html +++ b/frontend/app/features/apps/pages/apps-page.component.html @@ -48,7 +48,7 @@
- +

New App

@@ -62,7 +62,7 @@
- +

New Blog Sample

@@ -79,7 +79,7 @@
- +

New Profile Sample

@@ -96,7 +96,7 @@
- +

New Identity App

diff --git a/frontend/app/features/apps/pages/apps-page.component.scss b/frontend/app/features/apps/pages/apps-page.component.scss index 5776d6080..02998e6a5 100644 --- a/frontend/app/features/apps/pages/apps-page.component.scss +++ b/frontend/app/features/apps/pages/apps-page.component.scss @@ -105,6 +105,12 @@ } } +.card-image { + img { + height: 6rem; + } +} + .info { color: $color-border-dark; padding: 2rem; diff --git a/frontend/app/features/assets/pages/assets-page.component.html b/frontend/app/features/assets/pages/assets-page.component.html index 82a1b2527..f9d930107 100644 --- a/frontend/app/features/assets/pages/assets-page.component.html +++ b/frontend/app/features/assets/pages/assets-page.component.html @@ -1,6 +1,6 @@ - + Assets @@ -44,11 +44,32 @@
+
+ + + +
- + + + + + + +
+ +
+ + + + +
@@ -60,4 +81,10 @@ + + + + + diff --git a/frontend/app/features/assets/pages/assets-page.component.scss b/frontend/app/features/assets/pages/assets-page.component.scss index 98574b085..d615d5710 100644 --- a/frontend/app/features/assets/pages/assets-page.component.scss +++ b/frontend/app/features/assets/pages/assets-page.component.scss @@ -22,4 +22,13 @@ } } } +} + +.grid-header { + padding-left: 1rem; +} + +.grid-content { + padding-top: 1rem; + padding-bottom: 1rem; } \ No newline at end of file diff --git a/frontend/app/features/assets/pages/assets-page.component.ts b/frontend/app/features/assets/pages/assets-page.component.ts index eba5a29d4..ee7824fc3 100644 --- a/frontend/app/features/assets/pages/assets-page.component.ts +++ b/frontend/app/features/assets/pages/assets-page.component.ts @@ -10,6 +10,7 @@ import { FormControl } from '@angular/forms'; import { AssetsState, + DialogModel, LocalStoreService, Queries, Query, @@ -27,6 +28,8 @@ export class AssetsPageComponent extends ResourceOwner implements OnInit { public queries = new Queries(this.uiState, 'assets'); + public addAssetFolderDialog = new DialogModel(); + public isListView: boolean; constructor( diff --git a/frontend/app/features/content/pages/contents/contents-page.component.html b/frontend/app/features/content/pages/contents/contents-page.component.html index 26c62f71b..bb6676758 100644 --- a/frontend/app/features/content/pages/contents/contents-page.component.html +++ b/frontend/app/features/content/pages/contents/contents-page.component.html @@ -1,6 +1,6 @@ - + Contents @@ -38,71 +38,75 @@ -
- - - - - - - - -
- - - Actions - - - -
-
- -
- {{selectionCount}} items selected   - - - - -
+ + +
+ {{selectionCount}} items selected   + + + + +
+
-
-
- - - + +
+ + + + + + +
+ + + Actions + + + +
-
-
+
- + +
+ + + +
+
+
+ + + + + diff --git a/frontend/app/features/content/pages/contents/contents-page.component.scss b/frontend/app/features/content/pages/contents/contents-page.component.scss index 37ec92dda..2f1b55277 100644 --- a/frontend/app/features/content/pages/contents/contents-page.component.scss +++ b/frontend/app/features/content/pages/contents/contents-page.component.scss @@ -18,12 +18,6 @@ min-width: 100%; } -.grid-content { - overflow-y: auto; - overflow-x: auto; - padding-right: 0; -} - .icon-plus { font-size: .8rem; } diff --git a/frontend/app/features/content/shared/assets-editor.component.html b/frontend/app/features/content/shared/assets-editor.component.html index 71bc882ea..f4bb6a22f 100644 --- a/frontend/app/features/content/shared/assets-editor.component.html +++ b/frontend/app/features/content/shared/assets-editor.component.html @@ -22,7 +22,7 @@
- @@ -35,7 +35,7 @@
- diff --git a/frontend/app/features/content/shared/content-list-cell.directive.ts b/frontend/app/features/content/shared/content-list-cell.directive.ts index 77f150f12..1f0d46de4 100644 --- a/frontend/app/features/content/shared/content-list-cell.directive.ts +++ b/frontend/app/features/content/shared/content-list-cell.directive.ts @@ -68,7 +68,7 @@ export class ContentListWidthPipe implements PipeTransform { return 0; } - return `${getTableWidth(value.referenceFields) + 100}px`; + return `${getTableWidth(value.listFields) + 100}px`; } } diff --git a/frontend/app/features/content/shared/contents-selector.component.html b/frontend/app/features/content/shared/contents-selector.component.html index 799a9627f..9f36cd230 100644 --- a/frontend/app/features/content/shared/contents-selector.component.html +++ b/frontend/app/features/content/shared/contents-selector.component.html @@ -1,4 +1,4 @@ - +
@@ -38,47 +38,49 @@ -
- - - - - - - - -
- - - - - - -
-
- -
-
- - - + + +
+ + + + + + +
+ + + + + + +
-
-
- - +
+ + +
+ + + +
+
+
+ + + + +
diff --git a/frontend/app/features/dashboard/pages/dashboard-page.component.html b/frontend/app/features/dashboard/pages/dashboard-page.component.html index 64462fa58..a222cf389 100644 --- a/frontend/app/features/dashboard/pages/dashboard-page.component.html +++ b/frontend/app/features/dashboard/pages/dashboard-page.component.html @@ -15,7 +15,7 @@
- +

New Schema

@@ -29,7 +29,7 @@
- +

API Documentation

@@ -43,7 +43,7 @@
- +

Feedback & Support

@@ -57,7 +57,7 @@
- +

Github

diff --git a/frontend/app/features/dashboard/pages/dashboard-page.component.scss b/frontend/app/features/dashboard/pages/dashboard-page.component.scss index 33106b62d..1b5d18fe4 100644 --- a/frontend/app/features/dashboard/pages/dashboard-page.component.scss +++ b/frontend/app/features/dashboard/pages/dashboard-page.component.scss @@ -30,6 +30,12 @@ } } +.card-image { + img { + height: 5rem; + } +} + .card { & { margin-right: 1rem; diff --git a/frontend/app/features/rules/pages/events/rule-events-page.component.html b/frontend/app/features/rules/pages/events/rule-events-page.component.html index f40dcd544..0f3a847e1 100644 --- a/frontend/app/features/rules/pages/events/rule-events-page.component.html +++ b/frontend/app/features/rules/pages/events/rule-events-page.component.html @@ -1,6 +1,6 @@ - + Events @@ -14,83 +14,87 @@ - - - - - - - - - - + + +
- Status - - Event - - Description - - Created -
+ + + + + + + + + - - - - - - - - - - + + + + + + + + + - - - -
+ Status + + Event + + Description + + Created +
- {{event.jobResult}} - - {{event.eventName}} - - {{event.description}} - - {{event.created | sqxFromNow}} - - -
-
-

Last Invocation

-
- -
-
- {{event.result}} -
-
- Attempts: {{event.numCalls}} -
-
- Next: {{event.nextAttempt | sqxFromNow}} -
-
-
+ {{event.jobResult}} + + {{event.eventName}} + + {{event.description}} + + {{event.created | sqxFromNow}} + + +
+
+

Last Invocation

+
+ +
+
+ {{event.result}} +
+
+ Attempts: {{event.numCalls}} +
+
+ Next: {{event.nextAttempt | sqxFromNow}} +
+
+ - -
-
-
-
- -
-
-
+ +
+
+
+
+ +
+
+ + + + + - + + + diff --git a/frontend/app/features/rules/pages/rules/rules-page.component.html b/frontend/app/features/rules/pages/rules/rules-page.component.html index f7ab095ad..ef97afaa6 100644 --- a/frontend/app/features/rules/pages/rules/rules-page.component.html +++ b/frontend/app/features/rules/pages/rules/rules-page.component.html @@ -1,6 +1,6 @@ - + Rules @@ -22,38 +22,42 @@ - -
- No rule created yet. - - + +
+ +
+ No rule created yet. + + +
+ + + + +
+ + + + + +
- - - - -
- - - - - - +
diff --git a/frontend/app/features/rules/pages/rules/rules-page.component.ts b/frontend/app/features/rules/pages/rules/rules-page.component.ts index 7418562e5..2f3f66e0d 100644 --- a/frontend/app/features/rules/pages/rules/rules-page.component.ts +++ b/frontend/app/features/rules/pages/rules/rules-page.component.ts @@ -46,7 +46,7 @@ export class RulesPageComponent implements OnInit { this.ruleActions = actions; }); - this.schemasState.load(); + this.schemasState.loadIfNotLoaded(); } public reload() { diff --git a/frontend/app/features/schemas/pages/schema/schema-edit-form.component.ts b/frontend/app/features/schemas/pages/schema/schema-edit-form.component.ts index 65bdfa976..e1cfd177d 100644 --- a/frontend/app/features/schemas/pages/schema/schema-edit-form.component.ts +++ b/frontend/app/features/schemas/pages/schema/schema-edit-form.component.ts @@ -5,7 +5,7 @@ * Copyright (c) Squidex UG (haftungsbeschränkt). All rights reserved. */ -import { Component, Input, OnInit } from '@angular/core'; +import { Component, Input, OnChanges } from '@angular/core'; import { FormBuilder } from '@angular/forms'; import { @@ -20,7 +20,7 @@ import { styleUrls: ['./schema-edit-form.component.scss'], templateUrl: './schema-edit-form.component.html' }) -export class SchemaEditFormComponent implements OnInit { +export class SchemaEditFormComponent implements OnChanges { public readonly standalone = { standalone: true }; @Input() @@ -37,7 +37,7 @@ export class SchemaEditFormComponent implements OnInit { ) { } - public ngOnInit() { + public ngOnChanges() { this.isEditable = this.schema.canUpdate; this.editForm.load(this.schema.properties); diff --git a/frontend/app/features/schemas/pages/schema/schema-page.component.html b/frontend/app/features/schemas/pages/schema/schema-page.component.html index 888a7863f..dc2def78b 100644 --- a/frontend/app/features/schemas/pages/schema/schema-page.component.html +++ b/frontend/app/features/schemas/pages/schema/schema-page.component.html @@ -1,6 +1,6 @@ - +