sidebar: click-to-rename org/company/team + strip synthetics from world viz
ci / gates (push) Successful in 7s
ci / frontend (push) Successful in 25s
ci / rust (push) Successful in 3m51s
ci / e2e (push) Has been skipped
ci / publish (push) Successful in 2m43s

Two related pieces of the "kill My Workspace" cleanup, landed
together because they share the same file:

Backend

- Three tiny inline-rename endpoints:
    PATCH /api/orgs/{id}/name
    PATCH /api/companies/{id}/name
    PATCH /api/teams/{id}/name
  Each takes { name: string }, trims + rejects empty, returns 204.
  Backed by rename_org / rename_company / rename_team in cm-db —
  single-row UPDATEs scoped to the caller's workspace, NotFound if
  the id isn't visible.
- Registered next to the existing PATCH /:id (topology) routes so
  they don't collide.

Frontend

- StructureTree accepts an optional onRename and canRename.
  TreeRow: click on the label text of a renamable node → the span
  becomes an <input>, focus + select-all, save on Enter or blur,
  cancel on Escape. The rest of the row (row chevron / row body)
  still navigates + selects as before, so single-click behaviour
  is preserved for everything except the name text itself.
  react-hooks/set-state-in-effect avoided by resetting the draft
  in the enterEdit() click handler instead of inside a useEffect.

- Dashboard passes canRename={item.level !== "claw" && !synthetic}
  (claws don't have a rename endpoint yet; synthetic scaffolding
  gets reified into real rows in the next commit — the wizard
  auto-materialize + orphan-migration dialog).
  onRename fires the corresponding PATCH and calls router.refresh()
  so the label lands in every consumer of the tree.

- World viz seed: new stripSynthetics(roots) helper walks the tree
  and lifts children of any synthetic container up to their
  grandparent's level. worldCanvasRoots feeds through this before
  narrowRoots(). Result: the Live viz no longer shows "My Workspace"
  or "Teams" nodes — real agents orbit the world root directly
  (which is what you were asking for). Sidebar tree still shows
  them so orphaned agents remain visible until the migration lands.
This commit is contained in:
Omar Sobh
2026-07-09 11:18:52 -07:00
parent a7cb895099
commit 99e5207e69
9 changed files with 251 additions and 17 deletions
+12
View File
@@ -356,6 +356,10 @@ pub fn router(state: AppState) -> Router {
.patch(routes::teams::patch_team) .patch(routes::teams::patch_team)
.delete(routes::teams::delete_team), .delete(routes::teams::delete_team),
) )
.route(
"/api/teams/{id}/name",
axum::routing::patch(routes::teams::rename_team),
)
.route("/api/teams/{id}/run", post(routes::teams::run_team)) .route("/api/teams/{id}/run", post(routes::teams::run_team))
.route( .route(
"/api/companies", "/api/companies",
@@ -367,6 +371,10 @@ pub fn router(state: AppState) -> Router {
.patch(routes::companies::patch_company) .patch(routes::companies::patch_company)
.delete(routes::companies::delete_company), .delete(routes::companies::delete_company),
) )
.route(
"/api/companies/{id}/name",
axum::routing::patch(routes::companies::rename_company),
)
.route( .route(
"/api/companies/{id}/run", "/api/companies/{id}/run",
post(routes::companies::run_company), post(routes::companies::run_company),
@@ -379,6 +387,10 @@ pub fn router(state: AppState) -> Router {
"/api/orgs/{id}", "/api/orgs/{id}",
get(routes::orgs::get_org).delete(routes::orgs::delete_org), get(routes::orgs::get_org).delete(routes::orgs::delete_org),
) )
.route(
"/api/orgs/{id}/name",
axum::routing::patch(routes::orgs::rename_org),
)
.route("/api/orgs/{id}/run", post(routes::orgs::run_org)) .route("/api/orgs/{id}/run", post(routes::orgs::run_org))
.route( .route(
"/api/research", "/api/research",
+19
View File
@@ -163,6 +163,25 @@ pub async fn patch_company(
Ok(StatusCode::NO_CONTENT) Ok(StatusCode::NO_CONTENT)
} }
/// `PATCH /api/companies/{id}/name` — inline rename from the sidebar.
#[derive(Deserialize)]
pub struct RenameCompanyRequest {
pub name: String,
}
pub async fn rename_company(
State(state): State<AppState>,
Authed(user): Authed,
Path(id): Path<Uuid>,
Json(body): Json<RenameCompanyRequest>,
) -> Result<StatusCode, ApiError> {
let name = body.name.trim();
if name.is_empty() {
return Err(ApiError::BadRequest);
}
cm_db::repo::companies::rename_company(&state.pool, id, user.workspace_id, name).await?;
Ok(StatusCode::NO_CONTENT)
}
/// `DELETE /api/companies/{id}` — remove a company (its teams remain). /// `DELETE /api/companies/{id}` — remove a company (its teams remain).
pub async fn delete_company( pub async fn delete_company(
State(state): State<AppState>, State(state): State<AppState>,
+21
View File
@@ -152,6 +152,27 @@ pub struct OrgDetail {
} }
/// `GET /api/orgs/{id}` — an org's graph + node→company bindings. /// `GET /api/orgs/{id}` — an org's graph + node→company bindings.
/// `PATCH /api/orgs/{id}/name` — inline rename from the sidebar. Trims the
/// input and rejects empty; returns 204 on success, 404 if the id isn't
/// visible in the caller's workspace.
#[derive(Deserialize)]
pub struct RenameOrgRequest {
pub name: String,
}
pub async fn rename_org(
State(state): State<AppState>,
Authed(user): Authed,
Path(id): Path<Uuid>,
Json(body): Json<RenameOrgRequest>,
) -> Result<StatusCode, ApiError> {
let name = body.name.trim();
if name.is_empty() {
return Err(ApiError::BadRequest);
}
cm_db::repo::orgs::rename_org(&state.pool, id, user.workspace_id, name).await?;
Ok(StatusCode::NO_CONTENT)
}
/// `DELETE /api/orgs/{id}` — remove the org (structural: companies survive, just /// `DELETE /api/orgs/{id}` — remove the org (structural: companies survive, just
/// ungrouped from it). /// ungrouped from it).
pub async fn delete_org( pub async fn delete_org(
+19
View File
@@ -365,6 +365,25 @@ pub async fn patch_team(
Ok(StatusCode::NO_CONTENT) Ok(StatusCode::NO_CONTENT)
} }
/// `PATCH /api/teams/{id}/name` — inline rename from the sidebar.
#[derive(Deserialize)]
pub struct RenameTeamRequest {
pub name: String,
}
pub async fn rename_team(
State(state): State<AppState>,
Authed(user): Authed,
Path(id): Path<Uuid>,
Json(body): Json<RenameTeamRequest>,
) -> Result<StatusCode, ApiError> {
let name = body.name.trim();
if name.is_empty() {
return Err(ApiError::BadRequest);
}
cm_db::repo::teams::rename_team(&state.pool, id, user.workspace_id, name).await?;
Ok(StatusCode::NO_CONTENT)
}
/// `DELETE /api/teams/{id}` — remove a team and its node→claw bindings (the claws /// `DELETE /api/teams/{id}` — remove a team and its node→claw bindings (the claws
/// themselves remain in the workspace). /// themselves remain in the workspace).
pub async fn delete_team( pub async fn delete_team(
+20
View File
@@ -86,6 +86,26 @@ pub async fn set_topology(
} }
/// Delete a company and its node→team bindings (teams themselves remain). /// Delete a company and its node→team bindings (teams themselves remain).
/// Rename a company in-place. Only the display name changes; the graph +
/// bindings are untouched.
pub async fn rename_company(
pool: &PgPool,
id: Uuid,
workspace_id: WorkspaceId,
name: &str,
) -> Result<(), DbError> {
let res = sqlx::query("UPDATE companies SET name = $3 WHERE id = $1 AND workspace_id = $2")
.bind(id)
.bind(workspace_id.as_uuid())
.bind(name)
.execute(pool)
.await?;
if res.rows_affected() == 0 {
return Err(DbError::NotFound);
}
Ok(())
}
pub async fn delete_company( pub async fn delete_company(
pool: &PgPool, pool: &PgPool,
id: Uuid, id: Uuid,
+20
View File
@@ -149,6 +149,26 @@ pub async fn companies_for_org(pool: &PgPool, org_id: Uuid) -> Result<Vec<OrgCom
/// Delete an org. Structural only: the `org_companies` links are removed /// Delete an org. Structural only: the `org_companies` links are removed
/// (cascade) so the companies survive, just ungrouped from this org. /// (cascade) so the companies survive, just ungrouped from this org.
/// Rename an org in-place. The name is the only mutable identity field.
/// Returns NotFound when the id isn't visible in the caller's workspace.
pub async fn rename_org(
pool: &PgPool,
id: Uuid,
workspace_id: WorkspaceId,
name: &str,
) -> Result<(), DbError> {
let res = sqlx::query("UPDATE orgs SET name = $3 WHERE id = $1 AND workspace_id = $2")
.bind(id)
.bind(workspace_id.as_uuid())
.bind(name)
.execute(pool)
.await?;
if res.rows_affected() == 0 {
return Err(DbError::NotFound);
}
Ok(())
}
pub async fn delete_org(pool: &PgPool, id: Uuid, workspace_id: WorkspaceId) -> Result<(), DbError> { pub async fn delete_org(pool: &PgPool, id: Uuid, workspace_id: WorkspaceId) -> Result<(), DbError> {
sqlx::query("DELETE FROM org_companies WHERE org_id = $1") sqlx::query("DELETE FROM org_companies WHERE org_id = $1")
.bind(id) .bind(id)
+20
View File
@@ -102,6 +102,26 @@ pub async fn set_topology(
} }
/// Delete a team and its node→claw bindings. /// Delete a team and its node→claw bindings.
/// Rename a team in-place. Only the display name changes; the topology
/// graph + member bindings are untouched.
pub async fn rename_team(
pool: &PgPool,
id: Uuid,
workspace_id: WorkspaceId,
name: &str,
) -> Result<(), DbError> {
let res = sqlx::query("UPDATE teams SET name = $3 WHERE id = $1 AND workspace_id = $2")
.bind(id)
.bind(workspace_id.as_uuid())
.bind(name)
.execute(pool)
.await?;
if res.rows_affected() == 0 {
return Err(DbError::NotFound);
}
Ok(())
}
pub async fn delete_team( pub async fn delete_team(
pool: &PgPool, pool: &PgPool,
id: Uuid, id: Uuid,
@@ -515,7 +515,23 @@ export function Dashboard({ user, orgs, claws }: { user?: { display_name?: strin
} }
return roots; return roots;
}; };
const worldCanvasRoots: TreeItem[] = narrowRoots(worldRoots, worldSel); // Strip synthetic UI-only containers ("my-workspace", "ws-teams",
// "ungrouped-co", "ungrouped-team") from the world viz seed. If they
// have children (real teams / agents), lift those children up to the
// parent's level so the viz shows the real workforce directly under
// the world root instead of parked inside fake orgs. Sidebar keeps the
// synthetic containers so orphaned agents still have a visible home
// until the wizard-driven migration lands.
const stripSynthetics = (roots: TreeItem[]): TreeItem[] => {
const walk = (items: TreeItem[]): TreeItem[] =>
items.flatMap((n) =>
SYNTHETIC_TREE_IDS.has(n.id)
? walk(n.children ?? [])
: [{ ...n, children: walk(n.children ?? []) }],
);
return walk(roots);
};
const worldCanvasRoots: TreeItem[] = narrowRoots(stripSynthetics(worldRoots), worldSel);
// Both the World and Agents tiers now share the same collapsible org → // Both the World and Agents tiers now share the same collapsible org →
// company → team → agent tree — a single pane onto the whole workforce. // company → team → agent tree — a single pane onto the whole workforce.
// `selectLevel` below still constrains selection to agents on the Agents // `selectLevel` below still constrains selection to agents on the Agents
@@ -677,13 +693,39 @@ export function Dashboard({ user, orgs, claws }: { user?: { display_name?: strin
) : ( ) : (
<> <>
{/* The collapsible org → company → team → agent tree (World) or the flat agents list. */} {/* The collapsible org → company → team → agent tree (World) or the flat agents list. */}
<StructureTree roots={treeRoots} activeId={treeActiveId} autoExpand={treeAutoExpand} onSelectNode={onTreeSelect} selectMode={selectMode} selectLevel={"*"} selectedIds={selectedAgents} onToggleSelect={(id) => { <StructureTree
// Synthetic UI-only containers ("my-workspace", "ws-teams", roots={treeRoots}
// "ungrouped-co", "ungrouped-team") aren't real DB rows — the activeId={treeActiveId}
// backend would 422 on the non-UUID id. Silently ignore taps. autoExpand={treeAutoExpand}
if (SYNTHETIC_TREE_IDS.has(id)) return; onSelectNode={onTreeSelect}
setSelectedAgents((prev) => { const next = new Set(prev); if (next.has(id)) { next.delete(id); return next; } const lv = nodeLevel.get(id); const curLv = prev.size ? nodeLevel.get([...prev][0]) : lv; if (lv !== curLv) return new Set([id]); next.add(id); return next; }); selectMode={selectMode}
}} /> selectLevel={"*"}
selectedIds={selectedAgents}
onToggleSelect={(id) => {
// Synthetic UI-only containers ("my-workspace", "ws-teams",
// "ungrouped-co", "ungrouped-team") aren't real DB rows — the
// backend would 422 on the non-UUID id. Silently ignore taps.
if (SYNTHETIC_TREE_IDS.has(id)) return;
setSelectedAgents((prev) => { const next = new Set(prev); if (next.has(id)) { next.delete(id); return next; } const lv = nodeLevel.get(id); const curLv = prev.size ? nodeLevel.get([...prev][0]) : lv; if (lv !== curLv) return new Set([id]); next.add(id); return next; });
}}
// Only real (non-synthetic, non-claw) nodes accept an inline
// rename. Synthetic scaffolding gets swapped for real rows in the
// next commit (wizard auto-materialize + migration dialog).
canRename={(it) => it.level !== "claw" && !SYNTHETIC_TREE_IDS.has(it.id)}
onRename={async (id, level, newLabel) => {
const path = level === "org" ? "orgs" : level === "company" ? "companies" : level === "team" ? "teams" : null;
if (!path) return;
const res = await fetch(`/api/${path}/${id}/name`, {
method: "PATCH",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ name: newLabel }),
});
if (!res.ok) throw new Error(`PATCH /api/${path}/${id}/name → ${res.status}`);
// Force a re-fetch of the workspace tree so the new label lands
// everywhere (sidebar + viz + breadcrumbs).
router.refresh();
}}
/>
{selectMode ? ( {selectMode ? (
<div style={{ flex: "none", borderTop: "1px solid rgba(255,255,255,.08)", padding: "10px 12px", display: "flex", flexDirection: "column", gap: 8 }}> <div style={{ flex: "none", borderTop: "1px solid rgba(255,255,255,.08)", padding: "10px 12px", display: "flex", flexDirection: "column", gap: 8 }}>
<div style={{ fontFamily: mono, fontSize: 11, color: selectedItems.length ? "#ff8a7a" : "#6a6a72" }}>{selectedItems.length} selected{selectedItems.length ? ` · ${reapKind}` : ""}</div> <div style={{ fontFamily: mono, fontSize: 11, color: selectedItems.length ? "#ff8a7a" : "#6a6a72" }}>{selectedItems.length} selected{selectedItems.length ? ` · ${reapKind}` : ""}</div>
@@ -7,7 +7,7 @@
// clicking a claw opens that claw. Built to stay legible even when a whole org // clicking a claw opens that claw. Built to stay legible even when a whole org
// of claws is expanded (indented rows, scrollable). // of claws is expanded (indented rows, scrollable).
import { useState } from "react"; import { useEffect, useRef, useState } from "react";
import type { DemoAgent, DemoCompany, DemoOrg, DemoTeam } from "@/lib/dashboard-demo"; import type { DemoAgent, DemoCompany, DemoOrg, DemoTeam } from "@/lib/dashboard-demo";
@@ -64,7 +64,7 @@ function levelIcon(item: TreeItem, active: boolean) {
} }
function TreeRow({ function TreeRow({
item, depth, expanded, toggle, activeId, onSelectNode, selectMode, selectLevel, selectedIds, onToggleSelect, item, depth, expanded, toggle, activeId, onSelectNode, selectMode, selectLevel, selectedIds, onToggleSelect, onRename, canRename,
}: { }: {
item: TreeItem; item: TreeItem;
depth: number; depth: number;
@@ -76,6 +76,8 @@ function TreeRow({
selectLevel?: string; selectLevel?: string;
selectedIds?: Set<string>; selectedIds?: Set<string>;
onToggleSelect?: (id: string) => void; onToggleSelect?: (id: string) => void;
onRename?: (id: string, level: TreeLevel, newLabel: string) => Promise<void>;
canRename?: (item: TreeItem) => boolean;
}) { }) {
const hasChildren = !!item.children?.length; const hasChildren = !!item.children?.length;
const isOpen = expanded.has(item.id); const isOpen = expanded.has(item.id);
@@ -83,11 +85,42 @@ function TreeRow({
// selectLevel "*" → any node is selectable (World tree spans all levels). // selectLevel "*" → any node is selectable (World tree spans all levels).
const selectable = !!selectMode && (selectLevel === "*" ? true : item.level === (selectLevel ?? "claw")); const selectable = !!selectMode && (selectLevel === "*" ? true : item.level === (selectLevel ?? "claw"));
const checked = selectedIds?.has(item.id) ?? false; const checked = selectedIds?.has(item.id) ?? false;
const renamable =
!selectable && !!onRename && (canRename ? canRename(item) : item.level !== "claw");
const [editing, setEditing] = useState(false);
const [draft, setDraft] = useState(item.label);
const [saving, setSaving] = useState(false);
const inputRef = useRef<HTMLInputElement | null>(null);
// Focus + select-all in the next tick so the user can just type over.
// Reset of `draft` happens in the click handler that flips `editing`,
// not here — avoids react-hooks/set-state-in-effect.
useEffect(() => {
if (editing) queueMicrotask(() => inputRef.current?.select());
}, [editing]);
function enterEdit() {
setDraft(item.label);
setEditing(true);
}
async function commit() {
const next = draft.trim();
if (!next || next === item.label) {
setEditing(false);
return;
}
setSaving(true);
try {
await onRename?.(item.id, item.level, next);
} finally {
setSaving(false);
setEditing(false);
}
}
return ( return (
<> <>
<div <div
onClick={() => { if (selectable) { onToggleSelect?.(item.id); return; } if (hasChildren) toggle(item.id); onSelectNode(item); }} onClick={() => { if (editing) return; if (selectable) { onToggleSelect?.(item.id); return; } if (hasChildren) toggle(item.id); onSelectNode(item); }}
style={{ position: "relative", display: "flex", alignItems: "center", gap: 8, padding: "6px 8px", paddingLeft: 8 + depth * 14, borderRadius: 8, cursor: "pointer", background: checked ? "rgba(255,111,97,.16)" : active ? "rgba(255,111,97,.12)" : "transparent" }} style={{ position: "relative", display: "flex", alignItems: "center", gap: 8, padding: "6px 8px", paddingLeft: 8 + depth * 14, borderRadius: 8, cursor: editing ? "text" : "pointer", background: checked ? "rgba(255,111,97,.16)" : active ? "rgba(255,111,97,.12)" : "transparent" }}
> >
{active && !selectable ? <span style={{ position: "absolute", left: 0, top: 6, bottom: 6, width: 3, borderRadius: "0 3px 3px 0", background: "#ff6f61" }} /> : null} {active && !selectable ? <span style={{ position: "absolute", left: 0, top: 6, bottom: 6, width: 3, borderRadius: "0 3px 3px 0", background: "#ff6f61" }} /> : null}
{selectable ? ( {selectable ? (
@@ -102,14 +135,35 @@ function TreeRow({
)} )}
{levelIcon(item, active)} {levelIcon(item, active)}
<span style={{ flex: 1, minWidth: 0 }}> <span style={{ flex: 1, minWidth: 0 }}>
<span style={{ display: "block", fontSize: 12.5, fontWeight: active ? 600 : 500, color: active ? "#fff" : "#dcdce2", whiteSpace: "nowrap", overflow: "hidden", textOverflow: "ellipsis" }}>{item.label}</span> {editing ? (
{item.meta ? <span style={{ display: "block", fontFamily: mono, fontSize: 9.5, color: active ? "#ff8a7a" : "#6a6a72", whiteSpace: "nowrap", overflow: "hidden", textOverflow: "ellipsis" }}>{item.meta}</span> : null} <input
ref={inputRef}
value={draft}
disabled={saving}
autoFocus
onClick={(e) => e.stopPropagation()}
onChange={(e) => setDraft(e.target.value)}
onKeyDown={(e) => {
if (e.key === "Enter") { e.preventDefault(); void commit(); }
else if (e.key === "Escape") { e.preventDefault(); setEditing(false); }
}}
onBlur={() => { void commit(); }}
style={{ display: "block", width: "100%", fontSize: 12.5, fontWeight: 600, color: "#fff", background: "rgba(255,255,255,.06)", border: "1px solid rgba(255,111,97,.5)", borderRadius: 5, padding: "1px 5px", outline: "none" }}
/>
) : (
<span
onClick={renamable ? (e) => { e.stopPropagation(); enterEdit(); } : undefined}
title={renamable ? "Click to rename" : undefined}
style={{ display: "block", fontSize: 12.5, fontWeight: active ? 600 : 500, color: active ? "#fff" : "#dcdce2", whiteSpace: "nowrap", overflow: "hidden", textOverflow: "ellipsis", cursor: renamable ? "text" : "inherit" }}
>{item.label}</span>
)}
{item.meta && !editing ? <span style={{ display: "block", fontFamily: mono, fontSize: 9.5, color: active ? "#ff8a7a" : "#6a6a72", whiteSpace: "nowrap", overflow: "hidden", textOverflow: "ellipsis" }}>{item.meta}</span> : null}
</span> </span>
{item.level !== "claw" && item.status ? <span style={{ flex: "none", width: 6, height: 6, borderRadius: "50%", background: statusColor(item.status) }} /> : null} {item.level !== "claw" && item.status ? <span style={{ flex: "none", width: 6, height: 6, borderRadius: "50%", background: statusColor(item.status) }} /> : null}
</div> </div>
{hasChildren && isOpen {hasChildren && isOpen
? item.children!.map((c) => ( ? item.children!.map((c) => (
<TreeRow key={c.id} item={c} depth={depth + 1} expanded={expanded} toggle={toggle} activeId={activeId} onSelectNode={onSelectNode} selectMode={selectMode} selectLevel={selectLevel} selectedIds={selectedIds} onToggleSelect={onToggleSelect} /> <TreeRow key={c.id} item={c} depth={depth + 1} expanded={expanded} toggle={toggle} activeId={activeId} onSelectNode={onSelectNode} selectMode={selectMode} selectLevel={selectLevel} selectedIds={selectedIds} onToggleSelect={onToggleSelect} onRename={onRename} canRename={canRename} />
)) ))
: null} : null}
</> </>
@@ -117,7 +171,7 @@ function TreeRow({
} }
export function StructureTree({ export function StructureTree({
roots, activeId, autoExpand, onSelectNode, selectMode, selectLevel, selectedIds, onToggleSelect, roots, activeId, autoExpand, onSelectNode, selectMode, selectLevel, selectedIds, onToggleSelect, onRename, canRename,
}: { }: {
roots: TreeItem[]; roots: TreeItem[];
activeId: string | null; activeId: string | null;
@@ -127,6 +181,13 @@ export function StructureTree({
selectLevel?: string; selectLevel?: string;
selectedIds?: Set<string>; selectedIds?: Set<string>;
onToggleSelect?: (id: string) => void; onToggleSelect?: (id: string) => void;
/** Called when the user commits a new label. Awaited so the row can
* show a saving state; throw to signal failure (the row will still
* exit edit mode — parent should re-render with the DB-truth label). */
onRename?: (id: string, level: TreeLevel, newLabel: string) => Promise<void>;
/** Gate for which nodes accept an inline rename. Defaults to
* "everything except claw" — synthetic ids should be filtered here. */
canRename?: (item: TreeItem) => boolean;
}) { }) {
const autoKey = autoExpand.join("|"); const autoKey = autoExpand.join("|");
const [seenAuto, setSeenAuto] = useState<string>(autoKey); const [seenAuto, setSeenAuto] = useState<string>(autoKey);
@@ -148,7 +209,7 @@ export function StructureTree({
return ( return (
<div style={{ flex: 1, overflowY: "auto", padding: 8 }}> <div style={{ flex: 1, overflowY: "auto", padding: 8 }}>
{roots.map((r) => ( {roots.map((r) => (
<TreeRow key={r.id} item={r} depth={0} expanded={expanded} toggle={toggle} activeId={activeId} onSelectNode={onSelectNode} selectMode={selectMode} selectLevel={selectLevel} selectedIds={selectedIds} onToggleSelect={onToggleSelect} /> <TreeRow key={r.id} item={r} depth={0} expanded={expanded} toggle={toggle} activeId={activeId} onSelectNode={onSelectNode} selectMode={selectMode} selectLevel={selectLevel} selectedIds={selectedIds} onToggleSelect={onToggleSelect} onRename={onRename} canRename={canRename} />
))} ))}
</div> </div>
); );