diff --git a/backend/app/services/post_import_runner.rb b/backend/app/services/post_import_runner.rb index 2641b6b..d21c3e5 100644 --- a/backend/app/services/post_import_runner.rb +++ b/backend/app/services/post_import_runner.rb @@ -18,6 +18,7 @@ class PostImportRunner private def run_row row + validate_row_structure!(row) attributes = row.fetch('attributes', { }).to_h.transform_keys { _1.to_s.underscore } raise ArgumentError, '取込項目が不正です.' unless (attributes.keys - ATTRIBUTE_KEYS).empty? 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.sum(&:bytesize) > PostImportSourceParser::MAX_CELL_BYTES 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 diff --git a/backend/app/services/post_import_source_parser.rb b/backend/app/services/post_import_source_parser.rb index 999cb1c..a36b8c5 100644 --- a/backend/app/services/post_import_source_parser.rb +++ b/backend/app/services/post_import_source_parser.rb @@ -44,7 +44,7 @@ class PostImportSourceParser 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 }" } }, 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 raise ArgumentError, "JSON の形式が不正です: #{ e.message }" end @@ -53,4 +53,13 @@ class PostImportSourceParser raise ArgumentError, "取込件数は #{ MAX_ROWS } 件までです." if rows.length > MAX_ROWS raise ArgumentError, 'セルが大きすぎます.' if rows.flatten.any? { _1.to_s.bytesize > MAX_CELL_BYTES } 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 diff --git a/frontend/src/pages/posts/PostImportPage.tsx b/frontend/src/pages/posts/PostImportPage.tsx index 4b5ad89..084150a 100644 --- a/frontend/src/pages/posts/PostImportPage.tsx +++ b/frontend/src/pages/posts/PostImportPage.tsx @@ -32,6 +32,7 @@ type ImportRow = { status: 'ready' | 'warning' | 'error' existing: boolean metadataUrl?: string + createdPostId?: number importStatus?: 'pending' | 'created' | 'skipped' | 'failed' } type ParsedImport = { columns: Column[], rows: { sourceRow: number, values: string[] }[] } type Phase = 'source' | 'mapping' | 'preview' | 'result' @@ -75,6 +76,10 @@ const PostImportPage: FC = ({ user }) => { existing }) setColumns (data.columns) setParsed (data) + setMappings ({ }) + setRows ([]) + setResults ([]) + setUrlColumn ('') if (data.columns.length === 1) setUrlColumn ('0') else @@ -175,6 +180,24 @@ const PostImportPage: FC = ({ 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 targets = rows.filter (row => (row as ImportRow & { importStatus?: string }).importStatus !== 'created') if (targets.length === 0) @@ -192,7 +215,9 @@ const PostImportPage: FC = ({ user }) => { setResults (result.rows) setRows (current => current.map (row => { 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 @@ -273,7 +298,7 @@ const PostImportPage: FC = ({ user }) => { - {['title', 'thumbnail_base', 'original_created_from', 'original_created_before', 'duration', 'tags', 'parent_post_ids'].map (field => @@ -290,6 +315,7 @@ const PostImportPage: FC = ({ user }) => {

インポート処理が完了しました。

{results.map (result =>
行 {result.sourceRow}:{result.status} {result.post && 投稿を開く}
)} +
)} )