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