diff --git a/src/App.tsx b/src/App.tsx index 9a256e1b..5567bd75 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -15,9 +15,10 @@ import config from 'shogunApplicationConfig'; import Header from './Component/Header/Header'; import ShogunSpinner from './Component/ShogunSpinner/ShogunSpinner'; +import useExecuteWfsDescribeFeatureType, { DescribeFeatureType } from './Hooks/useExecuteWfsDescribeFeatureType'; import useSHOGunAPIClient from './Hooks/useSHOGunAPIClient'; import Portal from './Page/Portal/Portal'; -import { appInfoAtom, layerSuggestionListAtom, userInfoAtom } from './State/atoms'; +import { appInfoAtom, layerSuggestionListAtom, userInfoAtom, entityIdAtom } from './State/atoms'; import { setSwaggerDocs } from './State/static'; import ProviderResult = languages.ProviderResult; @@ -29,6 +30,8 @@ const App: React.FC = () => { const [, setAppInfo] = useRecoilState(appInfoAtom); const [layerSuggestionList, setLayerSuggestionList] = useRecoilState(layerSuggestionListAtom); const [loadingState, setLoadingState] = useState<'failed' | 'loading' | 'done'>(); + const [entityId, ] = useRecoilState(entityIdAtom); + const [propertyNames, setPropertyNames] = useState([]); const disposableCompletionItemProviderRef = useRef(); @@ -44,6 +47,24 @@ const App: React.FC = () => { t } = useTranslation(); + useEffect(() => { + const setLayers = async() => { + try { + const layers = await client?.layer().findAll(); + if (!_isNil(layers)) { + setLayerSuggestionList(layers.content); + } + + if (disposableCompletionItemProviderRef.current) { + disposableCompletionItemProviderRef.current.dispose(); + } + } catch (error) { + Logger.error(error); + } + }; + setLayers(); + }, [client, setLayerSuggestionList]); + const getInitialData = useCallback(async () => { try { setLoadingState('loading'); @@ -66,7 +87,31 @@ const App: React.FC = () => { } }, [setAppInfo, setUserInfo, client]); - const registerLayerIdCompletionProvider = useCallback(() => { + const executeWfsDescribeFeatureType = useExecuteWfsDescribeFeatureType(); + + const getPropertyNames = useCallback(async (layerId: number | undefined) => { + let response: DescribeFeatureType | undefined; + const propNames: string[] = []; + if (layerSuggestionList && layerId) { + const layer = layerSuggestionList.filter(item => item.id === layerId)[0]; + if (layer) { + try { + response = await executeWfsDescribeFeatureType(layer); + if (response !== undefined) { + response.featureTypes[0].properties.forEach(prop => { + propNames.push(prop.name); + }); + } + } catch (error) { + Logger.error(error); + propNames[0] = ''; + } + } + } + return propNames; + }, [executeWfsDescribeFeatureType, layerSuggestionList]); + + const registerCompletionProvider = useCallback(() => { if (!monaco) { return undefined; } @@ -76,63 +121,75 @@ const App: React.FC = () => { provideCompletionItems: async (model, position) => { const lineContent = model.getLineContent(position.lineNumber).trim(); - if (!lineContent.startsWith('"layerId"')) { - return null; + if (lineContent.startsWith('"layerId"')) { + const currentWord = model.getWordAtPosition(position); + const providerResult: ProviderResult = { + suggestions: layerSuggestionList.map((layer): CompletionItem => { + return { + insertText: layer?.id?.toString() ?? '', + label: `${layer.name} (${layer.id})`, + kind: monaco.languages.CompletionItemKind.Value, + documentation: `${JSON.stringify(layer, null, ' ')}`, + range: { + // replace the current word, if applicable + startColumn: currentWord ? currentWord.startColumn : position.column, + endColumn: currentWord ? currentWord.endColumn : position.column, + startLineNumber: position.lineNumber, + endLineNumber: position.lineNumber, + } + }; + }) + }; + + return providerResult; } - if (!layerSuggestionList) { - try { - const layers = await client?.layer().findAll(); - if (!_isNil(layers)) { - setLayerSuggestionList(layers.content); - } - - if (disposableCompletionItemProviderRef.current) { - disposableCompletionItemProviderRef.current.dispose(); - } - } catch (error) { - Logger.error(error); - } - return undefined; + if (lineContent.startsWith('"propertyName"')) { + const currentWord = model.getWordAtPosition(position); + const providerResult: ProviderResult = { + suggestions: propertyNames.map((prop): CompletionItem => { + return { + insertText: `"${prop}"`, + label: prop, + kind: monaco.languages.CompletionItemKind.Value, + documentation: `${JSON.stringify(prop, null, ' ')}`, + range: { + // replace the current word, if applicable + startColumn: currentWord ? currentWord.startColumn : position.column, + endColumn: currentWord ? currentWord.endColumn : position.column, + startLineNumber: position.lineNumber, + endLineNumber: position.lineNumber, + } + }; + }) + }; + return providerResult; } - - const currentWord = model.getWordAtPosition(position); - const providerResult: ProviderResult = { - suggestions: layerSuggestionList.map((layer): CompletionItem => { - return { - insertText: layer?.id?.toString() ?? '', - label: `${layer.name} (${layer.id})`, - kind: monaco.languages.CompletionItemKind.Value, - documentation: `${JSON.stringify(layer, null, ' ')}`, - range: { - // replace the current word, if applicable - startColumn: currentWord ? currentWord.startColumn : position.column, - endColumn: currentWord ? currentWord.endColumn : position.column, - startLineNumber: position.lineNumber, - endLineNumber: position.lineNumber, - } - }; - }) - }; - - return providerResult; } }); - }, [monaco, client, setLayerSuggestionList, layerSuggestionList]); + }, [monaco, layerSuggestionList, propertyNames]); useEffect(() => { getInitialData(); }, [getInitialData]); useEffect(() => { - registerLayerIdCompletionProvider(); + const propName = async() => { + const properties = await getPropertyNames(entityId); + setPropertyNames(properties); + }; + propName(); + }, [entityId, getPropertyNames]); + + useEffect(() => { + registerCompletionProvider(); return () => { if (disposableCompletionItemProviderRef.current) { disposableCompletionItemProviderRef.current.dispose(); } }; - }, [registerLayerIdCompletionProvider]); + }, [registerCompletionProvider]); if (loadingState === 'loading') { return ( diff --git a/src/Component/GeneralEntity/GeneralEntityRoot/GeneralEntityRoot.tsx b/src/Component/GeneralEntity/GeneralEntityRoot/GeneralEntityRoot.tsx index 37a70837..8d980e06 100644 --- a/src/Component/GeneralEntity/GeneralEntityRoot/GeneralEntityRoot.tsx +++ b/src/Component/GeneralEntity/GeneralEntityRoot/GeneralEntityRoot.tsx @@ -45,6 +45,7 @@ import { useNavigate } from 'react-router-dom'; +import { useSetRecoilState } from 'recoil'; import config from 'shogunApplicationConfig'; import Logger from '@terrestris/base-util/dist/Logger'; @@ -61,6 +62,7 @@ import { GenericEntityController } from '../../../Controller/GenericEntityController'; import useSHOGunAPIClient from '../../../Hooks/useSHOGunAPIClient'; +import { entityIdAtom } from '../../../State/atoms'; import TranslationUtil from '../../../Util/TranslationUtil'; import GeneralEntityForm, { FormConfig @@ -68,6 +70,7 @@ import GeneralEntityForm, { import GeneralEntityTable, { TableConfig } from '../GeneralEntityTable/GeneralEntityTable'; + import './GeneralEntityRoot.less'; export interface GeneralEntityConfigType { @@ -120,6 +123,7 @@ export function GeneralEntityRoot({ const location = useLocation(); const navigate = useNavigate(); + const setEntityId = useSetRecoilState(entityIdAtom); const match = matchPath({ path: `${config.appPrefix}/portal/${entityType}/:entityId` @@ -314,19 +318,22 @@ export function GeneralEntityRoot({ useEffect(() => { if (!entityId) { setId(undefined); + setEntityId(undefined); setEditEntity(undefined); setFormIsDirty(false); return; } if (entityId === 'create') { setId(entityId); + setEntityId(undefined); form.resetFields(); form.setFieldsValue(defaultEntity); } else { setId(parseInt(entityId, 10)); + setEntityId(parseInt(entityId, 10)); setFormIsDirty(false); } - }, [entityId, form, defaultEntity]); + }, [entityId, form, defaultEntity, setEntityId]); // Once the controller is known we need to set the formUpdater so we can update // a given form when the entity is updated via controller diff --git a/src/Hooks/useExecuteWfsDescribeFeatureType.ts b/src/Hooks/useExecuteWfsDescribeFeatureType.ts new file mode 100644 index 00000000..c8738ad2 --- /dev/null +++ b/src/Hooks/useExecuteWfsDescribeFeatureType.ts @@ -0,0 +1,96 @@ +import { + useCallback +} from 'react'; + +import { UrlUtil } from '@terrestris/base-util/dist/UrlUtil/UrlUtil'; + +import Layer from '@terrestris/shogun-util/dist/model/Layer'; +import { + getBearerTokenHeader +} from '@terrestris/shogun-util/dist/security/getBearerTokenHeader'; + +import useSHOGunAPIClient from './useSHOGunAPIClient'; + +export type LocalGeometryType = 'MultiPoint' | 'Point' | 'MultiLineString' | 'LineString' | 'MultiPolygon' | 'Polygon'; +export type GeometryType = 'gml:MultiPoint' | 'gml:Point' | 'gml:MultiLineString' | + 'gml:LineString' | 'gml:MultiPolygon' | 'gml:Polygon'; + +export interface Property { + localType: 'int' | 'number' | 'string' | 'boolean' | 'date' | LocalGeometryType; + maxOccurs: 0 | 1; + minOccurs: 0 | 1; + name: string; + nillable: boolean; + type: 'xsd:int' | 'xsd:number' | 'xsd:string' | 'xsd:boolean' | 'xsd:date' | GeometryType; +} + +export interface FeatureType { + typeName: string; + properties: Property[]; +} + +export interface DescribeFeatureType { + elementFormDefault: string; + featureTypes: FeatureType[]; + targetNamespace: string; + targetPrefix: string; +} + +export const isGeometryType = (propertyType: string): propertyType is GeometryType => { + const geometryTypes = [ + 'gml:MultiPoint', + 'gml:Point', + 'gml:MultiLineString', + 'gml:LineString', + 'gml:MultiPolygon', + 'gml:Polygon' + ]; + + return geometryTypes.includes(propertyType); +}; + +export const useExecuteWfsDescribeFeatureType = () => { + const client = useSHOGunAPIClient(); + + const executeWfsDescribeFeatureType = useCallback(async (layer: Layer) => { + let url = layer.sourceConfig.url; + + if (!url) { + return; + } + + if (url.endsWith('?')) { + url = url.slice(0, -1); + } + + const params = { + SERVICE: 'WFS', + REQUEST: 'DescribeFeatureType', + VERSION: '2.0.0', + OUTPUTFORMAT: 'application/json', + TYPENAMES: layer.sourceConfig.layerNames + }; + + const defaultHeaders = { + 'Content-Type': 'application/json' + }; + + const response = await fetch(`${url}?${UrlUtil.objectToRequestString(params)}`, { + method: 'GET', + headers: layer.sourceConfig.useBearerToken ? { + ...defaultHeaders, + ...getBearerTokenHeader(client?.getKeycloak()) + } : defaultHeaders + }); + + if (!response.ok) { + throw new Error('No successful response while executing a WFS DescribeFeatureType'); + } + + return await response.json() as DescribeFeatureType; + }, [client]); + + return executeWfsDescribeFeatureType; +}; + +export default useExecuteWfsDescribeFeatureType; diff --git a/src/State/atoms.ts b/src/State/atoms.ts index 7b1c0ad0..2ebe276e 100644 --- a/src/State/atoms.ts +++ b/src/State/atoms.ts @@ -33,3 +33,8 @@ export const layerSuggestionListAtom = atom({ key: 'layerSuggestionList', default: undefined }); + +export const entityIdAtom = atom({ + key: 'entityId', + default: undefined +});