Merge pull request #90 from utopia-os/remove-get-value

Improves typing of items
Removes getValue because it is super hard to type and was introduced for a customisation which is not used anymore. if needed, we could have a cleaner solution to offer that functionality.
This commit is contained in:
Max 2025-02-17 18:28:15 +01:00 committed by GitHub
commit c38d1283e1
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
38 changed files with 3557 additions and 1590 deletions

4648
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -94,6 +94,7 @@
"leaflet": "^1.9.4",
"leaflet.locatecontrol": "^0.79.0",
"prop-types": "^15.8.1",
"radash": "^12.1.0",
"react-colorful": "^5.6.1",
"react-image-crop": "^10.1.8",
"react-leaflet": "^4.2.1",

View File

@ -1,16 +1,9 @@
/* eslint-disable @typescript-eslint/restrict-plus-operands */
/* eslint-disable @typescript-eslint/no-unsafe-call */
/* eslint-disable @typescript-eslint/no-unsafe-return */
/* eslint-disable @typescript-eslint/no-unsafe-assignment */
/* eslint-disable @typescript-eslint/no-unnecessary-condition */
/* eslint-disable @typescript-eslint/no-unsafe-argument */
/* eslint-disable @typescript-eslint/prefer-optional-chain */
/* eslint-disable @typescript-eslint/no-unsafe-member-access */
import { Children, isValidElement, useEffect, useState } from 'react'
import { Marker, Tooltip } from 'react-leaflet'
import { encodeTag } from '#utils/FormatTags'
import { getValue } from '#utils/GetValue'
import { hashTagRegex } from '#utils/HashTagRegex'
import MarkerIconFactory from '#utils/MarkerIconFactory'
import { randomColor } from '#utils/RandomColor'
@ -47,17 +40,6 @@ export const Layer = ({
markerDefaultColor2 = 'RGBA(35, 31, 32, 0.2)',
api,
itemType,
itemNameField = 'name',
itemSubnameField,
itemTextField = 'text',
itemAvatarField,
itemColorField,
itemOwnerField,
itemLatitudeField = 'position.coordinates.1',
itemLongitudeField = 'position.coordinates.0',
itemTagsField,
itemOffersField,
itemNeedsField,
onlyOnePerOwner = false,
customEditLink,
customEditParameter,
@ -110,16 +92,8 @@ export const Layer = ({
markerDefaultColor2,
api,
itemType,
itemNameField,
itemSubnameField,
itemTextField,
itemAvatarField,
itemColorField,
itemOwnerField,
itemTagsField,
itemOffersField,
itemNeedsField,
onlyOnePerOwner,
// Can we just use editCallback for all cases?
customEditLink,
customEditParameter,
// eslint-disable-next-line camelcase
@ -127,6 +101,7 @@ export const Layer = ({
listed,
setItemFormPopup,
itemFormPopup,
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
clusterRef,
})
api &&
@ -143,15 +118,6 @@ export const Layer = ({
markerDefaultColor2,
api,
itemType,
itemNameField,
itemSubnameField,
itemTextField,
itemAvatarField,
itemColorField,
itemOwnerField,
itemTagsField,
itemOffersField,
itemNeedsField,
onlyOnePerOwner,
customEditLink,
customEditParameter,
@ -160,6 +126,7 @@ export const Layer = ({
listed,
setItemFormPopup,
itemFormPopup,
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
clusterRef,
})
// eslint-disable-next-line react-hooks/exhaustive-deps
@ -201,29 +168,19 @@ export const Layer = ({
visibleGroupTypes.length === 0,
)
.map((item: Item) => {
if (getValue(item, itemLongitudeField) && getValue(item, itemLatitudeField)) {
// eslint-disable-next-line security/detect-object-injection
if (getValue(item, itemTextField)) item[itemTextField] = getValue(item, itemTextField)
// eslint-disable-next-line security/detect-object-injection
else item[itemTextField] = ''
if (item.position?.coordinates[0] && item.position?.coordinates[1]) {
if (item.tags) {
// eslint-disable-next-line security/detect-object-injection
item[itemTextField] = item[itemTextField] + '\n\n'
item.text += '\n\n'
item.tags.map((tag) => {
// eslint-disable-next-line security/detect-object-injection
if (!item[itemTextField].includes(`#${encodeTag(tag)}`)) {
// eslint-disable-next-line security/detect-object-injection
return (item[itemTextField] = item[itemTextField] + `#${encodeTag(tag)} `)
if (!item.text?.includes(`#${encodeTag(tag)}`)) {
item.text += `#${encodeTag(tag)}`
}
// eslint-disable-next-line security/detect-object-injection
return item[itemTextField]
return item.text
})
}
if (allTagsLoaded && allItemsLoaded) {
// eslint-disable-next-line security/detect-object-injection
item[itemTextField].match(hashTagRegex)?.map((tag) => {
item.text?.match(hashTagRegex)?.map((tag) => {
if (
!tags.find(
(t) => t.name.toLocaleLowerCase() === tag.slice(1).toLocaleLowerCase(),
@ -246,20 +203,19 @@ export const Layer = ({
const itemTags = getItemTags(item)
const latitude =
itemLatitudeField && item ? getValue(item, itemLatitudeField) : undefined
const longitude =
itemLongitudeField && item ? getValue(item, itemLongitudeField) : undefined
const latitude = item.position.coordinates[1]
const longitude = item.position.coordinates[0]
let color1 = markerDefaultColor
let color2 = markerDefaultColor2
if (itemColorField && getValue(item, itemColorField) != null)
color1 = getValue(item, itemColorField)
else if (itemTags && itemTags[0]) {
if (item.color) {
color1 = item.color
} else if (itemTags[0]) {
color1 = itemTags[0].color
}
if (itemTags && itemTags[0] && itemColorField) color2 = itemTags[0].color
else if (itemTags && itemTags[1]) {
if (itemTags[0] && item.color) {
color2 = itemTags[0].color
} else if (itemTags[1]) {
color2 = itemTags[1].color
}
return (
@ -319,7 +275,7 @@ export const Layer = ({
)}
<Tooltip offset={[0, -38]} direction='top'>
{item.name ? item.name : getValue(item, itemNameField)}
{item.name}
</Tooltip>
</Marker>
)

View File

@ -23,7 +23,6 @@ import { useLeafletRefs } from '#components/Map/hooks/useLeafletRefs'
import { useTags } from '#components/Map/hooks/useTags'
import useWindowDimensions from '#components/Map/hooks/useWindowDimension'
import { decodeTag } from '#utils/FormatTags'
import { getValue } from '#utils/GetValue'
import MarkerIconFactory from '#utils/MarkerIconFactory'
import { LocateControl } from './LocateControl'
@ -73,12 +72,10 @@ export const SearchControl = () => {
searchGeo()
setItemsResults(
items.filter((item) => {
if (item.layer?.itemNameField) item.name = getValue(item, item.layer.itemNameField)
if (item.layer?.itemTextField) item.text = getValue(item, item.layer.itemTextField)
return (
value.length > 2 &&
((item.layer?.listed && item.name.toLowerCase().includes(value.toLowerCase())) ||
item.text.toLowerCase().includes(value.toLowerCase()))
item.text?.toLowerCase().includes(value.toLowerCase()))
)
}),
)

View File

@ -189,7 +189,7 @@ export function ItemFormPopup(props: ItemFormPopupProps) {
key={props.position.toString()}
placeholder='Text'
dataField='text'
defaultValue={props.item ? props.item.text : ''}
defaultValue={props.item?.text ?? ''}
inputStyle='tw-h-40 tw-mt-5'
/>
</>

View File

@ -15,7 +15,6 @@ import { useNavigate } from 'react-router-dom'
import { useAppState } from '#components/AppShell/hooks/useAppState'
import { useHasUserPermission } from '#components/Map/hooks/usePermissions'
import DialogModal from '#components/Templates/DialogModal'
import { getValue } from '#utils/GetValue'
import type { Item } from '#types/Item'
import type { ItemsApi } from '#types/ItemsApi'
@ -26,9 +25,6 @@ export function HeaderView({
editCallback,
deleteCallback,
setPositionCallback,
itemNameField,
itemSubnameField,
itemAvatarField,
loading,
hideMenu = false,
big = false,
@ -41,9 +37,6 @@ export function HeaderView({
editCallback?: any
deleteCallback?: any
setPositionCallback?: any
itemNameField?: string
itemAvatarField?: string
itemSubnameField?: string
loading?: boolean
hideMenu?: boolean
big?: boolean
@ -64,22 +57,10 @@ export function HeaderView({
}, [item])
const avatar =
itemAvatarField && getValue(item, itemAvatarField)
? appState.assetsApi.url +
getValue(item, itemAvatarField) +
`${big ? '?width=160&heigth=160' : '?width=80&heigth=80'}`
: item.layer?.itemAvatarField &&
item &&
getValue(item, item.layer?.itemAvatarField) &&
appState.assetsApi.url +
getValue(item, item.layer?.itemAvatarField) +
`${big ? '?width=160&heigth=160' : '?width=80&heigth=80'}`
const title = itemNameField
? getValue(item, itemNameField)
: item.layer?.itemNameField && item && getValue(item, item.layer.itemNameField)
const subtitle = itemSubnameField
? getValue(item, itemSubnameField)
: item.layer?.itemSubnameField && item && getValue(item, item.layer.itemSubnameField)
item.image &&
appState.assetsApi.url + item.image + `${big ? '?width=160&heigth=160' : '?width=80&heigth=80'}`
const title = item.name
const subtitle = item.subname
const [address] = useState<string>('')
@ -168,7 +149,7 @@ export function HeaderView({
onClick={(e) =>
item.layer?.customEditLink
? navigate(
`${item.layer.customEditLink}${item.layer.customEditParameter ? `/${getValue(item, item.layer.customEditParameter)}${params && '?' + params}` : ''} `,
`${item.layer.customEditLink}${item.layer.customEditParameter ? `/${item.id}${params && '?' + params}` : ''} `,
)
: editCallback(e)
}

View File

@ -3,7 +3,6 @@
import { Link } from 'react-router-dom'
import { useGetItemTags } from '#components/Map/hooks/useTags'
import { getValue } from '#utils/GetValue'
import type { Item } from '#types/Item'
@ -11,23 +10,21 @@ export const PopupButton = ({
url,
parameterField,
text,
colorField,
item,
}: {
url: string
parameterField?: string
text: string
colorField?: string
item?: Item
}) => {
const params = new URLSearchParams(window.location.search)
const getItemTags = useGetItemTags()
return (
<Link to={`${url}/${parameterField ? getValue(item, parameterField) : ''}?${params}`}>
<Link to={`${url}/${parameterField ? item?.id : ''}?${params}`}>
<button
style={{
backgroundColor: `${colorField && getValue(item, colorField) ? getValue(item, colorField) : item && getItemTags(item) && getItemTags(item)[0] && getItemTags(item)[0].color ? getItemTags(item)[0].color : item?.layer?.markerDefaultColor ? item?.layer?.markerDefaultColor : '#000'}`,
backgroundColor: `${item?.color ?? (item && (getItemTags(item) && getItemTags(item)[0] && getItemTags(item)[0].color ? getItemTags(item)[0].color : (item?.layer?.markerDefaultColor ?? '#000')))}`,
}}
className='tw-btn tw-text-white tw-btn-sm tw-float-right tw-mt-1'
>

View File

@ -12,7 +12,6 @@ import remarkBreaks from 'remark-breaks'
import { useAddFilterTag } from '#components/Map/hooks/useFilter'
import { useTags } from '#components/Map/hooks/useTags'
import { decodeTag } from '#utils/FormatTags'
import { getValue } from '#utils/GetValue'
import { hashTagRegex } from '#utils/HashTagRegex'
import { fixUrls, mailRegex } from '#utils/ReplaceURLs'
@ -21,32 +20,37 @@ import type { Tag } from '#types/Tag'
export const TextView = ({
item,
itemId,
text,
truncate = false,
itemTextField,
rawText,
}: {
item?: Item
itemId: string
text?: string
truncate?: boolean
itemTextField?: string
rawText?: string
}) => {
if (item) {
text = item.text
itemId = item.id
}
const tags = useTags()
const addFilterTag = useAddFilterTag()
let text = ''
let innerText = ''
let replacedText = ''
if (rawText) {
text = replacedText = rawText
} else if (itemTextField && item) {
text = getValue(item, itemTextField)
} else {
text = item?.layer?.itemTextField && item ? getValue(item, item.layer.itemTextField) : ''
innerText = replacedText = rawText
} else if (text) {
innerText = text
}
if (item && text && truncate) text = truncateText(removeMarkdownKeepLinksAndParagraphs(text), 100)
if (innerText && truncate)
innerText = truncateText(removeMarkdownKeepLinksAndParagraphs(innerText), 100)
if (item && text) replacedText = fixUrls(text)
if (innerText) replacedText = fixUrls(innerText)
if (replacedText) {
replacedText = replacedText.replace(/(?<!\]?\()https?:\/\/[^\s)]+(?!\))/g, (url) => {
@ -114,16 +118,16 @@ export const TextView = ({
const CustomHashTagLink = ({
children,
tag,
item,
itemId,
}: {
children: string
tag: Tag
item?: Item
itemId: string
}) => {
return (
<a
style={{ color: tag ? tag.color : '#faa', fontWeight: 'bold', cursor: 'pointer' }}
key={tag ? tag.name + item?.id : item?.id}
key={tag ? tag.name + itemId : itemId}
onClick={(e) => {
e.stopPropagation()
addFilterTag(tag)
@ -173,7 +177,7 @@ export const TextView = ({
)
if (tag)
return (
<CustomHashTagLink tag={tag} item={item}>
<CustomHashTagLink tag={tag} itemId={itemId}>
{children}
</CustomHashTagLink>
)

View File

@ -105,7 +105,7 @@ export const ItemViewPopup = forwardRef((props: ItemViewPopupProps, ref: any) =>
: '',
)
) : (
<TextView item={props.item} />
<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'>

View File

@ -15,7 +15,6 @@ import { toast } from 'react-toastify'
import './UtopiaMap.css'
import { containsUUID } from '#utils/ContainsUUID'
import { getValue } from '#utils/GetValue'
import { useClusterRef, useSetClusterRef } from './hooks/useClusterRef'
import { useAddVisibleLayer } from './hooks/useFilter'
@ -75,9 +74,10 @@ export function UtopiaMapInner({
setTimeout(() => {
toast(
<>
<TextView rawText={'## Do you like this Map?'} />
<TextView itemId='' rawText={'## Do you like this Map?'} />
<div>
<TextView
itemId=''
rawText={'Support us building free opensource maps and help us grow 🌱☀️'}
/>
<PopupButton url={'https://opencollective.com/utopia-project'} text={'Donate'} />
@ -120,7 +120,6 @@ export function UtopiaMapInner({
}
let title = ''
if (item?.name) title = item.name
else if (item?.layer?.itemNameField) title = getValue(item, item.layer.itemNameField)
document.title = `${document.title.split('-')[0]} - ${title}`
}
},
@ -142,15 +141,13 @@ export function UtopiaMapInner({
})
let title = ''
if (ref.item.name) title = ref.item.name
else if (ref.item.layer?.itemNameField)
title = getValue(ref.item.name, ref.item.layer.itemNameField)
document.title = `${document.title.split('-')[0]} - ${title}`
document
.querySelector('meta[property="og:title"]')
?.setAttribute('content', ref.item.name)
document
.querySelector('meta[property="og:description"]')
?.setAttribute('content', ref.item.text)
?.setAttribute('content', ref.item.text ?? '')
}
}
}

View File

@ -3,7 +3,7 @@
/* eslint-disable @typescript-eslint/no-empty-function */
/* eslint-disable @typescript-eslint/no-non-null-assertion */
/* eslint-disable @typescript-eslint/restrict-template-expressions */
/* eslint-disable @typescript-eslint/no-unsafe-assignment */
/* eslint-disable @typescript-eslint/no-misused-promises */
import { useCallback, useReducer, createContext, useContext, useState } from 'react'
import { toast } from 'react-toastify'
@ -82,6 +82,7 @@ function useItemsManager(initialItems: Item[]): {
},
})
result.map((item) => {
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
dispatch({ type: 'ADD', item: { ...item, layer } })
return null
})

View File

@ -63,7 +63,7 @@ function useSelectPositionManager(): {
if ('menuIcon' in selectPosition) {
mapClicked &&
mapClicked.setItemFormPopup({
layer: selectPosition as LayerProps,
layer: selectPosition,
position: mapClicked.position,
})
setSelectPosition(null)

View File

@ -5,12 +5,8 @@
/* eslint-disable @typescript-eslint/prefer-optional-chain */
/* eslint-disable @typescript-eslint/no-non-null-assertion */
/* eslint-disable @typescript-eslint/no-unnecessary-condition */
/* eslint-disable @typescript-eslint/no-unsafe-assignment */
/* eslint-disable @typescript-eslint/no-unsafe-call */
/* eslint-disable @typescript-eslint/no-unsafe-member-access */
import { useCallback, useReducer, createContext, useContext, useState } from 'react'
import { getValue } from '#utils/GetValue'
import { hashTagRegex } from '#utils/HashTagRegex'
import type { Item } from '#types/Item'
@ -96,8 +92,7 @@ function useTagsManager(initialTags: Tag[]): {
const getItemTags = useCallback(
(item: Item) => {
const text =
item.layer?.itemTextField && item ? getValue(item, item.layer.itemTextField) : undefined
const text = item.text
const itemTagStrings = text?.match(hashTagRegex)
const itemTags: Tag[] = []
itemTagStrings?.map((tag) => {
@ -108,18 +103,15 @@ function useTagsManager(initialTags: Tag[]): {
}
return null
})
item.layer?.itemOffersField &&
getValue(item, item.layer.itemOffersField)?.map((o) => {
const offer = tags.find((t) => t.id === o.tags_id)
offer && itemTags.push(offer)
return null
})
item.layer?.itemNeedsField &&
getValue(item, item.layer.itemNeedsField)?.map((n) => {
const need = tags.find((t) => t.id === n.tags_id)
need && itemTags.push(need)
return null
})
// Could be refactored as it occurs in multiple places
item.offers?.forEach((o) => {
const offer = tags.find((t) => t.id === o.tags_id)
offer && itemTags.push(offer)
})
item.needs?.forEach((n) => {
const need = tags.find((t) => t.id === n.tags_id)
need && itemTags.push(need)
})
return itemTags
},

View File

@ -1,8 +1,5 @@
/* eslint-disable @typescript-eslint/no-unsafe-member-access */
/* eslint-disable @typescript-eslint/no-unsafe-call */
/* eslint-disable @typescript-eslint/restrict-template-expressions */
/* eslint-disable @typescript-eslint/no-unnecessary-condition */
/* eslint-disable @typescript-eslint/no-unsafe-assignment */
/* eslint-disable @typescript-eslint/no-unsafe-argument */
import { useEffect, useState } from 'react'
import { useLocation, useNavigate } from 'react-router-dom'
@ -14,7 +11,6 @@ import { useLayers } from '#components/Map/hooks/useLayers'
import { useHasUserPermission } from '#components/Map/hooks/usePermissions'
import { useAddTag, useGetItemTags, useTags } from '#components/Map/hooks/useTags'
import { MapOverlayPage } from '#components/Templates'
import { getValue } from '#utils/GetValue'
import { linkItem, onUpdateItem, unlinkItem } from './itemFunctions'
import { FormHeader } from './Subcomponents/FormHeader'
@ -23,11 +19,12 @@ import { OnepagerForm } from './Templates/OnepagerForm'
import { SimpleForm } from './Templates/SimpleForm'
import { TabsForm } from './Templates/TabsForm'
import type { FormState } from '#types/FormState'
import type { Item } from '#types/Item'
import type { Tag } from '#types/Tag'
export function ProfileForm() {
const [state, setState] = useState({
const [state, setState] = useState<FormState>({
color: '',
id: '',
group_type: 'wuerdekompass',
@ -91,11 +88,10 @@ export function ProfileForm() {
useEffect(() => {
const newColor =
item.layer?.itemColorField && getValue(item, item.layer.itemColorField)
? getValue(item, item.layer.itemColorField)
: getItemTags(item) && getItemTags(item)[0]?.color
? getItemTags(item)[0].color
: item.layer?.markerDefaultColor
item.color ??
(getItemTags(item) && getItemTags(item)[0]?.color
? getItemTags(item)[0].color
: item.layer?.markerDefaultColor)
const offers = (item.offers ?? []).reduce((acc: Tag[], o) => {
const offer = tags.find((t) => t.id === o.tags_id)
@ -116,7 +112,7 @@ export function ProfileForm() {
}, [])
setState({
color: newColor,
color: newColor ?? '',
id: item?.id ?? '',
group_type: item?.group_type ?? '',
status: item?.status ?? '',
@ -127,7 +123,8 @@ export function ProfileForm() {
telephone: item?.telephone ?? '',
next_appointment: item?.next_appointment ?? '',
image: item?.image ?? '',
marker_icon: item?.marker_icon ?? '',
// Do we actually mean marker_icon here?
marker_icon: item?.markerIcon ?? '',
offers,
needs,
relations,
@ -140,7 +137,7 @@ export function ProfileForm() {
const [template, setTemplate] = useState<string>('')
useEffect(() => {
setTemplate(item.layer?.itemType.template || appState.userType)
setTemplate(item.layer?.itemType.template ?? appState.userType)
}, [appState.userType, item])
return (
@ -198,7 +195,8 @@ export function ProfileForm() {
className={loading ? ' tw-loading tw-btn tw-float-right' : 'tw-btn tw-float-right'}
type='submit'
style={{
backgroundColor: `${item.layer?.itemColorField && getValue(item, item.layer?.itemColorField) ? getValue(item, item.layer?.itemColorField) : getItemTags(item) && getItemTags(item)[0] && getItemTags(item)[0].color ? getItemTags(item)[0].color : item?.layer?.markerDefaultColor}`,
// We could refactor this, it is used several times at different locations
backgroundColor: `${item.color ?? (getItemTags(item) && getItemTags(item)[0] && getItemTags(item)[0].color ? getItemTags(item)[0].color : item?.layer?.markerDefaultColor)}`,
color: '#fff',
}}
>

View File

@ -1,10 +1,10 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
/* eslint-disable @typescript-eslint/no-unsafe-assignment */
/* eslint-disable @typescript-eslint/await-thenable */
/* eslint-disable @typescript-eslint/no-non-null-assertion */
/* eslint-disable @typescript-eslint/no-non-null-asserted-optional-chain */
/* eslint-disable @typescript-eslint/no-unsafe-argument */
/* eslint-disable @typescript-eslint/no-floating-promises */
/* eslint-disable @typescript-eslint/no-unsafe-member-access */
/* eslint-disable @typescript-eslint/no-unsafe-call */
import { LatLng } from 'leaflet'
@ -21,7 +21,6 @@ import { useSelectPosition, useSetSelectPosition } from '#components/Map/hooks/u
import { useTags } from '#components/Map/hooks/useTags'
import { HeaderView } from '#components/Map/Subcomponents/ItemPopupComponents/HeaderView'
import { MapOverlayPage } from '#components/Templates'
import { getValue } from '#utils/GetValue'
import { handleDelete, linkItem, unlinkItem } from './itemFunctions'
import { FlexView } from './Templates/FlexView'
@ -32,6 +31,7 @@ import { TabsView } from './Templates/TabsView'
import type { Item } from '#types/Item'
import type { ItemsApi } from '#types/ItemsApi'
import type { Tag } from '#types/Tag'
import type { Marker } from 'leaflet'
export function ProfileView({ attestationApi }: { attestationApi?: ItemsApi<any> }) {
const [item, setItem] = useState<Item>()
@ -88,30 +88,25 @@ export function ProfileView({ attestationApi }: { attestationApi?: ItemsApi<any>
setNeeds([])
setRelations([])
item?.layer?.itemOffersField &&
getValue(item, item.layer.itemOffersField)?.map((o) => {
const tag = tags.find((t) => t.id === o.tags_id)
tag && setOffers((current) => [...current, tag])
return null
})
item?.layer?.itemNeedsField &&
getValue(item, item.layer.itemNeedsField)?.map((n) => {
const tag = tags.find((t) => t.id === n.tags_id)
tag && setNeeds((current) => [...current, tag])
return null
})
item?.relations?.map((r) => {
item?.offers?.forEach((o) => {
const tag = tags.find((t) => t.id === o.tags_id)
tag && setOffers((current) => [...current, tag])
})
item?.needs?.forEach((n) => {
const tag = tags.find((t) => t.id === n.tags_id)
tag && setNeeds((current) => [...current, tag])
})
item?.relations?.forEach((r) => {
const item = items.find((i) => i.id === r.related_items_id)
item && setRelations((current) => [...current, item])
return null
})
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [item, items])
useEffect(() => {
const setMap = async (marker, x) => {
await map.setView(
const setMap = (marker: Marker, x: number) => {
map.setView(
new LatLng(item?.position?.coordinates[1]!, item?.position?.coordinates[0]! + x / 4),
undefined,
)
@ -164,7 +159,7 @@ export function ProfileView({ attestationApi }: { attestationApi?: ItemsApi<any>
}, [selectPosition])
useEffect(() => {
setTemplate(item?.layer?.itemType.template || appState.userType)
setTemplate(item?.layer?.itemType.template ?? appState.userType)
}, [appState.userType, item])
return (

View File

@ -10,7 +10,6 @@ import { useHasUserPermission } from '#components/Map/hooks/usePermissions'
import { useGetItemTags } from '#components/Map/hooks/useTags'
import { HeaderView } from '#components/Map/Subcomponents/ItemPopupComponents/HeaderView'
import DialogModal from '#components/Templates/DialogModal'
import { getValue } from '#utils/GetValue'
import type { Item } from '#types/Item'
@ -20,7 +19,6 @@ export function ActionButton({
triggerItemSelected,
existingRelations,
itemType,
colorField,
collection = 'items',
customStyle,
}: {
@ -28,7 +26,6 @@ export function ActionButton({
triggerItemSelected?: any
existingRelations: Item[]
itemType?: string
colorField?: string
collection?: string
customStyle?: string
item: Item
@ -45,6 +42,12 @@ export function ActionButton({
.filter((i) => !existingRelations.some((s) => s.id === i.id))
.filter((i) => i.id !== item.id)
const backgroundColor =
item.color ??
(getItemTags(item) && getItemTags(item)[0] && getItemTags(item)[0].color
? getItemTags(item)[0].color
: item.layer?.markerDefaultColor)
return (
<>
{hasUserPermission(collection, 'update', item) && (
@ -58,7 +61,7 @@ export function ActionButton({
setModalOpen(true)
}}
style={{
backgroundColor: `${colorField && getValue(item, colorField) ? getValue(item, colorField) : getItemTags(item) && getItemTags(item)[0] && getItemTags(item)[0].color ? getItemTags(item)[0].color : item.layer?.markerDefaultColor}`,
backgroundColor,
color: '#fff',
}}
>
@ -82,7 +85,7 @@ export function ActionButton({
triggerAddButton()
}}
style={{
backgroundColor: `${colorField && getValue(item, colorField) ? getValue(item, colorField) : getItemTags(item) && getItemTags(item)[0] && getItemTags(item)[0].color ? getItemTags(item)[0].color : item.layer?.markerDefaultColor}`,
backgroundColor,
color: '#fff',
}}
>

View File

@ -1,5 +1,5 @@
/* eslint-disable @typescript-eslint/no-unnecessary-condition */
/* eslint-disable @typescript-eslint/restrict-template-expressions */
/* eslint-disable @typescript-eslint/no-unsafe-return */
/* eslint-disable @typescript-eslint/restrict-plus-operands */
import { useEffect, useState } from 'react'

View File

@ -1,7 +1,3 @@
/* eslint-disable @typescript-eslint/no-unsafe-assignment */
/* eslint-disable @typescript-eslint/restrict-template-expressions */
/* eslint-disable @typescript-eslint/no-unsafe-call */
/* eslint-disable @typescript-eslint/no-unsafe-member-access */
import { useState } from 'react'
import { RowsPhotoAlbum } from 'react-photo-album'
import ReactLightbox from 'yet-another-react-lightbox'
@ -15,7 +11,7 @@ import type { Item } from '#types/Item'
export const GalleryView = ({ item }: { item: Item }) => {
const [index, setIndex] = useState(-1)
const appState = useAppState()
const images = item.gallery.map((i, j) => {
const images = item.gallery?.map((i, j) => {
return {
src: appState.assetsApi.url + `${i.directus_files_id.id}.jpg`,
width: i.directus_files_id.width,
@ -23,6 +19,9 @@ export const GalleryView = ({ item }: { item: Item }) => {
index: j,
}
})
if (!images) throw new Error('GalleryView: images is undefined')
return (
<div className='tw-mx-6 tw-mb-6'>
<RowsPhotoAlbum

View File

@ -4,46 +4,29 @@
/* eslint-disable @typescript-eslint/no-unsafe-call */
/* eslint-disable @typescript-eslint/restrict-plus-operands */
/* eslint-disable @typescript-eslint/no-unnecessary-condition */
/* eslint-disable @typescript-eslint/no-unsafe-assignment */
import { useEffect } from 'react'
import { useAppState } from '#components/AppShell/hooks/useAppState'
import { getValue } from '#utils/GetValue'
import type { Item } from '#types/Item'
export function LinkedItemsHeaderView({
item,
unlinkCallback,
itemNameField,
itemAvatarField,
loading,
unlinkPermission,
itemSubnameField,
}: {
item: Item
unlinkCallback?: any
itemNameField?: string
itemAvatarField?: string
itemSubnameField?: string
loading?: boolean
unlinkPermission: boolean
}) {
const appState = useAppState()
const avatar =
itemAvatarField && getValue(item, itemAvatarField)
? appState.assetsApi.url + getValue(item, itemAvatarField)
: item.layer?.itemAvatarField &&
item &&
getValue(item, item.layer?.itemAvatarField) &&
appState.assetsApi.url + getValue(item, item.layer?.itemAvatarField)
const title = itemNameField
? getValue(item, itemNameField)
: item.layer?.itemNameField && item && getValue(item, item.layer.itemNameField)
const subtitle = itemSubnameField
? getValue(item, itemSubnameField)
: item.layer?.itemSubnameField && item && getValue(item, item.layer.itemSubnameField)
const avatar = appState.assetsApi.url + item.image
const title = item.name
const subtitle = item.subname
useEffect(() => {}, [item])

View File

@ -5,7 +5,6 @@
import { useEffect, useState } from 'react'
import { TextAreaInput } from '#components/Input'
import { getValue } from '#utils/GetValue'
import { MarkdownHint } from './MarkdownHint'
@ -14,6 +13,7 @@ import type { FormState } from '#types/FormState'
export const ProfileTextForm = ({
state,
setState,
// Is this really used?
dataField,
heading,
size,
@ -49,7 +49,8 @@ export const ProfileTextForm = ({
</div>
<TextAreaInput
placeholder={'...'}
defaultValue={getValue(state, field)}
// eslint-disable-next-line security/detect-object-injection
defaultValue={state[field]}
updateFormValue={(v) =>
setState((prevState) => ({
...prevState,

View File

@ -1,12 +1,12 @@
/* eslint-disable @typescript-eslint/no-unsafe-assignment */
import { get } from 'radash'
import { TextView } from '#components/Map'
import { getValue } from '#utils/GetValue'
import type { Item } from '#types/Item'
export const ProfileTextView = ({
item,
dataField,
dataField = 'text',
heading,
hideWhenEmpty,
}: {
@ -15,13 +15,19 @@ export const ProfileTextView = ({
heading: string
hideWhenEmpty: boolean
}) => {
const text = get(item, dataField)
if (typeof text !== 'string') {
throw new Error('ProfileTextView: text is not a string')
}
return (
<div className='tw-my-10 tw-mt-2 tw-px-6'>
{!(getValue(item, dataField) === '' && hideWhenEmpty) && (
{!(text === '' && hideWhenEmpty) && (
<h2 className='tw-text-lg tw-font-semibold'>{heading}</h2>
)}
<div className='tw-mt-2 tw-text-sm'>
<TextView rawText={dataField ? getValue(item, dataField) : getValue(item, 'text')} />
<TextView itemId={item.id} rawText={text} />
</div>
</div>
)

View File

@ -1,7 +1,5 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
/* eslint-disable @typescript-eslint/no-unsafe-assignment */
/* eslint-disable @typescript-eslint/no-unsafe-member-access */
/* eslint-disable @typescript-eslint/no-unsafe-call */
import { ContactInfoForm } from '#components/Profile/Subcomponents/ContactInfoForm'
import { GroupSubheaderForm } from '#components/Profile/Subcomponents/GroupSubheaderForm'

View File

@ -1,6 +1,4 @@
/* eslint-disable @typescript-eslint/no-unsafe-assignment */
/* eslint-disable @typescript-eslint/no-unsafe-member-access */
/* eslint-disable @typescript-eslint/no-unsafe-call */
import { ContactInfoView } from '#components/Profile/Subcomponents/ContactInfoView'
import { GalleryView } from '#components/Profile/Subcomponents/GalleryView'
@ -9,6 +7,7 @@ import { ProfileStartEndView } from '#components/Profile/Subcomponents/ProfileSt
import { ProfileTextView } from '#components/Profile/Subcomponents/ProfileTextView'
import type { Item } from '#types/Item'
import type { Key } from 'react'
const componentMap = {
groupSubheaders: GroupSubHeaderView,
@ -24,14 +23,17 @@ export const FlexView = ({ item }: { item: Item }) => {
console.log(item)
return (
<div className='tw-h-full tw-overflow-y-auto fade'>
{item.layer?.itemType.profileTemplate.map((templateItem) => {
const TemplateComponent = componentMap[templateItem.collection]
return TemplateComponent ? (
<TemplateComponent key={templateItem.id} item={item} {...templateItem.item} />
) : (
<div key={templateItem.id}>Component not found</div>
)
})}
{item.layer?.itemType.profileTemplate.map(
// eslint-disable-next-line @typescript-eslint/no-explicit-any
(templateItem: { collection: string | number; id: Key | null | undefined; item: any }) => {
const TemplateComponent = componentMap[templateItem.collection]
return TemplateComponent ? (
<TemplateComponent key={templateItem.id} item={item} {...templateItem.item} />
) : (
<div key={templateItem.id}>Component not found</div>
)
},
)}
</div>
)
}

View File

@ -1,4 +1,3 @@
/* eslint-disable @typescript-eslint/no-unsafe-assignment */
/* eslint-disable @typescript-eslint/restrict-template-expressions */
import { TextView } from '#components/Map'
import { ContactInfoView } from '#components/Profile/Subcomponents/ContactInfoView'
@ -16,14 +15,14 @@ export const OnepagerView = ({ item }: { item: Item }) => {
{item.user_created?.first_name && <ContactInfoView heading='Du hast Fragen?' item={item} />}
{/* Description Section */}
<div className='tw-my-10 tw-mt-2 tw-px-6 tw-text-sm '>
<TextView rawText={item.text || 'Keine Beschreibung vorhanden'} />
<TextView itemId={item.id} rawText={item.text ?? 'Keine Beschreibung vorhanden'} />
</div>
{/* Next Appointment Section */}
{item.next_appointment && (
<div className='tw-my-10 tw-px-6'>
<h2 className='tw-text-lg tw-font-semibold'>Nächste Termine</h2>
<div className='tw-mt-2 tw-text-sm'>
<TextView rawText={item.next_appointment} />
<TextView itemId={item.id} rawText={item.next_appointment} />
</div>
</div>
)}

View File

@ -5,7 +5,7 @@ import type { Item } from '#types/Item'
export const SimpleView = ({ item }: { item: Item }) => {
return (
<div className='tw-mt-8 tw-h-full tw-overflow-y-auto fade tw-px-6'>
<TextView item={item} />
<TextView text={item.text} itemId={item.id} />
</div>
)
}

View File

@ -197,7 +197,7 @@ export const TabsForm = ({
loading={loading}
/>
<div className='tw-overflow-y-auto tw-overflow-x-hidden tw-max-h-64 fade'>
<TextView truncate item={i} />
<TextView truncate itemId={item.id} />
</div>
</div>
))}
@ -208,7 +208,6 @@ export const TabsForm = ({
item={item}
existingRelations={state.relations}
triggerItemSelected={(id) => linkItem(id, item, updateItem)}
colorField={item.layer.itemColorField}
></ActionButton>
)}
</div>

View File

@ -108,9 +108,9 @@ export const TabsView = ({
<StartEndView item={item}></StartEndView>
</div>
)}
<TextView item={item} />
<TextView text={item.text} itemId={item.id} />
<div className='tw-h-4'></div>
<TextView item={item} itemTextField='contact' />
<TextView text={item.contact} itemId={item.id} />
</div>
{item.layer?.itemType.questlog && (
<>
@ -267,7 +267,7 @@ export const TabsView = ({
loading={loading}
/>
<div className='tw-overflow-y-auto tw-overflow-x-hidden tw-max-h-64 fade'>
<TextView truncate item={i} />
<TextView truncate text={i.text} itemId={item.id} />
</div>
</div>
))}
@ -277,7 +277,6 @@ export const TabsView = ({
item={item}
existingRelations={relations}
triggerItemSelected={linkItem}
colorField={item.layer.itemColorField}
></ActionButton>
)}
</div>

View File

@ -46,7 +46,6 @@ const DialogModal = ({
<dialog
className={`${className ?? ''} tw-card tw-shadow-xl tw-absolute tw-right-0 tw-top-0 tw-bottom-0 tw-left-0 tw-m-auto tw-transition-opacity tw-duration-300 tw-p-4 tw-max-w-xl tw-bg-base-100`}
ref={ref}
// eslint-disable-next-line react/no-unknown-property
onCancel={onClose}
onClick={(e) =>
ref.current && !isClickInsideRectangle(e, ref.current) && closeOnClickOutside && onClose()

View File

@ -2,14 +2,12 @@
/* eslint-disable @typescript-eslint/no-unsafe-call */
/* eslint-disable @typescript-eslint/no-unsafe-return */
/* eslint-disable @typescript-eslint/restrict-template-expressions */
/* eslint-disable @typescript-eslint/restrict-plus-operands */
/* eslint-disable @typescript-eslint/no-explicit-any */
import { useNavigate } from 'react-router-dom'
import { StartEndView, TextView } from '#components/Map'
import useWindowDimensions from '#components/Map/hooks/useWindowDimension'
import { HeaderView } from '#components/Map/Subcomponents/ItemPopupComponents/HeaderView'
import { getValue } from '#utils/GetValue'
import { DateUserInfo } from './DateUserInfo'
@ -19,13 +17,11 @@ export const ItemCard = ({
i,
loading,
url,
parameterField,
deleteCallback,
}: {
i: Item
loading: boolean
url: string
parameterField: string
deleteCallback: any
}) => {
const navigate = useNavigate()
@ -35,27 +31,23 @@ export const ItemCard = ({
<div
className='tw-cursor-pointer tw-card tw-border-[1px] tw-border-base-300 tw-card-body tw-shadow-xl tw-bg-base-100 tw-text-base-content tw-p-4 tw-mb-4 tw-h-fit'
onClick={() => {
// We could have an onClick callback instead
const params = new URLSearchParams(window.location.search)
if (windowDimensions.width < 786 && i.position)
navigate('/' + getValue(i, parameterField) + `${params ? `?${params}` : ''}`)
else navigate(url + getValue(i, parameterField) + `${params ? `?${params}` : ''}`)
navigate('/' + i.id + `${params ? `?${params}` : ''}`)
else navigate(url + i.id + `${params ? `?${params}` : ''}`)
}}
>
<HeaderView
loading={loading}
item={i}
api={i.layer?.api}
itemAvatarField={i.layer?.itemAvatarField}
itemNameField={i.layer?.itemNameField}
itemSubnameField={i.layer?.itemSubnameField}
editCallback={() => navigate('/edit-item/' + i.id)}
deleteCallback={() => deleteCallback(i)}
></HeaderView>
<div className='tw-overflow-y-auto tw-overflow-x-hidden tw-max-h-64 fade'>
{i.layer?.itemType.show_start_end && <StartEndView item={i}></StartEndView>}
{i.layer?.itemType.show_text && (
<TextView truncate item={i} itemTextField={i.layer.itemTextField} />
)}
{i.layer?.itemType.show_text && <TextView truncate text={i.text} itemId={i.id} />}
</div>
<DateUserInfo item={i}></DateUserInfo>
</div>

View File

@ -8,7 +8,6 @@ import { useNavigate } from 'react-router-dom'
import { useItems } from '#components/Map/hooks/useItems'
import { useTags } from '#components/Map/hooks/useTags'
import { getValue } from '#utils/GetValue'
import { MapOverlayPage } from './MapOverlayPage'
import { TagView } from './TagView'
@ -42,21 +41,16 @@ export const MarketView = () => {
useEffect(() => {
setOffers([])
setNeeds([])
items.map((i) => {
i.layer?.itemOffersField &&
getValue(i, i.layer.itemOffersField)?.map((o) => {
const tag = tags.find((t) => t.id === o.tags_id)
tag && setOffers((current) => [...current, tag])
return null
})
i.layer?.itemNeedsField &&
getValue(i, i.layer.itemNeedsField)?.map((n) => {
const tag = tags.find((t) => t.id === n.tags_id)
tag && setNeeds((current) => [...current, tag])
return null
})
return null
})
for (const item of items) {
item.offers?.forEach((o) => {
const tag = tags.find((t) => t.id === o.tags_id)
tag && setOffers((current) => [...current, tag])
})
item.needs?.forEach((n) => {
const tag = tags.find((t) => t.id === n.tags_id)
tag && setNeeds((current) => [...current, tag])
})
}
// eslint-disable-next-line no-console
console.log(offers)

View File

@ -30,12 +30,10 @@ import type { Item } from '#types/Item'
export const OverlayItemsIndexPage = ({
url,
layerName,
parameterField,
plusButton = true,
}: {
layerName: string
url: string
parameterField: string
plusButton?: boolean
}) => {
const [loading, setLoading] = useState<boolean>(false)
@ -165,7 +163,6 @@ export const OverlayItemsIndexPage = ({
i={i}
loading={loading}
url={url}
parameterField={parameterField}
deleteCallback={() => deleteItem(i)}
/>
</div>

View File

@ -1,14 +0,0 @@
/* eslint-disable @typescript-eslint/no-unsafe-assignment */
/* eslint-disable @typescript-eslint/no-unsafe-return */
/* eslint-disable @typescript-eslint/no-unsafe-member-access */
export function getValue(obj, path) {
if (!obj || typeof path !== 'string') return undefined
const pathArray = path.split('.') // Use a different variable for the split path
for (let i = 0, len = pathArray.length; i < len; i++) {
if (!obj) return undefined // Check if obj is falsy at each step
// eslint-disable-next-line security/detect-object-injection
obj = obj[pathArray[i]] // Dive one level deeper
}
return obj // Return the final value
}

View File

@ -17,4 +17,6 @@ export interface FormState {
offers: Tag[]
needs: Tag[]
relations: Item[]
start: string
end: string
}

34
src/types/Item.d.ts vendored
View File

@ -1,14 +1,26 @@
import type { ItemsApi } from './ItemsApi'
import type { ItemType } from './ItemType'
import type { LayerProps } from './LayerProps'
import type { Relation } from './Relation'
import type { UserItem } from './UserItem'
import type { Point } from 'geojson'
type TagIds = { tags_id: string }[]
interface GalleryItem {
directus_files_id: {
id: number
width: number
height: number
}
}
export interface Item {
id: string
name: string
text: string
position?: Point
text?: string
data?: string
position?: Point | null
date_created?: string
date_updated?: string | null
start?: string
@ -24,8 +36,22 @@ export interface Item {
slug?: string
user_created?: UserItem
image?: string
// eslint-disable-next-line @typescript-eslint/no-explicit-any
[key: string]: any
group_type?: string
offers?: TagIds
needs?: TagIds
status?: string
color?: string
markerIcon?: string
avatar?: string
new?: boolean
contact?: string
telephone?: string
next_appointment?: string
type?: ItemType
gallery?: GalleryItem[]
// {
// coordinates: [number, number]
/* constructor(
id: string,
name: string,

View File

@ -1,5 +1,15 @@
import type { Key } from 'react'
export interface ItemType {
name: string
show_start_end: boolean
show_text: boolean
// eslint-disable-next-line @typescript-eslint/no-explicit-any
[key: string]: any
profileTemplate: { collection: string | number; id: Key | null | undefined; item: any }[]
offers_and_needs: boolean
icon_as_labels: unknown
relations: boolean
template: string
show_start_end_input: boolean
questlog: boolean
}

View File

@ -2,7 +2,7 @@ export interface ItemsApi<T> {
getItems(): Promise<T[]>
getItem?(id: string): Promise<T>
createItem?(item: T): Promise<T>
updateItem?(item: T): Promise<T>
updateItem?(item: Partial<T>): Promise<T>
deleteItem?(id: string): Promise<boolean>
collectionName?: string
}

View File

@ -18,17 +18,6 @@ export interface LayerProps {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
api?: ItemsApi<any>
itemType: ItemType
itemNameField?: string
itemSubnameField?: string
itemTextField?: string
itemAvatarField?: string
itemColorField?: string
itemOwnerField?: string
itemTagsField?: string
itemLatitudeField?: string
itemLongitudeField?: string
itemOffersField?: string
itemNeedsField?: string
onlyOnePerOwner?: boolean
customEditLink?: string
customEditParameter?: string

View File

@ -14,8 +14,8 @@ export default defineConfig({
exclude: [...configDefaults.exclude],
thresholds: {
lines: 0,
functions: 67,
branches: 67,
functions: 66,
branches: 66,
statements: 0,
},
},