diff --git a/demo/js/index.js b/demo/js/index.js index 7d3e5b1d..417cc4d0 100755 --- a/demo/js/index.js +++ b/demo/js/index.js @@ -34,8 +34,7 @@ var interactPlugin = createInteractPlugin({ }], interactionMode: 'select', // 'auto', 'select', 'marker' // defaults to 'marker' multiSelect: true, - contiguous: true, - // excludeModes: ['draw'] + contiguous: true }) var drawPlugin = createDrawPlugin({ diff --git a/plugins/interact/src/InteractInit.jsx b/plugins/interact/src/InteractInit.jsx index aea68074..66aba4c7 100755 --- a/plugins/interact/src/InteractInit.jsx +++ b/plugins/interact/src/InteractInit.jsx @@ -50,7 +50,7 @@ export const InteractInit = ({ useEffect(() => { if (!pluginState.enabled) { - return + return undefined // Explicit return } const cleanupEvents = attachEvents({ diff --git a/plugins/interact/src/events.js b/plugins/interact/src/events.js index a69e326c..a9ac0c17 100755 --- a/plugins/interact/src/events.js +++ b/plugins/interact/src/events.js @@ -1,3 +1,16 @@ +// Helper for feature toggling logic +const createFeatureHandler = (mapState, pluginState) => (args, addToExisting) => { + mapState.markers.remove('location') + pluginState.dispatch({ + type: 'TOGGLE_SELECTED_FEATURES', + payload: { + multiSelect: pluginState.multiSelect, + addToExisting, + ...args + } + }) +} + export function attachEvents ({ appState, mapState, @@ -8,41 +21,24 @@ export function attachEvents ({ handleInteraction, closeApp }) { - const { - selectDone, - selectAtTarget, - selectCancel - } = buttonConfig - + const { selectDone, selectAtTarget, selectCancel } = buttonConfig const { viewportRef } = appState.layoutRefs + // Keyboard Logic let enterOnViewport = false - const handleKeydown = (e) => { - enterOnViewport = e.key === 'Enter' && viewportRef.current === e.target - } - document.addEventListener('keydown', handleKeydown) - + const handleKeydown = (e) => { enterOnViewport = e.key === 'Enter' && viewportRef.current === e.target } const handleKeyup = (e) => { - if (e.key !== 'Enter' || !enterOnViewport) { - return + if (e.key === 'Enter' && enterOnViewport) { + e.preventDefault() + handleSelectAtTarget() } - e.preventDefault() - handleSelectAtTarget() } - document.addEventListener('keyup', handleKeyup) - // Allow tapping on touch devices as well as accurate placement - const handleMapClick = (e) => { - handleInteraction(e) - } - eventBus.on(events.MAP_CLICK, handleMapClick) + // Interaction Handlers + const handleMapClick = (e) => handleInteraction(e) + const handleSelectAtTarget = () => handleInteraction(mapState.crossHair.getDetail()) - const handleSelectAtTarget = () => { - handleInteraction(mapState.crossHair.getDetail()) - } - selectAtTarget.onClick = handleSelectAtTarget - - const handleSelectDone = (e) => { + const handleSelectDone = () => { const marker = mapState.markers.getMarker('location') const { coords } = marker || {} const { selectionBounds, selectedFeatures } = pluginState @@ -57,43 +53,32 @@ export function attachEvents ({ ...(!coords && selectionBounds && { selectionBounds }) }) - if (!(pluginState.closeOnAction ?? true)) { - return + if (pluginState.closeOnAction ?? true) { + closeApp() } - - closeApp() } - selectDone.onClick = handleSelectDone const handleSelectCancel = () => { eventBus.emit('interact:cancel') - - if (!(pluginState.closeOnAction ?? true)) { - return + if (pluginState.closeOnAction ?? true) { + closeApp() } - - closeApp() } - selectCancel.onClick = handleSelectCancel - const handleToggleFeature = (args, addToExisting) => { - mapState.markers.remove('location') + const toggleFeature = createFeatureHandler(mapState, pluginState) + const handleSelect = (args) => toggleFeature(args, true) + const handleUnselect = (args) => toggleFeature(args, false) - pluginState.dispatch({ - type: 'TOGGLE_SELECTED_FEATURES', - payload: { - multiSelect: pluginState.multiSelect, - addToExisting, - ...args - } - }) - } - const handleSelect = (args) => handleToggleFeature(args, true) - const handleUnselect = (args) => handleToggleFeature(args, false) + // Attach Listeners + document.addEventListener('keydown', handleKeydown) + document.addEventListener('keyup', handleKeyup) + eventBus.on(events.MAP_CLICK, handleMapClick) eventBus.on('interact:selectFeature', handleSelect) eventBus.on('interact:unselectFeature', handleUnselect) + selectAtTarget.onClick = handleSelectAtTarget + selectDone.onClick = handleSelectDone + selectCancel.onClick = handleSelectCancel - // Return cleanup function return () => { selectDone.onClick = null selectAtTarget.onClick = null diff --git a/plugins/interact/src/hooks/useHighlightSync.js b/plugins/interact/src/hooks/useHighlightSync.js index 6bb4d871..ca2b028c 100755 --- a/plugins/interact/src/hooks/useHighlightSync.js +++ b/plugins/interact/src/hooks/useHighlightSync.js @@ -32,7 +32,7 @@ export const useHighlightSync = ({ useEffect(() => { if (!mapProvider || !selectedFeatures || !stylesMap) { - return + return undefined // Explicit return to match the cleanup function return below } // Update updateHighlightedFeatures on interaction diff --git a/plugins/interact/src/hooks/useInteractionHandlers.js b/plugins/interact/src/hooks/useInteractionHandlers.js index b4c8cebe..97cf2917 100755 --- a/plugins/interact/src/hooks/useInteractionHandlers.js +++ b/plugins/interact/src/hooks/useInteractionHandlers.js @@ -2,6 +2,34 @@ import { useCallback, useEffect, useRef } from 'react' import { isContiguousWithAny, canSplitFeatures, areAllContiguous } from '../utils/spatial.js' import { getFeaturesAtPoint, findMatchingFeature, buildLayerConfigMap } from '../utils/featureQueries.js' +const useSelectionChangeEmitter = (eventBus, selectedFeatures, selectionBounds) => { + const lastEmittedSelectionChange = useRef(null) + + useEffect(() => { + // Skip if features exist but bounds not yet calculated + const awaitingBounds = selectedFeatures.length > 0 && !selectionBounds + if (awaitingBounds) { + return + } + + // Skip if selection was already empty and remains empty + const prev = lastEmittedSelectionChange.current + const wasEmpty = prev === null || prev.length === 0 + if (wasEmpty && selectedFeatures.length === 0) { + return + } + + eventBus.emit('interact:selectionchange', { + selectedFeatures, + selectionBounds, + canMerge: areAllContiguous(selectedFeatures), + canSplit: canSplitFeatures(selectedFeatures) + }) + + lastEmittedSelectionChange.current = selectedFeatures + }, [selectedFeatures, selectionBounds]) +} + export const useInteractionHandlers = ({ mapState, pluginState, @@ -11,83 +39,73 @@ export const useInteractionHandlers = ({ const { markers } = mapState const { dispatch, dataLayers, interactionMode, multiSelect, contiguous, markerColor, tolerance, selectedFeatures, selectionBounds } = pluginState const { eventBus } = services - const lastEmittedSelectionChange = useRef(null) const layerConfigMap = buildLayerConfigMap(dataLayers) const handleInteraction = useCallback(({ point, coords }) => { const allFeatures = getFeaturesAtPoint(mapProvider, point, { radius: tolerance }) const hasDataLayers = dataLayers.length > 0 - // Debug option to inspect the map style data if (pluginState?.debug) { - console.log(`--- Features at ${coords} ---`) - console.log(allFeatures) + console.log(`--- Features at ${coords} ---`, allFeatures) } - const canMatchFeature = hasDataLayers && (interactionMode === 'select' || interactionMode === 'auto') - const match = canMatchFeature ? findMatchingFeature(allFeatures, layerConfigMap) : null + const canMatch = hasDataLayers && (interactionMode === 'select' || interactionMode === 'auto') + const match = canMatch ? findMatchingFeature(allFeatures, layerConfigMap) : null + // 1. Handle Feature Match if (match) { - markers.remove('location') - const { feature, config } = match - - const isNewFeatureContiguous = contiguous && isContiguousWithAny(feature, selectedFeatures) - - const featureId = config.idProperty - ? feature.properties?.[config.idProperty] - : feature.id - - if (featureId) { - dispatch({ - type: 'TOGGLE_SELECTED_FEATURES', - payload: { - featureId, - multiSelect, - layerId: config.layerId, - idProperty: config.idProperty, - properties: feature.properties, - geometry: feature.geometry, - replaceAll: contiguous && !isNewFeatureContiguous - }, - }) - } - + processFeatureMatch(match) return } - // Marker mode - if (interactionMode === 'marker' || (interactionMode === 'auto' && hasDataLayers)) { + // 2. Handle Marker Mode (Fallback) + const isMarkerMode = interactionMode === 'marker' || (interactionMode === 'auto' && hasDataLayers) + if (isMarkerMode) { dispatch({ type: 'CLEAR_SELECTED_FEATURES' }) markers.add('location', coords, { color: markerColor }) - eventBus.emit('interact:markerchange', { coords }) } - }, [mapProvider, dataLayers, interactionMode, multiSelect, eventBus, dispatch, markers]) - - // Emit event when selectedFeatures change - useEffect(() => { - // Skip if features exist but bounds not yet calculated - const awaitingBounds = selectedFeatures.length > 0 && !selectionBounds - if (awaitingBounds) { - return - } - // Skip if selection was already empty and remains empty - const prev = lastEmittedSelectionChange.current - const wasEmpty = prev === null || prev.length === 0 - if (wasEmpty && selectedFeatures.length === 0) { - return - } + // Internal helper to keep complexity low + function processFeatureMatch({ feature, config }) { + markers.remove('location') + const isNewContiguous = contiguous && isContiguousWithAny(feature, selectedFeatures) + const featureId = feature.properties?.[config.idProperty] ?? feature.id - eventBus.emit('interact:selectionchange', { - selectedFeatures, - selectionBounds, - canMerge: areAllContiguous(selectedFeatures), - canSplit: canSplitFeatures(selectedFeatures) - }) + if (!featureId) { + return + } - lastEmittedSelectionChange.current = selectedFeatures - }, [selectedFeatures, selectionBounds]) + dispatch({ + type: 'TOGGLE_SELECTED_FEATURES', + payload: { + featureId, + multiSelect, + layerId: config.layerId, + idProperty: config.idProperty, + properties: feature.properties, + geometry: feature.geometry, + replaceAll: contiguous && !isNewContiguous + } + }) + } + }, [ + mapProvider, + dataLayers, + interactionMode, + multiSelect, + eventBus, + dispatch, + markers, + contiguous, + selectedFeatures, + layerConfigMap, + pluginState?.debug, + tolerance, + markerColor + ]) + + useSelectionChangeEmitter(eventBus, selectedFeatures, selectionBounds) return { handleInteraction diff --git a/plugins/interact/src/reducer.js b/plugins/interact/src/reducer.js index ae390e9e..bbb9d4da 100755 --- a/plugins/interact/src/reducer.js +++ b/plugins/interact/src/reducer.js @@ -33,48 +33,40 @@ const disable = (state) => { */ const toggleSelectedFeatures = (state, payload) => { const { featureId, multiSelect, layerId, idProperty, addToExisting = true, replaceAll = false, properties, geometry } = payload - const selected = Array.isArray(state.selectedFeatures) ? [...state.selectedFeatures] : [] - - const existingIndex = selected.findIndex( + const currentSelected = Array.isArray(state.selectedFeatures) ? state.selectedFeatures : [] + + const existingIndex = currentSelected.findIndex( f => f.featureId === featureId && f.layerId === layerId ) - // Handle explicit unselect + // 1. Handle explicit unselect if (addToExisting === false) { - if (existingIndex !== -1) { - selected.splice(existingIndex, 1) // remove the feature - } - return { ...state, selectedFeatures: selected, selectionBounds: null } + const filtered = currentSelected.filter((_, i) => i !== existingIndex) + return { ...state, selectedFeatures: filtered, selectionBounds: null } } - // Replace all selected features if flag is true - if (replaceAll) { - // Toggle off if clicking the same already-selected feature - if (existingIndex !== -1 && selected.length === 1) { - return { ...state, selectedFeatures: [], selectionBounds: null } - } - return { - ...state, - selectedFeatures: [{ featureId, layerId, idProperty, properties, geometry }], - selectionBounds: null - } - } + // Define the feature object once to avoid repetition + const featureObj = { featureId, layerId, idProperty, properties, geometry } + let nextSelected - // Multi-select mode (add to selection) - if (multiSelect) { + // 2. Determine New State + // We combine 'replaceAll' and 'single-select' because they share the same logic + if (multiSelect && !replaceAll) { + const selectedCopy = [...currentSelected] if (existingIndex === -1) { - selected.push({ featureId, layerId, idProperty, properties, geometry }) + selectedCopy.push(featureObj) } else { - selected.splice(existingIndex, 1) + selectedCopy.splice(existingIndex, 1) } - return { ...state, selectedFeatures: selected, selectionBounds: null } + nextSelected = selectedCopy + } else { + // Both 'replaceAll' and single-select mode logic: + // If same feature is already the only one, toggle off; otherwise return just this feature. + const isSameSingle = existingIndex !== -1 && currentSelected.length === 1 + nextSelected = isSameSingle ? [] : [featureObj] } - // Single-select mode - const isSameSingle = existingIndex !== -1 && selected.length === 1 - const newSelected = isSameSingle ? [] : [{ featureId, layerId, idProperty, properties, geometry }] - - return { ...state, selectedFeatures: newSelected, selectionBounds: null } + return { ...state, selectedFeatures: nextSelected, selectionBounds: null } } // Update bounds (called from useEffect after map provider calculates them) diff --git a/plugins/search/src/Search.jsx b/plugins/search/src/Search.jsx index a360ba0c..396f9509 100755 --- a/plugins/search/src/Search.jsx +++ b/plugins/search/src/Search.jsx @@ -8,7 +8,7 @@ import { attachEvents } from './events/index.js' export function Search({ appConfig, iconRegistry, pluginState, pluginConfig, appState, mapState, services, mapProvider }) { const { id } = appConfig - const { interfaceType, breakpoint } = appState + const { interfaceType } = appState const { isExpanded: defaultExpanded, customDatasets, osNamesURL } = pluginConfig const { dispatch, isExpanded, areSuggestionsVisible, suggestions } = pluginState @@ -58,7 +58,7 @@ export function Search({ appConfig, iconRegistry, pluginState, pluginConfig, app appState.dispatch({ type: 'TOGGLE_HAS_EXCLUSIVE_CONTROL', payload: isExpanded }) if (!searchOpen) { - return + return undefined } // Disable clicks on the viewport while search is open diff --git a/plugins/search/src/components/Form/Form.jsx b/plugins/search/src/components/Form/Form.jsx index ff4db9e2..befa15d5 100755 --- a/plugins/search/src/components/Form/Form.jsx +++ b/plugins/search/src/components/Form/Form.jsx @@ -44,7 +44,7 @@ export const Form = ({ aria-expanded={pluginState.suggestionsVisible} aria-controls={`${id}-search-suggestions`} aria-activedescendant={pluginState.selectedIndex >= 0 ? `${id}-search-suggestion-${pluginState.selectedIndex}` : undefined} - aria-describedby={!pluginState.value ? `${id}-search-hint` : undefined} + aria-describedby={pluginState.value ? undefined : `${id}-search-hint`} aria-autocomplete="list" autoComplete="off" placeholder="Search" diff --git a/plugins/search/src/defaults.js b/plugins/search/src/defaults.js new file mode 100644 index 00000000..10c81890 --- /dev/null +++ b/plugins/search/src/defaults.js @@ -0,0 +1,3 @@ +export const DEFAULTS = { + minSearchLength: 3 +} \ No newline at end of file diff --git a/plugins/search/src/events/fetchSuggestions.js b/plugins/search/src/events/fetchSuggestions.js index aad70492..1a69f9e8 100755 --- a/plugins/search/src/events/fetchSuggestions.js +++ b/plugins/search/src/events/fetchSuggestions.js @@ -1,24 +1,46 @@ // src/plugins/search/events/fetchSuggestions.js -import { fetchDataset } from '../utils/fetchDataset.js' -/** - * Sanitise input query - * Allows letters, numbers, spaces, dashes, commas, full stops - */ -const sanitiseQuery = (value) => value.replace(/[^a-zA-Z0-9\s\-.,]/g, '').trim() +export const sanitiseQuery = (value) => value.replace(/[^a-zA-Z0-9\s\-.,]/g, '').trim() + +const getRequestConfig = (ds, query, transformRequest) => { + const defaultRequest = { + url: ds.urlTemplate?.replace('{query}', encodeURIComponent(query)), + options: { method: 'GET' } + } + + if (typeof ds.buildRequest === 'function') { + return ds.buildRequest(query, () => defaultRequest) + } + + return typeof transformRequest === 'function' + ? transformRequest(defaultRequest, query) + : defaultRequest +} /** - * Fetch suggestions from multiple datasets - * - * - Applies dataset include/exclude regex filtering - * - Supports dataset-level buildRequest or urlTemplate with optional global transformRequest - * - Supports exclusive datasets: if exclusive dataset returns results, stop querying further datasets - * - Merges results in dataset order + * Helper to fetch and parse results for a single dataset + * This flattens the nesting in the main loop. */ -const fetchSuggestions = async (value, datasets, dispatch, transformRequest) => { +const fetchDatasetResults = async (ds, request, query) => { + try { + const response = await fetch(request.url, request.options) + + if (!response.ok) { + console.error(`Fetch error for ${ds.label || 'dataset'}: ${response.status}`) + return null + } + + const json = await response.json() + return ds.parseResults(json, query) + } catch (err) { + console.error(`Network error for ${ds.label || 'dataset'}:`, err) + return null + } +} + +export const fetchSuggestions = async (value, datasets, dispatch, transformRequest) => { const sanitisedValue = sanitiseQuery(value) - // Filter datasets based on includeRegex / excludeRegex const activeDatasets = datasets.filter(ds => { const include = ds.includeRegex ? ds.includeRegex.test(sanitisedValue) : true const exclude = ds.excludeRegex ? ds.excludeRegex.test(sanitisedValue) : false @@ -27,52 +49,25 @@ const fetchSuggestions = async (value, datasets, dispatch, transformRequest) => let finalResults = [] - // Query datasets sequentially to respect 'exclusive' flag for (const ds of activeDatasets) { - // Default GET request builder using urlTemplate - const defaultBuildRequest = (query) => ({ - url: ds.urlTemplate?.replace('{query}', encodeURIComponent(query)), - options: { method: 'GET' } - }) - - let request - - if (typeof ds.buildRequest === 'function') { - // Dataset-level buildRequest takes full control; global transformRequest is ignored - request = ds.buildRequest(sanitisedValue, defaultBuildRequest) - } else { - // Use default GET request and apply global transformRequest if provided - request = defaultBuildRequest(sanitisedValue) - if (typeof transformRequest === 'function') { - request = transformRequest(request, sanitisedValue) - } - } - - // Perform the fetch - const { url, options } = request - const json = await fetchDataset(url, options) + const request = getRequestConfig(ds, sanitisedValue, transformRequest) + const results = await fetchDatasetResults(ds, request, sanitisedValue) - // Parse dataset-specific results - const results = ds.parseResults(json, sanitisedValue) + // Check if we have results to add + if (results?.length) { + finalResults = [...finalResults, ...results] - // Merge results into final array - if (results.length) { - finalResults = finalResults.concat(results) - - // If this dataset is exclusive and returned results, stop querying further datasets + // Only one control-flow statement allowed: the break for exclusivity if (ds.exclusive) { break } } } - // Dispatch all merged results dispatch({ type: 'UPDATE_SUGGESTIONS', payload: finalResults }) - - return { results: finalResults, sanitisedValue } -} - -export { - sanitiseQuery, - fetchSuggestions -} + + return { + results: finalResults, + sanitisedValue + } +} \ No newline at end of file diff --git a/plugins/search/src/events/formHandlers.js b/plugins/search/src/events/formHandlers.js index 59afb4a4..616def26 100755 --- a/plugins/search/src/events/formHandlers.js +++ b/plugins/search/src/events/formHandlers.js @@ -1,5 +1,6 @@ import { fetchSuggestions } from './fetchSuggestions.js' import { updateMap } from '../utils/updateMap.js' +import { DEFAULTS } from '../defaults.js' export const createFormHandlers = ({ dispatch, @@ -20,7 +21,7 @@ export const createFormHandlers = ({ services.eventBus.emit('search:open') }, - handleCloseClick(e, buttonRef) { + handleCloseClick(_e, buttonRef) { dispatch({ type: 'TOGGLE_EXPANDED', payload: false }) dispatch({ type: 'UPDATE_SUGGESTIONS', payload: [] }) dispatch({ type: 'SET_VALUE', payload: '' }) @@ -47,7 +48,7 @@ export const createFormHandlers = ({ return } - if (trimmedValue?.length < 3) { + if (trimmedValue?.length < DEFAULTS.minSearchLength) { return } diff --git a/plugins/search/src/events/index.js b/plugins/search/src/events/index.js index ed624b7f..e27008be 100755 --- a/plugins/search/src/events/index.js +++ b/plugins/search/src/events/index.js @@ -7,7 +7,7 @@ import { createSuggestionHandlers } from './suggestionHandlers.js' const DEBOUNCE_FETCH_TIME = 350 export function attachEvents(args) { - const { dispatch, viewportRef, searchContainerRef } = args + const { dispatch, searchContainerRef } = args // Debounce data fetching const debouncedFetchSuggestions = debounce( diff --git a/plugins/search/src/events/inputHandlers.js b/plugins/search/src/events/inputHandlers.js index c97501b8..41826800 100755 --- a/plugins/search/src/events/inputHandlers.js +++ b/plugins/search/src/events/inputHandlers.js @@ -1,3 +1,5 @@ +import { DEFAULTS } from '../defaults.js' + export const createInputHandlers = ({ dispatch, debouncedFetchSuggestions }) => ({ handleInputClick() { dispatch({ type: 'SHOW_SUGGESTIONS' }) @@ -15,7 +17,7 @@ export const createInputHandlers = ({ dispatch, debouncedFetchSuggestions }) => const value = e.target.value dispatch({ type: 'SET_VALUE', payload: value }) - if (value.length < 3) { + if (value.length < DEFAULTS.minSearchLength) { debouncedFetchSuggestions.cancel() dispatch({ type: 'UPDATE_SUGGESTIONS', payload: [] }) dispatch({ type: 'HIDE_SUGGESTIONS' }) diff --git a/plugins/search/src/events/suggestionHandlers.js b/plugins/search/src/events/suggestionHandlers.js index 0048fc40..72e4da60 100755 --- a/plugins/search/src/events/suggestionHandlers.js +++ b/plugins/search/src/events/suggestionHandlers.js @@ -26,7 +26,9 @@ export const createSuggestionHandlers = ({ dispatch, services, mapProvider, mark switch (e.key) { case 'ArrowDown': { - if (!suggestions?.length) return + if (!suggestions?.length) { + return + } e.preventDefault() if (selectedIndex < suggestions.length - 1) { const newIndex = selectedIndex + 1 @@ -38,7 +40,9 @@ export const createSuggestionHandlers = ({ dispatch, services, mapProvider, mark } case 'ArrowUp': { - if (!suggestions?.length) return + if (!suggestions?.length) { + return + } e.preventDefault() const newIndex = selectedIndex > 0 ? selectedIndex - 1 : -1 services.announce(selectionMessage(suggestions, newIndex)) @@ -53,7 +57,12 @@ export const createSuggestionHandlers = ({ dispatch, services, mapProvider, mark dispatch({ type: 'SET_SELECTED', payload: -1 }) break } + + default: { + // This code runs for any other key press + break + } } - }, + } } } diff --git a/plugins/search/src/utils/fetchDataset.js b/plugins/search/src/utils/fetchDataset.js deleted file mode 100755 index 6a11a0d8..00000000 --- a/plugins/search/src/utils/fetchDataset.js +++ /dev/null @@ -1,23 +0,0 @@ -// src/plugins/search/utils/fetch.js -export async function fetchDataset(url, options = {}, transformRequest) { - let request = new Request(url, options) - - if (transformRequest) { - try { - request = await transformRequest(request) - } catch (err) { - console.error('Error transforming request:', err) - return null - } - } - - const response = await fetch(request) - - if (!response.ok) { - console.error('Fetch error', response.status) - return null - } - - const json = await response.json() - return json -} diff --git a/plugins/search/src/utils/parseOsNamesResults.js b/plugins/search/src/utils/parseOsNamesResults.js index da611256..bbde5d44 100755 --- a/plugins/search/src/utils/parseOsNamesResults.js +++ b/plugins/search/src/utils/parseOsNamesResults.js @@ -6,7 +6,7 @@ const MAX_RESULTS = 8 const isPostcode = (value) => { value = value.replace(/\s/g, '') - const regex = /^(([A-Z]{1,2}\d[A-Z\d]?|ASCN|STHL|TDCU|BBND|[BFS]IQQ|PCRN|TKCA) ?\d[A-Z]{2}|BFPO ?\d{1,4}|(KY\d|MSR|VG|AI)[ -]?\d{4}|[A-Z]{2} ?\d{2}|GE ?CX|GIR ?0A{2}|SAN ?TA1)$/i + const regex = /^(([A-Z]{1,2}\d[A-Z\d]?|ASCN|STHL|TDCU|BBND|[BFS]IQQ|PCRN|TKCA) ?\d[A-Z]{2}|BFPO ?\d{1,4}|(KY\d|MSR|VG|AI)[ -]?\d{4}|[A-Z]{2} ?\d{2}|GE ?CX|GIR ?0A{2}|SAN ?TA1)$/i //NOSONAR return regex.test(value) } @@ -22,7 +22,7 @@ const markString = (string, find) => { const clean = find.replace(/\s+/g, '') // Create a pattern where whitespace is optional between every character // e.g. "ab12cd" -> "a\s* b\s* 1\s* 2\s* c\s* d" - const spacedPattern = clean.split('').join('\\s*') + const spacedPattern = clean.split('').join(String.raw`\s*`) const reg = new RegExp(`(${spacedPattern})`, 'i') return string.replace(reg, '$1') } @@ -33,16 +33,16 @@ const bounds = (crs, { MBR_XMIN, MBR_YMIN, MBR_XMAX, MBR_YMAX, GEOMETRY_X, GEOME let minX, minY, maxX, maxY // Use either MBR or buffered point depending on what's provided - if (MBR_XMIN != null) { - minX = MBR_XMIN - minY = MBR_YMIN - maxX = MBR_XMAX - maxY = MBR_YMAX - } else { + if (MBR_XMIN == null) { minX = GEOMETRY_X - POINT_BUFFER minY = GEOMETRY_Y - POINT_BUFFER maxX = GEOMETRY_X + POINT_BUFFER maxY = GEOMETRY_Y + POINT_BUFFER + } else { + minX = MBR_XMIN + minY = MBR_YMIN + maxX = MBR_XMAX + maxY = MBR_YMAX } // If CRS is already EPSG:27700 (OSGB), just return the raw values @@ -78,7 +78,7 @@ const point = (crs, { GEOMETRY_X, GEOMETRY_Y }) => { } const label = (query, { NAME1, COUNTY_UNITARY, DISTRICT_BOROUGH, POSTCODE_DISTRICT, LOCAL_TYPE }) => { - const qualifier = `${!['City', 'Postcode'].includes(LOCAL_TYPE) ? POSTCODE_DISTRICT + ', ' : ''}${LOCAL_TYPE !== 'City' ? (COUNTY_UNITARY || DISTRICT_BOROUGH) : ''}` + const qualifier = `${['City', 'Postcode'].includes(LOCAL_TYPE) ? '' : POSTCODE_DISTRICT + ', '}${LOCAL_TYPE === 'City' ? '' : (COUNTY_UNITARY || DISTRICT_BOROUGH)}` const text = `${NAME1}${qualifier ? ', ' + qualifier : ''}` return { text, diff --git a/sonar-project.properties b/sonar-project.properties index 2338e602..91a1d8b3 100644 --- a/sonar-project.properties +++ b/sonar-project.properties @@ -17,29 +17,42 @@ sonar.test.inclusions=**/*.test.*,**/__mocks__/**,**/__stubs__/** sonar.cpd.exclusions=**/*.test.*,**/__mocks__/**,**/__stubs__/** # Ignored rules +sonar.issue.ignore.multicriteria=reactPropsJs,reactPropsJsx,preferGlobalThisJs,preferGlobalThisJsx,preferAtJs,preferAtJsx,replaceAllJs,replaceAllJsx,stringReplaceAllJs,stringReplaceAllJsx,pascalCaseFunctions # S6774: React props validation - using TypeScript/JSDoc for prop types instead -sonar.issue.ignore.multicriteria=reactPropsJs,reactPropsJsx,preferGlobalThisJs,preferGlobalThisJsx,preferAtJs,preferAtJsx sonar.issue.ignore.multicriteria.reactPropsJs.ruleKey=javascript:S6774 sonar.issue.ignore.multicriteria.reactPropsJs.resourceKey=**/*.js sonar.issue.ignore.multicriteria.reactPropsJsx.ruleKey=javascript:S6774 sonar.issue.ignore.multicriteria.reactPropsJsx.resourceKey=**/*.jsx -# S7764: Prefer globalThis over window - globalThis cannot be polyfilled for older browsers +# S7764: Prefer globalThis over window sonar.issue.ignore.multicriteria.preferGlobalThisJs.ruleKey=javascript:S7764 sonar.issue.ignore.multicriteria.preferGlobalThisJs.resourceKey=**/*.js sonar.issue.ignore.multicriteria.preferGlobalThisJsx.ruleKey=javascript:S7764 sonar.issue.ignore.multicriteria.preferGlobalThisJsx.resourceKey=**/*.jsx -# S7755: Prefer .at() over [array.length - index] - .at() is ES2022 and requires polyfilling +# S7755: Prefer .at() over [index] sonar.issue.ignore.multicriteria.preferAtJs.ruleKey=javascript:S7755 sonar.issue.ignore.multicriteria.preferAtJs.resourceKey=**/*.js sonar.issue.ignore.multicriteria.preferAtJsx.ruleKey=javascript:S7755 sonar.issue.ignore.multicriteria.preferAtJsx.resourceKey=**/*.jsx +<<<<<<< HEAD +# S6316: Array.prototype.replaceAll +======= # S6316: Array.prototype.replaceAll may not be supported in all browsers +>>>>>>> main sonar.issue.ignore.multicriteria.replaceAllJs.ruleKey=javascript:S6316 sonar.issue.ignore.multicriteria.replaceAllJs.resourceKey=**/*.js sonar.issue.ignore.multicriteria.replaceAllJsx.ruleKey=javascript:S6316 sonar.issue.ignore.multicriteria.replaceAllJsx.resourceKey=**/*.jsx -sonar.issue.ignore.multicriteria.preferAtJsx.resourceKey=**/*.jsx + +# S7781: Prefer .replaceAll() over .replace() with global regex +sonar.issue.ignore.multicriteria.stringReplaceAllJs.ruleKey=javascript:S7781 +sonar.issue.ignore.multicriteria.stringReplaceAllJs.resourceKey=**/*.js +sonar.issue.ignore.multicriteria.stringReplaceAllJsx.ruleKey=javascript:S7781 +sonar.issue.ignore.multicriteria.stringReplaceAllJsx.resourceKey=**/*.jsx + +# S100: Function names should comply with a naming convention (PascalCase for React/Inits) - mustn't pics up React functions +sonar.issue.ignore.multicriteria.pascalCaseFunctions.ruleKey=javascript:S100 +sonar.issue.ignore.multicriteria.pascalCaseFunctions.resourceKey=**/*.js,**/*.jsx,**/*.ts,**/*.tsx diff --git a/src/App/components/Actions/Actions.jsx b/src/App/components/Actions/Actions.jsx index 2abc791a..56caea1b 100755 --- a/src/App/components/Actions/Actions.jsx +++ b/src/App/components/Actions/Actions.jsx @@ -3,7 +3,7 @@ import { useApp } from '../../store/appContext' // eslint-disable-next-line camelcase, react/jsx-pascal-case // sonarjs/disable-next-line function-name -export const Actions = ({ slot, children }) => { +export const Actions = ({ children }) => { const { openPanels, panelConfig, breakpoint } = useApp() const childArray = React.Children.toArray(children) @@ -21,7 +21,7 @@ export const Actions = ({ slot, children }) => { ].filter(Boolean).join(' ') return ( -