1
0
forked from baron/baron-sso

feat: implement native drag and drop for organization hierarchy and add hover info tooltips

This commit is contained in:
2026-03-04 16:50:41 +09:00
parent f55374f827
commit f258b1d457

View File

@@ -580,13 +580,15 @@ const TenantTreeRow: React.FC<{
level: number;
isRoot: boolean;
onRemove: (id: string, name: string) => void;
onMove: (id: string, newParentId: string) => void;
isUpdating: boolean;
searchTerm?: string;
}> = ({ node, level, isRoot, onRemove, isUpdating, searchTerm }) => {
}> = ({ node, level, isRoot, onRemove, onMove, isUpdating, searchTerm }) => {
const navigate = useNavigate();
const [isExpanded, setIsExpanded] = useState(true);
const [isUserAddOpen, setIsUserAddOpen] = useState(false);
const [isMemberListOpen, setIsMemberListOpen] = useState(false);
const [isDragOver, setIsDragOver] = useState(false);
const hasChildren = node.children && node.children.length > 0;
// Auto expand if search matches children
@@ -613,10 +615,44 @@ const TenantTreeRow: React.FC<{
const TypeIcon = getTenantIcon(node.type);
// DnD Handlers
const handleDragStart = (e: React.DragEvent) => {
if (isRoot) return;
e.dataTransfer.setData("nodeId", node.id);
e.dataTransfer.setData("nodeName", node.name);
e.dataTransfer.effectAllowed = "move";
};
const handleDragOver = (e: React.DragEvent) => {
e.preventDefault();
if (isUpdating) return;
setIsDragOver(true);
};
const handleDragLeave = () => {
setIsDragOver(false);
};
const handleDrop = (e: React.DragEvent) => {
e.preventDefault();
setIsDragOver(false);
const draggedId = e.dataTransfer.getData("nodeId");
if (!draggedId || draggedId === node.id) return;
onMove(draggedId, node.id);
};
const hoverTitle = `${node.name} (${node.type})\n${t("ui.admin.tenants.members.direct", "소속 멤버")}: ${node.memberCount || 0}\n${t("ui.admin.tenants.members.total", "총 멤버")}: ${node.recursiveMemberCount || 0}`;
return (
<>
<TableRow
className={`group hover:bg-muted/30 transition-colors ${isRoot ? "bg-primary/5 font-bold" : ""} ${isMatching ? "bg-primary/10 ring-1 ring-primary/20" : ""}`}
draggable={!isRoot}
onDragStart={handleDragStart}
onDragOver={handleDragOver}
onDragLeave={handleDragLeave}
onDrop={handleDrop}
className={`group hover:bg-muted/30 transition-colors ${isRoot ? "bg-primary/5 font-bold" : ""} ${isMatching ? "bg-primary/10 ring-1 ring-primary/20" : ""} ${isDragOver ? "bg-primary/20 border-2 border-dashed border-primary" : ""}`}
title={hoverTitle}
>
<TableCell style={{ paddingLeft: `${1 + level * 2}rem` }}>
<div className="flex items-center gap-2">
@@ -676,6 +712,7 @@ const TenantTreeRow: React.FC<{
type="button"
className="flex items-center gap-2 cursor-pointer hover:bg-muted p-1.5 rounded-md transition-all group/members w-full text-left"
onClick={() => setIsMemberListOpen(true)}
title={t("msg.admin.org.hover_member_info", "클릭하여 멤버 상세 조회")}
>
<div className="bg-primary/10 p-1.5 rounded text-primary">
<Users size={16} />
@@ -772,6 +809,7 @@ const TenantTreeRow: React.FC<{
level={level + 1}
isRoot={false}
onRemove={onRemove}
onMove={onMove}
isUpdating={isUpdating}
searchTerm={searchTerm}
/>
@@ -841,6 +879,10 @@ function TenantUserGroupsTab() {
const handleAdd = (id: string) =>
updateParentMutation.mutate({ id, parentId: tenantId });
const handleMove = (id: string, newParentId: string) => {
if (id === newParentId) return;
updateParentMutation.mutate({ id, parentId: newParentId });
};
const handleRemove = (id: string, name: string) => {
if (
window.confirm(
@@ -1084,6 +1126,7 @@ function TenantUserGroupsTab() {
level={0}
isRoot={true}
onRemove={handleRemove}
onMove={handleMove}
isUpdating={updateParentMutation.isPending}
searchTerm={treeSearchTerm}
/>