このコミットが含まれているのは:
2026-07-18 21:19:53 +09:00
コミット 1c906d7432
6個のファイルの変更140行の追加138行の削除
+8 -1
ファイルの表示
@@ -87,7 +87,7 @@ class PostCreatePlan
end end
raise Tag::SectionLiteralParseError.new(raw_name, raw_name) if name.include?('[') || name.include?(']') raise Tag::SectionLiteralParseError.new(raw_name, raw_name) if name.include?('[') || name.include?(']')
[TagName.canonicalise(name).first, category&.to_sym, sections] [resolved_tag_name(name), category&.to_sym, sections]
end end
def build_default_tag_specs direct_tag_specs def build_default_tag_specs direct_tag_specs
@@ -254,4 +254,11 @@ class PostCreatePlan
end end
end end
end end
def resolved_tag_name name
tag_name = TagName.includes(:canonical).find_by(name:)
return name if tag_name.nil?
(tag_name.canonical || tag_name).name
end
end end
+9 -15
ファイルの表示
@@ -3,14 +3,14 @@ require 'timeout'
class PostImportPreviewer class PostImportPreviewer
FIELDS = [ FIELDS = [
'title', 'title',
'thumbnail_base', 'thumbnail_base',
'original_created_from', 'original_created_from',
'original_created_before', 'original_created_before',
'video_ms', 'video_ms',
'duration', 'duration',
'tags', 'tags',
'parent_post_ids'].freeze 'parent_post_ids'].freeze
FETCH_WARNING_FIELDS = ['url', 'title', 'thumbnail_base'].freeze FETCH_WARNING_FIELDS = ['url', 'title', 'thumbnail_base'].freeze
TITLE_FETCH_WARNING = 'タイトルを取得できませんでした.'.freeze TITLE_FETCH_WARNING = 'タイトルを取得できませんでした.'.freeze
THUMBNAIL_FETCH_WARNING = 'サムネールを取得できませんでした.'.freeze THUMBNAIL_FETCH_WARNING = 'サムネールを取得できませんでした.'.freeze
@@ -126,7 +126,6 @@ class PostImportPreviewer
validate_basic_data(attributes, validation_errors) validate_basic_data(attributes, validation_errors)
validate_preview_tags(merged_tags(tag_sources, provenance['tags']), validate_preview_tags(merged_tags(tag_sources, provenance['tags']),
validation_errors, validation_errors,
field_warnings,
known_tags) known_tags)
validate_parents(attributes['parent_post_ids'], validation_errors, existing_parent_ids) validate_parents(attributes['parent_post_ids'], validation_errors, existing_parent_ids)
attributes.delete('url') attributes.delete('url')
@@ -450,7 +449,7 @@ class PostImportPreviewer
[] []
end end
def validate_preview_tags raw, errors, field_warnings, known_tags def validate_preview_tags raw, errors, known_tags
names = raw.to_s.split names = raw.to_s.split
return if names.empty? return if names.empty?
if names.any? { _1.downcase.start_with?('nico:') } if names.any? { _1.downcase.start_with?('nico:') }
@@ -462,11 +461,6 @@ class PostImportPreviewer
existing = parsed.filter_map { known_tags[_1] } existing = parsed.filter_map { known_tags[_1] }
deprecated = existing.select(&:deprecated?).map(&:name) deprecated = existing.select(&:deprecated?).map(&:name)
errors[:tags] = ["廃止済みタグがあります: #{ deprecated.join(' ') }"] if deprecated.present? errors[:tags] = ["廃止済みタグがあります: #{ deprecated.join(' ') }"] if deprecated.present?
known = existing.reject(&:deprecated?).map(&:name)
new_tags = parsed.uniq - known
if new_tags.present?
add_field_warning!(field_warnings, 'tags', "新規タグを作成します: #{ new_tags.join(' ') }")
end
rescue Tag::SectionLiteralParseError rescue Tag::SectionLiteralParseError
errors[:tags] = ['タグ区間の記法が不正です.'] errors[:tags] = ['タグ区間の記法が不正です.']
end end
+119 -119
ファイルの表示
@@ -16,10 +16,10 @@ import type { FC } from 'react'
import type { PostImportRow } from '@/lib/postImportTypes' import type { PostImportRow } from '@/lib/postImportTypes'
type Props = { type Props = {
row: PostImportRow row: PostImportRow
displayNumber?: number displayNumber?: number
onEdit?: () => void onEdit?: () => void
onRetry?: () => void onRetry?: () => void
onToggleSkip?: (checked: boolean) => void onToggleSkip?: (checked: boolean) => void
rowMessages?: string[] rowMessages?: string[]
editDisabled?: boolean editDisabled?: boolean
@@ -75,123 +75,123 @@ const PostImportRowSummary: FC<Props> = (
return ( return (
<> <>
<div <div
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]',
'transition-shadow hover:shadow-sm')}> 'transition-shadow hover:shadow-sm')}>
<div className="space-y-1"> <div className="space-y-1">
<div className="text-sm font-medium">#{rowNumber}</div> <div className="text-sm font-medium">#{rowNumber}</div>
</div> </div>
<PostImportThumbnailPreview <PostImportThumbnailPreview
url={String (row.attributes.thumbnailBase ?? '')} url={String (row.attributes.thumbnailBase ?? '')}
file={row.thumbnailFile} file={row.thumbnailFile}
className="h-16 w-16"/> className="h-16 w-16"/>
<div className="min-w-0 space-y-1"> <div className="min-w-0 space-y-1">
<div className="line-clamp-2 text-sm font-medium"> <div className="line-clamp-2 text-sm font-medium">
{String (row.attributes.title ?? '')} {String (row.attributes.title ?? '')}
</div> </div>
<div className="truncate text-xs text-neutral-600 dark:text-neutral-300"> <div className="truncate text-xs text-neutral-600 dark:text-neutral-300">
{row.url} {row.url}
</div> </div>
<PostImportTagLinks tags={row.displayTags}/> <PostImportTagLinks tags={row.displayTags}/>
<div className="text-xs text-neutral-500 dark:text-neutral-400"> <div className="text-xs text-neutral-500 dark:text-neutral-400">
{summaryDate (row)} {summaryDate (row)}
</div> </div>
{showDuration && ( {showDuration && (
<div className="text-xs text-neutral-500 dark:text-neutral-400"> <div className="text-xs text-neutral-500 dark:text-neutral-400">
{duration} {duration}
</div>)} </div>)}
{warning && ( {warning && (
<div className="text-xs text-amber-700 dark:text-amber-200"> <div className="text-xs text-amber-700 dark:text-amber-200">
{warning} {warning}
</div>)} </div>)}
<FieldError messages={rowMessages}/> <FieldError messages={rowMessages}/>
</div> </div>
<div className="space-y-1"> <div className="space-y-1">
{displayStatus != null && <PostImportStatusBadge value={displayStatus}/>} {displayStatus != null && <PostImportStatusBadge value={displayStatus}/>}
</div> </div>
<div className="flex justify-end"> <div className="flex justify-end">
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
{skipControl} {skipControl}
{showActions && editVisible && ( {showActions && editVisible && (
<Button <Button
type="button" type="button"
variant="outline" variant="outline"
onClick={onEdit} onClick={onEdit}
disabled={editDisabled === true || !(editAllowed)}> disabled={editDisabled === true || !(editAllowed)}>
</Button>)} </Button>)}
{showActions && retryAllowed && ( {showActions && retryAllowed && (
<Button <Button
type="button" type="button"
variant="outline" variant="outline"
onClick={onRetry} onClick={onRetry}
disabled={retryDisabled === true}> disabled={retryDisabled === true}>
</Button>)} </Button>)}
</div> </div>
</div> </div>
</div> </div>
<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',
'transition-shadow hover:shadow-sm')}> 'transition-shadow hover:shadow-sm')}>
<div className="text-sm font-medium">#{rowNumber}</div> <div className="text-sm font-medium">#{rowNumber}</div>
<div className="flex items-start gap-3"> <div className="flex items-start gap-3">
<PostImportThumbnailPreview <PostImportThumbnailPreview
url={String (row.attributes.thumbnailBase ?? '')} url={String (row.attributes.thumbnailBase ?? '')}
file={row.thumbnailFile} file={row.thumbnailFile}
className="h-20 w-20 shrink-0"/> className="h-20 w-20 shrink-0"/>
<div className="min-w-0 flex-1 space-y-2"> <div className="min-w-0 flex-1 space-y-2">
<div className="line-clamp-2 text-sm font-medium"> <div className="line-clamp-2 text-sm font-medium">
{String (row.attributes.title ?? '')} {String (row.attributes.title ?? '')}
</div> </div>
<div className="truncate text-xs text-neutral-600 dark:text-neutral-300"> <div className="truncate text-xs text-neutral-600 dark:text-neutral-300">
{row.url} {row.url}
</div> </div>
<div className="flex flex-wrap gap-2"> <div className="flex flex-wrap gap-2">
{displayStatus != null && <PostImportStatusBadge value={displayStatus}/>} {displayStatus != null && <PostImportStatusBadge value={displayStatus}/>}
</div> </div>
<PostImportTagLinks tags={row.displayTags}/> <PostImportTagLinks tags={row.displayTags}/>
<div className="text-xs text-neutral-500 dark:text-neutral-400"> <div className="text-xs text-neutral-500 dark:text-neutral-400">
{summaryDate (row)} {summaryDate (row)}
</div> </div>
{showDuration && ( {showDuration && (
<div className="text-xs text-neutral-500 dark:text-neutral-400"> <div className="text-xs text-neutral-500 dark:text-neutral-400">
{duration} {duration}
</div>)} </div>)}
{warning && ( {warning && (
<div className="text-xs text-amber-700 dark:text-amber-200"> <div className="text-xs text-amber-700 dark:text-amber-200">
{warning} {warning}
</div>)} </div>)}
<FieldError messages={rowMessages}/> <FieldError messages={rowMessages}/>
{skipControl} {skipControl}
</div> </div>
</div> </div>
{showActions && (editVisible || retryAllowed) && ( {showActions && (editVisible || retryAllowed) && (
<div className="flex flex-col gap-2 md:flex-row"> <div className="flex flex-col gap-2 md:flex-row">
{editVisible && ( {editVisible && (
<Button <Button
type="button" type="button"
className="w-full md:w-auto" className="w-full md:w-auto"
variant="outline" variant="outline"
onClick={onEdit} onClick={onEdit}
disabled={editDisabled === true || !(editAllowed)}> disabled={editDisabled === true || !(editAllowed)}>
</Button>)} </Button>)}
{retryAllowed && ( {retryAllowed && (
<Button <Button
type="button" type="button"
className="w-full md:w-auto" className="w-full md:w-auto"
variant="outline" variant="outline"
onClick={onRetry} onClick={onRetry}
disabled={retryDisabled === true}> disabled={retryDisabled === true}>
</Button>)} </Button>)}
</div>)} </div>)}
</div> </div>
</>) </>)
} }
+2 -1
ファイルの表示
@@ -12,7 +12,7 @@ const PostImportTagLinks: FC<Props> = ({ tags }) => {
return null return null
return ( return (
<div className="flex flex-wrap gap-2"> <div className="flex flex-wrap text-xs gap-x-1">
{tags.map (tag => { {tags.map (tag => {
const key = `${ tag.category }:${ tag.name }:${ tag.sectionLiterals?.join ('|') ?? '' }` const key = `${ tag.category }:${ tag.name }:${ tag.sectionLiterals?.join ('|') ?? '' }`
return ( return (
@@ -21,6 +21,7 @@ const PostImportTagLinks: FC<Props> = ({ tags }) => {
tag={{ tag={{
name: tag.name, name: tag.name,
category: tag.category }} category: tag.category }}
linkFlg={false}
withWiki={false} withWiki={false}
withCount={false}/> withCount={false}/>
{tag.sectionLiterals?.map (literal => ( {tag.sectionLiterals?.map (literal => (
+1 -1
ファイルの表示
@@ -1,5 +1,5 @@
const POST_NEW_REVIEW_PATH_PREFIX = '/posts/new?urls=' const POST_NEW_REVIEW_PATH_PREFIX = '/posts/new?urls='
const MAX_POST_NEW_REVIEW_TARGET_BYTES = 4096 const MAX_POST_NEW_REVIEW_TARGET_BYTES = 6_144
const textEncoder = new TextEncoder () const textEncoder = new TextEncoder ()
+1 -1
ファイルの表示
@@ -96,7 +96,7 @@ export const UnsavedChangesGuardProvider: FC<PropsWithChildren> = ({ children })
try try
{ {
const confirmed = await dialogue.confirm ({ const confirmed = await dialogue.confirm ({
title: '変更破棄してページ移動しますか?', title: '変更破棄してページ移動しますか?',
confirmText: '変更を破棄して移動', confirmText: '変更を破棄して移動',
variant: 'danger' }) variant: 'danger' })
if (!(confirmed)) if (!(confirmed))