From 643259c394262f8ab2f0b62461ea7b7f5810c795 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?No=C3=A9mie=20Treff?= Date: Tue, 26 Nov 2024 16:05:05 +0100 Subject: [PATCH] fix: introduce suggestions for property name --- src/App.tsx | 62 +++++++++++- .../GeneralEntityRoot/GeneralEntityRoot.tsx | 9 +- src/Hooks/useExecuteWfsDescribeFeatureType.ts | 96 +++++++++++++++++++ src/State/atoms.ts | 5 + 4 files changed, 169 insertions(+), 3 deletions(-) create mode 100644 src/Hooks/useExecuteWfsDescribeFeatureType.ts diff --git a/src/App.tsx b/src/App.tsx index 9a256e1b..65aa0b0c 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,7 @@ 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 disposableCompletionItemProviderRef = useRef(); @@ -66,6 +68,24 @@ const App: React.FC = () => { } }, [setAppInfo, setUserInfo, client]); + const executeWfsDescribeFeatureType = useExecuteWfsDescribeFeatureType(); + const getPropertyNames = useCallback(async (layerId: number | undefined) => { + let response: DescribeFeatureType | undefined; + const propertyNames: string[] = []; + if (layerSuggestionList && layerId) { + const layer = layerSuggestionList.filter(item => item.id === layerId)[0]; + if (layer) { + response = await executeWfsDescribeFeatureType(layer); + if (response !== undefined) { + response.featureTypes[0].properties.forEach(prop => { + propertyNames.push(prop.name); + }); + } + } + } + return propertyNames; + }, [executeWfsDescribeFeatureType, layerSuggestionList]); + const registerLayerIdCompletionProvider = useCallback(() => { if (!monaco) { return undefined; @@ -120,19 +140,57 @@ const App: React.FC = () => { }); }, [monaco, client, setLayerSuggestionList, layerSuggestionList]); + const registerPropertyNameCompletionProvider = useCallback(() => { + if (!monaco || entityId === undefined) { + return undefined; + } + + disposableCompletionItemProviderRef.current = monaco.languages.registerCompletionItemProvider('json', { + triggerCharacters: ['.'], + provideCompletionItems: async (model, position) => { + const lineContent = model.getLineContent(position.lineNumber).trim(); + + if (lineContent.startsWith('"propertyName"')) { + const propertyNames = await getPropertyNames(entityId); + + 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; + } + } + }); + }, [monaco, getPropertyNames, entityId]); + useEffect(() => { getInitialData(); }, [getInitialData]); useEffect(() => { registerLayerIdCompletionProvider(); + registerPropertyNameCompletionProvider(); return () => { if (disposableCompletionItemProviderRef.current) { disposableCompletionItemProviderRef.current.dispose(); } }; - }, [registerLayerIdCompletionProvider]); + }, [registerLayerIdCompletionProvider, registerPropertyNameCompletionProvider]); 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..312b23d3 --- /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-Transaction'); + } + + 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 +});