| @@ -392,7 +392,7 @@ class PostsController < ApplicationController | |||||
| tag = tags_by_id[tag_id] | tag = tags_by_id[tag_id] | ||||
| return nil unless tag | return nil unless tag | ||||
| sections = PostTagSection.where(post_id: post.id, tag_id: tag.id) | |||||
| sections = PostTagSection.where(post_id: post.id, tag_id:) | |||||
| .as_json(only: [:begin_ms, :end_ms]) | .as_json(only: [:begin_ms, :end_ms]) | ||||
| if path.include?(tag_id) | if path.include?(tag_id) | ||||
| @@ -419,7 +419,7 @@ class PostsController < ApplicationController | |||||
| params[:parent_post_ids].to_s.split.map { |token| | params[:parent_post_ids].to_s.split.map { |token| | ||||
| id = Integer(token, exception: false) | id = Integer(token, exception: false) | ||||
| raise ArgumentError, "親投稿 Id. が不正です: #{ token }" if id.nil? || id <= 0 | |||||
| raise ArgumentError, "親投稿 Id. が不正です: #{ token }" if !(id) || id <= 0 | |||||
| id | id | ||||
| }.uniq | }.uniq | ||||
| @@ -102,7 +102,7 @@ class Tag < ApplicationRecord | |||||
| tags = tag_names.map do |name| | tags = tag_names.map do |name| | ||||
| pf, cat = CATEGORY_PREFIXES.find { |p, _| name.downcase.start_with?(p) } || ['', nil] | pf, cat = CATEGORY_PREFIXES.find { |p, _| name.downcase.start_with?(p) } || ['', nil] | ||||
| name = TagName.canonicalise(name.sub(/\A#{ pf }/i, '')).first | |||||
| name = name.sub(/\A#{ pf }/i, '') | |||||
| sections_by_tag = [] | sections_by_tag = [] | ||||
| while n = name.sub!(/^(\S*?)\[([0-9:.]*?)-([0-9:.]*?)\](\S*?)$/, '\1\4 \2 \3') | while n = name.sub!(/^(\S*?)\[([0-9:.]*?)-([0-9:.]*?)\](\S*?)$/, '\1\4 \2 \3') | ||||
| @@ -116,9 +116,14 @@ class Tag < ApplicationRecord | |||||
| sections_by_tag << [begin_ms, end_ms] | sections_by_tag << [begin_ms, end_ms] | ||||
| end | end | ||||
| name = TagName.canonicalise(name).first | |||||
| find_or_create_by_tag_name!(name, category: (cat || :general)).tap do |tag| | find_or_create_by_tag_name!(name, category: (cat || :general)).tap do |tag| | ||||
| tag.update!(category: cat) if cat && tag.category != cat | tag.update!(category: cat) if cat && tag.category != cat | ||||
| sections[tag.id] = sections_by_tag if sections_by_tag.present? | |||||
| next if sections_by_tag.blank? | |||||
| sections[tag.id] ||= [] | |||||
| sections[tag.id].concat(sections_by_tag) | |||||
| end | end | ||||
| end | end | ||||
| @@ -0,0 +1,8 @@ | |||||
| FactoryBot.define do | |||||
| factory :post_tag_section do | |||||
| association :post | |||||
| association :tag | |||||
| begin_ms { 1_000 } | |||||
| end_ms { 2_000 } | |||||
| end | |||||
| end | |||||
| @@ -0,0 +1,6 @@ | |||||
| FactoryBot.define do | |||||
| factory :post_tag do | |||||
| association :post | |||||
| association :tag | |||||
| end | |||||
| end | |||||
| @@ -0,0 +1,8 @@ | |||||
| FactoryBot.define do | |||||
| factory :post do | |||||
| sequence(:url) { |n| "https://example.com/factory-post-#{ n }" } | |||||
| title { 'factory post' } | |||||
| thumbnail_base { nil } | |||||
| uploaded_user { nil } | |||||
| end | |||||
| end | |||||
| @@ -12,21 +12,24 @@ import { msToTime } from '@/lib/utils' | |||||
| import type { FC, FormEvent } from 'react' | import type { FC, FormEvent } from 'react' | ||||
| import type { Post, Tag } from '@/types' | |||||
| import type { Post, TagWithSections } from '@/types' | |||||
| const tagsToStr = (tags: Tag[]): string => { | |||||
| const result: Tag[] = [] | |||||
| const tagsToStr = (tags: TagWithSections[]): string => { | |||||
| const result: Omit<TagWithSections, 'children'>[] = [] | |||||
| const walk = (tag: Tag) => { | |||||
| const walk = (tag: TagWithSections) => { | |||||
| const { children, ...rest } = tag | const { children, ...rest } = tag | ||||
| result.push (rest) | result.push (rest) | ||||
| children?.forEach (walk) | |||||
| children.forEach (walk) | |||||
| } | } | ||||
| tags.filter (t => t.category !== 'nico').forEach (walk) | tags.filter (t => t.category !== 'nico').forEach (walk) | ||||
| return [...(new Set (result.map (t => `${ t.name }${ t.sections.map (s => `[${ msToTime (s.beginMs) }-${ msToTime (s.endMs) }]`).join ('') }`)))].join (' ') | |||||
| return [...(new Set (result.map (t => | |||||
| `${ t.name }${ t.sections | |||||
| .map (s => `[${ msToTime (s.beginMs) }-${ msToTime (s.endMs) }]`) | |||||
| .join ('') }`)))].join (' ') | |||||
| } | } | ||||
| @@ -118,7 +121,7 @@ const PostEditForm: FC<Props> = ({ post, onSave }) => { | |||||
| } | } | ||||
| useEffect (() => { | useEffect (() => { | ||||
| setTags(tagsToStr (post.tags)) | |||||
| setTags (tagsToStr (post.tags)) | |||||
| }, [post]) | }, [post]) | ||||
| return ( | return ( | ||||
| @@ -26,13 +26,13 @@ import { dateString, originalCreatedAtString } from '@/lib/utils' | |||||
| import type { DragEndEvent } from '@dnd-kit/core' | import type { DragEndEvent } from '@dnd-kit/core' | ||||
| import type { FC, MutableRefObject, ReactNode } from 'react' | import type { FC, MutableRefObject, ReactNode } from 'react' | ||||
| import type { Category, Post, Tag } from '@/types' | |||||
| import type { Category, Post, TagWithSections } from '@/types' | |||||
| type TagByCategory = { [key in Category]: Tag[] } | |||||
| type TagByCategory = { [key in Category]: TagWithSections[] } | |||||
| const renderTagTree = ( | const renderTagTree = ( | ||||
| tag: Tag, | |||||
| tag: TagWithSections, | |||||
| nestLevel: number, | nestLevel: number, | ||||
| path: string, | path: string, | ||||
| suppressClickRef: MutableRefObject<boolean>, | suppressClickRef: MutableRefObject<boolean>, | ||||
| @@ -63,7 +63,7 @@ const renderTagTree = ( | |||||
| const isDescendant = ( | const isDescendant = ( | ||||
| root: Tag, | |||||
| root: TagWithSections, | |||||
| targetId: number, | targetId: number, | ||||
| ): boolean => { | ): boolean => { | ||||
| if (!(root.children)) | if (!(root.children)) | ||||
| @@ -84,8 +84,8 @@ const isDescendant = ( | |||||
| const findTag = ( | const findTag = ( | ||||
| byCat: TagByCategory, | byCat: TagByCategory, | ||||
| id: number, | id: number, | ||||
| ): Tag | undefined => { | |||||
| const walk = (nodes: Tag[]): Tag | undefined => { | |||||
| ): TagWithSections | undefined => { | |||||
| const walk = (nodes: TagWithSections[]): TagWithSections | undefined => { | |||||
| for (const t of nodes) | for (const t of nodes) | ||||
| { | { | ||||
| if (t.id === id) | if (t.id === id) | ||||
| @@ -167,7 +167,7 @@ const TagDetailSidebar: FC<Props> = ({ post, sp }) => { | |||||
| } | } | ||||
| for (const cat of Object.keys (tagsTmp) as (keyof typeof tagsTmp)[]) | for (const cat of Object.keys (tagsTmp) as (keyof typeof tagsTmp)[]) | ||||
| tagsTmp[cat].sort ((tagA: Tag, tagB: Tag) => tagA.name < tagB.name ? -1 : 1) | |||||
| tagsTmp[cat].sort ((tagA: TagWithSections, tagB: TagWithSections) => tagA.name < tagB.name ? -1 : 1) | |||||
| return tagsTmp | return tagsTmp | ||||
| }, [post]) | }, [post]) | ||||
| @@ -378,4 +378,4 @@ const TagDetailSidebar: FC<Props> = ({ post, sp }) => { | |||||
| </SidebarComponent>) | </SidebarComponent>) | ||||
| } | } | ||||
| export default TagDetailSidebar | |||||
| export default TagDetailSidebar | |||||
| @@ -77,7 +77,7 @@ export const msToTime = (ms: number): string => { | |||||
| const totalS = Math.trunc (ms / 1_000) | const totalS = Math.trunc (ms / 1_000) | ||||
| const s = String (totalS % 60) | const s = String (totalS % 60) | ||||
| const min = String (Math.trunc (totalS / 60) % 60) | const min = String (Math.trunc (totalS / 60) % 60) | ||||
| const h = String (Math.trunc (totalS / 3_600)) | |||||
| const h = Math.trunc (totalS / 3_600) | |||||
| return (h > 0 | return (h > 0 | ||||
| ? `${ h }:${ min.padStart (2, '0') }:${ s.padStart (2, '0') }` | ? `${ h }:${ min.padStart (2, '0') }:${ s.padStart (2, '0') }` | ||||
| @@ -157,4 +157,4 @@ const TagDetailPage: FC = () => { | |||||
| </MainArea>) | </MainArea>) | ||||
| } | } | ||||
| export default TagDetailPage | |||||
| export default TagDetailPage | |||||
| @@ -1,6 +1,6 @@ | |||||
| import type { Material, Post, Tag, User, WikiPage } from '@/types' | |||||
| import type { Material, Post, TagWithSections, User, WikiPage } from '@/types' | |||||
| export const buildTag = (overrides: Partial<Tag> = {}): Tag => ({ | |||||
| export const buildTag = (overrides: Partial<TagWithSections> = {}): TagWithSections => ({ | |||||
| id: 1, | id: 1, | ||||
| name: 'テストタグ', | name: 'テストタグ', | ||||
| category: 'general', | category: 'general', | ||||
| @@ -13,6 +13,8 @@ export const buildTag = (overrides: Partial<Tag> = {}): Tag => ({ | |||||
| materialId: null, | materialId: null, | ||||
| hasDeerjikists: false, | hasDeerjikists: false, | ||||
| matchedAlias: null, | matchedAlias: null, | ||||
| sections: [], | |||||
| children: [], | |||||
| ...overrides, | ...overrides, | ||||
| }) | }) | ||||