From d2f1b1ed487e430707ac8c5e90c136ef8cc28e4d Mon Sep 17 00:00:00 2001 From: miteruzo Date: Wed, 15 Jul 2026 20:25:51 +0900 Subject: [PATCH] #399 --- backend/app/controllers/posts_controller.rb | 3 +- backend/app/services/post_import_previewer.rb | 29 ++++++++++---- .../src/components/common/StatusBadge.tsx | 29 +++----------- .../posts/import/PostImportRowDialog.tsx | 39 ++++++++----------- .../posts/import/PostImportRowSummary.tsx | 8 +--- .../posts/import/PostImportStatusBadge.tsx | 4 +- .../posts/import/PostImportSummaryChip.tsx | 19 --------- .../posts/import/postImportRowStatus.ts | 25 +++++------- .../components/posts/import/postImportTone.ts | 36 ----------------- .../src/pages/posts/PostImportResultPage.tsx | 23 ++--------- .../src/pages/posts/PostImportReviewPage.tsx | 31 ++++----------- .../src/pages/posts/PostImportSourcePage.tsx | 16 ++++---- frontend/src/pages/posts/PostNewPage.tsx | 19 ++++----- 13 files changed, 85 insertions(+), 196 deletions(-) delete mode 100644 frontend/src/components/posts/import/PostImportSummaryChip.tsx delete mode 100644 frontend/src/components/posts/import/postImportTone.ts diff --git a/backend/app/controllers/posts_controller.rb b/backend/app/controllers/posts_controller.rb index 5bfbdb1..2ee9d53 100644 --- a/backend/app/controllers/posts_controller.rb +++ b/backend/app/controllers/posts_controller.rb @@ -148,8 +148,7 @@ class PostsController < ApplicationController original_created_from: params[:original_created_from], original_created_before: params[:original_created_before], parent_post_ids: parse_parent_post_ids, - video_ms: params[:video_ms], - duration: params[:duration] }).create! + video_ms: params[:video_ms] }).create! post.reload render json: PostRepr.base(post), status: :created diff --git a/backend/app/services/post_import_previewer.rb b/backend/app/services/post_import_previewer.rb index b572729..c2bd9fc 100644 --- a/backend/app/services/post_import_previewer.rb +++ b/backend/app/services/post_import_previewer.rb @@ -10,7 +10,6 @@ class PostImportPreviewer 'tags', 'parent_post_ids'].freeze FETCH_WARNING_FIELDS = ['url', 'title', 'thumbnail_base'].freeze - EXISTING_SKIP_WARNING = '既存投稿のためスキップします.'.freeze TITLE_FETCH_WARNING = 'タイトルを取得できませんでした.'.freeze THUMBNAIL_FETCH_WARNING = 'サムネールを取得できませんでした.'.freeze METADATA_FETCH_WARNING = 'メタデータを取得できませんでした.'.freeze @@ -91,7 +90,6 @@ class PostImportPreviewer end if validation_errors.blank? && normal_url.present? && existing_post - add_field_warning!(field_warnings, 'url', EXISTING_SKIP_WARNING) attributes['tags'] = merged_tags(tag_sources, provenance['tags']) warnings_present = field_warnings.values.any?(&:present?) || base_warnings.present? return { source_row: row[:source_row], @@ -113,8 +111,12 @@ class PostImportPreviewer if should_fetch clear_fetch_warnings!(field_warnings) metadata = metadata_for(url_for_metadata, metadata_cache) - apply_metadata!(attributes, provenance, tag_sources, metadata[:data]) - apply_fetch_warnings!(field_warnings, metadata[:warnings]) + if metadata[:validation_errors].present? + validation_errors.merge!(metadata[:validation_errors]) + else + apply_metadata!(attributes, provenance, tag_sources, metadata[:data]) + apply_fetch_warnings!(field_warnings, metadata[:warnings]) + end end attributes['url'] = url @@ -200,7 +202,8 @@ class PostImportPreviewer end def fetch_metadata url - return { data: { }, warnings: { 'url' => ['URL が空です.'] } } if url.blank? + return { data: { }, warnings: { 'url' => ['URL が空です.'] }, + validation_errors: { } } if url.blank? data = sanitise_metadata(PostMetadataFetcher.fetch(url).stringify_keys.compact) warnings = { } @@ -208,13 +211,18 @@ class PostImportPreviewer if data['thumbnail_base'].blank? add_field_warning!(warnings, 'thumbnail_base', THUMBNAIL_FETCH_WARNING) end - { data:, warnings: } + { data:, warnings:, validation_errors: { } } + rescue Preview::UrlSafety::UnsafeUrl => e + Rails.logger.info( + "post_import_metadata_fetch_unsafe_url "\ + "#{ { error: e.class.name, message: e.message }.to_json }") + { data: { }, warnings: { }, validation_errors: { url: [e.message] } } rescue Preview::HttpFetcher::FetchFailed, Preview::HttpFetcher::ResponseTooLarge => e Rails.logger.info( "post_import_metadata_fetch_failure "\ "#{ { error: e.class.name, message: e.message }.to_json }") - { data: { }, warnings: { 'url' => [METADATA_FETCH_WARNING] } } + { data: { }, warnings: { 'url' => [METADATA_FETCH_WARNING] }, validation_errors: { } } end def sanitise_metadata metadata @@ -295,11 +303,16 @@ class PostImportPreviewer def safe_fetch_metadata url fetch_metadata(url) + rescue Preview::UrlSafety::UnsafeUrl => e + Rails.logger.info( + "post_import_metadata_fetch_unsafe_url "\ + "#{ { error: e.class.name, message: e.message }.to_json }") + { data: { }, warnings: { }, validation_errors: { url: [e.message] } } rescue StandardError => e Rails.logger.error( "post_import_metadata_fetch_unexpected_failure "\ "#{ { error: e.class.name, message: e.message }.to_json }") - { data: { }, warnings: { 'url' => [METADATA_FETCH_WARNING] } } + { data: { }, warnings: { 'url' => [METADATA_FETCH_WARNING] }, validation_errors: { } } end def preload_known_tags prepared_rows, fetch_metadata, metadata_cache, existing_posts, url_counts diff --git a/frontend/src/components/common/StatusBadge.tsx b/frontend/src/components/common/StatusBadge.tsx index bfc70c2..fd14c46 100644 --- a/frontend/src/components/common/StatusBadge.tsx +++ b/frontend/src/components/common/StatusBadge.tsx @@ -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 = { - 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 = ({ children, tone }) => ( + TONES[tone])}> {children} ) diff --git a/frontend/src/components/posts/import/PostImportRowDialog.tsx b/frontend/src/components/posts/import/PostImportRowDialog.tsx index 4678999..f2f5c8f 100644 --- a/frontend/src/components/posts/import/PostImportRowDialog.tsx +++ b/frontend/src/components/posts/import/PostImportRowDialog.tsx @@ -70,10 +70,14 @@ const PostImportRowDialog: FC = ( { open, row, messageRow, saving, onOpenChange, onSave }, ) => { const [draft, setDraft] = useState (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 = ( } 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 = ( warnings={displayRow.fieldWarnings.thumbnailBase} errors={groupedMessages ( displayRow.validationErrors.thumbnailBase, - displayRow.importErrors?.thumbnailBase, - )} + displayRow.importErrors?.thumbnailBase)} onChange={value => update ('thumbnailBase', value)}/> = ( displayRow.validationErrors.originalCreatedBefore, displayRow.importErrors?.originalCreatedAt, displayRow.importErrors?.originalCreatedFrom, - displayRow.importErrors?.originalCreatedBefore, - )}/> + displayRow.importErrors?.originalCreatedBefore)}/> = ( displayRow.validationErrors.duration, displayRow.validationErrors.videoMs, displayRow.importErrors?.duration, - displayRow.importErrors?.videoMs, - )} + displayRow.importErrors?.videoMs)} onChange={value => update ('duration', value)}/> = ( value={draft.parentPostIds} errors={groupedMessages ( displayRow.validationErrors.parentPostIds, - displayRow.importErrors?.parentPostIds, - )} + displayRow.importErrors?.parentPostIds)} onChange={value => update ('parentPostIds', value)}/> @@ -191,14 +189,9 @@ const PostImportRowDialog: FC = ( diff --git a/frontend/src/components/posts/import/PostImportRowSummary.tsx b/frontend/src/components/posts/import/PostImportRowSummary.tsx index b004a0c..481be17 100644 --- a/frontend/src/components/posts/import/PostImportRowSummary.tsx +++ b/frontend/src/components/posts/import/PostImportRowSummary.tsx @@ -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 = ({ 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')}>
#{row.sourceRow}
@@ -80,8 +77,7 @@ const PostImportRowSummary: FC = ({ row, onEdit }) => {
+ 'transition-shadow hover:shadow-sm')}>
= { skipped: 'スキップ' } const TONES: Record = { - ready: 'ready', + ready: 'success', warning: 'warning', - skipped: 'skipped' } + skipped: 'neutral' } const PostImportStatusBadge: FC = ({ value }) => ( diff --git a/frontend/src/components/posts/import/PostImportSummaryChip.tsx b/frontend/src/components/posts/import/PostImportSummaryChip.tsx deleted file mode 100644 index 0ab2b54..0000000 --- a/frontend/src/components/posts/import/PostImportSummaryChip.tsx +++ /dev/null @@ -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 = ({ label, value, badge }) => ( -
- {badge != null && } - {label} {value} -
) - -export default PostImportSummaryChip diff --git a/frontend/src/components/posts/import/postImportRowStatus.ts b/frontend/src/components/posts/import/postImportRowStatus.ts index de076de..03debd5 100644 --- a/frontend/src/components/posts/import/postImportRowStatus.ts +++ b/frontend/src/components/posts/import/postImportRowStatus.ts @@ -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')) diff --git a/frontend/src/components/posts/import/postImportTone.ts b/frontend/src/components/posts/import/postImportTone.ts deleted file mode 100644 index b2fb730..0000000 --- a/frontend/src/components/posts/import/postImportTone.ts +++ /dev/null @@ -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') diff --git a/frontend/src/pages/posts/PostImportResultPage.tsx b/frontend/src/pages/posts/PostImportResultPage.tsx index b5b7d75..395ba29 100644 --- a/frontend/src/pages/posts/PostImportResultPage.tsx +++ b/frontend/src/pages/posts/PostImportResultPage.tsx @@ -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 = ({ 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 = ({ user }) => {
登録結果 -
-
- - - -
+
+ 登録成功 {counts.created}件 スキップ {counts.skipped}件 失敗 {counts.failed}件
@@ -193,18 +186,10 @@ const PostImportResultPage: FC = ({ 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 (
+ className="rounded-lg border p-4 transition-shadow hover:shadow-sm">
diff --git a/frontend/src/pages/posts/PostImportReviewPage.tsx b/frontend/src/pages/posts/PostImportReviewPage.tsx index 5b145b6..28fac34 100644 --- a/frontend/src/pages/posts/PostImportReviewPage.tsx +++ b/frontend/src/pages/posts/PostImportReviewPage.tsx @@ -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 = ({ 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 = ({ 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 = ({ user }) => {
投稿情報の確認・編輯 -
-
- - - -
-
-
{reviewRows.map (row => (
@@ -336,11 +323,9 @@ const PostImportFooter = (
- 処理対象 {processableCount} 件 - - 登録対象 {creatableCount} 件 - - スキップ予定 {skipPlannedCount} 件 + 処理対象 {processableCount}件 + 登録対象 {creatableCount}件 + スキップ予定 {skipPlannedCount}件