このコミットが含まれているのは:
@@ -211,6 +211,13 @@ records.each {
|
|||||||
- In TypeScript and TSX, block bodies for components, functions, callbacks,
|
- In TypeScript and TSX, block bodies for components, functions, callbacks,
|
||||||
`if`, `try`, `catch`, `finally`, loops, and JSX nesting use 2 spaces per
|
`if`, `try`, `catch`, `finally`, loops, and JSX nesting use 2 spaces per
|
||||||
level.
|
level.
|
||||||
|
- In TypeScript and TSX, put the opening brace of `try`, `catch`, and
|
||||||
|
`finally` blocks on the next line at the same indentation as the keyword.
|
||||||
|
- Do not indent the opening `{` one level deeper than `try`, `catch`, or
|
||||||
|
`finally`.
|
||||||
|
- Indent the block body 2 spaces deeper than the keyword and opening brace.
|
||||||
|
- Put the closing `}` on its own line at the same indentation as the keyword.
|
||||||
|
- Do not write `try {`, `catch {`, or `finally {`.
|
||||||
- In TypeScript and TSX, use 4-space continuation indentation for wrapped
|
- In TypeScript and TSX, use 4-space continuation indentation for wrapped
|
||||||
expressions, arguments, conditions, arrays, object literals, JSX
|
expressions, arguments, conditions, arrays, object literals, JSX
|
||||||
attributes, and similar continuations.
|
attributes, and similar continuations.
|
||||||
@@ -578,6 +585,13 @@ and layout reuse, follow `frontend/AGENTS.md`.
|
|||||||
declarations, unless imports, exports, or file boundaries make that awkward.
|
declarations, unless imports, exports, or file boundaries make that awkward.
|
||||||
- In TSX, use 2-space block indentation and 4-space continuation
|
- In TSX, use 2-space block indentation and 4-space continuation
|
||||||
indentation.
|
indentation.
|
||||||
|
- In TypeScript and TSX, put the opening brace of `try`, `catch`, and
|
||||||
|
`finally` blocks on the next line at the same indentation as the keyword.
|
||||||
|
- Do not indent the opening `{` one level deeper than `try`, `catch`, or
|
||||||
|
`finally`.
|
||||||
|
- Indent the block body 2 spaces deeper than the keyword and opening brace.
|
||||||
|
- Put the closing `}` on its own line at the same indentation as the keyword.
|
||||||
|
- Do not write `try {`, `catch {`, or `finally {`.
|
||||||
- In TypeScript and TSX, convert every complete leading run of 8 spaces to a
|
- In TypeScript and TSX, convert every complete leading run of 8 spaces to a
|
||||||
tab character.
|
tab character.
|
||||||
- A leading tab is exactly equivalent to 8 leading spaces.
|
- A leading tab is exactly equivalent to 8 leading spaces.
|
||||||
@@ -628,6 +642,22 @@ and layout reuse, follow `frontend/AGENTS.md`.
|
|||||||
single physical line.
|
single physical line.
|
||||||
- Always add braces around `if`, `else`, or `for` bodies when the body spans
|
- Always add braces around `if`, `else`, or `for` bodies when the body spans
|
||||||
two or more physical lines, even if it is one statement.
|
two or more physical lines, even if it is one statement.
|
||||||
|
- `try` / `catch` / `finally` brace placement example:
|
||||||
|
|
||||||
|
```ts
|
||||||
|
try
|
||||||
|
{
|
||||||
|
doWork ()
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
recover ()
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
cleanUp ()
|
||||||
|
}
|
||||||
|
```
|
||||||
- Do not use a leading semicolon for expression statements such as
|
- Do not use a leading semicolon for expression statements such as
|
||||||
`;([...]).forEach(...)`; rewrite the expression to avoid ASI hazards
|
`;([...]).forEach(...)`; rewrite the expression to avoid ASI hazards
|
||||||
explicitly, for example with `void`.
|
explicitly, for example with `void`.
|
||||||
|
|||||||
@@ -12,9 +12,7 @@ module Preview
|
|||||||
|
|
||||||
Response = Data.define(:body, :content_type, :url)
|
Response = Data.define(:body, :content_type, :url)
|
||||||
|
|
||||||
def self.fetch(raw_url,
|
def self.fetch(raw_url, max_bytes: DEFAULT_MAX_BYTES, redirects: MAX_REDIRECTS)
|
||||||
max_bytes: DEFAULT_MAX_BYTES,
|
|
||||||
redirects: MAX_REDIRECTS)
|
|
||||||
uri, addresses = UrlSafety.validate(raw_url)
|
uri, addresses = UrlSafety.validate(raw_url)
|
||||||
response = request(uri, addresses.first, max_bytes)
|
response = request(uri, addresses.first, max_bytes)
|
||||||
|
|
||||||
@@ -69,9 +67,7 @@ module Preview
|
|||||||
log_failure(:timeout, url: uri&.to_s || raw_url, error: e.class.name, message: e.message)
|
log_failure(:timeout, url: uri&.to_s || raw_url, error: e.class.name, message: e.message)
|
||||||
raise FetchTimeout, e.message
|
raise FetchTimeout, e.message
|
||||||
rescue SocketError, SystemCallError, OpenSSL::SSL::SSLError, EOFError => e
|
rescue SocketError, SystemCallError, OpenSSL::SSL::SSLError, EOFError => e
|
||||||
log_failure(:network_error,
|
log_failure(:network_error, url: uri&.to_s || raw_url, error: e.class.name,
|
||||||
url: uri&.to_s || raw_url,
|
|
||||||
error: e.class.name,
|
|
||||||
message: e.message)
|
message: e.message)
|
||||||
raise FetchFailed, e.message
|
raise FetchFailed, e.message
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -153,6 +153,13 @@ pass or the remaining failure is clearly blocked.
|
|||||||
closing `</div>)` forms when nearby code uses them.
|
closing `</div>)` forms when nearby code uses them.
|
||||||
- Block bodies for components, functions, callbacks, `if`, `try`, `catch`,
|
- Block bodies for components, functions, callbacks, `if`, `try`, `catch`,
|
||||||
`finally`, loops, and JSX nesting use 2 spaces per level.
|
`finally`, loops, and JSX nesting use 2 spaces per level.
|
||||||
|
- Put the opening brace of `try`, `catch`, and `finally` blocks on the next
|
||||||
|
line at the same indentation as the keyword.
|
||||||
|
- Do not indent the opening `{` one level deeper than `try`, `catch`, or
|
||||||
|
`finally`.
|
||||||
|
- Indent the block body 2 spaces deeper than the keyword and opening brace.
|
||||||
|
- Put the closing `}` on its own line at the same indentation as the keyword.
|
||||||
|
- Do not write `try {`, `catch {`, or `finally {`.
|
||||||
- Wrapped expressions, arguments, ternary branches, method chains, and object
|
- Wrapped expressions, arguments, ternary branches, method chains, and object
|
||||||
pairs use 4-space continuation indentation relative to the owning
|
pairs use 4-space continuation indentation relative to the owning
|
||||||
expression. Do not confuse this with 2-space block indentation.
|
expression. Do not confuse this with 2-space block indentation.
|
||||||
@@ -229,6 +236,23 @@ const Component = () => {
|
|||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
|
- `try` / `catch` / `finally` brace placement example:
|
||||||
|
|
||||||
|
```ts
|
||||||
|
try
|
||||||
|
{
|
||||||
|
doWork ()
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
recover ()
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
cleanUp ()
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
- Continuation indentation example:
|
- Continuation indentation example:
|
||||||
|
|
||||||
```ts
|
```ts
|
||||||
|
|||||||
@@ -19,7 +19,7 @@ import type { FC, MouseEvent } from 'react'
|
|||||||
|
|
||||||
import type { Material, Menu, MenuVisibleItem, Tag, User } from '@/types'
|
import type { Material, Menu, MenuVisibleItem, Tag, User } from '@/types'
|
||||||
|
|
||||||
type Props = { user: User | null, }
|
type Props = { user: User | null }
|
||||||
|
|
||||||
|
|
||||||
export const menuOutline = (
|
export const menuOutline = (
|
||||||
|
|||||||
@@ -140,15 +140,15 @@ const DialogueProvider: FC<Props> = ({ children }) => {
|
|||||||
|
|
||||||
setPendingIds (current => [...current, id])
|
setPendingIds (current => [...current, id])
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
const shouldClose = await action.onSelect ()
|
const shouldClose = await action.onSelect ()
|
||||||
if (shouldClose !== false)
|
if (shouldClose !== false)
|
||||||
closeRequest (id)
|
closeRequest (id)
|
||||||
}
|
}
|
||||||
finally
|
finally
|
||||||
{
|
{
|
||||||
setPendingIds (current => current.filter (_1 => _1 !== id))
|
setPendingIds (current => current.filter (_1 => _1 !== id))
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
[closeRequest, pendingIds])
|
[closeRequest, pendingIds])
|
||||||
|
|
||||||
|
|||||||
@@ -119,19 +119,19 @@ const PostImportRowForm: FC<Props> = (
|
|||||||
const save = useCallback (async (): Promise<boolean> => {
|
const save = useCallback (async (): Promise<boolean> => {
|
||||||
setSaving (true)
|
setSaving (true)
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
const result = await onSave ({ draft, resetRequested })
|
const result = await onSave ({ draft, resetRequested })
|
||||||
if (result.saved)
|
if (result.saved)
|
||||||
return true
|
return true
|
||||||
|
|
||||||
if (result.row != null)
|
if (result.row != null)
|
||||||
setMessageRow (result.row)
|
setMessageRow (result.row)
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
finally
|
finally
|
||||||
{
|
{
|
||||||
setSaving (false)
|
setSaving (false)
|
||||||
}
|
}
|
||||||
}, [draft, onSave, resetRequested])
|
}, [draft, onSave, resetRequested])
|
||||||
|
|
||||||
useEffect (() => {
|
useEffect (() => {
|
||||||
|
|||||||
@@ -25,88 +25,88 @@ const summaryDate = (row: PostImportRow): string =>
|
|||||||
|
|
||||||
|
|
||||||
const PostImportRowSummary: FC<Props> = ({ row, onEdit }) => {
|
const PostImportRowSummary: FC<Props> = ({ row, onEdit }) => {
|
||||||
const warning = summaryWarning (row)
|
const warning = summaryWarning (row)
|
||||||
const displayStatus = displayPostImportStatus (row)
|
const displayStatus = displayPostImportStatus (row)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<div
|
<div
|
||||||
className={cn (
|
className={cn (
|
||||||
'hidden items-center gap-4 rounded-lg border p-4 md:grid',
|
'hidden items-center gap-4 rounded-lg border p-4 md:grid',
|
||||||
'md:grid-cols-[4rem_5rem_minmax(0,1fr)_auto_auto]',
|
'md:grid-cols-[4rem_5rem_minmax(0,1fr)_auto_auto]',
|
||||||
'transition-shadow hover:shadow-sm')}>
|
'transition-shadow hover:shadow-sm')}>
|
||||||
<div className="space-y-1">
|
<div className="space-y-1">
|
||||||
<div className="text-sm font-medium">#{row.sourceRow}</div>
|
<div className="text-sm font-medium">#{row.sourceRow}</div>
|
||||||
</div>
|
</div>
|
||||||
<ThumbnailPreview
|
<ThumbnailPreview
|
||||||
url={String (row.attributes.thumbnailBase ?? '')}
|
url={String (row.attributes.thumbnailBase ?? '')}
|
||||||
className="h-16 w-16"/>
|
className="h-16 w-16"/>
|
||||||
<div className="min-w-0 space-y-1">
|
<div className="min-w-0 space-y-1">
|
||||||
<div className="line-clamp-2 text-sm font-medium">
|
<div className="line-clamp-2 text-sm font-medium">
|
||||||
{String (row.attributes.title ?? '') || '(タイトル未取得)'}
|
{String (row.attributes.title ?? '') || '(タイトル未取得)'}
|
||||||
</div>
|
</div>
|
||||||
<div className="truncate text-xs text-neutral-600 dark:text-neutral-300">
|
<div className="truncate text-xs text-neutral-600 dark:text-neutral-300">
|
||||||
{row.url}
|
{row.url}
|
||||||
</div>
|
</div>
|
||||||
<div className="truncate text-xs text-neutral-500 dark:text-neutral-400">
|
<div className="truncate text-xs text-neutral-500 dark:text-neutral-400">
|
||||||
{String (row.attributes.tags ?? '') || 'タグなし'}
|
{String (row.attributes.tags ?? '') || 'タグなし'}
|
||||||
</div>
|
</div>
|
||||||
<div className="text-xs text-neutral-500 dark:text-neutral-400">
|
<div className="text-xs text-neutral-500 dark:text-neutral-400">
|
||||||
{summaryDate (row) || '日時未取得'}
|
{summaryDate (row) || '日時未取得'}
|
||||||
{row.attributes.duration ? ` / ${ row.attributes.duration }` : ''}
|
{row.attributes.duration ? ` / ${ row.attributes.duration }` : ''}
|
||||||
</div>
|
</div>
|
||||||
{warning && (
|
{warning && (
|
||||||
<div className="text-xs text-amber-700 dark:text-amber-200">
|
<div className="text-xs text-amber-700 dark:text-amber-200">
|
||||||
{warning}
|
{warning}
|
||||||
</div>)}
|
</div>)}
|
||||||
</div>
|
</div>
|
||||||
<div className="space-y-1">
|
<div className="space-y-1">
|
||||||
{displayStatus != null && <PostImportStatusBadge value={displayStatus}/>}
|
{displayStatus != null && <PostImportStatusBadge value={displayStatus}/>}
|
||||||
</div>
|
</div>
|
||||||
<div className="flex justify-end">
|
<div className="flex justify-end">
|
||||||
<Button
|
<Button
|
||||||
type="button"
|
type="button"
|
||||||
variant="outline"
|
variant="outline"
|
||||||
onClick={onEdit}
|
onClick={onEdit}
|
||||||
disabled={row.importStatus === 'created'}>
|
disabled={row.importStatus === 'created'}>
|
||||||
編輯
|
編輯
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div
|
<div
|
||||||
className={cn (
|
className={cn (
|
||||||
'space-y-3 rounded-lg border p-4 md:hidden',
|
'space-y-3 rounded-lg border p-4 md:hidden',
|
||||||
'transition-shadow hover:shadow-sm')}>
|
'transition-shadow hover:shadow-sm')}>
|
||||||
<div className="flex items-start gap-3">
|
<div className="flex items-start gap-3">
|
||||||
<ThumbnailPreview
|
<ThumbnailPreview
|
||||||
url={String (row.attributes.thumbnailBase ?? '')}
|
url={String (row.attributes.thumbnailBase ?? '')}
|
||||||
className="h-20 w-20 shrink-0"/>
|
className="h-20 w-20 shrink-0"/>
|
||||||
<div className="min-w-0 flex-1 space-y-2">
|
<div className="min-w-0 flex-1 space-y-2">
|
||||||
<div className="line-clamp-2 text-sm font-medium">
|
<div className="line-clamp-2 text-sm font-medium">
|
||||||
{String (row.attributes.title ?? '') || '(タイトル未取得)'}
|
{String (row.attributes.title ?? '') || '(タイトル未取得)'}
|
||||||
</div>
|
</div>
|
||||||
<div className="truncate text-xs text-neutral-600 dark:text-neutral-300">
|
<div className="truncate text-xs text-neutral-600 dark:text-neutral-300">
|
||||||
{row.url}
|
{row.url}
|
||||||
</div>
|
</div>
|
||||||
<div className="flex flex-wrap gap-2">
|
<div className="flex flex-wrap gap-2">
|
||||||
{displayStatus != null && <PostImportStatusBadge value={displayStatus}/>}
|
{displayStatus != null && <PostImportStatusBadge value={displayStatus}/>}
|
||||||
</div>
|
</div>
|
||||||
{warning && (
|
{warning && (
|
||||||
<div className="text-xs text-amber-700 dark:text-amber-200">
|
<div className="text-xs text-amber-700 dark:text-amber-200">
|
||||||
{warning}
|
{warning}
|
||||||
</div>)}
|
</div>)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<Button
|
<Button
|
||||||
type="button"
|
type="button"
|
||||||
variant="outline"
|
variant="outline"
|
||||||
onClick={onEdit}
|
onClick={onEdit}
|
||||||
disabled={row.importStatus === 'created'}>
|
disabled={row.importStatus === 'created'}>
|
||||||
編輯
|
編輯
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</>)
|
</>)
|
||||||
}
|
}
|
||||||
|
|
||||||
export default PostImportRowSummary
|
export default PostImportRowSummary
|
||||||
|
|||||||
@@ -10,40 +10,40 @@ type Props = {
|
|||||||
|
|
||||||
|
|
||||||
const ThumbnailPreview: FC<Props> = ({ url, alt = 'サムネール', className = 'h-16 w-16' }) => {
|
const ThumbnailPreview: FC<Props> = ({ url, alt = 'サムネール', className = 'h-16 w-16' }) => {
|
||||||
const [failed, setFailed] = useState (false)
|
const [failed, setFailed] = useState (false)
|
||||||
|
|
||||||
useEffect (() => {
|
useEffect (() => {
|
||||||
setFailed (false)
|
setFailed (false)
|
||||||
}, [url])
|
}, [url])
|
||||||
|
|
||||||
if (!(url))
|
if (!(url))
|
||||||
{
|
{
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className={`${ className } flex items-center justify-center rounded border
|
className={`${ className } flex items-center justify-center rounded border
|
||||||
border-border bg-muted text-xs text-muted-foreground`}>
|
border-border bg-muted text-xs text-muted-foreground`}>
|
||||||
なし
|
なし
|
||||||
</div>)
|
</div>)
|
||||||
}
|
}
|
||||||
|
|
||||||
if (failed)
|
if (failed)
|
||||||
{
|
{
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className={`${ className } flex items-center justify-center rounded border
|
className={`${ className } flex items-center justify-center rounded border
|
||||||
border-amber-300 bg-amber-50 p-2 text-center text-xs
|
border-amber-300 bg-amber-50 p-2 text-center text-xs
|
||||||
text-amber-700 dark:border-amber-900 dark:bg-amber-950
|
text-amber-700 dark:border-amber-900 dark:bg-amber-950
|
||||||
dark:text-amber-200`}>
|
dark:text-amber-200`}>
|
||||||
サムネールを表示できません
|
サムネールを表示できません
|
||||||
</div>)
|
</div>)
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<img
|
<img
|
||||||
src={url}
|
src={url}
|
||||||
alt={alt}
|
alt={alt}
|
||||||
className={`${ className } rounded border border-border object-cover`}
|
className={`${ className } rounded border border-border object-cover`}
|
||||||
onError={() => setFailed (true)}/>)
|
onError={() => setFailed (true)}/>)
|
||||||
}
|
}
|
||||||
|
|
||||||
export default ThumbnailPreview
|
export default ThumbnailPreview
|
||||||
|
|||||||
+101
-101
@@ -22,15 +22,15 @@ const buildResetSnapshot = (row: PostImportRow) => ({
|
|||||||
|
|
||||||
export const processableImportRows = (rows: PostImportRow[]): PostImportRow[] =>
|
export const processableImportRows = (rows: PostImportRow[]): PostImportRow[] =>
|
||||||
rows.filter (row => {
|
rows.filter (row => {
|
||||||
if (row.importStatus === 'created')
|
if (row.importStatus === 'created')
|
||||||
return false
|
return false
|
||||||
if (row.importStatus === 'skipped')
|
if (row.importStatus === 'skipped')
|
||||||
return false
|
return false
|
||||||
if (row.importStatus === 'failed')
|
if (row.importStatus === 'failed')
|
||||||
return false
|
return false
|
||||||
if (hasValidationErrors (row))
|
if (hasValidationErrors (row))
|
||||||
return false
|
return false
|
||||||
return row.importStatus == null || row.importStatus === 'pending'
|
return row.importStatus == null || row.importStatus === 'pending'
|
||||||
})
|
})
|
||||||
|
|
||||||
export const creatableImportRows = (rows: PostImportRow[]): PostImportRow[] =>
|
export const creatableImportRows = (rows: PostImportRow[]): PostImportRow[] =>
|
||||||
@@ -40,31 +40,31 @@ export const creatableImportRows = (rows: PostImportRow[]): PostImportRow[] =>
|
|||||||
export const reviewSummaryCounts = (rows: PostImportRow[]) => ({
|
export const reviewSummaryCounts = (rows: PostImportRow[]) => ({
|
||||||
total: rows.length,
|
total: rows.length,
|
||||||
submittable: rows.filter (row =>
|
submittable: rows.filter (row =>
|
||||||
creatableImportRows ([row]).length > 0
|
creatableImportRows ([row]).length > 0
|
||||||
&& !(hasValidationErrors (row))).length,
|
&& !(hasValidationErrors (row))).length,
|
||||||
skipPlanned: rows.filter (row =>
|
skipPlanned: rows.filter (row =>
|
||||||
processableImportRows ([row]).length > 0
|
processableImportRows ([row]).length > 0
|
||||||
&& hasSkipReason (row)).length })
|
&& hasSkipReason (row)).length })
|
||||||
|
|
||||||
|
|
||||||
export const resultSummaryCounts = (rows: PostImportRow[]) =>
|
export const resultSummaryCounts = (rows: PostImportRow[]) =>
|
||||||
rows.reduce (
|
rows.reduce (
|
||||||
(counts, row) => {
|
(counts, row) => {
|
||||||
if (row.importStatus === 'created')
|
if (row.importStatus === 'created')
|
||||||
{
|
{
|
||||||
++counts.created
|
++counts.created
|
||||||
return counts
|
return counts
|
||||||
}
|
}
|
||||||
if (row.importStatus === 'skipped')
|
if (row.importStatus === 'skipped')
|
||||||
{
|
{
|
||||||
++counts.skipped
|
++counts.skipped
|
||||||
return counts
|
return counts
|
||||||
}
|
}
|
||||||
if (row.importStatus === 'failed')
|
if (row.importStatus === 'failed')
|
||||||
++counts.failed
|
++counts.failed
|
||||||
return counts
|
return counts
|
||||||
},
|
},
|
||||||
{ created: 0, skipped: 0, failed: 0 })
|
{ created: 0, skipped: 0, failed: 0 })
|
||||||
|
|
||||||
|
|
||||||
export const mergeValidatedImportRows = (
|
export const mergeValidatedImportRows = (
|
||||||
@@ -73,46 +73,46 @@ export const mergeValidatedImportRows = (
|
|||||||
): PostImportRow[] => {
|
): PostImportRow[] => {
|
||||||
const validatedMap = new Map (validated.map (row => [row.sourceRow, row]))
|
const validatedMap = new Map (validated.map (row => [row.sourceRow, row]))
|
||||||
return current.map (previous => {
|
return current.map (previous => {
|
||||||
if (
|
if (
|
||||||
previous.importStatus === 'created'
|
previous.importStatus === 'created'
|
||||||
|| previous.importStatus === 'skipped'
|
|| previous.importStatus === 'skipped'
|
||||||
|| previous.importStatus === 'failed')
|
|| previous.importStatus === 'failed')
|
||||||
return previous
|
return previous
|
||||||
|
|
||||||
const row = validatedMap.get (previous.sourceRow)
|
const row = validatedMap.get (previous.sourceRow)
|
||||||
if (row == null)
|
if (row == null)
|
||||||
return previous
|
return previous
|
||||||
|
|
||||||
const fieldWarnings =
|
const fieldWarnings =
|
||||||
Object.keys (row.fieldWarnings).length > 0 || row.metadataUrl !== previous.metadataUrl
|
Object.keys (row.fieldWarnings).length > 0 || row.metadataUrl !== previous.metadataUrl
|
||||||
? { ...row.fieldWarnings }
|
? { ...row.fieldWarnings }
|
||||||
: { ...previous.fieldWarnings }
|
: { ...previous.fieldWarnings }
|
||||||
for (const [field, origin] of Object.entries (row.provenance))
|
for (const [field, origin] of Object.entries (row.provenance))
|
||||||
{
|
{
|
||||||
if (origin === 'manual')
|
if (origin === 'manual')
|
||||||
delete fieldWarnings[field]
|
delete fieldWarnings[field]
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
...previous,
|
...previous,
|
||||||
url: row.url,
|
url: row.url,
|
||||||
attributes: row.attributes,
|
attributes: row.attributes,
|
||||||
provenance: row.provenance,
|
provenance: row.provenance,
|
||||||
tagSources: row.tagSources,
|
tagSources: row.tagSources,
|
||||||
skipReason: row.skipReason,
|
skipReason: row.skipReason,
|
||||||
existingPostId: row.existingPostId,
|
existingPostId: row.existingPostId,
|
||||||
fieldWarnings,
|
fieldWarnings,
|
||||||
baseWarnings:
|
baseWarnings:
|
||||||
row.baseWarnings.length > 0 || row.metadataUrl !== previous.metadataUrl
|
row.baseWarnings.length > 0 || row.metadataUrl !== previous.metadataUrl
|
||||||
? row.baseWarnings
|
? row.baseWarnings
|
||||||
: previous.baseWarnings,
|
: previous.baseWarnings,
|
||||||
validationErrors: row.validationErrors,
|
validationErrors: row.validationErrors,
|
||||||
status: row.status,
|
status: row.status,
|
||||||
metadataUrl: row.metadataUrl,
|
metadataUrl: row.metadataUrl,
|
||||||
resetSnapshot:
|
resetSnapshot:
|
||||||
row.metadataUrl !== previous.metadataUrl
|
row.metadataUrl !== previous.metadataUrl
|
||||||
? buildResetSnapshot (row)
|
? buildResetSnapshot (row)
|
||||||
: previous.resetSnapshot }
|
: previous.resetSnapshot }
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -123,37 +123,37 @@ export const mergeImportResults = (
|
|||||||
): PostImportRow[] => {
|
): PostImportRow[] => {
|
||||||
const resultMap = new Map (results.map (row => [row.sourceRow, row]))
|
const resultMap = new Map (results.map (row => [row.sourceRow, row]))
|
||||||
return rows.map (row => {
|
return rows.map (row => {
|
||||||
const result = resultMap.get (row.sourceRow)
|
const result = resultMap.get (row.sourceRow)
|
||||||
if (result == null)
|
if (result == null)
|
||||||
return row
|
return row
|
||||||
|
|
||||||
switch (result.status)
|
switch (result.status)
|
||||||
{
|
{
|
||||||
case 'created':
|
case 'created':
|
||||||
return {
|
return {
|
||||||
...row,
|
...row,
|
||||||
importStatus: 'created',
|
importStatus: 'created',
|
||||||
skipReason: undefined,
|
skipReason: undefined,
|
||||||
createdPostId: result.post.id,
|
createdPostId: result.post.id,
|
||||||
existingPostId: undefined,
|
existingPostId: undefined,
|
||||||
importErrors: result.errors }
|
importErrors: result.errors }
|
||||||
case 'skipped':
|
case 'skipped':
|
||||||
return {
|
return {
|
||||||
...row,
|
...row,
|
||||||
importStatus: 'skipped',
|
importStatus: 'skipped',
|
||||||
skipReason: 'existing',
|
skipReason: 'existing',
|
||||||
createdPostId: undefined,
|
createdPostId: undefined,
|
||||||
existingPostId: result.existingPostId,
|
existingPostId: result.existingPostId,
|
||||||
importErrors: result.errors }
|
importErrors: result.errors }
|
||||||
case 'failed':
|
case 'failed':
|
||||||
return {
|
return {
|
||||||
...row,
|
...row,
|
||||||
importStatus: 'failed',
|
importStatus: 'failed',
|
||||||
skipReason: undefined,
|
skipReason: undefined,
|
||||||
createdPostId: undefined,
|
createdPostId: undefined,
|
||||||
existingPostId: undefined,
|
existingPostId: undefined,
|
||||||
importErrors: result.errors }
|
importErrors: result.errors }
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -163,11 +163,11 @@ export const retryImportRow = (
|
|||||||
sourceRow: number,
|
sourceRow: number,
|
||||||
): PostImportRow[] =>
|
): PostImportRow[] =>
|
||||||
rows.map (row =>
|
rows.map (row =>
|
||||||
row.sourceRow === sourceRow && row.importStatus === 'failed'
|
row.sourceRow === sourceRow && row.importStatus === 'failed'
|
||||||
? { ...row, importStatus: 'pending', importErrors: undefined }
|
? { ...row, importStatus: 'pending', importErrors: undefined }
|
||||||
: row)
|
: row)
|
||||||
|
|
||||||
export const initialisePreviewRows = (rows: PostImportRow[]): PostImportRow[] =>
|
export const initialisePreviewRows = (rows: PostImportRow[]): PostImportRow[] =>
|
||||||
rows.map (row => ({
|
rows.map (row => ({
|
||||||
...row,
|
...row,
|
||||||
resetSnapshot: buildResetSnapshot (row) }))
|
resetSnapshot: buildResetSnapshot (row) }))
|
||||||
|
|||||||
@@ -19,30 +19,30 @@ const normaliseImportUrl = (value: string): string | null => {
|
|||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
const url = new URL (trimmed)
|
const url = new URL (trimmed)
|
||||||
if (!(url.protocol === 'http:' || url.protocol === 'https:'))
|
if (!(url.protocol === 'http:' || url.protocol === 'https:'))
|
||||||
return null
|
return null
|
||||||
if (!(url.host))
|
if (!(url.host))
|
||||||
return null
|
return null
|
||||||
|
|
||||||
url.hostname = url.hostname.toLowerCase ()
|
url.hostname = url.hostname.toLowerCase ()
|
||||||
if (url.pathname.endsWith ('/'))
|
if (url.pathname.endsWith ('/'))
|
||||||
url.pathname = url.pathname.replace (/\/+$/, '')
|
url.pathname = url.pathname.replace (/\/+$/, '')
|
||||||
return url.toString ()
|
return url.toString ()
|
||||||
}
|
}
|
||||||
catch
|
catch
|
||||||
{
|
{
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
export const countImportSourceLines = (source: string): number =>
|
export const countImportSourceLines = (source: string): number =>
|
||||||
source
|
source
|
||||||
.split (/\r\n|\n|\r/)
|
.split (/\r\n|\n|\r/)
|
||||||
.map (_1 => _1.trim ())
|
.map (_1 => _1.trim ())
|
||||||
.filter (_1 => _1 !== '')
|
.filter (_1 => _1 !== '')
|
||||||
.length
|
.length
|
||||||
|
|
||||||
|
|
||||||
export const validateImportSource = (
|
export const validateImportSource = (
|
||||||
@@ -54,56 +54,56 @@ export const validateImportSource = (
|
|||||||
let count = 0
|
let count = 0
|
||||||
|
|
||||||
lines.forEach ((rawLine, index) => {
|
lines.forEach ((rawLine, index) => {
|
||||||
const value = rawLine.trim ()
|
const value = rawLine.trim ()
|
||||||
if (!(value))
|
if (!(value))
|
||||||
return
|
return
|
||||||
|
|
||||||
++count
|
++count
|
||||||
const sourceRow = index + 1
|
const sourceRow = index + 1
|
||||||
const displayUrl = truncateUrl (value)
|
const displayUrl = truncateUrl (value)
|
||||||
const normalised = normaliseImportUrl (value)
|
const normalised = normaliseImportUrl (value)
|
||||||
if (count > MAX_ROWS)
|
if (count > MAX_ROWS)
|
||||||
{
|
{
|
||||||
issues.push ({
|
issues.push ({
|
||||||
sourceRow,
|
sourceRow,
|
||||||
message: `取込件数は ${ MAX_ROWS } 件までです.`,
|
message: `取込件数は ${ MAX_ROWS } 件までです.`,
|
||||||
url: displayUrl })
|
url: displayUrl })
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if (!(value.startsWith ('http://') || value.startsWith ('https://')))
|
if (!(value.startsWith ('http://') || value.startsWith ('https://')))
|
||||||
{
|
{
|
||||||
issues.push ({
|
issues.push ({
|
||||||
sourceRow,
|
sourceRow,
|
||||||
message: 'HTTP または HTTPS の URL ではありません.',
|
message: 'HTTP または HTTPS の URL ではありません.',
|
||||||
url: displayUrl })
|
url: displayUrl })
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if (bytesize (value) > MAX_URL_BYTES)
|
if (bytesize (value) > MAX_URL_BYTES)
|
||||||
{
|
{
|
||||||
issues.push ({
|
issues.push ({
|
||||||
sourceRow,
|
sourceRow,
|
||||||
message: 'URL が長すぎます.',
|
message: 'URL が長すぎます.',
|
||||||
url: displayUrl })
|
url: displayUrl })
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if (normalised == null)
|
if (normalised == null)
|
||||||
{
|
{
|
||||||
issues.push ({
|
issues.push ({
|
||||||
sourceRow,
|
sourceRow,
|
||||||
message: 'URL の形式が不正です.',
|
message: 'URL の形式が不正です.',
|
||||||
url: displayUrl })
|
url: displayUrl })
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
const duplicateRow = seen.get (normalised)
|
const duplicateRow = seen.get (normalised)
|
||||||
if (duplicateRow != null)
|
if (duplicateRow != null)
|
||||||
{
|
{
|
||||||
issues.push ({
|
issues.push ({
|
||||||
sourceRow,
|
sourceRow,
|
||||||
message: `${ duplicateRow } 行目と同じ URL です.`,
|
message: `${ duplicateRow } 行目と同じ URL です.`,
|
||||||
url: displayUrl })
|
url: displayUrl })
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
seen.set (normalised, sourceRow)
|
seen.set (normalised, sourceRow)
|
||||||
})
|
})
|
||||||
|
|
||||||
return issues
|
return issues
|
||||||
|
|||||||
+101
-101
@@ -40,12 +40,12 @@ const readStorage = (
|
|||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
return sessionStorage.getItem (key)
|
return sessionStorage.getItem (key)
|
||||||
}
|
}
|
||||||
catch
|
catch
|
||||||
{
|
{
|
||||||
onError?.('保存済みデータを読み込めませんでした.')
|
onError?.('保存済みデータを読み込めませんでした.')
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -60,13 +60,13 @@ const writeStorage = (
|
|||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
sessionStorage.setItem (key, value)
|
sessionStorage.setItem (key, value)
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
catch
|
catch
|
||||||
{
|
{
|
||||||
onError?.('ブラウザへ保存できませんでした.')
|
onError?.('ブラウザへ保存できませんでした.')
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -80,11 +80,11 @@ const removeStorage = (
|
|||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
sessionStorage.removeItem (key)
|
sessionStorage.removeItem (key)
|
||||||
}
|
}
|
||||||
catch
|
catch
|
||||||
{
|
{
|
||||||
onError?.('保存済みデータを削除できませんでした.')
|
onError?.('保存済みデータを削除できませんでした.')
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -96,9 +96,9 @@ const ensureStringListRecord = (value: unknown): Record<string, string[]> | null
|
|||||||
const result: Record<string, string[]> = { }
|
const result: Record<string, string[]> = { }
|
||||||
for (const [key, entry] of Object.entries (value))
|
for (const [key, entry] of Object.entries (value))
|
||||||
{
|
{
|
||||||
if (!(Array.isArray (entry)) || !(entry.every (_1 => typeof _1 === 'string')))
|
if (!(Array.isArray (entry)) || !(entry.every (_1 => typeof _1 === 'string')))
|
||||||
return null
|
return null
|
||||||
result[key] = entry
|
result[key] = entry
|
||||||
}
|
}
|
||||||
return result
|
return result
|
||||||
}
|
}
|
||||||
@@ -149,7 +149,7 @@ const sanitiseResetSnapshot = (value: unknown): PostImportResetSnapshot | null =
|
|||||||
if (!(hasOnlyKeys (value.attributes, ATTRIBUTE_KEYS)))
|
if (!(hasOnlyKeys (value.attributes, ATTRIBUTE_KEYS)))
|
||||||
return null
|
return null
|
||||||
if (!(Object.values (value.attributes).every (entry =>
|
if (!(Object.values (value.attributes).every (entry =>
|
||||||
typeof entry === 'string' || typeof entry === 'number')))
|
typeof entry === 'string' || typeof entry === 'number')))
|
||||||
return null
|
return null
|
||||||
if (!(isPlainObject (value.provenance)))
|
if (!(isPlainObject (value.provenance)))
|
||||||
return null
|
return null
|
||||||
@@ -169,19 +169,19 @@ const sanitiseResetSnapshot = (value: unknown): PostImportResetSnapshot | null =
|
|||||||
if (!(hasOnlyKeys (fieldWarnings, WARNING_KEYS)))
|
if (!(hasOnlyKeys (fieldWarnings, WARNING_KEYS)))
|
||||||
return null
|
return null
|
||||||
if (!(Array.isArray (value.baseWarnings))
|
if (!(Array.isArray (value.baseWarnings))
|
||||||
|| !(value.baseWarnings.every (_1 => typeof _1 === 'string')))
|
|| !(value.baseWarnings.every (_1 => typeof _1 === 'string')))
|
||||||
return null
|
return null
|
||||||
if (value.metadataUrl != null && typeof value.metadataUrl !== 'string')
|
if (value.metadataUrl != null && typeof value.metadataUrl !== 'string')
|
||||||
return null
|
return null
|
||||||
|
|
||||||
return {
|
return {
|
||||||
url: value.url,
|
url: value.url,
|
||||||
attributes: value.attributes as Record<string, string | number>,
|
attributes: value.attributes as Record<string, string | number>,
|
||||||
provenance: value.provenance as Record<string, PostImportOrigin>,
|
provenance: value.provenance as Record<string, PostImportOrigin>,
|
||||||
tagSources: value.tagSources as Record<PostImportOrigin, string>,
|
tagSources: value.tagSources as Record<PostImportOrigin, string>,
|
||||||
fieldWarnings,
|
fieldWarnings,
|
||||||
baseWarnings: value.baseWarnings,
|
baseWarnings: value.baseWarnings,
|
||||||
metadataUrl: value.metadataUrl as string | undefined }
|
metadataUrl: value.metadataUrl as string | undefined }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@@ -220,20 +220,20 @@ const sanitiseRow = (value: unknown): PostImportRow | null => {
|
|||||||
return null
|
return null
|
||||||
|
|
||||||
const importErrors =
|
const importErrors =
|
||||||
value.importErrors == null
|
value.importErrors == null
|
||||||
? undefined
|
? undefined
|
||||||
: ensureStringListRecord (value.importErrors)
|
: ensureStringListRecord (value.importErrors)
|
||||||
if (importErrors === null)
|
if (importErrors === null)
|
||||||
return null
|
return null
|
||||||
if (!(Array.isArray (value.baseWarnings))
|
if (!(Array.isArray (value.baseWarnings))
|
||||||
|| !(value.baseWarnings.every (_1 => typeof _1 === 'string')))
|
|| !(value.baseWarnings.every (_1 => typeof _1 === 'string')))
|
||||||
return null
|
return null
|
||||||
|
|
||||||
const provenanceEntries = Object.entries (value.provenance)
|
const provenanceEntries = Object.entries (value.provenance)
|
||||||
if (!(hasOnlyKeys (value.attributes, ATTRIBUTE_KEYS)))
|
if (!(hasOnlyKeys (value.attributes, ATTRIBUTE_KEYS)))
|
||||||
return null
|
return null
|
||||||
if (!(Object.values (value.attributes).every (entry =>
|
if (!(Object.values (value.attributes).every (entry =>
|
||||||
typeof entry === 'string' || typeof entry === 'number')))
|
typeof entry === 'string' || typeof entry === 'number')))
|
||||||
return null
|
return null
|
||||||
if (!(hasOnlyKeys (value.provenance, PROVENANCE_KEYS)))
|
if (!(hasOnlyKeys (value.provenance, PROVENANCE_KEYS)))
|
||||||
return null
|
return null
|
||||||
@@ -242,33 +242,33 @@ const sanitiseRow = (value: unknown): PostImportRow | null => {
|
|||||||
|
|
||||||
if (value.tagSources != null)
|
if (value.tagSources != null)
|
||||||
{
|
{
|
||||||
if (!(isPlainObject (value.tagSources)))
|
if (!(isPlainObject (value.tagSources)))
|
||||||
return null
|
return null
|
||||||
if (!(hasOnlyKeys (value.tagSources, TAG_SOURCE_KEYS)))
|
if (!(hasOnlyKeys (value.tagSources, TAG_SOURCE_KEYS)))
|
||||||
return null
|
return null
|
||||||
if (!(Object.values (value.tagSources).every (_1 => typeof _1 === 'string')))
|
if (!(Object.values (value.tagSources).every (_1 => typeof _1 === 'string')))
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
sourceRow: Number (value.sourceRow),
|
sourceRow: Number (value.sourceRow),
|
||||||
url: value.url,
|
url: value.url,
|
||||||
attributes: value.attributes as Record<string, string | number>,
|
attributes: value.attributes as Record<string, string | number>,
|
||||||
fieldWarnings,
|
fieldWarnings,
|
||||||
baseWarnings: value.baseWarnings,
|
baseWarnings: value.baseWarnings,
|
||||||
validationErrors,
|
validationErrors,
|
||||||
importErrors,
|
importErrors,
|
||||||
provenance: value.provenance as Record<string, PostImportOrigin>,
|
provenance: value.provenance as Record<string, PostImportOrigin>,
|
||||||
tagSources: value.tagSources as Record<PostImportOrigin, string> | undefined,
|
tagSources: value.tagSources as Record<PostImportOrigin, string> | undefined,
|
||||||
status: value.status,
|
status: value.status,
|
||||||
skipReason: value.skipReason ?? undefined,
|
skipReason: value.skipReason ?? undefined,
|
||||||
existingPostId:
|
existingPostId:
|
||||||
isPositiveInteger (value.existingPostId) ? Number (value.existingPostId) : undefined,
|
isPositiveInteger (value.existingPostId) ? Number (value.existingPostId) : undefined,
|
||||||
metadataUrl: typeof value.metadataUrl === 'string' ? value.metadataUrl : undefined,
|
metadataUrl: typeof value.metadataUrl === 'string' ? value.metadataUrl : undefined,
|
||||||
resetSnapshot,
|
resetSnapshot,
|
||||||
createdPostId:
|
createdPostId:
|
||||||
isPositiveInteger (value.createdPostId) ? Number (value.createdPostId) : undefined,
|
isPositiveInteger (value.createdPostId) ? Number (value.createdPostId) : undefined,
|
||||||
importStatus: value.importStatus ?? undefined }
|
importStatus: value.importStatus ?? undefined }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@@ -292,34 +292,34 @@ export const cleanupExpiredPostImportSessions = (
|
|||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
for (let i = 0; i < sessionStorage.length; ++i)
|
for (let i = 0; i < sessionStorage.length; ++i)
|
||||||
{
|
{
|
||||||
const key = sessionStorage.key (i)
|
const key = sessionStorage.key (i)
|
||||||
if (key == null || !(key.startsWith (SESSION_PREFIX)))
|
if (key == null || !(key.startsWith (SESSION_PREFIX)))
|
||||||
continue
|
continue
|
||||||
|
|
||||||
const raw = sessionStorage.getItem (key)
|
const raw = sessionStorage.getItem (key)
|
||||||
if (raw == null)
|
if (raw == null)
|
||||||
continue
|
continue
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
const value = JSON.parse (raw) as { savedAt?: string }
|
const value = JSON.parse (raw) as { savedAt?: string }
|
||||||
if (typeof value.savedAt !== 'string' || isExpiredSession (value.savedAt))
|
if (typeof value.savedAt !== 'string' || isExpiredSession (value.savedAt))
|
||||||
{
|
{
|
||||||
sessionStorage.removeItem (key)
|
sessionStorage.removeItem (key)
|
||||||
--i
|
--i
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
catch
|
catch
|
||||||
{
|
{
|
||||||
sessionStorage.removeItem (key)
|
sessionStorage.removeItem (key)
|
||||||
--i
|
--i
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
catch
|
catch
|
||||||
{
|
{
|
||||||
onError?.('保存済みデータを整理できませんでした.')
|
onError?.('保存済みデータを整理できませんでした.')
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -333,12 +333,12 @@ export const loadPostImportSourceDraft = (
|
|||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
const value = JSON.parse (raw) as { source?: string }
|
const value = JSON.parse (raw) as { source?: string }
|
||||||
return { source: typeof value.source === 'string' ? value.source : '' }
|
return { source: typeof value.source === 'string' ? value.source : '' }
|
||||||
}
|
}
|
||||||
catch
|
catch
|
||||||
{
|
{
|
||||||
return { source: '' }
|
return { source: '' }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -363,12 +363,12 @@ export const savePostImportSession = (
|
|||||||
onError?: StorageErrorHandler,
|
onError?: StorageErrorHandler,
|
||||||
): boolean =>
|
): boolean =>
|
||||||
writeStorage (
|
writeStorage (
|
||||||
sessionKey (sessionId),
|
sessionKey (sessionId),
|
||||||
JSON.stringify ({
|
JSON.stringify ({
|
||||||
...session,
|
...session,
|
||||||
version: SESSION_VERSION,
|
version: SESSION_VERSION,
|
||||||
savedAt: new Date ().toISOString () }),
|
savedAt: new Date ().toISOString () }),
|
||||||
onError)
|
onError)
|
||||||
|
|
||||||
|
|
||||||
export const loadPostImportSession = (
|
export const loadPostImportSession = (
|
||||||
@@ -381,28 +381,28 @@ export const loadPostImportSession = (
|
|||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
const value = JSON.parse (raw) as Partial<PostImportSession>
|
const value = JSON.parse (raw) as Partial<PostImportSession>
|
||||||
if (value.version !== SESSION_VERSION || !(Array.isArray (value.rows)))
|
if (value.version !== SESSION_VERSION || !(Array.isArray (value.rows)))
|
||||||
return null
|
return null
|
||||||
if (typeof value.savedAt !== 'string' || isExpiredSession (value.savedAt))
|
if (typeof value.savedAt !== 'string' || isExpiredSession (value.savedAt))
|
||||||
{
|
{
|
||||||
removeStorage (sessionKey (sessionId), onError)
|
removeStorage (sessionKey (sessionId), onError)
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
|
|
||||||
const rows = value.rows.map (sanitiseRow)
|
const rows = value.rows.map (sanitiseRow)
|
||||||
if (rows.some (_1 => _1 == null))
|
if (rows.some (_1 => _1 == null))
|
||||||
return null
|
return null
|
||||||
|
|
||||||
return {
|
return {
|
||||||
version: SESSION_VERSION,
|
version: SESSION_VERSION,
|
||||||
savedAt: value.savedAt,
|
savedAt: value.savedAt,
|
||||||
source: typeof value.source === 'string' ? value.source : '',
|
source: typeof value.source === 'string' ? value.source : '',
|
||||||
rows: rows as PostImportRow[],
|
rows: rows as PostImportRow[],
|
||||||
repairMode: value.repairMode === 'failed' ? 'failed' : 'all' }
|
repairMode: value.repairMode === 'failed' ? 'failed' : 'all' }
|
||||||
}
|
}
|
||||||
catch
|
catch
|
||||||
{
|
{
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -72,92 +72,92 @@ const PostImportResultPage: FC<Props> = ({ user }) => {
|
|||||||
|
|
||||||
setLoadingRow (sourceRow)
|
setLoadingRow (sourceRow)
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
const pendingRows = retryImportRow (session.rows, sourceRow)
|
const pendingRows = retryImportRow (session.rows, sourceRow)
|
||||||
setSession ({ ...session, rows: pendingRows })
|
setSession ({ ...session, rows: pendingRows })
|
||||||
const validated = await apiPost<{ rows: PostImportRow[] }> ('/posts/import/validate', {
|
const validated = await apiPost<{ rows: PostImportRow[] }> ('/posts/import/validate', {
|
||||||
rows: pendingRows
|
rows: pendingRows
|
||||||
.filter (row => row.importStatus !== 'created')
|
.filter (row => row.importStatus !== 'created')
|
||||||
.map (row => ({
|
.map (row => ({
|
||||||
sourceRow: row.sourceRow,
|
sourceRow: row.sourceRow,
|
||||||
url: row.url,
|
url: row.url,
|
||||||
attributes: row.attributes,
|
attributes: row.attributes,
|
||||||
provenance: row.provenance,
|
provenance: row.provenance,
|
||||||
tagSources: row.tagSources,
|
tagSources: row.tagSources,
|
||||||
metadataUrl: row.metadataUrl })),
|
metadataUrl: row.metadataUrl })),
|
||||||
changed_row: -1 })
|
changed_row: -1 })
|
||||||
const validatedRows = mergeValidatedImportRows (
|
const validatedRows = mergeValidatedImportRows (
|
||||||
pendingRows,
|
pendingRows,
|
||||||
initialisePreviewRows (validated.rows))
|
initialisePreviewRows (validated.rows))
|
||||||
const nextSession = {
|
const nextSession = {
|
||||||
...session,
|
...session,
|
||||||
rows: validatedRows,
|
rows: validatedRows,
|
||||||
repairMode: 'failed' as const }
|
repairMode: 'failed' as const }
|
||||||
setSession (nextSession)
|
setSession (nextSession)
|
||||||
const target = validatedRows.find (_1 => _1.sourceRow === sourceRow)
|
const target = validatedRows.find (_1 => _1.sourceRow === sourceRow)
|
||||||
if (target == null)
|
if (target == null)
|
||||||
return
|
return
|
||||||
if (Object.keys (target.validationErrors ?? { }).length > 0)
|
if (Object.keys (target.validationErrors ?? { }).length > 0)
|
||||||
{
|
{
|
||||||
const saved = savePostImportSession (sessionId, nextSession, message =>
|
const saved = savePostImportSession (sessionId, nextSession, message =>
|
||||||
toast ({ title: '取込状態を保存できませんでした', description: message }))
|
toast ({ title: '取込状態を保存できませんでした', description: message }))
|
||||||
if (saved)
|
if (saved)
|
||||||
navigate (`/posts/import/${ sessionId }/review?edit=${ sourceRow }`)
|
navigate (`/posts/import/${ sessionId }/review?edit=${ sourceRow }`)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
const result = await apiPost<{
|
const result = await apiPost<{
|
||||||
created: number
|
created: number
|
||||||
skipped: number
|
skipped: number
|
||||||
failed: number
|
failed: number
|
||||||
rows: PostImportResultRow[] }> ('/posts/import', {
|
rows: PostImportResultRow[] }> ('/posts/import', {
|
||||||
rows: [{
|
rows: [{
|
||||||
sourceRow: target.sourceRow,
|
sourceRow: target.sourceRow,
|
||||||
url: target.url,
|
url: target.url,
|
||||||
attributes: target.attributes,
|
attributes: target.attributes,
|
||||||
provenance: target.provenance,
|
provenance: target.provenance,
|
||||||
tagSources: target.tagSources,
|
tagSources: target.tagSources,
|
||||||
metadataUrl: target.metadataUrl }] })
|
metadataUrl: target.metadataUrl }] })
|
||||||
const mergedRows = mergeImportResults (nextSession.rows, result.rows)
|
const mergedRows = mergeImportResults (nextSession.rows, result.rows)
|
||||||
const recoverableRows = result.rows.filter (row =>
|
const recoverableRows = result.rows.filter (row =>
|
||||||
row.status === 'failed'
|
row.status === 'failed'
|
||||||
&& row.recoverable
|
&& row.recoverable
|
||||||
&& Object.keys (row.errors ?? { }).length > 0)
|
&& Object.keys (row.errors ?? { }).length > 0)
|
||||||
const nextRows = mergedRows.map ((row): PostImportRow => {
|
const nextRows = mergedRows.map ((row): PostImportRow => {
|
||||||
const recoverable = recoverableRows.find (_1 => _1.sourceRow === row.sourceRow)
|
const recoverable = recoverableRows.find (_1 => _1.sourceRow === row.sourceRow)
|
||||||
if (recoverable == null)
|
if (recoverable == null)
|
||||||
return row
|
return row
|
||||||
return {
|
return {
|
||||||
...row,
|
...row,
|
||||||
importStatus: 'pending',
|
importStatus: 'pending',
|
||||||
validationErrors: recoverable.errors ?? { },
|
validationErrors: recoverable.errors ?? { },
|
||||||
importErrors: undefined }
|
importErrors: undefined }
|
||||||
})
|
})
|
||||||
const recoverableTarget = nextRows.find (_1 => _1.sourceRow === sourceRow)
|
const recoverableTarget = nextRows.find (_1 => _1.sourceRow === sourceRow)
|
||||||
const resultSession = {
|
const resultSession = {
|
||||||
...nextSession,
|
...nextSession,
|
||||||
rows: nextRows,
|
rows: nextRows,
|
||||||
repairMode: recoverableTarget == null ? 'all' as const : 'failed' as const }
|
repairMode: recoverableTarget == null ? 'all' as const : 'failed' as const }
|
||||||
setSession (resultSession)
|
setSession (resultSession)
|
||||||
if (recoverableTarget != null
|
if (recoverableTarget != null
|
||||||
&& Object.keys (recoverableTarget.validationErrors).length > 0)
|
&& Object.keys (recoverableTarget.validationErrors).length > 0)
|
||||||
{
|
{
|
||||||
const saved = savePostImportSession (sessionId, resultSession, message =>
|
const saved = savePostImportSession (sessionId, resultSession, message =>
|
||||||
toast ({ title: '取込状態を保存できませんでした', description: message }))
|
toast ({ title: '取込状態を保存できませんでした', description: message }))
|
||||||
if (saved)
|
if (saved)
|
||||||
navigate (`/posts/import/${ sessionId }/review?edit=${ sourceRow }`)
|
navigate (`/posts/import/${ sessionId }/review?edit=${ sourceRow }`)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
catch
|
catch
|
||||||
{
|
{
|
||||||
setSession (session)
|
setSession (session)
|
||||||
toast ({ title: '再試行に失敗しました' })
|
toast ({ title: '再試行に失敗しました' })
|
||||||
}
|
}
|
||||||
finally
|
finally
|
||||||
{
|
{
|
||||||
setLoadingRow (null)
|
setLoadingRow (null)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const openRepair = (sourceRow: number) => {
|
const openRepair = (sourceRow: number) => {
|
||||||
|
|||||||
新しい課題から参照
ユーザをブロックする