このコミットが含まれているのは:
2026-07-17 23:14:37 +09:00
コミット 0ae41b6266
19個のファイルの変更、455行の追加、216行の削除
+24 -3
ファイルの表示
@@ -4,6 +4,13 @@ import type { PostImportEditableDraft,
const THUMBNAIL_MISSING_WARNING = 'サムネールなし'
export const hasThumbnailBaseValue = (value: unknown): boolean =>
typeof value === 'string' && value.trim () !== ''
export const hasVideoTag = (value: unknown): boolean =>
typeof value === 'string'
&& value.split (/\s+/).includes ('動画')
export const isExistingSkipRow = (row: PostImportRow): boolean =>
row.skipReason === 'existing'
@@ -64,7 +71,7 @@ const deduped = (values: string[]): string[] =>
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
return hasThumbnailBaseValue (row.attributes.thumbnailBase) || row.thumbnailFile != null
? others
: deduped ([...others, THUMBNAIL_MISSING_WARNING])
}
@@ -168,6 +175,14 @@ export const resultRowWarnings = (row: PostImportRow): string[] =>
...Object.values (row.fieldWarnings ?? { }).flat (),
...row.baseWarnings])]
const isManualChange = (
current: unknown,
next: string,
): boolean =>
next !== String (current ?? '')
export const buildNextEditedRow = (
editingRow: PostImportRow,
draft: PostImportEditableDraft,
@@ -183,16 +198,22 @@ export const buildNextEditedRow = (
['thumbnailBase', draft.thumbnailBase],
['originalCreatedFrom', draft.originalCreatedFrom],
['originalCreatedBefore', draft.originalCreatedBefore],
['duration', draft.duration],
['parentPostIds', draft.parentPostIds]] as const
draftFields.forEach (([field, value]) => {
nextAttributes[field] = value
nextProvenance[field] =
value !== String (editingRow.attributes[field] ?? '')
isManualChange (editingRow.attributes[field], value)
? 'manual'
: (editingRow.provenance[field] ?? 'automatic')
})
if (nextProvenance.duration === 'manual')
{
nextAttributes.videoMs = ''
nextProvenance.videoMs = 'manual'
}
nextAttributes.tags = draft.tags
if (draft.tags !== String (editingRow.attributes.tags ?? ''))
if (isManualChange (editingRow.attributes.tags, draft.tags))
{
nextProvenance.tags = 'manual'
nextTagSources.manual = draft.tags
+131 -2
ファイルの表示
@@ -25,6 +25,31 @@ const ATTRIBUTE_KEYS = [
const PROVENANCE_KEYS = [...ATTRIBUTE_KEYS, 'url'] as const
const TAG_SOURCE_KEYS = ['automatic', 'manual'] as const
const WARNING_KEYS = [...ATTRIBUTE_KEYS, 'url'] as const
const ROW_KEYS = [
'sourceRow',
'url',
'attributes',
'fieldWarnings',
'baseWarnings',
'validationErrors',
'importErrors',
'provenance',
'tagSources',
'status',
'skipReason',
'existingPostId',
'existingPost',
'metadataUrl',
'resetSnapshot',
'createdPostId',
'importStatus',
'recoverable'] as const
const SESSION_KEYS = [
'version',
'expiresAt',
'source',
'rows',
'repairMode'] as const
const isPlainObject = (value: unknown): value is Record<string, unknown> =>
@@ -220,10 +245,110 @@ const sanitiseExistingPost = (value: unknown): PostImportExistingPost | undefine
thumbnailUrl: value.thumbnailUrl as string | undefined }
}
const recalculateThumbnailWarning = (row: PostImportRow): PostImportRow => {
const others = (row.fieldWarnings.thumbnailBase ?? []).filter (
key => key !== 'サムネールなし')
const thumbnailBasePresent =
typeof row.attributes.thumbnailBase === 'string'
&& row.attributes.thumbnailBase.trim () !== ''
return {
...row,
fieldWarnings: {
...row.fieldWarnings,
thumbnailBase:
thumbnailBasePresent
? others
: [...new Set ([...others, 'サムネールなし'])] } }
}
const serialiseExistingPost = (value: PostImportExistingPost | undefined) =>
value == null
? undefined
: {
id: value.id,
title: value.title,
url: value.url,
thumbnailUrl: value.thumbnailUrl }
const serialiseResetSnapshot = (value: PostImportResetSnapshot) => ({
url: value.url,
attributes: Object.fromEntries (
Object.entries (value.attributes).map (([key, entry]) => [key, entry])),
provenance: Object.fromEntries (
Object.entries (value.provenance).map (([key, entry]) => [key, entry])),
tagSources: {
automatic: value.tagSources.automatic,
manual: value.tagSources.manual },
fieldWarnings: Object.fromEntries (
Object.entries (value.fieldWarnings).map (([key, entry]) => [key, [...entry]])),
baseWarnings: [...value.baseWarnings],
metadataUrl: value.metadataUrl })
export const serialisePostImportRow = (row: PostImportRow) => {
const thumbnailBaseWarnings = (() => {
const others = (row.fieldWarnings.thumbnailBase ?? []).filter (
message => message !== 'サムネールなし')
const thumbnailBasePresent =
typeof row.attributes.thumbnailBase === 'string'
&& row.attributes.thumbnailBase.trim () !== ''
return thumbnailBasePresent
? others
: [...new Set ([...others, 'サムネールなし'])]
}) ()
return {
sourceRow: row.sourceRow,
url: row.url,
attributes: Object.fromEntries (
Object.entries (row.attributes).map (([key, entry]) => [key, entry])),
fieldWarnings: Object.fromEntries (
Object.entries ({
...row.fieldWarnings,
thumbnailBase: thumbnailBaseWarnings }).map (([key, entry]) => [key, [...entry]])),
baseWarnings: [...row.baseWarnings],
validationErrors: Object.fromEntries (
Object.entries (row.validationErrors).map (([key, entry]) => [key, [...entry]])),
importErrors:
row.importErrors == null
? undefined
: Object.fromEntries (
Object.entries (row.importErrors).map (([key, entry]) => [key, [...entry]])),
provenance: Object.fromEntries (
Object.entries (row.provenance).map (([key, entry]) => [key, entry])),
tagSources:
row.tagSources == null
? undefined
: {
automatic: row.tagSources.automatic,
manual: row.tagSources.manual },
status: row.status,
skipReason: row.skipReason,
existingPostId: row.existingPostId,
existingPost: serialiseExistingPost (row.existingPost),
metadataUrl: row.metadataUrl,
resetSnapshot: serialiseResetSnapshot (row.resetSnapshot),
createdPostId: row.createdPostId,
importStatus: row.importStatus,
recoverable: row.recoverable }
}
export const serialisePostImportRows = (rows: PostImportRow[]) =>
rows.map (row => serialisePostImportRow (row))
export const serialisedPostImportRowsEqual = (
left: PostImportRow[],
right: PostImportRow[],
): boolean =>
JSON.stringify (serialisePostImportRows (left))
=== JSON.stringify (serialisePostImportRows (right))
const sanitiseRow = (value: unknown): PostImportRow | null => {
if (!(isPlainObject (value)))
return null
if (!(hasOnlyKeys (value, ROW_KEYS)))
return null
if (!(Number.isInteger (value.sourceRow)) || Number (value.sourceRow) <= 0)
return null
if (typeof value.url !== 'string')
@@ -321,6 +446,8 @@ const sanitiseRow = (value: unknown): PostImportRow | null => {
const sanitiseSession = (value: unknown): PostImportSession | null => {
if (!(isPlainObject (value)))
return null
if (!(hasOnlyKeys (value, SESSION_KEYS)))
return null
if (value.version !== SESSION_VERSION || !(Array.isArray (value.rows)))
return null
if (typeof value.expiresAt !== 'string' || isExpiredSession (value.expiresAt))
@@ -334,7 +461,7 @@ const sanitiseSession = (value: unknown): PostImportSession | null => {
version: SESSION_VERSION,
expiresAt: value.expiresAt,
source: typeof value.source === 'string' ? value.source : '',
rows: rows as PostImportRow[],
rows: (rows as PostImportRow[]).map (row => recalculateThumbnailWarning (row)),
repairMode: value.repairMode === 'failed' ? 'failed' : 'all' }
}
@@ -438,7 +565,9 @@ export const savePostImportSession = (
return writeStorage (
sessionKey (validatedId),
JSON.stringify ({
...session,
source: session.source,
rows: serialisePostImportRows (session.rows),
repairMode: session.repairMode,
version: SESSION_VERSION,
expiresAt: new Date (Date.now () + SESSION_MAX_AGE_MS).toISOString () }),
onError)
+2 -2
ファイルの表示
@@ -43,8 +43,7 @@ export type PostImportRow = {
createdPostId?: number
importStatus?: PostImportStatus
recoverable?: boolean
thumbnailFile?: File
thumbnailObjectUrl?: string }
thumbnailFile?: File }
export type PostImportResultRow =
| {
@@ -88,6 +87,7 @@ export type PostImportEditableDraft = {
thumbnailBase: string
originalCreatedFrom: string
originalCreatedBefore: string
duration: string
tags: string
parentPostIds: string
thumbnailFile?: File }
+5 -3
ファイルの表示
@@ -237,6 +237,7 @@ const manualFields = (row: PostImportRow): string[] =>
'thumbnailBase',
'originalCreatedFrom',
'originalCreatedBefore',
'duration',
'tags',
'parentPostIds']
.filter (field => row.provenance[field] === 'manual')
@@ -268,8 +269,8 @@ const buildRow = (state: FullRowState): PostImportRow => {
thumbnailBase: manual.has ('thumbnailBase') ? 'manual' : 'automatic',
originalCreatedFrom: manual.has ('originalCreatedFrom') ? 'manual' : 'automatic',
originalCreatedBefore: manual.has ('originalCreatedBefore') ? 'manual' : 'automatic',
videoMs: 'automatic',
duration: 'automatic',
videoMs: manual.has ('duration') ? 'manual' : 'automatic',
duration: manual.has ('duration') ? 'manual' : 'automatic',
tags: manual.has ('tags') ? 'manual' : 'automatic',
parentPostIds: manual.has ('parentPostIds') ? 'manual' : 'automatic' }
const tagSources = provenance.tags === 'manual'
@@ -280,7 +281,7 @@ const buildRow = (state: FullRowState): PostImportRow => {
thumbnailBase: state.thumbnail_base,
originalCreatedFrom: state.original_created_from,
originalCreatedBefore: state.original_created_before,
videoMs: state.video_ms,
videoMs: manual.has ('duration') ? '' : state.video_ms,
duration: state.duration,
tags: state.tags,
parentPostIds: state.parent_post_ids }
@@ -651,6 +652,7 @@ export const hasPostNewReviewState = (search: string): boolean => {
|| params.has ('urls')
|| params.has ('meta')
|| params.has ('url')
|| parsePostNewSessionId (search) != null
}