feat(lib): relation component (#387)

* Fix relations view

* extended relation component

* added backend fields

* bidirectional direction

* fix linting
This commit is contained in:
Anton Tranelis 2025-09-15 11:32:19 +02:00 committed by GitHub
parent a2b7c16133
commit c5232093dc
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
4 changed files with 204 additions and 5 deletions

View File

@ -0,0 +1,56 @@
{
"collection": "relations",
"field": "direction",
"type": "string",
"meta": {
"collection": "relations",
"conditions": null,
"display": null,
"display_options": null,
"field": "direction",
"group": null,
"hidden": false,
"interface": "select-dropdown",
"note": null,
"options": {
"choices": [
{
"icon": "arrow_forward",
"text": "outgoing",
"value": "outgoing"
},
{
"icon": "arrow_back",
"text": "ingoing",
"value": "ingoing"
}
]
},
"readonly": false,
"required": false,
"sort": 4,
"special": null,
"translations": null,
"validation": null,
"validation_message": null,
"width": "full"
},
"schema": {
"name": "direction",
"table": "relations",
"data_type": "character varying",
"default_value": "outgoing",
"max_length": 255,
"numeric_precision": null,
"numeric_scale": null,
"is_nullable": true,
"is_unique": false,
"is_indexed": false,
"is_primary_key": false,
"is_generated": false,
"generation_expression": null,
"has_auto_increment": false,
"foreign_key_table": null,
"foreign_key_column": null
}
}

View File

@ -0,0 +1,43 @@
{
"collection": "relations",
"field": "heading",
"type": "string",
"meta": {
"collection": "relations",
"conditions": null,
"display": null,
"display_options": null,
"field": "heading",
"group": null,
"hidden": false,
"interface": "input",
"note": null,
"options": null,
"readonly": false,
"required": false,
"sort": 3,
"special": null,
"translations": null,
"validation": null,
"validation_message": null,
"width": "full"
},
"schema": {
"name": "heading",
"table": "relations",
"data_type": "character varying",
"default_value": "Relations",
"max_length": 255,
"numeric_precision": null,
"numeric_scale": null,
"is_nullable": true,
"is_unique": false,
"is_indexed": false,
"is_primary_key": false,
"is_generated": false,
"generation_expression": null,
"has_auto_increment": false,
"foreign_key_table": null,
"foreign_key_column": null
}
}

View File

@ -0,0 +1,45 @@
{
"collection": "relations",
"field": "hideWhenEmpty",
"type": "boolean",
"meta": {
"collection": "relations",
"conditions": null,
"display": null,
"display_options": null,
"field": "hideWhenEmpty",
"group": null,
"hidden": false,
"interface": "boolean",
"note": null,
"options": null,
"readonly": false,
"required": false,
"sort": 5,
"special": [
"cast-boolean"
],
"translations": null,
"validation": null,
"validation_message": null,
"width": "full"
},
"schema": {
"name": "hideWhenEmpty",
"table": "relations",
"data_type": "boolean",
"default_value": true,
"max_length": null,
"numeric_precision": null,
"numeric_scale": null,
"is_nullable": true,
"is_unique": false,
"is_indexed": false,
"is_primary_key": false,
"is_generated": false,
"generation_expression": null,
"has_auto_increment": false,
"foreign_key_table": null,
"foreign_key_column": null
}
}

View File

@ -1,3 +1,6 @@
import { Link } from 'react-router-dom'
import { useAppState } from '#components/AppShell/hooks/useAppState'
import { useItems } from '#components/Map/hooks/useItems'
import type { Item } from '#types/Item'
@ -5,27 +8,79 @@ import type { Item } from '#types/Item'
interface Props {
item: Item
relation: string
heading: string
direction?: 'outgoing' | 'ingoing' | 'bidirectional'
hideWhenEmpty?: boolean
}
export const RelationsView = ({ item, relation }: Props) => {
export const RelationsView = ({
item,
relation,
heading,
direction = 'outgoing',
hideWhenEmpty = true,
}: Props) => {
const items = useItems()
const appState = useAppState()
if (!item.relations) return
const relationsOfRightType = item.relations.filter((r) => r.type === relation)
const relatedItems = items.filter((i) => relationsOfRightType.some((r) => r.id === i.id))
const relatedItems = (() => {
const outgoingItems = items.filter((i) =>
relationsOfRightType.some((r) => r.related_items_id === i.id),
)
const ingoingItems = items.filter((i) =>
i.relations?.some((r) => r.type === relation && r.related_items_id === item.id),
)
switch (direction) {
case 'outgoing':
return outgoingItems
case 'ingoing':
return ingoingItems
case 'bidirectional': {
// Combine both arrays and remove duplicates
const allItems = [...outgoingItems, ...ingoingItems]
return allItems.filter(
(item, index, self) => index === self.findIndex((i) => i.id === item.id),
)
}
default:
return outgoingItems
}
})()
const hasRelatedItems = relatedItems.length > 0
if (hideWhenEmpty && !hasRelatedItems) {
return null
}
return (
<div>
<h2>{relation}</h2>
<div className='tw:my-10 tw:mt-2 tw:px-6'>
<h2 className='tw:text-lg tw:font-bold'>{heading}</h2>
{hasRelatedItems ? (
<ul>
{relatedItems.map((relatedItem) => (
<li key={relatedItem.id}>
<a href={`/item/${relatedItem.id}`}>{relatedItem.name}</a>
<Link to={relatedItem.id} className='tw:flex tw:flex-row'>
<div>
{relatedItem.image ? (
<img
className='tw:size-10 tw:rounded-full'
src={appState.assetsApi.url + '/' + relatedItem.image}
/>
) : (
<div className='tw:size-10 tw:rounded-full tw:bg-gray-200' />
)}
</div>
<div className='tw:ml-2 tw:flex tw:items-center tw:min-h-[2.5rem]'>
<div>{relatedItem.name}</div>
</div>
</Link>
</li>
))}
</ul>