@khanacademy/perseus-editor 2.2.2 → 2.3.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (48) hide show
  1. package/CHANGELOG.md +22 -0
  2. package/dist/all-widgets.d.ts +2 -2
  3. package/dist/all-widgets.js.flow +2 -2
  4. package/dist/components/graph-points-count-selector.d.ts +7 -0
  5. package/dist/components/graph-points-count-selector.js.flow +12 -0
  6. package/dist/components/graph-type-selector.d.ts +7 -0
  7. package/dist/components/graph-type-selector.js.flow +12 -0
  8. package/dist/components/segment-count-selector.d.ts +6 -0
  9. package/dist/components/segment-count-selector.js.flow +11 -0
  10. package/dist/components/widget-editor.d.ts +31 -0
  11. package/dist/components/widget-editor.js.flow +91 -0
  12. package/dist/components/widget-select.d.ts +10 -0
  13. package/dist/components/widget-select.js.flow +16 -0
  14. package/dist/es/index.js +423 -229
  15. package/dist/es/index.js.map +1 -1
  16. package/dist/index.js +431 -225
  17. package/dist/index.js.map +1 -1
  18. package/dist/testing-widgets.d.ts +2 -2
  19. package/dist/testing-widgets.js.flow +2 -2
  20. package/dist/util/points.d.ts +3 -0
  21. package/dist/util/points.js.flow +9 -0
  22. package/dist/widgets/example-graphie-widget.d.ts +2 -2
  23. package/dist/widgets/example-graphie-widget.js.flow +2 -2
  24. package/dist/widgets/example-widget.d.ts +2 -2
  25. package/dist/widgets/example-widget.js.flow +2 -2
  26. package/dist/widgets/interactive-graph-editor.d.ts +3182 -1
  27. package/dist/widgets/interactive-graph-editor.js.flow +5290 -1
  28. package/package.json +9 -3
  29. package/src/__stories__/editor.stories.tsx +105 -4
  30. package/src/article-editor.tsx +1 -1
  31. package/src/{__stories__ → components/__stories__}/section-control-button.stories.tsx +1 -1
  32. package/src/components/graph-points-count-selector.tsx +29 -0
  33. package/src/components/graph-type-selector.tsx +31 -0
  34. package/src/components/json-editor.tsx +0 -1
  35. package/src/components/segment-count-selector.tsx +27 -0
  36. package/src/components/widget-editor.tsx +232 -0
  37. package/src/components/widget-select.tsx +48 -0
  38. package/src/editor.tsx +4 -270
  39. package/src/util/points.test.ts +24 -0
  40. package/src/util/points.ts +13 -0
  41. package/src/widgets/interactive-graph-editor.tsx +294 -48
  42. package/tsconfig-build.json +0 -1
  43. package/tsconfig-build.tsbuildinfo +1 -1
  44. package/dist/diffs/__stories__/perseus-diff-wrapper.d.ts +0 -6
  45. package/dist/diffs/__stories__/perseus-diff-wrapper.js.flow +0 -12
  46. /package/dist/{section-control-button.d.ts → components/section-control-button.d.ts} +0 -0
  47. /package/dist/{section-control-button.js.flow → components/section-control-button.js.flow} +0 -0
  48. /package/src/{section-control-button.tsx → components/section-control-button.tsx} +0 -0
package/package.json CHANGED
@@ -3,7 +3,7 @@
3
3
  "description": "Perseus editors",
4
4
  "author": "Khan Academy",
5
5
  "license": "MIT",
6
- "version": "2.2.2",
6
+ "version": "2.3.1",
7
7
  "publishConfig": {
8
8
  "access": "public"
9
9
  },
@@ -23,13 +23,16 @@
23
23
  "dependencies": {
24
24
  "@khanacademy/kas": "^0.3.1",
25
25
  "@khanacademy/kmath": "^0.1.1",
26
- "@khanacademy/perseus": "^6.2.1"
26
+ "@khanacademy/perseus": "^6.3.0"
27
27
  },
28
28
  "devDependencies": {
29
29
  "@khanacademy/wonder-blocks-button": "^4.0.8",
30
30
  "@khanacademy/wonder-blocks-clickable": "^3.0.8",
31
31
  "@khanacademy/wonder-blocks-color": "^2.0.1",
32
+ "@khanacademy/wonder-blocks-core": "^5.0.4",
32
33
  "@khanacademy/wonder-blocks-i18n": "^2.0.2",
34
+ "@khanacademy/wonder-blocks-spacing": "^4.0.1",
35
+ "@khanacademy/wonder-blocks-typography": "^2.0.8",
33
36
  "aphrodite": "^1.2.5",
34
37
  "classnames": "^1.1.4",
35
38
  "create-react-class": "^15.6.3",
@@ -45,7 +48,10 @@
45
48
  "@khanacademy/wonder-blocks-button": "^4.0.8",
46
49
  "@khanacademy/wonder-blocks-clickable": "^3.0.8",
47
50
  "@khanacademy/wonder-blocks-color": "^2.0.1",
51
+ "@khanacademy/wonder-blocks-core": "^5.0.4",
48
52
  "@khanacademy/wonder-blocks-i18n": "^2.0.2",
53
+ "@khanacademy/wonder-blocks-spacing": "^4.0.1",
54
+ "@khanacademy/wonder-blocks-typography": "^2.0.8",
49
55
  "aphrodite": "^1.2.5",
50
56
  "classnames": "^1.1.4",
51
57
  "create-react-class": "^15.6.3",
@@ -56,4 +62,4 @@
56
62
  "underscore": "^1.4.4"
57
63
  },
58
64
  "keywords": []
59
- }
65
+ }
@@ -1,17 +1,21 @@
1
- import {ApiOptions} from "@khanacademy/perseus";
1
+ /* eslint-disable react/prop-types */
2
+ import {ApiOptions, PerseusRenderer} from "@khanacademy/perseus";
3
+ import {View} from "@khanacademy/wonder-blocks-core";
4
+ import {action} from "@storybook/addon-actions";
2
5
  import * as React from "react";
3
6
 
7
+ import SideBySide from "../../../../testing/side-by-side";
4
8
  import {question1} from "../__testdata__/input-number.testdata";
5
9
  import Editor from "../editor";
6
10
  import {registerAllWidgetsAndEditorsForTesting} from "../util/register-all-widgets-and-editors-for-testing";
7
11
 
12
+ registerAllWidgetsAndEditorsForTesting(); // SIDE_EFFECTY!!!! :cry:
13
+
8
14
  export default {
9
15
  title: "Perseus/Editor",
10
16
  };
11
17
 
12
- export const Rational = (): React.ReactElement => {
13
- registerAllWidgetsAndEditorsForTesting();
14
-
18
+ export const Demo = (): React.ReactElement => {
15
19
  return (
16
20
  <Editor
17
21
  apiOptions={ApiOptions.defaults}
@@ -29,3 +33,100 @@ export const Rational = (): React.ReactElement => {
29
33
  />
30
34
  );
31
35
  };
36
+
37
+ export const DemoInteractiveGraph = (): React.ReactElement => {
38
+ const editorRef = React.useRef<Editor>(null);
39
+ const [options, setOptions] = React.useState({});
40
+ const [content, setContent] = React.useState(
41
+ "[[\u2603 interactive-graph 1]]",
42
+ );
43
+ const [images, setImages] = React.useState<PerseusRenderer["images"]>({});
44
+ const [widgets, setWidgets] = React.useState<PerseusRenderer["widgets"]>({
45
+ "interactive-graph 1": {
46
+ options: {
47
+ labels: ["x", "y"],
48
+ range: [
49
+ [-10, 10],
50
+ [-10, 10],
51
+ ],
52
+ gridStep: [1, 1],
53
+ snapStep: [1, 1],
54
+ step: [1, 1],
55
+ backgroundImage: {
56
+ url: null,
57
+ },
58
+ markings: "graph",
59
+ showProtractor: false,
60
+ showRuler: false,
61
+ showTooltips: false,
62
+ rulerLabel: "",
63
+ rulerTicks: 10,
64
+ graph: {
65
+ type: "linear",
66
+ coords: [
67
+ [1, 1],
68
+ [5, 5],
69
+ ],
70
+ },
71
+ correct: {
72
+ type: "linear",
73
+ },
74
+ },
75
+ type: "interactive-graph",
76
+ version: {
77
+ major: 0,
78
+ minor: 0,
79
+ },
80
+ },
81
+ });
82
+
83
+ return (
84
+ // Many of the editor components use scoped CSS that requires this
85
+ // class to be above it.
86
+ // TODO: Refactor to aphrodite styles instead of scoped CSS in Less.
87
+ <div className="framework-perseus">
88
+ <SideBySide
89
+ leftTitle="Editor"
90
+ left={
91
+ <View style={{width: "360px", margin: "20px"}}>
92
+ <Editor
93
+ ref={editorRef}
94
+ apiOptions={ApiOptions.defaults}
95
+ content={content}
96
+ placeholder=""
97
+ widgets={widgets}
98
+ images={images}
99
+ disabled={false}
100
+ widgetEnabled={true}
101
+ immutableWidgets={false}
102
+ showWordCount={true}
103
+ warnNoPrompt={false}
104
+ warnNoWidgets={true}
105
+ onChange={(props: Partial<PerseusRenderer>) => {
106
+ action("onChange")(props);
107
+ if (props.content) {
108
+ setContent(props.content);
109
+ } else if (props.widgets) {
110
+ setWidgets(props.widgets);
111
+ } else if (props.images) {
112
+ setImages(props.images);
113
+ }
114
+ // We need to wait for one tick so that the editor
115
+ // has been re-rendered with the changed props. If
116
+ // we don't wait, we get the values from the n-1
117
+ // render and miss the latest change.
118
+ setTimeout(() => {
119
+ setOptions(
120
+ editorRef.current?.serialize() || {},
121
+ );
122
+ }, 0);
123
+ }}
124
+ />
125
+ </View>
126
+ }
127
+ rightTitle="Serialized Widget Options"
128
+ jsonObject={options}
129
+ />
130
+ </div>
131
+ );
132
+ };
@@ -17,9 +17,9 @@ import _ from "underscore";
17
17
 
18
18
  import DeviceFramer from "./components/device-framer";
19
19
  import JsonEditor from "./components/json-editor";
20
+ import SectionControlButton from "./components/section-control-button";
20
21
  import Editor from "./editor";
21
22
  import IframeContentRenderer from "./iframe-content-renderer";
22
- import SectionControlButton from "./section-control-button";
23
23
 
24
24
  import type {APIOptions} from "@khanacademy/perseus";
25
25
 
@@ -10,7 +10,7 @@ type Story = {
10
10
  };
11
11
 
12
12
  export default {
13
- title: "Perseus Editor/Widgets/Section Control Button",
13
+ title: "Perseus/Editor/Components/Section Control Button",
14
14
  } as Story;
15
15
 
16
16
  export const ButtonForEditingSectionsOfContentWithInArticleEditor = (
@@ -0,0 +1,29 @@
1
+ import * as React from "react";
2
+
3
+ import {PointValue, UNLIMITED, parsePointCount} from "../util/points";
4
+
5
+ const GraphPointsCountSelector = ({
6
+ numPoints = 1,
7
+ onChange,
8
+ }: {
9
+ numPoints?: PointValue;
10
+ onChange: (points: PointValue) => void;
11
+ }) => {
12
+ return (
13
+ <select
14
+ value={numPoints}
15
+ onChange={(e) => {
16
+ onChange(parsePointCount(e.target.value));
17
+ }}
18
+ >
19
+ {[...Array(7).keys()].map((n) => (
20
+ <option key={n} value={n}>
21
+ {`${n} point${n > 1 ? "s" : ""}`}
22
+ </option>
23
+ ))}
24
+ <option value={UNLIMITED}>unlimited</option>
25
+ </select>
26
+ );
27
+ };
28
+
29
+ export default GraphPointsCountSelector;
@@ -0,0 +1,31 @@
1
+ import * as React from "react";
2
+
3
+ type GraphTypeSelectorProps = {
4
+ graphType: string;
5
+ onChange: (newGraphType: string) => void;
6
+ };
7
+
8
+ const GraphTypeSelector = (props: GraphTypeSelectorProps) => {
9
+ return (
10
+ <select
11
+ value={props.graphType}
12
+ onChange={(e) => {
13
+ const type = e.target.value;
14
+ props.onChange(type);
15
+ }}
16
+ >
17
+ <option value="linear">Linear function</option>
18
+ <option value="quadratic">Quadratic function</option>
19
+ <option value="sinusoid">Sinusoid function</option>
20
+ <option value="circle">Circle</option>
21
+ <option value="point">Point(s)</option>
22
+ <option value="linear-system">Linear System</option>
23
+ <option value="polygon">Polygon</option>
24
+ <option value="segment">Line Segment(s)</option>
25
+ <option value="ray">Ray</option>
26
+ <option value="angle">Angle</option>
27
+ </select>
28
+ );
29
+ };
30
+
31
+ export default GraphTypeSelector;
@@ -1,4 +1,3 @@
1
- /* eslint-disable react/prop-types */
2
1
  /* eslint-disable @babel/no-invalid-this */
3
2
  /* eslint-disable react/no-unsafe */
4
3
  /* eslint-disable react/sort-comp */
@@ -0,0 +1,27 @@
1
+ import * as React from "react";
2
+ import _ from "underscore";
3
+
4
+ const SegmentCountSelector = ({
5
+ numSegments = 1,
6
+ onChange,
7
+ }: {
8
+ numSegments?: number;
9
+ onChange: (numSegments: number) => void;
10
+ }) => (
11
+ <select
12
+ key="segment-select"
13
+ value={numSegments}
14
+ onChange={(e) => {
15
+ const num = +e.target.value;
16
+ onChange(num);
17
+ }}
18
+ >
19
+ {_.range(1, 7).map((n) => (
20
+ <option key={n} value={n}>
21
+ {`${n} segment${n > 1 ? "s" : ""}`}
22
+ </option>
23
+ ))}
24
+ </select>
25
+ );
26
+
27
+ export default SegmentCountSelector;
@@ -0,0 +1,232 @@
1
+ import {
2
+ components,
3
+ icons,
4
+ Widgets,
5
+ WIDGET_PROP_DENYLIST,
6
+ } from "@khanacademy/perseus";
7
+ import * as React from "react";
8
+ import _ from "underscore";
9
+
10
+ import SectionControlButton from "./section-control-button";
11
+
12
+ import type {Alignment, WidgetInfo} from "@khanacademy/perseus";
13
+
14
+ const {InlineIcon, PropCheckBox} = components;
15
+ const {iconChevronDown, iconChevronRight, iconTrash} = icons;
16
+
17
+ type WidgetEditorProps = {
18
+ // Unserialized props
19
+ id: string;
20
+ onChange: (
21
+ widgetInfo: WidgetInfo,
22
+ cb?: () => unknown,
23
+ silent?: boolean,
24
+ ) => unknown;
25
+ onRemove: () => unknown;
26
+ apiOptions: any;
27
+ } & WidgetInfo;
28
+
29
+ type WidgetEditorState = {
30
+ showWidget: boolean;
31
+ widgetInfo: WidgetInfo;
32
+ };
33
+
34
+ const _upgradeWidgetInfo = (props: WidgetEditorProps): React.ReactElement => {
35
+ // We can't call serialize here because this.refs.widget
36
+ // doesn't exist before this component is mounted.
37
+ const filteredProps = _.omit(props, WIDGET_PROP_DENYLIST);
38
+ // @ts-expect-error [FEI-5003] - TS2322 - Type 'PerseusWidget' is not assignable to type 'ReactElement<any, string | JSXElementConstructor<any>>'. | TS2345 - Argument of type 'Partial<{ id: string; onChange: (widgetInfo: PerseusWidget, cb?: (() => unknown) | undefined, silent?: boolean | undefined) => unknown; onRemove: () => unknown; apiOptions: any; } & CategorizerWidget & { ...; }> | ... 38 more ... | Partial<...>' is not assignable to parameter of type 'PerseusWidget'.
39
+ return Widgets.upgradeWidgetInfoToLatestVersion(filteredProps);
40
+ };
41
+
42
+ // This component handles upgading widget editor props via prop
43
+ // upgrade transforms. Widget editors will always be rendered
44
+ // with all available transforms applied, but the results of those
45
+ // transforms will not be propogated upwards until serialization.
46
+ // eslint-disable-next-line react/no-unsafe
47
+ class WidgetEditor extends React.Component<
48
+ WidgetEditorProps,
49
+ WidgetEditorState
50
+ > {
51
+ constructor(props: WidgetEditorProps) {
52
+ super(props);
53
+ this.state = {
54
+ showWidget: false,
55
+ // @ts-expect-error [FEI-5003] - TS2322 - Type 'ReactElement<any, any> | null' is not assignable to type 'PerseusWidget'.
56
+ widgetInfo: _upgradeWidgetInfo(props),
57
+ };
58
+ }
59
+
60
+ UNSAFE_componentWillReceiveProps(nextProps: WidgetEditorProps) {
61
+ // @ts-expect-error [FEI-5003] - TS2322 - Type 'ReactElement<any, any> | null' is not assignable to type 'PerseusWidget'.
62
+ this.setState({widgetInfo: _upgradeWidgetInfo(nextProps)});
63
+ }
64
+
65
+ _toggleWidget = (e: React.SyntheticEvent) => {
66
+ e.preventDefault();
67
+ this.setState({showWidget: !this.state.showWidget});
68
+ };
69
+
70
+ _handleWidgetChange = (
71
+ newProps: WidgetEditorProps,
72
+ cb: () => unknown,
73
+ silent: boolean,
74
+ ) => {
75
+ const newWidgetInfo = Object.assign(
76
+ {},
77
+ this.state.widgetInfo,
78
+ ) as WidgetInfo;
79
+ newWidgetInfo.options = Object.assign(
80
+ // eslint-disable-next-line react/no-string-refs
81
+ // @ts-expect-error [FEI-5003] - TS2339 - Property 'serialize' does not exist on type 'ReactInstance'.
82
+ this.refs.widget.serialize(),
83
+ newProps,
84
+ );
85
+ this.props.onChange(newWidgetInfo, cb, silent);
86
+ };
87
+
88
+ _toggleStatic = (e: Event) => {
89
+ e.preventDefault();
90
+ const newWidgetInfo = Object.assign({}, this.state.widgetInfo, {
91
+ static: !this.state.widgetInfo.static,
92
+ }) as WidgetInfo;
93
+ this.props.onChange(newWidgetInfo);
94
+ };
95
+
96
+ _handleAlignmentChange = (e: React.SyntheticEvent<HTMLSelectElement>) => {
97
+ const newAlignment = e.currentTarget.value as Alignment;
98
+ const newWidgetInfo = Object.assign(
99
+ {},
100
+ this.state.widgetInfo,
101
+ ) as WidgetInfo;
102
+ newWidgetInfo.alignment = newAlignment;
103
+ this.props.onChange(newWidgetInfo);
104
+ };
105
+
106
+ getSaveWarnings = () => {
107
+ // eslint-disable-next-line react/no-string-refs
108
+ // @ts-expect-error [FEI-5003] - TS2339 - Property 'getSaveWarnings' does not exist on type 'ReactInstance'.
109
+ const issuesFunc = this.refs.widget.getSaveWarnings;
110
+ return issuesFunc ? issuesFunc() : [];
111
+ };
112
+
113
+ serialize = () => {
114
+ // TODO(alex): Make this properly handle the case where we load json
115
+ // with a more recent widget version than this instance of Perseus
116
+ // knows how to handle.
117
+ const widgetInfo = this.state.widgetInfo;
118
+ return {
119
+ type: widgetInfo.type,
120
+ alignment: widgetInfo.alignment,
121
+ static: widgetInfo.static,
122
+ graded: widgetInfo.graded,
123
+ // eslint-disable-next-line react/no-string-refs
124
+ // @ts-expect-error [FEI-5003] - TS2339 - Property 'serialize' does not exist on type 'ReactInstance'.
125
+ options: this.refs.widget.serialize(),
126
+ version: widgetInfo.version,
127
+ };
128
+ };
129
+
130
+ render(): React.ReactNode {
131
+ const widgetInfo = this.state.widgetInfo;
132
+
133
+ const Ed = Widgets.getEditor(widgetInfo.type);
134
+ let supportedAlignments: ReadonlyArray<Alignment>;
135
+ if (this.props.apiOptions.showAlignmentOptions) {
136
+ supportedAlignments = Widgets.getSupportedAlignments(
137
+ widgetInfo.type,
138
+ );
139
+ } else {
140
+ // NOTE(kevinb): "default" is not one in `validAlignments` in widgets.js.
141
+ supportedAlignments = ["default"];
142
+ }
143
+
144
+ const supportsStaticMode = Widgets.supportsStaticMode(widgetInfo.type);
145
+
146
+ const isUngradedEnabled = widgetInfo.type === "transformer";
147
+ const gradedPropBox = (
148
+ <PropCheckBox
149
+ label="Graded:"
150
+ graded={widgetInfo.graded}
151
+ onChange={this.props.onChange}
152
+ />
153
+ );
154
+
155
+ return (
156
+ <div className="perseus-widget-editor">
157
+ <div
158
+ className={
159
+ "perseus-widget-editor-title " +
160
+ (this.state.showWidget ? "open" : "closed")
161
+ }
162
+ >
163
+ {/* eslint-disable-next-line jsx-a11y/anchor-is-valid */}
164
+ <a
165
+ className="perseus-widget-editor-title-id"
166
+ href="#"
167
+ onClick={this._toggleWidget}
168
+ >
169
+ {this.props.id}
170
+ {this.state.showWidget ? (
171
+ <InlineIcon {...iconChevronDown} />
172
+ ) : (
173
+ <InlineIcon {...iconChevronRight} />
174
+ )}
175
+ </a>
176
+
177
+ {supportsStaticMode && (
178
+ <input
179
+ type="button"
180
+ // @ts-expect-error [FEI-5003] - TS2322 - Type '(e: Event) => void' is not assignable to type 'MouseEventHandler<HTMLInputElement>'.
181
+ onClick={this._toggleStatic}
182
+ className="simple-button--small"
183
+ value={
184
+ widgetInfo.static
185
+ ? "Unset as static"
186
+ : "Set as static"
187
+ }
188
+ />
189
+ )}
190
+ {supportedAlignments.length > 1 && (
191
+ <select
192
+ className="alignment"
193
+ value={widgetInfo.alignment}
194
+ onChange={this._handleAlignmentChange}
195
+ >
196
+ {supportedAlignments.map((alignment) => (
197
+ <option key={alignment}>{alignment}</option>
198
+ ))}
199
+ </select>
200
+ )}
201
+ <SectionControlButton
202
+ icon={iconTrash}
203
+ onClick={() => {
204
+ this.props.onRemove();
205
+ }}
206
+ title="Remove image widget"
207
+ />
208
+ </div>
209
+ <div
210
+ className={
211
+ "perseus-widget-editor-content " +
212
+ (this.state.showWidget ? "enter" : "leave")
213
+ }
214
+ >
215
+ {isUngradedEnabled && gradedPropBox}
216
+ {Ed && (
217
+ <Ed
218
+ // eslint-disable-next-line react/no-string-refs
219
+ ref="widget"
220
+ onChange={this._handleWidgetChange}
221
+ static={widgetInfo.static}
222
+ apiOptions={this.props.apiOptions}
223
+ {...widgetInfo.options}
224
+ />
225
+ )}
226
+ </div>
227
+ </div>
228
+ );
229
+ }
230
+ }
231
+
232
+ export default WidgetEditor;
@@ -0,0 +1,48 @@
1
+ import {Widgets} from "@khanacademy/perseus";
2
+ import * as React from "react";
3
+ import _ from "underscore";
4
+
5
+ type WidgetSelectProps = {
6
+ onChange?: (widgetType: string) => unknown;
7
+ };
8
+
9
+ class WidgetSelect extends React.Component<WidgetSelectProps> {
10
+ shouldComponentUpdate() {
11
+ return false;
12
+ }
13
+
14
+ handleChange = (e: React.SyntheticEvent<HTMLSelectElement>) => {
15
+ const widgetType = e.currentTarget.value;
16
+ if (widgetType === "") {
17
+ // TODO(alpert): Not sure if change will trigger here
18
+ // but might as well be safe
19
+ return;
20
+ }
21
+ if (this.props.onChange) {
22
+ this.props.onChange(widgetType);
23
+ }
24
+ };
25
+
26
+ render(): React.ReactNode {
27
+ const widgets = Widgets.getPublicWidgets();
28
+ const orderedWidgetNames = _.sortBy(_.keys(widgets), (name) => {
29
+ return widgets[name].displayName;
30
+ });
31
+ const addWidgetString = "Add a widget\u2026";
32
+ return (
33
+ <select value="" onChange={this.handleChange}>
34
+ <option value="">{addWidgetString}</option>
35
+ <option disabled>--</option>
36
+ {_.map(orderedWidgetNames, (name) => {
37
+ return (
38
+ <option key={name} value={name}>
39
+ {widgets[name].displayName}
40
+ </option>
41
+ );
42
+ })}
43
+ </select>
44
+ );
45
+ }
46
+ }
47
+
48
+ export default WidgetSelect;