From 517c70304c6a581f281a08a855fddf36cfa7f620 Mon Sep 17 00:00:00 2001 From: Leonardo Giacone Date: Wed, 10 Apr 2024 11:47:07 +0200 Subject: [PATCH] feat(app-trash-bin): add trash bin to app-headless-cms (#4059) --- packages/app-aco/src/config/AcoConfig.tsx | 4 +- packages/app-aco/src/config/table/Sorting.tsx | 27 + packages/app-aco/src/config/table/index.ts | 6 +- .../src/components/BulkActions/Worker.ts | 2 +- .../useDialogWithReport.tsx | 11 +- .../src/components/SplitView/SplitView.tsx | 6 +- .../src/entries.graphql.ts | 62 +- .../src/types/index.ts | 8 + packages/app-headless-cms/package.json | 1 + .../BulkActions/ActionDelete.tsx | 14 +- .../Table/Actions/DeleteEntry.tsx | 2 +- .../TrashBinDeleteItemGraphQLGateway.ts | 43 + .../TrashBin/adapters/TrashBinItemMapper.ts | 19 + .../adapters/TrashBinListGraphQLGateway.ts | 53 ++ .../TrashBinRestoreItemGraphQLGateway.ts | 44 + .../ContentEntries/TrashBin/adapters/index.ts | 4 + .../TrashBin/components/TrashBin.styled.tsx | 34 + .../TrashBin/components/TrashBin.tsx | 54 ++ .../TrashBin/components/TrashBinButton.tsx | 18 + .../TrashBin/components/index.ts | 1 + .../ContentEntries/TrashBin/index.ts | 1 + .../src/admin/hooks/useDeleteEntry.tsx | 10 +- .../src/admin/hooks/usePermission.ts | 19 + .../plugins/entry/DefaultOnEntryDelete.tsx | 3 +- .../views/contentEntries/Table/Sidebar.tsx | 70 +- .../views/contentEntries/Table/styled.tsx | 20 +- packages/app-headless-cms/tsconfig.build.json | 1 + packages/app-headless-cms/tsconfig.json | 4 + .../src/admin/views/Pages/Table/styled.tsx | 2 +- packages/app-trash-bin/.babelrc.js | 1 + packages/app-trash-bin/LICENSE | 21 + packages/app-trash-bin/README.md | 17 + packages/app-trash-bin/jest.config.js | 5 + packages/app-trash-bin/package.json | 53 ++ .../TrashBinItem/ITrashBinItemMapper.ts | 5 + .../Models/TrashBinItem/TrashBinItem.ts | 33 + .../src/Domain/Models/TrashBinItem/index.ts | 2 + .../app-trash-bin/src/Domain/Models/index.ts | 1 + .../Repositories/Search/ISearchRepository.ts | 4 + .../Repositories/Search/SearchRepository.ts | 18 + .../Search/SearchRepositoryFactory.ts | 21 + .../src/Domain/Repositories/Search/index.ts | 3 + .../SelectedItems/ISelectedItemsRepository.ts | 6 + .../SelectedItems/SelectedItemsRepository.ts | 19 + .../SelectedItemsRepositoryFactory.ts | 21 + .../Repositories/SelectedItems/index.ts | 3 + .../Sorting/SortingRepositoryWithDefaults.ts | 29 + .../src/Domain/Repositories/Sorting/index.ts | 1 + .../TrashBinItems/ITrashBinItemsRepository.ts | 14 + .../TrashBinItems/TrashBinItemMapper.ts | 19 + .../TrashBinItems/TrashBinItemsRepository.ts | 123 +++ .../TrashBinItemsRepositoryFactory.ts | 43 + .../TrashBinItemsRepositoryWithLoading.ts | 62 ++ .../Repositories/TrashBinItems/index.ts | 5 + .../src/Domain/Repositories/index.ts | 5 + packages/app-trash-bin/src/Domain/index.ts | 2 + .../ITrashBinDeleteItemGateway.ts | 3 + .../src/Gateways/TrashBinDeleteItem/index.ts | 1 + .../TrashBinListItems/ITrashBinListGateway.ts | 5 + .../src/Gateways/TrashBinListItems/index.ts | 1 + .../ITrashBinRestoreItemGateway.ts | 3 + .../src/Gateways/TrashBinRestoreItem/index.ts | 1 + packages/app-trash-bin/src/Gateways/index.ts | 3 + .../Presentation/TrashBin/TrashBin.test.ts | 759 ++++++++++++++++++ .../src/Presentation/TrashBin/TrashBin.tsx | 125 +++ .../TrashBin/TrashBinControllers.ts | 90 +++ .../TrashBin/TrashBinPresenter.ts | 60 ++ .../DeleteItem/DeleteItemController.ts | 15 + .../DeleteItem/IDeleteItemController.ts | 3 + .../TrashBin/controllers/DeleteItem/index.ts | 2 + .../ListItems/IListItemsController.ts | 3 + .../ListItems/ListItemsController.ts | 15 + .../TrashBin/controllers/ListItems/index.ts | 2 + .../ListMoreItems/IListMoreItemsController.ts | 3 + .../ListMoreItems/ListMoreItemsController.ts | 15 + .../controllers/ListMoreItems/index.ts | 2 + .../RestoreItem/IRestoreItemController.ts | 3 + .../RestoreItem/RestoreItemController.ts | 15 + .../TrashBin/controllers/RestoreItem/index.ts | 2 + .../SearchItems/ISearchItemsController.ts | 3 + .../SearchItems/SearchItemsController.ts | 23 + .../TrashBin/controllers/SearchItems/index.ts | 2 + .../SelectItems/ISelectItemsController.ts | 5 + .../SelectItems/SelectItemsController.ts | 17 + .../TrashBin/controllers/SelectItems/index.ts | 2 + .../SortItems/ISortItemsController.ts | 5 + .../SortItems/SortItemsController.ts | 31 + .../TrashBin/controllers/SortItems/index.ts | 2 + .../TrashBin/controllers/index.ts | 7 + .../src/Presentation/TrashBin/index.ts | 1 + .../TrashBinConfigs/TrashBinConfigs.tsx | 61 ++ .../src/Presentation/TrashBinConfigs/index.ts | 1 + .../TrashBinRenderer/TrashBinRenderer.tsx | 27 + .../Presentation/TrashBinRenderer/index.ts | 1 + .../abstractions/ITrashBinControllers.ts | 19 + .../abstractions/ITrashBinPresenter.ts | 20 + .../src/Presentation/abstractions/index.ts | 2 + .../Actions/DeleteItem/DeleteItem.tsx | 12 + .../components/Actions/DeleteItem/index.ts | 1 + .../Actions/RestoreItem/RestoreItem.tsx | 14 + .../components/Actions/RestoreItem/index.ts | 1 + .../Presentation/components/Actions/index.ts | 2 + .../BottomInfoBar/BottomInfoBar.styled.tsx | 45 ++ .../BottomInfoBar/BottomInfoBar.tsx | 23 + .../components/BottomInfoBar/ListMeta.tsx | 19 + .../components/BottomInfoBar/ListStatus.tsx | 22 + .../components/BottomInfoBar/index.tsx | 1 + .../BulkActions/BulkActions.styled.tsx | 30 + .../BulkActions/BulkActions/BulkActions.tsx | 37 + .../BulkActions/BulkActions/index.ts | 1 + .../BulkActions/DeleteItems/DeleteItems.tsx | 59 ++ .../BulkActions/DeleteItems/index.ts | 1 + .../BulkActions/RestoreItems/RestoreItems.tsx | 78 ++ .../RestoreItemsReportMessage.tsx | 18 + .../RestoreItems/RestoredItems.styled.tsx | 6 + .../BulkActions/RestoreItems/index.ts | 1 + .../components/BulkActions/index.ts | 3 + .../Cells/CellActions/CellActions.tsx | 20 + .../components/Cells/CellActions/index.ts | 1 + .../Cells/CellCreatedBy/CellCreatedBy.tsx | 9 + .../components/Cells/CellCreatedBy/index.ts | 1 + .../Cells/CellDeletedBy/CellDeletedBy.tsx | 9 + .../components/Cells/CellDeletedBy/index.ts | 1 + .../Cells/CellDeletedOn/CellDeletedOn.tsx | 10 + .../components/Cells/CellDeletedOn/index.ts | 1 + .../Cells/CellTitle/CellTitle.styled.tsx | 19 + .../components/Cells/CellTitle/CellTitle.tsx | 18 + .../components/Cells/CellTitle/index.ts | 1 + .../Presentation/components/Cells/index.ts | 5 + .../components/Empty/Empty.styled.tsx | 27 + .../Presentation/components/Empty/Empty.tsx | 14 + .../Presentation/components/Empty/index.ts | 1 + .../SearchInput/SearchInput.styled.tsx | 39 + .../components/SearchInput/SearchInput.tsx | 33 + .../components/SearchInput/index.ts | 1 + .../Presentation/components/Table/Table.tsx | 22 + .../Presentation/components/Table/index.ts | 1 + .../components/Title/Title.styled.tsx | 11 + .../Presentation/components/Title/Title.tsx | 15 + .../Presentation/components/Title/index.tsx | 1 + .../TrashBinOverlay/TrashBinOverlay.tsx | 40 + .../components/TrashBinOverlay/index.ts | 1 + .../src/Presentation/configs/index.ts | 1 + .../configs/list/Browser/BulkAction.tsx | 85 ++ .../configs/list/Browser/EntryAction.tsx | 23 + .../configs/list/Browser/Table/Column.tsx | 25 + .../configs/list/Browser/Table/Sorting.tsx | 19 + .../configs/list/Browser/Table/index.ts | 12 + .../configs/list/Browser/index.ts | 15 + .../configs/list/TrashBinListConfig.tsx | 28 + .../src/Presentation/configs/list/index.ts | 1 + .../src/Presentation/hooks/index.ts | 4 + .../hooks/useDeleteTrashBinItem.tsx | 39 + .../hooks/useRestoreTrashBinItem.tsx | 50 ++ .../src/Presentation/hooks/useTrashBin.tsx | 79 ++ .../Presentation/hooks/useTrashBinItem.tsx | 11 + .../app-trash-bin/src/Presentation/index.tsx | 80 ++ .../UseCases/DeleteItem/DeleteItemUseCase.ts | 16 + .../UseCases/DeleteItem/IDeleteItemUseCase.ts | 3 + .../src/UseCases/DeleteItem/index.ts | 2 + .../UseCases/ListItems/IListItemsUseCase.ts | 5 + .../UseCases/ListItems/ListItemsUseCase.ts | 16 + .../ListItems/ListItemsUseCaseWithSearch.ts | 20 + .../ListItems/ListItemsUseCaseWithSorting.ts | 20 + .../src/UseCases/ListItems/index.ts | 4 + .../ListMoreItems/IListMoreItemsUseCase.ts | 3 + .../ListMoreItems/ListMoreItemsUseCase.ts | 16 + .../src/UseCases/ListMoreItems/index.ts | 2 + .../RestoreItem/IRestoreItemUseCase.ts | 3 + .../RestoreItem/RestoreItemUseCase.ts | 16 + .../src/UseCases/RestoreItem/index.ts | 2 + .../SearchItems/ISearchItemsUseCase.ts | 3 + .../SearchItems/SearchItemsUseCase.ts | 16 + .../src/UseCases/SearchItems/index.ts | 2 + .../SelectItems/ISelectItemsUseCase.ts | 5 + .../SelectItems/SelectItemsUseCase.ts | 16 + .../src/UseCases/SelectItems/index.ts | 2 + .../UseCases/SortItems/ISortItemsUseCase.ts | 5 + .../UseCases/SortItems/SortItemsUseCase.ts | 16 + .../src/UseCases/SortItems/index.ts | 2 + packages/app-trash-bin/src/UseCases/index.ts | 7 + packages/app-trash-bin/src/index.ts | 3 + packages/app-trash-bin/src/types.ts | 32 + packages/app-trash-bin/tsconfig.build.json | 19 + packages/app-trash-bin/tsconfig.json | 34 + packages/app-trash-bin/webiny.config.js | 8 + packages/app-utils/.babelrc.js | 1 + packages/app-utils/LICENSE | 21 + packages/app-utils/README.md | 18 + packages/app-utils/package.json | 36 + .../src/fta/Domain/Models/Meta/Meta.ts | 29 + .../src/fta/Domain/Models/Meta/MetaMapper.ts | 11 + .../src/fta/Domain/Models/Meta/index.ts | 2 + .../src/fta/Domain/Models/Sorting/Sorting.ts | 18 + .../Domain/Models/Sorting/SortingMapper.ts | 47 ++ .../src/fta/Domain/Models/Sorting/index.ts | 2 + .../app-utils/src/fta/Domain/Models/index.ts | 2 + .../Loading/ILoadingRepository.ts | 5 + .../Repositories/Loading/LoadingRepository.ts | 26 + .../Loading/LoadingRepositoryFactory.ts | 21 + .../fta/Domain/Repositories/Loading/index.ts | 3 + .../Repositories/Meta/IMetaRepository.ts | 8 + .../Repositories/Meta/MetaRepository.ts | 45 ++ .../Meta/MetaRepositoryFactory.ts | 21 + .../src/fta/Domain/Repositories/Meta/index.ts | 3 + .../Sorting/ISortingRepository.ts | 6 + .../Repositories/Sorting/SortingRepository.ts | 19 + .../Sorting/SortingRepositoryFactory.ts | 21 + .../fta/Domain/Repositories/Sorting/index.ts | 3 + .../src/fta/Domain/Repositories/index.ts | 3 + packages/app-utils/src/fta/Domain/index.ts | 2 + packages/app-utils/src/fta/index.ts | 1 + packages/app-utils/src/index.ts | 1 + packages/app-utils/tsconfig.build.json | 12 + packages/app-utils/tsconfig.json | 17 + packages/app-utils/webiny.config.js | 8 + packages/ui/src/DataTable/DataTable.tsx | 4 + scripts/listPackagesWithTests.js | 3 + yarn.lock | 55 ++ 219 files changed, 4292 insertions(+), 66 deletions(-) create mode 100644 packages/app-aco/src/config/table/Sorting.tsx create mode 100644 packages/app-headless-cms/src/admin/components/ContentEntries/TrashBin/adapters/TrashBinDeleteItemGraphQLGateway.ts create mode 100644 packages/app-headless-cms/src/admin/components/ContentEntries/TrashBin/adapters/TrashBinItemMapper.ts create mode 100644 packages/app-headless-cms/src/admin/components/ContentEntries/TrashBin/adapters/TrashBinListGraphQLGateway.ts create mode 100644 packages/app-headless-cms/src/admin/components/ContentEntries/TrashBin/adapters/TrashBinRestoreItemGraphQLGateway.ts create mode 100644 packages/app-headless-cms/src/admin/components/ContentEntries/TrashBin/adapters/index.ts create mode 100644 packages/app-headless-cms/src/admin/components/ContentEntries/TrashBin/components/TrashBin.styled.tsx create mode 100644 packages/app-headless-cms/src/admin/components/ContentEntries/TrashBin/components/TrashBin.tsx create mode 100644 packages/app-headless-cms/src/admin/components/ContentEntries/TrashBin/components/TrashBinButton.tsx create mode 100644 packages/app-headless-cms/src/admin/components/ContentEntries/TrashBin/components/index.ts create mode 100644 packages/app-headless-cms/src/admin/components/ContentEntries/TrashBin/index.ts create mode 100644 packages/app-trash-bin/.babelrc.js create mode 100644 packages/app-trash-bin/LICENSE create mode 100644 packages/app-trash-bin/README.md create mode 100644 packages/app-trash-bin/jest.config.js create mode 100644 packages/app-trash-bin/package.json create mode 100644 packages/app-trash-bin/src/Domain/Models/TrashBinItem/ITrashBinItemMapper.ts create mode 100644 packages/app-trash-bin/src/Domain/Models/TrashBinItem/TrashBinItem.ts create mode 100644 packages/app-trash-bin/src/Domain/Models/TrashBinItem/index.ts create mode 100644 packages/app-trash-bin/src/Domain/Models/index.ts create mode 100644 packages/app-trash-bin/src/Domain/Repositories/Search/ISearchRepository.ts create mode 100644 packages/app-trash-bin/src/Domain/Repositories/Search/SearchRepository.ts create mode 100644 packages/app-trash-bin/src/Domain/Repositories/Search/SearchRepositoryFactory.ts create mode 100644 packages/app-trash-bin/src/Domain/Repositories/Search/index.ts create mode 100644 packages/app-trash-bin/src/Domain/Repositories/SelectedItems/ISelectedItemsRepository.ts create mode 100644 packages/app-trash-bin/src/Domain/Repositories/SelectedItems/SelectedItemsRepository.ts create mode 100644 packages/app-trash-bin/src/Domain/Repositories/SelectedItems/SelectedItemsRepositoryFactory.ts create mode 100644 packages/app-trash-bin/src/Domain/Repositories/SelectedItems/index.ts create mode 100644 packages/app-trash-bin/src/Domain/Repositories/Sorting/SortingRepositoryWithDefaults.ts create mode 100644 packages/app-trash-bin/src/Domain/Repositories/Sorting/index.ts create mode 100644 packages/app-trash-bin/src/Domain/Repositories/TrashBinItems/ITrashBinItemsRepository.ts create mode 100644 packages/app-trash-bin/src/Domain/Repositories/TrashBinItems/TrashBinItemMapper.ts create mode 100644 packages/app-trash-bin/src/Domain/Repositories/TrashBinItems/TrashBinItemsRepository.ts create mode 100644 packages/app-trash-bin/src/Domain/Repositories/TrashBinItems/TrashBinItemsRepositoryFactory.ts create mode 100644 packages/app-trash-bin/src/Domain/Repositories/TrashBinItems/TrashBinItemsRepositoryWithLoading.ts create mode 100644 packages/app-trash-bin/src/Domain/Repositories/TrashBinItems/index.ts create mode 100644 packages/app-trash-bin/src/Domain/Repositories/index.ts create mode 100644 packages/app-trash-bin/src/Domain/index.ts create mode 100644 packages/app-trash-bin/src/Gateways/TrashBinDeleteItem/ITrashBinDeleteItemGateway.ts create mode 100644 packages/app-trash-bin/src/Gateways/TrashBinDeleteItem/index.ts create mode 100644 packages/app-trash-bin/src/Gateways/TrashBinListItems/ITrashBinListGateway.ts create mode 100644 packages/app-trash-bin/src/Gateways/TrashBinListItems/index.ts create mode 100644 packages/app-trash-bin/src/Gateways/TrashBinRestoreItem/ITrashBinRestoreItemGateway.ts create mode 100644 packages/app-trash-bin/src/Gateways/TrashBinRestoreItem/index.ts create mode 100644 packages/app-trash-bin/src/Gateways/index.ts create mode 100644 packages/app-trash-bin/src/Presentation/TrashBin/TrashBin.test.ts create mode 100644 packages/app-trash-bin/src/Presentation/TrashBin/TrashBin.tsx create mode 100644 packages/app-trash-bin/src/Presentation/TrashBin/TrashBinControllers.ts create mode 100644 packages/app-trash-bin/src/Presentation/TrashBin/TrashBinPresenter.ts create mode 100644 packages/app-trash-bin/src/Presentation/TrashBin/controllers/DeleteItem/DeleteItemController.ts create mode 100644 packages/app-trash-bin/src/Presentation/TrashBin/controllers/DeleteItem/IDeleteItemController.ts create mode 100644 packages/app-trash-bin/src/Presentation/TrashBin/controllers/DeleteItem/index.ts create mode 100644 packages/app-trash-bin/src/Presentation/TrashBin/controllers/ListItems/IListItemsController.ts create mode 100644 packages/app-trash-bin/src/Presentation/TrashBin/controllers/ListItems/ListItemsController.ts create mode 100644 packages/app-trash-bin/src/Presentation/TrashBin/controllers/ListItems/index.ts create mode 100644 packages/app-trash-bin/src/Presentation/TrashBin/controllers/ListMoreItems/IListMoreItemsController.ts create mode 100644 packages/app-trash-bin/src/Presentation/TrashBin/controllers/ListMoreItems/ListMoreItemsController.ts create mode 100644 packages/app-trash-bin/src/Presentation/TrashBin/controllers/ListMoreItems/index.ts create mode 100644 packages/app-trash-bin/src/Presentation/TrashBin/controllers/RestoreItem/IRestoreItemController.ts create mode 100644 packages/app-trash-bin/src/Presentation/TrashBin/controllers/RestoreItem/RestoreItemController.ts create mode 100644 packages/app-trash-bin/src/Presentation/TrashBin/controllers/RestoreItem/index.ts create mode 100644 packages/app-trash-bin/src/Presentation/TrashBin/controllers/SearchItems/ISearchItemsController.ts create mode 100644 packages/app-trash-bin/src/Presentation/TrashBin/controllers/SearchItems/SearchItemsController.ts create mode 100644 packages/app-trash-bin/src/Presentation/TrashBin/controllers/SearchItems/index.ts create mode 100644 packages/app-trash-bin/src/Presentation/TrashBin/controllers/SelectItems/ISelectItemsController.ts create mode 100644 packages/app-trash-bin/src/Presentation/TrashBin/controllers/SelectItems/SelectItemsController.ts create mode 100644 packages/app-trash-bin/src/Presentation/TrashBin/controllers/SelectItems/index.ts create mode 100644 packages/app-trash-bin/src/Presentation/TrashBin/controllers/SortItems/ISortItemsController.ts create mode 100644 packages/app-trash-bin/src/Presentation/TrashBin/controllers/SortItems/SortItemsController.ts create mode 100644 packages/app-trash-bin/src/Presentation/TrashBin/controllers/SortItems/index.ts create mode 100644 packages/app-trash-bin/src/Presentation/TrashBin/controllers/index.ts create mode 100644 packages/app-trash-bin/src/Presentation/TrashBin/index.ts create mode 100644 packages/app-trash-bin/src/Presentation/TrashBinConfigs/TrashBinConfigs.tsx create mode 100644 packages/app-trash-bin/src/Presentation/TrashBinConfigs/index.ts create mode 100644 packages/app-trash-bin/src/Presentation/TrashBinRenderer/TrashBinRenderer.tsx create mode 100644 packages/app-trash-bin/src/Presentation/TrashBinRenderer/index.ts create mode 100644 packages/app-trash-bin/src/Presentation/abstractions/ITrashBinControllers.ts create mode 100644 packages/app-trash-bin/src/Presentation/abstractions/ITrashBinPresenter.ts create mode 100644 packages/app-trash-bin/src/Presentation/abstractions/index.ts create mode 100644 packages/app-trash-bin/src/Presentation/components/Actions/DeleteItem/DeleteItem.tsx create mode 100644 packages/app-trash-bin/src/Presentation/components/Actions/DeleteItem/index.ts create mode 100644 packages/app-trash-bin/src/Presentation/components/Actions/RestoreItem/RestoreItem.tsx create mode 100644 packages/app-trash-bin/src/Presentation/components/Actions/RestoreItem/index.ts create mode 100644 packages/app-trash-bin/src/Presentation/components/Actions/index.ts create mode 100644 packages/app-trash-bin/src/Presentation/components/BottomInfoBar/BottomInfoBar.styled.tsx create mode 100644 packages/app-trash-bin/src/Presentation/components/BottomInfoBar/BottomInfoBar.tsx create mode 100644 packages/app-trash-bin/src/Presentation/components/BottomInfoBar/ListMeta.tsx create mode 100644 packages/app-trash-bin/src/Presentation/components/BottomInfoBar/ListStatus.tsx create mode 100644 packages/app-trash-bin/src/Presentation/components/BottomInfoBar/index.tsx create mode 100644 packages/app-trash-bin/src/Presentation/components/BulkActions/BulkActions/BulkActions.styled.tsx create mode 100644 packages/app-trash-bin/src/Presentation/components/BulkActions/BulkActions/BulkActions.tsx create mode 100644 packages/app-trash-bin/src/Presentation/components/BulkActions/BulkActions/index.ts create mode 100644 packages/app-trash-bin/src/Presentation/components/BulkActions/DeleteItems/DeleteItems.tsx create mode 100644 packages/app-trash-bin/src/Presentation/components/BulkActions/DeleteItems/index.ts create mode 100644 packages/app-trash-bin/src/Presentation/components/BulkActions/RestoreItems/RestoreItems.tsx create mode 100644 packages/app-trash-bin/src/Presentation/components/BulkActions/RestoreItems/RestoreItemsReportMessage.tsx create mode 100644 packages/app-trash-bin/src/Presentation/components/BulkActions/RestoreItems/RestoredItems.styled.tsx create mode 100644 packages/app-trash-bin/src/Presentation/components/BulkActions/RestoreItems/index.ts create mode 100644 packages/app-trash-bin/src/Presentation/components/BulkActions/index.ts create mode 100644 packages/app-trash-bin/src/Presentation/components/Cells/CellActions/CellActions.tsx create mode 100644 packages/app-trash-bin/src/Presentation/components/Cells/CellActions/index.ts create mode 100644 packages/app-trash-bin/src/Presentation/components/Cells/CellCreatedBy/CellCreatedBy.tsx create mode 100644 packages/app-trash-bin/src/Presentation/components/Cells/CellCreatedBy/index.ts create mode 100644 packages/app-trash-bin/src/Presentation/components/Cells/CellDeletedBy/CellDeletedBy.tsx create mode 100644 packages/app-trash-bin/src/Presentation/components/Cells/CellDeletedBy/index.ts create mode 100644 packages/app-trash-bin/src/Presentation/components/Cells/CellDeletedOn/CellDeletedOn.tsx create mode 100644 packages/app-trash-bin/src/Presentation/components/Cells/CellDeletedOn/index.ts create mode 100644 packages/app-trash-bin/src/Presentation/components/Cells/CellTitle/CellTitle.styled.tsx create mode 100644 packages/app-trash-bin/src/Presentation/components/Cells/CellTitle/CellTitle.tsx create mode 100644 packages/app-trash-bin/src/Presentation/components/Cells/CellTitle/index.ts create mode 100644 packages/app-trash-bin/src/Presentation/components/Cells/index.ts create mode 100644 packages/app-trash-bin/src/Presentation/components/Empty/Empty.styled.tsx create mode 100644 packages/app-trash-bin/src/Presentation/components/Empty/Empty.tsx create mode 100644 packages/app-trash-bin/src/Presentation/components/Empty/index.ts create mode 100644 packages/app-trash-bin/src/Presentation/components/SearchInput/SearchInput.styled.tsx create mode 100644 packages/app-trash-bin/src/Presentation/components/SearchInput/SearchInput.tsx create mode 100644 packages/app-trash-bin/src/Presentation/components/SearchInput/index.ts create mode 100644 packages/app-trash-bin/src/Presentation/components/Table/Table.tsx create mode 100644 packages/app-trash-bin/src/Presentation/components/Table/index.ts create mode 100644 packages/app-trash-bin/src/Presentation/components/Title/Title.styled.tsx create mode 100644 packages/app-trash-bin/src/Presentation/components/Title/Title.tsx create mode 100644 packages/app-trash-bin/src/Presentation/components/Title/index.tsx create mode 100644 packages/app-trash-bin/src/Presentation/components/TrashBinOverlay/TrashBinOverlay.tsx create mode 100644 packages/app-trash-bin/src/Presentation/components/TrashBinOverlay/index.ts create mode 100644 packages/app-trash-bin/src/Presentation/configs/index.ts create mode 100644 packages/app-trash-bin/src/Presentation/configs/list/Browser/BulkAction.tsx create mode 100644 packages/app-trash-bin/src/Presentation/configs/list/Browser/EntryAction.tsx create mode 100644 packages/app-trash-bin/src/Presentation/configs/list/Browser/Table/Column.tsx create mode 100644 packages/app-trash-bin/src/Presentation/configs/list/Browser/Table/Sorting.tsx create mode 100644 packages/app-trash-bin/src/Presentation/configs/list/Browser/Table/index.ts create mode 100644 packages/app-trash-bin/src/Presentation/configs/list/Browser/index.ts create mode 100644 packages/app-trash-bin/src/Presentation/configs/list/TrashBinListConfig.tsx create mode 100644 packages/app-trash-bin/src/Presentation/configs/list/index.ts create mode 100644 packages/app-trash-bin/src/Presentation/hooks/index.ts create mode 100644 packages/app-trash-bin/src/Presentation/hooks/useDeleteTrashBinItem.tsx create mode 100644 packages/app-trash-bin/src/Presentation/hooks/useRestoreTrashBinItem.tsx create mode 100644 packages/app-trash-bin/src/Presentation/hooks/useTrashBin.tsx create mode 100644 packages/app-trash-bin/src/Presentation/hooks/useTrashBinItem.tsx create mode 100644 packages/app-trash-bin/src/Presentation/index.tsx create mode 100644 packages/app-trash-bin/src/UseCases/DeleteItem/DeleteItemUseCase.ts create mode 100644 packages/app-trash-bin/src/UseCases/DeleteItem/IDeleteItemUseCase.ts create mode 100644 packages/app-trash-bin/src/UseCases/DeleteItem/index.ts create mode 100644 packages/app-trash-bin/src/UseCases/ListItems/IListItemsUseCase.ts create mode 100644 packages/app-trash-bin/src/UseCases/ListItems/ListItemsUseCase.ts create mode 100644 packages/app-trash-bin/src/UseCases/ListItems/ListItemsUseCaseWithSearch.ts create mode 100644 packages/app-trash-bin/src/UseCases/ListItems/ListItemsUseCaseWithSorting.ts create mode 100644 packages/app-trash-bin/src/UseCases/ListItems/index.ts create mode 100644 packages/app-trash-bin/src/UseCases/ListMoreItems/IListMoreItemsUseCase.ts create mode 100644 packages/app-trash-bin/src/UseCases/ListMoreItems/ListMoreItemsUseCase.ts create mode 100644 packages/app-trash-bin/src/UseCases/ListMoreItems/index.ts create mode 100644 packages/app-trash-bin/src/UseCases/RestoreItem/IRestoreItemUseCase.ts create mode 100644 packages/app-trash-bin/src/UseCases/RestoreItem/RestoreItemUseCase.ts create mode 100644 packages/app-trash-bin/src/UseCases/RestoreItem/index.ts create mode 100644 packages/app-trash-bin/src/UseCases/SearchItems/ISearchItemsUseCase.ts create mode 100644 packages/app-trash-bin/src/UseCases/SearchItems/SearchItemsUseCase.ts create mode 100644 packages/app-trash-bin/src/UseCases/SearchItems/index.ts create mode 100644 packages/app-trash-bin/src/UseCases/SelectItems/ISelectItemsUseCase.ts create mode 100644 packages/app-trash-bin/src/UseCases/SelectItems/SelectItemsUseCase.ts create mode 100644 packages/app-trash-bin/src/UseCases/SelectItems/index.ts create mode 100644 packages/app-trash-bin/src/UseCases/SortItems/ISortItemsUseCase.ts create mode 100644 packages/app-trash-bin/src/UseCases/SortItems/SortItemsUseCase.ts create mode 100644 packages/app-trash-bin/src/UseCases/SortItems/index.ts create mode 100644 packages/app-trash-bin/src/UseCases/index.ts create mode 100644 packages/app-trash-bin/src/index.ts create mode 100644 packages/app-trash-bin/src/types.ts create mode 100644 packages/app-trash-bin/tsconfig.build.json create mode 100644 packages/app-trash-bin/tsconfig.json create mode 100644 packages/app-trash-bin/webiny.config.js create mode 100644 packages/app-utils/.babelrc.js create mode 100644 packages/app-utils/LICENSE create mode 100644 packages/app-utils/README.md create mode 100644 packages/app-utils/package.json create mode 100644 packages/app-utils/src/fta/Domain/Models/Meta/Meta.ts create mode 100644 packages/app-utils/src/fta/Domain/Models/Meta/MetaMapper.ts create mode 100644 packages/app-utils/src/fta/Domain/Models/Meta/index.ts create mode 100644 packages/app-utils/src/fta/Domain/Models/Sorting/Sorting.ts create mode 100644 packages/app-utils/src/fta/Domain/Models/Sorting/SortingMapper.ts create mode 100644 packages/app-utils/src/fta/Domain/Models/Sorting/index.ts create mode 100644 packages/app-utils/src/fta/Domain/Models/index.ts create mode 100644 packages/app-utils/src/fta/Domain/Repositories/Loading/ILoadingRepository.ts create mode 100644 packages/app-utils/src/fta/Domain/Repositories/Loading/LoadingRepository.ts create mode 100644 packages/app-utils/src/fta/Domain/Repositories/Loading/LoadingRepositoryFactory.ts create mode 100644 packages/app-utils/src/fta/Domain/Repositories/Loading/index.ts create mode 100644 packages/app-utils/src/fta/Domain/Repositories/Meta/IMetaRepository.ts create mode 100644 packages/app-utils/src/fta/Domain/Repositories/Meta/MetaRepository.ts create mode 100644 packages/app-utils/src/fta/Domain/Repositories/Meta/MetaRepositoryFactory.ts create mode 100644 packages/app-utils/src/fta/Domain/Repositories/Meta/index.ts create mode 100644 packages/app-utils/src/fta/Domain/Repositories/Sorting/ISortingRepository.ts create mode 100644 packages/app-utils/src/fta/Domain/Repositories/Sorting/SortingRepository.ts create mode 100644 packages/app-utils/src/fta/Domain/Repositories/Sorting/SortingRepositoryFactory.ts create mode 100644 packages/app-utils/src/fta/Domain/Repositories/Sorting/index.ts create mode 100644 packages/app-utils/src/fta/Domain/Repositories/index.ts create mode 100644 packages/app-utils/src/fta/Domain/index.ts create mode 100644 packages/app-utils/src/fta/index.ts create mode 100644 packages/app-utils/src/index.ts create mode 100644 packages/app-utils/tsconfig.build.json create mode 100644 packages/app-utils/tsconfig.json create mode 100644 packages/app-utils/webiny.config.js diff --git a/packages/app-aco/src/config/AcoConfig.tsx b/packages/app-aco/src/config/AcoConfig.tsx index 4d9eaf38f48..3f7092c13d0 100644 --- a/packages/app-aco/src/config/AcoConfig.tsx +++ b/packages/app-aco/src/config/AcoConfig.tsx @@ -9,6 +9,7 @@ export { FieldRendererConfig as AdvancedSearchFieldRendererConfig } from "./adva export { ActionConfig as RecordActionConfig } from "./record/Action"; export { ActionConfig as FolderActionConfig } from "./folder/Action"; export { ColumnConfig as TableColumnConfig } from "./table/Column"; +export { SortingConfig as TableSortingConfig } from "./table/Sorting"; const base = createConfigurableComponent("AcoConfig"); @@ -46,7 +47,8 @@ export function useAcoConfig() { }, table: { ...table, - columns: [...(table.columns || [])] + columns: [...(table.columns || [])], + sorting: [...(table.sorting || [])] } }), [config] diff --git a/packages/app-aco/src/config/table/Sorting.tsx b/packages/app-aco/src/config/table/Sorting.tsx new file mode 100644 index 00000000000..5d289ec3cf4 --- /dev/null +++ b/packages/app-aco/src/config/table/Sorting.tsx @@ -0,0 +1,27 @@ +import React from "react"; +import { Property, useIdGenerator } from "@webiny/react-properties"; + +export interface SortingConfig { + field: string; + order: "asc" | "desc"; +} + +export interface SortingProps { + name: string; + field: string; + order?: "asc" | "desc"; +} + +export const Sorting = ({ name, field, order = "desc" }: SortingProps) => { + const getId = useIdGenerator("tableSorting"); + + return ( + + + + + + + + ); +}; diff --git a/packages/app-aco/src/config/table/index.ts b/packages/app-aco/src/config/table/index.ts index d585009cae2..cda19d4503f 100644 --- a/packages/app-aco/src/config/table/index.ts +++ b/packages/app-aco/src/config/table/index.ts @@ -1,9 +1,11 @@ import { Column, ColumnConfig } from "./Column"; - +import { Sorting, SortingConfig } from "./Sorting"; export interface TableConfig { columns: ColumnConfig[]; + sorting: SortingConfig[]; } export const Table = { - Column + Column, + Sorting }; diff --git a/packages/app-admin/src/components/BulkActions/Worker.ts b/packages/app-admin/src/components/BulkActions/Worker.ts index fe41d34b311..40790680252 100644 --- a/packages/app-admin/src/components/BulkActions/Worker.ts +++ b/packages/app-admin/src/components/BulkActions/Worker.ts @@ -10,7 +10,7 @@ export interface CallbackParams { export interface Result { title: string; status: "success" | "failure"; - message?: string; + message?: string | React.ReactElement; } /** diff --git a/packages/app-admin/src/components/BulkActions/useDialogWithReport/useDialogWithReport.tsx b/packages/app-admin/src/components/BulkActions/useDialogWithReport/useDialogWithReport.tsx index 1899214c492..eca574809e1 100644 --- a/packages/app-admin/src/components/BulkActions/useDialogWithReport/useDialogWithReport.tsx +++ b/packages/app-admin/src/components/BulkActions/useDialogWithReport/useDialogWithReport.tsx @@ -18,11 +18,13 @@ export interface ShowResultsDialogParams { results: Result[]; title?: string; message?: string; + onCancel?: () => Promise; } export interface UseDialogWithReportResponse { showConfirmationDialog: (params: ShowConfirmationDialogParams) => void; showResultsDialog: (results: ShowResultsDialogParams) => void; + hideResultsDialog: () => void; } export const useDialogWithReport = (): UseDialogWithReportResponse => { @@ -80,8 +82,15 @@ export const useDialogWithReport = (): UseDialogWithReportResponse => { }, 10); }; + const hideResultsDialog = () => { + ui.setState(ui => { + return { ...ui, dialog: null }; + }); + }; + return { showConfirmationDialog, - showResultsDialog + showResultsDialog, + hideResultsDialog }; }; diff --git a/packages/app-admin/src/components/SplitView/SplitView.tsx b/packages/app-admin/src/components/SplitView/SplitView.tsx index 37e7d5c7ad8..166c409ddd8 100644 --- a/packages/app-admin/src/components/SplitView/SplitView.tsx +++ b/packages/app-admin/src/components/SplitView/SplitView.tsx @@ -20,7 +20,7 @@ const grid = css({ const RightPanelWrapper = styled("div")({ backgroundColor: "var(--mdc-theme-background)", overflow: "auto", - height: "calc(100vh - 70px)" + height: "calc(100vh - 64px)" }); export const leftPanel = css({ @@ -28,7 +28,7 @@ export const leftPanel = css({ ">.webiny-data-list": { display: "flex", flexDirection: "column", - height: "calc(100vh - 70px)", + height: "calc(100vh - 64px)", ".mdc-list": { overflow: "auto" } @@ -36,7 +36,7 @@ export const leftPanel = css({ ">.mdc-list": { display: "flex", flexDirection: "column", - maxHeight: "calc(100vh - 70px)", + maxHeight: "calc(100vh - 64px)", overflow: "auto" } }); diff --git a/packages/app-headless-cms-common/src/entries.graphql.ts b/packages/app-headless-cms-common/src/entries.graphql.ts index 4fd5e31fb06..b3254a43399 100644 --- a/packages/app-headless-cms-common/src/entries.graphql.ts +++ b/packages/app-headless-cms-common/src/entries.graphql.ts @@ -27,7 +27,8 @@ const CONTENT_ENTRY_SYSTEM_FIELDS = /* GraphQL */ ` entryId createdOn savedOn - modifiedOn + modifiedOn, + deletedOn firstPublishedOn lastPublishedOn createdBy { @@ -45,6 +46,11 @@ const CONTENT_ENTRY_SYSTEM_FIELDS = /* GraphQL */ ` type displayName } + deletedBy { + id + type + displayName + } firstPublishedBy { id type @@ -58,6 +64,7 @@ const CONTENT_ENTRY_SYSTEM_FIELDS = /* GraphQL */ ` revisionCreatedOn revisionSavedOn revisionModifiedOn + revisionDeletedOn revisionFirstPublishedOn revisionLastPublishedOn revisionCreatedBy { @@ -75,6 +82,11 @@ const CONTENT_ENTRY_SYSTEM_FIELDS = /* GraphQL */ ` type displayName } + revisionDeletedBy { + id + type + displayName + } revisionFirstPublishedBy { id type @@ -213,14 +225,18 @@ export interface CmsEntriesListQueryVariables { after?: string; } -export const createListQuery = (model: CmsEditorContentModel, fields?: CmsModelField[]) => { +export const createListQuery = ( + model: CmsEditorContentModel, + fields?: CmsModelField[], + deleted?: boolean +) => { + const queryName = deleted ? `Deleted${model.pluralApiName}` : model.pluralApiName; + return gql` - query CmsEntriesList${model.pluralApiName}($where: ${ - model.singularApiName - }ListWhereInput, $sort: [${ + query CmsEntriesList${queryName}($where: ${model.singularApiName}ListWhereInput, $sort: [${ model.singularApiName }ListSorter], $limit: Int, $after: String, $search: String) { - content: list${model.pluralApiName}( + content: list${queryName}( where: $where sort: $sort limit: $limit @@ -256,12 +272,13 @@ export interface CmsEntryDeleteMutationResponse { export interface CmsEntryDeleteMutationVariables { revision: string; + permanently?: boolean; } export const createDeleteMutation = (model: CmsEditorContentModel) => { return gql` - mutation CmsEntriesDelete${model.singularApiName}($revision: ID!) { - content: delete${model.singularApiName}(revision: $revision) { + mutation CmsEntriesDelete${model.singularApiName}($revision: ID!, $permanently: Boolean) { + content: delete${model.singularApiName}(revision: $revision, options: {permanently: $permanently}) { data error ${ERROR_FIELD} } @@ -269,6 +286,35 @@ export const createDeleteMutation = (model: CmsEditorContentModel) => { `; }; +/** + * ############################################ + * Restore Mutation + */ +export interface CmsEntryRestoreMutationResponse { + content: { + data: CmsContentEntry | null; + error: CmsErrorResponse | null; + }; +} + +export interface CmsEntryRestoreMutationVariables { + revision: string; +} + +export const createRestoreMutation = (model: CmsEditorContentModel) => { + return gql` + mutation CmsEntriesRestore${model.singularApiName}($revision: ID!) { + content: restore${model.singularApiName}(revision: $revision) { + data { + ${CONTENT_ENTRY_SYSTEM_FIELDS} + ${createFieldsList({ model, fields: model.fields })} + } + error ${ERROR_FIELD} + } + } + `; +}; + /** * ############################################ * Create Mutation diff --git a/packages/app-headless-cms-common/src/types/index.ts b/packages/app-headless-cms-common/src/types/index.ts index 89c3a8399c8..210275590ab 100644 --- a/packages/app-headless-cms-common/src/types/index.ts +++ b/packages/app-headless-cms-common/src/types/index.ts @@ -351,6 +351,8 @@ export interface CmsContentEntry { savedBy: CmsIdentity; modifiedOn: string | null; modifiedBy: CmsIdentity | null; + deletedOn: string | null; + deletedBy: CmsIdentity | null; firstPublishedOn: string | null; firstPublishedBy: CmsIdentity | null; lastPublishedOn: string | null; @@ -361,6 +363,8 @@ export interface CmsContentEntry { revisionSavedBy: CmsIdentity; revisionModifiedOn: string | null; revisionModifiedBy: CmsIdentity | null; + revisionDeletedOn: string | null; + revisionDeletedBy: CmsIdentity | null; revisionFirstPublishedOn: string | null; revisionFirstPublishedBy: CmsIdentity | null; revisionLastPublishedOn: string | null; @@ -381,17 +385,21 @@ export interface CmsContentEntryRevision { id: string; modelId: string; savedOn: string; + deletedOn: string | null; firstPublishedOn: string | null; lastPublishedOn: string | null; createdBy: CmsIdentity; + deletedBy: CmsIdentity | null; revisionCreatedOn: string; revisionSavedOn: string; revisionModifiedOn: string; + revisionDeletedOn: string | null; revisionFirstPublishedOn: string; revisionLastPublishedOn: string; revisionCreatedBy: CmsIdentity; revisionSavedBy: CmsIdentity; revisionModifiedBy: CmsIdentity; + revisionDeletedBy: CmsIdentity | null; revisionFirstPublishedBy: CmsIdentity; revisionLastPublishedBy: CmsIdentity; wbyAco_location: Location; diff --git a/packages/app-headless-cms/package.json b/packages/app-headless-cms/package.json index 44c7456aa50..8b479fc60ae 100644 --- a/packages/app-headless-cms/package.json +++ b/packages/app-headless-cms/package.json @@ -36,6 +36,7 @@ "@webiny/app-i18n": "0.0.0", "@webiny/app-plugin-admin-welcome-screen": "0.0.0", "@webiny/app-security": "0.0.0", + "@webiny/app-trash-bin": "0.0.0", "@webiny/error": "0.0.0", "@webiny/feature-flags": "0.0.0", "@webiny/form": "0.0.0", diff --git a/packages/app-headless-cms/src/admin/components/ContentEntries/BulkActions/ActionDelete.tsx b/packages/app-headless-cms/src/admin/components/ContentEntries/BulkActions/ActionDelete.tsx index 51f1a91c4e9..5820c178f97 100644 --- a/packages/app-headless-cms/src/admin/components/ContentEntries/BulkActions/ActionDelete.tsx +++ b/packages/app-headless-cms/src/admin/components/ContentEntries/BulkActions/ActionDelete.tsx @@ -23,8 +23,8 @@ export const ActionDelete = observer(() => { const openDeleteEntriesDialog = () => showConfirmationDialog({ - title: "Delete entries", - message: `You are about to delete ${entriesLabel}. Are you sure you want to continue?`, + title: "Trash entries", + message: `You are about to trash ${entriesLabel}. Are you sure you want to continue?`, loadingLabel: `Processing ${entriesLabel}`, execute: async () => { await worker.processInSeries(async ({ item, report }) => { @@ -44,7 +44,7 @@ export const ActionDelete = observer(() => { if (error) { throw new Error( - error.message || "Unknown error while deleting the entry" + error.message || "Unknown error while trashing the entry." ); } @@ -52,7 +52,7 @@ export const ActionDelete = observer(() => { report.success({ title: `${item.meta.title}`, - message: "Entry successfully deleted." + message: "Entry successfully trashed." }); } catch (e) { report.error({ @@ -66,8 +66,8 @@ export const ActionDelete = observer(() => { showResultsDialog({ results: worker.results, - title: "Delete entries", - message: "Finished deleting entries! See full report below:" + title: "Trash entries", + message: "Finished trashing entries! See full report below:" }); } }); @@ -76,7 +76,7 @@ export const ActionDelete = observer(() => { } onAction={openDeleteEntriesDialog} - label={`Delete ${entriesLabel}`} + label={`Trash ${entriesLabel}`} tooltipPlacement={"bottom"} /> ); diff --git a/packages/app-headless-cms/src/admin/components/ContentEntries/Table/Actions/DeleteEntry.tsx b/packages/app-headless-cms/src/admin/components/ContentEntries/Table/Actions/DeleteEntry.tsx index 1607a700e49..19de4039527 100644 --- a/packages/app-headless-cms/src/admin/components/ContentEntries/Table/Actions/DeleteEntry.tsx +++ b/packages/app-headless-cms/src/admin/components/ContentEntries/Table/Actions/DeleteEntry.tsx @@ -16,7 +16,7 @@ export const DeleteEntry = () => { return ( } - label={"Delete"} + label={"Trash"} onAction={openDialogDeleteEntry} data-testid={"aco.actions.entry.delete"} /> diff --git a/packages/app-headless-cms/src/admin/components/ContentEntries/TrashBin/adapters/TrashBinDeleteItemGraphQLGateway.ts b/packages/app-headless-cms/src/admin/components/ContentEntries/TrashBin/adapters/TrashBinDeleteItemGraphQLGateway.ts new file mode 100644 index 00000000000..05e0ef3fb5f --- /dev/null +++ b/packages/app-headless-cms/src/admin/components/ContentEntries/TrashBin/adapters/TrashBinDeleteItemGraphQLGateway.ts @@ -0,0 +1,43 @@ +import { ApolloClient } from "apollo-client"; +import { ITrashBinDeleteItemGateway } from "@webiny/app-trash-bin"; +import { CmsModel } from "@webiny/app-headless-cms-common/types"; +import { + CmsEntryDeleteMutationResponse, + CmsEntryDeleteMutationVariables, + createDeleteMutation +} from "@webiny/app-headless-cms-common"; + +export class TrashBinDeleteItemGraphQLGateway implements ITrashBinDeleteItemGateway { + private client: ApolloClient; + private model: CmsModel; + + constructor(client: ApolloClient, model: CmsModel) { + this.client = client; + this.model = model; + } + + async execute(id: string) { + const { data: response } = await this.client.mutate< + CmsEntryDeleteMutationResponse, + CmsEntryDeleteMutationVariables + >({ + mutation: createDeleteMutation(this.model), + variables: { + revision: id, + permanently: true + } + }); + + if (!response) { + throw new Error("Network error while deleting entry."); + } + + const { data, error } = response.content; + + if (!data) { + throw new Error(error?.message || "Could not delete the entry."); + } + + return true; + } +} diff --git a/packages/app-headless-cms/src/admin/components/ContentEntries/TrashBin/adapters/TrashBinItemMapper.ts b/packages/app-headless-cms/src/admin/components/ContentEntries/TrashBin/adapters/TrashBinItemMapper.ts new file mode 100644 index 00000000000..63c82d182f2 --- /dev/null +++ b/packages/app-headless-cms/src/admin/components/ContentEntries/TrashBin/adapters/TrashBinItemMapper.ts @@ -0,0 +1,19 @@ +import { TrashBinItemDTO, ITrashBinItemMapper } from "@webiny/app-trash-bin"; +import { CmsContentEntry } from "@webiny/app-headless-cms-common/types"; + +export class TrashBinItemMapper implements ITrashBinItemMapper { + toDTO(data: CmsContentEntry): TrashBinItemDTO { + return { + id: data.entryId, + title: data.meta.title, + location: data.wbyAco_location, + createdBy: data.createdBy, + deletedBy: { + id: data.deletedBy?.id || "", + displayName: data.deletedBy?.displayName || "", + type: data.deletedBy?.type || "" + }, + deletedOn: data.deletedOn || "" + }; + } +} diff --git a/packages/app-headless-cms/src/admin/components/ContentEntries/TrashBin/adapters/TrashBinListGraphQLGateway.ts b/packages/app-headless-cms/src/admin/components/ContentEntries/TrashBin/adapters/TrashBinListGraphQLGateway.ts new file mode 100644 index 00000000000..b9b69080030 --- /dev/null +++ b/packages/app-headless-cms/src/admin/components/ContentEntries/TrashBin/adapters/TrashBinListGraphQLGateway.ts @@ -0,0 +1,53 @@ +import { ApolloClient } from "apollo-client"; +import { ITrashBinListGateway } from "@webiny/app-trash-bin"; +import { + CmsEntriesListQueryResponse, + CmsEntriesListQueryVariables, + createListQuery +} from "@webiny/app-headless-cms-common"; +import { CmsContentEntry, CmsMetaResponse, CmsModel } from "@webiny/app-headless-cms-common/types"; + +export class TrashBinListGraphQLGateway implements ITrashBinListGateway { + private client: ApolloClient; + private model: CmsModel; + + constructor(client: ApolloClient, model: CmsModel) { + this.client = client; + this.model = model; + } + + async execute( + params: CmsEntriesListQueryVariables + ): Promise<[CmsContentEntry[], CmsMetaResponse]> { + const { data: response } = await this.client.query< + CmsEntriesListQueryResponse, + CmsEntriesListQueryVariables + >({ + query: createListQuery(this.model, this.getFields(), true), + variables: { + ...params + }, + fetchPolicy: "network-only" + }); + + if (!response) { + throw new Error("Network error while listing deleted entries."); + } + + const { data, error, meta } = response.content; + + if (!data && !meta) { + throw new Error(error?.message || "Could not fetch deleted entries."); + } + + return [data, meta]; + } + + private getFields() { + return this.model.fields.filter(field => { + return ["text", "number", "boolean", "file", "long-text", "ref", "datetime"].includes( + field.type + ); + }); + } +} diff --git a/packages/app-headless-cms/src/admin/components/ContentEntries/TrashBin/adapters/TrashBinRestoreItemGraphQLGateway.ts b/packages/app-headless-cms/src/admin/components/ContentEntries/TrashBin/adapters/TrashBinRestoreItemGraphQLGateway.ts new file mode 100644 index 00000000000..74a2e90b453 --- /dev/null +++ b/packages/app-headless-cms/src/admin/components/ContentEntries/TrashBin/adapters/TrashBinRestoreItemGraphQLGateway.ts @@ -0,0 +1,44 @@ +import { ApolloClient } from "apollo-client"; +import { CmsContentEntry, CmsModel } from "@webiny/app-headless-cms-common/types"; +import { + CmsEntryRestoreMutationResponse, + CmsEntryRestoreMutationVariables, + createRestoreMutation +} from "@webiny/app-headless-cms-common"; +import { ITrashBinRestoreItemGateway } from "@webiny/app-trash-bin"; + +export class TrashBinRestoreItemGraphQLGateway + implements ITrashBinRestoreItemGateway +{ + private client: ApolloClient; + private model: CmsModel; + + constructor(client: ApolloClient, model: CmsModel) { + this.client = client; + this.model = model; + } + + async execute(id: string) { + const { data: response } = await this.client.mutate< + CmsEntryRestoreMutationResponse, + CmsEntryRestoreMutationVariables + >({ + mutation: createRestoreMutation(this.model), + variables: { + revision: id + } + }); + + if (!response) { + throw new Error("Network error while restoring entry."); + } + + const { data, error } = response.content; + + if (!data) { + throw new Error(error?.message || "Could not fetch the restored entry."); + } + + return data; + } +} diff --git a/packages/app-headless-cms/src/admin/components/ContentEntries/TrashBin/adapters/index.ts b/packages/app-headless-cms/src/admin/components/ContentEntries/TrashBin/adapters/index.ts new file mode 100644 index 00000000000..2415852b3aa --- /dev/null +++ b/packages/app-headless-cms/src/admin/components/ContentEntries/TrashBin/adapters/index.ts @@ -0,0 +1,4 @@ +export * from "./TrashBinListGraphQLGateway"; +export * from "./TrashBinDeleteItemGraphQLGateway"; +export * from "./TrashBinRestoreItemGraphQLGateway"; +export * from "./TrashBinItemMapper"; diff --git a/packages/app-headless-cms/src/admin/components/ContentEntries/TrashBin/components/TrashBin.styled.tsx b/packages/app-headless-cms/src/admin/components/ContentEntries/TrashBin/components/TrashBin.styled.tsx new file mode 100644 index 00000000000..3ff837314cd --- /dev/null +++ b/packages/app-headless-cms/src/admin/components/ContentEntries/TrashBin/components/TrashBin.styled.tsx @@ -0,0 +1,34 @@ +import styled from "@emotion/styled"; +import { ReactComponent as Plus } from "@material-design-icons/svg/filled/delete.svg"; + +export const Button = styled("button")` + background: none; + border: none; + cursor: pointer; + outline: none; + font-family: var(--mdc-typography-font-family); + color: var(--webiny-theme-color-text-secondary); + fill: currentColor; + display: flex; + align-items: center; + font-size: 1em; + padding: 16px; + width: 100%; + + &:disabled { + cursor: not-allowed; + opacity: 0.5; + } +`; + +export const IconContainer = styled.div` + display: flex; + align-items: center; + justify-content: center; + cursor: pointer; + fill: var(--mdc-theme-text-secondary-on-background); +`; + +export const Icon = styled(Plus)` + padding: 0 4px; +`; diff --git a/packages/app-headless-cms/src/admin/components/ContentEntries/TrashBin/components/TrashBin.tsx b/packages/app-headless-cms/src/admin/components/ContentEntries/TrashBin/components/TrashBin.tsx new file mode 100644 index 00000000000..30f477bb96f --- /dev/null +++ b/packages/app-headless-cms/src/admin/components/ContentEntries/TrashBin/components/TrashBin.tsx @@ -0,0 +1,54 @@ +import React, { useMemo } from "react"; +import { useApolloClient, useModel, usePermission } from "~/admin/hooks"; +import { TrashBin as BaseTrashBin } from "@webiny/app-trash-bin"; +import { + TrashBinDeleteItemGraphQLGateway, + TrashBinListGraphQLGateway, + TrashBinRestoreItemGraphQLGateway, + TrashBinItemMapper +} from "../adapters"; + +import { TrashBinButton } from "./TrashBinButton"; +import { useNavigateFolder } from "@webiny/app-aco"; + +export const TrashBin = () => { + const client = useApolloClient(); + const { canDeleteEntries } = usePermission(); + const { navigateToFolder } = useNavigateFolder(); + const { model } = useModel(); + + const listGateway = useMemo(() => { + return new TrashBinListGraphQLGateway(client, model); + }, [client, model]); + + const deleteGateway = useMemo(() => { + return new TrashBinDeleteItemGraphQLGateway(client, model); + }, [client, model]); + + const restoreGateway = useMemo(() => { + return new TrashBinRestoreItemGraphQLGateway(client, model); + }, [client, model]); + + const itemMapper = useMemo(() => { + return new TrashBinItemMapper(); + }, []); + + if (!canDeleteEntries("cms.contentEntry")) { + return null; + } + + return ( + { + return ; + }} + listGateway={listGateway} + deleteGateway={deleteGateway} + restoreGateway={restoreGateway} + itemMapper={itemMapper} + onItemRestore={async item => navigateToFolder(item.location.folderId)} + nameColumnId={model.titleFieldId || "id"} + title={`Trash - ${model.name}`} + /> + ); +}; diff --git a/packages/app-headless-cms/src/admin/components/ContentEntries/TrashBin/components/TrashBinButton.tsx b/packages/app-headless-cms/src/admin/components/ContentEntries/TrashBin/components/TrashBinButton.tsx new file mode 100644 index 00000000000..33878b5ed41 --- /dev/null +++ b/packages/app-headless-cms/src/admin/components/ContentEntries/TrashBin/components/TrashBinButton.tsx @@ -0,0 +1,18 @@ +import React from "react"; +import { Typography } from "@webiny/ui/Typography"; +import { Button, Icon, IconContainer } from "./TrashBin.styled"; + +export interface TrashBinButtonProps { + onClick: () => void; +} + +export const TrashBinButton = (props: TrashBinButtonProps) => { + return ( + + ); +}; diff --git a/packages/app-headless-cms/src/admin/components/ContentEntries/TrashBin/components/index.ts b/packages/app-headless-cms/src/admin/components/ContentEntries/TrashBin/components/index.ts new file mode 100644 index 00000000000..c9e88d06227 --- /dev/null +++ b/packages/app-headless-cms/src/admin/components/ContentEntries/TrashBin/components/index.ts @@ -0,0 +1 @@ +export * from "./TrashBin"; diff --git a/packages/app-headless-cms/src/admin/components/ContentEntries/TrashBin/index.ts b/packages/app-headless-cms/src/admin/components/ContentEntries/TrashBin/index.ts new file mode 100644 index 00000000000..40b494c5f87 --- /dev/null +++ b/packages/app-headless-cms/src/admin/components/ContentEntries/TrashBin/index.ts @@ -0,0 +1 @@ +export * from "./components"; diff --git a/packages/app-headless-cms/src/admin/hooks/useDeleteEntry.tsx b/packages/app-headless-cms/src/admin/hooks/useDeleteEntry.tsx index 6adb9b1ff10..1000cdfe457 100644 --- a/packages/app-headless-cms/src/admin/hooks/useDeleteEntry.tsx +++ b/packages/app-headless-cms/src/admin/hooks/useDeleteEntry.tsx @@ -22,12 +22,12 @@ export const useDeleteEntry = ({ entry, onAccept, onCancel }: UseDeleteEntryPara const title = get(entry, "meta.title"); const { showConfirmation } = useConfirmationDialog({ - title: "Delete content entry", + title: "Trash entry", message: (

- You are about to delete this content entry and all of its revisions! + Are you sure you want to trash {title}?
- Are you sure you want to permanently delete {title}? + This action will include all of its revisions?

), dataTestId: "cms.content-form.header.delete-dialog" @@ -44,11 +44,11 @@ export const useDeleteEntry = ({ entry, onAccept, onCancel }: UseDeleteEntryPara }); if (error) { - showDialog(error.message, { title: "Could not delete content!" }); + showDialog(error.message, { title: `Could not trash ${title}!` }); return; } - showSnackbar(`${title} was deleted successfully!`); + showSnackbar(`${title} has been trashed successfully!`); removeRecordFromCache(entry.id); navigateToLatestFolder(); diff --git a/packages/app-headless-cms/src/admin/hooks/usePermission.ts b/packages/app-headless-cms/src/admin/hooks/usePermission.ts index 1729d4354c9..52ec31fdc69 100644 --- a/packages/app-headless-cms/src/admin/hooks/usePermission.ts +++ b/packages/app-headless-cms/src/admin/hooks/usePermission.ts @@ -217,6 +217,24 @@ export const usePermission = () => { [identity] ); + const canDeleteEntries = useCallback( + (permissionName: string): boolean => { + if (hasFullAccess) { + return true; + } + const permissions = getPermissions(permissionName); + + if (!permissions.length) { + return false; + } + + return permissions.some(permission => { + return permission.rwd?.includes("d"); + }); + }, + [identity, hasFullAccess] + ); + const canPublish = useCallback( (permissionName: string): boolean => { if (hasFullAccess) { @@ -266,6 +284,7 @@ export const usePermission = () => { canEdit, canCreate, canDelete, + canDeleteEntries, canPublish, canUnpublish, canReadContentModels, diff --git a/packages/app-headless-cms/src/admin/plugins/entry/DefaultOnEntryDelete.tsx b/packages/app-headless-cms/src/admin/plugins/entry/DefaultOnEntryDelete.tsx index c7b75e2319f..0fc69fafa41 100644 --- a/packages/app-headless-cms/src/admin/plugins/entry/DefaultOnEntryDelete.tsx +++ b/packages/app-headless-cms/src/admin/plugins/entry/DefaultOnEntryDelete.tsx @@ -47,7 +47,8 @@ const OnEntryDelete = () => { >({ mutation, variables: { - revision: id + revision: id, + permanently: false } }); diff --git a/packages/app-headless-cms/src/admin/views/contentEntries/Table/Sidebar.tsx b/packages/app-headless-cms/src/admin/views/contentEntries/Table/Sidebar.tsx index 56fff865787..0c506bc1424 100644 --- a/packages/app-headless-cms/src/admin/views/contentEntries/Table/Sidebar.tsx +++ b/packages/app-headless-cms/src/admin/views/contentEntries/Table/Sidebar.tsx @@ -1,13 +1,14 @@ import React from "react"; import styled from "@emotion/styled"; import { FolderTree, useNavigateFolder } from "@webiny/app-aco"; -import { SidebarContainer } from "./styled"; +import { SidebarContainer, SidebarContent, SidebarFooter } from "./styled"; import { Typography } from "@webiny/ui/Typography"; import { Tooltip } from "@webiny/ui/Tooltip"; import { Link } from "@webiny/react-router"; import { useModel } from "~/admin/components/ModelProvider"; import { i18n } from "@webiny/app/i18n"; import { css } from "emotion"; +import { TrashBin } from "~/admin/components/ContentEntries/TrashBin/components/TrashBin"; const t = i18n.ns("app-headless-cms/admin/content-entries/table"); @@ -36,37 +37,42 @@ export const Sidebar = ({ folderId }: SidebarProps) => { return ( - - {model.name} -
- - - Model ID:{" "} - {model.plugin ? ( - - - {model.modelId} - - - ) : ( - - - {model.modelId} - - - )} - - -
- navigateToFolder(data.id)} - enableActions={true} - enableCreate={true} - /> + + + {model.name} +
+ + + Model ID:{" "} + {model.plugin ? ( + + + {model.modelId} + + + ) : ( + + + {model.modelId} + + + )} + + +
+ navigateToFolder(data.id)} + enableActions={true} + enableCreate={true} + /> +
+ + +
); }; diff --git a/packages/app-headless-cms/src/admin/views/contentEntries/Table/styled.tsx b/packages/app-headless-cms/src/admin/views/contentEntries/Table/styled.tsx index c9cc35f4a03..7ac85133359 100644 --- a/packages/app-headless-cms/src/admin/views/contentEntries/Table/styled.tsx +++ b/packages/app-headless-cms/src/admin/views/contentEntries/Table/styled.tsx @@ -21,9 +21,23 @@ export const Wrapper = styled("div")` `; export const SidebarContainer = styled("div")` - height: calc(100vh - 91px); - overflow-y: scroll; + height: calc(100vh - 64px); background: var(--mdc-theme-surface); border-right: 1px solid var(--mdc-theme-on-background); - padding: 10px 10px 0; + position: relative; +`; + +export const SidebarContent = styled("div")` + height: calc(100vh - 136px); + overflow-y: scroll; + padding: 8px; +`; + +export const SidebarFooter = styled("div")` + position: absolute; + bottom: 0; + left: 0; + width: 100%; + background: var(--mdc-theme-surface); + border-top: 1px solid var(--mdc-theme-on-background); `; diff --git a/packages/app-headless-cms/tsconfig.build.json b/packages/app-headless-cms/tsconfig.build.json index 50743cfb3f4..10bbf93e671 100644 --- a/packages/app-headless-cms/tsconfig.build.json +++ b/packages/app-headless-cms/tsconfig.build.json @@ -10,6 +10,7 @@ { "path": "../app-i18n/tsconfig.build.json" }, { "path": "../app-plugin-admin-welcome-screen/tsconfig.build.json" }, { "path": "../app-security/tsconfig.build.json" }, + { "path": "../app-trash-bin/tsconfig.build.json" }, { "path": "../error/tsconfig.build.json" }, { "path": "../feature-flags/tsconfig.build.json" }, { "path": "../form/tsconfig.build.json" }, diff --git a/packages/app-headless-cms/tsconfig.json b/packages/app-headless-cms/tsconfig.json index e00f78476cd..c07ef5c43ed 100644 --- a/packages/app-headless-cms/tsconfig.json +++ b/packages/app-headless-cms/tsconfig.json @@ -4,12 +4,14 @@ "references": [ { "path": "../app" }, { "path": "../app-aco" }, + { "path": "../app-bin" }, { "path": "../app-admin" }, { "path": "../app-graphql-playground" }, { "path": "../app-headless-cms-common" }, { "path": "../app-i18n" }, { "path": "../app-plugin-admin-welcome-screen" }, { "path": "../app-security" }, + { "path": "../app-trash-bin" }, { "path": "../error" }, { "path": "../feature-flags" }, { "path": "../form" }, @@ -48,6 +50,8 @@ "@webiny/app-plugin-admin-welcome-screen": ["../app-plugin-admin-welcome-screen/src"], "@webiny/app-security/*": ["../app-security/src/*"], "@webiny/app-security": ["../app-security/src"], + "@webiny/app-trash-bin/*": ["../app-trash-bin/src/*"], + "@webiny/app-trash-bin": ["../app-trash-bin/src"], "@webiny/error/*": ["../error/src/*"], "@webiny/error": ["../error/src"], "@webiny/feature-flags/*": ["../feature-flags/src/*"], diff --git a/packages/app-page-builder/src/admin/views/Pages/Table/styled.tsx b/packages/app-page-builder/src/admin/views/Pages/Table/styled.tsx index ac7a8a0be96..2dd6e9bec80 100644 --- a/packages/app-page-builder/src/admin/views/Pages/Table/styled.tsx +++ b/packages/app-page-builder/src/admin/views/Pages/Table/styled.tsx @@ -21,7 +21,7 @@ export const Wrapper = styled("div")` `; export const SidebarContainer = styled("div")` - height: calc(100vh - 91px); + height: calc(100vh - 64px); padding: 10px 10px 0; overflow-y: scroll; background: var(--mdc-theme-surface); diff --git a/packages/app-trash-bin/.babelrc.js b/packages/app-trash-bin/.babelrc.js new file mode 100644 index 00000000000..bec58b263bd --- /dev/null +++ b/packages/app-trash-bin/.babelrc.js @@ -0,0 +1 @@ +module.exports = require("@webiny/project-utils").createBabelConfigForReact({ path: __dirname }); diff --git a/packages/app-trash-bin/LICENSE b/packages/app-trash-bin/LICENSE new file mode 100644 index 00000000000..f772d04d4db --- /dev/null +++ b/packages/app-trash-bin/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) Webiny + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/packages/app-trash-bin/README.md b/packages/app-trash-bin/README.md new file mode 100644 index 00000000000..fe64af03358 --- /dev/null +++ b/packages/app-trash-bin/README.md @@ -0,0 +1,17 @@ +# @webiny/app-trash-bin +[![](https://img.shields.io/npm/dw/@webiny/app-trahs-bin.svg)](https://www.npmjs.com/package/@webiny/app-bin) +[![](https://img.shields.io/npm/v/@webiny/app-trash-bin.svg)](https://www.npmjs.com/package/@webiny/app-trash-bin) +[![code style: prettier](https://img.shields.io/badge/code_style-prettier-ff69b4.svg?style=flat-square)](https://github.com/prettier/prettier) +[![PRs Welcome](https://img.shields.io/badge/PRs-welcome-brightgreen.svg?style=flat-square)](http://makeapullrequest.com) + +A set of frontend trash bin related utilities. + +## Installation +``` +npm install --save @webiny/app-trash-bin +``` + +Or if you prefer yarn: +``` +yarn add @webiny/app-trash-bin +``` \ No newline at end of file diff --git a/packages/app-trash-bin/jest.config.js b/packages/app-trash-bin/jest.config.js new file mode 100644 index 00000000000..cc5ac2bb64f --- /dev/null +++ b/packages/app-trash-bin/jest.config.js @@ -0,0 +1,5 @@ +const base = require("../../jest.config.base"); + +module.exports = { + ...base({ path: __dirname }) +}; diff --git a/packages/app-trash-bin/package.json b/packages/app-trash-bin/package.json new file mode 100644 index 00000000000..9c696b7885b --- /dev/null +++ b/packages/app-trash-bin/package.json @@ -0,0 +1,53 @@ +{ + "name": "@webiny/app-trash-bin", + "version": "0.0.0", + "main": "index.js", + "repository": { + "type": "git", + "url": "https://github.com/webiny/webiny-js.git", + "directory": "packages/app-trash-bin" + }, + "description": "Frontend trash bin related features.", + "author": "Webiny Ltd.", + "license": "MIT", + "dependencies": { + "@emotion/styled": "^11.10.6", + "@material-design-icons/svg": "^0.12.1", + "@webiny/app-aco": "0.0.0", + "@webiny/app-admin": "0.0.0", + "@webiny/app-utils": "0.0.0", + "@webiny/react-composition": "0.0.0", + "@webiny/react-properties": "0.0.0", + "@webiny/ui": "0.0.0", + "graphql": "^15.7.2", + "lodash": "4.17.21", + "mobx": "^6.9.0", + "mobx-react-lite": "^3.4.3", + "react": "17.0.2", + "react-dom": "17.0.2" + }, + "devDependencies": { + "@babel/cli": "^7.23.9", + "@babel/core": "^7.24.0", + "@babel/preset-env": "^7.24.0", + "@babel/preset-react": "^7.23.3", + "@babel/preset-typescript": "^7.23.3", + "@babel/runtime": "^7.24.0", + "@types/react": "17.0.39", + "@webiny/cli": "0.0.0", + "@webiny/project-utils": "0.0.0", + "apollo-client": "^2.6.10", + "apollo-link": "^1.2.14", + "rimraf": "^5.0.5", + "ttypescript": "^1.5.12", + "typescript": "4.7.4" + }, + "publishConfig": { + "access": "public", + "directory": "dist" + }, + "scripts": { + "build": "yarn webiny run build", + "watch": "yarn webiny run watch" + } +} diff --git a/packages/app-trash-bin/src/Domain/Models/TrashBinItem/ITrashBinItemMapper.ts b/packages/app-trash-bin/src/Domain/Models/TrashBinItem/ITrashBinItemMapper.ts new file mode 100644 index 00000000000..02d821f5191 --- /dev/null +++ b/packages/app-trash-bin/src/Domain/Models/TrashBinItem/ITrashBinItemMapper.ts @@ -0,0 +1,5 @@ +import { TrashBinItemDTO } from "./TrashBinItem"; + +export interface ITrashBinItemMapper> { + toDTO: (data: TItem) => TrashBinItemDTO; +} diff --git a/packages/app-trash-bin/src/Domain/Models/TrashBinItem/TrashBinItem.ts b/packages/app-trash-bin/src/Domain/Models/TrashBinItem/TrashBinItem.ts new file mode 100644 index 00000000000..7cebdd2964c --- /dev/null +++ b/packages/app-trash-bin/src/Domain/Models/TrashBinItem/TrashBinItem.ts @@ -0,0 +1,33 @@ +import { TrashBinIdentity, TrashBinLocation } from "~/types"; + +export interface TrashBinItemDTO { + id: string; + title: string; + createdBy: TrashBinIdentity; + deletedBy: TrashBinIdentity; + deletedOn: string; + location: TrashBinLocation; + [key: string]: any; +} + +export class TrashBinItem { + public id: string; + public title: string; + public location: TrashBinLocation; + public createdBy: TrashBinIdentity; + public deletedOn: string; + public deletedBy: TrashBinIdentity; + + protected constructor(item: TrashBinItemDTO) { + this.id = item.id; + this.title = item.title; + this.location = item.location; + this.createdBy = item.createdBy; + this.deletedOn = item.deletedOn; + this.deletedBy = item.deletedBy; + } + + static create(item: TrashBinItemDTO) { + return new TrashBinItem(item); + } +} diff --git a/packages/app-trash-bin/src/Domain/Models/TrashBinItem/index.ts b/packages/app-trash-bin/src/Domain/Models/TrashBinItem/index.ts new file mode 100644 index 00000000000..ec549ee3f73 --- /dev/null +++ b/packages/app-trash-bin/src/Domain/Models/TrashBinItem/index.ts @@ -0,0 +1,2 @@ +export * from "./ITrashBinItemMapper"; +export * from "./TrashBinItem"; diff --git a/packages/app-trash-bin/src/Domain/Models/index.ts b/packages/app-trash-bin/src/Domain/Models/index.ts new file mode 100644 index 00000000000..5d535193636 --- /dev/null +++ b/packages/app-trash-bin/src/Domain/Models/index.ts @@ -0,0 +1 @@ +export * from "./TrashBinItem"; diff --git a/packages/app-trash-bin/src/Domain/Repositories/Search/ISearchRepository.ts b/packages/app-trash-bin/src/Domain/Repositories/Search/ISearchRepository.ts new file mode 100644 index 00000000000..b6796872375 --- /dev/null +++ b/packages/app-trash-bin/src/Domain/Repositories/Search/ISearchRepository.ts @@ -0,0 +1,4 @@ +export interface ISearchRepository { + get: () => string; + set: (query: string) => Promise; +} diff --git a/packages/app-trash-bin/src/Domain/Repositories/Search/SearchRepository.ts b/packages/app-trash-bin/src/Domain/Repositories/Search/SearchRepository.ts new file mode 100644 index 00000000000..bb6211d5270 --- /dev/null +++ b/packages/app-trash-bin/src/Domain/Repositories/Search/SearchRepository.ts @@ -0,0 +1,18 @@ +import { makeAutoObservable } from "mobx"; +import { ISearchRepository } from "./ISearchRepository"; + +export class SearchRepository implements ISearchRepository { + private query = ""; + + constructor() { + makeAutoObservable(this); + } + + get() { + return this.query; + } + + async set(query: string) { + this.query = query; + } +} diff --git a/packages/app-trash-bin/src/Domain/Repositories/Search/SearchRepositoryFactory.ts b/packages/app-trash-bin/src/Domain/Repositories/Search/SearchRepositoryFactory.ts new file mode 100644 index 00000000000..8fe91948758 --- /dev/null +++ b/packages/app-trash-bin/src/Domain/Repositories/Search/SearchRepositoryFactory.ts @@ -0,0 +1,21 @@ +import { SearchRepository } from "./SearchRepository"; + +export class SearchRepositoryFactory { + private cache: Map = new Map(); + + getRepository() { + const cacheKey = this.getCacheKey(); + + if (!this.cache.has(cacheKey)) { + this.cache.set(cacheKey, new SearchRepository()); + } + + return this.cache.get(cacheKey) as SearchRepository; + } + + private getCacheKey() { + return Date.now().toString(); + } +} + +export const searchRepositoryFactory = new SearchRepositoryFactory(); diff --git a/packages/app-trash-bin/src/Domain/Repositories/Search/index.ts b/packages/app-trash-bin/src/Domain/Repositories/Search/index.ts new file mode 100644 index 00000000000..944b8640f6b --- /dev/null +++ b/packages/app-trash-bin/src/Domain/Repositories/Search/index.ts @@ -0,0 +1,3 @@ +export * from "./ISearchRepository"; +export * from "./SearchRepository"; +export * from "./SearchRepositoryFactory"; diff --git a/packages/app-trash-bin/src/Domain/Repositories/SelectedItems/ISelectedItemsRepository.ts b/packages/app-trash-bin/src/Domain/Repositories/SelectedItems/ISelectedItemsRepository.ts new file mode 100644 index 00000000000..2a8d73a2f3f --- /dev/null +++ b/packages/app-trash-bin/src/Domain/Repositories/SelectedItems/ISelectedItemsRepository.ts @@ -0,0 +1,6 @@ +import { TrashBinItem } from "~/Domain"; + +export interface ISelectedItemsRepository { + selectItems: (items: TrashBinItem[]) => Promise; + getSelectedItems: () => TrashBinItem[]; +} diff --git a/packages/app-trash-bin/src/Domain/Repositories/SelectedItems/SelectedItemsRepository.ts b/packages/app-trash-bin/src/Domain/Repositories/SelectedItems/SelectedItemsRepository.ts new file mode 100644 index 00000000000..ec79a13f26c --- /dev/null +++ b/packages/app-trash-bin/src/Domain/Repositories/SelectedItems/SelectedItemsRepository.ts @@ -0,0 +1,19 @@ +import { makeAutoObservable } from "mobx"; +import { TrashBinItem } from "~/Domain"; +import { ISelectedItemsRepository } from "./ISelectedItemsRepository"; + +export class SelectedItemsRepository implements ISelectedItemsRepository { + private items: TrashBinItem[] = []; + + constructor() { + makeAutoObservable(this); + } + + getSelectedItems() { + return this.items; + } + + async selectItems(items: TrashBinItem[]) { + this.items = items; + } +} diff --git a/packages/app-trash-bin/src/Domain/Repositories/SelectedItems/SelectedItemsRepositoryFactory.ts b/packages/app-trash-bin/src/Domain/Repositories/SelectedItems/SelectedItemsRepositoryFactory.ts new file mode 100644 index 00000000000..fcb5a7ae5b1 --- /dev/null +++ b/packages/app-trash-bin/src/Domain/Repositories/SelectedItems/SelectedItemsRepositoryFactory.ts @@ -0,0 +1,21 @@ +import { SelectedItemsRepository } from "./SelectedItemsRepository"; + +export class SelectedItemsRepositoryFactory { + private cache: Map = new Map(); + + getRepository() { + const cacheKey = this.getCacheKey(); + + if (!this.cache.has(cacheKey)) { + this.cache.set(cacheKey, new SelectedItemsRepository()); + } + + return this.cache.get(cacheKey) as SelectedItemsRepository; + } + + private getCacheKey() { + return Date.now().toString(); + } +} + +export const selectedItemsRepositoryFactory = new SelectedItemsRepositoryFactory(); diff --git a/packages/app-trash-bin/src/Domain/Repositories/SelectedItems/index.ts b/packages/app-trash-bin/src/Domain/Repositories/SelectedItems/index.ts new file mode 100644 index 00000000000..55fee27bfe3 --- /dev/null +++ b/packages/app-trash-bin/src/Domain/Repositories/SelectedItems/index.ts @@ -0,0 +1,3 @@ +export * from "./ISelectedItemsRepository"; +export * from "./SelectedItemsRepository"; +export * from "./SelectedItemsRepositoryFactory"; diff --git a/packages/app-trash-bin/src/Domain/Repositories/Sorting/SortingRepositoryWithDefaults.ts b/packages/app-trash-bin/src/Domain/Repositories/Sorting/SortingRepositoryWithDefaults.ts new file mode 100644 index 00000000000..b5502f9037b --- /dev/null +++ b/packages/app-trash-bin/src/Domain/Repositories/Sorting/SortingRepositoryWithDefaults.ts @@ -0,0 +1,29 @@ +import { makeAutoObservable, runInAction } from "mobx"; +import { ISortingRepository, Sorting } from "@webiny/app-utils"; + +export class SortingRepositoryWithDefaults implements ISortingRepository { + private defaults: Sorting[]; + private repository: ISortingRepository; + + constructor(defaults: Sorting[], repository: ISortingRepository) { + this.defaults = defaults; + this.repository = repository; + makeAutoObservable(this); + } + + get() { + const existingSort = this.repository.get(); + + if (existingSort.length === 0) { + runInAction(() => { + this.set(this.defaults); + }); + } + + return this.repository.get(); + } + + set(sorts: Sorting[]) { + return this.repository.set(sorts); + } +} diff --git a/packages/app-trash-bin/src/Domain/Repositories/Sorting/index.ts b/packages/app-trash-bin/src/Domain/Repositories/Sorting/index.ts new file mode 100644 index 00000000000..ff9f33434e6 --- /dev/null +++ b/packages/app-trash-bin/src/Domain/Repositories/Sorting/index.ts @@ -0,0 +1 @@ +export * from "./SortingRepositoryWithDefaults"; diff --git a/packages/app-trash-bin/src/Domain/Repositories/TrashBinItems/ITrashBinItemsRepository.ts b/packages/app-trash-bin/src/Domain/Repositories/TrashBinItems/ITrashBinItemsRepository.ts new file mode 100644 index 00000000000..85298f54270 --- /dev/null +++ b/packages/app-trash-bin/src/Domain/Repositories/TrashBinItems/ITrashBinItemsRepository.ts @@ -0,0 +1,14 @@ +import { Meta } from "@webiny/app-utils"; +import { TrashBinItem } from "~/Domain"; +import { TrashBinListQueryVariables } from "~/types"; + +export interface ITrashBinItemsRepository { + listItems: (params?: TrashBinListQueryVariables) => Promise; + listMoreItems: () => Promise; + deleteItem: (id: string) => Promise; + restoreItem: (id: string) => Promise; + getItems: () => TrashBinItem[]; + getRestoredItems: () => TrashBinItem[]; + getMeta: () => Meta; + getLoading: () => Record; +} diff --git a/packages/app-trash-bin/src/Domain/Repositories/TrashBinItems/TrashBinItemMapper.ts b/packages/app-trash-bin/src/Domain/Repositories/TrashBinItems/TrashBinItemMapper.ts new file mode 100644 index 00000000000..da5ced734fb --- /dev/null +++ b/packages/app-trash-bin/src/Domain/Repositories/TrashBinItems/TrashBinItemMapper.ts @@ -0,0 +1,19 @@ +import { ITrashBinItemMapper, TrashBinItem } from "~/Domain"; + +export class TrashBinItemMapper implements ITrashBinItemMapper { + toDTO(data: TrashBinItem) { + return { + id: data.id, + $selectable: true, + title: data.title, + location: data.location, + createdBy: data.createdBy, + deletedBy: { + id: data.deletedBy?.id || "", + displayName: data.deletedBy?.displayName || "", + type: data.deletedBy?.type || "" + }, + deletedOn: data.deletedOn || "" + }; + } +} diff --git a/packages/app-trash-bin/src/Domain/Repositories/TrashBinItems/TrashBinItemsRepository.ts b/packages/app-trash-bin/src/Domain/Repositories/TrashBinItems/TrashBinItemsRepository.ts new file mode 100644 index 00000000000..6f1dd9955fa --- /dev/null +++ b/packages/app-trash-bin/src/Domain/Repositories/TrashBinItems/TrashBinItemsRepository.ts @@ -0,0 +1,123 @@ +import { makeAutoObservable, runInAction } from "mobx"; +import uniqBy from "lodash/uniqBy"; +import { ITrashBinItemMapper, TrashBinItem } from "~/Domain"; +import { + ITrashBinListGateway, + ITrashBinDeleteItemGateway, + ITrashBinRestoreItemGateway +} from "~/Gateways"; +import { IMetaRepository, Meta } from "@webiny/app-utils"; +import { TrashBinListQueryVariables } from "~/types"; +import { ITrashBinItemsRepository } from "./ITrashBinItemsRepository"; + +export class TrashBinItemsRepository> + implements ITrashBinItemsRepository +{ + private metaRepository: IMetaRepository; + private listGateway: ITrashBinListGateway; + private deleteGateway: ITrashBinDeleteItemGateway; + private restoreGateway: ITrashBinRestoreItemGateway; + private itemMapper: ITrashBinItemMapper; + private items: TrashBinItem[] = []; + private restoredItems: TrashBinItem[] = []; + private params: TrashBinListQueryVariables = {}; + + constructor( + metaRepository: IMetaRepository, + listGateway: ITrashBinListGateway, + deleteGateway: ITrashBinDeleteItemGateway, + restoreGateway: ITrashBinRestoreItemGateway, + entryMapper: ITrashBinItemMapper + ) { + this.metaRepository = metaRepository; + this.listGateway = listGateway; + this.deleteGateway = deleteGateway; + this.restoreGateway = restoreGateway; + this.itemMapper = entryMapper; + this.params = {}; + makeAutoObservable(this); + } + + getItems() { + return this.items; + } + + getRestoredItems() { + return this.restoredItems; + } + + getMeta() { + return this.metaRepository.get(); + } + + getLoading() { + return {}; + } + + async listItems(params?: TrashBinListQueryVariables) { + this.params = params || {}; + + const response = await this.listGateway.execute({ ...params }); + + if (!response) { + return; + } + + runInAction(() => { + const [items, meta] = response; + this.items = items.map(entry => TrashBinItem.create(this.itemMapper.toDTO(entry))); + this.metaRepository.set(Meta.create(meta)); + }); + } + + async listMoreItems() { + const { cursor } = this.metaRepository.get(); + + if (!cursor) { + return; + } + + const response = await this.listGateway.execute({ ...this.params, after: cursor }); + + if (!response) { + return; + } + + runInAction(() => { + const [items, meta] = response; + const itemsDTO = items.map(entry => TrashBinItem.create(this.itemMapper.toDTO(entry))); + this.items = uniqBy([...this.items, ...itemsDTO], "id"); + this.metaRepository.set(Meta.create(meta)); + }); + } + + async deleteItem(id: string) { + const response = await this.deleteGateway.execute(id); + + if (!response) { + return; + } + + runInAction(() => { + this.items = this.items.filter(item => item.id !== id); + this.metaRepository.decreaseTotalCount(1); + }); + } + + async restoreItem(id: string) { + const item = await this.restoreGateway.execute(id); + + if (!item) { + return; + } + + runInAction(() => { + this.items = this.items.filter(item => item.id !== id); + this.restoredItems = [ + ...this.restoredItems, + TrashBinItem.create(this.itemMapper.toDTO(item)) + ]; + this.metaRepository.decreaseTotalCount(1); + }); + } +} diff --git a/packages/app-trash-bin/src/Domain/Repositories/TrashBinItems/TrashBinItemsRepositoryFactory.ts b/packages/app-trash-bin/src/Domain/Repositories/TrashBinItems/TrashBinItemsRepositoryFactory.ts new file mode 100644 index 00000000000..84616840241 --- /dev/null +++ b/packages/app-trash-bin/src/Domain/Repositories/TrashBinItems/TrashBinItemsRepositoryFactory.ts @@ -0,0 +1,43 @@ +import { IMetaRepository } from "@webiny/app-utils"; +import { ITrashBinItemMapper } from "~/Domain"; +import { + ITrashBinDeleteItemGateway, + ITrashBinListGateway, + ITrashBinRestoreItemGateway +} from "~/Gateways"; +import { TrashBinItemsRepository } from "./TrashBinItemsRepository"; + +export class TrashBinItemsRepositoryFactory> { + private cache: Map> = new Map(); + + getRepository( + metaRepository: IMetaRepository, + listGateway: ITrashBinListGateway, + deleteGateway: ITrashBinDeleteItemGateway, + restoreGateway: ITrashBinRestoreItemGateway, + itemMapper: ITrashBinItemMapper + ) { + const cacheKey = this.getCacheKey(); + + if (!this.cache.has(cacheKey)) { + this.cache.set( + cacheKey, + new TrashBinItemsRepository( + metaRepository, + listGateway, + deleteGateway, + restoreGateway, + itemMapper + ) + ); + } + + return this.cache.get(cacheKey) as TrashBinItemsRepository; + } + + private getCacheKey() { + return Date.now().toString(); + } +} + +export const trashBinItemsRepositoryFactory = new TrashBinItemsRepositoryFactory(); diff --git a/packages/app-trash-bin/src/Domain/Repositories/TrashBinItems/TrashBinItemsRepositoryWithLoading.ts b/packages/app-trash-bin/src/Domain/Repositories/TrashBinItems/TrashBinItemsRepositoryWithLoading.ts new file mode 100644 index 00000000000..4975a123699 --- /dev/null +++ b/packages/app-trash-bin/src/Domain/Repositories/TrashBinItems/TrashBinItemsRepositoryWithLoading.ts @@ -0,0 +1,62 @@ +import { makeAutoObservable } from "mobx"; +import { ILoadingRepository } from "@webiny/app-utils"; +import { ITrashBinItemsRepository } from "./ITrashBinItemsRepository"; +import { LoadingActions, TrashBinListQueryVariables } from "~/types"; + +export class TrashBinItemsRepositoryWithLoading implements ITrashBinItemsRepository { + private loadingRepository: ILoadingRepository; + private trashBinItemsRepository: ITrashBinItemsRepository; + + constructor( + loadingRepository: ILoadingRepository, + trashBinItemsRepository: ITrashBinItemsRepository + ) { + this.loadingRepository = loadingRepository; + this.trashBinItemsRepository = trashBinItemsRepository; + makeAutoObservable(this); + } + + getItems() { + return this.trashBinItemsRepository.getItems(); + } + + getRestoredItems() { + return this.trashBinItemsRepository.getRestoredItems(); + } + + getMeta() { + return this.trashBinItemsRepository.getMeta(); + } + + getLoading() { + return this.loadingRepository.get(); + } + + async listItems(params?: TrashBinListQueryVariables) { + await this.loadingRepository.runCallBack( + this.trashBinItemsRepository.listItems(params), + LoadingActions.list + ); + } + + async listMoreItems() { + await this.loadingRepository.runCallBack( + this.trashBinItemsRepository.listMoreItems(), + LoadingActions.listMore + ); + } + + async deleteItem(id: string) { + await this.loadingRepository.runCallBack( + this.trashBinItemsRepository.deleteItem(id), + LoadingActions.delete + ); + } + + async restoreItem(id: string) { + await this.loadingRepository.runCallBack( + this.trashBinItemsRepository.restoreItem(id), + LoadingActions.restore + ); + } +} diff --git a/packages/app-trash-bin/src/Domain/Repositories/TrashBinItems/index.ts b/packages/app-trash-bin/src/Domain/Repositories/TrashBinItems/index.ts new file mode 100644 index 00000000000..eab28e0663c --- /dev/null +++ b/packages/app-trash-bin/src/Domain/Repositories/TrashBinItems/index.ts @@ -0,0 +1,5 @@ +export * from "./ITrashBinItemsRepository"; +export * from "./TrashBinItemMapper"; +export * from "./TrashBinItemsRepository"; +export * from "./TrashBinItemsRepositoryFactory"; +export * from "./TrashBinItemsRepositoryWithLoading"; diff --git a/packages/app-trash-bin/src/Domain/Repositories/index.ts b/packages/app-trash-bin/src/Domain/Repositories/index.ts new file mode 100644 index 00000000000..5ba70d2bfce --- /dev/null +++ b/packages/app-trash-bin/src/Domain/Repositories/index.ts @@ -0,0 +1,5 @@ +export * from "./Search"; +export * from "./SelectedItems"; +export * from "./Search"; +export * from "./Sorting"; +export * from "./TrashBinItems"; diff --git a/packages/app-trash-bin/src/Domain/index.ts b/packages/app-trash-bin/src/Domain/index.ts new file mode 100644 index 00000000000..02a7d080c57 --- /dev/null +++ b/packages/app-trash-bin/src/Domain/index.ts @@ -0,0 +1,2 @@ +export * from "./Models"; +export * from "./Repositories"; diff --git a/packages/app-trash-bin/src/Gateways/TrashBinDeleteItem/ITrashBinDeleteItemGateway.ts b/packages/app-trash-bin/src/Gateways/TrashBinDeleteItem/ITrashBinDeleteItemGateway.ts new file mode 100644 index 00000000000..787ef25c89b --- /dev/null +++ b/packages/app-trash-bin/src/Gateways/TrashBinDeleteItem/ITrashBinDeleteItemGateway.ts @@ -0,0 +1,3 @@ +export interface ITrashBinDeleteItemGateway { + execute: (id: string) => Promise; +} diff --git a/packages/app-trash-bin/src/Gateways/TrashBinDeleteItem/index.ts b/packages/app-trash-bin/src/Gateways/TrashBinDeleteItem/index.ts new file mode 100644 index 00000000000..8aa8a064442 --- /dev/null +++ b/packages/app-trash-bin/src/Gateways/TrashBinDeleteItem/index.ts @@ -0,0 +1 @@ +export * from "./ITrashBinDeleteItemGateway"; diff --git a/packages/app-trash-bin/src/Gateways/TrashBinListItems/ITrashBinListGateway.ts b/packages/app-trash-bin/src/Gateways/TrashBinListItems/ITrashBinListGateway.ts new file mode 100644 index 00000000000..0ab914a57c2 --- /dev/null +++ b/packages/app-trash-bin/src/Gateways/TrashBinListItems/ITrashBinListGateway.ts @@ -0,0 +1,5 @@ +import { TrashBinListQueryVariables, TrashBinMetaResponse } from "~/types"; + +export interface ITrashBinListGateway { + execute: (params: TrashBinListQueryVariables) => Promise<[TItem[], TrashBinMetaResponse]>; +} diff --git a/packages/app-trash-bin/src/Gateways/TrashBinListItems/index.ts b/packages/app-trash-bin/src/Gateways/TrashBinListItems/index.ts new file mode 100644 index 00000000000..3e837d300ce --- /dev/null +++ b/packages/app-trash-bin/src/Gateways/TrashBinListItems/index.ts @@ -0,0 +1 @@ +export * from "./ITrashBinListGateway"; diff --git a/packages/app-trash-bin/src/Gateways/TrashBinRestoreItem/ITrashBinRestoreItemGateway.ts b/packages/app-trash-bin/src/Gateways/TrashBinRestoreItem/ITrashBinRestoreItemGateway.ts new file mode 100644 index 00000000000..41aec96bd0b --- /dev/null +++ b/packages/app-trash-bin/src/Gateways/TrashBinRestoreItem/ITrashBinRestoreItemGateway.ts @@ -0,0 +1,3 @@ +export interface ITrashBinRestoreItemGateway { + execute: (id: string) => Promise; +} diff --git a/packages/app-trash-bin/src/Gateways/TrashBinRestoreItem/index.ts b/packages/app-trash-bin/src/Gateways/TrashBinRestoreItem/index.ts new file mode 100644 index 00000000000..2076ec171b1 --- /dev/null +++ b/packages/app-trash-bin/src/Gateways/TrashBinRestoreItem/index.ts @@ -0,0 +1 @@ +export * from "./ITrashBinRestoreItemGateway"; diff --git a/packages/app-trash-bin/src/Gateways/index.ts b/packages/app-trash-bin/src/Gateways/index.ts new file mode 100644 index 00000000000..8a0ca179f00 --- /dev/null +++ b/packages/app-trash-bin/src/Gateways/index.ts @@ -0,0 +1,3 @@ +export * from "./TrashBinDeleteItem"; +export * from "./TrashBinListItems"; +export * from "./TrashBinRestoreItem"; diff --git a/packages/app-trash-bin/src/Presentation/TrashBin/TrashBin.test.ts b/packages/app-trash-bin/src/Presentation/TrashBin/TrashBin.test.ts new file mode 100644 index 00000000000..2cfd58dcfe8 --- /dev/null +++ b/packages/app-trash-bin/src/Presentation/TrashBin/TrashBin.test.ts @@ -0,0 +1,759 @@ +import { TrashBinPresenter } from "./TrashBinPresenter"; +import { LoadingRepository, MetaRepository, Sorting, SortingRepository } from "@webiny/app-utils"; +import { LoadingActions, TrashBinIdentity, TrashBinLocation } from "~/types"; +import { TrashBinControllers } from "~/Presentation/TrashBin/TrashBinControllers"; +import { + ITrashBinDeleteItemGateway, + ITrashBinListGateway, + ITrashBinRestoreItemGateway +} from "~/Gateways"; +import { ITrashBinItemMapper } from "~/Domain/Models/TrashBinItem"; +import { SearchRepository } from "~/Domain/Repositories/Search"; +import { SelectedItemsRepository } from "~/Domain/Repositories/SelectedItems"; +import { SortingRepositoryWithDefaults } from "~/Domain/Repositories/Sorting"; +import { + TrashBinItemsRepository, + TrashBinItemsRepositoryWithLoading +} from "~/Domain/Repositories/TrashBinItems"; + +interface Item { + id: string; + title: string; + location: TrashBinLocation; + createdBy: TrashBinIdentity; + deletedOn: string; + deletedBy: TrashBinIdentity; + custom: string; +} + +const identity1: TrashBinIdentity = { + id: "1234", + displayName: "John Doe", + type: "admin" +}; + +const identity2: TrashBinIdentity = { + id: "5678", + displayName: "Jane Doe", + type: "admin" +}; + +const createBinListGateway = ({ + execute +}: ITrashBinListGateway): ITrashBinListGateway => ({ + execute +}); + +const createBinDeleteItemGateway = ({ + execute +}: ITrashBinDeleteItemGateway): ITrashBinDeleteItemGateway => ({ + execute +}); + +const createBinRestoreItemGateway = ({ + execute +}: ITrashBinRestoreItemGateway): ITrashBinRestoreItemGateway => ({ + execute +}); + +class CustomItemMapper implements ITrashBinItemMapper { + toDTO(data: Item) { + return { + id: data.id, + title: data.title, + location: data.location, + createdBy: data.createdBy, + deletedOn: data.deletedOn, + deletedBy: data.deletedBy + }; + } +} + +const defaultSorting: Sorting[] = [{ field: "deletedOn", order: "desc" }]; + +describe("TrashBin", () => { + const item1: Item = { + id: "item-1", + title: "Item 1", + location: { + folderId: "folder-a" + }, + createdBy: identity1, + deletedBy: identity2, + deletedOn: new Date().toString(), + custom: "any custom data" + }; + + const item2: Item = { + id: "item-2", + title: "Item 2", + location: { + folderId: "folder-a" + }, + createdBy: identity1, + deletedBy: identity1, + deletedOn: new Date().toString(), + custom: "any custom data" + }; + + const item3: Item = { + id: "item-3", + title: "Item 3", + location: { + folderId: "folder-b" + }, + createdBy: identity2, + deletedBy: identity2, + deletedOn: new Date().toString(), + custom: "any custom data" + }; + + const item4: Item = { + id: "item-4", + title: "Item 4", + location: { + folderId: "folder-b" + }, + createdBy: identity1, + deletedBy: identity1, + deletedOn: new Date().toString(), + custom: "any custom data" + }; + + const listGateway = createBinListGateway({ + execute: jest.fn().mockImplementation(() => { + return Promise.resolve([ + [item1, item2, item3], + { totalCount: 3, cursor: null, hasMoreItems: false } + ]); + }) + }); + + const deleteItemGateway = createBinDeleteItemGateway({ + execute: jest.fn().mockImplementation(() => { + return Promise.resolve(true); + }) + }); + + const restoreItemGateway = createBinRestoreItemGateway({ + execute: jest.fn().mockImplementation(() => { + return Promise.resolve(item1); + }) + }); + + const itemMapper = new CustomItemMapper(); + + const init = ( + listGateway: ITrashBinListGateway, + deleteItemGateway: ITrashBinDeleteItemGateway, + restoreItemGateway: ITrashBinRestoreItemGateway + ) => { + const selectedRepo = new SelectedItemsRepository(); + const loadingRepo = new LoadingRepository(); + const sortRepo = new SortingRepository(); + const sortRepoWithDefaults = new SortingRepositoryWithDefaults(defaultSorting, sortRepo); + const metaRepo = new MetaRepository(); + const searchRepo = new SearchRepository(); + const trashBinItemsRepo = new TrashBinItemsRepository( + metaRepo, + listGateway, + deleteItemGateway, + restoreItemGateway, + itemMapper + ); + + const itemsRepo = new TrashBinItemsRepositoryWithLoading(loadingRepo, trashBinItemsRepo); + + return { + presenter: new TrashBinPresenter( + itemsRepo, + selectedRepo, + sortRepoWithDefaults, + searchRepo + ), + controllers: new TrashBinControllers( + itemsRepo, + selectedRepo, + sortRepoWithDefaults, + searchRepo + ).getControllers() + }; + }; + + beforeEach(() => { + jest.clearAllMocks(); + }); + + it("should create a presenter and list trash bin entries from the gateway", async () => { + const { presenter, controllers } = init(listGateway, deleteItemGateway, restoreItemGateway); + + const listPromise = controllers.listItems.execute(); + + // Let's check the transition to loading state + expect(presenter.vm).toMatchObject({ + items: [], + loading: { + [LoadingActions.list]: true + } + }); + + await listPromise; + + expect(listGateway.execute).toHaveBeenCalledTimes(1); + expect(listGateway.execute).toHaveBeenCalledWith({ + sort: ["deletedOn_DESC"] + }); + + expect(presenter.vm).toMatchObject({ + items: [ + { + id: "item-1", + $selectable: true, + title: "Item 1", + createdBy: identity1, + deletedBy: identity2, + deletedOn: expect.any(String) + }, + { + id: "item-2", + $selectable: true, + title: "Item 2", + createdBy: identity1, + deletedBy: identity1, + deletedOn: expect.any(String) + }, + { + id: "item-3", + $selectable: true, + title: "Item 3", + createdBy: identity2, + deletedBy: identity2, + deletedOn: expect.any(String) + } + ], + loading: { + [LoadingActions.list]: false + } + }); + }); + + it("should list more items from the gateway", async () => { + const listGateway = createBinListGateway({ + execute: jest + .fn() + .mockImplementationOnce(() => { + return Promise.resolve([ + [item1, item2, item3], + { totalCount: 4, cursor: "IjMi", hasMoreItems: true } + ]); + }) + .mockImplementationOnce(() => { + return Promise.resolve([ + [item4], + { totalCount: 4, cursor: null, hasMoreItems: false } + ]); + }) + }); + + const { presenter, controllers } = init(listGateway, deleteItemGateway, restoreItemGateway); + + // Let's list some initial entries + await controllers.listItems.execute(); + expect(listGateway.execute).toHaveBeenCalledTimes(1); + + // Let's list more items from the gateway + const listMorePromise = controllers.listMoreItems.execute(); + + expect(presenter.vm).toMatchObject({ + loading: { + [LoadingActions.listMore]: true + } + }); + + await listMorePromise; + + expect(listGateway.execute).toHaveBeenCalledTimes(2); + expect(listGateway.execute).toHaveBeenCalledWith({ + after: "IjMi", + search: undefined, + sort: ["deletedOn_DESC"] + }); + + expect(presenter.vm).toMatchObject({ + items: [ + { + id: "item-1", + $selectable: true, + title: "Item 1", + createdBy: identity1, + deletedBy: identity2, + deletedOn: expect.any(String) + }, + { + id: "item-2", + $selectable: true, + title: "Item 2", + createdBy: identity1, + deletedBy: identity1, + deletedOn: expect.any(String) + }, + { + id: "item-3", + $selectable: true, + title: "Item 3", + createdBy: identity2, + deletedBy: identity2, + deletedOn: expect.any(String) + }, + { + id: "item-4", + $selectable: true, + title: "Item 4", + createdBy: identity1, + deletedBy: identity1, + deletedOn: expect.any(String) + } + ], + loading: { + [LoadingActions.listMore]: false + } + }); + }); + + it("should be able to sort items", async () => { + const sortListGateway = createBinListGateway({ + execute: jest + .fn() + .mockImplementationOnce(() => { + return Promise.resolve([ + [item1, item2, item3], + { totalCount: 3, cursor: null, hasMoreItems: false } + ]); + }) + .mockImplementation(() => { + return Promise.resolve([ + [item3, item2, item1], + { totalCount: 3, cursor: null, hasMoreItems: false } + ]); + }) + }); + + const { presenter, controllers } = init( + sortListGateway, + deleteItemGateway, + restoreItemGateway + ); + + // let's list some entries from the gateway + await controllers.listItems.execute(); + + expect(sortListGateway.execute).toHaveBeenNthCalledWith(1, { + sort: ["deletedOn_DESC"] + }); + + // Let's sort items, it should call back the list gateway to retrieve the items sorted + await controllers.sortItems.execute(() => [{ id: "deletedOn", desc: false }]); + + expect(sortListGateway.execute).toHaveBeenNthCalledWith(2, { + sort: ["deletedOn_ASC"] + }); + + expect(presenter.vm).toMatchObject({ + items: [ + { + id: "item-3", + $selectable: true, + title: "Item 3", + createdBy: identity2, + deletedBy: identity2, + deletedOn: expect.any(String) + }, + { + id: "item-2", + $selectable: true, + title: "Item 2", + createdBy: identity1, + deletedBy: identity1, + deletedOn: expect.any(String) + }, + { + id: "item-1", + $selectable: true, + title: "Item 1", + createdBy: identity1, + deletedBy: identity2, + deletedOn: expect.any(String) + } + ], + loading: { + [LoadingActions.list]: false + }, + sorting: [{ id: "deletedOn", desc: false }] + }); + }); + + it("should be able to search items", async () => { + const searchItemsGateway = createBinListGateway({ + execute: jest + .fn() + .mockImplementationOnce(() => { + return Promise.resolve([ + [item1, item2, item3], + { totalCount: 3, cursor: null, hasMoreItems: false } + ]); + }) + .mockImplementationOnce(() => { + return Promise.resolve([ + [item1], + { totalCount: 1, cursor: null, hasMoreItems: false } + ]); + }) + .mockImplementationOnce(() => { + return Promise.resolve([ + [], + { totalCount: 0, cursor: null, hasMoreItems: false } + ]); + }) + }); + + const { presenter, controllers } = init( + searchItemsGateway, + deleteItemGateway, + restoreItemGateway + ); + + // let's list some entries from the gateway + await controllers.listItems.execute(); + + expect(searchItemsGateway.execute).toHaveBeenNthCalledWith(1, { + sort: ["deletedOn_DESC"] + }); + + // Let's search for items, it should return items from the gateway + await controllers.searchItems.execute("Item 1"); + + expect(searchItemsGateway.execute).toHaveBeenNthCalledWith(2, { + sort: ["deletedOn_DESC"], + search: "Item 1" + }); + + expect(presenter.vm).toMatchObject({ + items: [ + { + id: "item-1", + $selectable: true, + title: "Item 1", + createdBy: identity1, + deletedBy: identity2, + deletedOn: expect.any(String) + } + ], + loading: { + [LoadingActions.list]: false + }, + searchQuery: "Item 1" + }); + + // Let's search for items, it should return no items from the gateway + await controllers.searchItems.execute("Not found query"); + + expect(searchItemsGateway.execute).toHaveBeenNthCalledWith(3, { + sort: ["deletedOn_DESC"], + search: "Not found query" + }); + + expect(presenter.vm).toMatchObject({ + items: [], + loading: { + [LoadingActions.list]: false + }, + searchQuery: "Not found query", + isEmptyView: true + }); + }); + + it("should be able to select items", async () => { + const { presenter, controllers } = init(listGateway, deleteItemGateway, restoreItemGateway); + + // let's list some entries from the gateway + await controllers.listItems.execute(); + + // No selected items found by default + expect(presenter.vm).toMatchObject({ + selectedItems: [] + }); + + // Let's select the first Item + await controllers.selectItems.execute([ + { + id: "item-1", + $selectable: true, + title: "Item 1", + location: { + folderId: "folder-a" + }, + createdBy: identity1, + deletedBy: identity2, + deletedOn: new Date().toString() + } + ]); + + expect(presenter.vm).toMatchObject({ + selectedItems: [ + { + id: "item-1", + $selectable: true, + title: "Item 1", + location: { + folderId: "folder-a" + }, + createdBy: identity1, + deletedBy: identity2, + deletedOn: expect.any(String) + } + ] + }); + + // Let's select the second item + await controllers.selectItems.execute([ + { + id: "item-1", + $selectable: true, + title: "Item 1", + location: { + folderId: "folder-a" + }, + createdBy: identity1, + deletedBy: identity2, + deletedOn: new Date().toString() + }, + { + id: "item-2", + $selectable: true, + title: "Item 2", + location: { + folderId: "folder-a" + }, + createdBy: identity1, + deletedBy: identity1, + deletedOn: new Date().toString() + } + ]); + + expect(presenter.vm).toMatchObject({ + selectedItems: [ + { + id: "item-1", + $selectable: true, + title: "Item 1", + location: { + folderId: "folder-a" + }, + createdBy: identity1, + deletedBy: identity2, + deletedOn: expect.any(String) + }, + { + id: "item-2", + $selectable: true, + title: "Item 2", + location: { + folderId: "folder-a" + }, + createdBy: identity1, + deletedBy: identity1, + deletedOn: expect.any(String) + } + ] + }); + }); + + it("should delete an item, removing it from the list", async () => { + const { presenter, controllers } = init(listGateway, deleteItemGateway, restoreItemGateway); + + // let's list some entries from the gateway + await controllers.listItems.execute(); + + expect(listGateway.execute).toHaveBeenCalledTimes(1); + + expect(presenter.vm).toMatchObject({ + items: [ + { + id: "item-1", + $selectable: true, + title: "Item 1", + location: { + folderId: "folder-a" + }, + createdBy: identity1, + deletedBy: identity2, + deletedOn: expect.any(String) + }, + { + id: "item-2", + $selectable: true, + title: "Item 2", + location: { + folderId: "folder-a" + }, + createdBy: identity1, + deletedBy: identity1, + deletedOn: expect.any(String) + }, + { + id: "item-3", + $selectable: true, + title: "Item 3", + location: { + folderId: "folder-b" + }, + createdBy: identity2, + deletedBy: identity2, + deletedOn: expect.any(String) + } + ] + }); + + const deletePromise = controllers.deleteItem.execute(item1.id); + + // Let's check the transition to loading state + expect(presenter.vm).toMatchObject({ + loading: { + [LoadingActions.delete]: true + } + }); + + await deletePromise; + + expect(deleteItemGateway.execute).toHaveBeenCalledTimes(1); + expect(deleteItemGateway.execute).toHaveBeenCalledWith(item1.id); + + expect(presenter.vm).toMatchObject({ + items: [ + { + id: "item-2", + $selectable: true, + title: "Item 2", + location: { + folderId: "folder-a" + }, + createdBy: identity1, + deletedBy: identity1, + deletedOn: expect.any(String) + }, + { + id: "item-3", + $selectable: true, + title: "Item 3", + location: { + folderId: "folder-b" + }, + createdBy: identity2, + deletedBy: identity2, + deletedOn: expect.any(String) + } + ] + }); + }); + + it("should restore an item, removing it from the list", async () => { + const { presenter, controllers } = init(listGateway, deleteItemGateway, restoreItemGateway); + + // let's list some entries from the gateway + await controllers.listItems.execute(); + + expect(listGateway.execute).toHaveBeenCalledTimes(1); + + expect(presenter.vm).toMatchObject({ + items: [ + { + id: "item-1", + $selectable: true, + title: "Item 1", + location: { + folderId: "folder-a" + }, + createdBy: identity1, + deletedBy: identity2, + deletedOn: expect.any(String) + }, + { + id: "item-2", + $selectable: true, + title: "Item 2", + location: { + folderId: "folder-a" + }, + createdBy: identity1, + deletedBy: identity1, + deletedOn: expect.any(String) + }, + { + id: "item-3", + $selectable: true, + title: "Item 3", + location: { + folderId: "folder-b" + }, + createdBy: identity2, + deletedBy: identity2, + deletedOn: expect.any(String) + } + ] + }); + + const restorePromise = controllers.restoreItem.execute(item1.id); + + // Let's check the transition to loading state + expect(presenter.vm).toMatchObject({ + loading: { + [LoadingActions.restore]: true + } + }); + + await restorePromise; + + expect(restoreItemGateway.execute).toHaveBeenCalledTimes(1); + expect(restoreItemGateway.execute).toHaveBeenCalledWith(item1.id); + + expect(presenter.vm).toMatchObject({ + items: [ + { + id: "item-2", + $selectable: true, + title: "Item 2", + location: { + folderId: "folder-a" + }, + createdBy: identity1, + deletedBy: identity1, + deletedOn: expect.any(String) + }, + { + id: "item-3", + $selectable: true, + title: "Item 3", + location: { + folderId: "folder-b" + }, + createdBy: identity2, + deletedBy: identity2, + deletedOn: expect.any(String) + } + ], + restoredItems: [ + { + id: "item-1", + $selectable: true, + title: "Item 1", + location: { + folderId: "folder-a" + }, + createdBy: identity1, + deletedBy: identity2, + deletedOn: expect.any(String) + } + ] + }); + }); +}); diff --git a/packages/app-trash-bin/src/Presentation/TrashBin/TrashBin.tsx b/packages/app-trash-bin/src/Presentation/TrashBin/TrashBin.tsx new file mode 100644 index 00000000000..233a155d0be --- /dev/null +++ b/packages/app-trash-bin/src/Presentation/TrashBin/TrashBin.tsx @@ -0,0 +1,125 @@ +import React, { useEffect, useMemo } from "react"; +import { observer } from "mobx-react-lite"; +import { + loadingRepositoryFactory, + metaRepositoryFactory, + Sorting, + sortRepositoryFactory +} from "@webiny/app-utils"; +import { TrashBinProvider } from "../hooks"; +import { TrashBinOverlay } from "../components/TrashBinOverlay"; +import { TrashBinPresenter } from "./TrashBinPresenter"; +import { + selectedItemsRepositoryFactory, + searchRepositoryFactory, + SortingRepositoryWithDefaults, + trashBinItemsRepositoryFactory, + TrashBinItemsRepositoryWithLoading, + ITrashBinItemMapper, + TrashBinItemDTO +} from "~/Domain"; +import { + ITrashBinDeleteItemGateway, + ITrashBinListGateway, + ITrashBinRestoreItemGateway +} from "~/Gateways"; +import { TrashBinControllers } from "~/Presentation/TrashBin/TrashBinControllers"; + +export interface TrashBinProps { + listGateway: ITrashBinListGateway; + deleteGateway: ITrashBinDeleteItemGateway; + restoreGateway: ITrashBinRestoreItemGateway; + itemMapper: ITrashBinItemMapper; + onClose: () => void; + onItemRestore: (item: TrashBinItemDTO) => Promise; + sorting: Sorting[]; + title: string; + nameColumnId?: string; +} + +export const TrashBin = observer((props: TrashBinProps) => { + const metaRepository = useMemo(() => { + return metaRepositoryFactory.getRepository(); + }, []); + + const searchRepository = useMemo(() => { + return searchRepositoryFactory.getRepository(); + }, []); + + const sortingRepository = useMemo(() => { + const sortRepository = sortRepositoryFactory.getRepository(); + return new SortingRepositoryWithDefaults(props.sorting, sortRepository); + }, [props.sorting]); + + const loadingRepository = useMemo(() => { + return loadingRepositoryFactory.getRepository(); + }, []); + + const selectedRepository = useMemo(() => { + return selectedItemsRepositoryFactory.getRepository(); + }, []); + + const itemsRepository = useMemo(() => { + const trashBinItemsRepository = trashBinItemsRepositoryFactory.getRepository( + metaRepository, + props.listGateway, + props.deleteGateway, + props.restoreGateway, + props.itemMapper + ); + + return new TrashBinItemsRepositoryWithLoading(loadingRepository, trashBinItemsRepository); + }, [ + metaRepository, + loadingRepository, + props.listGateway, + props.deleteGateway, + props.restoreGateway, + props.itemMapper + ]); + + const controllers = useMemo(() => { + return new TrashBinControllers( + itemsRepository, + selectedRepository, + sortingRepository, + searchRepository + ).getControllers(); + }, [ + itemsRepository, + selectedRepository, + sortingRepository, + searchRepository, + loadingRepository + ]); + + const presenter = useMemo(() => { + return new TrashBinPresenter( + itemsRepository, + selectedRepository, + sortingRepository, + searchRepository, + props.nameColumnId + ); + }, [ + itemsRepository, + selectedRepository, + sortingRepository, + searchRepository, + props.nameColumnId + ]); + + useEffect(() => { + controllers.listItems.execute(); + }, []); + + return ( + + + + ); +}); diff --git a/packages/app-trash-bin/src/Presentation/TrashBin/TrashBinControllers.ts b/packages/app-trash-bin/src/Presentation/TrashBin/TrashBinControllers.ts new file mode 100644 index 00000000000..1e360b69ba2 --- /dev/null +++ b/packages/app-trash-bin/src/Presentation/TrashBin/TrashBinControllers.ts @@ -0,0 +1,90 @@ +import { ISortingRepository } from "@webiny/app-utils"; +import { ISearchRepository, ISelectedItemsRepository, ITrashBinItemsRepository } from "~/Domain"; +import { + DeleteItemController, + ListItemsController, + ListMoreItemsController, + RestoreItemController, + SearchItemsController, + SelectItemsController, + SortItemsController +} from "~/Presentation/TrashBin/controllers"; +import { + DeleteItemUseCase, + ListItemsUseCase, + ListItemsUseCaseWithSearch, + ListItemsUseCaseWithSorting, + ListMoreItemsUseCase, + RestoreItemUseCase, + SearchItemsUseCase, + SelectItemsUseCase, + SortItemsUseCase +} from "~/UseCases"; + +export class TrashBinControllers { + private readonly itemsRepository: ITrashBinItemsRepository; + private readonly selectedRepository: ISelectedItemsRepository; + private readonly sortingRepository: ISortingRepository; + private readonly searchRepository: ISearchRepository; + + constructor( + itemsRepository: ITrashBinItemsRepository, + selectedRepository: ISelectedItemsRepository, + sortingRepository: ISortingRepository, + searchRepository: ISearchRepository + ) { + this.itemsRepository = itemsRepository; + this.selectedRepository = selectedRepository; + this.sortingRepository = sortingRepository; + this.searchRepository = searchRepository; + } + + getControllers() { + // Select Items UseCase + const selectItemsUseCase = () => new SelectItemsUseCase(this.selectedRepository); + + // Sort Items UseCase + const sortItemsUseCase = () => new SortItemsUseCase(this.sortingRepository); + + // Search Items UseCase + const searchItemsUseCase = () => new SearchItemsUseCase(this.searchRepository); + + // List Items UseCase + const listItemsUseCase = () => { + const baseListItemsUseCase = new ListItemsUseCase(this.itemsRepository); + const listItemsWithSearch = new ListItemsUseCaseWithSearch( + this.searchRepository, + baseListItemsUseCase + ); + return new ListItemsUseCaseWithSorting(this.sortingRepository, listItemsWithSearch); + }; + + // List More Items UseCase + const listMoreItemsUseCase = () => new ListMoreItemsUseCase(this.itemsRepository); + + // Delete Item UseCase + const deleteItemUseCase = () => new DeleteItemUseCase(this.itemsRepository); + + // Restore Item UseCase + const restoreItemUseCase = () => new RestoreItemUseCase(this.itemsRepository); + + // Create controllers + const listItems = new ListItemsController(listItemsUseCase); + const listMoreItems = new ListMoreItemsController(listMoreItemsUseCase); + const deleteItem = new DeleteItemController(deleteItemUseCase); + const restoreItem = new RestoreItemController(restoreItemUseCase); + const selectItems = new SelectItemsController(selectItemsUseCase); + const sortItems = new SortItemsController(listItemsUseCase, sortItemsUseCase); + const searchItems = new SearchItemsController(listItemsUseCase, searchItemsUseCase); + + return { + listItems, + listMoreItems, + deleteItem, + restoreItem, + selectItems, + sortItems, + searchItems + }; + } +} diff --git a/packages/app-trash-bin/src/Presentation/TrashBin/TrashBinPresenter.ts b/packages/app-trash-bin/src/Presentation/TrashBin/TrashBinPresenter.ts new file mode 100644 index 00000000000..58ca0bb0256 --- /dev/null +++ b/packages/app-trash-bin/src/Presentation/TrashBin/TrashBinPresenter.ts @@ -0,0 +1,60 @@ +import { makeAutoObservable } from "mobx"; +import { ITrashBinItemMapper, TrashBinItem } from "~/Domain"; +import { ISortingRepository, MetaMapper, SortingMapper } from "@webiny/app-utils"; +import { + TrashBinItemMapper, + ITrashBinItemsRepository, + ISelectedItemsRepository, + ISearchRepository +} from "~/Domain/Repositories"; +import { LoadingActions } from "~/types"; + +export class TrashBinPresenter { + private itemsRepository: ITrashBinItemsRepository; + private selectedRepository: ISelectedItemsRepository; + private sortingRepository: ISortingRepository; + private searchRepository: ISearchRepository; + private itemMapper: ITrashBinItemMapper; + private readonly nameColumnId: string | undefined; + + constructor( + itemsRepository: ITrashBinItemsRepository, + selectedRepository: ISelectedItemsRepository, + sortingRepository: ISortingRepository, + searchRepository: ISearchRepository, + nameColumnId?: string + ) { + this.itemsRepository = itemsRepository; + this.selectedRepository = selectedRepository; + this.sortingRepository = sortingRepository; + this.searchRepository = searchRepository; + this.itemMapper = new TrashBinItemMapper(); + this.nameColumnId = nameColumnId; + makeAutoObservable(this); + } + + get vm() { + return { + items: this.mapItemsToDTOs(this.itemsRepository.getItems()), + restoredItems: this.mapItemsToDTOs(this.itemsRepository.getRestoredItems()), + selectedItems: this.mapItemsToDTOs(this.selectedRepository.getSelectedItems()), + meta: MetaMapper.toDto(this.itemsRepository.getMeta()), + sorting: this.sortingRepository.get().map(sort => SortingMapper.fromDTOtoColumn(sort)), + loading: this.itemsRepository.getLoading(), + isEmptyView: this.getIsEmptyView(), + searchQuery: this.searchRepository.get(), + searchLabel: "Search all items", + nameColumnId: this.nameColumnId || "id" + }; + } + + private mapItemsToDTOs(items: TrashBinItem[]) { + return items.map(item => this.itemMapper.toDTO(item)); + } + + private getIsEmptyView() { + const loading = this.itemsRepository.getLoading(); + const items = this.itemsRepository.getItems(); + return !loading[LoadingActions.list] && !items.length; + } +} diff --git a/packages/app-trash-bin/src/Presentation/TrashBin/controllers/DeleteItem/DeleteItemController.ts b/packages/app-trash-bin/src/Presentation/TrashBin/controllers/DeleteItem/DeleteItemController.ts new file mode 100644 index 00000000000..aa8382809a9 --- /dev/null +++ b/packages/app-trash-bin/src/Presentation/TrashBin/controllers/DeleteItem/DeleteItemController.ts @@ -0,0 +1,15 @@ +import { IDeleteItemUseCase } from "~/UseCases"; +import { IDeleteItemController } from "./IDeleteItemController"; + +export class DeleteItemController implements IDeleteItemController { + private readonly useCaseFactory: () => IDeleteItemUseCase; + + constructor(useCaseFactory: () => IDeleteItemUseCase) { + this.useCaseFactory = useCaseFactory; + } + + async execute(id: string) { + const deleteItemUseCase = this.useCaseFactory(); + await deleteItemUseCase.execute(id); + } +} diff --git a/packages/app-trash-bin/src/Presentation/TrashBin/controllers/DeleteItem/IDeleteItemController.ts b/packages/app-trash-bin/src/Presentation/TrashBin/controllers/DeleteItem/IDeleteItemController.ts new file mode 100644 index 00000000000..2be451a9045 --- /dev/null +++ b/packages/app-trash-bin/src/Presentation/TrashBin/controllers/DeleteItem/IDeleteItemController.ts @@ -0,0 +1,3 @@ +export interface IDeleteItemController { + execute: (id: string) => Promise; +} diff --git a/packages/app-trash-bin/src/Presentation/TrashBin/controllers/DeleteItem/index.ts b/packages/app-trash-bin/src/Presentation/TrashBin/controllers/DeleteItem/index.ts new file mode 100644 index 00000000000..eb236120ad3 --- /dev/null +++ b/packages/app-trash-bin/src/Presentation/TrashBin/controllers/DeleteItem/index.ts @@ -0,0 +1,2 @@ +export * from "./IDeleteItemController"; +export * from "./DeleteItemController"; diff --git a/packages/app-trash-bin/src/Presentation/TrashBin/controllers/ListItems/IListItemsController.ts b/packages/app-trash-bin/src/Presentation/TrashBin/controllers/ListItems/IListItemsController.ts new file mode 100644 index 00000000000..818bb02557c --- /dev/null +++ b/packages/app-trash-bin/src/Presentation/TrashBin/controllers/ListItems/IListItemsController.ts @@ -0,0 +1,3 @@ +export interface IListItemsController { + execute: () => Promise; +} diff --git a/packages/app-trash-bin/src/Presentation/TrashBin/controllers/ListItems/ListItemsController.ts b/packages/app-trash-bin/src/Presentation/TrashBin/controllers/ListItems/ListItemsController.ts new file mode 100644 index 00000000000..043b0caf0ae --- /dev/null +++ b/packages/app-trash-bin/src/Presentation/TrashBin/controllers/ListItems/ListItemsController.ts @@ -0,0 +1,15 @@ +import { IListItemsController } from "./IListItemsController"; +import { IListItemsUseCase } from "~/UseCases"; + +export class ListItemsController implements IListItemsController { + private readonly useCaseFactory: () => IListItemsUseCase; + + constructor(useCaseFactory: () => IListItemsUseCase) { + this.useCaseFactory = useCaseFactory; + } + + async execute() { + const listItemsUseCase = this.useCaseFactory(); + await listItemsUseCase.execute(); + } +} diff --git a/packages/app-trash-bin/src/Presentation/TrashBin/controllers/ListItems/index.ts b/packages/app-trash-bin/src/Presentation/TrashBin/controllers/ListItems/index.ts new file mode 100644 index 00000000000..40ad9b213a9 --- /dev/null +++ b/packages/app-trash-bin/src/Presentation/TrashBin/controllers/ListItems/index.ts @@ -0,0 +1,2 @@ +export * from "./IListItemsController"; +export * from "./ListItemsController"; diff --git a/packages/app-trash-bin/src/Presentation/TrashBin/controllers/ListMoreItems/IListMoreItemsController.ts b/packages/app-trash-bin/src/Presentation/TrashBin/controllers/ListMoreItems/IListMoreItemsController.ts new file mode 100644 index 00000000000..6bd7e1c747b --- /dev/null +++ b/packages/app-trash-bin/src/Presentation/TrashBin/controllers/ListMoreItems/IListMoreItemsController.ts @@ -0,0 +1,3 @@ +export interface IListMoreItemsController { + execute: () => Promise; +} diff --git a/packages/app-trash-bin/src/Presentation/TrashBin/controllers/ListMoreItems/ListMoreItemsController.ts b/packages/app-trash-bin/src/Presentation/TrashBin/controllers/ListMoreItems/ListMoreItemsController.ts new file mode 100644 index 00000000000..8e9764c695b --- /dev/null +++ b/packages/app-trash-bin/src/Presentation/TrashBin/controllers/ListMoreItems/ListMoreItemsController.ts @@ -0,0 +1,15 @@ +import { IListItemsUseCase, IListMoreItemsUseCase } from "~/UseCases"; +import { IListMoreItemsController } from "./IListMoreItemsController"; + +export class ListMoreItemsController implements IListMoreItemsController { + private readonly useCaseFactory: () => IListItemsUseCase; + + constructor(useCaseFactory: () => IListMoreItemsUseCase) { + this.useCaseFactory = useCaseFactory; + } + + async execute() { + const listMoreItemsUseCase = this.useCaseFactory(); + await listMoreItemsUseCase.execute(); + } +} diff --git a/packages/app-trash-bin/src/Presentation/TrashBin/controllers/ListMoreItems/index.ts b/packages/app-trash-bin/src/Presentation/TrashBin/controllers/ListMoreItems/index.ts new file mode 100644 index 00000000000..c42e3187277 --- /dev/null +++ b/packages/app-trash-bin/src/Presentation/TrashBin/controllers/ListMoreItems/index.ts @@ -0,0 +1,2 @@ +export * from "./IListMoreItemsController"; +export * from "./ListMoreItemsController"; diff --git a/packages/app-trash-bin/src/Presentation/TrashBin/controllers/RestoreItem/IRestoreItemController.ts b/packages/app-trash-bin/src/Presentation/TrashBin/controllers/RestoreItem/IRestoreItemController.ts new file mode 100644 index 00000000000..86a9ac97e75 --- /dev/null +++ b/packages/app-trash-bin/src/Presentation/TrashBin/controllers/RestoreItem/IRestoreItemController.ts @@ -0,0 +1,3 @@ +export interface IRestoreItemController { + execute: (id: string) => Promise; +} diff --git a/packages/app-trash-bin/src/Presentation/TrashBin/controllers/RestoreItem/RestoreItemController.ts b/packages/app-trash-bin/src/Presentation/TrashBin/controllers/RestoreItem/RestoreItemController.ts new file mode 100644 index 00000000000..62b255d400f --- /dev/null +++ b/packages/app-trash-bin/src/Presentation/TrashBin/controllers/RestoreItem/RestoreItemController.ts @@ -0,0 +1,15 @@ +import { IRestoreItemUseCase } from "~/UseCases"; +import { IRestoreItemController } from "./IRestoreItemController"; + +export class RestoreItemController implements IRestoreItemController { + private readonly useCaseFactory: () => IRestoreItemUseCase; + + constructor(useCaseFactory: () => IRestoreItemUseCase) { + this.useCaseFactory = useCaseFactory; + } + + async execute(id: string) { + const restoreItemUseCase = this.useCaseFactory(); + await restoreItemUseCase.execute(id); + } +} diff --git a/packages/app-trash-bin/src/Presentation/TrashBin/controllers/RestoreItem/index.ts b/packages/app-trash-bin/src/Presentation/TrashBin/controllers/RestoreItem/index.ts new file mode 100644 index 00000000000..39dece0d710 --- /dev/null +++ b/packages/app-trash-bin/src/Presentation/TrashBin/controllers/RestoreItem/index.ts @@ -0,0 +1,2 @@ +export * from "./IRestoreItemController"; +export * from "./RestoreItemController"; diff --git a/packages/app-trash-bin/src/Presentation/TrashBin/controllers/SearchItems/ISearchItemsController.ts b/packages/app-trash-bin/src/Presentation/TrashBin/controllers/SearchItems/ISearchItemsController.ts new file mode 100644 index 00000000000..ec3d52d320f --- /dev/null +++ b/packages/app-trash-bin/src/Presentation/TrashBin/controllers/SearchItems/ISearchItemsController.ts @@ -0,0 +1,3 @@ +export interface ISearchItemsController { + execute: (query: string) => Promise; +} diff --git a/packages/app-trash-bin/src/Presentation/TrashBin/controllers/SearchItems/SearchItemsController.ts b/packages/app-trash-bin/src/Presentation/TrashBin/controllers/SearchItems/SearchItemsController.ts new file mode 100644 index 00000000000..284a30158a8 --- /dev/null +++ b/packages/app-trash-bin/src/Presentation/TrashBin/controllers/SearchItems/SearchItemsController.ts @@ -0,0 +1,23 @@ +import { IListItemsUseCase, ISearchItemsUseCase } from "~/UseCases"; +import { ISearchItemsController } from "./ISearchItemsController"; + +export class SearchItemsController implements ISearchItemsController { + private readonly listItemsUseCaseFactory: () => IListItemsUseCase; + private readonly searchItemsUseCaseFactory: () => ISearchItemsUseCase; + + constructor( + listItemsUseCaseFactory: () => IListItemsUseCase, + searchItemsUseCaseFactory: () => ISearchItemsUseCase + ) { + this.listItemsUseCaseFactory = listItemsUseCaseFactory; + this.searchItemsUseCaseFactory = searchItemsUseCaseFactory; + } + + async execute(query: string) { + const searchItemsUseCase = this.searchItemsUseCaseFactory(); + const listItemsUseCase = this.listItemsUseCaseFactory(); + + await searchItemsUseCase.execute(query); + await listItemsUseCase.execute(); + } +} diff --git a/packages/app-trash-bin/src/Presentation/TrashBin/controllers/SearchItems/index.ts b/packages/app-trash-bin/src/Presentation/TrashBin/controllers/SearchItems/index.ts new file mode 100644 index 00000000000..9efc34937ac --- /dev/null +++ b/packages/app-trash-bin/src/Presentation/TrashBin/controllers/SearchItems/index.ts @@ -0,0 +1,2 @@ +export * from "./ISearchItemsController"; +export * from "./SearchItemsController"; diff --git a/packages/app-trash-bin/src/Presentation/TrashBin/controllers/SelectItems/ISelectItemsController.ts b/packages/app-trash-bin/src/Presentation/TrashBin/controllers/SelectItems/ISelectItemsController.ts new file mode 100644 index 00000000000..7ce7844afa3 --- /dev/null +++ b/packages/app-trash-bin/src/Presentation/TrashBin/controllers/SelectItems/ISelectItemsController.ts @@ -0,0 +1,5 @@ +import { TrashBinItemDTO } from "~/Domain"; + +export interface ISelectItemsController { + execute: (items: TrashBinItemDTO[]) => Promise; +} diff --git a/packages/app-trash-bin/src/Presentation/TrashBin/controllers/SelectItems/SelectItemsController.ts b/packages/app-trash-bin/src/Presentation/TrashBin/controllers/SelectItems/SelectItemsController.ts new file mode 100644 index 00000000000..7ff8350c067 --- /dev/null +++ b/packages/app-trash-bin/src/Presentation/TrashBin/controllers/SelectItems/SelectItemsController.ts @@ -0,0 +1,17 @@ +import { TrashBinItem, TrashBinItemDTO } from "~/Domain"; +import { ISelectItemsUseCase } from "~/UseCases"; +import { ISelectItemsController } from "./ISelectItemsController"; + +export class SelectItemsController implements ISelectItemsController { + private readonly useCaseFactory: () => ISelectItemsUseCase; + + constructor(useCaseFactory: () => ISelectItemsUseCase) { + this.useCaseFactory = useCaseFactory; + } + + async execute(items: TrashBinItemDTO[]) { + const selectItemsUseCase = this.useCaseFactory(); + const itemsDTOs = items.map(item => TrashBinItem.create(item)); + await selectItemsUseCase.execute(itemsDTOs); + } +} diff --git a/packages/app-trash-bin/src/Presentation/TrashBin/controllers/SelectItems/index.ts b/packages/app-trash-bin/src/Presentation/TrashBin/controllers/SelectItems/index.ts new file mode 100644 index 00000000000..b90b5325f34 --- /dev/null +++ b/packages/app-trash-bin/src/Presentation/TrashBin/controllers/SelectItems/index.ts @@ -0,0 +1,2 @@ +export * from "./ISelectItemsController"; +export * from "./SelectItemsController"; diff --git a/packages/app-trash-bin/src/Presentation/TrashBin/controllers/SortItems/ISortItemsController.ts b/packages/app-trash-bin/src/Presentation/TrashBin/controllers/SortItems/ISortItemsController.ts new file mode 100644 index 00000000000..18c2ea97058 --- /dev/null +++ b/packages/app-trash-bin/src/Presentation/TrashBin/controllers/SortItems/ISortItemsController.ts @@ -0,0 +1,5 @@ +import { OnSortingChange } from "@webiny/ui/DataTable"; + +export interface ISortItemsController { + execute: OnSortingChange; +} diff --git a/packages/app-trash-bin/src/Presentation/TrashBin/controllers/SortItems/SortItemsController.ts b/packages/app-trash-bin/src/Presentation/TrashBin/controllers/SortItems/SortItemsController.ts new file mode 100644 index 00000000000..087bfc2f7e8 --- /dev/null +++ b/packages/app-trash-bin/src/Presentation/TrashBin/controllers/SortItems/SortItemsController.ts @@ -0,0 +1,31 @@ +import { OnSortingChange } from "@webiny/ui/DataTable"; +import { ColumnSorting, SortingMapper } from "@webiny/app-utils"; +import { IListItemsUseCase, ISortItemsUseCase } from "~/UseCases"; +import { ISortItemsController } from "./ISortItemsController"; + +export class SortItemsController implements ISortItemsController { + private listItemsUseCaseFactory: () => IListItemsUseCase; + private sortItemsUseCaseFactory: () => ISortItemsUseCase; + + constructor( + listItemsUseCaseFactory: () => IListItemsUseCase, + sortItemsUseCaseFactory: () => ISortItemsUseCase + ) { + this.listItemsUseCaseFactory = listItemsUseCaseFactory; + this.sortItemsUseCaseFactory = sortItemsUseCaseFactory; + } + + public execute: OnSortingChange = async updaterOrValue => { + let newSorts: ColumnSorting[] = []; + + if (typeof updaterOrValue === "function") { + newSorts = updaterOrValue(newSorts || []); + } + + const sortItemsUseCase = this.sortItemsUseCaseFactory(); + const listItemsUseCase = this.listItemsUseCaseFactory(); + + await sortItemsUseCase.execute(newSorts.map(sort => SortingMapper.fromColumnToDTO(sort))); + await listItemsUseCase.execute(); + }; +} diff --git a/packages/app-trash-bin/src/Presentation/TrashBin/controllers/SortItems/index.ts b/packages/app-trash-bin/src/Presentation/TrashBin/controllers/SortItems/index.ts new file mode 100644 index 00000000000..ec0a55e798e --- /dev/null +++ b/packages/app-trash-bin/src/Presentation/TrashBin/controllers/SortItems/index.ts @@ -0,0 +1,2 @@ +export * from "./ISortItemsController"; +export * from "./SortItemsController"; diff --git a/packages/app-trash-bin/src/Presentation/TrashBin/controllers/index.ts b/packages/app-trash-bin/src/Presentation/TrashBin/controllers/index.ts new file mode 100644 index 00000000000..2ffd2f50fa7 --- /dev/null +++ b/packages/app-trash-bin/src/Presentation/TrashBin/controllers/index.ts @@ -0,0 +1,7 @@ +export * from "./DeleteItem"; +export * from "./ListItems"; +export * from "./ListMoreItems"; +export * from "./RestoreItem"; +export * from "./SearchItems"; +export * from "./SelectItems"; +export * from "./SortItems"; diff --git a/packages/app-trash-bin/src/Presentation/TrashBin/index.ts b/packages/app-trash-bin/src/Presentation/TrashBin/index.ts new file mode 100644 index 00000000000..c9e88d06227 --- /dev/null +++ b/packages/app-trash-bin/src/Presentation/TrashBin/index.ts @@ -0,0 +1 @@ +export * from "./TrashBin"; diff --git a/packages/app-trash-bin/src/Presentation/TrashBinConfigs/TrashBinConfigs.tsx b/packages/app-trash-bin/src/Presentation/TrashBinConfigs/TrashBinConfigs.tsx new file mode 100644 index 00000000000..f93e568b272 --- /dev/null +++ b/packages/app-trash-bin/src/Presentation/TrashBinConfigs/TrashBinConfigs.tsx @@ -0,0 +1,61 @@ +import React from "react"; + +import { TrashBinListConfig } from "~/Presentation/configs"; +import { BulkActionsDeleteItems, BulkActionsRestoreItems } from "../components/BulkActions"; +import { + CellActions, + CellCreatedBy, + CellDeletedBy, + CellDeletedOn, + CellTitle +} from "~/Presentation/components/Cells"; +import { DeleteItemAction, RestoreItemAction } from "~/Presentation/components/Actions"; + +const { Browser } = TrashBinListConfig; + +export const TrashBinConfigs = () => { + return ( + <> + + } + sortable={true} + hideable={false} + size={200} + /> + } + /> + } + /> + } + sortable={true} + /> + } + size={80} + className={"rmwc-data-table__cell--align-end"} + hideable={false} + resizable={false} + /> + + } /> + } /> + } /> + } /> + + + ); +}; diff --git a/packages/app-trash-bin/src/Presentation/TrashBinConfigs/index.ts b/packages/app-trash-bin/src/Presentation/TrashBinConfigs/index.ts new file mode 100644 index 00000000000..48dc38b1bcd --- /dev/null +++ b/packages/app-trash-bin/src/Presentation/TrashBinConfigs/index.ts @@ -0,0 +1 @@ +export * from "./TrashBinConfigs"; diff --git a/packages/app-trash-bin/src/Presentation/TrashBinRenderer/TrashBinRenderer.tsx b/packages/app-trash-bin/src/Presentation/TrashBinRenderer/TrashBinRenderer.tsx new file mode 100644 index 00000000000..678e1affab8 --- /dev/null +++ b/packages/app-trash-bin/src/Presentation/TrashBinRenderer/TrashBinRenderer.tsx @@ -0,0 +1,27 @@ +import React from "react"; +import { useAcoConfig } from "@webiny/app-aco"; +import { Sorting } from "@webiny/app-utils"; +import { TrashBinItemDTO } from "~/Domain"; +import { TrashBinProps } from "~/Presentation"; +import { TrashBin } from "../TrashBin"; + +export type TrashBinRendererProps = Omit & { + onClose: () => void; + onItemRestore: (item: TrashBinItemDTO) => void; +}; + +export const TrashBinRenderer = ({ title = "Trash Bin", ...props }: TrashBinRendererProps) => { + const { table } = useAcoConfig(); + + if (!table.sorting.length) { + return null; + } + + return ( + Sorting.create(sort))} + /> + ); +}; diff --git a/packages/app-trash-bin/src/Presentation/TrashBinRenderer/index.ts b/packages/app-trash-bin/src/Presentation/TrashBinRenderer/index.ts new file mode 100644 index 00000000000..762f62550b0 --- /dev/null +++ b/packages/app-trash-bin/src/Presentation/TrashBinRenderer/index.ts @@ -0,0 +1 @@ +export * from "./TrashBinRenderer"; diff --git a/packages/app-trash-bin/src/Presentation/abstractions/ITrashBinControllers.ts b/packages/app-trash-bin/src/Presentation/abstractions/ITrashBinControllers.ts new file mode 100644 index 00000000000..baa5b85853c --- /dev/null +++ b/packages/app-trash-bin/src/Presentation/abstractions/ITrashBinControllers.ts @@ -0,0 +1,19 @@ +import { + IDeleteItemController, + IListItemsController, + IListMoreItemsController, + IRestoreItemController, + ISearchItemsController, + ISelectItemsController, + ISortItemsController +} from "~/Presentation/TrashBin/controllers"; + +export interface ITrashBinControllers { + deleteItem: IDeleteItemController; + restoreItem: IRestoreItemController; + listMoreItems: IListMoreItemsController; + listItems: IListItemsController; + selectItems: ISelectItemsController; + sortItems: ISortItemsController; + searchItems: ISearchItemsController; +} diff --git a/packages/app-trash-bin/src/Presentation/abstractions/ITrashBinPresenter.ts b/packages/app-trash-bin/src/Presentation/abstractions/ITrashBinPresenter.ts new file mode 100644 index 00000000000..0bc5224b373 --- /dev/null +++ b/packages/app-trash-bin/src/Presentation/abstractions/ITrashBinPresenter.ts @@ -0,0 +1,20 @@ +import { ColumnSorting } from "@webiny/app-utils"; +import { TrashBinItemDTO } from "~/Domain"; +import { TrashBinMetaResponse } from "~/types"; + +export interface TrashBinPresenterViewModel { + items: TrashBinItemDTO[]; + restoredItems: TrashBinItemDTO[]; + selectedItems: TrashBinItemDTO[]; + sorting: ColumnSorting[]; + loading: Record; + isEmptyView: boolean; + meta: TrashBinMetaResponse; + searchQuery: string | undefined; + searchLabel: string; + nameColumnId: string; +} + +export interface ITrashBinPresenter { + get vm(): TrashBinPresenterViewModel; +} diff --git a/packages/app-trash-bin/src/Presentation/abstractions/index.ts b/packages/app-trash-bin/src/Presentation/abstractions/index.ts new file mode 100644 index 00000000000..35ee6c6ed5d --- /dev/null +++ b/packages/app-trash-bin/src/Presentation/abstractions/index.ts @@ -0,0 +1,2 @@ +export * from "./ITrashBinControllers"; +export * from "./ITrashBinPresenter"; diff --git a/packages/app-trash-bin/src/Presentation/components/Actions/DeleteItem/DeleteItem.tsx b/packages/app-trash-bin/src/Presentation/components/Actions/DeleteItem/DeleteItem.tsx new file mode 100644 index 00000000000..ee6f3a7a9ad --- /dev/null +++ b/packages/app-trash-bin/src/Presentation/components/Actions/DeleteItem/DeleteItem.tsx @@ -0,0 +1,12 @@ +import React from "react"; +import { ReactComponent as Delete } from "@material-design-icons/svg/outlined/delete.svg"; +import { useDeleteTrashBinItem, useTrashBinItem } from "~/Presentation/hooks"; +import { TrashBinListConfig } from "~/Presentation/configs"; + +export const DeleteItemAction = () => { + const { item } = useTrashBinItem(); + const { openDialogDeleteItem } = useDeleteTrashBinItem({ item }); + const { OptionsMenuItem } = TrashBinListConfig.Browser.EntryAction; + + return } label={"Delete"} onAction={openDialogDeleteItem} />; +}; diff --git a/packages/app-trash-bin/src/Presentation/components/Actions/DeleteItem/index.ts b/packages/app-trash-bin/src/Presentation/components/Actions/DeleteItem/index.ts new file mode 100644 index 00000000000..5c0843058da --- /dev/null +++ b/packages/app-trash-bin/src/Presentation/components/Actions/DeleteItem/index.ts @@ -0,0 +1 @@ +export * from "./DeleteItem"; diff --git a/packages/app-trash-bin/src/Presentation/components/Actions/RestoreItem/RestoreItem.tsx b/packages/app-trash-bin/src/Presentation/components/Actions/RestoreItem/RestoreItem.tsx new file mode 100644 index 00000000000..90378125478 --- /dev/null +++ b/packages/app-trash-bin/src/Presentation/components/Actions/RestoreItem/RestoreItem.tsx @@ -0,0 +1,14 @@ +import React from "react"; +import { ReactComponent as Restore } from "@material-design-icons/svg/outlined/restore.svg"; +import { useRestoreTrashBinItem, useTrashBinItem } from "~/Presentation/hooks"; +import { TrashBinListConfig } from "~/Presentation/configs"; + +export const RestoreItemAction = () => { + const { item } = useTrashBinItem(); + const { openDialogRestoreItem } = useRestoreTrashBinItem({ item }); + const { OptionsMenuItem } = TrashBinListConfig.Browser.EntryAction; + + return ( + } label={"Restore"} onAction={openDialogRestoreItem} /> + ); +}; diff --git a/packages/app-trash-bin/src/Presentation/components/Actions/RestoreItem/index.ts b/packages/app-trash-bin/src/Presentation/components/Actions/RestoreItem/index.ts new file mode 100644 index 00000000000..358af2e168e --- /dev/null +++ b/packages/app-trash-bin/src/Presentation/components/Actions/RestoreItem/index.ts @@ -0,0 +1 @@ +export * from "./RestoreItem"; diff --git a/packages/app-trash-bin/src/Presentation/components/Actions/index.ts b/packages/app-trash-bin/src/Presentation/components/Actions/index.ts new file mode 100644 index 00000000000..86c4b7d6b28 --- /dev/null +++ b/packages/app-trash-bin/src/Presentation/components/Actions/index.ts @@ -0,0 +1,2 @@ +export * from "./DeleteItem"; +export * from "./RestoreItem"; diff --git a/packages/app-trash-bin/src/Presentation/components/BottomInfoBar/BottomInfoBar.styled.tsx b/packages/app-trash-bin/src/Presentation/components/BottomInfoBar/BottomInfoBar.styled.tsx new file mode 100644 index 00000000000..46d00e7ff0b --- /dev/null +++ b/packages/app-trash-bin/src/Presentation/components/BottomInfoBar/BottomInfoBar.styled.tsx @@ -0,0 +1,45 @@ +import styled from "@emotion/styled"; + +export const BottomInfoBarWrapper = styled("div")` + font-size: 0.8rem; + position: sticky; + bottom: 0; + height: 30px; + color: var(--mdc-theme-text-secondary-on-background); + border-top: 1px solid var(--mdc-theme-on-background); + background: var(--mdc-theme-surface); + width: 100%; + transform: translateZ(0); + overflow: hidden; + display: flex; + align-items: center; + z-index: 1; +`; + +export const BottomInfoBarInner = styled("div")` + padding: 0 10px; + width: 100%; +`; + +export const StatusWrapper = styled("div")` + color: var(--mdc-theme-primary); + position: absolute; + right: 0; + bottom: 10px; + margin-right: 10px; + display: flex; + align-items: center; + > div { + display: inline-block; + } +`; + +export const CircularProgressHolder = styled("div")` + position: relative; + height: 12px; + width: 12px; +`; + +export const UploadingLabel = styled("div")` + margin-right: 5px; +`; diff --git a/packages/app-trash-bin/src/Presentation/components/BottomInfoBar/BottomInfoBar.tsx b/packages/app-trash-bin/src/Presentation/components/BottomInfoBar/BottomInfoBar.tsx new file mode 100644 index 00000000000..ee57f635869 --- /dev/null +++ b/packages/app-trash-bin/src/Presentation/components/BottomInfoBar/BottomInfoBar.tsx @@ -0,0 +1,23 @@ +import React from "react"; +import { ListMeta } from "./ListMeta"; +import { ListStatus } from "./ListStatus"; +import { BottomInfoBarInner, BottomInfoBarWrapper } from "./BottomInfoBar.styled"; +import { LoadingActions } from "~/types"; +import { useTrashBin } from "~/Presentation/hooks"; + +export const BottomInfoBar = () => { + const { vm } = useTrashBin(); + + return ( + + + + + + + ); +}; diff --git a/packages/app-trash-bin/src/Presentation/components/BottomInfoBar/ListMeta.tsx b/packages/app-trash-bin/src/Presentation/components/BottomInfoBar/ListMeta.tsx new file mode 100644 index 00000000000..daa0fde97f9 --- /dev/null +++ b/packages/app-trash-bin/src/Presentation/components/BottomInfoBar/ListMeta.tsx @@ -0,0 +1,19 @@ +import React, { useCallback } from "react"; + +export interface ListMetaProps { + loading: boolean; + currentCount: number; + totalCount: number; +} + +export const ListMeta = (props: ListMetaProps) => { + const getLabel = useCallback((count = 0): string => { + return `${count} ${count === 1 ? "item" : "items"}`; + }, []); + + if (props.loading) { + return null; + } + + return {`Showing ${props.currentCount} out of ${getLabel(props.totalCount)}.`}; +}; diff --git a/packages/app-trash-bin/src/Presentation/components/BottomInfoBar/ListStatus.tsx b/packages/app-trash-bin/src/Presentation/components/BottomInfoBar/ListStatus.tsx new file mode 100644 index 00000000000..f55afb9df69 --- /dev/null +++ b/packages/app-trash-bin/src/Presentation/components/BottomInfoBar/ListStatus.tsx @@ -0,0 +1,22 @@ +import React from "react"; +import { CircularProgress } from "@webiny/ui/Progress"; +import { CircularProgressHolder, StatusWrapper, UploadingLabel } from "./BottomInfoBar.styled"; + +export interface ListStatusProps { + loading: boolean; +} + +export const ListStatus = ({ loading }: ListStatusProps) => { + if (!loading) { + return null; + } + + return ( + + {"Loading more items..."} + + + + + ); +}; diff --git a/packages/app-trash-bin/src/Presentation/components/BottomInfoBar/index.tsx b/packages/app-trash-bin/src/Presentation/components/BottomInfoBar/index.tsx new file mode 100644 index 00000000000..c627a1591a9 --- /dev/null +++ b/packages/app-trash-bin/src/Presentation/components/BottomInfoBar/index.tsx @@ -0,0 +1 @@ +export * from "./BottomInfoBar"; diff --git a/packages/app-trash-bin/src/Presentation/components/BulkActions/BulkActions/BulkActions.styled.tsx b/packages/app-trash-bin/src/Presentation/components/BulkActions/BulkActions/BulkActions.styled.tsx new file mode 100644 index 00000000000..c8ed13ba467 --- /dev/null +++ b/packages/app-trash-bin/src/Presentation/components/BulkActions/BulkActions/BulkActions.styled.tsx @@ -0,0 +1,30 @@ +import styled from "@emotion/styled"; +import { ButtonContainer } from "@webiny/app-admin"; + +export const BulkActionsContainer = styled.div` + width: 100%; + height: 64px; + background-color: var(--mdc-theme-surface); + border-bottom: 1px solid rgba(0, 0, 0, 0.12); + position: absolute; + top: 0; + left: 0; + z-index: 4; +`; + +export const BulkActionsInner = styled.div` + height: 100%; + display: flex; + align-items: center; + justify-content: space-between; + padding: 0 16px; +`; + +export const ButtonsContainer = styled.div` + display: flex; + align-items: center; + + ${ButtonContainer} { + margin: 0; + } +`; diff --git a/packages/app-trash-bin/src/Presentation/components/BulkActions/BulkActions/BulkActions.tsx b/packages/app-trash-bin/src/Presentation/components/BulkActions/BulkActions/BulkActions.tsx new file mode 100644 index 00000000000..d5072011808 --- /dev/null +++ b/packages/app-trash-bin/src/Presentation/components/BulkActions/BulkActions/BulkActions.tsx @@ -0,0 +1,37 @@ +import React, { useMemo } from "react"; +import { ReactComponent as Close } from "@material-design-icons/svg/outlined/close.svg"; +import { Buttons } from "@webiny/app-admin"; +import { IconButton } from "@webiny/ui/Button"; +import { Typography } from "@webiny/ui/Typography"; +import { useTrashBinListConfig } from "~/Presentation/configs"; +import { useTrashBin } from "~/Presentation/hooks"; +import { BulkActionsContainer, BulkActionsInner, ButtonsContainer } from "./BulkActions.styled"; + +export const getEntriesLabel = (count = 0): string => { + return `${count} ${count === 1 ? "item" : "items"}`; +}; + +export const BulkActions = () => { + const { browser } = useTrashBinListConfig(); + const { vm, selectItems } = useTrashBin(); + + const headline = useMemo((): string => { + return getEntriesLabel(vm.selectedItems.length) + ` selected:`; + }, [vm.selectedItems]); + + if (!vm.selectedItems.length) { + return null; + } + + return ( + + + + {headline} + + + } onClick={() => selectItems([])} /> + + + ); +}; diff --git a/packages/app-trash-bin/src/Presentation/components/BulkActions/BulkActions/index.ts b/packages/app-trash-bin/src/Presentation/components/BulkActions/BulkActions/index.ts new file mode 100644 index 00000000000..488ed8eb8ab --- /dev/null +++ b/packages/app-trash-bin/src/Presentation/components/BulkActions/BulkActions/index.ts @@ -0,0 +1 @@ +export * from "./BulkActions"; diff --git a/packages/app-trash-bin/src/Presentation/components/BulkActions/DeleteItems/DeleteItems.tsx b/packages/app-trash-bin/src/Presentation/components/BulkActions/DeleteItems/DeleteItems.tsx new file mode 100644 index 00000000000..2830ae9e8ba --- /dev/null +++ b/packages/app-trash-bin/src/Presentation/components/BulkActions/DeleteItems/DeleteItems.tsx @@ -0,0 +1,59 @@ +import React, { useMemo } from "react"; +import { ReactComponent as DeleteIcon } from "@material-design-icons/svg/outlined/delete.svg"; +import { observer } from "mobx-react-lite"; +import { TrashBinListConfig } from "~/Presentation/configs"; +import { useTrashBin } from "~/Presentation/hooks"; +import { getEntriesLabel } from "../BulkActions"; + +export const BulkActionsDeleteItems = observer(() => { + const { deleteItem } = useTrashBin(); + + const { useWorker, useButtons, useDialog } = TrashBinListConfig.Browser.BulkAction; + const { IconButton } = useButtons(); + const worker = useWorker(); + const { showConfirmationDialog, showResultsDialog } = useDialog(); + + const entriesLabel = useMemo(() => { + return getEntriesLabel(worker.items.length); + }, [worker.items.length]); + + const openDeleteEntriesDialog = () => + showConfirmationDialog({ + title: "Delete items", + message: `You are about to permanent delete ${entriesLabel}. Are you sure you want to continue?`, + loadingLabel: `Processing ${entriesLabel}`, + execute: async () => { + await worker.processInSeries(async ({ item, report }) => { + try { + await deleteItem(item.id); + report.success({ + title: `${item.title}`, + message: "Item successfully deleted." + }); + } catch (e) { + report.error({ + title: `${item.title}`, + message: e.message || "Unknown error while deleting the item" + }); + } + }); + + worker.resetItems(); + + showResultsDialog({ + results: worker.results, + title: "Delete items", + message: "Finished deleting items! See full report below:" + }); + } + }); + + return ( + } + onAction={openDeleteEntriesDialog} + label={`Delete ${entriesLabel}`} + tooltipPlacement={"bottom"} + /> + ); +}); diff --git a/packages/app-trash-bin/src/Presentation/components/BulkActions/DeleteItems/index.ts b/packages/app-trash-bin/src/Presentation/components/BulkActions/DeleteItems/index.ts new file mode 100644 index 00000000000..9157fda633a --- /dev/null +++ b/packages/app-trash-bin/src/Presentation/components/BulkActions/DeleteItems/index.ts @@ -0,0 +1 @@ +export * from "./DeleteItems"; diff --git a/packages/app-trash-bin/src/Presentation/components/BulkActions/RestoreItems/RestoreItems.tsx b/packages/app-trash-bin/src/Presentation/components/BulkActions/RestoreItems/RestoreItems.tsx new file mode 100644 index 00000000000..55323680c8c --- /dev/null +++ b/packages/app-trash-bin/src/Presentation/components/BulkActions/RestoreItems/RestoreItems.tsx @@ -0,0 +1,78 @@ +import React, { useCallback, useMemo } from "react"; +import { ReactComponent as RestoreIcon } from "@material-design-icons/svg/outlined/restore.svg"; +import { observer } from "mobx-react-lite"; +import { TrashBinListConfig } from "~/Presentation/configs"; +import { useTrashBin } from "~/Presentation/hooks"; +import { getEntriesLabel } from "../BulkActions"; +import { RestoreItemsReportMessage } from "~/Presentation/components/BulkActions/RestoreItems/RestoreItemsReportMessage"; +import { TrashBinItemDTO } from "~/Domain"; + +export const BulkActionsRestoreItems = observer(() => { + const { vm, restoreItem, onItemRestore } = useTrashBin(); + + const { useWorker, useButtons, useDialog } = TrashBinListConfig.Browser.BulkAction; + const { IconButton } = useButtons(); + const worker = useWorker(); + const { showConfirmationDialog, showResultsDialog, hideResultsDialog } = useDialog(); + + const entriesLabel = useMemo(() => { + return getEntriesLabel(worker.items.length); + }, [worker.items.length]); + + const onLocationClick = useCallback( + async (item: TrashBinItemDTO) => { + hideResultsDialog(); + await onItemRestore(item); + }, + [onItemRestore] + ); + + const openRestoreEntriesDialog = () => + showConfirmationDialog({ + title: "Restore items", + message: `You are about to restore ${entriesLabel}. Are you sure you want to continue?`, + loadingLabel: `Processing ${entriesLabel}`, + execute: async () => { + await worker.processInSeries(async ({ item, report }) => { + try { + await restoreItem(item.id); + + const restoredItem = vm.restoredItems.find( + restored => restored.id === item.id + ); + + report.success({ + title: `${item.title}`, + message: restoredItem && ( + onLocationClick(restoredItem)} + /> + ) + }); + } catch (e) { + report.error({ + title: `${item.title}`, + message: e.message || "Unknown error while restoring the item." + }); + } + }); + + worker.resetItems(); + + showResultsDialog({ + results: worker.results, + title: "Restore items", + message: "Finished restoring items! See full report below:" + }); + } + }); + + return ( + } + onAction={openRestoreEntriesDialog} + label={`Restore ${entriesLabel}`} + tooltipPlacement={"bottom"} + /> + ); +}); diff --git a/packages/app-trash-bin/src/Presentation/components/BulkActions/RestoreItems/RestoreItemsReportMessage.tsx b/packages/app-trash-bin/src/Presentation/components/BulkActions/RestoreItems/RestoreItemsReportMessage.tsx new file mode 100644 index 00000000000..8cda4543cdb --- /dev/null +++ b/packages/app-trash-bin/src/Presentation/components/BulkActions/RestoreItems/RestoreItemsReportMessage.tsx @@ -0,0 +1,18 @@ +import React from "react"; +import { LocationAction } from "./RestoredItems.styled"; + +export interface RestoreItemsReportMessageProps { + onLocationClick: () => void; +} + +export const RestoreItemsReportMessage = (props: RestoreItemsReportMessageProps) => { + return ( + <> + Item successfully restored ( + props.onLocationClick()}> + {"see location"} + + ). + + ); +}; diff --git a/packages/app-trash-bin/src/Presentation/components/BulkActions/RestoreItems/RestoredItems.styled.tsx b/packages/app-trash-bin/src/Presentation/components/BulkActions/RestoreItems/RestoredItems.styled.tsx new file mode 100644 index 00000000000..65221a98176 --- /dev/null +++ b/packages/app-trash-bin/src/Presentation/components/BulkActions/RestoreItems/RestoredItems.styled.tsx @@ -0,0 +1,6 @@ +import styled from "@emotion/styled"; + +export const LocationAction = styled.span` + color: var(--mdc-theme-primary); + cursor: pointer; +`; diff --git a/packages/app-trash-bin/src/Presentation/components/BulkActions/RestoreItems/index.ts b/packages/app-trash-bin/src/Presentation/components/BulkActions/RestoreItems/index.ts new file mode 100644 index 00000000000..d4408aabc58 --- /dev/null +++ b/packages/app-trash-bin/src/Presentation/components/BulkActions/RestoreItems/index.ts @@ -0,0 +1 @@ +export * from "./RestoreItems"; diff --git a/packages/app-trash-bin/src/Presentation/components/BulkActions/index.ts b/packages/app-trash-bin/src/Presentation/components/BulkActions/index.ts new file mode 100644 index 00000000000..71e5be370da --- /dev/null +++ b/packages/app-trash-bin/src/Presentation/components/BulkActions/index.ts @@ -0,0 +1,3 @@ +export * from "./BulkActions"; +export * from "./DeleteItems"; +export * from "./RestoreItems"; diff --git a/packages/app-trash-bin/src/Presentation/components/Cells/CellActions/CellActions.tsx b/packages/app-trash-bin/src/Presentation/components/Cells/CellActions/CellActions.tsx new file mode 100644 index 00000000000..936e4bc289f --- /dev/null +++ b/packages/app-trash-bin/src/Presentation/components/Cells/CellActions/CellActions.tsx @@ -0,0 +1,20 @@ +import React from "react"; +import { useAcoConfig } from "@webiny/app-aco"; +import { OptionsMenu } from "@webiny/app-admin"; +import { TrashBinListConfig } from "~/Presentation/configs"; +import { TrashBinItemProvider } from "~/Presentation/hooks"; + +export const CellActions = () => { + const { useTableRow } = TrashBinListConfig.Browser.Table.Column; + const { row } = useTableRow(); + const { record: recordConfig } = useAcoConfig(); + + return ( + + + + ); +}; diff --git a/packages/app-trash-bin/src/Presentation/components/Cells/CellActions/index.ts b/packages/app-trash-bin/src/Presentation/components/Cells/CellActions/index.ts new file mode 100644 index 00000000000..5b5abbb9ee8 --- /dev/null +++ b/packages/app-trash-bin/src/Presentation/components/Cells/CellActions/index.ts @@ -0,0 +1 @@ +export * from "./CellActions"; diff --git a/packages/app-trash-bin/src/Presentation/components/Cells/CellCreatedBy/CellCreatedBy.tsx b/packages/app-trash-bin/src/Presentation/components/Cells/CellCreatedBy/CellCreatedBy.tsx new file mode 100644 index 00000000000..f1b244e4e5b --- /dev/null +++ b/packages/app-trash-bin/src/Presentation/components/Cells/CellCreatedBy/CellCreatedBy.tsx @@ -0,0 +1,9 @@ +import React from "react"; +import { TrashBinListConfig } from "~/Presentation/configs"; + +export const CellCreatedBy = () => { + const { useTableRow } = TrashBinListConfig.Browser.Table.Column; + const { row } = useTableRow(); + + return <>{row.createdBy.displayName}; +}; diff --git a/packages/app-trash-bin/src/Presentation/components/Cells/CellCreatedBy/index.ts b/packages/app-trash-bin/src/Presentation/components/Cells/CellCreatedBy/index.ts new file mode 100644 index 00000000000..a353875d5d1 --- /dev/null +++ b/packages/app-trash-bin/src/Presentation/components/Cells/CellCreatedBy/index.ts @@ -0,0 +1 @@ +export * from "./CellCreatedBy"; diff --git a/packages/app-trash-bin/src/Presentation/components/Cells/CellDeletedBy/CellDeletedBy.tsx b/packages/app-trash-bin/src/Presentation/components/Cells/CellDeletedBy/CellDeletedBy.tsx new file mode 100644 index 00000000000..30a5bc50771 --- /dev/null +++ b/packages/app-trash-bin/src/Presentation/components/Cells/CellDeletedBy/CellDeletedBy.tsx @@ -0,0 +1,9 @@ +import React from "react"; +import { TrashBinListConfig } from "~/Presentation/configs"; + +export const CellDeletedBy = () => { + const { useTableRow } = TrashBinListConfig.Browser.Table.Column; + const { row } = useTableRow(); + + return <>{row.deletedBy.displayName}; +}; diff --git a/packages/app-trash-bin/src/Presentation/components/Cells/CellDeletedBy/index.ts b/packages/app-trash-bin/src/Presentation/components/Cells/CellDeletedBy/index.ts new file mode 100644 index 00000000000..870589904fa --- /dev/null +++ b/packages/app-trash-bin/src/Presentation/components/Cells/CellDeletedBy/index.ts @@ -0,0 +1 @@ +export * from "./CellDeletedBy"; diff --git a/packages/app-trash-bin/src/Presentation/components/Cells/CellDeletedOn/CellDeletedOn.tsx b/packages/app-trash-bin/src/Presentation/components/Cells/CellDeletedOn/CellDeletedOn.tsx new file mode 100644 index 00000000000..c8f17af4bbb --- /dev/null +++ b/packages/app-trash-bin/src/Presentation/components/Cells/CellDeletedOn/CellDeletedOn.tsx @@ -0,0 +1,10 @@ +import React from "react"; +import { TimeAgo } from "@webiny/ui/TimeAgo"; +import { TrashBinListConfig } from "~/Presentation/configs"; + +export const CellDeletedOn = () => { + const { useTableRow } = TrashBinListConfig.Browser.Table.Column; + const { row } = useTableRow(); + + return ; +}; diff --git a/packages/app-trash-bin/src/Presentation/components/Cells/CellDeletedOn/index.ts b/packages/app-trash-bin/src/Presentation/components/Cells/CellDeletedOn/index.ts new file mode 100644 index 00000000000..d0de3e94b26 --- /dev/null +++ b/packages/app-trash-bin/src/Presentation/components/Cells/CellDeletedOn/index.ts @@ -0,0 +1 @@ +export * from "./CellDeletedOn"; diff --git a/packages/app-trash-bin/src/Presentation/components/Cells/CellTitle/CellTitle.styled.tsx b/packages/app-trash-bin/src/Presentation/components/Cells/CellTitle/CellTitle.styled.tsx new file mode 100644 index 00000000000..8b472745de9 --- /dev/null +++ b/packages/app-trash-bin/src/Presentation/components/Cells/CellTitle/CellTitle.styled.tsx @@ -0,0 +1,19 @@ +import styled from "@emotion/styled"; +import { Typography } from "@webiny/ui/Typography"; + +export const RowTitle = styled("div")` + display: flex; + align-items: center; + cursor: pointer; +`; + +export const RowIcon = styled("div")` + margin-right: 8px; + height: 24px; +`; + +export const RowText = styled(Typography)` + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; +`; diff --git a/packages/app-trash-bin/src/Presentation/components/Cells/CellTitle/CellTitle.tsx b/packages/app-trash-bin/src/Presentation/components/Cells/CellTitle/CellTitle.tsx new file mode 100644 index 00000000000..a5c45b14d74 --- /dev/null +++ b/packages/app-trash-bin/src/Presentation/components/Cells/CellTitle/CellTitle.tsx @@ -0,0 +1,18 @@ +import React from "react"; +import { ReactComponent as File } from "@material-design-icons/svg/outlined/description.svg"; +import { RowIcon, RowText, RowTitle } from "./CellTitle.styled"; +import { TrashBinListConfig } from "~/Presentation/configs"; + +export const CellTitle = () => { + const { useTableRow } = TrashBinListConfig.Browser.Table.Column; + const { row } = useTableRow(); + + return ( + + + + + {row.title} + + ); +}; diff --git a/packages/app-trash-bin/src/Presentation/components/Cells/CellTitle/index.ts b/packages/app-trash-bin/src/Presentation/components/Cells/CellTitle/index.ts new file mode 100644 index 00000000000..7ada279ab8e --- /dev/null +++ b/packages/app-trash-bin/src/Presentation/components/Cells/CellTitle/index.ts @@ -0,0 +1 @@ +export * from "./CellTitle"; diff --git a/packages/app-trash-bin/src/Presentation/components/Cells/index.ts b/packages/app-trash-bin/src/Presentation/components/Cells/index.ts new file mode 100644 index 00000000000..42d952cc482 --- /dev/null +++ b/packages/app-trash-bin/src/Presentation/components/Cells/index.ts @@ -0,0 +1,5 @@ +export * from "./CellActions"; +export * from "./CellCreatedBy"; +export * from "./CellDeletedBy"; +export * from "./CellDeletedOn"; +export * from "./CellTitle"; diff --git a/packages/app-trash-bin/src/Presentation/components/Empty/Empty.styled.tsx b/packages/app-trash-bin/src/Presentation/components/Empty/Empty.styled.tsx new file mode 100644 index 00000000000..93cc469012b --- /dev/null +++ b/packages/app-trash-bin/src/Presentation/components/Empty/Empty.styled.tsx @@ -0,0 +1,27 @@ +import styled from "@emotion/styled"; +import { ReactComponent as DeleteIcon } from "@material-design-icons/svg/outlined/delete_forever.svg"; +import { Typography } from "@webiny/ui/Typography"; + +export const EmptyWrapper = styled("div")` + width: 100%; + height: calc(100% - 95px); + display: flex; + justify-content: center; + align-items: center; +`; + +export const EmptyOuter = styled("div")` + display: flex; + flex-direction: column; + align-items: center; +`; + +export const EmptyIcon = styled(DeleteIcon)` + width: 72px !important; + height: auto; + color: var(--mdc-theme-text-secondary-on-background); +`; + +export const EmptyTitle = styled(Typography)` + margin-top: 8px; +`; diff --git a/packages/app-trash-bin/src/Presentation/components/Empty/Empty.tsx b/packages/app-trash-bin/src/Presentation/components/Empty/Empty.tsx new file mode 100644 index 00000000000..b03f779bf6e --- /dev/null +++ b/packages/app-trash-bin/src/Presentation/components/Empty/Empty.tsx @@ -0,0 +1,14 @@ +import React from "react"; +import { Icon } from "@webiny/ui/Icon"; +import { EmptyIcon, EmptyOuter, EmptyTitle, EmptyWrapper } from "./Empty.styled"; + +export const Empty = () => { + return ( + + + } /> + {"No items found."} + + + ); +}; diff --git a/packages/app-trash-bin/src/Presentation/components/Empty/index.ts b/packages/app-trash-bin/src/Presentation/components/Empty/index.ts new file mode 100644 index 00000000000..7aa85b1b7df --- /dev/null +++ b/packages/app-trash-bin/src/Presentation/components/Empty/index.ts @@ -0,0 +1 @@ +export * from "./Empty"; diff --git a/packages/app-trash-bin/src/Presentation/components/SearchInput/SearchInput.styled.tsx b/packages/app-trash-bin/src/Presentation/components/SearchInput/SearchInput.styled.tsx new file mode 100644 index 00000000000..ae7329721fb --- /dev/null +++ b/packages/app-trash-bin/src/Presentation/components/SearchInput/SearchInput.styled.tsx @@ -0,0 +1,39 @@ +import styled from "@emotion/styled"; +import { Icon } from "@webiny/ui/Icon"; +import { IconButton } from "@webiny/ui/Button"; + +export const SearchIconContainer = styled(Icon)` + &.mdc-button__icon { + color: var(--mdc-theme-text-secondary-on-background); + position: absolute; + width: 24px; + height: 24px; + left: 8px; + top: 8px; + } +`; + +export const FilterButton = styled(IconButton)` + position: absolute; + top: -4px; + right: -3px; +`; + +export const InputContainer = styled.div` + background-color: var(--mdc-theme-on-background); + position: relative; + height: 32px; + padding: 3px; + width: 100%; + border-radius: 2px; + > input { + border: none; + font-size: 14px; + width: calc(100% - 10px); + height: 100%; + margin-left: 32px; + background-color: transparent; + outline: none; + color: var(--mdc-theme-text-primary-on-background); + } +`; diff --git a/packages/app-trash-bin/src/Presentation/components/SearchInput/SearchInput.tsx b/packages/app-trash-bin/src/Presentation/components/SearchInput/SearchInput.tsx new file mode 100644 index 00000000000..4bdc3a8fad8 --- /dev/null +++ b/packages/app-trash-bin/src/Presentation/components/SearchInput/SearchInput.tsx @@ -0,0 +1,33 @@ +import React from "react"; +import { ReactComponent as SearchIcon } from "@material-design-icons/svg/outlined/search.svg"; +import { DelayedOnChange } from "@webiny/ui/DelayedOnChange"; +import { InputContainer, SearchIconContainer } from "./SearchInput.styled"; +import { useTrashBin } from "~/Presentation/hooks"; + +export const SearchInput = () => { + const { vm, searchItems } = useTrashBin(); + + return ( + + } /> + { + if (value === vm.searchQuery) { + return; + } + searchItems(value); + }} + > + {({ value, onChange }) => ( + onChange(e.target.value)} + placeholder={vm.searchLabel} + data-testid={"trash-bin.search-input"} + /> + )} + + + ); +}; diff --git a/packages/app-trash-bin/src/Presentation/components/SearchInput/index.ts b/packages/app-trash-bin/src/Presentation/components/SearchInput/index.ts new file mode 100644 index 00000000000..e5150fe3fa0 --- /dev/null +++ b/packages/app-trash-bin/src/Presentation/components/SearchInput/index.ts @@ -0,0 +1 @@ +export * from "./SearchInput"; diff --git a/packages/app-trash-bin/src/Presentation/components/Table/Table.tsx b/packages/app-trash-bin/src/Presentation/components/Table/Table.tsx new file mode 100644 index 00000000000..f0dcf826435 --- /dev/null +++ b/packages/app-trash-bin/src/Presentation/components/Table/Table.tsx @@ -0,0 +1,22 @@ +import React from "react"; +import { Table as AcoTable } from "@webiny/app-aco"; +import { useTrashBin } from "~/Presentation/hooks"; +import { TrashBinItemDTO } from "~/Domain"; +import { LoadingActions } from "~/types"; + +export const Table = () => { + const { vm, selectItems, sortItems } = useTrashBin(); + + return ( + + data={vm.items} + loading={vm.loading[LoadingActions.list]} + onSelectRow={entries => selectItems(entries)} + sorting={vm.sorting} + onSortingChange={sort => sortItems(sort)} + selected={vm.selectedItems} + nameColumnId={vm.nameColumnId} + namespace={"trash-bin"} + /> + ); +}; diff --git a/packages/app-trash-bin/src/Presentation/components/Table/index.ts b/packages/app-trash-bin/src/Presentation/components/Table/index.ts new file mode 100644 index 00000000000..e40efa4761d --- /dev/null +++ b/packages/app-trash-bin/src/Presentation/components/Table/index.ts @@ -0,0 +1 @@ +export * from "./Table"; diff --git a/packages/app-trash-bin/src/Presentation/components/Title/Title.styled.tsx b/packages/app-trash-bin/src/Presentation/components/Title/Title.styled.tsx new file mode 100644 index 00000000000..5a0da8f8674 --- /dev/null +++ b/packages/app-trash-bin/src/Presentation/components/Title/Title.styled.tsx @@ -0,0 +1,11 @@ +import styled from "@emotion/styled"; +import { Typography, TypographyProps } from "@webiny/ui/Typography"; + +export const Name = styled(Typography)` + color: var(--mdc-theme-text-primary-on-background); + padding-left: 8px; + line-height: 48px; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; +`; diff --git a/packages/app-trash-bin/src/Presentation/components/Title/Title.tsx b/packages/app-trash-bin/src/Presentation/components/Title/Title.tsx new file mode 100644 index 00000000000..c7f8107915a --- /dev/null +++ b/packages/app-trash-bin/src/Presentation/components/Title/Title.tsx @@ -0,0 +1,15 @@ +import React from "react"; +import { Skeleton } from "@webiny/ui/Skeleton"; +import { Name } from "./Title.styled"; + +export interface TitleProps { + title?: string; +} + +export const Title = ({ title }: TitleProps) => { + return ( + + {title || } + + ); +}; diff --git a/packages/app-trash-bin/src/Presentation/components/Title/index.tsx b/packages/app-trash-bin/src/Presentation/components/Title/index.tsx new file mode 100644 index 00000000000..2a7f3c58777 --- /dev/null +++ b/packages/app-trash-bin/src/Presentation/components/Title/index.tsx @@ -0,0 +1 @@ +export * from "./Title"; diff --git a/packages/app-trash-bin/src/Presentation/components/TrashBinOverlay/TrashBinOverlay.tsx b/packages/app-trash-bin/src/Presentation/components/TrashBinOverlay/TrashBinOverlay.tsx new file mode 100644 index 00000000000..87d4ca084c4 --- /dev/null +++ b/packages/app-trash-bin/src/Presentation/components/TrashBinOverlay/TrashBinOverlay.tsx @@ -0,0 +1,40 @@ +import React from "react"; +import debounce from "lodash/debounce"; +import { OverlayLayout } from "@webiny/app-admin"; +import { Scrollbar } from "@webiny/ui/Scrollbar"; +import { Title } from "~/Presentation/components/Title"; +import { SearchInput } from "~/Presentation/components/SearchInput"; +import { BulkActions } from "~/Presentation/components/BulkActions"; +import { Empty } from "~/Presentation/components/Empty"; +import { Table } from "~/Presentation/components/Table"; +import { BottomInfoBar } from "~/Presentation/components/BottomInfoBar"; +import { useTrashBin } from "~/Presentation/hooks"; + +interface TrashBinOverlayProps { + title: string; + onExited: () => void; +} + +export const TrashBinOverlay = (props: TrashBinOverlayProps) => { + const { listMoreItems, vm } = useTrashBin(); + + const onTableScroll = debounce(async ({ scrollFrame }) => { + if (scrollFrame.top > 0.8) { + await listMoreItems(); + } + }, 200); + + return ( + } + barMiddle={} + > + + onTableScroll({ scrollFrame })}> + {vm.isEmptyView ? : } + + + + ); +}; diff --git a/packages/app-trash-bin/src/Presentation/components/TrashBinOverlay/index.ts b/packages/app-trash-bin/src/Presentation/components/TrashBinOverlay/index.ts new file mode 100644 index 00000000000..2d3e6b890f9 --- /dev/null +++ b/packages/app-trash-bin/src/Presentation/components/TrashBinOverlay/index.ts @@ -0,0 +1 @@ +export * from "./TrashBinOverlay"; diff --git a/packages/app-trash-bin/src/Presentation/configs/index.ts b/packages/app-trash-bin/src/Presentation/configs/index.ts new file mode 100644 index 00000000000..491ccf0c124 --- /dev/null +++ b/packages/app-trash-bin/src/Presentation/configs/index.ts @@ -0,0 +1 @@ +export * from "./list"; diff --git a/packages/app-trash-bin/src/Presentation/configs/list/Browser/BulkAction.tsx b/packages/app-trash-bin/src/Presentation/configs/list/Browser/BulkAction.tsx new file mode 100644 index 00000000000..83deefb896a --- /dev/null +++ b/packages/app-trash-bin/src/Presentation/configs/list/Browser/BulkAction.tsx @@ -0,0 +1,85 @@ +import React, { useCallback, useEffect, useRef } from "react"; +import { CallbackParams, useButtons, useDialogWithReport, Worker } from "@webiny/app-admin"; +import { Property, useIdGenerator } from "@webiny/react-properties"; +import { useTrashBin } from "~/Presentation/hooks"; +import { TrashBinItemDTO } from "~/Domain"; + +export interface BulkActionConfig { + name: string; + element: React.ReactElement; +} + +export interface BulkActionProps { + name: string; + remove?: boolean; + before?: string; + after?: string; + element?: React.ReactElement; +} + +export const BaseBulkAction = ({ + name, + after = undefined, + before = undefined, + remove = false, + element +}: BulkActionProps) => { + const getId = useIdGenerator("bulkAction"); + + const placeAfter = after !== undefined ? getId(after) : undefined; + const placeBefore = before !== undefined ? getId(before) : undefined; + + return ( + + + + {element ? ( + + ) : null} + + + ); +}; + +const useWorker = () => { + const { vm, selectItems } = useTrashBin(); + const { current: worker } = useRef(new Worker()); + + useEffect(() => { + worker.items = vm.selectedItems; + }, [vm.selectedItems.length]); + + // Reset selected items in both repository and Worker + const resetItems = useCallback(() => { + worker.items = []; + selectItems([]); + }, []); + + return { + items: vm.selectedItems, + process: (callback: (items: TrashBinItemDTO[]) => void) => worker.process(callback), + processInSeries: async ( + callback: ({ + item, + allItems, + report + }: CallbackParams) => Promise, + chunkSize?: number + ) => worker.processInSeries(callback, chunkSize), + resetItems: resetItems, + results: worker.results + }; +}; + +export const BulkAction = Object.assign(BaseBulkAction, { + useButtons, + useWorker, + useDialog: useDialogWithReport +}); diff --git a/packages/app-trash-bin/src/Presentation/configs/list/Browser/EntryAction.tsx b/packages/app-trash-bin/src/Presentation/configs/list/Browser/EntryAction.tsx new file mode 100644 index 00000000000..46ed41e2baf --- /dev/null +++ b/packages/app-trash-bin/src/Presentation/configs/list/Browser/EntryAction.tsx @@ -0,0 +1,23 @@ +import React from "react"; +import { CompositionScope } from "@webiny/react-composition"; +import { AcoConfig, RecordActionConfig } from "@webiny/app-aco"; + +const { Record } = AcoConfig; + +export { RecordActionConfig as EntryActionConfig }; + +type EntryActionProps = React.ComponentProps; + +const BaseEntryAction = (props: EntryActionProps) => { + return ( + + + + + + ); +}; + +export const EntryAction = Object.assign(BaseEntryAction, { + OptionsMenuItem: Record.Action.OptionsMenuItem +}); diff --git a/packages/app-trash-bin/src/Presentation/configs/list/Browser/Table/Column.tsx b/packages/app-trash-bin/src/Presentation/configs/list/Browser/Table/Column.tsx new file mode 100644 index 00000000000..327d2c0b877 --- /dev/null +++ b/packages/app-trash-bin/src/Presentation/configs/list/Browser/Table/Column.tsx @@ -0,0 +1,25 @@ +import React from "react"; +import { CompositionScope } from "@webiny/react-composition"; +import { AcoConfig, TableColumnConfig as ColumnConfig } from "@webiny/app-aco"; +import { TrashBinItemDTO } from "~/Domain"; + +const { Table } = AcoConfig; + +export { ColumnConfig }; + +type ColumnProps = React.ComponentProps; + +const BaseColumn = (props: ColumnProps) => { + return ( + + + + + + ); +}; + +export const Column = Object.assign(BaseColumn, { + useTableRow: Table.Column.useTableRow, + isFolderRow: Table.Column.isFolderRow +}); diff --git a/packages/app-trash-bin/src/Presentation/configs/list/Browser/Table/Sorting.tsx b/packages/app-trash-bin/src/Presentation/configs/list/Browser/Table/Sorting.tsx new file mode 100644 index 00000000000..cf282678ce9 --- /dev/null +++ b/packages/app-trash-bin/src/Presentation/configs/list/Browser/Table/Sorting.tsx @@ -0,0 +1,19 @@ +import React from "react"; +import { CompositionScope } from "@webiny/react-composition"; +import { AcoConfig, TableSortingConfig as SortingConfig } from "@webiny/app-aco"; + +const { Table } = AcoConfig; + +export { SortingConfig }; + +type SortingProps = React.ComponentProps; + +export const Sorting = (props: SortingProps) => { + return ( + + + + + + ); +}; diff --git a/packages/app-trash-bin/src/Presentation/configs/list/Browser/Table/index.ts b/packages/app-trash-bin/src/Presentation/configs/list/Browser/Table/index.ts new file mode 100644 index 00000000000..48b5a640ef8 --- /dev/null +++ b/packages/app-trash-bin/src/Presentation/configs/list/Browser/Table/index.ts @@ -0,0 +1,12 @@ +import { Column, ColumnConfig } from "./Column"; +import { Sorting, SortingConfig } from "./Sorting"; + +export interface TableConfig { + columns: ColumnConfig[]; + sorting: SortingConfig[]; +} + +export const Table = { + Column, + Sorting +}; diff --git a/packages/app-trash-bin/src/Presentation/configs/list/Browser/index.ts b/packages/app-trash-bin/src/Presentation/configs/list/Browser/index.ts new file mode 100644 index 00000000000..1c140bf660f --- /dev/null +++ b/packages/app-trash-bin/src/Presentation/configs/list/Browser/index.ts @@ -0,0 +1,15 @@ +import { BulkAction, BulkActionConfig } from "./BulkAction"; +import { EntryAction, EntryActionConfig } from "./EntryAction"; +import { Table, TableConfig } from "./Table"; + +export interface BrowserConfig { + bulkActions: BulkActionConfig[]; + entryActions: EntryActionConfig[]; + table: TableConfig; +} + +export const Browser = { + BulkAction, + EntryAction, + Table +}; diff --git a/packages/app-trash-bin/src/Presentation/configs/list/TrashBinListConfig.tsx b/packages/app-trash-bin/src/Presentation/configs/list/TrashBinListConfig.tsx new file mode 100644 index 00000000000..2960021e3c2 --- /dev/null +++ b/packages/app-trash-bin/src/Presentation/configs/list/TrashBinListConfig.tsx @@ -0,0 +1,28 @@ +import { useMemo } from "react"; +import { createConfigurableComponent } from "@webiny/react-properties"; +import { Browser, BrowserConfig } from "./Browser"; + +const base = createConfigurableComponent("TrashBinListConfig"); + +export const TrashBinListConfig = Object.assign(base.Config, { Browser }); +export const TrashBinListWithConfig = base.WithConfig; + +interface TrashBinListConfig { + browser: BrowserConfig; +} + +export function useTrashBinListConfig() { + const config = base.useConfig(); + + const browser = config.browser || {}; + + return useMemo( + () => ({ + browser: { + ...browser, + bulkActions: [...(browser.bulkActions || [])] + } + }), + [config] + ); +} diff --git a/packages/app-trash-bin/src/Presentation/configs/list/index.ts b/packages/app-trash-bin/src/Presentation/configs/list/index.ts new file mode 100644 index 00000000000..67ef4b80c5f --- /dev/null +++ b/packages/app-trash-bin/src/Presentation/configs/list/index.ts @@ -0,0 +1 @@ +export * from "./TrashBinListConfig"; diff --git a/packages/app-trash-bin/src/Presentation/hooks/index.ts b/packages/app-trash-bin/src/Presentation/hooks/index.ts new file mode 100644 index 00000000000..ada42a2c361 --- /dev/null +++ b/packages/app-trash-bin/src/Presentation/hooks/index.ts @@ -0,0 +1,4 @@ +export * from "./useTrashBin"; +export * from "./useTrashBinItem"; +export * from "./useDeleteTrashBinItem"; +export * from "./useRestoreTrashBinItem"; diff --git a/packages/app-trash-bin/src/Presentation/hooks/useDeleteTrashBinItem.tsx b/packages/app-trash-bin/src/Presentation/hooks/useDeleteTrashBinItem.tsx new file mode 100644 index 00000000000..5c62c71b098 --- /dev/null +++ b/packages/app-trash-bin/src/Presentation/hooks/useDeleteTrashBinItem.tsx @@ -0,0 +1,39 @@ +import React, { useCallback } from "react"; +import { useConfirmationDialog, useSnackbar } from "@webiny/app-admin"; +import { TrashBinItemDTO } from "~/Domain"; +import { useTrashBin } from "./useTrashBin"; + +interface UseDeleteItemParams { + item: TrashBinItemDTO; +} + +export const useDeleteTrashBinItem = ({ item }: UseDeleteItemParams) => { + const { deleteItem } = useTrashBin(); + const { showSnackbar } = useSnackbar(); + + const { showConfirmation } = useConfirmationDialog({ + title: "Delete item", + message: ( +

+ You are about to delete this item and all of its revisions! +
+ Are you sure you want to permanently delete {item.title}? +

+ ) + }); + + const openDialogDeleteItem = useCallback( + () => + showConfirmation(async () => { + try { + await deleteItem(item.id); + showSnackbar(`${item.title} was deleted successfully!`); + } catch (ex) { + showSnackbar(ex.message || `Error while deleting ${item.title}`); + } + }), + [item] + ); + + return { openDialogDeleteItem }; +}; diff --git a/packages/app-trash-bin/src/Presentation/hooks/useRestoreTrashBinItem.tsx b/packages/app-trash-bin/src/Presentation/hooks/useRestoreTrashBinItem.tsx new file mode 100644 index 00000000000..74478d15d54 --- /dev/null +++ b/packages/app-trash-bin/src/Presentation/hooks/useRestoreTrashBinItem.tsx @@ -0,0 +1,50 @@ +import React, { useCallback } from "react"; +import { useConfirmationDialog, useSnackbar } from "@webiny/app-admin"; +import { useTrashBin } from "./useTrashBin"; +import { SnackbarAction } from "@webiny/ui/Snackbar"; +import { TrashBinItemDTO } from "~/Domain"; + +interface UseRestoreItemParams { + item: TrashBinItemDTO; +} + +export const useRestoreTrashBinItem = ({ item }: UseRestoreItemParams) => { + const { restoreItem, onItemRestore, vm } = useTrashBin(); + const { showSnackbar } = useSnackbar(); + + const { showConfirmation } = useConfirmationDialog({ + title: "Restore item", + message: ( +

+ You are about to restore {item.title}. +
+ Are you sure you want to continue? +

+ ) + }); + + const openDialogRestoreItem = useCallback( + () => + showConfirmation(async () => { + try { + await restoreItem(item.id); + + const restoredItem = vm.restoredItems.find(restored => restored.id === item.id); + + showSnackbar(`${item.title} was restored successfully!`, { + action: restoredItem && ( + onItemRestore(restoredItem)} + /> + ) + }); + } catch (ex) { + showSnackbar(ex.message || `Error while restoring ${item.title}`); + } + }), + [item] + ); + + return { openDialogRestoreItem }; +}; diff --git a/packages/app-trash-bin/src/Presentation/hooks/useTrashBin.tsx b/packages/app-trash-bin/src/Presentation/hooks/useTrashBin.tsx new file mode 100644 index 00000000000..42c0f7bada2 --- /dev/null +++ b/packages/app-trash-bin/src/Presentation/hooks/useTrashBin.tsx @@ -0,0 +1,79 @@ +import { useEffect, useMemo, useState, useCallback } from "react"; +import { autorun } from "mobx"; +import { createGenericContext } from "@webiny/app-admin"; +import { ITrashBinControllers, ITrashBinPresenter } from "~/Presentation/abstractions"; +import { TrashBinItemDTO } from "~/Domain"; + +export interface TrashBinContext { + controllers: ITrashBinControllers; + presenter: ITrashBinPresenter; + onItemRestore: (item: TrashBinItemDTO) => Promise; +} + +const { Provider, useHook } = createGenericContext("TrashBinContext"); + +export const useTrashBin = () => { + const context = useHook(); + const [vm, setVm] = useState(context.presenter.vm); + + useEffect(() => { + return autorun(() => { + const newState = context.presenter.vm; + setVm(newState); + }); + }, [context.presenter]); + + const onItemRestore = useCallback( + (item: TrashBinItemDTO) => context.onItemRestore(item), + [context.onItemRestore] + ); + + const deleteItem = useCallback( + (id: string) => context.controllers.deleteItem.execute(id), + [context.controllers.deleteItem] + ); + + const restoreItem = useCallback( + (id: string) => context.controllers.restoreItem.execute(id), + [context.controllers.restoreItem] + ); + + const listItems = useCallback( + () => context.controllers.listItems.execute(), + [context.controllers.listItems] + ); + + const listMoreItems = useCallback( + () => context.controllers.listMoreItems.execute(), + [context.controllers.listMoreItems] + ); + + const searchItems = useCallback( + (query: string) => context.controllers.searchItems.execute(query), + [context.controllers.searchItems] + ); + + const selectItems = useCallback( + (items: TrashBinItemDTO[]) => context.controllers.selectItems.execute(items), + [context.controllers.selectItems] + ); + + const sortItems = useMemo( + () => context.controllers.sortItems.execute, + [context.controllers.sortItems] + ); + + return { + vm, + onItemRestore, + deleteItem, + restoreItem, + listItems, + listMoreItems, + searchItems, + selectItems, + sortItems + }; +}; + +export const TrashBinProvider = Provider; diff --git a/packages/app-trash-bin/src/Presentation/hooks/useTrashBinItem.tsx b/packages/app-trash-bin/src/Presentation/hooks/useTrashBinItem.tsx new file mode 100644 index 00000000000..206c5f1a35a --- /dev/null +++ b/packages/app-trash-bin/src/Presentation/hooks/useTrashBinItem.tsx @@ -0,0 +1,11 @@ +import { createGenericContext } from "@webiny/app-admin"; +import { TrashBinItemDTO } from "~/Domain"; + +export interface TrashBinItemContext { + item: TrashBinItemDTO; +} + +const { Provider, useHook } = createGenericContext("TrashBinItemContext"); + +export const useTrashBinItem = useHook; +export const TrashBinItemProvider = Provider; diff --git a/packages/app-trash-bin/src/Presentation/index.tsx b/packages/app-trash-bin/src/Presentation/index.tsx new file mode 100644 index 00000000000..ec5a2b58cdb --- /dev/null +++ b/packages/app-trash-bin/src/Presentation/index.tsx @@ -0,0 +1,80 @@ +import React, { useCallback, useState } from "react"; +import { AcoWithConfig } from "@webiny/app-aco"; +import { + ITrashBinDeleteItemGateway, + ITrashBinListGateway, + ITrashBinRestoreItemGateway +} from "~/Gateways"; +import { ITrashBinItemMapper, TrashBinItemDTO } from "~/Domain"; +import { TrashBinRenderer } from "~/Presentation/TrashBinRenderer"; +import { TrashBinConfigs } from "~/Presentation/TrashBinConfigs"; +import { CompositionScope } from "@webiny/react-composition"; +import { TrashBinListWithConfig } from "~/Presentation/configs"; + +export type TrashBinRenderPropParams = { + showTrashBin: () => void; +}; + +interface TrashBinRenderProps { + (params: TrashBinRenderPropParams): React.ReactNode; +} + +export type TrashBinProps = { + render: TrashBinRenderProps; + listGateway: ITrashBinListGateway; + deleteGateway: ITrashBinDeleteItemGateway; + restoreGateway: ITrashBinRestoreItemGateway; + itemMapper: ITrashBinItemMapper; + onClose?: () => void; + onItemRestore?: (item: TrashBinItemDTO) => Promise; + show?: boolean; + nameColumnId?: string; + title?: string; +}; + +export const TrashBin = ({ render, ...rest }: TrashBinProps) => { + const [show, setShow] = useState(rest.show ?? false); + + const showTrashBin = useCallback(() => { + setShow(true); + }, []); + + const onClose = useCallback(() => { + if (typeof rest.onClose === "function") { + rest.onClose(); + } + + setShow(false); + }, [rest.onClose]); + + const onItemRestore = useCallback( + async (item: any) => { + if (typeof rest.onItemRestore === "function") { + rest.onItemRestore(item); + } + + onClose(); + }, + [rest.onItemRestore, onClose] + ); + + return ( + <> + {show && ( + + + + + + + + + )} + {render ? render({ showTrashBin }) : null} + + ); +}; diff --git a/packages/app-trash-bin/src/UseCases/DeleteItem/DeleteItemUseCase.ts b/packages/app-trash-bin/src/UseCases/DeleteItem/DeleteItemUseCase.ts new file mode 100644 index 00000000000..615652b32b7 --- /dev/null +++ b/packages/app-trash-bin/src/UseCases/DeleteItem/DeleteItemUseCase.ts @@ -0,0 +1,16 @@ +import { makeAutoObservable } from "mobx"; +import { ITrashBinItemsRepository } from "~/Domain/Repositories"; +import { IDeleteItemUseCase } from "./IDeleteItemUseCase"; + +export class DeleteItemUseCase implements IDeleteItemUseCase { + private repository: ITrashBinItemsRepository; + + constructor(repository: ITrashBinItemsRepository) { + this.repository = repository; + makeAutoObservable(this); + } + + async execute(id: string) { + await this.repository.deleteItem(id); + } +} diff --git a/packages/app-trash-bin/src/UseCases/DeleteItem/IDeleteItemUseCase.ts b/packages/app-trash-bin/src/UseCases/DeleteItem/IDeleteItemUseCase.ts new file mode 100644 index 00000000000..09f85e36b01 --- /dev/null +++ b/packages/app-trash-bin/src/UseCases/DeleteItem/IDeleteItemUseCase.ts @@ -0,0 +1,3 @@ +export interface IDeleteItemUseCase { + execute: (id: string) => Promise; +} diff --git a/packages/app-trash-bin/src/UseCases/DeleteItem/index.ts b/packages/app-trash-bin/src/UseCases/DeleteItem/index.ts new file mode 100644 index 00000000000..09bae818501 --- /dev/null +++ b/packages/app-trash-bin/src/UseCases/DeleteItem/index.ts @@ -0,0 +1,2 @@ +export * from "./IDeleteItemUseCase"; +export * from "./DeleteItemUseCase"; diff --git a/packages/app-trash-bin/src/UseCases/ListItems/IListItemsUseCase.ts b/packages/app-trash-bin/src/UseCases/ListItems/IListItemsUseCase.ts new file mode 100644 index 00000000000..2632bec4e87 --- /dev/null +++ b/packages/app-trash-bin/src/UseCases/ListItems/IListItemsUseCase.ts @@ -0,0 +1,5 @@ +import { TrashBinListQueryVariables } from "~/types"; + +export interface IListItemsUseCase { + execute: (params?: TrashBinListQueryVariables) => Promise; +} diff --git a/packages/app-trash-bin/src/UseCases/ListItems/ListItemsUseCase.ts b/packages/app-trash-bin/src/UseCases/ListItems/ListItemsUseCase.ts new file mode 100644 index 00000000000..7bfe55ede5e --- /dev/null +++ b/packages/app-trash-bin/src/UseCases/ListItems/ListItemsUseCase.ts @@ -0,0 +1,16 @@ +import { makeAutoObservable } from "mobx"; +import { ITrashBinItemsRepository } from "~/Domain"; +import { IListItemsUseCase } from "./IListItemsUseCase"; +import { TrashBinListQueryVariables } from "~/types"; + +export class ListItemsUseCase implements IListItemsUseCase { + private itemsRepository: ITrashBinItemsRepository; + constructor(itemsRepository: ITrashBinItemsRepository) { + this.itemsRepository = itemsRepository; + makeAutoObservable(this); + } + + async execute(params?: TrashBinListQueryVariables) { + await this.itemsRepository.listItems({ ...params }); + } +} diff --git a/packages/app-trash-bin/src/UseCases/ListItems/ListItemsUseCaseWithSearch.ts b/packages/app-trash-bin/src/UseCases/ListItems/ListItemsUseCaseWithSearch.ts new file mode 100644 index 00000000000..a2fc75e9b3e --- /dev/null +++ b/packages/app-trash-bin/src/UseCases/ListItems/ListItemsUseCaseWithSearch.ts @@ -0,0 +1,20 @@ +import { makeAutoObservable } from "mobx"; +import { ISearchRepository } from "~/Domain"; +import { IListItemsUseCase } from "./IListItemsUseCase"; +import { TrashBinListQueryVariables } from "~/types"; + +export class ListItemsUseCaseWithSearch implements IListItemsUseCase { + private searchRepository: ISearchRepository; + private useCase: IListItemsUseCase; + + constructor(searchRepository: ISearchRepository, useCase: IListItemsUseCase) { + this.searchRepository = searchRepository; + this.useCase = useCase; + makeAutoObservable(this); + } + + async execute(params?: TrashBinListQueryVariables) { + const search = this.searchRepository.get(); + await this.useCase.execute({ ...params, search: search || undefined }); + } +} diff --git a/packages/app-trash-bin/src/UseCases/ListItems/ListItemsUseCaseWithSorting.ts b/packages/app-trash-bin/src/UseCases/ListItems/ListItemsUseCaseWithSorting.ts new file mode 100644 index 00000000000..9e75771d4a6 --- /dev/null +++ b/packages/app-trash-bin/src/UseCases/ListItems/ListItemsUseCaseWithSorting.ts @@ -0,0 +1,20 @@ +import { makeAutoObservable } from "mobx"; +import { ISortingRepository, SortingMapper } from "@webiny/app-utils"; +import { IListItemsUseCase } from "./IListItemsUseCase"; +import { TrashBinListQueryVariables } from "~/types"; + +export class ListItemsUseCaseWithSorting implements IListItemsUseCase { + private sortingRepository: ISortingRepository; + private useCase: IListItemsUseCase; + + constructor(sortingRepository: ISortingRepository, useCase: IListItemsUseCase) { + this.sortingRepository = sortingRepository; + this.useCase = useCase; + makeAutoObservable(this); + } + + async execute(params?: TrashBinListQueryVariables) { + const sort = this.sortingRepository.get().map(sort => SortingMapper.fromDTOtoDb(sort)); + await this.useCase.execute({ ...params, sort }); + } +} diff --git a/packages/app-trash-bin/src/UseCases/ListItems/index.ts b/packages/app-trash-bin/src/UseCases/ListItems/index.ts new file mode 100644 index 00000000000..fea2aa91c1d --- /dev/null +++ b/packages/app-trash-bin/src/UseCases/ListItems/index.ts @@ -0,0 +1,4 @@ +export * from "./IListItemsUseCase"; +export * from "./ListItemsUseCase"; +export * from "./ListItemsUseCaseWithSearch"; +export * from "./ListItemsUseCaseWithSorting"; diff --git a/packages/app-trash-bin/src/UseCases/ListMoreItems/IListMoreItemsUseCase.ts b/packages/app-trash-bin/src/UseCases/ListMoreItems/IListMoreItemsUseCase.ts new file mode 100644 index 00000000000..c3efbe1785b --- /dev/null +++ b/packages/app-trash-bin/src/UseCases/ListMoreItems/IListMoreItemsUseCase.ts @@ -0,0 +1,3 @@ +export interface IListMoreItemsUseCase { + execute: () => Promise; +} diff --git a/packages/app-trash-bin/src/UseCases/ListMoreItems/ListMoreItemsUseCase.ts b/packages/app-trash-bin/src/UseCases/ListMoreItems/ListMoreItemsUseCase.ts new file mode 100644 index 00000000000..42e1f2c18ec --- /dev/null +++ b/packages/app-trash-bin/src/UseCases/ListMoreItems/ListMoreItemsUseCase.ts @@ -0,0 +1,16 @@ +import { makeAutoObservable } from "mobx"; +import { ITrashBinItemsRepository } from "~/Domain/Repositories"; +import { IListMoreItemsUseCase } from "./IListMoreItemsUseCase"; + +export class ListMoreItemsUseCase implements IListMoreItemsUseCase { + private itemsRepository: ITrashBinItemsRepository; + + constructor(itemsRepository: ITrashBinItemsRepository) { + this.itemsRepository = itemsRepository; + makeAutoObservable(this); + } + + async execute() { + await this.itemsRepository.listMoreItems(); + } +} diff --git a/packages/app-trash-bin/src/UseCases/ListMoreItems/index.ts b/packages/app-trash-bin/src/UseCases/ListMoreItems/index.ts new file mode 100644 index 00000000000..17371b524a0 --- /dev/null +++ b/packages/app-trash-bin/src/UseCases/ListMoreItems/index.ts @@ -0,0 +1,2 @@ +export * from "./IListMoreItemsUseCase"; +export * from "./ListMoreItemsUseCase"; diff --git a/packages/app-trash-bin/src/UseCases/RestoreItem/IRestoreItemUseCase.ts b/packages/app-trash-bin/src/UseCases/RestoreItem/IRestoreItemUseCase.ts new file mode 100644 index 00000000000..d519b3a0134 --- /dev/null +++ b/packages/app-trash-bin/src/UseCases/RestoreItem/IRestoreItemUseCase.ts @@ -0,0 +1,3 @@ +export interface IRestoreItemUseCase { + execute: (id: string) => Promise; +} diff --git a/packages/app-trash-bin/src/UseCases/RestoreItem/RestoreItemUseCase.ts b/packages/app-trash-bin/src/UseCases/RestoreItem/RestoreItemUseCase.ts new file mode 100644 index 00000000000..b939dc9fadc --- /dev/null +++ b/packages/app-trash-bin/src/UseCases/RestoreItem/RestoreItemUseCase.ts @@ -0,0 +1,16 @@ +import { makeAutoObservable } from "mobx"; +import { ITrashBinItemsRepository } from "~/Domain/Repositories"; +import { IRestoreItemUseCase } from "./IRestoreItemUseCase"; + +export class RestoreItemUseCase implements IRestoreItemUseCase { + private repository: ITrashBinItemsRepository; + + constructor(repository: ITrashBinItemsRepository) { + this.repository = repository; + makeAutoObservable(this); + } + + async execute(id: string) { + await this.repository.restoreItem(id); + } +} diff --git a/packages/app-trash-bin/src/UseCases/RestoreItem/index.ts b/packages/app-trash-bin/src/UseCases/RestoreItem/index.ts new file mode 100644 index 00000000000..119237f3d1f --- /dev/null +++ b/packages/app-trash-bin/src/UseCases/RestoreItem/index.ts @@ -0,0 +1,2 @@ +export * from "./IRestoreItemUseCase"; +export * from "./RestoreItemUseCase"; diff --git a/packages/app-trash-bin/src/UseCases/SearchItems/ISearchItemsUseCase.ts b/packages/app-trash-bin/src/UseCases/SearchItems/ISearchItemsUseCase.ts new file mode 100644 index 00000000000..9d9136ee5cf --- /dev/null +++ b/packages/app-trash-bin/src/UseCases/SearchItems/ISearchItemsUseCase.ts @@ -0,0 +1,3 @@ +export interface ISearchItemsUseCase { + execute: (query: string) => Promise; +} diff --git a/packages/app-trash-bin/src/UseCases/SearchItems/SearchItemsUseCase.ts b/packages/app-trash-bin/src/UseCases/SearchItems/SearchItemsUseCase.ts new file mode 100644 index 00000000000..1f63da611b0 --- /dev/null +++ b/packages/app-trash-bin/src/UseCases/SearchItems/SearchItemsUseCase.ts @@ -0,0 +1,16 @@ +import { makeAutoObservable } from "mobx"; +import { ISearchItemsUseCase } from "./ISearchItemsUseCase"; +import { ISearchRepository } from "~/Domain/Repositories"; + +export class SearchItemsUseCase implements ISearchItemsUseCase { + private searchRepository: ISearchRepository; + + constructor(searchRepository: ISearchRepository) { + this.searchRepository = searchRepository; + makeAutoObservable(this); + } + + async execute(query: string) { + await this.searchRepository.set(query); + } +} diff --git a/packages/app-trash-bin/src/UseCases/SearchItems/index.ts b/packages/app-trash-bin/src/UseCases/SearchItems/index.ts new file mode 100644 index 00000000000..82f75df6136 --- /dev/null +++ b/packages/app-trash-bin/src/UseCases/SearchItems/index.ts @@ -0,0 +1,2 @@ +export * from "./ISearchItemsUseCase"; +export * from "./SearchItemsUseCase"; diff --git a/packages/app-trash-bin/src/UseCases/SelectItems/ISelectItemsUseCase.ts b/packages/app-trash-bin/src/UseCases/SelectItems/ISelectItemsUseCase.ts new file mode 100644 index 00000000000..d4eabfc0229 --- /dev/null +++ b/packages/app-trash-bin/src/UseCases/SelectItems/ISelectItemsUseCase.ts @@ -0,0 +1,5 @@ +import { TrashBinItem } from "~/Domain"; + +export interface ISelectItemsUseCase { + execute: (items: TrashBinItem[]) => Promise; +} diff --git a/packages/app-trash-bin/src/UseCases/SelectItems/SelectItemsUseCase.ts b/packages/app-trash-bin/src/UseCases/SelectItems/SelectItemsUseCase.ts new file mode 100644 index 00000000000..dedf20a23a6 --- /dev/null +++ b/packages/app-trash-bin/src/UseCases/SelectItems/SelectItemsUseCase.ts @@ -0,0 +1,16 @@ +import { makeAutoObservable } from "mobx"; +import { TrashBinItem, ISelectedItemsRepository } from "~/Domain"; +import { ISelectItemsUseCase } from "./ISelectItemsUseCase"; + +export class SelectItemsUseCase implements ISelectItemsUseCase { + private repository: ISelectedItemsRepository; + + constructor(repository: ISelectedItemsRepository) { + this.repository = repository; + makeAutoObservable(this); + } + + async execute(items: TrashBinItem[]) { + await this.repository.selectItems(items); + } +} diff --git a/packages/app-trash-bin/src/UseCases/SelectItems/index.ts b/packages/app-trash-bin/src/UseCases/SelectItems/index.ts new file mode 100644 index 00000000000..66289e2d57f --- /dev/null +++ b/packages/app-trash-bin/src/UseCases/SelectItems/index.ts @@ -0,0 +1,2 @@ +export * from "./ISelectItemsUseCase"; +export * from "./SelectItemsUseCase"; diff --git a/packages/app-trash-bin/src/UseCases/SortItems/ISortItemsUseCase.ts b/packages/app-trash-bin/src/UseCases/SortItems/ISortItemsUseCase.ts new file mode 100644 index 00000000000..73016af7dc0 --- /dev/null +++ b/packages/app-trash-bin/src/UseCases/SortItems/ISortItemsUseCase.ts @@ -0,0 +1,5 @@ +import { SortingDTO } from "@webiny/app-utils"; + +export interface ISortItemsUseCase { + execute: (sorts: SortingDTO[]) => Promise; +} diff --git a/packages/app-trash-bin/src/UseCases/SortItems/SortItemsUseCase.ts b/packages/app-trash-bin/src/UseCases/SortItems/SortItemsUseCase.ts new file mode 100644 index 00000000000..4434bcfffde --- /dev/null +++ b/packages/app-trash-bin/src/UseCases/SortItems/SortItemsUseCase.ts @@ -0,0 +1,16 @@ +import { makeAutoObservable } from "mobx"; +import { ISortingRepository, SortingDTO } from "@webiny/app-utils"; +import { ISortItemsUseCase } from "./ISortItemsUseCase"; + +export class SortItemsUseCase implements ISortItemsUseCase { + private sortingRepository: ISortingRepository; + + constructor(sortingRepository: ISortingRepository) { + this.sortingRepository = sortingRepository; + makeAutoObservable(this); + } + + async execute(sorts: SortingDTO[]) { + this.sortingRepository.set(sorts); + } +} diff --git a/packages/app-trash-bin/src/UseCases/SortItems/index.ts b/packages/app-trash-bin/src/UseCases/SortItems/index.ts new file mode 100644 index 00000000000..a818856ef4c --- /dev/null +++ b/packages/app-trash-bin/src/UseCases/SortItems/index.ts @@ -0,0 +1,2 @@ +export * from "./ISortItemsUseCase"; +export * from "./SortItemsUseCase"; diff --git a/packages/app-trash-bin/src/UseCases/index.ts b/packages/app-trash-bin/src/UseCases/index.ts new file mode 100644 index 00000000000..b2d6e72745a --- /dev/null +++ b/packages/app-trash-bin/src/UseCases/index.ts @@ -0,0 +1,7 @@ +export * from "./DeleteItem"; +export * from "./ListItems"; +export * from "./ListMoreItems"; +export * from "./RestoreItem"; +export * from "./SearchItems"; +export * from "./SortItems"; +export * from "./SelectItems"; diff --git a/packages/app-trash-bin/src/index.ts b/packages/app-trash-bin/src/index.ts new file mode 100644 index 00000000000..3a9a249a66f --- /dev/null +++ b/packages/app-trash-bin/src/index.ts @@ -0,0 +1,3 @@ +export * from "./Domain"; +export * from "./Gateways"; +export * from "./Presentation"; diff --git a/packages/app-trash-bin/src/types.ts b/packages/app-trash-bin/src/types.ts new file mode 100644 index 00000000000..2fceb5eaf7d --- /dev/null +++ b/packages/app-trash-bin/src/types.ts @@ -0,0 +1,32 @@ +export interface TrashBinListQueryVariables { + where?: { + [key: string]: any; + }; + sort?: string[]; + limit?: number; + after?: string; + search?: string; +} + +export interface TrashBinMetaResponse { + totalCount: number; + cursor: string | null; + hasMoreItems: boolean; +} + +export interface TrashBinIdentity { + id: string; + displayName: string; + type: string; +} + +export interface TrashBinLocation { + folderId: string; +} + +export enum LoadingActions { + list = "LIST", + listMore = "LIST_MORE", + delete = "DELETE", + restore = "RESTORE" +} diff --git a/packages/app-trash-bin/tsconfig.build.json b/packages/app-trash-bin/tsconfig.build.json new file mode 100644 index 00000000000..46d65875e03 --- /dev/null +++ b/packages/app-trash-bin/tsconfig.build.json @@ -0,0 +1,19 @@ +{ + "extends": "../../tsconfig.build.json", + "include": ["src"], + "references": [ + { "path": "../app-aco/tsconfig.build.json" }, + { "path": "../app-admin/tsconfig.build.json" }, + { "path": "../app-utils/tsconfig.build.json" }, + { "path": "../react-composition/tsconfig.build.json" }, + { "path": "../react-properties/tsconfig.build.json" }, + { "path": "../ui/tsconfig.build.json" } + ], + "compilerOptions": { + "rootDir": "./src", + "outDir": "./dist", + "declarationDir": "./dist", + "paths": { "~/*": ["./src/*"], "~tests/*": ["./__tests__/*"] }, + "baseUrl": "." + } +} diff --git a/packages/app-trash-bin/tsconfig.json b/packages/app-trash-bin/tsconfig.json new file mode 100644 index 00000000000..af2773b4eae --- /dev/null +++ b/packages/app-trash-bin/tsconfig.json @@ -0,0 +1,34 @@ +{ + "extends": "../../tsconfig.json", + "include": ["src", "__tests__"], + "references": [ + { "path": "../app-aco" }, + { "path": "../app-admin" }, + { "path": "../app-utils" }, + { "path": "../react-composition" }, + { "path": "../react-properties" }, + { "path": "../ui" } + ], + "compilerOptions": { + "rootDirs": ["./src", "./__tests__"], + "outDir": "./dist", + "declarationDir": "./dist", + "paths": { + "~/*": ["./src/*"], + "~tests/*": ["./__tests__/*"], + "@webiny/app-aco/*": ["../app-aco/src/*"], + "@webiny/app-aco": ["../app-aco/src"], + "@webiny/app-admin/*": ["../app-admin/src/*"], + "@webiny/app-admin": ["../app-admin/src"], + "@webiny/app-utils/*": ["../app-utils/src/*"], + "@webiny/app-utils": ["../app-utils/src"], + "@webiny/react-composition/*": ["../react-composition/src/*"], + "@webiny/react-composition": ["../react-composition/src"], + "@webiny/react-properties/*": ["../react-properties/src/*"], + "@webiny/react-properties": ["../react-properties/src"], + "@webiny/ui/*": ["../ui/src/*"], + "@webiny/ui": ["../ui/src"] + }, + "baseUrl": "." + } +} diff --git a/packages/app-trash-bin/webiny.config.js b/packages/app-trash-bin/webiny.config.js new file mode 100644 index 00000000000..6dff86766c9 --- /dev/null +++ b/packages/app-trash-bin/webiny.config.js @@ -0,0 +1,8 @@ +const { createWatchPackage, createBuildPackage } = require("@webiny/project-utils"); + +module.exports = { + commands: { + build: createBuildPackage({ cwd: __dirname }), + watch: createWatchPackage({ cwd: __dirname }) + } +}; diff --git a/packages/app-utils/.babelrc.js b/packages/app-utils/.babelrc.js new file mode 100644 index 00000000000..bec58b263bd --- /dev/null +++ b/packages/app-utils/.babelrc.js @@ -0,0 +1 @@ +module.exports = require("@webiny/project-utils").createBabelConfigForReact({ path: __dirname }); diff --git a/packages/app-utils/LICENSE b/packages/app-utils/LICENSE new file mode 100644 index 00000000000..f772d04d4db --- /dev/null +++ b/packages/app-utils/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) Webiny + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/packages/app-utils/README.md b/packages/app-utils/README.md new file mode 100644 index 00000000000..d4248b7f58b --- /dev/null +++ b/packages/app-utils/README.md @@ -0,0 +1,18 @@ +# @webiny/app-utils + +[![](https://img.shields.io/npm/dw/@webiny/app-utils.svg)](https://www.npmjs.com/package/@webiny/app-utils) +[![](https://img.shields.io/npm/v/@webiny/app-utils.svg)](https://www.npmjs.com/package/@webiny/app-utils) +[![code style: prettier](https://img.shields.io/badge/code_style-prettier-ff69b4.svg?style=flat-square)](https://github.com/prettier/prettier) +[![PRs Welcome](https://img.shields.io/badge/PRs-welcome-brightgreen.svg?style=flat-square)](http://makeapullrequest.com) + +## Install + +``` +npm install --save @webiny/app-utils +``` + +Or if you prefer yarn: + +``` +yarn add @webiny/app-utils +``` diff --git a/packages/app-utils/package.json b/packages/app-utils/package.json new file mode 100644 index 00000000000..04096dc6ca1 --- /dev/null +++ b/packages/app-utils/package.json @@ -0,0 +1,36 @@ +{ + "name": "@webiny/app-utils", + "version": "0.0.0", + "main": "index.js", + "repository": { + "type": "git", + "url": "https://github.com/webiny/webiny-js.git", + "directory": "packages/app-utils" + }, + "author": "Webiny Ltd.", + "license": "MIT", + "dependencies": { + "@webiny/utils": "0.0.0", + "mobx": "^6.9.0" + }, + "devDependencies": { + "@babel/cli": "^7.23.9", + "@babel/core": "^7.24.0", + "@babel/preset-env": "^7.24.0", + "@babel/preset-react": "^7.23.3", + "@babel/preset-typescript": "^7.23.3", + "@babel/runtime": "^7.24.0", + "@webiny/cli": "0.0.0", + "@webiny/project-utils": "0.0.0", + "ttypescript": "^1.5.12", + "typescript": "4.7.4" + }, + "publishConfig": { + "access": "public", + "directory": "dist" + }, + "scripts": { + "build": "yarn webiny run build", + "watch": "yarn webiny run watch" + } +} diff --git a/packages/app-utils/src/fta/Domain/Models/Meta/Meta.ts b/packages/app-utils/src/fta/Domain/Models/Meta/Meta.ts new file mode 100644 index 00000000000..71db5e096e1 --- /dev/null +++ b/packages/app-utils/src/fta/Domain/Models/Meta/Meta.ts @@ -0,0 +1,29 @@ +export interface MetaDTO { + totalCount: number; + cursor: string | null; + hasMoreItems: boolean; +} + +export class Meta { + public totalCount: number; + public cursor: string | null; + public hasMoreItems: boolean; + + protected constructor(meta: MetaDTO) { + this.totalCount = meta.totalCount; + this.cursor = meta.cursor; + this.hasMoreItems = meta.hasMoreItems; + } + + static create(meta: MetaDTO) { + return new Meta(meta); + } + + static createEmpty() { + return new Meta({ + totalCount: 0, + cursor: null, + hasMoreItems: false + }); + } +} diff --git a/packages/app-utils/src/fta/Domain/Models/Meta/MetaMapper.ts b/packages/app-utils/src/fta/Domain/Models/Meta/MetaMapper.ts new file mode 100644 index 00000000000..8de299cb230 --- /dev/null +++ b/packages/app-utils/src/fta/Domain/Models/Meta/MetaMapper.ts @@ -0,0 +1,11 @@ +import { Meta, MetaDTO } from "./Meta"; + +export class MetaMapper { + static toDto(data: Meta | MetaDTO): MetaDTO { + return { + totalCount: data.totalCount ?? 0, + cursor: data.cursor ?? null, + hasMoreItems: data.hasMoreItems ?? false + }; + } +} diff --git a/packages/app-utils/src/fta/Domain/Models/Meta/index.ts b/packages/app-utils/src/fta/Domain/Models/Meta/index.ts new file mode 100644 index 00000000000..2eec9bc8b65 --- /dev/null +++ b/packages/app-utils/src/fta/Domain/Models/Meta/index.ts @@ -0,0 +1,2 @@ +export * from "./Meta"; +export * from "./MetaMapper"; diff --git a/packages/app-utils/src/fta/Domain/Models/Sorting/Sorting.ts b/packages/app-utils/src/fta/Domain/Models/Sorting/Sorting.ts new file mode 100644 index 00000000000..cd4e84f3758 --- /dev/null +++ b/packages/app-utils/src/fta/Domain/Models/Sorting/Sorting.ts @@ -0,0 +1,18 @@ +export interface SortingDTO { + field: string; + order: "asc" | "desc"; +} + +export class Sorting { + public field: string; + public order: "asc" | "desc"; + + protected constructor(sorting: SortingDTO) { + this.field = sorting.field; + this.order = sorting.order; + } + + static create(sorting: SortingDTO) { + return new Sorting(sorting); + } +} diff --git a/packages/app-utils/src/fta/Domain/Models/Sorting/SortingMapper.ts b/packages/app-utils/src/fta/Domain/Models/Sorting/SortingMapper.ts new file mode 100644 index 00000000000..1dd45f3322d --- /dev/null +++ b/packages/app-utils/src/fta/Domain/Models/Sorting/SortingMapper.ts @@ -0,0 +1,47 @@ +import { Sorting, SortingDTO } from "./Sorting"; + +export type DbSorting = `${string}_ASC` | `${string}_DESC`; + +export interface ColumnSorting { + id: string; + desc: boolean; +} + +export class SortingMapper { + static toDTO(data: Sorting | SortingDTO): SortingDTO { + const { field, order } = data; + + return { + field, + order + }; + } + + static fromColumnToDTO(data: ColumnSorting): SortingDTO { + const { id, desc } = data; + + return { + field: id, + order: desc ? "desc" : "asc" + }; + } + + static fromDTOtoColumn(data: SortingDTO): ColumnSorting { + const { field, order } = data; + + return { + id: field, + desc: order === "desc" + }; + } + + static fromDTOtoDb(data: SortingDTO): DbSorting { + const { field, order } = data; + + if (order === "asc") { + return `${field}_ASC`; + } + + return `${field}_DESC`; + } +} diff --git a/packages/app-utils/src/fta/Domain/Models/Sorting/index.ts b/packages/app-utils/src/fta/Domain/Models/Sorting/index.ts new file mode 100644 index 00000000000..dfc02b9c395 --- /dev/null +++ b/packages/app-utils/src/fta/Domain/Models/Sorting/index.ts @@ -0,0 +1,2 @@ +export * from "./Sorting"; +export * from "./SortingMapper"; diff --git a/packages/app-utils/src/fta/Domain/Models/index.ts b/packages/app-utils/src/fta/Domain/Models/index.ts new file mode 100644 index 00000000000..8e8472e08e0 --- /dev/null +++ b/packages/app-utils/src/fta/Domain/Models/index.ts @@ -0,0 +1,2 @@ +export * from "./Meta"; +export * from "./Sorting"; diff --git a/packages/app-utils/src/fta/Domain/Repositories/Loading/ILoadingRepository.ts b/packages/app-utils/src/fta/Domain/Repositories/Loading/ILoadingRepository.ts new file mode 100644 index 00000000000..d52ed3be714 --- /dev/null +++ b/packages/app-utils/src/fta/Domain/Repositories/Loading/ILoadingRepository.ts @@ -0,0 +1,5 @@ +export interface ILoadingRepository { + get: () => Record; + set: (action: string, isLoading?: boolean) => Promise; + runCallBack: (callback: Promise, action: string) => Promise; +} diff --git a/packages/app-utils/src/fta/Domain/Repositories/Loading/LoadingRepository.ts b/packages/app-utils/src/fta/Domain/Repositories/Loading/LoadingRepository.ts new file mode 100644 index 00000000000..52274e3a05d --- /dev/null +++ b/packages/app-utils/src/fta/Domain/Repositories/Loading/LoadingRepository.ts @@ -0,0 +1,26 @@ +import { makeAutoObservable } from "mobx"; +import { ILoadingRepository } from "./ILoadingRepository"; + +export class LoadingRepository implements ILoadingRepository { + private loadings: Map; + + constructor() { + this.loadings = new Map(); + makeAutoObservable(this); + } + + get() { + return Object.fromEntries(this.loadings); + } + + async set(action: string, isLoading = true) { + this.loadings.set(action, isLoading); + } + + async runCallBack(callback: Promise, action: string) { + await this.set(action, true); + const result = await callback; + await this.set(action, false); + return result; + } +} diff --git a/packages/app-utils/src/fta/Domain/Repositories/Loading/LoadingRepositoryFactory.ts b/packages/app-utils/src/fta/Domain/Repositories/Loading/LoadingRepositoryFactory.ts new file mode 100644 index 00000000000..e761c3768ae --- /dev/null +++ b/packages/app-utils/src/fta/Domain/Repositories/Loading/LoadingRepositoryFactory.ts @@ -0,0 +1,21 @@ +import { LoadingRepository } from "./LoadingRepository"; + +export class LoadingRepositoryFactory { + private cache: Map = new Map(); + + getRepository() { + const cacheKey = this.getCacheKey(); + + if (!this.cache.has(cacheKey)) { + this.cache.set(cacheKey, new LoadingRepository()); + } + + return this.cache.get(cacheKey) as LoadingRepository; + } + + private getCacheKey() { + return Date.now().toString(); + } +} + +export const loadingRepositoryFactory = new LoadingRepositoryFactory(); diff --git a/packages/app-utils/src/fta/Domain/Repositories/Loading/index.ts b/packages/app-utils/src/fta/Domain/Repositories/Loading/index.ts new file mode 100644 index 00000000000..8cd09311d20 --- /dev/null +++ b/packages/app-utils/src/fta/Domain/Repositories/Loading/index.ts @@ -0,0 +1,3 @@ +export * from "./ILoadingRepository"; +export * from "./LoadingRepository"; +export * from "./LoadingRepositoryFactory"; diff --git a/packages/app-utils/src/fta/Domain/Repositories/Meta/IMetaRepository.ts b/packages/app-utils/src/fta/Domain/Repositories/Meta/IMetaRepository.ts new file mode 100644 index 00000000000..dad28c2bd6c --- /dev/null +++ b/packages/app-utils/src/fta/Domain/Repositories/Meta/IMetaRepository.ts @@ -0,0 +1,8 @@ +import { Meta } from "../../Models"; + +export interface IMetaRepository { + get: () => Meta; + set: (meta: Meta) => Promise; + increaseTotalCount: (count?: number) => Promise; + decreaseTotalCount: (count?: number) => Promise; +} diff --git a/packages/app-utils/src/fta/Domain/Repositories/Meta/MetaRepository.ts b/packages/app-utils/src/fta/Domain/Repositories/Meta/MetaRepository.ts new file mode 100644 index 00000000000..944ab6c3030 --- /dev/null +++ b/packages/app-utils/src/fta/Domain/Repositories/Meta/MetaRepository.ts @@ -0,0 +1,45 @@ +import { makeAutoObservable } from "mobx"; +import { decodeCursor, encodeCursor } from "@webiny/utils"; +import { IMetaRepository } from "./IMetaRepository"; +import { Meta, MetaDTO, MetaMapper } from "~/fta/Domain/Models/Meta"; + +export class MetaRepository implements IMetaRepository { + private meta: Meta; + + constructor() { + this.meta = Meta.createEmpty(); + makeAutoObservable(this); + } + + async set(meta: MetaDTO) { + this.meta = Meta.create(meta); + } + + get() { + return MetaMapper.toDto(this.meta); + } + + async decreaseTotalCount(count = 1) { + return await this.updateMetaOnColumnDeltaChange(-count); + } + + async increaseTotalCount(count = 1) { + return await this.updateMetaOnColumnDeltaChange(count); + } + + private async updateMetaOnColumnDeltaChange(countDelta: number) { + // Retrieve the current meta + const current = this.get(); + + // Calculate the new totalCount based on the delta change + const totalCount = current.totalCount + countDelta; + + // Calculate the new cursor position based on the delta change + const cursorDecoded = decodeCursor(current.cursor); + const newCursorDecoded = String(Number(cursorDecoded) + countDelta); + const cursor = encodeCursor(newCursorDecoded); + + // Update the meta with the new totalCount and cursor + return await this.set({ ...current, totalCount, cursor }); + } +} diff --git a/packages/app-utils/src/fta/Domain/Repositories/Meta/MetaRepositoryFactory.ts b/packages/app-utils/src/fta/Domain/Repositories/Meta/MetaRepositoryFactory.ts new file mode 100644 index 00000000000..d02ce526695 --- /dev/null +++ b/packages/app-utils/src/fta/Domain/Repositories/Meta/MetaRepositoryFactory.ts @@ -0,0 +1,21 @@ +import { MetaRepository } from "./MetaRepository"; + +export class MetaRepositoryFactory { + private cache: Map = new Map(); + + getRepository() { + const cacheKey = this.getCacheKey(); + + if (!this.cache.has(cacheKey)) { + this.cache.set(cacheKey, new MetaRepository()); + } + + return this.cache.get(cacheKey) as MetaRepository; + } + + private getCacheKey() { + return Date.now().toString(); + } +} + +export const metaRepositoryFactory = new MetaRepositoryFactory(); diff --git a/packages/app-utils/src/fta/Domain/Repositories/Meta/index.ts b/packages/app-utils/src/fta/Domain/Repositories/Meta/index.ts new file mode 100644 index 00000000000..b2167c1d02d --- /dev/null +++ b/packages/app-utils/src/fta/Domain/Repositories/Meta/index.ts @@ -0,0 +1,3 @@ +export * from "./IMetaRepository"; +export * from "./MetaRepository"; +export * from "./MetaRepositoryFactory"; diff --git a/packages/app-utils/src/fta/Domain/Repositories/Sorting/ISortingRepository.ts b/packages/app-utils/src/fta/Domain/Repositories/Sorting/ISortingRepository.ts new file mode 100644 index 00000000000..9a5065bd27b --- /dev/null +++ b/packages/app-utils/src/fta/Domain/Repositories/Sorting/ISortingRepository.ts @@ -0,0 +1,6 @@ +import { Sorting } from "~/fta/Domain/Models"; + +export interface ISortingRepository { + set: (sorts: Sorting[]) => void; + get: () => Sorting[]; +} diff --git a/packages/app-utils/src/fta/Domain/Repositories/Sorting/SortingRepository.ts b/packages/app-utils/src/fta/Domain/Repositories/Sorting/SortingRepository.ts new file mode 100644 index 00000000000..976392faac7 --- /dev/null +++ b/packages/app-utils/src/fta/Domain/Repositories/Sorting/SortingRepository.ts @@ -0,0 +1,19 @@ +import { makeAutoObservable } from "mobx"; +import { ISortingRepository } from "./ISortingRepository"; +import { Sorting } from "~/fta/Domain/Models"; + +export class SortingRepository implements ISortingRepository { + private sorting: Sorting[] = []; + + constructor() { + makeAutoObservable(this); + } + + get() { + return this.sorting; + } + + set(sorts: Sorting[]) { + this.sorting = sorts; + } +} diff --git a/packages/app-utils/src/fta/Domain/Repositories/Sorting/SortingRepositoryFactory.ts b/packages/app-utils/src/fta/Domain/Repositories/Sorting/SortingRepositoryFactory.ts new file mode 100644 index 00000000000..cccc7ddf528 --- /dev/null +++ b/packages/app-utils/src/fta/Domain/Repositories/Sorting/SortingRepositoryFactory.ts @@ -0,0 +1,21 @@ +import { SortingRepository } from "./SortingRepository"; + +export class SortingRepositoryFactory { + private cache: Map = new Map(); + + getRepository() { + const cacheKey = this.getCacheKey(); + + if (!this.cache.has(cacheKey)) { + this.cache.set(cacheKey, new SortingRepository()); + } + + return this.cache.get(cacheKey) as SortingRepository; + } + + private getCacheKey() { + return Date.now().toString(); + } +} + +export const sortRepositoryFactory = new SortingRepositoryFactory(); diff --git a/packages/app-utils/src/fta/Domain/Repositories/Sorting/index.ts b/packages/app-utils/src/fta/Domain/Repositories/Sorting/index.ts new file mode 100644 index 00000000000..1591590c9d5 --- /dev/null +++ b/packages/app-utils/src/fta/Domain/Repositories/Sorting/index.ts @@ -0,0 +1,3 @@ +export * from "./ISortingRepository"; +export * from "./SortingRepository"; +export * from "./SortingRepositoryFactory"; diff --git a/packages/app-utils/src/fta/Domain/Repositories/index.ts b/packages/app-utils/src/fta/Domain/Repositories/index.ts new file mode 100644 index 00000000000..cb829cef162 --- /dev/null +++ b/packages/app-utils/src/fta/Domain/Repositories/index.ts @@ -0,0 +1,3 @@ +export * from "./Loading"; +export * from "./Meta"; +export * from "./Sorting"; diff --git a/packages/app-utils/src/fta/Domain/index.ts b/packages/app-utils/src/fta/Domain/index.ts new file mode 100644 index 00000000000..02a7d080c57 --- /dev/null +++ b/packages/app-utils/src/fta/Domain/index.ts @@ -0,0 +1,2 @@ +export * from "./Models"; +export * from "./Repositories"; diff --git a/packages/app-utils/src/fta/index.ts b/packages/app-utils/src/fta/index.ts new file mode 100644 index 00000000000..b8feca1a598 --- /dev/null +++ b/packages/app-utils/src/fta/index.ts @@ -0,0 +1 @@ +export * from "./Domain"; diff --git a/packages/app-utils/src/index.ts b/packages/app-utils/src/index.ts new file mode 100644 index 00000000000..480d7fde2cd --- /dev/null +++ b/packages/app-utils/src/index.ts @@ -0,0 +1 @@ +export * from "./fta"; diff --git a/packages/app-utils/tsconfig.build.json b/packages/app-utils/tsconfig.build.json new file mode 100644 index 00000000000..101a416961c --- /dev/null +++ b/packages/app-utils/tsconfig.build.json @@ -0,0 +1,12 @@ +{ + "extends": "../../tsconfig.build.json", + "include": ["src"], + "references": [{ "path": "../utils/tsconfig.build.json" }], + "compilerOptions": { + "rootDir": "./src", + "outDir": "./dist", + "declarationDir": "./dist", + "paths": { "~/*": ["./src/*"], "~tests/*": ["./__tests__/*"] }, + "baseUrl": "." + } +} diff --git a/packages/app-utils/tsconfig.json b/packages/app-utils/tsconfig.json new file mode 100644 index 00000000000..198b286cf66 --- /dev/null +++ b/packages/app-utils/tsconfig.json @@ -0,0 +1,17 @@ +{ + "extends": "../../tsconfig.json", + "include": ["src", "__tests__"], + "references": [{ "path": "../utils" }], + "compilerOptions": { + "rootDirs": ["./src", "./__tests__"], + "outDir": "./dist", + "declarationDir": "./dist", + "paths": { + "~/*": ["./src/*"], + "~tests/*": ["./__tests__/*"], + "@webiny/utils/*": ["../utils/src/*"], + "@webiny/utils": ["../utils/src"] + }, + "baseUrl": "." + } +} diff --git a/packages/app-utils/webiny.config.js b/packages/app-utils/webiny.config.js new file mode 100644 index 00000000000..6dff86766c9 --- /dev/null +++ b/packages/app-utils/webiny.config.js @@ -0,0 +1,8 @@ +const { createWatchPackage, createBuildPackage } = require("@webiny/project-utils"); + +module.exports = { + commands: { + build: createBuildPackage({ cwd: __dirname }), + watch: createWatchPackage({ cwd: __dirname }) + } +}; diff --git a/packages/ui/src/DataTable/DataTable.tsx b/packages/ui/src/DataTable/DataTable.tsx index d9f52eb2f26..e5deaf362f8 100644 --- a/packages/ui/src/DataTable/DataTable.tsx +++ b/packages/ui/src/DataTable/DataTable.tsx @@ -24,6 +24,7 @@ import { Row, RowSelectionState, SortingState, + ColumnSort, VisibilityState, flexRender, getCoreRowModel, @@ -98,6 +99,8 @@ export type TableRow = Row; export type Sorting = SortingState; +export { ColumnSort }; + export type OnSortingChange = OnChangeFn; export type ColumnVisibility = VisibilityState; @@ -446,6 +449,7 @@ export const DataTable = & DefaultData>({ enableRowSelection: isRowSelectable, onRowSelectionChange, enableSorting: !!onSortingChange, + enableSortingRemoval: false, manualSorting: true, onSortingChange, enableHiding: !!onColumnVisibilityChange, diff --git a/scripts/listPackagesWithTests.js b/scripts/listPackagesWithTests.js index 4f113f4e02d..56fb2b5b397 100644 --- a/scripts/listPackagesWithTests.js +++ b/scripts/listPackagesWithTests.js @@ -118,6 +118,9 @@ const CUSTOM_HANDLERS = { "app-headless-cms": () => { return ["packages/app-headless-cms"]; }, + "app-trash-bin": () => { + return ["packages/app-trash-bin"]; + }, tasks: () => { return ["packages/tasks --storage=ddb", "packages/tasks --storage=ddb-es,ddb"]; }, diff --git a/yarn.lock b/yarn.lock index 78b38e1f39e..63307b49b0d 100644 --- a/yarn.lock +++ b/yarn.lock @@ -14990,6 +14990,7 @@ __metadata: "@webiny/app-i18n": 0.0.0 "@webiny/app-plugin-admin-welcome-screen": 0.0.0 "@webiny/app-security": 0.0.0 + "@webiny/app-trash-bin": 0.0.0 "@webiny/cli": 0.0.0 "@webiny/error": 0.0.0 "@webiny/feature-flags": 0.0.0 @@ -15612,6 +15613,60 @@ __metadata: languageName: unknown linkType: soft +"@webiny/app-trash-bin@0.0.0, @webiny/app-trash-bin@workspace:packages/app-trash-bin": + version: 0.0.0-use.local + resolution: "@webiny/app-trash-bin@workspace:packages/app-trash-bin" + dependencies: + "@babel/cli": ^7.23.9 + "@babel/core": ^7.24.0 + "@babel/preset-env": ^7.24.0 + "@babel/preset-react": ^7.23.3 + "@babel/preset-typescript": ^7.23.3 + "@babel/runtime": ^7.24.0 + "@emotion/styled": ^11.10.6 + "@material-design-icons/svg": ^0.12.1 + "@types/react": 17.0.39 + "@webiny/app-aco": 0.0.0 + "@webiny/app-admin": 0.0.0 + "@webiny/app-utils": 0.0.0 + "@webiny/cli": 0.0.0 + "@webiny/project-utils": 0.0.0 + "@webiny/react-composition": 0.0.0 + "@webiny/react-properties": 0.0.0 + "@webiny/ui": 0.0.0 + apollo-client: ^2.6.10 + apollo-link: ^1.2.14 + graphql: ^15.7.2 + lodash: 4.17.21 + mobx: ^6.9.0 + mobx-react-lite: ^3.4.3 + react: 17.0.2 + react-dom: 17.0.2 + rimraf: ^5.0.5 + ttypescript: ^1.5.12 + typescript: 4.7.4 + languageName: unknown + linkType: soft + +"@webiny/app-utils@0.0.0, @webiny/app-utils@workspace:packages/app-utils": + version: 0.0.0-use.local + resolution: "@webiny/app-utils@workspace:packages/app-utils" + dependencies: + "@babel/cli": ^7.23.9 + "@babel/core": ^7.24.0 + "@babel/preset-env": ^7.24.0 + "@babel/preset-react": ^7.23.3 + "@babel/preset-typescript": ^7.23.3 + "@babel/runtime": ^7.24.0 + "@webiny/cli": 0.0.0 + "@webiny/project-utils": 0.0.0 + "@webiny/utils": 0.0.0 + mobx: ^6.9.0 + ttypescript: ^1.5.12 + typescript: 4.7.4 + languageName: unknown + linkType: soft + "@webiny/app-wcp@0.0.0, @webiny/app-wcp@workspace:packages/app-wcp": version: 0.0.0-use.local resolution: "@webiny/app-wcp@workspace:packages/app-wcp"