このコミットが含まれているのは:
@@ -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 を一行に一件ずつ貼り付けるか、CSV・TSV・JSON を入力してください。
|
||||
@@ -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>)
|
||||
}
|
||||
|
||||
新しい課題から参照
ユーザをブロックする