-
Notifications
You must be signed in to change notification settings - Fork 360
Remove score prop from ServerItemRenderer
#3101
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
b31406a
2f3b178
a2166c2
c69d95a
5822e30
bed332a
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,5 @@ | ||
| --- | ||
| "@khanacademy/perseus": major | ||
| --- | ||
|
|
||
| Remove `score` prop from ServerItemRenderer |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -123,14 +123,12 @@ type Props = Partial<React.ContextType<typeof DependenciesContext>> & { | |
| apiOptions?: APIOptions; | ||
| alwaysUpdate?: boolean; | ||
| findExternalWidgets: any; | ||
| highlightedWidgets?: ReadonlyArray<any>; | ||
| images: PerseusRenderer["images"]; | ||
| keypadElement?: KeypadAPI | null; | ||
| onInteractWithWidget: (id: string) => void; | ||
| onRender: (node?: any) => void; | ||
| problemNum?: number; | ||
| questionCompleted?: boolean; | ||
| reviewMode?: boolean | null | undefined; | ||
| highlightEmptyWidgets?: boolean; | ||
| /** | ||
| * If set to "all", all rationales or solutions will be shown. If set to | ||
| * "selected", soltions will only be shown for selected choices. If set to | ||
|
|
@@ -173,12 +171,9 @@ type DefaultProps = Required< | |
| | "alwaysUpdate" | ||
| | "content" | ||
| | "findExternalWidgets" | ||
| | "highlightedWidgets" | ||
| | "images" | ||
| | "linterContext" | ||
| | "onInteractWithWidget" | ||
| | "onRender" | ||
| | "questionCompleted" | ||
| | "showSolutions" | ||
| | "reviewMode" | ||
| | "widgets" | ||
|
|
@@ -253,14 +248,11 @@ class Renderer | |
| content: "", | ||
| widgets: {}, | ||
| images: {}, | ||
| highlightedWidgets: [], | ||
| questionCompleted: false, | ||
| showSolutions: "none", | ||
| // onRender may be called multiple times per render, for example | ||
| // if there are multiple images or TeX pieces within `content`. | ||
| // It is a good idea to debounce any functions passed here. | ||
| onRender: noopOnRender, | ||
| onInteractWithWidget: function () {}, | ||
| findExternalWidgets: () => [], | ||
| alwaysUpdate: false, | ||
| reviewMode: false, | ||
|
|
@@ -304,6 +296,8 @@ class Renderer | |
| this.handletranslationLintErrors, | ||
| ); | ||
| } | ||
|
|
||
| this.props.apiOptions?.answerableCallback?.(this._isAnswerable()); | ||
| } | ||
|
|
||
| UNSAFE_componentWillReceiveProps(nextProps: Props) { | ||
|
|
@@ -358,6 +352,13 @@ class Renderer | |
| this.handletranslationLintErrors, | ||
| ); | ||
| } | ||
|
|
||
| if ( | ||
| this.props.userInput && | ||
| !_.isEqual(this.props.userInput, prevProps.userInput) | ||
| ) { | ||
| this.props.apiOptions?.answerableCallback?.(this._isAnswerable()); | ||
| } | ||
| } | ||
|
|
||
| componentWillUnmount() { | ||
|
|
@@ -431,6 +432,13 @@ class Renderer | |
| ); | ||
| }; | ||
|
|
||
| _isAnswerable(): boolean { | ||
| if (this.props.userInput) { | ||
| return this.emptyWidgets().length === 0; | ||
| } | ||
| return false; | ||
| } | ||
|
|
||
| renderWidget: ( | ||
| impliedType: string, | ||
| id: string, | ||
|
|
@@ -448,11 +456,11 @@ class Renderer | |
|
|
||
| if (widgetInfo) { | ||
| const type = (widgetInfo && widgetInfo.type) || impliedType; | ||
| const shouldHighlight = _.contains( | ||
| // @ts-expect-error - TS2345 - Argument of type 'readonly any[] | undefined' is not assignable to parameter of type 'Collection<any>'. | ||
| this.props.highlightedWidgets, | ||
| id, | ||
| ); | ||
|
|
||
| let shouldHighlight = false; | ||
| if (this.props.highlightEmptyWidgets && this.props.userInput) { | ||
| shouldHighlight = this.emptyWidgets().includes(id); | ||
| } | ||
|
|
||
| // By this point we should have no duplicates, which are | ||
| // filtered out in this.render(), so we shouldn't have to | ||
|
|
@@ -537,7 +545,6 @@ class Renderer | |
| problemNum: this.props.problemNum, | ||
| apiOptions: this.getApiOptions(), | ||
| keypadElement: this.props.keypadElement, | ||
| questionCompleted: this.props.questionCompleted, | ||
| showSolutions: this.props.showSolutions, | ||
| onFocus: _.partial(this._onWidgetFocus, widgetId), | ||
| onBlur: _.partial(this._onWidgetBlur, widgetId), | ||
|
|
@@ -561,7 +568,7 @@ class Renderer | |
| newUserInput, | ||
| widgetsEmpty, | ||
| ); | ||
| this.props.onInteractWithWidget(widgetId); | ||
| this.props.apiOptions?.interactionCallback?.(updatedUserInput); | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🤔 I wonder... with the |
||
| }, | ||
| trackInteraction: interactionTracker.track, | ||
| }; | ||
|
|
@@ -1442,35 +1449,6 @@ class Renderer | |
| ); | ||
| } | ||
|
|
||
| handleStateUpdate(id: string, cb: () => boolean, silent?: boolean) { | ||
handeyeco marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| // Wait until all components have rendered. In React 16 setState | ||
| // callback fires immediately after this componentDidUpdate, and | ||
| // there is no guarantee that parent/siblings components have | ||
| // finished rendering. | ||
| // TODO(jeff, CP-3128): Use Wonder Blocks Timing API | ||
| // eslint-disable-next-line no-restricted-syntax | ||
| setTimeout(() => { | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. These setTimeout()s throughout Perseus made/make testing annoying. Thanks for removing this unused function! |
||
| // eslint-disable-next-line @typescript-eslint/strict-boolean-expressions | ||
| const cbResult = cb && cb(); | ||
| if (!silent) { | ||
| this.props.onInteractWithWidget(id); | ||
| } | ||
| if (cbResult !== false) { | ||
| // TODO(jack): For some reason, some widgets don't always | ||
| // end up in refs here, which is repro-able if you make an | ||
| // [[ orderer 1 ]] and copy-paste this, then change it to | ||
| // be an [[ orderer 2 ]]. The resulting Renderer ends up | ||
| // with an "orderer 2" ref but not an "orderer 1" ref. | ||
| // @_@?? | ||
| // TODO(jack): Figure out why this is happening and fix it | ||
| // As far as I can tell, this is only an issue in the | ||
| // editor-page, so doing this shouldn't break clients | ||
| // hopefully | ||
| this._setCurrentFocus([id]); | ||
| } | ||
| }, 0); | ||
| } | ||
|
|
||
| /** | ||
| * Returns an array of the widget `.getUserInput()` results | ||
| * | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -37,7 +37,6 @@ import type { | |
| ShowSolutions, | ||
| KeypadContextRendererInterface, | ||
| RendererInterface, | ||
| KEScore, | ||
| UserInputArray, | ||
| UserInputMap, | ||
| } from "@khanacademy/perseus-core"; | ||
|
|
@@ -46,9 +45,9 @@ import type {PropsFor} from "@khanacademy/wonder-blocks-core"; | |
| type OwnProps = { | ||
| hintsVisible?: number; | ||
| item: PerseusItem; | ||
| score?: KEScore | null; | ||
| problemNum?: number; | ||
| reviewMode?: boolean; | ||
| highlightEmptyWidgets?: boolean; | ||
| keypadElement?: KeypadAPI | null | undefined; | ||
| dependencies: PerseusDependenciesV2; | ||
| showSolutions?: ShowSolutions; | ||
|
|
@@ -65,19 +64,6 @@ type DefaultProps = Required< | |
| >; | ||
|
|
||
| type State = { | ||
| /** | ||
| * questionCompleted is used to signal that a learner has attempted | ||
| * the exercise. This is used when widgets want to show things like | ||
| * rationale or partial correctness. | ||
| */ | ||
| questionCompleted: boolean; | ||
| /** | ||
| * As far as I can tell, this is used to highlight empty widgets | ||
| * after a learner has clicked the "check" button. I don't think this could | ||
| * still be used though, because the "check" button is disabled while there | ||
| * are empty widgets. | ||
| */ | ||
| questionHighlightedWidgets: ReadonlyArray<string>; | ||
| /** | ||
| * Keeps track of whether each asset (SvgImage or TeX) rendered by | ||
| * the questionRenderer has finished loading or rendering. | ||
|
|
@@ -124,8 +110,6 @@ export class ServerItemRenderer | |
| super(props); | ||
|
|
||
| this.state = { | ||
| questionCompleted: false, | ||
| questionHighlightedWidgets: [], | ||
| assetStatuses: {}, | ||
| }; | ||
| this._fullyRendered = false; | ||
|
|
@@ -145,32 +129,8 @@ export class ServerItemRenderer | |
| this.maybeCallOnRendered(); | ||
| } | ||
|
|
||
| // eslint-disable-next-line react/no-unsafe | ||
| UNSAFE_componentWillReceiveProps(nextProps: Props) { | ||
| this.setState({ | ||
| questionHighlightedWidgets: [], | ||
| }); | ||
| } | ||
|
|
||
| componentDidUpdate(prevProps: Props, prevState: State) { | ||
| const answerableCallback = this.props.apiOptions.answerableCallback; | ||
| if (answerableCallback != null) { | ||
| const isAnswerable = | ||
| this.questionRenderer.emptyWidgets().length === 0; | ||
| answerableCallback(isAnswerable); | ||
| } | ||
|
|
||
| componentDidUpdate() { | ||
| this.maybeCallOnRendered(); | ||
|
|
||
| if (this.props.score && this.props.score !== prevProps.score) { | ||
| const emptyQuestionAreaWidgets = | ||
| this.questionRenderer.emptyWidgets(); | ||
|
|
||
| this.setState({ | ||
| questionCompleted: this.props.score.correct, | ||
| questionHighlightedWidgets: emptyQuestionAreaWidgets, | ||
| }); | ||
| } | ||
|
Comment on lines
-165
to
-173
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I wanted to get rid of this because I don't think we really need |
||
| } | ||
|
|
||
| componentWillUnmount() { | ||
|
|
@@ -304,28 +264,6 @@ export class ServerItemRenderer | |
| return questionAreaInputPaths; | ||
| } | ||
|
|
||
| handleInteractWithWidget: (widgetId: string) => void = (widgetId) => { | ||
| const withRemoved = _.difference( | ||
| this.state.questionHighlightedWidgets, | ||
| [widgetId], | ||
| ); | ||
|
|
||
| this.setState( | ||
| { | ||
| questionCompleted: false, | ||
| questionHighlightedWidgets: withRemoved, | ||
| }, | ||
| () => { | ||
| // Call the interactionCallback, if it exists, | ||
| // with the current user input data | ||
| // (in the setState callback to avoid stale state) | ||
| this.props.apiOptions?.interactionCallback?.( | ||
| this.questionRenderer.getUserInputMap(), | ||
| ); | ||
| }, | ||
| ); | ||
| }; | ||
|
|
||
| focus(): boolean | null | undefined { | ||
| return this.questionRenderer.focus(); | ||
| } | ||
|
|
@@ -422,14 +360,7 @@ export class ServerItemRenderer | |
| <Renderer | ||
| keypadElement={this.props.keypadElement} | ||
| problemNum={this.props.problemNum} | ||
| onInteractWithWidget={ | ||
| this.handleInteractWithWidget | ||
| } | ||
| highlightedWidgets={ | ||
| this.state.questionHighlightedWidgets | ||
| } | ||
| apiOptions={apiOptions} | ||
| questionCompleted={this.state.questionCompleted} | ||
| reviewMode={this.props.reviewMode} | ||
| showSolutions={this.props.showSolutions} | ||
| ref={(elem) => { | ||
|
|
@@ -457,7 +388,6 @@ export class ServerItemRenderer | |
| userInput, | ||
| widgetsEmpty, | ||
| ); | ||
| this.handleInteractWithWidget(id); | ||
| }} | ||
| initializeUserInput={initializeUserInput} | ||
| /> | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm actually not sure we ever highlight empty widgets at all. We only enable the "Check" button when we're "answerable" but we're only answerable when there are no empty widgets...
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm pretty sure that not all frontends want to behave this way (requiring everything filled in before "Check Answer" is enabled).
However, I don't think that should block this PR. Rather, we probably want to have a prop that we set to enable empty widget highlighting. Then the frontend could allow the "Check Answer" button and if the learner clicks it, the frontend could validate the user input and if it sees that client validation fails, it could turn on the empty widget highlighting.