diff --git a/docs/User_documentation_en.md b/docs/User_documentation_en.md index 25ea8f8ee..824c80c5c 100644 --- a/docs/User_documentation_en.md +++ b/docs/User_documentation_en.md @@ -9,11 +9,14 @@ Project management, time management or ideation, Deck makes it easier for you to ## Using Deck Overall, Deck is easy to use. You can create boards, add users, share the Deck, work collaboratively and in real time. -1. [Create my first board](#1-create-my-first-board) -2. [Create stacks and cards](#2-create-stacks-and-cards) -3. [Handle cards options](#3-handle-cards-options) -4. [Archive old tasks](#4-archive-old-tasks) -5. [Manage your board](#5-manage-your-board) +- 1. [Create my first board](#1-create-my-first-board) +- 2. [Create stacks and cards](#2-create-stacks-and-cards) +- 3. [Handle cards options](#3-handle-cards-options) +- 4. [Archive old tasks](#4-archive-old-tasks) +- 5. [Manage your board](#5-manage-your-board) +- 6. [Import boards](#6-import-boards) + - [Trello JSON](#trello-json) + - [Trello API](#trello-api) ### 1. Create my first board In this example, we're going to create a board and share it with an other nextcloud user. @@ -69,6 +72,70 @@ The **sharing tab** allows you to add users or even groups to your boards. **Deleted objects** allows you to return previously deleted stacks or cards. The **Timeline** allows you to see everything that happened in your boards. Everything! +### 6. Import boards + +Importing can be done using the API or the `occ` `deck:import` command. + +It is possible to import from the following sources: + +#### Trello JSON + +Steps: +* Create the data file + * Access Trello + * go to the board you want to export + * Follow the steps in [Trello documentation](https://help.trello.com/article/747-exporting-data-from-trello-1) and export as JSON +* Create the configuration file +* Execute the import informing the import file path, data file and source as `Trello JSON` + +Create the configuration file respecting the [JSON Schema](https://github.com/nextcloud/deck/blob/master/lib/Service/fixtures/config-trelloJson-schema.json) for import `Trello JSON` + +Example configuration file: +```json +{ + "owner": "admin", + "color": "0800fd", + "uidRelation": { + "johndoe": "johndoe" + } +} +``` + +**Limitations**: + +Importing from a JSON file imports up to 1000 actions. To find out how many actions the board to be imported has, identify how many actions the JSON has. + +#### Trello API + +Import using API is recommended for boards with more than 1000 actions. + +Trello makes it possible to attach links to a card. Deck does not have this feature. Attachments and attachment links are added in a markdown table at the end of the description for every imported card that has attachments in Trello. + +* Get the API Key and API Token [here](https://developer.atlassian.com/cloud/trello/guides/rest-api/api-introduction/#authentication-and-authorization) +* Get the ID of the board you want to import by making a request to: +https://api.trello.com/1/members/me/boards?key={yourKey}&token={yourToken}&fields=id,name + + This ID you will use in the configuration file in the `board` property +* Create the configuration file + +Create the configuration file respecting the [JSON Schema](https://github.com/nextcloud/deck/blob/master/lib/Service/fixtures/config-trelloApi-schema.json) for import `Trello JSON` + +Example configuration file: +```json +{ + "owner": "admin", + "color": "0800fd", + "api": { + "key": "0cc175b9c0f1b6a831c399e269772661", + "token": "92eb5ffee6ae2fec3ad71c777531578f4a8a08f09d37b73795649038408b5f33" + }, + "board": "8277e0910d750195b4487976", + "uidRelation": { + "johndoe": "johndoe" + } +} +``` + ## Search Deck provides a global search either through the unified search in the Nextcloud header or with the inline search next to the board controls. diff --git a/docs/implement-import.md b/docs/implement-import.md index cca83357d..51972c56d 100644 --- a/docs/implement-import.md +++ b/docs/implement-import.md @@ -1,4 +1,4 @@ ## Implement import * Create a new class `lib/service/BoardImportService.php` where `` is the name of the source system. -* Use the `lib/service/BoardImportTrelloService.php` class as inspiration \ No newline at end of file +* Use the `lib/service/BoardImportTrelloJsonService.php` class as inspiration \ No newline at end of file diff --git a/docs/resources/BoardImport.svg b/docs/resources/BoardImport.svg index 700538bdb..1d17034a8 100644 --- a/docs/resources/BoardImport.svg +++ b/docs/resources/BoardImport.svg @@ -4,175 +4,211 @@ - - + + G - + A0 - - - -Classes used on -board import. -Methods just to -illustrate. + + + +Classes used on +board import. +Methods just to +illustrate. A1 - -ApiController + +ApiController A2 - -BoardImportApiController - -+import() -+getAllowedSystems() -+getConfigSchema() + +BoardImportApiController + ++import() ++getAllowedSystems() ++getConfigSchema() A1->A2 - - + + A3 - -BoardImportService - -+import() -+bootstrap() -+validateSystem() -#validateConfig() -#validateData() + +BoardImportService + ++import() ++bootstrap() ++validateSystem() +#validateConfig() +#validateData() A2->A3 - - -uses + + +uses A7 - -BoardImportTrelloService + +BoardImportTrelloApiService + ++name:string A3->A7 - - -uses - - - -A8 - - - -validateSystem is -public because is -used on Api. - - - -A3->A8 - - - - -A4 - -Command - - - -A5 - -BoardImport - -+boardImportCommandService - -#configure() -#execute(input,output) - - - -A4->A5 - - - - - -A6 - -BoardImportCommandService - -+bootstrap() -+import() -+validateSystem() -#validateConfig() -#validateData() - - - -A5->A6 - - -uses - - - -A6->A3 - - - - - -A7->A3 - - -uses + + +uses A9 - - - -To create an import -to another system, -create another class -similar to this. + +BoardImportTrelloJsonService + ++name:string +#needValidateData:true - + -A7->A9 - +A3->A9 + + +uses A10 - -<<abstract>> -ABoardImportService + + + +validateSystem is +public because is +used on Api. - + + +A3->A10 + + + + +A4 + +Command + + + +A5 + +BoardImport + ++boardImportCommandService + +#configure() +#execute(input,output) + + + +A4->A5 + + + + + +A6 + +BoardImportCommandService + ++bootstrap() ++import() ++validateSystem() +#validateConfig() +#validateData() + + + +A5->A6 + + +uses + + + +A6->A3 + + + + + +A7->A3 + + +uses + + + +A8 + +<<abstract>> +ABoardImportService + +#needValidateData:false + ++needValidateData():bool + + + +A7->A8 + + +implements + + -A7->A10 - - -implements +A9->A3 + + +uses + + + +A9->A8 + + +implements + + + +A11 + + + +To create an import +to another system, +create another class +similar to this. + + + +A9->A11 + diff --git a/docs/resources/BoardImport.yuml b/docs/resources/BoardImport.yuml index 69aa222da..cbe89c829 100644 --- a/docs/resources/BoardImport.yuml +++ b/docs/resources/BoardImport.yuml @@ -5,13 +5,20 @@ // {generate:true} [note: Classes used on board import. Methods just to illustrate. {bg:cornsilk}] + [ApiController]<-[BoardImportApiController|+import();+getAllowedSystems();+getConfigSchema()] [BoardImportApiController]uses-.->[BoardImportService|+import();+bootstrap();+validateSystem();#validateConfig();#validateData();] + [Command]<-[BoardImport|+boardImportCommandService|#configure();#execute(input,output)] [BoardImport]uses-.->[BoardImportCommandService|+bootstrap();+import();+validateSystem();#validateConfig();#validateData()] [BoardImportCommandService]->[BoardImportService] -[BoardImportService]uses-.->[BoardImportTrelloService] -[BoardImportTrelloService]uses-.->[BoardImportService] + +[BoardImportService]uses-.->[BoardImportTrelloApiService|+name:string] +[BoardImportTrelloApiService]uses-.->[BoardImportService] +[BoardImportTrelloApiService]implements-.-^[<> ABoardImportService|#needValidateData:false|+needValidateData():bool] + +[BoardImportService]uses-.->[BoardImportTrelloJsonService|+name:string;#needValidateData:true] +[BoardImportTrelloJsonService]uses-.->[BoardImportService] [BoardImportService]-[note: validateSystem is public because is used on Api. {bg:cornsilk}] -[BoardImportTrelloService]-[note: To create an import to another system, create another class similar to this. {bg:cornsilk}] -[BoardImportTrelloService]implements-.-^[<> ABoardImportService] \ No newline at end of file +[BoardImportTrelloJsonService]-[note: To create an import to another system, create another class similar to this. {bg:cornsilk}] +[BoardImportTrelloJsonService]implements-.-^[<> ABoardImportService] diff --git a/lib/Service/ABoardImportService.php b/lib/Service/ABoardImportService.php index 746febbc9..cbadca6b6 100644 --- a/lib/Service/ABoardImportService.php +++ b/lib/Service/ABoardImportService.php @@ -37,6 +37,7 @@ abstract class ABoardImportService { public static $name = ''; /** @var BoardImportService */ private $boardImportService; + /** @var bool */ protected $needValidateData = true; /** @var Stack[] */ protected $stacks = []; diff --git a/lib/Service/BoardImportService.php b/lib/Service/BoardImportService.php index 3a35a57aa..07cfb790d 100644 --- a/lib/Service/BoardImportService.php +++ b/lib/Service/BoardImportService.php @@ -180,7 +180,7 @@ class BoardImportService { $className = 'OCA\Deck\Service\\'.$matches['class']; if (!class_exists($className)) { /** @psalm-suppress UnresolvableInclude */ - require_once $name; + require_once $className; } /** @psalm-suppress InvalidPropertyFetch */ $name = $className::$name; diff --git a/lib/Service/BoardImportTrelloApiService.php b/lib/Service/BoardImportTrelloApiService.php index 5a8c54117..94e8f2432 100644 --- a/lib/Service/BoardImportTrelloApiService.php +++ b/lib/Service/BoardImportTrelloApiService.php @@ -23,14 +23,11 @@ namespace OCA\Deck\Service; -use GuzzleHttp\Exception\ClientException; -use GuzzleHttp\Exception\RequestException; -use OCP\AppFramework\Http; use OCP\Http\Client\IClient; use OCP\Http\Client\IClientService; use OCP\IL10N; -use OCP\ILogger; use OCP\IUserManager; +use Psr\Log\LoggerInterface; class BoardImportTrelloApiService extends BoardImportTrelloJsonService { /** @var string */ @@ -38,16 +35,17 @@ class BoardImportTrelloApiService extends BoardImportTrelloJsonService { protected $needValidateData = false; /** @var IClient */ private $httpClient; - /** @var ILogger */ + /** @var LoggerInterface */ protected $logger; /** @var string */ private $baseApiUrl = 'https://api.trello.com/1'; - + /** @var ?\stdClass[] */ + private $boards; public function __construct( IUserManager $userManager, IL10N $l10n, - ILogger $logger, + LoggerInterface $logger, IClientService $httpClientService ) { parent::__construct($userManager, $l10n); @@ -56,44 +54,150 @@ class BoardImportTrelloApiService extends BoardImportTrelloJsonService { } public function bootstrap(): void { - $this->getBoards(); + $this->populateBoard(); + $this->populateMembers(); + $this->populateLabels(); + $this->populateLists(); + $this->populateCheckLists(); + $this->populateCards(); + $this->populateActions(); parent::bootstrap(); } - private function getBoards() { - $boards = $this->doRequest('/members/me/boards'); + private function populateActions(): void { + $data = $this->getImportService()->getData(); + $data->actions = $this->doRequest( + '/boards/' . $data->id . '/actions', + [ + 'filter' => 'commentCard', + 'fields=memberCreator,type,data,date', + 'memberCreator_fields' => 'username', + 'limit' => 1000 + ] + ); } - private function doRequest($path, $queryString = []) { + private function populateCards(): void { + $data = $this->getImportService()->getData(); + $data->cards = $this->doRequest( + '/boards/' . $data->id . '/cards', + [ + 'fields' => 'id,idMembers,dateLastActivity,closed,idChecklists,name,idList,pos,desc,due,labels', + 'attachments' => true, + 'attachment_fields' => 'name,url,date', + 'limit' => 1000 + ] + ); + } + + private function populateCheckLists(): void { + $data = $this->getImportService()->getData(); + $data->checklists = $this->doRequest( + '/boards/' . $data->id . '/checkLists', + [ + 'fields' => 'id,idCard,name', + 'checkItem_fields' => 'id,state,name', + 'limit' => 1000 + ] + ); + } + + private function populateLists(): void { + $data = $this->getImportService()->getData(); + $data->lists = $this->doRequest( + '/boards/' . $data->id . '/lists', + [ + 'fields' => 'id,name,closed', + 'limit' => 1000 + ] + ); + } + + private function populateLabels(): void { + $data = $this->getImportService()->getData(); + $data->labels = $this->doRequest( + '/boards/' . $data->id . '/labels', + [ + 'fields' => 'id,color,name', + 'limit' => 1000 + ] + ); + } + + private function populateMembers(): void { + $data = $this->getImportService()->getData(); + $data->members = $this->doRequest( + '/boards/' . $data->id . '/members', + [ + 'fields' => 'username', + 'limit' => 1000 + ] + ); + } + + private function populateBoard(): void { + $toImport = $this->getImportService()->getConfig('board'); + $board = $this->doRequest( + '/boards/' . $toImport, + ['fields' => 'id,name'] + ); + if ($board instanceof \stdClass) { + $this->getImportService()->setData($board); + return; + } + throw new \Exception('Invalid board id to import'); + } + + /** + * @return array|\stdClass + */ + private function doRequest(string $path = '', array $queryString = []) { + $target = $this->baseApiUrl . $path; try { - $target = $this->baseApiUrl . $path; $result = $this->httpClient ->get($target, $this->getQueryString($queryString)) ->getBody(); - $data = json_decode($result); - } catch (ClientException $e) { - $status = $e->getCode(); - if ($status === Http::STATUS_FORBIDDEN) { - $this->logger->info($target . ' refused.', ['app' => 'deck']); - } else { - $this->logger->info($target . ' responded with a ' . $status . ' containing: ' . $e->getMessage(), ['app' => 'deck']); + if (is_string($result)) { + $data = json_decode($result); + if (is_array($data)) { + $data = array_merge( + $data, + $this->paginate($path, $queryString, $data) + ); + } + return $data; } - } catch (RequestException $e) { - $this->logger->logException($e, [ - 'message' => 'Could not connect to ' . $target, - 'level' => ILogger::INFO, - 'app' => 'deck', - ]); + throw new \Exception('Invalid return of api'); } catch (\Throwable $e) { - $this->logger->logException($e, ['app' => 'deck']); + $this->logger->critical( + $e->getMessage(), + ['app' => 'deck'] + ); + throw new \Exception($e->getMessage()); } - return $data; } - private function getQueryString($params = []): array { + private function paginate(string $path = '', array $queryString = [], array $data = []): array { + if (empty($queryString['limit'])) { + return []; + } + if (count($data) < $queryString['limit']) { + return []; + } + $queryString['before'] = end($data)->id; + $return = $this->doRequest($path, $queryString); + if (is_array($return)) { + return $return; + } + throw new \Exception('Invalid return of api'); + } + + private function getQueryString(array $params = []): array { $apiSettings = $this->getImportService()->getConfig('api'); $params['key'] = $apiSettings->key; - $params['value'] = $apiSettings->token; - return $params; + $params['token'] = $apiSettings->token; + return [ + 'query' => $params + ]; } } diff --git a/lib/Service/BoardImportTrelloJsonService.php b/lib/Service/BoardImportTrelloJsonService.php index 9bd7f6ff4..5ea3fa429 100644 --- a/lib/Service/BoardImportTrelloJsonService.php +++ b/lib/Service/BoardImportTrelloJsonService.php @@ -111,6 +111,7 @@ class BoardImportTrelloJsonService extends ABoardImportService { return $c->id; }, $values); $trelloComments = array_combine($keys, $values); + $trelloComments = $this->sortComments($trelloComments); foreach ($trelloComments as $commentId => $trelloComment) { $comment = new Comment(); if (!empty($this->getImportService()->getConfig('uidRelation')->{$trelloComment->memberCreator->username})) { @@ -131,6 +132,18 @@ class BoardImportTrelloJsonService extends ABoardImportService { return $comments; } + private function sortComments(array $comments): array { + $comparison = function($a, $b) { + if ($a->date == $b->date) { + return 0; + } + return ($a->date < $b->date) ? -1 : 1; + }; + + usort($comments, $comparison); + return $comments; + } + public function getCardLabelAssignment(): array { $cardsLabels = []; foreach ($this->getImportService()->getData()->cards as $trelloCard) { @@ -221,7 +234,7 @@ class BoardImportTrelloJsonService extends ABoardImportService { $cardsOnStack[] = $card; $this->stacks[$trelloCard->idList]->setCards($cardsOnStack); $card->setType('plain'); - $card->setOrder($trelloCard->idShort); + $card->setOrder($trelloCard->pos); $card->setOwner($this->getImportService()->getConfig('owner')->getUID()); $card->setDescription($trelloCard->desc); if ($trelloCard->due) { diff --git a/lib/Service/fixtures/config-trelloApi-schema.json b/lib/Service/fixtures/config-trelloApi-schema.json index baef76ce5..056c9e519 100644 --- a/lib/Service/fixtures/config-trelloApi-schema.json +++ b/lib/Service/fixtures/config-trelloApi-schema.json @@ -6,20 +6,17 @@ "properties": { "key": { "type": "string", - "pattern": "^\\w{32}$" + "pattern": "^[0-9a-fA-F]{32}$" }, "token": { "type": "string", - "pattern": "^\\w{1,}$" + "pattern": "^[0-9a-fA-F]{64}$" } } }, - "boards": { - "type": "array", - "items": { - "type": "string", - "pattern": "^\\w{1,}$" - } + "board": { + "type": "string", + "pattern": "^\\w{1,}$" }, "uidRelation": { "type": "object",