このコミットが含まれているのは:
@@ -115,7 +115,7 @@ class PostsController < ApplicationController
|
||||
render json: PostRepr.base(post, current_user)
|
||||
end
|
||||
|
||||
def preview
|
||||
def metadata
|
||||
return head :unauthorized unless current_user
|
||||
return head :forbidden unless current_user.gte_member?
|
||||
return render_bad_request('URL は必須です.') if params[:url].blank?
|
||||
@@ -128,6 +128,7 @@ class PostsController < ApplicationController
|
||||
provenance: { },
|
||||
tag_sources: { } }],
|
||||
fetch_metadata: true).first
|
||||
return render_validation_error fields: preview[:validation_errors] if preview[:validation_errors].present?
|
||||
|
||||
render json: {
|
||||
url: preview[:url],
|
||||
@@ -140,7 +141,6 @@ class PostsController < ApplicationController
|
||||
video_ms: preview[:attributes]['video_ms'],
|
||||
field_warnings: preview[:field_warnings],
|
||||
base_warnings: preview[:base_warnings],
|
||||
validation_errors: preview[:validation_errors],
|
||||
existing_post: compact_post(preview[:existing_post_id]) }
|
||||
rescue ArgumentError => e
|
||||
render_bad_request e.message
|
||||
@@ -178,6 +178,11 @@ class PostsController < ApplicationController
|
||||
attributes: post_create_attributes,
|
||||
thumbnail: params[:thumbnail]).run
|
||||
return render json: dry_run_json(preflight) if bool?(:dry)
|
||||
if preflight[:existing_post].present?
|
||||
post = Post.new(url: preflight[:url])
|
||||
post.errors.add :url, :taken
|
||||
return render_post_form_record_invalid post
|
||||
end
|
||||
|
||||
post = PostCreator.new(actor: current_user,
|
||||
attributes: post_create_attributes.merge(
|
||||
@@ -191,7 +196,10 @@ class PostsController < ApplicationController
|
||||
:original_created_before,
|
||||
:duration,
|
||||
:video_ms,
|
||||
:normalised_tags,
|
||||
:direct_tag_specs,
|
||||
:default_tag_specs,
|
||||
:snapshot_tag_specs,
|
||||
:post_tag_specs,
|
||||
:tag_sections,
|
||||
:normalised_parent_post_ids).symbolize_keys).merge(
|
||||
thumbnail: params[:thumbnail])).create!
|
||||
|
||||
@@ -12,6 +12,7 @@ class PostBulkCreator
|
||||
|
||||
workers = Array.new(2) do
|
||||
Thread.new do
|
||||
Rails.application.executor.wrap do
|
||||
ActiveRecord::Base.connection_pool.with_connection do
|
||||
actor = User.find(@actor_id)
|
||||
loop do
|
||||
@@ -41,8 +42,19 @@ class PostBulkCreator
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
workers.each(&:join)
|
||||
|
||||
results.each_index do |index|
|
||||
next if results[index].present?
|
||||
|
||||
results[index] = {
|
||||
status: 'failed',
|
||||
recoverable: false,
|
||||
errors: { base: ['登録中にエラーが発生しました.'] },
|
||||
base_errors: [] }
|
||||
end
|
||||
|
||||
{ results: }
|
||||
end
|
||||
|
||||
@@ -156,7 +168,10 @@ class PostBulkCreator
|
||||
original_created_before: preflight[:original_created_before],
|
||||
duration: preflight[:duration],
|
||||
video_ms: preflight[:video_ms],
|
||||
normalised_tags: preflight[:normalised_tags],
|
||||
direct_tag_specs: preflight[:direct_tag_specs],
|
||||
default_tag_specs: preflight[:default_tag_specs],
|
||||
snapshot_tag_specs: preflight[:snapshot_tag_specs],
|
||||
post_tag_specs: preflight[:post_tag_specs],
|
||||
tag_sections: preflight[:tag_sections],
|
||||
normalised_parent_post_ids: preflight[:normalised_parent_post_ids] }
|
||||
end
|
||||
|
||||
@@ -1,16 +1,20 @@
|
||||
class PostCreatePlan
|
||||
VIDEO_TAG_NAME = '動画'.freeze
|
||||
TAGME_TAG_NAME = 'タグ希望'.freeze
|
||||
NO_DEERJIKIST_TAG_NAME = 'ニジラー情報不詳'.freeze
|
||||
|
||||
def initialize attributes:
|
||||
@attributes = attributes.symbolize_keys
|
||||
@existing_tags_by_name = nil
|
||||
end
|
||||
|
||||
def build!
|
||||
Tag.normalise_tags!(tag_names, with_tagme: false, deny_deprecated: true,
|
||||
with_sections: true) =>
|
||||
{ tags:, sections: }
|
||||
|
||||
tags = Tag.expand_parent_tags(tags).reject(&:deprecated?)
|
||||
video_ms = normalise_video_ms(tags)
|
||||
validate_video_sections!(video_ms, sections)
|
||||
direct_tag_specs, tag_sections = parse_direct_tag_specs
|
||||
default_tag_specs = build_default_tag_specs(direct_tag_specs)
|
||||
snapshot_tag_specs = merge_tag_specs(direct_tag_specs + default_tag_specs)
|
||||
post_tag_specs = expand_parent_tag_specs(snapshot_tag_specs)
|
||||
video_ms = normalise_video_ms(snapshot_tag_specs)
|
||||
validate_video_sections!(video_ms, tag_sections)
|
||||
parent_post_ids = normalise_parent_post_ids
|
||||
validate_parent_post_ids!(parent_post_ids)
|
||||
|
||||
@@ -20,12 +24,15 @@ class PostCreatePlan
|
||||
thumbnail_base: @attributes[:thumbnail_base].presence,
|
||||
original_created_from: @attributes[:original_created_from].presence,
|
||||
original_created_before: @attributes[:original_created_before].presence,
|
||||
tags: serialised_tags(tags, sections),
|
||||
tags: serialised_tags(direct_tag_specs, tag_sections),
|
||||
duration: @attributes[:duration].to_s,
|
||||
video_ms: video_ms,
|
||||
parent_post_ids: parent_post_ids.join(' '),
|
||||
normalised_tags: tags,
|
||||
tag_sections: sections,
|
||||
direct_tag_specs: direct_tag_specs,
|
||||
default_tag_specs: default_tag_specs,
|
||||
snapshot_tag_specs: snapshot_tag_specs,
|
||||
post_tag_specs: post_tag_specs,
|
||||
tag_sections: tag_sections,
|
||||
normalised_parent_post_ids: parent_post_ids }
|
||||
end
|
||||
|
||||
@@ -33,6 +40,112 @@ class PostCreatePlan
|
||||
|
||||
def tag_names = @attributes[:tags].to_s.split
|
||||
|
||||
def parse_direct_tag_specs
|
||||
tag_sections = { }
|
||||
direct_tag_specs = []
|
||||
|
||||
tag_names.each do |raw_name|
|
||||
tag_name, category, sections = parse_raw_tag_name(raw_name)
|
||||
existing_tag = existing_tags_by_name[tag_name]
|
||||
raise Tag::NicoTagNormalisationError if existing_tag&.nico?
|
||||
raise Tag::DeprecatedTagNormalisationError, [existing_tag.name] if existing_tag&.deprecated?
|
||||
|
||||
direct_tag_specs << {
|
||||
name: tag_name,
|
||||
category: (category || existing_tag&.category || 'general').to_sym }
|
||||
if sections.present?
|
||||
tag_sections[tag_name] ||= []
|
||||
tag_sections[tag_name].concat(sections)
|
||||
tag_sections[tag_name] = Tag.merge_section_ranges(tag_sections[tag_name])
|
||||
tag_sections.delete(tag_name) if tag_sections[tag_name] == [[0, nil]]
|
||||
end
|
||||
end
|
||||
|
||||
[merge_tag_specs(direct_tag_specs), tag_sections]
|
||||
end
|
||||
|
||||
def parse_raw_tag_name raw_name
|
||||
name = raw_name.to_s
|
||||
prefix, category =
|
||||
Tag::CATEGORY_PREFIXES.find {
|
||||
name.downcase.start_with?(_1[0])
|
||||
} || ['', nil]
|
||||
name = name.sub(/\A#{ prefix }/i, '')
|
||||
|
||||
sections = []
|
||||
while (match = name.match(/\A(\S*?)\[([^\[\]\s]*)-([^\[\]\s]*)\](\S*)\z/))
|
||||
name = "#{ match[1] }#{ match[4] }"
|
||||
next if match[2].empty? && match[3].empty?
|
||||
|
||||
sections << Tag.normalise_section_range!(
|
||||
begin_raw: match[2],
|
||||
end_raw: match[3],
|
||||
tag_name: name)
|
||||
end
|
||||
raise Tag::SectionLiteralParseError.new(raw_name, raw_name) if name.include?('[') || name.include?(']')
|
||||
|
||||
[TagName.canonicalise(name).first, category&.to_sym, sections]
|
||||
end
|
||||
|
||||
def build_default_tag_specs direct_tag_specs
|
||||
default_tag_specs = []
|
||||
if direct_tag_specs.length < 10 && direct_tag_specs.none? { _1[:name] == TAGME_TAG_NAME }
|
||||
default_tag_specs << {
|
||||
name: TAGME_TAG_NAME,
|
||||
category: :meta }
|
||||
end
|
||||
if direct_tag_specs.none? { deerjikist_tag_spec?(_1) }
|
||||
default_tag_specs << {
|
||||
name: NO_DEERJIKIST_TAG_NAME,
|
||||
category: :meta }
|
||||
end
|
||||
|
||||
default_tag_specs
|
||||
end
|
||||
|
||||
def expand_parent_tag_specs snapshot_tag_specs
|
||||
existing_snapshot_tags = snapshot_tag_specs.filter_map { existing_tags_by_name[_1[:name]] }
|
||||
expanded_parent_specs =
|
||||
Tag.expand_parent_tags(existing_snapshot_tags)
|
||||
.reject(&:deprecated?)
|
||||
.map { |tag|
|
||||
{
|
||||
name: tag.name,
|
||||
category: tag.category.to_sym } }
|
||||
merge_tag_specs(snapshot_tag_specs + expanded_parent_specs)
|
||||
end
|
||||
|
||||
def merge_tag_specs specs
|
||||
specs.each_with_object({ }) do |spec, merged|
|
||||
merged[spec[:name]] =
|
||||
if merged.key?(spec[:name]) && merged[spec[:name]][:category] != :general
|
||||
merged[spec[:name]]
|
||||
else
|
||||
{
|
||||
name: spec[:name],
|
||||
category: spec[:category] }
|
||||
end
|
||||
end.values.sort_by { _1[:name] }
|
||||
end
|
||||
|
||||
def existing_tags_by_name
|
||||
@existing_tags_by_name ||= begin
|
||||
names = tag_names.map { canonical_tag_name_without_sections(_1) }.uniq
|
||||
Tag.joins(:tag_name).where(tag_names: { name: names }).index_by(&:name)
|
||||
end
|
||||
end
|
||||
|
||||
def canonical_tag_name_without_sections raw_name
|
||||
name, = parse_raw_tag_name(raw_name)
|
||||
name
|
||||
end
|
||||
|
||||
def deerjikist_tag_spec? spec
|
||||
return true if spec[:category] == :deerjikist
|
||||
|
||||
existing_tags_by_name[spec[:name]]&.deerjikist?
|
||||
end
|
||||
|
||||
def normalise_parent_post_ids
|
||||
Array(@attributes[:parent_post_ids]).flat_map { _1.to_s.split }.map { |token|
|
||||
id = Integer(token, exception: false)
|
||||
@@ -47,14 +160,14 @@ class PostCreatePlan
|
||||
raise ArgumentError, "存在しない親投稿 Id. があります: #{ missing.join(' ') }" if missing.present?
|
||||
end
|
||||
|
||||
def serialised_tags tags, sections
|
||||
tags.uniq(&:id).map { |tag|
|
||||
"#{ tag.name }#{ sections[tag.id].to_a.map { Post.section_literal(_1) }.join }"
|
||||
def serialised_tags direct_tag_specs, tag_sections
|
||||
direct_tag_specs.map { |spec|
|
||||
"#{ spec[:name] }#{ tag_sections[spec[:name]].to_a.map { Post.section_literal(_1) }.join }"
|
||||
}.sort.join(' ')
|
||||
end
|
||||
|
||||
def normalise_video_ms tags
|
||||
return nil unless tags.any? { _1.id == Tag.video.id }
|
||||
def normalise_video_ms snapshot_tag_specs
|
||||
return nil unless snapshot_tag_specs.any? { _1[:name] == VIDEO_TAG_NAME }
|
||||
|
||||
video_ms = @attributes[:video_ms]
|
||||
if video_ms.present?
|
||||
@@ -75,17 +188,18 @@ class PostCreatePlan
|
||||
raise PostCreator::VideoMsParseError
|
||||
end
|
||||
|
||||
def validate_video_sections! video_ms, sections
|
||||
def validate_video_sections! video_ms, tag_sections
|
||||
return unless video_ms
|
||||
|
||||
sections.each_value do |ranges|
|
||||
tag_sections.each_value do |ranges|
|
||||
ranges.each do |begin_ms, end_ms|
|
||||
post = Post.new
|
||||
if begin_ms >= video_ms
|
||||
post = Post.new
|
||||
post.errors.add :video_ms, 'タグ区間の開始が動画時間以上です.'
|
||||
raise ActiveRecord::RecordInvalid, post
|
||||
end
|
||||
if end_ms && end_ms > video_ms
|
||||
post = Post.new
|
||||
post.errors.add :video_ms, 'タグ区間の終端が動画時間を超えてゐます.'
|
||||
raise ActiveRecord::RecordInvalid, post
|
||||
end
|
||||
|
||||
@@ -62,7 +62,10 @@ class PostCreatePreflight
|
||||
original_created_before: plan[:original_created_before],
|
||||
duration: plan[:duration],
|
||||
video_ms: plan[:video_ms],
|
||||
normalised_tags: plan[:normalised_tags],
|
||||
direct_tag_specs: plan[:direct_tag_specs],
|
||||
default_tag_specs: plan[:default_tag_specs],
|
||||
snapshot_tag_specs: plan[:snapshot_tag_specs],
|
||||
post_tag_specs: plan[:post_tag_specs],
|
||||
tag_sections: plan[:tag_sections],
|
||||
normalised_parent_post_ids: plan[:normalised_parent_post_ids],
|
||||
field_warnings: final_field_warnings(preview[:field_warnings] || { }),
|
||||
@@ -126,8 +129,8 @@ class PostCreatePreflight
|
||||
thumbnail_warnings = (thumbnail_warnings + ['サムネールなし']).uniq
|
||||
end
|
||||
|
||||
{
|
||||
**field_warnings,
|
||||
'thumbnail_base' => thumbnail_warnings }
|
||||
next_warnings = field_warnings.except('thumbnail_base')
|
||||
next_warnings['thumbnail_base'] = thumbnail_warnings if thumbnail_warnings.present?
|
||||
next_warnings
|
||||
end
|
||||
end
|
||||
|
||||
@@ -21,12 +21,13 @@ class PostCreator
|
||||
ApplicationRecord.transaction do
|
||||
post.save!
|
||||
post.thumbnail.attach(thumbnail_attachment) if thumbnail_attachment.present?
|
||||
tags = planned_tags
|
||||
snapshot_tags = planned_snapshot_tags
|
||||
post_tags = planned_post_tags
|
||||
sections = planned_sections
|
||||
TagVersioning.record_tag_snapshots!(tags, created_by_user: @actor)
|
||||
TagVersioning.record_tag_snapshots!(snapshot_tags, created_by_user: @actor)
|
||||
post.video_ms = planned_video_ms
|
||||
post.save!
|
||||
sync_post_tags!(post, tags, sections)
|
||||
sync_post_tags!(post, post_tags, sections)
|
||||
sync_parent_posts!(post, planned_parent_post_ids)
|
||||
PostVersionRecorder.record!(post:, event_type: :create, created_by_user: @actor)
|
||||
end
|
||||
@@ -44,7 +45,9 @@ class PostCreator
|
||||
thumbnail_base: @attributes[:thumbnail_base].presence)
|
||||
end
|
||||
|
||||
def planned_tags = planned_create_attributes[:normalised_tags]
|
||||
def planned_snapshot_tags = planned_create_attributes[:snapshot_tags]
|
||||
|
||||
def planned_post_tags = planned_create_attributes[:post_tags]
|
||||
|
||||
def planned_sections = planned_create_attributes[:tag_sections]
|
||||
|
||||
@@ -56,18 +59,65 @@ class PostCreator
|
||||
|
||||
def planned_create_attributes
|
||||
@planned_create_attributes ||= begin
|
||||
if @attributes.key?(:normalised_tags)
|
||||
if @attributes.key?(:snapshot_tag_specs)
|
||||
snapshot_tags = materialise_tags(@attributes[:snapshot_tag_specs] || [])
|
||||
post_tags = materialise_tags(@attributes[:post_tag_specs] || [])
|
||||
{
|
||||
normalised_tags: @attributes[:normalised_tags],
|
||||
tag_sections: @attributes[:tag_sections] || { },
|
||||
snapshot_tags: snapshot_tags,
|
||||
post_tags: post_tags,
|
||||
tag_sections: materialise_sections(
|
||||
@attributes[:tag_sections] || { },
|
||||
snapshot_tags,
|
||||
post_tags),
|
||||
normalised_parent_post_ids: @attributes[:normalised_parent_post_ids] || [],
|
||||
video_ms: @attributes[:video_ms] }
|
||||
else
|
||||
PostCreatePlan.new(attributes: @attributes).build!
|
||||
build_materialised_plan
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def build_materialised_plan
|
||||
plan = PostCreatePlan.new(attributes: @attributes).build!
|
||||
snapshot_tags = materialise_tags(plan[:snapshot_tag_specs] || [])
|
||||
post_tags = materialise_tags(plan[:post_tag_specs] || [])
|
||||
{
|
||||
snapshot_tags: snapshot_tags,
|
||||
post_tags: post_tags,
|
||||
tag_sections: materialise_sections(
|
||||
plan[:tag_sections] || { },
|
||||
snapshot_tags,
|
||||
post_tags),
|
||||
normalised_parent_post_ids: plan[:normalised_parent_post_ids] || [],
|
||||
video_ms: plan[:video_ms] }
|
||||
end
|
||||
|
||||
def materialise_tags specs
|
||||
Array(specs).each_with_object({ }) do |spec, tags|
|
||||
name = spec[:name] || spec['name']
|
||||
category = spec[:category] || spec['category']
|
||||
next if name.blank? || category.blank?
|
||||
|
||||
tag = Tag.find_or_create_by_tag_name!(name, category:)
|
||||
tag.update!(category:) if tag.category.to_sym != category.to_sym
|
||||
tags[name] ||= tag
|
||||
end.values
|
||||
end
|
||||
|
||||
def materialise_sections sections_by_name, snapshot_tags, post_tags
|
||||
tags_by_name = post_tags.index_by(&:name)
|
||||
snapshot_tags.each do |tag|
|
||||
tags_by_name[tag.name] ||= tag
|
||||
end
|
||||
|
||||
sections_by_name.each_with_object({ }) do |(tag_name, ranges), sections|
|
||||
tag = tags_by_name[tag_name.to_s]
|
||||
next if tag.nil?
|
||||
|
||||
sections[tag.id] = Array(ranges).map { |range| [range[0], range[1]] }
|
||||
end
|
||||
end
|
||||
|
||||
def sync_post_tags! post, desired_tags, sections
|
||||
desired_ids = desired_tags.map(&:id).to_set
|
||||
current_ids = post.tags.pluck(:id).to_set
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
class PostThumbnailUploadValidator
|
||||
MAX_THUMBNAIL_BYTES = 20 * 1024 * 1024
|
||||
ALLOWED_CONTENT_TYPES = Preview::ThumbnailFetcher::ALLOWED_IMAGE_CONTENT_TYPES.freeze
|
||||
|
||||
class InvalidUpload < StandardError; end
|
||||
|
||||
@@ -7,6 +8,7 @@ class PostThumbnailUploadValidator
|
||||
return if thumbnail.blank?
|
||||
raise InvalidUpload, 'thumbnail upload が不正です.' unless thumbnail.is_a?(ActionDispatch::Http::UploadedFile)
|
||||
raise InvalidUpload, 'thumbnail file size が大きすぎます.' if thumbnail.size > MAX_THUMBNAIL_BYTES
|
||||
raise InvalidUpload, 'サムネイル画像の形式が不正です.' unless allowed_content_type?(thumbnail.content_type)
|
||||
|
||||
attachment = Post.resized_thumbnail_attachment(thumbnail)
|
||||
attachment[:io].close if attachment[:io].respond_to?(:close)
|
||||
@@ -15,4 +17,9 @@ class PostThumbnailUploadValidator
|
||||
ensure
|
||||
thumbnail&.rewind if thumbnail.respond_to?(:rewind)
|
||||
end
|
||||
|
||||
def self.allowed_content_type? content_type
|
||||
mime_type = content_type.to_s.split(';', 2).first.to_s.downcase.strip
|
||||
ALLOWED_CONTENT_TYPES.include?(mime_type)
|
||||
end
|
||||
end
|
||||
|
||||
@@ -52,7 +52,7 @@ Rails.application.routes.draw do
|
||||
|
||||
resources :posts, only: [:index, :show, :create, :update] do
|
||||
collection do
|
||||
get :preview
|
||||
get :metadata
|
||||
post :bulk
|
||||
get :random
|
||||
get :changes
|
||||
|
||||
@@ -90,6 +90,8 @@ export const applyThumbnailWarnings = (rows: PostImportRow[]): PostImportRow[] =
|
||||
|
||||
export const processableImportRows = (rows: PostImportRow[]): PostImportRow[] =>
|
||||
validatableImportRows (rows).filter (row => {
|
||||
if (row.metadataUrl == null)
|
||||
return false
|
||||
if (row.importStatus === 'created')
|
||||
return false
|
||||
if (row.importStatus === 'skipped')
|
||||
|
||||
@@ -30,7 +30,6 @@ import {
|
||||
clearPostImportSourceDraft,
|
||||
generatePostImportSessionId,
|
||||
hasThumbnailBaseValue,
|
||||
initialisePreviewRows,
|
||||
isCompletedReviewRow,
|
||||
isExistingSkipRow,
|
||||
isNonRecoverableFailedRow,
|
||||
@@ -62,7 +61,7 @@ import type { User } from '@/types'
|
||||
|
||||
type Props = { user: User | null }
|
||||
|
||||
type PreviewResponse = {
|
||||
type PostMetadataResponse = {
|
||||
url: string
|
||||
title?: string
|
||||
thumbnailBase?: string
|
||||
@@ -105,6 +104,47 @@ const isRepairRow = (row: PostImportRow): boolean =>
|
||||
|| (row.importStatus === 'pending'
|
||||
&& Object.keys (row.validationErrors).length > 0))
|
||||
|
||||
const DUPLICATE_URL_MESSAGE = 'URL が重複しています.'
|
||||
|
||||
const applyDuplicateUrlErrors = (
|
||||
rows: PostImportRow[],
|
||||
): PostImportRow[] => {
|
||||
const counts = rows.reduce<Record<string, number>> ((result, row) => {
|
||||
if (row.url !== '')
|
||||
result[row.url] = (result[row.url] ?? 0) + 1
|
||||
return result
|
||||
}, { })
|
||||
|
||||
return rows.map (row => {
|
||||
const urlErrors = (row.validationErrors.url ?? []).filter (
|
||||
message => message !== DUPLICATE_URL_MESSAGE)
|
||||
if ((counts[row.url] ?? 0) < 2)
|
||||
{
|
||||
const nextValidationErrors = {
|
||||
...row.validationErrors,
|
||||
url: urlErrors }
|
||||
const hasErrors = Object.values (nextValidationErrors).some (
|
||||
messages => messages.length > 0)
|
||||
return urlErrors.length === (row.validationErrors.url ?? []).length
|
||||
? row
|
||||
: {
|
||||
...row,
|
||||
validationErrors: nextValidationErrors,
|
||||
status: hasErrors ? 'error' : row.status }
|
||||
}
|
||||
|
||||
return {
|
||||
...row,
|
||||
skipReason: row.skipReason === 'manual' ? 'manual' : undefined,
|
||||
existingPostId: undefined,
|
||||
existingPost: undefined,
|
||||
validationErrors: {
|
||||
...row.validationErrors,
|
||||
url: [...new Set ([...urlErrors, DUPLICATE_URL_MESSAGE])] },
|
||||
status: 'error' }
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
const buildSession = (rows: PostImportRow[]): PostImportSession => ({
|
||||
version: 3,
|
||||
@@ -137,9 +177,12 @@ const buildDryRunFormData = (row: PostImportRow): FormData => {
|
||||
|
||||
const mergeDryRunRow = (
|
||||
currentRow: PostImportRow,
|
||||
result: PreviewResponse,
|
||||
): PostImportRow =>
|
||||
applyThumbnailWarning ({
|
||||
result: PostMetadataResponse,
|
||||
): PostImportRow => {
|
||||
const hasWarnings =
|
||||
Object.values (result.fieldWarnings ?? { }).some (messages => messages.length > 0)
|
||||
|| (result.baseWarnings?.length ?? 0) > 0
|
||||
return applyThumbnailWarning ({
|
||||
...currentRow,
|
||||
url: result.url,
|
||||
attributes: {
|
||||
@@ -157,8 +200,7 @@ const mergeDryRunRow = (
|
||||
validationErrors: { },
|
||||
importErrors: undefined,
|
||||
status:
|
||||
Object.keys (result.fieldWarnings ?? { }).length > 0
|
||||
|| (result.baseWarnings?.length ?? 0) > 0
|
||||
hasWarnings
|
||||
? 'warning'
|
||||
: 'ready',
|
||||
skipReason: result.existingPost?.id != null ? 'existing' : undefined,
|
||||
@@ -169,10 +211,11 @@ const mergeDryRunRow = (
|
||||
? 'created'
|
||||
: 'pending',
|
||||
recoverable: undefined })
|
||||
}
|
||||
|
||||
|
||||
const buildPreviewResetSnapshot = (
|
||||
preview: PreviewResponse,
|
||||
preview: PostMetadataResponse,
|
||||
): PostImportRow['resetSnapshot'] => ({
|
||||
url: preview.url,
|
||||
attributes: {
|
||||
@@ -246,7 +289,7 @@ const indexedBulkResults = (
|
||||
|
||||
const mergePreviewRow = (
|
||||
currentRow: PostImportRow,
|
||||
preview: PreviewResponse,
|
||||
preview: PostMetadataResponse,
|
||||
): PostImportRow => {
|
||||
const fieldWarnings = preview.fieldWarnings ?? { }
|
||||
const baseWarnings = preview.baseWarnings ?? []
|
||||
@@ -405,7 +448,7 @@ const PostImportReviewPage: FC<Props> = ({ user }) => {
|
||||
if (serialised == null)
|
||||
return null
|
||||
if (persistSequenceRef.current !== sequence)
|
||||
return sessionRef.current
|
||||
return null
|
||||
|
||||
const saved = savePostImportSession (sessionId, nextSession)
|
||||
const loadedSession = loadPostImportSession (sessionId)
|
||||
@@ -447,8 +490,23 @@ const PostImportReviewPage: FC<Props> = ({ user }) => {
|
||||
}
|
||||
|
||||
const refreshRows = async (baseSession: PostImportSession) => {
|
||||
setLoading (true)
|
||||
let nextIndex = 0
|
||||
const previews = new Map<number, PreviewResponse> ()
|
||||
const previewErrors = new Map<number, Record<string, string[]>> ()
|
||||
let workingRows = [...(sessionRef.current ?? baseSession).rows]
|
||||
|
||||
const mergeWorkingRow = (
|
||||
sourceRow: number,
|
||||
updater: (row: PostImportRow) => PostImportRow,
|
||||
) => {
|
||||
workingRows = workingRows.map (row =>
|
||||
row.sourceRow === sourceRow
|
||||
? updater (row)
|
||||
: row)
|
||||
const nextSession = buildSession (workingRows)
|
||||
sessionRef.current = nextSession
|
||||
setSession (nextSession)
|
||||
}
|
||||
const worker = async () => {
|
||||
while (active)
|
||||
{
|
||||
@@ -461,18 +519,19 @@ const PostImportReviewPage: FC<Props> = ({ user }) => {
|
||||
if (baseRow.skipReason === 'manual'
|
||||
|| baseRow.importStatus === 'created'
|
||||
|| baseRow.importStatus === 'skipped'
|
||||
|| isNonRecoverableFailedRow (baseRow))
|
||||
|| isNonRecoverableFailedRow (baseRow)
|
||||
|| baseRow.metadataUrl != null)
|
||||
continue
|
||||
const controller = new AbortController ()
|
||||
controllers.add (controller)
|
||||
try
|
||||
{
|
||||
const preview = await apiGet<PreviewResponse> ('/posts/preview', {
|
||||
const preview = await apiGet<PostMetadataResponse> ('/posts/metadata', {
|
||||
params: { url: baseRow.url },
|
||||
signal: controller.signal })
|
||||
if (!(active) || previewSequenceRef.current !== previewSequence)
|
||||
return
|
||||
previews.set (baseRow.sourceRow, preview)
|
||||
mergeWorkingRow (baseRow.sourceRow, row => mergePreviewRow (row, preview))
|
||||
}
|
||||
catch (requestError)
|
||||
{
|
||||
@@ -480,8 +539,25 @@ const PostImportReviewPage: FC<Props> = ({ user }) => {
|
||||
return
|
||||
if (!(active) || previewSequenceRef.current !== previewSequence)
|
||||
return
|
||||
if (!(isApiError (requestError)))
|
||||
if (!(isApiError<{
|
||||
errors?: Record<string, string[]>
|
||||
baseErrors?: string[]
|
||||
}> (requestError)))
|
||||
continue
|
||||
if (requestError.response?.status === 422)
|
||||
{
|
||||
const rowErrors = {
|
||||
...(requestError.response.data.errors ?? { }),
|
||||
...(requestError.response.data.baseErrors?.length
|
||||
? {
|
||||
base: requestError.response.data.baseErrors }
|
||||
: { }) }
|
||||
previewErrors.set (baseRow.sourceRow, rowErrors)
|
||||
mergeWorkingRow (baseRow.sourceRow, row => ({
|
||||
...row,
|
||||
validationErrors: rowErrors,
|
||||
status: 'error' }))
|
||||
}
|
||||
}
|
||||
finally
|
||||
{
|
||||
@@ -493,20 +569,22 @@ const PostImportReviewPage: FC<Props> = ({ user }) => {
|
||||
await Promise.all (
|
||||
Array.from ({ length: Math.min (4, baseSession.rows.length) }, () => worker ()))
|
||||
if (!(active) || previewSequenceRef.current !== previewSequence)
|
||||
{
|
||||
setLoading (false)
|
||||
return
|
||||
}
|
||||
|
||||
const latestSession = sessionRef.current ?? baseSession
|
||||
const nextRows = latestSession.rows.map (row => {
|
||||
const preview = previews.get (row.sourceRow)
|
||||
if (preview == null
|
||||
|| row.skipReason === 'manual'
|
||||
|| row.importStatus === 'created'
|
||||
|| row.importStatus === 'skipped'
|
||||
|| isNonRecoverableFailedRow (row))
|
||||
const nextRows = applyDuplicateUrlErrors (workingRows.map (row => {
|
||||
const rowErrors = previewErrors.get (row.sourceRow)
|
||||
if (rowErrors == null)
|
||||
return row
|
||||
return mergePreviewRow (row, preview)
|
||||
})
|
||||
return {
|
||||
...row,
|
||||
validationErrors: rowErrors,
|
||||
status: 'error' as const }
|
||||
}))
|
||||
await persistSession (buildSession (nextRows))
|
||||
setLoading (false)
|
||||
}
|
||||
|
||||
const hydrate = async () => {
|
||||
@@ -528,89 +606,6 @@ const PostImportReviewPage: FC<Props> = ({ user }) => {
|
||||
sessionRef.current = loaded
|
||||
setSession (loaded)
|
||||
|
||||
if (loadedSession == null && parsed?.shortcut)
|
||||
{
|
||||
try
|
||||
{
|
||||
const preview = await apiGet<PreviewResponse> ('/posts/preview', {
|
||||
params: { url: parsed.source } })
|
||||
if (!(active))
|
||||
return
|
||||
const rows = [applyThumbnailWarning (initialisePreviewRows ([{
|
||||
sourceRow: 1,
|
||||
url: preview.url,
|
||||
attributes: {
|
||||
title: preview.title ?? '',
|
||||
thumbnailBase: preview.thumbnailBase ?? '',
|
||||
originalCreatedFrom: preview.originalCreatedFrom ?? '',
|
||||
originalCreatedBefore: preview.originalCreatedBefore ?? '',
|
||||
duration: preview.duration ?? '',
|
||||
videoMs: preview.videoMs ?? '',
|
||||
tags: preview.tags ?? '',
|
||||
parentPostIds: String (preview.parentPostIds ?? '') },
|
||||
fieldWarnings: preview.fieldWarnings ?? { },
|
||||
baseWarnings: preview.baseWarnings ?? [],
|
||||
validationErrors: { },
|
||||
provenance: {
|
||||
url: 'manual',
|
||||
title: 'automatic',
|
||||
thumbnailBase: 'automatic',
|
||||
originalCreatedFrom: 'automatic',
|
||||
originalCreatedBefore: 'automatic',
|
||||
duration: 'automatic',
|
||||
videoMs: 'automatic',
|
||||
tags: 'automatic',
|
||||
parentPostIds: 'automatic' },
|
||||
tagSources: {
|
||||
automatic: preview.tags ?? '',
|
||||
manual: '' },
|
||||
status:
|
||||
Object.keys (preview.fieldWarnings ?? { }).length > 0
|
||||
|| (preview.baseWarnings?.length ?? 0) > 0
|
||||
? 'warning'
|
||||
: 'ready',
|
||||
skipReason: preview.existingPost?.id != null ? 'existing' : undefined,
|
||||
existingPostId: preview.existingPost?.id,
|
||||
existingPost: preview.existingPost ?? undefined,
|
||||
resetSnapshot: {
|
||||
url: preview.url,
|
||||
attributes: {
|
||||
title: preview.title ?? '',
|
||||
thumbnailBase: preview.thumbnailBase ?? '',
|
||||
originalCreatedFrom: preview.originalCreatedFrom ?? '',
|
||||
originalCreatedBefore: preview.originalCreatedBefore ?? '',
|
||||
duration: preview.duration ?? '',
|
||||
videoMs: preview.videoMs ?? '',
|
||||
tags: preview.tags ?? '',
|
||||
parentPostIds: String (preview.parentPostIds ?? '') },
|
||||
provenance: {
|
||||
url: 'manual',
|
||||
title: 'automatic',
|
||||
thumbnailBase: 'automatic',
|
||||
originalCreatedFrom: 'automatic',
|
||||
originalCreatedBefore: 'automatic',
|
||||
duration: 'automatic',
|
||||
videoMs: 'automatic',
|
||||
tags: 'automatic',
|
||||
parentPostIds: 'automatic' },
|
||||
tagSources: {
|
||||
automatic: preview.tags ?? '',
|
||||
manual: '' },
|
||||
fieldWarnings: preview.fieldWarnings ?? { },
|
||||
baseWarnings: preview.baseWarnings ?? [],
|
||||
metadataUrl: preview.url } }])[0])]
|
||||
const persisted = await persistSession (buildSession (rows))
|
||||
if (persisted == null)
|
||||
return
|
||||
}
|
||||
catch
|
||||
{
|
||||
if (active)
|
||||
navigate ('/posts/new', { replace: true })
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
void refreshRows (loaded)
|
||||
}
|
||||
|
||||
@@ -649,6 +644,9 @@ const PostImportReviewPage: FC<Props> = ({ user }) => {
|
||||
const existingRows = sortedRows.filter (row => isExistingSkipRow (row))
|
||||
const reviewRows = sortedRows.filter (row => !(isExistingSkipRow (row)))
|
||||
const busy = loading || loadingRow != null
|
||||
const canSubmit =
|
||||
rows.every (row => isCompletedReviewRow (row))
|
||||
|| processableImportRows (rows).length > 0
|
||||
|
||||
const toggleManualSkip = async (sourceRow: number, checked: boolean) => {
|
||||
if (busy)
|
||||
@@ -709,10 +707,10 @@ const PostImportReviewPage: FC<Props> = ({ user }) => {
|
||||
urlChanged
|
||||
? mergePreviewRow (
|
||||
nextRow,
|
||||
await apiGet<PreviewResponse> ('/posts/preview', {
|
||||
await apiGet<PostMetadataResponse> ('/posts/metadata', {
|
||||
params: { url: nextRow.url } }))
|
||||
: nextRow
|
||||
const dryRun = await apiPost<PreviewResponse> (
|
||||
const dryRun = await apiPost<PostMetadataResponse> (
|
||||
'/posts?dry=1',
|
||||
buildDryRunFormData (candidateRow))
|
||||
const latestSession = sessionRef.current
|
||||
@@ -971,6 +969,7 @@ const PostImportReviewPage: FC<Props> = ({ user }) => {
|
||||
|
||||
<PostImportFooter
|
||||
loading={busy}
|
||||
canSubmit={canSubmit}
|
||||
creatableCount={counts.creatable}
|
||||
manualSkippedCount={counts.manualSkipped}
|
||||
existingSkippedCount={counts.existingSkipped}
|
||||
@@ -982,12 +981,14 @@ const PostImportReviewPage: FC<Props> = ({ user }) => {
|
||||
|
||||
const PostImportFooter = (
|
||||
{ loading,
|
||||
canSubmit,
|
||||
creatableCount,
|
||||
manualSkippedCount,
|
||||
existingSkippedCount,
|
||||
pendingOrErrorCount,
|
||||
onBack,
|
||||
onSubmit }: { loading: boolean
|
||||
canSubmit: boolean
|
||||
creatableCount: number
|
||||
manualSkippedCount: number
|
||||
existingSkippedCount: number
|
||||
@@ -1013,7 +1014,7 @@ const PostImportFooter = (
|
||||
<Button
|
||||
type="button"
|
||||
onClick={onSubmit}
|
||||
disabled={loading}>
|
||||
disabled={loading || !(canSubmit)}>
|
||||
{creatableCount > 1 ? '一括追加' : '追加'}
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
@@ -11,7 +11,6 @@ import MainArea from '@/components/layout/MainArea'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { toast } from '@/components/ui/use-toast'
|
||||
import { SITE_TITLE } from '@/config'
|
||||
import { apiGet, isApiError } from '@/lib/api'
|
||||
import {
|
||||
appendPostNewSessionId,
|
||||
serialisePostNewState,
|
||||
@@ -37,66 +36,32 @@ import type { User } from '@/types'
|
||||
|
||||
type Props = { user: User | null }
|
||||
|
||||
type PreviewResponse = {
|
||||
url: string
|
||||
title?: string
|
||||
thumbnailBase?: string
|
||||
originalCreatedFrom?: string
|
||||
originalCreatedBefore?: string
|
||||
duration?: string
|
||||
videoMs?: number
|
||||
tags?: string
|
||||
fieldWarnings?: Record<string, string[]>
|
||||
baseWarnings?: string[]
|
||||
validationErrors?: Record<string, string[]>
|
||||
existingPost?: {
|
||||
id: number
|
||||
title: string
|
||||
url: string
|
||||
thumbnailUrl?: string } | null }
|
||||
|
||||
const MAX_ROWS = 100
|
||||
const SOURCE_ERROR_ID = 'post-import-source-error'
|
||||
const SOURCE_ISSUES_ID = 'post-import-source-issues'
|
||||
|
||||
const urlIssuesFromRows = (rows: PostImportRow[], source: string) => {
|
||||
const sourceLines = source.split (/\r\n|\n|\r/)
|
||||
|
||||
return rows.flatMap (row =>
|
||||
(row.validationErrors.url ?? []).map (message => ({
|
||||
const buildInitialRows = (source: string): PostImportRow[] =>
|
||||
source
|
||||
.split (/\r\n|\n|\r/)
|
||||
.map ((line, index) => ({
|
||||
sourceRow: index + 1,
|
||||
url: line.trim () }))
|
||||
.filter (row => row.url !== '')
|
||||
.map (row => ({
|
||||
sourceRow: row.sourceRow,
|
||||
message,
|
||||
url: sourceLines[row.sourceRow - 1]?.trim () ?? row.url })))
|
||||
}
|
||||
|
||||
|
||||
const previewRowFromResult = (
|
||||
sourceRow: number,
|
||||
result: PreviewResponse,
|
||||
): PostImportRow => {
|
||||
const fieldWarnings = result.fieldWarnings ?? { }
|
||||
const baseWarnings = result.baseWarnings ?? []
|
||||
const validationErrors = result.validationErrors ?? { }
|
||||
const hasWarnings =
|
||||
Object.values (fieldWarnings).some (messages => messages.length > 0)
|
||||
|| baseWarnings.length > 0
|
||||
const hasErrors =
|
||||
Object.values (validationErrors).some (messages => messages.length > 0)
|
||||
const row: PostImportRow = {
|
||||
sourceRow,
|
||||
url: result.url,
|
||||
url: row.url,
|
||||
attributes: {
|
||||
title: result.title ?? '',
|
||||
thumbnailBase: result.thumbnailBase ?? '',
|
||||
originalCreatedFrom: result.originalCreatedFrom ?? '',
|
||||
originalCreatedBefore: result.originalCreatedBefore ?? '',
|
||||
duration: result.duration ?? '',
|
||||
videoMs: result.videoMs ?? '',
|
||||
tags: result.tags ?? '',
|
||||
title: '',
|
||||
thumbnailBase: '',
|
||||
originalCreatedFrom: '',
|
||||
originalCreatedBefore: '',
|
||||
duration: '',
|
||||
videoMs: '',
|
||||
tags: '',
|
||||
parentPostIds: '' },
|
||||
fieldWarnings,
|
||||
baseWarnings,
|
||||
validationErrors,
|
||||
fieldWarnings: { },
|
||||
baseWarnings: [],
|
||||
validationErrors: { },
|
||||
provenance: {
|
||||
url: 'manual',
|
||||
title: 'automatic',
|
||||
@@ -108,27 +73,19 @@ const previewRowFromResult = (
|
||||
tags: 'automatic',
|
||||
parentPostIds: 'automatic' },
|
||||
tagSources: {
|
||||
automatic: result.tags ?? '',
|
||||
automatic: '',
|
||||
manual: '' },
|
||||
status:
|
||||
hasErrors
|
||||
? 'error'
|
||||
: hasWarnings
|
||||
? 'warning'
|
||||
: 'ready',
|
||||
skipReason: result.existingPost?.id != null ? 'existing' : undefined,
|
||||
existingPostId: result.existingPost?.id,
|
||||
existingPost: result.existingPost ?? undefined,
|
||||
status: 'ready' as const,
|
||||
resetSnapshot: {
|
||||
url: result.url,
|
||||
url: row.url,
|
||||
attributes: {
|
||||
title: result.title ?? '',
|
||||
thumbnailBase: result.thumbnailBase ?? '',
|
||||
originalCreatedFrom: result.originalCreatedFrom ?? '',
|
||||
originalCreatedBefore: result.originalCreatedBefore ?? '',
|
||||
duration: result.duration ?? '',
|
||||
videoMs: result.videoMs ?? '',
|
||||
tags: result.tags ?? '',
|
||||
title: '',
|
||||
thumbnailBase: '',
|
||||
originalCreatedFrom: '',
|
||||
originalCreatedBefore: '',
|
||||
duration: '',
|
||||
videoMs: '',
|
||||
tags: '',
|
||||
parentPostIds: '' },
|
||||
provenance: {
|
||||
url: 'manual',
|
||||
@@ -141,87 +98,10 @@ const previewRowFromResult = (
|
||||
tags: 'automatic',
|
||||
parentPostIds: 'automatic' },
|
||||
tagSources: {
|
||||
automatic: result.tags ?? '',
|
||||
automatic: '',
|
||||
manual: '' },
|
||||
fieldWarnings,
|
||||
baseWarnings } }
|
||||
return row
|
||||
}
|
||||
|
||||
|
||||
const fetchPreviewRows = async (
|
||||
rows: Array<{ sourceRow: number
|
||||
url: string }>,
|
||||
signal: AbortSignal,
|
||||
): Promise<{
|
||||
rows: PostImportRow[]
|
||||
issues: Array<{ sourceRow: number
|
||||
message: string
|
||||
url: string }>
|
||||
}> => {
|
||||
const results: Array<PostImportRow | null> = new Array (rows.length).fill (null)
|
||||
const issues: Array<{ sourceRow: number
|
||||
message: string
|
||||
url: string }> = []
|
||||
let nextIndex = 0
|
||||
|
||||
const previewIssues = (
|
||||
sourceRow: number,
|
||||
url: string,
|
||||
requestError: unknown,
|
||||
): Array<{ sourceRow: number
|
||||
message: string
|
||||
url: string }> => {
|
||||
if (!(isApiError<{
|
||||
message?: string
|
||||
errors?: Record<string, string[]>
|
||||
baseErrors?: string[]
|
||||
}> (requestError)))
|
||||
{
|
||||
return [{
|
||||
sourceRow,
|
||||
message: '入力を確認してください.',
|
||||
url }]
|
||||
}
|
||||
|
||||
const fieldMessages = Object.values (requestError.response?.data?.errors ?? { }).flat ()
|
||||
const baseMessages = requestError.response?.data?.baseErrors ?? []
|
||||
const messages = [...new Set ([...fieldMessages, ...baseMessages])]
|
||||
return (messages.length > 0 ? messages : [requestError.response?.data?.message ?? '入力を確認してください.'])
|
||||
.map (message => ({
|
||||
sourceRow,
|
||||
message,
|
||||
url }))
|
||||
}
|
||||
|
||||
const worker = async () => {
|
||||
while (nextIndex < rows.length)
|
||||
{
|
||||
const index = nextIndex
|
||||
++nextIndex
|
||||
const row = rows[index]
|
||||
try
|
||||
{
|
||||
const result = await apiGet<PreviewResponse> ('/posts/preview', {
|
||||
params: { url: row.url },
|
||||
signal })
|
||||
results[index] = previewRowFromResult (row.sourceRow, result)
|
||||
}
|
||||
catch (requestError)
|
||||
{
|
||||
if (signal.aborted)
|
||||
return
|
||||
issues.push (...previewIssues (row.sourceRow, row.url, requestError))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
await Promise.all (
|
||||
Array.from ({ length: Math.min (4, rows.length) }, () => worker ()))
|
||||
return {
|
||||
rows: results.filter ((row): row is PostImportRow => row != null),
|
||||
issues }
|
||||
}
|
||||
fieldWarnings: { },
|
||||
baseWarnings: [] } }))
|
||||
|
||||
|
||||
const PostImportSourcePage: FC<Props> = ({ user }) => {
|
||||
@@ -233,7 +113,6 @@ const PostImportSourcePage: FC<Props> = ({ user }) => {
|
||||
const [sourceError, setSourceError] = useState<string | null> (null)
|
||||
const saveTimer = useRef<number | null> (null)
|
||||
const editedRef = useRef (false)
|
||||
const previewController = useRef<AbortController | null> (null)
|
||||
|
||||
const lineCount = countImportSourceLines (source)
|
||||
const messages = sourceError != null ? [sourceError] : []
|
||||
@@ -274,10 +153,6 @@ const PostImportSourcePage: FC<Props> = ({ user }) => {
|
||||
}
|
||||
}, [source])
|
||||
|
||||
useEffect (() => () => {
|
||||
previewController.current?.abort ()
|
||||
}, [])
|
||||
|
||||
const preview = async () => {
|
||||
if (lineCount === 0)
|
||||
{
|
||||
@@ -299,42 +174,7 @@ const PostImportSourcePage: FC<Props> = ({ user }) => {
|
||||
setSourceError (null)
|
||||
try
|
||||
{
|
||||
previewController.current?.abort ()
|
||||
const controller = new AbortController ()
|
||||
previewController.current = controller
|
||||
const previewed = await fetchPreviewRows (
|
||||
source
|
||||
.split (/\r\n|\n|\r/)
|
||||
.map ((line, index) => ({
|
||||
sourceRow: index + 1,
|
||||
url: line.trim () }))
|
||||
.filter (row => row.url !== ''),
|
||||
controller.signal)
|
||||
if (previewed.issues.length > 0)
|
||||
{
|
||||
setSourceIssues (previewed.issues)
|
||||
return
|
||||
}
|
||||
const urlIssues = urlIssuesFromRows (previewed.rows, source)
|
||||
const duplicateIssues = Object.entries (
|
||||
previewed.rows.reduce<Record<string, PostImportRow[]>> ((acc, row) => {
|
||||
acc[row.url] ||= []
|
||||
acc[row.url].push (row)
|
||||
return acc
|
||||
}, { }))
|
||||
.flatMap (([, rows]) =>
|
||||
rows.length < 2
|
||||
? []
|
||||
: rows.map (row => ({
|
||||
sourceRow: row.sourceRow,
|
||||
message: 'URL が重複しています.',
|
||||
url: row.url })))
|
||||
if (urlIssues.length > 0 || duplicateIssues.length > 0)
|
||||
{
|
||||
setSourceIssues ([...urlIssues, ...duplicateIssues])
|
||||
return
|
||||
}
|
||||
const nextRows = initialisePreviewRows (previewed.rows)
|
||||
const nextRows = initialisePreviewRows (buildInitialRows (source))
|
||||
const serialised = await serialisePostNewState (nextRows)
|
||||
if (serialised == null)
|
||||
return
|
||||
@@ -355,16 +195,9 @@ const PostImportSourcePage: FC<Props> = ({ user }) => {
|
||||
|
||||
navigate (appendPostNewSessionId (serialised.path, sessionId))
|
||||
}
|
||||
catch (requestError)
|
||||
catch
|
||||
{
|
||||
const message =
|
||||
isApiError<{ message?: string, baseErrors?: string[] }> (requestError)
|
||||
? (requestError.response?.data?.message
|
||||
?? requestError.response?.data?.baseErrors?.[0])
|
||||
: undefined
|
||||
setSourceError (message ?? '入力を確認してください.')
|
||||
toast ({ title: '投稿情報の取得に失敗しました',
|
||||
description: message ?? '入力を確認してください.' })
|
||||
return
|
||||
}
|
||||
finally
|
||||
{
|
||||
|
||||
新しい課題から参照
ユーザをブロックする