diff --git a/package.json b/package.json index 49011429..2e882c0c 100644 --- a/package.json +++ b/package.json @@ -17,6 +17,7 @@ "d3": "^7.8.5", "dompurify": "^3.0.9", "formik": "^2.4.6", + "html-react-parser": "^5.1.18", "jwt-decode": "^4.0.0", "leaflet": "^1.9.4", "leaflet-draw": "^1.0.4", diff --git a/src/components/DynamicObject/DynamicObjectForm/DynamicField/extensions/area.ts b/src/components/DynamicObject/DynamicObjectForm/DynamicField/extensions/area.ts index 04bd562f..d0dbab97 100644 --- a/src/components/DynamicObject/DynamicObjectForm/DynamicField/extensions/area.ts +++ b/src/components/DynamicObject/DynamicObjectForm/DynamicField/extensions/area.ts @@ -5,6 +5,7 @@ export const AREA_DATA_ATTRS = { type: 'data-hint-gebiedsaanwijzingtype', location: 'data-hint-locatie', label: 'data-gebiedengroep-label', + id: 'data-gebiedengroep-id', } as const type AreaAttributes = { @@ -37,6 +38,7 @@ export const Area = Mark.create({ [AREA_DATA_ATTRS.type]: null, [AREA_DATA_ATTRS.location]: null, [AREA_DATA_ATTRS.label]: null, + [AREA_DATA_ATTRS.id]: null, }, } }, @@ -58,6 +60,9 @@ export const Area = Mark.create({ [AREA_DATA_ATTRS.label]: { default: this.options.HTMLAttributes[AREA_DATA_ATTRS.label], }, + [AREA_DATA_ATTRS.id]: { + default: this.options.HTMLAttributes[AREA_DATA_ATTRS.id], + }, } }, diff --git a/src/components/DynamicObject/ObjectContent/ObjectContent.tsx b/src/components/DynamicObject/ObjectContent/ObjectContent.tsx index 7b9df04c..c1e7dd1f 100644 --- a/src/components/DynamicObject/ObjectContent/ObjectContent.tsx +++ b/src/components/DynamicObject/ObjectContent/ObjectContent.tsx @@ -1,12 +1,16 @@ -import { Heading } from '@pzh-ui/components' +import { Button, Heading, Text, Tooltip } from '@pzh-ui/components' import classNames from 'clsx' import DOMPurify from 'dompurify' +import parse, { domToReact, HTMLReactParserOptions } from 'html-react-parser' +import { useParams } from 'react-router-dom' import { ReadRelationShortNationaalBelangMinimal, ReadRelationShortWettelijkeTaakMinimal, } from '@/api/fetchers.schemas' +import ObjectAreaModal from '@/components/Modals/ObjectModals/ObjectAreaModal' import { ModelReturnType } from '@/config/objects/types' +import useModalStore from '@/store/modalStore' interface ObjectContentProps { /** Object data */ @@ -62,6 +66,9 @@ interface ContentProps { } const Content = ({ title, value, hidden, html, customTitle }: ContentProps) => { + const { moduleId } = useParams() + const setActiveModal = useModalStore(state => state.setActiveModal) + let cleanHtml = DOMPurify.sanitize(html) if (value === 'Weblink') { @@ -71,6 +78,65 @@ const Content = ({ title, value, hidden, html, customTitle }: ContentProps) => { ) } + const options: HTMLReactParserOptions = { + replace: domNode => { + if (domNode.type === 'tag' && domNode.name === 'a') { + const element = domNode as any + const label = element.attribs['data-gebiedengroep-label'] + + if (label) { + return ( + <Tooltip + label={ + <Text + as="span" + size="s" + color="text-pzh-white" + className="block"> + Gebiedsaanwijzing: + <strong className="ml-1 font-bold text-pzh-white"> + {label} + </strong> + </Text> + }> + <Button + key={label} + variant="default" + className="text-pzh-red-900 underline hover:text-pzh-blue-900" + onPress={() => + setActiveModal('objectArea', { + moduleId, + label, + id: element.attribs[ + 'data-gebiedengroep-id' + ], + locatie: + element.attribs[ + 'data-hint-locatie' + ], + gebiedsaanwijzingtype: + element.attribs[ + 'data-hint-gebiedsaanwijzingtype' + ], + gebiedengroep: + element.attribs[ + 'data-hint-gebiedengroep' + ], + }) + } + {...element.attribs}> + {domToReact(element.children, options)} + </Button> + </Tooltip> + ) + } + } + return undefined // Return undefined for other elements to be parsed as usual + }, + } + + const parsedContent = parse(cleanHtml, options) + const Wrapper = value === 'Description' ? 'p' : 'div' return ( @@ -85,10 +151,10 @@ const Content = ({ title, value, hidden, html, customTitle }: ContentProps) => { {customTitle?.[value] || title} </Heading> )} - <Wrapper - className="prose prose-neutral mb-4 max-w-full whitespace-pre-line text-m text-pzh-blue-900 marker:text-pzh-blue-900 prose-li:my-0 md:mb-8" - dangerouslySetInnerHTML={{ __html: cleanHtml }} - /> + <Wrapper className="prose prose-neutral mb-4 max-w-full whitespace-pre-line text-m text-pzh-blue-900 marker:text-pzh-blue-900 prose-li:my-0 md:mb-8"> + {parsedContent} + </Wrapper> + <ObjectAreaModal /> </> ) } diff --git a/src/components/Modals/ObjectModals/ObjectAreaAnnotateModal/ObjectAreaAnnotateModal.tsx b/src/components/Modals/ObjectModals/ObjectAreaAnnotateModal/ObjectAreaAnnotateModal.tsx index f8055698..67a69f83 100644 --- a/src/components/Modals/ObjectModals/ObjectAreaAnnotateModal/ObjectAreaAnnotateModal.tsx +++ b/src/components/Modals/ObjectModals/ObjectAreaAnnotateModal/ObjectAreaAnnotateModal.tsx @@ -43,12 +43,12 @@ const ObjectAreaAnnotateModal = ({ model }: ObjectAreaAnnotateModalProps) => { type: previousValues?.[AREA_DATA_ATTRS.type] ?? '', location: previousValues?.[AREA_DATA_ATTRS.location] ?? '', label: previousValues?.[AREA_DATA_ATTRS.label] ?? '', + id: previousValues?.[AREA_DATA_ATTRS.id] ?? '', } // eslint-disable-next-line react-hooks/exhaustive-deps }, [modalState?.editor, modalState?.editor?.state.selection]) const handleSubmit = (payload: Values) => { - console.log(payload) modalState?.editor ?.chain() .focus() @@ -58,6 +58,7 @@ const ObjectAreaAnnotateModal = ({ model }: ObjectAreaAnnotateModalProps) => { [AREA_DATA_ATTRS.type]: payload.type, [AREA_DATA_ATTRS.location]: payload.location, [AREA_DATA_ATTRS.label]: payload.label, + [AREA_DATA_ATTRS.id]: payload.id, text: isEmptySelection ? payload.label : undefined, }) .run() @@ -161,9 +162,10 @@ const InnerForm = <TData extends Values>({ required placeholder="Zoek op gebiedengroep" objectKey="Werkingsgebied_Code" - onChange={object => + onChange={object => { setFieldValue('label', object?.Title) - } + setFieldValue('id', object?.Object_ID.toString()) + }} defaultValue={ values.label && values.location && { @@ -189,6 +191,7 @@ const InnerForm = <TData extends Values>({ }} /> <FormikInput name="label" type="hidden" /> + <FormikInput name="id" type="hidden" /> </div> <div> <FormikSelect diff --git a/src/components/Modals/ObjectModals/ObjectAreaModal/ObjectAreaModal.tsx b/src/components/Modals/ObjectModals/ObjectAreaModal/ObjectAreaModal.tsx new file mode 100644 index 00000000..b2105aab --- /dev/null +++ b/src/components/Modals/ObjectModals/ObjectAreaModal/ObjectAreaModal.tsx @@ -0,0 +1,79 @@ +import { Text } from '@pzh-ui/components' + +import { + useModulesModuleIdObjectWerkingsgebiedLatestLineageIdGet, + useWerkingsgebiedenLatestLineageIdGet, +} from '@/api/fetchers' +import { LeafletTinyViewer } from '@/components/Leaflet' +import Modal from '@/components/Modal' +import useAuth from '@/hooks/useAuth' +import useModalStore from '@/store/modalStore' + +import { ModalStateMap } from '../../types' + +const ObjectAreaModal = () => { + const { user } = useAuth() + + const modalState = useModalStore( + state => state.modalStates['objectArea'] + ) as ModalStateMap['objectArea'] + + const { + data: moduleData, + isSuccess, + isError, + } = useModulesModuleIdObjectWerkingsgebiedLatestLineageIdGet( + parseInt(modalState?.moduleId), + parseInt(modalState?.id), + { + query: { + enabled: !!modalState?.moduleId && !!modalState?.id && !!user, + }, + } + ) + + const { data: validData } = useWerkingsgebiedenLatestLineageIdGet( + parseInt(modalState?.id), + { + query: { + enabled: + (!modalState?.moduleId && !!modalState?.id) || + (!!modalState?.moduleId && + !!modalState?.id && + !moduleData && + isSuccess) || + isError, + }, + } + ) + + const data = modalState?.moduleId && isSuccess ? moduleData : validData + + return ( + <Modal id="objectArea" size="xl" title="Gebiedsaanwijzing"> + <div className="flex gap-4"> + <div className="flex w-1/2 flex-col gap-4"> + <div> + <Text>Gebiedengroep</Text> + <Text bold>{modalState?.label}</Text> + </div> + <div> + <Text>Type gebiedsaanwijzing</Text> + <Text bold>{modalState?.gebiedsaanwijzingtype}</Text> + </div> + <div> + <Text>Gebiedsaanwijzinggroep</Text> + <Text bold>{modalState?.gebiedengroep}</Text> + </div> + </div> + <div className="h-[360px] w-full overflow-hidden rounded-lg"> + {data?.Area_UUID && ( + <LeafletTinyViewer uuid={data.Area_UUID} /> + )} + </div> + </div> + </Modal> + ) +} + +export default ObjectAreaModal diff --git a/src/components/Modals/ObjectModals/ObjectAreaModal/index.ts b/src/components/Modals/ObjectModals/ObjectAreaModal/index.ts new file mode 100644 index 00000000..ce0bf9a8 --- /dev/null +++ b/src/components/Modals/ObjectModals/ObjectAreaModal/index.ts @@ -0,0 +1 @@ +export { default } from './ObjectAreaModal' diff --git a/src/components/Modals/types.ts b/src/components/Modals/types.ts index e7d2050e..d432b74a 100644 --- a/src/components/Modals/types.ts +++ b/src/components/Modals/types.ts @@ -27,6 +27,7 @@ export type ModalType = | 'moduleEditObject' | 'moduleDeleteObject' | 'areaAdd' + | 'objectArea' | 'objectAreaAnnotate' | 'objectDetails' | 'objectAddConnection' @@ -57,6 +58,14 @@ export interface ModalStateMap { object: ModuleObjectShort module: Module } + objectArea: { + moduleId: string + id: string + locatie: string + gebiedsaanwijzingtype: string + gebiedengroep: string + label: string + } objectAreaAnnotate: { editor: Editor } diff --git a/src/validation/objectAnnotate.ts b/src/validation/objectAnnotate.ts index b623e01f..8f618d11 100644 --- a/src/validation/objectAnnotate.ts +++ b/src/validation/objectAnnotate.ts @@ -13,4 +13,5 @@ export const SCHEMA_OBJECT_ANNOTATE_AREA = object({ }), location: schemaDefaults.requiredString(), label: schemaDefaults.requiredString(), + id: schemaDefaults.requiredString(), }) diff --git a/yarn.lock b/yarn.lock index 985b99c1..6ca7b8bf 100644 --- a/yarn.lock +++ b/yarn.lock @@ -9235,6 +9235,33 @@ __metadata: languageName: node linkType: hard +"dom-serializer@npm:^2.0.0": + version: 2.0.0 + resolution: "dom-serializer@npm:2.0.0" + dependencies: + domelementtype: "npm:^2.3.0" + domhandler: "npm:^5.0.2" + entities: "npm:^4.2.0" + checksum: e3bf9027a64450bca0a72297ecdc1e3abb7a2912268a9f3f5d33a2e29c1e2c3502c6e9f860fc6625940bfe0cfb57a44953262b9e94df76872fdfb8151097eeb3 + languageName: node + linkType: hard + +"domelementtype@npm:^2.3.0": + version: 2.3.0 + resolution: "domelementtype@npm:2.3.0" + checksum: ee837a318ff702622f383409d1f5b25dd1024b692ef64d3096ff702e26339f8e345820f29a68bcdcea8cfee3531776b3382651232fbeae95612d6f0a75efb4f6 + languageName: node + linkType: hard + +"domhandler@npm:5.0.3, domhandler@npm:^5.0.2, domhandler@npm:^5.0.3": + version: 5.0.3 + resolution: "domhandler@npm:5.0.3" + dependencies: + domelementtype: "npm:^2.3.0" + checksum: 809b805a50a9c6884a29f38aec0a4e1b4537f40e1c861950ed47d10b049febe6b79ab72adaeeebb3cc8fc1cd33f34e97048a72a9265103426d93efafa78d3e96 + languageName: node + linkType: hard + "dompurify@npm:^3.0.9": version: 3.0.9 resolution: "dompurify@npm:3.0.9" @@ -9242,6 +9269,17 @@ __metadata: languageName: node linkType: hard +"domutils@npm:^3.1.0": + version: 3.1.0 + resolution: "domutils@npm:3.1.0" + dependencies: + dom-serializer: "npm:^2.0.0" + domelementtype: "npm:^2.3.0" + domhandler: "npm:^5.0.3" + checksum: 9a169a6e57ac4c738269a73ab4caf785114ed70e46254139c1bbc8144ac3102aacb28a6149508395ae34aa5d6a40081f4fa5313855dc8319c6d8359866b6dfea + languageName: node + linkType: hard + "dot-case@npm:^3.0.4": version: 3.0.4 resolution: "dot-case@npm:3.0.4" @@ -9313,7 +9351,7 @@ __metadata: languageName: node linkType: hard -"entities@npm:^4.4.0": +"entities@npm:^4.2.0, entities@npm:^4.4.0, entities@npm:^4.5.0": version: 4.5.0 resolution: "entities@npm:4.5.0" checksum: ede2a35c9bce1aeccd055a1b445d41c75a14a2bb1cd22e242f20cf04d236cdcd7f9c859eb83f76885327bfae0c25bf03303665ee1ce3d47c5927b98b0e3e3d48 @@ -10716,6 +10754,16 @@ __metadata: languageName: node linkType: hard +"html-dom-parser@npm:5.0.10": + version: 5.0.10 + resolution: "html-dom-parser@npm:5.0.10" + dependencies: + domhandler: "npm:5.0.3" + htmlparser2: "npm:9.1.0" + checksum: 14041ffde3907703e1b857657d2f1b20eb3403a85d12f0812311a11c9405c6f3b114177c1867acd231b8bd01e3f69ea278bd38f01ba5278219fdef862cc9bef6 + languageName: node + linkType: hard + "html-encoding-sniffer@npm:^4.0.0": version: 4.0.0 resolution: "html-encoding-sniffer@npm:4.0.0" @@ -10732,6 +10780,36 @@ __metadata: languageName: node linkType: hard +"html-react-parser@npm:^5.1.18": + version: 5.1.18 + resolution: "html-react-parser@npm:5.1.18" + dependencies: + domhandler: "npm:5.0.3" + html-dom-parser: "npm:5.0.10" + react-property: "npm:2.0.2" + style-to-js: "npm:1.1.16" + peerDependencies: + "@types/react": 0.14 || 15 || 16 || 17 || 18 + react: 0.14 || 15 || 16 || 17 || 18 + peerDependenciesMeta: + "@types/react": + optional: true + checksum: 8a4178ff2dd7d4887e047e1f95eea7811e66c0c5e55fd946eebf704f32e47960adb635aa64eae3f11de92a56b33f8719349633a2fe6916c00ebe70794b5b064f + languageName: node + linkType: hard + +"htmlparser2@npm:9.1.0": + version: 9.1.0 + resolution: "htmlparser2@npm:9.1.0" + dependencies: + domelementtype: "npm:^2.3.0" + domhandler: "npm:^5.0.3" + domutils: "npm:^3.1.0" + entities: "npm:^4.5.0" + checksum: 6352fa2a5495781fa9a02c9049908334cd068ff36d753870d30cd13b841e99c19646717567a2f9e9c44075bbe43d364e102f9d013a731ce962226d63746b794f + languageName: node + linkType: hard + "http-cache-semantics@npm:^4.1.1": version: 4.1.1 resolution: "http-cache-semantics@npm:4.1.1" @@ -10864,6 +10942,13 @@ __metadata: languageName: node linkType: hard +"inline-style-parser@npm:0.2.4": + version: 0.2.4 + resolution: "inline-style-parser@npm:0.2.4" + checksum: 80814479d1f3c9cbd102f9de4cd6558cf43cc2e48640e81c4371c3634f1e8b6dfeb2f21063cfa31d46cc83e834c20cd59ed9eeed9bfd45ef5bc02187ad941faf + languageName: node + linkType: hard + "internal-slot@npm:^1.0.4, internal-slot@npm:^1.0.5, internal-slot@npm:^1.0.7": version: 1.0.7 resolution: "internal-slot@npm:1.0.7" @@ -13445,6 +13530,7 @@ __metadata: eslint: "npm:^8.48.0" eslint-plugin-prettier: "npm:^5.0.0" formik: "npm:^2.4.6" + html-react-parser: "npm:^5.1.18" jsdom: "npm:^25.0.1" jwt-decode: "npm:^4.0.0" leaflet: "npm:^1.9.4" @@ -13748,6 +13834,13 @@ __metadata: languageName: node linkType: hard +"react-property@npm:2.0.2": + version: 2.0.2 + resolution: "react-property@npm:2.0.2" + checksum: 3a4bc1951b2b7992cb8a2d3f12016dd0920d1c06eb58b456204a6ae1210401d62baece098d3200ed8a0513dde247a5d96ffdb24f354e32ce5a9b26fbd8552668 + languageName: node + linkType: hard + "react-refresh@npm:^0.14.2": version: 0.14.2 resolution: "react-refresh@npm:0.14.2" @@ -14842,6 +14935,24 @@ __metadata: languageName: node linkType: hard +"style-to-js@npm:1.1.16": + version: 1.1.16 + resolution: "style-to-js@npm:1.1.16" + dependencies: + style-to-object: "npm:1.0.8" + checksum: a876cc49a29ac90c7723b4d6f002ac6c1ac5ccc6b5bc963d9c607cfc74b15927b704c9324df6f824f576c65689fe4b4ff79caabcd44a13d8a02641f721f1b316 + languageName: node + linkType: hard + +"style-to-object@npm:1.0.8": + version: 1.0.8 + resolution: "style-to-object@npm:1.0.8" + dependencies: + inline-style-parser: "npm:0.2.4" + checksum: 530b067325e3119bfaf75bdbe25cc86b02b559db00d881a74b98a2d5bb10ac953d1b455ed90c825963cf3b4bdaa1bda45f406d78d987391434b8d8ab3835df4e + languageName: node + linkType: hard + "stylis@npm:4.2.0": version: 4.2.0 resolution: "stylis@npm:4.2.0"