From ba313d0c4806e6dd2fe87a46a909f4f0c2db4bc0 Mon Sep 17 00:00:00 2001 From: Luka Trovic Date: Tue, 1 Apr 2025 18:54:12 +0200 Subject: [PATCH 1/2] feat: add board import and export Signed-off-by: Luka Trovic [skip ci] --- appinfo/routes.php | 1 + cypress/fixtures/import-board.json | 102 ++++++++++++++++++ lib/Controller/BoardController.php | 62 +++++++++++ src/components/navigation/AppNavigation.vue | 3 + .../navigation/AppNavigationBoard.vue | 11 +- .../navigation/AppNavigationImportBoard.vue | 55 ++++++++++ .../navigation/BoardExportModal.vue | 78 ++++++++++++++ src/services/BoardApi.js | 23 +++- .../Middleware/ExceptionMiddlewareTest.php | 11 +- tests/unit/controller/BoardControllerTest.php | 7 ++ 10 files changed, 350 insertions(+), 3 deletions(-) create mode 100644 cypress/fixtures/import-board.json create mode 100644 src/components/navigation/AppNavigationImportBoard.vue create mode 100644 src/components/navigation/BoardExportModal.vue diff --git a/appinfo/routes.php b/appinfo/routes.php index 0231397cbb..e96142db60 100644 --- a/appinfo/routes.php +++ b/appinfo/routes.php @@ -28,6 +28,7 @@ ['name' => 'board#clone', 'url' => '/boards/{boardId}/clone', 'verb' => 'POST'], ['name' => 'board#transferOwner', 'url' => '/boards/{boardId}/transferOwner', 'verb' => 'PUT'], ['name' => 'board#export', 'url' => '/boards/{boardId}/export', 'verb' => 'GET'], + ['name' => 'board#import', 'url' => '/boards/import', 'verb' => 'POST'], // stacks ['name' => 'stack#index', 'url' => '/stacks/{boardId}', 'verb' => 'GET'], diff --git a/cypress/fixtures/import-board.json b/cypress/fixtures/import-board.json new file mode 100644 index 0000000000..be8b18f94f --- /dev/null +++ b/cypress/fixtures/import-board.json @@ -0,0 +1,102 @@ +{ + "boards": [ + { + "id": 70, + "title": "Imported board", + "owner": "unvjrmwuag", + "color": "00ff00", + "archived": false, + "labels": [ + { + "id": 293, + "title": "Finished", + "color": "31CC7C", + "boardId": 70, + "cardId": null, + "lastModified": 0, + "ETag": "cfcd208495d565ef66e7dff9f98764da" + }, + { + "id": 294, + "title": "To review", + "color": "317CCC", + "boardId": 70, + "cardId": null, + "lastModified": 0, + "ETag": "cfcd208495d565ef66e7dff9f98764da" + }, + { + "id": 295, + "title": "Action needed", + "color": "FF7A66", + "boardId": 70, + "cardId": null, + "lastModified": 0, + "ETag": "cfcd208495d565ef66e7dff9f98764da" + }, + { + "id": 296, + "title": "Later", + "color": "F1DB50", + "boardId": 70, + "cardId": null, + "lastModified": 0, + "ETag": "cfcd208495d565ef66e7dff9f98764da" + } + ], + "acl": [], + "permissions": [], + "users": [], + "stacks": { + "114": { + "id": 114, + "title": "TestList", + "boardId": 70, + "deletedAt": 0, + "lastModified": 1743495533, + "cards": [ + { + "id": 124, + "title": "Hello world", + "description": "# Hello world", + "descriptionPrev": null, + "stackId": 114, + "type": "plain", + "lastModified": 1743495533, + "lastEditor": null, + "createdAt": 1743495533, + "labels": [], + "assignedUsers": null, + "attachments": null, + "attachmentCount": null, + "owner": { + "primaryKey": "unvjrmwuag", + "uid": "unvjrmwuag", + "displayname": "unvjrmwuag", + "type": 0 + }, + "order": 999, + "archived": false, + "done": null, + "duedate": null, + "notified": false, + "deletedAt": 0, + "commentsUnread": 0, + "commentsCount": 0, + "relatedStack": null, + "relatedBoard": null, + "ETag": "aa85bb973089e7fbc0bbf122e926c23f" + } + ], + "order": 0, + "ETag": "aa85bb973089e7fbc0bbf122e926c23f" + } + }, + "activeSessions": [], + "deletedAt": 0, + "lastModified": 1743495533, + "settings": [], + "ETag": "aa85bb973089e7fbc0bbf122e926c23f" + } + ] +} diff --git a/lib/Controller/BoardController.php b/lib/Controller/BoardController.php index 3b60a457de..1065da2441 100644 --- a/lib/Controller/BoardController.php +++ b/lib/Controller/BoardController.php @@ -9,10 +9,12 @@ use OCA\Deck\Db\Acl; use OCA\Deck\Db\Board; use OCA\Deck\Service\BoardService; +use OCA\Deck\Service\Importer\BoardImportService; use OCA\Deck\Service\PermissionService; use OCP\AppFramework\ApiController; use OCP\AppFramework\Http; use OCP\AppFramework\Http\DataResponse; +use OCP\IL10N; use OCP\IRequest; class BoardController extends ApiController { @@ -21,6 +23,8 @@ public function __construct( IRequest $request, private BoardService $boardService, private PermissionService $permissionService, + private BoardImportService $boardImportService, + private IL10N $l10n, private $userId, ) { parent::__construct($appName, $request); @@ -162,4 +166,62 @@ public function transferOwner(int $boardId, string $newOwner): DataResponse { public function export($boardId) { return $this->boardService->export($boardId); } + + /** + * @NoAdminRequired + */ + public function import(): DataResponse { + $file = $this->request->getUploadedFile('file'); + $error = null; + $phpFileUploadErrors = [ + UPLOAD_ERR_OK => $this->l10n->t('The file was uploaded'), + UPLOAD_ERR_INI_SIZE => $this->l10n->t('The uploaded file exceeds the upload_max_filesize directive in php.ini'), + UPLOAD_ERR_FORM_SIZE => $this->l10n->t('The uploaded file exceeds the MAX_FILE_SIZE directive that was specified in the HTML form'), + UPLOAD_ERR_PARTIAL => $this->l10n->t('The file was only partially uploaded'), + UPLOAD_ERR_NO_FILE => $this->l10n->t('No file was uploaded'), + UPLOAD_ERR_NO_TMP_DIR => $this->l10n->t('Missing a temporary folder'), + UPLOAD_ERR_CANT_WRITE => $this->l10n->t('Could not write file to disk'), + UPLOAD_ERR_EXTENSION => $this->l10n->t('A PHP extension stopped the file upload'), + ]; + + if (empty($file)) { + $error = $this->l10n->t('No file uploaded or file size exceeds maximum of %s', [\OCP\Util::humanFileSize(\OCP\Util::uploadLimit())]); + } + if (!empty($file) && array_key_exists('error', $file) && $file['error'] !== UPLOAD_ERR_OK) { + $error = $phpFileUploadErrors[$file['error']]; + } + if (!empty($file) && $file['error'] === UPLOAD_ERR_OK && !in_array($file['type'], ['application/json', 'text/plain'])) { + $error = $this->l10n->t('Invalid file type. Only JSON files are allowed.'); + } + if ($error !== null) { + return new DataResponse([ + 'status' => 'error', + 'message' => $error, + ], Http::STATUS_BAD_REQUEST); + } + + try { + $fileContent = file_get_contents($file['tmp_name']); + $this->boardImportService->setSystem('DeckJson'); + $config = new \stdClass(); + $config->owner = $this->userId; + $this->boardImportService->setConfigInstance($config); + $this->boardImportService->setData(json_decode($fileContent)); + $this->boardImportService->import(); + $importedBoard = $this->boardImportService->getBoard(); + $board = $this->boardService->find($importedBoard->getId()); + + return new DataResponse($board, Http::STATUS_OK); + } catch (\TypeError $e) { + return new DataResponse([ + 'status' => 'error', + 'message' => $this->l10n->t('Invalid JSON data'), + ], Http::STATUS_BAD_REQUEST); + } catch (\Exception $e) { + return new DataResponse([ + 'status' => 'error', + 'message' => $this->l10n->t('Failed to import board'), + ], Http::STATUS_BAD_REQUEST); + } + } } diff --git a/src/components/navigation/AppNavigation.vue b/src/components/navigation/AppNavigation.vue index 013c268446..edab125fb0 100644 --- a/src/components/navigation/AppNavigation.vue +++ b/src/components/navigation/AppNavigation.vue @@ -42,6 +42,7 @@ +