@@ -162,20 +162,104 @@ const helpTextStyle = {
162162
163163const fullWidthButtonClassName = "h-9 min-h-9 w-full justify-center" ;
164164
165- function asRecord ( value : unknown ) : JsonRecord {
165+ export function normalizeObjectValue ( value : unknown ) : JsonRecord {
166166 return value && typeof value === "object" && ! Array . isArray ( value ) ? ( value as JsonRecord ) : { } ;
167167}
168168
169- function asList ( value : unknown ) : JsonRecord [ ] {
170- return Array . isArray ( value ) ? value . map ( ( item ) => asRecord ( item ) ) : [ ] ;
169+ export function updateObjectValue ( value : unknown , key : string , nextValue : unknown ) : JsonRecord {
170+ return { ... normalizeObjectValue ( value ) , [ key ] : nextValue } ;
171171}
172172
173- function choices ( value ?: FieldsChoice [ ] | string [ ] ) : FieldsChoice [ ] {
173+ export function normalizeStructureValue ( value : unknown ) : JsonRecord [ ] {
174+ return Array . isArray ( value ) ? value . map ( ( item ) => normalizeObjectValue ( item ) ) : [ ] ;
175+ }
176+
177+ export function addStructureItem ( value : unknown ) : JsonRecord [ ] {
178+ return [ ...normalizeStructureValue ( value ) , { } ] ;
179+ }
180+
181+ export function updateStructureItem (
182+ value : unknown ,
183+ index : number ,
184+ nextItem : unknown ,
185+ ) : JsonRecord [ ] {
186+ const items = normalizeStructureValue ( value ) ;
187+ if ( index < 0 || index >= items . length ) {
188+ return items ;
189+ }
190+
191+ const nextItems = [ ...items ] ;
192+ nextItems [ index ] = normalizeObjectValue ( nextItem ) ;
193+ return nextItems ;
194+ }
195+
196+ export function removeStructureItem ( value : unknown , index : number ) : JsonRecord [ ] {
197+ return normalizeStructureValue ( value ) . filter ( ( _item , itemIndex ) => itemIndex !== index ) ;
198+ }
199+
200+ export function moveStructureItem (
201+ value : unknown ,
202+ fromIndex : number ,
203+ toIndex : number ,
204+ ) : JsonRecord [ ] {
205+ const items = normalizeStructureValue ( value ) ;
206+ if ( fromIndex < 0 || fromIndex >= items . length || toIndex < 0 || toIndex >= items . length ) {
207+ return items ;
208+ }
209+
210+ const nextItems = [ ...items ] ;
211+ const [ moved ] = nextItems . splice ( fromIndex , 1 ) ;
212+ if ( ! moved ) {
213+ return items ;
214+ }
215+
216+ nextItems . splice ( toIndex , 0 , moved ) ;
217+ return nextItems ;
218+ }
219+
220+ export function normalizeLinkValue ( value : unknown ) : LinkValue {
221+ return normalizeObjectValue ( value ) as LinkValue ;
222+ }
223+
224+ export function updateLinkValue ( value : unknown , nextValue : Partial < LinkValue > ) : LinkValue {
225+ return { ...normalizeLinkValue ( value ) , ...nextValue } ;
226+ }
227+
228+ export function normalizeChoices ( value ?: FieldsChoice [ ] | string [ ] ) : FieldsChoice [ ] {
174229 return ( value ?? [ ] ) . map ( ( choice ) =>
175230 typeof choice === "string" ? { value : choice , label : choice } : choice ,
176231 ) ;
177232}
178233
234+ export function normalizeChoiceSelection ( value : unknown , multiple : boolean ) : string [ ] {
235+ if ( multiple ) {
236+ return Array . isArray ( value )
237+ ? [ ...new Set ( value . filter ( ( item ) : item is string => typeof item === "string" ) ) ]
238+ : [ ] ;
239+ }
240+
241+ return typeof value === "string" ? [ value ] : [ ] ;
242+ }
243+
244+ export function updateChoiceSelection (
245+ value : unknown ,
246+ choiceValue : string ,
247+ checked : boolean ,
248+ multiple : boolean ,
249+ ) : string | string [ ] {
250+ if ( ! multiple ) {
251+ return checked ? choiceValue : ( normalizeChoiceSelection ( value , false ) [ 0 ] ?? "" ) ;
252+ }
253+
254+ const nextSelected = new Set ( normalizeChoiceSelection ( value , true ) ) ;
255+ if ( checked ) {
256+ nextSelected . add ( choiceValue ) ;
257+ } else {
258+ nextSelected . delete ( choiceValue ) ;
259+ }
260+ return [ ...nextSelected ] ;
261+ }
262+
179263function choiceColumnCount ( columns : number | undefined , total : number ) {
180264 if ( typeof columns === "number" && Number . isFinite ( columns ) ) {
181265 return Math . max ( 1 , Math . min ( Math . floor ( columns ) , Math . max ( total , 1 ) ) ) ;
@@ -276,7 +360,7 @@ function renderSubField(
276360 onChange ( readInputValue ( event , type ) ) ,
277361 } ;
278362
279- const selectChoices = choices ( field . options ) ;
363+ const selectChoices = normalizeChoices ( field . options ) ;
280364
281365 return (
282366 < div key = { field . key } style = { fieldStyle } >
@@ -339,7 +423,7 @@ function renderObjectFields(
339423 renderSubField (
340424 field ,
341425 value [ field . key ] ,
342- ( nextValue ) => onChange ( { ... value , [ field . key ] : nextValue } ) ,
426+ ( nextValue ) => onChange ( updateObjectValue ( value , field . key , nextValue ) ) ,
343427 idPrefix ,
344428 ) ,
345429 ) ;
@@ -359,7 +443,7 @@ export function ObjectField({
359443 id = "fields-object" ,
360444 options,
361445} : FieldWidgetProps < ObjectOptions > ) {
362- const data = asRecord ( value ) ;
446+ const data = normalizeObjectValue ( value ) ;
363447 const fields = options ?. fields ?? [ ] ;
364448
365449 if ( ! fields . length ) {
@@ -380,7 +464,7 @@ export function StructureField({
380464 id = "fields-structure" ,
381465 options,
382466} : FieldWidgetProps < StructureOptions > ) {
383- const items = asList ( value ) ;
467+ const items = normalizeStructureValue ( value ) ;
384468 const fields = options ?. fields ?? [ ] ;
385469 const itemLabel = options ?. itemLabel ?? "Item" ;
386470 const sortable = options ?. sortable !== false ;
@@ -393,14 +477,6 @@ export function StructureField({
393477 onChange ( nextItems ) ;
394478 }
395479
396- function moveItem ( fromIndex : number , toIndex : number ) {
397- const nextItems = [ ...items ] ;
398- const [ moved ] = nextItems . splice ( fromIndex , 1 ) ;
399- if ( ! moved ) return ;
400- nextItems . splice ( toIndex , 0 , moved ) ;
401- updateItems ( nextItems ) ;
402- }
403-
404480 return (
405481 < div id = { id } tabIndex = { - 1 } style = { wrapperStyle } >
406482 { items . map ( ( item , index ) => (
@@ -410,9 +486,7 @@ export function StructureField({
410486 fields ,
411487 item ,
412488 ( nextItem ) => {
413- const nextItems = [ ...items ] ;
414- nextItems [ index ] = nextItem ;
415- updateItems ( nextItems ) ;
489+ updateItems ( updateStructureItem ( items , index , nextItem ) ) ;
416490 } ,
417491 `${ id } -${ index } ` ,
418492 ) }
@@ -423,7 +497,7 @@ export function StructureField({
423497 variant = "secondary-destructive"
424498 icon = { TrashIcon }
425499 disabled = { typeof options ?. min === "number" && items . length <= options . min }
426- onClick = { ( ) => updateItems ( items . filter ( ( _item , itemIndex ) => itemIndex !== index ) ) }
500+ onClick = { ( ) => updateItems ( removeStructureItem ( items , index ) ) }
427501 >
428502 Remove
429503 </ Button >
@@ -434,7 +508,7 @@ export function StructureField({
434508 size = "sm"
435509 icon = { ArrowUpIcon }
436510 disabled = { index === 0 }
437- onClick = { ( ) => moveItem ( index , index - 1 ) }
511+ onClick = { ( ) => updateItems ( moveStructureItem ( items , index , index - 1 ) ) }
438512 >
439513 Up
440514 </ Button >
@@ -443,7 +517,7 @@ export function StructureField({
443517 size = "sm"
444518 icon = { ArrowDownIcon }
445519 disabled = { index === items . length - 1 }
446- onClick = { ( ) => moveItem ( index , index + 1 ) }
520+ onClick = { ( ) => updateItems ( moveStructureItem ( items , index , index + 1 ) ) }
447521 >
448522 Down
449523 </ Button >
@@ -458,7 +532,7 @@ export function StructureField({
458532 className = { fullWidthButtonClassName }
459533 icon = { PlusIcon }
460534 disabled = { typeof options ?. max === "number" && items . length >= options . max }
461- onClick = { ( ) => updateItems ( [ ... items , { } ] ) }
535+ onClick = { ( ) => updateItems ( addStructureItem ( items ) ) }
462536 >
463537 Add { itemLabel }
464538 </ Button >
@@ -478,10 +552,10 @@ export function LinkField({
478552 onChange,
479553 id = "fields-link" ,
480554} : FieldWidgetProps < Record < string , unknown > > ) {
481- const data = asRecord ( value ) as LinkValue ;
555+ const data = normalizeLinkValue ( value ) ;
482556
483557 function update ( nextValue : Partial < LinkValue > ) {
484- onChange ( { ... data , ... nextValue } ) ;
558+ onChange ( updateLinkValue ( data , nextValue ) ) ;
485559 }
486560
487561 return (
@@ -545,17 +619,11 @@ export function ChoicesField({
545619 id = "fields-choices" ,
546620 options,
547621} : FieldWidgetProps < ChoicesOptions > ) {
548- const choicesList = choices ( options ?. choices ?? options ?. options ) ;
622+ const choicesList = normalizeChoices ( options ?. choices ?? options ?. options ) ;
549623 const legend = label ?? "Choices" ;
550624 const multiple = Boolean ( options ?. multiple ) ;
551625 const horizontal = options ?. orientation === "horizontal" ;
552- const selected = multiple
553- ? new Set (
554- Array . isArray ( value )
555- ? value . filter ( ( item ) : item is string => typeof item === "string" )
556- : [ ] ,
557- )
558- : new Set ( typeof value === "string" ? [ value ] : [ ] ) ;
626+ const selected = new Set ( normalizeChoiceSelection ( value , multiple ) ) ;
559627
560628 if ( ! choicesList . length ) {
561629 return < p > Widget misconfigured: choices requires options.choices.</ p > ;
@@ -589,18 +657,19 @@ export function ChoicesField({
589657 style = { choiceControlStyle }
590658 onChange = { ( event : ChangeEvent < HTMLInputElement > ) => {
591659 if ( multiple ) {
592- const nextSelected = new Set ( selected ) ;
593- if ( event . currentTarget . checked ) {
594- nextSelected . add ( choice . value ) ;
595- } else {
596- nextSelected . delete ( choice . value ) ;
597- }
598- onChange ( [ ...nextSelected ] ) ;
660+ onChange (
661+ updateChoiceSelection (
662+ value ,
663+ choice . value ,
664+ event . currentTarget . checked ,
665+ true ,
666+ ) ,
667+ ) ;
599668 return ;
600669 }
601670
602671 if ( event . currentTarget . checked ) {
603- onChange ( choice . value ) ;
672+ onChange ( updateChoiceSelection ( value , choice . value , true , false ) ) ;
604673 }
605674 } }
606675 />
@@ -629,13 +698,9 @@ export function ChoicesField({
629698 value = { choice . value }
630699 style = { choiceControlStyle }
631700 onChange = { ( event : ChangeEvent < HTMLInputElement > ) => {
632- const nextSelected = new Set ( selected ) ;
633- if ( event . currentTarget . checked ) {
634- nextSelected . add ( choice . value ) ;
635- } else {
636- nextSelected . delete ( choice . value ) ;
637- }
638- onChange ( [ ...nextSelected ] ) ;
701+ onChange (
702+ updateChoiceSelection ( value , choice . value , event . currentTarget . checked , true ) ,
703+ ) ;
639704 } }
640705 />
641706 { choice . icon ? renderChoiceCardLabel ( choice ) : ( choice . label ?? choice . value ) }
@@ -653,7 +718,9 @@ export function ChoicesField({
653718 legend = { legend }
654719 appearance = "card"
655720 value = { typeof value === "string" ? value : "" }
656- onValueChange = { ( nextValue ) => onChange ( nextValue ) }
721+ onValueChange = { ( nextValue ) =>
722+ onChange ( updateChoiceSelection ( value , String ( nextValue ) , true , false ) )
723+ }
657724 description = { options ?. helpText }
658725 >
659726 { choicesList . map ( ( choice ) => {
0 commit comments