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