Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[stable27] enh/import from deck #5003

Merged
merged 25 commits into from
Aug 12, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
4e4da92
fix: Properly export cards as a child element of the related stack
juliusknorr Jul 17, 2023
b381588
fix: Avoid failing due to uninitialized acces of systemInstance
juliusknorr Jul 17, 2023
c683044
WIP: enh(import): import deck json exports
max-nextcloud Jul 12, 2023
4c05c40
feat: Implement logic to import multiple boards
juliusknorr Jul 17, 2023
7a4ae5f
feat: Add app version to the deck app export
juliusknorr Jul 17, 2023
fb7f316
test: Add some basic integration test skeleton for import
juliusknorr Jul 18, 2023
6318a31
test: Add example test data for deck import
juliusknorr Jul 18, 2023
ab8d4b8
docs: Add dedicated documentation section for import/export
juliusknorr Jul 19, 2023
e2ac4df
feat: Let occ deck:import default to deck json importer
juliusknorr Jul 19, 2023
e48a1c6
draft: todos
juliusknorr Jul 19, 2023
5f4c4cd
fix: request full details for board export
juliusknorr Jul 20, 2023
73c6487
test: Add reimport test case
juliusknorr Jul 20, 2023
8cabd60
fix: Avoid duplicate data on board export
juliusknorr Jul 27, 2023
cc9750a
fix: Only set last modified if not already set manually
juliusknorr Jul 27, 2023
4b9bae2
fix: Do not fail on missing owner details
juliusknorr Jul 27, 2023
0af05d6
tests: assert json diff between import/export
juliusknorr Jul 27, 2023
8feeb70
fix: Add output for individual failures or skipped parts
juliusknorr Aug 11, 2023
b0af2fe
fix: Map card assignments through mapping config
juliusknorr Aug 11, 2023
a8466d1
chore: Cleanup some outdated fixme comments
juliusknorr Aug 11, 2023
07ba4b2
fix: Only map owner for user mapping
juliusknorr Aug 11, 2023
1881010
tests: ignore version of stored json for import tests
juliusknorr Aug 11, 2023
beafcfa
style: fix php-cs
juliusknorr Aug 11, 2023
3da4e24
fix: use proper owner source
juliusknorr Aug 12, 2023
84c8d70
ci(cypress): Catch resize observer loop limit
juliusknorr Aug 12, 2023
4881de7
ci(cypress): Catch resize observer loop limit (2)
juliusknorr Aug 12, 2023
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions cypress/support/e2e.js
Original file line number Diff line number Diff line change
Expand Up @@ -16,5 +16,9 @@
// Import commands.js using ES2015 syntax:
import './commands.js'

Cypress.on('uncaught:exception', (err) => {
return !err.message.includes('ResizeObserver loop limit exceeded')
})

// Alternatively you can use CommonJS syntax:
// require('./commands')
98 changes: 98 additions & 0 deletions docs/export-import.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
## Export

Deck currently supports exporting all boards a user owns in a single JSON file. The format is based on the database schema that deck uses. It can be used to re-import boards on the same or other instances.

The export currently has some kown limitations in terms of specific data not included:
- Activity information
- File attachments to deck cards
- Comments
-
```
occ deck:export > my-file.json
```

## Import boards

Importing can be done using the API or the `occ` `deck:import` command.

It is possible to import from the following sources:

### Deck JSON

A json file that has been obtained from the above described `occ deck:export [userid]` command can be imported.

```
occ deck:import my-file.json
```

In case you are importing from a different instance you may use an additional config file to provide custom user id mapping in case users have different identifiers.

```
{
"owner": "admin",
"uidRelation": {
"johndoe": "test-user-1"
}
}
```

#### Trello JSON

Limitations:
* Comments with more than 1000 characters are placed as attached files to the card.

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/main/lib/Service/Importer/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/main/lib/Service/Importer/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"
}
}
```
17 changes: 11 additions & 6 deletions lib/Command/BoardImport.php
Original file line number Diff line number Diff line change
Expand Up @@ -25,17 +25,15 @@

use OCA\Deck\Service\Importer\BoardImportCommandService;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Input\InputArgument;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Input\InputOption;
use Symfony\Component\Console\Output\OutputInterface;

class BoardImport extends Command {
private BoardImportCommandService $boardImportCommandService;

public function __construct(
BoardImportCommandService $boardImportCommandService
private BoardImportCommandService $boardImportCommandService
) {
$this->boardImportCommandService = $boardImportCommandService;
parent::__construct();
}

Expand All @@ -44,7 +42,9 @@ public function __construct(
*/
protected function configure() {
$allowedSystems = $this->boardImportCommandService->getAllowedImportSystems();
$names = array_column($allowedSystems, 'name');
$names = array_map(function ($name) {
return '"' . $name . '"';
}, array_column($allowedSystems, 'internalName'));
$this
->setName('deck:import')
->setDescription('Import data')
Expand All @@ -53,7 +53,7 @@ protected function configure() {
null,
InputOption::VALUE_REQUIRED,
'Source system for import. Available options: ' . implode(', ', $names) . '.',
null
'DeckJson',
)
->addOption(
'config',
Expand All @@ -69,6 +69,11 @@ protected function configure() {
'Data file to import.',
'data.json'
)
->addArgument(
'file',
InputArgument::OPTIONAL,
'File to import',
)
;
}

Expand Down
58 changes: 22 additions & 36 deletions lib/Command/UserExport.php
Original file line number Diff line number Diff line change
Expand Up @@ -29,39 +29,23 @@
use OCA\Deck\Db\StackMapper;
use OCA\Deck\Model\CardDetails;
use OCA\Deck\Service\BoardService;
use OCP\AppFramework\Db\DoesNotExistException;
use OCP\AppFramework\Db\MultipleObjectsReturnedException;
use OCP\IGroupManager;
use OCP\IUserManager;
use OCP\App\IAppManager;
use OCP\DB\Exception;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Input\InputArgument;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;

class UserExport extends Command {
protected $boardService;
protected $cardMapper;
private $userManager;
private $groupManager;
private $assignedUsersMapper;

public function __construct(BoardMapper $boardMapper,
BoardService $boardService,
StackMapper $stackMapper,
CardMapper $cardMapper,
AssignmentMapper $assignedUsersMapper,
IUserManager $userManager,
IGroupManager $groupManager) {
public function __construct(
private IAppManager $appManager,
private BoardMapper $boardMapper,
private BoardService $boardService,
private StackMapper $stackMapper,
private CardMapper $cardMapper,
private AssignmentMapper $assignedUsersMapper,
) {
parent::__construct();

$this->cardMapper = $cardMapper;
$this->boardService = $boardService;
$this->stackMapper = $stackMapper;
$this->assignedUsersMapper = $assignedUsersMapper;
$this->boardMapper = $boardMapper;

$this->userManager = $userManager;
$this->groupManager = $groupManager;
}

protected function configure() {
Expand All @@ -73,30 +57,27 @@ protected function configure() {
InputArgument::REQUIRED,
'User ID of the user'
)
->addOption('legacy-format', 'l')
;
}

/**
* @param InputInterface $input
* @param OutputInterface $output
* @return int
* @throws DoesNotExistException
* @throws MultipleObjectsReturnedException
* @throws \ReflectionException
* @throws Exception
*/
protected function execute(InputInterface $input, OutputInterface $output): int {
$userId = $input->getArgument('user-id');
$legacyFormat = $input->getOption('legacy-format');

$this->boardService->setUserId($userId);
$boards = $this->boardService->findAll();
$boards = $this->boardService->findAll(fullDetails: false);

$data = [];
foreach ($boards as $board) {
$fullBoard = $this->boardMapper->find($board->getId(), true, true);
$data[$board->getId()] = (array)$fullBoard->jsonSerialize();
$data[$board->getId()] = $fullBoard->jsonSerialize();
$stacks = $this->stackMapper->findAll($board->getId());
foreach ($stacks as $stack) {
$data[$board->getId()]['stacks'][] = (array)$stack->jsonSerialize();
$data[$board->getId()]['stacks'][$stack->getId()] = $stack->jsonSerialize();
$cards = $this->cardMapper->findAllByStack($stack->getId());
foreach ($cards as $card) {
$fullCard = $this->cardMapper->find($card->getId());
Expand All @@ -108,7 +89,12 @@ protected function execute(InputInterface $input, OutputInterface $output): int
}
}
}
$output->writeln(json_encode($data, JSON_PRETTY_PRINT));
$output->writeln(json_encode(
$legacyFormat ? $data : [
'version' => $this->appManager->getAppVersion('deck'),
'boards' => $data
],
JSON_PRETTY_PRINT));
return 0;
}
}
13 changes: 13 additions & 0 deletions lib/Db/Assignment.php
Original file line number Diff line number Diff line change
Expand Up @@ -41,4 +41,17 @@ public function __construct() {
$this->addType('type', 'integer');
$this->addResolvable('participant');
}

public function getTypeString(): string {
switch ($this->getType()) {
case self::TYPE_USER:
return 'user';
case self::TYPE_GROUP:
return 'group';
case self::TYPE_CIRCLE:
return 'circle';
}

return 'unknown';
}
}
4 changes: 2 additions & 2 deletions lib/Db/BoardMapper.php
Original file line number Diff line number Diff line change
Expand Up @@ -532,12 +532,12 @@ public function flushCache(?int $boardId = null, ?string $userId = null) {
if ($boardId) {
unset($this->boardCache[$boardId]);
} else {
$this->boardCache = null;
$this->boardCache = new CappedMemoryCache();
}
if ($userId) {
unset($this->userBoardCache[$userId]);
} else {
$this->userBoardCache = null;
$this->userBoardCache = new CappedMemoryCache();
}
}
}
4 changes: 3 additions & 1 deletion lib/Db/LabelMapper.php
Original file line number Diff line number Diff line change
Expand Up @@ -115,7 +115,9 @@ public function findAssignedLabelsForBoard($boardId, $limit = null, $offset = nu
}

public function insert(Entity $entity): Entity {
$entity->setLastModified(time());
if (!in_array('lastModified', $entity->getUpdatedFields())) {
$entity->setLastModified(time());
}
return parent::insert($entity);
}

Expand Down
2 changes: 1 addition & 1 deletion lib/Db/RelationalEntity.php
Original file line number Diff line number Diff line change
Expand Up @@ -138,7 +138,7 @@ public function __call(string $methodName, array $args) {

$attr = lcfirst(substr($methodName, 3));
if (array_key_exists($attr, $this->_resolvedProperties) && str_starts_with($methodName, 'set')) {
if (!is_scalar($args[0])) {
if ($args[0] !== null && !is_scalar($args[0])) {
$args[0] = $args[0]['primaryKey'];
}
parent::setter($attr, $args);
Expand Down
13 changes: 13 additions & 0 deletions lib/Service/Importer/ABoardImportService.php
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,10 @@ abstract class ABoardImportService {
*/
abstract public function bootstrap(): void;

public function getBoards(): array {
return [$this->getImportService()->getData()];
}

abstract public function getBoard(): ?Board;

/**
Expand Down Expand Up @@ -133,4 +137,13 @@ public function getImportService(): BoardImportService {
public function needValidateData(): bool {
return $this->needValidateData;
}

public function reset(): void {
// FIXME: Would be cleaner if we could just get a new instance per board
// but currently https://github.com/nextcloud/deck/blob/7d820aa3f9fc69ada8188549b9a2fbb9093ffb95/lib/Service/Importer/BoardImportService.php#L194 returns a singleton
$this->labels = [];
$this->stacks = [];
$this->acls = [];
$this->cards = [];
}
}
Loading
Loading