このコミットが含まれているのは:
@@ -131,6 +131,7 @@ class PostsController < ApplicationController
|
|||||||
title: nil,
|
title: nil,
|
||||||
thumbnail_base: nil,
|
thumbnail_base: nil,
|
||||||
tags: nil,
|
tags: nil,
|
||||||
|
display_tags: [],
|
||||||
original_created_from: nil,
|
original_created_from: nil,
|
||||||
original_created_before: nil,
|
original_created_before: nil,
|
||||||
duration: nil,
|
duration: nil,
|
||||||
@@ -153,6 +154,7 @@ class PostsController < ApplicationController
|
|||||||
title: metadata[:title],
|
title: metadata[:title],
|
||||||
thumbnail_base: metadata[:thumbnail_base],
|
thumbnail_base: metadata[:thumbnail_base],
|
||||||
tags: metadata[:tags],
|
tags: metadata[:tags],
|
||||||
|
display_tags: metadata[:display_tags],
|
||||||
original_created_from: metadata[:original_created_from],
|
original_created_from: metadata[:original_created_from],
|
||||||
original_created_before: metadata[:original_created_before],
|
original_created_before: metadata[:original_created_before],
|
||||||
duration: metadata[:duration],
|
duration: metadata[:duration],
|
||||||
@@ -173,6 +175,7 @@ class PostsController < ApplicationController
|
|||||||
title: nil,
|
title: nil,
|
||||||
thumbnail_base: nil,
|
thumbnail_base: nil,
|
||||||
tags: nil,
|
tags: nil,
|
||||||
|
display_tags: [],
|
||||||
original_created_from: nil,
|
original_created_from: nil,
|
||||||
original_created_before: nil,
|
original_created_before: nil,
|
||||||
duration: nil,
|
duration: nil,
|
||||||
@@ -633,6 +636,7 @@ class PostsController < ApplicationController
|
|||||||
:title,
|
:title,
|
||||||
:thumbnail_base,
|
:thumbnail_base,
|
||||||
:tags,
|
:tags,
|
||||||
|
:display_tags,
|
||||||
:parent_post_ids,
|
:parent_post_ids,
|
||||||
:original_created_from,
|
:original_created_from,
|
||||||
:original_created_before,
|
:original_created_before,
|
||||||
|
|||||||
@@ -7,20 +7,13 @@ module PostCompactRepr
|
|||||||
def base post
|
def base post
|
||||||
return nil if post.nil?
|
return nil if post.nil?
|
||||||
|
|
||||||
{
|
PostRepr
|
||||||
id: post.id,
|
.common(post)
|
||||||
title: post.title,
|
.slice(
|
||||||
url: post.url,
|
'id',
|
||||||
thumbnail_url: thumbnail_url(post) }
|
'title',
|
||||||
end
|
'url',
|
||||||
|
'thumbnail',
|
||||||
def thumbnail_url post
|
'thumbnail_base')
|
||||||
return nil unless post.thumbnail.attached?
|
|
||||||
|
|
||||||
Rails.application.routes.url_helpers.rails_storage_proxy_url(
|
|
||||||
post.thumbnail,
|
|
||||||
only_path: false)
|
|
||||||
rescue ActionController::UrlGenerationError, ArgumentError, URI::InvalidURIError
|
|
||||||
nil
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -27,6 +27,7 @@ class PostCreatePlan
|
|||||||
original_created_from: @attributes[:original_created_from].presence,
|
original_created_from: @attributes[:original_created_from].presence,
|
||||||
original_created_before: @attributes[:original_created_before].presence,
|
original_created_before: @attributes[:original_created_before].presence,
|
||||||
tags: serialised_tags(direct_tag_specs, tag_sections),
|
tags: serialised_tags(direct_tag_specs, tag_sections),
|
||||||
|
display_tags: display_tags(direct_tag_specs, tag_sections),
|
||||||
duration: @attributes[:duration].to_s,
|
duration: @attributes[:duration].to_s,
|
||||||
video_ms: video_ms,
|
video_ms: video_ms,
|
||||||
parent_post_ids: parent_post_ids.join(' '),
|
parent_post_ids: parent_post_ids.join(' '),
|
||||||
@@ -204,6 +205,15 @@ class PostCreatePlan
|
|||||||
}.sort.join(' ')
|
}.sort.join(' ')
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def display_tags direct_tag_specs, tag_sections
|
||||||
|
direct_tag_specs.map { |spec|
|
||||||
|
{
|
||||||
|
name: spec[:name],
|
||||||
|
category: spec[:category].to_s,
|
||||||
|
section_literals: tag_sections[spec[:name]].to_a.map { Post.section_literal(_1) } }
|
||||||
|
}.sort_by { _1[:name] }
|
||||||
|
end
|
||||||
|
|
||||||
def normalise_video_ms snapshot_tag_specs
|
def normalise_video_ms snapshot_tag_specs
|
||||||
return nil unless snapshot_tag_specs.any? { _1[:name] == VIDEO_TAG_NAME }
|
return nil unless snapshot_tag_specs.any? { _1[:name] == VIDEO_TAG_NAME }
|
||||||
|
|
||||||
|
|||||||
@@ -29,6 +29,7 @@ class PostCreatePreflight
|
|||||||
original_created_before: preview[:attributes]['original_created_before'],
|
original_created_before: preview[:attributes]['original_created_before'],
|
||||||
duration: preview[:attributes]['duration'],
|
duration: preview[:attributes]['duration'],
|
||||||
video_ms: preview[:attributes]['video_ms'],
|
video_ms: preview[:attributes]['video_ms'],
|
||||||
|
display_tags: preview[:display_tags] || [],
|
||||||
field_warnings: final_field_warnings(preview[:field_warnings] || { }),
|
field_warnings: final_field_warnings(preview[:field_warnings] || { }),
|
||||||
base_warnings: preview[:base_warnings],
|
base_warnings: preview[:base_warnings],
|
||||||
existing_post_id: preview[:existing_post_id],
|
existing_post_id: preview[:existing_post_id],
|
||||||
@@ -63,6 +64,7 @@ class PostCreatePreflight
|
|||||||
original_created_before: plan[:original_created_before],
|
original_created_before: plan[:original_created_before],
|
||||||
duration: plan[:duration],
|
duration: plan[:duration],
|
||||||
video_ms: plan[:video_ms],
|
video_ms: plan[:video_ms],
|
||||||
|
display_tags: plan[:display_tags],
|
||||||
direct_tag_specs: plan[:direct_tag_specs],
|
direct_tag_specs: plan[:direct_tag_specs],
|
||||||
default_tag_specs: plan[:default_tag_specs],
|
default_tag_specs: plan[:default_tag_specs],
|
||||||
snapshot_tag_specs: plan[:snapshot_tag_specs],
|
snapshot_tag_specs: plan[:snapshot_tag_specs],
|
||||||
|
|||||||
@@ -32,6 +32,7 @@ class PostMetadataFetcher
|
|||||||
original_created_from: serialise_time(created_range&.first),
|
original_created_from: serialise_time(created_range&.first),
|
||||||
original_created_before: serialise_time(created_range&.last),
|
original_created_before: serialise_time(created_range&.last),
|
||||||
duration: serialise_duration(duration),
|
duration: serialise_duration(duration),
|
||||||
|
display_tags: display_tags(platform_tags),
|
||||||
tags: platform_tags.join(' ') }
|
tags: platform_tags.join(' ') }
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -42,6 +43,24 @@ class PostMetadataFetcher
|
|||||||
[]
|
[]
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def self.display_tags names
|
||||||
|
return [] if names.blank?
|
||||||
|
|
||||||
|
existing_tags =
|
||||||
|
Tag
|
||||||
|
.joins(:tag_name)
|
||||||
|
.where(tag_names: { name: names })
|
||||||
|
.index_by(&:name)
|
||||||
|
|
||||||
|
names.map { |name|
|
||||||
|
tag = existing_tags[name]
|
||||||
|
{
|
||||||
|
name: name,
|
||||||
|
category: (tag&.category || 'meta'),
|
||||||
|
section_literals: [] }
|
||||||
|
}
|
||||||
|
end
|
||||||
|
|
||||||
def self.original_created_range value
|
def self.original_created_range value
|
||||||
return nil if value.blank?
|
return nil if value.blank?
|
||||||
|
|
||||||
@@ -168,6 +187,7 @@ class PostMetadataFetcher
|
|||||||
end
|
end
|
||||||
|
|
||||||
private_class_method :platform_tags,
|
private_class_method :platform_tags,
|
||||||
|
:display_tags,
|
||||||
:original_created_range,
|
:original_created_range,
|
||||||
:parse_timestamp_range,
|
:parse_timestamp_range,
|
||||||
:parse_nanoseconds,
|
:parse_nanoseconds,
|
||||||
|
|||||||
@@ -4,9 +4,13 @@ import { cn } from '@/lib/utils'
|
|||||||
|
|
||||||
import type { ComponentProps, CSSProperties, FC, HTMLAttributes } from 'react'
|
import type { ComponentProps, CSSProperties, FC, HTMLAttributes } from 'react'
|
||||||
|
|
||||||
import type { Tag } from '@/types'
|
import type { Category, Tag } from '@/types'
|
||||||
|
|
||||||
type CommonProps = {
|
type LightweightTag = {
|
||||||
|
name: string
|
||||||
|
category: Category }
|
||||||
|
|
||||||
|
type FullCommonProps = {
|
||||||
tag: Tag
|
tag: Tag
|
||||||
nestLevel?: number
|
nestLevel?: number
|
||||||
truncateOnMobile?: boolean
|
truncateOnMobile?: boolean
|
||||||
@@ -14,18 +18,43 @@ type CommonProps = {
|
|||||||
withCount?: boolean }
|
withCount?: boolean }
|
||||||
|
|
||||||
type PropsWithLink =
|
type PropsWithLink =
|
||||||
& CommonProps
|
& FullCommonProps
|
||||||
& { linkFlg?: true }
|
& { linkFlg?: true }
|
||||||
& Partial<ComponentProps<typeof PrefetchLink>>
|
& Partial<ComponentProps<typeof PrefetchLink>>
|
||||||
|
|
||||||
type PropsWithoutLink =
|
type PropsWithoutLink =
|
||||||
& CommonProps
|
& FullCommonProps
|
||||||
& { linkFlg: false }
|
& { linkFlg: false }
|
||||||
& Partial<HTMLAttributes<HTMLSpanElement>>
|
& Partial<HTMLAttributes<HTMLSpanElement>>
|
||||||
|
|
||||||
|
type LightweightPropsWithLink =
|
||||||
|
& {
|
||||||
|
tag: LightweightTag
|
||||||
|
nestLevel?: number
|
||||||
|
truncateOnMobile?: boolean
|
||||||
|
withWiki: false
|
||||||
|
withCount: false
|
||||||
|
linkFlg?: true }
|
||||||
|
& Partial<ComponentProps<typeof PrefetchLink>>
|
||||||
|
|
||||||
|
type LightweightPropsWithoutLink =
|
||||||
|
& {
|
||||||
|
tag: LightweightTag
|
||||||
|
nestLevel?: number
|
||||||
|
truncateOnMobile?: boolean
|
||||||
|
withWiki: false
|
||||||
|
withCount: false
|
||||||
|
linkFlg: false }
|
||||||
|
& Partial<HTMLAttributes<HTMLSpanElement>>
|
||||||
|
|
||||||
type Props =
|
type Props =
|
||||||
| PropsWithLink
|
| PropsWithLink
|
||||||
| PropsWithoutLink
|
| PropsWithoutLink
|
||||||
|
| LightweightPropsWithLink
|
||||||
|
| LightweightPropsWithoutLink
|
||||||
|
|
||||||
|
const isFullTag = (tag: Tag | LightweightTag): tag is Tag =>
|
||||||
|
'id' in tag
|
||||||
|
|
||||||
|
|
||||||
const TagLink: FC<Props> = ({ tag,
|
const TagLink: FC<Props> = ({ tag,
|
||||||
@@ -50,12 +79,13 @@ const TagLink: FC<Props> = ({ tag,
|
|||||||
'inline-flex min-w-0 max-w-full flex-nowrap items-stretch align-baseline gap-x-1 md:items-baseline'
|
'inline-flex min-w-0 max-w-full flex-nowrap items-stretch align-baseline gap-x-1 md:items-baseline'
|
||||||
const markerWrapClass = 'shrink-0 self-start md:self-auto'
|
const markerWrapClass = 'shrink-0 self-start md:self-auto'
|
||||||
const countClass = 'shrink-0 self-end md:self-auto'
|
const countClass = 'shrink-0 self-end md:self-auto'
|
||||||
|
const matchedAlias = isFullTag (tag) ? tag.matchedAlias : null
|
||||||
const textTitle = title
|
const textTitle = title
|
||||||
?? (tag.matchedAlias == null ? tag.name : `${ tag.matchedAlias } → ${ tag.name }`)
|
?? (matchedAlias == null ? tag.name : `${ matchedAlias } → ${ tag.name }`)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<span className={rootClass}>
|
<span className={rootClass}>
|
||||||
{(linkFlg && withWiki) && (
|
{(linkFlg && withWiki && isFullTag (tag)) && (
|
||||||
<span className={markerWrapClass}>
|
<span className={markerWrapClass}>
|
||||||
{(tag.materialId != null || tag.hasWiki || tag.hasDeerjikists)
|
{(tag.materialId != null || tag.hasWiki || tag.hasDeerjikists)
|
||||||
? (
|
? (
|
||||||
@@ -118,7 +148,7 @@ const TagLink: FC<Props> = ({ tag,
|
|||||||
style={{ paddingLeft: `${ (nestLevel - 1) }rem` }}>
|
style={{ paddingLeft: `${ (nestLevel - 1) }rem` }}>
|
||||||
↳
|
↳
|
||||||
</span>)}
|
</span>)}
|
||||||
{tag.matchedAlias != null && (
|
{matchedAlias != null && (
|
||||||
<>
|
<>
|
||||||
<span
|
<span
|
||||||
title={textTitle}
|
title={textTitle}
|
||||||
@@ -126,7 +156,7 @@ const TagLink: FC<Props> = ({ tag,
|
|||||||
style={colourStyle}
|
style={colourStyle}
|
||||||
{...props}>
|
{...props}>
|
||||||
<ResponsiveMarqueeText
|
<ResponsiveMarqueeText
|
||||||
text={tag.matchedAlias}
|
text={matchedAlias}
|
||||||
title={textTitle}
|
title={textTitle}
|
||||||
truncateOnMobile={truncateOnMobile}/>
|
truncateOnMobile={truncateOnMobile}/>
|
||||||
</span>
|
</span>
|
||||||
@@ -156,7 +186,7 @@ const TagLink: FC<Props> = ({ tag,
|
|||||||
title={textTitle}
|
title={textTitle}
|
||||||
truncateOnMobile={truncateOnMobile}/>
|
truncateOnMobile={truncateOnMobile}/>
|
||||||
</span>)}
|
</span>)}
|
||||||
{withCount && (
|
{(withCount && isFullTag (tag)) && (
|
||||||
<span className={countClass}>{tag.postCount}</span>)}
|
<span className={countClass}>{tag.postCount}</span>)}
|
||||||
</span>)
|
</span>)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import FieldError from '@/components/common/FieldError'
|
import FieldError from '@/components/common/FieldError'
|
||||||
|
import PostImportTagLinks from '@/components/posts/import/PostImportTagLinks'
|
||||||
import { Button } from '@/components/ui/button'
|
import { Button } from '@/components/ui/button'
|
||||||
import PostImportThumbnailPreview from '@/components/posts/import/PostImportThumbnailPreview'
|
import PostImportThumbnailPreview from '@/components/posts/import/PostImportThumbnailPreview'
|
||||||
import PostImportStatusBadge from '@/components/posts/import/PostImportStatusBadge'
|
import PostImportStatusBadge from '@/components/posts/import/PostImportStatusBadge'
|
||||||
@@ -90,10 +91,7 @@ const PostImportRowSummary: FC<Props> = (
|
|||||||
<div className="truncate text-xs text-neutral-600 dark:text-neutral-300">
|
<div className="truncate text-xs text-neutral-600 dark:text-neutral-300">
|
||||||
{row.url}
|
{row.url}
|
||||||
</div>
|
</div>
|
||||||
{String (row.attributes.tags ?? '') && (
|
<PostImportTagLinks tags={row.displayTags}/>
|
||||||
<div className="truncate text-xs text-neutral-500 dark:text-neutral-400">
|
|
||||||
{String (row.attributes.tags ?? '')}
|
|
||||||
</div>)}
|
|
||||||
<div className="text-xs text-neutral-500 dark:text-neutral-400">
|
<div className="text-xs text-neutral-500 dark:text-neutral-400">
|
||||||
{summaryDate (row)}
|
{summaryDate (row)}
|
||||||
</div>
|
</div>
|
||||||
@@ -153,10 +151,7 @@ const PostImportRowSummary: FC<Props> = (
|
|||||||
<div className="flex flex-wrap gap-2">
|
<div className="flex flex-wrap gap-2">
|
||||||
{displayStatus != null && <PostImportStatusBadge value={displayStatus}/>}
|
{displayStatus != null && <PostImportStatusBadge value={displayStatus}/>}
|
||||||
</div>
|
</div>
|
||||||
{String (row.attributes.tags ?? '') && (
|
<PostImportTagLinks tags={row.displayTags}/>
|
||||||
<div className="text-xs text-neutral-500 dark:text-neutral-400">
|
|
||||||
{String (row.attributes.tags ?? '')}
|
|
||||||
</div>)}
|
|
||||||
<div className="text-xs text-neutral-500 dark:text-neutral-400">
|
<div className="text-xs text-neutral-500 dark:text-neutral-400">
|
||||||
{summaryDate (row)}
|
{summaryDate (row)}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -0,0 +1,35 @@
|
|||||||
|
import TagLink from '@/components/TagLink'
|
||||||
|
|
||||||
|
import type { FC } from 'react'
|
||||||
|
|
||||||
|
import type { PostImportDisplayTag } from '@/lib/postImportTypes'
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
tags: PostImportDisplayTag[] | undefined }
|
||||||
|
|
||||||
|
const PostImportTagLinks: FC<Props> = ({ tags }) => {
|
||||||
|
if (tags == null || tags.length === 0)
|
||||||
|
return null
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="flex flex-wrap gap-2">
|
||||||
|
{tags.map (tag => {
|
||||||
|
const key = `${ tag.category }:${ tag.name }:${ tag.sectionLiterals?.join ('|') ?? '' }`
|
||||||
|
return (
|
||||||
|
<span key={key} className="inline-flex flex-nowrap items-baseline gap-1">
|
||||||
|
<TagLink
|
||||||
|
tag={{
|
||||||
|
name: tag.name,
|
||||||
|
category: tag.category }}
|
||||||
|
withWiki={false}
|
||||||
|
withCount={false}/>
|
||||||
|
{tag.sectionLiterals?.map (literal => (
|
||||||
|
<span key={literal} className="text-xs text-neutral-500 dark:text-neutral-400">
|
||||||
|
{literal}
|
||||||
|
</span>))}
|
||||||
|
</span>)
|
||||||
|
})}
|
||||||
|
</div>)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default PostImportTagLinks
|
||||||
@@ -68,6 +68,10 @@ export const validatableImportRows = (rows: PostImportRow[]): PostImportRow[] =>
|
|||||||
const buildResetSnapshot = (row: PostImportRow) => ({
|
const buildResetSnapshot = (row: PostImportRow) => ({
|
||||||
url: row.url,
|
url: row.url,
|
||||||
attributes: { ...row.attributes },
|
attributes: { ...row.attributes },
|
||||||
|
displayTags: row.displayTags?.map (tag => ({
|
||||||
|
name: tag.name,
|
||||||
|
category: tag.category,
|
||||||
|
sectionLiterals: tag.sectionLiterals == null ? undefined : [...tag.sectionLiterals] })) ?? [],
|
||||||
provenance: { ...row.provenance },
|
provenance: { ...row.provenance },
|
||||||
tagSources: {
|
tagSources: {
|
||||||
automatic: row.tagSources?.automatic ?? '',
|
automatic: row.tagSources?.automatic ?? '',
|
||||||
@@ -243,6 +247,11 @@ export const buildNextEditedRow = (
|
|||||||
...editingRow,
|
...editingRow,
|
||||||
url: draft.url,
|
url: draft.url,
|
||||||
attributes: nextAttributes,
|
attributes: nextAttributes,
|
||||||
|
displayTags:
|
||||||
|
editingRow.displayTags?.map (tag => ({
|
||||||
|
name: tag.name,
|
||||||
|
category: tag.category,
|
||||||
|
sectionLiterals: tag.sectionLiterals == null ? undefined : [...tag.sectionLiterals] })),
|
||||||
thumbnailFile: draft.thumbnailFile,
|
thumbnailFile: draft.thumbnailFile,
|
||||||
provenance: {
|
provenance: {
|
||||||
...nextProvenance,
|
...nextProvenance,
|
||||||
@@ -316,6 +325,12 @@ export const mergeValidatedImportRows = (
|
|||||||
...previous,
|
...previous,
|
||||||
url: row.url,
|
url: row.url,
|
||||||
attributes: row.attributes,
|
attributes: row.attributes,
|
||||||
|
displayTags:
|
||||||
|
row.displayTags?.map (tag => ({
|
||||||
|
name: tag.name,
|
||||||
|
category: tag.category,
|
||||||
|
sectionLiterals:
|
||||||
|
tag.sectionLiterals == null ? undefined : [...tag.sectionLiterals] })),
|
||||||
provenance: row.provenance,
|
provenance: row.provenance,
|
||||||
tagSources: row.tagSources,
|
tagSources: row.tagSources,
|
||||||
skipReason: row.skipReason,
|
skipReason: row.skipReason,
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import type { PostImportOrigin,
|
import type { PostImportOrigin,
|
||||||
|
PostImportDisplayTag,
|
||||||
PostImportExistingPost,
|
PostImportExistingPost,
|
||||||
PostImportResetSnapshot,
|
PostImportResetSnapshot,
|
||||||
PostImportRow,
|
PostImportRow,
|
||||||
@@ -43,6 +44,7 @@ const ROW_KEYS = [
|
|||||||
'existingPostId',
|
'existingPostId',
|
||||||
'existingPost',
|
'existingPost',
|
||||||
'metadataUrl',
|
'metadataUrl',
|
||||||
|
'displayTags',
|
||||||
'resetSnapshot',
|
'resetSnapshot',
|
||||||
'createdPostId',
|
'createdPostId',
|
||||||
'importStatus',
|
'importStatus',
|
||||||
@@ -216,11 +218,53 @@ const hasOnlyKeys = (
|
|||||||
): boolean =>
|
): boolean =>
|
||||||
Object.keys (value).every (key => allowedKeys.includes (key))
|
Object.keys (value).every (key => allowedKeys.includes (key))
|
||||||
|
|
||||||
|
const sanitiseDisplayTag = (value: unknown): PostImportDisplayTag | null => {
|
||||||
|
if (!(isPlainObject (value)))
|
||||||
|
return null
|
||||||
|
if (!(hasOnlyKeys (value, ['name', 'category', 'sectionLiterals'])))
|
||||||
|
return null
|
||||||
|
if (typeof value.name !== 'string' || value.name === '')
|
||||||
|
return null
|
||||||
|
if (typeof value.category !== 'string')
|
||||||
|
return null
|
||||||
|
if (!(['deerjikist',
|
||||||
|
'meme',
|
||||||
|
'character',
|
||||||
|
'general',
|
||||||
|
'material',
|
||||||
|
'meta',
|
||||||
|
'nico'] as const).includes (value.category as never))
|
||||||
|
return null
|
||||||
|
if (value.sectionLiterals != null
|
||||||
|
&& (!(Array.isArray (value.sectionLiterals))
|
||||||
|
|| !(value.sectionLiterals.every (entry => typeof entry === 'string'))))
|
||||||
|
return null
|
||||||
|
|
||||||
|
return {
|
||||||
|
name: value.name,
|
||||||
|
category: value.category,
|
||||||
|
sectionLiterals:
|
||||||
|
value.sectionLiterals == null
|
||||||
|
? undefined
|
||||||
|
: [...value.sectionLiterals] }
|
||||||
|
}
|
||||||
|
|
||||||
|
const sanitiseDisplayTags = (value: unknown): PostImportDisplayTag[] | null => {
|
||||||
|
if (value == null)
|
||||||
|
return []
|
||||||
|
if (!(Array.isArray (value)))
|
||||||
|
return null
|
||||||
|
|
||||||
|
const tags = value.map (sanitiseDisplayTag)
|
||||||
|
return tags.some (tag => tag == null) ? null : (tags as PostImportDisplayTag[])
|
||||||
|
}
|
||||||
|
|
||||||
const sanitiseResetSnapshot = (value: unknown): PostImportResetSnapshot | null => {
|
const sanitiseResetSnapshot = (value: unknown): PostImportResetSnapshot | null => {
|
||||||
if (!(isPlainObject (value)))
|
if (!(isPlainObject (value)))
|
||||||
return null
|
return null
|
||||||
if (!(hasOnlyKeys (value, ['url',
|
if (!(hasOnlyKeys (value, ['url',
|
||||||
'attributes',
|
'attributes',
|
||||||
|
'displayTags',
|
||||||
'provenance',
|
'provenance',
|
||||||
'tagSources',
|
'tagSources',
|
||||||
'fieldWarnings',
|
'fieldWarnings',
|
||||||
@@ -258,10 +302,14 @@ const sanitiseResetSnapshot = (value: unknown): PostImportResetSnapshot | null =
|
|||||||
return null
|
return null
|
||||||
if (value.metadataUrl != null && typeof value.metadataUrl !== 'string')
|
if (value.metadataUrl != null && typeof value.metadataUrl !== 'string')
|
||||||
return null
|
return null
|
||||||
|
const displayTags = sanitiseDisplayTags (value.displayTags)
|
||||||
|
if (displayTags == null)
|
||||||
|
return null
|
||||||
|
|
||||||
return {
|
return {
|
||||||
url: value.url,
|
url: value.url,
|
||||||
attributes: value.attributes as Record<string, string | number>,
|
attributes: value.attributes as Record<string, string | number>,
|
||||||
|
displayTags,
|
||||||
provenance: value.provenance as Record<string, PostImportOrigin>,
|
provenance: value.provenance as Record<string, PostImportOrigin>,
|
||||||
tagSources: value.tagSources as Record<PostImportOrigin, string>,
|
tagSources: value.tagSources as Record<PostImportOrigin, string>,
|
||||||
fieldWarnings,
|
fieldWarnings,
|
||||||
@@ -278,14 +326,39 @@ const sanitiseExistingPost = (value: unknown): PostImportExistingPost | undefine
|
|||||||
return undefined
|
return undefined
|
||||||
if (typeof value.title !== 'string' || typeof value.url !== 'string')
|
if (typeof value.title !== 'string' || typeof value.url !== 'string')
|
||||||
return undefined
|
return undefined
|
||||||
if (value.thumbnailUrl != null && typeof value.thumbnailUrl !== 'string')
|
if (value.thumbnail !== undefined
|
||||||
|
&& value.thumbnail !== null
|
||||||
|
&& typeof value.thumbnail !== 'string')
|
||||||
|
return undefined
|
||||||
|
if (value.thumbnailBase !== undefined
|
||||||
|
&& value.thumbnailBase !== null
|
||||||
|
&& typeof value.thumbnailBase !== 'string')
|
||||||
|
return undefined
|
||||||
|
if (value.thumbnailUrl !== undefined
|
||||||
|
&& value.thumbnailUrl !== null
|
||||||
|
&& typeof value.thumbnailUrl !== 'string')
|
||||||
return undefined
|
return undefined
|
||||||
|
|
||||||
return {
|
return {
|
||||||
id: Number (value.id),
|
id: Number (value.id),
|
||||||
title: value.title,
|
title: value.title,
|
||||||
url: value.url,
|
url: value.url,
|
||||||
thumbnailUrl: value.thumbnailUrl as string | undefined }
|
thumbnail:
|
||||||
|
value.thumbnail === null
|
||||||
|
? null
|
||||||
|
: typeof value.thumbnail === 'string'
|
||||||
|
? value.thumbnail
|
||||||
|
: value.thumbnailUrl === null
|
||||||
|
? null
|
||||||
|
: typeof value.thumbnailUrl === 'string'
|
||||||
|
? value.thumbnailUrl
|
||||||
|
: undefined,
|
||||||
|
thumbnailBase:
|
||||||
|
value.thumbnailBase === null
|
||||||
|
? null
|
||||||
|
: typeof value.thumbnailBase === 'string'
|
||||||
|
? value.thumbnailBase
|
||||||
|
: undefined }
|
||||||
}
|
}
|
||||||
|
|
||||||
const canonicalisePersistedRow = (
|
const canonicalisePersistedRow = (
|
||||||
@@ -319,12 +392,18 @@ const serialiseExistingPost = (value: PostImportExistingPost | undefined) =>
|
|||||||
id: value.id,
|
id: value.id,
|
||||||
title: value.title,
|
title: value.title,
|
||||||
url: value.url,
|
url: value.url,
|
||||||
thumbnailUrl: value.thumbnailUrl }
|
thumbnail: value.thumbnail,
|
||||||
|
thumbnailBase: value.thumbnailBase }
|
||||||
|
|
||||||
const serialiseResetSnapshot = (value: PostImportResetSnapshot) => ({
|
const serialiseResetSnapshot = (value: PostImportResetSnapshot) => ({
|
||||||
url: value.url,
|
url: value.url,
|
||||||
attributes: Object.fromEntries (
|
attributes: Object.fromEntries (
|
||||||
Object.entries (value.attributes).map (([key, entry]) => [key, entry])),
|
Object.entries (value.attributes).map (([key, entry]) => [key, entry])),
|
||||||
|
displayTags: value.displayTags.map (tag => ({
|
||||||
|
name: tag.name,
|
||||||
|
category: tag.category,
|
||||||
|
sectionLiterals:
|
||||||
|
tag.sectionLiterals == null ? undefined : [...tag.sectionLiterals] })),
|
||||||
provenance: Object.fromEntries (
|
provenance: Object.fromEntries (
|
||||||
Object.entries (value.provenance).map (([key, entry]) => [key, entry])),
|
Object.entries (value.provenance).map (([key, entry]) => [key, entry])),
|
||||||
tagSources: {
|
tagSources: {
|
||||||
@@ -366,6 +445,12 @@ export const serialisePostImportRow = (row: PostImportRow) => {
|
|||||||
Object.entries (row.importErrors).map (([key, entry]) => [key, [...entry]])),
|
Object.entries (row.importErrors).map (([key, entry]) => [key, [...entry]])),
|
||||||
provenance: Object.fromEntries (
|
provenance: Object.fromEntries (
|
||||||
Object.entries (row.provenance).map (([key, entry]) => [key, entry])),
|
Object.entries (row.provenance).map (([key, entry]) => [key, entry])),
|
||||||
|
displayTags:
|
||||||
|
row.displayTags?.map (tag => ({
|
||||||
|
name: tag.name,
|
||||||
|
category: tag.category,
|
||||||
|
sectionLiterals:
|
||||||
|
tag.sectionLiterals == null ? undefined : [...tag.sectionLiterals] })) ?? [],
|
||||||
tagSources:
|
tagSources:
|
||||||
row.tagSources == null
|
row.tagSources == null
|
||||||
? undefined
|
? undefined
|
||||||
@@ -475,6 +560,9 @@ const sanitiseRow = (value: unknown): PostImportRow | null => {
|
|||||||
if (!(Object.values (value.tagSources).every (entry => typeof entry === 'string')))
|
if (!(Object.values (value.tagSources).every (entry => typeof entry === 'string')))
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
|
const displayTags = sanitiseDisplayTags (value.displayTags)
|
||||||
|
if (displayTags == null)
|
||||||
|
return null
|
||||||
|
|
||||||
const existingPost = sanitiseExistingPost (value.existingPost)
|
const existingPost = sanitiseExistingPost (value.existingPost)
|
||||||
const existingPostId =
|
const existingPostId =
|
||||||
@@ -496,6 +584,7 @@ const sanitiseRow = (value: unknown): PostImportRow | null => {
|
|||||||
? importErrors
|
? importErrors
|
||||||
: undefined,
|
: undefined,
|
||||||
provenance: value.provenance as Record<string, PostImportOrigin>,
|
provenance: value.provenance as Record<string, PostImportOrigin>,
|
||||||
|
displayTags,
|
||||||
tagSources: value.tagSources as Record<PostImportOrigin, string> | undefined,
|
tagSources: value.tagSources as Record<PostImportOrigin, string> | undefined,
|
||||||
status: value.status,
|
status: value.status,
|
||||||
skipReason: value.skipReason ?? undefined,
|
skipReason: value.skipReason ?? undefined,
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
import type { Category } from '@/types'
|
||||||
|
|
||||||
export type PostImportOrigin = 'automatic' | 'manual'
|
export type PostImportOrigin = 'automatic' | 'manual'
|
||||||
export type PostImportRepairMode = 'all' | 'failed'
|
export type PostImportRepairMode = 'all' | 'failed'
|
||||||
export type PostImportStatus =
|
export type PostImportStatus =
|
||||||
@@ -9,9 +11,15 @@ export type PostImportSkipReason = 'existing' | 'manual'
|
|||||||
export type PostImportResultStatus = 'created' | 'skipped' | 'failed'
|
export type PostImportResultStatus = 'created' | 'skipped' | 'failed'
|
||||||
export type PostImportAttributeValue = string | number
|
export type PostImportAttributeValue = string | number
|
||||||
|
|
||||||
|
export type PostImportDisplayTag = {
|
||||||
|
name: string
|
||||||
|
category: Category
|
||||||
|
sectionLiterals?: string[] }
|
||||||
|
|
||||||
export type PostImportResetSnapshot = {
|
export type PostImportResetSnapshot = {
|
||||||
url: string
|
url: string
|
||||||
attributes: Record<string, PostImportAttributeValue>
|
attributes: Record<string, PostImportAttributeValue>
|
||||||
|
displayTags: PostImportDisplayTag[]
|
||||||
provenance: Record<string, PostImportOrigin>
|
provenance: Record<string, PostImportOrigin>
|
||||||
tagSources: Record<PostImportOrigin, string>
|
tagSources: Record<PostImportOrigin, string>
|
||||||
fieldWarnings: Record<string, string[]>
|
fieldWarnings: Record<string, string[]>
|
||||||
@@ -22,7 +30,8 @@ export type PostImportExistingPost = {
|
|||||||
id: number
|
id: number
|
||||||
title: string
|
title: string
|
||||||
url: string
|
url: string
|
||||||
thumbnailUrl?: string }
|
thumbnail?: string | null
|
||||||
|
thumbnailBase?: string | null }
|
||||||
|
|
||||||
export type PostImportRow = {
|
export type PostImportRow = {
|
||||||
sourceRow: number
|
sourceRow: number
|
||||||
@@ -39,6 +48,7 @@ export type PostImportRow = {
|
|||||||
existingPostId?: number
|
existingPostId?: number
|
||||||
existingPost?: PostImportExistingPost
|
existingPost?: PostImportExistingPost
|
||||||
metadataUrl?: string
|
metadataUrl?: string
|
||||||
|
displayTags?: PostImportDisplayTag[]
|
||||||
resetSnapshot: PostImportResetSnapshot
|
resetSnapshot: PostImportResetSnapshot
|
||||||
createdPostId?: number
|
createdPostId?: number
|
||||||
importStatus?: PostImportStatus
|
importStatus?: PostImportStatus
|
||||||
|
|||||||
@@ -1,7 +1,9 @@
|
|||||||
|
import { CATEGORIES } from '@/consts'
|
||||||
import { resultRepairMode } from '@/lib/postImportRows'
|
import { resultRepairMode } from '@/lib/postImportRows'
|
||||||
import { validatePostImportSessionId } from '@/lib/postImportStorage'
|
import { validatePostImportSessionId } from '@/lib/postImportStorage'
|
||||||
|
|
||||||
import type {
|
import type {
|
||||||
|
PostImportDisplayTag,
|
||||||
PostImportOrigin,
|
PostImportOrigin,
|
||||||
PostImportRepairMode,
|
PostImportRepairMode,
|
||||||
PostImportRow,
|
PostImportRow,
|
||||||
@@ -18,9 +20,11 @@ type QueryRowState = {
|
|||||||
duration: string
|
duration: string
|
||||||
tags: string
|
tags: string
|
||||||
parent_post_ids: string
|
parent_post_ids: string
|
||||||
|
display_tags: PostImportDisplayTag[]
|
||||||
manual: string[]
|
manual: string[]
|
||||||
skip: PostImportSkipReason | null
|
skip: PostImportSkipReason | null
|
||||||
existing_post_id: number | null
|
existing_post_id: number | null
|
||||||
|
existing_post: PostImportRow['existingPost'] | null
|
||||||
import_status: PostImportRow['importStatus'] | null
|
import_status: PostImportRow['importStatus'] | null
|
||||||
created_post_id: number | null
|
created_post_id: number | null
|
||||||
recoverable: boolean | null }
|
recoverable: boolean | null }
|
||||||
@@ -45,9 +49,11 @@ type EncodedRowState = {
|
|||||||
d?: string
|
d?: string
|
||||||
g?: string
|
g?: string
|
||||||
p?: string
|
p?: string
|
||||||
|
k?: EncodedDisplayTag[]
|
||||||
m?: string[]
|
m?: string[]
|
||||||
s?: EncodedSkip
|
s?: EncodedSkip
|
||||||
e?: number
|
e?: number
|
||||||
|
o?: EncodedExistingPost
|
||||||
i?: EncodedImportStatus
|
i?: EncodedImportStatus
|
||||||
c?: number
|
c?: number
|
||||||
r?: boolean }
|
r?: boolean }
|
||||||
@@ -56,6 +62,18 @@ type EncodedState = {
|
|||||||
v: 1
|
v: 1
|
||||||
rows: EncodedRowState[] }
|
rows: EncodedRowState[] }
|
||||||
|
|
||||||
|
type EncodedDisplayTag = {
|
||||||
|
n: string
|
||||||
|
c: PostImportDisplayTag['category']
|
||||||
|
s?: string[] }
|
||||||
|
|
||||||
|
type EncodedExistingPost = {
|
||||||
|
i: number
|
||||||
|
t: string
|
||||||
|
u: string
|
||||||
|
h?: string | null
|
||||||
|
b?: string | null }
|
||||||
|
|
||||||
export type ParsedPostNewState = {
|
export type ParsedPostNewState = {
|
||||||
rows: PostImportRow[]
|
rows: PostImportRow[]
|
||||||
source: string
|
source: string
|
||||||
@@ -77,9 +95,11 @@ const BASIC_KEYS = [
|
|||||||
'tags',
|
'tags',
|
||||||
'parent_post_ids'] as const
|
'parent_post_ids'] as const
|
||||||
const STATE_KEYS = [
|
const STATE_KEYS = [
|
||||||
|
'display_tags',
|
||||||
'manual',
|
'manual',
|
||||||
'skip',
|
'skip',
|
||||||
'existing_post_id',
|
'existing_post_id',
|
||||||
|
'existing_post',
|
||||||
'import_status',
|
'import_status',
|
||||||
'created_post_id',
|
'created_post_id',
|
||||||
'recoverable'] as const
|
'recoverable'] as const
|
||||||
@@ -95,9 +115,11 @@ const ENCODED_ROW_KEYS = [
|
|||||||
'd',
|
'd',
|
||||||
'g',
|
'g',
|
||||||
'p',
|
'p',
|
||||||
|
'k',
|
||||||
'm',
|
'm',
|
||||||
's',
|
's',
|
||||||
'e',
|
'e',
|
||||||
|
'o',
|
||||||
'i',
|
'i',
|
||||||
'c',
|
'c',
|
||||||
'r'] as const
|
'r'] as const
|
||||||
@@ -200,6 +222,153 @@ const decodeImportStatus = (
|
|||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const sanitiseDisplayTag = (value: unknown): PostImportDisplayTag | null => {
|
||||||
|
if (!(isPlainObject (value)))
|
||||||
|
return null
|
||||||
|
if (!(hasOnlyKeys (value, ['n', 'c', 's'])))
|
||||||
|
return null
|
||||||
|
if (typeof value.n !== 'string' || value.n === '')
|
||||||
|
return null
|
||||||
|
if (!(CATEGORIES.includes (value.c as never)))
|
||||||
|
return null
|
||||||
|
if (value.s != null && !(Array.isArray (value.s)) || !(value.s ?? []).every (entry => typeof entry === 'string'))
|
||||||
|
return null
|
||||||
|
|
||||||
|
return {
|
||||||
|
name: value.n,
|
||||||
|
category: value.c,
|
||||||
|
sectionLiterals: value.s == null ? undefined : [...value.s] }
|
||||||
|
}
|
||||||
|
|
||||||
|
const sanitiseDisplayTags = (
|
||||||
|
value: unknown,
|
||||||
|
): PostImportDisplayTag[] | null => {
|
||||||
|
if (value == null)
|
||||||
|
return []
|
||||||
|
if (!(Array.isArray (value)))
|
||||||
|
return null
|
||||||
|
|
||||||
|
const tags = value.map (sanitiseDisplayTag)
|
||||||
|
return tags.some (tag => tag == null) ? null : (tags as PostImportDisplayTag[])
|
||||||
|
}
|
||||||
|
|
||||||
|
const encodeDisplayTags = (
|
||||||
|
tags: PostImportDisplayTag[] | undefined,
|
||||||
|
): EncodedDisplayTag[] | undefined =>
|
||||||
|
tags == null || tags.length === 0
|
||||||
|
? undefined
|
||||||
|
: tags.map (tag => ({
|
||||||
|
n: tag.name,
|
||||||
|
c: tag.category,
|
||||||
|
s:
|
||||||
|
tag.sectionLiterals == null || tag.sectionLiterals.length === 0
|
||||||
|
? undefined
|
||||||
|
: [...tag.sectionLiterals] }))
|
||||||
|
|
||||||
|
const encodeDisplayTagsParam = (
|
||||||
|
tags: PostImportDisplayTag[] | undefined,
|
||||||
|
): string | null => {
|
||||||
|
const encodedTags = encodeDisplayTags (tags)
|
||||||
|
if (encodedTags == null)
|
||||||
|
return null
|
||||||
|
|
||||||
|
return encodeBase64UrlBytes (textEncoder.encode (JSON.stringify (encodedTags)))
|
||||||
|
}
|
||||||
|
|
||||||
|
const decodeDisplayTagsParam = (
|
||||||
|
value: string | null,
|
||||||
|
): PostImportDisplayTag[] | null => {
|
||||||
|
if (value == null || value === '')
|
||||||
|
return []
|
||||||
|
|
||||||
|
const bytes = decodeBase64UrlBytes (value)
|
||||||
|
if (bytes == null || bytes.byteLength > MAX_DECOMPRESSED_STATE_BYTES)
|
||||||
|
return null
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
return sanitiseDisplayTags (JSON.parse (textDecoder.decode (bytes)))
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
const sanitiseExistingPost = (
|
||||||
|
value: unknown,
|
||||||
|
): PostImportRow['existingPost'] | null => {
|
||||||
|
if (value == null)
|
||||||
|
return null
|
||||||
|
if (!(isPlainObject (value)))
|
||||||
|
return null
|
||||||
|
if (!(hasOnlyKeys (value, ['i', 't', 'u', 'h', 'b'])))
|
||||||
|
return null
|
||||||
|
if (!(typeof value.i === 'number'
|
||||||
|
&& Number.isSafeInteger (value.i)
|
||||||
|
&& value.i > 0))
|
||||||
|
return null
|
||||||
|
if (typeof value.t !== 'string' || typeof value.u !== 'string')
|
||||||
|
return null
|
||||||
|
if (value.h != null && typeof value.h !== 'string')
|
||||||
|
return null
|
||||||
|
if (value.b != null && typeof value.b !== 'string')
|
||||||
|
return null
|
||||||
|
|
||||||
|
return {
|
||||||
|
id: value.i,
|
||||||
|
title: value.t,
|
||||||
|
url: value.u,
|
||||||
|
thumbnail: value.h,
|
||||||
|
thumbnailBase: value.b }
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
const encodeExistingPost = (
|
||||||
|
post: PostImportRow['existingPost'] | undefined,
|
||||||
|
): EncodedExistingPost | undefined =>
|
||||||
|
post == null
|
||||||
|
? undefined
|
||||||
|
: {
|
||||||
|
i: post.id,
|
||||||
|
t: post.title,
|
||||||
|
u: post.url,
|
||||||
|
h: post.thumbnail,
|
||||||
|
b: post.thumbnailBase }
|
||||||
|
|
||||||
|
|
||||||
|
const encodeExistingPostParam = (
|
||||||
|
post: PostImportRow['existingPost'] | undefined,
|
||||||
|
): string | null => {
|
||||||
|
const encodedPost = encodeExistingPost (post)
|
||||||
|
if (encodedPost == null)
|
||||||
|
return null
|
||||||
|
|
||||||
|
return encodeBase64UrlBytes (textEncoder.encode (JSON.stringify (encodedPost)))
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
const decodeExistingPostParam = (
|
||||||
|
value: string | null,
|
||||||
|
): PostImportRow['existingPost'] | null => {
|
||||||
|
if (value == null || value === '')
|
||||||
|
return null
|
||||||
|
|
||||||
|
const bytes = decodeBase64UrlBytes (value)
|
||||||
|
if (bytes == null || bytes.byteLength > MAX_DECOMPRESSED_STATE_BYTES)
|
||||||
|
return null
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
return sanitiseExistingPost (JSON.parse (textDecoder.decode (bytes)))
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
const isStrictPositiveIntegerString = (value: string): boolean =>
|
const isStrictPositiveIntegerString = (value: string): boolean =>
|
||||||
/^[1-9][0-9]*$/.test (value)
|
/^[1-9][0-9]*$/.test (value)
|
||||||
@@ -338,9 +507,11 @@ const baseRowState = (row: PostImportRow): QueryRowState => ({
|
|||||||
duration: String (row.attributes.duration ?? ''),
|
duration: String (row.attributes.duration ?? ''),
|
||||||
tags: String (row.attributes.tags ?? ''),
|
tags: String (row.attributes.tags ?? ''),
|
||||||
parent_post_ids: String (row.attributes.parentPostIds ?? ''),
|
parent_post_ids: String (row.attributes.parentPostIds ?? ''),
|
||||||
|
display_tags: row.displayTags ?? [],
|
||||||
manual: manualFields (row),
|
manual: manualFields (row),
|
||||||
skip: row.skipReason ?? null,
|
skip: row.skipReason ?? null,
|
||||||
existing_post_id: row.existingPostId ?? null,
|
existing_post_id: row.existingPostId ?? null,
|
||||||
|
existing_post: row.existingPost ?? null,
|
||||||
import_status: row.importStatus ?? null,
|
import_status: row.importStatus ?? null,
|
||||||
created_post_id: row.createdPostId ?? null,
|
created_post_id: row.createdPostId ?? null,
|
||||||
recoverable: row.recoverable ?? null })
|
recoverable: row.recoverable ?? null })
|
||||||
@@ -379,13 +550,36 @@ const buildRow = (state: FullRowState): PostImportRow => {
|
|||||||
baseWarnings: [],
|
baseWarnings: [],
|
||||||
validationErrors: { },
|
validationErrors: { },
|
||||||
provenance,
|
provenance,
|
||||||
|
displayTags: state.display_tags.map (tag => ({
|
||||||
|
name: tag.name,
|
||||||
|
category: tag.category,
|
||||||
|
sectionLiterals:
|
||||||
|
tag.sectionLiterals == null
|
||||||
|
? undefined
|
||||||
|
: [...tag.sectionLiterals] })),
|
||||||
tagSources,
|
tagSources,
|
||||||
status: 'pending',
|
status: 'pending',
|
||||||
skipReason: state.skip ?? undefined,
|
skipReason: state.skip ?? undefined,
|
||||||
existingPostId: state.existing_post_id ?? undefined,
|
existingPostId: state.existing_post_id ?? undefined,
|
||||||
|
existingPost:
|
||||||
|
state.existing_post == null
|
||||||
|
? undefined
|
||||||
|
: {
|
||||||
|
id: state.existing_post.id,
|
||||||
|
title: state.existing_post.title,
|
||||||
|
url: state.existing_post.url,
|
||||||
|
thumbnail: state.existing_post.thumbnail,
|
||||||
|
thumbnailBase: state.existing_post.thumbnailBase },
|
||||||
resetSnapshot: {
|
resetSnapshot: {
|
||||||
url: state.url,
|
url: state.url,
|
||||||
attributes: { ...attributes },
|
attributes: { ...attributes },
|
||||||
|
displayTags: state.display_tags.map (tag => ({
|
||||||
|
name: tag.name,
|
||||||
|
category: tag.category,
|
||||||
|
sectionLiterals:
|
||||||
|
tag.sectionLiterals == null
|
||||||
|
? undefined
|
||||||
|
: [...tag.sectionLiterals] })),
|
||||||
provenance: { ...provenance },
|
provenance: { ...provenance },
|
||||||
tagSources: { ...tagSources },
|
tagSources: { ...tagSources },
|
||||||
fieldWarnings: { },
|
fieldWarnings: { },
|
||||||
@@ -429,6 +623,8 @@ const parseEncodedRow = (
|
|||||||
const createdPostId = row['c']
|
const createdPostId = row['c']
|
||||||
const recoverable = row['r']
|
const recoverable = row['r']
|
||||||
const sourceRow = row['n']
|
const sourceRow = row['n']
|
||||||
|
const displayTags = sanitiseDisplayTags (row['k'])
|
||||||
|
const existingPost = sanitiseExistingPost (row['o'])
|
||||||
|
|
||||||
if (requireUrl && typeof url !== 'string')
|
if (requireUrl && typeof url !== 'string')
|
||||||
return null
|
return null
|
||||||
@@ -454,6 +650,10 @@ const parseEncodedRow = (
|
|||||||
return null
|
return null
|
||||||
if (recoverable != null && typeof recoverable !== 'boolean')
|
if (recoverable != null && typeof recoverable !== 'boolean')
|
||||||
return null
|
return null
|
||||||
|
if (displayTags == null)
|
||||||
|
return null
|
||||||
|
if (row['o'] != null && existingPost == null)
|
||||||
|
return null
|
||||||
if (sourceRow != null
|
if (sourceRow != null
|
||||||
&& !(typeof sourceRow === 'number'
|
&& !(typeof sourceRow === 'number'
|
||||||
&& Number.isSafeInteger (sourceRow)
|
&& Number.isSafeInteger (sourceRow)
|
||||||
@@ -461,6 +661,8 @@ const parseEncodedRow = (
|
|||||||
return null
|
return null
|
||||||
if (skip === 'existing' && existingPostId == null)
|
if (skip === 'existing' && existingPostId == null)
|
||||||
return null
|
return null
|
||||||
|
if (existingPost != null && existingPost.id !== existingPostId)
|
||||||
|
return null
|
||||||
if (importStatus === 'created' && createdPostId == null)
|
if (importStatus === 'created' && createdPostId == null)
|
||||||
return null
|
return null
|
||||||
if (importStatus !== 'created' && createdPostId != null)
|
if (importStatus !== 'created' && createdPostId != null)
|
||||||
@@ -512,9 +714,11 @@ const parseEncodedRow = (
|
|||||||
duration: parsedStrings.duration,
|
duration: parsedStrings.duration,
|
||||||
tags: parsedStrings.tags,
|
tags: parsedStrings.tags,
|
||||||
parent_post_ids: parsedStrings.parent_post_ids,
|
parent_post_ids: parsedStrings.parent_post_ids,
|
||||||
|
display_tags: displayTags,
|
||||||
manual: manual ?? [],
|
manual: manual ?? [],
|
||||||
skip,
|
skip,
|
||||||
existing_post_id: existingPostId ?? null,
|
existing_post_id: existingPostId ?? null,
|
||||||
|
existing_post: existingPost,
|
||||||
import_status: importStatus,
|
import_status: importStatus,
|
||||||
created_post_id: createdPostId ?? null,
|
created_post_id: createdPostId ?? null,
|
||||||
recoverable: recoverable ?? null }
|
recoverable: recoverable ?? null }
|
||||||
@@ -543,9 +747,11 @@ const parseSingleRow = (params: URLSearchParams): ParsedPostNewState | null => {
|
|||||||
duration: '',
|
duration: '',
|
||||||
tags: '',
|
tags: '',
|
||||||
parent_post_ids: '',
|
parent_post_ids: '',
|
||||||
|
display_tags: [],
|
||||||
manual: [],
|
manual: [],
|
||||||
skip: null,
|
skip: null,
|
||||||
existing_post_id: null,
|
existing_post_id: null,
|
||||||
|
existing_post: null,
|
||||||
import_status: null,
|
import_status: null,
|
||||||
created_post_id: null,
|
created_post_id: null,
|
||||||
recoverable: null })
|
recoverable: null })
|
||||||
@@ -580,14 +786,22 @@ const parseSingleRow = (params: URLSearchParams): ParsedPostNewState | null => {
|
|||||||
return null
|
return null
|
||||||
const existingPostIdValue = params.get ('existing_post_id')
|
const existingPostIdValue = params.get ('existing_post_id')
|
||||||
const createdPostIdValue = params.get ('created_post_id')
|
const createdPostIdValue = params.get ('created_post_id')
|
||||||
|
const displayTags = decodeDisplayTagsParam (params.get ('display_tags'))
|
||||||
|
const existingPost = decodeExistingPostParam (params.get ('existing_post'))
|
||||||
const existingPostId = parsePositiveInt (existingPostIdValue)
|
const existingPostId = parsePositiveInt (existingPostIdValue)
|
||||||
const createdPostId = parsePositiveInt (createdPostIdValue)
|
const createdPostId = parsePositiveInt (createdPostIdValue)
|
||||||
|
if (displayTags == null)
|
||||||
|
return null
|
||||||
|
if (params.get ('existing_post') != null && existingPost == null)
|
||||||
|
return null
|
||||||
if (existingPostIdValue != null && existingPostId == null)
|
if (existingPostIdValue != null && existingPostId == null)
|
||||||
return null
|
return null
|
||||||
if (createdPostIdValue != null && createdPostId == null)
|
if (createdPostIdValue != null && createdPostId == null)
|
||||||
return null
|
return null
|
||||||
if (skipValue === 'existing' && existingPostId == null)
|
if (skipValue === 'existing' && existingPostId == null)
|
||||||
return null
|
return null
|
||||||
|
if (existingPost != null && existingPost.id !== existingPostId)
|
||||||
|
return null
|
||||||
if (importStatus === 'created' && createdPostId == null)
|
if (importStatus === 'created' && createdPostId == null)
|
||||||
return null
|
return null
|
||||||
if (importStatus !== 'created' && createdPostIdValue != null)
|
if (importStatus !== 'created' && createdPostIdValue != null)
|
||||||
@@ -608,9 +822,11 @@ const parseSingleRow = (params: URLSearchParams): ParsedPostNewState | null => {
|
|||||||
duration: params.get ('duration') ?? '',
|
duration: params.get ('duration') ?? '',
|
||||||
tags: params.get ('tags') ?? '',
|
tags: params.get ('tags') ?? '',
|
||||||
parent_post_ids: params.get ('parent_post_ids') ?? '',
|
parent_post_ids: params.get ('parent_post_ids') ?? '',
|
||||||
|
display_tags: displayTags,
|
||||||
manual: parseManual (params.get ('manual')),
|
manual: parseManual (params.get ('manual')),
|
||||||
skip: skipValue,
|
skip: skipValue,
|
||||||
existing_post_id: existingPostId,
|
existing_post_id: existingPostId,
|
||||||
|
existing_post: existingPost,
|
||||||
import_status: importStatus,
|
import_status: importStatus,
|
||||||
created_post_id: createdPostId,
|
created_post_id: createdPostId,
|
||||||
recoverable })
|
recoverable })
|
||||||
@@ -727,6 +943,12 @@ const regularQuery = (rows: PostImportRow[]): URLSearchParams => {
|
|||||||
params.set ('duration', String (row?.attributes.duration ?? ''))
|
params.set ('duration', String (row?.attributes.duration ?? ''))
|
||||||
params.set ('tags', String (row?.attributes.tags ?? ''))
|
params.set ('tags', String (row?.attributes.tags ?? ''))
|
||||||
params.set ('parent_post_ids', String (row?.attributes.parentPostIds ?? ''))
|
params.set ('parent_post_ids', String (row?.attributes.parentPostIds ?? ''))
|
||||||
|
const displayTags = encodeDisplayTagsParam (row?.displayTags)
|
||||||
|
if (displayTags != null)
|
||||||
|
params.set ('display_tags', displayTags)
|
||||||
|
const existingPost = encodeExistingPostParam (row?.existingPost)
|
||||||
|
if (existingPost != null)
|
||||||
|
params.set ('existing_post', existingPost)
|
||||||
const manual = manualFields (row).join (',')
|
const manual = manualFields (row).join (',')
|
||||||
if (manual !== '')
|
if (manual !== '')
|
||||||
params.set ('manual', manual)
|
params.set ('manual', manual)
|
||||||
@@ -776,12 +998,16 @@ const encodeRowState = (
|
|||||||
encoded.g = base.tags
|
encoded.g = base.tags
|
||||||
if (base.parent_post_ids !== '')
|
if (base.parent_post_ids !== '')
|
||||||
encoded.p = base.parent_post_ids
|
encoded.p = base.parent_post_ids
|
||||||
|
if (base.display_tags.length > 0)
|
||||||
|
encoded.k = encodeDisplayTags (base.display_tags)
|
||||||
if (base.manual.length > 0)
|
if (base.manual.length > 0)
|
||||||
encoded.m = [...base.manual]
|
encoded.m = [...base.manual]
|
||||||
if (base.skip != null)
|
if (base.skip != null)
|
||||||
encoded.s = encodeSkip (base.skip)
|
encoded.s = encodeSkip (base.skip)
|
||||||
if (base.existing_post_id != null)
|
if (base.existing_post_id != null)
|
||||||
encoded.e = base.existing_post_id
|
encoded.e = base.existing_post_id
|
||||||
|
if (base.existing_post != null)
|
||||||
|
encoded.o = encodeExistingPost (base.existing_post)
|
||||||
if (base.import_status != null)
|
if (base.import_status != null)
|
||||||
encoded.i = encodeImportStatus (base.import_status)
|
encoded.i = encodeImportStatus (base.import_status)
|
||||||
if (base.created_post_id != null)
|
if (base.created_post_id != null)
|
||||||
|
|||||||
@@ -74,6 +74,7 @@ type PostMetadataResponse = {
|
|||||||
duration?: string
|
duration?: string
|
||||||
videoMs?: number
|
videoMs?: number
|
||||||
tags?: string
|
tags?: string
|
||||||
|
displayTags?: PostImportRow['displayTags']
|
||||||
fieldWarnings?: Record<string, string[]>
|
fieldWarnings?: Record<string, string[]>
|
||||||
baseWarnings?: string[]
|
baseWarnings?: string[]
|
||||||
validationErrors?: Record<string, string[]>
|
validationErrors?: Record<string, string[]>
|
||||||
@@ -82,7 +83,8 @@ type PostMetadataResponse = {
|
|||||||
id: number
|
id: number
|
||||||
title: string
|
title: string
|
||||||
url: string
|
url: string
|
||||||
thumbnailUrl?: string } | null }
|
thumbnail?: string | null
|
||||||
|
thumbnailBase?: string | null } | null }
|
||||||
|
|
||||||
type BulkApiRow = {
|
type BulkApiRow = {
|
||||||
status: 'created' | 'skipped' | 'failed'
|
status: 'created' | 'skipped' | 'failed'
|
||||||
@@ -92,7 +94,8 @@ type BulkApiRow = {
|
|||||||
id: number
|
id: number
|
||||||
title: string
|
title: string
|
||||||
url: string
|
url: string
|
||||||
thumbnailUrl?: string } | null
|
thumbnail?: string | null
|
||||||
|
thumbnailBase?: string | null } | null
|
||||||
fieldWarnings?: Record<string, string[]>
|
fieldWarnings?: Record<string, string[]>
|
||||||
baseWarnings?: string[]
|
baseWarnings?: string[]
|
||||||
errors?: Record<string, string[]>
|
errors?: Record<string, string[]>
|
||||||
@@ -238,6 +241,14 @@ const mergeDryRunRow = (
|
|||||||
videoMs: result.videoMs ?? '',
|
videoMs: result.videoMs ?? '',
|
||||||
tags: result.tags ?? '',
|
tags: result.tags ?? '',
|
||||||
parentPostIds: String (result.parentPostIds ?? currentRow.attributes.parentPostIds ?? '') },
|
parentPostIds: String (result.parentPostIds ?? currentRow.attributes.parentPostIds ?? '') },
|
||||||
|
displayTags:
|
||||||
|
result.displayTags?.map (tag => ({
|
||||||
|
name: tag.name,
|
||||||
|
category: tag.category,
|
||||||
|
sectionLiterals:
|
||||||
|
tag.sectionLiterals == null
|
||||||
|
? undefined
|
||||||
|
: [...tag.sectionLiterals] })) ?? [],
|
||||||
fieldWarnings,
|
fieldWarnings,
|
||||||
baseWarnings: result.baseWarnings ?? [],
|
baseWarnings: result.baseWarnings ?? [],
|
||||||
validationErrors: { },
|
validationErrors: { },
|
||||||
@@ -273,6 +284,14 @@ const buildPreviewResetSnapshot = (
|
|||||||
videoMs: preview.videoMs ?? '',
|
videoMs: preview.videoMs ?? '',
|
||||||
tags: preview.tags ?? '',
|
tags: preview.tags ?? '',
|
||||||
parentPostIds: String (preview.parentPostIds ?? '') },
|
parentPostIds: String (preview.parentPostIds ?? '') },
|
||||||
|
displayTags:
|
||||||
|
preview.displayTags?.map (tag => ({
|
||||||
|
name: tag.name,
|
||||||
|
category: tag.category,
|
||||||
|
sectionLiterals:
|
||||||
|
tag.sectionLiterals == null
|
||||||
|
? undefined
|
||||||
|
: [...tag.sectionLiterals] })) ?? [],
|
||||||
provenance: {
|
provenance: {
|
||||||
url: 'manual',
|
url: 'manual',
|
||||||
title: 'automatic',
|
title: 'automatic',
|
||||||
@@ -368,6 +387,14 @@ const mergePreviewRow = (
|
|||||||
...currentRow,
|
...currentRow,
|
||||||
attributes: { ...currentRow.attributes },
|
attributes: { ...currentRow.attributes },
|
||||||
provenance: { ...currentRow.provenance },
|
provenance: { ...currentRow.provenance },
|
||||||
|
displayTags:
|
||||||
|
currentRow.displayTags?.map (tag => ({
|
||||||
|
name: tag.name,
|
||||||
|
category: tag.category,
|
||||||
|
sectionLiterals:
|
||||||
|
tag.sectionLiterals == null
|
||||||
|
? undefined
|
||||||
|
: [...tag.sectionLiterals] })) ?? [],
|
||||||
tagSources: {
|
tagSources: {
|
||||||
automatic: currentRow.tagSources?.automatic ?? '',
|
automatic: currentRow.tagSources?.automatic ?? '',
|
||||||
manual: currentRow.tagSources?.manual ?? '' },
|
manual: currentRow.tagSources?.manual ?? '' },
|
||||||
@@ -406,6 +433,14 @@ const mergePreviewRow = (
|
|||||||
if (currentRow.provenance.tags !== 'manual')
|
if (currentRow.provenance.tags !== 'manual')
|
||||||
{
|
{
|
||||||
nextRow.attributes.tags = preview.tags ?? ''
|
nextRow.attributes.tags = preview.tags ?? ''
|
||||||
|
nextRow.displayTags =
|
||||||
|
preview.displayTags?.map (tag => ({
|
||||||
|
name: tag.name,
|
||||||
|
category: tag.category,
|
||||||
|
sectionLiterals:
|
||||||
|
tag.sectionLiterals == null
|
||||||
|
? undefined
|
||||||
|
: [...tag.sectionLiterals] })) ?? []
|
||||||
}
|
}
|
||||||
nextRow.tagSources.automatic = preview.tags ?? ''
|
nextRow.tagSources.automatic = preview.tags ?? ''
|
||||||
if (currentRow.provenance.parentPostIds !== 'manual')
|
if (currentRow.provenance.parentPostIds !== 'manual')
|
||||||
@@ -459,7 +494,7 @@ const ExistingSkippedRows: FC<ExistingSkippedRowsProps> = ({ id, rows }) => {
|
|||||||
rel="noopener noreferrer"
|
rel="noopener noreferrer"
|
||||||
className="flex items-center gap-3 p-3">
|
className="flex items-center gap-3 p-3">
|
||||||
<PostThumbnailPreview
|
<PostThumbnailPreview
|
||||||
url={post.thumbnailUrl ?? ''}
|
url={post.thumbnail || post.thumbnailBase || ''}
|
||||||
className="h-10 w-10 shrink-0"/>
|
className="h-10 w-10 shrink-0"/>
|
||||||
<div className="min-w-0 flex-1">
|
<div className="min-w-0 flex-1">
|
||||||
<div className="truncate text-sm font-medium">
|
<div className="truncate text-sm font-medium">
|
||||||
@@ -809,6 +844,14 @@ const PostImportReviewPage: FC<Props> = ({ user }) => {
|
|||||||
? { ...currentRow,
|
? { ...currentRow,
|
||||||
url: currentRow.resetSnapshot.url,
|
url: currentRow.resetSnapshot.url,
|
||||||
attributes: { ...currentRow.resetSnapshot.attributes },
|
attributes: { ...currentRow.resetSnapshot.attributes },
|
||||||
|
displayTags:
|
||||||
|
currentRow.resetSnapshot.displayTags.map (tag => ({
|
||||||
|
name: tag.name,
|
||||||
|
category: tag.category,
|
||||||
|
sectionLiterals:
|
||||||
|
tag.sectionLiterals == null
|
||||||
|
? undefined
|
||||||
|
: [...tag.sectionLiterals] })),
|
||||||
provenance: { ...currentRow.resetSnapshot.provenance },
|
provenance: { ...currentRow.resetSnapshot.provenance },
|
||||||
tagSources: { ...currentRow.resetSnapshot.tagSources },
|
tagSources: { ...currentRow.resetSnapshot.tagSources },
|
||||||
fieldWarnings: Object.fromEntries (
|
fieldWarnings: Object.fromEntries (
|
||||||
|
|||||||
@@ -76,6 +76,7 @@ const buildInitialRows = (source: string): PostImportRow[] =>
|
|||||||
automatic: '',
|
automatic: '',
|
||||||
manual: '' },
|
manual: '' },
|
||||||
status: 'pending' as const,
|
status: 'pending' as const,
|
||||||
|
displayTags: [],
|
||||||
resetSnapshot: {
|
resetSnapshot: {
|
||||||
url: row.url,
|
url: row.url,
|
||||||
attributes: {
|
attributes: {
|
||||||
@@ -100,6 +101,7 @@ const buildInitialRows = (source: string): PostImportRow[] =>
|
|||||||
tagSources: {
|
tagSources: {
|
||||||
automatic: '',
|
automatic: '',
|
||||||
manual: '' },
|
manual: '' },
|
||||||
|
displayTags: [],
|
||||||
fieldWarnings: { },
|
fieldWarnings: { },
|
||||||
baseWarnings: [] } }))
|
baseWarnings: [] } }))
|
||||||
|
|
||||||
|
|||||||
新しい課題から参照
ユーザをブロックする