diff --git a/.github/ISSUE_TEMPLATE/release_steps.md b/.github/ISSUE_TEMPLATE/release_steps.md index f6a2208c43..e2b5519299 100644 --- a/.github/ISSUE_TEMPLATE/release_steps.md +++ b/.github/ISSUE_TEMPLATE/release_steps.md @@ -88,6 +88,7 @@ If stable release (YYYY.XX.00) follow these sub-steps: - [ ] `mapstore-printing.zip` on github release - [ ] Publish the release - [ ] create on [ReadTheDocs](https://readthedocs.org/projects/mapstore/) project the version build for `vYYYY.XX.mm` (click on "Versions" and activate the version of the tag, created when release was published) +- [ ] Update `Default version` to point the release version in the `Advanced Settings` menu of the [ReadTheDocs](https://readthedocs.org/dashboard/mapstore/advanced/) admin panel ## Build and publish MapStoreExtension release diff --git a/package.json b/package.json index 194f6bd9d7..3a9c09df72 100644 --- a/package.json +++ b/package.json @@ -220,7 +220,7 @@ "lodash": "4.17.21", "lrucache": "1.0.3", "md5": "2.3.0", - "moment": "2.21.0", + "moment": "2.29.4", "node-geo-distance": "1.2.0", "object-assign": "4.1.1", "object-fit-images": "3.2.4", diff --git a/web/client/actions/__tests__/catalog-test.js b/web/client/actions/__tests__/catalog-test.js index b50ae764b2..17fdf54de5 100644 --- a/web/client/actions/__tests__/catalog-test.js +++ b/web/client/actions/__tests__/catalog-test.js @@ -238,6 +238,13 @@ describe('Test correctness of the catalog actions', () => { expect(retval).toExist(); expect(retval.type).toBe(ADD_SERVICE); }); + it('addService with options', () => { + const options = {"test": "1"}; + var retval = addService(options); + expect(retval).toExist(); + expect(retval.type).toBe(ADD_SERVICE); + expect(retval.options).toEqual(options); + }); it('addCatalogService', () => { var retval = addCatalogService(service); diff --git a/web/client/actions/__tests__/details-test.js b/web/client/actions/__tests__/details-test.js index a0474fa074..254b876ada 100644 --- a/web/client/actions/__tests__/details-test.js +++ b/web/client/actions/__tests__/details-test.js @@ -28,13 +28,21 @@ describe('details actions tests', () => { const a = closeDetailsPanel(); expect(a.type).toBe(CLOSE_DETAILS_PANEL); }); - it('detailsLoaded', () => { + it('detailsLoaded for map', () => { const mapId = 1; const detailsUri = "sada/da/"; const a = detailsLoaded(mapId, detailsUri); expect(a.type).toBe(DETAILS_LOADED); expect(a.detailsUri).toBe(detailsUri); - expect(a.mapId).toBe(mapId); + expect(a.id).toBe(mapId); + }); + it('detailsLoaded for dashboard', () => { + const dashboardId = 1; + const detailsUri = "sada/da/"; + const a = detailsLoaded(dashboardId, detailsUri); + expect(a.type).toBe(DETAILS_LOADED); + expect(a.detailsUri).toBe(detailsUri); + expect(a.id).toBe(dashboardId); }); it('updateDetails', () => { const a = updateDetails('text'); diff --git a/web/client/actions/catalog.js b/web/client/actions/catalog.js index d4fd402bc2..41147acc91 100644 --- a/web/client/actions/catalog.js +++ b/web/client/actions/catalog.js @@ -45,6 +45,7 @@ export const SAVING_SERVICE = 'CATALOG:SAVING_SERVICE'; export const CATALOG_INITED = 'CATALOG:INIT'; export const GET_METADATA_RECORD_BY_ID = 'CATALOG:GET_METADATA_RECORD_BY_ID'; export const SET_LOADING = 'CATALOG:SET_LOADING'; +export const SHOW_FORMAT_ERROR = 'CATALOG:SHOW_FORMAT_ERROR'; export const TOGGLE_TEMPLATE = 'CATALOG:TOGGLE_TEMPLATE'; export const TOGGLE_THUMBNAIL = 'CATALOG:TOGGLE_THUMBNAIL'; export const TOGGLE_ADVANCED_SETTINGS = 'CATALOG:TOGGLE_ADVANCED_SETTINGS'; @@ -170,9 +171,10 @@ export function changeUrl(url) { url }; } -export function addService() { +export function addService(options) { return { - type: ADD_SERVICE + type: ADD_SERVICE, + options }; } export function addCatalogService(service) { @@ -284,6 +286,7 @@ export const toggleThumbnail = () => ({type: TOGGLE_THUMBNAIL}); export const formatOptionsFetch = (url, force) => ({type: FORMAT_OPTIONS_FETCH, url, force}); export const formatsLoading = (loading) => ({type: FORMAT_OPTIONS_LOADING, loading}); export const setSupportedFormats = (formats, url) => ({type: SET_FORMAT_OPTIONS, formats, url}); +export const showFormatError = (status) => ({type: SHOW_FORMAT_ERROR, status}); import {error} from './notifications'; diff --git a/web/client/actions/details.js b/web/client/actions/details.js index a5f4c288da..83de607bb7 100644 --- a/web/client/actions/details.js +++ b/web/client/actions/details.js @@ -17,9 +17,9 @@ export const NO_DETAILS_AVAILABLE = "NO_DETAILS_AVAILABLE"; * @memberof actions.details * @return {action} type `UPDATE_DETAILS` */ -export const updateDetails = (detailsText) => ({ +export const updateDetails = (detailsText, resourceId) => ({ type: UPDATE_DETAILS, - detailsText + detailsText, id: resourceId }); /** @@ -27,9 +27,9 @@ export const updateDetails = (detailsText) => ({ * @memberof actions.details * @return {action} type `DETAILS_LOADED` */ -export const detailsLoaded = (mapId, detailsUri, detailsSettings) => ({ +export const detailsLoaded = (resourceId, detailsUri, detailsSettings) => ({ type: DETAILS_LOADED, - mapId, + id: resourceId, detailsUri, detailsSettings }); diff --git a/web/client/api/catalog/COG.js b/web/client/api/catalog/COG.js index a566aa4813..a7de6e4814 100644 --- a/web/client/api/catalog/COG.js +++ b/web/client/api/catalog/COG.js @@ -8,8 +8,9 @@ import get from 'lodash/get'; import isEmpty from 'lodash/isEmpty'; +import isNil from 'lodash/isNil'; import { Observable } from 'rxjs'; -import { fromUrl } from 'geotiff'; +import { fromUrl as fromGeotiffUrl } from 'geotiff'; import { isValidURL } from '../../utils/URLUtils'; import ConfigUtils from '../../utils/ConfigUtils'; @@ -57,8 +58,25 @@ export const getProjectionFromGeoKeys = (image) => { return null; }; +const abortError = (reject) => reject(new DOMException("Aborted", "AbortError")); +/** + * fromUrl with abort fetching of data and data slices + * Note: The abort action will not cancel data fetch request but just the promise, + * because of the issue in https://github.com/geotiffjs/geotiff.js/issues/408 + */ +const fromUrl = (url, signal) => { + if (signal?.aborted) { + return abortError(Promise.reject); + } + return new Promise((resolve, reject) => { + signal?.addEventListener("abort", () => abortError(reject)); + return fromGeotiffUrl(url) + .then((image)=> image.getImage()) // Fetch and read first image to get medatadata of the tif + .then((image) => resolve(image)) + .catch(()=> abortError(reject)); + }); +}; let capabilitiesCache = {}; - export const getRecords = (_url, startPosition, maxRecords, text, info = {}) => { const service = get(info, 'options.service'); let layers = []; @@ -73,29 +91,43 @@ export const getRecords = (_url, startPosition, maxRecords, text, info = {}) => sources: [{url}], options: service.options || {} }; - if (service.fetchMetadata) { + const controller = get(info, 'options.controller'); + const isSave = get(info, 'options.save', false); + // Fetch metadata only on saving the service (skip on search) + if ((isNil(service.fetchMetadata) || service.fetchMetadata) && isSave) { const cached = capabilitiesCache[url]; if (cached && new Date().getTime() < cached.timestamp + (ConfigUtils.getConfigProp('cacheExpire') || 60) * 1000) { return {...cached.data}; } - return fromUrl(url) - .then(geotiff => geotiff.getImage()) + return fromUrl(url, controller?.signal) .then(image => { const crs = getProjectionFromGeoKeys(image); const extent = image.getBoundingBox(); const isProjectionDefined = isProjectionAvailable(crs); layer = { ...layer, + sourceMetadata: { + crs, + extent: extent, + width: image.getWidth(), + height: image.getHeight(), + tileWidth: image.getTileWidth(), + tileHeight: image.getTileHeight(), + origin: image.getOrigin(), + resolution: image.getResolution() + }, // skip adding bbox when geokeys or extent is empty - ...(!isEmpty(extent) && !isEmpty(crs) && isProjectionDefined && { + ...(!isEmpty(extent) && !isEmpty(crs) && { bbox: { crs, - bounds: { - minx: extent[0], - miny: extent[1], - maxx: extent[2], - maxy: extent[3] - } + ...(isProjectionDefined && { + bounds: { + minx: extent[0], + miny: extent[1], + maxx: extent[2], + maxy: extent[3] + }} + ) } }) }; diff --git a/web/client/api/geofence/RuleService.js b/web/client/api/geofence/RuleService.js index 5238140f09..a376f0a42d 100644 --- a/web/client/api/geofence/RuleService.js +++ b/web/client/api/geofence/RuleService.js @@ -43,6 +43,8 @@ const normalizeKey = (key) => { return 'userName'; case 'rolename': return 'groupName'; + case 'roleAny': + return 'groupAny'; default: return key; } diff --git a/web/client/components/TOC/DefaultLayer.jsx b/web/client/components/TOC/DefaultLayer.jsx index 254d2fe511..2fb8f61ada 100644 --- a/web/client/components/TOC/DefaultLayer.jsx +++ b/web/client/components/TOC/DefaultLayer.jsx @@ -10,7 +10,7 @@ import React from 'react'; import PropTypes from 'prop-types'; import Node from './Node'; -import { isObject, castArray, find } from 'lodash'; +import { isObject, castArray, find, isNil } from 'lodash'; import { Grid, Row, Col, Glyphicon } from 'react-bootstrap'; import draggableComponent from './enhancers/draggableComponent'; import VisibilityCheck from './fragments/VisibilityCheck'; @@ -23,6 +23,7 @@ import tooltip from '../misc/enhancers/tooltip'; import localizedProps from '../misc/enhancers/localizedProps'; import { isInsideResolutionsLimits, getLayerTypeGlyph } from '../../utils/LayersUtils'; import StyleBasedLegend from './fragments/StyleBasedLegend'; +import { isSRSAllowed } from '../../utils/CoordinatesUtils'; const GlyphIndicator = localizedProps('tooltip')(tooltip(Glyphicon)); @@ -106,6 +107,17 @@ class DefaultLayer extends React.Component { : 'toc.notVisibleZoomOut'; }; + getErrorTooltipParams = () => { + if (!this.isCRSCompatible()) { + return { + tooltip: "toc.sourceCRSNotCompatible", + msgParams: {sourceCRS: this.getSourceCRS()} + }; + } + return { tooltip: "toc.loadingerror" }; + } + getSourceCRS = () => this.props.node?.bbox?.crs || this.props.node?.sourceMetadata?.crs; + renderOpacitySlider = (hideOpacityTooltip) => { return (this.props.activateOpacityTool && this.props.node?.type !== '3dtiles') ? ( { - return this.props.node.loadingError === 'Error' ? + return this.isLayerError() ? () : ( { - return this.props.node.loadingError === 'Error' || isEmpty ? + return this.isLayerError() || isEmpty ? null : ( {other.isDraggable && !isDummy ? this.props.connectDragPreview(head) : head} - {isDummy || !this.props.activateOpacityTool || this.props.node.expanded || !this.props.node.visibility || this.props.node.loadingError === 'Error' ? null : this.renderOpacitySlider(this.props.hideOpacityTooltip)} + {isDummy || !this.props.activateOpacityTool || this.props.node.expanded || !this.props.node.visibility || this.isLayerError() ? null : this.renderOpacitySlider(this.props.hideOpacityTooltip)} {isDummy || isEmpty ? null : this.renderCollapsible()} ); @@ -212,7 +224,7 @@ class DefaultLayer extends React.Component { let {children, propertiesChangeHandler, onToggle, connectDragSource, connectDropTarget, ...other } = this.props; const hide = !this.props.node.visibility || this.props.node.invalid || this.props.node.exclusiveMapType || !isInsideResolutionsLimits(this.props.node, this.props.resolution) ? ' visibility' : ''; const selected = this.props.selectedNodes.filter((s) => s === this.props.node.id).length > 0 ? ' selected' : ''; - const error = this.props.node.loadingError === 'Error' ? ' layer-error' : ''; + const error = this.isLayerError() ? ' layer-error' : ''; const warning = this.props.node.loadingError === 'Warning' ? ' layer-warning' : ''; const grab = other.isDraggable ? : ; const isDummy = !!this.props.node.dummy; @@ -234,6 +246,13 @@ class DefaultLayer extends React.Component { return (title || '').toLowerCase().indexOf(this.props.filterText.toLowerCase()) !== -1; }; + isLayerError = () => this.props.node.loadingError === 'Error' || !this.isCRSCompatible(); + + isCRSCompatible = () => { + const CRS = this.getSourceCRS(); + // Check if source crs is compatible + return !isNil(CRS) ? isSRSAllowed(CRS) : true; + } } export default draggableComponent('LayerOrGroup', DefaultLayer); diff --git a/web/client/components/TOC/__tests__/DefaultLayer-test.jsx b/web/client/components/TOC/__tests__/DefaultLayer-test.jsx index f68aace8dd..2c9ca943d1 100644 --- a/web/client/components/TOC/__tests__/DefaultLayer-test.jsx +++ b/web/client/components/TOC/__tests__/DefaultLayer-test.jsx @@ -466,4 +466,43 @@ describe('test DefaultLayer module component', () => { expect(button.length).toBe(1); } }); + it('test with layer source crs', () => { + // Invalid CRS + let node = { + name: 'layer00', + title: 'Layer', + visibility: false, + storeIndex: 9, + opacity: 0.5, + bbox: { + crs: "EPSG:3946" + } + }; + + let comp = ReactDOM.render(, document.getElementById("container")); + expect(ReactDOM.findDOMNode(comp)).toBeTruthy(); + let layerNode = document.querySelector('.toc-default-layer.layer-error'); + let errorTooltip = document.querySelector('.toc-layer-tool.toc-error'); + expect(layerNode).toBeTruthy(); + expect(errorTooltip).toBeTruthy(); + + // Valid CRS + node = { + name: 'layer00', + title: 'Layer', + visibility: false, + storeIndex: 9, + opacity: 0.5, + bbox: { + crs: "EPSG:4326" + } + }; + + comp = ReactDOM.render(, document.getElementById("container")); + expect(ReactDOM.findDOMNode(comp)).toBeTruthy(); + layerNode = document.querySelector('.toc-default-layer.layer-error'); + errorTooltip = document.querySelector('.toc-layer-tool.toc-error'); + expect(layerNode).toBeFalsy(); + expect(errorTooltip).toBeFalsy(); + }); }); diff --git a/web/client/components/TOC/fragments/LayersTool.jsx b/web/client/components/TOC/fragments/LayersTool.jsx index 2f81933b45..b2b8116d68 100644 --- a/web/client/components/TOC/fragments/LayersTool.jsx +++ b/web/client/components/TOC/fragments/LayersTool.jsx @@ -21,6 +21,7 @@ class LayersTool extends React.Component { style: PropTypes.object, glyph: PropTypes.string, tooltip: PropTypes.string, + msgParams: PropTypes.object, className: PropTypes.string }; @@ -34,7 +35,7 @@ class LayersTool extends React.Component { glyph={this.props.glyph} onClick={() => this.props.onClick(this.props.node)}/>); return this.props.tooltip ? - )}> + )}> {tool} : tool; diff --git a/web/client/components/catalog/CatalogServiceEditor.jsx b/web/client/components/catalog/CatalogServiceEditor.jsx index b4e747e75b..351000fd6d 100644 --- a/web/client/components/catalog/CatalogServiceEditor.jsx +++ b/web/client/components/catalog/CatalogServiceEditor.jsx @@ -17,7 +17,20 @@ import Message from "../I18N/Message"; import AdvancedSettings from './editor/AdvancedSettings'; import MainForm from './editor/MainForm'; -export default ({ +const withAbort = (Component) => { + return (props) => { + const [abortController, setAbortController] = useState(null); + const onSave = () => { + // Currently abort request on saving is applicable only for COG service + const controller = props.format === 'cog' ? new AbortController() : null; + setAbortController(controller); + return props.onAddService({save: true, controller}); + }; + const onCancel = () => abortController && props.saving ? abortController?.abort() : props.onChangeCatalogMode("view"); + return ; + }; +}; +const CatalogServiceEditor = ({ service = { title: "", type: "wms", @@ -33,15 +46,16 @@ export default ({ formatOptions, buttonStyle, saving, + showFormatError, onChangeServiceFormat = () => {}, onChangeMetadataTemplate = () => {}, onToggleAdvancedSettings = () => { }, onChangeServiceProperty = () => {}, onToggleTemplate = () => {}, onToggleThumbnail = () => {}, - onAddService = () => {}, onDeleteService = () => {}, - onChangeCatalogMode = () => {}, + onCancel = () => {}, + onSaveService = () => {}, onFormatOptionsFetch = () => {}, selectedService, isLocalizedLayerStylesEnabled, @@ -50,7 +64,8 @@ export default ({ layerOptions = {}, infoFormatOptions, services, - autoSetVisibilityLimits = false + autoSetVisibilityLimits = false, + disabled }) => { const [valid, setValid] = useState(true); return ( - {service && !service.isNew - ? : null } - @@ -110,3 +126,5 @@ export default ({ ); }; + +export default withAbort(CatalogServiceEditor); diff --git a/web/client/components/catalog/RecordItem.jsx b/web/client/components/catalog/RecordItem.jsx index 7fa87dc45b..b290d35047 100644 --- a/web/client/components/catalog/RecordItem.jsx +++ b/web/client/components/catalog/RecordItem.jsx @@ -14,7 +14,7 @@ import { buildSRSMap, getRecordLinks } from '../../utils/CatalogUtils'; -import {isAllowedSRS} from '../../utils/CoordinatesUtils'; +import { isAllowedSRS, isSRSAllowed } from '../../utils/CoordinatesUtils'; import HtmlRenderer from '../misc/HtmlRenderer'; import {parseCustomTemplate} from '../../utils/TemplateUtils'; import {getMessageById} from '../../utils/LocaleUtils'; @@ -125,6 +125,16 @@ class RecordItem extends React.Component { }; + isSRSNotAllowed = (record) => { + if (record.serviceType !== 'cog') { + const ogcReferences = record.ogcReferences || { SRS: [] }; + const allowedSRS = ogcReferences?.SRS?.length > 0 && buildSRSMap(ogcReferences.SRS); + return allowedSRS && !isAllowedSRS(this.props.crs, allowedSRS); + } + const crs = record?.sourceMetadata?.crs; + return crs && !isSRSAllowed(crs); + } + getButtons = (record) => { const links = this.props.showGetCapLinks ? getRecordLinks(record) : []; return [ @@ -136,9 +146,7 @@ class RecordItem extends React.Component { loading: this.state.loading, glyph: 'plus', onClick: () => { - const ogcReferences = record.ogcReferences || { SRS: [] }; - const allowedSRS = ogcReferences?.SRS?.length > 0 && buildSRSMap(ogcReferences.SRS); - if (allowedSRS && !isAllowedSRS(this.props.crs, allowedSRS)) { + if (this.isSRSNotAllowed(record)) { return this.props.onError('catalog.srs_not_allowed'); } this.setState({ loading: true }); diff --git a/web/client/components/catalog/__tests__/CatalogServiceEditor-test.jsx b/web/client/components/catalog/__tests__/CatalogServiceEditor-test.jsx index 05e2d96463..90865ca442 100644 --- a/web/client/components/catalog/__tests__/CatalogServiceEditor-test.jsx +++ b/web/client/components/catalog/__tests__/CatalogServiceEditor-test.jsx @@ -8,6 +8,7 @@ import React from 'react'; import ReactDOM from 'react-dom'; import expect from 'expect'; +import TestUtils from 'react-dom/test-utils'; import CatalogServiceEditor from '../CatalogServiceEditor'; import {defaultPlaceholder} from "../editor/MainFormUtils"; @@ -57,12 +58,9 @@ describe('Test CatalogServiceEditor', () => { layerOptions={{tileSize: 256}} />, document.getElementById("container")); - const formatFormGroups = [...document.querySelectorAll('.form-group')].filter(fg => { - const labels = [...fg.querySelectorAll('label')]; - return labels.length === 1 && labels[0].textContent === 'layerProperties.format.title'; - }); - expect(formatFormGroups.length).toBe(1); - const formatSelect = formatFormGroups[0].querySelector('.Select-value-label'); + const formatFormGroups = [...document.querySelectorAll('.form-group-flex')]; + expect(formatFormGroups.length).toBe(5); + const formatSelect = formatFormGroups[2].querySelector('.Select-value-label'); expect(formatSelect).toExist(); expect(formatSelect.textContent).toBe('image/png8'); // expect(formatSelect.props.options).toEqual(formatOptions); TODO: test properties are passed to select @@ -149,4 +147,93 @@ describe('Test CatalogServiceEditor', () => { let placeholder = defaultPlaceholder(service); expect(placeholder).toBe("e.g. https://mydomain.com/geoserver/wms"); }); + it('test save and delete button when saving', () => { + ReactDOM.render(, document.getElementById("container")); + let buttons = document.querySelectorAll('.form-group button'); + let saveBtn; let deleteBtn; + buttons.forEach(btn => {if (btn.textContent === 'save') saveBtn = btn;}); + buttons.forEach(btn => {if (btn.textContent === 'catalog.delete') deleteBtn = btn;}); + expect(saveBtn).toBeTruthy(); + expect(deleteBtn).toBeTruthy(); + expect(saveBtn.classList.contains("disabled")).toBeTruthy(); + expect(deleteBtn.classList.contains("disabled")).toBeTruthy(); + }); + it('test saving service for COG type', () => { + const actions = { + onAddService: () => {} + }; + const spyOnAdd = expect.spyOn(actions, 'onAddService'); + ReactDOM.render(, document.getElementById("container")); + let buttons = document.querySelectorAll('.form-group button'); + let saveBtn; + buttons.forEach(btn => {if (btn.textContent === 'save') saveBtn = btn;}); + expect(saveBtn).toBeTruthy(); + TestUtils.Simulate.click(saveBtn); + expect(spyOnAdd).toHaveBeenCalled(); + let arg = spyOnAdd.calls[0].arguments[0]; + expect(arg.save).toBe(true); + expect(arg.controller).toBeTruthy(); + + ReactDOM.render(, document.getElementById("container")); + buttons = document.querySelectorAll('.form-group button'); + buttons.forEach(btn => {if (btn.textContent === 'save') saveBtn = btn;}); + expect(saveBtn).toBeTruthy(); + TestUtils.Simulate.click(saveBtn); + expect(spyOnAdd).toHaveBeenCalled(); + arg = spyOnAdd.calls[1].arguments[0]; + expect(arg.save).toBeTruthy(); + expect(arg.controller).toBeFalsy(); + }); + it('test cancel service', () => { + const actions = { + onChangeCatalogMode: () => {}, + onAddService: () => {} + }; + const spyOnCancel = expect.spyOn(actions, 'onChangeCatalogMode'); + ReactDOM.render(, document.getElementById("container")); + let buttons = document.querySelectorAll('.form-group button'); + let cancelBtn; + buttons.forEach(btn => {if (btn.textContent === 'cancel') cancelBtn = btn;}); + expect(cancelBtn).toBeTruthy(); + TestUtils.Simulate.click(cancelBtn); + expect(spyOnCancel).toHaveBeenCalled(); + let arg = spyOnCancel.calls[0].arguments[0]; + expect(arg).toBe('view'); + + const spyOnAdd = expect.spyOn(actions, 'onAddService'); + ReactDOM.render(, document.getElementById("container")); + buttons = document.querySelectorAll('.form-group button'); + let saveBtn; + buttons.forEach(btn => {if (btn.textContent === 'save') saveBtn = btn;}); + TestUtils.Simulate.click(saveBtn); + expect(spyOnAdd).toHaveBeenCalled(); + + ReactDOM.render(, document.getElementById("container")); + buttons = document.querySelectorAll('.form-group button'); + buttons.forEach(btn => {if (btn.textContent === 'cancel') cancelBtn = btn;}); + TestUtils.Simulate.click(cancelBtn); + expect(spyOnCancel.calls[1]).toBeFalsy(); + }); }); diff --git a/web/client/components/catalog/__tests__/RecordItem-test.jsx b/web/client/components/catalog/__tests__/RecordItem-test.jsx index d4933ce90b..16b8bbd2c0 100644 --- a/web/client/components/catalog/__tests__/RecordItem-test.jsx +++ b/web/client/components/catalog/__tests__/RecordItem-test.jsx @@ -605,7 +605,7 @@ describe('This test for RecordItem', () => { } }; let actionsSpy = expect.spyOn(actions, "onError"); - const item = ReactDOM.render(, document.getElementById("container")); @@ -617,6 +617,22 @@ describe('This test for RecordItem', () => { expect(button).toBeTruthy(); button.click(); expect(actionsSpy.calls.length).toBe(1); + + // With source metadata + const record = {...sampleRecord2, serviceType: "cog", sourceMetadata: {crs: "EPSG:3946"}}; + item = ReactDOM.render(, document.getElementById("container")); + expect(item).toBeTruthy(); + + button = TestUtils.findRenderedDOMComponentWithTag( + item, 'button' + ); + expect(button).toBeTruthy(); + button.click(); + expect(actionsSpy.calls.length).toBe(2); + }); it('check add layer with bounding box', (done) => { let actions = { diff --git a/web/client/components/catalog/editor/AdvancedSettings/CSWFilters.jsx b/web/client/components/catalog/editor/AdvancedSettings/CSWFilters.jsx index 96bf34715b..29e440f891 100644 --- a/web/client/components/catalog/editor/AdvancedSettings/CSWFilters.jsx +++ b/web/client/components/catalog/editor/AdvancedSettings/CSWFilters.jsx @@ -1,6 +1,5 @@ import React, { useState, useEffect } from "react"; import { - Col, ControlLabel, FormGroup, Glyphicon, @@ -59,14 +58,14 @@ const FilterCode = ({ type, code, setCode, error }) => { const filterProp = `${type}Filter`; return ( - +
{error[type] && renderError} - - +
+
{ }} /> {type === 'dynamic' && renderHelpText} - +
); }; diff --git a/web/client/components/catalog/editor/AdvancedSettings/CommonAdvancedSettings.jsx b/web/client/components/catalog/editor/AdvancedSettings/CommonAdvancedSettings.jsx index 048fd385ae..5368fe49b9 100644 --- a/web/client/components/catalog/editor/AdvancedSettings/CommonAdvancedSettings.jsx +++ b/web/client/components/catalog/editor/AdvancedSettings/CommonAdvancedSettings.jsx @@ -7,7 +7,7 @@ */ import React from 'react'; import { isNil } from 'lodash'; -import { FormGroup, Checkbox, Col } from "react-bootstrap"; +import { FormGroup, Checkbox } from "react-bootstrap"; import Message from "../../../I18N/Message"; import InfoPopover from '../../../widgets/widget/InfoPopover'; @@ -25,44 +25,36 @@ export default ({ onChangeServiceProperty = () => { }, onToggleThumbnail = () => { } }) => ( -
+ <> - - {service.autoload !== undefined && onChangeServiceProperty("autoload", e.target.checked)} - checked={!isNil(service.autoload) ? service.autoload : false}> - - } - + {service.autoload !== undefined && onChangeServiceProperty("autoload", e.target.checked)} + checked={!isNil(service.autoload) ? service.autoload : false}> + + } - - onToggleThumbnail()} - checked={!isNil(service.hideThumbnail) ? !service.hideThumbnail : true}> - - - + onToggleThumbnail()} + checked={!isNil(service.hideThumbnail) ? !service.hideThumbnail : true}> + + {!isNil(service.type) && service.type === "wfs" && - - onChangeServiceProperty("allowUnsecureLayers", e.target.checked)} - checked={!isNil(service.allowUnsecureLayers) ? service.allowUnsecureLayers : false}> -  } /> - - + onChangeServiceProperty("allowUnsecureLayers", e.target.checked)} + checked={!isNil(service.allowUnsecureLayers) ? service.allowUnsecureLayers : false}> +  } /> + } {!isNil(service.type) && service.type === "cog" && - - onChangeServiceProperty("fetchMetadata", e.target.checked)} - checked={!isNil(service.fetchMetadata) ? service.fetchMetadata : false}> -  } /> - - + onChangeServiceProperty("fetchMetadata", e.target.checked)} + checked={!isNil(service.fetchMetadata) ? service.fetchMetadata : true}> +  } /> + } {children} -
+ ); diff --git a/web/client/components/catalog/editor/AdvancedSettings/RasterAdvancedSettings.js b/web/client/components/catalog/editor/AdvancedSettings/RasterAdvancedSettings.js index b5abdf7f04..42a7909dac 100644 --- a/web/client/components/catalog/editor/AdvancedSettings/RasterAdvancedSettings.js +++ b/web/client/components/catalog/editor/AdvancedSettings/RasterAdvancedSettings.js @@ -6,22 +6,22 @@ * LICENSE file in the root directory of this source tree. */ import React, {useEffect} from 'react'; -import {FormGroup, Col, ControlLabel, Checkbox, Button as ButtonRB, Glyphicon } from "react-bootstrap"; +import {FormGroup, ControlLabel, Checkbox, Button as ButtonRB, Glyphicon, InputGroup } from "react-bootstrap"; import RS from 'react-select'; -import localizedProps from '../../../misc/enhancers/localizedProps'; -const Select = localizedProps('noResultsText')(RS); +import {isNil, camelCase} from "lodash"; +import localizedProps from '../../../misc/enhancers/localizedProps'; import CommonAdvancedSettings from './CommonAdvancedSettings'; -import {isNil, camelCase} from "lodash"; import ReactQuill from '../../../../libs/quill/react-quill-suspense'; import { ServerTypes } from '../../../../utils/LayersUtils'; - import InfoPopover from '../../../widgets/widget/InfoPopover'; import CSWFilters from "./CSWFilters"; import Message from "../../../I18N/Message"; import WMSDomainAliases from "./WMSDomainAliases"; import tooltip from '../../../misc/enhancers/buttonTooltip'; + const Button = tooltip(ButtonRB); +const Select = localizedProps('noResultsText')(RS); /** * Generates an array of options in the form e.g. [{value: "256", label: "256x256"}] @@ -58,6 +58,7 @@ const getServerTypeOptions = () => { * */ export default ({ + showFormatError, service, formatOptions = [], infoFormatOptions = [], @@ -82,52 +83,42 @@ export default ({ const serverTypeOptions = getServerTypeOptions(); return ( {(isLocalizedLayerStylesEnabled && !isNil(service.type) ? service.type === "wms" : false) && ( - - onChangeServiceProperty("localizedLayerStyles", e.target.checked)} - checked={!isNil(service.localizedLayerStyles) ? service.localizedLayerStyles : false}> -  } /> - - + onChangeServiceProperty("localizedLayerStyles", e.target.checked)} + checked={!isNil(service.localizedLayerStyles) ? service.localizedLayerStyles : false}> +  } /> + )} - - onChangeServiceProperty("autoSetVisibilityLimits", e.target.checked)} - checked={!isNil(service.autoSetVisibilityLimits) ? service.autoSetVisibilityLimits : false}> -  } /> - - + onChangeServiceProperty("autoSetVisibilityLimits", e.target.checked)} + checked={!isNil(service.autoSetVisibilityLimits) ? service.autoSetVisibilityLimits : false}> +  } /> + {!isNil(service.type) && service.type === "wms" && - - onChangeServiceProperty("layerOptions", { ...service.layerOptions, singleTile: e.target.checked })} - checked={!isNil(service?.layerOptions?.singleTile) ? service.layerOptions.singleTile : false}> -  } /> - - + onChangeServiceProperty("layerOptions", { ...service.layerOptions, singleTile: e.target.checked })} + checked={!isNil(service?.layerOptions?.singleTile) ? service.layerOptions.singleTile : false}> +  } /> + } {!isNil(service.type) && service.type === "wms" && - - onChangeServiceProperty("allowUnsecureLayers", e.target.checked)} - checked={!isNil(service.allowUnsecureLayers) ? service.allowUnsecureLayers : false}> -  } /> - - + onChangeServiceProperty("allowUnsecureLayers", e.target.checked)} + checked={!isNil(service.allowUnsecureLayers) ? service.allowUnsecureLayers : false}> +  } /> + } {(!isNil(service.type) ? (service.type === "csw" && !service.excludeShowTemplate) : false) && ( - - onToggleTemplate()} - checked={service && service.showTemplate}> - - -
- + onToggleTemplate()} + checked={service && service.showTemplate}> + + +
{service && service.showTemplate && - ( + (

@@ -138,8 +129,8 @@ export default ({ {" ${ description }"} - )} - +

)} +
{service && service.showTemplate && } - +
)} - - - - - -
- onChangeServiceProperty("layerOptions", { ...service.layerOptions, serverType: event?.value })} /> + - - - - - -
- onFormatOptionsFetch(service.url)} + value={service && service.format} + clearable + noResultsText={props.formatsLoading + ? "catalog.format.loading" : "catalog.format.noOption"} + options={props.formatsLoading ? [] : formatOptions.map((format) => format?.value ? format : ({ value: format, label: format }))} + onChange={event => onChangeServiceFormat(event && event.value)} /> + - - - - - + + + + onChangeServiceProperty("layerOptions", { ...service.layerOptions, tileSize: event && event.value })} /> - - - - - - - -