このコミットが含まれているのは:
@@ -1,3 +1,4 @@
|
|||||||
|
require 'time'
|
||||||
require 'timeout'
|
require 'timeout'
|
||||||
|
|
||||||
class PostImportPreviewer
|
class PostImportPreviewer
|
||||||
@@ -86,7 +87,8 @@ class PostImportPreviewer
|
|||||||
|
|
||||||
if row[:metadata_url].present? && row[:metadata_url] != url_for_metadata
|
if row[:metadata_url].present? && row[:metadata_url] != url_for_metadata
|
||||||
clear_automatic_values!(attributes, provenance, tag_sources)
|
clear_automatic_values!(attributes, provenance, tag_sources)
|
||||||
clear_fetch_warnings!(field_warnings)
|
field_warnings = { }
|
||||||
|
base_warnings = [ ]
|
||||||
end
|
end
|
||||||
|
|
||||||
if validation_errors.blank? && normal_url.present? && existing_post
|
if validation_errors.blank? && normal_url.present? && existing_post
|
||||||
@@ -248,7 +250,8 @@ class PostImportPreviewer
|
|||||||
def sanitise_metadata_time value
|
def sanitise_metadata_time value
|
||||||
return nil unless value.is_a?(String)
|
return nil unless value.is_a?(String)
|
||||||
|
|
||||||
Time.zone.parse(value)&.iso8601
|
time = Time.iso8601(value)
|
||||||
|
time.nsec.zero? ? time.iso8601 : time.iso8601(9)
|
||||||
rescue ArgumentError, TypeError
|
rescue ArgumentError, TypeError
|
||||||
nil
|
nil
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -78,8 +78,12 @@ class PostImportRowNormaliser
|
|||||||
private_class_method :normalise_source_row!
|
private_class_method :normalise_source_row!
|
||||||
|
|
||||||
def self.normalise_url! value
|
def self.normalise_url! value
|
||||||
raise ArgumentError, 'URL の形式が不正です.' unless value.is_a?(String)
|
unless value.is_a?(String)
|
||||||
raise ArgumentError, 'URL が長すぎます.' if value.bytesize > PostImportUrlListParser::MAX_URL_BYTES
|
raise ArgumentError, 'URL の形式が不正です.'
|
||||||
|
end
|
||||||
|
if value.bytesize > PostImportUrlListParser::MAX_URL_BYTES
|
||||||
|
raise ArgumentError, 'URL が長すぎます.'
|
||||||
|
end
|
||||||
end
|
end
|
||||||
private_class_method :normalise_url!
|
private_class_method :normalise_url!
|
||||||
|
|
||||||
@@ -112,20 +116,32 @@ class PostImportRowNormaliser
|
|||||||
private_class_method :normalise_attributes!
|
private_class_method :normalise_attributes!
|
||||||
|
|
||||||
def self.normalise_provenance! provenance
|
def self.normalise_provenance! provenance
|
||||||
raise ArgumentError, 'provenance の形式が不正です.' unless provenance.is_a?(Hash)
|
unless provenance.is_a?(Hash)
|
||||||
|
raise ArgumentError, 'provenance の形式が不正です.'
|
||||||
|
end
|
||||||
|
|
||||||
allowed = ATTRIBUTE_FIELDS + ['url']
|
allowed = ATTRIBUTE_FIELDS + ['url']
|
||||||
raise ArgumentError, '値の由来が不正です.' unless (provenance.keys - allowed).empty?
|
unless (provenance.keys - allowed).empty?
|
||||||
raise ArgumentError, '値の由来が不正です.' unless provenance.values.all? { ORIGINS.include?(_1) }
|
raise ArgumentError, '値の由来が不正です.'
|
||||||
|
end
|
||||||
|
unless provenance.values.all? { ORIGINS.include?(_1) }
|
||||||
|
raise ArgumentError, '値の由来が不正です.'
|
||||||
|
end
|
||||||
end
|
end
|
||||||
private_class_method :normalise_provenance!
|
private_class_method :normalise_provenance!
|
||||||
|
|
||||||
def self.normalise_tag_sources! tag_sources
|
def self.normalise_tag_sources! tag_sources
|
||||||
return if tag_sources.nil?
|
return if tag_sources.nil?
|
||||||
|
|
||||||
raise ArgumentError, 'タグ由来の形式が不正です.' unless tag_sources.is_a?(Hash)
|
unless tag_sources.is_a?(Hash)
|
||||||
raise ArgumentError, 'タグ由来の形式が不正です.' unless (tag_sources.keys - ORIGINS).empty?
|
raise ArgumentError, 'タグ由来の形式が不正です.'
|
||||||
raise ArgumentError, 'タグ由来の形式が不正です.' unless tag_sources.values.all? { _1.is_a?(String) }
|
end
|
||||||
|
unless (tag_sources.keys - ORIGINS).empty?
|
||||||
|
raise ArgumentError, 'タグ由来の形式が不正です.'
|
||||||
|
end
|
||||||
|
unless tag_sources.values.all? { _1.is_a?(String) }
|
||||||
|
raise ArgumentError, 'タグ由来の形式が不正です.'
|
||||||
|
end
|
||||||
if tag_sources.values.any? { _1.bytesize > PostImportUrlListParser::MAX_URL_BYTES }
|
if tag_sources.values.any? { _1.bytesize > PostImportUrlListParser::MAX_URL_BYTES }
|
||||||
raise ArgumentError, 'タグ由来が大きすぎます.'
|
raise ArgumentError, 'タグ由来が大きすぎます.'
|
||||||
end
|
end
|
||||||
@@ -143,7 +159,9 @@ class PostImportRowNormaliser
|
|||||||
raise ArgumentError, '警告の形式が不正です.'
|
raise ArgumentError, '警告の形式が不正です.'
|
||||||
end
|
end
|
||||||
field_warnings&.each do |key, values|
|
field_warnings&.each do |key, values|
|
||||||
raise ArgumentError, '警告の形式が不正です.' unless ATTRIBUTE_FIELDS.include?(key) || key == 'url'
|
unless ATTRIBUTE_FIELDS.include?(key) || key == 'url'
|
||||||
|
raise ArgumentError, '警告の形式が不正です.'
|
||||||
|
end
|
||||||
unless values.is_a?(Array) && values.all? { _1.is_a?(String) }
|
unless values.is_a?(Array) && values.all? { _1.is_a?(String) }
|
||||||
raise ArgumentError, '警告の形式が不正です.'
|
raise ArgumentError, '警告の形式が不正です.'
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -26,7 +26,8 @@ class PostImportRunner
|
|||||||
attributes = row.fetch('attributes', { }).transform_keys { _1.to_s.underscore }
|
attributes = row.fetch('attributes', { }).transform_keys { _1.to_s.underscore }
|
||||||
return { source_row: row['source_row'],
|
return { source_row: row['source_row'],
|
||||||
status: 'failed',
|
status: 'failed',
|
||||||
errors: preview[:validation_errors] } if preview[:validation_errors].present?
|
errors: preview[:validation_errors],
|
||||||
|
recoverable: true } if preview[:validation_errors].present?
|
||||||
if preview[:skip_reason] == 'existing'
|
if preview[:skip_reason] == 'existing'
|
||||||
return { source_row: row['source_row'],
|
return { source_row: row['source_row'],
|
||||||
status: 'skipped',
|
status: 'skipped',
|
||||||
@@ -45,8 +46,13 @@ class PostImportRunner
|
|||||||
existing_post_id: existing_post.id }
|
existing_post_id: existing_post.id }
|
||||||
end
|
end
|
||||||
|
|
||||||
{ source_row: row['source_row'], status: 'failed', errors: e.record.errors.to_hash }
|
{ source_row: row['source_row'],
|
||||||
rescue ActiveRecord::RecordNotUnique
|
status: 'failed',
|
||||||
|
errors: e.record.errors.to_hash,
|
||||||
|
recoverable: true }
|
||||||
|
rescue ActiveRecord::RecordNotUnique => e
|
||||||
|
raise unless url_record_not_unique?(e)
|
||||||
|
|
||||||
existing_post = existing_post_for_race(row)
|
existing_post = existing_post_for_race(row)
|
||||||
raise unless existing_post
|
raise unless existing_post
|
||||||
|
|
||||||
@@ -56,15 +62,23 @@ class PostImportRunner
|
|||||||
rescue Tag::NicoTagNormalisationError
|
rescue Tag::NicoTagNormalisationError
|
||||||
{ source_row: row['source_row'],
|
{ source_row: row['source_row'],
|
||||||
status: 'failed',
|
status: 'failed',
|
||||||
errors: { tags: ['ニコニコ・タグは直接指定できません.'] } }
|
errors: { tags: ['ニコニコ・タグは直接指定できません.'] },
|
||||||
|
recoverable: true }
|
||||||
rescue Tag::DeprecatedTagNormalisationError
|
rescue Tag::DeprecatedTagNormalisationError
|
||||||
{ source_row: row['source_row'],
|
{ source_row: row['source_row'],
|
||||||
status: 'failed',
|
status: 'failed',
|
||||||
errors: { tags: ['廃止済みタグは付与できません.'] } }
|
errors: { tags: ['廃止済みタグは付与できません.'] },
|
||||||
|
recoverable: true }
|
||||||
|
rescue PostCreator::VideoMsParseError
|
||||||
|
{ source_row: row['source_row'],
|
||||||
|
status: 'failed',
|
||||||
|
errors: { duration: ['動画時間の記法が不正です.'] },
|
||||||
|
recoverable: true }
|
||||||
rescue ArgumentError
|
rescue ArgumentError
|
||||||
{ source_row: row['source_row'],
|
{ source_row: row['source_row'],
|
||||||
status: 'failed',
|
status: 'failed',
|
||||||
errors: { base: ['入力値が不正です.'] } }
|
errors: { base: ['入力値が不正です.'] },
|
||||||
|
recoverable: true }
|
||||||
rescue StandardError => e
|
rescue StandardError => e
|
||||||
Rails.logger.error("post_import_runner_failure #{ { error: e.class.name,
|
Rails.logger.error("post_import_runner_failure #{ { error: e.class.name,
|
||||||
message: e.message }.to_json }")
|
message: e.message }.to_json }")
|
||||||
@@ -83,4 +97,8 @@ class PostImportRunner
|
|||||||
|
|
||||||
Post.find_by(url: normal_url)
|
Post.find_by(url: normal_url)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def url_record_not_unique? error
|
||||||
|
error.message.include?('index_posts_on_url')
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
require 'time'
|
||||||
|
|
||||||
class PostMetadataFetcher
|
class PostMetadataFetcher
|
||||||
TIMESTAMP_PATTERN =
|
TIMESTAMP_PATTERN =
|
||||||
Regexp.new(
|
Regexp.new(
|
||||||
@@ -26,8 +28,8 @@ class PostMetadataFetcher
|
|||||||
{ title: metadata[:title],
|
{ title: metadata[:title],
|
||||||
thumbnail_base:
|
thumbnail_base:
|
||||||
Preview::KnownSiteExtractor.thumbnail_url(uri) || metadata[:image_url],
|
Preview::KnownSiteExtractor.thumbnail_url(uri) || metadata[:image_url],
|
||||||
original_created_from: created_range&.first&.iso8601,
|
original_created_from: serialise_time(created_range&.first),
|
||||||
original_created_before: created_range&.last&.iso8601,
|
original_created_before: serialise_time(created_range&.last),
|
||||||
duration: duration&.to_f&.then { _1.positive? ? (_1 * 1_000).round : nil },
|
duration: duration&.to_f&.then { _1.positive? ? (_1 * 1_000).round : nil },
|
||||||
tags: platform_tags.join(' ') }
|
tags: platform_tags.join(' ') }
|
||||||
end
|
end
|
||||||
@@ -114,9 +116,16 @@ class PostMetadataFetcher
|
|||||||
Rational(value.to_i, 10**value.length)
|
Rational(value.to_i, 10**value.length)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def self.serialise_time value
|
||||||
|
return nil if value.nil?
|
||||||
|
|
||||||
|
value.nsec.zero? ? value.iso8601 : value.iso8601(9)
|
||||||
|
end
|
||||||
|
|
||||||
private_class_method :platform_tags,
|
private_class_method :platform_tags,
|
||||||
:original_created_range,
|
:original_created_range,
|
||||||
:parse_timestamp_range,
|
:parse_timestamp_range,
|
||||||
:parse_offset,
|
:parse_offset,
|
||||||
:fractional_seconds
|
:fractional_seconds,
|
||||||
|
:serialise_time
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -73,7 +73,7 @@ const PostImportRowDialog: FC<Props> = (
|
|||||||
const [resetRequested, setResetRequested] = useState (false)
|
const [resetRequested, setResetRequested] = useState (false)
|
||||||
|
|
||||||
useEffect (() => {
|
useEffect (() => {
|
||||||
if (open && row)
|
if (open && row != null)
|
||||||
{
|
{
|
||||||
setDraft (buildDraft (row))
|
setDraft (buildDraft (row))
|
||||||
setResetRequested (false)
|
setResetRequested (false)
|
||||||
@@ -90,7 +90,7 @@ const PostImportRowDialog: FC<Props> = (
|
|||||||
value: Draft[Key],
|
value: Draft[Key],
|
||||||
) => {
|
) => {
|
||||||
setDraft (current =>
|
setDraft (current =>
|
||||||
current ? { ...current, [key]: value } : current)
|
current != null ? { ...current, [key]: value } : current)
|
||||||
}
|
}
|
||||||
|
|
||||||
const save = async () => {
|
const save = async () => {
|
||||||
|
|||||||
@@ -14,6 +14,9 @@ const buildResetSnapshot = (row: PostImportRow) => ({
|
|||||||
tagSources: {
|
tagSources: {
|
||||||
automatic: row.tagSources?.automatic ?? '',
|
automatic: row.tagSources?.automatic ?? '',
|
||||||
manual: row.tagSources?.manual ?? '' },
|
manual: row.tagSources?.manual ?? '' },
|
||||||
|
fieldWarnings: Object.fromEntries (
|
||||||
|
Object.entries (row.fieldWarnings).map (([key, values]) => [key, [...values]])),
|
||||||
|
baseWarnings: [...row.baseWarnings],
|
||||||
metadataUrl: row.metadataUrl })
|
metadataUrl: row.metadataUrl })
|
||||||
|
|
||||||
|
|
||||||
@@ -73,8 +76,7 @@ export const mergeValidatedImportRows = (
|
|||||||
if (
|
if (
|
||||||
previous.importStatus === 'created'
|
previous.importStatus === 'created'
|
||||||
|| previous.importStatus === 'skipped'
|
|| previous.importStatus === 'skipped'
|
||||||
|| previous.importStatus === 'failed'
|
|| previous.importStatus === 'failed')
|
||||||
)
|
|
||||||
return previous
|
return previous
|
||||||
|
|
||||||
const row = validatedMap.get (previous.sourceRow)
|
const row = validatedMap.get (previous.sourceRow)
|
||||||
|
|||||||
@@ -21,6 +21,7 @@ const ATTRIBUTE_KEYS = [
|
|||||||
'parentPostIds'] as const
|
'parentPostIds'] as const
|
||||||
const PROVENANCE_KEYS = [...ATTRIBUTE_KEYS, 'url'] as const
|
const PROVENANCE_KEYS = [...ATTRIBUTE_KEYS, 'url'] as const
|
||||||
const TAG_SOURCE_KEYS = ['automatic', 'manual'] as const
|
const TAG_SOURCE_KEYS = ['automatic', 'manual'] as const
|
||||||
|
const WARNING_KEYS = [...ATTRIBUTE_KEYS, 'url'] as const
|
||||||
|
|
||||||
|
|
||||||
const isPlainObject = (value: unknown): value is Record<string, unknown> =>
|
const isPlainObject = (value: unknown): value is Record<string, unknown> =>
|
||||||
@@ -162,6 +163,14 @@ const sanitiseResetSnapshot = (value: unknown): PostImportResetSnapshot | null =
|
|||||||
return null
|
return null
|
||||||
if (!(Object.values (value.tagSources).every (_1 => typeof _1 === 'string')))
|
if (!(Object.values (value.tagSources).every (_1 => typeof _1 === 'string')))
|
||||||
return null
|
return null
|
||||||
|
const fieldWarnings = ensureStringListRecord (value.fieldWarnings)
|
||||||
|
if (fieldWarnings == null)
|
||||||
|
return null
|
||||||
|
if (!(hasOnlyKeys (fieldWarnings, WARNING_KEYS)))
|
||||||
|
return null
|
||||||
|
if (!(Array.isArray (value.baseWarnings))
|
||||||
|
|| !(value.baseWarnings.every (_1 => typeof _1 === 'string')))
|
||||||
|
return null
|
||||||
if (value.metadataUrl != null && typeof value.metadataUrl !== 'string')
|
if (value.metadataUrl != null && typeof value.metadataUrl !== 'string')
|
||||||
return null
|
return null
|
||||||
|
|
||||||
@@ -170,6 +179,8 @@ const sanitiseResetSnapshot = (value: unknown): PostImportResetSnapshot | null =
|
|||||||
attributes: value.attributes as Record<string, string | number>,
|
attributes: value.attributes as Record<string, string | number>,
|
||||||
provenance: value.provenance as Record<string, PostImportOrigin>,
|
provenance: value.provenance as Record<string, PostImportOrigin>,
|
||||||
tagSources: value.tagSources as Record<PostImportOrigin, string>,
|
tagSources: value.tagSources as Record<PostImportOrigin, string>,
|
||||||
|
fieldWarnings,
|
||||||
|
baseWarnings: value.baseWarnings,
|
||||||
metadataUrl: value.metadataUrl as string | undefined }
|
metadataUrl: value.metadataUrl as string | undefined }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -14,6 +14,8 @@ export type PostImportResetSnapshot = {
|
|||||||
attributes: Record<string, PostImportAttributeValue>
|
attributes: Record<string, PostImportAttributeValue>
|
||||||
provenance: Record<string, PostImportOrigin>
|
provenance: Record<string, PostImportOrigin>
|
||||||
tagSources: Record<PostImportOrigin, string>
|
tagSources: Record<PostImportOrigin, string>
|
||||||
|
fieldWarnings: Record<string, string[]>
|
||||||
|
baseWarnings: string[]
|
||||||
metadataUrl?: string }
|
metadataUrl?: string }
|
||||||
|
|
||||||
export type PostImportRow = {
|
export type PostImportRow = {
|
||||||
@@ -48,7 +50,8 @@ export type PostImportResultRow =
|
|||||||
| {
|
| {
|
||||||
sourceRow: number
|
sourceRow: number
|
||||||
status: 'failed'
|
status: 'failed'
|
||||||
errors?: Record<string, string[]> }
|
errors?: Record<string, string[]>
|
||||||
|
recoverable?: boolean }
|
||||||
|
|
||||||
export type PostImportSession = {
|
export type PostImportSession = {
|
||||||
version: number
|
version: number
|
||||||
|
|||||||
@@ -120,10 +120,36 @@ const PostImportResultPage: FC<Props> = ({ user }) => {
|
|||||||
provenance: target.provenance,
|
provenance: target.provenance,
|
||||||
tagSources: target.tagSources,
|
tagSources: target.tagSources,
|
||||||
metadataUrl: target.metadataUrl }] })
|
metadataUrl: target.metadataUrl }] })
|
||||||
setSession (current =>
|
const mergedRows = mergeImportResults (nextSession.rows, result.rows)
|
||||||
current
|
const recoverableRows = result.rows.filter (row =>
|
||||||
? { ...current, rows: mergeImportResults (current.rows, result.rows) }
|
row.status === 'failed'
|
||||||
: current)
|
&& row.recoverable
|
||||||
|
&& Object.keys (row.errors ?? { }).length > 0)
|
||||||
|
const nextRows = mergedRows.map (row => {
|
||||||
|
const recoverable = recoverableRows.find (_1 => _1.sourceRow === row.sourceRow)
|
||||||
|
if (recoverable == null)
|
||||||
|
return row
|
||||||
|
return {
|
||||||
|
...row,
|
||||||
|
importStatus: 'pending',
|
||||||
|
validationErrors: recoverable.errors ?? { },
|
||||||
|
importErrors: undefined }
|
||||||
|
})
|
||||||
|
const recoverableTarget = nextRows.find (_1 => _1.sourceRow === sourceRow)
|
||||||
|
const resultSession = {
|
||||||
|
...nextSession,
|
||||||
|
rows: nextRows,
|
||||||
|
repairMode: recoverableTarget == null ? 'all' as const : 'failed' as const }
|
||||||
|
setSession (resultSession)
|
||||||
|
if (recoverableTarget != null
|
||||||
|
&& Object.keys (recoverableTarget.validationErrors).length > 0)
|
||||||
|
{
|
||||||
|
const saved = savePostImportSession (sessionId, resultSession, message =>
|
||||||
|
toast ({ title: '取込状態を保存できませんでした', description: message }))
|
||||||
|
if (saved)
|
||||||
|
navigate (`/posts/import/${ sessionId }/review?edit=${ sourceRow }`)
|
||||||
|
return
|
||||||
|
}
|
||||||
}
|
}
|
||||||
catch
|
catch
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -115,7 +115,7 @@ const PostImportReviewPage: FC<Props> = ({ user }) => {
|
|||||||
|
|
||||||
const updateSessionRows = (nextRows: PostImportRow[]) =>
|
const updateSessionRows = (nextRows: PostImportRow[]) =>
|
||||||
setSession (current =>
|
setSession (current =>
|
||||||
current ? { ...current, rows: nextRows } : current)
|
current != null ? { ...current, rows: nextRows } : current)
|
||||||
|
|
||||||
const saveDraft = async (
|
const saveDraft = async (
|
||||||
{ draft, resetRequested, resetSnapshot }: {
|
{ draft, resetRequested, resetSnapshot }: {
|
||||||
@@ -136,6 +136,10 @@ const PostImportReviewPage: FC<Props> = ({ user }) => {
|
|||||||
attributes: { ...resetSnapshot.attributes },
|
attributes: { ...resetSnapshot.attributes },
|
||||||
provenance: { ...resetSnapshot.provenance },
|
provenance: { ...resetSnapshot.provenance },
|
||||||
tagSources: { ...resetSnapshot.tagSources },
|
tagSources: { ...resetSnapshot.tagSources },
|
||||||
|
fieldWarnings: Object.fromEntries (
|
||||||
|
Object.entries (resetSnapshot.fieldWarnings)
|
||||||
|
.map (([key, values]) => [key, [...values]])),
|
||||||
|
baseWarnings: [...resetSnapshot.baseWarnings],
|
||||||
metadataUrl: resetSnapshot.metadataUrl }
|
metadataUrl: resetSnapshot.metadataUrl }
|
||||||
: editingRow
|
: editingRow
|
||||||
const urlChanged = draft.url !== baseRow.url
|
const urlChanged = draft.url !== baseRow.url
|
||||||
@@ -223,7 +227,37 @@ const PostImportReviewPage: FC<Props> = ({ user }) => {
|
|||||||
provenance: row.provenance,
|
provenance: row.provenance,
|
||||||
tagSources: row.tagSources,
|
tagSources: row.tagSources,
|
||||||
metadataUrl: row.metadataUrl })) })
|
metadataUrl: row.metadataUrl })) })
|
||||||
const nextRows = mergeImportResults (mergedRows, result.rows)
|
const mergedResults = mergeImportResults (mergedRows, result.rows)
|
||||||
|
const recoverableRows = result.rows.filter (row =>
|
||||||
|
row.status === 'failed'
|
||||||
|
&& row.recoverable
|
||||||
|
&& Object.keys (row.errors ?? { }).length > 0)
|
||||||
|
const nextRows = mergedResults.map (row => {
|
||||||
|
const recoverable = recoverableRows.find (_1 => _1.sourceRow === row.sourceRow)
|
||||||
|
if (recoverable == null)
|
||||||
|
return row
|
||||||
|
return {
|
||||||
|
...row,
|
||||||
|
importStatus: 'pending',
|
||||||
|
validationErrors: recoverable.errors ?? { },
|
||||||
|
importErrors: undefined }
|
||||||
|
})
|
||||||
|
const firstRecoverable = nextRows.find (row =>
|
||||||
|
Object.keys (row.validationErrors).length > 0
|
||||||
|
&& row.importStatus !== 'created')
|
||||||
|
if (firstRecoverable != null)
|
||||||
|
{
|
||||||
|
const repairSession = { ...session,
|
||||||
|
rows: nextRows,
|
||||||
|
repairMode: 'failed' as const }
|
||||||
|
setSession (repairSession)
|
||||||
|
setDialogMessageRow (firstRecoverable)
|
||||||
|
const saved = savePostImportSession (sessionId, repairSession, message =>
|
||||||
|
toast ({ title: '取込状態を保存できませんでした', description: message }))
|
||||||
|
if (saved)
|
||||||
|
setSearchParams ({ edit: String (firstRecoverable.sourceRow) })
|
||||||
|
return
|
||||||
|
}
|
||||||
const nextSession = { ...session, rows: nextRows, repairMode: 'all' as const }
|
const nextSession = { ...session, rows: nextRows, repairMode: 'all' as const }
|
||||||
setSession (nextSession)
|
setSession (nextSession)
|
||||||
const saved = savePostImportSession (sessionId, nextSession, message =>
|
const saved = savePostImportSession (sessionId, nextSession, message =>
|
||||||
|
|||||||
@@ -56,9 +56,9 @@ const PostImportSourcePage: FC<Props> = ({ user }) => {
|
|||||||
const editedRef = useRef (false)
|
const editedRef = useRef (false)
|
||||||
|
|
||||||
const lineCount = countImportSourceLines (source)
|
const lineCount = countImportSourceLines (source)
|
||||||
const messages = sourceError ? [sourceError] : []
|
const messages = sourceError != null ? [sourceError] : []
|
||||||
const sourceDescribedBy = [
|
const sourceDescribedBy = [
|
||||||
sourceError ? SOURCE_ERROR_ID : null,
|
sourceError != null ? SOURCE_ERROR_ID : null,
|
||||||
sourceIssues.length > 0 ? SOURCE_ISSUES_ID : null]
|
sourceIssues.length > 0 ? SOURCE_ISSUES_ID : null]
|
||||||
.filter (_1 => _1 != null)
|
.filter (_1 => _1 != null)
|
||||||
.join (' ')
|
.join (' ')
|
||||||
|
|||||||
新しい課題から参照
ユーザをブロックする