このコミットが含まれているのは:
@@ -3,12 +3,8 @@ import { cn } from '@/lib/utils'
|
||||
import type { FC, ReactNode } from 'react'
|
||||
|
||||
export type StatusBadgeTone =
|
||||
'danger'
|
||||
| 'default'
|
||||
| 'info'
|
||||
| 'manual'
|
||||
| 'ready'
|
||||
| 'skipped'
|
||||
'success'
|
||||
| 'neutral'
|
||||
| 'warning'
|
||||
|
||||
type Props = {
|
||||
@@ -16,35 +12,22 @@ type Props = {
|
||||
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: [
|
||||
success: [
|
||||
'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: [
|
||||
neutral: [
|
||||
'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'] }
|
||||
'dark:border-stone-700 dark:bg-stone-900 dark:text-stone-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],
|
||||
)}>
|
||||
TONES[tone])}>
|
||||
{children}
|
||||
</span>)
|
||||
|
||||
|
||||
@@ -70,10 +70,14 @@ const PostImportRowDialog: FC<Props> = (
|
||||
{ open, row, messageRow, saving, onOpenChange, onSave },
|
||||
) => {
|
||||
const [draft, setDraft] = useState<Draft | null> (null)
|
||||
const [resetRequested, setResetRequested] = useState (false)
|
||||
|
||||
useEffect (() => {
|
||||
if (open && row)
|
||||
setDraft (buildDraft (row))
|
||||
{
|
||||
setDraft (buildDraft (row))
|
||||
setResetRequested (false)
|
||||
}
|
||||
}, [open, row])
|
||||
|
||||
if (row == null || draft == null)
|
||||
@@ -90,12 +94,10 @@ const PostImportRowDialog: FC<Props> = (
|
||||
}
|
||||
|
||||
const save = async () => {
|
||||
if (
|
||||
await onSave ({
|
||||
draft,
|
||||
resetRequested: false,
|
||||
resetSnapshot: row.resetSnapshot })
|
||||
)
|
||||
if (await onSave ({
|
||||
draft,
|
||||
resetRequested,
|
||||
resetSnapshot: row.resetSnapshot }))
|
||||
onOpenChange (false)
|
||||
}
|
||||
|
||||
@@ -139,8 +141,7 @@ const PostImportRowDialog: FC<Props> = (
|
||||
warnings={displayRow.fieldWarnings.thumbnailBase}
|
||||
errors={groupedMessages (
|
||||
displayRow.validationErrors.thumbnailBase,
|
||||
displayRow.importErrors?.thumbnailBase,
|
||||
)}
|
||||
displayRow.importErrors?.thumbnailBase)}
|
||||
onChange={value => update ('thumbnailBase', value)}/>
|
||||
<PostOriginalCreatedTimeField
|
||||
originalCreatedFrom={draft.originalCreatedFrom || null}
|
||||
@@ -153,8 +154,7 @@ const PostImportRowDialog: FC<Props> = (
|
||||
displayRow.validationErrors.originalCreatedBefore,
|
||||
displayRow.importErrors?.originalCreatedAt,
|
||||
displayRow.importErrors?.originalCreatedFrom,
|
||||
displayRow.importErrors?.originalCreatedBefore,
|
||||
)}/>
|
||||
displayRow.importErrors?.originalCreatedBefore)}/>
|
||||
<DialogTextField
|
||||
label="動画時間"
|
||||
value={draft.duration}
|
||||
@@ -162,8 +162,7 @@ const PostImportRowDialog: FC<Props> = (
|
||||
displayRow.validationErrors.duration,
|
||||
displayRow.validationErrors.videoMs,
|
||||
displayRow.importErrors?.duration,
|
||||
displayRow.importErrors?.videoMs,
|
||||
)}
|
||||
displayRow.importErrors?.videoMs)}
|
||||
onChange={value => update ('duration', value)}/>
|
||||
<DialogAreaField
|
||||
label="タグ"
|
||||
@@ -177,8 +176,7 @@ const PostImportRowDialog: FC<Props> = (
|
||||
value={draft.parentPostIds}
|
||||
errors={groupedMessages (
|
||||
displayRow.validationErrors.parentPostIds,
|
||||
displayRow.importErrors?.parentPostIds,
|
||||
)}
|
||||
displayRow.importErrors?.parentPostIds)}
|
||||
onChange={value => update ('parentPostIds', value)}/>
|
||||
<FieldWarning messages={displayRow.baseWarnings}/>
|
||||
<FieldError messages={displayRow.validationErrors.base}/>
|
||||
@@ -191,14 +189,9 @@ const PostImportRowDialog: FC<Props> = (
|
||||
<Button
|
||||
type="button"
|
||||
variant="outline"
|
||||
onClick={async () => {
|
||||
const resetDraft = buildResetDraft (row)
|
||||
setDraft (resetDraft)
|
||||
if (await onSave ({
|
||||
draft: resetDraft,
|
||||
resetRequested: true,
|
||||
resetSnapshot: row.resetSnapshot }))
|
||||
onOpenChange (false)
|
||||
onClick={() => {
|
||||
setDraft (buildResetDraft (row))
|
||||
setResetRequested (true)
|
||||
}}>
|
||||
変更をリセット
|
||||
</Button>
|
||||
|
||||
@@ -4,7 +4,6 @@ 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 { importPanelToneForRow } from '@/components/posts/import/postImportTone'
|
||||
|
||||
import type { FC } from 'react'
|
||||
|
||||
@@ -35,9 +34,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]',
|
||||
importPanelToneForRow (row),
|
||||
'transition-shadow hover:shadow-sm',
|
||||
)}>
|
||||
'transition-shadow hover:shadow-sm')}>
|
||||
<div className="space-y-1">
|
||||
<div className="text-sm font-medium">#{row.sourceRow}</div>
|
||||
</div>
|
||||
@@ -80,8 +77,7 @@ const PostImportRowSummary: FC<Props> = ({ row, onEdit }) => {
|
||||
<div
|
||||
className={cn (
|
||||
'space-y-3 rounded-lg border p-4 md:hidden',
|
||||
importPanelToneForRow (row),
|
||||
)}>
|
||||
'transition-shadow hover:shadow-sm')}>
|
||||
<div className="flex items-start gap-3">
|
||||
<ThumbnailPreview
|
||||
url={String (row.attributes.thumbnailBase ?? '')}
|
||||
|
||||
@@ -14,9 +14,9 @@ const LABELS: Record<PostImportBadgeValue, string> = {
|
||||
skipped: 'スキップ' }
|
||||
|
||||
const TONES: Record<PostImportBadgeValue, StatusBadgeTone> = {
|
||||
ready: 'ready',
|
||||
ready: 'success',
|
||||
warning: 'warning',
|
||||
skipped: 'skipped' }
|
||||
skipped: 'neutral' }
|
||||
|
||||
|
||||
const PostImportStatusBadge: FC<Props> = ({ value }) => (
|
||||
|
||||
@@ -1,19 +0,0 @@
|
||||
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
|
||||
@@ -10,20 +10,13 @@ const hasWarnings = (row: PostImportRow): boolean =>
|
||||
|
||||
export const displayPostImportStatus = (
|
||||
row: PostImportRow,
|
||||
): PostImportDisplayStatus | null => {
|
||||
const skipped =
|
||||
row.skipReason === 'existing'
|
||||
|| row.importStatus === 'skipped'
|
||||
const hidden =
|
||||
Object.keys (row.validationErrors ?? { }).length > 0
|
||||
): PostImportDisplayStatus | null =>
|
||||
(row.skipReason === 'existing' || row.importStatus === 'skipped')
|
||||
? 'skipped'
|
||||
: ((Object.keys (row.validationErrors ?? { }).length > 0
|
||||
|| row.importStatus === 'failed'
|
||||
|| row.importStatus === 'created'
|
||||
|
||||
if (skipped)
|
||||
return 'skipped'
|
||||
if (hidden)
|
||||
return null
|
||||
return hasWarnings (row) || row.status === 'warning'
|
||||
? 'warning'
|
||||
: 'ready'
|
||||
}
|
||||
|| row.importStatus === 'created')
|
||||
? null
|
||||
: ((hasWarnings (row) || row.status === 'warning')
|
||||
? 'warning'
|
||||
: 'ready'))
|
||||
|
||||
@@ -1,36 +0,0 @@
|
||||
import { displayPostImportStatus } 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 (displayPostImportStatus (row) ?? 'ready')
|
||||
@@ -9,8 +9,6 @@ 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 { 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'
|
||||
@@ -71,7 +69,7 @@ const PostImportResultPage: FC<Props> = ({ user }) => {
|
||||
[session])
|
||||
|
||||
const retry = async (sourceRow: number) => {
|
||||
if (session == null)
|
||||
if (session == null || sessionId == null)
|
||||
return
|
||||
|
||||
setLoadingRow (sourceRow)
|
||||
@@ -177,13 +175,8 @@ const PostImportResultPage: FC<Props> = ({ user }) => {
|
||||
<div className="mx-auto max-w-5xl 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 gap-2">
|
||||
<PostImportSummaryChip label="登録成功" value={counts.created}/>
|
||||
<PostImportSummaryChip label="スキップ" value={counts.skipped} badge="skipped"/>
|
||||
<PostImportSummaryChip label="失敗" value={counts.failed}/>
|
||||
</div>
|
||||
<div className="text-sm text-neutral-700 dark:text-neutral-200">
|
||||
登録成功 {counts.created}件 スキップ {counts.skipped}件 失敗 {counts.failed}件
|
||||
</div>
|
||||
|
||||
<div className="space-y-3">
|
||||
@@ -193,18 +186,10 @@ const PostImportResultPage: FC<Props> = ({ user }) => {
|
||||
const canRetry =
|
||||
row.importStatus === 'failed'
|
||||
&& Object.keys (row.validationErrors).length === 0
|
||||
const panelStatus =
|
||||
row.importStatus === 'created'
|
||||
? 'created'
|
||||
: (row.importStatus === 'failed'
|
||||
? 'failed'
|
||||
: displayStatus ?? 'ready')
|
||||
return (
|
||||
<div
|
||||
key={row.sourceRow}
|
||||
className={[
|
||||
'rounded-lg border p-4',
|
||||
...importPanelToneClass (panelStatus)].join (' ')}>
|
||||
className="rounded-lg border p-4 transition-shadow hover:shadow-sm">
|
||||
<div className="flex flex-col gap-3 md:flex-row md:items-start
|
||||
md:justify-between">
|
||||
<div className="space-y-2">
|
||||
|
||||
@@ -8,8 +8,6 @@ import PageTitle from '@/components/common/PageTitle'
|
||||
import MainArea from '@/components/layout/MainArea'
|
||||
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'
|
||||
@@ -130,8 +128,7 @@ const PostImportReviewPage: FC<Props> = ({ user }) => {
|
||||
if (editingRow == null)
|
||||
return false
|
||||
|
||||
const urlChanged = draft.url !== editingRow.url
|
||||
const nextRow =
|
||||
const baseRow =
|
||||
resetRequested
|
||||
? {
|
||||
...editingRow,
|
||||
@@ -139,10 +136,10 @@ const PostImportReviewPage: FC<Props> = ({ user }) => {
|
||||
attributes: { ...resetSnapshot.attributes },
|
||||
provenance: { ...resetSnapshot.provenance },
|
||||
tagSources: { ...resetSnapshot.tagSources },
|
||||
metadataUrl: resetSnapshot.metadataUrl,
|
||||
importStatus: editingRow.importStatus === 'created' ? 'created' : 'pending',
|
||||
importErrors: undefined }
|
||||
: buildNextEditedRow (editingRow, draft, urlChanged)
|
||||
metadataUrl: resetSnapshot.metadataUrl }
|
||||
: editingRow
|
||||
const urlChanged = draft.url !== baseRow.url
|
||||
const nextRow = buildNextEditedRow (baseRow, draft, urlChanged)
|
||||
|
||||
setSavingRow (editingRow.sourceRow)
|
||||
const nextRows = session.rows.map (row =>
|
||||
@@ -272,16 +269,6 @@ const PostImportReviewPage: FC<Props> = ({ user }) => {
|
||||
<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">
|
||||
<PostImportSummaryChip label="全件" value={counts.total}/>
|
||||
<PostImportSummaryChip label="登録対象" value={counts.submittable} badge="ready"/>
|
||||
<PostImportSummaryChip label="スキップ予定" value={counts.skipPlanned}
|
||||
badge="skipped"/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="space-y-3">
|
||||
{reviewRows.map (row => (
|
||||
<div key={row.sourceRow} id={`post-import-row-${ row.sourceRow }`}>
|
||||
@@ -336,11 +323,9 @@ const PostImportFooter = (
|
||||
<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>処理対象 {processableCount}件</span>
|
||||
<span>登録対象 {creatableCount}件</span>
|
||||
<span>スキップ予定 {skipPlannedCount}件</span>
|
||||
</div>
|
||||
<div className="flex flex-col gap-2 sm:flex-row">
|
||||
<Button type="button" variant="outline" onClick={onBack}>
|
||||
|
||||
@@ -34,12 +34,15 @@ const MAX_ROWS = 100
|
||||
const SOURCE_ERROR_ID = 'post-import-source-error'
|
||||
const SOURCE_ISSUES_ID = 'post-import-source-issues'
|
||||
|
||||
const urlIssuesFromRows = (rows: PostImportRow[]) =>
|
||||
rows.flatMap (row =>
|
||||
const urlIssuesFromRows = (rows: PostImportRow[], source: string) => {
|
||||
const sourceLines = source.split (/\r\n|\n|\r/)
|
||||
|
||||
return rows.flatMap (row =>
|
||||
(row.validationErrors.url ?? []).map (message => ({
|
||||
sourceRow: row.sourceRow,
|
||||
message,
|
||||
url: row.url })))
|
||||
url: sourceLines[row.sourceRow - 1]?.trim () ?? row.url })))
|
||||
}
|
||||
|
||||
|
||||
const PostImportSourcePage: FC<Props> = ({ user }) => {
|
||||
@@ -108,7 +111,7 @@ const PostImportSourcePage: FC<Props> = ({ user }) => {
|
||||
{
|
||||
const data = await apiPost<{ rows: PostImportRow[] }> ('/posts/import/preview', {
|
||||
source })
|
||||
const urlIssues = urlIssuesFromRows (data.rows)
|
||||
const urlIssues = urlIssuesFromRows (data.rows, source)
|
||||
if (urlIssues.length > 0)
|
||||
{
|
||||
setSourceIssues (urlIssues)
|
||||
@@ -151,7 +154,7 @@ const PostImportSourcePage: FC<Props> = ({ user }) => {
|
||||
<title>{`投稿インポート | ${ SITE_TITLE }`}</title>
|
||||
</Helmet>
|
||||
|
||||
<Form className="max-w-4xl p-0">
|
||||
<Form className="max-w-4xl">
|
||||
<PageTitle>投稿インポート</PageTitle>
|
||||
<FormField label="URL リスト">
|
||||
{() => (
|
||||
@@ -162,8 +165,7 @@ const PostImportSourcePage: FC<Props> = ({ user }) => {
|
||||
aria-describedby={sourceDescribedBy || undefined}
|
||||
className={inputClass (
|
||||
messages.length > 0 || sourceIssues.length > 0,
|
||||
'font-mono text-sm',
|
||||
)}
|
||||
'font-mono text-sm')}
|
||||
onBlur={() => {
|
||||
savePostImportSourceDraft (source, message =>
|
||||
toast ({
|
||||
|
||||
@@ -25,13 +25,8 @@ import type { User } from '@/types'
|
||||
type Props = { user: User | null }
|
||||
|
||||
type PostFormField =
|
||||
'url'
|
||||
| 'title'
|
||||
| 'tags'
|
||||
| 'parentPostIds'
|
||||
| 'videoMs'
|
||||
| 'originalCreatedAt'
|
||||
| 'thumbnail'
|
||||
'url' | 'title' | 'tags' | 'parentPostIds'
|
||||
| 'videoMs' | 'originalCreatedAt' | 'thumbnail'
|
||||
|
||||
|
||||
const PostNewPage: FC<Props> = ({ user }) => {
|
||||
@@ -42,8 +37,10 @@ const PostNewPage: FC<Props> = ({ user }) => {
|
||||
const { baseErrors, fieldErrors, clearValidationErrors, applyValidationError } =
|
||||
useValidationErrors<PostFormField> ()
|
||||
|
||||
const [originalCreatedBefore, setOriginalCreatedBefore] = useState<string | null> (null)
|
||||
const [originalCreatedFrom, setOriginalCreatedFrom] = useState<string | null> (null)
|
||||
const [originalCreatedBefore, setOriginalCreatedBefore] =
|
||||
useState<string | null> (null)
|
||||
const [originalCreatedFrom, setOriginalCreatedFrom] =
|
||||
useState<string | null> (null)
|
||||
const [parentPostIds, setParentPostIds] = useState ('')
|
||||
const [tags, setTags] = useState ('')
|
||||
const [duration, setDuration] = useState ('')
|
||||
@@ -56,9 +53,7 @@ const PostNewPage: FC<Props> = ({ user }) => {
|
||||
|
||||
const thumbnailPreviewRef = useRef ('')
|
||||
const videoFlg =
|
||||
useMemo (() =>
|
||||
tags.split (/\s+/).some (
|
||||
tag => tag.replace (/\[.*\]$/, '') === '動画'),
|
||||
useMemo (() => tags.split (/\s+/).some (tag => tag.replace (/\[.*\]$/, '') === '動画'),
|
||||
[tags])
|
||||
|
||||
const handleSubmit = async () => {
|
||||
|
||||
新しい課題から参照
ユーザをブロックする