広場投稿追加画面の刷新 (#399) #413

マージ済み
みてるぞ が 103 個のコミットを feature/399 から main へマージ 2026-07-19 00:03:11 +09:00
2個のファイルの変更114行の追加99行の削除
コミット 2d2a9b4bd6 の変更だけを表示してゐます - すべてのコミットを表示
+4 -2
ファイルの表示
@@ -226,12 +226,14 @@ class Post < ApplicationRecord
end
def self.sanitised_svg_bytes(bytes)
parse_options =
Nokogiri::XML::ParseOptions::STRICT |
Nokogiri::XML::ParseOptions::NONET
document = Nokogiri::XML(
bytes,
nil,
nil,
Nokogiri::XML::ParseOptions::STRICT
| Nokogiri::XML::ParseOptions::NONET)
parse_options)
root = document.root
raise MiniMagick::Error, 'SVG が不正です.' if root == nil || root.name != 'svg'
raise MiniMagick::Error, 'SVG が不正です.' if document.internal_subset != nil
+110 -97
ファイルの表示
@@ -427,7 +427,8 @@ const PostImportReviewPage: FC<Props> = ({ user }) => {
: { duration: .2, ease: 'easeOut' as const }
const [session, setSession] = useState<PostImportSession | null> (null)
const [loading, setLoading] = useState (false)
const [metadataLoading, setMetadataLoading] = useState (false)
const [submitting, setSubmitting] = useState (false)
const [loadingRow, setLoadingRow] = useState<number | null> (null)
const [editingRow, setEditingRow] = useState<PostImportRow | null> (null)
const [showExistingRows, setShowExistingRows] = useState (false)
@@ -490,101 +491,106 @@ const PostImportReviewPage: FC<Props> = ({ user }) => {
}
const refreshRows = async (baseSession: PostImportSession) => {
setLoading (true)
let nextIndex = 0
const previewErrors = new Map<number, Record<string, string[]>> ()
let workingRows = [...(sessionRef.current ?? baseSession).rows]
if (active)
setMetadataLoading (true)
try
{
let nextIndex = 0
const previewErrors = new Map<number, Record<string, string[]>> ()
let workingRows = [...(sessionRef.current ?? baseSession).rows]
const mergeWorkingRow = (
sourceRow: number,
updater: (row: PostImportRow) => PostImportRow,
) => {
workingRows = workingRows.map (row =>
row.sourceRow === sourceRow
? updater (row)
: row)
const nextSession = buildSession (workingRows)
sessionRef.current = nextSession
setSession (nextSession)
}
const worker = async () => {
while (active)
{
const index = nextIndex
++nextIndex
if (index >= baseSession.rows.length)
return
const mergeWorkingRow = (
sourceRow: number,
updater: (row: PostImportRow) => PostImportRow,
) => {
workingRows = workingRows.map (row =>
row.sourceRow === sourceRow
? updater (row)
: row)
const nextSession = buildSession (workingRows)
sessionRef.current = nextSession
setSession (nextSession)
}
const worker = async () => {
while (active)
{
const index = nextIndex
++nextIndex
if (index >= baseSession.rows.length)
return
const baseRow = baseSession.rows[index]
if (baseRow.skipReason === 'manual'
|| baseRow.importStatus === 'created'
|| baseRow.importStatus === 'skipped'
|| isNonRecoverableFailedRow (baseRow)
|| baseRow.metadataUrl != null)
continue
const controller = new AbortController ()
controllers.add (controller)
try
{
const preview = await apiGet<PostMetadataResponse> ('/posts/metadata', {
params: { url: baseRow.url },
signal: controller.signal })
if (!(active) || previewSequenceRef.current !== previewSequence)
return
mergeWorkingRow (baseRow.sourceRow, row => mergePreviewRow (row, preview))
}
catch (requestError)
{
if (controller.signal.aborted)
return
if (!(active) || previewSequenceRef.current !== previewSequence)
return
if (!(isApiError<{
errors?: Record<string, string[]>
baseErrors?: string[]
}> (requestError)))
const baseRow = baseSession.rows[index]
if (baseRow.skipReason === 'manual'
|| baseRow.importStatus === 'created'
|| baseRow.importStatus === 'skipped'
|| isNonRecoverableFailedRow (baseRow)
|| baseRow.metadataUrl != null)
continue
if (requestError.response?.status === 422)
{
const rowErrors = {
...(requestError.response.data.errors ?? { }),
...(requestError.response.data.baseErrors?.length
? {
base: requestError.response.data.baseErrors }
: { }) }
previewErrors.set (baseRow.sourceRow, rowErrors)
mergeWorkingRow (baseRow.sourceRow, row => ({
...row,
validationErrors: rowErrors,
status: 'error' }))
}
}
finally
{
controllers.delete (controller)
const controller = new AbortController ()
controllers.add (controller)
try
{
const preview = await apiGet<PostMetadataResponse> ('/posts/metadata', {
params: { url: baseRow.url },
signal: controller.signal })
if (!(active) || previewSequenceRef.current !== previewSequence)
return
mergeWorkingRow (baseRow.sourceRow, row => mergePreviewRow (row, preview))
}
catch (requestError)
{
if (controller.signal.aborted)
return
if (!(active) || previewSequenceRef.current !== previewSequence)
return
if (!(isApiError<{
errors?: Record<string, string[]>
baseErrors?: string[]
}> (requestError)))
continue
if (requestError.response?.status === 422)
{
const rowErrors = {
...(requestError.response.data.errors ?? { }),
...(requestError.response.data.baseErrors?.length
? {
base: requestError.response.data.baseErrors }
: { }) }
previewErrors.set (baseRow.sourceRow, rowErrors)
mergeWorkingRow (baseRow.sourceRow, row => ({
...row,
validationErrors: rowErrors,
status: 'error' }))
}
}
finally
{
controllers.delete (controller)
}
}
}
}
await Promise.all (
Array.from ({ length: Math.min (4, baseSession.rows.length) }, () => worker ()))
if (!(active) || previewSequenceRef.current !== previewSequence)
{
setLoading (false)
await Promise.all (
Array.from ({ length: Math.min (4, baseSession.rows.length) }, () => worker ()))
if (!(active) || previewSequenceRef.current !== previewSequence)
return
}
const nextRows = applyDuplicateUrlErrors (workingRows.map (row => {
const rowErrors = previewErrors.get (row.sourceRow)
if (rowErrors == null)
return row
return {
...row,
validationErrors: rowErrors,
status: 'error' as const }
}))
await persistSession (buildSession (nextRows))
setLoading (false)
const nextRows = applyDuplicateUrlErrors (workingRows.map (row => {
const rowErrors = previewErrors.get (row.sourceRow)
if (rowErrors == null)
return row
return {
...row,
validationErrors: rowErrors,
status: 'error' as const }
}))
await persistSession (buildSession (nextRows))
}
finally
{
if (active)
setMetadataLoading (false)
}
}
const hydrate = async () => {
@@ -643,7 +649,7 @@ const PostImportReviewPage: FC<Props> = ({ user }) => {
: rows
const existingRows = sortedRows.filter (row => isExistingSkipRow (row))
const reviewRows = sortedRows.filter (row => !(isExistingSkipRow (row)))
const busy = loading || loadingRow != null
const busy = metadataLoading || submitting || loadingRow != null
const canSubmit =
rows.every (row => isCompletedReviewRow (row))
|| processableImportRows (rows).length > 0
@@ -856,7 +862,7 @@ const PostImportReviewPage: FC<Props> = ({ user }) => {
return
}
setLoading (true)
setSubmitting (true)
try
{
const processingRows = processableImportRows (currentSession.rows)
@@ -887,7 +893,7 @@ const PostImportReviewPage: FC<Props> = ({ user }) => {
}
finally
{
setLoading (false)
setSubmitting (false)
}
}
@@ -968,26 +974,29 @@ const PostImportReviewPage: FC<Props> = ({ user }) => {
</MainArea>
<PostImportFooter
loading={busy}
metadataLoading={metadataLoading}
submitting={submitting}
canSubmit={canSubmit}
creatableCount={counts.creatable}
manualSkippedCount={counts.manualSkipped}
existingSkippedCount={counts.existingSkipped}
pendingOrErrorCount={counts.pendingOrError}
onBack={() => navigate (-1)}
onBack={() => navigate ('/posts/new')}
onSubmit={() => submit ()}/>
</>)
}
const PostImportFooter = (
{ loading,
{ metadataLoading,
submitting,
canSubmit,
creatableCount,
manualSkippedCount,
existingSkippedCount,
pendingOrErrorCount,
onBack,
onSubmit }: { loading: boolean
onSubmit }: { metadataLoading: boolean
submitting: boolean
canSubmit: boolean
creatableCount: number
manualSkippedCount: number
@@ -1008,13 +1017,17 @@ const PostImportFooter = (
<span> {pendingOrErrorCount}</span>
</div>
<div className="flex flex-col gap-2 sm:flex-row">
<Button type="button" variant="outline" onClick={onBack} disabled={loading}>
<Button
type="button"
variant="outline"
onClick={onBack}
disabled={submitting}>
URL
</Button>
<Button
type="button"
onClick={onSubmit}
disabled={loading || !(canSubmit)}>
disabled={metadataLoading || submitting || !(canSubmit)}>
{creatableCount > 1 ? '一括追加' : '追加'}
</Button>
</div>