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