このコミットが含まれているのは:
@@ -215,7 +215,7 @@ const PostImportResultPage: FC<Props> = ({ user }) => {
|
||||
type="button"
|
||||
variant="outline"
|
||||
onClick={() => openRepair (row.sourceRow)}>
|
||||
修正
|
||||
編輯
|
||||
</Button>
|
||||
<Button
|
||||
type="button"
|
||||
|
||||
@@ -4,7 +4,6 @@ import { useNavigate,
|
||||
useParams,
|
||||
useSearchParams } from 'react-router-dom'
|
||||
|
||||
import FieldError from '@/components/common/FieldError'
|
||||
import PageTitle from '@/components/common/PageTitle'
|
||||
import MainArea from '@/components/layout/MainArea'
|
||||
import PostImportRowDialog from '@/components/posts/import/PostImportRowDialog'
|
||||
@@ -98,7 +97,7 @@ const PostImportReviewPage: FC<Props> = ({ user }) => {
|
||||
return
|
||||
|
||||
const element = document.getElementById (`post-import-row-${ editingRow.sourceRow }`)
|
||||
element?.scrollIntoView ({ block: 'center', behaviour: 'smooth' })
|
||||
element?.scrollIntoView ({ block: 'center', behavior: 'smooth' })
|
||||
}, [editingRow, session?.repairMode])
|
||||
|
||||
const updateSessionRows = (nextRows: PostImportRow[]) =>
|
||||
@@ -233,7 +232,7 @@ const PostImportReviewPage: FC<Props> = ({ user }) => {
|
||||
<MainArea>
|
||||
<div className="mx-auto max-w-4xl space-y-4 p-4">
|
||||
<PageTitle>投稿インポート</PageTitle>
|
||||
<FieldError messages={['取込状態が見つかりません.']}/>
|
||||
<div className="text-red-700 dark:text-red-300">取込状態が見つかりません.</div>
|
||||
<Button type="button" onClick={() => navigate ('/posts/import')}>
|
||||
URL リスト入力へ戻る
|
||||
</Button>
|
||||
@@ -242,65 +241,46 @@ const PostImportReviewPage: FC<Props> = ({ user }) => {
|
||||
}
|
||||
|
||||
return (
|
||||
<MainArea>
|
||||
<>
|
||||
<Helmet>
|
||||
<title>{`投稿インポート確認 | ${ SITE_TITLE }`}</title>
|
||||
</Helmet>
|
||||
|
||||
<div className="mx-auto flex max-w-6xl flex-col gap-4 p-4 pb-8">
|
||||
<PageTitle>投稿情報の確認</PageTitle>
|
||||
<MainArea className="min-h-0">
|
||||
<div className="mx-auto max-w-6xl space-y-4 p-4">
|
||||
<PageTitle>投稿情報の確認・編輯</PageTitle>
|
||||
|
||||
<div className="rounded-lg border bg-white p-4 dark:border-neutral-700
|
||||
dark:bg-neutral-900">
|
||||
<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.skipPlanned}/>
|
||||
<SummaryChip label="登録済み" value={counts.created}/>
|
||||
<SummaryChip label="失敗" value={counts.failed}/>
|
||||
<div className="rounded-lg border bg-white p-4 dark:border-neutral-700
|
||||
dark:bg-neutral-900">
|
||||
<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.skipPlanned}/>
|
||||
<SummaryChip label="登録済み" value={counts.created}/>
|
||||
<SummaryChip label="失敗" value={counts.failed}/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="space-y-3">
|
||||
{reviewRows.map (row => (
|
||||
<div key={row.sourceRow} id={`post-import-row-${ row.sourceRow }`}>
|
||||
<PostImportRowSummary
|
||||
row={row}
|
||||
onEdit={() => {
|
||||
setSearchParams ({ edit: String (row.sourceRow) })
|
||||
}}/>
|
||||
</div>))}
|
||||
</div>
|
||||
</div>
|
||||
</MainArea>
|
||||
|
||||
<div className="flex-1 space-y-3 pb-24">
|
||||
{reviewRows.map (row => (
|
||||
<div key={row.sourceRow} id={`post-import-row-${ row.sourceRow }`}>
|
||||
<PostImportRowSummary
|
||||
row={row}
|
||||
onEdit={() => {
|
||||
setSearchParams ({ edit: String (row.sourceRow) })
|
||||
}}/>
|
||||
</div>))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div
|
||||
className="sticky bottom-0 border-t bg-white/95 p-4 backdrop-blur
|
||||
dark:border-neutral-700 dark:bg-neutral-950/95">
|
||||
<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>登録対象 {submittable.length} 件</span>
|
||||
<PostImportStatusBadge value="error"/>
|
||||
<span>要修正 {counts.invalid} 件</span>
|
||||
</div>
|
||||
<div className="flex flex-col gap-2 sm:flex-row">
|
||||
<Button
|
||||
type="button"
|
||||
variant="outline"
|
||||
onClick={() => navigate ('/posts/import')}>
|
||||
URL リスト入力へ戻る
|
||||
</Button>
|
||||
<Button
|
||||
type="button"
|
||||
onClick={submit}
|
||||
disabled={loading || submittable.length === 0}>
|
||||
有効な投稿を一括登録
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<PostImportFooter
|
||||
loading={loading}
|
||||
invalidCount={counts.invalid}
|
||||
submittableCount={submittable.length}
|
||||
onBack={() => navigate ('/posts/import')}
|
||||
onSubmit={submit}/>
|
||||
|
||||
<PostImportRowDialog
|
||||
open={editingRow != null}
|
||||
@@ -311,9 +291,42 @@ const PostImportReviewPage: FC<Props> = ({ user }) => {
|
||||
setSearchParams ({ })
|
||||
}}
|
||||
onSave={saveDraft}/>
|
||||
</MainArea>)
|
||||
</>)
|
||||
}
|
||||
|
||||
const PostImportFooter = (
|
||||
{ loading, invalidCount, submittableCount, onBack, onSubmit }: {
|
||||
loading: boolean
|
||||
invalidCount: number
|
||||
submittableCount: number
|
||||
onBack: () => void
|
||||
onSubmit: () => void },
|
||||
) => (
|
||||
<div
|
||||
className="shrink-0 border-t bg-white/95 p-4 backdrop-blur
|
||||
dark:border-neutral-700 dark:bg-neutral-950/95">
|
||||
<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>登録対象 {submittableCount} 件</span>
|
||||
<PostImportStatusBadge value="error"/>
|
||||
<span>要修正 {invalidCount} 件</span>
|
||||
</div>
|
||||
<div className="flex flex-col gap-2 sm:flex-row">
|
||||
<Button type="button" variant="outline" onClick={onBack}>
|
||||
URL リスト入力へ戻る
|
||||
</Button>
|
||||
<Button
|
||||
type="button"
|
||||
onClick={onSubmit}
|
||||
disabled={loading || submittableCount === 0}>
|
||||
有効な投稿を一括登録
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>)
|
||||
|
||||
const SummaryChip = ({ label, value }: { label: string
|
||||
value: number }) => (
|
||||
<div className="rounded-full border border-border bg-background px-3 py-1 text-sm">
|
||||
|
||||
@@ -39,15 +39,12 @@ const PostImportSourcePage: FC<Props> = ({ user }) => {
|
||||
|
||||
const [source, setSource] = useState (draft.source)
|
||||
const [loading, setLoading] = useState (false)
|
||||
const [error, setError] = useState<string | null> (null)
|
||||
const [touched, setTouched] = useState (false)
|
||||
const [sourceIssues, setSourceIssues] = useState<ReturnType<typeof validateImportSource>> ([])
|
||||
const [sourceError, setSourceError] = useState<string | null> (null)
|
||||
const saveTimer = useRef<number | null> (null)
|
||||
|
||||
const lineCount = countImportSourceLines (source)
|
||||
const issues = useMemo (() => validateImportSource (source), [source])
|
||||
const messages = [
|
||||
...(((touched) && lineCount === 0) ? ['URL を入力してください.'] : []),
|
||||
...(error ? [error] : [])]
|
||||
const messages = sourceError ? [sourceError] : []
|
||||
|
||||
useEffect (() => {
|
||||
cleanupExpiredPostImportSessions (message =>
|
||||
@@ -68,12 +65,24 @@ const PostImportSourcePage: FC<Props> = ({ user }) => {
|
||||
}, [source])
|
||||
|
||||
const preview = async () => {
|
||||
setTouched (true)
|
||||
if (lineCount === 0 || issues.length > 0)
|
||||
return
|
||||
if (lineCount === 0)
|
||||
{
|
||||
setSourceIssues ([])
|
||||
setSourceError ('URL を入力してください.')
|
||||
return
|
||||
}
|
||||
|
||||
const issues = validateImportSource (source)
|
||||
if (issues.length > 0)
|
||||
{
|
||||
setSourceIssues (issues)
|
||||
setSourceError (null)
|
||||
return
|
||||
}
|
||||
|
||||
setLoading (true)
|
||||
setError (null)
|
||||
setSourceIssues ([])
|
||||
setSourceError (null)
|
||||
try
|
||||
{
|
||||
const data = await apiPost<{ rows: PostImportRow[] }> ('/posts/import/preview', {
|
||||
@@ -95,7 +104,7 @@ const PostImportSourcePage: FC<Props> = ({ user }) => {
|
||||
? requestError.response?.data?.message
|
||||
?? requestError.response?.data?.baseErrors?.[0]
|
||||
: undefined
|
||||
setError (message ?? '入力を確認してください.')
|
||||
setSourceError (message ?? '入力を確認してください.')
|
||||
toast ({
|
||||
title: '投稿情報の取得に失敗しました',
|
||||
description: message ?? '入力を確認してください.' })
|
||||
@@ -126,11 +135,10 @@ const PostImportSourcePage: FC<Props> = ({ user }) => {
|
||||
value={source}
|
||||
rows={14}
|
||||
className={inputClass (
|
||||
messages.length > 0 || issues.length > 0,
|
||||
messages.length > 0 || sourceIssues.length > 0,
|
||||
'font-mono text-sm',
|
||||
)}
|
||||
onBlur={() => {
|
||||
setTouched (true)
|
||||
savePostImportSourceDraft (source, message =>
|
||||
toast ({
|
||||
title: '入力内容を保存できませんでした',
|
||||
@@ -138,12 +146,13 @@ const PostImportSourcePage: FC<Props> = ({ user }) => {
|
||||
}}
|
||||
onChange={ev => {
|
||||
setSource (ev.target.value)
|
||||
setError (null)
|
||||
setSourceError (null)
|
||||
setSourceIssues ([])
|
||||
}}/>)}
|
||||
</FormField>
|
||||
<FieldError messages={messages}/>
|
||||
<div className="space-y-2">
|
||||
{issues.map (issue => (
|
||||
{sourceIssues.map (issue => (
|
||||
<div
|
||||
key={`${ issue.sourceRow }-${ issue.message }-${ issue.url }`}
|
||||
className="rounded-md border border-red-300 bg-red-50 p-3 text-sm
|
||||
@@ -163,8 +172,8 @@ const PostImportSourcePage: FC<Props> = ({ user }) => {
|
||||
<Button
|
||||
type="button"
|
||||
onClick={preview}
|
||||
disabled={loading || lineCount === 0 || issues.length > 0}>
|
||||
投稿情報を取得
|
||||
disabled={loading}>
|
||||
次へ
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
新しい課題から参照
ユーザをブロックする