このコミットが含まれているのは:
2026-07-15 19:43:39 +09:00
コミット 21d6c1408a
12個のファイルの変更350行の追加218行の削除
+14 -64
ファイルの表示
@@ -8,7 +8,9 @@ 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 { displayPostImportStatus } from '@/components/posts/import/postImportRowStatus'
import { effectivePostImportStatus } from '@/components/posts/import/postImportRowStatus'
import { importPanelToneClass } from '@/components/posts/import/postImportTone'
import PostImportSummaryChip from '@/components/posts/import/PostImportSummaryChip'
import { Button } from '@/components/ui/button'
import { toast } from '@/components/ui/use-toast'
import { SITE_TITLE } from '@/config'
@@ -33,38 +35,6 @@ import type { User } from '@/types'
type Props = { user: User | null }
const resultToneClass = (
status: 'created' | 'failed' | 'invalid' | 'skipped' | 'warning' | 'ready',
): string[] => {
switch (status)
{
case 'created':
return [
'border-sky-200 bg-sky-50',
'dark:border-sky-900 dark:bg-sky-950/30']
case 'skipped':
return [
'border-stone-200 bg-stone-50',
'dark:border-stone-800 dark:bg-stone-900/60']
case 'failed':
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']
case 'warning':
return [
'border-amber-200 bg-amber-50',
'dark:border-amber-900 dark:bg-amber-950/30']
default:
return [
'border-border bg-white',
'dark:border-neutral-700 dark:bg-neutral-900']
}
}
const rowMessages = (row: PostImportRow): string[] => [
...Object.values (row.validationErrors ?? { }).flat (),
...Object.values (row.importErrors ?? { }).flat ()]
@@ -204,23 +174,16 @@ 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}/>
<SummaryChip label="スキップ" value={counts.skipped} badge="skipped"/>
<SummaryChip label="失敗" value={counts.failed}/>
<SummaryChip label="要修正" value={counts.invalid}/>
<PostImportSummaryChip label="登録成功" value={counts.created} badge="created"/>
<PostImportSummaryChip label="スキップ" value={counts.skipped} badge="skipped"/>
<PostImportSummaryChip label="失敗" value={counts.failed} badge="failed"/>
<PostImportSummaryChip label="要修正" value={counts.invalid} badge="error"/>
</div>
</div>
<div className="space-y-3">
{session.rows.map (row => {
const displayStatus =
Object.keys (row.validationErrors).length > 0
? 'invalid'
: row.importStatus === 'created'
? 'created'
: row.importStatus === 'failed'
? 'failed'
: displayPostImportStatus (row)
const displayStatus = effectivePostImportStatus (row)
const canEdit =
Object.keys (row.validationErrors).length > 0
|| row.importStatus === 'failed'
@@ -230,18 +193,17 @@ const PostImportResultPage: FC<Props> = ({ user }) => {
: row.importStatus === 'failed'
&& Object.keys (row.validationErrors).length === 0
return (
<div
key={row.sourceRow}
className={[
'rounded-lg border p-4',
...resultToneClass (displayStatus)].join (' ')}>
<div
key={row.sourceRow}
className={[
'rounded-lg border p-4',
...importPanelToneClass (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>
{displayStatus === 'skipped' && (
<PostImportStatusBadge value="skipped"/>)}
<PostImportStatusBadge value={displayStatus}/>
</div>
<div className="text-sm text-neutral-700 dark:text-neutral-200">
{String (row.attributes.title ?? '') || row.url}
@@ -311,16 +273,4 @@ const PostImportResultPage: FC<Props> = ({ user }) => {
</MainArea>)
}
const SummaryChip = (
{ label, value, badge }: {
label: string
value: number
badge?: 'skipped' },
) => (
<div className="flex items-center gap-2 rounded-full border border-border
bg-background px-3 py-1 text-sm">
{badge && <PostImportStatusBadge value={badge}/>}
<span>{label} {value}</span>
</div>)
export default PostImportResultPage
+32 -36
ファイルの表示
@@ -6,9 +6,11 @@ import { useNavigate,
import PageTitle from '@/components/common/PageTitle'
import MainArea from '@/components/layout/MainArea'
import PageActionFooter from '@/components/posts/import/PageActionFooter'
import PostImportRowDialog from '@/components/posts/import/PostImportRowDialog'
import PostImportRowSummary from '@/components/posts/import/PostImportRowSummary'
import PostImportStatusBadge from '@/components/posts/import/PostImportStatusBadge'
import PostImportSummaryChip from '@/components/posts/import/PostImportSummaryChip'
import { Button } from '@/components/ui/button'
import { toast } from '@/components/ui/use-toast'
import { SITE_TITLE } from '@/config'
@@ -296,10 +298,11 @@ const PostImportReviewPage: 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 items-center gap-2">
<SummaryChip label="全件" value={counts.total}/>
<SummaryChip label="登録対象" value={counts.submittable}/>
<SummaryChip label="要確認" value={counts.invalid}/>
<SummaryChip label="スキップ予定" value={counts.skipPlanned}/>
<PostImportSummaryChip label="全件" value={counts.total}/>
<PostImportSummaryChip label="登録対象" value={counts.submittable} badge="ready"/>
<PostImportSummaryChip label="要確認" value={counts.invalid} badge="error"/>
<PostImportSummaryChip label="スキップ予定" value={counts.skipPlanned}
badge="skipped"/>
</div>
</div>
@@ -354,37 +357,30 @@ const PostImportFooter = (
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">
<span> {processableCount} </span>
<PostImportStatusBadge value="ready"/>
<span> {creatableCount} </span>
<PostImportStatusBadge value="skipped"/>
<span> {skipPlannedCount} </span>
<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 || processableCount === 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">
{label} {value}
</div>)
<PageActionFooter
summary={
<>
<span> {processableCount} </span>
<PostImportStatusBadge value="ready"/>
<span> {creatableCount} </span>
<PostImportStatusBadge value="skipped"/>
<span> {skipPlannedCount} </span>
<PostImportStatusBadge value="error"/>
<span> {invalidCount} </span>
</>
}
actions={
<>
<Button type="button" variant="outline" onClick={onBack}>
URL
</Button>
<Button
type="button"
onClick={onSubmit}
disabled={loading || processableCount === 0}>
</Button>
</>
}/>)
export default PostImportReviewPage
+51 -55
ファイルの表示
@@ -3,6 +3,7 @@ import { Helmet } from 'react-helmet-async'
import { useNavigate } from 'react-router-dom'
import FieldError from '@/components/common/FieldError'
import Form from '@/components/common/Form'
import FormField from '@/components/common/FormField'
import PageTitle from '@/components/common/PageTitle'
import MainArea from '@/components/layout/MainArea'
@@ -150,63 +151,58 @@ const PostImportSourcePage: FC<Props> = ({ user }) => {
<title>{`投稿インポート | ${ SITE_TITLE }`}</title>
</Helmet>
<div className="mx-auto max-w-4xl space-y-4 p-4">
<Form className="max-w-4xl p-0">
<PageTitle>稿</PageTitle>
<div className="rounded-lg border bg-white p-4 dark:border-neutral-700
dark:bg-neutral-900 md:p-6">
<div className="space-y-4">
<FormField label="URL リスト">
{() => (
<textarea
value={source}
rows={14}
aria-invalid={messages.length > 0 || sourceIssues.length > 0}
aria-describedby={sourceDescribedBy || undefined}
className={inputClass (
messages.length > 0 || sourceIssues.length > 0,
'font-mono text-sm',
)}
onBlur={() => {
savePostImportSourceDraft (source, message =>
toast ({
title: '入力内容を保存できませんでした',
description: message }))
}}
onChange={ev => {
editedRef.current = true
setSource (ev.target.value)
setSourceError (null)
setSourceIssues ([])
}}/>)}
</FormField>
<div id={messages.length > 0 ? SOURCE_ERROR_ID : undefined}>
<FieldError messages={messages}/>
</div>
<ul
id={SOURCE_ISSUES_ID}
className="space-y-2 text-sm text-red-700 dark:text-red-300">
{sourceIssues.map (issue => (
<li key={`${ issue.sourceRow }-${ issue.message }-${ issue.url }`}>
<div>{issue.sourceRow} : {issue.message}</div>
<div className="break-all font-mono text-xs">{issue.url}</div>
</li>))}
</ul>
<div className="flex items-center justify-between gap-3">
<div className="text-sm text-neutral-600 dark:text-neutral-300">
{lineCount} / {MAX_ROWS}
</div>
<Button
type="button"
className="shrink-0"
onClick={preview}
disabled={loading}>
</Button>
</div>
</div>
<FormField label="URL リスト">
{() => (
<textarea
value={source}
rows={14}
aria-invalid={messages.length > 0 || sourceIssues.length > 0}
aria-describedby={sourceDescribedBy || undefined}
className={inputClass (
messages.length > 0 || sourceIssues.length > 0,
'font-mono text-sm',
)}
onBlur={() => {
savePostImportSourceDraft (source, message =>
toast ({
title: '入力内容を保存できませんでした',
description: message }))
}}
onChange={ev => {
editedRef.current = true
setSource (ev.target.value)
setSourceError (null)
setSourceIssues ([])
}}/>)}
</FormField>
<div id={messages.length > 0 ? SOURCE_ERROR_ID : undefined}>
<FieldError messages={messages}/>
</div>
</div>
<ul
id={SOURCE_ISSUES_ID}
className="space-y-2 text-sm text-red-700 dark:text-red-300">
{sourceIssues.map (issue => (
<li key={`${ issue.sourceRow }-${ issue.message }-${ issue.url }`}>
<div>{issue.sourceRow} : {issue.message}</div>
<div className="break-all font-mono text-xs">{issue.url}</div>
</li>))}
</ul>
<div className="flex items-center justify-between gap-3">
<div className="text-sm text-neutral-600 dark:text-neutral-300">
{lineCount} / {MAX_ROWS}
</div>
<Button
type="button"
className="shrink-0"
onClick={preview}
disabled={loading}>
</Button>
</div>
</Form>
</MainArea>)
}