-
Notifications
You must be signed in to change notification settings - Fork 353
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
Expose a way to get user input from ServerItemRenderer #1753
base: main
Are you sure you want to change the base?
Changes from 9 commits
09b98b3
7eb8573
1d6d8e8
1739781
bc9bd82
395a798
4103af1
4bfc547
c0d6dfd
0e921b8
159bf19
d772fbe
8f704e4
028ffe0
9482abe
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,6 @@ | ||
--- | ||
"@khanacademy/perseus": major | ||
"@khanacademy/perseus-dev-ui": patch | ||
--- | ||
|
||
Change ServerItemRenderer scoring APIs to externalize scoring |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,7 +1,8 @@ | ||
import Util from "./util"; | ||
import {getWidgetIdsFromContent} from "./widget-type-utils"; | ||
import {getWidgetValidator} from "./widgets"; | ||
|
||
import type {PerseusWidgetsMap} from "./perseus-types"; | ||
import type {PerseusRenderer, PerseusWidgetsMap} from "./perseus-types"; | ||
import type {PerseusStrings} from "./strings"; | ||
import type {PerseusScore} from "./types"; | ||
import type {UserInput, UserInputMap} from "./validation.types"; | ||
|
@@ -50,11 +51,35 @@ export function emptyWidgetsFunctional( | |
}); | ||
} | ||
|
||
// TODO: combine scorePerseusItem with scoreWidgetsFunctional | ||
// once scorePerseusItem is the only one calling scoreWidgetsFunctional | ||
export function scorePerseusItem( | ||
perseusRenderData: PerseusRenderer, | ||
userInputMap: UserInputMap, | ||
// TODO(LEMS-2461,LEMS-2391): these probably | ||
// need to be removed before we move this to the server | ||
strings: PerseusStrings, | ||
locale: string, | ||
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 suspect we'll always need the locale for scoring, because of things like decimal separators. But localized strings I agree with you on. |
||
): PerseusScore { | ||
// There seems to be a chance that PerseusRenderer.widgets might include | ||
// widget data for widgets that are not in PerseusRenderer.content, | ||
// so this checks that the widgets are being used before scoring them | ||
Comment on lines
+64
to
+66
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. Yes. Our editor doesn't (or hasn't always) cleared out widget configs when they are removed from |
||
const usedWidgetIds = getWidgetIdsFromContent(perseusRenderData.content); | ||
const scores = scoreWidgetsFunctional( | ||
perseusRenderData.widgets, | ||
usedWidgetIds, | ||
userInputMap, | ||
strings, | ||
locale, | ||
); | ||
return Util.flattenScores(scores); | ||
} | ||
|
||
export function scoreWidgetsFunctional( | ||
widgets: PerseusWidgetsMap, | ||
// This is a port of old code, I'm not sure why | ||
// we need widgetIds vs the keys of the widgets object | ||
widgetIds: Array<string>, | ||
widgetIds: ReadonlyArray<string>, | ||
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. 👍 Thanks. |
||
userInputMap: UserInputMap, | ||
strings: PerseusStrings, | ||
locale: string, | ||
|
@@ -79,7 +104,7 @@ export function scoreWidgetsFunctional( | |
if (widget.type === "group") { | ||
const scores = scoreWidgetsFunctional( | ||
widget.options.widgets, | ||
Object.keys(widget.options.widgets), | ||
getWidgetIdsFromContent(widget.options.content), | ||
userInputMap[id] as UserInputMap, | ||
strings, | ||
locale, | ||
|
Original file line number | Diff line number | Diff line change | ||||
---|---|---|---|---|---|---|
|
@@ -375,7 +375,7 @@ class Renderer extends React.Component<Props, State> { | |||||
// WidgetContainers don't update their widgets' props when | ||||||
// they are re-rendered, so even if they've been | ||||||
// re-rendered we need to call these methods on them. | ||||||
_.each(this.widgetIds, (id) => { | ||||||
this.widgetIds.forEach((id) => { | ||||||
const container = this._widgetContainers.get(makeContainerId(id)); | ||||||
container && container.replaceWidgetProps(this.getWidgetProps(id)); | ||||||
}); | ||||||
|
@@ -1119,7 +1119,7 @@ class Renderer extends React.Component<Props, State> { | |||||
// /cry(aria) | ||||||
this._foundTextNodes = true; | ||||||
|
||||||
if (_.contains(this.widgetIds, node.id)) { | ||||||
if (this.widgetIds.includes(node.id)) { | ||||||
// We don't want to render a duplicate widget key/ref, | ||||||
// as this causes problems with react (for obvious | ||||||
// reasons). Instead we just notify the | ||||||
|
@@ -1505,15 +1505,15 @@ class Renderer extends React.Component<Props, State> { | |||||
|
||||||
getInputPaths: () => ReadonlyArray<FocusPath> = () => { | ||||||
const inputPaths: Array<FocusPath> = []; | ||||||
_.each(this.widgetIds, (widgetId: string) => { | ||||||
this.widgetIds.forEach((widgetId: string) => { | ||||||
const widget = this.getWidgetInstance(widgetId); | ||||||
if (widget && widget.getInputPaths) { | ||||||
// Grab all input paths and add widgetID to the front | ||||||
const widgetInputPaths = widget.getInputPaths(); | ||||||
// Prefix paths with their widgetID and add to collective | ||||||
// list of paths. | ||||||
// @ts-expect-error - TS2345 - Argument of type '(inputPath: string) => void' is not assignable to parameter of type 'CollectionIterator<FocusPath, void, readonly FocusPath[]>'. | ||||||
_.each(widgetInputPaths, (inputPath: string) => { | ||||||
widgetInputPaths.forEach((inputPath: string) => { | ||||||
const relativeInputPath = [widgetId].concat(inputPath); | ||||||
inputPaths.push(relativeInputPath); | ||||||
}); | ||||||
|
@@ -1717,46 +1717,42 @@ class Renderer extends React.Component<Props, State> { | |||||
}; | ||||||
|
||||||
/** | ||||||
* Returns an object mapping from widget ID to perseus-style score. | ||||||
* The keys of this object are the values of the array returned | ||||||
* from `getWidgetIds`. | ||||||
* Grades the content. | ||||||
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. nit: We've been asked to use "score" over "grade". :) EDIT: Oh, I see that was the original comment from below. Would you mind updating it still?
Suggested change
|
||||||
* | ||||||
* @deprecated use scorePerseusItem | ||||||
*/ | ||||||
scoreWidgets(): {[widgetId: string]: PerseusScore} { | ||||||
return scoreWidgetsFunctional( | ||||||
score(): PerseusScore { | ||||||
const scores = scoreWidgetsFunctional( | ||||||
this.state.widgetInfo, | ||||||
this.widgetIds, | ||||||
this.getUserInputMap(), | ||||||
this.props.strings, | ||||||
this.context.locale, | ||||||
); | ||||||
} | ||||||
|
||||||
/** | ||||||
* Grades the content. | ||||||
*/ | ||||||
score(): PerseusScore { | ||||||
const scores = this.scoreWidgets(); | ||||||
const combinedScore = Util.flattenScores(scores); | ||||||
return combinedScore; | ||||||
} | ||||||
|
||||||
guessAndScore(): [UserInputArray, PerseusScore] { | ||||||
/** | ||||||
* @deprecated use scorePerseusItem | ||||||
*/ | ||||||
guessAndScore: () => [UserInputArray, PerseusScore] = () => { | ||||||
const totalGuess = this.getUserInput(); | ||||||
const totalScore = this.score(); | ||||||
|
||||||
return [totalGuess, totalScore]; | ||||||
} | ||||||
}; | ||||||
|
||||||
examples: () => ReadonlyArray<string> | null | undefined = () => { | ||||||
const widgetIds = this.widgetIds; | ||||||
const examples = _.compact( | ||||||
_.map(widgetIds, (widgetId) => { | ||||||
const examples = widgetIds | ||||||
.map((widgetId) => { | ||||||
const widget = this.getWidgetInstance(widgetId); | ||||||
return widget != null && widget.examples | ||||||
? widget.examples() | ||||||
: null; | ||||||
}), | ||||||
); | ||||||
}) | ||||||
.filter(Boolean); | ||||||
|
||||||
// no widgets with examples | ||||||
if (!examples.length) { | ||||||
|
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.
Scoring split from user input! A red-letter day in history! 🎉