feat: add new page and migration scripts for access rules and collections
- Introduced new page component for library topics with type checks. - Added migration scripts to update access rules for various collections including badges, curriculum versions, and themes. - Implemented PocketBase integration for managing collection access rules dynamically. - Ensured proper type validation for page props and metadata generation functions.
This commit is contained in:
@@ -38,19 +38,26 @@ export function DocumentUpload({ onSuccess }: DocumentUploadProps) {
|
||||
|
||||
try {
|
||||
// Create record in PocketBase
|
||||
const format = ext.slice(1) as 'pdf' | 'md' | 'txt'
|
||||
const fd = new FormData()
|
||||
fd.append('file', file)
|
||||
fd.append('filename', file.name)
|
||||
fd.append('format', ext.slice(1) as 'pdf' | 'md' | 'txt')
|
||||
fd.append('format', format)
|
||||
fd.append('status', 'processing')
|
||||
|
||||
const doc = await pb.collection('source_documents').create<{ id: string }>(fd)
|
||||
const doc = await pb.collection('source_documents').create<{ id: string; file: string }>(fd)
|
||||
|
||||
// Build PocketBase file download URL
|
||||
const pbUrl = process.env.NEXT_PUBLIC_POCKETBASE_URL ?? 'http://localhost:8090'
|
||||
const fileUrl = `${pbUrl}/api/files/source_documents/${doc.id}/${doc.file}`
|
||||
|
||||
// Kick off ingestion
|
||||
const ingestFd = new FormData()
|
||||
ingestFd.append('documentId', doc.id)
|
||||
|
||||
const { jobId: jId } = await postIngest(ingestFd)
|
||||
const { jobId: jId } = await postIngest({
|
||||
documentId: doc.id,
|
||||
filename: file.name,
|
||||
format,
|
||||
fileUrl,
|
||||
})
|
||||
setJobId(jId)
|
||||
} catch (err) {
|
||||
setUploadError(err instanceof Error ? err.message : 'Upload failed')
|
||||
|
||||
@@ -4,6 +4,7 @@ import Link from 'next/link'
|
||||
import type { Theme } from '@/types'
|
||||
import { StatusBadge } from '@/components/ui/StatusBadge'
|
||||
import { Button } from '@/components/ui/Button'
|
||||
import { pb } from '@/lib/pocketbase'
|
||||
import { postGenerateAll } from '@/lib/services'
|
||||
|
||||
interface ThemeCardProps {
|
||||
@@ -12,24 +13,57 @@ interface ThemeCardProps {
|
||||
}
|
||||
|
||||
export function ThemeCard({ theme, onUpdate }: ThemeCardProps) {
|
||||
const [generating, setGenerating] = useState(false)
|
||||
const [approving, setApproving] = useState(false)
|
||||
const [rejecting, setRejecting] = useState(false)
|
||||
const [toast, setToast] = useState<string | null>(null)
|
||||
const [error, setError] = useState<string | null>(null)
|
||||
const [queued, setQueued] = useState<number | null>(null)
|
||||
|
||||
async function handleGenerate() {
|
||||
setGenerating(true)
|
||||
async function handleApprove() {
|
||||
setApproving(true)
|
||||
setError(null)
|
||||
setToast(null)
|
||||
try {
|
||||
const { queued: q } = await postGenerateAll(theme.id)
|
||||
setQueued(q)
|
||||
// 1. Fetch all topics in this theme
|
||||
const topics = await pb.collection('topics').getFullList<{ id: string }>({
|
||||
filter: `theme="${theme.id}"`,
|
||||
fields: 'id',
|
||||
})
|
||||
|
||||
// 2. Publish all topics
|
||||
await Promise.all(
|
||||
topics.map((t) => pb.collection('topics').update(t.id, { status: 'published' }))
|
||||
)
|
||||
|
||||
// 3. Publish the theme
|
||||
await pb.collection('themes').update(theme.id, { status: 'published' })
|
||||
|
||||
// 4. Trigger generation service
|
||||
const { queued } = await postGenerateAll(theme.id)
|
||||
setToast(`Generation queued for ${queued} micro-learnings`)
|
||||
onUpdate()
|
||||
} catch (err) {
|
||||
setError(err instanceof Error ? err.message : 'Generation failed')
|
||||
setError(err instanceof Error ? err.message : 'Approval failed')
|
||||
} finally {
|
||||
setGenerating(false)
|
||||
setApproving(false)
|
||||
}
|
||||
}
|
||||
|
||||
async function handleReject() {
|
||||
setRejecting(true)
|
||||
setError(null)
|
||||
try {
|
||||
await pb.collection('themes').update(theme.id, { status: 'rejected' })
|
||||
onUpdate()
|
||||
} catch (err) {
|
||||
setError(err instanceof Error ? err.message : 'Rejection failed')
|
||||
} finally {
|
||||
setRejecting(false)
|
||||
}
|
||||
}
|
||||
|
||||
const isPublished = theme.status === 'published'
|
||||
const isRejected = theme.status === 'rejected'
|
||||
|
||||
return (
|
||||
<div
|
||||
style={{
|
||||
@@ -40,28 +74,15 @@ export function ThemeCard({ theme, onUpdate }: ThemeCardProps) {
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
gap: 'var(--s-3)',
|
||||
opacity: isRejected ? 0.6 : 1,
|
||||
}}
|
||||
>
|
||||
<div style={{ display: 'flex', alignItems: 'flex-start', justifyContent: 'space-between', gap: 'var(--s-3)' }}>
|
||||
<div style={{ flex: 1, minWidth: 0 }}>
|
||||
<h3
|
||||
style={{
|
||||
margin: '0 0 var(--s-1)',
|
||||
fontSize: 'var(--t-body)',
|
||||
fontWeight: 700,
|
||||
color: 'var(--fg)',
|
||||
}}
|
||||
>
|
||||
<h3 style={{ margin: '0 0 var(--s-1)', fontSize: 'var(--t-body)', fontWeight: 700, color: 'var(--fg)' }}>
|
||||
{theme.title}
|
||||
</h3>
|
||||
<p
|
||||
style={{
|
||||
margin: 0,
|
||||
fontSize: 'var(--t-small)',
|
||||
color: 'var(--fg-muted)',
|
||||
lineHeight: 1.5,
|
||||
}}
|
||||
>
|
||||
<p style={{ margin: 0, fontSize: 'var(--t-small)', color: 'var(--fg-muted)', lineHeight: 1.5 }}>
|
||||
{theme.description}
|
||||
</p>
|
||||
</div>
|
||||
@@ -80,49 +101,34 @@ export function ThemeCard({ theme, onUpdate }: ThemeCardProps) {
|
||||
{theme.source_documents.length} source doc{theme.source_documents.length !== 1 ? 's' : ''}
|
||||
</div>
|
||||
|
||||
{queued !== null && (
|
||||
<div
|
||||
style={{
|
||||
background: 'var(--success)',
|
||||
color: 'var(--teal-900)',
|
||||
borderRadius: 'var(--r-sm)',
|
||||
padding: 'var(--s-2) var(--s-3)',
|
||||
fontSize: 'var(--t-small)',
|
||||
fontWeight: 500,
|
||||
}}
|
||||
>
|
||||
{queued} micro-learnings queued for generation
|
||||
{toast && (
|
||||
<div style={{ background: 'var(--success)', color: 'var(--teal-900)', borderRadius: 'var(--r-sm)', padding: 'var(--s-2) var(--s-3)', fontSize: 'var(--t-small)', fontWeight: 500 }}>
|
||||
{toast}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{error && (
|
||||
<div
|
||||
style={{
|
||||
background: '#f8d7da',
|
||||
color: '#c0392b',
|
||||
borderRadius: 'var(--r-sm)',
|
||||
padding: 'var(--s-2) var(--s-3)',
|
||||
fontSize: 'var(--t-small)',
|
||||
}}
|
||||
>
|
||||
<div style={{ background: '#f8d7da', color: '#c0392b', borderRadius: 'var(--r-sm)', padding: 'var(--s-2) var(--s-3)', fontSize: 'var(--t-small)' }}>
|
||||
{error}
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div style={{ display: 'flex', gap: 'var(--s-2)', flexWrap: 'wrap' }}>
|
||||
<Link href={`/admin/knowledge/${theme.id}`} style={{ textDecoration: 'none' }}>
|
||||
<Button variant="secondary" size="sm">
|
||||
View topics
|
||||
</Button>
|
||||
</Link>
|
||||
<Button
|
||||
variant="primary"
|
||||
size="sm"
|
||||
onClick={() => void handleGenerate()}
|
||||
loading={generating}
|
||||
>
|
||||
Generate micro-learnings
|
||||
</Button>
|
||||
{!isRejected && (
|
||||
<Link href={`/admin/knowledge/${theme.id}`} style={{ textDecoration: 'none' }}>
|
||||
<Button variant="secondary" size="sm">Edit topics</Button>
|
||||
</Link>
|
||||
)}
|
||||
{!isPublished && !isRejected && (
|
||||
<>
|
||||
<Button variant="primary" size="sm" onClick={() => void handleApprove()} loading={approving}>
|
||||
Approve batch
|
||||
</Button>
|
||||
<Button variant="danger" size="sm" onClick={() => void handleReject()} loading={rejecting}>
|
||||
Reject
|
||||
</Button>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user