178 lines
6.0 KiB
TypeScript
178 lines
6.0 KiB
TypeScript
import { useEffect, useState } from 'react'
|
|
import { Link } from 'react-router-dom'
|
|
import { api, downloadWithAuth } from '../api'
|
|
import ConfirmDialog from '../components/ConfirmDialog'
|
|
import { useAuth } from '../context/AuthContext'
|
|
import { useI18n } from '../context/I18nContext'
|
|
import type { TemplateListItem } from '../types'
|
|
|
|
export default function TemplatesPage() {
|
|
const { user } = useAuth()
|
|
const { t, formatDate } = useI18n()
|
|
const [templates, setTemplates] = useState<TemplateListItem[]>([])
|
|
const [loading, setLoading] = useState(true)
|
|
const [error, setError] = useState('')
|
|
const [deleteTarget, setDeleteTarget] = useState<TemplateListItem | null>(null)
|
|
|
|
const load = async () => {
|
|
try {
|
|
setTemplates(await api.listTemplates())
|
|
} catch (err) {
|
|
setError(err instanceof Error ? err.message : t('errors.loadTemplatesFailed'))
|
|
} finally {
|
|
setLoading(false)
|
|
}
|
|
}
|
|
|
|
useEffect(() => {
|
|
load()
|
|
}, [])
|
|
|
|
const handleDelete = async () => {
|
|
if (!deleteTarget) return
|
|
try {
|
|
await api.deleteTemplate(deleteTarget.id)
|
|
setTemplates((list) => list.filter((x) => x.id !== deleteTarget.id))
|
|
setDeleteTarget(null)
|
|
} catch (err) {
|
|
alert(err instanceof Error ? err.message : t('errors.deleteFailed'))
|
|
}
|
|
}
|
|
|
|
const handleTogglePublic = async (item: TemplateListItem) => {
|
|
try {
|
|
const updated = await api.updateTemplate(item.id, { is_public: !item.is_public })
|
|
setTemplates((list) =>
|
|
list.map((x) =>
|
|
x.id === item.id ? { ...x, is_public: updated.is_public } : x,
|
|
),
|
|
)
|
|
} catch (err) {
|
|
alert(err instanceof Error ? err.message : t('errors.updateFailed'))
|
|
}
|
|
}
|
|
|
|
const handleDownloadSource = async (item: TemplateListItem) => {
|
|
try {
|
|
await downloadWithAuth(api.getTemplateSourceUrl(item.id), item.original_filename)
|
|
} catch (err) {
|
|
alert(err instanceof Error ? err.message : t('errors.downloadFailed'))
|
|
}
|
|
}
|
|
|
|
const canManage = (item: TemplateListItem) =>
|
|
item.is_owner || user?.role === 'admin'
|
|
|
|
return (
|
|
<div className="container">
|
|
<ConfirmDialog
|
|
open={deleteTarget !== null}
|
|
title={t('templates.deleteTitle')}
|
|
message={
|
|
deleteTarget
|
|
? t('templates.deleteMessage', {
|
|
name: deleteTarget.name,
|
|
cannotUndo: t('common.cannotUndo'),
|
|
})
|
|
: ''
|
|
}
|
|
confirmLabel={t('common.delete')}
|
|
danger
|
|
onConfirm={() => void handleDelete()}
|
|
onCancel={() => setDeleteTarget(null)}
|
|
/>
|
|
|
|
<div className="page-header">
|
|
<h1>{t('templates.title')}</h1>
|
|
<Link to="/templates/new" className="btn btn-primary">
|
|
{t('templates.upload')}
|
|
</Link>
|
|
</div>
|
|
|
|
{error && <div className="error">{error}</div>}
|
|
|
|
{loading ? (
|
|
<p>{t('common.loading')}</p>
|
|
) : templates.length === 0 ? (
|
|
<div className="card empty-state">
|
|
<p>{t('templates.empty')}</p>
|
|
<Link to="/templates/new" className="btn btn-primary" style={{ marginTop: 16 }}>
|
|
{t('templates.uploadShort')}
|
|
</Link>
|
|
</div>
|
|
) : (
|
|
<div className="card">
|
|
<table className="table">
|
|
<thead>
|
|
<tr>
|
|
<th>{t('common.name')}</th>
|
|
<th>{t('common.file')}</th>
|
|
<th>{t('common.owner')}</th>
|
|
<th>{t('templates.visibility')}</th>
|
|
<th>{t('templates.variables')}</th>
|
|
<th>{t('common.created')}</th>
|
|
<th>{t('common.actions')}</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
{templates.map((item) => (
|
|
<tr key={item.id}>
|
|
<td>
|
|
<strong>{item.name}</strong>
|
|
{item.description && (
|
|
<div className="style-hint">{item.description}</div>
|
|
)}
|
|
</td>
|
|
<td>{item.original_filename}</td>
|
|
<td>{item.owner_username ?? `#${item.owner_id}`}</td>
|
|
<td>
|
|
{item.is_public ? (
|
|
<span className="badge" style={{ background: '#dbeafe', color: '#1d4ed8' }}>
|
|
{t('visibility.public')}
|
|
</span>
|
|
) : (
|
|
<span className="badge badge-user">{t('visibility.private')}</span>
|
|
)}
|
|
</td>
|
|
<td>{item.variable_count}</td>
|
|
<td>{formatDate(item.created_at)}</td>
|
|
<td style={{ display: 'flex', gap: 8, flexWrap: 'wrap' }}>
|
|
<Link to={`/templates/${item.id}/fill`} className="btn btn-primary btn-sm">
|
|
{t('templates.fill')}
|
|
</Link>
|
|
<Link to={`/templates/${item.id}`} className="btn btn-secondary btn-sm">
|
|
{t('common.view')}
|
|
</Link>
|
|
<button
|
|
className="btn btn-secondary btn-sm"
|
|
onClick={() => handleDownloadSource(item)}
|
|
>
|
|
{t('templates.source')}
|
|
</button>
|
|
{canManage(item) && (
|
|
<>
|
|
<button
|
|
className="btn btn-secondary btn-sm"
|
|
onClick={() => handleTogglePublic(item)}
|
|
>
|
|
{item.is_public ? t('visibility.makePrivate') : t('visibility.makePublic')}
|
|
</button>
|
|
<button
|
|
className="btn btn-danger btn-sm"
|
|
onClick={() => setDeleteTarget(item)}
|
|
>
|
|
{t('common.delete')}
|
|
</button>
|
|
</>
|
|
)}
|
|
</td>
|
|
</tr>
|
|
))}
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
)}
|
|
</div>
|
|
)
|
|
}
|