From f887d8721f49c26c4e5eb3b03ae05e918142bccc Mon Sep 17 00:00:00 2001 From: Robert Zondervan Date: Fri, 26 Jul 2024 16:36:55 +0200 Subject: [PATCH 01/65] Voortschrijdend inzicht op aanmeldingsspel --- appinfo/routes.php | 1 + lib/Controller/DirectoryController.php | 10 +++- lib/Service/DirectoryService.php | 63 +++++++++++++++++--------- 3 files changed, 51 insertions(+), 23 deletions(-) diff --git a/appinfo/routes.php b/appinfo/routes.php index 7aadd29c..7869186a 100644 --- a/appinfo/routes.php +++ b/appinfo/routes.php @@ -17,6 +17,7 @@ ['name' => 'search#index', 'url' => '/api/search', 'verb' => 'GET'], ['name' => 'search#show', 'url' => '/api/search/{id}', 'verb' => 'GET'], ['name' => 'directory#page', 'url' => '/directory', 'verb' => 'GET'], + ['name' => 'directory#add', 'url' => '/api/directory/add', 'verb' => 'POST'], ['name' => 'configuration#index', 'url' => '/configuration', 'verb' => 'GET'], ['name' => 'configuration#create', 'url' => '/configuration', 'verb' => 'POST'] ], diff --git a/lib/Controller/DirectoryController.php b/lib/Controller/DirectoryController.php index 17539119..d93b9c8f 100644 --- a/lib/Controller/DirectoryController.php +++ b/lib/Controller/DirectoryController.php @@ -70,11 +70,17 @@ public function page(?string $getParameter) ); } + + /** + * @PublicPage + * @NoCSRFRequired + */ public function add(?string $url, DirectoryService $directoryService): JSONResponse { + $directories = []; + $directoryService->registerToExternalDirectory(url: $url, externalDirectories: $directories); - - return new JSONResponse($listing); + return new JSONResponse(['listings added' => $directories]); } diff --git a/lib/Service/DirectoryService.php b/lib/Service/DirectoryService.php index f6848563..b718f2b7 100644 --- a/lib/Service/DirectoryService.php +++ b/lib/Service/DirectoryService.php @@ -4,17 +4,23 @@ use DateTime; use GuzzleHttp\Client; +use OCA\OpenCatalogi\Db\Catalog; +use OCA\OpenCatalogi\Db\CatalogMapper; +use OCA\OpenCatalogi\Db\ListingMapper; use OCP\IAppConfig; use OCP\IURLGenerator; class DirectoryService { + private string $appName = 'opencatalogi'; private Client $client; public function __construct( private readonly IURLGenerator $urlGenerator, private readonly IAppConfig $config, private readonly ObjectService $objectService, + private readonly CatalogMapper $catalogMapper, + private readonly ListingMapper $listingMapper, ) { $this->client = new Client([]); @@ -37,22 +43,32 @@ private function getDirectoryEntry(string $catalogId): array ]; } - public function registerToExternalDirectory (array $newDirectory): int + public function registerToExternalDirectory (array $newDirectory = [], ?string $url = null, array &$externalDirectories = []): int { + if($newDirectory !== [] && $url === null) { + $url = $newDirectory['directory']; + } + - if($this->config->getValueString()) - $dbConfig['base_uri'] = $this->config->getValueString('opencatalogi', 'mongodbLocation'); - $dbConfig['headers']['api-key'] = $this->config->getValueString('opencatalogi', 'mongodbKey'); - $dbConfig['mongodbCluster'] = $this->config->getValueString('opencatalogi', 'mongodbCluster'); + if($this->config->getValueString($this->appName, 'mongoStorage') !== '1') { + $catalogi = $this->catalogMapper->findAll(); + } else { + $dbConfig['base_uri'] = $this->config->getValueString('opencatalogi', 'mongodbLocation'); + $dbConfig['headers']['api-key'] = $this->config->getValueString('opencatalogi', 'mongodbKey'); + $dbConfig['mongodbCluster'] = $this->config->getValueString('opencatalogi', 'mongodbCluster'); - $catalogi = $this->objectService->findObjects(filters: ['_schema' => 'catalog'], config: $dbConfig)['documents']; + $catalogi = $this->objectService->findObjects(filters: ['_schema' => 'catalog'], config: $dbConfig)['documents']; + } foreach($catalogi as $catalog) { + if($catalog instanceof Catalog) { + $catalog = $catalog->jsonSerialize(); + } $directory = $this->getDirectoryEntry($catalog['id']); - $result = $this->client->post(uri: $newDirectory['directory'], options: ['json' => $directory, 'http_errors' => false]); + $result = $this->client->post(uri: $url, options: ['json' => $directory, 'http_errors' => false]); } - $externalDirectories = $this->fetchFromExternalDirectory($newDirectory); + $externalDirectories = $this->fetchFromExternalDirectory(url: $url); return $result->getStatusCode(); @@ -66,30 +82,35 @@ private function createDirectoryFromResult(array $result): ?array return null; } - $dbConfig['base_uri'] = $this->config->getValueString(app: 'opencatalogi', key: 'mongodbLocation'); - $dbConfig['headers']['api-key'] = $this->config->getValueString(app: 'opencatalogi', key: 'mongodbKey'); - $dbConfig['mongodbCluster'] = $this->config->getValueString(app: 'opencatalogi', key: 'mongodbCluster'); + if($this->config->getValueString($this->appName, 'mongoStorage') === '1') { + $dbConfig['base_uri'] = $this->config->getValueString(app: 'opencatalogi', key: 'mongodbLocation'); + $dbConfig['headers']['api-key'] = $this->config->getValueString(app: 'opencatalogi', key: 'mongodbKey'); + $dbConfig['mongodbCluster'] = $this->config->getValueString(app: 'opencatalogi', key: 'mongodbCluster'); - $result['_schema'] = 'directory'; + $result['_schema'] = 'directory'; - $returnData = $this->objectService->saveObject( - data: $result, - config: $dbConfig - ); + $returnData = $this->objectService->saveObject( + data: $result, + config: $dbConfig + ); + } else { + $this->listingMapper->createFromArray($result); + } $this->registerToExternalDirectory(newDirectory: $result); return $returnData; } - public function fetchFromExternalDirectory(array $directory): array + public function fetchFromExternalDirectory(array $directory = [], ?string $url = null): array { - $result = $this->client->get($directory['directory']); + if($directory !== [] && $url === null) { + $url = $directory['directory']; + } + $result = $this->client->get($url); $results = json_decode($result->getBody()->getContents(), true); - var_dump($results); - foreach($results['results'] as $record) { $this->createDirectoryFromResult($record); } @@ -99,6 +120,6 @@ public function fetchFromExternalDirectory(array $directory): array public function updateToExternalDirectory(): array { - + return []; } } From 8edab70a63f6f2bb021ba0f81f94c3c227f98869 Mon Sep 17 00:00:00 2001 From: Wilco Louwerse Date: Tue, 30 Jul 2024 17:10:49 +0200 Subject: [PATCH 02/65] Use $this->client not $client --- lib/Service/SearchService.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/Service/SearchService.php b/lib/Service/SearchService.php index bd03bcb7..4d0ca09f 100644 --- a/lib/Service/SearchService.php +++ b/lib/Service/SearchService.php @@ -109,7 +109,7 @@ public function search(array $parameters, array $elasticConfig, array $dbConfig, $parameters['_catalogi'] = $catalogi; - $promises[] = $client->getAsync($searchEndpoint, ['query' => $parameters]); + $promises[] = $this->client->getAsync($searchEndpoint, ['query' => $parameters]); } $responses = Utils::settle($promises)->wait(); From 8d91d9fe58255f0ffac900466c1775fa86052b9c Mon Sep 17 00:00:00 2001 From: Robert Zondervan Date: Wed, 31 Jul 2024 13:43:41 +0200 Subject: [PATCH 03/65] Fix serialization of featured --- lib/Db/Publication.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/Db/Publication.php b/lib/Db/Publication.php index 076cbd78..508bed58 100644 --- a/lib/Db/Publication.php +++ b/lib/Db/Publication.php @@ -18,7 +18,7 @@ class Publication extends Entity implements JsonSerializable protected ?string $portal = null; protected ?string $catalogi = null; protected ?string $metaData = null; - protected ?DateTime $published = null; + protected ?DateTime $published = null; protected ?DateTime $modified = null; protected ?string $featured = null; protected ?array $organization = []; @@ -110,7 +110,7 @@ public function jsonSerialize(): array 'metaData' => $this->metaData, 'published' => $this->published->format('c'), 'modified' => $this->modified->format('c'), - 'featured' => $this->featured, + 'featured' => $this->featured !== null ? (bool) $this->featured : null, 'organization' => $this->organization, 'data' => $this->data, 'attachments' => $this->attachments, From 463d6046cd709d4c42924dd91e6144402ee28e76 Mon Sep 17 00:00:00 2001 From: Robert Zondervan Date: Wed, 31 Jul 2024 15:16:11 +0200 Subject: [PATCH 04/65] Fixes from redesign --- composer.json | 3 ++- lib/Db/ListingMapper.php | 6 +++++- lib/Service/DirectoryService.php | 16 ++++++++++++++++ lib/Service/SearchService.php | 21 ++++++++++++++------- 4 files changed, 37 insertions(+), 9 deletions(-) diff --git a/composer.json b/composer.json index ec3d5b23..27f35662 100644 --- a/composer.json +++ b/composer.json @@ -43,7 +43,8 @@ "php": "^8.1", "elasticsearch/elasticsearch": "^v8.14.0", "adbario/php-dot-notation": "^3.3.0", - "guzzlehttp/guzzle": "^7.0" + "guzzlehttp/guzzle": "^7.0", + "symfony/uid": "^6.4" }, "require-dev": { "nextcloud/ocp": "dev-stable29", diff --git a/lib/Db/ListingMapper.php b/lib/Db/ListingMapper.php index 2400a3c7..098e0f8b 100644 --- a/lib/Db/ListingMapper.php +++ b/lib/Db/ListingMapper.php @@ -28,7 +28,7 @@ public function find(int $id): Listing return $this->findEntity(query: $qb); } - public function findAll($limit = null, $offset = null): array + public function findAll($limit = null, $offset = null, $filters = []): array { $qb = $this->db->getQueryBuilder(); @@ -37,6 +37,10 @@ public function findAll($limit = null, $offset = null): array ->setMaxResults($limit) ->setFirstResult($offset); + foreach($filters as $filter => $value) { + $qb->andWhere($qb->expr()->eq($filter, $qb->createNamedParameter($value))); + } + return $this->findEntities(query: $qb); } diff --git a/lib/Service/DirectoryService.php b/lib/Service/DirectoryService.php index b718f2b7..71662ecd 100644 --- a/lib/Service/DirectoryService.php +++ b/lib/Service/DirectoryService.php @@ -122,4 +122,20 @@ public function updateToExternalDirectory(): array { return []; } + + public function listDirectory(array $filters = [], int $limit = 30, int $offset = 0): array + { + if ($this->config->hasKey($this->appName, 'mongoStorage') === false + || $this->config->getValueString($this->appName, 'mongoStorage') !== '1' + ) { + return $this->listingMapper->findAll(limit: $limit, offset: $offset, filters: $filters); + } + $filters['_schema'] = 'directory'; + + $dbConfig['base_uri'] = $this->config->getValueString(app: $this->appName, key: 'mongodbLocation'); + $dbConfig['headers']['api-key'] = $this->config->getValueString(app: $this->appName, key: 'mongodbKey'); + $dbConfig['mongodbCluster'] = $this->config->getValueString(app: $this->appName, key: 'mongodbCluster'); + + return $this->objectService->findObjects(filters: $filters, config: $dbConfig); + } } diff --git a/lib/Service/SearchService.php b/lib/Service/SearchService.php index bd03bcb7..1844e648 100644 --- a/lib/Service/SearchService.php +++ b/lib/Service/SearchService.php @@ -16,8 +16,8 @@ class SearchService ]; public function __construct( - private readonly ObjectService $objectService, - private readonly ElasticSearchService $elasticService + private readonly ElasticSearchService $elasticService, + private readonly DirectoryService $directoryService, ) { $this->client = new Client(); } @@ -78,11 +78,18 @@ public function sortResultArray(array $a, array $b): int public function search(array $parameters, array $elasticConfig, array $dbConfig, array $catalogi = []): array { - $localResults = $this->elasticService->searchObject($parameters, $elasticConfig); + $localResults['results'] = []; + $localResults['facets'] = []; - $directory = $this->objectService->findObjects(filters: ['_schema' => 'directory'], config: $dbConfig); + if($elasticConfig['location'] !== '') { + $localResults = $this->elasticService->searchObject($parameters, $elasticConfig); + } + + $directory = $this->directoryService->listDirectory(limit: 1000); + +// $directory = $this->objectService->findObjects(filters: ['_schema' => 'directory'], config: $dbConfig); - if(count($directory['documents']) === 0) { + if(count($directory) === 0) { return $localResults; } @@ -92,7 +99,7 @@ public function search(array $parameters, array $elasticConfig, array $dbConfig, $searchEndpoints = []; $promises = []; - foreach($directory['documents'] as $instance) { + foreach($directory as $instance) { if( $instance['default'] === false && isset($parameters['.catalogi']) === true @@ -109,7 +116,7 @@ public function search(array $parameters, array $elasticConfig, array $dbConfig, $parameters['_catalogi'] = $catalogi; - $promises[] = $client->getAsync($searchEndpoint, ['query' => $parameters]); + $promises[] = $this->client->getAsync($searchEndpoint, ['query' => $parameters]); } $responses = Utils::settle($promises)->wait(); From 3a5a422736afbd120e050f20df3ffda0f90cd9a1 Mon Sep 17 00:00:00 2001 From: Robert Zondervan Date: Wed, 31 Jul 2024 16:54:26 +0200 Subject: [PATCH 05/65] Fixes from tests --- lib/Controller/DirectoryController.php | 4 +- lib/Db/Listing.php | 19 +++--- lib/Migration/Version6Date20240731141731.php | 61 ++++++++++++++++++++ lib/Service/DirectoryService.php | 21 +++++-- 4 files changed, 89 insertions(+), 16 deletions(-) create mode 100644 lib/Migration/Version6Date20240731141731.php diff --git a/lib/Controller/DirectoryController.php b/lib/Controller/DirectoryController.php index 64bee71f..522b2446 100644 --- a/lib/Controller/DirectoryController.php +++ b/lib/Controller/DirectoryController.php @@ -80,7 +80,7 @@ public function add(?string $url, DirectoryService $directoryService): JSONRespo $directories = []; $directoryService->registerToExternalDirectory(url: $url, externalDirectories: $directories); - return new JSONResponse(['listings added' => $directories]); + return new JSONResponse(['listingsAdded' => $directories]); } @@ -172,8 +172,6 @@ public function create(ObjectService $objectService, DirectoryService $directory config: $dbConfig ); - $directoryService->registerToExternalDirectory(newDirectory: $data); - // get post from requests return new JSONResponse($returnData); } diff --git a/lib/Db/Listing.php b/lib/Db/Listing.php index e0222dca..f4503bbb 100644 --- a/lib/Db/Listing.php +++ b/lib/Db/Listing.php @@ -16,10 +16,11 @@ class Listing extends Entity implements JsonSerializable protected ?string $search = null; protected ?string $directory = null; protected ?string $metadata = null; + protected ?string $catalogus = null; protected ?string $status = null; protected ?DateTime $lastSync = null; - protected bool $default = false; - protected bool $available = false; + protected ?bool $default = false; + protected ?bool $available = false; public function __construct() { $this->addType(fieldName: 'title', type: 'string'); @@ -28,6 +29,7 @@ public function __construct() { $this->addType(fieldName: 'search', type: 'string'); $this->addType(fieldName: 'directory', type: 'string'); $this->addType(fieldName: 'metadata', type: 'string'); + $this->addType(fieldName: 'catalogus', type: 'string'); $this->addType(fieldName: 'status', type: 'string'); $this->addType(fieldName: 'lastSync', type: 'datetime'); $this->addType(fieldName: 'default', type: 'boolean'); @@ -72,12 +74,13 @@ public function jsonSerialize(): array 'summary' => $this->summary, 'description' => $this->description, 'search' => $this->search, - 'directory' => $this->search, - 'metadata' => $this->search, - 'status' => $this->search, - 'lastSync' => $this->search, - 'default' => $this->search, - 'available' => $this->search, + 'directory' => $this->directory, + 'metadata' => $this->metadata, + 'catalogus' => $this->catalogus, + 'status' => $this->status, + 'lastSync' => $this->lastSync, + 'default' => $this->default, + 'available' => $this->available, ]; $jsonFields = $this->getJsonFields(); diff --git a/lib/Migration/Version6Date20240731141731.php b/lib/Migration/Version6Date20240731141731.php new file mode 100644 index 00000000..2bee8979 --- /dev/null +++ b/lib/Migration/Version6Date20240731141731.php @@ -0,0 +1,61 @@ +hasTable(tableName: 'listings') === true) { + $table = $schema->getTable(tableName: 'listings'); + + if($table->hasColumn(name: 'catalogus') === false) { + $table->addColumn(name: 'catalogus', typeName: Types::STRING); + } + } + + return $schema; + } + + /** + * @param IOutput $output + * @param Closure(): ISchemaWrapper $schemaClosure + * @param array $options + */ + public function postSchemaChange(IOutput $output, Closure $schemaClosure, array $options): void { + } +} diff --git a/lib/Service/DirectoryService.php b/lib/Service/DirectoryService.php index 71662ecd..1b41fc30 100644 --- a/lib/Service/DirectoryService.php +++ b/lib/Service/DirectoryService.php @@ -70,15 +70,24 @@ public function registerToExternalDirectory (array $newDirectory = [], ?string $ $externalDirectories = $this->fetchFromExternalDirectory(url: $url); - return $result->getStatusCode(); + if($result !== null) { + return $result->getStatusCode(); + } + return 200; } private function createDirectoryFromResult(array $result): ?array { + unset($result['id']); + $myDirectory = $this->getDirectoryEntry(''); - if(isset($result['directory']) === false || $result['directory'] === $myDirectory['directory']) { + if( + isset($result['directory']) === false + || $result['directory'] === $myDirectory['directory'] + || count($this->listDirectory(filters: ['catalogus' => $result['catalogus'], 'directory' => $result['directory']])) > 0 + ) { return null; } @@ -111,11 +120,13 @@ public function fetchFromExternalDirectory(array $directory = [], ?string $url = $results = json_decode($result->getBody()->getContents(), true); + $addedDirectories = []; + foreach($results['results'] as $record) { - $this->createDirectoryFromResult($record); + $addedDirectories[] = $this->createDirectoryFromResult($record); } - return $results['results']; + return $addedDirectories; } public function updateToExternalDirectory(): array @@ -136,6 +147,6 @@ public function listDirectory(array $filters = [], int $limit = 30, int $offset $dbConfig['headers']['api-key'] = $this->config->getValueString(app: $this->appName, key: 'mongodbKey'); $dbConfig['mongodbCluster'] = $this->config->getValueString(app: $this->appName, key: 'mongodbCluster'); - return $this->objectService->findObjects(filters: $filters, config: $dbConfig); + return $this->objectService->findObjects(filters: $filters, config: $dbConfig)['documents']; } } From 8a04d3c35de8560d7ff1e4c385652e324bf374b8 Mon Sep 17 00:00:00 2001 From: Thijn Date: Fri, 2 Aug 2024 14:25:27 +0200 Subject: [PATCH 06/65] removed some fields - changed the way publication displays need to wait on the backend for a fix --- .../publication/AddPublicationModal.vue | 111 ++++++++---------- 1 file changed, 46 insertions(+), 65 deletions(-) diff --git a/src/modals/publication/AddPublicationModal.vue b/src/modals/publication/AddPublicationModal.vue index 9cb2c09a..f5b08366 100644 --- a/src/modals/publication/AddPublicationModal.vue +++ b/src/modals/publication/AddPublicationModal.vue @@ -21,62 +21,6 @@ import { navigationStore, publicationStore } from '../../store/store.js'
- - - - - - - -

Published

- -
- -

Modified

- -
- - - - - - - Featured - - - - Juridisch - +
+ + + + + + + +

Published

+ +
+ +

Modified

+ +
+ + + Featured + + + + Juridisch + +
Date: Fri, 2 Aug 2024 15:13:22 +0200 Subject: [PATCH 07/65] Handle empty fields --- lib/Db/Publication.php | 9 +++++++-- lib/Db/PublicationMapper.php | 4 ++-- 2 files changed, 9 insertions(+), 4 deletions(-) diff --git a/lib/Db/Publication.php b/lib/Db/Publication.php index 076cbd78..c7d762e6 100644 --- a/lib/Db/Publication.php +++ b/lib/Db/Publication.php @@ -72,10 +72,13 @@ public function hydrate(array $object): self $jsonFields = $this->getJsonFields(); $this->setStatus('concept'); + $this->setAttachments(null); + $this->setOrganization(null); + $this->setData(null); foreach($object as $key => $value) { if (in_array($key, $jsonFields) === true && $value === []) { - $value = null; + $value = []; } $method = 'set'.ucfirst($key); @@ -87,7 +90,9 @@ public function hydrate(array $object): self } } - $this->setAttachmentCount(0); + $this->setSchema($this->getMetaData()); + + $this->setAttachmentCount('0'); if($this->attachments !== null) { $this->setAttachmentCount(count($this->getAttachments())); } diff --git a/lib/Db/PublicationMapper.php b/lib/Db/PublicationMapper.php index 68378e74..c834dfbf 100644 --- a/lib/Db/PublicationMapper.php +++ b/lib/Db/PublicationMapper.php @@ -63,8 +63,8 @@ public function createFromArray(array $object): Publication public function updateFromArray(int $id, array $object): Publication { - $publication = $this->find($id); - $publication->hydrate($object); + $publication = $this->find(id: $id); + $publication->hydrate(object: $object); return $this->update($publication); } From 56ab381b7ba6fff84b8ce3319f49cb234ea7e87b Mon Sep 17 00:00:00 2001 From: Thijn Date: Fri, 2 Aug 2024 16:24:28 +0200 Subject: [PATCH 08/65] removed fields from edit modal --- src/modals/publication/EditPublicationModal.vue | 16 ---------------- 1 file changed, 16 deletions(-) diff --git a/src/modals/publication/EditPublicationModal.vue b/src/modals/publication/EditPublicationModal.vue index 0addd0e0..f8487000 100644 --- a/src/modals/publication/EditPublicationModal.vue +++ b/src/modals/publication/EditPublicationModal.vue @@ -50,16 +50,6 @@ import { navigationStore, publicationStore } from '../../store/store.js' :disabled="loading" label="Modified" /> - - - -

Featured

Date: Fri, 2 Aug 2024 17:00:37 +0200 Subject: [PATCH 09/65] Validate elastic config --- lib/Controller/SearchController.php | 27 +++++++++++++++++++++++++++ 1 file changed, 27 insertions(+) diff --git a/lib/Controller/SearchController.php b/lib/Controller/SearchController.php index 47bf9253..eb5572bc 100644 --- a/lib/Controller/SearchController.php +++ b/lib/Controller/SearchController.php @@ -72,6 +72,20 @@ public function index(SearchService $searchService): JSONResponse $filters = array_combine(keys: $keys, values: $values); + $requiredElasticConfig = ['location', 'key', 'index']; + $missingFields = null; + foreach ($requiredElasticConfig as $key) { + if (isset($elasticConfig[$key]) === false || empty($elasticConfig[$key])) { + $missingFields .= "$key, "; + } + } + + if ($missingFields !== null) { + $errorMessage = "Missing the following elastic configuration: {$missingFields}please update your elastic connection in application settings."; + $response = new JSONResponse(data: ['code' => 403, 'message' => $errorMessage], statusCode: 403); + + return $response; + } $data = $searchService->search(parameters: $filters, elasticConfig: $elasticConfig, dbConfig: $dbConfig); @@ -94,6 +108,19 @@ public function show(string|int $id, SearchService $searchService): JSONResponse $filters = ['_id' => (string) $id]; + $requiredElasticConfig = ['location', 'key', 'index']; + $missingFields = null; + foreach ($requiredElasticConfig as $key) { + if (isset($elasticConfig[$key]) === false) { + $missingFields .= "$key "; + } + } + + if ($missingFields !== null) { + $errorMessage = "Missing the following elastic configuration: {$missingFields}please update your elastic connection in application settings."; + return new JSONResponse(['message' => $errorMessage], 403); + } + $data = $searchService->search(parameters: $filters, elasticConfig: $elasticConfig, dbConfig: $dbConfig); if(count($data['results']) > 0) { From b4aff181b7d57ebc7c81c49239c4e8e2f9adf65a Mon Sep 17 00:00:00 2001 From: Barry Brands Date: Fri, 2 Aug 2024 17:11:30 +0200 Subject: [PATCH 10/65] Search federatief improvements --- src/sidebars/dashboard/DashboardSideBar.vue | 9 +++++--- src/sidebars/search/SearchSideBar.vue | 10 ++++++--- src/store/modules/search.js | 12 +++++++++- src/views/dashboard/DashboardIndex.vue | 25 +++++++++++++++++++++ 4 files changed, 49 insertions(+), 7 deletions(-) diff --git a/src/sidebars/dashboard/DashboardSideBar.vue b/src/sidebars/dashboard/DashboardSideBar.vue index d8855344..481c7db9 100644 --- a/src/sidebars/dashboard/DashboardSideBar.vue +++ b/src/sidebars/dashboard/DashboardSideBar.vue @@ -9,11 +9,14 @@ import { navigationStore, searchStore, publicationStore } from '../../store/stor - Zoek snel in het voor uw beschickbare federatieve netwerk + test2 + Zoek snel in het voor uw beschikbare federatieve netwerk
+ label="Zoeken" /> + +

{{ searchStore.searchError }}

+
test1 + Zoek snel in het voor uw beschikbare federatieve netwerk
+ label="Zoeken" /> + +

{{ searchStore.searchError }}

+
+ + + + + - + + + + @@ -37,194 +42,28 @@ import { navigationStore, catalogiStore, publicationStore } from '../store/store - + - + - - + - - + - - - - -

- Hier kunt u de details instellen voor verschillende verbindingen. -

- - {{ t('forms', 'Gebruik externe opslag (bijv. MongoDb) in plaats van de interne opslag van Next Cloud.') }} - - - {{ t('forms', 'Gebruik VNG API\'s in plaats van MongoDB.') }} - -

- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
- DRC - Location - - Key - -
- ORC - Location - - Key - -
- Elastic - Location - - Key - - Index - -
- Mongo DB - Location - - Key - - Cluster name - -
-

- - - Opslaan - -
- -

- {{ configurationSuccess ? - 'Configuratie succesvol opgeslagen.' : - 'Opslaan van configuratie mislukt.' - }} -

-
-
-
- - - -

- Hier kunt u de details voor uw organisatie instellen. -

- - - - - - - - Opslaan - -
- -

- {{ configurationSuccess ? - 'Configuratie succesvol opgeslagen.' : - 'Opslaan van configuratie mislukt.' - }} -

-
-
-
-
+ @@ -236,26 +75,23 @@ import { NcAppNavigationItem, NcAppNavigationNew, NcAppNavigationSettings, - NcAppSettingsDialog, - NcAppSettingsSection, - NcButton, - NcTextField, - NcTextArea, - NcNoteCard, - NcCheckboxRadioSwitch, } from '@nextcloud/vue' -import Connection from 'vue-material-design-icons/Connection.vue' +// Configuration +import Configuration from './Configuration.vue' + +// Icons + import Plus from 'vue-material-design-icons/Plus.vue' import DatabaseEyeOutline from 'vue-material-design-icons/DatabaseEyeOutline.vue' import DatabaseCogOutline from 'vue-material-design-icons/DatabaseCogOutline.vue' import LayersSearchOutline from 'vue-material-design-icons/LayersSearchOutline.vue' import LayersOutline from 'vue-material-design-icons/LayersOutline.vue' import FileTreeOutline from 'vue-material-design-icons/FileTreeOutline.vue' -import CogOutline from 'vue-material-design-icons/CogOutline.vue' -import ContentSave from 'vue-material-design-icons/ContentSave.vue' import Finance from 'vue-material-design-icons/Finance.vue' -import HelpCircleOutline from 'vue-material-design-icons/HelpCircleOutline.vue' +import BookOpenVariantOutline from 'vue-material-design-icons/BookOpenVariantOutline.vue' +import OfficeBuildingOutline from 'vue-material-design-icons/OfficeBuildingOutline.vue' +import ShapeOutline from 'vue-material-design-icons/ShapeOutline.vue' export default { name: 'MainMenu', @@ -266,24 +102,18 @@ export default { NcAppNavigationItem, NcAppNavigationNew, NcAppNavigationSettings, - NcAppSettingsDialog, - NcAppSettingsSection, - NcTextField, - NcTextArea, - NcButton, - NcNoteCard, - NcCheckboxRadioSwitch, + Configuration, // icons Plus, - Connection, DatabaseEyeOutline, DatabaseCogOutline, LayersSearchOutline, LayersOutline, FileTreeOutline, - CogOutline, - ContentSave, Finance, + BookOpenVariantOutline, + OfficeBuildingOutline, + ShapeOutline, }, data() { return { @@ -322,70 +152,16 @@ export default { } }, mounted() { - this.fetchData() catalogiStore.refreshCatalogiList() }, methods: { - // We use the catalogi in the menu so lets fetch those - fetchData(newPage) { - this.loading = true - - fetch( - '/index.php/apps/opencatalogi/configuration', - { - method: 'GET', - }, - ) - .then((response) => { - response.json().then((data) => { - this.configuration = data - }) - }) - .catch((err) => { - console.error(err) - }) - }, - saveConfig() { - // Simple POST request with a JSON body using fetch - const requestOptions = { - method: 'POST', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify(this.configuration), - } - - const debounceNotification = (status) => { - this.configurationSuccess = status - - if (this.debounceTimeout) { - clearTimeout(this.debounceTimeout) - } - - this.debounceTimeout = setTimeout(() => { - this.feedbackPosition = undefined - this.configurationSuccess = -1 - }, 1500) - } - - fetch('/index.php/apps/opencatalogi/configuration', requestOptions) - .then((response) => { - debounceNotification(response.ok) - - response.json().then((data) => { - this.configuration = data - }) - }) - .catch((err) => { - debounceNotification(false) - console.error(err) - }) - }, switchCatalogus(catalogus) { if (catalogus.id !== navigationStore.selectedCatalogus) publicationStore.setPublicationItem(false) // for when you switch catalogus navigationStore.setSelected('publication') navigationStore.setSelectedCatalogus(catalogus.id) catalogiStore.setCatalogiItem(catalogus) }, - open(url, type) { + open(url, type = '') { window.open(url, type) }, }, diff --git a/src/store/modules/catalogi.js b/src/store/modules/catalogi.js index e2a0f84c..f293a01f 100644 --- a/src/store/modules/catalogi.js +++ b/src/store/modules/catalogi.js @@ -20,10 +20,10 @@ export const useCatalogiStore = defineStore('catalogi', { }, async refreshCatalogiList(search = null) { // @todo this might belong in a service? - let endpoint = '/index.php/apps/opencatalogi/api/catalogi'; - if (search !== null && search !== '') { - endpoint = endpoint + '?_search=' + search - } + let endpoint = '/index.php/apps/opencatalogi/api/catalogi' + if (search !== null && search !== '') { + endpoint = endpoint + '?_search=' + search + } return fetch(endpoint, { method: 'GET', }) diff --git a/src/store/modules/directory.js b/src/store/modules/directory.js index a2bfe30a..419f614d 100644 --- a/src/store/modules/directory.js +++ b/src/store/modules/directory.js @@ -20,10 +20,10 @@ export const useDirectoryStore = defineStore('directory', { }, async refreshListingList(search = null) { // @todo this might belong in a service? - let endpoint = '/index.php/apps/opencatalogi/api/directory' - if (search !== null && search !== '') { - endpoint = endpoint + '?_search=' + search - } + let endpoint = '/index.php/apps/opencatalogi/api/directory' + if (search !== null && search !== '') { + endpoint = endpoint + '?_search=' + search + } return fetch(endpoint, { method: 'GET', }) diff --git a/src/store/modules/metadata.js b/src/store/modules/metadata.js index b3cb85c0..89cf9e0b 100644 --- a/src/store/modules/metadata.js +++ b/src/store/modules/metadata.js @@ -25,12 +25,12 @@ export const useMetadataStore = defineStore('metadata', { ) console.log('Active metadata lest set') }, - async refreshMetaDataList(search = null) { - // @todo this might belong in a service? - let endpoint = '/index.php/apps/opencatalogi/api/metadata' - if (search !== null && search !== '') { - endpoint = endpoint + '?_search=' + search - } + async refreshMetaDataList(search = null) { + // @todo this might belong in a service? + let endpoint = '/index.php/apps/opencatalogi/api/metadata' + if (search !== null && search !== '') { + endpoint = endpoint + '?_search=' + search + } return fetch( endpoint, { diff --git a/src/store/modules/publication.js b/src/store/modules/publication.js index 32a5ae6c..4c5226e7 100644 --- a/src/store/modules/publication.js +++ b/src/store/modules/publication.js @@ -22,12 +22,12 @@ export const usePublicationStore = defineStore('publication', { this.publicationList = publicationList.map((publicationItem) => new Publication(publicationItem)) console.log('Active publication item set to ' + publicationList.length) }, - async refreshPublicationList(search = null) { - // @todo this might belong in a service? - let endpoint = '/index.php/apps/opencatalogi/api/publications' - if (search !== null && search !== '') { - endpoint = endpoint + '?_search=' + search - } + async refreshPublicationList(search = null) { + // @todo this might belong in a service? + let endpoint = '/index.php/apps/opencatalogi/api/publications' + if (search !== null && search !== '') { + endpoint = endpoint + '?_search=' + search + } return fetch( endpoint, { diff --git a/src/views/catalogi/CatalogiDetails.vue b/src/views/catalogi/CatalogiDetails.vue index f77b12eb..5b67dd24 100644 --- a/src/views/catalogi/CatalogiDetails.vue +++ b/src/views/catalogi/CatalogiDetails.vue @@ -8,43 +8,46 @@ import { catalogiStore, navigationStore } from '../../store/store.js'

{{ catalogi.title }}

-
- - - - - - + + + + + + Help + + + + Bewerken + + + + Catalogus bekijken + + - - - Bewerken - - - - Catalogus bekijken - - - - Verwijderen - - -
+ Verwijderen + + {{ catalogi.summary }}
@@ -68,7 +71,6 @@ import { NcActions, NcActionButton, NcLoadingIcon, - NcButton, } from '@nextcloud/vue' import { BTabs, BTab } from 'bootstrap-vue' @@ -139,6 +141,9 @@ export default { this.loading = false }) }, + open(url, type = '') { + window.open(url, type) + }, }, } diff --git a/src/views/catalogi/CatalogiList.vue b/src/views/catalogi/CatalogiList.vue index 3b23bc60..8a158338 100644 --- a/src/views/catalogi/CatalogiList.vue +++ b/src/views/catalogi/CatalogiList.vue @@ -108,6 +108,10 @@ export default { Pencil, Delete, }, + beforeRouteLeave(to, from, next) { + search = '' + next() + }, props: { search: { type: String, @@ -118,7 +122,6 @@ export default { return { loading: false, catalogi: [], - search: '', } }, watch: { @@ -143,10 +146,6 @@ export default { this.fetchData(search) }, 500), }, - beforeRouteLeave(to, from, next) { - search = '' - next() - }, } diff --git a/src/views/organizations/OrganizationIndex.vue b/src/views/organizations/OrganizationIndex.vue new file mode 100644 index 00000000..4cb314e1 --- /dev/null +++ b/src/views/organizations/OrganizationIndex.vue @@ -0,0 +1,51 @@ + + + + + diff --git a/src/views/organizations/OrganizationList.vue b/src/views/organizations/OrganizationList.vue new file mode 100644 index 00000000..a68c5b09 --- /dev/null +++ b/src/views/organizations/OrganizationList.vue @@ -0,0 +1,233 @@ + + + + + diff --git a/templates/OrganizationsIndex.php b/templates/OrganizationsIndex.php new file mode 100644 index 00000000..77531997 --- /dev/null +++ b/templates/OrganizationsIndex.php @@ -0,0 +1,9 @@ + +
\ No newline at end of file From c1d1d16eacf0f53f0cfc3a407c559281b8ac1f57 Mon Sep 17 00:00:00 2001 From: Barry Brands Date: Mon, 5 Aug 2024 19:26:20 +0200 Subject: [PATCH 25/65] Added local search to SearchController --- lib/Controller/SearchController.php | 19 ++++++- lib/Service/SearchService.php | 85 +++++++++++++++++++++++++++++ 2 files changed, 103 insertions(+), 1 deletion(-) diff --git a/lib/Controller/SearchController.php b/lib/Controller/SearchController.php index eb5572bc..7fc12b75 100644 --- a/lib/Controller/SearchController.php +++ b/lib/Controller/SearchController.php @@ -3,6 +3,7 @@ namespace OCA\OpenCatalogi\Controller; use OCA\OpenCatalogi\Service\ElasticSearchService; +use OCA\OpenCatalogi\Db\PublicationMapper; use OCA\OpenCatalogi\Service\SearchService; use OCP\AppFramework\Controller; use OCP\AppFramework\Http\TemplateResponse; @@ -25,7 +26,11 @@ class SearchController extends Controller ] ]; - public function __construct($appName, IRequest $request, private readonly IAppConfig $config) + public function __construct( + $appName, + IRequest $request, + private readonly PublicationMapper $publicationMapper, + private readonly IAppConfig $config) { parent::__construct($appName, $request); } @@ -64,6 +69,18 @@ public function index(SearchService $searchService): JSONResponse $filters = $this->request->getParams(); unset($filters['_route']); + $fieldsToSearch = ['title', 'description', 'summary']; + + if($this->config->hasKey($this->appName, 'mongoStorage') === false + || $this->config->getValueString($this->appName, 'mongoStorage') !== '1' + ) { + $searchParams = $searchService->createMySQLSearchParams($filters, $fieldsToSearch); + $searchConditions = $searchService->createMySQLSearchConditions($filters, $fieldsToSearch); + $filters = $searchService->unsetSpecialQueryParams($filters); + + return new JSONResponse(['results' => $this->publicationMapper->findAll(filters: $filters, searchParams: $searchParams, searchConditions: $searchConditions)]); + } + //@TODO: find a better way to get query params. This fixes it for now. $keys = array_keys(array: $filters); $values = array_values(array: $filters); diff --git a/lib/Service/SearchService.php b/lib/Service/SearchService.php index 4d0ca09f..08817ffa 100644 --- a/lib/Service/SearchService.php +++ b/lib/Service/SearchService.php @@ -174,6 +174,91 @@ private function recursiveRequestQueryKey(array &$vars, string $name, string $na }//end recursiveRequestQueryKey() + /** + * This function creates a mongodb filter array. + * + * Also unsets _search in filters ! + * + * @param array $filters Query parameters from request. + * @param array $fieldsToSearch Database field names to filter/search on. + * + * @return array $filters + */ + public function createMongoDBSearchFilter(array $filters, array $fieldsToSearch): array + { + if (isset($filters['_search']) === true) { + $searchRegex = ['$regex' => $filters['_search'], '$options' => 'i']; + $filters['$or'] = []; + + foreach ($fieldsToSearch as $field) { + $filters['$or'][] = [$field => $searchRegex]; + } + + unset($filters['_search']); + } + + return $filters; + + }//end createMongoDBSearchFilter() + + /** + * This function creates mysql search conditions based on given filters from request. + * + * @param array $filters Query parameters from request. + * @param array $fieldsToSearch Fields to search on in sql. + * + * @return array $searchConditions + */ + public function createMySQLSearchConditions(array $filters, array $fieldsToSearch): array + { + $searchConditions = []; + if (isset($filters['_search']) === true) { + foreach ($fieldsToSearch as $field) { + $searchConditions[] = "LOWER($field) LIKE :search"; + } + } + + return $searchConditions; + + }//end createMongoDBSearchFilter() + + /** + * This function unsets all keys starting with _ from filters. + * + * @param array $filters Query parameters from request. + * + * @return array $filters + */ + public function unsetSpecialQueryParams(array $filters): array + { + foreach ($filters as $key => $value) { + if (str_starts_with($key, '_')) { + unset($filters[$key]); + } + } + + return $filters; + + }//end createMongoDBSearchFilter() + + /** + * This function creates mysql search parameters based on given filters from request. + * + * @param array $filters Query parameters from request. + * + * @return array $searchParams + */ + public function createMySQLSearchParams(array $filters): array + { + $searchParams = []; + if (isset($filters['_search']) === true) { + $searchParams['search'] = '%' . strtolower($filters['_search']) . '%'; + } + + return $searchParams; + + }//end createMongoDBSearchFilter() + /** * Parses the request query string and returns it as an array of queries. * From a708afd91947cdfd2b7651bceb87183a35beaedd Mon Sep 17 00:00:00 2001 From: Barry Brands Date: Mon, 5 Aug 2024 19:26:35 +0200 Subject: [PATCH 26/65] Cleanup/refactor --- lib/Controller/CatalogiController.php | 39 +++++---------------- lib/Controller/DirectoryController.php | 39 +++++---------------- lib/Controller/MetaDataController.php | 37 +++++-------------- lib/Controller/PublicationsController.php | 39 +++++---------------- src/sidebars/dashboard/DashboardSideBar.vue | 2 +- src/sidebars/search/SearchSideBar.vue | 2 +- 6 files changed, 37 insertions(+), 121 deletions(-) diff --git a/lib/Controller/CatalogiController.php b/lib/Controller/CatalogiController.php index 299c23e7..5f5b1e5e 100644 --- a/lib/Controller/CatalogiController.php +++ b/lib/Controller/CatalogiController.php @@ -4,6 +4,7 @@ use OCA\OpenCatalogi\Db\CatalogMapper; use OCA\OpenCatalogi\Service\ObjectService; +use OCA\OpenCatalogi\Service\SearchService; use OCP\AppFramework\Controller; use OCP\AppFramework\Http\TemplateResponse; use OCP\AppFramework\Http\JSONResponse; @@ -35,46 +36,24 @@ public function page(?string $getParameter): TemplateResponse * @NoAdminRequired * @NoCSRFRequired */ - public function index(ObjectService $objectService): JSONResponse + public function index(ObjectService $objectService, SearchService $searchService): JSONResponse { $filters = $this->request->getParams(); - - $searchConditions = []; - $searchParams = []; $fieldsToSearch = ['title', 'description', 'summary']; - foreach ($filters as $key => $value) { - if ($key === '_search') { - // MongoDB - $searchRegex = ['$regex' => $value, '$options' => 'i']; - $filters['$or'] = []; - - // MySQL - $searchParams['search'] = '%' . strtolower($value) . '%'; - - foreach ($fieldsToSearch as $field) { - // MongoDB - $filters['$or'][] = [$field => $searchRegex]; - - // MySQL - $searchConditions[] = "LOWER($field) LIKE :search"; - } - } - - if (str_starts_with($key, '_')) { - unset($filters[$key]); - } - } - if($this->config->hasKey($this->appName, 'mongoStorage') === false || $this->config->getValueString($this->appName, 'mongoStorage') !== '1' ) { - // Unset mongodb filter - unset($filters['$or']); + $searchParams = $searchService->createMySQLSearchParams($filters, $fieldsToSearch); + $searchConditions = $searchService->createMySQLSearchConditions($filters, $fieldsToSearch); + $filters = $searchService->unsetSpecialQueryParams($filters); return new JSONResponse(['results' => $this->catalogMapper->findAll(filters: $filters, searchParams: $searchParams, searchConditions: $searchConditions)]); } - + + $filters = $searchService->createMongoDBSearchFilter($filters, $fieldsToSearch); + $filters = $searchService->unsetSpecialQueryParams($filters, $fieldsToSearch); + try { $dbConfig = [ 'base_uri' => $this->config->getValueString($this->appName, 'mongodbLocation'), diff --git a/lib/Controller/DirectoryController.php b/lib/Controller/DirectoryController.php index 2e60a53a..556fe126 100644 --- a/lib/Controller/DirectoryController.php +++ b/lib/Controller/DirectoryController.php @@ -5,6 +5,7 @@ use OCA\OpenCatalogi\Db\ListingMapper; use OCA\OpenCatalogi\Service\DirectoryService; use OCA\OpenCatalogi\Service\ObjectService; +use OCA\OpenCatalogi\Service\SearchService; use OCP\AppFramework\Controller; use OCP\AppFramework\Http\TemplateResponse; use OCP\AppFramework\Http\JSONResponse; @@ -82,46 +83,24 @@ public function add(?string $url, DirectoryService $directoryService): JSONRespo * @PublicPage * @NoCSRFRequired */ - public function index(ObjectService $objectService): JSONResponse + public function index(ObjectService $objectService, SearchService $searchService): JSONResponse { $filters = $this->request->getParams(); - - $searchConditions = []; - $searchParams = []; $fieldsToSearch = ['summary']; - foreach ($filters as $key => $value) { - if ($key === '_search') { - // MongoDB - $searchRegex = ['$regex' => $value, '$options' => 'i']; - $filters['$or'] = []; - - // MySQL - $searchParams['search'] = '%' . strtolower($value) . '%'; - - foreach ($fieldsToSearch as $field) { - // MongoDB - $filters['$or'][] = [$field => $searchRegex]; - - // MySQL - $searchConditions[] = "LOWER($field) LIKE :search"; - } - } - - if (str_starts_with($key, '_')) { - unset($filters[$key]); - } - } - if($this->config->hasKey($this->appName, 'mongoStorage') === false || $this->config->getValueString($this->appName, 'mongoStorage') !== '1' ) { - // Unset mongodb filter - unset($filters['$or']); + $searchParams = $searchService->createMySQLSearchParams($filters, $fieldsToSearch); + $searchConditions = $searchService->createMySQLSearchConditions($filters, $fieldsToSearch); + $filters = $searchService->unsetSpecialQueryParams($filters, $fieldsToSearch); return new JSONResponse(['results' => $this->listingMapper->findAll(filters: $filters, searchParams: $searchParams, searchConditions: $searchConditions)]); } - + + $filters = $searchService->createMongoDBSearchFilter($filters, $fieldsToSearch); + $filters = $searchService->unsetSpecialQueryParams($filters, $fieldsToSearch); + $dbConfig['base_uri'] = $this->config->getValueString(app: $this->appName, key: 'mongodbLocation'); $dbConfig['headers']['api-key'] = $this->config->getValueString(app: $this->appName, key: 'mongodbKey'); $dbConfig['mongodbCluster'] = $this->config->getValueString(app: $this->appName, key: 'mongodbCluster'); diff --git a/lib/Controller/MetaDataController.php b/lib/Controller/MetaDataController.php index 0001149b..b2f9cdf8 100644 --- a/lib/Controller/MetaDataController.php +++ b/lib/Controller/MetaDataController.php @@ -4,6 +4,7 @@ use OCA\OpenCatalogi\Db\MetaDataMapper; use OCA\OpenCatalogi\Service\ObjectService; +use OCA\OpenCatalogi\Service\SearchService; use OCP\AppFramework\Controller; use OCP\AppFramework\Http\TemplateResponse; use OCP\AppFramework\Http\JSONResponse; @@ -44,46 +45,24 @@ public function page(?string $getParameter) * @NoAdminRequired * @NoCSRFRequired */ - public function index(ObjectService $objectService): JSONResponse + public function index(ObjectService $objectService, SearchService $searchService): JSONResponse { $filters = $this->request->getParams(); - - $searchConditions = []; - $searchParams = []; $fieldsToSearch = ['title', 'description']; - foreach ($filters as $key => $value) { - if ($key === '_search') { - // MongoDB - $searchRegex = ['$regex' => $value, '$options' => 'i']; - $filters['$or'] = []; - - // MySQL - $searchParams['search'] = '%' . strtolower($value) . '%'; - - foreach ($fieldsToSearch as $field) { - // MongoDB - $filters['$or'][] = [$field => $searchRegex]; - - // MySQL - $searchConditions[] = "LOWER($field) LIKE :search"; - } - } - - if (str_starts_with($key, '_')) { - unset($filters[$key]); - } - } - if($this->config->hasKey($this->appName, 'mongoStorage') === false || $this->config->getValueString($this->appName, 'mongoStorage') !== '1' ) { - // Unset mongodb filter - unset($filters['$or']); + $searchParams = $searchService->createMySQLSearchParams($filters, $fieldsToSearch); + $searchConditions = $searchService->createMySQLSearchConditions($filters, $fieldsToSearch); + $filters = $searchService->unsetSpecialQueryParams($filters, $fieldsToSearch); return new JSONResponse(['results' =>$this->metaDataMapper->findAll(filters: $filters, searchParams: $searchParams, searchConditions: $searchConditions)]); } + $filters = $searchService->createMongoDBSearchFilter($filters, $fieldsToSearch); + $filters = $searchService->unsetSpecialQueryParams($filters, $fieldsToSearch); + $dbConfig['base_uri'] = $this->config->getValueString(app: $this->appName, key: 'mongodbLocation'); $dbConfig['headers']['api-key'] = $this->config->getValueString(app: $this->appName, key: 'mongodbKey'); $dbConfig['mongodbCluster'] = $this->config->getValueString(app: $this->appName, key: 'mongodbCluster'); diff --git a/lib/Controller/PublicationsController.php b/lib/Controller/PublicationsController.php index 9095c371..05f9dd67 100644 --- a/lib/Controller/PublicationsController.php +++ b/lib/Controller/PublicationsController.php @@ -8,6 +8,7 @@ use OCA\OpenCatalogi\Db\PublicationMapper; use OCA\OpenCatalogi\Service\ElasticSearchService; use OCA\OpenCatalogi\Service\ObjectService; +use OCA\OpenCatalogi\Service\SearchService; use OCP\AppFramework\Controller; use OCP\AppFramework\Http\TemplateResponse; use OCP\AppFramework\Http\JSONResponse; @@ -110,50 +111,28 @@ public function catalog(string|int $id): TemplateResponse * @NoAdminRequired * @NoCSRFRequired */ - public function index(ObjectService $objectService): JSONResponse + public function index(ObjectService $objectService, SearchService $searchService): JSONResponse { $filters = $this->request->getParams(); - - $searchConditions = []; - $searchParams = []; $fieldsToSearch = ['title', 'description', 'summary']; - foreach ($filters as $key => $value) { - if ($key === '_search') { - // MongoDB - $searchRegex = ['$regex' => $value, '$options' => 'i']; - $filters['$or'] = []; - - // MySQL - $searchParams['search'] = '%' . strtolower($value) . '%'; - - foreach ($fieldsToSearch as $field) { - // MongoDB - $filters['$or'][] = [$field => $searchRegex]; - - // MySQL - $searchConditions[] = "LOWER($field) LIKE :search"; - } - } - - if (str_starts_with($key, '_')) { - unset($filters[$key]); - } - } - if($this->config->hasKey($this->appName, 'mongoStorage') === false || $this->config->getValueString($this->appName, 'mongoStorage') !== '1' ) { - // Unset mongodb filter - unset($filters['$or']); + $searchParams = $searchService->createMySQLSearchParams($filters, $fieldsToSearch); + $searchConditions = $searchService->createMySQLSearchConditions($filters, $fieldsToSearch); + $filters = $searchService->unsetSpecialQueryParams($filters, $fieldsToSearch); return new JSONResponse(['results' => $this->publicationMapper->findAll(filters: $filters, searchParams: $searchParams, searchConditions: $searchConditions)]); } + $filters = $searchService->createMongoDBSearchFilter($filters, $fieldsToSearch); + $filters = $searchService->unsetSpecialQueryParams($filters, $fieldsToSearch); + $dbConfig['base_uri'] = $this->config->getValueString(app: $this->appName, key: 'mongodbLocation'); $dbConfig['headers']['api-key'] = $this->config->getValueString(app: $this->appName, key: 'mongodbKey'); $dbConfig['mongodbCluster'] = $this->config->getValueString(app: $this->appName, key: 'mongodbCluster'); - + $filters['_schema'] = 'publication'; $result = $objectService->findObjects(filters: $filters, config: $dbConfig); diff --git a/src/sidebars/dashboard/DashboardSideBar.vue b/src/sidebars/dashboard/DashboardSideBar.vue index 481c7db9..95dbcf3c 100644 --- a/src/sidebars/dashboard/DashboardSideBar.vue +++ b/src/sidebars/dashboard/DashboardSideBar.vue @@ -9,7 +9,7 @@ import { navigationStore, searchStore, publicationStore } from '../../store/stor test2 + Zoek snel in het voor uw beschikbare federatieve netwerk
test1 + Zoek snel in het voor uw beschikbare federatieve netwerk
Date: Mon, 5 Aug 2024 21:48:26 +0200 Subject: [PATCH 27/65] Themes meegenomen en controller voor het gemak even omgezet naar mock data --- appinfo/routes.php | 1 + 1 file changed, 1 insertion(+) diff --git a/appinfo/routes.php b/appinfo/routes.php index 1cb90aca..dfbe2df9 100644 --- a/appinfo/routes.php +++ b/appinfo/routes.php @@ -5,6 +5,7 @@ 'metadata' => ['url' => '/api/metadata'], 'publications' => ['url' => '/api/publications'], 'organizations' => ['url' => '/api/organizations'], + 'themes' => ['url' => '/api/themes'], 'attachments' => ['url' => '/api/attachments'], 'catalogi' => ['url' => '/api/catalogi'], 'directory' => ['url' => '/api/directory'] From 2132aa1e13b1dd367cbd0733861708b69130d748 Mon Sep 17 00:00:00 2001 From: Ruben van der Linde Date: Mon, 5 Aug 2024 22:37:48 +0200 Subject: [PATCH 28/65] A bit of entity managment --- lib/Controller/OrganizationsController.php | 374 +++-------- lib/Controller/ThemesController.php | 143 +++++ src/entities/organisation/index.js | 2 + .../organisation/organisation.spec.ts | 68 ++ src/entities/organisation/organisation.ts | 46 ++ .../organisation/organisation.types.ts | 10 + src/entities/theme/index.js | 2 + src/entities/theme/theme.spec.ts | 59 ++ src/entities/theme/theme.ts | 37 ++ src/entities/theme/theme.types.ts | 7 + src/store/modules/configuration.js | 114 ++++ src/store/modules/configuration.specs.js | 220 +++++++ src/store/modules/theme.js | 114 ++++ src/store/modules/theme.spec copy.js | 220 +++++++ src/store/store.js | 6 + src/views/Views.vue | 7 + src/views/organizations/OrganizationIndex.vue | 27 +- src/views/themes/ThemeDetail.vue | 592 ++++++++++++++++++ src/views/themes/ThemeIndex.vue | 52 ++ src/views/themes/ThemeList.vue | 233 +++++++ 20 files changed, 2031 insertions(+), 302 deletions(-) create mode 100644 lib/Controller/ThemesController.php create mode 100644 src/entities/organisation/index.js create mode 100644 src/entities/organisation/organisation.spec.ts create mode 100644 src/entities/organisation/organisation.ts create mode 100644 src/entities/organisation/organisation.types.ts create mode 100644 src/entities/theme/index.js create mode 100644 src/entities/theme/theme.spec.ts create mode 100644 src/entities/theme/theme.ts create mode 100644 src/entities/theme/theme.types.ts create mode 100644 src/store/modules/configuration.js create mode 100644 src/store/modules/configuration.specs.js create mode 100644 src/store/modules/theme.js create mode 100644 src/store/modules/theme.spec copy.js create mode 100644 src/views/themes/ThemeDetail.vue create mode 100644 src/views/themes/ThemeIndex.vue create mode 100644 src/views/themes/ThemeList.vue diff --git a/lib/Controller/OrganizationsController.php b/lib/Controller/OrganizationsController.php index 357ff731..e36fa64e 100644 --- a/lib/Controller/OrganizationsController.php +++ b/lib/Controller/OrganizationsController.php @@ -41,307 +41,103 @@ public function __construct parent::__construct($appName, $request); } - private function insertNestedObjects(array $object, ObjectService $objectService, array $config): array - { - //@TODO keep in mind that unpublished objects should not be inserted, and that objects should be updated if a subobject is updated. - foreach($object as $key => $value) { - try { - if( - is_string(value: $value) - && $key !== 'id' - && Uuid::isValid(uuid: $value) === true - && $subObject = $objectService->findObject(filters: ['_id' => $value], config: $config) - ) { - $object[$key] = $subObject; - } - - if( - is_array(value: $value) === true - && array_is_list(array: $value) === true - ) { - $object[$key] = $this->insertNestedObjects(object: $value, objectService: $objectService, config: $config); - } - } catch (GuzzleException $exception) { - continue; - } - } - - return $object; - } - - /** - * @NoAdminRequired - * @NoCSRFRequired - */ - public function page(?string $getParameter) - { - // The TemplateResponse loads the 'main.php' - // defined in our app's 'templates' folder. - // We pass the $getParameter variable to the template - // so that the value is accessible in the template. - return new TemplateResponse( - $this->appName, - 'OrganizationsIndex', - [] - ); - } - - /** - * Taking it from a catalogue point of view is just adding a filter - * - * @NoAdminRequired - * @NoCSRFRequired - */ - public function catalog(string|int $id): TemplateResponse - { - // The TemplateResponse loads the 'main.php' - // defined in our app's 'templates' folder. - // We pass the $getParameter variable to the template - // so that the value is accessible in the template. + * This returns the template of the main app's page + * It adds some data to the template (app version) + * + * @NoAdminRequired + * @NoCSRFRequired + * + * @return TemplateResponse + */ + public function page(): TemplateResponse + { return new TemplateResponse( - $this->appName, - 'OrganizationsIndex', + //Application::APP_ID, + 'zaakafhandelapp', + 'index', [] ); - } - - /** - * @NoAdminRequired - * @NoCSRFRequired - */ - public function index(ObjectService $objectService): JSONResponse - { - $filters = $this->request->getParams(); - - $searchConditions = []; - $searchParams = []; - $fieldsToSearch = ['title', 'description', 'summary']; - - foreach ($filters as $key => $value) { - if ($key === '_search') { - // MongoDB - $searchRegex = ['$regex' => $value, '$options' => 'i']; - $filters['$or'] = []; - - // MySQL - $searchParams['search'] = '%' . strtolower($value) . '%'; - - foreach ($fieldsToSearch as $field) { - // MongoDB - $filters['$or'][] = [$field => $searchRegex]; - - // MySQL - $searchConditions[] = "LOWER($field) LIKE :search"; - } - } - - if (str_starts_with($key, '_')) { - unset($filters[$key]); - } - } - - if($this->config->hasKey($this->appName, 'mongoStorage') === false - || $this->config->getValueString($this->appName, 'mongoStorage') !== '1' - ) { - // Unset mongodb filter - unset($filters['$or']); - - return new JSONResponse(['results' => $this->publicationMapper->findAll(filters: $filters, searchParams: $searchParams, searchConditions: $searchConditions)]); - } - - $dbConfig['base_uri'] = $this->config->getValueString(app: $this->appName, key: 'mongodbLocation'); - $dbConfig['headers']['api-key'] = $this->config->getValueString(app: $this->appName, key: 'mongodbKey'); - $dbConfig['mongodbCluster'] = $this->config->getValueString(app: $this->appName, key: 'mongodbCluster'); - - $filters['_schema'] = 'publication'; - - $result = $objectService->findObjects(filters: $filters, config: $dbConfig); - - $results = ["results" => $result['documents']]; - return new JSONResponse($results); - } - - /** - * @NoAdminRequired - * @NoCSRFRequired - */ - public function show(string|int $id, ObjectService $objectService): JSONResponse - { - if($this->config->hasKey($this->appName, 'mongoStorage') === false - || $this->config->getValueString($this->appName, 'mongoStorage') !== '1' - ) { - return new JSONResponse($this->publicationMapper->find(id: (int) $id)); - } - - $dbConfig['base_uri'] = $this->config->getValueString(app: $this->appName, key: 'mongodbLocation'); - $dbConfig['headers']['api-key'] = $this->config->getValueString(app: $this->appName, key: 'mongodbKey'); - $dbConfig['mongodbCluster'] = $this->config->getValueString(app: $this->appName, key: 'mongodbCluster'); - - $filters['_id'] = (string) $id; - - $result = $objectService->findObject(filters: $filters, config: $dbConfig); - - return new JSONResponse($result); - } - - - /** - * @NoAdminRequired - * @NoCSRFRequired - */ - public function create(ObjectService $objectService, ElasticSearchService $elasticSearchService): JSONResponse - { - $data = $this->request->getParams(); - - foreach($data as $key => $value) { - if(str_starts_with($key, '_')) { - unset($data[$key]); - } - } - - if($this->config->hasKey($this->appName, 'mongoStorage') === false - || $this->config->getValueString($this->appName, 'mongoStorage') !== '1' - ) { - $returnData = $this->publicationMapper->createFromArray($data); - $returnData = $returnData->jsonSerialize(); - $dbConfig = []; - } else { - $data['_schema'] = 'publication'; - - $dbConfig['base_uri'] = $this->config->getValueString(app: $this->appName, key: 'mongodbLocation'); - $dbConfig['headers']['api-key'] = $this->config->getValueString(app: $this->appName, key: 'mongodbKey'); - $dbConfig['mongodbCluster'] = $this->config->getValueString(app: $this->appName, key: 'mongodbCluster'); - $returnData = $objectService->saveObject( - data: $data, - config: $dbConfig - ); - } - if( - $this->config->hasKey(app: $this->appName, key: 'elasticLocation') === true - && $this->config->getValueString(app: $this->appName, key: 'elasticLocation') !== '' - && $this->config->hasKey(app: $this->appName, key: 'elasticKey') === true - && $this->config->getValueString(app: $this->appName, key: 'elasticKey') !== '' - && $this->config->hasKey(app: $this->appName, key: 'elasticIndex') === true - && $this->config->getValueString(app: $this->appName, key: 'elasticIndex') !== '' - && $returnData['status'] === 'published' - ) { - $elasticConfig['location'] = $this->config->getValueString(app: $this->appName, key: 'elasticLocation'); - $elasticConfig['key'] = $this->config->getValueString(app: $this->appName, key: 'elasticKey'); - $elasticConfig['index'] = $this->config->getValueString(app: $this->appName, key: 'elasticIndex'); - - $returnData = $this->insertNestedObjects($returnData, $objectService, $dbConfig); - - $returnData = $elasticSearchService->addObject(object: $returnData, config: $elasticConfig); - - } - // get post from requests - return new JSONResponse($returnData); - } - - /** - * @NoAdminRequired - * @NoCSRFRequired - */ - public function update(string|int $id, ObjectService $objectService, ElasticSearchService $elasticSearchService): JSONResponse - { - - $data = $this->request->getParams(); - - foreach($data as $key => $value) { - if(str_starts_with($key, '_')) { - unset($data[$key]); - } - } - if (isset($data['id'])) { - unset( $data['id']); - } - - if($this->config->hasKey($this->appName, 'mongoStorage') === false - || $this->config->getValueString($this->appName, 'mongoStorage') !== '1' - ) { - $returnData = $this->publicationMapper->updateFromArray(id: (int) $id, object: $data); - $returnData = $returnData->jsonSerialize(); - - $dbConfig = []; - } else { - $dbConfig['base_uri'] = $this->config->getValueString(app: $this->appName, key: 'mongodbLocation'); - $dbConfig['headers']['api-key'] = $this->config->getValueString(app: $this->appName, key: 'mongodbKey'); - $dbConfig['mongodbCluster'] = $this->config->getValueString(app: $this->appName, key: 'mongodbCluster'); + } - $filters['_id'] = (string) $id; - $returnData = $objectService->updateObject( - filters: $filters, - update: $data, - config: $dbConfig - ); - } + /** + * Return (and serach) all objects + * + * @NoAdminRequired + * @NoCSRFRequired + * + * @return JSONResponse + */ + public function index(CallService $callService): JSONResponse + { + // Latere zorg + $query= $this->request->getParams(); - if( - $this->config->hasKey(app: $this->appName, key: 'elasticLocation') === true - && $this->config->getValueString(app: $this->appName, key: 'elasticLocation') !== '' - && $this->config->hasKey(app: $this->appName, key: 'elasticKey') === true - && $this->config->getValueString(app: $this->appName, key: 'elasticKey') !== '' - && $this->config->hasKey(app: $this->appName, key: 'elasticIndex') === true - && $this->config->getValueString(app: $this->appName, key: 'elasticIndex') !== '' - && $returnData['status'] === 'published' - ) { - $elasticConfig['location'] = $this->config->getValueString(app: $this->appName, key: 'elasticLocation'); - $elasticConfig['key'] = $this->config->getValueString(app: $this->appName, key: 'elasticKey'); - $elasticConfig['index'] = $this->config->getValueString(app: $this->appName, key: 'elasticIndex'); + $results = $callService->index(source: 'brc', endpoint: 'besluiten'); + return new JSONResponse($results); + } - $returnData = $this->insertNestedObjects($returnData, $objectService, $dbConfig); + /** + * Read a single object + * + * @NoAdminRequired + * @NoCSRFRequired + * + * @return JSONResponse + */ + public function show(string $id, CallService $callService): JSONResponse + { + // Latere zorg + $query= $this->request->getParams(); - $returnData = $elasticSearchService->updateObject(id: $id, object: $returnData, config: $elasticConfig); + $results = $callService->show(source: 'brc', endpoint: 'besluiten', id: $id); + return new JSONResponse($results); + } - } + /** + * Creatue an object + * + * @NoAdminRequired + * @NoCSRFRequired + * + * @return JSONResponse + */ + public function create(CallService $callService): JSONResponse + { // get post from requests - return new JSONResponse($returnData); - } - - /** - * @NoAdminRequired - * @NoCSRFRequired - */ - public function destroy(string|int $id, ObjectService $objectService, ElasticSearchService $elasticSearchService): JSONResponse - { - if($this->config->hasKey($this->appName, 'mongoStorage') === false - || $this->config->getValueString($this->appName, 'mongoStorage') !== '1' - ) { - $this->publicationMapper->delete($this->publicationMapper->find(id: (int) $id)); - - $returnData = []; - } else { - $dbConfig['base_uri'] = $this->config->getValueString(app: $this->appName, key: 'mongodbLocation'); - $dbConfig['headers']['api-key'] = $this->config->getValueString(app: $this->appName, key: 'mongodbKey'); - $dbConfig['mongodbCluster'] = $this->config->getValueString(app: $this->appName, key: 'mongodbCluster'); - - $filters['_id'] = (string) $id; - $returnData = $objectService->deleteObject( - filters: $filters, - config: $dbConfig - ); - } - - if( - $this->config->hasKey(app: $this->appName, key: 'elasticLocation') === true - && $this->config->getValueString(app: $this->appName, key: 'elasticLocation') !== '' - && $this->config->hasKey(app: $this->appName, key: 'elasticKey') === true - && $this->config->getValueString(app: $this->appName, key: 'elasticKey') !== '' - && $this->config->hasKey(app: $this->appName, key: 'elasticIndex') === true - && $this->config->getValueString(app: $this->appName, key: 'elasticIndex') !== '' - && $returnData['status'] === 'published' - ) { - $elasticConfig['location'] = $this->config->getValueString(app: $this->appName, key: 'elasticLocation'); - $elasticConfig['key'] = $this->config->getValueString(app: $this->appName, key: 'elasticKey'); - $elasticConfig['index'] = $this->config->getValueString(app: $this->appName, key: 'elasticIndex'); + $body = $this->request->getParams(); + $results = $callService->create(source: 'brc', endpoint: 'besluiten', data: $body); + return new JSONResponse($results); + } - $returnData = $elasticSearchService->removeObject(id: $id, config: $elasticConfig); + /** + * Update an object + * + * @NoAdminRequired + * @NoCSRFRequired + * + * @return JSONResponse + */ + public function update(string $id, CallService $callService): JSONResponse + { + $body = $this->request->getParams(); + $results = $callService->update(source: 'brc', endpoint: 'besluiten', data: $body, id: $id); + return new JSONResponse($results); + } - } + /** + * Delate an object + * + * @NoAdminRequired + * @NoCSRFRequired + * + * @return JSONResponse + */ + public function destroy(string $id, CallService $callService): JSONResponse + { + $callService->destroy(source: 'brc', endpoint: 'besluiten', id: $id); - // get post from requests - return new JSONResponse($returnData); - } + return new JsonResponse([]); + } } diff --git a/lib/Controller/ThemesController.php b/lib/Controller/ThemesController.php new file mode 100644 index 00000000..960aaeaa --- /dev/null +++ b/lib/Controller/ThemesController.php @@ -0,0 +1,143 @@ + [ + "id" => "354980e5-c967-4ba5-989b-65c2b0cd2ff4", + "name" => "Input voor OpenCatalogi", + "summary" => "Dit is een selectie van high-value datasets in DCAT-AP 2.0 standaard x" + ], + "2ab0011e-9b4c-4c50-a50d-a16fc0be0178" => [ + "id" => "2ab0011e-9b4c-4c50-a50d-a16fc0be0178", + "title" => "Publication two", + "description" => "summary for two" + ] + ]; + + public function __construct + ( + $appName, + IRequest $request, + private readonly PublicationMapper $publicationMapper, + private readonly IAppConfig $config, + ) + { + parent::__construct($appName, $request); + } + + /** + * This returns the template of the main app's page + * It adds some data to the template (app version) + * + * @NoAdminRequired + * @NoCSRFRequired + * + * @return TemplateResponse + */ + public function page(): TemplateResponse + { + return new TemplateResponse( + //Application::APP_ID, + 'zaakafhandelapp', + 'index', + [] + ); + } + + /** + * Return (and serach) all objects + * + * @NoAdminRequired + * @NoCSRFRequired + * + * @return JSONResponse + */ + public function index(CallService $callService): JSONResponse + { + // Latere zorg + $query= $this->request->getParams(); + + $results = $callService->index(source: 'brc', endpoint: 'besluiten'); + return new JSONResponse($results); + } + + /** + * Read a single object + * + * @NoAdminRequired + * @NoCSRFRequired + * + * @return JSONResponse + */ + public function show(string $id, CallService $callService): JSONResponse + { + // Latere zorg + $query= $this->request->getParams(); + + $results = $callService->show(source: 'brc', endpoint: 'besluiten', id: $id); + return new JSONResponse($results); + } + + + /** + * Creatue an object + * + * @NoAdminRequired + * @NoCSRFRequired + * + * @return JSONResponse + */ + public function create(CallService $callService): JSONResponse + { + // get post from requests + $body = $this->request->getParams(); + $results = $callService->create(source: 'brc', endpoint: 'besluiten', data: $body); + return new JSONResponse($results); + } + + /** + * Update an object + * + * @NoAdminRequired + * @NoCSRFRequired + * + * @return JSONResponse + */ + public function update(string $id, CallService $callService): JSONResponse + { + $body = $this->request->getParams(); + $results = $callService->update(source: 'brc', endpoint: 'besluiten', data: $body, id: $id); + return new JSONResponse($results); + } + + /** + * Delate an object + * + * @NoAdminRequired + * @NoCSRFRequired + * + * @return JSONResponse + */ + public function destroy(string $id, CallService $callService): JSONResponse + { + $callService->destroy(source: 'brc', endpoint: 'besluiten', id: $id); + + return new JsonResponse([]); + } +} diff --git a/src/entities/organisation/index.js b/src/entities/organisation/index.js new file mode 100644 index 00000000..17f72d4a --- /dev/null +++ b/src/entities/organisation/index.js @@ -0,0 +1,2 @@ +export * from './organisation.ts' +export * from './organisation.types.ts' diff --git a/src/entities/organisation/organisation.spec.ts b/src/entities/organisation/organisation.spec.ts new file mode 100644 index 00000000..695dcfc6 --- /dev/null +++ b/src/entities/organisation/organisation.spec.ts @@ -0,0 +1,68 @@ +/* eslint-disable no-console */ +import { Organisation } from './organisation' +import { TOrganisation } from './organisation.types' + +describe('Organisation Store', () => { + it('create Organisation entity with full data', () => { + const organisation = new Organisation(testData[0]) + + expect(organisation).toBeInstanceOf(Organisation) + expect(organisation).toEqual(testData[0]) + + expect(organisation.validate()).toBe(true) + }) + + it('create Organisation entity with partial data', () => { + const organisation = new Organisation(testData[1]) + + expect(organisation).toBeInstanceOf(Organisation) + expect(organisation.id).toBe(testData[1].id) + expect(organisation.title).toBe(testData[1].title) + expect(organisation.summary).toBe(testData[1].summary) + expect(organisation.description).toBe(testData[1].description) + expect(organisation.oin).toBe('') + expect(organisation.tooi).toBe('') + expect(organisation.rsin).toBe('') + expect(organisation.pki).toBe('') + + expect(organisation.validate()).toBe(true) + }) + + it('create Catalogi entity with falsy data', () => { + const organisation = new Organisation(testData[2]) + + expect(organisation).toBeInstanceOf(Organisation) + expect(organisation).toEqual(testData[2]) + + expect(organisation.validate()).toBe(false) + }) +}) + +const testData: TOrganisation[] = [ + { // full data + id: '1', + title: 'Decat', + summary: 'a short form summary', + description: 'a really really long description about this organisation', + oin: 'string', + tooi: 'string', + rsin: 'string', + pki: 'string', + }, + { // partial data + id: '2', + title: 'Woo', + summary: 'a short form summary', + description: 'a really really long description about this organisation', + }, + { // invalid data + id: '3', + title: '', + summary: 'a short form summary', + description: 'a really really long description about this organisation', + oin: 'string', + tooi: 'string', + rsin: 'string', + pki: 'string', + }, +] diff --git a/src/entities/organisation/organisation.ts b/src/entities/organisation/organisation.ts new file mode 100644 index 00000000..618bd832 --- /dev/null +++ b/src/entities/organisation/organisation.ts @@ -0,0 +1,46 @@ +import { TOrganisation } from './organisation.types' + +export class Organisation implements TOrganisation { + + public id: string + public title: string + public summary: string + public description?: string + public oin?: string + public tooi?: string + public rsin?: string + public pki?: string + + constructor(data: TOrganisation) { + this.hydrate(data) + } + + /* istanbul ignore next */ // Jest does not recognize the code coverage of these 2 methods + private hydrate(data: TOrganisation) { + this.id = data?.id?.toString() || '' + // @ts-expect-error data.name is not supposed to exist but you can still get it from the backend, so this is just backwards compatibility + this.title = data?.title || data?.name || '' + this.summary = data?.summary || '' + this.description = data?.description || '' + this.oin = data?.oin || '' + this.tooi = data?.tooi || '' + this.rsin = data?.rsin || '' + this.pki = data?.pki || '' + } + + /* istanbul ignore next */ + public validate(): boolean { + // these have to exist + if (!this.id || typeof this.id !== 'string') return false + if (!this.title || typeof this.title !== 'string') return false + if (!this.summary || typeof this.summary !== 'string') return false + // these can be optional + if (typeof this.description !== 'string') return false + if (typeof this.oin !== 'string') return false + if (typeof this.tooi !== 'string') return false + if (typeof this.rsin !== 'string') return false + if (typeof this.pki !== 'string') return false + return true + } + +} diff --git a/src/entities/organisation/organisation.types.ts b/src/entities/organisation/organisation.types.ts new file mode 100644 index 00000000..449aa687 --- /dev/null +++ b/src/entities/organisation/organisation.types.ts @@ -0,0 +1,10 @@ +export type TOrganisation = { + id: string + title: string + summary: string + description?: string + oin?: string + tooi?: string + rsin?: string + pki?: string +} diff --git a/src/entities/theme/index.js b/src/entities/theme/index.js new file mode 100644 index 00000000..d2f310fc --- /dev/null +++ b/src/entities/theme/index.js @@ -0,0 +1,2 @@ +export * from './theme.ts' +export * from './theme.types.ts' diff --git a/src/entities/theme/theme.spec.ts b/src/entities/theme/theme.spec.ts new file mode 100644 index 00000000..a59449dd --- /dev/null +++ b/src/entities/theme/theme.spec.ts @@ -0,0 +1,59 @@ +/* eslint-disable no-console */ +import { Theme } from './theme' +import { TTheme } from './theme.types' + +describe('Theme Store', () => { + it('create Theme entity with full data', () => { + const theme = new Theme(testData[0]) + + expect(theme).toBeInstanceOf(Theme) + expect(theme).toEqual(testData[0]) + + expect(theme.validate()).toBe(true) + }) + + it('create Theme entity with partial data', () => { + const theme = new Theme(testData[1]) + + expect(theme).toBeInstanceOf(Theme) + expect(theme.id).toBe(testData[1].id) + expect(theme.title).toBe(testData[1].title) + expect(theme.summary).toBe(testData[1].summary) + expect(theme.description).toBe(testData[1].description) + expect(theme.image).toBe('') + + expect(theme.validate()).toBe(true) + }) + + it('create Theme entity with falsy data', () => { + const theme = new Theme(testData[2]) + + expect(theme).toBeInstanceOf(Theme) + expect(theme).toEqual(testData[2]) + + expect(theme.validate()).toBe(false) + }) +}) + +const testData: TTheme[] = [ + { // full data + id: '1', + title: 'Decat', + summary: 'a short form summary', + description: 'a really really long description about this Theme', + image: 'string', + }, + { // partial data + id: '2', + title: 'Woo', + summary: 'a short form summary', + description: 'a really really long description about this Theme', + }, + { // invalid data + id: '3', + title: '', + summary: 'a short form summary', + description: 'a really really long description about this Theme', + image: 'string', + }, +] diff --git a/src/entities/theme/theme.ts b/src/entities/theme/theme.ts new file mode 100644 index 00000000..eac7f103 --- /dev/null +++ b/src/entities/theme/theme.ts @@ -0,0 +1,37 @@ +import { TTheme } from './theme.types' + +export class Theme implements TTheme { + + public id: string + public title: string + public summary: string + public description?: string + public image?: string + + constructor(data: TTheme) { + this.hydrate(data) + } + + /* istanbul ignore next */ // Jest does not recognize the code coverage of these 2 methods + private hydrate(data: TTheme) { + this.id = data?.id?.toString() || '' + // @ts-expect-error data.name is not supposed to exist but you can still get it from the backend, so this is just backwards compatibility + this.title = data?.title || data?.name || '' + this.summary = data?.summary || '' + this.description = data?.description || '' + this.image = data?.image || '' + } + + /* istanbul ignore next */ + public validate(): boolean { + // these have to exist + if (!this.id || typeof this.id !== 'string') return false + if (!this.title || typeof this.title !== 'string') return false + if (!this.summary || typeof this.summary !== 'string') return false + // these can be optional + if (typeof this.description !== 'string') return false + if (typeof this.image !== 'string') return false + return true + } + +} diff --git a/src/entities/theme/theme.types.ts b/src/entities/theme/theme.types.ts new file mode 100644 index 00000000..aa248935 --- /dev/null +++ b/src/entities/theme/theme.types.ts @@ -0,0 +1,7 @@ +export type TTheme = { + id: string + title: string + summary: string + description?: string + image?: string +} diff --git a/src/store/modules/configuration.js b/src/store/modules/configuration.js new file mode 100644 index 00000000..6901345a --- /dev/null +++ b/src/store/modules/configuration.js @@ -0,0 +1,114 @@ +/* eslint-disable no-console */ +import { Attachment, Publication } from '../../entities/index.js' +import { defineStore } from 'pinia' + +export const useOrganizationStore = defineStore('organization', { + state: () => ({ + publicationItem: false, + publicationList: [], + publicationDataKey: false, + attachmentItem: false, + publicationAttachments: [], + conceptPublications: [], + conceptAttachments: [], + }), + actions: { + setPublicationItem(publicationItem) { + // To prevent forms etc from braking we alway use a default/skeleton object + this.publicationItem = publicationItem && new Publication(publicationItem) + console.log('Active publication item set to ' + publicationItem && publicationItem.id) + }, + setPublicationList(publicationList) { + this.publicationList = publicationList.map((publicationItem) => new Publication(publicationItem)) + console.log('Active publication item set to ' + publicationList.length) + }, + async refreshPublicationList(search = null) { + // @todo this might belong in a service? + let endpoint = '/index.php/apps/opencatalogi/api/publications' + if (search !== null && search !== '') { + endpoint = endpoint + '?_search=' + search + } + return fetch( + endpoint, + { + method: 'GET', + }, + ) + .then((response) => { + response.json().then((data) => { + this.setPublicationList(data?.results) + return data + }) + }) + .catch((err) => { + console.error(err) + return err + }) + }, + getPublicationAttachments(publication) { // @todo this might belong in a service? + fetch( + '/index.php/apps/opencatalogi/api/attachments', + { + method: 'GET', + }, + ) + .then((response) => { + response.json().then((data) => { + this.publicationAttachments = data.results.map( + (attachmentItem) => new Attachment(attachmentItem), + ) + return data + }) + }) + .catch((err) => { + console.error(err) + return err + }) + }, + getConceptPublications() { // @todo this might belong in a service? + fetch( + '/index.php/apps/opencatalogi/api/publications?status=concept', + { + method: 'GET', + }, + ) + .then((response) => { + response.json().then((data) => { + this.conceptPublications = data + return data + }) + }) + .catch((err) => { + console.error(err) + return err + }) + }, + getConceptAttachments() { // @todo this might belong in a service? + fetch( + '/index.php/apps/opencatalogi/api/attachments?status=concept', + { + method: 'GET', + }, + ) + .then((response) => { + response.json().then((data) => { + this.conceptAttachments = data + return data + }) + }) + .catch((err) => { + console.error(err) + return err + }) + }, + // @todo why does the following run through the store? -- because its impossible with props, and its vital information for the modal. + setPublicationDataKey(publicationDataKey) { + this.publicationDataKey = publicationDataKey + console.log('Active publication data key set to ' + publicationDataKey) + }, + setAttachmentItem(attachmentItem) { + this.attachmentItem = attachmentItem && new Attachment(attachmentItem) + console.log('Active attachment item set to ' + attachmentItem) + }, + }, +}) diff --git a/src/store/modules/configuration.specs.js b/src/store/modules/configuration.specs.js new file mode 100644 index 00000000..8adee3ba --- /dev/null +++ b/src/store/modules/configuration.specs.js @@ -0,0 +1,220 @@ +/* eslint-disable no-console */ +import { setActivePinia, createPinia } from 'pinia' + +import { usePublicationStore } from './publication.js' +import { Attachment, Publication } from '../../entities/index.js' + +describe('Metadata Store', () => { + beforeEach(() => { + setActivePinia(createPinia()) + }) + + it('sets publication item correctly', () => { + const store = usePublicationStore() + + store.setPublicationItem(testData[0]) + + expect(store.publicationItem).toBeInstanceOf(Publication) + expect(store.publicationItem).toEqual(testData[0]) + expect(store.publicationItem.validate()).toBe(true) + + store.setPublicationItem(testData[1]) + + expect(store.publicationItem).toBeInstanceOf(Publication) + expect(store.publicationItem).not.toEqual(testData[1]) + expect(store.publicationItem.validate()).toBe(true) + + store.setPublicationItem(testData[2]) + + expect(store.publicationItem).toBeInstanceOf(Publication) + expect(store.publicationItem).toEqual(testData[2]) + expect(store.publicationItem.validate()).toBe(false) + }) + + it('sets publication list correctly', () => { + const store = usePublicationStore() + + store.setPublicationList(testData) + + expect(store.publicationList).toHaveLength(testData.length) + + expect(store.publicationList[0]).toBeInstanceOf(Publication) + expect(store.publicationList[0]).toEqual(testData[0]) + expect(store.publicationList[0].validate()).toBe(true) + + expect(store.publicationList[1]).toBeInstanceOf(Publication) + expect(store.publicationList[1]).not.toEqual(testData[1]) + expect(store.publicationList[1].validate()).toBe(true) + + expect(store.publicationList[2]).toBeInstanceOf(Publication) + expect(store.publicationList[2]).toEqual(testData[2]) + expect(store.publicationList[2].validate()).toBe(false) + }) + + // TODO: fix this + it('set publication data.data property key correctly', () => { + const store = usePublicationStore() + + store.setPublicationDataKey('contactPoint') + + expect(store.publicationDataKey).toBe('contactPoint') + }) + + it('set attachment item correctly', () => { + const store = usePublicationStore() + + store.setAttachmentItem(attachmentTestData[0]) + + expect(store.attachmentItem).toBeInstanceOf(Attachment) + expect(store.attachmentItem).toEqual(attachmentTestData[0]) + expect(store.attachmentItem.validate()).toBe(true) + + store.setAttachmentItem(attachmentTestData[1]) + + expect(store.attachmentItem).toBeInstanceOf(Attachment) + expect(store.attachmentItem).not.toEqual(attachmentTestData[1]) + expect(store.attachmentItem.validate()).toBe(true) + + store.setAttachmentItem(attachmentTestData[2]) + + expect(store.attachmentItem).toBeInstanceOf(Attachment) + expect(store.attachmentItem).toEqual(attachmentTestData[2]) + expect(store.attachmentItem.validate()).toBe(false) + }) +}) + +const testData = [ + { // full data + id: '1', + reference: 'ref1', + title: 'test 1', + summary: 'a short form summary', + description: 'a really really long description about this catalogus', + image: 'https://example.com/image.jpg', + category: 'category1', + portal: 'portal1', + catalogi: 'catalogi1', + metaData: 'meta1', + publicationDate: '2024-01-01', + modified: '2024-01-02', + featured: true, + organization: [{ name: 'Org1' }], + data: [{ key: 'value1' }], + attachments: ['attachment1'], + attachmentCount: 1, + schema: 'schema1', + status: 'status1', + license: 'MIT', + themes: 'theme1', + anonymization: { anonymized: 'yes', results: 'success' }, + language: { code: 'en', level: 'native' }, + }, + { // partial data + id: '2', + reference: 'ref2', + title: 'test 2', + summary: 'a short form summary', + description: 'a really really long description about this catalogus', + image: 'https://example.com/image.jpg', + category: 'category2', + portal: 'portal2', + catalogi: 'catalogi2', + metaData: 'meta2', + publicationDate: '2024-01-01', + modified: '2024-01-02', + featured: true, + organization: [{ name: 'Org1' }], + data: [{ key: 'value1' }], + attachments: ['attachment1'], + attachmentCount: 1, + + themes: 'theme1', + anonymization: { anonymized: 'yes', results: 'success' }, + language: { code: 'en', level: 'native' }, + }, + { // invalid data + id: '3', + reference: 'ref3', + title: '', + summary: 'a short form summary', + description: 'a really really long description about this catalogus', + image: 'https://example.com/image.jpg', + category: 'category3', + portal: 'portal3', + catalogi: 'catalogi3', + metaData: 'meta3', + publicationDate: '2024-01-01', + modified: '2024-01-02', + featured: true, + organization: [{ name: 'Org1' }], + data: [{ key: 'value1' }], + attachments: ['attachment1'], + attachmentCount: 1, + schema: 'schema1', + status: 'status1', + license: 'MIT', + themes: 'theme1', + anonymization: { anonymized: 'yes', results: 'success' }, + language: { code: 'en', level: 'native' }, + }, +] + +const attachmentTestData = [ + { // full data + id: '9044ab1e-cf5a-490a-be74-6be7a0c48a5f', + reference: 'ref1', + title: 'test 1', + summary: 'a short form summary', + description: 'a really really long description about this catalogus', + labels: [{ tag: 'tag1' }], + accessURL: 'https://example.com/access', + downloadURL: 'https://example.com/download', + type: 'document', + extension: 'pdf', + size: 1024, + anonymization: { anonymized: 'yes', results: 'success' }, + language: { code: 'en', level: 'native' }, + versionOf: 'v1.0', + hash: 'abc123', + published: '2024-01-01', + modified: '2024-01-02', + license: 'MIT', + }, + { // partial data + id: 'f849f287-492d-4100-91e1-1c4137f0abb5', + reference: 'ref2', + title: 'test 2', + summary: 'a short form summary', + description: 'a really really long description about this catalogus', + labels: [{ tag: 'tag2' }], + accessURL: 'https://example.com/access', + downloadURL: 'https://example.com/download', + type: 'document', + extension: 'pdf', + size: 1024, + anonymization: { anonymized: 'yes', results: 'success' }, + language: { code: 'en', level: 'native' }, + versionOf: 'v1.0', + license: 'MIT', + }, + { // invalid data + id: 'e193ea6b-1222-44cf-a71c-6ddcc232a79b', + reference: 'ref3', + title: '', + summary: 'a short form summary', + description: 'a really really long description about this catalogus', + labels: [{ tag: 'tag3' }], + accessURL: 'https://example.com/access', + downloadURL: 'https://example.com/download', + type: 'document', + extension: 'pdf', + size: 1024, + anonymization: { anonymized: 'yes', results: 'success' }, + language: { code: 'en', level: 'native' }, + versionOf: 'v1.0', + hash: 'abc123', + published: '2024-01-01', + modified: '2024-01-02', + license: 'MIT', + }, +] diff --git a/src/store/modules/theme.js b/src/store/modules/theme.js new file mode 100644 index 00000000..6901345a --- /dev/null +++ b/src/store/modules/theme.js @@ -0,0 +1,114 @@ +/* eslint-disable no-console */ +import { Attachment, Publication } from '../../entities/index.js' +import { defineStore } from 'pinia' + +export const useOrganizationStore = defineStore('organization', { + state: () => ({ + publicationItem: false, + publicationList: [], + publicationDataKey: false, + attachmentItem: false, + publicationAttachments: [], + conceptPublications: [], + conceptAttachments: [], + }), + actions: { + setPublicationItem(publicationItem) { + // To prevent forms etc from braking we alway use a default/skeleton object + this.publicationItem = publicationItem && new Publication(publicationItem) + console.log('Active publication item set to ' + publicationItem && publicationItem.id) + }, + setPublicationList(publicationList) { + this.publicationList = publicationList.map((publicationItem) => new Publication(publicationItem)) + console.log('Active publication item set to ' + publicationList.length) + }, + async refreshPublicationList(search = null) { + // @todo this might belong in a service? + let endpoint = '/index.php/apps/opencatalogi/api/publications' + if (search !== null && search !== '') { + endpoint = endpoint + '?_search=' + search + } + return fetch( + endpoint, + { + method: 'GET', + }, + ) + .then((response) => { + response.json().then((data) => { + this.setPublicationList(data?.results) + return data + }) + }) + .catch((err) => { + console.error(err) + return err + }) + }, + getPublicationAttachments(publication) { // @todo this might belong in a service? + fetch( + '/index.php/apps/opencatalogi/api/attachments', + { + method: 'GET', + }, + ) + .then((response) => { + response.json().then((data) => { + this.publicationAttachments = data.results.map( + (attachmentItem) => new Attachment(attachmentItem), + ) + return data + }) + }) + .catch((err) => { + console.error(err) + return err + }) + }, + getConceptPublications() { // @todo this might belong in a service? + fetch( + '/index.php/apps/opencatalogi/api/publications?status=concept', + { + method: 'GET', + }, + ) + .then((response) => { + response.json().then((data) => { + this.conceptPublications = data + return data + }) + }) + .catch((err) => { + console.error(err) + return err + }) + }, + getConceptAttachments() { // @todo this might belong in a service? + fetch( + '/index.php/apps/opencatalogi/api/attachments?status=concept', + { + method: 'GET', + }, + ) + .then((response) => { + response.json().then((data) => { + this.conceptAttachments = data + return data + }) + }) + .catch((err) => { + console.error(err) + return err + }) + }, + // @todo why does the following run through the store? -- because its impossible with props, and its vital information for the modal. + setPublicationDataKey(publicationDataKey) { + this.publicationDataKey = publicationDataKey + console.log('Active publication data key set to ' + publicationDataKey) + }, + setAttachmentItem(attachmentItem) { + this.attachmentItem = attachmentItem && new Attachment(attachmentItem) + console.log('Active attachment item set to ' + attachmentItem) + }, + }, +}) diff --git a/src/store/modules/theme.spec copy.js b/src/store/modules/theme.spec copy.js new file mode 100644 index 00000000..8adee3ba --- /dev/null +++ b/src/store/modules/theme.spec copy.js @@ -0,0 +1,220 @@ +/* eslint-disable no-console */ +import { setActivePinia, createPinia } from 'pinia' + +import { usePublicationStore } from './publication.js' +import { Attachment, Publication } from '../../entities/index.js' + +describe('Metadata Store', () => { + beforeEach(() => { + setActivePinia(createPinia()) + }) + + it('sets publication item correctly', () => { + const store = usePublicationStore() + + store.setPublicationItem(testData[0]) + + expect(store.publicationItem).toBeInstanceOf(Publication) + expect(store.publicationItem).toEqual(testData[0]) + expect(store.publicationItem.validate()).toBe(true) + + store.setPublicationItem(testData[1]) + + expect(store.publicationItem).toBeInstanceOf(Publication) + expect(store.publicationItem).not.toEqual(testData[1]) + expect(store.publicationItem.validate()).toBe(true) + + store.setPublicationItem(testData[2]) + + expect(store.publicationItem).toBeInstanceOf(Publication) + expect(store.publicationItem).toEqual(testData[2]) + expect(store.publicationItem.validate()).toBe(false) + }) + + it('sets publication list correctly', () => { + const store = usePublicationStore() + + store.setPublicationList(testData) + + expect(store.publicationList).toHaveLength(testData.length) + + expect(store.publicationList[0]).toBeInstanceOf(Publication) + expect(store.publicationList[0]).toEqual(testData[0]) + expect(store.publicationList[0].validate()).toBe(true) + + expect(store.publicationList[1]).toBeInstanceOf(Publication) + expect(store.publicationList[1]).not.toEqual(testData[1]) + expect(store.publicationList[1].validate()).toBe(true) + + expect(store.publicationList[2]).toBeInstanceOf(Publication) + expect(store.publicationList[2]).toEqual(testData[2]) + expect(store.publicationList[2].validate()).toBe(false) + }) + + // TODO: fix this + it('set publication data.data property key correctly', () => { + const store = usePublicationStore() + + store.setPublicationDataKey('contactPoint') + + expect(store.publicationDataKey).toBe('contactPoint') + }) + + it('set attachment item correctly', () => { + const store = usePublicationStore() + + store.setAttachmentItem(attachmentTestData[0]) + + expect(store.attachmentItem).toBeInstanceOf(Attachment) + expect(store.attachmentItem).toEqual(attachmentTestData[0]) + expect(store.attachmentItem.validate()).toBe(true) + + store.setAttachmentItem(attachmentTestData[1]) + + expect(store.attachmentItem).toBeInstanceOf(Attachment) + expect(store.attachmentItem).not.toEqual(attachmentTestData[1]) + expect(store.attachmentItem.validate()).toBe(true) + + store.setAttachmentItem(attachmentTestData[2]) + + expect(store.attachmentItem).toBeInstanceOf(Attachment) + expect(store.attachmentItem).toEqual(attachmentTestData[2]) + expect(store.attachmentItem.validate()).toBe(false) + }) +}) + +const testData = [ + { // full data + id: '1', + reference: 'ref1', + title: 'test 1', + summary: 'a short form summary', + description: 'a really really long description about this catalogus', + image: 'https://example.com/image.jpg', + category: 'category1', + portal: 'portal1', + catalogi: 'catalogi1', + metaData: 'meta1', + publicationDate: '2024-01-01', + modified: '2024-01-02', + featured: true, + organization: [{ name: 'Org1' }], + data: [{ key: 'value1' }], + attachments: ['attachment1'], + attachmentCount: 1, + schema: 'schema1', + status: 'status1', + license: 'MIT', + themes: 'theme1', + anonymization: { anonymized: 'yes', results: 'success' }, + language: { code: 'en', level: 'native' }, + }, + { // partial data + id: '2', + reference: 'ref2', + title: 'test 2', + summary: 'a short form summary', + description: 'a really really long description about this catalogus', + image: 'https://example.com/image.jpg', + category: 'category2', + portal: 'portal2', + catalogi: 'catalogi2', + metaData: 'meta2', + publicationDate: '2024-01-01', + modified: '2024-01-02', + featured: true, + organization: [{ name: 'Org1' }], + data: [{ key: 'value1' }], + attachments: ['attachment1'], + attachmentCount: 1, + + themes: 'theme1', + anonymization: { anonymized: 'yes', results: 'success' }, + language: { code: 'en', level: 'native' }, + }, + { // invalid data + id: '3', + reference: 'ref3', + title: '', + summary: 'a short form summary', + description: 'a really really long description about this catalogus', + image: 'https://example.com/image.jpg', + category: 'category3', + portal: 'portal3', + catalogi: 'catalogi3', + metaData: 'meta3', + publicationDate: '2024-01-01', + modified: '2024-01-02', + featured: true, + organization: [{ name: 'Org1' }], + data: [{ key: 'value1' }], + attachments: ['attachment1'], + attachmentCount: 1, + schema: 'schema1', + status: 'status1', + license: 'MIT', + themes: 'theme1', + anonymization: { anonymized: 'yes', results: 'success' }, + language: { code: 'en', level: 'native' }, + }, +] + +const attachmentTestData = [ + { // full data + id: '9044ab1e-cf5a-490a-be74-6be7a0c48a5f', + reference: 'ref1', + title: 'test 1', + summary: 'a short form summary', + description: 'a really really long description about this catalogus', + labels: [{ tag: 'tag1' }], + accessURL: 'https://example.com/access', + downloadURL: 'https://example.com/download', + type: 'document', + extension: 'pdf', + size: 1024, + anonymization: { anonymized: 'yes', results: 'success' }, + language: { code: 'en', level: 'native' }, + versionOf: 'v1.0', + hash: 'abc123', + published: '2024-01-01', + modified: '2024-01-02', + license: 'MIT', + }, + { // partial data + id: 'f849f287-492d-4100-91e1-1c4137f0abb5', + reference: 'ref2', + title: 'test 2', + summary: 'a short form summary', + description: 'a really really long description about this catalogus', + labels: [{ tag: 'tag2' }], + accessURL: 'https://example.com/access', + downloadURL: 'https://example.com/download', + type: 'document', + extension: 'pdf', + size: 1024, + anonymization: { anonymized: 'yes', results: 'success' }, + language: { code: 'en', level: 'native' }, + versionOf: 'v1.0', + license: 'MIT', + }, + { // invalid data + id: 'e193ea6b-1222-44cf-a71c-6ddcc232a79b', + reference: 'ref3', + title: '', + summary: 'a short form summary', + description: 'a really really long description about this catalogus', + labels: [{ tag: 'tag3' }], + accessURL: 'https://example.com/access', + downloadURL: 'https://example.com/download', + type: 'document', + extension: 'pdf', + size: 1024, + anonymization: { anonymized: 'yes', results: 'success' }, + language: { code: 'en', level: 'native' }, + versionOf: 'v1.0', + hash: 'abc123', + published: '2024-01-01', + modified: '2024-01-02', + license: 'MIT', + }, +] diff --git a/src/store/store.js b/src/store/store.js index 31a4a839..e65a2676 100644 --- a/src/store/store.js +++ b/src/store/store.js @@ -8,6 +8,8 @@ import { useDirectoryStore } from './modules/directory.js' import { useMetadataStore } from './modules/metadata.js' import { usePublicationStore } from './modules/publication.js' import { useOrganizationStore } from './modules/organization.js' +import { useThemeStore } from './modules/theme.js' +import { useConfigurationStore } from './modules/configuration.js' const navigationStore = useNavigationStore(pinia) const searchStore = useSearchStore(pinia) @@ -16,6 +18,8 @@ const directoryStore = useDirectoryStore(pinia) const metadataStore = useMetadataStore(pinia) const publicationStore = usePublicationStore(pinia) const organizationStore = useOrganizationStore(pinia) +const themeStore = useThemeStore(pinia) +const configurationStore = useConfigurationStore(pinia) export { // generic @@ -27,4 +31,6 @@ export { metadataStore, publicationStore, organizationStore, + themeStore, + configurationStore, } diff --git a/src/views/Views.vue b/src/views/Views.vue index ae5905ed..aa31b576 100644 --- a/src/views/Views.vue +++ b/src/views/Views.vue @@ -6,6 +6,9 @@ import { navigationStore, searchStore } from '../store/store.js' diff --git a/src/views/themes/ThemeList.vue b/src/views/themes/ThemeList.vue index 4d5b3290..ca9d415a 100644 --- a/src/views/themes/ThemeList.vue +++ b/src/views/themes/ThemeList.vue @@ -1,5 +1,5 @@ Ververs - + - Publicatie toevoegen + Theme toevoegen
- + :active="themeStore.themeItem.id === theme.id" + :details="theme?.status" + :counter-number="theme?.attachmentCount.toString()" + @click="themeStore.setThemeItem(theme)"> @@ -124,13 +94,8 @@ import Refresh from 'vue-material-design-icons/Refresh.vue' import Plus from 'vue-material-design-icons/Plus.vue' import Pencil from 'vue-material-design-icons/Pencil.vue' import Delete from 'vue-material-design-icons/Delete.vue' -import PublishOff from 'vue-material-design-icons/PublishOff.vue' -import FilePlusOutline from 'vue-material-design-icons/FilePlusOutline.vue' -import FileTreeOutline from 'vue-material-design-icons/FileTreeOutline.vue' import ContentCopy from 'vue-material-design-icons/ContentCopy.vue' import ShapeOutline from 'vue-material-design-icons/ShapeOutline.vue' -import Publish from 'vue-material-design-icons/Publish.vue' -import ArchivePlusOutline from 'vue-material-design-icons/ArchivePlusOutline.vue' import HelpCircleOutline from 'vue-material-design-icons/HelpCircleOutline.vue' export default { @@ -146,13 +111,9 @@ export default { // Icons Refresh, Plus, - FilePlusOutline, - FileTreeOutline, ContentCopy, ShapeOutline, Pencil, - Publish, - ArchivePlusOutline, HelpCircleOutline, }, beforeRouteLeave(to, from, next) { @@ -171,10 +132,10 @@ export default { } }, computed: { - filteredPublications() { - if (!publicationStore?.publicationList) return [] - return publicationStore.publicationList.filter((publication) => { - return publication.catalogi.toString() === navigationStore.selectedCatalogus.toString() + filteredThemes() { + if (!themeStore?.themeList) return [] + return themeStore.themeList.filter((theme) => { + return theme.catalogi.toString() === navigationStore.selectedCatalogus.toString() }) }, }, @@ -191,7 +152,7 @@ export default { methods: { fetchData(search = null) { this.loading = true - publicationStore.refreshPublicationList(search) + themeStore.refreshThemeList(search) .then(() => { this.loading = false }) @@ -213,10 +174,10 @@ export default { margin-inline-end: 10px; } -.active.publicationDetails-actionsDelete { +.active.themeDetails-actionsDelete { background-color: var(--color-error) !important; } -.active.publicationDetails-actionsDelete button { +.active.themeDetails-actionsDelete button { color: #EBEBEB !important; } From 2257717e6588636e7f67486440ea9b47e8dde00f Mon Sep 17 00:00:00 2001 From: Ruben van der Linde Date: Tue, 6 Aug 2024 07:29:35 +0200 Subject: [PATCH 34/65] Connecting it al together --- lib/Controller/OrganisationsController.php | 83 ++++++++++++++++------ lib/Controller/ThemesController.php | 83 ++++++++++++++++------ 2 files changed, 122 insertions(+), 44 deletions(-) diff --git a/lib/Controller/OrganisationsController.php b/lib/Controller/OrganisationsController.php index 8c0d2d28..6f57da25 100644 --- a/lib/Controller/OrganisationsController.php +++ b/lib/Controller/OrganisationsController.php @@ -2,18 +2,13 @@ namespace OCA\OpenCatalogi\Controller; -use Elastic\Elasticsearch\Client; -use GuzzleHttp\Exception\GuzzleException; -use OCA\opencatalogi\lib\Db\Organisation; -use OCA\OpenCatalogi\Db\PublicationMapper; -use OCA\OpenCatalogi\Service\ElasticSearchService; +use OCA\OpenCatalogi\Db\OrganisationMapper; use OCA\OpenCatalogi\Service\ObjectService; use OCP\AppFramework\Controller; use OCP\AppFramework\Http\TemplateResponse; use OCP\AppFramework\Http\JSONResponse; use OCP\IAppConfig; use OCP\IRequest; -use Symfony\Component\Uid\Uuid; class OrganisationsController extends Controller { @@ -34,7 +29,7 @@ public function __construct ( $appName, IRequest $request, - private readonly PublicationMapper $publicationMapper, + private readonly OrganisationMapper $organisationMapper, private readonly IAppConfig $config ) { @@ -52,12 +47,7 @@ public function __construct */ public function page(): TemplateResponse { - return new TemplateResponse( - //Application::APP_ID, - 'zaakafhandelapp', - 'index', - [] - ); + return new TemplateResponse($this->appName, 'OrganisationIndex', []); } /** @@ -68,13 +58,62 @@ public function page(): TemplateResponse * * @return JSONResponse */ - public function index(CallService $callService): JSONResponse + public function index(ObjectService $objectService): JSONResponse { - // Latere zorg - $query= $this->request->getParams(); + + $filters = $this->request->getParams(); - $results = $callService->index(source: 'brc', endpoint: 'besluiten'); - return new JSONResponse($results); + $searchConditions = []; + $searchParams = []; + $fieldsToSearch = ['title', 'description', 'summary']; + + foreach ($filters as $key => $value) { + if ($key === '_search') { + // MongoDB + $searchRegex = ['$regex' => $value, '$options' => 'i']; + $filters['$or'] = []; + + // MySQL + $searchParams['search'] = '%' . strtolower($value) . '%'; + + foreach ($fieldsToSearch as $field) { + // MongoDB + $filters['$or'][] = [$field => $searchRegex]; + + // MySQL + $searchConditions[] = "LOWER($field) LIKE :search"; + } + } + + if (str_starts_with($key, '_')) { + unset($filters[$key]); + } + } + + if($this->config->hasKey($this->appName, 'mongoStorage') === false + || $this->config->getValueString($this->appName, 'mongoStorage') !== '1' + ) { + // Unset mongodb filter + unset($filters['$or']); + + return new JSONResponse(['results' => $this->organisationMapper->findAll(filters: $filters, searchParams: $searchParams, searchConditions: $searchConditions)]); + } + + try { + $dbConfig = [ + 'base_uri' => $this->config->getValueString($this->appName, 'mongodbLocation'), + 'headers' => ['api-key' => $this->config->getValueString($this->appName, 'mongodbKey')], + 'mongodbCluster' => $this->config->getValueString($this->appName, 'mongodbCluster') + ]; + + $filters['_schema'] = 'organisation'; + + $result = $objectService->findObjects(filters: $filters, config: $dbConfig); + + return new JSONResponse(["results" => $result['documents']]); + } catch (\Exception $e) { + return new JSONResponse(['error' => $e->getMessage()], 500); + } } /** @@ -85,7 +124,7 @@ public function index(CallService $callService): JSONResponse * * @return JSONResponse */ - public function show(string $id, CallService $callService): JSONResponse + public function show(string $id, ObjectService $objectService): JSONResponse { // Latere zorg $query= $this->request->getParams(); @@ -103,7 +142,7 @@ public function show(string $id, CallService $callService): JSONResponse * * @return JSONResponse */ - public function create(CallService $callService): JSONResponse + public function create(ObjectService $objectService): JSONResponse { // get post from requests $body = $this->request->getParams(); @@ -119,7 +158,7 @@ public function create(CallService $callService): JSONResponse * * @return JSONResponse */ - public function update(string $id, CallService $callService): JSONResponse + public function update(string $id, ObjectService $objectService): JSONResponse { $body = $this->request->getParams(); $results = $callService->update(source: 'brc', endpoint: 'besluiten', data: $body, id: $id); @@ -134,7 +173,7 @@ public function update(string $id, CallService $callService): JSONResponse * * @return JSONResponse */ - public function destroy(string $id, CallService $callService): JSONResponse + public function destroy(string $id, ObjectService $objectService): JSONResponse { $callService->destroy(source: 'brc', endpoint: 'besluiten', id: $id); diff --git a/lib/Controller/ThemesController.php b/lib/Controller/ThemesController.php index fa2e7e9b..31738788 100644 --- a/lib/Controller/ThemesController.php +++ b/lib/Controller/ThemesController.php @@ -2,18 +2,13 @@ namespace OCA\OpenCatalogi\Controller; -use Elastic\Elasticsearch\Client; -use GuzzleHttp\Exception\GuzzleException; -use OCA\opencatalogi\lib\Db\Theme; -use OCA\OpenCatalogi\Db\PublicationMapper; -use OCA\OpenCatalogi\Service\ElasticSearchService; +use OCA\OpenCatalogi\Db\ThemesMapper; use OCA\OpenCatalogi\Service\ObjectService; use OCP\AppFramework\Controller; use OCP\AppFramework\Http\TemplateResponse; use OCP\AppFramework\Http\JSONResponse; use OCP\IAppConfig; use OCP\IRequest; -use Symfony\Component\Uid\Uuid; class ThemesController extends Controller { @@ -34,7 +29,7 @@ public function __construct ( $appName, IRequest $request, - private readonly PublicationMapper $publicationMapper, + private readonly ThemesMapper $themesMapper, private readonly IAppConfig $config, ) { @@ -52,12 +47,7 @@ public function __construct */ public function page(): TemplateResponse { - return new TemplateResponse( - //Application::APP_ID, - 'zaakafhandelapp', - 'index', - [] - ); + return new TemplateResponse($this->appName, 'ThemesIndex', []); } /** @@ -68,13 +58,62 @@ public function page(): TemplateResponse * * @return JSONResponse */ - public function index(CallService $callService): JSONResponse + public function index(ObjectService $objectService): JSONResponse { - // Latere zorg - $query= $this->request->getParams(); + + $filters = $this->request->getParams(); - $results = $callService->index(source: 'brc', endpoint: 'besluiten'); - return new JSONResponse($results); + $searchConditions = []; + $searchParams = []; + $fieldsToSearch = ['title', 'description', 'summary']; + + foreach ($filters as $key => $value) { + if ($key === '_search') { + // MongoDB + $searchRegex = ['$regex' => $value, '$options' => 'i']; + $filters['$or'] = []; + + // MySQL + $searchParams['search'] = '%' . strtolower($value) . '%'; + + foreach ($fieldsToSearch as $field) { + // MongoDB + $filters['$or'][] = [$field => $searchRegex]; + + // MySQL + $searchConditions[] = "LOWER($field) LIKE :search"; + } + } + + if (str_starts_with($key, '_')) { + unset($filters[$key]); + } + } + + if($this->config->hasKey($this->appName, 'mongoStorage') === false + || $this->config->getValueString($this->appName, 'mongoStorage') !== '1' + ) { + // Unset mongodb filter + unset($filters['$or']); + + return new JSONResponse(['results' => $this->themesMapper->findAll(filters: $filters, searchParams: $searchParams, searchConditions: $searchConditions)]); + } + + try { + $dbConfig = [ + 'base_uri' => $this->config->getValueString($this->appName, 'mongodbLocation'), + 'headers' => ['api-key' => $this->config->getValueString($this->appName, 'mongodbKey')], + 'mongodbCluster' => $this->config->getValueString($this->appName, 'mongodbCluster') + ]; + + $filters['_schema'] = 'theme'; + + $result = $objectService->findObjects(filters: $filters, config: $dbConfig); + + return new JSONResponse(["results" => $result['documents']]); + } catch (\Exception $e) { + return new JSONResponse(['error' => $e->getMessage()], 500); + } } /** @@ -85,7 +124,7 @@ public function index(CallService $callService): JSONResponse * * @return JSONResponse */ - public function show(string $id, CallService $callService): JSONResponse + public function show(string $id, ObjectService $objectService): JSONResponse { // Latere zorg $query= $this->request->getParams(); @@ -103,7 +142,7 @@ public function show(string $id, CallService $callService): JSONResponse * * @return JSONResponse */ - public function create(CallService $callService): JSONResponse + public function create(ObjectService $objectService): JSONResponse { // get post from requests $body = $this->request->getParams(); @@ -119,7 +158,7 @@ public function create(CallService $callService): JSONResponse * * @return JSONResponse */ - public function update(string $id, CallService $callService): JSONResponse + public function update(string $id, ObjectService $objectService): JSONResponse { $body = $this->request->getParams(); $results = $callService->update(source: 'brc', endpoint: 'besluiten', data: $body, id: $id); @@ -134,7 +173,7 @@ public function update(string $id, CallService $callService): JSONResponse * * @return JSONResponse */ - public function destroy(string $id, CallService $callService): JSONResponse + public function destroy(string $id, ObjectService $objectService): JSONResponse { $callService->destroy(source: 'brc', endpoint: 'besluiten', id: $id); From e6a06ed7473c68027a1e5d6c5b9c432d5ef1dcda Mon Sep 17 00:00:00 2001 From: Ruben van der Linde Date: Tue, 6 Aug 2024 09:05:18 +0200 Subject: [PATCH 35/65] Fix for the reflection errors --- lib/Controller/ThemesController.php | 6 +++--- lib/Db/{Organization.php => Organisation.php} | 2 +- lib/Db/{OrganizationMapper.php => OrganisationMapper.php} | 2 +- 3 files changed, 5 insertions(+), 5 deletions(-) rename lib/Db/{Organization.php => Organisation.php} (97%) rename lib/Db/{OrganizationMapper.php => OrganisationMapper.php} (96%) diff --git a/lib/Controller/ThemesController.php b/lib/Controller/ThemesController.php index 31738788..bbd024be 100644 --- a/lib/Controller/ThemesController.php +++ b/lib/Controller/ThemesController.php @@ -2,7 +2,7 @@ namespace OCA\OpenCatalogi\Controller; -use OCA\OpenCatalogi\Db\ThemesMapper; +use OCA\OpenCatalogi\Db\ThemeMapper; use OCA\OpenCatalogi\Service\ObjectService; use OCP\AppFramework\Controller; use OCP\AppFramework\Http\TemplateResponse; @@ -29,7 +29,7 @@ public function __construct ( $appName, IRequest $request, - private readonly ThemesMapper $themesMapper, + private readonly ThemeMapper $themeMapper, private readonly IAppConfig $config, ) { @@ -96,7 +96,7 @@ public function index(ObjectService $objectService): JSONResponse // Unset mongodb filter unset($filters['$or']); - return new JSONResponse(['results' => $this->themesMapper->findAll(filters: $filters, searchParams: $searchParams, searchConditions: $searchConditions)]); + return new JSONResponse(['results' => $this->themeMapper->findAll(filters: $filters, searchParams: $searchParams, searchConditions: $searchConditions)]); } try { diff --git a/lib/Db/Organization.php b/lib/Db/Organisation.php similarity index 97% rename from lib/Db/Organization.php rename to lib/Db/Organisation.php index 3695a277..4818a144 100644 --- a/lib/Db/Organization.php +++ b/lib/Db/Organisation.php @@ -6,7 +6,7 @@ use JsonSerializable; use OCP\AppFramework\Db\Entity; -class Organization extends Entity implements JsonSerializable +class Organisation extends Entity implements JsonSerializable { protected ?string $title = null; diff --git a/lib/Db/OrganizationMapper.php b/lib/Db/OrganisationMapper.php similarity index 96% rename from lib/Db/OrganizationMapper.php rename to lib/Db/OrganisationMapper.php index 74c0e153..c99b1f2a 100644 --- a/lib/Db/OrganizationMapper.php +++ b/lib/Db/OrganisationMapper.php @@ -8,7 +8,7 @@ use OCP\DB\QueryBuilder\IQueryBuilder; use OCP\IDBConnection; -class OrganizationMapper extends QBMapper +class OrganisationMapper extends QBMapper { public function __construct(IDBConnection $db) { From 2d92dfbf3d6dc487776351f9b9ef8a989c59ef2e Mon Sep 17 00:00:00 2001 From: Ruben van der Linde Date: Tue, 6 Aug 2024 09:08:45 +0200 Subject: [PATCH 36/65] Added filters to organisations --- lib/Db/OrganisationMapper.php | 15 +++++++++++++-- 1 file changed, 13 insertions(+), 2 deletions(-) diff --git a/lib/Db/OrganisationMapper.php b/lib/Db/OrganisationMapper.php index c99b1f2a..26dcc784 100644 --- a/lib/Db/OrganisationMapper.php +++ b/lib/Db/OrganisationMapper.php @@ -28,15 +28,26 @@ public function find(int $id): Organization return $this->findEntity(query: $qb); } - public function findAll($limit = null, $offset = null): array + public function findAll(?int $limit = null, ?int $offset = null, ?array $filters = [], ?array $searchConditions = [], ?array $searchParams = []): array { $qb = $this->db->getQueryBuilder(); $qb->select('*') - ->from('organizations') + ->from('organisations') ->setMaxResults($limit) ->setFirstResult($offset); + foreach($filters as $filter => $value) { + $qb->andWhere($qb->expr()->eq($filter, $qb->createNamedParameter($value))); + } + + if (!empty($searchConditions)) { + $qb->andWhere('(' . implode(' OR ', $searchConditions) . ')'); + foreach ($searchParams as $param => $value) { + $qb->setParameter($param, $value); + } + } + return $this->findEntities(query: $qb); } From b59ab7a98a783a7e092c48552210b56189cfc981 Mon Sep 17 00:00:00 2001 From: Thijn Date: Tue, 6 Aug 2024 09:43:57 +0200 Subject: [PATCH 37/65] fixed attachment entity spec --- src/entities/attachment/attachment.mock.ts | 78 +++++++++++++++ src/entities/attachment/attachment.spec.ts | 97 ++---------------- src/entities/attachment/attachment.ts | 103 +++++++++++--------- src/entities/attachment/attachment.types.ts | 38 ++++---- 4 files changed, 162 insertions(+), 154 deletions(-) create mode 100644 src/entities/attachment/attachment.mock.ts diff --git a/src/entities/attachment/attachment.mock.ts b/src/entities/attachment/attachment.mock.ts new file mode 100644 index 00000000..fe1bd260 --- /dev/null +++ b/src/entities/attachment/attachment.mock.ts @@ -0,0 +1,78 @@ +import { Attachment } from './attachment' +import { TAttachment } from './attachment.types' + +export const mockAttachmentsData = (): TAttachment[] => [ + { // full data + id: '1', + reference: 'ref1', + title: 'test 1', + summary: 'a short form summary', + description: 'a really really long description about this catalogus', + labels: ['label1'], + accessURL: 'https://example.com/access', + downloadURL: 'https://example.com/download', + type: 'document', + extension: 'pdf', + size: '1024', + anonymization: { + anonymized: true, + results: 'success', + }, + language: { code: 'en-us', level: 'C1' }, + versionOf: 'v1.0', + hash: 'abc123', + published: new Date(2022, 9, 14).toISOString(), + modified: new Date(2022, 11, 2).toISOString(), + license: 'MIT', + }, + { + id: '2', + reference: 'ref2', + title: 'test 2', + summary: 'a short form summary', + description: 'a really really long description about this catalogus', + labels: [], + accessURL: '', + downloadURL: '', + type: 'document', + extension: 'pdf', + size: '1024', + anonymization: { + anonymized: true, + results: 'success', + }, + language: { code: '', level: '' }, + versionOf: 'v1.0', + hash: 'hash', + published: '', + modified: '', + license: 'MIT', + }, + { // invalid data + id: '3', + reference: 'ref3', + title: 'test 3', + summary: 'a short form summary', + description: 'a really really long description about this catalogus', + labels: ['label3'], + // this is supposed to be a URL + accessURL: 'non url', + downloadURL: 'https://example.com/download', + type: 'document', + extension: 'pdf', + size: '1024', + anonymization: { + anonymized: true, + results: 'success', + }, + // invalid code and level + language: { code: 'foo bar', level: 'funny' }, + versionOf: 'v1.0', + hash: 'abc123', + published: new Date(2022, 9, 14).toISOString(), + modified: new Date(2022, 11, 2).toISOString(), + license: 'MIT', + }, +] + +export const mockAttachments = (data: TAttachment[] = mockAttachmentsData()): TAttachment[] => data.map(item => new Attachment(item)) diff --git a/src/entities/attachment/attachment.spec.ts b/src/entities/attachment/attachment.spec.ts index d315e81c..b14a5977 100644 --- a/src/entities/attachment/attachment.spec.ts +++ b/src/entities/attachment/attachment.spec.ts @@ -1,109 +1,32 @@ /* eslint-disable no-console */ import { Attachment } from './attachment' -import { TAttachment } from './attachment.types' +import { mockAttachments } from './attachment.mock' describe('Attachment Store', () => { it('create Attachment entity with full data', () => { - const attachment = new Attachment(testData[0]) + const attachment = new Attachment(mockAttachments()[0]) expect(attachment).toBeInstanceOf(Attachment) - expect(attachment).toEqual(testData[0]) + expect(attachment).toEqual(mockAttachments()[0]) - expect(attachment.validate()).toBe(true) + expect(attachment.validate().success).toBe(true) }) it('create Attachment entity with partial data', () => { - const attachment = new Attachment(testData[1]) + const attachment = new Attachment(mockAttachments()[1]) expect(attachment).toBeInstanceOf(Attachment) - expect(attachment.id).toBe(testData[1].id) - expect(attachment.reference).toBe(testData[1].reference) - expect(attachment.title).toBe(testData[1].title) - expect(attachment.summary).toBe(testData[1].summary) - expect(attachment.description).toBe(testData[1].description) - expect(attachment.labels).toBe(testData[1].labels) - expect(attachment.accessURL).toBe(testData[1].accessURL) - expect(attachment.downloadURL).toBe(testData[1].downloadURL) - expect(attachment.type).toBe(testData[1].type) - expect(attachment.extension).toBe(testData[1].extension) - expect(attachment.size).toBe(testData[1].size) - expect(attachment.anonymization).toBe(testData[1].anonymization) - expect(attachment.language).toBe(testData[1].language) - expect(attachment.versionOf).toBe(testData[1].versionOf) - expect(attachment.hash).toBe('') - expect(attachment.published).toBe('') - expect(attachment.modified).toBe('') - expect(attachment.license).toBe(testData[1].license) + expect(attachment).not.toBe(mockAttachments()[1]) - expect(attachment.validate()).toBe(true) + expect(attachment.validate().success).toBe(true) }) it('create Attachment entity with falsy data', () => { - const attachment = new Attachment(testData[2]) + const attachment = new Attachment(mockAttachments()[2]) expect(attachment).toBeInstanceOf(Attachment) - expect(attachment).toEqual(testData[2]) + expect(attachment).toEqual(mockAttachments()[2]) - expect(attachment.validate()).toBe(false) + expect(attachment.validate().success).toBe(false) }) }) - -const testData: TAttachment[] = [ - { // full data - id: '1', - reference: 'ref1', - title: 'test 1', - summary: 'a short form summary', - description: 'a really really long description about this catalogus', - labels: [{ tag: 'tag1' }], - accessURL: 'https://example.com/access', - downloadURL: 'https://example.com/download', - type: 'document', - extension: 'pdf', - size: 1024, - anonymization: { anonymized: 'yes', results: 'success' }, - language: { code: 'en', level: 'native' }, - versionOf: 'v1.0', - hash: 'abc123', - published: '2024-01-01', - modified: '2024-01-02', - license: 'MIT', - }, - { // partial data - id: '2', - reference: 'ref2', - title: 'test 2', - summary: 'a short form summary', - description: 'a really really long description about this catalogus', - labels: [{ tag: 'tag2' }], - accessURL: 'https://example.com/access', - downloadURL: 'https://example.com/download', - type: 'document', - extension: 'pdf', - size: 1024, - anonymization: { anonymized: 'yes', results: 'success' }, - language: { code: 'en', level: 'native' }, - versionOf: 'v1.0', - license: 'MIT', - }, - { // invalid data - id: '3', - reference: 'ref3', - title: '', - summary: 'a short form summary', - description: 'a really really long description about this catalogus', - labels: [{ tag: 'tag3' }], - accessURL: 'https://example.com/access', - downloadURL: 'https://example.com/download', - type: 'document', - extension: 'pdf', - size: 1024, - anonymization: { anonymized: 'yes', results: 'success' }, - language: { code: 'en', level: 'native' }, - versionOf: 'v1.0', - hash: 'abc123', - published: '2024-01-01', - modified: '2024-01-02', - license: 'MIT', - }, -] diff --git a/src/entities/attachment/attachment.ts b/src/entities/attachment/attachment.ts index 04f12928..dcca058d 100644 --- a/src/entities/attachment/attachment.ts +++ b/src/entities/attachment/attachment.ts @@ -1,34 +1,34 @@ import { TAttachment } from './attachment.types' -import { z } from 'zod' +import { SafeParseReturnType, z } from 'zod' export class Attachment implements TAttachment { public id: string - public reference?: string + public reference: string public title: string public summary: string - public description?: string - public labels?: object[] - public accessURL?: string - public downloadURL?: string - public type?: string - public extension?: string - public size?: number - public anonymization?: { - anonymized?: string - results?: string + public description: string + public labels: string[] + public accessURL: string + public downloadURL: string + public type: string + public extension: string + public size: string + public anonymization: { + anonymized: boolean + results: string } - public language?: { - code?: string - level?: string + public language: { + code: string + level: string } - public versionOf?: string - public hash?: string - public published?: string - public modified?: string - public license?: string + public versionOf: string + public hash: string + public published: string | Date + public modified: string | Date + public license: string constructor(data: TAttachment) { this.hydrate(data) @@ -46,9 +46,17 @@ export class Attachment implements TAttachment { this.downloadURL = data.downloadURL || '' this.type = data.type || '' this.extension = data.extension || '' - this.size = data.size || 0 - this.anonymization = (!Array.isArray(data.anonymization) && data.anonymization) || {} - this.language = (!Array.isArray(data.language) && data.language) || {} + this.size = data.size || '' + this.anonymization = (!Array.isArray(data.anonymization) && data.anonymization) || { + anonymized: false, + results: '', + } + + this.language = (!Array.isArray(data.language) && data.language) || { + code: '', + level: '', + } + this.versionOf = data.versionOf || '' this.hash = data.hash || '' this.published = data.published || '' @@ -57,43 +65,42 @@ export class Attachment implements TAttachment { } /* istanbul ignore next */ - public validate(): boolean { - // https://conduction.stoplight.io/docs/open-catalogi/9zm7p6fnazuod-attachment + public validate(): SafeParseReturnType { + // https://conduction.stoplight.io/docs/open-catalogi/lsigtx7cafbr7-create-attachment const schema = z.object({ - title: z.string().min(25).max(255), // .min(1) on a string functionally works the same as a nonEmpty check (SHOULD NOT BE COMBINED WITH .OPTIONAL()) - summary: z.string().min(50).max(2500), - description: z.string().max(2500).optional(), - reference: z.string().max(255).optional(), - labels: z.string().array().optional(), - accessURL: z.string().url().optional(), - downloadURL: z.string().url().optional(), - type: z.string().optional(), - extension: z.string().optional(), - size: z.number().optional(), + reference: z.string().max(255), + title: z.string().max(255), // .min(1) on a string functionally works the same as a nonEmpty check (SHOULD NOT BE COMBINED WITH .OPTIONAL()) + summary: z.string().max(255), + description: z.string().max(2555), + labels: z.string().array(), + accessURL: z.string().url().or(z.literal('')), + downloadURL: z.string().url().or(z.literal('')), + type: z.string(), anonymization: z.object({ - anonymized: z.boolean().optional(), - results: z.string().max(2500).optional(), - }).optional(), + anonymized: z.boolean(), + results: z.string().max(2500), + }), language: z.object({ // this regex checks if the code has either 2 or 3 characters per group, and the -aaa after the first is optional code: z.string() .max(7) .regex(/([a-z]{2,3})(-[a-z]{2,3})?/g, 'language code is not a valid ISO 639-1 code (e.g. en-us)') - .optional(), - title: z.string().min(1), - }).optional(), + .or(z.literal('')), + level: z.string() + .max(2) + .regex(/(A|B|C)(1|2)/g, 'language level is not a valid CEFRL level (e.g. A1)') + .or(z.literal('')), + }), + versionOf: z.string(), + published: z.string().datetime().or(z.literal('')), + license: z.string(), }) const result = schema.safeParse({ - id: this.id, - title: this.title, - description: this.description, - // version: this.version, - // required: this.required, - // properties: this.properties, + ...this, }) - return result.success + return result } } diff --git a/src/entities/attachment/attachment.types.ts b/src/entities/attachment/attachment.types.ts index 2861e1d1..7e462660 100644 --- a/src/entities/attachment/attachment.types.ts +++ b/src/entities/attachment/attachment.types.ts @@ -1,26 +1,26 @@ export type TAttachment = { id: string - reference?: string + reference: string title: string summary: string - description?: string - labels?: object[] - accessURL?: string - downloadURL?: string - type?: string - extension?: string - size?: number - anonymization?: { - anonymized?: string - results?: string + description: string + labels: string[] + accessURL: string + downloadURL: string + type: string + extension: string + size: string + anonymization: { + anonymized: boolean + results: string } - language?: { - code?: string - level?: string + language: { + code: string + level: string } - versionOf?: string - hash?: string - published?: string - modified?: string - license?: string + versionOf: string + hash: string + published: string | Date + modified: string | Date + license: string } From 2eb41494dc29da1e6f18e7c2761e15e0d4546c6e Mon Sep 17 00:00:00 2001 From: Ruben van der Linde Date: Tue, 6 Aug 2024 09:45:48 +0200 Subject: [PATCH 38/65] Database fix en migration --- lib/Db/OrganisationMapper.php | 2 +- lib/Db/ThemeMapper.php | 2 +- lib/Migration/Version6Date20240806073229.php | 84 ++++++++++++++++++++ 3 files changed, 86 insertions(+), 2 deletions(-) create mode 100644 lib/Migration/Version6Date20240806073229.php diff --git a/lib/Db/OrganisationMapper.php b/lib/Db/OrganisationMapper.php index 26dcc784..51a738e1 100644 --- a/lib/Db/OrganisationMapper.php +++ b/lib/Db/OrganisationMapper.php @@ -33,7 +33,7 @@ public function findAll(?int $limit = null, ?int $offset = null, ?array $filters $qb = $this->db->getQueryBuilder(); $qb->select('*') - ->from('organisations') + ->from('organizations') ->setMaxResults($limit) ->setFirstResult($offset); diff --git a/lib/Db/ThemeMapper.php b/lib/Db/ThemeMapper.php index ae01cdfb..9b35d682 100644 --- a/lib/Db/ThemeMapper.php +++ b/lib/Db/ThemeMapper.php @@ -33,7 +33,7 @@ public function findAll(?int $limit = null, ?int $offset = null, ?array $filters $qb = $this->db->getQueryBuilder(); $qb->select('*') - ->from('themes') + ->from('themas') ->setMaxResults($limit) ->setFirstResult($offset); diff --git a/lib/Migration/Version6Date20240806073229.php b/lib/Migration/Version6Date20240806073229.php new file mode 100644 index 00000000..2a522019 --- /dev/null +++ b/lib/Migration/Version6Date20240806073229.php @@ -0,0 +1,84 @@ +hasTable(tableName: 'themas') === false) { + $table = $schema->createTable(tableName: 'themas'); + + $table->addColumn(name: 'id', typeName: Types::BIGINT, options: [ + 'autoincrement' => true, + 'notnull' => true, + 'length' => 4, + ]); + $table->addColumn(name: 'title', typeName: TYPES::STRING, options: [ + 'notnull' => true, + 'length' => 255, + ]); + $table->addColumn(name: 'summary', typeName: TYPES::STRING, options: [ + 'notnull' => true, + 'length' => 255 + ]); + $table->addColumn(name: 'description', typeName: TYPES::STRING, options: [ + 'length' => 255, + 'notnull' => false, + ]); + $table->addColumn(name: 'image', typeName: TYPES::STRING, options: [ + 'notnull' => false, + ]); + + + $table->setPrimaryKey(columnNames: ['id']); + } + + + return $schema; + } + + /** + * @param IOutput $output + * @param Closure(): ISchemaWrapper $schemaClosure + * @param array $options + */ + public function postSchemaChange(IOutput $output, Closure $schemaClosure, array $options): void { + } +} From f91e329c1a7b213947d8f4cc33abe83d81373255 Mon Sep 17 00:00:00 2001 From: Ruben van der Linde Date: Tue, 6 Aug 2024 09:52:44 +0200 Subject: [PATCH 39/65] Fix for the migrations --- lib/Migration/Version6Date20240806073229.php | 1 + 1 file changed, 1 insertion(+) diff --git a/lib/Migration/Version6Date20240806073229.php b/lib/Migration/Version6Date20240806073229.php index 2a522019..93cf243e 100644 --- a/lib/Migration/Version6Date20240806073229.php +++ b/lib/Migration/Version6Date20240806073229.php @@ -11,6 +11,7 @@ use Closure; use OCP\DB\ISchemaWrapper; +use OCP\DB\Types; use OCP\Migration\IOutput; use OCP\Migration\SimpleMigrationStep; From 7eb6ff3b6ea48b71349af6297972384d2f11cee9 Mon Sep 17 00:00:00 2001 From: Ruben van der Linde Date: Tue, 6 Aug 2024 09:58:47 +0200 Subject: [PATCH 40/65] Typo fix --- lib/Db/Catalog.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/Db/Catalog.php b/lib/Db/Catalog.php index fa2bc2aa..cb0ab06e 100644 --- a/lib/Db/Catalog.php +++ b/lib/Db/Catalog.php @@ -19,7 +19,7 @@ public function __construct() { $this->addType(fieldName: 'title', type: 'string'); $this->addType(fieldName: 'summary', type: 'string'); $this->addType(fieldName: 'description', type: 'string'); - $this->addType(fieldName: 'image', type: 'image'); + $this->addType(fieldName: 'image', type: 'string'); $this->addType(fieldName: 'search', type: 'string'); } From 8b297c23ff21a1d6215dcc2d4f6bd39b88f102f9 Mon Sep 17 00:00:00 2001 From: Remko Date: Tue, 6 Aug 2024 10:39:36 +0200 Subject: [PATCH 41/65] Added better headings to dashboard and search --- css/main.css | 9 +++++++++ package.json | 2 +- .../configuration/configuration.spec.ts | 2 +- src/store/modules/configuration.specs.js | 2 +- src/views/dashboard/DashboardIndex.vue | 13 +++---------- src/views/search/SearchIndex.vue | 17 ++++------------- 6 files changed, 19 insertions(+), 26 deletions(-) diff --git a/css/main.css b/css/main.css index 36dd36e1..3b7d7694 100644 --- a/css/main.css +++ b/css/main.css @@ -4,6 +4,15 @@ --OC-margin-50: 50px; } +/* Pages */ + +.pageHeader { + margin-block-start: var(--app-navigation-padding); + margin-inline-start: calc(var(--default-clickable-area) + var(--app-navigation-padding)* 2); + min-height: var(--default-clickable-area); + line-height: var(--default-clickable-area); +} + /* Lists */ .listHeader { diff --git a/package.json b/package.json index a81bc78b..906b249b 100644 --- a/package.json +++ b/package.json @@ -43,7 +43,7 @@ "zod": "^3.23.8" }, "devDependencies": { - "@babel/preset-env": "^7.24.8", + "@babel/preset-env": "^7.25.3", "@nextcloud/browserslist-config": "^3.0.1", "@nextcloud/eslint-config": "^8.4.1", "@nextcloud/stylelint-config": "^2.4.0", diff --git a/src/entities/configuration/configuration.spec.ts b/src/entities/configuration/configuration.spec.ts index 64bdfb05..73b244cd 100644 --- a/src/entities/configuration/configuration.spec.ts +++ b/src/entities/configuration/configuration.spec.ts @@ -40,6 +40,6 @@ const testData: TConfiguration[] = [ useElastic: true, }, { // invalid data - useElastic: 'string', + useElastic: false, }, ] diff --git a/src/store/modules/configuration.specs.js b/src/store/modules/configuration.specs.js index b9e9fd1f..4b441784 100644 --- a/src/store/modules/configuration.specs.js +++ b/src/store/modules/configuration.specs.js @@ -2,7 +2,7 @@ import { setActivePinia, createPinia } from 'pinia' import { useConfigurationStore } from './configuration.js' -import { Configuration} from '../../entities/index.js' +import { Configuration } from '../../entities/index.js' describe('Metadata Store', () => { beforeEach(() => { diff --git a/src/views/dashboard/DashboardIndex.vue b/src/views/dashboard/DashboardIndex.vue index e4adff06..09e9b8e5 100644 --- a/src/views/dashboard/DashboardIndex.vue +++ b/src/views/dashboard/DashboardIndex.vue @@ -1,8 +1,8 @@ Kies een eigenschap - Oplopend - Aflopend + + Oplopend + + + Aflopend + -
\ No newline at end of file +
\ No newline at end of file From 7921238de266ea2a21647ff77571e28791958e28 Mon Sep 17 00:00:00 2001 From: Barry Brands Date: Tue, 6 Aug 2024 12:55:13 +0200 Subject: [PATCH 47/65] Small cleanup --- src/views/catalogi/CatalogiList.vue | 11 +++++------ src/views/directory/DirectoryList.vue | 11 +++++------ src/views/metaData/MetaDataList.vue | 11 +++++------ src/views/publications/PublicationList.vue | 15 +++++++-------- 4 files changed, 22 insertions(+), 26 deletions(-) diff --git a/src/views/catalogi/CatalogiList.vue b/src/views/catalogi/CatalogiList.vue index eb11bb68..980f1b80 100644 --- a/src/views/catalogi/CatalogiList.vue +++ b/src/views/catalogi/CatalogiList.vue @@ -108,6 +108,10 @@ export default { Pencil, Delete, }, + beforeRouteLeave(to, from, next) { + search = ''; + next(); + }, props: { search: { type: String, @@ -118,7 +122,6 @@ export default { return { loading: false, catalogi: [], - search: '', } }, watch: { @@ -141,12 +144,8 @@ export default { }, debouncedFetchData: debounce(function(search) { this.fetchData(search); - }, 500), + }, 500), }, - beforeRouteLeave(to, from, next) { - search = ''; - next(); - }, } diff --git a/src/dialogs/organisation/DeleteOrganisationDialog.vue b/src/dialogs/organisation/DeleteOrganisationDialog.vue new file mode 100644 index 00000000..674cac42 --- /dev/null +++ b/src/dialogs/organisation/DeleteOrganisationDialog.vue @@ -0,0 +1,116 @@ + + + + + + + diff --git a/src/dialogs/theme/CopyThemeDialog.vue b/src/dialogs/theme/CopyThemeDialog.vue new file mode 100644 index 00000000..dd8c7198 --- /dev/null +++ b/src/dialogs/theme/CopyThemeDialog.vue @@ -0,0 +1,123 @@ + + + + + + + diff --git a/src/dialogs/theme/DeleteThemeDialog.vue b/src/dialogs/theme/DeleteThemeDialog.vue new file mode 100644 index 00000000..83564a86 --- /dev/null +++ b/src/dialogs/theme/DeleteThemeDialog.vue @@ -0,0 +1,116 @@ + + + + + + + diff --git a/src/modals/Modals.vue b/src/modals/Modals.vue index 528aad33..b6b86ac8 100644 --- a/src/modals/Modals.vue +++ b/src/modals/Modals.vue @@ -16,6 +16,9 @@ + + +
@@ -24,10 +27,10 @@ import AddAttachmentModal from './attachment/AddAttachmentModal.vue' import EditAttachmentModal from './attachment/EditAttachmentModal.vue' -import AddPublicationModal from './publication/AddPublicationModal.vue' -import EditPublicationModal from './publication/EditPublicationModal.vue' import AddMetaDataModal from './metaData/AddMetaDataModal.vue' import EditMetaDataModal from './metaData/EditMetaDataModal.vue' +import AddPublicationModal from './publication/AddPublicationModal.vue' +import EditPublicationModal from './publication/EditPublicationModal.vue' import AddMetaDataPropertyModal from './metaData/AddMetaDataPropertyModal.vue' import EditMetaDataPropertyModal from './metaData/EditMetaDataPropertyModal.vue' @@ -36,9 +39,12 @@ import AddCatalogModal from './catalog/AddCatalogModal.vue' import EditCatalogModal from './catalog/EditCatalogModal.vue' import AddListingModal from './directory/AddListingModal.vue' import EditListingModal from './directory/EditListingModal.vue' -import EditPublicationDataModal from './publicationData/EditPublicationDataModal.vue' -import AddPublicationDataModal from './publicationData/AddPublicationDataModal.vue' import AddOrganisationModal from './organisation/AddOrganisationModal.vue' +import EditOrganisatioModal from './organisation/EditOrganisationModal.vue' +import AddPublicationDataModal from './publicationData/AddPublicationDataModal.vue' +import EditPublicationDataModal from './publicationData/EditPublicationDataModal.vue' +import AddThemeModal from './theme/AddThemeModal.vue' +import EditThemeModal from './theme/EditThemeModal.vue' export default { name: 'Modals', @@ -58,6 +64,9 @@ export default { AddPublicationDataModal, EditPublicationDataModal, AddOrganisationModal, + EditOrganisatioModal, + AddThemeModal, + EditThemeModal, // EditOrganisationModal, }, } diff --git a/src/modals/organisation/AddOrganisationModal.vue b/src/modals/organisation/AddOrganisationModal.vue index 76150c9a..45251bb3 100644 --- a/src/modals/organisation/AddOrganisationModal.vue +++ b/src/modals/organisation/AddOrganisationModal.vue @@ -144,7 +144,7 @@ export default { response.json().then((data) => { organisationStore.setOrganisationList(data) }) - navigationStore.setSelected('organisation') + navigationStore.setSelected('organisations') // Wait for the user to read the feedback then close the model const self = this setTimeout(function() { diff --git a/src/modals/organisation/EditOrganisationModal.vue b/src/modals/organisation/EditOrganisationModal.vue new file mode 100644 index 00000000..a13ea1ae --- /dev/null +++ b/src/modals/organisation/EditOrganisationModal.vue @@ -0,0 +1,227 @@ + + + + + + diff --git a/src/modals/theme/AddThemeModal.vue b/src/modals/theme/AddThemeModal.vue new file mode 100644 index 00000000..ee1024ae --- /dev/null +++ b/src/modals/theme/AddThemeModal.vue @@ -0,0 +1,187 @@ + + + + + + diff --git a/src/modals/theme/EditThemeModal.vue b/src/modals/theme/EditThemeModal.vue new file mode 100644 index 00000000..dccaaf97 --- /dev/null +++ b/src/modals/theme/EditThemeModal.vue @@ -0,0 +1,207 @@ + + + + + + diff --git a/src/views/organisations/OrganisationDetail.vue b/src/views/organisations/OrganisationDetail.vue index fe685920..5ce0e28a 100644 --- a/src/views/organisations/OrganisationDetail.vue +++ b/src/views/organisations/OrganisationDetail.vue @@ -1,12 +1,12 @@ Kopiëren - - - Publiceren - - - - Depubliceren - - - - Archiveren - - - - Eigenschap toevoegen - - - - Bijlage toevoegen -