This commit is contained in:
@@ -109,7 +109,7 @@ class PostsController < ApplicationController
|
|||||||
|
|
||||||
render json: PostRepr.base(post, current_user)
|
render json: PostRepr.base(post, current_user)
|
||||||
.merge(tags: build_tag_tree_for(post.tags),
|
.merge(tags: build_tag_tree_for(post.tags),
|
||||||
related: post.related(limit: 20))
|
related: PostRepr.many(post.related(limit: 20)))
|
||||||
end
|
end
|
||||||
|
|
||||||
def create
|
def create
|
||||||
|
|||||||
@@ -37,18 +37,26 @@ class Post < ApplicationRecord
|
|||||||
|
|
||||||
def parent_posts = parents
|
def parent_posts = parents
|
||||||
|
|
||||||
|
def child_posts = children
|
||||||
|
|
||||||
|
def sibling_posts
|
||||||
|
parent_post_ids = parent_posts.order(:id).pluck(:id)
|
||||||
|
|
||||||
|
parent_post_ids.to_h { [_1, PostImplication.where(parent_post: _1).map(&:post)] }
|
||||||
|
end
|
||||||
|
|
||||||
def as_json options = { }
|
def as_json options = { }
|
||||||
super(options).merge({ thumbnail: thumbnail.attached? ?
|
super(options).merge(thumbnail: thumbnail.attached? ?
|
||||||
Rails.application.routes.url_helpers.rails_blob_url(
|
Rails.application.routes.url_helpers.rails_blob_url(
|
||||||
thumbnail, only_path: false) :
|
thumbnail, only_path: false) :
|
||||||
nil })
|
nil)
|
||||||
rescue
|
rescue
|
||||||
super(options).merge(thumbnail: nil)
|
super(options).merge(thumbnail: nil)
|
||||||
end
|
end
|
||||||
|
|
||||||
def snapshot_tag_names = tags.joins(:tag_name).order('tag_names.name').pluck('tag_names.name')
|
def snapshot_tag_names = tags.joins(:tag_name).order('tag_names.name').pluck('tag_names.name')
|
||||||
|
|
||||||
def snapshot_parent_post_ids = parents.order(:parent_post_id).pulck(:parent_post_id)
|
def snapshot_parent_post_ids = parents.order(:parent_post_id).pluck(:parent_post_id)
|
||||||
|
|
||||||
def related limit: nil
|
def related limit: nil
|
||||||
ids = post_similarities.order(cos: :desc)
|
ids = post_similarities.order(cos: :desc)
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
|
|
||||||
module PostRepr
|
module PostRepr
|
||||||
BASE = { include: { tags: TagRepr::BASE, uploaded_user: UserRepr::BASE },
|
BASE = { include: { tags: TagRepr::BASE, uploaded_user: UserRepr::BASE },
|
||||||
methods: [:parent_posts] }.freeze
|
methods: [:parent_posts, :child_posts, :sibling_posts] }.freeze
|
||||||
|
|
||||||
module_function
|
module_function
|
||||||
|
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import PostFormTagsArea from '@/components/PostFormTagsArea'
|
|||||||
import PostOriginalCreatedTimeField from '@/components/PostOriginalCreatedTimeField'
|
import PostOriginalCreatedTimeField from '@/components/PostOriginalCreatedTimeField'
|
||||||
import Label from '@/components/common/Label'
|
import Label from '@/components/common/Label'
|
||||||
import { Button } from '@/components/ui/button'
|
import { Button } from '@/components/ui/button'
|
||||||
|
import { toast } from '@/components/ui/use-toast'
|
||||||
import { apiPut } from '@/lib/api'
|
import { apiPut } from '@/lib/api'
|
||||||
|
|
||||||
import type { FC } from 'react'
|
import type { FC } from 'react'
|
||||||
@@ -35,13 +36,17 @@ export default (({ post, onSave }: Props) => {
|
|||||||
useState<string | null> (post.originalCreatedBefore)
|
useState<string | null> (post.originalCreatedBefore)
|
||||||
const [originalCreatedFrom, setOriginalCreatedFrom] =
|
const [originalCreatedFrom, setOriginalCreatedFrom] =
|
||||||
useState<string | null> (post.originalCreatedFrom)
|
useState<string | null> (post.originalCreatedFrom)
|
||||||
const [title, setTitle] = useState (post.title)
|
const [parentPostIds, setParentPostIds] = useState (post.parentPosts!.map (p => p.id).join (' '))
|
||||||
const [tags, setTags] = useState<string> ('')
|
const [tags, setTags] = useState<string> ('')
|
||||||
|
const [title, setTitle] = useState (post.title)
|
||||||
|
|
||||||
const handleSubmit = async () => {
|
const handleSubmit = async () => {
|
||||||
|
try
|
||||||
|
{
|
||||||
const data = await apiPut<Post> (
|
const data = await apiPut<Post> (
|
||||||
`/posts/${ post.id }`,
|
`/posts/${ post.id }`,
|
||||||
{ title, tags, original_created_from: originalCreatedFrom,
|
{ title, tags, parent_post_ids: parentPostIds,
|
||||||
|
original_created_from: originalCreatedFrom,
|
||||||
original_created_before: originalCreatedBefore },
|
original_created_before: originalCreatedBefore },
|
||||||
{ headers: { 'Content-Type': 'multipart/form-data' } })
|
{ headers: { 'Content-Type': 'multipart/form-data' } })
|
||||||
onSave ({ ...post,
|
onSave ({ ...post,
|
||||||
@@ -49,6 +54,12 @@ export default (({ post, onSave }: Props) => {
|
|||||||
tags: data.tags,
|
tags: data.tags,
|
||||||
originalCreatedFrom: data.originalCreatedFrom,
|
originalCreatedFrom: data.originalCreatedFrom,
|
||||||
originalCreatedBefore: data.originalCreatedBefore } as Post)
|
originalCreatedBefore: data.originalCreatedBefore } as Post)
|
||||||
|
toast ({ description: '更新しました.' })
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
toast ({ description: '更新はできなかったよ……' })
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
useEffect (() => {
|
useEffect (() => {
|
||||||
@@ -66,6 +77,16 @@ export default (({ post, onSave }: Props) => {
|
|||||||
onChange={ev => setTitle (ev.target.value)}/>
|
onChange={ev => setTitle (ev.target.value)}/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{/* 親投稿 */}
|
||||||
|
<div>
|
||||||
|
<Label>親投稿</Label>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
value={parentPostIds}
|
||||||
|
onChange={e => setParentPostIds (e.target.value)}
|
||||||
|
className="w-full border p-2 rounded"/>
|
||||||
|
</div>
|
||||||
|
|
||||||
{/* タグ */}
|
{/* タグ */}
|
||||||
<PostFormTagsArea tags={tags} setTags={setTags}/>
|
<PostFormTagsArea tags={tags} setTags={setTags}/>
|
||||||
|
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import { useRef } from 'react'
|
|||||||
import { useLocation } from 'react-router-dom'
|
import { useLocation } from 'react-router-dom'
|
||||||
|
|
||||||
import PrefetchLink from '@/components/PrefetchLink'
|
import PrefetchLink from '@/components/PrefetchLink'
|
||||||
|
import { cn } from '@/lib/utils'
|
||||||
import { useSharedTransitionStore } from '@/stores/sharedTransitionStore'
|
import { useSharedTransitionStore } from '@/stores/sharedTransitionStore'
|
||||||
|
|
||||||
import type { FC, MouseEvent } from 'react'
|
import type { FC, MouseEvent } from 'react'
|
||||||
@@ -39,8 +40,10 @@ export default (({ posts, onClick }: Props) => {
|
|||||||
<motion.div
|
<motion.div
|
||||||
ref={cardRef}
|
ref={cardRef}
|
||||||
layoutId={layoutId}
|
layoutId={layoutId}
|
||||||
className="w-full h-full overflow-hidden rounded-xl shadow
|
className={cn ('w-full h-full overflow-hidden rounded-xl shadow',
|
||||||
transform-gpu will-change-transform"
|
'transform-gpu will-change-transform',
|
||||||
|
(post.childPosts ?? []).length > 0 && 'border-4 border-green-500',
|
||||||
|
(post.parentPosts ?? []).length > 0 && 'border-4 border-yellow-500')}
|
||||||
whileHover={{ scale: 1.02 }}
|
whileHover={{ scale: 1.02 }}
|
||||||
onLayoutAnimationStart={() => {
|
onLayoutAnimationStart={() => {
|
||||||
if (!(cardRef.current))
|
if (!(cardRef.current))
|
||||||
|
|||||||
@@ -21,7 +21,7 @@ import ServiceUnavailable from '@/pages/ServiceUnavailable'
|
|||||||
|
|
||||||
import type { FC } from 'react'
|
import type { FC } from 'react'
|
||||||
|
|
||||||
import type { NiconicoViewerHandle, User } from '@/types'
|
import type { NiconicoViewerHandle, Post, User } from '@/types'
|
||||||
|
|
||||||
type Props = { user: User | null }
|
type Props = { user: User | null }
|
||||||
|
|
||||||
@@ -108,7 +108,34 @@ export default (({ user }: Props) => {
|
|||||||
{post
|
{post
|
||||||
? (
|
? (
|
||||||
<>
|
<>
|
||||||
{/* TODO: 親投稿リスト */}
|
{(post.childPosts ?? []).length > 0 && (
|
||||||
|
<div className="mb-4 bg-green-200 dark:bg-green-800 text-sm p-2 rounded-md">
|
||||||
|
<p>この投稿には {post.childPosts!.length} 件の子投稿があります.</p>
|
||||||
|
<PostList posts={[{ ...post, childPosts: [{ } as Post] },
|
||||||
|
...post.childPosts!.map (p => ({
|
||||||
|
...p, parentPosts: [{ } as Post] }))]}/>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{(post.parentPosts ?? []).map (pp => {
|
||||||
|
const siblings = post.siblingPosts?.[String (pp.id) as `${ number }`]
|
||||||
|
if (!(siblings))
|
||||||
|
return
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
key={pp.id}
|
||||||
|
className="mb-4 bg-yellow-200 dark:bg-yellow-800 text-sm p-2 rounded-md">
|
||||||
|
<p>
|
||||||
|
この投稿には 1 件の親投稿{
|
||||||
|
siblings.length > 1
|
||||||
|
&& `と ${ siblings.length - 1 } 件の姉妹投稿`}があります.
|
||||||
|
</p>
|
||||||
|
<PostList posts={[{ ...pp, childPosts: [{ } as Post] },
|
||||||
|
...siblings.map (p => ({
|
||||||
|
...p, parentPosts: [{ } as Post] }))]}/>
|
||||||
|
</div>)
|
||||||
|
})}
|
||||||
|
|
||||||
{(post.thumbnail || post.thumbnailBase) && (
|
{(post.thumbnail || post.thumbnailBase) && (
|
||||||
<motion.div
|
<motion.div
|
||||||
layoutId={`page-${ id }`}
|
layoutId={`page-${ id }`}
|
||||||
@@ -147,7 +174,6 @@ export default (({ user }: Props) => {
|
|||||||
(prev: any) => newPost ?? prev)
|
(prev: any) => newPost ?? prev)
|
||||||
qc.invalidateQueries ({ queryKey: postsKeys.root })
|
qc.invalidateQueries ({ queryKey: postsKeys.root })
|
||||||
qc.invalidateQueries ({ queryKey: tagsKeys.root })
|
qc.invalidateQueries ({ queryKey: tagsKeys.root })
|
||||||
toast ({ description: '更新しました.' })
|
|
||||||
}}/>
|
}}/>
|
||||||
</Tab>)}
|
</Tab>)}
|
||||||
</TabGroup>
|
</TabGroup>
|
||||||
|
|||||||
@@ -95,6 +95,8 @@ export default (() => {
|
|||||||
<col className="w-96"/>
|
<col className="w-96"/>
|
||||||
{/* タグ */}
|
{/* タグ */}
|
||||||
<col className="w-[48rem]"/>
|
<col className="w-[48rem]"/>
|
||||||
|
{/* 親投稿 */}
|
||||||
|
<col className="w-[48rem]"/>
|
||||||
{/* オリジナルの投稿日時 */}
|
{/* オリジナルの投稿日時 */}
|
||||||
<col className="w-96"/>
|
<col className="w-96"/>
|
||||||
{/* 更新日時 */}
|
{/* 更新日時 */}
|
||||||
@@ -110,6 +112,7 @@ export default (() => {
|
|||||||
<th className="p-2 text-left">タイトル</th>
|
<th className="p-2 text-left">タイトル</th>
|
||||||
<th className="p-2 text-left">URL</th>
|
<th className="p-2 text-left">URL</th>
|
||||||
<th className="p-2 text-left">タグ</th>
|
<th className="p-2 text-left">タグ</th>
|
||||||
|
<th className="p-2 text-left">親投稿</th>
|
||||||
<th className="p-2 text-left">オリジナルの投稿日時</th>
|
<th className="p-2 text-left">オリジナルの投稿日時</th>
|
||||||
<th className="p-2 text-left">更新日時</th>
|
<th className="p-2 text-left">更新日時</th>
|
||||||
<th className="p-2"/>
|
<th className="p-2"/>
|
||||||
@@ -180,6 +183,28 @@ export default (() => {
|
|||||||
{tag.name}
|
{tag.name}
|
||||||
</span>))))}
|
</span>))))}
|
||||||
</td>
|
</td>
|
||||||
|
<td className="p-2">
|
||||||
|
{change.parentPosts.map ((pp, i) => (
|
||||||
|
pp.type === 'added'
|
||||||
|
? (
|
||||||
|
<ins
|
||||||
|
key={i}
|
||||||
|
className="mr-2 text-green-600 dark:text-green-400">
|
||||||
|
{pp.title}
|
||||||
|
</ins>)
|
||||||
|
: (
|
||||||
|
pp.type === 'removed'
|
||||||
|
? (
|
||||||
|
<del
|
||||||
|
key={i}
|
||||||
|
className="mr-2 text-red-600 dark:text-red-400">
|
||||||
|
{pp.title}
|
||||||
|
</del>)
|
||||||
|
: (
|
||||||
|
<span key={i} className="mr-2">
|
||||||
|
{pp.title}
|
||||||
|
</span>))))}
|
||||||
|
</td>
|
||||||
<td className="p-2">
|
<td className="p-2">
|
||||||
{change.versionNo === 1
|
{change.versionNo === 1
|
||||||
? originalCreatedAtString (change.originalCreatedFrom.current,
|
? originalCreatedAtString (change.originalCreatedFrom.current,
|
||||||
@@ -225,6 +250,8 @@ export default (() => {
|
|||||||
.map (t => t.name)
|
.map (t => t.name)
|
||||||
.filter (t => t.slice (0, 5) !== 'nico:')
|
.filter (t => t.slice (0, 5) !== 'nico:')
|
||||||
.join (' '),
|
.join (' '),
|
||||||
|
parent_post_ids:
|
||||||
|
change.parentPosts.map (p => p.id).join (' '),
|
||||||
original_created_from:
|
original_created_from:
|
||||||
change.originalCreatedFrom.current,
|
change.originalCreatedFrom.current,
|
||||||
original_created_before:
|
original_created_before:
|
||||||
|
|||||||
@@ -29,6 +29,7 @@ export default (({ user }: Props) => {
|
|||||||
|
|
||||||
const [originalCreatedBefore, setOriginalCreatedBefore] = useState<string | null> (null)
|
const [originalCreatedBefore, setOriginalCreatedBefore] = useState<string | null> (null)
|
||||||
const [originalCreatedFrom, setOriginalCreatedFrom] = useState<string | null> (null)
|
const [originalCreatedFrom, setOriginalCreatedFrom] = useState<string | null> (null)
|
||||||
|
const [parentPostIds, setParentPostIds] = useState ('')
|
||||||
const [tags, setTags] = useState ('')
|
const [tags, setTags] = useState ('')
|
||||||
const [thumbnailAutoFlg, setThumbnailAutoFlg] = useState (true)
|
const [thumbnailAutoFlg, setThumbnailAutoFlg] = useState (true)
|
||||||
const [thumbnailFile, setThumbnailFile] = useState<File | null> (null)
|
const [thumbnailFile, setThumbnailFile] = useState<File | null> (null)
|
||||||
@@ -46,6 +47,7 @@ export default (({ user }: Props) => {
|
|||||||
formData.append ('title', title)
|
formData.append ('title', title)
|
||||||
formData.append ('url', url)
|
formData.append ('url', url)
|
||||||
formData.append ('tags', tags)
|
formData.append ('tags', tags)
|
||||||
|
formData.append ('parent_post_ids', parentPostIds)
|
||||||
if (thumbnailFile)
|
if (thumbnailFile)
|
||||||
formData.append ('thumbnail', thumbnailFile)
|
formData.append ('thumbnail', thumbnailFile)
|
||||||
if (originalCreatedFrom)
|
if (originalCreatedFrom)
|
||||||
@@ -177,6 +179,16 @@ export default (({ user }: Props) => {
|
|||||||
className="mt-2 max-h-48 rounded border"/>)}
|
className="mt-2 max-h-48 rounded border"/>)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{/* 親投稿 */}
|
||||||
|
<div>
|
||||||
|
<Label>親投稿</Label>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
value={parentPostIds}
|
||||||
|
onChange={e => setParentPostIds (e.target.value)}
|
||||||
|
className="w-full border p-2 rounded"/>
|
||||||
|
</div>
|
||||||
|
|
||||||
{/* タグ */}
|
{/* タグ */}
|
||||||
<PostFormTagsArea tags={tags} setTags={setTags}/>
|
<PostFormTagsArea tags={tags} setTags={setTags}/>
|
||||||
|
|
||||||
|
|||||||
@@ -121,6 +121,9 @@ export type Post = {
|
|||||||
thumbnail: string | null
|
thumbnail: string | null
|
||||||
thumbnailBase: string | null
|
thumbnailBase: string | null
|
||||||
tags: Tag[]
|
tags: Tag[]
|
||||||
|
parentPosts?: Post[]
|
||||||
|
childPosts?: Post[]
|
||||||
|
siblingPosts?: Record<`${ number }`, Post[]>
|
||||||
viewed: boolean
|
viewed: boolean
|
||||||
related: Post[]
|
related: Post[]
|
||||||
originalCreatedFrom: string | null
|
originalCreatedFrom: string | null
|
||||||
@@ -144,7 +147,11 @@ export type PostVersion = {
|
|||||||
url: { current: string; prev: string | null }
|
url: { current: string; prev: string | null }
|
||||||
thumbnail: { current: string | null; prev: string | null }
|
thumbnail: { current: string | null; prev: string | null }
|
||||||
thumbnailBase: { current: string | null; prev: string | null }
|
thumbnailBase: { current: string | null; prev: string | null }
|
||||||
tags: { name: string; type: 'context' | 'added' | 'removed' }[]
|
tags: { name: string
|
||||||
|
type: 'context' | 'added' | 'removed' }[]
|
||||||
|
parentPosts: { id: number
|
||||||
|
title: string
|
||||||
|
type: 'context' | 'added' | 'removed' }[]
|
||||||
originalCreatedFrom: { current: string | null; prev: string | null }
|
originalCreatedFrom: { current: string | null; prev: string | null }
|
||||||
originalCreatedBefore: { current: string | null; prev: string | null }
|
originalCreatedBefore: { current: string | null; prev: string | null }
|
||||||
createdAt: string
|
createdAt: string
|
||||||
|
|||||||
Reference in New Issue
Block a user