settingspanel.tsx 4.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142
  1. /* -----------------------------------------------------------------------------
  2. | Copyright (c) Jupyter Development Team.
  3. | Distributed under the terms of the Modified BSD License.
  4. |----------------------------------------------------------------------------*/
  5. import { Settings } from '@jupyterlab/settingregistry';
  6. import { ITranslator } from '@jupyterlab/translation';
  7. import { IFormComponentRegistry } from '@jupyterlab/ui-components';
  8. import { ISignal } from '@lumino/signaling';
  9. import React, { useEffect, useState } from 'react';
  10. import { PluginList } from './pluginlist';
  11. import { SettingsFormEditor } from './SettingsFormEditor';
  12. export interface ISettingsPanelProps {
  13. /**
  14. * List of Settings objects that provide schema and values
  15. * of plugins.
  16. */
  17. settings: Settings[];
  18. /**
  19. * Form component registry that provides renderers
  20. * for the form editor.
  21. */
  22. editorRegistry: IFormComponentRegistry;
  23. /**
  24. * Handler for when selection change is triggered by scrolling
  25. * in the SettingsPanel.
  26. */
  27. onSelect: (id: string) => void;
  28. /**
  29. * Signal that fires when a selection is made in the plugin list.
  30. */
  31. handleSelectSignal: ISignal<PluginList, string>;
  32. /**
  33. * Translator object
  34. */
  35. translator: ITranslator;
  36. /**
  37. * Callback to update the plugin list to display plugins with
  38. * invalid / unsaved settings in red.
  39. */
  40. hasError: (id: string, error: boolean) => void;
  41. /**
  42. * Sends the updated dirty state to the parent class.
  43. */
  44. updateDirtyState: (dirty: boolean) => void;
  45. }
  46. /**
  47. * React component that displays a list of SettingsFormEditor
  48. * components.
  49. */
  50. export const SettingsPanel: React.FC<ISettingsPanelProps> = ({
  51. settings,
  52. editorRegistry,
  53. onSelect,
  54. handleSelectSignal,
  55. hasError,
  56. updateDirtyState,
  57. translator
  58. }: ISettingsPanelProps): JSX.Element => {
  59. const [expandedPlugin, setExpandedPlugin] = useState<string | null>(null);
  60. // Refs used to keep track of "selected" plugin based on scroll location
  61. const editorRefs: {
  62. [pluginId: string]: React.RefObject<HTMLDivElement>;
  63. } = {};
  64. for (const setting of settings) {
  65. editorRefs[setting.id] = React.useRef(null);
  66. }
  67. const wrapperRef: React.RefObject<HTMLDivElement> = React.useRef(null);
  68. const editorDirtyStates: React.RefObject<{
  69. [id: string]: boolean;
  70. }> = React.useRef({});
  71. useEffect(() => {
  72. const onSelectChange = (list: PluginList, pluginId: string) => {
  73. setExpandedPlugin(expandedPlugin !== pluginId ? pluginId : null);
  74. // Scroll to the plugin when a selection is made in the left panel.
  75. editorRefs[pluginId].current?.scrollIntoView(true);
  76. };
  77. handleSelectSignal?.connect?.(onSelectChange);
  78. return () => {
  79. handleSelectSignal?.disconnect?.(onSelectChange);
  80. };
  81. }, []);
  82. const updateDirtyStates = (id: string, dirty: boolean) => {
  83. if (editorDirtyStates.current) {
  84. editorDirtyStates.current[id] = dirty;
  85. for (const editor in editorDirtyStates.current) {
  86. if (editorDirtyStates.current[editor]) {
  87. updateDirtyState(true);
  88. return;
  89. }
  90. }
  91. }
  92. updateDirtyState(false);
  93. };
  94. return (
  95. <div className="jp-SettingsPanel" ref={wrapperRef}>
  96. {settings.map(pluginSettings => {
  97. return (
  98. <div
  99. ref={editorRefs[pluginSettings.id]}
  100. className="jp-SettingsForm"
  101. key={`${pluginSettings.id}SettingsEditor`}
  102. >
  103. <SettingsFormEditor
  104. isCollapsed={pluginSettings.id !== expandedPlugin}
  105. onCollapseChange={(willCollapse: boolean) => {
  106. if (!willCollapse) {
  107. setExpandedPlugin(pluginSettings.id);
  108. } else if (pluginSettings.id === expandedPlugin) {
  109. setExpandedPlugin(null);
  110. }
  111. }}
  112. settings={pluginSettings}
  113. renderers={editorRegistry.renderers}
  114. hasError={(error: boolean) => {
  115. hasError(pluginSettings.id, error);
  116. }}
  117. updateDirtyState={(dirty: boolean) => {
  118. updateDirtyStates(pluginSettings.id, dirty);
  119. }}
  120. onSelect={onSelect}
  121. translator={translator}
  122. />
  123. </div>
  124. );
  125. })}
  126. </div>
  127. );
  128. };