diff --git a/blocks/adp-add-to-collection-modal/adp-add-to-collection-modal.js b/blocks/adp-add-to-collection-modal/adp-add-to-collection-modal.js index fd61cc56..a3d88cd4 100644 --- a/blocks/adp-add-to-collection-modal/adp-add-to-collection-modal.js +++ b/blocks/adp-add-to-collection-modal/adp-add-to-collection-modal.js @@ -1,6 +1,6 @@ import { decorateIcons } from '../../scripts/lib-franklin.js'; import { - listCollection, createCollection, patchCollection, getCollection, + searchListCollection, createCollection, patchCollection, getCollection, } from '../../scripts/collections.js'; import { getSelectedAssetsFromInfiniteResultsBlock, populateAssetViewLeftDialog } from '../../scripts/scripts.js'; import createMultiSelectedAssetsTable from '../../scripts/multi-selected-assets-table.js'; @@ -43,38 +43,18 @@ async function createDropdown(addToExistingRadioDropboxContainer) { dropdownSelect.classList.add('add-to-existing-dropdown'); // Add the new class // Function to load more data when reaching the end of the dropdown - const loadMoreData = async (cursor) => { - const collectionData = await listCollection({ cursor, limit: 10 }); // Adjust the limit as needed + const loadMoreData = async (page) => { + // Call to get the nbHits for the total number of Collections + const collectionMax = await searchListCollection(0, 0); + // Get all the collections + const collectionData = await searchListCollection(collectionMax.nbHits, page); return collectionData; }; - let cursor = null; - - // Event listener to detect scroll and load more data when at the bottom - dropdownSelect.addEventListener('scroll', async () => { - if (dropdownSelect.scrollTop + dropdownSelect.clientHeight >= dropdownSelect.scrollHeight) { - const moreData = await loadMoreData(cursor); - if (moreData.items.length > 0) { - // Update the cursor for the next load - cursor = moreData.cursor; - - // Populate the options in the dropdown from the additional data - moreData.items.forEach((collection) => { - const option = document.createElement('option'); - option.value = collection.id; - option.textContent = collection.title; - dropdownSelect.appendChild(option); - }); - } - } - }); - + const page = 0; // Initial data loading - const initialData = await loadMoreData(cursor); + const initialData = await loadMoreData(page); if (initialData.items.length > 0) { - // Update the cursor for the next load - cursor = initialData.cursor; - // Populate the options in the dropdown with the initial data initialData.items.forEach((collection) => { const option = document.createElement('option'); @@ -160,6 +140,7 @@ export async function openModal(items) { const { etag } = collection; patchCollection(collectionId, etag, payload); }); + resetDialogState(); } diff --git a/blocks/adp-collection-header/adp-collection-header.js b/blocks/adp-collection-header/adp-collection-header.js index 618a4ce5..d4a63582 100644 --- a/blocks/adp-collection-header/adp-collection-header.js +++ b/blocks/adp-collection-header/adp-collection-header.js @@ -36,7 +36,7 @@ function createCollectionInfoHeader(collectionInfoHeader, collection) { 'Delete collection', `Are you sure you want to delete the collection "${collection.title}"?`, async () => { - await deleteCollection(collectionId, collection.title); + await deleteCollection(collectionId, collection.title, collection.etag); navigateTo(createLinkHref('/collections')); }, 'Proceed', diff --git a/blocks/adp-infinite-results-collections/CollectionsDatasource.js b/blocks/adp-infinite-results-collections/CollectionsDatasource.js index c1d16937..4e8455d8 100644 --- a/blocks/adp-infinite-results-collections/CollectionsDatasource.js +++ b/blocks/adp-infinite-results-collections/CollectionsDatasource.js @@ -2,23 +2,29 @@ import { createLinkHref, navigateTo } from '../../scripts/scripts.js'; import { getCollectionID, getCollectionTitle, - listCollection, + searchListCollection, } from '../../scripts/collections.js'; import { createCollectionCardElement } from '../../scripts/card-html-builder.js'; export default class CollectionsDatasource { - cursor; - infiniteResultsContainer = null; container = null; pageNumber = 0; + lastPage = false; + async showMore() { - const list = await listCollection(5, this.cursor); - this.cursor = list.cursor; + + const list = await searchListCollection(5, this.pageNumber); + this.pageNumber += 1; + + if (this.pageNumber >= list.nbPages){ + this.lastPage = true; + } + this.infiniteResultsContainer.resultsCallback( this.container, list.items, @@ -31,14 +37,15 @@ export default class CollectionsDatasource { } isLastPage() { - return !this.cursor; + return this.lastPage; } async registerResultsCallback(container, infiniteResultsContainer) { this.infiniteResultsContainer = infiniteResultsContainer; this.container = container; - const list = await listCollection(5, this.cursor); - this.cursor = list.cursor; + + const list = await searchListCollection(5, this.pageNumber); + infiniteResultsContainer.resultsCallback( container, list.items, diff --git a/scripts/collections.js b/scripts/collections.js index 1f5c0788..b9d56a99 100644 --- a/scripts/collections.js +++ b/scripts/collections.js @@ -2,12 +2,15 @@ import { getBearerToken } from './security.js'; import { getAssetHandlerApiKey, getDeliveryEnvironment, + getBackendApiKey, } from './polaris.js'; import { getPathParams, logError } from './scripts.js'; import { emitEvent, EventNames } from './events.js'; +import { getAdminConfig } from './site-config.js'; + export function getCollectionIdFromURL() { - if (window.location.pathname.startsWith('/collection/')) { + if (window.location.pathname.includes('/collection/')) { return getPathParams().at(-1); } return undefined; @@ -28,24 +31,54 @@ export function getAssetIdFromCollectionItem(assetItem) { return assetItem.id; } +async function getRequestHeadersSearchCollections() { + const token = await getBearerToken(); + return { + 'Content-Type': 'application/json', + 'x-api-key': getBackendApiKey(), + Authorization: token, + 'x-adobe-accept-experimental': '1', + 'x-adp-request': 'search', + }; +} + async function getRequestHeaders() { + const token = await getBearerToken(); + return { 'Content-Type': 'application/json', 'x-api-key': await getAssetHandlerApiKey(), Authorization: token, + 'X-Adobe-Accept-Experimental': '1', }; } -async function getRequestHeadersWithIfMatch(etag) { +async function getRequestHeadersWithEtag(etag) { + const token = await getBearerToken(); + return { 'Content-Type': 'application/json', 'x-api-key': await getAssetHandlerApiKey(), Authorization: token, + 'X-Adobe-Accept-Experimental': '1', + 'If-Match': etag, + }; +} + +async function getRequestHeadersWithIfMatchPatchJSON(etag) { + const token = await getBearerToken(); + return { + 'Content-Type': 'application/json-patch+json', + 'x-api-key': await getAssetHandlerApiKey(), + Authorization: token, + 'X-Adobe-Accept-Experimental': '1', 'If-Match': etag, }; } + + /** * Constructs and returns the base URL for collections. * @@ -55,6 +88,26 @@ export function getBaseCollectionsUrl() { return `${getDeliveryEnvironment()}/adobe/collections`; } +/** + * Constructs and returns the base URL for search collections. + * + * @returns {string} The search collections URL. + */ +export function getSearchCollectionsUrl() { + return `${getDeliveryEnvironment()}/adobe/assets/search`; +} + + + +/** + * Constructs and returns the base URL for assets collections. (New) + * + * @returns {string} The base collections URL. + */ +export function getBaseAssetsCollectionsUrl() { + return `${getDeliveryEnvironment()}/adobe/assets/collections`; +} + /** * Retrieves a collection by its unique identifier. * @@ -64,18 +117,22 @@ export function getBaseCollectionsUrl() { */ export async function getCollection(collectionId) { try { + + const options = { method: 'GET', headers: await getRequestHeaders(), }; - const response = await fetch(`${getBaseCollectionsUrl()}/${collectionId}`, options); + const response = await fetch(`${getBaseAssetsCollectionsUrl()}/${collectionId}/items`, options); // Handle response codes if (response.status === 200) { - // Collection retrieved successfully + const responseBody = await response.json(); - responseBody.etag = response.headers.get('If-none-match'); + + responseBody.etag = response.headers.get('Etag'); + responseBody.title = responseBody.self[0].collectionMetadata?.title ?? ''; return responseBody; } if (response.status === 404) { // Handle 404 error @@ -110,7 +167,7 @@ export async function createCollection(title, description, items) { body: JSON.stringify({ title, description, items }), }; - const response = await fetch(`${getBaseCollectionsUrl()}`, options); + const response = await fetch(`${getBaseAssetsCollectionsUrl()}`, options); // Handle response codes if (response.status === 200) { @@ -148,14 +205,15 @@ export async function createCollection(title, description, items) { } /** - * Lists collections with optional limit and cursor parameters. + * Search Lists collections with optional limit and page parameters. * * @param {number} - The optional maximum number of collections to retrieve. - * @param {string} - The optional cursor for paginating the results. + * @param {number} - The optional page number for paginating the results. * @returns {Promise} A promise that resolves with a list of collections. * @throws {Error} If an HTTP error or network error occurs. */ -export async function listCollection(limit = undefined, cursor = '') { + +export async function searchListCollection(limit = undefined, page = 0) { // Construct the query parameters const queryParams = new URLSearchParams(); @@ -163,18 +221,38 @@ export async function listCollection(limit = undefined, cursor = '') { queryParams.append('limit', limit); } - if (cursor) { - queryParams.append('cursor', cursor); + if (page) { + queryParams.append('page', page); } + const adminConfig = await getAdminConfig(); + const indexName = adminConfig.searchCollectionIndex; + const data = { + requests: [ + { + indexName: indexName, + params: { + facets: [], + highlightPostTag: '__/ais-highlight__', + highlightPreTag: '__ais-highlight__', + hitsPerPage: limit, + page: page, + query: '', + tagFilters: '' + } + } + ] + }; + const options = { - method: 'GET', - headers: await getRequestHeaders(), + method: 'POST', + headers: await getRequestHeadersSearchCollections(), + body: JSON.stringify(data), }; // Include the query parameters in the URL const queryString = queryParams.toString(); - const url = `${getBaseCollectionsUrl()}${queryString ? `?${queryString}` : ''}`; + const url = `${getSearchCollectionsUrl()}${queryString ? `?${queryString}` : ''}`; try { const response = await fetch(url, options); @@ -182,12 +260,25 @@ export async function listCollection(limit = undefined, cursor = '') { if (response.status === 200) { // Collection retrieved successfully const responseBody = await response.json(); - return responseBody; + + // Transform the data + const transformedData = { + page: responseBody.results[0].page, + nbHits: responseBody.results[0].nbHits, + nbPages: responseBody.results[0].nbPages, + items: responseBody.results[0].hits.map(hit => ({ + id: hit.collectionId, + title: hit.collectionMetadata.metadata?.title ?? hit.collectionMetadata.title, + description: hit.collectionMetadata.metadata?.description ?? hit.collectionMetadata.description + })) + }; + + return transformedData; } // Handle other response codes - throw new Error(`Failed to list collection: ${response.status} ${response.statusText}`); + throw new Error(`Failed to search list collection: ${response.status} ${response.statusText}`); } catch (error) { - logError('listCollection', error); + logError('searchListCollection', error); throw error; } } @@ -204,47 +295,50 @@ export async function listCollection(limit = undefined, cursor = '') { */ export async function patchCollection(collectionId, etag, addOperation = '', deleteOperation = '') { try { + const patchOperations = []; if (addOperation) { for (const op of addOperation) { - op.op = 'add'; - patchOperations.push(op); + patchOperations.push({ 'op': 'add', 'id': op.value.id, 'type': 'asset' }); } } if (deleteOperation) { for (const op of deleteOperation) { - op.op = 'remove'; - patchOperations.push(op); + patchOperations.push({ 'op': 'remove', 'id': op.value.id, 'type': 'asset' }); } } + const options = { - method: 'PATCH', - headers: await getRequestHeadersWithIfMatch(etag), + method: 'POST', + headers: await getRequestHeadersWithIfMatchPatchJSON(etag), body: JSON.stringify(patchOperations), }; - const response = await fetch(`${getBaseCollectionsUrl()}/${collectionId}`, options); - if (response.status === 200) { - const responseBody = await response.json(); + const response = await fetch(`${getBaseAssetsCollectionsUrl()}/${collectionId}/items`, options); + + if (response.status === 200 || response.status === 204) { + + const responseBody = await getCollection(collectionId); + + const collectionData = responseBody.self[0]; if (addOperation) { const assetsArray = addOperation.map((obj) => ({ assetId: obj.value.id, assetName: obj.value.name })); const collectionDetails = { - collectionName: responseBody.title, - collectionId: responseBody.id, + collectionName: collectionData.collectionMetadata.title, + collectionId: collectionData.id, assets: assetsArray, }; emitEvent(document.documentElement, EventNames.ADD_TO_COLLECTION, collectionDetails); } else if (deleteOperation) { const assetsArray = deleteOperation.map((obj) => ({ assetId: obj.value.id, assetName: obj.value.name })); const collectionDetails = { - collectionName: responseBody.title, - collectionId: responseBody.id, + collectionName: collectionData.collectionMetadata.title, + collectionId: collectionData.id, assets: assetsArray, }; emitEvent(document.documentElement, EventNames.DELETE_FROM_COLLECTION, collectionDetails); } - return responseBody; } if (response.status === 400) { // Handle 400 error @@ -271,17 +365,20 @@ export async function patchCollection(collectionId, etag, addOperation = '', del * Deletes a collection. * * @param {string} collectionId - The unique identifier of the collection to delete. + * @param {string} collectionName - Collection name used for emit delete event. + * @param {string} etag - Used for in the header for the delete operation. * @returns {Promise} A promise that resolves when the collection is updated. * @throws {Error} If an HTTP error or network error occurs. */ -export async function deleteCollection(collectionId, collectionName) { +export async function deleteCollection(collectionId, collectionName, etag) { try { + const options = { method: 'DELETE', - headers: await getRequestHeaders(), + headers: await getRequestHeadersWithEtag(etag), }; - const response = await fetch(`${getBaseCollectionsUrl()}/${collectionId}`, options); + const response = await fetch(`${getBaseAssetsCollectionsUrl()}/${collectionId}`, options); if (response.status !== 204) { throw new Error(`Failed to delete collection: ${response.status} ${response.statusText}`);