このコミットが含まれているのは:
2026-07-12 00:54:21 +09:00
コミット 440a6c9961
3個のファイルの変更51行の追加3行の削除
+13
ファイルの表示
@@ -18,6 +18,7 @@ class PostImportRunner
private private
def run_row row def run_row row
validate_row_structure!(row)
attributes = row.fetch('attributes', { }).to_h.transform_keys { _1.to_s.underscore } attributes = row.fetch('attributes', { }).to_h.transform_keys { _1.to_s.underscore }
raise ArgumentError, '取込項目が不正です.' unless (attributes.keys - ATTRIBUTE_KEYS).empty? raise ArgumentError, '取込項目が不正です.' unless (attributes.keys - ATTRIBUTE_KEYS).empty?
raise ArgumentError, '取込項目が大きすぎます.' if attributes.values.any? { _1.to_s.bytesize > PostImportSourceParser::MAX_CELL_BYTES } raise ArgumentError, '取込項目が大きすぎます.' if attributes.values.any? { _1.to_s.bytesize > PostImportSourceParser::MAX_CELL_BYTES }
@@ -57,4 +58,16 @@ class PostImportRunner
raise ArgumentError if sources.values.any? { _1.bytesize > PostImportSourceParser::MAX_CELL_BYTES } raise ArgumentError if sources.values.any? { _1.bytesize > PostImportSourceParser::MAX_CELL_BYTES }
raise ArgumentError if sources.values.sum(&:bytesize) > PostImportSourceParser::MAX_CELL_BYTES raise ArgumentError if sources.values.sum(&:bytesize) > PostImportSourceParser::MAX_CELL_BYTES
end end
def validate_row_structure! row
provenance = row['provenance']
raise ArgumentError unless provenance.is_a?(Hash)
allowed = ATTRIBUTE_KEYS + ['url']
raise ArgumentError unless (provenance.keys - allowed).empty?
raise ArgumentError unless provenance.values.all? { _1.in?(['automatic', 'mapped', 'fixed', 'manual']) }
metadata_url = row['metadata_url']
raise ArgumentError unless metadata_url.nil? || metadata_url.is_a?(String)
raise ArgumentError if metadata_url.to_s.bytesize > PostImportSourceParser::MAX_CELL_BYTES
raise ArgumentError if row.to_json.bytesize > PostImportSourceParser::MAX_BYTES
end
end end
+10 -1
ファイルの表示
@@ -44,7 +44,7 @@ class PostImportSourceParser
keys = value.grep(Hash).flat_map(&:keys).map(&:to_s).uniq keys = value.grep(Hash).flat_map(&:keys).map(&:to_s).uniq
{ columns: keys.each_with_index.map { |key, index| { key:, label: "#{ ('A'.ord + index).chr }#{ key }" } }, { columns: keys.each_with_index.map { |key, index| { key:, label: "#{ ('A'.ord + index).chr }#{ 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 { item.is_a?(Hash) ? item[_1] || item[_1.to_sym] : nil } } } } values: keys.map { json_cell(item.is_a?(Hash) ? item[_1] || item[_1.to_sym] : nil) } } } }
rescue JSON::ParserError, KeyError, ArgumentError => e rescue JSON::ParserError, KeyError, ArgumentError => e
raise ArgumentError, "JSON の形式が不正です: #{ e.message }" raise ArgumentError, "JSON の形式が不正です: #{ e.message }"
end end
@@ -53,4 +53,13 @@ class PostImportSourceParser
raise ArgumentError, "取込件数は #{ MAX_ROWS } 件までです." if rows.length > MAX_ROWS raise ArgumentError, "取込件数は #{ MAX_ROWS } 件までです." if rows.length > MAX_ROWS
raise ArgumentError, 'セルが大きすぎます.' if rows.flatten.any? { _1.to_s.bytesize > MAX_CELL_BYTES } raise ArgumentError, 'セルが大きすぎます.' if rows.flatten.any? { _1.to_s.bytesize > MAX_CELL_BYTES }
end end
def json_cell value
case value
when nil then ''
when String, Numeric, TrueClass, FalseClass then value.to_s
when Array, Hash then JSON.generate(value)
else raise ArgumentError, 'JSON のセル値が不正です.'
end
end
end end
+28 -2
ファイルの表示
@@ -32,6 +32,7 @@ type ImportRow = {
status: 'ready' | 'warning' | 'error' status: 'ready' | 'warning' | 'error'
existing: boolean existing: boolean
metadataUrl?: string metadataUrl?: string
createdPostId?: number
importStatus?: 'pending' | 'created' | 'skipped' | 'failed' } importStatus?: 'pending' | 'created' | 'skipped' | 'failed' }
type ParsedImport = { columns: Column[], rows: { sourceRow: number, values: string[] }[] } type ParsedImport = { columns: Column[], rows: { sourceRow: number, values: string[] }[] }
type Phase = 'source' | 'mapping' | 'preview' | 'result' type Phase = 'source' | 'mapping' | 'preview' | 'result'
@@ -75,6 +76,10 @@ const PostImportPage: FC<Props> = ({ user }) => {
existing }) existing })
setColumns (data.columns) setColumns (data.columns)
setParsed (data) setParsed (data)
setMappings ({ })
setRows ([])
setResults ([])
setUrlColumn ('')
if (data.columns.length === 1) if (data.columns.length === 1)
setUrlColumn ('0') setUrlColumn ('0')
else else
@@ -175,6 +180,24 @@ const PostImportPage: FC<Props> = ({ user }) => {
} }
} }
const updateExisting = async (value: 'skip' | 'error') => {
setExisting (value)
const generation = ++validationGeneration.current
setLoading (true)
try
{
const validated = await apiPost<{ rows: ImportRow[] }> ('/posts/import/validate',
{ rows, existing: value, changed_row: -1 })
if (generation === validationGeneration.current)
setRows (validated.rows)
}
finally
{
if (generation === validationGeneration.current)
setLoading (false)
}
}
const submit = async () => { const submit = async () => {
const targets = rows.filter (row => (row as ImportRow & { importStatus?: string }).importStatus !== 'created') const targets = rows.filter (row => (row as ImportRow & { importStatus?: string }).importStatus !== 'created')
if (targets.length === 0) if (targets.length === 0)
@@ -192,7 +215,9 @@ const PostImportPage: FC<Props> = ({ user }) => {
setResults (result.rows) setResults (result.rows)
setRows (current => current.map (row => { setRows (current => current.map (row => {
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 } : row return resultRow ? { ...row, importStatus: resultRow.status,
createdPostId: resultRow.post?.id,
errors: resultRow.errors ?? row.errors } : row
})) }))
} }
catch catch
@@ -273,7 +298,7 @@ const PostImportPage: FC<Props> = ({ user }) => {
<label>URL <select value={urlColumn} onChange={ev => setUrlColumn (ev.target.value)}> <label>URL <select value={urlColumn} onChange={ev => setUrlColumn (ev.target.value)}>
{columns.map ((column, index) => <option key={column.key} value={index}>{column.label}</option>)} {columns.map ((column, index) => <option key={column.key} value={index}>{column.label}</option>)}
</select></label> </select></label>
<label>稿 <select value={existing} onChange={ev => setExisting (ev.target.value as typeof existing)}> <label>稿 <select value={existing} onChange={ev => updateExisting (ev.target.value as typeof existing)}>
<option value="skip"></option><option value="error"></option> <option value="skip"></option><option value="error"></option>
</select></label> </select></label>
{['title', 'thumbnail_base', 'original_created_from', 'original_created_before', 'duration', 'tags', 'parent_post_ids'].map (field => {['title', 'thumbnail_base', 'original_created_from', 'original_created_before', 'duration', 'tags', 'parent_post_ids'].map (field =>
@@ -290,6 +315,7 @@ const PostImportPage: FC<Props> = ({ user }) => {
<div className="space-y-3"><p></p> <div className="space-y-3"><p></p>
{results.map (result => <div key={result.sourceRow} className="rounded border p-2 dark:border-neutral-700"> {result.sourceRow}{result.status} {result.post && <PrefetchLink to={`/posts/${result.post.id}`}>稿</PrefetchLink>}<FieldError messages={Object.values (result.errors ?? { }).flat ()}/></div>)} {results.map (result => <div key={result.sourceRow} className="rounded border p-2 dark:border-neutral-700"> {result.sourceRow}{result.status} {result.post && <PrefetchLink to={`/posts/${result.post.id}`}>稿</PrefetchLink>}<FieldError messages={Object.values (result.errors ?? { }).flat ()}/></div>)}
<Button type="button" onClick={() => setPhase ('preview')}></Button> <Button type="button" onClick={() => setPhase ('preview')}></Button>
<Button type="button" variant="outline" onClick={() => setPhase ('preview')}></Button>
<Button type="button" variant="outline" onClick={() => setPhase ('source')}></Button> <Button type="button" variant="outline" onClick={() => setPhase ('source')}></Button>
</div>)} </div>)}
</MainArea>) </MainArea>)