Skip to content

Commit

Permalink
feat: Added plugin data lookup to audio links page [PT-187885744]
Browse files Browse the repository at this point in the history
  • Loading branch information
dougmartin committed Jul 17, 2024
1 parent c54071f commit 3470db6
Show file tree
Hide file tree
Showing 10 changed files with 305 additions and 66 deletions.
17 changes: 17 additions & 0 deletions css/glossary-audio/glossary-audio-app.less
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,12 @@
color: #3f3f3f;
font-family: lato, arial, helvetica, sans-serif;

&.demo {
.studentId {
color: #f00;
}
}

.logo {
height: 35px;
}
Expand All @@ -17,6 +23,17 @@
.contents {
padding: 15px;

.studentInfo {
font-size: 18px;
font-weight: bold;
margin-bottom: 10px;
}

.error {
margin-bottom: 10px;
color: red;
}

.info {
margin-bottom: 10px;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -260,10 +260,10 @@ context("Portal Dashboard Anonymous Mode",() =>{
score.selectActivityScoreSettingsOption("Manual");
score.getSaveButton().click();
score.getActivityFeedbackScore().eq(0).find('input').click().type(50);
score.getActivityFeedbackScore().eq(1).find('input').click();
score.getActivityFeedbackScore().eq(1).find('input').click();
cy.get('[data-cy=feedback-settings-toggle-button]').click();
score.getSaveButton().click();
score.getNewMaxScoreDialog().should("exist");
score.getNewMaxScoreDialog().should("exist");
score.getNewMaxScoreDialog().find('[data-cy=feedback-settings-modal-header]').should("contain", "Activity Score Settings");
score.getNewMaxScoreDialog().find('[data-cy=feedback-settings-modal-content-area]')
.should("contain", "Some of the current student scores will be above the new max score of 10.");
Expand Down Expand Up @@ -345,7 +345,7 @@ context("Portal Dashboard Anonymous Mode",() =>{
cy.get('[data-cy=item-number]').should("contain", "4");
cy.get('[data-cy=feedback-textarea]').eq(0).click().type("Feedback");
cy.get('[data-cy=feedback-badge] circle').eq(0).invoke("attr", "fill").should("contain", "#FFF");
cy.get('[data-cy=item-number]').should("contain", "4");
cy.get('[data-cy=item-number]').should("contain", "4");
score.selectRubricScore(1, 1, 1);
score.selectRubricScore(1, 2, 1);
cy.get('[data-cy=feedback-badge] circle').eq(0).invoke("attr", "fill").should("contain", "#4EA15A");
Expand All @@ -371,12 +371,12 @@ context("Portal Dashboard Anonymous Mode",() =>{
cy.get('[data-cy=item-number]').should("contain", "4");
score.getActivityFeedbackScore().eq(2).find('input').click().type(10);
cy.get('[data-cy=feedback-badge] circle').eq(2).invoke("attr", "fill").should("contain", "#4EA15A");
cy.get('[data-cy=item-number]').should("contain", "3");
cy.get('[data-cy=item-number]').should("contain", "3");
});
it('verify previous activity info is not displayed in the activity level feedback',()=>{
score.selectRubricScore(1, 1, 1);
score.selectRubricScore(1, 2, 1);
cy.get("[class^='feedback-legend--feedbackBadgeLegend__rubric_score_avg--']").should("contain", "6 / 6");
cy.get("[class^='feedback-legend--feedbackBadgeLegend__rubric_score_avg--']").should("contain", "6 / 6");
cy.get('[data-cy=activity-navigator-next-button] [class^=activity-navigator--icon--]').click();
cy.get('[data-cy=activity-title]').should("contain", "Activity #2");
cy.get("[class^='feedback-legend--feedbackBadgeLegend__rubric_score_avg--']").should("contain", "0 / 6");
Expand All @@ -392,7 +392,7 @@ context("Portal Dashboard Anonymous Mode",() =>{
score.getContinueButton().click();
cy.get('[data-cy=activity-navigator-previous-button] [class^=activity-navigator--icon--]').click();
cy.get('[data-cy=activity-title]').should("contain", "Activity #1");
cy.get("[class^='feedback-legend--feedbackBadgeLegend__rubric_score_avg--']").should("contain", "6 / 6");
cy.get("[class^='feedback-legend--feedbackBadgeLegend__rubric_score_avg--']").should("contain", "6 / 6");
cy.get('[data-cy=feedback-settings-toggle-button]').click();
score.selectActivityScoreSettingsOption("Manual");
score.getSaveButton().click();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,13 +16,13 @@ class ActivityScore {
}
getCancelButton() {
return cy.get('[data-cy=feedback-settings-modal-close-button]').eq(0);
}
}
getSaveButton() {
return cy.get('[data-cy=feedback-settings-modal-close-button]').eq(1);
}
getActivityFeedbackScore() {
return cy.get('[class*=activity-feedback-score--activityFeedbackScore--]');
}
}
verifyScoreNotDisplayedInRubricScoreHeader() {
cy.get('[class^=rubric-table--rubricScoreHeader--]').eq(0).should("not.contain", "3");
cy.get('[class^=rubric-table--rubricScoreHeader--]').eq(1).should("not.contain", "2");
Expand Down
2 changes: 1 addition & 1 deletion js/api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -442,7 +442,7 @@ export class APIError extends Error{
}
}

function checkStatus(response: Response) {
export function checkStatus(response: Response) {
if (response.status >= 200 && response.status < 300) {
return response;
} else {
Expand Down
25 changes: 25 additions & 0 deletions js/containers/glossary-audio/components/glossary-word-button.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import React, { useMemo } from "react";
import { CurrentWord } from "../glossary-audio-app";

import css from "../../../../css/glossary-audio/glossary-word.less";

type Props = {
word: string;
index: number;
currentWord?: CurrentWord;
onClick: (word: string, index: number) => void;
}

function GlossaryWordButton({word, index, currentWord, onClick}: Props) {
const handleClick = () => onClick(word, index);

const isPlaying = useMemo(() => currentWord && currentWord.word === word && currentWord.index === index && currentWord.playing, [currentWord, word, index]);

const className = `${css.button} ${isPlaying ? css.playing : ""}`;
if (isPlaying) {
return <button className={className} onClick={handleClick}>Stop</button>;
}
return <button className={className} onClick={handleClick}>Play</button>;
}

export default GlossaryWordButton;
24 changes: 10 additions & 14 deletions js/containers/glossary-audio/components/glossary-word.tsx
Original file line number Diff line number Diff line change
@@ -1,31 +1,27 @@
import React, { useMemo } from "react";
import React from "react";
import { CurrentWord } from "../glossary-audio-app";

import css from "../../../../css/glossary-audio/glossary-word.less";
import GlossaryWordButton from "./glossary-word-button";

type Props = {
word: string;
numAudioRecordings: number;
currentWord?: CurrentWord;
onClick: (word: string) => void;
onClick: (word: string, index: number) => void;
}

function GlossaryWord({word, currentWord, onClick}: Props) {
const handleClick = () => onClick(word);
function GlossaryWord({word, numAudioRecordings, currentWord, onClick}: Props) {

const isPlaying = useMemo(() => currentWord && currentWord.word === word && currentWord.playing, [currentWord, word]);

const renderPlayStop = () => {
const className = `${css.button} ${isPlaying ? css.playing : ""}`;
if (isPlaying) {
return <button className={className} onClick={handleClick}>Stop</button>;
}
return <button className={className} onClick={handleClick}>Play</button>;
};
const buttons: React.ReactElement[] = [];
for (let i = 0; i < numAudioRecordings; i++) {
buttons.push(<GlossaryWordButton key={`${i}-${word}`} word={word} index={i} currentWord={currentWord} onClick={onClick} />);
}

return (
<tr>
<td><span className={css.word}>{word}</span></td>
<td>{renderPlayStop()}</td>
<td>{buttons}</td>
</tr>
);
}
Expand Down
62 changes: 51 additions & 11 deletions js/containers/glossary-audio/glossary-audio-app.tsx
Original file line number Diff line number Diff line change
@@ -1,36 +1,46 @@
import React, { useCallback, useEffect, useMemo, useState } from "react";
import classNames from "classnames";

import { useGlossaryAudio } from "./hooks/use-glossary-audio";
import GlossaryWord from "./components/glossary-word";
import { useGlossaryAudioParams } from "./hooks/use-glossary-audio-params";

import ccLogoSrc from "../../../img/cc-logo.png";

import css from "../../../css/glossary-audio/glossary-audio-app.less";

import ccLogoSrc from "../../../img/cc-logo.png";

export type CurrentWord = {
word: string;
index: number;
audio: HTMLAudioElement;
setAt: number;
playing: boolean;
}

function GlossaryAudioApp() {
const {loading, glossaryAudio, loadAudio} = useGlossaryAudio();
const params = useGlossaryAudioParams();
const {loading, error, classInfo, glossaryAudio, loadAudio} = useGlossaryAudio(params);
const [currentWord, setCurrentWord] = useState<CurrentWord|undefined>();
const words = useMemo(() => Object.keys(glossaryAudio), [glossaryAudio]);
const words = useMemo(() => {
const words = Object.keys(glossaryAudio);
words.sort();
return words;
}, [glossaryAudio]);

useEffect(() => {
document.title = "Glossary Audio";
}, []);

const handleWordClicked = useCallback((word: string) => {
const handleWordClicked = useCallback((word: string, index: number) => {
if (currentWord && currentWord.word === word && currentWord.playing) {
currentWord.audio.pause();
return;
}

loadAudio(word)
loadAudio(word, index)
.then(audio => {
setCurrentWord({word, audio, setAt: Date.now(), playing: true});
setCurrentWord({word, index, audio, setAt: Date.now(), playing: true});
audio.addEventListener("pause", () => {
setCurrentWord(prev => prev ? {...prev, playing: false} : prev);
});
Expand All @@ -39,6 +49,23 @@ function GlossaryAudioApp() {
.catch(alert);
}, [currentWord, loadAudio, setCurrentWord]);

const renderStudentInfo = () => {
if (params.demo) {
return <div className={css.studentInfo}>DEMO STUDENT</div>;
}

const {studentId, classId, offeringId} = params;
const className = `Class ${classId}`; // need to anonymize name
const offering = classInfo?.offerings.find(offering => String(offering.id) === offeringId);
const offeringName = offering ? `${offering.name} (Offering ${offeringId})` : `Offering ${offeringId}`;

return (
<div className={css.studentInfo}>
{`Student ${studentId} / ${className} / ${offeringName}`}
</div>
);
};

const renderAudio = () => {
if (loading) {
return (<div>Loading...</div>);
Expand All @@ -48,22 +75,35 @@ function GlossaryAudioApp() {
return (<div>No audio definitions found for student.</div>);
}

words.sort();
return (
<table>
<tbody>
{words.map(word => <GlossaryWord key={word} word={word} currentWord={currentWord} onClick={handleWordClicked} />)}
{words.map(word => <GlossaryWord key={word} word={word} numAudioRecordings={glossaryAudio[word].length} currentWord={currentWord} onClick={handleWordClicked} />)}
</tbody>
</table>
);
};

const renderError = () => <div className={css.error}>{error}</div>;

const renderContents = () => {
return (
<>
{renderStudentInfo()}
<div className={css.info}><strong>NOTE:</strong> Each word may have multiple audio definitions. If so, they are ordered from newest to oldest.</div>
{renderAudio()}
</>
);
};

const className = classNames(css.glossaryAudioApp, {[css.demo]: params.demo});

return (
<div className={css.glossaryAudioApp}>
<div className={className}>
<h1><img src={ccLogoSrc} className={css.logo} data-cy="header-logo"/> Glossary Audio</h1>
<div className={css.contents}>
<div className={css.info}>There are {words.length} audio definitions for words in the glossary by this user.</div>
{renderAudio()}
{error && renderError()}
{loading ? "Loading ..." : renderContents()}
</div>
</div>
);
Expand Down
29 changes: 29 additions & 0 deletions js/containers/glossary-audio/hooks/use-glossary-audio-params.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
export type GlossaryAudioDemoParams = {
demo: true;
}

export type GlossaryAudioPortalParams = {
demo: false;
sourceKey: string;
pluginDataKey: string;
portalUrl: string;
studentId: string;
classId: string;
offeringId: string;
}

export type GlossaryAudioParams = GlossaryAudioDemoParams | GlossaryAudioPortalParams

export const useGlossaryAudioParams = (): GlossaryAudioParams => {
const params = new URLSearchParams(window.location.search);
const sourceKey = params.get("sourceKey") ?? undefined;
const portalUrl = params.get("portalUrl") ?? undefined;
const pluginDataKey = params.get("pluginDataKey") ?? undefined;
const studentId = params.get("studentId") ?? undefined;
const classId = params.get("classId") ?? undefined;
const offeringId = params.get("offeringId") ?? undefined;
if (sourceKey !== undefined && portalUrl !== undefined && pluginDataKey !== undefined && studentId !== undefined && classId !== undefined && offeringId !== undefined) {
return {demo: false, sourceKey, portalUrl, pluginDataKey, studentId, classId, offeringId};
}
return { demo: true };
};
Loading

0 comments on commit 3470db6

Please sign in to comment.