From 881d710bd4ea02e736fd4e8ff1628f2d36225c25 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Petra=20Mi=C5=A1eta?= Date: Mon, 17 Apr 2023 10:25:03 +0200 Subject: [PATCH 1/5] ECI-15 implement backend search by metadata field --- .../Controller/EzAdminUI/BrowseController.php | 5 ++- bundle/Controller/EzAdminUI/Facets/Load.php | 12 ++++++- .../Cloudinary/CloudinaryProvider.php | 8 +++++ .../Provider/Cloudinary/Gateway.php | 7 +++++ .../Gateway/Cache/Psr6CachedGateway.php | 29 +++++++++++++++++ .../Gateway/CloudinaryApiGateway.php | 31 ++++++++++++++++++- .../Provider/Cloudinary/Search/Query.php | 23 ++++++++++++++ bundle/RemoteMedia/RemoteMediaProvider.php | 5 +++ 8 files changed, 117 insertions(+), 3 deletions(-) diff --git a/bundle/Controller/EzAdminUI/BrowseController.php b/bundle/Controller/EzAdminUI/BrowseController.php index 4eee03d2..015bd286 100644 --- a/bundle/Controller/EzAdminUI/BrowseController.php +++ b/bundle/Controller/EzAdminUI/BrowseController.php @@ -31,11 +31,13 @@ public function __invoke(Request $request) $limit = 25; $userQuery = $request->get('q', ''); $tag = $request->get('tag', 'all'); + $metadata = $request->get('metadata', 'all'); $type = $request->get('mediatype', 'all'); $folder = $request->get('folder', 'all'); $type = $type !== 'all' ? $type : null; $folder = $folder !== 'all' ? $folder : null; $tag = $tag !== 'all' ? $tag : null; + $metadata = $metadata !== 'all' ? $metadata : null; switch ($folder) { case '(all)': @@ -60,7 +62,8 @@ public function __invoke(Request $request) $limit, $folder, $tag, - $nextCursor, + $metadata, + $nextCursor ); $results = $this->remoteMediaProvider->searchResources($query); diff --git a/bundle/Controller/EzAdminUI/Facets/Load.php b/bundle/Controller/EzAdminUI/Facets/Load.php index b9155260..a3937925 100644 --- a/bundle/Controller/EzAdminUI/Facets/Load.php +++ b/bundle/Controller/EzAdminUI/Facets/Load.php @@ -24,7 +24,8 @@ public function __invoke(): Response { $folders = $this->remoteMediaProvider->listFolders(); $tags = $this->remoteMediaProvider->listTags(); - + $metadataFields = $this->remoteMediaProvider->listMetadataFields(); + $formattedFolders = []; foreach ($folders as $folder) { $formattedFolders[] = [ @@ -42,9 +43,18 @@ public function __invoke(): Response ]; } + $formattedMetadataFields = []; + foreach ($metadataFields as $metadataField) { + $formattedMetadataFields[] = [ + 'id' => $metadataField['external_id'], + 'label' => $metadataField['label'] + ]; + } + $result = [ 'folders' => $formattedFolders, 'tags' => $formattedTags, + 'metadataFields' => $formattedMetadataFields, ]; return new JsonResponse($result); diff --git a/bundle/RemoteMedia/Provider/Cloudinary/CloudinaryProvider.php b/bundle/RemoteMedia/Provider/Cloudinary/CloudinaryProvider.php index 0f81053f..b8841763 100644 --- a/bundle/RemoteMedia/Provider/Cloudinary/CloudinaryProvider.php +++ b/bundle/RemoteMedia/Provider/Cloudinary/CloudinaryProvider.php @@ -258,6 +258,14 @@ public function updateTags(string $resourceId, string $tags, string $resourceTyp $this->gateway->update($resourceId, $resourceType, $options); } + /** + * Lists metadata fields. + */ + public function listMetadataFields(): array + { + return $this->gateway->listMetadataFields(); + } + /** * Updates the resource context. * eg. alt text and caption: diff --git a/bundle/RemoteMedia/Provider/Cloudinary/Gateway.php b/bundle/RemoteMedia/Provider/Cloudinary/Gateway.php index f104cb6f..a286b57a 100644 --- a/bundle/RemoteMedia/Provider/Cloudinary/Gateway.php +++ b/bundle/RemoteMedia/Provider/Cloudinary/Gateway.php @@ -135,6 +135,13 @@ abstract public function removeTag($id, $type, $tag); */ abstract public function removeAllTags($id, $type); + /** + * Lists metadata fields. + * + * @return array + */ + abstract public function listMetadataFields(); + /** * Updates the remote resource. * diff --git a/bundle/RemoteMedia/Provider/Cloudinary/Gateway/Cache/Psr6CachedGateway.php b/bundle/RemoteMedia/Provider/Cloudinary/Gateway/Cache/Psr6CachedGateway.php index f457e642..6cf01e91 100644 --- a/bundle/RemoteMedia/Provider/Cloudinary/Gateway/Cache/Psr6CachedGateway.php +++ b/bundle/RemoteMedia/Provider/Cloudinary/Gateway/Cache/Psr6CachedGateway.php @@ -45,6 +45,10 @@ class Psr6CachedGateway extends Gateway * @var string */ const TAG_LIST = 'tag_list'; + /** + * @var string + */ + const METADATA_FIELDS = 'metadata_fields'; /** * @var string */ @@ -386,6 +390,31 @@ public function removeAllTags($id, $type) return $value; } + /** + * Lists metadata fields. + * + * @return array + */ + public function listMetadataFields() + { + $listMetadataCacheKey = $this->washKey( + implode('-', [self::PROJECT_KEY, self::PROVIDER_KEY, self::METADATA_FIELDS]), + ); + $cacheItem = $this->cache->getItem($listMetadataCacheKey); + + if ($cacheItem->isHit()) { + return $cacheItem->get(); + } + + $list = $this->gateway->listMetadataFields(); + $cacheItem->set($list); + $cacheItem->expiresAfter($this->ttl); + $cacheItem->tag($this->getCacheTags(self::METADATA_FIELDS)); + $this->cache->save($cacheItem); + + return $list; + } + /** * Updates the remote resource. * diff --git a/bundle/RemoteMedia/Provider/Cloudinary/Gateway/CloudinaryApiGateway.php b/bundle/RemoteMedia/Provider/Cloudinary/Gateway/CloudinaryApiGateway.php index 58c70892..f746bb71 100644 --- a/bundle/RemoteMedia/Provider/Cloudinary/Gateway/CloudinaryApiGateway.php +++ b/bundle/RemoteMedia/Provider/Cloudinary/Gateway/CloudinaryApiGateway.php @@ -165,7 +165,8 @@ public function search(Query $query): Result ->expression($expression) ->max_results($query->getLimit()) ->with_field('context') - ->with_field('tags'); + ->with_field('tags') + ->with_field('metadata'); if ($query->getNextCursor() !== null) { $search->next_cursor($query->getNextCursor()); @@ -336,6 +337,30 @@ public function removeAllTags($id, $type) return $this->cloudinaryUploader->remove_all_tags([$id], ['resource_type' => $type]); } + /** + * Lists metadata fields. + * + * @return array + */ + public function listMetadataFields() + { + $options = [ + 'max_results' => 500, + ]; + + $metadataFields = []; + do { + $result = $this->cloudinaryApi->list_metadata_fields($options); + $metadataFields = array_merge($metadataFields, $result['metadata_fields']); + + if (array_key_exists('next_cursor', $result)) { + $options['next_cursor'] = $result['next_cursor']; + } + } while (array_key_exists('next_cursor', $result)); + + return $metadataFields; + } + /** * Updates the remote resource. * @@ -431,6 +456,10 @@ private function buildSearchExpression(Query $query) $expressions[] = sprintf('tags:%s', $query->getTag()); } + if ($query->getMetadata()) { + $expressions[] = sprintf('metadata:%s', $query->getMetadata()); + } + if ($query->getFolder() !== null) { $expressions[] = sprintf('folder:"%s"', $query->getFolder()); } diff --git a/bundle/RemoteMedia/Provider/Cloudinary/Search/Query.php b/bundle/RemoteMedia/Provider/Cloudinary/Search/Query.php index 500fa63a..3237601b 100644 --- a/bundle/RemoteMedia/Provider/Cloudinary/Search/Query.php +++ b/bundle/RemoteMedia/Provider/Cloudinary/Search/Query.php @@ -34,6 +34,10 @@ final class Query /** @var array */ private $sortBy = ['created_at' => 'desc']; + /** + * @var string|null + */ + private $metadata; public function __construct( string $query, @@ -41,6 +45,7 @@ public function __construct( int $limit, ?string $folder = null, ?string $tag = null, + ?string $metadata = null, ?string $nextCursor = null, array $sortBy = ['created_at' => 'desc'], array $resourceIds = [] @@ -49,6 +54,7 @@ public function __construct( $this->resourceType = $resourceType; $this->folder = $folder; $this->tag = $tag; + $this->metadata = $metadata; $this->limit = $limit; $this->nextCursor = $nextCursor; $this->sortBy = $sortBy; @@ -83,6 +89,7 @@ public static function createResourceIdsSearchQuery( $limit, null, null, + null, $nextCursor, $sortBy, $resourceIds @@ -118,6 +125,14 @@ public function getTag(): ?string return $this->tag; } + /** + * @return string|null + */ + public function getMetadata(): ?string + { + return $this->metadata; + } + /** * @return string[] */ @@ -148,4 +163,12 @@ public function getSortBy(): array { return $this->sortBy; } + + /** + * @param string|null $metadata + */ + public function setMetadata(?string $metadata): void + { + $this->metadata = $metadata; + } } diff --git a/bundle/RemoteMedia/RemoteMediaProvider.php b/bundle/RemoteMedia/RemoteMediaProvider.php index c13ce3c1..d2c6a291 100644 --- a/bundle/RemoteMedia/RemoteMediaProvider.php +++ b/bundle/RemoteMedia/RemoteMediaProvider.php @@ -139,6 +139,11 @@ abstract public function removeAllTagsFromResource(string $resourceId, string $r */ abstract public function updateTags(string $resourceId, string $tags, string $resourceType = 'image'); + /** + * Lists metadata fields. + */ + abstract public function listMetadataFields(): array; + /** * Updates the resource context. * eg. alt text and caption: From 336508c8a2ed813b1be6d70df29456df2687e919 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Petra=20Mi=C5=A1eta?= Date: Mon, 17 Apr 2023 10:28:07 +0200 Subject: [PATCH 2/5] ECI-15 implement search by metadata fields on frontend --- .../translations/ngremotemedia.en.yml | 2 ++ .../translations/ngremotemedia.no.yml | 2 ++ .../views/ezadminui/js_config.html.twig | 2 ++ frontend/src/components/Interactions.vue | 7 ++++++- frontend/src/components/MediaFacets.vue | 20 ++++++++++++++++++- frontend/src/components/MediaModal.vue | 12 ++++++----- frontend/src/constants/facets.js | 1 + 7 files changed, 39 insertions(+), 7 deletions(-) diff --git a/bundle/Resources/translations/ngremotemedia.en.yml b/bundle/Resources/translations/ngremotemedia.en.yml index 905a0781..75f02896 100644 --- a/bundle/Resources/translations/ngremotemedia.en.yml +++ b/bundle/Resources/translations/ngremotemedia.en.yml @@ -36,6 +36,8 @@ ngrm: select_tag: 'Select tag' loading_tags: 'Loading tags...' all_tags: 'All tags' + metadata_fields: 'Meta' + all_metadata_fields: 'All' search: 'Search (by name, tag, folder, alternate text etc.)' search_placeholder: 'Enter your search term' empty_folder: 'Folder is empty' diff --git a/bundle/Resources/translations/ngremotemedia.no.yml b/bundle/Resources/translations/ngremotemedia.no.yml index 05688039..4c48b98a 100644 --- a/bundle/Resources/translations/ngremotemedia.no.yml +++ b/bundle/Resources/translations/ngremotemedia.no.yml @@ -35,6 +35,8 @@ ngrm: all_folders: 'All folders' select_tag: 'Select tag' loading_tags: 'Loading tags...' + metadata_fields: 'Meta' + all_metadata_fields: 'All' all_tags: 'All tags' search: 'Search (by name, tag, folder, alternate text etc.)' search_placeholder: 'Enter your search term' diff --git a/bundle/Resources/views/ezadminui/js_config.html.twig b/bundle/Resources/views/ezadminui/js_config.html.twig index c950092e..ab8f74d4 100644 --- a/bundle/Resources/views/ezadminui/js_config.html.twig +++ b/bundle/Resources/views/ezadminui/js_config.html.twig @@ -15,6 +15,8 @@ 'browse_select_tag': "{{ "ngrm.edit.vue.browse.facets.select_tag"|trans }}", 'browse_loading_tags': "{{ "ngrm.edit.vue.browse.facets.loading_tags"|trans }}", 'browse_all_tags': "{{ "ngrm.edit.vue.browse.facets.all_tags"|trans }}", + 'browse_metadata_fields': "{{ "ngrm.edit.vue.browse.facets.metadata_fields"|trans }}", + 'browse_all_metadata_fields': "{{ "ngrm.edit.vue.browse.facets.all_metadata_fields"|trans }}", 'search': "{{ "ngrm.edit.vue.browse.facets.search"|trans }}", 'search_placeholder': "{{ "ngrm.edit.vue.browse.facets.search_placeholder"|trans }}", 'browse_empty_folder': "{{ "ngrm.edit.vue.browse.empty_folder"|trans }}", diff --git a/frontend/src/components/Interactions.vue b/frontend/src/components/Interactions.vue index 12f2ee60..e03843b0 100644 --- a/frontend/src/components/Interactions.vue +++ b/frontend/src/components/Interactions.vue @@ -27,7 +27,7 @@ - + @@ -67,6 +67,7 @@ export default { uploadModalOpen: false, folders: [], tags: [], + metadataFields: [], facetsLoading: true }; }, @@ -108,6 +109,7 @@ export default { previewUrl: item.preview_url, alternateText: item.alt_text, tags: item.tags, + metadataFields: item.metadata, size: item.filesize, variations: {}, height: item.height, @@ -148,6 +150,7 @@ export default { previewUrl: '', alternateText: '', tags: [], + metadataFields: [], size: 0, variations: {}, height: 0, @@ -159,6 +162,7 @@ export default { const response = await fetch(this.config.paths.load_facets); const data = await response.json(); this.tags = data.tags; + this.metadataFields = data.metadataFields; this.facetsLoading = false; }, async handleBrowseMediaClicked() { @@ -203,6 +207,7 @@ export default { previewUrl: '', alternateText: '', tags: [], + metadataFields: [], size: file.size, variations: {}, height: 0, diff --git a/frontend/src/components/MediaFacets.vue b/frontend/src/components/MediaFacets.vue index c6197099..42eff063 100644 --- a/frontend/src/components/MediaFacets.vue +++ b/frontend/src/components/MediaFacets.vue @@ -39,6 +39,19 @@ /> +
+ + +
+
{{ this.$root.$data.NgRemoteMediaTranslations.search }}