#64 おそらく完成
This commit is contained in:
@@ -52,9 +52,12 @@ class PostsController < ApplicationController
|
||||
|
||||
viewed = current_user&.viewed?(post) || false
|
||||
|
||||
render json: (post
|
||||
.as_json(include: { tags: { only: [:id, :name, :category, :post_count] } })
|
||||
.merge(related: post.related(limit: 20), viewed:))
|
||||
json = post.as_json
|
||||
json['tags'] = build_tag_tree_for(post.tags)
|
||||
json['related'] = post.related(limit: 20)
|
||||
json['viewed'] = viewed
|
||||
|
||||
render json:
|
||||
end
|
||||
|
||||
# POST /posts
|
||||
@@ -115,8 +118,9 @@ class PostsController < ApplicationController
|
||||
Tag.normalise_tags(tag_names, with_tagme: false)
|
||||
tags = Tag.expand_parent_tags(tags)
|
||||
if post.update(title:, tags:, original_created_from:, original_created_before:)
|
||||
render json: post.as_json(include: { tags: { only: [:id, :name, :category, :post_count] } }),
|
||||
status: :ok
|
||||
json = post.as_json
|
||||
json['tags'] = build_tag_tree_for(post.tags)
|
||||
render json:, status: :ok
|
||||
else
|
||||
render json: post.errors, status: :unprocessable_entity
|
||||
end
|
||||
@@ -145,4 +149,46 @@ class PostsController < ApplicationController
|
||||
end
|
||||
posts.distinct
|
||||
end
|
||||
|
||||
def build_tag_tree_for tags
|
||||
tags = tags.to_a
|
||||
tag_ids = tags.map(&:id)
|
||||
|
||||
implications = TagImplication.where(parent_tag_id: tag_ids, tag_id: tag_ids)
|
||||
|
||||
children_ids_by_parent = Hash.new { |h, k| h[k] = [] }
|
||||
implications.each do |imp|
|
||||
children_ids_by_parent[imp.parent_tag_id] << imp.tag_id
|
||||
end
|
||||
|
||||
child_ids = children_ids_by_parent.values.flatten.uniq
|
||||
|
||||
root_ids = tag_ids - child_ids
|
||||
|
||||
tags_by_id = tags.index_by(&:id)
|
||||
|
||||
memo = { }
|
||||
|
||||
build_node = -> tag_id, path do
|
||||
tag = tags_by_id[tag_id]
|
||||
return nil unless tag
|
||||
|
||||
if path.include?(tag_id)
|
||||
return tag.as_json(only: [:id, :name, :category, :post_count]).merge(children: [])
|
||||
end
|
||||
|
||||
if memo.key?(tag_id)
|
||||
return memo[tag_id]
|
||||
end
|
||||
|
||||
new_path = path + [tag_id]
|
||||
child_ids = children_ids_by_parent[tag_id] || []
|
||||
|
||||
children = child_ids.filter_map { |cid| build_node.(cid, new_path) }
|
||||
|
||||
memo[tag_id] = tag.as_json(only: [:id, :name, :category, :post_count]).merge(children:)
|
||||
end
|
||||
|
||||
root_ids.filter_map { |id| build_node.call(id, []) }
|
||||
end
|
||||
end
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import axios from 'axios'
|
||||
import toCamel from 'camelcase-keys'
|
||||
import { useState } from 'react'
|
||||
import { useEffect, useState } from 'react'
|
||||
|
||||
import PostFormTagsArea from '@/components/PostFormTagsArea'
|
||||
import PostOriginalCreatedTimeField from '@/components/PostOriginalCreatedTimeField'
|
||||
@@ -10,7 +10,23 @@ import { API_BASE_URL } from '@/config'
|
||||
|
||||
import type { FC } from 'react'
|
||||
|
||||
import type { Post } from '@/types'
|
||||
import type { Post, Tag } from '@/types'
|
||||
|
||||
|
||||
const tagsToStr = (tags: Tag[]): string => {
|
||||
const result: Tag[] = []
|
||||
|
||||
const walk = (tag: Tag) => {
|
||||
const { children, ...rest } = tag
|
||||
result.push (rest)
|
||||
children?.forEach (walk)
|
||||
}
|
||||
|
||||
tags.filter (t => t.category !== 'nico').forEach (walk)
|
||||
|
||||
return [...(new Set (result.map (t => t.name)))].join (' ')
|
||||
}
|
||||
|
||||
|
||||
type Props = { post: Post
|
||||
onSave: (newPost: Post) => void }
|
||||
@@ -22,10 +38,7 @@ export default (({ post, onSave }: Props) => {
|
||||
const [originalCreatedFrom, setOriginalCreatedFrom] =
|
||||
useState<string | null> (post.originalCreatedFrom)
|
||||
const [title, setTitle] = useState (post.title)
|
||||
const [tags, setTags] = useState<string> (post.tags
|
||||
.filter (t => t.category !== 'nico')
|
||||
.map (t => t.name)
|
||||
.join (' '))
|
||||
const [tags, setTags] = useState<string> ('')
|
||||
|
||||
const handleSubmit = async () => {
|
||||
const res = await axios.put (
|
||||
@@ -43,6 +56,10 @@ export default (({ post, onSave }: Props) => {
|
||||
originalCreatedBefore: data.originalCreatedBefore } as Post)
|
||||
}
|
||||
|
||||
useEffect (() => {
|
||||
setTags(tagsToStr (post.tags))
|
||||
}, [post])
|
||||
|
||||
return (
|
||||
<div className="max-w-xl pt-2 space-y-4">
|
||||
{/* タイトル */}
|
||||
|
||||
@@ -7,12 +7,30 @@ import SubsectionTitle from '@/components/common/SubsectionTitle'
|
||||
import SidebarComponent from '@/components/layout/SidebarComponent'
|
||||
import { CATEGORIES } from '@/consts'
|
||||
|
||||
import type { FC } from 'react'
|
||||
import type { FC, ReactNode } from 'react'
|
||||
|
||||
import type { Category, Post, Tag } from '@/types'
|
||||
|
||||
type TagByCategory = { [key in Category]: Tag[] }
|
||||
|
||||
|
||||
const renderTagTree = (
|
||||
tag: Tag,
|
||||
nestLevel: number,
|
||||
path: string,
|
||||
): ReactNode[] => {
|
||||
const key = `${ path }-${ tag.id }`
|
||||
|
||||
const self = (
|
||||
<li key={key} className="mb-1">
|
||||
<TagLink tag={tag} nestLevel={nestLevel}/>
|
||||
</li>)
|
||||
|
||||
return [self,
|
||||
...(tag.children?.flatMap (child => renderTagTree (child, nestLevel + 1, key)) ?? [])]
|
||||
}
|
||||
|
||||
|
||||
type Props = { post: Post | null }
|
||||
|
||||
|
||||
@@ -54,10 +72,7 @@ export default (({ post }: Props) => {
|
||||
<div className="my-3" key={cat}>
|
||||
<SubsectionTitle>{categoryNames[cat]}</SubsectionTitle>
|
||||
<ul>
|
||||
{tags[cat].map (tag => (
|
||||
<li key={tag.id} className="mb-1">
|
||||
<TagLink tag={tag}/>
|
||||
</li>))}
|
||||
{tags[cat].map (tag => renderTagTree (tag, 0, `cat-${ cat }`))}
|
||||
</ul>
|
||||
</div>))}
|
||||
{post && (
|
||||
|
||||
@@ -8,6 +8,7 @@ import type { ComponentProps, FC, HTMLAttributes } from 'react'
|
||||
import type { Tag } from '@/types'
|
||||
|
||||
type CommonProps = { tag: Tag
|
||||
nestLevel?: number
|
||||
withWiki?: boolean
|
||||
withCount?: boolean }
|
||||
|
||||
@@ -21,6 +22,7 @@ type Props = PropsWithLink | PropsWithoutLink
|
||||
|
||||
|
||||
export default (({ tag,
|
||||
nestLevel = 0,
|
||||
linkFlg = true,
|
||||
withWiki = true,
|
||||
withCount = true,
|
||||
@@ -42,6 +44,12 @@ export default (({ tag,
|
||||
?
|
||||
</Link>
|
||||
</span>)}
|
||||
{nestLevel > 0 && (
|
||||
<span
|
||||
className="ml-1 mr-1"
|
||||
style={{ paddingLeft: `${ (nestLevel - 1) }rem` }}>
|
||||
↳
|
||||
</span>)}
|
||||
{linkFlg
|
||||
? (
|
||||
<Link to={`/posts?${ (new URLSearchParams ({ tags: tag.name })).toString () }`}
|
||||
|
||||
+10
-10
@@ -1,7 +1,7 @@
|
||||
import React from 'react'
|
||||
|
||||
import { CATEGORIES, USER_ROLES, ViewFlagBehavior } from '@/consts'
|
||||
|
||||
import type { ReactNode } from 'react'
|
||||
|
||||
export type Category = typeof CATEGORIES[number]
|
||||
|
||||
export type Menu = MenuItem[]
|
||||
@@ -29,19 +29,19 @@ export type Post = {
|
||||
originalCreatedFrom: string | null
|
||||
originalCreatedBefore: string | null }
|
||||
|
||||
export type SubMenuItem = {
|
||||
component: React.ReactNode
|
||||
visible: boolean
|
||||
} | {
|
||||
name: string
|
||||
to: string
|
||||
visible?: boolean }
|
||||
export type SubMenuItem =
|
||||
| { component: ReactNode
|
||||
visible: boolean }
|
||||
| { name: string
|
||||
to: string
|
||||
visible?: boolean }
|
||||
|
||||
export type Tag = {
|
||||
id: number
|
||||
name: string
|
||||
category: Category
|
||||
postCount: number }
|
||||
postCount: number
|
||||
children?: Tag[] }
|
||||
|
||||
export type User = {
|
||||
id: number
|
||||
|
||||
Reference in New Issue
Block a user