structure polish: post-reify nav + ensure-chain + TeamWizard auto-parent
Two small quality-of-life fixes on top of the reify commit:
Post-reify navigation
OrphanMigrationDialog already returned team_id in its result;
Dashboard now pushes /?team=<team_id> before router.refresh() so
the user lands on the freshly-materialized team and sees exactly
where their agents just moved. Previously they had to hunt for it
in the newly-rebuilt sidebar.
Wizard auto-materialize (POST /api/structure/ensure-chain)
cm-db: ensure_chain(pool, ws, fallback_org, fallback_company) —
fast path returns coordinates of the first org+company already
bound in this workspace (workspace's oldest org, oldest company
under it). Slow path inserts a new org+company with the
fallback names ("My Workspace" / "General") + binds them via
org_companies. Returns { org_id, company_id, created }. Small
txn — leaves the workspace consistent whether it was already
wired or not.
cm-api: POST /api/structure/ensure-chain accepts optional
fallback_org_name and fallback_company_name in the body (trimmed,
else default). Returns the ids.
CreateTeamRequest gains an optional attach_to_company_id. When
set, after build_team() completes, we look up the company
(workspace ownership check enforced by companies::get), count
its existing teams for a stable n_i node id, and insert a
company_teams binding — so the team lands under the parent
atomically instead of a follow-up round-trip.
TeamWizard now calls ensure-chain before POST /api/teams and
passes the returned company_id in attach_to_company_id. Both
calls are best-effort — if ensure-chain fails (network etc.)
we still try to create the team, and the migration dialog stays
available as the fallback UX. Wizard flow now: fresh workspace's
first team is fully wired from the moment it appears in the
tree — no synthetic "My Workspace" scaffolding ever gets
rendered around it.
The Team/Company create paths not touched here (create_team_from_claws,
company create, org create, MasterPlannerModal scaffold) still
work as before — they just won't auto-parent yet. Later commits
can wire them the same way.
This commit is contained in:
@@ -117,6 +117,97 @@ pub async fn count_orphans(
|
||||
})
|
||||
}
|
||||
|
||||
/// Result of [`ensure_chain`]: the ids of the org + company the caller
|
||||
/// should attach new teams under. `created` is `true` if either row was
|
||||
/// freshly inserted by this call, `false` when we reused what already
|
||||
/// existed. Wizards use this to guarantee a workspace has a parent chain
|
||||
/// before creating a team, without asking the user for names — the two
|
||||
/// created rows use the `fallback_*` names and can be renamed inline.
|
||||
#[derive(Debug, Clone, Serialize)]
|
||||
pub struct EnsuredChain {
|
||||
pub org_id: Uuid,
|
||||
pub company_id: Uuid,
|
||||
pub created: bool,
|
||||
}
|
||||
|
||||
/// Ensure the workspace has at least one org + one company; return
|
||||
/// coordinates for the first available. Idempotent: reuses existing rows
|
||||
/// when present, creates placeholder rows otherwise. Safe to call before
|
||||
/// every `POST /api/teams` so a fresh workspace's first team lands under
|
||||
/// a real parent chain instead of falling into synthesized scaffolding.
|
||||
pub async fn ensure_chain(
|
||||
pool: &PgPool,
|
||||
workspace_id: WorkspaceId,
|
||||
fallback_org_name: &str,
|
||||
fallback_company_name: &str,
|
||||
) -> Result<EnsuredChain, DbError> {
|
||||
// Fast path: workspace already has an org with a company bound. Use
|
||||
// the first one we find — the wizard doesn't need to be clever about
|
||||
// which parent to pick; the user can rename or reparent later.
|
||||
if let Some(row) = sqlx::query!(
|
||||
"SELECT oc.org_id AS org_id, oc.company_id AS company_id
|
||||
FROM org_companies oc
|
||||
JOIN orgs o ON o.id = oc.org_id
|
||||
WHERE o.workspace_id = $1
|
||||
ORDER BY o.created_at
|
||||
LIMIT 1",
|
||||
workspace_id.as_uuid(),
|
||||
)
|
||||
.fetch_optional(pool)
|
||||
.await?
|
||||
{
|
||||
return Ok(EnsuredChain {
|
||||
org_id: row.org_id,
|
||||
company_id: row.company_id,
|
||||
created: false,
|
||||
});
|
||||
}
|
||||
|
||||
// Slow path: materialize both. Reuses the reify shape but skips the
|
||||
// team + orphan moves — the caller is about to create its own team.
|
||||
let mut tx = pool.begin().await?;
|
||||
let now = OffsetDateTime::now_utc();
|
||||
let org_id = Uuid::now_v7();
|
||||
let company_id = Uuid::now_v7();
|
||||
let empty_graph = serde_json::json!({"kind":"flat","nodes":[],"edges":[]});
|
||||
sqlx::query!(
|
||||
"INSERT INTO orgs (id, workspace_id, name, kind, graph, status, created_at)
|
||||
VALUES ($1, $2, $3, 'flat', $4, 'active', $5)",
|
||||
org_id,
|
||||
workspace_id.as_uuid(),
|
||||
fallback_org_name,
|
||||
empty_graph,
|
||||
now,
|
||||
)
|
||||
.execute(&mut *tx)
|
||||
.await?;
|
||||
sqlx::query!(
|
||||
"INSERT INTO companies (id, workspace_id, name, kind, graph, status, created_at)
|
||||
VALUES ($1, $2, $3, 'flat', $4, 'active', $5)",
|
||||
company_id,
|
||||
workspace_id.as_uuid(),
|
||||
fallback_company_name,
|
||||
empty_graph,
|
||||
now,
|
||||
)
|
||||
.execute(&mut *tx)
|
||||
.await?;
|
||||
sqlx::query!(
|
||||
"INSERT INTO org_companies (org_id, node_id, company_id, role)
|
||||
VALUES ($1, 'n0', $2, 'company')",
|
||||
org_id,
|
||||
company_id,
|
||||
)
|
||||
.execute(&mut *tx)
|
||||
.await?;
|
||||
tx.commit().await?;
|
||||
Ok(EnsuredChain {
|
||||
org_id,
|
||||
company_id,
|
||||
created: true,
|
||||
})
|
||||
}
|
||||
|
||||
/// Result of a successful reify — ids of the freshly-created holder rows
|
||||
/// so the frontend can select the new org/company/team after refresh.
|
||||
#[derive(Debug, Clone, Serialize)]
|
||||
|
||||
Reference in New Issue
Block a user