このコミットが含まれているのは:
2026-07-11 23:21:59 +09:00
コミット 440d3d38be
5個のファイルの変更63行の追加27行の削除
+17 -8
ファイルの表示
@@ -1,4 +1,4 @@
import { useRef, useState } from 'react'
import { useEffect, useRef, useState } from 'react'
import { Helmet } from 'react-helmet-async'
import FieldError from '@/components/common/FieldError'
@@ -28,8 +28,10 @@ type ImportRow = {
warnings: string[]
errors: Record<string, string[]>
provenance: Record<string, 'automatic' | 'mapped' | 'fixed' | 'manual'>
tagSources?: Record<'automatic' | 'mapped' | 'fixed' | 'manual', string>
status: 'ready' | 'warning' | 'error'
existing: boolean }
existing: boolean
importStatus?: 'pending' | 'created' | 'skipped' | 'failed' }
type ParsedImport = { columns: Column[], rows: { sourceRow: number, values: string[] }[] }
type Phase = 'source' | 'mapping' | 'preview' | 'result'
@@ -172,18 +174,22 @@ const PostImportPage: FC<Props> = ({ user }) => {
}
const submit = async () => {
const targets = rows.filter (row => row.status !== 'error' && !(row.existing && existing === 'skip'))
const targets = rows.filter (row => (row as ImportRow & { importStatus?: string }).importStatus !== 'created')
if (targets.length === 0)
return
setLoading (true)
try
{
const result = await apiPost<{ created: number, skipped: number, failed: number,
rows: { sourceRow: number, status: string, post?: { id: number }, errors?: Record<string, string[]> }[] }> ('/posts/import', { rows: targets })
rows: { sourceRow: number, status: string, post?: { id: number }, errors?: Record<string, string[]> }[] }> ('/posts/import', { rows: targets, existing })
toast ({ title: `登録完了: ${result.created}`,
description: `スキップ ${result.skipped} 件、失敗 ${result.failed}` })
setPhase ('result')
setResults (result.rows)
setRows (current => current.map (row => {
const resultRow = result.rows.find (_1 => _1.sourceRow === row.sourceRow)
return resultRow ? { ...row, importStatus: resultRow.status } : row
}))
}
catch
{
@@ -304,14 +310,17 @@ const RowCard = ({ row, index, update, updateURL }: { row: ImportRow, index: num
const EditableValue = ({ label, value, origin, onCommit }: { label: string, value: string, origin: string | undefined, onCommit: (value: string) => void }) => {
const [editing, setEditing] = useState (false)
const [draft, setDraft] = useState (value)
const finished = useRef (false)
useEffect (() => { if (!editing) setDraft (value) }, [editing, value])
const originLabel = { automatic: '自動取得', mapped: '入力データ', fixed: '固定値', manual: '手修正' }[origin ?? 'automatic']
const commit = () => { setEditing (false); if (draft !== value) onCommit (draft) }
const cancel = () => { setDraft (value); setEditing (false) }
const begin = () => { finished.current = false; setDraft (value); setEditing (true) }
const commit = () => { if (finished.current) return; finished.current = true; setEditing (false); if (draft !== value) onCommit (draft) }
const cancel = () => { if (finished.current) return; finished.current = true; setDraft (value); setEditing (false) }
return <div className={origin === 'manual' ? 'rounded bg-amber-100 p-1 dark:bg-amber-900' : 'p-1'}>
<span className="text-xs">{label}{originLabel}</span>
{editing ? <input autoFocus value={draft} onChange={ev => setDraft (ev.target.value)} onBlur={commit}
onKeyDown={ev => { if (ev.key === 'Escape') cancel (); if (ev.key === 'Enter') commit () } } className={inputClass (false)}/>
: <button type="button" className="block w-full text-left" onDoubleClick={() => setEditing (true)} onClick={() => setEditing (true)}>{value || '(未指定)'} </button>}
onKeyDown={ev => { if (ev.key === 'Escape') cancel (); if (ev.key === 'Enter') { ev.preventDefault (); commit () } } } className={inputClass (false)}/>
: <button type="button" className="block w-full text-left" onDoubleClick={begin} onClick={begin}>{value || '(未指定)'} </button>}
</div>
}