Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Extend layerSpecificExport for APIs #2053

Open
wants to merge 10 commits into
base: master
Choose a base branch
from
1 change: 1 addition & 0 deletions css/svg/material-icons.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
4 changes: 4 additions & 0 deletions scss/_infowindow.scss
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
16 changes: 15 additions & 1 deletion src/infowindow.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ let exportContainer;
let groupFooterContainer;
let sublists;
let subexports;
let subexportResponses;
let urvalElements;
let footerContainers;
let expandableContents;
Expand Down Expand Up @@ -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) => {
Expand Down Expand Up @@ -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) {
Expand Down Expand Up @@ -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();
Expand Down
164 changes: 122 additions & 42 deletions src/infowindow_exporthandler.js
Original file line number Diff line number Diff line change
Expand Up @@ -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<string>} 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 `<img class="pointer" src="${url}"></img>`;
} 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 `<span>${responseText}</span>`;
} else if (contentType.startsWith('application/json')) {
return `<pre><code>${responseText}</code></pre>`;
}
}
return '<span>Unsupported response Content-Type</span>';
} 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.');
}
Expand Down Expand Up @@ -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: {
Expand Down Expand Up @@ -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');
Expand All @@ -268,10 +348,12 @@ function createExportButtons(
const selectedItems = selectionManager.getSelectedItemsForASelectionGroup(selectionGroup);
layerSpecificExportHandler(
url,
requestMethod,
urlParameters,
activeLayer,
selectedItems,
attributesToSendToExport,
layerSpecificExportedFileName
exportedFileName
)
.then((data) => {
if (data) {
Expand All @@ -285,6 +367,9 @@ function createExportButtons(
default:
break;
}
if (requestMethod === 'GET' && displayExportResponse) {
responseHandler(selectionGroup, data);
}
}
btn.loadStop();
})
Expand All @@ -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.
Expand All @@ -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;
Expand Down
Loading