このコミットが含まれているのは:
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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>
|
||||||
</>)
|
</>)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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,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 ()
|
||||||
|
|
||||||
|
|||||||
@@ -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))
|
||||||
|
|||||||
新しい課題から参照
ユーザをブロックする