integrate LinkedItems
into /article
pages
This commit is contained in:
parent
faac402aa6
commit
afa94b999c
@ -9,6 +9,13 @@ type Field<T> = {
|
|||||||
data: keyof T
|
data: keyof T
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type ExtraFieldConfig = {
|
||||||
|
type: 'number'
|
||||||
|
label: string
|
||||||
|
minValue: number
|
||||||
|
maxValue: (linkedItems: any[]) => number
|
||||||
|
}
|
||||||
|
|
||||||
type LinkedItemsProps<T> = {
|
type LinkedItemsProps<T> = {
|
||||||
parentId: string | number
|
parentId: string | number
|
||||||
parentResource: string
|
parentResource: string
|
||||||
@ -16,14 +23,16 @@ type LinkedItemsProps<T> = {
|
|||||||
fields: Field<T>[]
|
fields: Field<T>[]
|
||||||
title: string
|
title: string
|
||||||
type: 'show' | 'edit'
|
type: 'show' | 'edit'
|
||||||
|
extraField?: ExtraFieldConfig
|
||||||
}
|
}
|
||||||
|
|
||||||
export const LinkedItems = <T extends {id: number; [key: string]: any}>({parentId, parentResource, childResource, fields, title, type}: LinkedItemsProps<T>) => {
|
export const LinkedItems = <T extends {id: number; [key: string]: any}>({parentId, parentResource, childResource, fields, title, type}: LinkedItemsProps<T>) => {
|
||||||
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 | null>(null)
|
||||||
const [pageNum, setPageNum] = useState<number>(1)
|
const [pageNum, setPageNum] = useState<number>(1)
|
||||||
const [isLoading, setIsLoading] = useState<boolean>(true)
|
const [isLoading, setIsLoading] = useState<boolean>(true)
|
||||||
|
const [mediaOrder, setMediaOrder] = useState<number>(1)
|
||||||
const theme = useTheme()
|
const theme = useTheme()
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@ -65,13 +74,18 @@ export const LinkedItems = <T extends {id: number; [key: string]: any}>({parentI
|
|||||||
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 !== null) {
|
||||||
const requestData =
|
const requestData =
|
||||||
childResource === 'article'
|
childResource === 'article'
|
||||||
? {
|
? {
|
||||||
[`${childResource}_id`]: selectedItemId,
|
[`${childResource}_id`]: selectedItemId,
|
||||||
page_num: pageNum,
|
page_num: pageNum,
|
||||||
}
|
}
|
||||||
|
: childResource === 'media'
|
||||||
|
? {
|
||||||
|
[`${childResource}_id`]: selectedItemId,
|
||||||
|
media_order: mediaOrder,
|
||||||
|
}
|
||||||
: {
|
: {
|
||||||
[`${childResource}_id`]: selectedItemId,
|
[`${childResource}_id`]: selectedItemId,
|
||||||
}
|
}
|
||||||
@ -81,7 +95,7 @@ export const LinkedItems = <T extends {id: number; [key: string]: any}>({parentI
|
|||||||
.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(null)
|
||||||
if (childResource === 'article') {
|
if (childResource === 'article') {
|
||||||
setPageNum(pageNum + 1)
|
setPageNum(pageNum + 1)
|
||||||
}
|
}
|
||||||
@ -107,7 +121,7 @@ export const LinkedItems = <T extends {id: number; [key: string]: any}>({parentI
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Accordion defaultExpanded>
|
<Accordion>
|
||||||
<AccordionSummary
|
<AccordionSummary
|
||||||
expandIcon={<ExpandMoreIcon />}
|
expandIcon={<ExpandMoreIcon />}
|
||||||
sx={{
|
sx={{
|
||||||
@ -160,9 +174,10 @@ export const LinkedItems = <T extends {id: number; [key: string]: any}>({parentI
|
|||||||
<Typography variant="subtitle1">Добавить {title}</Typography>
|
<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(e.target.value as number)} label={`Выберите ${title}`}>
|
||||||
{availableItems.map((item) => (
|
{availableItems.map((item) => (
|
||||||
<MenuItem key={item.id} value={item.id}>
|
<MenuItem key={item.id} value={item.id}>
|
||||||
|
{/* {fields.map((field) => item[field.data]).join(' - ')} */}
|
||||||
{item[fields[0].data]}
|
{item[fields[0].data]}
|
||||||
</MenuItem>
|
</MenuItem>
|
||||||
))}
|
))}
|
||||||
@ -187,6 +202,24 @@ export const LinkedItems = <T extends {id: number; [key: string]: any}>({parentI
|
|||||||
</FormControl>
|
</FormControl>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
{childResource === 'media' && type === 'edit' && (
|
||||||
|
<FormControl fullWidth>
|
||||||
|
<TextField
|
||||||
|
type="number"
|
||||||
|
label="Порядок отображения медиа"
|
||||||
|
value={mediaOrder}
|
||||||
|
onChange={(e) => {
|
||||||
|
const newValue = Number(e.target.value)
|
||||||
|
const maxValue = linkedItems.length + 1
|
||||||
|
const value = Math.max(1, Math.min(newValue, maxValue))
|
||||||
|
setMediaOrder(value)
|
||||||
|
}}
|
||||||
|
fullWidth
|
||||||
|
InputLabelProps={{shrink: true}}
|
||||||
|
/>
|
||||||
|
</FormControl>
|
||||||
|
)}
|
||||||
|
|
||||||
<Button variant="contained" onClick={linkItem} disabled={!selectedItemId}>
|
<Button variant="contained" onClick={linkItem} disabled={!selectedItemId}>
|
||||||
Добавить
|
Добавить
|
||||||
</Button>
|
</Button>
|
||||||
|
@ -66,7 +66,7 @@ const StyledMarkdownEditor = styled('div')(({theme}) => ({
|
|||||||
}))
|
}))
|
||||||
|
|
||||||
export const MarkdownEditor = (props: SimpleMDEReactProps) => (
|
export const MarkdownEditor = (props: SimpleMDEReactProps) => (
|
||||||
<StyledMarkdownEditor className="my-markdown-editor">
|
<StyledMarkdownEditor className="my-markdown-editor" sx={{marginTop: 1.5, marginBottom: 3}}>
|
||||||
<SimpleMDE {...props} />
|
<SimpleMDE {...props} />
|
||||||
</StyledMarkdownEditor>
|
</StyledMarkdownEditor>
|
||||||
)
|
)
|
||||||
|
@ -2,9 +2,12 @@ import {Box, TextField} from '@mui/material'
|
|||||||
import {Edit} from '@refinedev/mui'
|
import {Edit} 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 React from 'react'
|
import React from 'react'
|
||||||
|
|
||||||
import {MarkdownEditor} from '../../components/MarkdownEditor'
|
import {MarkdownEditor} from '../../components/MarkdownEditor'
|
||||||
|
import {LinkedItems} from '../../components/LinkedItems'
|
||||||
|
import {MediaItem, mediaFields} from './types'
|
||||||
import 'easymde/dist/easymde.min.css'
|
import 'easymde/dist/easymde.min.css'
|
||||||
|
|
||||||
const MemoizedSimpleMDE = React.memo(MarkdownEditor)
|
const MemoizedSimpleMDE = React.memo(MarkdownEditor)
|
||||||
@ -17,6 +20,8 @@ export const ArticleEdit = () => {
|
|||||||
formState: {errors},
|
formState: {errors},
|
||||||
} = useForm()
|
} = useForm()
|
||||||
|
|
||||||
|
const {id: articleId} = useParams<{id: string}>()
|
||||||
|
|
||||||
const simpleMDEOptions = React.useMemo(
|
const simpleMDEOptions = React.useMemo(
|
||||||
() => ({
|
() => ({
|
||||||
placeholder: 'Введите контент в формате Markdown...',
|
placeholder: 'Введите контент в формате Markdown...',
|
||||||
@ -56,6 +61,8 @@ export const ArticleEdit = () => {
|
|||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
{articleId && <LinkedItems<MediaItem> type="edit" parentId={articleId} parentResource="article" childResource="media" fields={mediaFields} title="медиа" />}
|
||||||
</Box>
|
</Box>
|
||||||
</Edit>
|
</Edit>
|
||||||
)
|
)
|
||||||
|
@ -1,118 +1,18 @@
|
|||||||
import {Stack, Typography, Grid2 as Grid, Button, MenuItem, Select, FormControl, InputLabel, TextField, Card, CardMedia, CardContent, CardActions} 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 {MediaItem, articleFields, mediaFields} from './types'
|
||||||
import axios from 'axios'
|
|
||||||
|
|
||||||
import {BACKEND_URL} from '../../lib/constants'
|
|
||||||
|
|
||||||
type MediaItem = {
|
|
||||||
id: string
|
|
||||||
filename: string
|
|
||||||
media_type: string
|
|
||||||
}
|
|
||||||
|
|
||||||
export const ArticleShow = () => {
|
export const ArticleShow = () => {
|
||||||
const {query} = useShow({})
|
const {query} = useShow({})
|
||||||
const {data, isLoading} = query
|
const {data, isLoading} = query
|
||||||
const record = data?.data
|
const record = data?.data
|
||||||
|
|
||||||
const [media, setMedia] = useState<MediaItem[]>([])
|
|
||||||
const [linkedMedia, setLinkedMedia] = useState<MediaItem[]>([])
|
|
||||||
const [selectedMediaId, setSelectedMediaId] = useState<string>('')
|
|
||||||
const [mediaOrder, setMediaOrder] = useState<number>(1)
|
|
||||||
const [mediaLoading, setMediaLoading] = useState<boolean>(true)
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (record?.id) {
|
|
||||||
axios
|
|
||||||
.get(`${BACKEND_URL}/article/${record.id}/media`)
|
|
||||||
.then((response) => {
|
|
||||||
setLinkedMedia(response?.data || [])
|
|
||||||
})
|
|
||||||
.catch(() => {
|
|
||||||
setLinkedMedia([])
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}, [record?.id])
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
axios
|
|
||||||
.get(`${BACKEND_URL}/media`)
|
|
||||||
.then((response) => {
|
|
||||||
setMedia(response?.data || [])
|
|
||||||
setMediaLoading(false)
|
|
||||||
})
|
|
||||||
.catch(() => {
|
|
||||||
setMedia([])
|
|
||||||
setMediaLoading(false)
|
|
||||||
})
|
|
||||||
}, [])
|
|
||||||
|
|
||||||
const availableMedia = media.filter((mediaItem) => !linkedMedia.some((linkedItem) => linkedItem.id === mediaItem.id))
|
|
||||||
|
|
||||||
const linkMedia = () => {
|
|
||||||
if (selectedMediaId) {
|
|
||||||
const requestData = {
|
|
||||||
media_id: selectedMediaId,
|
|
||||||
media_order: mediaOrder,
|
|
||||||
}
|
|
||||||
|
|
||||||
axios
|
|
||||||
.post(`${BACKEND_URL}/article/${record?.id}/media`, requestData, {
|
|
||||||
headers: {
|
|
||||||
accept: 'application/json',
|
|
||||||
'Content-Type': 'application/json',
|
|
||||||
},
|
|
||||||
})
|
|
||||||
.then(() => {
|
|
||||||
axios
|
|
||||||
.get(`${BACKEND_URL}/article/${record?.id}/media`)
|
|
||||||
.then((response) => {
|
|
||||||
setLinkedMedia(response?.data || [])
|
|
||||||
setMediaOrder(mediaOrder + 1)
|
|
||||||
setSelectedMediaId('')
|
|
||||||
})
|
|
||||||
.catch(() => {
|
|
||||||
setLinkedMedia([])
|
|
||||||
})
|
|
||||||
})
|
|
||||||
.catch((error) => {
|
|
||||||
console.error('Error linking media:', error)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const deleteMedia = (mediaId: string) => {
|
|
||||||
axios
|
|
||||||
.delete(`${BACKEND_URL}/article/${record?.id}/media`, {
|
|
||||||
data: {media_id: mediaId},
|
|
||||||
})
|
|
||||||
.then(() => {
|
|
||||||
setLinkedMedia((prevMedia) => prevMedia.filter((item) => item.id !== mediaId))
|
|
||||||
})
|
|
||||||
.catch((error) => {
|
|
||||||
console.error('Error deleting media:', error)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
const fields = [
|
|
||||||
// {label: 'ID', data: 'id'},
|
|
||||||
{label: 'Заголовок', data: 'heading'},
|
|
||||||
{label: 'Контент', data: 'body'},
|
|
||||||
]
|
|
||||||
|
|
||||||
const mediaFields = [
|
|
||||||
// {label: 'ID', data: 'id' as keyof MediaItem},
|
|
||||||
{label: 'Имя', data: 'filename' as keyof MediaItem},
|
|
||||||
{label: 'Тип', data: 'media_type' as keyof MediaItem},
|
|
||||||
]
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Show isLoading={isLoading}>
|
<Show isLoading={isLoading}>
|
||||||
<Stack gap={4}>
|
<Stack gap={4}>
|
||||||
{fields.map(({label, data}) => (
|
{articleFields.map(({label, data}) => (
|
||||||
<Stack key={data} gap={1}>
|
<Stack key={data} gap={1}>
|
||||||
<Typography variant="h6" fontWeight="bold">
|
<Typography variant="h6" fontWeight="bold">
|
||||||
{label}
|
{label}
|
||||||
@ -121,66 +21,7 @@ export const ArticleShow = () => {
|
|||||||
</Stack>
|
</Stack>
|
||||||
))}
|
))}
|
||||||
|
|
||||||
<Stack gap={2}>
|
{record?.id && <LinkedItems<MediaItem> parentId={record.id} parentResource="article" childResource="media" fields={mediaFields} title="медиа" type="show" />}
|
||||||
<Typography variant="h6" fontWeight="bold">
|
|
||||||
Медиа
|
|
||||||
</Typography>
|
|
||||||
<Grid container spacing={2}>
|
|
||||||
{mediaLoading ? (
|
|
||||||
<Typography>Загрузка медиа...</Typography>
|
|
||||||
) : linkedMedia.length > 0 ? (
|
|
||||||
linkedMedia.map((mediaItem) => (
|
|
||||||
<Grid key={mediaItem.id}>
|
|
||||||
<Card
|
|
||||||
sx={{
|
|
||||||
marginBottom: '8px',
|
|
||||||
borderRadius: 2,
|
|
||||||
border: (theme) => `2px solid ${theme.palette.divider}`,
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<CardMedia component="img" height="200" image={`${BACKEND_URL}/media/${mediaItem?.id}/download`} alt={mediaItem?.filename} sx={{objectFit: 'contain'}} />
|
|
||||||
<CardContent>
|
|
||||||
{mediaFields.map(({label, data}) => (
|
|
||||||
<Typography key={data} variant="body2" color="textSecondary">
|
|
||||||
<strong>{label}:</strong> {mediaItem?.[data]}
|
|
||||||
</Typography>
|
|
||||||
))}
|
|
||||||
</CardContent>
|
|
||||||
<CardActions>
|
|
||||||
<Button variant="outlined" color="error" onClick={() => deleteMedia(mediaItem?.id)} fullWidth>
|
|
||||||
Отвязать
|
|
||||||
</Button>
|
|
||||||
</CardActions>
|
|
||||||
</Card>
|
|
||||||
</Grid>
|
|
||||||
))
|
|
||||||
) : (
|
|
||||||
<Typography>Нет привязанных медиа</Typography>
|
|
||||||
)}
|
|
||||||
</Grid>
|
|
||||||
|
|
||||||
{/* sx={{width: '650px'}} */}
|
|
||||||
<Stack gap={2} mt={4}>
|
|
||||||
{' '}
|
|
||||||
<Typography variant="h6" fontWeight="bold">
|
|
||||||
Привязать медиа
|
|
||||||
</Typography>
|
|
||||||
<FormControl fullWidth>
|
|
||||||
<InputLabel>Медиа</InputLabel>
|
|
||||||
<Select value={selectedMediaId} onChange={(e) => setSelectedMediaId(e.target.value)} fullWidth>
|
|
||||||
{availableMedia.map((mediaItem) => (
|
|
||||||
<MenuItem key={mediaItem.id} value={mediaItem.id}>
|
|
||||||
{mediaItem.filename}
|
|
||||||
</MenuItem>
|
|
||||||
))}
|
|
||||||
</Select>
|
|
||||||
</FormControl>
|
|
||||||
<TextField type="number" label="Порядок отображения медиа" value={mediaOrder} onChange={(e) => setMediaOrder(Number(e.target.value))} fullWidth InputLabelProps={{shrink: true}} />
|
|
||||||
<Button variant="contained" onClick={linkMedia} disabled={!selectedMediaId}>
|
|
||||||
Привязать
|
|
||||||
</Button>
|
|
||||||
</Stack>
|
|
||||||
</Stack>
|
|
||||||
</Stack>
|
</Stack>
|
||||||
</Show>
|
</Show>
|
||||||
)
|
)
|
||||||
|
28
src/pages/article/types.ts
Normal file
28
src/pages/article/types.ts
Normal file
@ -0,0 +1,28 @@
|
|||||||
|
export type MediaItem = {
|
||||||
|
id: number
|
||||||
|
filename: string
|
||||||
|
media_type: string
|
||||||
|
media_order?: number
|
||||||
|
}
|
||||||
|
|
||||||
|
export type ArticleItem = {
|
||||||
|
id: number
|
||||||
|
heading: string
|
||||||
|
body: string
|
||||||
|
}
|
||||||
|
|
||||||
|
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'},
|
||||||
|
]
|
||||||
|
|
||||||
|
export const mediaFields: Array<FieldType<MediaItem>> = [
|
||||||
|
{label: 'Имя', data: 'filename'},
|
||||||
|
{label: 'Тип', data: 'media_type'},
|
||||||
|
]
|
Loading…
Reference in New Issue
Block a user