このコミットが含まれているのは:
@@ -57,7 +57,8 @@ class PostImportPreviewer
|
|||||||
attributes['tags'] = merged_tags(tag_sources, provenance['tags'])
|
attributes['tags'] = merged_tags(tag_sources, provenance['tags'])
|
||||||
{ source_row: row[:source_row], url: normal_url || url, attributes:, provenance:, tag_sources:,
|
{ source_row: row[:source_row], url: normal_url || url, attributes:, provenance:, tag_sources:,
|
||||||
metadata_url: url,
|
metadata_url: url,
|
||||||
fetch_warnings:, validation_warnings:, warnings: fetch_warnings + validation_warnings, errors:,
|
fetch_warnings:, validation_warnings:, warnings: fetch_warnings + validation_warnings,
|
||||||
|
validation_errors: errors, errors:,
|
||||||
status: errors.present? ? 'error' : ((fetch_warnings + validation_warnings).present? ? 'warning' : 'ready'),
|
status: errors.present? ? 'error' : ((fetch_warnings + validation_warnings).present? ? 'warning' : 'ready'),
|
||||||
existing: normal_url.present? && Post.exists?(url: normal_url) }
|
existing: normal_url.present? && Post.exists?(url: normal_url) }
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -43,11 +43,18 @@ class PostImportSourceParser
|
|||||||
raise ArgumentError, 'JSON の投稿候補は配列にしてください.' unless value.is_a?(Array)
|
raise ArgumentError, 'JSON の投稿候補は配列にしてください.' unless value.is_a?(Array)
|
||||||
|
|
||||||
ensure_limits!(value)
|
ensure_limits!(value)
|
||||||
keys = value.grep(Hash).flat_map(&:keys).map(&:to_s).uniq
|
unless value.all?(Hash)
|
||||||
|
invalid = value.each_with_index.filter_map { |item, index| index + 1 unless item.is_a?(Hash) }
|
||||||
|
raise ArgumentError, "JSON の投稿候補は object の配列にしてください: 行 #{ invalid.join(', ') }"
|
||||||
|
end
|
||||||
|
keys = value.flat_map(&:keys).map(&:to_s).uniq
|
||||||
raise ArgumentError, "列数は #{ MAX_COLUMNS } 列までです." if keys.length > MAX_COLUMNS
|
raise ArgumentError, "列数は #{ MAX_COLUMNS } 列までです." if keys.length > MAX_COLUMNS
|
||||||
{ columns: keys.each_with_index.map { |key, index| { key:, label: "#{ column_name(index) }:#{ key }" } },
|
{ columns: keys.each_with_index.map { |key, index| { key:, label: "#{ column_name(index) }:#{ key }" } },
|
||||||
rows: value.each_with_index.map { |item, index| { source_row: index + 1,
|
rows: value.each_with_index.map { |item, index| { source_row: index + 1,
|
||||||
values: keys.map { json_cell(item.is_a?(Hash) ? item[_1] || item[_1.to_sym] : nil) } } } }
|
values: keys.map { |key|
|
||||||
|
value = item.key?(key) ? item[key] : item[key.to_sym]
|
||||||
|
json_cell(value)
|
||||||
|
} } } }
|
||||||
rescue JSON::ParserError, KeyError, ArgumentError => e
|
rescue JSON::ParserError, KeyError, ArgumentError => e
|
||||||
raise ArgumentError, "JSON の形式が不正です: #{ e.message }"
|
raise ArgumentError, "JSON の形式が不正です: #{ e.message }"
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -26,7 +26,8 @@ type ImportRow = {
|
|||||||
url: string
|
url: string
|
||||||
attributes: Record<string, string>
|
attributes: Record<string, string>
|
||||||
warnings: string[]
|
warnings: string[]
|
||||||
errors: Record<string, string[]>
|
validationErrors: Record<string, string[]>
|
||||||
|
importErrors?: Record<string, string[]>
|
||||||
provenance: Record<string, 'automatic' | 'mapped' | 'fixed' | 'manual'>
|
provenance: Record<string, 'automatic' | 'mapped' | 'fixed' | 'manual'>
|
||||||
tagSources?: Record<'automatic' | 'mapped' | 'fixed' | 'manual', string>
|
tagSources?: Record<'automatic' | 'mapped' | 'fixed' | 'manual', string>
|
||||||
status: 'ready' | 'warning' | 'error'
|
status: 'ready' | 'warning' | 'error'
|
||||||
@@ -127,19 +128,24 @@ const PostImportPage: FC<Props> = ({ user }) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const mergeValidatedRows = (current: ImportRow[], validated: ImportRow[]): ImportRow[] =>
|
const mergeValidatedRows = (current: ImportRow[], validated: ImportRow[]): ImportRow[] =>
|
||||||
current.filter (row => row.importStatus === 'created').concat (validated.map (row => {
|
current.map (previous => {
|
||||||
const previous = current.find (_1 => _1.sourceRow === row.sourceRow)
|
if (previous.importStatus === 'created')
|
||||||
return previous ? { ...row,
|
return previous
|
||||||
importStatus: previous.importStatus,
|
const row = validated.find (_1 => _1.sourceRow === previous.sourceRow)
|
||||||
createdPostId: previous.createdPostId,
|
return row ? { ...row,
|
||||||
errors: previous.importStatus === 'failed' ? previous.errors : row.errors } : row
|
importStatus: previous.importStatus,
|
||||||
}))
|
createdPostId: previous.createdPostId,
|
||||||
|
importErrors: previous.importErrors,
|
||||||
|
validationErrors: row.validationErrors ?? row.errors ?? { } } : previous
|
||||||
|
}).concat (validated.filter (row => !current.some (_1 => _1.sourceRow === row.sourceRow)))
|
||||||
|
|
||||||
const updateAttribute = async (index: number, field: string, value: string) => {
|
const updateAttribute = async (index: number, field: string, value: string) => {
|
||||||
const row = rows[index]
|
const row = rows[index]
|
||||||
const next = { ...row,
|
const next = { ...row,
|
||||||
attributes: { ...row.attributes, [field]: value },
|
attributes: { ...row.attributes, [field]: value },
|
||||||
provenance: { ...row.provenance, [field]: 'manual' } }
|
provenance: { ...row.provenance, [field]: 'manual' },
|
||||||
|
importStatus: 'pending' as const,
|
||||||
|
importErrors: undefined }
|
||||||
const nextRows = rows.map ((currentRow, rowIndex) => rowIndex === index ? next : currentRow)
|
const nextRows = rows.map ((currentRow, rowIndex) => rowIndex === index ? next : currentRow)
|
||||||
setRows (nextRows)
|
setRows (nextRows)
|
||||||
const generation = ++validationGeneration.current
|
const generation = ++validationGeneration.current
|
||||||
@@ -165,7 +171,8 @@ const PostImportPage: FC<Props> = ({ user }) => {
|
|||||||
|
|
||||||
const updateURL = async (index: number, value: string) => {
|
const updateURL = async (index: number, value: string) => {
|
||||||
const row = rows[index]
|
const row = rows[index]
|
||||||
const next = { ...row, url: value, provenance: { ...row.provenance, url: 'manual' } }
|
const next = { ...row, url: value, provenance: { ...row.provenance, url: 'manual' },
|
||||||
|
importStatus: 'pending' as const, importErrors: undefined }
|
||||||
const nextRows = rows.map ((currentRow, rowIndex) => rowIndex === index ? next : currentRow)
|
const nextRows = rows.map ((currentRow, rowIndex) => rowIndex === index ? next : currentRow)
|
||||||
setRows (nextRows)
|
setRows (nextRows)
|
||||||
const generation = ++validationGeneration.current
|
const generation = ++validationGeneration.current
|
||||||
@@ -226,7 +233,9 @@ const PostImportPage: FC<Props> = ({ user }) => {
|
|||||||
const resultRow = result.rows.find (_1 => _1.sourceRow === row.sourceRow)
|
const resultRow = result.rows.find (_1 => _1.sourceRow === row.sourceRow)
|
||||||
return resultRow ? { ...row, importStatus: resultRow.status,
|
return resultRow ? { ...row, importStatus: resultRow.status,
|
||||||
createdPostId: resultRow.post?.id,
|
createdPostId: resultRow.post?.id,
|
||||||
errors: resultRow.errors ?? row.errors } : row
|
importErrors: resultRow.errors,
|
||||||
|
importStatus: resultRow.status,
|
||||||
|
validationErrors: row.validationErrors } : row
|
||||||
}))
|
}))
|
||||||
}
|
}
|
||||||
catch
|
catch
|
||||||
@@ -334,7 +343,7 @@ const PostImportPage: FC<Props> = ({ user }) => {
|
|||||||
const RowCard = ({ row, index, update, updateURL }: { row: ImportRow, index: number, update: (index: number, field: string, value: string) => void, updateURL: (index: number, value: string) => void }) => (
|
const RowCard = ({ row, index, update, updateURL }: { row: ImportRow, index: number, update: (index: number, field: string, value: string) => void, updateURL: (index: number, value: string) => void }) => (
|
||||||
<article className="space-y-2 rounded border bg-white p-3 text-neutral-900 dark:border-neutral-700 dark:bg-neutral-900 dark:text-neutral-100">
|
<article className="space-y-2 rounded border bg-white p-3 text-neutral-900 dark:border-neutral-700 dark:bg-neutral-900 dark:text-neutral-100">
|
||||||
<p>行 {row.sourceRow}・{row.status}</p>
|
<p>行 {row.sourceRow}・{row.status}</p>
|
||||||
{row.importStatus === 'created' ? <p className="rounded bg-green-100 p-2 dark:bg-green-900">登録済み。この行は編集・再登録できません。</p> : <>
|
{row.importStatus === 'created' ? <p className="rounded bg-green-100 p-2 dark:bg-green-900">登録済み。この行は編集・再登録できません。{row.createdPostId && <PrefetchLink to={`/posts/${row.createdPostId}`}>投稿を開く</PrefetchLink>}</p> : <>
|
||||||
<EditableValue label="URL" value={row.url} origin={row.provenance.url} onCommit={value => updateURL (index, value)}/>
|
<EditableValue label="URL" value={row.url} origin={row.provenance.url} onCommit={value => updateURL (index, value)}/>
|
||||||
<EditableValue label="タイトル" value={row.attributes.title ?? ''} origin={row.provenance.title} onCommit={value => update (index, 'title', value)}/>
|
<EditableValue label="タイトル" value={row.attributes.title ?? ''} origin={row.provenance.title} onCommit={value => update (index, 'title', value)}/>
|
||||||
<EditableValue label="サムネール基底 URL" value={row.attributes.thumbnailBase ?? ''} origin={row.provenance.thumbnailBase} onCommit={value => update (index, 'thumbnailBase', value)}/>
|
<EditableValue label="サムネール基底 URL" value={row.attributes.thumbnailBase ?? ''} origin={row.provenance.thumbnailBase} onCommit={value => update (index, 'thumbnailBase', value)}/>
|
||||||
@@ -345,7 +354,9 @@ const RowCard = ({ row, index, update, updateURL }: { row: ImportRow, index: num
|
|||||||
<EditableValue label="タグ" value={row.attributes.tags ?? ''} origin={row.provenance.tags} onCommit={value => update (index, 'tags', value)}/>
|
<EditableValue label="タグ" value={row.attributes.tags ?? ''} origin={row.provenance.tags} onCommit={value => update (index, 'tags', value)}/>
|
||||||
<EditableValue label="親投稿" value={row.attributes.parentPostIds ?? ''} origin={row.provenance.parentPostIds} onCommit={value => update (index, 'parentPostIds', value)}/>
|
<EditableValue label="親投稿" value={row.attributes.parentPostIds ?? ''} origin={row.provenance.parentPostIds} onCommit={value => update (index, 'parentPostIds', value)}/>
|
||||||
</>}
|
</>}
|
||||||
<FieldError messages={[...row.warnings, ...Object.values (row.errors).flat ()]}/>
|
<FieldError messages={[...row.warnings,
|
||||||
|
...Object.values (row.validationErrors ?? { }).flat (),
|
||||||
|
...Object.values (row.importErrors ?? { }).flat ()]}/>
|
||||||
</article>)
|
</article>)
|
||||||
|
|
||||||
const EditableValue = ({ label, value, origin, onCommit }: { label: string, value: string, origin: string | undefined, onCommit: (value: string) => void }) => {
|
const EditableValue = ({ label, value, origin, onCommit }: { label: string, value: string, origin: string | undefined, onCommit: (value: string) => void }) => {
|
||||||
|
|||||||
新しい課題から参照
ユーザをブロックする