Refactor PopupForm and PopupView

This commit is contained in:
Maximilian Harz 2025-03-18 11:17:16 +01:00
parent a6c0c273c7
commit eb054f4d1f
11 changed files with 72 additions and 90 deletions

View File

@ -1,6 +1,3 @@
import { useContext } from 'react'
import LayerContext from '#components/Map/LayerContext'
import { ItemFormPopup } from '#components/Map/Subcomponents/ItemFormPopup'
import TemplateItemContext from './TemplateItemContext'
@ -9,10 +6,8 @@ import TemplateItemContext from './TemplateItemContext'
* @category Item
*/
export const PopupForm = ({ children }: { children?: React.ReactNode }) => {
const { itemFormPopup, setItemFormPopup } = useContext(LayerContext)
return (
itemFormPopup && (
<ItemFormPopup
key={setItemFormPopup?.name}
position={itemFormPopup.position}

View File

@ -29,14 +29,7 @@ import type { Popup } from 'leaflet'
*/
export const PopupView = ({ children }: { children?: React.ReactNode }) => {
const cardViewContext = useContext(LayerContext)
const {
name,
markerDefaultColor,
markerDefaultColor2,
markerShape,
markerIcon,
setItemFormPopup,
} = cardViewContext
const { name, markerDefaultColor, markerDefaultColor2, markerShape, markerIcon } = cardViewContext
const filterTags = useFilterTags()
@ -94,10 +87,6 @@ export const PopupView = ({ children }: { children?: React.ReactNode }) => {
],
)
if (!setItemFormPopup) {
throw new Error('setItemFormPopup is not defined')
}
return visibleItems.map((item: Item) => {
if (!(item.position?.coordinates[0] && item.position.coordinates[1])) return null
@ -170,7 +159,6 @@ export const PopupView = ({ children }: { children?: React.ReactNode }) => {
}
}}
item={item}
setItemFormPopup={setItemFormPopup}
>
{children}
</ItemViewPopup>

View File

@ -35,9 +35,6 @@ export const Layer = ({
// eslint-disable-next-line camelcase
public_edit_items,
listed = true,
setItemFormPopup,
itemFormPopup,
clusterRef,
}: LayerProps) => {
const setItemsApi = useSetItemsApi()
const setItemsData = useSetItemsData()
@ -68,10 +65,6 @@ export const Layer = ({
// eslint-disable-next-line camelcase
public_edit_items,
listed,
setItemFormPopup,
itemFormPopup,
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
clusterRef,
})
api &&
setItemsApi({
@ -93,10 +86,6 @@ export const Layer = ({
// eslint-disable-next-line camelcase
public_edit_items,
listed,
setItemFormPopup,
itemFormPopup,
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
clusterRef,
})
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [data, api])
@ -123,8 +112,6 @@ export const Layer = ({
markerDefaultColor2,
markerShape,
markerIcon,
itemFormPopup,
setItemFormPopup,
}}
>
{children}

View File

@ -1,15 +1,11 @@
import { createContext } from 'react'
import type { ItemFormPopupProps } from '#types/ItemFormPopupProps'
interface LayerContextType {
name: string
markerDefaultColor: string
markerDefaultColor2: string
markerShape: string
markerIcon: string
itemFormPopup: ItemFormPopupProps | null | undefined
setItemFormPopup: React.Dispatch<React.SetStateAction<ItemFormPopupProps | null>> | undefined
}
const LayerContext = createContext<LayerContextType>({
@ -18,8 +14,6 @@ const LayerContext = createContext<LayerContextType>({
markerDefaultColor2: '',
markerShape: '',
markerIcon: '',
itemFormPopup: undefined,
setItemFormPopup: undefined,
})
export default LayerContext

View File

@ -2,7 +2,6 @@
/* eslint-disable @typescript-eslint/no-misused-promises */
/* eslint-disable @typescript-eslint/prefer-optional-chain */
/* eslint-disable @typescript-eslint/no-unsafe-argument */
/* eslint-disable @typescript-eslint/no-unused-vars */
/* eslint-disable @typescript-eslint/no-unsafe-call */
/* eslint-disable @typescript-eslint/no-non-null-assertion */
/* eslint-disable @typescript-eslint/no-unsafe-member-access */
@ -14,18 +13,22 @@ import { useAuth } from '#components/Auth/useAuth'
import { TextAreaInput } from '#components/Input/TextAreaInput'
import { TextInput } from '#components/Input/TextInput'
import { useResetFilterTags } from '#components/Map/hooks/useFilter'
import { useAddItem, useItems, useRemoveItem, useUpdateItem } from '#components/Map/hooks/useItems'
import { useAddItem, useItems, useUpdateItem } from '#components/Map/hooks/useItems'
import { usePopupForm } from '#components/Map/hooks/usePopupForm'
import { useAddTag, useTags } from '#components/Map/hooks/useTags'
import { hashTagRegex } from '#utils/HashTagRegex'
import { randomColor } from '#utils/RandomColor'
import type { Item } from '#types/Item'
import type { ItemFormPopupProps } from '#types/ItemFormPopupProps'
export function ItemFormPopup(props: ItemFormPopupProps) {
interface Props {
children?: React.ReactNode
}
export function ItemFormPopup(props: Props) {
const [spinner, setSpinner] = useState(false)
const [popupTitle, setPopupTitle] = useState<string>('')
const [, setPopupTitle] = useState<string>('')
const formRef = useRef<HTMLFormElement>(null)
@ -35,8 +38,6 @@ export function ItemFormPopup(props: ItemFormPopupProps) {
const updateItem = useUpdateItem()
const items = useItems()
const removeItem = useRemoveItem()
const tags = useTags()
const addTag = useAddTag()
@ -44,14 +45,22 @@ export function ItemFormPopup(props: ItemFormPopupProps) {
const { user } = useAuth()
const { popupForm, setPopupForm } = usePopupForm()
const handleSubmit = async (evt: any) => {
if (!popupForm) {
throw new Error('Popup form is not defined')
}
const formItem: Item = {} as Item
Array.from(evt.target).forEach((input: HTMLInputElement) => {
if (input.name) {
formItem[input.name] = input.value
}
})
formItem.position = { type: 'Point', coordinates: [props.position.lng, props.position.lat] }
formItem.position = {
type: 'Point',
coordinates: [popupForm.position.lng, popupForm.position.lat],
}
evt.preventDefault()
const name = formItem.name ? formItem.name : user?.first_name
@ -126,7 +135,7 @@ export function ItemFormPopup(props: ItemFormPopupProps) {
setSpinner(false)
map.closePopup()
}
props.setItemFormPopup!(null)
setPopupForm(null)
}
const resetPopup = () => {

View File

@ -8,12 +8,13 @@
/* eslint-disable @typescript-eslint/restrict-template-expressions */
/* eslint-disable @typescript-eslint/no-unsafe-member-access */
import { LatLng } from 'leaflet'
import { Children, cloneElement, forwardRef, isValidElement, useState } from 'react'
import { forwardRef, useState } from 'react'
import { Popup as LeafletPopup, useMap } from 'react-leaflet'
import { useNavigate } from 'react-router-dom'
import { toast } from 'react-toastify'
import { useRemoveItem, useUpdateItem } from '#components/Map/hooks/useItems'
import { usePopupForm } from '#components/Map/hooks/usePopupForm'
import { useSetSelectPosition } from '#components/Map/hooks/useSelectPosition'
import { timeAgo } from '#utils/TimeAgo'
@ -21,12 +22,10 @@ import { HeaderView } from './ItemPopupComponents/HeaderView'
import { TextView } from './ItemPopupComponents/TextView'
import type { Item } from '#types/Item'
import type { ItemFormPopupProps } from '#types/ItemFormPopupProps'
export interface ItemViewPopupProps {
item: Item
children?: React.ReactNode
setItemFormPopup?: React.Dispatch<React.SetStateAction<ItemFormPopupProps | null>>
}
// eslint-disable-next-line react/display-name
@ -37,22 +36,22 @@ export const ItemViewPopup = forwardRef((props: ItemViewPopupProps, ref: any) =>
const updadateItem = useUpdateItem()
const navigate = useNavigate()
const setSelectPosition = useSetSelectPosition()
const { setPopupForm } = usePopupForm()
const [infoExpanded, setInfoExpanded] = useState<boolean>(false)
const handleEdit = (event: React.MouseEvent<HTMLElement>) => {
event.stopPropagation()
map.closePopup()
props.setItemFormPopup &&
props.setItemFormPopup({
position: new LatLng(
props.item.position?.coordinates[1]!,
props.item.position?.coordinates[0]!,
),
layer: props.item.layer!,
item: props.item,
setItemFormPopup: props.setItemFormPopup,
})
setPopupForm({
position: new LatLng(
props.item.position?.coordinates[1]!,
props.item.position?.coordinates[0]!,
),
layer: props.item.layer!,
item: props.item,
})
}
const handleDelete = async (event: React.MouseEvent<HTMLElement>) => {
@ -97,16 +96,8 @@ export const ItemViewPopup = forwardRef((props: ItemViewPopupProps, ref: any) =>
}}
loading={loading}
/>
<div className='tw:overflow-y-auto tw:overflow-x-hidden tw:max-h-64 fade'>
{props.children ? (
Children.toArray(props.children).map((child) =>
isValidElement<{ item: Item; test: string }>(child)
? cloneElement(child, { item: props.item })
: '',
)
) : (
<TextView text={props.item.text} itemId={props.item.id} />
)}
<div className='tw-overflow-y-auto tw-overflow-x-hidden tw-max-h-64 fade'>
{props.children ?? <TextView text={props.item.text} itemId={props.item.id} />}
</div>
<div className='tw:flex tw:-mb-1 tw:flex-row tw:mr-2 tw:mt-1'>
{infoExpanded ? (

View File

@ -6,7 +6,7 @@
/* eslint-disable @typescript-eslint/no-unsafe-member-access */
/* eslint-disable @typescript-eslint/no-unsafe-call */
/* eslint-disable @typescript-eslint/no-unsafe-argument */
import { Children, cloneElement, isValidElement, useEffect, useRef, useState } from 'react'
import { useEffect, useRef } from 'react'
import { TileLayer, useMapEvents, GeoJSON, useMap } from 'react-leaflet'
import MarkerClusterGroup from 'react-leaflet-cluster'
import { Outlet, useLocation } from 'react-router-dom'
@ -20,6 +20,7 @@ import { useClusterRef, useSetClusterRef } from './hooks/useClusterRef'
import { useAddVisibleLayer } from './hooks/useFilter'
import { useLayers } from './hooks/useLayers'
import { useLeafletRefs } from './hooks/useLeafletRefs'
import { usePopupForm } from './hooks/usePopupForm'
import {
useSelectPosition,
useSetMapClicked,
@ -35,7 +36,6 @@ import { TagsControl } from './Subcomponents/Controls/TagsControl'
import { TextView } from './Subcomponents/ItemPopupComponents/TextView'
import { SelectPosition } from './Subcomponents/SelectPosition'
import type { ItemFormPopupProps } from '#types/ItemFormPopupProps'
import type { Feature, Geometry as GeoJSONGeometry, GeoJsonObject } from 'geojson'
export function UtopiaMapInner({
@ -62,7 +62,7 @@ export function UtopiaMapInner({
const setClusterRef = useSetClusterRef()
const clusterRef = useClusterRef()
const setMapClicked = useSetMapClicked()
const [itemFormPopup, setItemFormPopup] = useState<ItemFormPopupProps | null>(null)
const { setPopupForm } = usePopupForm()
useTheme(defaultTheme)
@ -119,7 +119,7 @@ export function UtopiaMapInner({
// eslint-disable-next-line no-console
console.log(e.latlng.lat + ',' + e.latlng.lng)
if (selectNewItemPosition) {
setMapClicked({ position: e.latlng, setItemFormPopup })
setMapClicked({ position: e.latlng, setItemFormPopup: setPopupForm })
}
},
moveend: () => {},
@ -224,15 +224,7 @@ export function UtopiaMapInner({
maxClusterRadius={50}
removeOutsideVisibleBounds={false}
>
{Children.toArray(children).map((child) =>
isValidElement<{
setItemFormPopup: React.Dispatch<React.SetStateAction<ItemFormPopupProps>>
itemFormPopup: ItemFormPopupProps | null
clusterRef: React.MutableRefObject<undefined>
}>(child)
? cloneElement(child, { setItemFormPopup, itemFormPopup, clusterRef })
: child,
)}
{children}
</MarkerClusterGroup>
{geo && (
<GeoJSON
@ -242,7 +234,7 @@ export function UtopiaMapInner({
click: (e) => {
if (selectNewItemPosition) {
e.layer.closePopup()
setMapClicked({ position: e.latlng, setItemFormPopup })
setMapClicked({ position: e.latlng, setItemFormPopup: setPopupForm })
}
},
}}

View File

@ -0,0 +1,32 @@
import { createContext, useContext, useState } from 'react'
import type { ItemFormPopupProps } from '#types/ItemFormPopupProps'
type UsePopupFormManagerResult = ReturnType<typeof usePopupFormManager>
const PoupFormContext = createContext<UsePopupFormManagerResult>({
popupForm: {} as ItemFormPopupProps | null,
setPopupForm: () => {},
})
function usePopupFormManager(): {
popupForm: ItemFormPopupProps | null
setPopupForm: React.Dispatch<React.SetStateAction<ItemFormPopupProps | null>>
} {
const [popupForm, setPopupForm] = useState<ItemFormPopupProps | null>(null)
return { popupForm, setPopupForm }
}
interface Props {
children?: React.ReactNode
}
export const ClusterRefProvider: React.FunctionComponent<Props> = ({ children }: Props) => (
<PoupFormContext.Provider value={usePopupFormManager()}>{children}</PoupFormContext.Provider>
)
export const usePopupForm = (): UsePopupFormManagerResult => {
const { popupForm, setPopupForm } = useContext(PoupFormContext)
return { popupForm, setPopupForm }
}

View File

@ -7,5 +7,4 @@ export interface ItemFormPopupProps {
layer: LayerProps
item?: Item
children?: React.ReactNode
setItemFormPopup?: React.Dispatch<React.SetStateAction<ItemFormPopupProps | null>>
}

View File

@ -1,5 +1,4 @@
import type { Item } from './Item'
import type { ItemFormPopupProps } from './ItemFormPopupProps'
import type { ItemsApi } from './ItemsApi'
import type { ItemType } from './ItemType'
@ -26,8 +25,4 @@ export interface LayerProps {
public_edit_items?: boolean
listed?: boolean
item_presets?: Record<string, unknown>
setItemFormPopup?: React.Dispatch<React.SetStateAction<ItemFormPopupProps | null>>
itemFormPopup?: ItemFormPopupProps | null
// eslint-disable-next-line @typescript-eslint/no-explicit-any
clusterRef?: any
}

View File

@ -15,7 +15,7 @@ export default defineConfig({
exclude: [...configDefaults.exclude, 'src/**/*.cy.tsx'],
reporter: ['html', 'json-summary'],
thresholds: {
lines: 1,
lines: 2,
functions: 54,
branches: 56,
statements: 1,