このコミットが含まれているのは:
@@ -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
|
||||
@@ -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 { displayPostImportStatus } from '@/components/posts/import/postImportRowStatus'
|
||||
import { effectivePostImportStatus } from '@/components/posts/import/postImportRowStatus'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { Dialog,
|
||||
DialogContent,
|
||||
@@ -17,7 +17,8 @@ import { inputClass } from '@/lib/utils'
|
||||
import type { FC } from 'react'
|
||||
import { useEffect, useState } from 'react'
|
||||
|
||||
import type { PostImportRow } from '@/lib/postImportSession'
|
||||
import type { PostImportOrigin,
|
||||
PostImportRow } from '@/lib/postImportSession'
|
||||
|
||||
type Draft = {
|
||||
url: string
|
||||
@@ -37,6 +38,36 @@ type Props = {
|
||||
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'
|
||||
}
|
||||
|
||||
const buildDraft = (row: PostImportRow): Draft => ({
|
||||
url: row.url,
|
||||
title: String (row.attributes.title ?? ''),
|
||||
@@ -77,6 +108,9 @@ const PostImportRowDialog: FC<Props> = (
|
||||
return null
|
||||
|
||||
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,> (
|
||||
key: Key,
|
||||
@@ -109,7 +143,7 @@ const PostImportRowDialog: FC<Props> = (
|
||||
url={draft.thumbnailBase}
|
||||
className="h-28 w-28"/>
|
||||
<div className="flex flex-wrap gap-2">
|
||||
<PostImportStatusBadge value={displayPostImportStatus (displayRow)}/>
|
||||
<PostImportStatusBadge value={effectivePostImportStatus (displayRow)}/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -117,6 +151,7 @@ const PostImportRowDialog: FC<Props> = (
|
||||
<DialogTextField
|
||||
label="URL"
|
||||
value={draft.url}
|
||||
origin={fieldOrigin ('url')}
|
||||
warnings={displayRow.fieldWarnings.url}
|
||||
errors={groupedMessages (displayRow.validationErrors.url,
|
||||
displayRow.importErrors?.url)}
|
||||
@@ -124,6 +159,7 @@ const PostImportRowDialog: FC<Props> = (
|
||||
<DialogTextField
|
||||
label="タイトル"
|
||||
value={draft.title}
|
||||
origin={fieldOrigin ('title')}
|
||||
warnings={displayRow.fieldWarnings.title}
|
||||
errors={groupedMessages (displayRow.validationErrors.title,
|
||||
displayRow.importErrors?.title)}
|
||||
@@ -131,6 +167,7 @@ const PostImportRowDialog: FC<Props> = (
|
||||
<DialogTextField
|
||||
label="サムネール基底 URL"
|
||||
value={draft.thumbnailBase}
|
||||
origin={fieldOrigin ('thumbnailBase')}
|
||||
warnings={displayRow.fieldWarnings.thumbnailBase}
|
||||
errors={groupedMessages (
|
||||
displayRow.validationErrors.thumbnailBase,
|
||||
@@ -138,6 +175,10 @@ const PostImportRowDialog: FC<Props> = (
|
||||
)}
|
||||
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}
|
||||
@@ -153,6 +194,7 @@ const PostImportRowDialog: FC<Props> = (
|
||||
<DialogTextField
|
||||
label="動画時間"
|
||||
value={draft.duration}
|
||||
origin={fieldOrigin ('duration')}
|
||||
errors={groupedMessages (
|
||||
displayRow.validationErrors.duration,
|
||||
displayRow.validationErrors.videoMs,
|
||||
@@ -163,6 +205,7 @@ const PostImportRowDialog: FC<Props> = (
|
||||
<DialogAreaField
|
||||
label="タグ"
|
||||
value={draft.tags}
|
||||
origin={fieldOrigin ('tags')}
|
||||
warnings={displayRow.fieldWarnings.tags}
|
||||
errors={groupedMessages (displayRow.validationErrors.tags,
|
||||
displayRow.importErrors?.tags)}
|
||||
@@ -170,6 +213,7 @@ const PostImportRowDialog: FC<Props> = (
|
||||
<DialogTextField
|
||||
label="親投稿"
|
||||
value={draft.parentPostIds}
|
||||
origin={fieldOrigin ('parentPostIds')}
|
||||
errors={groupedMessages (
|
||||
displayRow.validationErrors.parentPostIds,
|
||||
displayRow.importErrors?.parentPostIds,
|
||||
@@ -207,14 +251,15 @@ const PostImportRowDialog: FC<Props> = (
|
||||
}
|
||||
|
||||
const DialogTextField = (
|
||||
{ label, value, warnings, errors, onChange }: {
|
||||
{ label, value, origin, warnings, errors, onChange }: {
|
||||
label: string
|
||||
value: string
|
||||
origin: PostImportOrigin
|
||||
warnings?: string[]
|
||||
errors?: string[]
|
||||
onChange: (value: string) => void },
|
||||
) => (
|
||||
<FormField label={label} messages={errors}>
|
||||
<FormField label={<DialogLabel label={label} origin={origin}/>} messages={errors}>
|
||||
{({ describedBy, invalid }) => (
|
||||
<>
|
||||
<input
|
||||
@@ -228,14 +273,15 @@ const DialogTextField = (
|
||||
</FormField>)
|
||||
|
||||
const DialogAreaField = (
|
||||
{ label, value, warnings, errors, onChange }: {
|
||||
{ label, value, origin, warnings, errors, onChange }: {
|
||||
label: string
|
||||
value: string
|
||||
origin: PostImportOrigin
|
||||
warnings?: string[]
|
||||
errors?: string[]
|
||||
onChange: (value: string) => void },
|
||||
) => (
|
||||
<FormField label={label} messages={errors}>
|
||||
<FormField label={<DialogLabel label={label} origin={origin}/>} messages={errors}>
|
||||
{({ describedBy, invalid }) => (
|
||||
<>
|
||||
<textarea
|
||||
@@ -249,4 +295,14 @@ 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
|
||||
|
||||
@@ -3,7 +3,8 @@ import { cn } from '@/lib/utils'
|
||||
|
||||
import PostImportStatusBadge from '@/components/posts/import/PostImportStatusBadge'
|
||||
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'
|
||||
|
||||
@@ -13,25 +14,6 @@ type Props = {
|
||||
row: PostImportRow
|
||||
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 validation = Object.values (row.validationErrors ?? { }).flat ()[0]
|
||||
if (validation)
|
||||
@@ -54,7 +36,7 @@ const summaryDate = (row: PostImportRow): string =>
|
||||
const PostImportRowSummary: FC<Props> = ({ row, onEdit }) => {
|
||||
const error = summaryError (row)
|
||||
const warning = error == null ? summaryWarning (row) : null
|
||||
const displayStatus = displayPostImportStatus (row)
|
||||
const displayStatus = effectivePostImportStatus (row)
|
||||
|
||||
return (
|
||||
<>
|
||||
@@ -62,7 +44,7 @@ const PostImportRowSummary: FC<Props> = ({ row, onEdit }) => {
|
||||
className={cn (
|
||||
'hidden items-center gap-4 rounded-lg border p-4 md:grid',
|
||||
'md:grid-cols-[4rem_5rem_minmax(0,1fr)_auto_auto]',
|
||||
toneClass (row),
|
||||
importPanelToneForRow (row),
|
||||
'transition-shadow hover:shadow-sm',
|
||||
)}>
|
||||
<div className="space-y-1">
|
||||
@@ -111,7 +93,7 @@ const PostImportRowSummary: FC<Props> = ({ row, onEdit }) => {
|
||||
<div
|
||||
className={cn (
|
||||
'space-y-3 rounded-lg border p-4 md:hidden',
|
||||
toneClass (row),
|
||||
importPanelToneForRow (row),
|
||||
)}>
|
||||
<div className="flex items-start gap-3">
|
||||
<ThumbnailPreview
|
||||
|
||||
@@ -1,36 +1,37 @@
|
||||
import { cn } from '@/lib/utils'
|
||||
import StatusBadge from '@/components/common/StatusBadge'
|
||||
|
||||
import type { FC } from 'react'
|
||||
|
||||
import type { StatusBadgeTone } from '@/components/common/StatusBadge'
|
||||
import type { PostImportBadgeValue } from '@/components/posts/import/postImportRowStatus'
|
||||
|
||||
type Props = {
|
||||
value: PostImportBadgeValue }
|
||||
|
||||
const LABELS: Record<PostImportBadgeValue, string> = {
|
||||
ready: '登録可能',
|
||||
warning: '警告',
|
||||
skipped: 'スキップ' }
|
||||
ready: '登録可能',
|
||||
warning: '警告',
|
||||
error: '要修正',
|
||||
created: '登録済み',
|
||||
skipped: 'スキップ',
|
||||
failed: '失敗',
|
||||
automatic: '自動取得',
|
||||
manual: '手修正' }
|
||||
|
||||
const STYLES: Record<PostImportBadgeValue, string[]> = {
|
||||
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'],
|
||||
skipped: [
|
||||
'border-stone-300 bg-stone-50 text-stone-700',
|
||||
'dark:border-stone-700 dark:bg-stone-900 dark:text-stone-200'] }
|
||||
const TONES: Record<PostImportBadgeValue, StatusBadgeTone> = {
|
||||
ready: 'ready',
|
||||
warning: 'warning',
|
||||
error: 'danger',
|
||||
created: 'info',
|
||||
skipped: 'skipped',
|
||||
failed: 'danger',
|
||||
automatic: 'default',
|
||||
manual: 'manual' }
|
||||
|
||||
|
||||
const PostImportStatusBadge: FC<Props> = ({ value }) => (
|
||||
<span
|
||||
className={cn (
|
||||
'inline-flex items-center rounded-full border px-2.5 py-1 text-xs font-medium',
|
||||
STYLES[value],
|
||||
)}>
|
||||
<StatusBadge tone={TONES[value]}>
|
||||
{LABELS[value]}
|
||||
</span>)
|
||||
</StatusBadge>)
|
||||
|
||||
export default PostImportStatusBadge
|
||||
|
||||
@@ -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
|
||||
@@ -1,18 +1,37 @@
|
||||
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 =>
|
||||
Object.values (row.fieldWarnings ?? { }).some (_1 => _1.length > 0)
|
||||
|| row.baseWarnings.length > 0
|
||||
|
||||
export const displayPostImportStatus = (
|
||||
export const effectivePostImportStatus = (
|
||||
row: PostImportRow,
|
||||
): PostImportDisplayStatus =>
|
||||
row.skipReason === 'existing' || row.importStatus === 'skipped'
|
||||
? 'skipped'
|
||||
: hasWarnings (row)
|
||||
? 'warning'
|
||||
: 'ready'
|
||||
): PostImportEffectiveStatus => {
|
||||
if (Object.keys (row.validationErrors ?? { }).length > 0 || row.status === 'error')
|
||||
return 'error'
|
||||
|
||||
switch (row.importStatus)
|
||||
{
|
||||
case 'created':
|
||||
case 'skipped':
|
||||
case 'failed':
|
||||
return row.importStatus
|
||||
default:
|
||||
return hasWarnings (row) || row.status === 'warning'
|
||||
? 'warning'
|
||||
: 'ready'
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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))
|
||||
新しい課題から参照
ユーザをブロックする