Skip to content

Commit

Permalink
Merge pull request #197 from open-formulieren/feature/2173-map-backgr…
Browse files Browse the repository at this point in the history
…ound-configuration

Add background image configuration to map component
  • Loading branch information
robinmolen authored Dec 12, 2024
2 parents e130091 + 7013ba4 commit 72b2faa
Show file tree
Hide file tree
Showing 11 changed files with 137 additions and 15 deletions.
2 changes: 2 additions & 0 deletions .storybook/decorators.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import {
DEFAULT_COMPONENT_TREE,
DEFAULT_DOCUMENT_TYPES,
DEFAULT_FILE_TYPES,
DEFAULT_MAP_TILE_LAYERS,
DEFAULT_PREFILL_ATTRIBUTES,
DEFAULT_PREFILL_PLUGINS,
DEFAULT_REGISTRATION_ATTRIBUTES,
Expand Down Expand Up @@ -74,6 +75,7 @@ export const BuilderContextDecorator: Decorator = (Story, context) => {
uniquifyKey: key => key,
supportedLanguageCodes: supportedLanguageCodes,
richTextColors: DEFAULT_COLORS,
getMapTileLayers: async () => DEFAULT_MAP_TILE_LAYERS,
theme,
getFormComponents: () => context?.args?.componentTree || defaultComponentTree,
getValidatorPlugins: async () => {
Expand Down
17 changes: 9 additions & 8 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -66,7 +66,7 @@
"@formatjs/cli": "^6.1.1",
"@formatjs/ts-transformer": "^3.12.0",
"@fortawesome/fontawesome-free": "^6.4.0",
"@open-formulieren/types": "^0.35.0",
"@open-formulieren/types": "^0.37.0",
"@storybook/addon-actions": "^8.3.5",
"@storybook/addon-essentials": "^8.3.5",
"@storybook/addon-interactions": "^8.3.5",
Expand Down
2 changes: 2 additions & 0 deletions src/components/ComponentConfiguration.stories.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import {
DEFAULT_COLORS,
DEFAULT_DOCUMENT_TYPES,
DEFAULT_FILE_TYPES,
DEFAULT_MAP_TILE_LAYERS,
} from '@/tests/sharedUtils';
import {AnyComponentSchema} from '@/types';

Expand Down Expand Up @@ -109,6 +110,7 @@ const Template: StoryFn<TemplateArgs> = ({
getPrefillPlugins={async () => prefillPlugins}
getPrefillAttributes={async (plugin: string) => prefillAttributes[plugin]}
getFileTypes={async () => fileTypes}
getMapTileLayers={async () => DEFAULT_MAP_TILE_LAYERS}
serverUploadLimit="50MB"
getDocumentTypes={async () => DEFAULT_DOCUMENT_TYPES}
getConfidentialityLevels={async () => CONFIDENTIALITY_LEVELS}
Expand Down
2 changes: 2 additions & 0 deletions src/components/ComponentConfiguration.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ const ComponentConfiguration: React.FC<ComponentConfigurationProps> = ({
uniquifyKey,
supportedLanguageCodes = ['nl', 'en'],
richTextColors,
getMapTileLayers,
theme,
getFormComponents,
getValidatorPlugins,
Expand Down Expand Up @@ -60,6 +61,7 @@ const ComponentConfiguration: React.FC<ComponentConfigurationProps> = ({
uniquifyKey,
supportedLanguageCodes,
richTextColors,
getMapTileLayers,
theme,
getFormComponents,
getValidatorPlugins,
Expand Down
15 changes: 15 additions & 0 deletions src/components/builder/loader.stories.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import {Meta, StoryObj} from '@storybook/react';

import Loader from '@/components/builder/loader';

type Story = StoryObj<typeof Loader>;

export default {
title: 'Formio/Builder/Loader',
component: Loader,
parameters: {
modal: {noModal: true},
},
} satisfies Meta<typeof Loader>;

export const Default: Story = {};
13 changes: 13 additions & 0 deletions src/components/builder/loader.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import {FormattedMessage} from 'react-intl';

const Loader: React.FC = () => {
return (
<div className="spinner-border text-primary" role="status">
<span className="sr-only">
<FormattedMessage description="Loading content text" defaultMessage="Loading..." />
</span>
</div>
);
};

export default Loader;
13 changes: 13 additions & 0 deletions src/context.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,17 @@ export interface DocumentTypeOption {
description: string;
}

/*
Map tile layers
This datastructure is created by the Open Forms backend.
*/
export interface MapTileLayer {
identifier: string;
url: string;
label: string;
}

/*
Builder
*/
Expand All @@ -48,6 +59,7 @@ export interface BuilderContextType {
getDocumentTypes: () => Promise<Array<DocumentTypeOption>>;
getConfidentialityLevels: () => Promise<SelectOption[]>;
getAuthPlugins: () => Promise<AuthPluginOption[]>;
getMapTileLayers: () => Promise<MapTileLayer[]>;
}

const BuilderContext = React.createContext<BuilderContextType>({
Expand All @@ -65,6 +77,7 @@ const BuilderContext = React.createContext<BuilderContextType>({
getDocumentTypes: async () => [],
getConfidentialityLevels: async () => [],
getAuthPlugins: async () => [],
getMapTileLayers: async () => [],
});

BuilderContext.displayName = 'BuilderContext';
Expand Down
45 changes: 42 additions & 3 deletions src/registry/map/edit.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
import {MapComponentSchema} from '@open-formulieren/types';
import {useFormikContext} from 'formik';
import {useEffect} from 'react';
import {useContext, useEffect} from 'react';
import {FormattedMessage, useIntl} from 'react-intl';
import useAsync from 'react-use/esm/useAsync';

import {
BuilderTabs,
Expand All @@ -20,7 +21,8 @@ import {
useDeriveComponentKey,
} from '@/components/builder';
import {LABELS} from '@/components/builder/messages';
import {Checkbox, TabList, TabPanel, Tabs} from '@/components/formio';
import {Checkbox, Select, TabList, TabPanel, Tabs} from '@/components/formio';
import {BuilderContext} from '@/context';
import {useErrorChecker} from '@/utils/errors';

import {EditFormDefinition} from '../types';
Expand Down Expand Up @@ -62,7 +64,8 @@ const EditForm: EditFormDefinition<MapComponentSchema> = () => {
'isSensitiveData',
'useConfigDefaultMapSettings',
'defaultZoom',
'initialCenter'
'initialCenter',
'tileLayerIdentifier'
)}
/>
<BuilderTabs.Advanced hasErrors={hasAnyError('conditional')} />
Expand All @@ -83,6 +86,7 @@ const EditForm: EditFormDefinition<MapComponentSchema> = () => {
<IsSensitiveData />
<UseConfigDefaultMapSettings />
{!values.useConfigDefaultMapSettings && <MapConfiguration />}
<TileLayer />
</TabPanel>

{/* Advanced tab */}
Expand Down Expand Up @@ -134,6 +138,7 @@ EditForm.defaultValues = {
lat: undefined,
lng: undefined,
},
tileLayerIdentifier: undefined,
defaultValue: null,
// Advanced tab
conditional: {
Expand Down Expand Up @@ -174,4 +179,38 @@ const UseConfigDefaultMapSettings: React.FC<{}> = () => {
);
};

const TileLayer: React.FC = () => {
const {getMapTileLayers} = useContext(BuilderContext);
const intl = useIntl();
const {value: options, loading, error} = useAsync(async () => await getMapTileLayers(), []);
if (error) {
throw error;
}

const tooltip = intl.formatMessage({
description: "Tooltip for 'tileLayerIdentifier' builder field",
defaultMessage:
'The tile layer is responsible for showing the map background. ' +
'This effects the map style at particular coordinates and zoom levels.',
});
return (
<Select
name="tileLayerIdentifier"
label={
<FormattedMessage
description="Label for 'tileLayerIdentifier' builder field"
defaultMessage="Tile layer"
/>
}
isClearable
tooltip={tooltip}
isLoading={loading}
options={options?.map(tileLayer => ({
label: tileLayer.label,
value: tileLayer.identifier,
}))}
/>
);
};

export default EditForm;
26 changes: 24 additions & 2 deletions src/registry/map/preview.tsx
Original file line number Diff line number Diff line change
@@ -1,9 +1,12 @@
import {CRS_RD, TILE_LAYER_RD} from '@open-formulieren/leaflet-tools';
import {MapComponentSchema} from '@open-formulieren/types';
import {useLayoutEffect} from 'react';
import {useContext, useLayoutEffect} from 'react';
import {MapContainer, TileLayer, useMap} from 'react-leaflet';
import useAsync from 'react-use/esm/useAsync';

import Loader from '@/components/builder/loader';
import {Component, Description} from '@/components/formio';
import {BuilderContext} from '@/context';

import {ComponentPreviewProps} from '../types';

Expand Down Expand Up @@ -37,10 +40,29 @@ const Preview: React.FC<ComponentPreviewProps<MapComponentSchema>> = ({component
validate = {},
defaultZoom,
initialCenter = {},
tileLayerIdentifier,
} = component;
const {getMapTileLayers} = useContext(BuilderContext);
const {value: tileLayers, loading, error} = useAsync(async () => await getMapTileLayers(), []);
if (error) {
throw error;
}
if (loading) {
return <Loader />;
}

const {required = false} = validate;
const {lat = 52.1326332, lng = 5.291266} = initialCenter;
const zoom = defaultZoom ?? 8;

const tileLayerUrl = (): string => {
// Try to find the url of the chosen tile layer. If it is found, return the url,
// else return the default tile layer url.
return (
tileLayers?.find(tileLayer => tileLayer.identifier === tileLayerIdentifier)?.url ??
TILE_LAYER_RD.url
);
};
return (
<Component
type={component.type}
Expand All @@ -61,7 +83,7 @@ const Preview: React.FC<ComponentPreviewProps<MapComponentSchema>> = ({component
minBlockSize: '400px',
}}
>
<TileLayer {...TILE_LAYER_RD} />
<TileLayer {...TILE_LAYER_RD} url={tileLayerUrl()} />
<MapView lat={lat} lng={lng} zoom={zoom} />
</MapContainer>
{description && <Description text={description} />}
Expand Down
15 changes: 14 additions & 1 deletion src/tests/sharedUtils.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
// This module contains shared utilities and constants between Jest and Storybook.
import type {DocumentTypeOption, SelectOption} from '@/context';
import type {DocumentTypeOption, MapTileLayer, SelectOption} from '@/context';
import {ColorOption} from '@/registry/content/rich-text';
import {AnyComponentSchema} from '@/types';

Expand Down Expand Up @@ -235,6 +235,19 @@ export const DEFAULT_COLORS: ColorOption[] = [
},
];

export const DEFAULT_MAP_TILE_LAYERS: MapTileLayer[] = [
{
identifier: 'brt',
url: 'https://service.pdok.nl/brt/achtergrondkaart/wmts/v2_0/standaard/EPSG:28992/{z}/{x}/{y}.png',
label: 'BRT',
},
{
identifier: 'luchtfoto',
url: 'https://service.pdok.nl/hwh/luchtfotorgb/wmts/v1_0/Actueel_orthoHR/EPSG:28992/{z}/{x}/{y}.png',
label: 'Luchtfoto',
},
];

export function sleep(ms: number) {
return new Promise(resolve => setTimeout(resolve, ms));
}

0 comments on commit 72b2faa

Please sign in to comment.