forked from baron/baron-sso
feat: implement native drag and drop for organization hierarchy and add hover info tooltips
This commit is contained in:
@@ -580,13 +580,15 @@ const TenantTreeRow: React.FC<{
|
|||||||
level: number;
|
level: number;
|
||||||
isRoot: boolean;
|
isRoot: boolean;
|
||||||
onRemove: (id: string, name: string) => void;
|
onRemove: (id: string, name: string) => void;
|
||||||
|
onMove: (id: string, newParentId: string) => void;
|
||||||
isUpdating: boolean;
|
isUpdating: boolean;
|
||||||
searchTerm?: string;
|
searchTerm?: string;
|
||||||
}> = ({ node, level, isRoot, onRemove, isUpdating, searchTerm }) => {
|
}> = ({ node, level, isRoot, onRemove, onMove, isUpdating, searchTerm }) => {
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
const [isExpanded, setIsExpanded] = useState(true);
|
const [isExpanded, setIsExpanded] = useState(true);
|
||||||
const [isUserAddOpen, setIsUserAddOpen] = useState(false);
|
const [isUserAddOpen, setIsUserAddOpen] = useState(false);
|
||||||
const [isMemberListOpen, setIsMemberListOpen] = useState(false);
|
const [isMemberListOpen, setIsMemberListOpen] = useState(false);
|
||||||
|
const [isDragOver, setIsDragOver] = useState(false);
|
||||||
const hasChildren = node.children && node.children.length > 0;
|
const hasChildren = node.children && node.children.length > 0;
|
||||||
|
|
||||||
// Auto expand if search matches children
|
// Auto expand if search matches children
|
||||||
@@ -613,10 +615,44 @@ const TenantTreeRow: React.FC<{
|
|||||||
|
|
||||||
const TypeIcon = getTenantIcon(node.type);
|
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 (
|
return (
|
||||||
<>
|
<>
|
||||||
<TableRow
|
<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` }}>
|
<TableCell style={{ paddingLeft: `${1 + level * 2}rem` }}>
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
@@ -676,6 +712,7 @@ const TenantTreeRow: React.FC<{
|
|||||||
type="button"
|
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"
|
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)}
|
onClick={() => setIsMemberListOpen(true)}
|
||||||
|
title={t("msg.admin.org.hover_member_info", "클릭하여 멤버 상세 조회")}
|
||||||
>
|
>
|
||||||
<div className="bg-primary/10 p-1.5 rounded text-primary">
|
<div className="bg-primary/10 p-1.5 rounded text-primary">
|
||||||
<Users size={16} />
|
<Users size={16} />
|
||||||
@@ -772,6 +809,7 @@ const TenantTreeRow: React.FC<{
|
|||||||
level={level + 1}
|
level={level + 1}
|
||||||
isRoot={false}
|
isRoot={false}
|
||||||
onRemove={onRemove}
|
onRemove={onRemove}
|
||||||
|
onMove={onMove}
|
||||||
isUpdating={isUpdating}
|
isUpdating={isUpdating}
|
||||||
searchTerm={searchTerm}
|
searchTerm={searchTerm}
|
||||||
/>
|
/>
|
||||||
@@ -841,6 +879,10 @@ function TenantUserGroupsTab() {
|
|||||||
|
|
||||||
const handleAdd = (id: string) =>
|
const handleAdd = (id: string) =>
|
||||||
updateParentMutation.mutate({ id, parentId: tenantId });
|
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) => {
|
const handleRemove = (id: string, name: string) => {
|
||||||
if (
|
if (
|
||||||
window.confirm(
|
window.confirm(
|
||||||
@@ -1084,6 +1126,7 @@ function TenantUserGroupsTab() {
|
|||||||
level={0}
|
level={0}
|
||||||
isRoot={true}
|
isRoot={true}
|
||||||
onRemove={handleRemove}
|
onRemove={handleRemove}
|
||||||
|
onMove={handleMove}
|
||||||
isUpdating={updateParentMutation.isPending}
|
isUpdating={updateParentMutation.isPending}
|
||||||
searchTerm={treeSearchTerm}
|
searchTerm={treeSearchTerm}
|
||||||
/>
|
/>
|
||||||
|
|||||||
Reference in New Issue
Block a user