11import type React from "react" ;
2+ import { useEffect , useMemo , useState } from "react" ;
23import { Input } from "@/components/primitives/input" ;
34import { Label } from "@/components/primitives/label" ;
45import { Select } from "@/components/primitives/select" ;
6+ import { Button } from "@/components/primitives/button" ;
57import { TierLabel } from "@/components/TierLabel" ;
68import type { ProposalDraftForm } from "../types" ;
9+ import { apiActiveHumans } from "@/lib/apiClient" ;
710import {
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 >
0 commit comments