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