| @@ -12,6 +12,7 @@ import TagSearch from '@/components/TagSearch' | |||||
| import SectionTitle from '@/components/common/SectionTitle' | import SectionTitle from '@/components/common/SectionTitle' | ||||
| import SubsectionTitle from '@/components/common/SubsectionTitle' | import SubsectionTitle from '@/components/common/SubsectionTitle' | ||||
| import SidebarComponent from '@/components/layout/SidebarComponent' | import SidebarComponent from '@/components/layout/SidebarComponent' | ||||
| import { toast } from '@/components/ui/use-toast' | |||||
| import { API_BASE_URL } from '@/config' | import { API_BASE_URL } from '@/config' | ||||
| import { CATEGORIES } from '@/consts' | 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 = ( | const addAsChild = ( | ||||
| list: Tag[], | list: Tag[], | ||||
| parentId: number, | 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 = ( | const isDescendant = ( | ||||
| root: Tag, | root: Tag, | ||||
| targetId: number, | 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 ({ | const { setNodeRef, isOver: over } = useDroppable ({ | ||||
| id: `slot:${ cat }:${ index }`, | |||||
| data: { kind: 'slot', cat, index } }) | |||||
| id: `slot:${ cat }`, | |||||
| data: { kind: 'slot', cat } }) | |||||
| return ( | return ( | ||||
| <li ref={setNodeRef} className="h-1"> | <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 } | type Props = { post: Post | null } | ||||
| @@ -189,15 +237,36 @@ export default (({ post }: Props) => { | |||||
| const overKind = e.over?.data.current?.kind | const overKind = e.over?.data.current?.kind | ||||
| if (!(childId) || !(overKind)) | |||||
| if (childId == null || !(overKind)) | |||||
| return | return | ||||
| switch (overKind) | switch (overKind) | ||||
| { | { | ||||
| case 'tag': | case 'tag': | ||||
| const parentId: number | undefined = e.over?.data.current?.tagId | 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 | return | ||||
| } | |||||
| setTags (prev => { | setTags (prev => { | ||||
| const child = findTag (prev, childId) | const child = findTag (prev, childId) | ||||
| @@ -205,38 +274,33 @@ export default (({ post }: Props) => { | |||||
| if (!(child) || !(parent) || isDescendant (child, parentId)) | if (!(child) || !(parent) || isDescendant (child, parentId)) | ||||
| return prev | 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 | break | ||||
| case 'slot': | case 'slot': | ||||
| const cat: Category | undefined = e.over?.data.current?.cat | 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 | return | ||||
| } | |||||
| setTags (prev => { | setTags (prev => { | ||||
| const child = findTag (prev, childId) | const child = findTag (prev, childId) | ||||
| @@ -245,18 +309,16 @@ export default (({ post }: Props) => { | |||||
| const next: TagByCategory = { ...prev } | const next: TagByCategory = { ...prev } | ||||
| if (fromParentId) | |||||
| if (fromParentId != null) | |||||
| next[cat] = detachEdge (next[cat], fromParentId, childId) as any | 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 | return next | ||||
| }) | }) | ||||
| await axios.delete ( | |||||
| `${ API_BASE_URL }/tags/${ fromParentId }/children/${ childId }`, | |||||
| { headers: { 'X-Transfer-Code': localStorage.getItem ('user_code') ?? '' } }) | |||||
| break | break | ||||
| } | } | ||||
| } | } | ||||
| @@ -322,7 +384,7 @@ export default (({ post }: Props) => { | |||||
| <> | <> | ||||
| {renderTagTree (tag, 0, `cat-${ cat }`, suppressClickRef, undefined)} | {renderTagTree (tag, 0, `cat-${ cat }`, suppressClickRef, undefined)} | ||||
| </>))} | </>))} | ||||
| <DropSlot cat={cat} index={0}/> | |||||
| <DropSlot cat={cat}/> | |||||
| </AnimatePresence> | </AnimatePresence> | ||||
| </motion.ul> | </motion.ul> | ||||
| </motion.div>))} | </motion.div>))} | ||||