integrate LinkedItems
into /sight
pages
This commit is contained in:
parent
451e1da308
commit
faac402aa6
@ -1,5 +1,5 @@
|
|||||||
import {useState, useEffect} from 'react'
|
import {useState, useEffect} from 'react'
|
||||||
import {Stack, Typography, Button, MenuItem, Select, FormControl, InputLabel, Grid, Box, Accordion, AccordionSummary, AccordionDetails, useTheme} from '@mui/material'
|
import {Stack, Typography, Button, MenuItem, Select, FormControl, InputLabel, Grid, Box, Accordion, AccordionSummary, AccordionDetails, useTheme, TextField} from '@mui/material'
|
||||||
import ExpandMoreIcon from '@mui/icons-material/ExpandMore'
|
import ExpandMoreIcon from '@mui/icons-material/ExpandMore'
|
||||||
import axios from 'axios'
|
import axios from 'axios'
|
||||||
import {BACKEND_URL} from '../lib/constants'
|
import {BACKEND_URL} from '../lib/constants'
|
||||||
@ -22,6 +22,7 @@ export const LinkedItems = <T extends {id: number; [key: string]: any}>({parentI
|
|||||||
const [items, setItems] = useState<T[]>([])
|
const [items, setItems] = useState<T[]>([])
|
||||||
const [linkedItems, setLinkedItems] = useState<T[]>([])
|
const [linkedItems, setLinkedItems] = useState<T[]>([])
|
||||||
const [selectedItemId, setSelectedItemId] = useState<number | ''>('')
|
const [selectedItemId, setSelectedItemId] = useState<number | ''>('')
|
||||||
|
const [pageNum, setPageNum] = useState<number>(1)
|
||||||
const [isLoading, setIsLoading] = useState<boolean>(true)
|
const [isLoading, setIsLoading] = useState<boolean>(true)
|
||||||
const theme = useTheme()
|
const theme = useTheme()
|
||||||
|
|
||||||
@ -55,18 +56,35 @@ export const LinkedItems = <T extends {id: number; [key: string]: any}>({parentI
|
|||||||
}
|
}
|
||||||
}, [childResource, type])
|
}, [childResource, type])
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (childResource === 'article' && parentResource === 'sight') {
|
||||||
|
setPageNum(linkedItems.length + 1)
|
||||||
|
}
|
||||||
|
}, [linkedItems, childResource, parentResource])
|
||||||
|
|
||||||
const availableItems = items.filter((item) => !linkedItems.some((linked) => linked.id === item.id))
|
const availableItems = items.filter((item) => !linkedItems.some((linked) => linked.id === item.id))
|
||||||
|
|
||||||
const linkItem = () => {
|
const linkItem = () => {
|
||||||
if (selectedItemId) {
|
if (selectedItemId) {
|
||||||
|
const requestData =
|
||||||
|
childResource === 'article'
|
||||||
|
? {
|
||||||
|
[`${childResource}_id`]: selectedItemId,
|
||||||
|
page_num: pageNum,
|
||||||
|
}
|
||||||
|
: {
|
||||||
|
[`${childResource}_id`]: selectedItemId,
|
||||||
|
}
|
||||||
|
|
||||||
axios
|
axios
|
||||||
.post(`${BACKEND_URL}/${parentResource}/${parentId}/${childResource}`, {
|
.post(`${BACKEND_URL}/${parentResource}/${parentId}/${childResource}`, requestData)
|
||||||
[`${childResource}_id`]: selectedItemId,
|
|
||||||
})
|
|
||||||
.then(() => {
|
.then(() => {
|
||||||
axios.get(`${BACKEND_URL}/${parentResource}/${parentId}/${childResource}`).then((response) => {
|
axios.get(`${BACKEND_URL}/${parentResource}/${parentId}/${childResource}`).then((response) => {
|
||||||
setLinkedItems(response?.data || [])
|
setLinkedItems(response?.data || [])
|
||||||
setSelectedItemId('')
|
setSelectedItemId('')
|
||||||
|
if (childResource === 'article') {
|
||||||
|
setPageNum(pageNum + 1)
|
||||||
|
}
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
.catch((error) => {
|
.catch((error) => {
|
||||||
@ -98,7 +116,7 @@ export const LinkedItems = <T extends {id: number; [key: string]: any}>({parentI
|
|||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Typography variant="subtitle1" fontWeight="bold">
|
<Typography variant="subtitle1" fontWeight="bold">
|
||||||
{type === 'show' ? `Привязанные ${title}` : title}
|
Привязанные {title}
|
||||||
</Typography>
|
</Typography>
|
||||||
</AccordionSummary>
|
</AccordionSummary>
|
||||||
|
|
||||||
@ -138,11 +156,11 @@ export const LinkedItems = <T extends {id: number; [key: string]: any}>({parentI
|
|||||||
</Grid>
|
</Grid>
|
||||||
|
|
||||||
{type === 'edit' && (
|
{type === 'edit' && (
|
||||||
<>
|
<Stack gap={2}>
|
||||||
|
<Typography variant="subtitle1">Добавить {title}</Typography>
|
||||||
<FormControl fullWidth>
|
<FormControl fullWidth>
|
||||||
<InputLabel>{title}</InputLabel>
|
<InputLabel>Выберите {title}</InputLabel>
|
||||||
|
<Select value={selectedItemId} onChange={(e) => setSelectedItemId(Number(e.target.value))} label={`Выберите ${title}`} fullWidth>
|
||||||
<Select value={selectedItemId} onChange={(e) => setSelectedItemId(Number(e.target.value))} label={title} fullWidth>
|
|
||||||
{availableItems.map((item) => (
|
{availableItems.map((item) => (
|
||||||
<MenuItem key={item.id} value={item.id}>
|
<MenuItem key={item.id} value={item.id}>
|
||||||
{item[fields[0].data]}
|
{item[fields[0].data]}
|
||||||
@ -151,10 +169,28 @@ export const LinkedItems = <T extends {id: number; [key: string]: any}>({parentI
|
|||||||
</Select>
|
</Select>
|
||||||
</FormControl>
|
</FormControl>
|
||||||
|
|
||||||
|
{childResource === 'article' && (
|
||||||
|
<FormControl fullWidth>
|
||||||
|
<TextField
|
||||||
|
type="number"
|
||||||
|
label="Номер страницы"
|
||||||
|
name="page_num"
|
||||||
|
value={pageNum}
|
||||||
|
onChange={(e) => {
|
||||||
|
const newValue = Number(e.target.value)
|
||||||
|
const minValue = linkedItems.length + 1 // page number on articles lenght
|
||||||
|
setPageNum(newValue < minValue ? minValue : newValue)
|
||||||
|
}}
|
||||||
|
fullWidth
|
||||||
|
InputLabelProps={{shrink: true}}
|
||||||
|
/>
|
||||||
|
</FormControl>
|
||||||
|
)}
|
||||||
|
|
||||||
<Button variant="contained" onClick={linkItem} disabled={!selectedItemId}>
|
<Button variant="contained" onClick={linkItem} disabled={!selectedItemId}>
|
||||||
Добавить
|
Добавить
|
||||||
</Button>
|
</Button>
|
||||||
</>
|
</Stack>
|
||||||
)}
|
)}
|
||||||
</Stack>
|
</Stack>
|
||||||
</AccordionDetails>
|
</AccordionDetails>
|
||||||
|
@ -2,6 +2,9 @@ import {Autocomplete, Box, TextField} from '@mui/material'
|
|||||||
import {Edit, useAutocomplete} from '@refinedev/mui'
|
import {Edit, useAutocomplete} from '@refinedev/mui'
|
||||||
import {useForm} from '@refinedev/react-hook-form'
|
import {useForm} from '@refinedev/react-hook-form'
|
||||||
import {Controller} from 'react-hook-form'
|
import {Controller} from 'react-hook-form'
|
||||||
|
import {useParams} from 'react-router'
|
||||||
|
import {LinkedItems} from '../../components/LinkedItems'
|
||||||
|
import {ArticleItem, articleFields} from './types'
|
||||||
|
|
||||||
export const SightEdit = () => {
|
export const SightEdit = () => {
|
||||||
const {
|
const {
|
||||||
@ -11,6 +14,7 @@ export const SightEdit = () => {
|
|||||||
formState: {errors},
|
formState: {errors},
|
||||||
} = useForm({})
|
} = useForm({})
|
||||||
|
|
||||||
|
const {id: sightId} = useParams<{id: string}>()
|
||||||
const {autocompleteProps: cityAutocompleteProps} = useAutocomplete({
|
const {autocompleteProps: cityAutocompleteProps} = useAutocomplete({
|
||||||
resource: 'city',
|
resource: 'city',
|
||||||
})
|
})
|
||||||
@ -83,6 +87,7 @@ export const SightEdit = () => {
|
|||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
</Box>
|
</Box>
|
||||||
|
{sightId && <LinkedItems<ArticleItem> type="edit" parentId={sightId} parentResource="sight" childResource="article" fields={articleFields} title="статьи" />}
|
||||||
</Edit>
|
</Edit>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
@ -1,101 +1,14 @@
|
|||||||
import {Stack, Typography, Box, Grid2 as Grid, Button, MenuItem, Select, FormControl, InputLabel, TextField} from '@mui/material'
|
import {Stack, Typography} from '@mui/material'
|
||||||
import {useShow} from '@refinedev/core'
|
import {useShow} from '@refinedev/core'
|
||||||
import {Show, TextFieldComponent} from '@refinedev/mui'
|
import {Show, TextFieldComponent} from '@refinedev/mui'
|
||||||
|
import {LinkedItems} from '../../components/LinkedItems'
|
||||||
import {useEffect, useState} from 'react'
|
import {ArticleItem, articleFields} from './types'
|
||||||
import axios from 'axios'
|
|
||||||
|
|
||||||
import {BACKEND_URL} from '../../lib/constants'
|
|
||||||
|
|
||||||
type ArticleItem = {
|
|
||||||
id: number
|
|
||||||
heading: string
|
|
||||||
body: string
|
|
||||||
}
|
|
||||||
|
|
||||||
export const SightShow = () => {
|
export const SightShow = () => {
|
||||||
const {query} = useShow({})
|
const {query} = useShow({})
|
||||||
const {data, isLoading} = query
|
const {data, isLoading} = query
|
||||||
const record = data?.data
|
const record = data?.data
|
||||||
|
|
||||||
const [articles, setArticles] = useState<ArticleItem[]>([])
|
|
||||||
const [linkedArticles, setLinkedArticles] = useState<ArticleItem[]>([])
|
|
||||||
const [selectedArticleId, setSelectedArticleId] = useState<number | ''>('')
|
|
||||||
const [pageNum, setPageNum] = useState<number>(1)
|
|
||||||
const [articlesLoading, setArticlesLoading] = useState<boolean>(true)
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (record?.id) {
|
|
||||||
axios
|
|
||||||
.get(`${BACKEND_URL}/sight/${record.id}/article`)
|
|
||||||
.then((response) => {
|
|
||||||
setLinkedArticles(response?.data || [])
|
|
||||||
})
|
|
||||||
.catch(() => {
|
|
||||||
setLinkedArticles([])
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}, [record?.id])
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
axios
|
|
||||||
.get(`${BACKEND_URL}/article/`) // without "/" throws CORS error
|
|
||||||
.then((response) => {
|
|
||||||
setArticles(response?.data || [])
|
|
||||||
setArticlesLoading(false)
|
|
||||||
})
|
|
||||||
.catch(() => {
|
|
||||||
setArticles([])
|
|
||||||
setArticlesLoading(false)
|
|
||||||
})
|
|
||||||
}, [])
|
|
||||||
|
|
||||||
const availableArticles = articles.filter((article) => !linkedArticles.some((linked) => linked.id === article.id))
|
|
||||||
|
|
||||||
const linkArticle = () => {
|
|
||||||
if (selectedArticleId) {
|
|
||||||
const requestData = {
|
|
||||||
article_id: selectedArticleId,
|
|
||||||
page_num: pageNum,
|
|
||||||
}
|
|
||||||
|
|
||||||
axios
|
|
||||||
.post(`${BACKEND_URL}/sight/${record?.id}/article`, requestData, {
|
|
||||||
headers: {
|
|
||||||
accept: 'application/json',
|
|
||||||
'Content-Type': 'application/json',
|
|
||||||
},
|
|
||||||
})
|
|
||||||
.then(() => {
|
|
||||||
axios
|
|
||||||
.get(`${BACKEND_URL}/sight/${record?.id}/article`)
|
|
||||||
.then((response) => {
|
|
||||||
setLinkedArticles(response?.data || [])
|
|
||||||
setPageNum(pageNum + 1)
|
|
||||||
})
|
|
||||||
.catch(() => {
|
|
||||||
setLinkedArticles([])
|
|
||||||
})
|
|
||||||
})
|
|
||||||
.catch((error) => {
|
|
||||||
console.error('Error linking article:', error)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const deleteArticle = (articleId: number) => {
|
|
||||||
axios
|
|
||||||
.delete(`${BACKEND_URL}/sight/${record?.id}/article`, {
|
|
||||||
data: {article_id: articleId},
|
|
||||||
})
|
|
||||||
.then(() => {
|
|
||||||
setLinkedArticles((prev) => prev.filter((item) => item.id !== articleId))
|
|
||||||
})
|
|
||||||
.catch((error) => {
|
|
||||||
console.error('Error unlinking article:', error)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
const fields = [
|
const fields = [
|
||||||
// {label: 'ID', data: 'id'},
|
// {label: 'ID', data: 'id'},
|
||||||
{label: 'Название', data: 'name'},
|
{label: 'Название', data: 'name'},
|
||||||
@ -117,79 +30,7 @@ export const SightShow = () => {
|
|||||||
</Stack>
|
</Stack>
|
||||||
))}
|
))}
|
||||||
|
|
||||||
<Stack gap={2}>
|
{record?.id && <LinkedItems<ArticleItem> type="show" parentId={record.id} parentResource="sight" childResource="article" fields={articleFields} title="статьи" />}
|
||||||
<Typography variant="body1" fontWeight="bold">
|
|
||||||
Привязанные статьи
|
|
||||||
</Typography>
|
|
||||||
|
|
||||||
<Grid container gap={2}>
|
|
||||||
{articlesLoading ? (
|
|
||||||
<Typography>Загрузка статей...</Typography>
|
|
||||||
) : linkedArticles.length > 0 ? (
|
|
||||||
linkedArticles.map((article) => (
|
|
||||||
<Box
|
|
||||||
key={article.id}
|
|
||||||
sx={{
|
|
||||||
marginBottom: '2px',
|
|
||||||
width: '100%',
|
|
||||||
padding: '14px',
|
|
||||||
borderRadius: 2,
|
|
||||||
border: (theme) => `2px solid ${theme.palette.divider}`,
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<Stack gap={1}>
|
|
||||||
<Typography variant="h5">
|
|
||||||
<strong>{article.heading}</strong>
|
|
||||||
</Typography>
|
|
||||||
|
|
||||||
<Typography
|
|
||||||
className="limited-text"
|
|
||||||
sx={{
|
|
||||||
whiteSpace: 'pre-wrap',
|
|
||||||
lineClamp: 3,
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{article.body}
|
|
||||||
</Typography>
|
|
||||||
|
|
||||||
<Button variant="outlined" color="error" onClick={() => deleteArticle(article.id)} sx={{mt: 1.5}}>
|
|
||||||
Отвязать статью
|
|
||||||
</Button>
|
|
||||||
</Stack>
|
|
||||||
</Box>
|
|
||||||
))
|
|
||||||
) : (
|
|
||||||
<Typography>Статьи не найдены</Typography>
|
|
||||||
)}
|
|
||||||
</Grid>
|
|
||||||
|
|
||||||
<Stack gap={2}>
|
|
||||||
<Typography variant="body1" fontWeight="bold">
|
|
||||||
Привязать статью
|
|
||||||
</Typography>
|
|
||||||
|
|
||||||
<Stack gap={2.5}>
|
|
||||||
<FormControl fullWidth>
|
|
||||||
<InputLabel>Статья</InputLabel>
|
|
||||||
<Select value={selectedArticleId} onChange={(e) => setSelectedArticleId(Number(e.target.value))} label="Статья" fullWidth>
|
|
||||||
{availableArticles.map((article) => (
|
|
||||||
<MenuItem key={article.id} value={article.id}>
|
|
||||||
{article.heading}
|
|
||||||
</MenuItem>
|
|
||||||
))}
|
|
||||||
</Select>
|
|
||||||
</FormControl>
|
|
||||||
|
|
||||||
<FormControl fullWidth>
|
|
||||||
<TextField type="number" label="Номер страницы" name="page_num" value={pageNum} onChange={(e) => setPageNum(Number(e.target.value))} fullWidth InputLabelProps={{shrink: true}} />
|
|
||||||
</FormControl>
|
|
||||||
|
|
||||||
<Button variant="contained" onClick={linkArticle} disabled={!selectedArticleId}>
|
|
||||||
Привязать
|
|
||||||
</Button>
|
|
||||||
</Stack>
|
|
||||||
</Stack>
|
|
||||||
</Stack>
|
|
||||||
</Stack>
|
</Stack>
|
||||||
</Show>
|
</Show>
|
||||||
)
|
)
|
||||||
|
17
src/pages/sight/types.ts
Normal file
17
src/pages/sight/types.ts
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
export type ArticleItem = {
|
||||||
|
id: number
|
||||||
|
heading: string
|
||||||
|
body: string
|
||||||
|
[key: string]: string | number
|
||||||
|
}
|
||||||
|
|
||||||
|
export type FieldType<T> = {
|
||||||
|
label: string
|
||||||
|
data: keyof T
|
||||||
|
render?: (value: any) => React.ReactNode
|
||||||
|
}
|
||||||
|
|
||||||
|
export const articleFields: Array<FieldType<ArticleItem>> = [
|
||||||
|
{label: 'Заголовок', data: 'heading'},
|
||||||
|
{label: 'Текст', data: 'body'},
|
||||||
|
]
|
Loading…
Reference in New Issue
Block a user