diff --git a/README.org b/README.org index 0443fbc..b9163b6 100644 --- a/README.org +++ b/README.org @@ -17,7 +17,7 @@ ** Install/Getting started - Get ~.phar~ file from r, Run configuration wizzard (planned for version 1.0). + Get ~.phar~ file from r, Run configruration wizzard (planned for version 1.0). Before version 1.0 is released you have to navigate into ~/app~ directory and call command through ~.rprt.php~ file. @@ -75,6 +75,11 @@ curl 'https://drunomics.myjetbrains.com/youtrack/api/reports?$top=-1&fields=id,n 5 days of development. - remove errors from reports +* Support the work + +If you find this Free Software useful you can consider donating to my +[[https://liberapay.com/tehnoklistir/][@tehno-klistir liberapay account.]] + * Development ** Testing diff --git a/app/dependencies.php b/app/dependencies.php index 6d5e8c0..77cc097 100644 --- a/app/dependencies.php +++ b/app/dependencies.php @@ -1,5 +1,6 @@ create()->constructor(get('config.service')), + 'youtrack_rest_api.client' => get(YoutrackRestApiClient::class), 'pdf_export.service' => get(PdfExportInterface::class), // 'locale' => get('config.service')->method('get', 'en'), // Translator::class => create()->constructor('sl')->method('addLoader', 'po', new PoFileLoader), diff --git a/app/rprt.php b/app/rprt.php index c6cde93..d17cfa8 100755 --- a/app/rprt.php +++ b/app/rprt.php @@ -21,4 +21,5 @@ $reportCommand = $container->get(ReportCommand::class); $application->add($reportCommand); // eval(\Psy\sh()); + $application->run(); diff --git a/app/src/Commands/InvoiceCommand.php b/app/src/Commands/InvoiceCommand.php index 71a3d90..6e147d9 100644 --- a/app/src/Commands/InvoiceCommand.php +++ b/app/src/Commands/InvoiceCommand.php @@ -51,6 +51,9 @@ class InvoiceCommand extends Command protected PdfExportInterface $pdfExport; + /** + * Mailer service. + */ protected MailerInterface $mailer; protected const TYPE_WORK = 1; @@ -177,9 +180,12 @@ class InvoiceCommand extends Command } $output->writeln("report: {$report_name}"); $data = $this->csv->getInvoiceData($file); - if (! empty($expenses)) { + if (!empty($expenses)) { $data = array_merge($data, $expenses); } + if (!empty($custom)) { + $data = array_merge($data, $custom); + } $table = $this->getTable($output, $data); $table->render(); if ($input->getOption('pdf')) { diff --git a/app/src/Utils/CsvReport/ReportCsv.php b/app/src/Utils/CsvReport/ReportCsv.php index 873413c..c098e60 100644 --- a/app/src/Utils/CsvReport/ReportCsv.php +++ b/app/src/Utils/CsvReport/ReportCsv.php @@ -145,7 +145,7 @@ class ReportCsv implements ReportCsvInterface $rows[] = [ null, null, - 'Kosten', + 'Extra', // Kosten 'EUR', ]; // Don't make next line bold. See RprtCli\PdfExport\PdfExportService::parsedDataToHtml. @@ -161,6 +161,7 @@ class ReportCsv implements ReportCsvInterface ]; $totalPrice += $invoice_element->getValue(); } + // @todo Add Extra time as well! } if ($add_separator) { $rows[] = ReportCsvInterface::SEPARATOR_MEDIUM; diff --git a/app/src/Utils/TimeTrackingServices/EntityDefinition.php b/app/src/Utils/TimeTrackingServices/EntityDefinition.php index 7707e00..eed84af 100644 --- a/app/src/Utils/TimeTrackingServices/EntityDefinition.php +++ b/app/src/Utils/TimeTrackingServices/EntityDefinition.php @@ -14,35 +14,63 @@ use Attribute; #[Attribute] class EntityDefinition { + /** + * @param string $id + * @param string $provider + * @param string $name + * @param array $fields + * @param array $filters + * @param array $resources + */ public function __construct( private string $id, private string $provider, private string $name, private array $fields, - private array $filters + private array $filters, + private array $resources ) { } - public function getId() { + public function getId() :string { return $this->id; } - public function getProvider() { + public function getProvider() :string { return $this->provider; } - public function getName() { + public function getName() :string { return $this->name; } - public function getFields() { + public function getFields() :array { return $this->fields; } - public function getFilters() { + public function getFilters() :array { return $this->filters; } - public function getDefinition() { + /** + * Should be of type ResourceInterface. + */ + public function getResources() :array { + return $this->resources; + } + + public function getResource(string $key) :?Resource { + return $this->resources[$key] ?? NULL; + } + + /** + * Get definition as array. + * + * @todo I'm not really sure why I implemented this method. Definition + * is a definition already. + * + * @return array + */ + public function getDefinition() :array { return [ 'id' => $this->id, 'provider' => $this->provider, diff --git a/app/src/Utils/TimeTrackingServices/EntityManagerInterface.php b/app/src/Utils/TimeTrackingServices/EntityManagerInterface.php index 616a517..3afdf56 100644 --- a/app/src/Utils/TimeTrackingServices/EntityManagerInterface.php +++ b/app/src/Utils/TimeTrackingServices/EntityManagerInterface.php @@ -4,7 +4,6 @@ declare(strict_types=1); namespace RprtCli\Utils\TimeTrackingServices; -use RprtCli\Utils\TimeTrackingServices\EntityDefinition; use RprtCli\Utils\TimeTrackingServices\EntityInterface; /** @@ -14,6 +13,8 @@ interface EntityManagerInterface { /** * List supported entity types. + * + * @return array */ public function list(): array; diff --git a/app/src/Utils/TimeTrackingServices/Resource.php b/app/src/Utils/TimeTrackingServices/Resource.php new file mode 100644 index 0000000..ea3609b --- /dev/null +++ b/app/src/Utils/TimeTrackingServices/Resource.php @@ -0,0 +1,40 @@ +id; + } + + public function getPath() :string { + return $this->path; + } + + public function getMethod() :string { + return $this->method; + } + + public function getDescription() :string { + return $this->description; + } +} diff --git a/app/src/Utils/TimeTrackingServices/YoutrackRestApi/EntityManager.php b/app/src/Utils/TimeTrackingServices/YoutrackRestApi/EntityManager.php index ad42896..aaeace1 100644 --- a/app/src/Utils/TimeTrackingServices/YoutrackRestApi/EntityManager.php +++ b/app/src/Utils/TimeTrackingServices/YoutrackRestApi/EntityManager.php @@ -4,6 +4,7 @@ declare(strict_types=1); namespace RprtCli\Utils\TimeTrackingServices\YoutrackRestApi; +use ReflectionAttribute; use RprtCli\Utils\TimeTrackingServices\EntityManagerInterface; use RprtCli\Utils\TimeTrackingServices\EntityDefinition; use RprtCli\Utils\TimeTrackingServices\EntityInterface; @@ -30,8 +31,11 @@ class EntityManager implements EntityManagerInterface { /** * Returns all the entity definitions. + * + * @return array + * List of entity definitions keyed with their id. */ - public function listEntityDefinitions() { + public function listEntityDefinitions() :array { if (!$this->entityDefinitions) { $this->discoverEntities(); } @@ -40,8 +44,10 @@ class EntityManager implements EntityManagerInterface { /** * Discovers entity definitions. + * + * @return array */ - private function discoverEntities() { + private function discoverEntities() :?array { if (!empty($this->entityDefinitions)) { return $this->entityDefinitions; } @@ -49,8 +55,8 @@ class EntityManager implements EntityManagerInterface { $definitions = []; // @todo create a proxy service for finder. $this->finder->files()->in($path); - /** @var SplFileInfo $file */ foreach ($this->finder as $file) { + /** @var SplFileInfo $file */ $class = self::ENTITIES_NAMESPACE . '\\' . self::ENTITIES_DIR . '\\' . $file->getBasename('.php'); $reflection = new \ReflectionClass($class); $attribute = $this->getAttributeOfInstance($reflection, EntityDefinition::class); @@ -61,8 +67,10 @@ class EntityManager implements EntityManagerInterface { } $instance = $attribute->newInstance(); $id = $instance->getId(); - $content = $instance->getDefinition(); - $content['class'] = $class; + $content = [ + 'definition' => $instance, + 'class' => $class, + ]; $definitions[$id] = $content; } $this->entityDefinitions = $definitions; @@ -77,19 +85,11 @@ class EntityManager implements EntityManagerInterface { * @param string $instance * The instance the attribute should be of. * - * @return + * @return ?ReflectionAttribute * The attribute instance. */ - protected function getAttributeOfInstance(\ReflectionClass $reflection, string $instance) { - $t = $reflection->getAttributes(); - var_dump($t); - var_dump($t[0]->getName()); - var_dump($t[0]->getArguments()); - var_dump($t[0]->newInstance()); - $s = $reflection->getAttributes(EntityDefinition::class, \ReflectionAttribute::IS_INSTANCEOF); - var_dump($s); + protected function getAttributeOfInstance(\ReflectionClass $reflection, string $instance) :?ReflectionAttribute { $attributes = $reflection->getAttributes($instance, \ReflectionAttribute::IS_INSTANCEOF); - var_dump($attributes); if (empty($attributes)) { return NULL; } @@ -97,6 +97,15 @@ class EntityManager implements EntityManagerInterface { return reset($attributes); } + /** + * Get entity definition by id. + * + * @param string $id + * Id of an entity definition (work_item, issue, project, comment). + * + * @return ?EntityDefinition + * Entity definition (maybe even EntityDefinition object). + */ public function getDefinition(string $id) :?array { if (!isset($this->entityDefinitions)) { $this->list(); @@ -108,6 +117,20 @@ class EntityManager implements EntityManagerInterface { return $this->entityDefinitions[$id]; } + /** + * Create entity instance. + * + * This method should be used for transfering the data between report cli + * tools and the youtrack rest api. + * + * @param string $id + * Instance id (issue, project, work_item, comment ...). + * @param array $values + * Time, description ... + * + * @return ?EntityInterface + * + */ public function createInstance(string $id, array $values) :?EntityInterface { if (!isset($this->entityDefinitions)) { $this->list(); @@ -117,8 +140,10 @@ class EntityManager implements EntityManagerInterface { return NULL; } $definition = $this->getDefinition($id); + // This is not ok. definition is of type EntityDefinition but we want + // EntityInterface. This is where $class would come in handy. $reflection = new \ReflectionClass($definition['class']); - return new $reflection->newInstanceArgs($values); + return $reflection->newInstanceArgs($values); } } diff --git a/app/src/Utils/TimeTrackingServices/YoutrackRestApi/FilterAttribute.php b/app/src/Utils/TimeTrackingServices/YoutrackRestApi/FilterAttribute.php new file mode 100644 index 0000000..a0f68d4 --- /dev/null +++ b/app/src/Utils/TimeTrackingServices/YoutrackRestApi/FilterAttribute.php @@ -0,0 +1,68 @@ +id; + } + + public function getProvider() :string { + return $this->provider; + } + + public function getName() :string { + return $this->name; + } + + public function getAllowedFields() :array { + return $this->allowedFields; + } + + public function getAllowedEntities() :array { + return $this->allowedEntities; + } + + /** + * Get definition as array. + * + * @todo I'm not really sure why I implemented this method. Definition + * is a definition already. + * + * @return array + */ + public function getDefinition() :array { + return [ + 'id' => $this->id, + 'provider' => $this->provider, + 'name' => $this->name, + 'allowedFields' => $this->allowedFields, + 'allowedEntities' => $this->allowedEntities, + ]; + } +} diff --git a/app/src/Utils/TimeTrackingServices/YoutrackRestApi/Filters/YoutrackFilterInterface.php b/app/src/Utils/TimeTrackingServices/YoutrackRestApi/Filters/YoutrackFilterInterface.php new file mode 100644 index 0000000..4e3baba --- /dev/null +++ b/app/src/Utils/TimeTrackingServices/YoutrackRestApi/Filters/YoutrackFilterInterface.php @@ -0,0 +1,15 @@ +getAttributes(EntityDefinition::class, \ReflectionAttribute::IS_INSTANCEOF); + if (empty($attributes)) { + return NULL; + } + return reset($attributes); + } + } diff --git a/app/src/Utils/TimeTrackingServices/YoutrackRestApi/YoutrackEntityTypes/YoutrackComment.php b/app/src/Utils/TimeTrackingServices/YoutrackRestApi/YoutrackEntityTypes/YoutrackComment.php new file mode 100644 index 0000000..1dd00b2 --- /dev/null +++ b/app/src/Utils/TimeTrackingServices/YoutrackRestApi/YoutrackEntityTypes/YoutrackComment.php @@ -0,0 +1,56 @@ + [ + 'id', + 'fullName', + 'email', + ], + 'issue' => [ + 'id', + 'idReadable', + 'project' => [ + 'id', + 'shortName', + ], + 'summary', + ], + ], + filters: [ + 'issue' => [ + 'required' => YoutrackFilterInterface::REQUIRED, + ], + ], + resources: [ + 'list' => new Resource( + id: 'list', + method: Resource::GET, + path: '/api/issues/{issueID}/comments', + description: 'Lists comments of issue.' + ), + ] + )] +class YoutrackComment extends YoutrackEntity { + +} diff --git a/app/src/Utils/TimeTrackingServices/YoutrackRestApi/YoutrackEntityTypes/YoutrackIssue.php b/app/src/Utils/TimeTrackingServices/YoutrackRestApi/YoutrackEntityTypes/YoutrackIssue.php index ff2dbf7..d7eb102 100644 --- a/app/src/Utils/TimeTrackingServices/YoutrackRestApi/YoutrackEntityTypes/YoutrackIssue.php +++ b/app/src/Utils/TimeTrackingServices/YoutrackRestApi/YoutrackEntityTypes/YoutrackIssue.php @@ -6,16 +6,38 @@ namespace RprtCli\Utils\TimeTrackingServices\YoutrackRestApi\YoutrackEntityTypes use RprtCli\Utils\TimeTrackingServices\YoutrackRestApi\YoutrackEntity; use RprtCli\Utils\TimeTrackingServices\EntityDefinition; +use RprtCli\Utils\TimeTrackingServices\Resource; /** * https://www.jetbrains.com/help/youtrack/devportal/api-entity-Issue.html + * https://www.jetbrains.com/help/youtrack/devportal/resource-api-issues.html */ #[EntityDefinition( - 'issue', - 'youtrack_rest_api', - 'Issue', - [], - [] + id: 'issue', + provider: 'youtrack_rest_api', + name: 'Issue', + fields: [ + 'id', + 'idReadable', + 'project' => [ + 'id', + 'shortName', + ], + 'summary', + ], + filters: [ + 'state', + 'project', + 'assignee', + ], + resources: [ + 'list' => new Resource( + id: 'list', + method: Resource::GET, + path: '/api/issues', + description: 'Lists issues.' + ), + ] )] class YoutrackIssue extends YoutrackEntity { const ID = 'issue'; diff --git a/app/src/Utils/TimeTrackingServices/YoutrackRestApi/YoutrackEntityTypes/YoutrackProject.php b/app/src/Utils/TimeTrackingServices/YoutrackRestApi/YoutrackEntityTypes/YoutrackProject.php index 27ea8ff..1db28cf 100644 --- a/app/src/Utils/TimeTrackingServices/YoutrackRestApi/YoutrackEntityTypes/YoutrackProject.php +++ b/app/src/Utils/TimeTrackingServices/YoutrackRestApi/YoutrackEntityTypes/YoutrackProject.php @@ -4,16 +4,39 @@ declare(strict_types=1); namespace RprtCli\Utils\TimeTrackingServices\YoutrackRestApi\YoutrackEntityTypes; +use RprtCli\Utils\TimeTrackingServices\Resource; use RprtCli\Utils\TimeTrackingServices\YoutrackRestApi\YoutrackEntity; use RprtCli\Utils\TimeTrackingServices\EntityDefinition; +/** + * https://www.jetbrains.com/help/youtrack/devportal/resource-api-admin-projects.html + */ #[EntityDefinition( - 'project', - 'youtrack_rest_api', - 'Youtrack Project', - [], - [] + id: 'project', + provider: 'youtrack_rest_api', + name: 'Youtrack Project', + fields: [ + 'id', + 'shortName', + 'description', + 'leader' => ['id', 'fullName'], + ], + filters: [], + resources: [ + 'list' => new Resource( + id: 'list', + method: Resource::GET, + path: '/api/admin/projects', + description: 'List projects' + ), + 'read' => new Resource( + id: 'read', + method: Resource::GET, + path: '/api/admin/projects/{projectId}', + description: 'Sprecific project' + ), + ] )] class YoutrackProject extends YoutrackEntity { - + } diff --git a/app/src/Utils/TimeTrackingServices/YoutrackRestApi/YoutrackEntityTypes/YoutrackWorkItem.php b/app/src/Utils/TimeTrackingServices/YoutrackRestApi/YoutrackEntityTypes/YoutrackWorkItem.php index 94eb89f..0fc5577 100644 --- a/app/src/Utils/TimeTrackingServices/YoutrackRestApi/YoutrackEntityTypes/YoutrackWorkItem.php +++ b/app/src/Utils/TimeTrackingServices/YoutrackRestApi/YoutrackEntityTypes/YoutrackWorkItem.php @@ -4,29 +4,59 @@ declare(strict_types=1); namespace RprtCli\Utils\TimeTrackingServices\YoutrackRestApi\YoutrackEntityTypes; +use RprtCli\Utils\TimeTrackingServices\Resource; use RprtCli\Utils\TimeTrackingServices\YoutrackRestApi\YoutrackEntity; use RprtCli\Utils\TimeTrackingServices\EntityDefinition; +/** + * https://www.jetbrains.com/help/youtrack/devportal/api-entity-IssueWorkItem.html + * https://www.jetbrains.com/help/youtrack/devportal/resource-api-workItems.html + */ #[EntityDefinition( - 'work_item', - 'youtrack_rest_api', - 'Issue Work Item', - [ + id: 'work_item', + provider: 'youtrack_rest_api', + name: 'Issue Work Item', + fields: [ 'id', - 'author', + 'author' => [ + 'id', + 'fullName', + 'email' + ], 'text', 'type', - 'duration', + 'duration' => [ + 'id', + 'minutes', + 'presentation' + ], 'date', - 'issue', + 'created', + 'issue' => [ + 'id', + 'idReadable', + 'project' => [ + 'id', + 'shortName' + ], + 'summary', + ], ], - [ + filters: [ 'project', 'issue', 'user', 'date', + ], + resources: [ + 'list' => new Resource( + id: 'list', + method: Resource::GET, + path: '/api/workItems', + description: 'List workItems' + ), ] )] class YoutrackWorkItem extends YoutrackEntity { - + } diff --git a/app/src/Utils/TimeTrackingServices/YoutrackRestApi/YoutrackRestApiClient.php b/app/src/Utils/TimeTrackingServices/YoutrackRestApi/YoutrackRestApiClient.php new file mode 100644 index 0000000..792498a --- /dev/null +++ b/app/src/Utils/TimeTrackingServices/YoutrackRestApi/YoutrackRestApiClient.php @@ -0,0 +1,50 @@ +config = $config; + $this->client = $this->createYoutrackClient(); + } + + protected function createYoutrackClient() :YouTrackClient { + // Could this all go into __construct method? + $apiBaseUri = $this->config->get('tracking_service.youtrack.base_url', false); + $apiToken = $this->config->get('tracking_service.youtrack.auth_token', false); + // Instantiate PSR-7 HTTP Client + $psrHttpClient = new \GuzzleHttp\Client([ + 'base_uri' => $apiBaseUri, + 'debug' => true, + ]); + // Instantiate YouTrack API HTTP Client + $httpClient = new GuzzleHttpClient($psrHttpClient); + // Instantiate YouTrack API Token Authorizer + $authorizer = new TokenAuthorizer($apiToken); + // Instantiate YouTrack API Client + $client = new YouTrackClient($httpClient, $authorizer); + return $client; + } + + public function getClient() :YouTrackClient { + return $this->client; + } +} diff --git a/app/src/Utils/TimeTrackingServices/YoutrackRestApi/YoutrackRestApiRequestBuilder.php b/app/src/Utils/TimeTrackingServices/YoutrackRestApi/YoutrackRestApiRequestBuilder.php new file mode 100644 index 0000000..d389c59 --- /dev/null +++ b/app/src/Utils/TimeTrackingServices/YoutrackRestApi/YoutrackRestApiRequestBuilder.php @@ -0,0 +1,87 @@ +entityManager = $entity_manager; + $this->clientFactory = $clientFactory; + // EntityMangerInterface + } + + /** + * Method that creates path to youtrack entity resource. + * + * Examples: + * list issue => GET '/api/issues?fields=id,idReadable,summary,..' + * create issue => POST '/api/issues' + * but create action is only allowed on worktItems. + * list work_item => GET '/api/workItem?fields=...' + * + * First impression seems simple. We need path and method. + * What bothers me whether it would be better to create a new attribute + * wehere these "mappings" would live: allowed actions and then method and path. + * + * @TODO Add filter system? + * + * @param string $action + * Name of the action: list, create, read, update, delete. + * @param string $entity_id + * Name of the entity id (project, issue, comment, work_item). + * + * @return ?string + * Path to the resource. + */ + public function buildPath(string $action, string $entity_id) :?string { + $defintion = $this->entityManager->getDefinition($entity_id); + if (!isset($defintion['definition'])) { + // Missing entity definition! + return NULL; + } + else { + $defintion = $defintion['definition']; + } + $fields = $definition->getFields(); + $fieldsQuery = $this->fieldsQuery($fields); + /** @var RprtCli\Utils\TimeTrackingServices\Resource $resource */ + $resource = $defintion->getResource($action); + if (!$resource) { + // Missing resource! + return NULL; + } + $path = $resource->getPath(); + // @TODO add query, create client request. + // @see https://github.com/cybercog/youtrack-rest-php/blob/eb0315133d1d3d161da23d26537201afb253dec9/src/Client/YouTrackClient.php#L89 + if ($fieldsQuery) { + $path .= '?' . $fieldsQuery; + } + // @TODO create request and handle request. + + } + + protected function fieldsQuery(array $fields) :?string { + $query = implode(',', $this->stringifyFields($fields)); + if (!empty($query)) { + return "fields=${query}"; + } + return $query; + } + + protected function stringifyFields(array $fields) :array { + foreach ($fields as $key => &$field) { + if (is_array($field)) { + $field = $key . '(' . implode(',', $this->stringifyFields($field)) . ')'; + } + } + return $fields; + } +} diff --git a/app/tests/Unit/YoutrackRestApi/EntityManagerTest.php b/app/tests/Unit/YoutrackRestApi/EntityManagerTest.php index 7b9bf3a..8dd5e80 100644 --- a/app/tests/Unit/YoutrackRestApi/EntityManagerTest.php +++ b/app/tests/Unit/YoutrackRestApi/EntityManagerTest.php @@ -6,6 +6,8 @@ namespace RprtCli\Tests\Unit\YoutrackRestApi; use DI\ContainerBuilder; use PHPUnit\Framework\TestCase; +use RprtCli\Utils\TimeTrackingServices\EntityDefinition; +use RprtCli\Utils\TimeTrackingServices\EntityInterface; /** * Test the entity manager service and system. @@ -24,15 +26,26 @@ class EntityManagerTest extends TestCase { // Instance of EntityMangerInterface; // $this->assertInstanceOf('EntityManagerInterface', $entityManagerService); $definitions = $entityManagerService->list(); - var_dump($definitions); + // var_dump($definitions); $this->assertCount(3, $definitions); $entities = ['project', 'issue', 'work_item']; - foreach ($entities as $entity) { - $this->assertArrayHasKey($entity, $definitions, 'Check that key exists in entity types definitions.'); + foreach ($entities as $entity_id) { + $this->assertArrayHasKey($entity_id, $definitions, 'Check that key exists in entity types definitions.'); + var_dump($definitions[$entity_id]); + + $this->assertInstanceOf(EntityDefinition::class, $definitions[$entity_id]['definition'], 'Check that definiton is of type entity definition'); + $definition = $entityManagerService->getDefinition($entity_id); + $entity = $entityManagerService->createInstance($entity_id, []); + $this->assertArrayHasKey('definition', $definition); + $this->assertArrayHasKey('class', $definition); + $this->assertInstanceOf(EntityDefinition::class, $definition['definition']); + $this->assertInstanceOf(EntityInterface::class, $entity); } // list method returns definitions: project, issue, worktItem // Check one definition, compare. + // Test creating the instance } } + diff --git a/box.phar b/box.phar new file mode 100755 index 0000000..283ef76 Binary files /dev/null and b/box.phar differ diff --git a/todo.txt b/todo.txt index e16d420..ae0d66e 100644 --- a/todo.txt +++ b/todo.txt @@ -15,6 +15,13 @@ TODO: - invoice command improvements - pdf and output option could be combined +TODO: (start of 2023) +- youtrack rest api: + - resources class and parameter in EntityDefinition + - compose fields method + - create filter system +- connect filter system to youtrack rest api command + TODO (end of 2022): - DONE fix project list output (first is missing) - DONE report selection trait