diff --git a/backend/app/controllers/tag_children_controller.rb b/backend/app/controllers/tag_children_controller.rb deleted file mode 100644 index 4b352b4..0000000 --- a/backend/app/controllers/tag_children_controller.rb +++ /dev/null @@ -1,27 +0,0 @@ -class TagChildrenController < ApplicationController - def create - return head :unauthorized unless current_user - return head :forbidden unless current_user.admin? - - parent_id = params[:parent_id] - child_id = params[:child_id] - return head :bad_request if parent_id.blank? || child_id.blank? - - Tag.find(parent_id).children << Tag.find(child_id) rescue nil - - head :no_content - end - - def destroy - return head :unauthorized unless current_user - return head :forbidden unless current_user.admin? - - parent_id = params[:parent_id] - child_id = params[:child_id] - return head :bad_request if parent_id.blank? || child_id.blank? - - Tag.find(parent_id).children.delete(Tag.find(child_id)) rescue nil - - head :no_content - end -end diff --git a/backend/app/controllers/tags_controller.rb b/backend/app/controllers/tags_controller.rb index c9203ee..6597a43 100644 --- a/backend/app/controllers/tags_controller.rb +++ b/backend/app/controllers/tags_controller.rb @@ -39,17 +39,6 @@ class TagsController < ApplicationController end def update - return head :unauthorized unless current_user - return head :forbidden unless current_user.member? - - tag = Tag.find(params[:id]) - - attrs = { name: params[:name].presence, - category: params[:category].presence }.compact - - tag.update!(attrs) if attrs.present? - - render json: tag end def destroy diff --git a/backend/config/routes.rb b/backend/config/routes.rb index b8ac379..206d1a6 100644 --- a/backend/config/routes.rb +++ b/backend/config/routes.rb @@ -1,11 +1,6 @@ Rails.application.routes.draw do resources :nico_tags, path: 'tags/nico', only: [:index, :update] - scope 'tags/:parent_id/children', controller: :tag_children do - post ':child_id', action: :create - delete ':child_id', action: :destroy - end - resources :tags do collection do get :autocomplete diff --git a/frontend/package-lock.json b/frontend/package-lock.json index 46c04cd..43fbc44 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -9,8 +9,6 @@ "version": "0.0.0", "license": "ISC", "dependencies": { - "@dnd-kit/core": "^6.3.1", - "@dnd-kit/utilities": "^3.2.2", "@fontsource-variable/noto-sans-jp": "^5.2.9", "@radix-ui/react-dialog": "^1.1.14", "@radix-ui/react-switch": "^1.2.5", @@ -372,45 +370,6 @@ "node": ">=6.9.0" } }, - "node_modules/@dnd-kit/accessibility": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/@dnd-kit/accessibility/-/accessibility-3.1.1.tgz", - "integrity": "sha512-2P+YgaXF+gRsIihwwY1gCsQSYnu9Zyj2py8kY5fFvUM1qm2WA2u639R6YNVfU4GWr+ZM5mqEsfHZZLoRONbemw==", - "license": "MIT", - "dependencies": { - "tslib": "^2.0.0" - }, - "peerDependencies": { - "react": ">=16.8.0" - } - }, - "node_modules/@dnd-kit/core": { - "version": "6.3.1", - "resolved": "https://registry.npmjs.org/@dnd-kit/core/-/core-6.3.1.tgz", - "integrity": "sha512-xkGBRQQab4RLwgXxoqETICr6S5JlogafbhNsidmrkVv2YRs5MLwpjoF2qpiGjQt8S9AoxtIV603s0GIUpY5eYQ==", - "license": "MIT", - "dependencies": { - "@dnd-kit/accessibility": "^3.1.1", - "@dnd-kit/utilities": "^3.2.2", - "tslib": "^2.0.0" - }, - "peerDependencies": { - "react": ">=16.8.0", - "react-dom": ">=16.8.0" - } - }, - "node_modules/@dnd-kit/utilities": { - "version": "3.2.2", - "resolved": "https://registry.npmjs.org/@dnd-kit/utilities/-/utilities-3.2.2.tgz", - "integrity": "sha512-+MKAJEOfaBe5SmV6t34p80MMKhjvUz0vRrvVJbPT0WElzaOJ/1xs+D+KDv+tD/NE5ujfrChEcshd4fLn0wpiqg==", - "license": "MIT", - "dependencies": { - "tslib": "^2.0.0" - }, - "peerDependencies": { - "react": ">=16.8.0" - } - }, "node_modules/@esbuild/aix-ppc64": { "version": "0.25.4", "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.4.tgz", diff --git a/frontend/package.json b/frontend/package.json index 8de6bae..cbe44ff 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -11,8 +11,6 @@ "preview": "vite preview" }, "dependencies": { - "@dnd-kit/core": "^6.3.1", - "@dnd-kit/utilities": "^3.2.2", "@fontsource-variable/noto-sans-jp": "^5.2.9", "@radix-ui/react-dialog": "^1.1.14", "@radix-ui/react-switch": "^1.2.5", diff --git a/frontend/src/components/DraggableDroppableTagRow.tsx b/frontend/src/components/DraggableDroppableTagRow.tsx deleted file mode 100644 index c12ab09..0000000 --- a/frontend/src/components/DraggableDroppableTagRow.tsx +++ /dev/null @@ -1,93 +0,0 @@ -import { useDraggable, useDroppable } from '@dnd-kit/core' -import { CSS } from '@dnd-kit/utilities' -import { useRef } from 'react' - -import TagLink from '@/components/TagLink' -import { cn } from '@/lib/utils' - -import type { CSSProperties, FC, MutableRefObject } from 'react' - -import type { Tag } from '@/types' - -type Props = { - tag: Tag - nestLevel: number - pathKey: string - parentTagId?: number - suppressClickRef: MutableRefObject } - - -export default (({ tag, nestLevel, pathKey, parentTagId, suppressClickRef }: Props) => { - const dndId = `tag-node:${ pathKey }` - - const downPosRef = useRef<{ x: number; y: number } | null> (null) - const armedRef = useRef (false) - - const armEatNextClick = () => { - if (armedRef.current) - return - - armedRef.current = true - suppressClickRef.current = true - - const handler = (ev: MouseEvent) => { - ev.preventDefault () - ev.stopPropagation () - armedRef.current = false - suppressClickRef.current = false - } - - addEventListener ('click', handler, { capture: true, once: true }) - } - - const { attributes, - listeners, - setNodeRef: setDragRef, - transform, - isDragging: dragging } = useDraggable ({ id: dndId, - data: { kind: 'tag', - tagId: tag.id, - parentTagId } }) - - const { setNodeRef: setDropRef, isOver: over } = useDroppable ({ - id: dndId, - data: { kind: 'tag', tagId: tag.id } }) - - const style: CSSProperties = { transform: CSS.Translate.toString (transform), - opacity: dragging ? .5 : 1 } - - return ( -
{ - downPosRef.current = { x: e.clientX, y: e.clientY } - }} - onPointerMoveCapture={e => { - const p = downPosRef.current - if (!(p)) - return - const dx = e.clientX - p.x - const dy = e.clientY - p.y - if (dx * dx + dy * dy >= 9) - armEatNextClick () - }} - onPointerUpCapture={() => { - downPosRef.current = null - }} - onClickCapture={e => { - if (suppressClickRef.current) - { - e.preventDefault () - e.stopPropagation () - } - }} - ref={node => { - setDragRef (node) - setDropRef (node) - }} - style={style} - className={cn ('rounded select-none', over && 'ring-2 ring-offset-2')} - {...attributes} - {...listeners}> - -
) -}) satisfies FC diff --git a/frontend/src/components/TagDetailSidebar.tsx b/frontend/src/components/TagDetailSidebar.tsx index eb3f835..a897fa8 100644 --- a/frontend/src/components/TagDetailSidebar.tsx +++ b/frontend/src/components/TagDetailSidebar.tsx @@ -1,26 +1,15 @@ -import { DndContext, - MouseSensor, - TouchSensor, - useDroppable, - useSensor, - useSensors } from '@dnd-kit/core' -import axios from 'axios' -import toCamel from 'camelcase-keys' import { AnimatePresence, motion } from 'framer-motion' -import { useEffect, useRef, useState } from 'react' +import { useEffect, useState } from 'react' import { Link } from 'react-router-dom' -import DraggableDroppableTagRow from '@/components/DraggableDroppableTagRow' +import TagLink from '@/components/TagLink' import TagSearch from '@/components/TagSearch' import SectionTitle from '@/components/common/SectionTitle' import SubsectionTitle from '@/components/common/SubsectionTitle' import SidebarComponent from '@/components/layout/SidebarComponent' -import { toast } from '@/components/ui/use-toast' -import { API_BASE_URL } from '@/config' import { CATEGORIES } from '@/consts' -import type { DragEndEvent } from '@dnd-kit/core' -import type { FC, MutableRefObject, ReactNode } from 'react' +import type { FC, ReactNode } from 'react' import type { Category, Post, Tag } from '@/types' @@ -28,11 +17,9 @@ type TagByCategory = { [key in Category]: Tag[] } const renderTagTree = ( - tag: Tag, - nestLevel: number, - path: string, - suppressClickRef: MutableRefObject, - parentTagId?: number, + tag: Tag, + nestLevel: number, + path: string, ): ReactNode[] => { const key = `${ path }-${ tag.id }` @@ -42,109 +29,14 @@ const renderTagTree = ( layout transition={{ duration: .2, ease: 'easeOut' }} className="mb-1"> - + ) - return [ - self, - ...((tag.children - ?.sort ((a, b) => a.name < b.name ? -1 : 1) - .flatMap (child => renderTagTree (child, nestLevel + 1, key, suppressClickRef, tag.id))) - ?? [])] -} - - -const isDescendant = ( - root: Tag, - targetId: number, -): boolean => { - if (!(root.children)) - return false - - for (const c of root.children) - { - if (c.id === targetId) - return true - if (isDescendant (c, targetId)) - return true - } - - return false -} - - -const findTag = ( - byCat: TagByCategory, - id: number, -): Tag | undefined => { - const walk = (nodes: Tag[]): Tag | undefined => { - for (const t of nodes) - { - if (t.id === id) - return t - - const found = t.children ? walk (t.children) : undefined - if (found) - return found - } - - return undefined - } - - for (const cat of Object.keys (byCat) as (keyof typeof byCat)[]) - { - const found = walk (byCat[cat] ?? []) - if (found) - return found - } - - return undefined -} - - -const buildTagByCategory = (post: Post): TagByCategory => { - const tagsTmp = { } as TagByCategory - - for (const tag of post.tags) - { - if (!(tag.category in tagsTmp)) - tagsTmp[tag.category] = [] - - tagsTmp[tag.category].push (tag) - } - - for (const cat of Object.keys (tagsTmp) as (keyof typeof tagsTmp)[]) - tagsTmp[cat].sort ((a, b) => a.name < b.name ? -1 : 1) - - return tagsTmp -} - - -const changeCategory = async ( - tagId: number, - category: Category, -): Promise => { - await axios.patch ( - `${ API_BASE_URL }/tags/${ tagId }`, - { category }, - { headers: { 'X-Transfer-Code': localStorage.getItem ('user_code') ?? '' } }) -} - - -const DropSlot = ({ cat }: { cat: Category }) => { - const { setNodeRef, isOver: over } = useDroppable ({ - id: `slot:${ cat }`, - data: { kind: 'slot', cat } }) - - return ( -
  • - {over &&
    } -
  • ) + return [self, + ...((tag.children + ?.sort ((a, b) => a.name < b.name ? -1 : 1) + .flatMap (child => renderTagTree (child, nestLevel + 1, key))) + ?? [])] } @@ -152,125 +44,8 @@ type Props = { post: Post | null } export default (({ post }: Props) => { - const [dragging, setDragging] = useState (false) - const [saving, setSaving] = useState (false) const [tags, setTags] = useState ({ } as TagByCategory) - const suppressClickRef = useRef (false) - - const sensors = useSensors ( - useSensor (MouseSensor, { activationConstraint: { distance: 6 } }), - useSensor (TouchSensor, { activationConstraint: { delay: 250, tolerance: 8 } })) - - const reloadTags = async (): Promise => { - if (!(post)) - return - - const res = await axios.get ( - `${ API_BASE_URL }/posts/${ post.id }`, - { headers: { 'X-Transfer-Code': localStorage.getItem ('user_code') ?? '' } }) - const data = toCamel (res.data as any, { deep: true }) as Post - - setTags (buildTagByCategory (data)) - } - - const onDragEnd = async (e: DragEndEvent) => { - if (saving) - return - - const activeKind = e.active.data.current?.kind - if (activeKind !== 'tag') - return - - const childId: number | undefined = e.active.data.current?.tagId - const fromParentId: number | undefined = e.active.data.current?.parentTagId - - const overKind = e.over?.data.current?.kind - - if (childId == null || !(overKind)) - return - - const child = findTag (tags, childId) - - try - { - setSaving (true) - - switch (overKind) - { - case 'tag': - { - const parentId: number | undefined = e.over?.data.current?.tagId - if (parentId == null || childId === parentId) - return - - const parent = findTag (tags, parentId) - - if (!(child) - || !(parent) - || isDescendant (child, parentId)) - return - - if (fromParentId != null) - { - await axios.delete ( - `${ API_BASE_URL }/tags/${ fromParentId }/children/${ childId }`, - { headers: { 'X-Transfer-Code': localStorage.getItem ('user_code') ?? '' } }) - } - - await axios.post ( - `${ API_BASE_URL }/tags/${ parentId }/children/${ childId }`, - { }, - { headers: { 'X-Transfer-Code': localStorage.getItem ('user_code') ?? '' } }) - - await reloadTags () - toast ({ - title: '上位タグ対応追加', - description: `《${ child?.name }》を《${ parent?.name }》の子タグに設定しました.` }) - - break - } - - case 'slot': - { - const cat: Category | undefined = e.over?.data.current?.cat - if (!(cat) || !(child)) - return - - if (child.category !== cat) - await changeCategory (childId, cat) - - if (fromParentId != null) - { - await axios.delete ( - `${ API_BASE_URL }/tags/${ fromParentId }/children/${ childId }`, - { headers: { 'X-Transfer-Code': localStorage.getItem ('user_code') ?? '' } }) - } - - const fromParent = fromParentId == null ? null : findTag (tags, fromParentId) - - await reloadTags () - toast ({ - title: '上位タグ対応解除', - description: ( - fromParent - ? `《${ child.name }》を《${ fromParent.name }》の子タグから外しました.` - : `《${ child.name }》のカテゴリを変更しました.`) }) - - break - } - } - } - catch - { - toast ({ title: '上位タグ対応失敗', description: '独裁者である必要があります.' }) - } - finally - { - setSaving (false) - } - } - const categoryNames: Record = { deerjikist: 'ニジラー', meme: '原作・ネタ元・ミーム等', @@ -302,89 +77,63 @@ export default (({ post }: Props) => { return ( - { - setDragging (true) - suppressClickRef.current = true - document.body.style.userSelect = 'none' - getSelection?.()?.removeAllRanges?.() - addEventListener ('click', e => { - e.preventDefault () - e.stopPropagation () - suppressClickRef.current = false - }, { capture: true, once: true }) - }} - onDragCancel={() => { - setDragging (false) - document.body.style.userSelect = '' - suppressClickRef.current = false - }} - onDragEnd={e => { - setDragging (false) - onDragEnd (e) - document.body.style.userSelect = '' - }}> - - {CATEGORIES.map ((cat: Category) => ((tags[cat] ?? []).length > 0 || dragging) && ( - - {categoryNames[cat]} - - - - {(tags[cat] ?? []).flatMap (tag => ( - renderTagTree (tag, 0, `cat-${ cat }`, suppressClickRef, undefined)))} - - - - ))} - {post && ( -
    - 情報 -
      -
    • Id.: {post.id}
    • - {/* TODO: uploadedUser の取得を対応したらコメント外す */} - {/* -
    • - <>耕作者: - {post.uploadedUser - ? ( - - {post.uploadedUser.name || '名もなきニジラー'} - ) - : 'bot操作'} -
    • - */} -
    • 耕作日時: {(new Date (post.createdAt)).toLocaleString ()}
    • -
    • - <>リンク: - - {post.url} - -
    • -
    • - {/* TODO: 表示形式きしょすぎるので何とかする */} - <>オリジナルの投稿日時: - {!(post.originalCreatedFrom) && !(post.originalCreatedBefore) - ? '不明' - : ( - <> - {post.originalCreatedFrom - && `${ (new Date (post.originalCreatedFrom)).toLocaleString () } 以降 `} - {post.originalCreatedBefore - && `${ (new Date (post.originalCreatedBefore)).toLocaleString () } より前`} - )} -
    • -
    • - 履歴 -
    • -
    -
    )} -
    -
    + + {CATEGORIES.map ((cat: Category) => cat in tags && ( + + {categoryNames[cat]} + + + + {tags[cat].map (tag => renderTagTree (tag, 0, `cat-${ cat }`))} + + + ))} + {post && ( +
    + 情報 +
      +
    • Id.: {post.id}
    • + {/* TODO: uploadedUser の取得を対応したらコメント外す */} + {/* +
    • + <>耕作者: + {post.uploadedUser + ? ( + + {post.uploadedUser.name || '名もなきニジラー'} + ) + : 'bot操作'} +
    • + */} +
    • 耕作日時: {(new Date (post.createdAt)).toLocaleString ()}
    • +
    • + <>リンク: + + {post.url} + +
    • +
    • + {/* TODO: 表示形式きしょすぎるので何とかする */} + <>オリジナルの投稿日時: + {!(post.originalCreatedFrom) && !(post.originalCreatedBefore) + ? '不明' + : ( + <> + {post.originalCreatedFrom + && `${ (new Date (post.originalCreatedFrom)).toLocaleString () } 以降 `} + {post.originalCreatedBefore + && `${ (new Date (post.originalCreatedBefore)).toLocaleString () } より前`} + )} +
    • +
    • + 履歴 +
    • +
    +
    )} +
    ) }) satisfies FC