diff --git a/css/svg/material-icons.svg b/css/svg/material-icons.svg index 9cc5d6245..001660226 100644 --- a/css/svg/material-icons.svg +++ b/css/svg/material-icons.svg @@ -62,4 +62,5 @@ + diff --git a/scss/_infowindow.scss b/scss/_infowindow.scss index 3325a9ec3..12a1ce376 100644 --- a/scss/_infowindow.scss +++ b/scss/_infowindow.scss @@ -239,6 +239,10 @@ text-align: left; } +.export-response-container { + margin: 0.5em 0.5em 0.5em 3.6em; +} + .toaster { border-radius: 0.5em; box-shadow: 0 0 8px #888; diff --git a/src/infowindow.js b/src/infowindow.js index 8b3210841..7d684cd5c 100644 --- a/src/infowindow.js +++ b/src/infowindow.js @@ -10,6 +10,7 @@ let exportContainer; let groupFooterContainer; let sublists; let subexports; +let subexportResponses; let urvalElements; let footerContainers; let expandableContents; @@ -203,6 +204,8 @@ function showSelectedList(selectionGroup) { } const subexportToAppend = subexports.get(selectionGroup); exportContainer.appendChild(subexportToAppend); + const subexportResponseToAppend = subexportResponses.get(selectionGroup); + exportContainer.appendChild(subexportResponseToAppend); selectionManager.clearHighlightedFeatures(); selectionManager.refreshAllLayers(); urvalElements.forEach((value, key) => { @@ -238,8 +241,18 @@ function createUrvalElement(selectionGroup, selectionGroupTitle) { const footerContainer = document.createElement('div'); footerContainers.set(selectionGroup, footerContainer); - const subexportComponent = createSubexportComponent({ selectionGroup, viewer, exportOptions }); + // Updates the response content for the given selectionGroup + const responseHandler = function responseHandler(responseSelectionGroup, text) { + const responseContainer = subexportResponses.get(responseSelectionGroup); + responseContainer.innerHTML = text; + }; + + const subexportComponent = createSubexportComponent({ selectionGroup, viewer, exportOptions, responseHandler }); subexports.set(selectionGroup, subexportComponent); + + const subexportResponseComponent = document.createElement('div'); + subexportResponseComponent.classList.add('export-response-container'); + subexportResponses.set(selectionGroup, subexportResponseComponent); } function createExpandableContent(listElementContentContainer, content, elementId) { @@ -427,6 +440,7 @@ function init(options) { exportOptions = infowindowOptions.export || {}; sublists = new Map(); subexports = new Map(); + subexportResponses = new Map(); urvalElements = new Map(); expandableContents = new Map(); footerContainers = new Map(); diff --git a/src/infowindow_exporthandler.js b/src/infowindow_exporthandler.js index a10dcd0ba..bfb127684 100644 --- a/src/infowindow_exporthandler.js +++ b/src/infowindow_exporthandler.js @@ -52,7 +52,46 @@ export function simpleExportHandler(simpleExportUrl, activeLayer, selectedItems, }); } -export function layerSpecificExportHandler(url, activeLayer, selectedItems, attributesToSendToExport, exportedFileName) { +/** + * Makes a HEAD request to find out what the content type of the response will be, so that the + * response can be handled accordingly. Non-images will be blocked by CORS restrictions unless + * the server/API is set to allow the client's origin. + * @param {string} url The url to fetch + * @returns {Promise} HTML content containing the response + */ +async function fetchByContentTypes(url) { + try { + // Perform the HEAD request to check response content type + const headResponse = await fetch(url, { method: 'HEAD' }); + if (!headResponse.ok) { + throw new Error(`HEAD request failed with status: ${headResponse.status}`); + } + // Get the content-type header + const contentType = headResponse.headers.get('Content-Type'); + + // Generate content to display based on Content-Type + if (contentType.startsWith('image/')) { + return ``; + } else if (contentType.startsWith('text/plain') || contentType.startsWith('application/json')) { + const getResponse = await fetch(url, { method: 'GET' }); + if (!getResponse.ok) { + throw new Error(`GET request failed with status: ${getResponse.status}`); + } + const responseText = await getResponse.text(); + if (contentType.startsWith('text/plain')) { + return `${responseText}`; + } else if (contentType.startsWith('application/json')) { + return `
${responseText}
`; + } + } + return 'Unsupported response Content-Type'; + } catch (err) { + console.error(err); + throw err; + } +} + +export function layerSpecificExportHandler(url, requestMethod, urlParameters, activeLayer, selectedItems, attributesToSendToExport, exportedFileName) { if (!url) { throw new Error('Export URL is not specified.'); } @@ -80,8 +119,39 @@ export function layerSpecificExportHandler(url, activeLayer, selectedItems, attr features[layerName].push(obj); }); + // Generates the request URL using the urlParameters config option. + // Keys and values are translated to url parameters as is, except when a value is an object with + // an "attribute" property, in which case the value will be a list of the values from the + // corresponding attribute of the selectedItems. Unless specified with a "separator" property, + // the list will be separated by semicolons. + // Specifying a value as "{{no_value}}" will add a valueless parameter, e g "?Param1&Param2&etc". + let requestUrl = url; + const requestParams = { ...urlParameters }; + if (requestParams) { + Object.keys(requestParams).forEach((param) => { + if (requestParams[param] && typeof requestParams[param] === 'object' && requestParams[param].attribute) { + const attributeValues = []; + selectedItems.forEach((item) => { + const attributeValue = item.getFeature().get(requestParams[param].attribute); + if (attributeValue) { + attributeValues.push(attributeValue); + } + }); + requestParams[param] = attributeValues.join(requestParams[param].separator || ';'); + } + }); + requestUrl = new URL(url); + requestUrl.search = new URLSearchParams(requestParams); + requestUrl = requestUrl.toString().replace(/=%7B%7Bno_value%7D%7D/gm, ''); + } + + if (requestMethod === 'OPEN') { + return window.open(requestUrl, '_blank') ? Promise.resolve() : Promise.reject(); + } else if (requestMethod === 'GET') { + return fetchByContentTypes(requestUrl); + } // eslint-disable-next-line consistent-return - return fetch(url, { + return fetch(requestUrl, { method: 'POST', // or 'PUT' body: JSON.stringify(features), // data can be `string` or {object}! headers: { @@ -239,23 +309,33 @@ function createToaster(status, exportOptions, message) { function createExportButtons( obj, + buttonPerLayer, + requestMethodPerLayer, + urlParametersPerLayer, attributesToSendToExportPerLayer, + exportedFileNamePerLayer, + displayExportResponsePerLayer, selectionGroup, activeLayer, selectionManager, - exportOptions + exportOptions, + responseHandler ) { - const roundButton = obj.button.roundButton || false; - const buttonText = obj.button.buttonText || defaultText; const url = obj.url; - const layerSpecificExportedFileName = obj.exportedFileName; - const attributesToSendToExport = obj.attributesToSendToExport - ? obj.attributesToSendToExport - : attributesToSendToExportPerLayer; + const buttonText = obj.button?.buttonText || buttonPerLayer?.buttonText || defaultText; + const roundButton = obj.button?.roundButton ?? buttonPerLayer?.roundButton ?? false; + const roundButtonIcon = obj.button?.roundButtonIcon || buttonPerLayer?.roundButtonIcon || defaultIcon; + const roundButtonTooltipText = obj.button?.roundButtonTooltipText || buttonPerLayer?.roundButtonTooltipText || defaultText; + const requestMethod = obj.requestMethod || requestMethodPerLayer || 'POST_JSON'; + const urlParameters = obj.urlParameters || urlParametersPerLayer; + const attributesToSendToExport = obj.attributesToSendToExport || attributesToSendToExportPerLayer; + const exportedFileName = obj.exportedFileName || exportedFileNamePerLayer; + const displayExportResponse = obj.displayExportResponse || displayExportResponsePerLayer || false; + const exportBtn = roundButton ? createCustomExportButton( - obj.button.roundButtonIcon || defaultIcon, - obj.button.roundButtonTooltipText || defaultText + roundButtonIcon, + roundButtonTooltipText ) : createExportButton(buttonText); const btn = exportBtn.querySelector('button'); @@ -268,10 +348,12 @@ function createExportButtons( const selectedItems = selectionManager.getSelectedItemsForASelectionGroup(selectionGroup); layerSpecificExportHandler( url, + requestMethod, + urlParameters, activeLayer, selectedItems, attributesToSendToExport, - layerSpecificExportedFileName + exportedFileName ) .then((data) => { if (data) { @@ -285,6 +367,9 @@ function createExportButtons( default: break; } + if (requestMethod === 'GET' && displayExportResponse) { + responseHandler(selectionGroup, data); + } } btn.loadStop(); }) @@ -297,7 +382,7 @@ function createExportButtons( return exportBtn; } -export function createSubexportComponent({ selectionGroup, viewer, exportOptions }) { +export function createSubexportComponent({ selectionGroup, viewer, exportOptions, responseHandler }) { viewerId = viewer.getId(); const selectionManager = viewer.getSelectionManager(); // OBS! selectionGroup corresponds to a layer with the same name in most cases, but in case of a group layer it can contain selected items from all the layers in that GroupLayer. @@ -314,36 +399,31 @@ export function createSubexportComponent({ selectionGroup, viewer, exportOptions } if (layerSpecificExportOptions) { const exportUrls = layerSpecificExportOptions.exportUrls || []; + const buttonPerLayer = layerSpecificExportOptions.button; + const requestMethodPerLayer = layerSpecificExportOptions.requestMethod; + const urlParametersPerLayer = layerSpecificExportOptions.urlParameters; const attributesToSendToExportPerLayer = layerSpecificExportOptions.attributesToSendToExport; - const customButtonExportUrls = exportUrls.filter( - (e) => e.button.roundButton - ); - const standardButtonExportUrls = exportUrls.filter( - (e) => !e.button.roundButton - ); - - customButtonExportUrls.forEach((obj) => { - const button = createExportButtons( - obj, - attributesToSendToExportPerLayer, - selectionGroup, - activeLayer, - selectionManager, - exportOptions - ); - subexportContainer.appendChild(button); - }); - standardButtonExportUrls.forEach((obj) => { - const button = createExportButtons( - obj, - attributesToSendToExportPerLayer, - selectionGroup, - activeLayer, - selectionManager, - exportOptions - ); - subexportContainer.appendChild(button); - }); + const exportedFileNamePerLayer = layerSpecificExportOptions.exportedFileName; + const displayExportResponsePerLayer = layerSpecificExportOptions.displayExportResponse; + + exportUrls.sort((exportUrl) => (exportUrl.button.roundButton ? -1 : 1)) + .forEach((obj) => { + const button = createExportButtons( + obj, + buttonPerLayer, + requestMethodPerLayer, + urlParametersPerLayer, + attributesToSendToExportPerLayer, + exportedFileNamePerLayer, + displayExportResponsePerLayer, + selectionGroup, + activeLayer, + selectionManager, + exportOptions, + responseHandler + ); + subexportContainer.appendChild(button); + }); } if (exportOptions.simpleExport && exportOptions.simpleExport.url) { const simpleExport = exportOptions.simpleExport;