Compare commits

...

50 Commits

Author SHA1 Message Date
Max
d6c7481661
Merge pull request #78 from utopia-os/eslint-react-refresh
fix(workflow): add eslint-react-refresh
2025-02-17 18:34:59 +01:00
Maximilian Harz
a3d7fa8496 Merge main 2025-02-17 18:32:14 +01:00
Max
c38d1283e1
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.
2025-02-17 18:28:15 +01:00
Maximilian Harz
83d65b0107 Fix package-lock.json 2025-02-17 18:21:45 +01:00
Maximilian Harz
8f624b90fa Make item optional 2025-02-17 18:07:01 +01:00
Maximilian Harz
10e81c38c2 Use item in TextView when given 2025-02-17 18:07:01 +01:00
Maximilian Harz
b9295128fd Add ? to item references 2025-02-17 18:07:01 +01:00
Maximilian Harz
23145b0a14 Remove unnecessary eslint disable 2025-02-17 18:07:01 +01:00
Maximilian Harz
134d2ef123 Use ?? instead of || 2025-02-17 18:07:01 +01:00
Maximilian Harz
2556d8c047 item.text is optional 2025-02-17 18:07:01 +01:00
Maximilian Harz
cb727d433d Fix paranthesis in ProfileForm 2025-02-17 18:07:01 +01:00
Maximilian Harz
aa616ed295 Refactor background style 2025-02-17 18:07:00 +01:00
Maximilian Harz
f71929b4d8 Fix paranthesis in ActionsButton 2025-02-17 18:07:00 +01:00
Maximilian Harz
0395c6393b Fix package-lock 2025-02-17 18:06:58 +01:00
Maximilian Harz
bface9c397 Lower test coverage 2025-02-17 18:05:06 +01:00
Maximilian Harz
5835a71588 Type item.gallery 2025-02-17 18:05:06 +01:00
Maximilian Harz
990f837aaf Use radash.get to support deep properties in ProfileTextView 2025-02-17 18:05:06 +01:00
Maximilian Harz
876aa65f4f Ignore any 2025-02-17 18:05:06 +01:00
Maximilian Harz
a949df25c2 Simplify conditional and fix color calculation 2025-02-17 18:05:06 +01:00
Maximilian Harz
7e326590bb Don't type api for now; re-add hasColor 2025-02-17 18:05:06 +01:00
Maximilian Harz
bc0aa25cd2 Make updateItem type partial 2025-02-17 18:05:06 +01:00
Maximilian Harz
a3acf592dd Allow item.position to be null 2025-02-17 18:05:06 +01:00
Maximilian Harz
f0a3cbada7 Hide empty avatar 2025-02-17 18:05:06 +01:00
Maximilian Harz
5846637747 Improve api type 2025-02-17 18:05:06 +01:00
Maximilian Harz
5741070854 Remove unused props and comment 2025-02-17 18:05:06 +01:00
Maximilian Harz
bbd6e2dd61 Remove unneeded props 2025-02-17 18:05:06 +01:00
Maximilian Harz
9511dc0408 Use .image as avatar 2025-02-17 18:05:06 +01:00
Maximilian Harz
d096ffe456 Fix PopupButton 2025-02-17 18:05:04 +01:00
Maximilian Harz
ea80e6e94c Use better type name for tag ids 2025-02-17 18:03:30 +01:00
Maximilian Harz
7f4594ad53 Fix functionality of ProfileTextView 2025-02-17 18:03:30 +01:00
Maximilian Harz
4316387ecb Improve typing of items, remove getValue 2025-02-17 18:03:25 +01:00
Maximilian Harz
f5b7b9267f Try to type Item and getValue (WIP) 2025-02-17 17:56:56 +01:00
mahula
c9a66461e1
Merge pull request #117 from utopia-os/setup-component-testing
feat(other): set up component testing
2025-02-17 17:52:01 +01:00
mahula
13754e6da7
remove double entry from package.json 2025-02-17 17:32:21 +01:00
mahula
cab457ad23
fix mistake in package.json 2025-02-17 17:26:10 +01:00
mahula
126af43ead
Merge branch 'main' into setup-component-testing 2025-02-17 17:20:24 +01:00
be5513947c reorderd exports to avoid cypress warning 2025-02-17 15:51:35 +00:00
Anton Tranelis
987540652a
Merge pull request #112 from utopia-os/simple-script-names
feat(workflow): add simpler names for linting and lintfix
2025-02-17 13:04:40 +00:00
mahula
054db690d0 fix linting 2025-02-14 22:23:24 +01:00
mahula
1a33891a21 add cypress commands and compontents files to tsconfig include 2025-02-14 22:11:18 +01:00
mahula
0b509d1963 fix linting 2025-02-14 21:54:58 +01:00
mahula
e75602d365 add component test script to package.json 2025-02-14 21:45:13 +01:00
mahula
b1b3376aee add first rudimentary component tests 2025-02-14 21:42:37 +01:00
mahula
d632e2c5b7 add first rudimentary component tests 2025-02-14 21:20:18 +01:00
mahula
f8a14cd942 add cypress config to tsconfig 2025-02-14 18:24:30 +01:00
mahula
45037737f4 add basic cypress component testing config 2025-02-14 18:22:13 +01:00
mahula
7b905135aa add cypress to package.json 2025-02-14 18:19:37 +01:00
Maximilian Harz
db94b7517d Add simpler names for linting and lintfix 2025-02-10 23:26:32 +01:00
Maximilian Harz
eeafacb074 Add eslint-plugin-react-refresh package 2025-01-29 14:18:52 +01:00
Maximilian Harz
cdf4770333 Add eslint-react-refresh 2025-01-29 14:18:51 +01:00
45 changed files with 5492 additions and 1607 deletions

View File

@ -28,6 +28,7 @@ module.exports = {
'no-catch-all',
'react',
'react-hooks',
'react-refresh',
],
settings: {
'import/resolver': {

11
cypress.config.ts Normal file
View File

@ -0,0 +1,11 @@
import { defineConfig } from 'cypress'
export default defineConfig({
component: {
devServer: {
framework: 'react',
bundler: 'vite',
},
specPattern: ['**/**/*.cy.{ts,tsx}'],
},
})

View File

@ -0,0 +1,37 @@
/// <reference types="cypress" />
// ***********************************************
// This example commands.ts shows you how to
// create various custom commands and overwrite
// existing commands.
//
// For more comprehensive examples of custom
// commands please read more here:
// https://on.cypress.io/custom-commands
// ***********************************************
//
//
// -- This is a parent command --
// Cypress.Commands.add('login', (email, password) => { ... })
//
//
// -- This is a child command --
// Cypress.Commands.add('drag', { prevSubject: 'element'}, (subject, options) => { ... })
//
//
// -- This is a dual command --
// Cypress.Commands.add('dismiss', { prevSubject: 'optional'}, (subject, options) => { ... })
//
//
// -- This will overwrite an existing command --
// Cypress.Commands.overwrite('visit', (originalFn, url, options) => { ... })
//
// declare global {
// namespace Cypress {
// interface Chainable {
// login(email: string, password: string): Chainable<void>
// drag(subject: string, options?: Partial<TypeOptions>): Chainable<Element>
// dismiss(subject: string, options?: Partial<TypeOptions>): Chainable<Element>
// visit(originalFn: CommandOriginalFn, url: string, options: Partial<VisitOptions>): Chainable<Element>
// }
// }
// }

View File

@ -0,0 +1,12 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width,initial-scale=1.0">
<title>Components App</title>
</head>
<body>
<div data-cy-root></div>
</body>
</html>

View File

@ -0,0 +1,38 @@
// ***********************************************************
// This example support/component.ts is processed and
// loaded automatically before your test files.
//
// This is a great place to put global configuration and
// behavior that modifies Cypress.
//
// You can change the location of this file or turn off
// automatically serving support files with the
// 'supportFile' configuration option.
//
// You can read more here:
// https://on.cypress.io/configuration
// ***********************************************************
// Import commands.js using ES2015 syntax:
// eslint-disable-next-line import/no-unassigned-import
import './commands'
import { mount } from 'cypress/react'
// Augment the Cypress namespace to include type definitions for
// your custom command.
// Alternatively, can be defined in cypress/support/component.d.ts
// with a <reference path="./component" /> at the top of your spec.
declare global {
// eslint-disable-next-line @typescript-eslint/no-namespace
namespace Cypress {
interface Chainable {
mount: typeof mount
}
}
}
Cypress.Commands.add('mount', mount)
// Example use:
// cy.mount(<MyComponent />)

6424
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -9,9 +9,9 @@
"types": "./dist/index.d.ts",
"exports": {
".": {
"types": "./dist/index.d.ts",
"import": "./dist/index.esm.js",
"require": "./dist/index.cjs",
"types": "./dist/index.d.ts"
"require": "./dist/index.cjs"
}
},
"type": "module",
@ -19,6 +19,9 @@
"build": "rollup -c",
"start": "rollup -c -w",
"test:lint:eslint": "eslint --ext .ts,.tsx,.js,.jsx,.cjs,.mjs,.json,.yml,.yaml --max-warnings 0 .",
"lint": "npm run test:lint:eslint",
"lintfix": "npm run test:lint:eslint -- --fix",
"test:component": "cypress run --component --browser electron",
"test:unit": "npm run test:unit:dev -- run --coverage",
"test:unit:dev": "vitest",
"docs:generate": "typedoc --plugin typedoc-plugin-coverage src/index.tsx",
@ -47,6 +50,7 @@
"@vitejs/plugin-react": "^4.3.4",
"@vitest/coverage-v8": "^3.0.5",
"autoprefixer": "^10.4.14",
"cypress": "^14.0.3",
"daisyui": "^4.6.1",
"eslint": "^8.24.0",
"eslint-config-prettier": "^9.1.0",
@ -59,6 +63,7 @@
"eslint-plugin-promise": "^6.1.1",
"eslint-plugin-react": "^7.31.8",
"eslint-plugin-react-hooks": "^4.6.0",
"eslint-plugin-react-refresh": "^0.4.18",
"eslint-plugin-security": "^3.0.1",
"eslint-plugin-yml": "^1.14.0",
"globals": "^15.14.0",
@ -90,6 +95,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

@ -0,0 +1,66 @@
/// <reference types="cypress" />
import { mount } from 'cypress/react'
import { TextInput } from './TextInput'
describe('<TextInput />', () => {
it('renders with default props', () => {
mount(<TextInput />)
cy.get('input').should('have.attr', 'type', 'text')
cy.get('input').should('have.attr', 'placeholder', '')
cy.get('input').should('have.attr', 'required')
cy.get('input').should('have.class', 'tw-input')
cy.get('input').should('have.class', 'tw-input-bordered')
cy.get('input').should('have.class', 'tw-w-full')
})
it('renders with given labelTitle', () => {
mount(<TextInput labelTitle='Test Title' />)
cy.get('label').should('contain.text', 'Test Title')
})
it('renders with given type', () => {
mount(<TextInput type='email' />)
cy.get('input').should('have.attr', 'type', 'email')
})
it('accepts user input', () => {
mount(<TextInput dataField='test-input' />)
cy.get('input[name="test-input"]').type('Hello Test')
cy.get('input[name="test-input"]').should('have.value', 'Hello Test')
})
it('renders a label, if labelTitle is set', () => {
mount(<TextInput dataField='test-input' labelTitle='Test Label' />)
cy.contains('Test Label').should('exist')
})
it('handles default value correctly', () => {
mount(<TextInput dataField='test-input' defaultValue='Default Value' />)
cy.get('input[name="test-input"]').should('have.value', 'Default Value')
})
it('calls updateFormValue on change', () => {
const onChangeSpy = cy.spy().as('updateFormValueSpy')
mount(<TextInput dataField='test-input' updateFormValue={onChangeSpy} />)
cy.get('input[name="test-input"]').type('Test')
cy.get('@updateFormValueSpy').should('have.been.calledWith', 'Test')
})
it('accepts a specific input type', () => {
mount(<TextInput dataField='test-input' type='email' />)
cy.get('input[name="test-input"]').should('have.attr', 'type', 'email')
})
it('respects the autocomplete attribute', () => {
mount(<TextInput dataField='test-input' autocomplete='off' />)
cy.get('input[name="test-input"]').should('have.attr', 'autocomplete', 'off')
})
it('updates form value on change', () => {
const updateFormValue = cy.stub()
mount(<TextInput updateFormValue={updateFormValue} />)
cy.get('input').type('Hello')
cy.wrap(updateFormValue).should('have.been.calledWith', 'Hello')
})
})

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

@ -27,7 +27,7 @@
"#root/*": ["./*"]
}
},
"include": ["src", "vite.config.ts", "setupTest.ts"],
"include": ["src", "vite.config.ts", "setupTest.ts", "cypress.config.ts", "cypress/support/commands.ts", "cypress/support/component.ts"],
"exclude": ["node_modules", "dist", "example", "rollup.config.mjss"],
"typeRoots": [
"./types",

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,
},
},