このコミットが含まれているのは:
2026-07-15 19:33:18 +09:00
コミット eebbd5d069
11個のファイルの変更244行の追加189行の削除
+36 -14
ファイルの表示
@@ -8,13 +8,14 @@ import PageTitle from '@/components/common/PageTitle'
import PrefetchLink from '@/components/PrefetchLink'
import MainArea from '@/components/layout/MainArea'
import PostImportStatusBadge from '@/components/posts/import/PostImportStatusBadge'
import { effectivePostImportStatus } from '@/components/posts/import/postImportRowStatus'
import { displayPostImportStatus } from '@/components/posts/import/postImportRowStatus'
import { Button } from '@/components/ui/button'
import { toast } from '@/components/ui/use-toast'
import { SITE_TITLE } from '@/config'
import { apiPost } from '@/lib/api'
import { canEditContent } from '@/lib/users'
import { clearPostImportSourceDraft,
initialisePreviewRows,
loadPostImportSession,
mergeImportResults,
mergeValidatedImportRows,
@@ -33,7 +34,7 @@ import type { User } from '@/types'
type Props = { user: User | null }
const resultToneClass = (
status: ReturnType<typeof effectivePostImportStatus>,
status: 'created' | 'failed' | 'invalid' | 'skipped' | 'warning' | 'ready',
): string[] => {
switch (status)
{
@@ -46,7 +47,10 @@ const resultToneClass = (
'border-stone-200 bg-stone-50',
'dark:border-stone-800 dark:bg-stone-900/60']
case 'failed':
case 'error':
return [
'border-rose-200 bg-rose-50',
'dark:border-rose-900 dark:bg-rose-950/30']
case 'invalid':
return [
'border-rose-200 bg-rose-50',
'dark:border-rose-900 dark:bg-rose-950/30']
@@ -117,7 +121,9 @@ const PostImportResultPage: FC<Props> = ({ user }) => {
tagSources: row.tagSources,
metadataUrl: row.metadataUrl })),
changed_row: -1 })
const validatedRows = mergeValidatedImportRows (pendingRows, validated.rows)
const validatedRows = mergeValidatedImportRows (
pendingRows,
initialisePreviewRows (validated.rows))
setSession (current =>
current
? { ...current, rows: validatedRows }
@@ -198,28 +204,44 @@ const PostImportResultPage: FC<Props> = ({ user }) => {
<div className="rounded-lg border bg-white p-4 dark:border-neutral-700
dark:bg-neutral-900">
<div className="flex flex-wrap gap-2">
<SummaryChip label="登録成功" value={counts.created} badge="created"/>
<SummaryChip label="登録成功" value={counts.created}/>
<SummaryChip label="スキップ" value={counts.skipped} badge="skipped"/>
<SummaryChip label="失敗" value={counts.failed} badge="failed"/>
<SummaryChip label="要修正" value={counts.invalid} badge="error"/>
<SummaryChip label="失敗" value={counts.failed}/>
<SummaryChip label="要修正" value={counts.invalid}/>
</div>
</div>
<div className="space-y-3">
{session.rows.map (row => {
const status = effectivePostImportStatus (row)
const displayStatus =
Object.keys (row.validationErrors).length > 0
? 'invalid'
: row.importStatus === 'created'
? 'created'
: row.importStatus === 'failed'
? 'failed'
: displayPostImportStatus (row)
const canEdit =
Object.keys (row.validationErrors).length > 0
|| row.importStatus === 'failed'
const canRetry =
row.importStatus === 'created'
? false
: row.importStatus === 'failed'
&& Object.keys (row.validationErrors).length === 0
return (
<div
key={row.sourceRow}
className={[
'rounded-lg border p-4',
...resultToneClass (status)].join (' ')}>
...resultToneClass (displayStatus)].join (' ')}>
<div className="flex flex-col gap-3 md:flex-row md:items-start
md:justify-between">
<div className="space-y-2">
<div className="flex flex-wrap items-center gap-2">
<span className="text-sm font-medium"> {row.sourceRow}</span>
<PostImportStatusBadge value={status}/>
{displayStatus === 'skipped' && (
<PostImportStatusBadge value="skipped"/>)}
</div>
<div className="text-sm text-neutral-700 dark:text-neutral-200">
{String (row.attributes.title ?? '') || row.url}
@@ -238,14 +260,14 @@ const PostImportResultPage: FC<Props> = ({ user }) => {
稿
</PrefetchLink>
</Button>)}
{(status === 'error' || row.importStatus === 'failed') && (
{canEdit && (
<Button
type="button"
variant="outline"
onClick={() => openRepair (row.sourceRow)}>
</Button>)}
{row.importStatus === 'failed' && status !== 'error' && (
{canRetry && (
<>
<Button
type="button"
@@ -293,11 +315,11 @@ const SummaryChip = (
{ label, value, badge }: {
label: string
value: number
badge: 'created' | 'skipped' | 'failed' | 'error' },
badge?: 'skipped' },
) => (
<div className="flex items-center gap-2 rounded-full border border-border
bg-background px-3 py-1 text-sm">
<PostImportStatusBadge value={badge}/>
{badge && <PostImportStatusBadge value={badge}/>}
<span>{label} {value}</span>
</div>)
+49 -10
ファイルの表示
@@ -16,6 +16,7 @@ import { apiPost } from '@/lib/api'
import { canEditContent } from '@/lib/users'
import { loadPostImportSession,
creatableImportRows,
initialisePreviewRows,
mergeImportResults,
mergeValidatedImportRows,
processableImportRows,
@@ -53,6 +54,7 @@ const PostImportReviewPage: FC<Props> = ({ user }) => {
const [loading, setLoading] = useState (false)
const [missing, setMissing] = useState (false)
const [savingRow, setSavingRow] = useState<number | null> (null)
const [dialogMessageRow, setDialogMessageRow] = useState<PostImportRow | null> (null)
useEffect (() => {
if (sessionId == null)
@@ -102,6 +104,16 @@ const PostImportReviewPage: FC<Props> = ({ user }) => {
element?.scrollIntoView ({ block: 'center', behavior: 'smooth' })
}, [editingRow, session?.repairMode])
useEffect (() => {
if (editingRow == null)
{
setDialogMessageRow (null)
return
}
if (dialogMessageRow?.sourceRow !== editingRow.sourceRow)
setDialogMessageRow (null)
}, [editingRow, dialogMessageRow])
const updateSessionRows = (nextRows: PostImportRow[]) =>
setSession (current =>
current ? { ...current, rows: nextRows } : current)
@@ -171,7 +183,15 @@ const PostImportReviewPage: FC<Props> = ({ user }) => {
tagSources: row.tagSources,
metadataUrl: row.metadataUrl })),
changed_row: urlChanged ? editingRow.sourceRow : -1 })
updateSessionRows (mergeValidatedImportRows (nextRows, validated.rows))
const validatedRows = initialisePreviewRows (validated.rows)
const target = validatedRows.find (row => row.sourceRow === editingRow.sourceRow)
if (target != null && Object.keys (target.validationErrors).length > 0)
{
setDialogMessageRow (target)
return false
}
updateSessionRows (mergeValidatedImportRows (nextRows, validatedRows))
setDialogMessageRow (null)
setSearchParams ({ })
return true
}
@@ -193,19 +213,41 @@ const PostImportReviewPage: FC<Props> = ({ user }) => {
setLoading (true)
try
{
const validated = await apiPost<{ rows: PostImportRow[] }> ('/posts/import/validate', {
rows: session.rows
.filter (row => row.importStatus !== 'created')
.map (row => ({
sourceRow: row.sourceRow,
url: row.url,
attributes: row.attributes,
provenance: row.provenance,
tagSources: row.tagSources,
metadataUrl: row.metadataUrl })),
changed_row: -1 })
const validatedRows = initialisePreviewRows (validated.rows)
const mergedRows = mergeValidatedImportRows (session.rows, validatedRows)
const firstInvalid = mergedRows.find (row => Object.keys (row.validationErrors).length > 0)
if (firstInvalid != null)
{
setSession ({ ...session, rows: mergedRows })
setDialogMessageRow (firstInvalid)
setSearchParams ({ edit: String (firstInvalid.sourceRow) })
return
}
const result = await apiPost<{
created: number
skipped: number
failed: number
rows: PostImportResultRow[] }> ('/posts/import', {
rows: processable.map (row => ({
rows: processableImportRows (mergedRows).map (row => ({
sourceRow: row.sourceRow,
url: row.url,
attributes: row.attributes,
provenance: row.provenance,
tagSources: row.tagSources,
metadataUrl: row.metadataUrl })) })
const nextRows = mergeImportResults (session.rows, result.rows)
const nextRows = mergeImportResults (mergedRows, result.rows)
const nextSession = { ...session, rows: nextRows, repairMode: 'all' as const }
setSession (nextSession)
const saved = savePostImportSession (sessionId, nextSession, message =>
@@ -256,10 +298,8 @@ const PostImportReviewPage: FC<Props> = ({ user }) => {
<div className="flex flex-wrap items-center gap-2">
<SummaryChip label="全件" value={counts.total}/>
<SummaryChip label="登録対象" value={counts.submittable}/>
<SummaryChip label="要修正" value={counts.invalid}/>
<SummaryChip label="要確認" value={counts.invalid}/>
<SummaryChip label="スキップ予定" value={counts.skipPlanned}/>
<SummaryChip label="登録済み" value={counts.created}/>
<SummaryChip label="失敗" value={counts.failed}/>
</div>
</div>
@@ -288,9 +328,10 @@ const PostImportReviewPage: FC<Props> = ({ user }) => {
<PostImportRowDialog
open={editingRow != null}
row={editingRow}
messageRow={dialogMessageRow}
saving={savingRow != null}
onOpenChange={open => {
if (!(open))
if (!open)
setSearchParams ({ })
}}
onSave={saveDraft}/>
@@ -319,14 +360,12 @@ const PostImportFooter = (
<div className="mx-auto flex max-w-6xl flex-col gap-3 md:flex-row
md:items-center md:justify-between">
<div className="flex flex-wrap items-center gap-2 text-sm">
<PostImportStatusBadge value="pending"/>
<span> {processableCount} </span>
<PostImportStatusBadge value="ready"/>
<span> {creatableCount} </span>
<PostImportStatusBadge value="skipped"/>
<span> {skipPlannedCount} </span>
<PostImportStatusBadge value="error"/>
<span> {invalidCount} </span>
<span> {invalidCount} </span>
</div>
<div className="flex flex-col gap-2 sm:flex-row">
<Button type="button" variant="outline" onClick={onBack}>
+2 -1
ファイルの表示
@@ -15,6 +15,7 @@ import { inputClass } from '@/lib/utils'
import { countImportSourceLines,
cleanupExpiredPostImportSessions,
createPostImportSessionId,
initialisePreviewRows,
loadPostImportSourceDraft,
savePostImportSession,
savePostImportSourceDraft,
@@ -115,7 +116,7 @@ const PostImportSourcePage: FC<Props> = ({ user }) => {
const sessionId = createPostImportSessionId ()
const saved = savePostImportSession (sessionId, {
source,
rows: data.rows,
rows: initialisePreviewRows (data.rows),
repairMode: 'all' }, message =>
toast ({ title: '取込状態を保存できませんでした', description: message }))
if (!(saved))