Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions packages/perseus-editor/src/__tests__/issues-panel.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,9 @@ import {getFeatureFlags} from "../../../../testing/feature-flags-util";
import IssuesPanel from "../components/issues-panel";

import type {IssueImpact} from "../components/issues-panel";
import type {APIOptions} from "@khanacademy/perseus";
import type {APIOptionsWithDefaults} from "@khanacademy/perseus";

const imageUpdateFFOptions: APIOptions = {
const imageUpdateFFOptions: APIOptionsWithDefaults = {
...ApiOptions.defaults,
flags: getFeatureFlags({
"image-widget-upgrade": true,
Expand Down
206 changes: 112 additions & 94 deletions packages/perseus-editor/src/components/widget-editor.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,9 @@
/* eslint-disable @khanacademy/ts-no-error-suppressions */
import {Widgets, excludeDenylistKeys} from "@khanacademy/perseus";
import {
APIOptionsContext,
Widgets,
excludeDenylistKeys,
} from "@khanacademy/perseus";
import {
CoreWidgetRegistry,
applyDefaultsToWidget,
Expand All @@ -17,9 +21,10 @@ import SectionControlButton from "./section-control-button";
import ToggleableCaret from "./toggleable-caret";

import type Editor from "../editor";
import type {APIOptions} from "@khanacademy/perseus";
import type {Alignment, PerseusWidget} from "@khanacademy/perseus-core";

type EditorType = React.ElementRef<typeof Editor>;

type WidgetEditorProps = {
// Unserialized props
id: string;
Expand All @@ -29,7 +34,6 @@ type WidgetEditorProps = {
silent?: boolean,
) => unknown;
onRemove: () => unknown;
apiOptions: APIOptions;
widgetIsOpen?: boolean;
} & Omit<PerseusWidget, "key">;

Expand All @@ -54,7 +58,7 @@ class WidgetEditor extends React.Component<
WidgetEditorProps,
WidgetEditorState
> {
widget: React.RefObject<React.ElementRef<typeof Editor>>;
widget: React.RefObject<EditorType>;
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We could just move the initialization up here (which I feel is pretty common in class components.

Suggested change
widget: React.RefObject<EditorType>;
widget = React.createRef<EditorType>;


constructor(props: WidgetEditorProps) {
super(props);
Expand Down Expand Up @@ -135,106 +139,120 @@ class WidgetEditor extends React.Component<
alignment: widgetInfo.alignment,
static: widgetInfo.static,
graded: widgetInfo.graded,
// eslint-disable-next-line react/no-string-refs
options: this.widget.current?.serialize(),
options: this.widget.current?.serialize() ?? widgetInfo.options,
version: widgetInfo.version,
};
};

render(): React.ReactNode {
const widgetInfo = this.state.widgetInfo;
const isEditingDisabled =
this.props.apiOptions.editingDisabled ?? false;
return (
<APIOptionsContext.Consumer>
{(apiOptions) => {
const widgetInfo = this.state.widgetInfo;
const isEditingDisabled =
apiOptions.editingDisabled ?? false;

const Ed = Widgets.getEditor(widgetInfo.type);
let supportedAlignments: ReadonlyArray<Alignment>;
const imageUpgradeFF = isFeatureOn(this.props, "image-widget-upgrade");
const Ed = Widgets.getEditor(widgetInfo.type);
let supportedAlignments: ReadonlyArray<Alignment>;
const imageUpgradeFF = isFeatureOn(
{apiOptions},
"image-widget-upgrade",
);

if (widgetInfo.type === "image" && !imageUpgradeFF) {
// TODO(LEMS-3520): Feature flag cleanup
supportedAlignments = ["block", "full-width"];
} else if (this.props.apiOptions.showAlignmentOptions) {
supportedAlignments = CoreWidgetRegistry.getSupportedAlignments(
widgetInfo.type,
);
} else {
// NOTE(kevinb): "default" is not one in `validAlignments` in widgets.js.
supportedAlignments = ["default"];
}
if (widgetInfo.type === "image" && !imageUpgradeFF) {
// TODO(LEMS-3520): Feature flag cleanup
supportedAlignments = ["block", "full-width"];
} else if (apiOptions.showAlignmentOptions) {
supportedAlignments =
CoreWidgetRegistry.getSupportedAlignments(
widgetInfo.type,
);
} else {
// NOTE(kevinb): "default" is not one in `validAlignments` in widgets.js.
supportedAlignments = ["default"];
}

const supportsStaticMode = Widgets.supportsStaticMode(widgetInfo.type);
const supportsStaticMode = Widgets.supportsStaticMode(
widgetInfo.type,
);

return (
<div className="perseus-widget-editor">
<div
className={
"perseus-widget-editor-title " +
(this.state.showWidget ? "open" : "closed")
}
>
<div className="perseus-widget-editor-title-id">
<View
style={{
display: "flex",
flexDirection: "row",
alignItems: "center",
gap: "0.25em",
}}
onClick={this._toggleWidget}
>
<ToggleableCaret
isExpanded={this.state.showWidget}
/>
<span>{this.props.id}</span>
</View>
</div>
return (
<div className="perseus-widget-editor">
<div
className={
"perseus-widget-editor-title " +
(this.state.showWidget ? "open" : "closed")
}
>
<div className="perseus-widget-editor-title-id">
<View
style={{
display: "flex",
flexDirection: "row",
alignItems: "center",
gap: "0.25em",
}}
onClick={this._toggleWidget}
>
<ToggleableCaret
isExpanded={this.state.showWidget}
/>
<span>{this.props.id}</span>
</View>
</div>

{supportsStaticMode && (
<LabeledSwitch
label="Static"
checked={!!widgetInfo.static}
disabled={isEditingDisabled}
onChange={this._setStatic}
/>
)}
{supportedAlignments.length > 1 && (
<select
className="alignment"
value={widgetInfo.alignment}
disabled={isEditingDisabled}
onChange={this._handleAlignmentChange}
>
{supportedAlignments.map((alignment) => (
<option key={alignment}>{alignment}</option>
))}
</select>
)}
<SectionControlButton
icon={trashIcon}
disabled={isEditingDisabled}
onClick={() => {
this.props.onRemove();
}}
title="Remove image widget"
/>
</div>
<div
className={
"perseus-widget-editor-content " +
(this.state.showWidget ? "enter" : "leave")
}
>
{Ed && (
<Ed
ref={this.widget}
onChange={this._handleWidgetChange}
static={widgetInfo.static}
apiOptions={this.props.apiOptions}
{...widgetInfo.options}
/>
)}
</div>
</div>
{supportsStaticMode && (
<LabeledSwitch
label="Static"
checked={!!widgetInfo.static}
disabled={isEditingDisabled}
onChange={this._setStatic}
/>
)}
{supportedAlignments.length > 1 && (
<select
className="alignment"
value={widgetInfo.alignment}
disabled={isEditingDisabled}
onChange={this._handleAlignmentChange}
>
{supportedAlignments.map(
(alignment) => (
<option key={alignment}>
{alignment}
</option>
),
)}
</select>
)}
<SectionControlButton
icon={trashIcon}
disabled={isEditingDisabled}
onClick={() => {
this.props.onRemove();
}}
title="Remove image widget"
/>
</div>
<div
className={
"perseus-widget-editor-content " +
(this.state.showWidget ? "enter" : "leave")
}
>
{Ed && (
<Ed
ref={this.widget}
onChange={this._handleWidgetChange}
static={widgetInfo.static}
{...widgetInfo.options}
/>
)}
</div>
</div>
);
}}
</APIOptionsContext.Consumer>
);
}
}
Expand Down
4 changes: 2 additions & 2 deletions packages/perseus-editor/src/editor.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -163,7 +163,7 @@ type State = {
};

// eslint-disable-next-line react/no-unsafe
class EditorInner extends React.Component<Props, State> {
class EditorClass extends React.Component<Props, State> {
lastUserValue: string | null | undefined;
deferredChange: any | null | undefined;
widgetIds: any | null | undefined;
Expand Down Expand Up @@ -1137,5 +1137,5 @@ class EditorInner extends React.Component<Props, State> {
}
}

const Editor = withAPIOptions<EditorInner>(EditorInner);
const Editor = withAPIOptions<EditorClass>(EditorClass);
export default Editor;
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
import {ApiOptions} from "@khanacademy/perseus";
import * as React from "react";
import {action} from "storybook/actions";

Expand All @@ -23,7 +22,6 @@ type Story = StoryObj<typeof meta>;
export const Default: Story = {
args: {
onChange: action("onChange"),
apiOptions: ApiOptions.defaults,
},
};

Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
import {ApiOptions} from "@khanacademy/perseus";
import {action} from "storybook/actions";

import PlotterEditor from "../plotter-editor";
Expand All @@ -16,7 +15,6 @@ type Story = StoryObj<typeof meta>;
export const Default: Story = {
args: {
onChange: action("onChange"),
apiOptions: ApiOptions.defaults,
categories: ["0", "1", "2"],
plotDimensions: [300, 300],
correct: [0, 1, 2],
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,6 @@ type Story = StoryObj<typeof meta>;
export const Default: Story = {
args: {
onChange: action("onChange"),
apiOptions: Object.freeze({}),
static: false,
},
};
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import {ApiOptions, Dependencies} from "@khanacademy/perseus";
import {Dependencies} from "@khanacademy/perseus";
import {render, screen} from "@testing-library/react";
import {userEvent as userEventLib} from "@testing-library/user-event";
import * as React from "react";
Expand All @@ -21,12 +21,7 @@ describe("categorizer-editor", () => {
});

it("should render", async () => {
render(
<CategorizerEditor
onChange={() => undefined}
apiOptions={ApiOptions.defaults}
/>,
);
render(<CategorizerEditor onChange={() => undefined} />);

expect(
await screen.findByText("Randomize item order"),
Expand All @@ -36,12 +31,7 @@ describe("categorizer-editor", () => {
it("should be possible to change randomize item order", async () => {
const onChangeMock = jest.fn();

render(
<CategorizerEditor
onChange={onChangeMock}
apiOptions={ApiOptions.defaults}
/>,
);
render(<CategorizerEditor onChange={onChangeMock} />);

await userEvent.click(
screen.getByRole("checkbox", {
Expand Down
Loading
Loading