このコミットが含まれているのは:
@@ -2,6 +2,8 @@ import type { PostImportEditableDraft,
|
||||
PostImportResultRow,
|
||||
PostImportRow } from '@/lib/postImportTypes'
|
||||
|
||||
const THUMBNAIL_MISSING_WARNING = 'サムネールなし'
|
||||
|
||||
export const isExistingSkipRow = (row: PostImportRow): boolean =>
|
||||
row.skipReason === 'existing'
|
||||
|
||||
@@ -55,6 +57,29 @@ const buildResetSnapshot = (row: PostImportRow) => ({
|
||||
baseWarnings: [...row.baseWarnings],
|
||||
metadataUrl: row.metadataUrl })
|
||||
|
||||
const deduped = (values: string[]): string[] =>
|
||||
[...new Set (values)]
|
||||
|
||||
|
||||
const thumbnailWarnings = (row: PostImportRow): string[] => {
|
||||
const current = row.fieldWarnings.thumbnailBase ?? []
|
||||
const others = current.filter (message => message !== THUMBNAIL_MISSING_WARNING)
|
||||
return row.attributes.thumbnailBase || row.thumbnailFile != null
|
||||
? others
|
||||
: deduped ([...others, THUMBNAIL_MISSING_WARNING])
|
||||
}
|
||||
|
||||
|
||||
export const applyThumbnailWarning = (row: PostImportRow): PostImportRow => ({
|
||||
...row,
|
||||
fieldWarnings: {
|
||||
...row.fieldWarnings,
|
||||
thumbnailBase: thumbnailWarnings (row) } })
|
||||
|
||||
|
||||
export const applyThumbnailWarnings = (rows: PostImportRow[]): PostImportRow[] =>
|
||||
rows.map (row => applyThumbnailWarning (row))
|
||||
|
||||
|
||||
export const processableImportRows = (rows: PostImportRow[]): PostImportRow[] =>
|
||||
validatableImportRows (rows).filter (row => {
|
||||
@@ -182,6 +207,7 @@ export const buildNextEditedRow = (
|
||||
...editingRow,
|
||||
url: draft.url,
|
||||
attributes: nextAttributes,
|
||||
thumbnailFile: draft.thumbnailFile,
|
||||
provenance: {
|
||||
...nextProvenance,
|
||||
url: urlChanged ? 'manual' : (editingRow.provenance.url ?? 'manual') },
|
||||
@@ -258,6 +284,7 @@ export const mergeValidatedImportRows = (
|
||||
tagSources: row.tagSources,
|
||||
skipReason: row.skipReason,
|
||||
existingPostId: row.existingPostId,
|
||||
existingPost: row.existingPost,
|
||||
fieldWarnings,
|
||||
baseWarnings:
|
||||
row.baseWarnings.length > 0 || row.metadataUrl !== previous.metadataUrl
|
||||
@@ -294,6 +321,7 @@ export const mergeImportResults = (
|
||||
skipReason: undefined,
|
||||
createdPostId: result.post.id,
|
||||
existingPostId: undefined,
|
||||
existingPost: undefined,
|
||||
fieldWarnings: result.fieldWarnings ?? row.fieldWarnings,
|
||||
baseWarnings: result.baseWarnings ?? row.baseWarnings,
|
||||
importErrors: result.errors }
|
||||
@@ -305,6 +333,7 @@ export const mergeImportResults = (
|
||||
skipReason: 'existing',
|
||||
createdPostId: undefined,
|
||||
existingPostId: result.existingPostId,
|
||||
existingPost: result.existingPost ?? row.existingPost,
|
||||
fieldWarnings: result.fieldWarnings ?? row.fieldWarnings,
|
||||
baseWarnings: result.baseWarnings ?? row.baseWarnings,
|
||||
importErrors: result.errors }
|
||||
@@ -316,6 +345,7 @@ export const mergeImportResults = (
|
||||
skipReason: undefined,
|
||||
createdPostId: undefined,
|
||||
existingPostId: undefined,
|
||||
existingPost: undefined,
|
||||
fieldWarnings: result.fieldWarnings ?? row.fieldWarnings,
|
||||
baseWarnings: result.baseWarnings ?? row.baseWarnings,
|
||||
importErrors: result.errors }
|
||||
@@ -336,6 +366,7 @@ export const retryImportRow = (
|
||||
: row)
|
||||
|
||||
export const initialisePreviewRows = (rows: PostImportRow[]): PostImportRow[] =>
|
||||
rows.map (row => ({
|
||||
...row,
|
||||
resetSnapshot: buildResetSnapshot (row) }))
|
||||
applyThumbnailWarnings (
|
||||
rows.map (row => ({
|
||||
...row,
|
||||
resetSnapshot: buildResetSnapshot (row) })))
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import type { PostImportOrigin,
|
||||
PostImportExistingPost,
|
||||
PostImportResetSnapshot,
|
||||
PostImportRow,
|
||||
PostImportSession,
|
||||
@@ -200,6 +201,25 @@ const sanitiseResetSnapshot = (value: unknown): PostImportResetSnapshot | null =
|
||||
metadataUrl: value.metadataUrl as string | undefined }
|
||||
}
|
||||
|
||||
const sanitiseExistingPost = (value: unknown): PostImportExistingPost | undefined => {
|
||||
if (value == null)
|
||||
return undefined
|
||||
if (!(isPlainObject (value)))
|
||||
return undefined
|
||||
if (!(isPositiveInteger (value.id)))
|
||||
return undefined
|
||||
if (typeof value.title !== 'string' || typeof value.url !== 'string')
|
||||
return undefined
|
||||
if (value.thumbnailUrl != null && typeof value.thumbnailUrl !== 'string')
|
||||
return undefined
|
||||
|
||||
return {
|
||||
id: Number (value.id),
|
||||
title: value.title,
|
||||
url: value.url,
|
||||
thumbnailUrl: value.thumbnailUrl as string | undefined }
|
||||
}
|
||||
|
||||
|
||||
const sanitiseRow = (value: unknown): PostImportRow | null => {
|
||||
if (!(isPlainObject (value)))
|
||||
@@ -289,6 +309,7 @@ const sanitiseRow = (value: unknown): PostImportRow | null => {
|
||||
skipReason: value.skipReason ?? undefined,
|
||||
existingPostId:
|
||||
isPositiveInteger (value.existingPostId) ? Number (value.existingPostId) : undefined,
|
||||
existingPost: sanitiseExistingPost (value.existingPost),
|
||||
metadataUrl: typeof value.metadataUrl === 'string' ? value.metadataUrl : undefined,
|
||||
resetSnapshot,
|
||||
createdPostId:
|
||||
@@ -297,6 +318,26 @@ const sanitiseRow = (value: unknown): PostImportRow | null => {
|
||||
recoverable: value.recoverable === true ? true : undefined }
|
||||
}
|
||||
|
||||
const sanitiseSession = (value: unknown): PostImportSession | null => {
|
||||
if (!(isPlainObject (value)))
|
||||
return null
|
||||
if (value.version !== SESSION_VERSION || !(Array.isArray (value.rows)))
|
||||
return null
|
||||
if (typeof value.expiresAt !== 'string' || isExpiredSession (value.expiresAt))
|
||||
return null
|
||||
|
||||
const rows = value.rows.map (sanitiseRow)
|
||||
if (rows.some (row => row == null))
|
||||
return null
|
||||
|
||||
return {
|
||||
version: SESSION_VERSION,
|
||||
expiresAt: value.expiresAt,
|
||||
source: typeof value.source === 'string' ? value.source : '',
|
||||
rows: rows as PostImportRow[],
|
||||
repairMode: value.repairMode === 'failed' ? 'failed' : 'all' }
|
||||
}
|
||||
|
||||
|
||||
const isExpiredSession = (expiresAt: string): boolean => {
|
||||
const value = Date.parse (expiresAt)
|
||||
@@ -332,11 +373,7 @@ export const cleanupExpiredPostImportSessions = (
|
||||
|
||||
try
|
||||
{
|
||||
const value = JSON.parse (raw) as { expiresAt?: string
|
||||
rows?: unknown[] }
|
||||
if (typeof value.expiresAt !== 'string'
|
||||
|| isExpiredSession (value.expiresAt)
|
||||
|| !(Array.isArray (value.rows)))
|
||||
if (sanitiseSession (JSON.parse (raw)) == null)
|
||||
{
|
||||
sessionStorage.removeItem (key)
|
||||
--i
|
||||
@@ -422,25 +459,13 @@ export const loadPostImportSession = (
|
||||
|
||||
try
|
||||
{
|
||||
const value = JSON.parse (raw) as Partial<PostImportSession>
|
||||
if (value.version !== SESSION_VERSION || !(Array.isArray (value.rows)))
|
||||
return null
|
||||
if (typeof value.expiresAt !== 'string' || isExpiredSession (value.expiresAt))
|
||||
const session = sanitiseSession (JSON.parse (raw))
|
||||
if (session == null)
|
||||
{
|
||||
removeStorage (sessionKey (validatedId), onError)
|
||||
return null
|
||||
}
|
||||
|
||||
const rows = value.rows.map (sanitiseRow)
|
||||
if (rows.some (row => row == null))
|
||||
return null
|
||||
|
||||
return {
|
||||
version: SESSION_VERSION,
|
||||
expiresAt: value.expiresAt,
|
||||
source: typeof value.source === 'string' ? value.source : '',
|
||||
rows: rows as PostImportRow[],
|
||||
repairMode: value.repairMode === 'failed' ? 'failed' : 'all' }
|
||||
return session
|
||||
}
|
||||
catch
|
||||
{
|
||||
|
||||
@@ -18,6 +18,12 @@ export type PostImportResetSnapshot = {
|
||||
baseWarnings: string[]
|
||||
metadataUrl?: string }
|
||||
|
||||
export type PostImportExistingPost = {
|
||||
id: number
|
||||
title: string
|
||||
url: string
|
||||
thumbnailUrl?: string }
|
||||
|
||||
export type PostImportRow = {
|
||||
sourceRow: number
|
||||
url: string
|
||||
@@ -31,6 +37,7 @@ export type PostImportRow = {
|
||||
status: 'ready' | 'warning' | 'error'
|
||||
skipReason?: PostImportSkipReason
|
||||
existingPostId?: number
|
||||
existingPost?: PostImportExistingPost
|
||||
metadataUrl?: string
|
||||
resetSnapshot: PostImportResetSnapshot
|
||||
createdPostId?: number
|
||||
@@ -51,6 +58,7 @@ export type PostImportResultRow =
|
||||
sourceRow: number
|
||||
status: 'skipped'
|
||||
existingPostId: number
|
||||
existingPost?: PostImportExistingPost
|
||||
fieldWarnings?: Record<string, string[]>
|
||||
baseWarnings?: string[]
|
||||
errors?: Record<string, string[]> }
|
||||
@@ -81,6 +89,7 @@ export type PostImportEditableDraft = {
|
||||
originalCreatedFrom: string
|
||||
originalCreatedBefore: string
|
||||
tags: string
|
||||
parentPostIds: string }
|
||||
parentPostIds: string
|
||||
thumbnailFile?: File }
|
||||
|
||||
export type StorageErrorHandler = (message: string) => void
|
||||
|
||||
@@ -9,6 +9,7 @@ import type {
|
||||
} from '@/lib/postImportTypes'
|
||||
|
||||
type QueryRowState = {
|
||||
source_row: number
|
||||
title: string
|
||||
thumbnail_base: string
|
||||
original_created_from: string
|
||||
@@ -34,6 +35,7 @@ type EncodedSkip = 'e' | 'm'
|
||||
type EncodedImportStatus = 'p' | 'c' | 's' | 'f'
|
||||
|
||||
type EncodedRowState = {
|
||||
n?: number
|
||||
u?: string
|
||||
t?: string
|
||||
h?: string
|
||||
@@ -241,6 +243,7 @@ const manualFields = (row: PostImportRow): string[] =>
|
||||
|
||||
|
||||
const baseRowState = (row: PostImportRow): QueryRowState => ({
|
||||
source_row: row.sourceRow,
|
||||
title: String (row.attributes.title ?? ''),
|
||||
thumbnail_base: String (row.attributes.thumbnailBase ?? ''),
|
||||
original_created_from: String (row.attributes.originalCreatedFrom ?? ''),
|
||||
@@ -257,10 +260,7 @@ const baseRowState = (row: PostImportRow): QueryRowState => ({
|
||||
recoverable: row.recoverable ?? null })
|
||||
|
||||
|
||||
const buildRow = (
|
||||
sourceRow: number,
|
||||
state: FullRowState,
|
||||
): PostImportRow => {
|
||||
const buildRow = (state: FullRowState): PostImportRow => {
|
||||
const manual = new Set (state.manual)
|
||||
const provenance: Record<string, PostImportOrigin> = {
|
||||
url: 'manual',
|
||||
@@ -286,7 +286,7 @@ const buildRow = (
|
||||
parentPostIds: state.parent_post_ids }
|
||||
|
||||
return {
|
||||
sourceRow,
|
||||
sourceRow: state.source_row,
|
||||
url: state.url,
|
||||
attributes,
|
||||
fieldWarnings: { },
|
||||
@@ -336,6 +336,7 @@ const parseEncodedRow = (
|
||||
const existingPostId = row['e']
|
||||
const createdPostId = row['c']
|
||||
const recoverable = row['r']
|
||||
const sourceRow = row['n']
|
||||
|
||||
if (requireUrl && typeof url !== 'string')
|
||||
return null
|
||||
@@ -357,6 +358,11 @@ const parseEncodedRow = (
|
||||
return null
|
||||
if (recoverable != null && typeof recoverable !== 'boolean')
|
||||
return null
|
||||
if (sourceRow != null
|
||||
&& !(typeof sourceRow === 'number'
|
||||
&& Number.isInteger (sourceRow)
|
||||
&& sourceRow > 0))
|
||||
return null
|
||||
|
||||
const stringFields = [
|
||||
['t', 'title'],
|
||||
@@ -385,6 +391,10 @@ const parseEncodedRow = (
|
||||
}
|
||||
|
||||
return {
|
||||
source_row:
|
||||
typeof sourceRow === 'number'
|
||||
? sourceRow
|
||||
: 1,
|
||||
url: typeof url === 'string' ? url : '',
|
||||
title: parsedStrings.title,
|
||||
thumbnail_base: parsedStrings.thumbnail_base,
|
||||
@@ -412,7 +422,8 @@ const parseSingleRow = (params: URLSearchParams): ParsedPostNewState | null => {
|
||||
key => key === 'url' || key === 'session_id')
|
||||
if (hasOnlyUrl)
|
||||
{
|
||||
const row = buildRow (1, {
|
||||
const row = buildRow ({
|
||||
source_row: 1,
|
||||
url,
|
||||
title: '',
|
||||
thumbnail_base: '',
|
||||
@@ -449,7 +460,8 @@ const parseSingleRow = (params: URLSearchParams): ParsedPostNewState | null => {
|
||||
if (importStatus != null && !(isValidImportStatus (importStatus)))
|
||||
return null
|
||||
|
||||
const row = buildRow (1, {
|
||||
const row = buildRow ({
|
||||
source_row: 1,
|
||||
url,
|
||||
title: params.get ('title') ?? '',
|
||||
thumbnail_base: params.get ('thumbnail_base') ?? '',
|
||||
@@ -508,7 +520,7 @@ const parseMetaRows = (
|
||||
if (fullRows.some (row => row == null))
|
||||
return null
|
||||
|
||||
const rows = fullRows.map ((row, index) => buildRow (index + 1, row as FullRowState))
|
||||
const rows = fullRows.map (row => buildRow (row as FullRowState))
|
||||
return {
|
||||
rows,
|
||||
source: urls.join ('\n'),
|
||||
@@ -539,7 +551,7 @@ const parseFullState = async (stateValue: string): Promise<ParsedPostNewState |
|
||||
if (fullRows.some (row => row == null))
|
||||
return null
|
||||
|
||||
const rows = fullRows.map ((row, index) => buildRow (index + 1, row as FullRowState))
|
||||
const rows = fullRows.map (row => buildRow (row as FullRowState))
|
||||
return {
|
||||
rows,
|
||||
source: rows.map (row => row.url).join ('\n'),
|
||||
@@ -594,6 +606,7 @@ const encodeRowState = (
|
||||
const base = baseRowState (row)
|
||||
const encoded: EncodedRowState = includeUrl ? { u: row.url } : { }
|
||||
|
||||
encoded.n = base.source_row
|
||||
if (base.title !== '')
|
||||
encoded.t = base.title
|
||||
if (base.thumbnail_base !== '')
|
||||
@@ -684,19 +697,19 @@ const serialiseCompressedRows = async (
|
||||
rows: PostImportRow[],
|
||||
): Promise<SerialisedPostNewState | null> => {
|
||||
try
|
||||
{
|
||||
for (let count = rows.length; count > 0; --count)
|
||||
{
|
||||
const nextRows = rows.slice (0, count)
|
||||
const encoded = encodeStateRows (nextRows)
|
||||
const state = await encodeCompressedState (JSON.stringify (encoded))
|
||||
const payload = state.slice (COMPRESSED_STATE_PREFIX.length)
|
||||
if (payload.length <= MAX_COMPRESSED_STATE_LENGTH)
|
||||
return {
|
||||
path: `/posts/new?state=${ state }`,
|
||||
complete: count === rows.length }
|
||||
}
|
||||
}
|
||||
{
|
||||
for (let count = rows.length; count > 0; --count)
|
||||
{
|
||||
const nextRows = rows.slice (0, count)
|
||||
const encoded = encodeStateRows (nextRows)
|
||||
const state = await encodeCompressedState (JSON.stringify (encoded))
|
||||
const path = `/posts/new?state=${ state }`
|
||||
if (path.length <= MAX_COMPRESSED_STATE_LENGTH)
|
||||
return {
|
||||
path,
|
||||
complete: count === rows.length }
|
||||
}
|
||||
}
|
||||
catch
|
||||
{
|
||||
}
|
||||
|
||||
新しいイシューから参照
ユーザーをブロックする