Skip to content

Commit f324a95

Browse files
feat(web): dissolve-in-chamber UX + active human selector
- unlock system chamber selector for dissolution (self‑dissolve) - replace genesis members text with active human node picker - update system actions/validation for censure+dissolve - sync sim-config RPC URL
1 parent 0476a7d commit f324a95

File tree

7 files changed

+262
-75
lines changed

7 files changed

+262
-75
lines changed

public/sim-config.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
{
2-
"humanodeRpcUrl": "https://explorer-rpc.mainnet.stages.humanode.io",
2+
"humanodeRpcUrl": "https://explorer-rpc-http.mainnet.stages.humanode.io",
33
"genesisChambers": [
44
{ "id": "general", "title": "General", "multiplier": 1.2 }
55
],

src/lib/apiClient.ts

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -593,6 +593,13 @@ export async function apiHumans(): Promise<GetHumansResponse> {
593593
return await apiGet<GetHumansResponse>("/api/humans");
594594
}
595595

596+
export type ActiveHumanDto = { address: string };
597+
export type GetActiveHumansResponse = { items: ActiveHumanDto[] };
598+
599+
export async function apiActiveHumans(): Promise<GetActiveHumansResponse> {
600+
return await apiGet<GetActiveHumansResponse>("/api/humans/active");
601+
}
602+
596603
export async function apiHuman(id: string): Promise<HumanNodeProfileDto> {
597604
return await apiGet<HumanNodeProfileDto>(`/api/humans/${id}`);
598605
}
@@ -647,7 +654,7 @@ export type ProposalDraftFormPayload = {
647654
| "administrative"
648655
| "dao-core";
649656
metaGovernance?: {
650-
action: "chamber.create" | "chamber.dissolve";
657+
action: "chamber.create" | "chamber.dissolve" | "chamber.censure";
651658
chamberId: string;
652659
title?: string;
653660
multiplier?: number;

src/pages/proposals/proposalCreation/steps/EssentialsStep.tsx

Lines changed: 226 additions & 65 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,12 @@
11
import type React from "react";
2+
import { useEffect, useMemo, useState } from "react";
23
import { Input } from "@/components/primitives/input";
34
import { Label } from "@/components/primitives/label";
45
import { Select } from "@/components/primitives/select";
6+
import { Button } from "@/components/primitives/button";
57
import { TierLabel } from "@/components/TierLabel";
68
import type { ProposalDraftForm } from "../types";
9+
import { apiActiveHumans } from "@/lib/apiClient";
710
import {
811
SYSTEM_ACTIONS,
912
getSystemActionMeta,
@@ -78,10 +81,52 @@ export function EssentialsStep(props: {
7881
const hasGeneralOption = chamberOptions.some(
7982
(opt) => opt.value === "general",
8083
);
84+
const systemChamberOptions = chamberOptions.filter(
85+
(opt) => opt.value !== "general",
86+
);
8187
const systemAction = draft.metaGovernance?.action as
8288
| SystemActionId
8389
| undefined;
8490
const systemActionMeta = getSystemActionMeta(systemAction);
91+
const allowSystemChamberSelect =
92+
isSystemProposal && systemAction === "chamber.dissolve";
93+
const [activeHumanOptions, setActiveHumanOptions] = useState<
94+
Array<{ value: string; label: string }>
95+
>([]);
96+
const [activeHumanError, setActiveHumanError] = useState<string | null>(null);
97+
const [selectedGenesisMember, setSelectedGenesisMember] = useState("");
98+
99+
useEffect(() => {
100+
let cancelled = false;
101+
if (!systemActionMeta.showGenesisMembers) {
102+
return () => {
103+
cancelled = true;
104+
};
105+
}
106+
apiActiveHumans()
107+
.then((res) => {
108+
if (cancelled) return;
109+
setActiveHumanError(null);
110+
const items = res.items
111+
.map((item) => item.address)
112+
.filter(Boolean)
113+
.map((address) => ({ value: address, label: address }));
114+
setActiveHumanOptions(items);
115+
})
116+
.catch((error) => {
117+
if (cancelled) return;
118+
setActiveHumanError((error as Error).message);
119+
setActiveHumanOptions([]);
120+
});
121+
return () => {
122+
cancelled = true;
123+
};
124+
}, [systemActionMeta.showGenesisMembers]);
125+
126+
const availableGenesisOptions = useMemo(() => {
127+
const selected = new Set(draft.metaGovernance?.genesisMembers ?? []);
128+
return activeHumanOptions.filter((opt) => !selected.has(opt.value));
129+
}, [activeHumanOptions, draft.metaGovernance?.genesisMembers]);
85130

86131
return (
87132
<div className="space-y-5">
@@ -194,8 +239,14 @@ export function EssentialsStep(props: {
194239
</Label>
195240
<Select
196241
id="chamber"
197-
value={isSystemProposal ? "general" : draft.chamberId}
198-
disabled={isSystemProposal}
242+
value={
243+
allowSystemChamberSelect
244+
? draft.chamberId
245+
: isSystemProposal
246+
? "general"
247+
: draft.chamberId
248+
}
249+
disabled={isSystemProposal && !allowSystemChamberSelect}
199250
onChange={(e) =>
200251
setDraft((prev) => ({
201252
...prev,
@@ -215,7 +266,9 @@ export function EssentialsStep(props: {
215266
</Select>
216267
{isSystemProposal ? (
217268
<p className="text-xs text-muted">
218-
System proposals must target General chamber.
269+
{allowSystemChamberSelect
270+
? "Dissolution can be submitted in General or the target chamber."
271+
: "System proposals must target General chamber."}
219272
</p>
220273
) : null}
221274
</div>
@@ -233,19 +286,33 @@ export function EssentialsStep(props: {
233286
onChange={(e) => {
234287
const action = e.target
235288
.value as MetaGovernanceDraft["action"];
236-
setDraft((prev) => ({
237-
...prev,
238-
metaGovernance: {
239-
...(prev.metaGovernance ?? {
240-
action,
241-
chamberId: "",
242-
title: "",
243-
genesisMembers: [],
244-
}),
289+
setDraft((prev) => {
290+
const nextChamberId =
291+
action === "chamber.dissolve"
292+
? prev.chamberId
293+
: "general";
294+
const nextMeta: MetaGovernanceDraft = {
245295
action,
246-
},
247-
chamberId: "general",
248-
}));
296+
chamberId: prev.metaGovernance?.chamberId ?? "",
297+
title:
298+
action === "chamber.create"
299+
? (prev.metaGovernance?.title ?? "")
300+
: "",
301+
multiplier:
302+
action === "chamber.create"
303+
? prev.metaGovernance?.multiplier
304+
: undefined,
305+
genesisMembers:
306+
action === "chamber.create"
307+
? (prev.metaGovernance?.genesisMembers ?? [])
308+
: [],
309+
};
310+
return {
311+
...prev,
312+
metaGovernance: nextMeta,
313+
chamberId: nextChamberId,
314+
};
315+
});
249316
}}
250317
>
251318
{Object.entries(SYSTEM_ACTIONS).map(([value, meta]) => (
@@ -260,27 +327,61 @@ export function EssentialsStep(props: {
260327
</div>
261328
<div className="space-y-1">
262329
<Label htmlFor="target-chamber-id">Target chamber id *</Label>
263-
<Input
264-
id="target-chamber-id"
265-
value={draft.metaGovernance?.chamberId ?? ""}
266-
onChange={(e) => {
267-
const chamberId = e.target.value;
268-
setDraft((prev) => ({
269-
...prev,
270-
metaGovernance: {
271-
...(prev.metaGovernance ?? {
272-
action: "chamber.create",
273-
chamberId: "",
274-
title: "",
275-
genesisMembers: [],
276-
}),
277-
chamberId,
278-
},
279-
chamberId: "general",
280-
}));
281-
}}
282-
placeholder="e.g., engineering"
283-
/>
330+
{systemActionMeta.requiresTitle ? (
331+
<Input
332+
id="target-chamber-id"
333+
value={draft.metaGovernance?.chamberId ?? ""}
334+
onChange={(e) => {
335+
const chamberId = e.target.value;
336+
setDraft((prev) => ({
337+
...prev,
338+
metaGovernance: {
339+
...(prev.metaGovernance ?? {
340+
action: "chamber.create",
341+
chamberId: "",
342+
title: "",
343+
genesisMembers: [],
344+
}),
345+
chamberId,
346+
},
347+
chamberId: "general",
348+
}));
349+
}}
350+
placeholder="e.g., engineering"
351+
/>
352+
) : (
353+
<Select
354+
id="target-chamber-id"
355+
value={draft.metaGovernance?.chamberId ?? ""}
356+
onChange={(e) => {
357+
const chamberId = e.target.value;
358+
setDraft((prev) => ({
359+
...prev,
360+
metaGovernance: {
361+
...(prev.metaGovernance ?? {
362+
action: "chamber.create",
363+
chamberId: "",
364+
title: "",
365+
genesisMembers: [],
366+
}),
367+
chamberId,
368+
},
369+
chamberId:
370+
systemAction === "chamber.dissolve" &&
371+
prev.chamberId !== "general"
372+
? chamberId
373+
: prev.chamberId,
374+
}));
375+
}}
376+
>
377+
<option value="">Select a chamber…</option>
378+
{systemChamberOptions.map((opt) => (
379+
<option key={opt.value} value={opt.value}>
380+
{opt.label}
381+
</option>
382+
))}
383+
</Select>
384+
)}
284385
{attemptedNext &&
285386
(draft.metaGovernance?.chamberId ?? "").trim().length === 0 ? (
286387
<p className="text-xs text-destructive">
@@ -371,36 +472,96 @@ export function EssentialsStep(props: {
371472
{systemActionMeta.showGenesisMembers ? (
372473
<div className="space-y-1">
373474
<Label htmlFor="genesis-members">
374-
Genesis members (optional, one address per line)
475+
Genesis members (optional, choose active human nodes)
375476
</Label>
376-
<textarea
377-
id="genesis-members"
378-
rows={4}
379-
className={textareaClassName}
380-
value={(draft.metaGovernance?.genesisMembers ?? []).join(
381-
"\n",
382-
)}
383-
onChange={(e) => {
384-
const genesisMembers = e.target.value
385-
.split("\n")
386-
.map((v) => v.trim())
387-
.filter(Boolean);
388-
setDraft((prev) => ({
389-
...prev,
390-
metaGovernance: {
391-
...(prev.metaGovernance ?? {
392-
action: "chamber.create",
393-
chamberId: "",
394-
title: "",
395-
genesisMembers: [],
396-
}),
397-
genesisMembers,
398-
},
399-
chamberId: "general",
400-
}));
401-
}}
402-
placeholder={"5F...Alice\n5F...Bob"}
403-
/>
477+
<div className="flex flex-col gap-2 sm:flex-row sm:items-center">
478+
<Select
479+
id="genesis-members"
480+
value={selectedGenesisMember}
481+
onChange={(e) => setSelectedGenesisMember(e.target.value)}
482+
>
483+
<option value="">
484+
{activeHumanOptions.length === 0
485+
? "No active human nodes available"
486+
: "Select a human node…"}
487+
</option>
488+
{availableGenesisOptions.map((opt) => (
489+
<option key={opt.value} value={opt.value}>
490+
{opt.label}
491+
</option>
492+
))}
493+
</Select>
494+
<Button
495+
type="button"
496+
variant="outline"
497+
disabled={!selectedGenesisMember}
498+
onClick={() => {
499+
if (!selectedGenesisMember) return;
500+
setDraft((prev) => ({
501+
...prev,
502+
metaGovernance: {
503+
...(prev.metaGovernance ?? {
504+
action: "chamber.create",
505+
chamberId: "",
506+
title: "",
507+
genesisMembers: [],
508+
}),
509+
genesisMembers: Array.from(
510+
new Set([
511+
...(prev.metaGovernance?.genesisMembers ?? []),
512+
selectedGenesisMember,
513+
]),
514+
),
515+
},
516+
chamberId: "general",
517+
}));
518+
setSelectedGenesisMember("");
519+
}}
520+
>
521+
Add
522+
</Button>
523+
</div>
524+
{activeHumanError ? (
525+
<p className="text-xs text-destructive">
526+
{activeHumanError}
527+
</p>
528+
) : null}
529+
{(draft.metaGovernance?.genesisMembers ?? []).length > 0 ? (
530+
<div className="flex flex-wrap gap-2 pt-1">
531+
{(draft.metaGovernance?.genesisMembers ?? []).map(
532+
(member) => (
533+
<button
534+
key={member}
535+
type="button"
536+
className="hover:border-border-strong rounded-full border border-border bg-panel px-3 py-1 text-xs text-text"
537+
onClick={() => {
538+
setDraft((prev) => ({
539+
...prev,
540+
metaGovernance: {
541+
...(prev.metaGovernance ?? {
542+
action: "chamber.create",
543+
chamberId: "",
544+
title: "",
545+
genesisMembers: [],
546+
}),
547+
genesisMembers: (
548+
prev.metaGovernance?.genesisMembers ?? []
549+
).filter((value) => value !== member),
550+
},
551+
chamberId: "general",
552+
}));
553+
}}
554+
>
555+
{member} · remove
556+
</button>
557+
),
558+
)}
559+
</div>
560+
) : (
561+
<p className="text-xs text-muted">
562+
Leave empty to auto-include only the proposer.
563+
</p>
564+
)}
404565
</div>
405566
) : null}
406567
</div>

src/pages/proposals/proposalCreation/steps/ReviewStep.tsx

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -92,7 +92,9 @@ export function ReviewStep(props: {
9292
<p className="text-muted">
9393
{draft.metaGovernance?.action === "chamber.dissolve"
9494
? "Dissolve chamber"
95-
: "Create chamber"}
95+
: draft.metaGovernance?.action === "chamber.censure"
96+
? "Censure chamber"
97+
: "Create chamber"}
9698
</p>
9799
</div>
98100
<div>

0 commit comments

Comments
 (0)