このコミットが含まれているのは:
2026-07-11 23:07:51 +09:00
コミット 19bf24432a
5個のファイルの変更70行の追加16行の削除
+23 -3
ファイルの表示
@@ -30,6 +30,7 @@ type ImportRow = {
status: 'ready' | 'warning' | 'error'
existing: boolean }
type ParsedImport = { columns: Column[], rows: { sourceRow: number, values: string[] }[] }
type Phase = 'source' | 'mapping' | 'preview' | 'result'
const detectFormat = (source: string): 'csv' | 'tsv' =>
@@ -42,6 +43,7 @@ const publicGoogleSheetURL = (source: string): boolean =>
const PostImportPage: FC<Props> = ({ user }) => {
const [source, setSource] = useState ('')
const [phase, setPhase] = useState<Phase> ('source')
const [format, setFormat] = useState<'csv' | 'tsv' | 'json' | 'google_sheets'> ('tsv')
const [hasHeader, setHasHeader] = useState (false)
const [jsonPath, setJsonPath] = useState ('')
@@ -69,6 +71,15 @@ const PostImportPage: FC<Props> = ({ user }) => {
setParsed (data)
if (data.columns.length === 1)
setUrlColumn ('0')
else
{
const candidate = data.columns.map ((_, index) => ({ index,
count: data.rows.filter (row => /^https?:\/\//.test (row.values[index] ?? '')).length }))
.sort ((a, b) => b.count - a.count)[0]
if (candidate?.count)
setUrlColumn (String (candidate.index))
}
setPhase ('mapping')
}
catch
{
@@ -92,6 +103,7 @@ const PostImportPage: FC<Props> = ({ user }) => {
mappings,
existing })
setRows (data.rows)
setPhase ('preview')
}
catch
{
@@ -168,6 +180,7 @@ const PostImportPage: FC<Props> = ({ user }) => {
{ rows: targets })
toast ({ title: `登録完了: ${result.created}`,
description: `スキップ ${result.skipped} 件、失敗 ${result.failed}` })
setPhase ('result')
}
catch
{
@@ -186,7 +199,7 @@ const PostImportPage: FC<Props> = ({ user }) => {
<MainArea>
<Helmet><title>{`投稿インポート | ${SITE_TITLE}`}</title></Helmet>
<PageTitle>稿</PageTitle>
{rows.length === 0 ? (
{phase === 'source' ? (
<Form>
<p className="text-sm text-neutral-600 dark:text-neutral-300">
URL CSVTSVJSON
@@ -224,7 +237,7 @@ const PostImportPage: FC<Props> = ({ user }) => {
URL 使CSV
</p>}
<Button type="button" onClick={parseSource} disabled={loading || !(source)}></Button>
</Form>) : parsed && rows.length === 0 ? (
</Form>) : phase === 'mapping' && parsed ? (
<Form>
<PageTitle></PageTitle>
<p>URL 稿</p>
@@ -238,8 +251,10 @@ const PostImportPage: FC<Props> = ({ user }) => {
<option value=""></option>
{columns.map ((column, index) => <option key={column.key} value={index}>{column.label}</option>)}
</select></label>)}
<div className="overflow-x-auto"><table className="text-sm"><thead><tr>{columns.map (column => <th key={column.key}>{column.label}</th>)}</tr></thead><tbody>{parsed.rows.slice (0, 5).map (row => <tr key={row.sourceRow}>{row.values.map ((value, index) => <td key={index}>{value}</td>)}</tr>)}</tbody></table></div>
<Button type="button" variant="outline" onClick={() => setPhase ('source')}></Button>
<Button type="button" onClick={preview} disabled={loading || urlColumn === ''}>稿</Button>
</Form>) : (
</Form>) : phase === 'preview' ? (
<div className="space-y-4">
<div className="flex flex-wrap gap-3 rounded border p-3 dark:border-neutral-700">
<label>URL <select value={urlColumn} onChange={ev => setUrlColumn (ev.target.value)}>
@@ -258,7 +273,12 @@ const PostImportPage: FC<Props> = ({ user }) => {
<div className="space-y-3 md:hidden">{rows.map ((row, index) => <RowCard key={row.sourceRow} row={row} index={index} update={updateAttribute}/>)}</div>
<div className="hidden overflow-x-auto md:block"><table className="w-full text-sm"><thead><tr><th></th><th>URL</th><th></th><th></th><th></th><th></th><th></th><th>稿</th><th></th></tr></thead>
<tbody>{rows.map ((row, index) => <tr key={row.sourceRow} className="border-t dark:border-neutral-700"><td>{row.sourceRow}</td><td><input value={row.url} onChange={ev => updateURL (index, ev.target.value)}/></td><td><input value={row.attributes.title ?? ''} onChange={ev => updateAttribute (index, 'title', ev.target.value)}/></td><td><input value={row.attributes.thumbnailBase ?? ''} onChange={ev => updateAttribute (index, 'thumbnailBase', ev.target.value)}/></td><td><input value={row.attributes.originalCreatedFrom ?? ''} onChange={ev => updateAttribute (index, 'originalCreatedFrom', ev.target.value)}/><input value={row.attributes.originalCreatedBefore ?? ''} onChange={ev => updateAttribute (index, 'originalCreatedBefore', ev.target.value)}/></td><td><input value={String (row.attributes.duration ?? '')} onChange={ev => updateAttribute (index, 'duration', ev.target.value)}/></td><td><input value={row.attributes.tags ?? ''} onChange={ev => updateAttribute (index, 'tags', ev.target.value)}/></td><td><input value={row.attributes.parentPostIds ?? ''} onChange={ev => updateAttribute (index, 'parentPostIds', ev.target.value)}/></td><td>{row.status}<FieldError messages={[...row.warnings, ...Object.values (row.errors).flat ()]}/></td></tr>)}</tbody></table></div>
<Button type="button" variant="outline" onClick={() => setPhase ('mapping')}></Button>
<Button type="button" onClick={submit} disabled={loading}>稿</Button>
</div>) : (
<div className="space-y-3"><p></p>
<Button type="button" onClick={() => setPhase ('preview')}></Button>
<Button type="button" variant="outline" onClick={() => setPhase ('source')}></Button>
</div>)}
</MainArea>)
}