このコミットが含まれているのは:
@@ -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
|
||||
|
||||
@@ -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>
|
||||
|
||||
新しい課題から参照
ユーザをブロックする