5
5
6
6
import { showErrorMessage } from '@jupyterlab/apputils' ;
7
7
import { caretDownIcon , caretRightIcon } from '@jupyterlab/ui-components' ;
8
- import { Settings } from '@jupyterlab/settingregistry' ;
8
+ import { ISettingRegistry , Settings } from '@jupyterlab/settingregistry' ;
9
9
import { ITranslator } from '@jupyterlab/translation' ;
10
10
import { reduce } from '@lumino/algorithm' ;
11
11
import { JSONExt , ReadonlyPartialJSONObject } from '@lumino/coreutils' ;
@@ -88,13 +88,40 @@ export namespace SettingsFormEditor {
88
88
/**
89
89
* The current form values being displayed in the editor.
90
90
*/
91
- formData : any ;
91
+ // formData: any;
92
92
93
93
/**
94
94
* Indicates whether the settings have been modified. Used for hiding
95
95
* the "Restore to Default" button when there are no changes.
96
96
*/
97
97
isModified : boolean ;
98
+
99
+ // The following are state derived from props for memoization to avoid
100
+ // `Form` update that results in focus lost
101
+ // A better fix (that will break the API) would be to move this as props
102
+ // of the component
103
+ /**
104
+ * Form UI schema
105
+ */
106
+ uiSchema : UiSchema ;
107
+ /**
108
+ * Filtered schema
109
+ */
110
+ filteredSchema : ISettingRegistry . ISchema ;
111
+ /**
112
+ * Array Field template
113
+ */
114
+ arrayFieldTemplate ?: React . StatelessComponent < ArrayFieldTemplateProps < any > > ;
115
+ /**
116
+ * Object Field template
117
+ */
118
+ objectFieldTemplate ?: React . StatelessComponent <
119
+ ObjectFieldTemplateProps < any >
120
+ > ;
121
+ /**
122
+ * Form context
123
+ */
124
+ formContext ?: any ;
98
125
}
99
126
}
100
127
@@ -332,18 +359,43 @@ export class SettingsFormEditor extends React.Component<
332
359
SettingsFormEditor . IProps ,
333
360
SettingsFormEditor . IState
334
361
> {
335
- private formState : any ;
336
362
constructor ( props : SettingsFormEditor . IProps ) {
337
363
super ( props ) ;
338
364
const { settings } = props ;
339
- this . formState = {
340
- formData : settings . composite ,
341
- isModified : settings . isModified
365
+ this . _formData = settings . composite ;
366
+ this . state = {
367
+ isModified : settings . isModified ,
368
+ uiSchema : { } ,
369
+ filteredSchema : this . props . settings . schema ,
370
+ arrayFieldTemplate : CustomArrayTemplateFactory ( this . props . translator ) ,
371
+ objectFieldTemplate : CustomObjectTemplateFactory ( this . props . translator ) ,
372
+ formContext : { settings : this . props . settings }
342
373
} ;
343
374
this . handleChange = this . handleChange . bind ( this ) ;
344
375
this . _debouncer = new Debouncer ( this . handleChange ) ;
345
376
}
346
377
378
+ componentDidMount ( ) : void {
379
+ this . _setUiSchema ( ) ;
380
+ this . _setFilteredSchema ( ) ;
381
+ }
382
+
383
+ componentDidUpdate ( prevProps : SettingsFormEditor . IProps ) : void {
384
+ this . _setUiSchema ( prevProps . renderers ) ;
385
+ this . _setFilteredSchema ( prevProps . filteredValues ) ;
386
+
387
+ if ( prevProps . translator !== this . props . translator ) {
388
+ this . setState ( {
389
+ arrayFieldTemplate : CustomArrayTemplateFactory ( this . props . translator ) ,
390
+ objectFieldTemplate : CustomObjectTemplateFactory ( this . props . translator )
391
+ } ) ;
392
+ }
393
+
394
+ if ( prevProps . settings !== this . props . settings ) {
395
+ this . setState ( { formContext : { settings : this . props . settings } } ) ;
396
+ }
397
+ }
398
+
347
399
/**
348
400
* Handler for edits made in the form editor.
349
401
* @param data - Form data sent from the form editor
@@ -352,18 +404,16 @@ export class SettingsFormEditor extends React.Component<
352
404
// Prevent unnecessary save when opening settings that haven't been modified.
353
405
if (
354
406
! this . props . settings . isModified &&
355
- this . props . settings . isDefault ( this . formState . formData )
407
+ this . props . settings . isDefault ( this . _formData )
356
408
) {
357
409
this . props . updateDirtyState ( false ) ;
358
410
return ;
359
411
}
360
412
this . props . settings
361
- . save (
362
- JSON . stringify ( this . formState . formData , undefined , JSON_INDENTATION )
363
- )
413
+ . save ( JSON . stringify ( this . _formData , undefined , JSON_INDENTATION ) )
364
414
. then ( ( ) => {
365
415
this . props . updateDirtyState ( false ) ;
366
- this . formState = { isModified : this . props . settings . isModified } ;
416
+ this . setState ( { isModified : this . props . settings . isModified } ) ;
367
417
} )
368
418
. catch ( ( reason : string ) => {
369
419
this . props . updateDirtyState ( false ) ;
@@ -377,48 +427,17 @@ export class SettingsFormEditor extends React.Component<
377
427
* modified settings then calls `setFormData` to restore the
378
428
* values.
379
429
*/
380
- reset = async ( ) : Promise < void > => {
430
+ reset = async ( event : React . MouseEvent ) : Promise < void > => {
431
+ event . stopPropagation ( ) ;
381
432
for ( const field in this . props . settings . user ) {
382
433
await this . props . settings . remove ( field ) ;
383
434
}
384
- this . formState = {
385
- formData : this . props . settings . composite ,
386
- isModified : false
387
- } ;
435
+ this . _formData = this . props . settings . composite ;
436
+ this . setState ( { isModified : false } ) ;
388
437
} ;
389
438
390
439
render ( ) : JSX . Element {
391
440
const trans = this . props . translator . load ( 'jupyterlab' ) ;
392
-
393
- /**
394
- * Construct uiSchema to pass any custom renderers to the form editor.
395
- */
396
- const uiSchema : UiSchema = { } ;
397
- for ( const id in this . props . renderers ) {
398
- if (
399
- Object . keys ( this . props . settings . schema . properties ?? { } ) . includes ( id )
400
- ) {
401
- uiSchema [ id ] = {
402
- 'ui:field' : id
403
- } ;
404
- }
405
- }
406
-
407
- /**
408
- * Only show fields that match search value.
409
- */
410
- const filteredSchema = JSONExt . deepCopy ( this . props . settings . schema ) ;
411
- if ( this . props . filteredValues ?. length ?? 0 > 0 ) {
412
- for ( const field in filteredSchema . properties ) {
413
- if (
414
- ! this . props . filteredValues ?. includes (
415
- filteredSchema . properties [ field ] . title ?? field
416
- )
417
- ) {
418
- delete filteredSchema . properties [ field ] ;
419
- }
420
- }
421
- }
422
441
const icon = this . props . isCollapsed ? caretRightIcon : caretDownIcon ;
423
442
424
443
return (
@@ -441,48 +460,102 @@ export class SettingsFormEditor extends React.Component<
441
460
{ this . props . settings . schema . description }
442
461
</ div >
443
462
</ header >
444
- { this . formState . isModified && (
463
+ { this . state . isModified && (
445
464
< button className = "jp-RestoreButton" onClick = { this . reset } >
446
465
{ trans . __ ( 'Restore to Defaults' ) }
447
466
</ button >
448
467
) }
449
468
</ div >
450
469
{ ! this . props . isCollapsed && (
451
470
< Form
452
- schema = { filteredSchema as JSONSchema7 }
453
- formData = { this . formState . formData }
471
+ schema = { this . state . filteredSchema as JSONSchema7 }
472
+ formData = { this . _formData }
454
473
FieldTemplate = { CustomTemplate }
455
- ArrayFieldTemplate = { CustomArrayTemplateFactory (
456
- this . props . translator
457
- ) }
458
- ObjectFieldTemplate = { CustomObjectTemplateFactory (
459
- this . props . translator
460
- ) }
461
- uiSchema = { uiSchema }
474
+ ArrayFieldTemplate = { this . state . arrayFieldTemplate }
475
+ ObjectFieldTemplate = { this . state . objectFieldTemplate }
476
+ uiSchema = { this . state . uiSchema }
462
477
fields = { this . props . renderers }
463
- formContext = { { settings : this . props . settings } }
478
+ formContext = { this . state . formContext }
464
479
liveValidate
465
480
idPrefix = { `jp-SettingsEditor-${ this . props . settings . id } ` }
466
- onChange = { ( e : IChangeEvent < ReadonlyPartialJSONObject > ) => {
467
- this . props . hasError ( e . errors . length !== 0 ) ;
468
- this . formState = { formData : e . formData } ;
469
- if ( e . errors . length === 0 ) {
470
- this . props . updateDirtyState ( true ) ;
471
- void this . _debouncer . invoke ( ) ;
472
- }
473
- this . props . onSelect ( this . props . settings . id ) ;
474
- } }
481
+ onChange = { this . _onChange }
475
482
/>
476
483
) }
477
484
</ div >
478
485
) ;
479
486
}
480
487
488
+ /**
489
+ * Callback on plugin selection
490
+ * @param list Plugin list
491
+ * @param id Plugin id
492
+ */
481
493
protected onSelect = ( list : PluginList , id : string ) : void => {
482
494
if ( id === this . props . settings . id ) {
483
495
this . props . onCollapseChange ( false ) ;
484
496
}
485
497
} ;
486
498
499
+ private _onChange = ( e : IChangeEvent < ReadonlyPartialJSONObject > ) : void => {
500
+ this . props . hasError ( e . errors . length !== 0 ) ;
501
+ this . _formData = e . formData ;
502
+ if ( e . errors . length === 0 ) {
503
+ this . props . updateDirtyState ( true ) ;
504
+ void this . _debouncer . invoke ( ) ;
505
+ }
506
+ this . props . onSelect ( this . props . settings . id ) ;
507
+ } ;
508
+
509
+ private _setUiSchema ( prevRenderers ?: { [ id : string ] : Field } ) {
510
+ if (
511
+ ! prevRenderers ||
512
+ ! JSONExt . deepEqual (
513
+ Object . keys ( prevRenderers ) . sort ( ) ,
514
+ Object . keys ( this . props . renderers ) . sort ( )
515
+ )
516
+ ) {
517
+ /**
518
+ * Construct uiSchema to pass any custom renderers to the form editor.
519
+ */
520
+ const uiSchema : UiSchema = { } ;
521
+ for ( const id in this . props . renderers ) {
522
+ if (
523
+ Object . keys ( this . props . settings . schema . properties ?? { } ) . includes ( id )
524
+ ) {
525
+ uiSchema [ id ] = {
526
+ 'ui:field' : id
527
+ } ;
528
+ }
529
+ }
530
+ this . setState ( { uiSchema } ) ;
531
+ }
532
+ }
533
+
534
+ private _setFilteredSchema ( prevFilteredValues ?: string [ ] | null ) {
535
+ if (
536
+ prevFilteredValues === undefined ||
537
+ ! JSONExt . deepEqual ( prevFilteredValues , this . props . filteredValues )
538
+ ) {
539
+ /**
540
+ * Only show fields that match search value.
541
+ */
542
+ const filteredSchema = JSONExt . deepCopy ( this . props . settings . schema ) ;
543
+ if ( this . props . filteredValues ?. length ?? 0 > 0 ) {
544
+ for ( const field in filteredSchema . properties ) {
545
+ if (
546
+ ! this . props . filteredValues ?. includes (
547
+ filteredSchema . properties [ field ] . title ?? field
548
+ )
549
+ ) {
550
+ delete filteredSchema . properties [ field ] ;
551
+ }
552
+ }
553
+ }
554
+
555
+ this . setState ( { filteredSchema } ) ;
556
+ }
557
+ }
558
+
487
559
private _debouncer : Debouncer < void , any > ;
560
+ private _formData : any ;
488
561
}
0 commit comments