| @@ -12,6 +12,7 @@ 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' | |||
| @@ -55,6 +56,31 @@ const renderTagTree = ( | |||
| } | |||
| const removeEverywhere = ( | |||
| list: Tag[], | |||
| tagId: number, | |||
| ): { next: Tag[]; picked?: Tag } => { | |||
| let picked: Tag | undefined | |||
| const walk = (nodes: Tag[]): Tag[] => ( | |||
| nodes | |||
| .map (t => { | |||
| const children = t.children ? walk (t.children) : undefined | |||
| return children ? { ...t, children } : t | |||
| }) | |||
| .filter (t => { | |||
| if (t.id === tagId) | |||
| { | |||
| picked = picked ?? t | |||
| return false | |||
| } | |||
| return true | |||
| })) | |||
| return { next: walk (list), picked } | |||
| } | |||
| const addAsChild = ( | |||
| list: Tag[], | |||
| parentId: number, | |||
| @@ -76,6 +102,34 @@ const addAsChild = ( | |||
| } | |||
| const attachChildOptimistic = ( | |||
| prev: TagByCategory, | |||
| parentId: number, | |||
| childId: number, | |||
| ): TagByCategory => { | |||
| const next: TagByCategory = { ...prev } | |||
| let picked: Tag | undefined | |||
| for (const cat of Object.keys (next) as (keyof typeof next)[]) | |||
| { | |||
| const r = removeEverywhere (next[cat], childId) | |||
| next[cat] = r.next | |||
| picked = picked ?? r.picked | |||
| } | |||
| if (!(picked)) | |||
| return prev | |||
| for (const cat of Object.keys (next) as (keyof typeof next)[]) | |||
| { | |||
| next[cat] = | |||
| addAsChild (next[cat], parentId, picked) | |||
| .sort ((a: Tag, b: Tag) => a.name < b.name ? -1 : 1) | |||
| } | |||
| return next | |||
| } | |||
| const isDescendant = ( | |||
| root: Tag, | |||
| targetId: number, | |||
| @@ -150,10 +204,10 @@ const insertRootAt = ( | |||
| } | |||
| const DropSlot = ({ cat, index }: { cat: Category, index: number }) => { | |||
| const DropSlot = ({ cat }: { cat: Category }) => { | |||
| const { setNodeRef, isOver: over } = useDroppable ({ | |||
| id: `slot:${ cat }:${ index }`, | |||
| data: { kind: 'slot', cat, index } }) | |||
| id: `slot:${ cat }`, | |||
| data: { kind: 'slot', cat } }) | |||
| return ( | |||
| <li ref={setNodeRef} className="h-1"> | |||
| @@ -162,12 +216,6 @@ const DropSlot = ({ cat, index }: { cat: Category, index: number }) => { | |||
| } | |||
| const removeFromRoot = ( | |||
| roots: Tag[], | |||
| childId: number, | |||
| ): Tag[] => roots.filter (t => t.id !== childId) | |||
| type Props = { post: Post | null } | |||
| @@ -189,15 +237,36 @@ export default (({ post }: Props) => { | |||
| const overKind = e.over?.data.current?.kind | |||
| if (!(childId) || !(overKind)) | |||
| if (childId == null || !(overKind)) | |||
| return | |||
| switch (overKind) | |||
| { | |||
| case 'tag': | |||
| const parentId: number | undefined = e.over?.data.current?.tagId | |||
| if (!(parentId) || childId === parentId) | |||
| if (parentId == null || childId === parentId) | |||
| return | |||
| try | |||
| { | |||
| 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') ?? '' } }) | |||
| } | |||
| catch | |||
| { | |||
| toast ({ description: '管理者権限が必要です.' }) | |||
| return | |||
| } | |||
| setTags (prev => { | |||
| const child = findTag (prev, childId) | |||
| @@ -205,38 +274,33 @@ export default (({ post }: Props) => { | |||
| if (!(child) || !(parent) || isDescendant (child, parentId)) | |||
| return prev | |||
| const cat = child.category | |||
| const next: TagByCategory = { ...prev } | |||
| if (fromParentId) | |||
| next[cat] = detachEdge (next[cat], fromParentId, childId) | |||
| else | |||
| next[cat] = removeFromRoot (next[cat], childId) | |||
| next[cat] = addAsChild (next[cat], parentId, child) | |||
| toast ({ description: `《${ child.name }》を《${ parent.name }》の子タグに設定しました.` }) | |||
| return next | |||
| return attachChildOptimistic (prev, parentId, childId) | |||
| }) | |||
| if (fromParentId) | |||
| { | |||
| 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') ?? '' } }) | |||
| break | |||
| case 'slot': | |||
| const cat: Category | undefined = e.over?.data.current?.cat | |||
| const index: number | undefined = e.over?.data.current?.index | |||
| if (!(cat) || index == null) | |||
| if (fromParentId == null | |||
| || !(cat) | |||
| || cat !== findTag (tags, childId)?.category) | |||
| return | |||
| try | |||
| { | |||
| await axios.delete ( | |||
| `${ API_BASE_URL }/tags/${ fromParentId }/children/${ childId }`, | |||
| { headers: { 'X-Transfer-Code': localStorage.getItem ('user_code') ?? '' } }) | |||
| } | |||
| catch | |||
| { | |||
| toast ({ description: '管理者権限が必要です.' }) | |||
| return | |||
| } | |||
| setTags (prev => { | |||
| const child = findTag (prev, childId) | |||
| @@ -245,18 +309,16 @@ export default (({ post }: Props) => { | |||
| const next: TagByCategory = { ...prev } | |||
| if (fromParentId) | |||
| if (fromParentId != null) | |||
| next[cat] = detachEdge (next[cat], fromParentId, childId) as any | |||
| next[cat] = insertRootAt (next[cat], index, child) | |||
| next[cat] = insertRootAt (next[cat], 0, child) | |||
| next[cat].sort ((a: Tag, b: Tag) => a.name < b.name ? -1 : 1) | |||
| return next | |||
| }) | |||
| await axios.delete ( | |||
| `${ API_BASE_URL }/tags/${ fromParentId }/children/${ childId }`, | |||
| { headers: { 'X-Transfer-Code': localStorage.getItem ('user_code') ?? '' } }) | |||
| break | |||
| } | |||
| } | |||
| @@ -322,7 +384,7 @@ export default (({ post }: Props) => { | |||
| <> | |||
| {renderTagTree (tag, 0, `cat-${ cat }`, suppressClickRef, undefined)} | |||
| </>))} | |||
| <DropSlot cat={cat} index={0}/> | |||
| <DropSlot cat={cat}/> | |||
| </AnimatePresence> | |||
| </motion.ul> | |||
| </motion.div>))} | |||