Files
colanode/packages/ui/src/components/spaces/space-sidebar-item.tsx
2025-11-11 09:49:16 -08:00

131 lines
4.3 KiB
TypeScript

import { ChevronRight } from 'lucide-react';
import { RefAttributes, useRef } from 'react';
import { useDrop } from 'react-dnd';
import { toast } from 'sonner';
import { LocalSpaceNode } from '@colanode/client/types';
import { extractNodeRole } from '@colanode/core';
import { Avatar } from '@colanode/ui/components/avatars/avatar';
import { WorkspaceSidebarItem } from '@colanode/ui/components/layouts/sidebars/sidebar-item';
import { SpaceSidebarDropdown } from '@colanode/ui/components/spaces/space-sidebar-dropdown';
import {
Collapsible,
CollapsibleContent,
CollapsibleTrigger,
} from '@colanode/ui/components/ui/collapsible';
import { Link } from '@colanode/ui/components/ui/link';
import { useWorkspace } from '@colanode/ui/contexts/workspace';
import { useLiveQuery } from '@colanode/ui/hooks/use-live-query';
import { useMutation } from '@colanode/ui/hooks/use-mutation';
import { sortSpaceChildren } from '@colanode/ui/lib/spaces';
import { cn } from '@colanode/ui/lib/utils';
interface SpaceSidebarItemProps {
space: LocalSpaceNode;
}
export const SpaceSidebarItem = ({ space }: SpaceSidebarItemProps) => {
const workspace = useWorkspace();
const mutation = useMutation();
const role = extractNodeRole(space, workspace.userId);
const canEdit = role === 'admin';
const nodeChildrenGetQuery = useLiveQuery({
type: 'node.children.get',
nodeId: space.id,
userId: workspace.userId,
types: ['page', 'channel', 'database', 'folder'],
});
const [dropMonitor, dropRef] = useDrop({
accept: 'sidebar-item',
drop: () => ({
after: null,
}),
collect: (monitor) => ({
isOver: monitor.isOver(),
canDrop: monitor.canDrop(),
}),
});
const divRef = useRef<HTMLDivElement>(null);
const dropDivRef = dropRef(divRef);
const children = sortSpaceChildren(space, nodeChildrenGetQuery.data ?? []);
const handleDragEnd = (childId: string, after: string | null) => {
mutation.mutate({
input: {
type: 'space.child.reorder',
userId: workspace.userId,
spaceId: space.id,
childId,
after,
},
onError(error) {
toast.error(error.message);
},
});
};
return (
<Collapsible
key={space.id}
defaultOpen={true}
className="group/sidebar-space"
>
<div
className={cn(
'text-sm flex h-7 items-center gap-2 overflow-hidden rounded-md px-2 text-sidebar-foreground hover:bg-sidebar-accent hover:text-sidebar-accent-foreground cursor-pointer',
dropMonitor.isOver &&
dropMonitor.canDrop &&
'border-b-2 border-blue-300'
)}
ref={dropDivRef as RefAttributes<HTMLDivElement>['ref']}
>
<CollapsibleTrigger asChild>
<button className="group/space-button flex items-center gap-2 overflow-hidden rounded-md p-2 text-left text-sm flex-1 cursor-pointer">
<Avatar
id={space.id}
avatar={space.attributes.avatar}
name={space.attributes.name}
className="size-4 group-hover/space-button:hidden"
/>
<ChevronRight className="hidden size-4 transition-transform duration-200 group-hover/space-button:block group-data-[state=open]/sidebar-space:rotate-90" />
<span>{space.attributes.name}</span>
</button>
</CollapsibleTrigger>
<SpaceSidebarDropdown space={space} />
</div>
<CollapsibleContent>
<ul className="mx-3.5 flex min-w-0 translate-x-px flex-col gap-1 border-l border-sidebar-border px-2.5 py-0.5 mr-0 pr-0">
{children.map((child) => (
<li key={child.id}>
<Link
from="/workspace/$userId"
to="$nodeId"
params={{
nodeId: child.id,
}}
className="cursor-pointer select-none"
>
{({ isActive }) => (
<WorkspaceSidebarItem
node={child}
isActive={isActive}
canDrag={canEdit}
onDragEnd={(after) => {
handleDragEnd(child.id, after);
}}
/>
)}
</Link>
</li>
))}
</ul>
</CollapsibleContent>
</Collapsible>
);
};