このコミットが含まれているのは:
2026-07-12 14:03:05 +09:00
コミット aa96ec95d1
10個のファイルの変更1469行の追加639行の削除
+221
ファイルの表示
@@ -0,0 +1,221 @@
export type PostImportOrigin = 'automatic' | 'manual'
export type PostImportRepairMode = 'all' | 'failed'
export type PostImportStatus =
'pending'
| 'created'
| 'skipped'
| 'failed'
export type PostImportResultStatus = 'created' | 'skipped' | 'failed'
export type PostImportAttributeValue = string | number
export type PostImportRow = {
sourceRow: number
url: string
attributes: Record<string, PostImportAttributeValue>
warnings: string[]
validationErrors: Record<string, string[]>
importErrors?: Record<string, string[]>
provenance: Record<string, PostImportOrigin>
tagSources?: Record<PostImportOrigin, string>
status: 'ready' | 'warning' | 'error'
existing: boolean
metadataUrl?: string
createdPostId?: number
importStatus?: PostImportStatus }
export type PostImportResultRow = {
sourceRow: number
status: PostImportResultStatus
post?: { id: number }
errors?: Record<string, string[]> }
export type PostImportSession = {
version: number
savedAt: string
source: string
existing: 'skip' | 'error'
rows: PostImportRow[]
repairMode: PostImportRepairMode }
const SESSION_VERSION = 1
const SESSION_PREFIX = 'post-import-session:'
const SOURCE_DRAFT_KEY = 'post-import-source-draft'
export const createPostImportSessionId = (): string =>
typeof crypto !== 'undefined' && 'randomUUID' in crypto
? crypto.randomUUID ()
: `${ Date.now () }-${ Math.random ().toString (36).slice (2) }`
export const loadPostImportSourceDraft = (): {
source: string
existing: 'skip' | 'error' } => {
if (typeof window === 'undefined')
return { source: '', existing: 'skip' }
try
{
const raw = sessionStorage.getItem (SOURCE_DRAFT_KEY)
if (raw == null)
return { source: '', existing: 'skip' }
const value = JSON.parse (raw) as {
source?: string
existing?: 'skip' | 'error' }
return {
source: typeof value.source === 'string' ? value.source : '',
existing: value.existing === 'error' ? 'error' : 'skip' }
}
catch
{
return { source: '', existing: 'skip' }
}
}
export const savePostImportSourceDraft = (
source: string,
existing: 'skip' | 'error',
) => {
if (typeof window === 'undefined')
return
sessionStorage.setItem (SOURCE_DRAFT_KEY, JSON.stringify ({ source, existing }))
}
const sessionKey = (sessionId: string): string => `${ SESSION_PREFIX }${ sessionId }`
export const savePostImportSession = (
sessionId: string,
session: Omit<PostImportSession, 'version' | 'savedAt'>,
) => {
if (typeof window === 'undefined')
return
sessionStorage.setItem (sessionKey (sessionId), JSON.stringify ({
...session,
version: SESSION_VERSION,
savedAt: new Date ().toISOString () }))
}
export const loadPostImportSession = (
sessionId: string,
): PostImportSession | null => {
if (typeof window === 'undefined')
return null
try
{
const raw = sessionStorage.getItem (sessionKey (sessionId))
if (raw == null)
return null
const value = JSON.parse (raw) as Partial<PostImportSession>
if (value.version !== SESSION_VERSION || !(Array.isArray (value.rows)))
return null
return {
version: SESSION_VERSION,
savedAt: typeof value.savedAt === 'string' ? value.savedAt : '',
source: typeof value.source === 'string' ? value.source : '',
existing: value.existing === 'error' ? 'error' : 'skip',
rows: value.rows as PostImportRow[],
repairMode: value.repairMode === 'failed' ? 'failed' : 'all' }
}
catch
{
return null
}
}
export const mergeValidatedImportRows = (
current: PostImportRow[],
validated: PostImportRow[],
): PostImportRow[] =>
current
.map (previous => {
if (previous.importStatus === 'created')
return previous
const row = validated.find (_1 => _1.sourceRow === previous.sourceRow)
return row
? {
...row,
importStatus: previous.importStatus,
createdPostId: previous.createdPostId,
importErrors: previous.importErrors,
validationErrors: row.validationErrors ?? { } }
: previous
})
.concat (validated.filter (row =>
!(current.some (_1 => _1.sourceRow === row.sourceRow))))
export const mergeImportResults = (
rows: PostImportRow[],
results: PostImportResultRow[],
): PostImportRow[] =>
rows.map (row => {
const result = results.find (_1 => _1.sourceRow === row.sourceRow)
return result
? {
...row,
importStatus: result.status,
createdPostId: result.post?.id,
importErrors: result.errors,
validationErrors: row.validationErrors }
: row
})
export const retryImportRow = (
rows: PostImportRow[],
sourceRow: number,
): PostImportRow[] =>
rows.map (row =>
row.sourceRow === sourceRow && row.importStatus === 'failed'
? { ...row, importStatus: 'pending', importErrors: undefined }
: row)
export const rowMessages = (row: PostImportRow): string[] => [
...row.warnings,
...Object.values (row.validationErrors ?? { }).flat (),
...Object.values (row.importErrors ?? { }).flat ()]
export const countImportSourceLines = (source: string): number =>
source
.split (/\r\n|\n|\r/)
.map (_1 => _1.trim ())
.filter (_1 => _1 !== '')
.length
export const countImportSourceValidLines = (source: string): number =>
source
.split (/\r\n|\n|\r/)
.map (_1 => _1.trim ())
.filter (_1 => /^https?:\/\//.test (_1))
.length
export const submittableImportRows = (rows: PostImportRow[]): PostImportRow[] =>
rows.filter (row => row.importStatus == null || row.importStatus === 'pending')
export const reviewSummaryCounts = (rows: PostImportRow[]) => ({
total: rows.length,
submittable: rows.filter (row =>
(row.importStatus == null || row.importStatus === 'pending')
&& Object.keys (row.validationErrors ?? { }).length === 0).length,
invalid: rows.filter (row => Object.keys (row.validationErrors ?? { }).length > 0).length,
skipPlanned: rows.filter (row =>
row.existing && row.importStatus !== 'created' && row.status !== 'error').length,
created: rows.filter (row => row.importStatus === 'created').length,
failed: rows.filter (row => row.importStatus === 'failed').length })