このコミットが含まれているのは:
2026-07-15 19:33:18 +09:00
コミット eebbd5d069
11個のファイルの変更244行の追加189行の削除
+58 -93
ファイルの表示
@@ -4,7 +4,7 @@ import FieldWarning from '@/components/common/FieldWarning'
import FormField from '@/components/common/FormField'
import PostImportStatusBadge from '@/components/posts/import/PostImportStatusBadge'
import ThumbnailPreview from '@/components/posts/import/ThumbnailPreview'
import { effectivePostImportStatus } from '@/components/posts/import/postImportRowStatus'
import { displayPostImportStatus } from '@/components/posts/import/postImportRowStatus'
import { Button } from '@/components/ui/button'
import { Dialog,
DialogContent,
@@ -17,8 +17,7 @@ import { inputClass } from '@/lib/utils'
import type { FC } from 'react'
import { useEffect, useState } from 'react'
import type { PostImportOrigin,
PostImportRow } from '@/lib/postImportSession'
import type { PostImportRow } from '@/lib/postImportSession'
type Draft = {
url: string
@@ -31,41 +30,12 @@ type Draft = {
parentPostIds: string }
type Props = {
open: boolean
row: PostImportRow | null
saving: boolean
onOpenChange: (open: boolean) => void
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'
}
open: boolean
row: PostImportRow | null
messageRow?: PostImportRow | null
saving: boolean
onOpenChange: (open: boolean) => void
onSave: (draft: Draft) => Promise<boolean> }
const buildDraft = (row: PostImportRow): Draft => ({
url: row.url,
@@ -77,6 +47,16 @@ const buildDraft = (row: PostImportRow): Draft => ({
tags: String (row.attributes.tags ?? ''),
parentPostIds: String (row.attributes.parentPostIds ?? '') })
const buildResetDraft = (row: PostImportRow): Draft => ({
url: row.resetSnapshot.url,
title: String (row.resetSnapshot.attributes.title ?? ''),
thumbnailBase: String (row.resetSnapshot.attributes.thumbnailBase ?? ''),
originalCreatedFrom: String (row.resetSnapshot.attributes.originalCreatedFrom ?? ''),
originalCreatedBefore: String (row.resetSnapshot.attributes.originalCreatedBefore ?? ''),
duration: String (row.resetSnapshot.attributes.duration ?? ''),
tags: String (row.resetSnapshot.attributes.tags ?? ''),
parentPostIds: String (row.resetSnapshot.attributes.parentPostIds ?? '') })
const groupedMessages = (
...values: Array<string[] | undefined>
): string[] =>
@@ -84,7 +64,7 @@ const groupedMessages = (
const PostImportRowDialog: FC<Props> = (
{ open, row, saving, onOpenChange, onSave },
{ open, row, messageRow, saving, onOpenChange, onSave },
) => {
const [draft, setDraft] = useState<Draft | null> (null)
@@ -96,9 +76,7 @@ const PostImportRowDialog: FC<Props> = (
if (row == null || draft == null)
return null
const originalDraft = buildDraft (row)
const fieldOrigin = (field: keyof Draft): PostImportOrigin =>
changedOrigin (draft[field] !== originalDraft[field], row, field)
const displayRow = messageRow ?? row
const update = <Key extends keyof Draft,> (
key: Key,
@@ -131,7 +109,7 @@ const PostImportRowDialog: FC<Props> = (
url={draft.thumbnailBase}
className="h-28 w-28"/>
<div className="flex flex-wrap gap-2">
<PostImportStatusBadge value={effectivePostImportStatus (row)}/>
<PostImportStatusBadge value={displayPostImportStatus (displayRow)}/>
</div>
</div>
@@ -139,79 +117,78 @@ const PostImportRowDialog: FC<Props> = (
<DialogTextField
label="URL"
value={draft.url}
origin={fieldOrigin ('url')}
warnings={row.fieldWarnings.url}
errors={groupedMessages (row.validationErrors.url, row.importErrors?.url)}
warnings={displayRow.fieldWarnings.url}
errors={groupedMessages (displayRow.validationErrors.url,
displayRow.importErrors?.url)}
onChange={value => update ('url', value)}/>
<DialogTextField
label="タイトル"
value={draft.title}
origin={fieldOrigin ('title')}
warnings={row.fieldWarnings.title}
errors={groupedMessages (row.validationErrors.title, row.importErrors?.title)}
warnings={displayRow.fieldWarnings.title}
errors={groupedMessages (displayRow.validationErrors.title,
displayRow.importErrors?.title)}
onChange={value => update ('title', value)}/>
<DialogTextField
label="サムネール基底 URL"
value={draft.thumbnailBase}
origin={fieldOrigin ('thumbnailBase')}
warnings={row.fieldWarnings.thumbnailBase}
warnings={displayRow.fieldWarnings.thumbnailBase}
errors={groupedMessages (
row.validationErrors.thumbnailBase,
row.importErrors?.thumbnailBase,
displayRow.validationErrors.thumbnailBase,
displayRow.importErrors?.thumbnailBase,
)}
onChange={value => update ('thumbnailBase', value)}/>
<PostOriginalCreatedTimeField
labelAddon={
<PostImportStatusBadge
value={originalCreatedOrigin (row, originalDraft, draft)}/>
}
originalCreatedFrom={draft.originalCreatedFrom || null}
setOriginalCreatedFrom={value => update ('originalCreatedFrom', value ?? '')}
originalCreatedBefore={draft.originalCreatedBefore || null}
setOriginalCreatedBefore={value => update ('originalCreatedBefore', value ?? '')}
errors={groupedMessages (
row.validationErrors.originalCreatedAt,
row.validationErrors.originalCreatedFrom,
row.validationErrors.originalCreatedBefore,
row.importErrors?.originalCreatedAt,
row.importErrors?.originalCreatedFrom,
row.importErrors?.originalCreatedBefore,
displayRow.validationErrors.originalCreatedAt,
displayRow.validationErrors.originalCreatedFrom,
displayRow.validationErrors.originalCreatedBefore,
displayRow.importErrors?.originalCreatedAt,
displayRow.importErrors?.originalCreatedFrom,
displayRow.importErrors?.originalCreatedBefore,
)}/>
<DialogTextField
label="動画時間"
value={draft.duration}
origin={fieldOrigin ('duration')}
errors={groupedMessages (
row.validationErrors.duration,
row.validationErrors.videoMs,
row.importErrors?.duration,
row.importErrors?.videoMs,
displayRow.validationErrors.duration,
displayRow.validationErrors.videoMs,
displayRow.importErrors?.duration,
displayRow.importErrors?.videoMs,
)}
onChange={value => update ('duration', value)}/>
<DialogAreaField
label="タグ"
value={draft.tags}
origin={fieldOrigin ('tags')}
warnings={row.fieldWarnings.tags}
errors={groupedMessages (row.validationErrors.tags, row.importErrors?.tags)}
warnings={displayRow.fieldWarnings.tags}
errors={groupedMessages (displayRow.validationErrors.tags,
displayRow.importErrors?.tags)}
onChange={value => update ('tags', value)}/>
<DialogTextField
label="親投稿"
value={draft.parentPostIds}
origin={fieldOrigin ('parentPostIds')}
errors={groupedMessages (
row.validationErrors.parentPostIds,
row.importErrors?.parentPostIds,
displayRow.validationErrors.parentPostIds,
displayRow.importErrors?.parentPostIds,
)}
onChange={value => update ('parentPostIds', value)}/>
<FieldWarning messages={row.baseWarnings}/>
<FieldError messages={row.validationErrors.base}/>
<FieldError messages={row.importErrors?.base}/>
<FieldWarning messages={displayRow.baseWarnings}/>
<FieldError messages={displayRow.validationErrors.base}/>
<FieldError messages={displayRow.importErrors?.base}/>
</div>
</div>
</div>
<DialogFooter className="shrink-0 px-6 pb-6 pt-4">
<Button
type="button"
variant="outline"
onClick={() => setDraft (buildResetDraft (row))}>
</Button>
<Button
type="button"
variant="outline"
@@ -230,15 +207,14 @@ const PostImportRowDialog: FC<Props> = (
}
const DialogTextField = (
{ label, value, origin, warnings, errors, onChange }: {
{ label, value, warnings, errors, onChange }: {
label: string
value: string
origin: PostImportOrigin
warnings?: string[]
errors?: string[]
onChange: (value: string) => void },
) => (
<FormField label={<DialogLabel label={label} origin={origin}/>} messages={errors}>
<FormField label={label} messages={errors}>
{({ describedBy, invalid }) => (
<>
<input
@@ -252,15 +228,14 @@ const DialogTextField = (
</FormField>)
const DialogAreaField = (
{ label, value, origin, warnings, errors, onChange }: {
{ label, value, warnings, errors, onChange }: {
label: string
value: string
origin: PostImportOrigin
warnings?: string[]
errors?: string[]
onChange: (value: string) => void },
) => (
<FormField label={<DialogLabel label={label} origin={origin}/>} messages={errors}>
<FormField label={label} messages={errors}>
{({ describedBy, invalid }) => (
<>
<textarea
@@ -274,14 +249,4 @@ const DialogAreaField = (
</>)}
</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
+5 -14
ファイルの表示
@@ -3,7 +3,7 @@ import { cn } from '@/lib/utils'
import PostImportStatusBadge from '@/components/posts/import/PostImportStatusBadge'
import ThumbnailPreview from '@/components/posts/import/ThumbnailPreview'
import { effectivePostImportStatus } from '@/components/posts/import/postImportRowStatus'
import { displayPostImportStatus } from '@/components/posts/import/postImportRowStatus'
import type { FC } from 'react'
@@ -14,22 +14,13 @@ type Props = {
onEdit: () => void }
const toneClass = (row: PostImportRow): string[] => {
const value = effectivePostImportStatus (row)
const value = displayPostImportStatus (row)
switch (value)
{
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':
case 'error':
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',
@@ -63,7 +54,7 @@ const summaryDate = (row: PostImportRow): string =>
const PostImportRowSummary: FC<Props> = ({ row, onEdit }) => {
const error = summaryError (row)
const warning = error == null ? summaryWarning (row) : null
const effectiveStatus = effectivePostImportStatus (row)
const displayStatus = displayPostImportStatus (row)
return (
<>
@@ -104,7 +95,7 @@ const PostImportRowSummary: FC<Props> = ({ row, onEdit }) => {
</div>)}
</div>
<div className="space-y-1">
<PostImportStatusBadge value={effectiveStatus}/>
<PostImportStatusBadge value={displayStatus}/>
</div>
<div className="flex justify-end">
<Button
@@ -134,7 +125,7 @@ const PostImportRowSummary: FC<Props> = ({ row, onEdit }) => {
{row.url}
</div>
<div className="flex flex-wrap gap-2">
<PostImportStatusBadge value={effectiveStatus}/>
<PostImportStatusBadge value={displayStatus}/>
</div>
{error && (
<div className="text-xs text-red-700 dark:text-red-200">
+2 -26
ファイルの表示
@@ -10,13 +10,7 @@ type Props = {
const LABELS: Record<PostImportBadgeValue, string> = {
ready: '登録可能',
warning: '警告',
error: '要修正',
pending: '未登録',
created: '登録済み',
skipped: 'スキップ',
failed: '失敗',
automatic: '自動取得',
manual: '手修正' }
skipped: 'スキップ' }
const STYLES: Record<PostImportBadgeValue, string[]> = {
ready: [
@@ -25,27 +19,9 @@ const STYLES: Record<PostImportBadgeValue, string[]> = {
warning: [
'border-amber-300 bg-amber-50 text-amber-700',
'dark:border-amber-900 dark:bg-amber-950 dark:text-amber-200'],
error: [
'border-red-300 bg-red-50 text-red-700',
'dark:border-red-900 dark:bg-red-950 dark:text-red-200'],
pending: [
'border-slate-300 bg-slate-50 text-slate-700',
'dark:border-slate-700 dark:bg-slate-900 dark:text-slate-200'],
created: [
'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'],
failed: [
'border-rose-300 bg-rose-50 text-rose-700',
'dark:border-rose-900 dark:bg-rose-950 dark:text-rose-200'],
automatic: [
'border-slate-300 bg-slate-50 text-slate-700',
'dark:border-slate-700 dark:bg-slate-900 dark:text-slate-200'],
manual: [
'border-purple-300 bg-purple-50 text-purple-700',
'dark:border-purple-900 dark:bg-purple-950 dark:text-purple-200'] }
'dark:border-stone-700 dark:bg-stone-900 dark:text-stone-200'] }
const PostImportStatusBadge: FC<Props> = ({ value }) => (
+13 -28
ファイルの表示
@@ -1,33 +1,18 @@
import type { PostImportOrigin,
PostImportRow } from '@/lib/postImportSession'
import type { PostImportRow } from '@/lib/postImportSession'
export type PostImportEffectiveStatus =
'created'
| 'skipped'
| 'failed'
| 'error'
| 'warning'
| 'ready'
export type PostImportDisplayStatus = 'ready' | 'warning' | 'skipped'
export type PostImportBadgeValue =
PostImportEffectiveStatus
| 'pending'
| PostImportOrigin
export type PostImportBadgeValue = PostImportDisplayStatus
const hasWarnings = (row: PostImportRow): boolean =>
Object.values (row.fieldWarnings ?? { }).some (_1 => _1.length > 0)
|| row.baseWarnings.length > 0
export const effectivePostImportStatus = (
export const displayPostImportStatus = (
row: PostImportRow,
): PostImportEffectiveStatus => {
if (Object.keys (row.validationErrors ?? { }).length > 0)
return 'error'
switch (row.importStatus)
{
case 'created':
case 'skipped':
case 'failed':
return row.importStatus
default:
return row.status
}
}
): PostImportDisplayStatus =>
row.skipReason === 'existing' || row.importStatus === 'skipped'
? 'skipped'
: hasWarnings (row)
? 'warning'
: 'ready'