318 lines
9.1 KiB
JavaScript
318 lines
9.1 KiB
JavaScript
|
|
import {useRef, useState} from 'react'
|
||
|
|
import PropTypes from 'prop-types'
|
||
|
|
import Box from '@mui/material/Box'
|
||
|
|
import Typography from '@mui/material/Typography'
|
||
|
|
import Card from '@mui/material/Card'
|
||
|
|
import CardContent from '@mui/material/CardContent'
|
||
|
|
import CardActions from '@mui/material/CardActions'
|
||
|
|
import Button from '@mui/material/Button'
|
||
|
|
import Chip from '@mui/material/Chip'
|
||
|
|
import Avatar from '@mui/material/Avatar'
|
||
|
|
import Divider from '@mui/material/Divider'
|
||
|
|
import Timeline from '@mui/lab/Timeline'
|
||
|
|
import TimelineItem from '@mui/lab/TimelineItem'
|
||
|
|
import TimelineSeparator from '@mui/lab/TimelineSeparator'
|
||
|
|
import TimelineConnector from '@mui/lab/TimelineConnector'
|
||
|
|
import TimelineContent from '@mui/lab/TimelineContent'
|
||
|
|
import TimelineDot from '@mui/lab/TimelineDot'
|
||
|
|
import TimelineOppositeContent from '@mui/lab/TimelineOppositeContent'
|
||
|
|
import AccessTimeIcon from '@mui/icons-material/AccessTime'
|
||
|
|
import CheckCircleIcon from '@mui/icons-material/CheckCircle'
|
||
|
|
import ErrorIcon from '@mui/icons-material/Error'
|
||
|
|
import EditIcon from '@mui/icons-material/Edit'
|
||
|
|
import SessionExpired from '../session/session-expired.js'
|
||
|
|
import VersionDialog from './version-dialog.js'
|
||
|
|
import {formatDate} from '@/lib/format.js'
|
||
|
|
import {compareVersion} from '@/lib/directus.js'
|
||
|
|
|
||
|
|
function getVersionStatus(version, index, totalVersions) {
|
||
|
|
// Logic to determine version status based on position and data
|
||
|
|
if (index === 0) {
|
||
|
|
return 'current' // Most recent
|
||
|
|
}
|
||
|
|
|
||
|
|
if (index === totalVersions - 1) {
|
||
|
|
return 'initial' // First version
|
||
|
|
}
|
||
|
|
|
||
|
|
return 'archived' // Intermediate versions
|
||
|
|
}
|
||
|
|
|
||
|
|
function getStatusConfig(status) {
|
||
|
|
switch (status) {
|
||
|
|
case 'current': {
|
||
|
|
return {
|
||
|
|
color: '#1976D2',
|
||
|
|
bgColor: '#E3F2FD',
|
||
|
|
icon: <EditIcon />,
|
||
|
|
label: 'En cours',
|
||
|
|
chipColor: 'primary'
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
case 'published': {
|
||
|
|
return {
|
||
|
|
color: '#2E7D32',
|
||
|
|
bgColor: '#E8F5E9',
|
||
|
|
icon: <CheckCircleIcon />,
|
||
|
|
label: 'Publié',
|
||
|
|
chipColor: 'success'
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
case 'archived': {
|
||
|
|
return {
|
||
|
|
color: '#757575',
|
||
|
|
bgColor: '#F5F5F5',
|
||
|
|
icon: <AccessTimeIcon />,
|
||
|
|
label: 'Archivé',
|
||
|
|
chipColor: 'default'
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
case 'outdated': {
|
||
|
|
return {
|
||
|
|
color: '#D32F2F',
|
||
|
|
bgColor: '#F9E8E8',
|
||
|
|
icon: <ErrorIcon />,
|
||
|
|
label: 'Obsolète',
|
||
|
|
chipColor: 'error'
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
default: {
|
||
|
|
return {
|
||
|
|
color: '#757575',
|
||
|
|
bgColor: '#F5F5F5',
|
||
|
|
icon: <AccessTimeIcon />,
|
||
|
|
label: 'Archivé',
|
||
|
|
chipColor: 'default'
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
function VersionCard({
|
||
|
|
version,
|
||
|
|
index,
|
||
|
|
totalVersions,
|
||
|
|
accessToken,
|
||
|
|
userId,
|
||
|
|
countdownRef,
|
||
|
|
setError,
|
||
|
|
setIsErrorAlertOpen,
|
||
|
|
setIsOpenComparison,
|
||
|
|
setVersionCompare
|
||
|
|
}) {
|
||
|
|
const status = getVersionStatus(version, index, totalVersions)
|
||
|
|
const statusConfig = getStatusConfig(status)
|
||
|
|
const userDisplayName = version.user_created?.split('-')[0] || 'Système'
|
||
|
|
|
||
|
|
const handleCompareClick = async () => {
|
||
|
|
const comparisonData = await compareVersion({
|
||
|
|
accessToken,
|
||
|
|
userId,
|
||
|
|
versionId: version.id,
|
||
|
|
countdownRef,
|
||
|
|
setError,
|
||
|
|
setIsErrorAlertOpen
|
||
|
|
})
|
||
|
|
|
||
|
|
if (comparisonData) {
|
||
|
|
setVersionCompare({...comparisonData, versionId: version.id})
|
||
|
|
setIsOpenComparison(true)
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
// Estimate content preview (first 100 chars)
|
||
|
|
const contentPreview = version?.delta?.contenu
|
||
|
|
? version.delta.contenu.slice(0, 100) + (version.delta.contenu.length > 100 ? '...' : '')
|
||
|
|
: 'Contenu non disponible'
|
||
|
|
|
||
|
|
return (
|
||
|
|
<Card
|
||
|
|
sx={{
|
||
|
|
borderLeft: `4px solid ${statusConfig.color}`,
|
||
|
|
backgroundColor: statusConfig.bgColor,
|
||
|
|
mb: 2,
|
||
|
|
transition: 'all 0.2s ease-in-out',
|
||
|
|
'&:hover': {
|
||
|
|
transform: 'translateY(-2px)',
|
||
|
|
boxShadow: 3
|
||
|
|
}
|
||
|
|
}}
|
||
|
|
>
|
||
|
|
<CardContent>
|
||
|
|
<Box sx={{
|
||
|
|
display: 'flex',
|
||
|
|
justifyContent: 'space-between',
|
||
|
|
alignItems: 'flex-start',
|
||
|
|
mb: 2
|
||
|
|
}}
|
||
|
|
>
|
||
|
|
<Box sx={{display: 'flex', alignItems: 'center', gap: 1}}>
|
||
|
|
<Avatar sx={{bgcolor: statusConfig.color, width: 32, height: 32}}>
|
||
|
|
{statusConfig.icon}
|
||
|
|
</Avatar>
|
||
|
|
<Box>
|
||
|
|
<Typography variant='h6' sx={{fontWeight: 'bold', color: statusConfig.color}}>
|
||
|
|
{version.name}
|
||
|
|
</Typography>
|
||
|
|
<Typography variant='caption' color='text.secondary'>
|
||
|
|
par @{userDisplayName}
|
||
|
|
</Typography>
|
||
|
|
</Box>
|
||
|
|
</Box>
|
||
|
|
<Chip
|
||
|
|
label={statusConfig.label}
|
||
|
|
color={statusConfig.chipColor}
|
||
|
|
size='small'
|
||
|
|
variant='outlined'
|
||
|
|
/>
|
||
|
|
</Box>
|
||
|
|
|
||
|
|
<Typography variant='body2' color='text.secondary' sx={{mb: 2, fontStyle: 'italic'}}>
|
||
|
|
{contentPreview}
|
||
|
|
</Typography>
|
||
|
|
|
||
|
|
<Box sx={{display: 'flex', justifyContent: 'space-between', alignItems: 'center'}}>
|
||
|
|
<Typography variant='caption' color='text.secondary'>
|
||
|
|
{formatDate(version.date_created, 'PPpp')}
|
||
|
|
</Typography>
|
||
|
|
|
||
|
|
{/* Placeholder for vote count - would need API enhancement */}
|
||
|
|
<Box sx={{display: 'flex', gap: 1}}>
|
||
|
|
<Chip
|
||
|
|
label='👍 0'
|
||
|
|
size='small'
|
||
|
|
variant='outlined'
|
||
|
|
sx={{fontSize: '0.7rem'}}
|
||
|
|
/>
|
||
|
|
<Chip
|
||
|
|
label='👎 0'
|
||
|
|
size='small'
|
||
|
|
variant='outlined'
|
||
|
|
sx={{fontSize: '0.7rem'}}
|
||
|
|
/>
|
||
|
|
</Box>
|
||
|
|
</Box>
|
||
|
|
</CardContent>
|
||
|
|
|
||
|
|
<Divider />
|
||
|
|
<CardActions sx={{justifyContent: 'flex-end'}}>
|
||
|
|
<Button
|
||
|
|
size='small'
|
||
|
|
variant='outlined'
|
||
|
|
color='primary'
|
||
|
|
onClick={handleCompareClick}
|
||
|
|
>
|
||
|
|
Comparer
|
||
|
|
</Button>
|
||
|
|
</CardActions>
|
||
|
|
</Card>
|
||
|
|
)
|
||
|
|
}
|
||
|
|
|
||
|
|
export default function VersionTimeline({
|
||
|
|
collection,
|
||
|
|
data,
|
||
|
|
accessToken,
|
||
|
|
userId,
|
||
|
|
setError,
|
||
|
|
setIsErrorAlertOpen
|
||
|
|
}) {
|
||
|
|
const countdownRef = useRef()
|
||
|
|
const [isOpenComparison, setIsOpenComparison] = useState(false)
|
||
|
|
const [versionCompare, setVersionCompare] = useState(null)
|
||
|
|
|
||
|
|
const versionData = data.find(({id}) => id === versionCompare?.versionId)
|
||
|
|
|
||
|
|
return (
|
||
|
|
<>
|
||
|
|
<Box>
|
||
|
|
<Typography variant='h5' textAlign='center' sx={{mb: 3, fontWeight: 'bold'}}>
|
||
|
|
Historique des versions - {collection}
|
||
|
|
</Typography>
|
||
|
|
|
||
|
|
<Timeline position='right'>
|
||
|
|
{data.map((version, index) => (
|
||
|
|
<TimelineItem key={version.id}>
|
||
|
|
<TimelineOppositeContent sx={{flex: 0.3, pr: 2}}>
|
||
|
|
<Typography variant='caption' color='text.secondary'>
|
||
|
|
{formatDate(version.date_created, 'dd/MM/yyyy')}
|
||
|
|
</Typography>
|
||
|
|
<br />
|
||
|
|
<Typography variant='caption' color='text.secondary'>
|
||
|
|
{formatDate(version.date_created, 'HH:mm')}
|
||
|
|
</Typography>
|
||
|
|
</TimelineOppositeContent>
|
||
|
|
|
||
|
|
<TimelineSeparator>
|
||
|
|
<TimelineDot
|
||
|
|
color={index === 0 ? 'primary' : 'grey'}
|
||
|
|
variant={index === 0 ? 'filled' : 'outlined'}
|
||
|
|
>
|
||
|
|
{index === 0 ? <EditIcon /> : <AccessTimeIcon />}
|
||
|
|
</TimelineDot>
|
||
|
|
{index < data.length - 1 && <TimelineConnector />}
|
||
|
|
</TimelineSeparator>
|
||
|
|
|
||
|
|
<TimelineContent sx={{flex: 1}}>
|
||
|
|
<VersionCard
|
||
|
|
version={version}
|
||
|
|
index={index}
|
||
|
|
totalVersions={data.length}
|
||
|
|
accessToken={accessToken}
|
||
|
|
userId={userId}
|
||
|
|
countdownRef={countdownRef}
|
||
|
|
setError={setError}
|
||
|
|
setIsErrorAlertOpen={setIsErrorAlertOpen}
|
||
|
|
setIsOpenComparison={setIsOpenComparison}
|
||
|
|
setVersionCompare={setVersionCompare}
|
||
|
|
/>
|
||
|
|
</TimelineContent>
|
||
|
|
</TimelineItem>
|
||
|
|
))}
|
||
|
|
</Timeline>
|
||
|
|
</Box>
|
||
|
|
|
||
|
|
{isOpenComparison && (
|
||
|
|
<VersionDialog
|
||
|
|
versionData={versionData}
|
||
|
|
versionCompare={versionCompare}
|
||
|
|
isOpen={isOpenComparison}
|
||
|
|
setIsOpen={setIsOpenComparison}
|
||
|
|
/>
|
||
|
|
)}
|
||
|
|
|
||
|
|
<SessionExpired
|
||
|
|
ref={countdownRef}
|
||
|
|
setError={setError}
|
||
|
|
setIsErrorAlertOpen={setIsErrorAlertOpen}
|
||
|
|
/>
|
||
|
|
</>
|
||
|
|
)
|
||
|
|
}
|
||
|
|
|
||
|
|
VersionTimeline.propTypes = {
|
||
|
|
collection: PropTypes.oneOf(['titres', 'articles']).isRequired,
|
||
|
|
data: PropTypes.array.isRequired,
|
||
|
|
accessToken: PropTypes.string.isRequired,
|
||
|
|
userId: PropTypes.string.isRequired,
|
||
|
|
setError: PropTypes.func.isRequired,
|
||
|
|
setIsErrorAlertOpen: PropTypes.func.isRequired
|
||
|
|
}
|
||
|
|
|
||
|
|
VersionCard.propTypes = {
|
||
|
|
version: PropTypes.object.isRequired,
|
||
|
|
index: PropTypes.number.isRequired,
|
||
|
|
totalVersions: PropTypes.number.isRequired,
|
||
|
|
accessToken: PropTypes.string.isRequired,
|
||
|
|
userId: PropTypes.string.isRequired,
|
||
|
|
countdownRef: PropTypes.object.isRequired,
|
||
|
|
setError: PropTypes.func.isRequired,
|
||
|
|
setIsErrorAlertOpen: PropTypes.func.isRequired,
|
||
|
|
setIsOpenComparison: PropTypes.func.isRequired,
|
||
|
|
setVersionCompare: PropTypes.func.isRequired
|
||
|
|
}
|