Skip to content
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

Allow APIOptions.trackInteraction()'s arg object to have extra keys #616

Merged
merged 4 commits into from
Jul 25, 2023
Merged
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
5 changes: 5 additions & 0 deletions .changeset/sour-bears-fail.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@khanacademy/perseus": minor
---

Extend argument object type for APIOptions.trackInteraction callback to support the arbitrary data each widget may add
6 changes: 3 additions & 3 deletions packages/perseus/src/interaction-tracker.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import type {APIOptions} from "./types";
import type {APIOptions, Tracking} from "./types";

/**
* This alternate version of `.track` does nothing as an optimization.
Expand All @@ -12,7 +12,7 @@ class InteractionTracker {
// @ts-expect-error [FEI-5003] - TS2564 - Property '_tracked' has no initializer and is not definitely assigned in the constructor.
_tracked: boolean;
// @ts-expect-error [FEI-5003] - TS2564 - Property 'setting' has no initializer and is not definitely assigned in the constructor.
setting: string;
setting: Tracking;
track: (extraData?: any) => void;
trackApi: any;
// @ts-expect-error [FEI-5003] - TS2564 - Property 'widgetID' has no initializer and is not definitely assigned in the constructor.
Expand All @@ -24,7 +24,7 @@ class InteractionTracker {
trackApi: APIOptions["trackInteraction"],
widgetType: string,
widgetID: string,
setting: "" | "all", // "" means track once
setting: Tracking,
) {
if (!trackApi) {
this.track = _noop;
Expand Down
28 changes: 18 additions & 10 deletions packages/perseus/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -179,15 +179,18 @@ export type APIOptions = Readonly<{
// Function that takes dimensions and returns a React component
// to display while an image is loading
imagePreloader?: (dimensions: Dimensions) => React.ReactNode;
// Function that takes an object argument. The object should
// include type and id, both strings, at least and can optionally
// include a boolean "correct" value. This is used for keeping
// track of widget interactions.
trackInteraction?: (args: {
type: string;
id: string;
correct?: boolean;
}) => void;
// A function that is called when the user has interacted with a widget. It
// also includes any extra parameters that the originating widget provided.
// This is used for keeping track of widget interactions.
trackInteraction?: (
args: {
// The widget type that this interaction originates from
type: string;
// The widget id that this interaction originates from
id: string;
correct?: boolean;
} & Record<string, unknown>,
) => void;
// A boolean that indicates whether or not a custom keypad is
// being used. For mobile web this will be the ProvidedKeypad
// component. In this situation we use the MathInput component
Expand Down Expand Up @@ -375,7 +378,12 @@ export type LinterContextProps = {
// additional properties can be added to the context by widgets
};

export type Tracking = "" | "all";
export type Tracking =
// Track interactions once
| ""
// Track all interactions
| "all";

export type Alignment =
| "default"
| "block"
Expand Down
45 changes: 21 additions & 24 deletions packages/perseus/src/widgets/sequence.tsx
Copy link
Collaborator Author

Choose a reason for hiding this comment

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

The changes here are just migrating this widget to use TypeScript types for Props and State.

Copy link
Contributor

Choose a reason for hiding this comment

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

This is awesome, thank you 🌈

Original file line number Diff line number Diff line change
@@ -1,36 +1,33 @@
import {
linterContextProps,
linterContextDefault,
} from "@khanacademy/perseus-linter";
import PropTypes from "prop-types";
import {linterContextDefault} from "@khanacademy/perseus-linter";
import * as React from "react";
import _ from "underscore";

import InlineIcon from "../components/inline-icon";
import {iconOk} from "../icon-paths";
import * as Changeable from "../mixins/changeable";
import {ApiOptions} from "../perseus-api";
import {PerseusSequenceWidgetOptions} from "../perseus-types";
import Renderer from "../renderer";
import Util from "../util";

import type {WidgetExports} from "../types";

class Sequence extends React.Component<any, any> {
static propTypes = {
...Changeable.propTypes,
apiOptions: ApiOptions.propTypes,
json: PropTypes.arrayOf(
PropTypes.shape({
content: PropTypes.string,
images: PropTypes.objectOf(PropTypes.any),
widgets: PropTypes.objectOf(PropTypes.any),
}),
),
trackInteraction: PropTypes.func.isRequired,
linterContext: linterContextProps,
};
import type {WidgetExports, WidgetProps} from "../types";

type Rubric = PerseusSequenceWidgetOptions;

type ExternalProps = WidgetProps<PerseusSequenceWidgetOptions, Rubric>;

type Props = ExternalProps;

type DefaultProps = {
Comment on lines +14 to +19
Copy link
Collaborator Author

Choose a reason for hiding this comment

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

This is our more modern way to define prop types for widgets. It uses WidgetProps<> as that defines all the standard props that the Renderer hands down to every widget. The PerseusSequenceWidgetOptions are then the option data that the content author configured for this widget instance.

json: Props["json"];
linterContext: Props["linterContext"];
};

type State = {
visible: number;
};

static defaultProps: any = {
class Sequence extends React.Component<Props, State> {
static defaultProps: DefaultProps = {
json: [
{
content: "",
Expand All @@ -41,7 +38,7 @@ class Sequence extends React.Component<any, any> {
linterContext: linterContextDefault,
};

state: any = {
state = {
visible: 1,
};

Expand Down
Loading