Skip to content

Commit

Permalink
Merge pull request #1067 from geoadmin/feat-PB-984-error-feedback-for…
Browse files Browse the repository at this point in the history
…-all-url-parameters

PB-984: add error feedback for all URL parameters
  • Loading branch information
ltkum authored Oct 3, 2024
2 parents 2a35f62 + bf8e56e commit 02fe4d1
Show file tree
Hide file tree
Showing 18 changed files with 270 additions and 46 deletions.
23 changes: 23 additions & 0 deletions src/api/errorQueues.api.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import ErrorMessage from '@/utils/ErrorMessage.class'

export function getStandardErrorMessage(query, urlParamName) {
return new ErrorMessage('url_parameter_error', {
param: urlParamName,
value: query,
})
}

/**
* Return the standard feedback for most parameters given in the URL: if the query is validated, it
* can proceed and be set in the store.
*
* @param {any} query The value of the URL parameter given
* @param {Boolean} isValid Is the value valid or not
* @returns
*/
export function getStandardValidationResponse(query, isValid, urlParamName) {
return {
valid: isValid,
errors: isValid ? null : getStandardErrorMessage(query, urlParamName),
}
}
4 changes: 2 additions & 2 deletions src/modules/map/components/openlayers/OpenLayersKMLLayer.vue
Original file line number Diff line number Diff line change
Expand Up @@ -87,7 +87,7 @@ function iconUrlProxy(url) {
return iconUrlProxyFy(
url,
(url) => {
store.dispatch('addWarning', {
store.dispatch('addWarnings', {
warning: new WarningMessage('kml_icon_url_cors_issue', {
layerName: layerName.value,
url: url,
Expand All @@ -96,7 +96,7 @@ function iconUrlProxy(url) {
})
},
(url) => {
store.dispatch('addWarning', {
store.dispatch('addWarnings', {
warning: new WarningMessage('kml_icon_url_scheme_http', {
layerName: layerName.value,
url: url,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -278,7 +278,7 @@ export default function useMapInteractions(map) {
errorKey = 'invalid_import_file_error'
log.error(`Failed to load file`, error)
}
store.dispatch('addError', { error: new ErrorMessage(errorKey, null), ...dispatcher })
store.dispatch('addErrors', { error: new ErrorMessage(errorKey, null), ...dispatcher })
}
}

Expand Down
9 changes: 9 additions & 0 deletions src/router/storeSync/CameraParamConfig.class.js
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { getStandardValidationResponse } from '@/api/errorQueues.api'
import AbstractParamConfig, {
STORE_DISPATCHER_ROUTER_PLUGIN,
} from '@/router/storeSync/abstractParamConfig.class'
Expand Down Expand Up @@ -72,6 +73,14 @@ export default class CameraParamConfig extends AbstractParamConfig {
extractValueFromStore: generateCameraUrlParamFromStoreValues,
keepInUrlWhenDefault: false,
valueType: String,
validateUrlInput: (store, query) =>
getStandardValidationResponse(
query,
query &&
query.split(',').length === 6 &&
query.split(',').every((value) => value === '' || !isNaN(value)),
this.urlParamName
),
})
}
}
7 changes: 7 additions & 0 deletions src/router/storeSync/CompareSliderParamConfig.class.js
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { getStandardValidationResponse } from '@/api/errorQueues.api'
import AbstractParamConfig, {
STORE_DISPATCHER_ROUTER_PLUGIN,
} from '@/router/storeSync/abstractParamConfig.class'
Expand Down Expand Up @@ -46,6 +47,12 @@ export default class CompareSliderParamConfig extends AbstractParamConfig {
keepInUrlWhenDefault: false,
valueType: Number,
defaultValue: null,
validateUrlInput: (store, query) =>
getStandardValidationResponse(
query,
query && Number(query) <= 1.0 && Number(query) >= 0.0,
this.urlParamName
),
})
}
}
48 changes: 26 additions & 22 deletions src/router/storeSync/CrossHairParamConfig.class.js
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
import { getStandardValidationResponse } from '@/api/errorQueues.api'
import AbstractParamConfig, {
STORE_DISPATCHER_ROUTER_PLUGIN,
} from '@/router/storeSync/abstractParamConfig.class'
import { CrossHairs } from '@/store/modules/position.store'
import ErrorMessage from '@/utils/ErrorMessage.class'
import { round } from '@/utils/numberUtils'

/**
Expand All @@ -21,10 +21,6 @@ import { round } from '@/utils/numberUtils'

function dispatchCrossHairFromUrlIntoStore(to, store, urlParamValue) {
const promisesForAllDispatch = []
const error = new ErrorMessage('url_parameter_error', {
param: 'crosshair',
value: urlParamValue,
})

if (typeof urlParamValue !== 'string' && !(urlParamValue instanceof String)) {
promisesForAllDispatch.push(
Expand All @@ -33,10 +29,6 @@ function dispatchCrossHairFromUrlIntoStore(to, store, urlParamValue) {
dispatcher: STORE_DISPATCHER_ROUTER_PLUGIN,
})
)

promisesForAllDispatch.push(
store.dispatch('addError', { error, dispatcher: STORE_DISPATCHER_ROUTER_PLUGIN })
)
} else {
const parts = urlParamValue.split(',')
let crossHair = parts[0]
Expand All @@ -55,19 +47,6 @@ function dispatchCrossHairFromUrlIntoStore(to, store, urlParamValue) {
dispatcher: STORE_DISPATCHER_ROUTER_PLUGIN,
})
)
if (urlParamValue !== undefined) {
/**
* There are situations where the function is called without any parameter, and it
* makes some tests crash because the error message is over some necessary buttons.
* We consider that if the parameter is undefined, it's a choice made by the user.
*/
promisesForAllDispatch.push(
store.dispatch('addError', {
error,
dispatcher: STORE_DISPATCHER_ROUTER_PLUGIN,
})
)
}
} else {
if (crossHair === '') {
crossHair = CrossHairs.marker
Expand Down Expand Up @@ -99,6 +78,30 @@ function generateCrossHairUrlParamFromStoreValues(store) {
return null
}

/**
* @param {Object} store
* @param {String} query The crossHair parameter can have multiple values, either one identifier for
* the crossHair itself, or one identifier with two coordinates in a coma separated string, or a
* blank identifier with coordinates. For example, crossHair=marker , crossHair=marker,x,y and
* crossHair=,x,y are all valid values.
* @returns
*/
function validateUrlInput(store, query) {
if (query) {
const parts = query.split(',')
let crossHair = parts[0]
let crossHairPosition = [parseFloat(parts[1]), parseFloat(parts[2])]
return getStandardValidationResponse(
query,
(crossHair ||
crossHairPosition.filter((coordinate) => !isNaN(coordinate)).length === 2) &&
(Object.values(CrossHairs).includes(crossHair) || crossHair === ''),
this.urlParamName
)
}
return getStandardValidationResponse(query, false, this.urlParamName)
}

/**
* Concat the crosshair type with its position, if the crosshair's position is not the same as the
* current center of the map.
Expand All @@ -116,6 +119,7 @@ export default class CrossHairParamConfig extends AbstractParamConfig {
keepInUrlWhenDefault: false,
valueType: String,
defaultValue: null,
validateUrlInput: validateUrlInput,
})
}
}
39 changes: 39 additions & 0 deletions src/router/storeSync/LayerParamConfig.class.js
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { getStandardValidationResponse } from '@/api/errorQueues.api'
import getFeature from '@/api/features/features.api'
import ExternalWMSLayer from '@/api/layers/ExternalWMSLayer.class'
import ExternalWMTSLayer from '@/api/layers/ExternalWMTSLayer.class'
Expand All @@ -15,6 +16,7 @@ import {
transformLayerIntoUrlString,
} from '@/router/storeSync/layersParamParser'
import { flattenExtent } from '@/utils/coordinates/coordinateUtils.js'
import ErrorMessage from '@/utils/ErrorMessage.class'
import { getExtentOfGeometries } from '@/utils/geoJsonUtils'
import log from '@/utils/logging'

Expand Down Expand Up @@ -254,6 +256,42 @@ function generateLayerUrlParamFromStoreValues(store) {
.join(';')
}

// this one differs from the usual validateUrlInput, as it handles each layer separately, telling the user
// which layer won't render. It's basic, which means it will only tells the user when he gives a non
// external layer that doesn't exist, or when he forgets the scheme for its external layer.
function validateUrlInput(store, query) {
if (query === '') {
return {
valid: true,
errors: null,
}
}
const parsed = parseLayersParam(query)
const url_matcher = /https?:\/\//
const faultyLayers = []
parsed
.filter((layer) => !store.getters.getLayerConfigById(layer.id))
.forEach((layer) => {
if (!layer.baseUrl) {
faultyLayers.push(new ErrorMessage('url_layer_error', { layer: layer.id }))
} else if (!layer.baseUrl?.match(url_matcher)?.length > 0) {
faultyLayers.push(
new ErrorMessage('url_external_layer_no_scheme_error', {
layer: `${layer.type}|${layer.baseUrl}`,
})
)
}
})
const valid = faultyLayers.length < parsed.length
if (!valid) {
return getStandardValidationResponse(query, valid, this.urlParamName)
}
return {
valid,
errors: faultyLayers.length === 0 ? null : faultyLayers,
}
}

export default class LayerParamConfig extends AbstractParamConfig {
constructor() {
super({
Expand All @@ -275,6 +313,7 @@ export default class LayerParamConfig extends AbstractParamConfig {
extractValueFromStore: generateLayerUrlParamFromStoreValues,
keepInUrlWhenDefault: true,
valueType: String,
validateUrlInput: validateUrlInput,
})
}
}
14 changes: 14 additions & 0 deletions src/router/storeSync/PositionParamConfig.class.js
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { getStandardValidationResponse } from '@/api/errorQueues.api'
import AbstractParamConfig, {
STORE_DISPATCHER_ROUTER_PLUGIN,
} from '@/router/storeSync/abstractParamConfig.class'
Expand Down Expand Up @@ -32,6 +33,18 @@ function generateCenterUrlParamFromStoreValues(store) {
return null
}

function validateUrlInput(store, query) {
if (query) {
const center = query.split(',')
return getStandardValidationResponse(
query,
center.length === 2 && store.state.position.projection.isInBounds(center[0], center[1]),
this.urlParamName
)
}
return getStandardValidationResponse(query, false, this.urlParamName)
}

/**
* Describe the position (center) of the map in the URL. It will make sure that the URL values are
* read as floating numbers.
Expand All @@ -45,6 +58,7 @@ export default class PositionParamConfig extends AbstractParamConfig {
extractValueFromStore: generateCenterUrlParamFromStoreValues,
keepInUrlWhenDefault: true,
valueType: String,
validateUrlInput: validateUrlInput,
})
}
}
1 change: 1 addition & 0 deletions src/router/storeSync/SearchParamConfig.class.js
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ export default class SearchParamConfig extends AbstractParamConfig {
keepInUrlWhenDefault: false,
valueType: String,
defaultValue: '',
validateUrlInput: null,
})
}
}
2 changes: 2 additions & 0 deletions src/router/storeSync/SimpleUrlParamConfig.class.js
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ export default class SimpleUrlParamConfig extends AbstractParamConfig {
keepInUrlWhenDefault = false,
valueType = String,
defaultValue = null,
validateUrlInput = null,
} = {}) {
super({
urlParamName,
Expand All @@ -46,6 +47,7 @@ export default class SimpleUrlParamConfig extends AbstractParamConfig {
keepInUrlWhenDefault,
valueType,
defaultValue,
validateUrlInput,
})
}
}
22 changes: 22 additions & 0 deletions src/router/storeSync/TimeSliderParamConfig.class.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
import { getStandardValidationResponse } from '@/api/errorQueues.api'
import { OLDEST_YEAR, YOUNGEST_YEAR } from '@/config/time.config'
import AbstractParamConfig, {
STORE_DISPATCHER_ROUTER_PLUGIN,
} from '@/router/storeSync/abstractParamConfig.class'
import WarningMessage from '@/utils/WarningMessage.class'

function dispatchTimeSliderFromUrlParam(to, store, urlParamValue) {
const promisesForAllDispatch = []
Expand Down Expand Up @@ -35,6 +37,25 @@ function generateTimeSliderUrlParamFromStore(store) {
return store.state.ui.isTimeSliderActive ? store.state.layers.previewYear : null
}

function validateUrlInput(store, query) {
const validationObject = getStandardValidationResponse(
query,
!isNaN(query) &&
Number.isInteger(Number(query)) &&
OLDEST_YEAR <= query &&
YOUNGEST_YEAR >= query,
this.urlParamName
)

if (store.getters.visibleLayers.filter((layer) => layer.hasMultipleTimestamps).length === 0) {
validationObject['warnings'] = new WarningMessage(
'time_slider_no_time_layer_active_url_warning',
{}
)
}
return validationObject
}

/**
* When the timeSlider parameter is set in the URL, if the year is a valid year, it will set the
* timeSlider to active to the correct year. The parameter only appears if the time Slider is
Expand All @@ -50,6 +71,7 @@ export default class TimeSliderParamConfig extends AbstractParamConfig {
keepInUrlWhenDefault: false,
valueType: Number,
defaultValue: null,
validateUrlInput: validateUrlInput,
})
}
}
7 changes: 7 additions & 0 deletions src/router/storeSync/ZoomParamConfig.class.js
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { getStandardValidationResponse } from '@/api/errorQueues.api'
import AbstractParamConfig, {
STORE_DISPATCHER_ROUTER_PLUGIN,
} from '@/router/storeSync/abstractParamConfig.class'
Expand Down Expand Up @@ -40,6 +41,12 @@ export default class ZoomParamConfig extends AbstractParamConfig {
extractValueFromStore: generateZoomUrlParamFromStoreValues,
keepInUrlWhenDefault: true,
valueType: Number,
validateUrlInput: (store, query) =>
getStandardValidationResponse(
query,
query && !isNaN(query) && Number(query) >= 0,
this.urlParamName
),
})
}
}
Loading

0 comments on commit 02fe4d1

Please sign in to comment.