diff --git a/backend/app/controllers/posts_controller.rb b/backend/app/controllers/posts_controller.rb index 96f520c..000577b 100644 --- a/backend/app/controllers/posts_controller.rb +++ b/backend/app/controllers/posts_controller.rb @@ -392,7 +392,7 @@ class PostsController < ApplicationController tag = tags_by_id[tag_id] 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]) if path.include?(tag_id) @@ -419,7 +419,7 @@ class PostsController < ApplicationController params[:parent_post_ids].to_s.split.map { |token| id = Integer(token, exception: false) - raise ArgumentError, "親投稿 Id. が不正です: #{ token }" if id.nil? || id <= 0 + raise ArgumentError, "親投稿 Id. が不正です: #{ token }" if !(id) || id <= 0 id }.uniq diff --git a/backend/app/models/tag.rb b/backend/app/models/tag.rb index 0015250..f65ce24 100644 --- a/backend/app/models/tag.rb +++ b/backend/app/models/tag.rb @@ -102,7 +102,7 @@ class Tag < ApplicationRecord tags = tag_names.map do |name| 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 = [] 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] end + name = TagName.canonicalise(name).first + find_or_create_by_tag_name!(name, category: (cat || :general)).tap do |tag| 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 diff --git a/backend/spec/factories/post_tag_sections.rb b/backend/spec/factories/post_tag_sections.rb new file mode 100644 index 0000000..2560395 --- /dev/null +++ b/backend/spec/factories/post_tag_sections.rb @@ -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 diff --git a/backend/spec/factories/post_tags.rb b/backend/spec/factories/post_tags.rb new file mode 100644 index 0000000..6956ca3 --- /dev/null +++ b/backend/spec/factories/post_tags.rb @@ -0,0 +1,6 @@ +FactoryBot.define do + factory :post_tag do + association :post + association :tag + end +end diff --git a/backend/spec/factories/posts.rb b/backend/spec/factories/posts.rb new file mode 100644 index 0000000..9f46b2e --- /dev/null +++ b/backend/spec/factories/posts.rb @@ -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 diff --git a/frontend/src/components/PostEditForm.tsx b/frontend/src/components/PostEditForm.tsx index 4655f01..2a2083e 100644 --- a/frontend/src/components/PostEditForm.tsx +++ b/frontend/src/components/PostEditForm.tsx @@ -12,21 +12,24 @@ import { msToTime } from '@/lib/utils' 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[] = [] - const walk = (tag: Tag) => { + const walk = (tag: TagWithSections) => { const { children, ...rest } = tag result.push (rest) - children?.forEach (walk) + children.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 = ({ post, onSave }) => { } useEffect (() => { - setTags(tagsToStr (post.tags)) + setTags (tagsToStr (post.tags)) }, [post]) return ( diff --git a/frontend/src/components/TagDetailSidebar.tsx b/frontend/src/components/TagDetailSidebar.tsx index be95412..24dae59 100644 --- a/frontend/src/components/TagDetailSidebar.tsx +++ b/frontend/src/components/TagDetailSidebar.tsx @@ -26,13 +26,13 @@ import { dateString, originalCreatedAtString } from '@/lib/utils' import type { DragEndEvent } from '@dnd-kit/core' 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 = ( - tag: Tag, + tag: TagWithSections, nestLevel: number, path: string, suppressClickRef: MutableRefObject, @@ -63,7 +63,7 @@ const renderTagTree = ( const isDescendant = ( - root: Tag, + root: TagWithSections, targetId: number, ): boolean => { if (!(root.children)) @@ -84,8 +84,8 @@ const isDescendant = ( const findTag = ( byCat: TagByCategory, id: number, -): Tag | undefined => { - const walk = (nodes: Tag[]): Tag | undefined => { +): TagWithSections | undefined => { + const walk = (nodes: TagWithSections[]): TagWithSections | undefined => { for (const t of nodes) { if (t.id === id) @@ -167,7 +167,7 @@ const TagDetailSidebar: FC = ({ post, sp }) => { } 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 }, [post]) @@ -378,4 +378,4 @@ const TagDetailSidebar: FC = ({ post, sp }) => { ) } -export default TagDetailSidebar \ No newline at end of file +export default TagDetailSidebar diff --git a/frontend/src/lib/utils.ts b/frontend/src/lib/utils.ts index 672ebd2..6493039 100644 --- a/frontend/src/lib/utils.ts +++ b/frontend/src/lib/utils.ts @@ -77,7 +77,7 @@ export const msToTime = (ms: number): string => { const totalS = Math.trunc (ms / 1_000) const s = String (totalS % 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 ? `${ h }:${ min.padStart (2, '0') }:${ s.padStart (2, '0') }` diff --git a/frontend/src/pages/tags/TagDetailPage.tsx b/frontend/src/pages/tags/TagDetailPage.tsx index fc10727..7d6eb57 100644 --- a/frontend/src/pages/tags/TagDetailPage.tsx +++ b/frontend/src/pages/tags/TagDetailPage.tsx @@ -157,4 +157,4 @@ const TagDetailPage: FC = () => { ) } -export default TagDetailPage \ No newline at end of file +export default TagDetailPage diff --git a/frontend/src/test/factories.ts b/frontend/src/test/factories.ts index 2092619..929f4f1 100644 --- a/frontend/src/test/factories.ts +++ b/frontend/src/test/factories.ts @@ -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 => ({ +export const buildTag = (overrides: Partial = {}): TagWithSections => ({ id: 1, name: 'テストタグ', category: 'general', @@ -13,6 +13,8 @@ export const buildTag = (overrides: Partial = {}): Tag => ({ materialId: null, hasDeerjikists: false, matchedAlias: null, + sections: [], + children: [], ...overrides, })