Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
38 commits
Select commit Hold shift + click to select a range
3865f5e
Add Geospatial component type
davidjamesstone Feb 23, 2026
f36b462
Add GeospatialField to i18n
davidjamesstone Feb 23, 2026
f9d8700
Add condition to geospatial options
davidjamesstone Feb 24, 2026
48ef9a0
Add new GeospatialField component type with feature flag
davidjamesstone Mar 9, 2026
1ae57d9
Add test to filter geospatial component option when feature flagged
davidjamesstone Mar 9, 2026
ed6a354
Geospatial component preview
davidjamesstone Mar 10, 2026
74f81b0
Merge branch 'main' into feature/DF-854-geojson-type
davidjamesstone Mar 10, 2026
11940aa
Add geospatial preview tests
davidjamesstone Mar 10, 2026
9807784
Update QuestionTypeDescriptions to include geospatial component descr…
davidjamesstone Mar 10, 2026
ba2d980
Add geospatial field info text
davidjamesstone Mar 10, 2026
5ffad65
Make Geospatial list bulleted
davidjamesstone Mar 17, 2026
b52f0f4
Update geospatial question content
davidjamesstone Mar 19, 2026
16d9d69
Add maps plugin and OS config
davidjamesstone Mar 19, 2026
34462b4
Add getSubmissionRecord to the submission service
davidjamesstone Mar 19, 2026
365ba95
Add getFormDefinitionVersion to the forms service
davidjamesstone Mar 19, 2026
0655bcf
Add map review page
davidjamesstone Mar 19, 2026
f9c33ef
Add interactive map assets to webpack
davidjamesstone Mar 19, 2026
d309eea
Update webpack plugin path resolution
davidjamesstone Mar 19, 2026
d229efb
Fix plugin path in webpack
davidjamesstone Mar 19, 2026
295631b
Add form versions API test
davidjamesstone Mar 19, 2026
9bbe11b
Add submission map review tests
davidjamesstone Mar 19, 2026
38118eb
Fix webpack map location
davidjamesstone Mar 19, 2026
49e3c7a
Fix webpack map location
davidjamesstone Mar 19, 2026
3b52e6e
Formatting
davidjamesstone Mar 23, 2026
b2763d8
Add "Show" links to focus the map submission view
davidjamesstone Mar 23, 2026
48c9cf8
Merge branch 'main' into feature/DF-854-geojson-type
davidjamesstone Mar 25, 2026
def67cf
Bump plugin version
davidjamesstone Mar 25, 2026
80f69d6
Update webpack config for interactive-map paths
davidjamesstone Mar 25, 2026
03f0afe
Update test snapshots
davidjamesstone Mar 25, 2026
e073a7c
Remove unnecessary ts comment
davidjamesstone Mar 25, 2026
ff423e2
Sonar fixes
davidjamesstone Mar 25, 2026
224477a
Add map review tests
davidjamesstone Mar 25, 2026
a85f628
Merge branch 'main' into feature/DF-854-geojson-type
davidjamesstone Mar 25, 2026
84e21b7
Add getSubmissionRecord test
davidjamesstone Mar 25, 2026
e913c20
Sonar fixes (fn depth)
davidjamesstone Mar 25, 2026
f33f6a6
Sonar fixes
davidjamesstone Mar 25, 2026
67d7dc8
Sonar fixes (negated condition)
davidjamesstone Mar 25, 2026
7664f14
Add ordnance survey keys to automated test env file
davidjamesstone Mar 25, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions designer/.automated-tests.env
Original file line number Diff line number Diff line change
Expand Up @@ -49,3 +49,7 @@ NODE_TLS_REJECT_UNAUTHORIZED=0
FEATURE_FLAG_USE_ENTITLEMENT_API=false
FEATURE_FLAG_ALLOW_PAYMENTS=true
FEATURE_FLAG_PUBLISH_AUDIT_EVENTS=true

# Ordnance Survey API credentials
ORDNANCE_SURVEY_API_KEY=test
ORDNANCE_SURVEY_API_SECRET=test
3 changes: 2 additions & 1 deletion designer/client/jest.config.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,8 @@ export default {
'nanoid', // Supports ESM only
'slug', // Supports ESM only
'@defra/forms-engine-plugin',
'@defra/forms-model'
'@defra/forms-model',
'geodesy' // Supports ESM only
].join('|')}/)`
]
}
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified designer/client/src/assets/images/map-placeholder.jpg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
2 changes: 1 addition & 1 deletion designer/client/src/assets/nunjucks/govuk-components.js

Large diffs are not rendered by default.

152 changes: 152 additions & 0 deletions designer/client/src/javascripts/maps.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,152 @@
import {
geospatialMap,
map as mapImports
} from '@defra/forms-engine-plugin/shared.js'

const { createMap, defaultMapConfig } = mapImports
const {
addFeatureToMap,
createFeaturesHTML,
getBoundingBox,
getGeoJSON,
focusFeature
} = geospatialMap

/**
* Factory clousure to create the map ready callback with access to the map provider, geojson and list element
* @param {any} mapProvider - the map provider instance
* @param {GeoJSON} geojson - the geojson data
* @param {HTMLDivElement} listEl - the list element to render the features list into
* @param {any} drawPlugin - the map draw plugin instance
* @param {any} map - the initialised map instance
* @param {string} mapId - the map id string
*/
function onMapReadyFactory(
mapProvider,
geojson,
listEl,
drawPlugin,
map,
mapId
) {
/**
* Callback function which fires when the draw plugin is ready
*/
return function () {
const { features } = geojson

// Add all features to the map
features.forEach((feature) => addFeatureToMap(feature, drawPlugin, map))

// Create the list (in readonly mode)
listEl.innerHTML = createFeaturesHTML(features, mapId, true)

// Listen to anchor click events to focus features
listEl.addEventListener(
'click',
function (e) {
const target = e.target

if (!(target instanceof HTMLElement)) {
return
}

if (
target.tagName === 'A' &&
target.dataset.action &&
target.dataset.id
) {
const { action, id } = target.dataset
const feature = geojson.features.find((f) => f.id === id)

if (action === 'focus' && feature) {
focusFeature(feature, mapProvider)
}
}
},
false
)
}
}

/**
* Process a geospatial component preview by rendering the map and features, and setting up event listeners
* @param {Element} preview
* @param {number} index
*/
function processPreview(preview, index) {
// @ts-expect-error - Defra namespace currently comes from UMD support files
const defra = window.defra

const mapId = `map_${index}`
const geospatialInput = preview.querySelector('.govuk-textarea')

if (!(geospatialInput instanceof HTMLTextAreaElement)) {
return
}

const listEl = preview.querySelector(`#list_${index}`)
if (!(listEl instanceof HTMLDivElement)) {
return
}

/**
* @type {GeoJSON}
*/
const geojson = getGeoJSON(geospatialInput)
const bounds = geojson.features.length ? getBoundingBox(geojson) : undefined
const drawPlugin = defra.drawMLPlugin()

const initConfig = {
...defaultMapConfig,
bounds,
plugins: [drawPlugin]
}

const { map } = createMap(mapId, initConfig, {
assetPath: '/assets',
apiPath: '/maps/api',
data: {
VTS_OUTDOOR_URL: '/maps/api/maps/vts/OS_VTS_3857_Outdoor.json',
VTS_DARK_URL: '/maps/api/maps/vts/OS_VTS_3857_Dark.json',
VTS_BLACK_AND_WHITE_URL:
'/maps/api/maps/vts/OS_VTS_3857_Black_and_White.json'
}
})

map.on(
'map:ready',
/**
* Callback function which fires when the map is ready
* @param {object} e - the event
* @param {any} e.map - the map provider instance
*/
function onMapReady({ map: mapProvider }) {
map.on(
'draw:ready',
onMapReadyFactory(mapProvider, geojson, listEl, drawPlugin, map, mapId)
)
}
)
}

/**
* Processes all geospatial component previews on the page by rendering maps and features, and setting up event listeners
*/
export function processMapPreview() {
const previews = document.querySelectorAll('.app-geospatial-field--preview')

previews.forEach(processPreview)
}

processMapPreview()

/**
* @import { FeatureCollection } from '@defra/forms-engine-plugin/engine/types.js'
*/

/**
* @typedef {object} GeoJSON
* @property {'FeatureCollection'} type - the GeoJSON type string
* @property {FeatureCollection} features - the features
*/
145 changes: 145 additions & 0 deletions designer/client/src/javascripts/maps.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,145 @@
import { processMapPreview } from '~/src/javascripts/maps.js'

describe('Maps Client JS', () => {
/** @type {jest.Mock} */
let onMock

/** @type {jest.Mock} */
let addMarkerMock

/** @type {jest.Mock} */
let interactPlugin

/** @type {jest.Mock} */
let drawMLPlugin

/** @type {jest.Mock} */
let drawPluginAddFeature

/** @type {jest.Mock} */
let fitBoundsMock

/** @type {any} */
let mapProvider

beforeEach(() => {
jest.resetAllMocks()

// eslint-disable-next-line @typescript-eslint/no-empty-function
const noop = () => {}
onMock = jest.fn()
addMarkerMock = jest.fn()
fitBoundsMock = jest.fn()
mapProvider = {
fitBounds: fitBoundsMock
}
drawPluginAddFeature = jest.fn()
interactPlugin = jest.fn()
drawMLPlugin = jest.fn(() => ({
addFeature: drawPluginAddFeature
}))

class MockInteractiveMap {
on = onMock
addMarker = addMarkerMock
}

// @ts-expect-error - loaded via UMD
window.defra = {
InteractiveMap: MockInteractiveMap,
maplibreProvider: noop,
openNamesProvider: noop,
mapStylesPlugin: noop,
interactPlugin,
searchPlugin: noop,
zoomControlsPlugin: noop,
scaleBarPlugin: noop,
drawMLPlugin
}
})

afterEach(() => {
document.body.innerHTML = ''
})

describe('Geospatial component', () => {
beforeEach(() => {
document.body.innerHTML = `
<div class="app-geospatial-field--preview">
<div id="map_0">
<textarea class="govuk-textarea" id="data_0" readonly>[{"id":"5e7fb59c-e9bf-49df-a1fc-46833aa6ff4b","type":"Feature","properties":{"description":"s","coordinateGridReference":"SK 07539 43333","centroidGridReference":"SK 22238 54636"},"geometry":{"coordinates":[[[-1.8891381,52.987284],[-1.4386986,53.0797763],[-1.6803979,53.1984038],[-1.8891381,52.987284]]],"type":"Polygon"}},{"type":"Feature","properties":{"description":"p","coordinateGridReference":"SE 05990 03286","centroidGridReference":"SE 05990 03286"},"geometry":{"type":"Point","coordinates":[-1.9111107,53.5262079]},"id":"ae717bbb-011c-4d73-a60d-a73399c8475c"},{"id":"2ea88bee-73da-43c3-9fc7-e75d07615020","type":"Feature","properties":{"description":"l","coordinateGridReference":"SK 83223 60207","centroidGridReference":"SK 83476 87718"},"geometry":{"coordinates":[[-0.7575463,53.1325401],[-0.5707787,53.4739286],[-0.9113549,53.5327382]],"type":"LineString"}}]</textarea>
</div>
<div id="list_0">
</div>
</div>
`
})

/**
* Initialise geospatial maps preview helper
*/
function initialiseGeospatialMapsPreview() {
expect(() => processMapPreview()).not.toThrow()
expect(drawMLPlugin).toHaveBeenCalledTimes(1)
expect(onMock).toHaveBeenCalledTimes(1)
expect(onMock).toHaveBeenNthCalledWith(
1,
'map:ready',
expect.any(Function)
)

const onMapReady = onMock.mock.calls[0][1]
expect(typeof onMapReady).toBe('function')

// Manually invoke onMapReady callback
onMapReady({ map: mapProvider })

expect(onMock).toHaveBeenCalledTimes(2)
expect(onMock).toHaveBeenNthCalledWith(
2,
'draw:ready',
expect.any(Function)
)

const onDrawReady = onMock.mock.calls[1][1]
expect(typeof onDrawReady).toBe('function')

// Manually invoke onDrawReady callback
onDrawReady()

const listContainer = document.body.querySelector('#list_0')

if (listContainer === null) {
throw new Error('Unexpected null found for listContainer')
}

expect(listContainer).toBeDefined()

return {
listContainer: /** @type {HTMLDivElement} */ (listContainer)
}
}

describe('Map preview initialisation', () => {
test('processMapPreview initializes without errors when DOM elements are present', () => {
const { listContainer } = initialiseGeospatialMapsPreview()
expect(listContainer).toBeDefined()
})

test('click on show link focuses the correct feature', () => {
const { listContainer } = initialiseGeospatialMapsPreview()
expect(listContainer).toBeDefined()

const showLinks = listContainer.querySelectorAll(
'.govuk-link.govuk-link--no-visited-state[data-action="focus"]'
)
expect(showLinks).toHaveLength(3)

// Simulate click on the first "Show" link
showLinks[0].dispatchEvent(new MouseEvent('click', { bubbles: true }))

expect(fitBoundsMock).toHaveBeenCalledTimes(1)
})
})
})
})
1 change: 1 addition & 0 deletions designer/client/src/javascripts/preview.js
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import '~/src/views/preview-components/textfield.njk'
import '~/src/views/preview-components/ukaddressfield.njk'
import '~/src/views/preview-components/hiddenfield.njk'
import '~/src/views/preview-components/paymentfield.njk'
import '~/src/views/preview-components/geospatialfield.njk'
import '~/src/views/preview-components/unsupportedquestion.njk'

import { ErrorPreview } from '~/src/javascripts/error-preview/error-preview'
Expand Down
1 change: 1 addition & 0 deletions designer/client/src/javascripts/preview.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@ jest.mock('~/src/views/preview-components/monthyearfield.njk', () => '')
jest.mock('~/src/views/preview-components/fileuploadfield.njk', () => '')
jest.mock('~/src/views/preview-components/hiddenfield.njk', () => '')
jest.mock('~/src/views/preview-components/paymentfield.njk', () => '')
jest.mock('~/src/views/preview-components/geospatialfield.njk', () => '')
jest.mock('~/src/views/preview-components/unsupportedquestion.njk', () => '')

jest.mock('~/src/javascripts/preview/nunjucks-renderer.js')
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ import '~/src/views/preview-components/textfield.njk'
import '~/src/views/preview-components/ukaddressfield.njk'
import '~/src/views/preview-components/yesnofield.njk'
import '~/src/views/preview-components/hiddenfield.njk'
import '~/src/views/preview-components/geospatialfield.njk'
import '~/src/views/preview-components/unsupportedquestion.njk'
import '~/src/views/preview-controllers/page-controller.njk'
import '~/src/views/preview-controllers/summary-controller.njk'
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,7 @@ jest.mock('~/src/views/preview-components/datepartsfield.njk', () => '')
jest.mock('~/src/views/preview-components/monthyearfield.njk', () => '')
jest.mock('~/src/views/preview-components/fileuploadfield.njk', () => '')
jest.mock('~/src/views/preview-components/hiddenfield.njk', () => '')
jest.mock('~/src/views/preview-components/geospatialfield.njk', () => '')
jest.mock('~/src/views/preview-components/paymentfield.njk', () => '')
jest.mock('~/src/views/preview-components/unsupportedquestion.njk', () => '')
jest.mock('~/src/views/page-preview-component/template.njk', () => '')
Expand Down
16 changes: 16 additions & 0 deletions designer/client/src/javascripts/setup-preview.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
DeclarationQuestion,
EastingNorthingQuestion,
EmailAddressQuestion,
GeospatialQuestion,
LatLongQuestion,
ListSortableQuestion,
LongAnswerQuestion,
Expand Down Expand Up @@ -439,6 +440,21 @@
listeners.setupListeners()

return paymentField
},
/**
* @returns {GeospatialQuestion}
*/
GeospatialField: () => {

Check warning on line 447 in designer/client/src/javascripts/setup-preview.js

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Rename this 'GeospatialField' function to match the regular expression '^[_a-z][a-zA-Z0-9]*$'.

See more on https://sonarcloud.io/project/issues?id=DEFRA_forms-designer&issues=AZzXSMywo20_9UCAuct-&open=AZzXSMywo20_9UCAuct-&pullRequest=1331
const questionElements = new QuestionDomElements()
const nunjucksRenderer = new NunjucksRenderer(questionElements)
const geospatial = new GeospatialQuestion(
questionElements,
nunjucksRenderer
)
const listeners = new EventListeners(geospatial, questionElements)
listeners.setupListeners()

return geospatial
}
})

Expand Down
7 changes: 7 additions & 0 deletions designer/client/src/javascripts/setup-preview.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -258,4 +258,11 @@ Helium:2
expect(result).toBeDefined()
})
})

describe('GeospatialField', () => {
it('should create Question instance', () => {
const result = SetupPreview(ComponentType.GeospatialField)
expect(result).toBeDefined()
})
})
})
Loading
Loading