このコミットが含まれているのは:
2026-07-15 19:43:39 +09:00
コミット 21d6c1408a
12個のファイルの変更350行の追加218行の削除
+7 -3
ファイルの表示
@@ -1,10 +1,14 @@
import { cn } from '@/lib/utils'
import type { FC, ReactNode } from 'react' import type { FC, ReactNode } from 'react'
type Props = { children: ReactNode } type Props = {
children: ReactNode
className?: string }
const Form: FC<Props> = ({ children }) => ( const Form: FC<Props> = ({ children, className }) => (
<div className="max-w-xl mx-auto p-4 space-y-4"> <div className={cn ('mx-auto max-w-xl space-y-4 p-4', className)}>
{children} {children}
</div>) </div>)
+51
ファイルの表示
@@ -0,0 +1,51 @@
import { cn } from '@/lib/utils'
import type { FC, ReactNode } from 'react'
export type StatusBadgeTone =
'danger'
| 'default'
| 'info'
| 'manual'
| 'ready'
| 'skipped'
| 'warning'
type Props = {
children: ReactNode
tone: StatusBadgeTone }
const TONES: Record<StatusBadgeTone, string[]> = {
default: [
'border-slate-300 bg-slate-50 text-slate-700',
'dark:border-slate-700 dark:bg-slate-900 dark:text-slate-200'],
ready: [
'border-emerald-300 bg-emerald-50 text-emerald-700',
'dark:border-emerald-900 dark:bg-emerald-950 dark:text-emerald-200'],
warning: [
'border-amber-300 bg-amber-50 text-amber-700',
'dark:border-amber-900 dark:bg-amber-950 dark:text-amber-200'],
danger: [
'border-rose-300 bg-rose-50 text-rose-700',
'dark:border-rose-900 dark:bg-rose-950 dark:text-rose-200'],
info: [
'border-sky-300 bg-sky-50 text-sky-700',
'dark:border-sky-900 dark:bg-sky-950 dark:text-sky-200'],
skipped: [
'border-stone-300 bg-stone-50 text-stone-700',
'dark:border-stone-700 dark:bg-stone-900 dark:text-stone-200'],
manual: [
'border-purple-300 bg-purple-50 text-purple-700',
'dark:border-purple-900 dark:bg-purple-950 dark:text-purple-200'] }
const StatusBadge: FC<Props> = ({ children, tone }) => (
<span
className={cn (
'inline-flex items-center rounded-full border px-2.5 py-1 text-xs font-medium',
TONES[tone],
)}>
{children}
</span>)
export default StatusBadge
+22
ファイルの表示
@@ -0,0 +1,22 @@
import type { FC, ReactNode } from 'react'
type Props = {
summary: ReactNode
actions: ReactNode }
const PageActionFooter: FC<Props> = ({ summary, actions }) => (
<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">
{summary}
</div>
<div className="flex flex-col gap-2 sm:flex-row">
{actions}
</div>
</div>
</div>)
export default PageActionFooter
+63 -7
ファイルの表示
@@ -4,7 +4,7 @@ import FieldWarning from '@/components/common/FieldWarning'
import FormField from '@/components/common/FormField' import FormField from '@/components/common/FormField'
import PostImportStatusBadge from '@/components/posts/import/PostImportStatusBadge' import PostImportStatusBadge from '@/components/posts/import/PostImportStatusBadge'
import ThumbnailPreview from '@/components/posts/import/ThumbnailPreview' import ThumbnailPreview from '@/components/posts/import/ThumbnailPreview'
import { displayPostImportStatus } from '@/components/posts/import/postImportRowStatus' import { effectivePostImportStatus } from '@/components/posts/import/postImportRowStatus'
import { Button } from '@/components/ui/button' import { Button } from '@/components/ui/button'
import { Dialog, import { Dialog,
DialogContent, DialogContent,
@@ -17,7 +17,8 @@ import { inputClass } from '@/lib/utils'
import type { FC } from 'react' import type { FC } from 'react'
import { useEffect, useState } from 'react' import { useEffect, useState } from 'react'
import type { PostImportRow } from '@/lib/postImportSession' import type { PostImportOrigin,
PostImportRow } from '@/lib/postImportSession'
type Draft = { type Draft = {
url: string url: string
@@ -37,6 +38,36 @@ type Props = {
onOpenChange: (open: boolean) => void onOpenChange: (open: boolean) => void
onSave: (draft: Draft) => Promise<boolean> } onSave: (draft: Draft) => Promise<boolean> }
const originOf = (
row: PostImportRow,
field: string,
): PostImportOrigin =>
row.provenance[field] ?? 'automatic'
const changedOrigin = (
changed: boolean,
row: PostImportRow,
field: string,
): PostImportOrigin =>
changed ? 'manual' : originOf (row, field)
const originalCreatedOrigin = (
row: PostImportRow,
originalDraft: Draft,
draft: Draft,
): PostImportOrigin => {
const changed =
originalDraft.originalCreatedFrom !== draft.originalCreatedFrom
|| originalDraft.originalCreatedBefore !== draft.originalCreatedBefore
if (changed)
return 'manual'
const manualOrigin =
originOf (row, 'originalCreatedFrom') === 'manual'
|| originOf (row, 'originalCreatedBefore') === 'manual'
return manualOrigin ? 'manual' : 'automatic'
}
const buildDraft = (row: PostImportRow): Draft => ({ const buildDraft = (row: PostImportRow): Draft => ({
url: row.url, url: row.url,
title: String (row.attributes.title ?? ''), title: String (row.attributes.title ?? ''),
@@ -77,6 +108,9 @@ const PostImportRowDialog: FC<Props> = (
return null return null
const displayRow = messageRow ?? row const displayRow = messageRow ?? row
const originalDraft = buildDraft (row)
const fieldOrigin = (field: keyof Draft): PostImportOrigin =>
changedOrigin (draft[field] !== originalDraft[field], row, field)
const update = <Key extends keyof Draft,> ( const update = <Key extends keyof Draft,> (
key: Key, key: Key,
@@ -109,7 +143,7 @@ const PostImportRowDialog: FC<Props> = (
url={draft.thumbnailBase} url={draft.thumbnailBase}
className="h-28 w-28"/> className="h-28 w-28"/>
<div className="flex flex-wrap gap-2"> <div className="flex flex-wrap gap-2">
<PostImportStatusBadge value={displayPostImportStatus (displayRow)}/> <PostImportStatusBadge value={effectivePostImportStatus (displayRow)}/>
</div> </div>
</div> </div>
@@ -117,6 +151,7 @@ const PostImportRowDialog: FC<Props> = (
<DialogTextField <DialogTextField
label="URL" label="URL"
value={draft.url} value={draft.url}
origin={fieldOrigin ('url')}
warnings={displayRow.fieldWarnings.url} warnings={displayRow.fieldWarnings.url}
errors={groupedMessages (displayRow.validationErrors.url, errors={groupedMessages (displayRow.validationErrors.url,
displayRow.importErrors?.url)} displayRow.importErrors?.url)}
@@ -124,6 +159,7 @@ const PostImportRowDialog: FC<Props> = (
<DialogTextField <DialogTextField
label="タイトル" label="タイトル"
value={draft.title} value={draft.title}
origin={fieldOrigin ('title')}
warnings={displayRow.fieldWarnings.title} warnings={displayRow.fieldWarnings.title}
errors={groupedMessages (displayRow.validationErrors.title, errors={groupedMessages (displayRow.validationErrors.title,
displayRow.importErrors?.title)} displayRow.importErrors?.title)}
@@ -131,6 +167,7 @@ const PostImportRowDialog: FC<Props> = (
<DialogTextField <DialogTextField
label="サムネール基底 URL" label="サムネール基底 URL"
value={draft.thumbnailBase} value={draft.thumbnailBase}
origin={fieldOrigin ('thumbnailBase')}
warnings={displayRow.fieldWarnings.thumbnailBase} warnings={displayRow.fieldWarnings.thumbnailBase}
errors={groupedMessages ( errors={groupedMessages (
displayRow.validationErrors.thumbnailBase, displayRow.validationErrors.thumbnailBase,
@@ -138,6 +175,10 @@ const PostImportRowDialog: FC<Props> = (
)} )}
onChange={value => update ('thumbnailBase', value)}/> onChange={value => update ('thumbnailBase', value)}/>
<PostOriginalCreatedTimeField <PostOriginalCreatedTimeField
labelAddon={
<PostImportStatusBadge
value={originalCreatedOrigin (row, originalDraft, draft)}/>
}
originalCreatedFrom={draft.originalCreatedFrom || null} originalCreatedFrom={draft.originalCreatedFrom || null}
setOriginalCreatedFrom={value => update ('originalCreatedFrom', value ?? '')} setOriginalCreatedFrom={value => update ('originalCreatedFrom', value ?? '')}
originalCreatedBefore={draft.originalCreatedBefore || null} originalCreatedBefore={draft.originalCreatedBefore || null}
@@ -153,6 +194,7 @@ const PostImportRowDialog: FC<Props> = (
<DialogTextField <DialogTextField
label="動画時間" label="動画時間"
value={draft.duration} value={draft.duration}
origin={fieldOrigin ('duration')}
errors={groupedMessages ( errors={groupedMessages (
displayRow.validationErrors.duration, displayRow.validationErrors.duration,
displayRow.validationErrors.videoMs, displayRow.validationErrors.videoMs,
@@ -163,6 +205,7 @@ const PostImportRowDialog: FC<Props> = (
<DialogAreaField <DialogAreaField
label="タグ" label="タグ"
value={draft.tags} value={draft.tags}
origin={fieldOrigin ('tags')}
warnings={displayRow.fieldWarnings.tags} warnings={displayRow.fieldWarnings.tags}
errors={groupedMessages (displayRow.validationErrors.tags, errors={groupedMessages (displayRow.validationErrors.tags,
displayRow.importErrors?.tags)} displayRow.importErrors?.tags)}
@@ -170,6 +213,7 @@ const PostImportRowDialog: FC<Props> = (
<DialogTextField <DialogTextField
label="親投稿" label="親投稿"
value={draft.parentPostIds} value={draft.parentPostIds}
origin={fieldOrigin ('parentPostIds')}
errors={groupedMessages ( errors={groupedMessages (
displayRow.validationErrors.parentPostIds, displayRow.validationErrors.parentPostIds,
displayRow.importErrors?.parentPostIds, displayRow.importErrors?.parentPostIds,
@@ -207,14 +251,15 @@ const PostImportRowDialog: FC<Props> = (
} }
const DialogTextField = ( const DialogTextField = (
{ label, value, warnings, errors, onChange }: { { label, value, origin, warnings, errors, onChange }: {
label: string label: string
value: string value: string
origin: PostImportOrigin
warnings?: string[] warnings?: string[]
errors?: string[] errors?: string[]
onChange: (value: string) => void }, onChange: (value: string) => void },
) => ( ) => (
<FormField label={label} messages={errors}> <FormField label={<DialogLabel label={label} origin={origin}/>} messages={errors}>
{({ describedBy, invalid }) => ( {({ describedBy, invalid }) => (
<> <>
<input <input
@@ -228,14 +273,15 @@ const DialogTextField = (
</FormField>) </FormField>)
const DialogAreaField = ( const DialogAreaField = (
{ label, value, warnings, errors, onChange }: { { label, value, origin, warnings, errors, onChange }: {
label: string label: string
value: string value: string
origin: PostImportOrigin
warnings?: string[] warnings?: string[]
errors?: string[] errors?: string[]
onChange: (value: string) => void }, onChange: (value: string) => void },
) => ( ) => (
<FormField label={label} messages={errors}> <FormField label={<DialogLabel label={label} origin={origin}/>} messages={errors}>
{({ describedBy, invalid }) => ( {({ describedBy, invalid }) => (
<> <>
<textarea <textarea
@@ -249,4 +295,14 @@ const DialogAreaField = (
</>)} </>)}
</FormField>) </FormField>)
const DialogLabel = (
{ label, origin }: {
label: string
origin: PostImportOrigin },
) => (
<span className="flex items-center gap-2">
<span>{label}</span>
<PostImportStatusBadge value={origin}/>
</span>)
export default PostImportRowDialog export default PostImportRowDialog
+5 -23
ファイルの表示
@@ -3,7 +3,8 @@ import { cn } from '@/lib/utils'
import PostImportStatusBadge from '@/components/posts/import/PostImportStatusBadge' import PostImportStatusBadge from '@/components/posts/import/PostImportStatusBadge'
import ThumbnailPreview from '@/components/posts/import/ThumbnailPreview' import ThumbnailPreview from '@/components/posts/import/ThumbnailPreview'
import { displayPostImportStatus } from '@/components/posts/import/postImportRowStatus' import { effectivePostImportStatus } from '@/components/posts/import/postImportRowStatus'
import { importPanelToneForRow } from '@/components/posts/import/postImportTone'
import type { FC } from 'react' import type { FC } from 'react'
@@ -13,25 +14,6 @@ type Props = {
row: PostImportRow row: PostImportRow
onEdit: () => void } onEdit: () => void }
const toneClass = (row: PostImportRow): string[] => {
const value = displayPostImportStatus (row)
switch (value)
{
case 'skipped':
return [
'border-stone-200 bg-stone-50',
'dark:border-stone-800 dark:bg-stone-900/60']
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 summaryError = (row: PostImportRow): string | null => { const summaryError = (row: PostImportRow): string | null => {
const validation = Object.values (row.validationErrors ?? { }).flat ()[0] const validation = Object.values (row.validationErrors ?? { }).flat ()[0]
if (validation) if (validation)
@@ -54,7 +36,7 @@ const summaryDate = (row: PostImportRow): string =>
const PostImportRowSummary: FC<Props> = ({ row, onEdit }) => { const PostImportRowSummary: FC<Props> = ({ row, onEdit }) => {
const error = summaryError (row) const error = summaryError (row)
const warning = error == null ? summaryWarning (row) : null const warning = error == null ? summaryWarning (row) : null
const displayStatus = displayPostImportStatus (row) const displayStatus = effectivePostImportStatus (row)
return ( return (
<> <>
@@ -62,7 +44,7 @@ const PostImportRowSummary: FC<Props> = ({ row, onEdit }) => {
className={cn ( className={cn (
'hidden items-center gap-4 rounded-lg border p-4 md:grid', 'hidden items-center gap-4 rounded-lg border p-4 md:grid',
'md:grid-cols-[4rem_5rem_minmax(0,1fr)_auto_auto]', 'md:grid-cols-[4rem_5rem_minmax(0,1fr)_auto_auto]',
toneClass (row), importPanelToneForRow (row),
'transition-shadow hover:shadow-sm', 'transition-shadow hover:shadow-sm',
)}> )}>
<div className="space-y-1"> <div className="space-y-1">
@@ -111,7 +93,7 @@ const PostImportRowSummary: FC<Props> = ({ row, onEdit }) => {
<div <div
className={cn ( className={cn (
'space-y-3 rounded-lg border p-4 md:hidden', 'space-y-3 rounded-lg border p-4 md:hidden',
toneClass (row), importPanelToneForRow (row),
)}> )}>
<div className="flex items-start gap-3"> <div className="flex items-start gap-3">
<ThumbnailPreview <ThumbnailPreview
+21 -20
ファイルの表示
@@ -1,36 +1,37 @@
import { cn } from '@/lib/utils' import StatusBadge from '@/components/common/StatusBadge'
import type { FC } from 'react' import type { FC } from 'react'
import type { StatusBadgeTone } from '@/components/common/StatusBadge'
import type { PostImportBadgeValue } from '@/components/posts/import/postImportRowStatus' import type { PostImportBadgeValue } from '@/components/posts/import/postImportRowStatus'
type Props = { type Props = {
value: PostImportBadgeValue } value: PostImportBadgeValue }
const LABELS: Record<PostImportBadgeValue, string> = { const LABELS: Record<PostImportBadgeValue, string> = {
ready: '登録可能', ready: '登録可能',
warning: '警告', warning: '警告',
skipped: 'スキップ' } error: '要修正',
created: '登録済み',
skipped: 'スキップ',
failed: '失敗',
automatic: '自動取得',
manual: '手修正' }
const STYLES: Record<PostImportBadgeValue, string[]> = { const TONES: Record<PostImportBadgeValue, StatusBadgeTone> = {
ready: [ ready: 'ready',
'border-emerald-300 bg-emerald-50 text-emerald-700', warning: 'warning',
'dark:border-emerald-900 dark:bg-emerald-950 dark:text-emerald-200'], error: 'danger',
warning: [ created: 'info',
'border-amber-300 bg-amber-50 text-amber-700', skipped: 'skipped',
'dark:border-amber-900 dark:bg-amber-950 dark:text-amber-200'], failed: 'danger',
skipped: [ automatic: 'default',
'border-stone-300 bg-stone-50 text-stone-700', manual: 'manual' }
'dark:border-stone-700 dark:bg-stone-900 dark:text-stone-200'] }
const PostImportStatusBadge: FC<Props> = ({ value }) => ( const PostImportStatusBadge: FC<Props> = ({ value }) => (
<span <StatusBadge tone={TONES[value]}>
className={cn (
'inline-flex items-center rounded-full border px-2.5 py-1 text-xs font-medium',
STYLES[value],
)}>
{LABELS[value]} {LABELS[value]}
</span>) </StatusBadge>)
export default PostImportStatusBadge export default PostImportStatusBadge
+19
ファイルの表示
@@ -0,0 +1,19 @@
import PostImportStatusBadge from '@/components/posts/import/PostImportStatusBadge'
import type { FC } from 'react'
import type { PostImportBadgeValue } from '@/components/posts/import/postImportRowStatus'
type Props = {
label: string
value: number
badge?: PostImportBadgeValue }
const PostImportSummaryChip: FC<Props> = ({ label, value, badge }) => (
<div className="flex items-center gap-2 rounded-full border border-border
bg-background px-3 py-1 text-sm">
{badge != null && <PostImportStatusBadge value={badge}/>}
<span>{label} {value}</span>
</div>)
export default PostImportSummaryChip
+28 -9
ファイルの表示
@@ -1,18 +1,37 @@
import type { PostImportRow } from '@/lib/postImportSession' import type { PostImportRow } from '@/lib/postImportSession'
export type PostImportDisplayStatus = 'ready' | 'warning' | 'skipped' export type PostImportEffectiveStatus =
'created'
| 'error'
| 'failed'
| 'ready'
| 'skipped'
| 'warning'
export type PostImportBadgeValue = PostImportDisplayStatus export type PostImportBadgeValue =
PostImportEffectiveStatus
| 'automatic'
| 'manual'
const hasWarnings = (row: PostImportRow): boolean => const hasWarnings = (row: PostImportRow): boolean =>
Object.values (row.fieldWarnings ?? { }).some (_1 => _1.length > 0) Object.values (row.fieldWarnings ?? { }).some (_1 => _1.length > 0)
|| row.baseWarnings.length > 0 || row.baseWarnings.length > 0
export const displayPostImportStatus = ( export const effectivePostImportStatus = (
row: PostImportRow, row: PostImportRow,
): PostImportDisplayStatus => ): PostImportEffectiveStatus => {
row.skipReason === 'existing' || row.importStatus === 'skipped' if (Object.keys (row.validationErrors ?? { }).length > 0 || row.status === 'error')
? 'skipped' return 'error'
: hasWarnings (row)
? 'warning' switch (row.importStatus)
: 'ready' {
case 'created':
case 'skipped':
case 'failed':
return row.importStatus
default:
return hasWarnings (row) || row.status === 'warning'
? 'warning'
: 'ready'
}
}
+36
ファイルの表示
@@ -0,0 +1,36 @@
import { effectivePostImportStatus } from '@/components/posts/import/postImportRowStatus'
import type { PostImportRow } from '@/lib/postImportSession'
export const importPanelToneClass = (
status: 'created' | 'error' | '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 'error':
case 'failed':
case 'invalid':
return [
'border-rose-200 bg-rose-50',
'dark:border-rose-900 dark:bg-rose-950/30']
case 'skipped':
return [
'border-stone-200 bg-stone-50',
'dark:border-stone-800 dark:bg-stone-900/60']
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']
}
}
export const importPanelToneForRow = (row: PostImportRow): string[] =>
importPanelToneClass (effectivePostImportStatus (row))
+14 -64
ファイルの表示
@@ -8,7 +8,9 @@ import PageTitle from '@/components/common/PageTitle'
import PrefetchLink from '@/components/PrefetchLink' import PrefetchLink from '@/components/PrefetchLink'
import MainArea from '@/components/layout/MainArea' import MainArea from '@/components/layout/MainArea'
import PostImportStatusBadge from '@/components/posts/import/PostImportStatusBadge' 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 { Button } from '@/components/ui/button'
import { toast } from '@/components/ui/use-toast' import { toast } from '@/components/ui/use-toast'
import { SITE_TITLE } from '@/config' import { SITE_TITLE } from '@/config'
@@ -33,38 +35,6 @@ import type { User } from '@/types'
type Props = { user: User | null } 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[] => [ const rowMessages = (row: PostImportRow): string[] => [
...Object.values (row.validationErrors ?? { }).flat (), ...Object.values (row.validationErrors ?? { }).flat (),
...Object.values (row.importErrors ?? { }).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 <div className="rounded-lg border bg-white p-4 dark:border-neutral-700
dark:bg-neutral-900"> dark:bg-neutral-900">
<div className="flex flex-wrap gap-2"> <div className="flex flex-wrap gap-2">
<SummaryChip label="登録成功" value={counts.created}/> <PostImportSummaryChip label="登録成功" value={counts.created} badge="created"/>
<SummaryChip label="スキップ" value={counts.skipped} badge="skipped"/> <PostImportSummaryChip label="スキップ" value={counts.skipped} badge="skipped"/>
<SummaryChip label="失敗" value={counts.failed}/> <PostImportSummaryChip label="失敗" value={counts.failed} badge="failed"/>
<SummaryChip label="要修正" value={counts.invalid}/> <PostImportSummaryChip label="要修正" value={counts.invalid} badge="error"/>
</div> </div>
</div> </div>
<div className="space-y-3"> <div className="space-y-3">
{session.rows.map (row => { {session.rows.map (row => {
const displayStatus = const displayStatus = effectivePostImportStatus (row)
Object.keys (row.validationErrors).length > 0
? 'invalid'
: row.importStatus === 'created'
? 'created'
: row.importStatus === 'failed'
? 'failed'
: displayPostImportStatus (row)
const canEdit = const canEdit =
Object.keys (row.validationErrors).length > 0 Object.keys (row.validationErrors).length > 0
|| row.importStatus === 'failed' || row.importStatus === 'failed'
@@ -230,18 +193,17 @@ const PostImportResultPage: FC<Props> = ({ user }) => {
: row.importStatus === 'failed' : row.importStatus === 'failed'
&& Object.keys (row.validationErrors).length === 0 && Object.keys (row.validationErrors).length === 0
return ( return (
<div <div
key={row.sourceRow} key={row.sourceRow}
className={[ className={[
'rounded-lg border p-4', 'rounded-lg border p-4',
...resultToneClass (displayStatus)].join (' ')}> ...importPanelToneClass (displayStatus)].join (' ')}>
<div className="flex flex-col gap-3 md:flex-row md:items-start <div className="flex flex-col gap-3 md:flex-row md:items-start
md:justify-between"> md:justify-between">
<div className="space-y-2"> <div className="space-y-2">
<div className="flex flex-wrap items-center gap-2"> <div className="flex flex-wrap items-center gap-2">
<span className="text-sm font-medium"> {row.sourceRow}</span> <span className="text-sm font-medium"> {row.sourceRow}</span>
{displayStatus === 'skipped' && ( <PostImportStatusBadge value={displayStatus}/>
<PostImportStatusBadge value="skipped"/>)}
</div> </div>
<div className="text-sm text-neutral-700 dark:text-neutral-200"> <div className="text-sm text-neutral-700 dark:text-neutral-200">
{String (row.attributes.title ?? '') || row.url} {String (row.attributes.title ?? '') || row.url}
@@ -311,16 +273,4 @@ const PostImportResultPage: FC<Props> = ({ user }) => {
</MainArea>) </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 export default PostImportResultPage
+32 -36
ファイルの表示
@@ -6,9 +6,11 @@ import { useNavigate,
import PageTitle from '@/components/common/PageTitle' import PageTitle from '@/components/common/PageTitle'
import MainArea from '@/components/layout/MainArea' import MainArea from '@/components/layout/MainArea'
import PageActionFooter from '@/components/posts/import/PageActionFooter'
import PostImportRowDialog from '@/components/posts/import/PostImportRowDialog' import PostImportRowDialog from '@/components/posts/import/PostImportRowDialog'
import PostImportRowSummary from '@/components/posts/import/PostImportRowSummary' import PostImportRowSummary from '@/components/posts/import/PostImportRowSummary'
import PostImportStatusBadge from '@/components/posts/import/PostImportStatusBadge' import PostImportStatusBadge from '@/components/posts/import/PostImportStatusBadge'
import PostImportSummaryChip from '@/components/posts/import/PostImportSummaryChip'
import { Button } from '@/components/ui/button' import { Button } from '@/components/ui/button'
import { toast } from '@/components/ui/use-toast' import { toast } from '@/components/ui/use-toast'
import { SITE_TITLE } from '@/config' 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 <div className="rounded-lg border bg-white p-4 dark:border-neutral-700
dark:bg-neutral-900"> dark:bg-neutral-900">
<div className="flex flex-wrap items-center gap-2"> <div className="flex flex-wrap items-center gap-2">
<SummaryChip label="全件" value={counts.total}/> <PostImportSummaryChip label="全件" value={counts.total}/>
<SummaryChip label="登録対象" value={counts.submittable}/> <PostImportSummaryChip label="登録対象" value={counts.submittable} badge="ready"/>
<SummaryChip label="要確認" value={counts.invalid}/> <PostImportSummaryChip label="要確認" value={counts.invalid} badge="error"/>
<SummaryChip label="スキップ予定" value={counts.skipPlanned}/> <PostImportSummaryChip label="スキップ予定" value={counts.skipPlanned}
badge="skipped"/>
</div> </div>
</div> </div>
@@ -354,37 +357,30 @@ const PostImportFooter = (
onBack: () => void onBack: () => void
onSubmit: () => void }, onSubmit: () => void },
) => ( ) => (
<div <PageActionFooter
className="shrink-0 border-t bg-white/95 p-4 backdrop-blur summary={
dark:border-neutral-700 dark:bg-neutral-950/95"> <>
<div className="mx-auto flex max-w-6xl flex-col gap-3 md:flex-row <span> {processableCount} </span>
md:items-center md:justify-between"> <PostImportStatusBadge value="ready"/>
<div className="flex flex-wrap items-center gap-2 text-sm"> <span> {creatableCount} </span>
<span> {processableCount} </span> <PostImportStatusBadge value="skipped"/>
<PostImportStatusBadge value="ready"/> <span> {skipPlannedCount} </span>
<span> {creatableCount} </span> <PostImportStatusBadge value="error"/>
<PostImportStatusBadge value="skipped"/> <span> {invalidCount} </span>
<span> {skipPlannedCount} </span> </>
<span> {invalidCount} </span> }
</div> actions={
<div className="flex flex-col gap-2 sm:flex-row"> <>
<Button type="button" variant="outline" onClick={onBack}> <Button type="button" variant="outline" onClick={onBack}>
URL URL
</Button> </Button>
<Button <Button
type="button" type="button"
onClick={onSubmit} onClick={onSubmit}
disabled={loading || processableCount === 0}> disabled={loading || processableCount === 0}>
</Button> </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>)
export default PostImportReviewPage export default PostImportReviewPage
+51 -55
ファイルの表示
@@ -3,6 +3,7 @@ import { Helmet } from 'react-helmet-async'
import { useNavigate } from 'react-router-dom' import { useNavigate } from 'react-router-dom'
import FieldError from '@/components/common/FieldError' import FieldError from '@/components/common/FieldError'
import Form from '@/components/common/Form'
import FormField from '@/components/common/FormField' import FormField from '@/components/common/FormField'
import PageTitle from '@/components/common/PageTitle' import PageTitle from '@/components/common/PageTitle'
import MainArea from '@/components/layout/MainArea' import MainArea from '@/components/layout/MainArea'
@@ -150,63 +151,58 @@ const PostImportSourcePage: FC<Props> = ({ user }) => {
<title>{`投稿インポート | ${ SITE_TITLE }`}</title> <title>{`投稿インポート | ${ SITE_TITLE }`}</title>
</Helmet> </Helmet>
<div className="mx-auto max-w-4xl space-y-4 p-4"> <Form className="max-w-4xl p-0">
<PageTitle>稿</PageTitle> <PageTitle>稿</PageTitle>
<div className="rounded-lg border bg-white p-4 dark:border-neutral-700 <FormField label="URL リスト">
dark:bg-neutral-900 md:p-6"> {() => (
<div className="space-y-4"> <textarea
<FormField label="URL リスト"> value={source}
{() => ( rows={14}
<textarea aria-invalid={messages.length > 0 || sourceIssues.length > 0}
value={source} aria-describedby={sourceDescribedBy || undefined}
rows={14} className={inputClass (
aria-invalid={messages.length > 0 || sourceIssues.length > 0} messages.length > 0 || sourceIssues.length > 0,
aria-describedby={sourceDescribedBy || undefined} 'font-mono text-sm',
className={inputClass ( )}
messages.length > 0 || sourceIssues.length > 0, onBlur={() => {
'font-mono text-sm', savePostImportSourceDraft (source, message =>
)} toast ({
onBlur={() => { title: '入力内容を保存できませんでした',
savePostImportSourceDraft (source, message => description: message }))
toast ({ }}
title: '入力内容を保存できませんでした', onChange={ev => {
description: message })) editedRef.current = true
}} setSource (ev.target.value)
onChange={ev => { setSourceError (null)
editedRef.current = true setSourceIssues ([])
setSource (ev.target.value) }}/>)}
setSourceError (null) </FormField>
setSourceIssues ([]) <div id={messages.length > 0 ? SOURCE_ERROR_ID : undefined}>
}}/>)} <FieldError messages={messages}/>
</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>
</div> </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>) </MainArea>)
} }