このコミットが含まれているのは:
2026-07-12 01:17:45 +09:00
コミット 10dc776313
3個のファイルの変更35行の追加16行の削除
+2 -1
ファイルの表示
@@ -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
+9 -2
ファイルの表示
@@ -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
+24 -13
ファイルの表示
@@ -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 }) => {