From 3c18054532a65a8818d99046912bec54b096e240 Mon Sep 17 00:00:00 2001 From: Michael Reyna Date: Wed, 10 Apr 2024 16:29:05 -0500 Subject: [PATCH 01/14] Implement initial version of renderSearchBlocks --- src/components/SearchBlock.tsx | 31 +++++++++++++++++++++++++++++++ src/lib/blocks.tsx | 10 ++++++++++ src/lib/env.ts | 1 + src/lib/render-moodle.ts | 8 +++++++- src/main.tsx | 5 ++++- 5 files changed, 53 insertions(+), 2 deletions(-) create mode 100644 src/components/SearchBlock.tsx diff --git a/src/components/SearchBlock.tsx b/src/components/SearchBlock.tsx new file mode 100644 index 00000000..49176b90 --- /dev/null +++ b/src/components/SearchBlock.tsx @@ -0,0 +1,31 @@ +import { Formik, Form, Field, ErrorMessage } from 'formik' +// import { useCallback, useContext, useState } from 'react' +// import { ENV } from '../lib/env' + +// interface SearchBlockProps { +// } + +export const SearchBlock = (): JSX.Element => { + return ( +
+

HERE IS THE AMAZING SEARCHBLOCK

+ {}} + validateOnBlur={false} + > + {() => ( +
+ + +
+ +
+ + )} +
+
+ ) +} diff --git a/src/lib/blocks.tsx b/src/lib/blocks.tsx index 70167a86..29753b7a 100644 --- a/src/lib/blocks.tsx +++ b/src/lib/blocks.tsx @@ -12,6 +12,7 @@ import { PROBLEM_TYPE_MULTISELECT } from '../components/ProblemSetBlock' import { UserInputBlock } from '../components/UserInputBlock' +import { SearchBlock } from '../components/SearchBlock' import { queueIbPsetProblemAttemptedV1Event, queueIbInputSubmittedV1Event } from './events' export const OS_RAISE_IB_EVENT_PREFIX = 'os-raise-ib-event' @@ -21,6 +22,7 @@ export const CTA_CONTENT_CLASS = 'os-raise-ib-cta-content' export const CTA_PROMPT_CLASS = 'os-raise-ib-cta-prompt' export const OS_RAISE_IB_INPUT_CLASS = 'os-raise-ib-input' export const OS_RAISE_IB_DESMOS_CLASS = 'os-raise-ib-desmos-gc' +export const OS_RAISE_SEARCH_CLASS = 'os-raise-search' const INPUT_CONTENT_CLASS = 'os-raise-ib-input-content' const INPUT_PROMPT_CLASS = 'os-raise-ib-input-prompt' const INPUT_ACK_CLASS = 'os-raise-ib-input-ack' @@ -365,3 +367,11 @@ export const parseDesmosBlock = (element: HTMLElement): JSX.Element | null => { /> } + +export const parseSearchBlock = (element: HTMLElement): JSX.Element | null => { + if (!element.classList.contains(OS_RAISE_SEARCH_CLASS)) { + return null + } + + return +} diff --git a/src/lib/env.ts b/src/lib/env.ts index 940bb949..7661bb76 100644 --- a/src/lib/env.ts +++ b/src/lib/env.ts @@ -1,5 +1,6 @@ export const ENV = { OS_RAISE_CONTENT_URL_PREFIX: import.meta.env.MODE === 'production' ? 'https://k12.openstax.org/contents/raise' : 'http://localhost:8800/contents', + OS_RAISE_SEARCHAPI_URL_PREFIX: import.meta.env.MODE === 'development' ? 'http://localhost:9400' : 'https://search.raiselearning.org', OS_RAISE_EVENTSAPI_URL_MAP: {}, EVENT_FLUSH_PERIOD: 60000 } diff --git a/src/lib/render-moodle.ts b/src/lib/render-moodle.ts index 17ef2ed7..4342df50 100644 --- a/src/lib/render-moodle.ts +++ b/src/lib/render-moodle.ts @@ -5,11 +5,13 @@ import { OS_RAISE_IB_INPUT_CLASS, OS_RAISE_IB_PSET_CLASS, OS_RAISE_IB_DESMOS_CLASS, + OS_RAISE_SEARCH_CLASS, parseContentOnlyBlock, parseCTABlock, parseUserInputBlock, parseProblemSetBlock, - parseDesmosBlock + parseDesmosBlock, + parseSearchBlock } from './blocks' const replaceElementWithBlock = (element: HTMLElement, component: JSX.Element): void => { @@ -103,3 +105,7 @@ export const renderProblemSetBlocks = (element: HTMLElement): void => { export const renderDesmosBlocks = (element: HTMLElement): void => { renderContentBlocksByClass(element, OS_RAISE_IB_DESMOS_CLASS, parseDesmosBlock) } + +export const renderSearchBlocks = (element: HTMLElement): void => { + renderContentBlocksByClass(element, OS_RAISE_SEARCH_CLASS, parseSearchBlock) +} diff --git a/src/main.tsx b/src/main.tsx index 871d5661..091e4cfd 100644 --- a/src/main.tsx +++ b/src/main.tsx @@ -3,7 +3,8 @@ import { renderCTABlocks, renderProblemSetBlocks, renderUserInputBlocks, - renderDesmosBlocks + renderDesmosBlocks, + renderSearchBlocks } from './lib/render-moodle' import { renderContentElements } from './lib/content' import { tooltipify } from './lib/tooltip' @@ -25,6 +26,8 @@ const processPage = (): void => { renderProblemSetBlocks(document.body) renderDesmosBlocks(document.body) } + + renderSearchBlocks(document.body) } processPage() From 541ca938859f34061e2b05ce571118e1de13e765 Mon Sep 17 00:00:00 2001 From: Michael Reyna Date: Fri, 12 Apr 2024 16:13:47 -0500 Subject: [PATCH 02/14] Add search styles to stylesheet --- src/styles/interactives.scss | 41 +++++++++++++++++++++++++++++++++++- 1 file changed, 40 insertions(+), 1 deletion(-) diff --git a/src/styles/interactives.scss b/src/styles/interactives.scss index 83795681..3922d9b6 100644 --- a/src/styles/interactives.scss +++ b/src/styles/interactives.scss @@ -172,4 +172,43 @@ math-field::part(menu-toggle) { // Textarea Styles .os-textarea-disabled.os-textarea-disabled { box-shadow: none; -} \ No newline at end of file +} + +// Search Styles +.os-search-form { + display: flex; + flex-direction: column; + background-color: #00504721; + border: 1px solid #00504721; + border-radius: 5px; + padding: 1rem +} + +.os-search-button { + padding: 4px 16px +} + +.os-search-results-container { + margin-top: 1rem; +} + +.os-search-results-list { + list-style: none; + padding: 0; +} + +.os-search-results-list-item { + background-color: #00504721; + border-radius: .5rem; + margin-bottom: .5rem; + padding: .5rem; +} + +.os-search-results-highlights { + border-bottom: .1rem solid #d9d9d9; + padding-bottom: .5rem; +} + +.os-search-no-results { + color: $os-wrong-answer-border-color; +} From c2eb29227a98de5d0cc6d02f0ebd535f86cedc1f Mon Sep 17 00:00:00 2001 From: Michael Reyna Date: Fri, 12 Apr 2024 16:14:10 -0500 Subject: [PATCH 03/14] Add filter prop for search component --- src/lib/blocks.tsx | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/src/lib/blocks.tsx b/src/lib/blocks.tsx index 29753b7a..bcb163c4 100644 --- a/src/lib/blocks.tsx +++ b/src/lib/blocks.tsx @@ -14,6 +14,7 @@ import { import { UserInputBlock } from '../components/UserInputBlock' import { SearchBlock } from '../components/SearchBlock' import { queueIbPsetProblemAttemptedV1Event, queueIbInputSubmittedV1Event } from './events' +import { getVersionId } from './utils' export const OS_RAISE_IB_EVENT_PREFIX = 'os-raise-ib-event' export const OS_RAISE_IB_CONTENT_CLASS = 'os-raise-ib-content' @@ -372,6 +373,10 @@ export const parseSearchBlock = (element: HTMLElement): JSX.Element | null => { if (!element.classList.contains(OS_RAISE_SEARCH_CLASS)) { return null } + const maybeFilter = element.dataset.filter ?? '' - return + return } From c45321dfacc5cbbbb1d30aa85a4268fab08db1e4 Mon Sep 17 00:00:00 2001 From: Michael Reyna Date: Fri, 12 Apr 2024 16:15:20 -0500 Subject: [PATCH 04/14] Add potential interface and UI classes --- src/components/SearchBlock.tsx | 174 +++++++++++++++++++++++++++++---- 1 file changed, 157 insertions(+), 17 deletions(-) diff --git a/src/components/SearchBlock.tsx b/src/components/SearchBlock.tsx index 49176b90..b7ddb56c 100644 --- a/src/components/SearchBlock.tsx +++ b/src/components/SearchBlock.tsx @@ -1,31 +1,171 @@ import { Formik, Form, Field, ErrorMessage } from 'formik' -// import { useCallback, useContext, useState } from 'react' -// import { ENV } from '../lib/env' +import { ENV } from '../lib/env' +import React, { useState } from 'react' +// May need to bring in uuid or some other library to generate keys +// Determine the neccessity of ErrorMessage component from formik -// interface SearchBlockProps { -// } +interface SearchBlockProps { + versionId: string + filter?: string +} + +interface HitValue { + value: number +} + +interface HitSource { + section: string + activity_name: string + lesson_page: string + teacher_only: boolean +} + +interface HitHighlight { + lesson_page: string[] + visible_content: string[] + activity_name?: string[] +} + +interface HitIdSourceHighlight { + _id: string + _source: HitSource + highlight: HitHighlight +} + +interface HitTotalHits { + total: HitValue + hits: HitIdSourceHighlight[] +} + +interface Hits { + hits: HitTotalHits +} + +export const SearchBlock = ({ versionId, filter }: SearchBlockProps): JSX.Element => { + const [query, setQuery] = useState('') + const [searchResults, setSearchResults] = useState(undefined) + const fetchContent = async (): Promise => { + // Confirm the final url to be fetched + const response = await fetch(`${ENV.OS_RAISE_SEARCHAPI_URL_PREFIX}?q=${query}&version_id=${versionId}&filter=${filter}`) + console.log('here is response:', response) + + if (!response.ok) { + throw new Error('Failed to get search results') + } -export const SearchBlock = (): JSX.Element => { + const data: Hits = await response.json() + // const data: Hits = { + // hits: { + // total: { + // value: 34 + // }, + // hits: [ + // { + // _id: '1a9844da-0262-49c0-9194-1496f9cfc4ed', + // _source: { + // section: 'Unit 4: Functions', + // activity_name: 'Lesson 4.16: Different Types of Sequences', + // lesson_page: '4.16.3: A Sequence Is a Type of Function', + // teacher_only: true + // }, + // highlight: { + // lesson_page: [ + // '4.16.3: A Sequence Is a Type of Function' + // ], + // visible_content: [ + // 'Activity \nJada and Mai are trying to decide what type of sequence this could be:\n\n\n\n\nterm number\nvalue', + // '2\n \n\n\n\n 2\n \n\n 6\n \n\n\n\n 5\n \n\n 18\n \n\n\n\nJada', + // 'says: \u201cI think this sequence is geometric because in the value column, each row is 3 times the previous', + // 'Do you agree with Jada or Mai? Be prepared to show your reasoning using a graph.', + // 'Jada noticed that each value is multiplied by 3 to get to the next row, but the table skips terms.' + // ] + // } + // }, + // { + // _id: 'f27ad080-9923-48c5-9355-54e4934a95d8', + // _source: { + // section: 'Unit 4: Functions', + // activity_name: 'Lesson 4.14: Sequences', + // lesson_page: '4.14.2: What Is a Sequence?', + // teacher_only: false + // }, + // highlight: { + // lesson_page: [ + // '4.14.2: What Is a Sequence?' + // ], + // visible_content: [ + // 'What is the smallest number of moves in which you are able to complete the puzzle with 3 discs?', + // 'Enter your answer here:\n\n\nCompare your answer:\n15 moves\n\n\n\n\n\n\n\n\n\nJada says she used the solution for', + // 'Enter your answer here:\n\n\nCompare your answer:\nJada moved the tower of discs 1\u20133, then moved disc 4,', + // 'The number of moves is 31 because \\(2\\cdot15+1=31\\).', + // 'The term (of a sequence) is one of the numbers in a sequence.' + // ] + // } + // } + // ] + // } + // } + setSearchResults(data) + } return (
-

HERE IS THE AMAZING SEARCHBLOCK

- {}} - validateOnBlur={false} - > - {() => ( -
+ onSubmit={fetchContent} + > + {({ setFieldValue }) => ( + + name="response" + type='text' + onChange={(e: React.ChangeEvent) => { + setQuery(e.target.value) + void setFieldValue('response', e.target.value) + }} + />
- +
- + )} -
+ + {searchResults !== undefined && searchResults.hits.total.value !== 0 && +
+

Total search results: {searchResults.hits.total.value}

+

Total search results displayed: {searchResults.hits.hits.length}

+
    + {searchResults.hits.hits.map((hit) => ( +
  • +
    +

    Location

    +

    {hit._source.teacher_only && `This is ${filter} content`}

    +

    {hit._source.section}

    +

    {hit._source.activity_name}

    +

    {hit._source.lesson_page !== '' && hit._source.lesson_page}

    +
    +
    +

    Results

    + {hit.highlight.visible_content.map((content: string) => ( +

    + ))} + {hit.highlight.lesson_page.map((page: string) => ( +

    + ))} + {hit.highlight.activity_name?.map((activity: string) => ( +

    + ))} +
    +
  • + ))} +
+
+ } + {searchResults !== undefined && searchResults.hits.total.value === 0 && +
+

Your query did not produce any results. Please try again.

+
+ }
) } From f00ab01ad6fcbf92cac690413bbb1a3f785ece1b Mon Sep 17 00:00:00 2001 From: Michael Reyna Date: Fri, 12 Apr 2024 16:16:12 -0500 Subject: [PATCH 05/14] Create initial test for fetch and display of results --- src/tests/SearchBlock.test.tsx | 103 +++++++++++++++++++++++++++++++++ 1 file changed, 103 insertions(+) create mode 100644 src/tests/SearchBlock.test.tsx diff --git a/src/tests/SearchBlock.test.tsx b/src/tests/SearchBlock.test.tsx new file mode 100644 index 00000000..d7730878 --- /dev/null +++ b/src/tests/SearchBlock.test.tsx @@ -0,0 +1,103 @@ +import { act, fireEvent, render, screen } from '@testing-library/react' +import { SearchBlock } from '../components/SearchBlock' + +const mockQueryResults = { + took: 55, + timed_out: false, + _shards: { + total: 1, + successful: 1, + skipped: 0, + failed: 0 + }, + hits: { + total: { + value: 37, + relation: 'eq' + }, + max_score: 12.129974, + hits: [ + { + _index: 'test-index2', + _id: '1a9844da-0262-49c0-9194-1496f9cfc4ed', + _score: 12.129974, + _source: { + content_id: '1a9844da-0262-49c0-9194-1496f9cfc4ed', + section: 'Unit 4: Functions', + activity_name: 'Lesson 4.16: Different Types of Sequences', + lesson_page: '4.16.3: A Sequence Is a Type of Function', + lesson_page_type: 'content', + teacher_only: false + }, + highlight: { + lesson_page: [ + '4.16.3: A Sequence Is a Type of Function' + ], + visible_content: [ + 'Activity \nJada and Mai are trying to decide what type of sequence this could be:\n\n\n\n\nterm number\nvalue', + '2\n \n\n\n\n 2\n \n\n 6\n \n\n\n\n 5\n \n\n 18\n \n\n\n\nJada', + 'says: \u201cI think this sequence is geometric because in the value column, each row is 3 times the previous', + 'Do you agree with Jada or Mai? Be prepared to show your reasoning using a graph.', + 'Jada noticed that each value is multiplied by 3 to get to the next row, but the table skips terms.' + ] + } + }, + { + _index: 'test-index2', + _id: 'f27ad080-9923-48c5-9355-54e4934a95d8', + _score: 11.537502, + _source: { + content_id: 'f27ad080-9923-48c5-9355-54e4934a95d8', + section: 'Unit 4: Functions', + activity_name: 'Lesson 4.14: Sequences', + lesson_page: '4.14.2: What Is a Sequence?', + lesson_page_type: 'content', + teacher_only: false + }, + highlight: { + lesson_page: [ + '4.14.2: What Is a Sequence?' + ], + visible_content: [ + 'What is the smallest number of moves in which you are able to complete the puzzle with 3 discs?', + 'Enter your answer here:\n\n\nCompare your answer:\n15 moves\n\n\n\n\n\n\n\n\n\nJada says she used the solution for', + 'Enter your answer here:\n\n\nCompare your answer:\nJada moved the tower of discs 1\u20133, then moved disc 4,', + 'The number of moves is 31 because \\(2\\cdot15+1=31\\).', + 'The term (of a sequence) is one of the numbers in a sequence.' + ] + } + } + ] + } +} + +jest.mock('../lib/env.ts', () => ({ + ENV: { + OS_RAISE_SEARCHAPI_URL_PREFIX: 'http://searchapi' + } +})) +// Test passes, but need to clean up the \n and strong tags for testing purposes. +// Will need to update the url +describe('search', () => { + it('fetches and displays search results from API', async () => { + global.fetch = jest.fn().mockResolvedValue({ + ok: true, + json: async () => await Promise.resolve(mockQueryResults) + }) + + render( + + ) + + const queryInput = screen.getByRole('textbox') + await act(async () => { + fireEvent.change(queryInput, { target: { value: 'math' } }) + fireEvent.click(screen.getByText('Button')) + }) + + await screen.findAllByText('Unit 4: Functions') + expect(await screen.findByText('Lesson 4.16: Different Types of Sequences')) + expect(await screen.findByText('Lesson 4.14: Sequences')) + expect(global.fetch).toHaveBeenCalledWith(`http://searchapi?q=math&version_id=12345&filter=${''}`) + }) +}) From 0917fc89e4bad8a74feef3e740505135548da366 Mon Sep 17 00:00:00 2001 From: Michael Reyna Date: Tue, 16 Apr 2024 10:59:44 -0500 Subject: [PATCH 06/14] Update prev test and add teacher filter test --- src/tests/SearchBlock.test.tsx | 83 ++++++++++++++++++++++++++++++++-- 1 file changed, 79 insertions(+), 4 deletions(-) diff --git a/src/tests/SearchBlock.test.tsx b/src/tests/SearchBlock.test.tsx index d7730878..f8d165bd 100644 --- a/src/tests/SearchBlock.test.tsx +++ b/src/tests/SearchBlock.test.tsx @@ -1,7 +1,7 @@ import { act, fireEvent, render, screen } from '@testing-library/react' import { SearchBlock } from '../components/SearchBlock' -const mockQueryResults = { +const mockStudentAndTeacherQueryResults = { took: 55, timed_out: false, _shards: { @@ -71,18 +71,62 @@ const mockQueryResults = { } } +const mockTeacherFilterQueryResults = { + took: 55, + timed_out: false, + _shards: { + total: 1, + successful: 1, + skipped: 0, + failed: 0 + }, + hits: { + total: { + value: 37, + relation: 'eq' + }, + max_score: 12.129974, + hits: [ + { + _index: 'test-index1', + _id: '1a9844da-0262-49c0-9194-1496f9cfc4ed', + _score: 12.129974, + _source: { + content_id: '1a9844da-0262-49c0-9194-1496f9cfc4ed', + section: 'Unit 4: Functions', + activity_name: 'Lesson 4.16: Different Types of Sequences', + lesson_page: '4.16.3: A Sequence Is a Type of Function', + lesson_page_type: 'content', + teacher_only: true + }, + highlight: { + lesson_page: [ + '4.16.3: A Sequence Is a Type of Function' + ], + visible_content: [ + 'Activity \nJada and Mai are trying to decide what type of sequence this could be:\n\n\n\n\nterm number\nvalue', + '2\n \n\n\n\n 2\n \n\n 6\n \n\n\n\n 5\n \n\n 18\n \n\n\n\nJada', + 'says: \u201cI think this sequence is geometric because in the value column, each row is 3 times the previous', + 'Do you agree with Jada or Mai? Be prepared to show your reasoning using a graph.', + 'Jada noticed that each value is multiplied by 3 to get to the next row, but the table skips terms.' + ] + } + } + ] + } +} + jest.mock('../lib/env.ts', () => ({ ENV: { OS_RAISE_SEARCHAPI_URL_PREFIX: 'http://searchapi' } })) -// Test passes, but need to clean up the \n and strong tags for testing purposes. // Will need to update the url describe('search', () => { - it('fetches and displays search results from API', async () => { + it('fetches and displays student and teacher search results from API', async () => { global.fetch = jest.fn().mockResolvedValue({ ok: true, - json: async () => await Promise.resolve(mockQueryResults) + json: async () => await Promise.resolve(mockStudentAndTeacherQueryResults) }) render( @@ -96,8 +140,39 @@ describe('search', () => { }) await screen.findAllByText('Unit 4: Functions') + const firstHitLessonPage = await screen.findAllByText('4.16.3', { exact: false }) + const secondHitLessonPage = await screen.findAllByText('4.14.2', { exact: false }) + expect(firstHitLessonPage.length === 2) + expect(secondHitLessonPage.length === 2) expect(await screen.findByText('Lesson 4.16: Different Types of Sequences')) expect(await screen.findByText('Lesson 4.14: Sequences')) + expect(await screen.findByText('table skips terms', { exact: false })) + expect(await screen.findByText('smallest', { exact: false })) expect(global.fetch).toHaveBeenCalledWith(`http://searchapi?q=math&version_id=12345&filter=${''}`) }) + + it('fetches and displays teacher filter search results from API', async () => { + global.fetch = jest.fn().mockResolvedValue({ + ok: true, + json: async () => await Promise.resolve(mockTeacherFilterQueryResults) + }) + + render( + + ) + + const queryInput = screen.getByRole('textbox') + await act(async () => { + fireEvent.change(queryInput, { target: { value: 'math' } }) + fireEvent.click(screen.getByText('Button')) + }) + + await screen.findByText('Unit 4: Functions') + const firstHitLessonPage = await screen.findAllByText('4.16.3', { exact: false }) + expect(firstHitLessonPage.length === 1) + expect(await screen.findByText('Lesson 4.16: Different Types of Sequences')) + expect(await screen.findByText('table skips terms', { exact: false })) + expect(await screen.findByText('This is teacher content')) + expect(global.fetch).toHaveBeenCalledWith('http://searchapi?q=math&version_id=67890&filter=teacher') + }) }) From d730b96f66c6a57d9435d79ba22f78a54cdb575f Mon Sep 17 00:00:00 2001 From: Michael Reyna Date: Tue, 16 Apr 2024 11:01:18 -0500 Subject: [PATCH 07/14] Update interface and add note regarding react keys --- src/components/SearchBlock.tsx | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/src/components/SearchBlock.tsx b/src/components/SearchBlock.tsx index b7ddb56c..14957890 100644 --- a/src/components/SearchBlock.tsx +++ b/src/components/SearchBlock.tsx @@ -1,7 +1,6 @@ import { Formik, Form, Field, ErrorMessage } from 'formik' import { ENV } from '../lib/env' import React, { useState } from 'react' -// May need to bring in uuid or some other library to generate keys // Determine the neccessity of ErrorMessage component from formik interface SearchBlockProps { @@ -21,9 +20,9 @@ interface HitSource { } interface HitHighlight { - lesson_page: string[] + lesson_page?: string[] visible_content: string[] - activity_name?: string[] + activity_name: string[] } interface HitIdSourceHighlight { @@ -47,7 +46,6 @@ export const SearchBlock = ({ versionId, filter }: SearchBlockProps): JSX.Elemen const fetchContent = async (): Promise => { // Confirm the final url to be fetched const response = await fetch(`${ENV.OS_RAISE_SEARCHAPI_URL_PREFIX}?q=${query}&version_id=${versionId}&filter=${filter}`) - console.log('here is response:', response) if (!response.ok) { throw new Error('Failed to get search results') @@ -139,20 +137,21 @@ export const SearchBlock = ({ versionId, filter }: SearchBlockProps): JSX.Elemen
  • Location

    -

    {hit._source.teacher_only && `This is ${filter} content`}

    {hit._source.section}

    {hit._source.activity_name}

    {hit._source.lesson_page !== '' && hit._source.lesson_page}

    + {/* The keys for each item below are generated using the item's index in the array */}

    Results

    +

    {hit._source.teacher_only && `This is ${filter} content`}

    {hit.highlight.visible_content.map((content: string) => (

    ))} - {hit.highlight.lesson_page.map((page: string) => ( + {hit.highlight.lesson_page?.map((page: string) => (

    ))} - {hit.highlight.activity_name?.map((activity: string) => ( + {hit.highlight.activity_name.map((activity: string) => (

    ))}
    From 3619b422550d008656d59ec31d8890ca379769cf Mon Sep 17 00:00:00 2001 From: Michael Reyna Date: Thu, 18 Apr 2024 16:13:55 -0500 Subject: [PATCH 08/14] Update to reflect FE interface and student filter --- src/tests/SearchBlock.test.tsx | 114 ++++++++++++++++++++------------- 1 file changed, 71 insertions(+), 43 deletions(-) diff --git a/src/tests/SearchBlock.test.tsx b/src/tests/SearchBlock.test.tsx index f8d165bd..db17b699 100644 --- a/src/tests/SearchBlock.test.tsx +++ b/src/tests/SearchBlock.test.tsx @@ -2,32 +2,18 @@ import { act, fireEvent, render, screen } from '@testing-library/react' import { SearchBlock } from '../components/SearchBlock' const mockStudentAndTeacherQueryResults = { - took: 55, - timed_out: false, - _shards: { - total: 1, - successful: 1, - skipped: 0, - failed: 0 - }, hits: { total: { - value: 37, - relation: 'eq' + value: 37 }, - max_score: 12.129974, hits: [ { - _index: 'test-index2', _id: '1a9844da-0262-49c0-9194-1496f9cfc4ed', - _score: 12.129974, _source: { - content_id: '1a9844da-0262-49c0-9194-1496f9cfc4ed', section: 'Unit 4: Functions', activity_name: 'Lesson 4.16: Different Types of Sequences', lesson_page: '4.16.3: A Sequence Is a Type of Function', - lesson_page_type: 'content', - teacher_only: false + teacher_only: true }, highlight: { lesson_page: [ @@ -43,15 +29,11 @@ const mockStudentAndTeacherQueryResults = { } }, { - _index: 'test-index2', _id: 'f27ad080-9923-48c5-9355-54e4934a95d8', - _score: 11.537502, _source: { - content_id: 'f27ad080-9923-48c5-9355-54e4934a95d8', section: 'Unit 4: Functions', activity_name: 'Lesson 4.14: Sequences', lesson_page: '4.14.2: What Is a Sequence?', - lesson_page_type: 'content', teacher_only: false }, highlight: { @@ -72,31 +54,17 @@ const mockStudentAndTeacherQueryResults = { } const mockTeacherFilterQueryResults = { - took: 55, - timed_out: false, - _shards: { - total: 1, - successful: 1, - skipped: 0, - failed: 0 - }, hits: { total: { - value: 37, - relation: 'eq' + value: 37 }, - max_score: 12.129974, hits: [ { - _index: 'test-index1', _id: '1a9844da-0262-49c0-9194-1496f9cfc4ed', - _score: 12.129974, _source: { - content_id: '1a9844da-0262-49c0-9194-1496f9cfc4ed', section: 'Unit 4: Functions', activity_name: 'Lesson 4.16: Different Types of Sequences', lesson_page: '4.16.3: A Sequence Is a Type of Function', - lesson_page_type: 'content', teacher_only: true }, highlight: { @@ -116,27 +84,58 @@ const mockTeacherFilterQueryResults = { } } +const mockStudentFilterQueryResults = { + hits: { + total: { + value: 37 + }, + hits: [ + { + _id: '1a9844da-0262-49c0-9194-1496f9cfc4ed', + _source: { + section: 'Unit 4: Functions', + activity_name: 'Lesson 4.16: Different Types of Sequences', + lesson_page: '4.16.3: A Sequence Is a Type of Function', + teacher_only: false + }, + highlight: { + lesson_page: [ + '4.16.3: A Sequence Is a Type of Function' + ], + visible_content: [ + 'Activity \nJada and Mai are trying to decide what type of sequence this could be:\n\n\n\n\nterm number\nvalue', + '2\n \n\n\n\n 2\n \n\n 6\n \n\n\n\n 5\n \n\n 18\n \n\n\n\nJada', + 'says: \u201cI think this sequence is geometric because in the value column, each row is 3 times the previous', + 'Do you agree with Jada or Mai? Be prepared to show your reasoning using a graph.', + 'Jada noticed that each value is multiplied by 3 to get to the next row, but the table skips terms.' + ] + } + } + ] + } +} + jest.mock('../lib/env.ts', () => ({ ENV: { OS_RAISE_SEARCHAPI_URL_PREFIX: 'http://searchapi' } })) -// Will need to update the url + describe('search', () => { - it('fetches and displays student and teacher search results from API', async () => { + it('fetches and displays unfiltered results from API', async () => { global.fetch = jest.fn().mockResolvedValue({ ok: true, json: async () => await Promise.resolve(mockStudentAndTeacherQueryResults) }) render( - + ) const queryInput = screen.getByRole('textbox') await act(async () => { fireEvent.change(queryInput, { target: { value: 'math' } }) - fireEvent.click(screen.getByText('Button')) + fireEvent.click(screen.getByText('Search')) }) await screen.findAllByText('Unit 4: Functions') @@ -148,7 +147,9 @@ describe('search', () => { expect(await screen.findByText('Lesson 4.14: Sequences')) expect(await screen.findByText('table skips terms', { exact: false })) expect(await screen.findByText('smallest', { exact: false })) - expect(global.fetch).toHaveBeenCalledWith(`http://searchapi?q=math&version_id=12345&filter=${''}`) + expect(await screen.findByText('Teacher Content')) + expect(await screen.findByText('Content')) + expect(global.fetch).toHaveBeenCalledWith('http://searchapi/v1/search?q=math&version=12345') }) it('fetches and displays teacher filter search results from API', async () => { @@ -164,7 +165,33 @@ describe('search', () => { const queryInput = screen.getByRole('textbox') await act(async () => { fireEvent.change(queryInput, { target: { value: 'math' } }) - fireEvent.click(screen.getByText('Button')) + fireEvent.click(screen.getByText('Search')) + }) + + await screen.findByText('Unit 4: Functions') + const firstHitLessonPage = await screen.findAllByText('4.16.3', { exact: false }) + expect(firstHitLessonPage.length === 1) + expect(await screen.findByText('Lesson 4.16: Different Types of Sequences')) + expect(await screen.findByText('table skips terms', { exact: false })) + expect(await screen.findByText('Teacher Content')) + expect(screen.queryByText('Content')).toBeNull() + expect(global.fetch).toHaveBeenCalledWith('http://searchapi/v1/search?q=math&version=67890&filter=teacher') + }) + + it('fetches and displays student filter search results from API', async () => { + global.fetch = jest.fn().mockResolvedValue({ + ok: true, + json: async () => await Promise.resolve(mockStudentFilterQueryResults) + }) + + render( + + ) + + const queryInput = screen.getByRole('textbox') + await act(async () => { + fireEvent.change(queryInput, { target: { value: 'math' } }) + fireEvent.click(screen.getByText('Search')) }) await screen.findByText('Unit 4: Functions') @@ -172,7 +199,8 @@ describe('search', () => { expect(firstHitLessonPage.length === 1) expect(await screen.findByText('Lesson 4.16: Different Types of Sequences')) expect(await screen.findByText('table skips terms', { exact: false })) - expect(await screen.findByText('This is teacher content')) - expect(global.fetch).toHaveBeenCalledWith('http://searchapi?q=math&version_id=67890&filter=teacher') + expect(await screen.findByText('Content')) + expect(screen.queryByText('Teacher Content')).toBeNull() + expect(global.fetch).toHaveBeenCalledWith('http://searchapi/v1/search?q=math&version=13579&filter=student') }) }) From 3317671c766d66db491075ff0ed2530d4d56de60 Mon Sep 17 00:00:00 2001 From: Michael Reyna Date: Thu, 18 Apr 2024 16:14:30 -0500 Subject: [PATCH 09/14] Update no results style and name --- src/styles/interactives.scss | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/styles/interactives.scss b/src/styles/interactives.scss index 3922d9b6..f3982781 100644 --- a/src/styles/interactives.scss +++ b/src/styles/interactives.scss @@ -185,7 +185,8 @@ math-field::part(menu-toggle) { } .os-search-button { - padding: 4px 16px + padding: 4px 16px; + margin-bottom: 1rem } .os-search-results-container { @@ -209,6 +210,7 @@ math-field::part(menu-toggle) { padding-bottom: .5rem; } -.os-search-no-results { +.os-search-error-message { color: $os-wrong-answer-border-color; + text-align: center; } From a7c76054b3e35460c6485dedb5e3a97a190c8a93 Mon Sep 17 00:00:00 2001 From: Michael Reyna Date: Thu, 18 Apr 2024 16:15:33 -0500 Subject: [PATCH 10/14] Update filter variable if no filter provided --- src/lib/blocks.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/lib/blocks.tsx b/src/lib/blocks.tsx index bcb163c4..a7cb10b8 100644 --- a/src/lib/blocks.tsx +++ b/src/lib/blocks.tsx @@ -373,7 +373,7 @@ export const parseSearchBlock = (element: HTMLElement): JSX.Element | null => { if (!element.classList.contains(OS_RAISE_SEARCH_CLASS)) { return null } - const maybeFilter = element.dataset.filter ?? '' + const maybeFilter = element.dataset.filter ?? undefined return Date: Thu, 18 Apr 2024 16:16:23 -0500 Subject: [PATCH 11/14] Update to account for actual calls to the API --- src/components/SearchBlock.tsx | 109 ++++++++++----------------------- 1 file changed, 32 insertions(+), 77 deletions(-) diff --git a/src/components/SearchBlock.tsx b/src/components/SearchBlock.tsx index 14957890..b71e937c 100644 --- a/src/components/SearchBlock.tsx +++ b/src/components/SearchBlock.tsx @@ -1,7 +1,6 @@ -import { Formik, Form, Field, ErrorMessage } from 'formik' +import { Formik, Form, Field } from 'formik' import { ENV } from '../lib/env' -import React, { useState } from 'react' -// Determine the neccessity of ErrorMessage component from formik +import { useState } from 'react' interface SearchBlockProps { versionId: string @@ -21,89 +20,45 @@ interface HitSource { interface HitHighlight { lesson_page?: string[] - visible_content: string[] - activity_name: string[] + visible_content?: string[] + activity_name?: string[] } -interface HitIdSourceHighlight { +interface Hit { _id: string _source: HitSource highlight: HitHighlight } -interface HitTotalHits { +interface Hits { total: HitValue - hits: HitIdSourceHighlight[] + hits: Hit[] } -interface Hits { - hits: HitTotalHits +interface SearchResults { + hits: Hits } export const SearchBlock = ({ versionId, filter }: SearchBlockProps): JSX.Element => { const [query, setQuery] = useState('') - const [searchResults, setSearchResults] = useState(undefined) + const [searchResults, setSearchResults] = useState(undefined) + const [errorMessage, setErrorMessage] = useState('') const fetchContent = async (): Promise => { - // Confirm the final url to be fetched - const response = await fetch(`${ENV.OS_RAISE_SEARCHAPI_URL_PREFIX}?q=${query}&version_id=${versionId}&filter=${filter}`) + try { + const response = filter !== undefined + ? await fetch(`${ENV.OS_RAISE_SEARCHAPI_URL_PREFIX}/v1/search?q=${query}&version=${versionId}&filter=${filter}`) + : await fetch(`${ENV.OS_RAISE_SEARCHAPI_URL_PREFIX}/v1/search?q=${query}&version=${versionId}`) - if (!response.ok) { - throw new Error('Failed to get search results') - } + if (!response.ok) { + setErrorMessage('Failed to get search results, please try again.') + throw new Error('Failed to get search results') + } - const data: Hits = await response.json() - // const data: Hits = { - // hits: { - // total: { - // value: 34 - // }, - // hits: [ - // { - // _id: '1a9844da-0262-49c0-9194-1496f9cfc4ed', - // _source: { - // section: 'Unit 4: Functions', - // activity_name: 'Lesson 4.16: Different Types of Sequences', - // lesson_page: '4.16.3: A Sequence Is a Type of Function', - // teacher_only: true - // }, - // highlight: { - // lesson_page: [ - // '4.16.3: A Sequence Is a Type of Function' - // ], - // visible_content: [ - // 'Activity \nJada and Mai are trying to decide what type of sequence this could be:\n\n\n\n\nterm number\nvalue', - // '2\n \n\n\n\n 2\n \n\n 6\n \n\n\n\n 5\n \n\n 18\n \n\n\n\nJada', - // 'says: \u201cI think this sequence is geometric because in the value column, each row is 3 times the previous', - // 'Do you agree with Jada or Mai? Be prepared to show your reasoning using a graph.', - // 'Jada noticed that each value is multiplied by 3 to get to the next row, but the table skips terms.' - // ] - // } - // }, - // { - // _id: 'f27ad080-9923-48c5-9355-54e4934a95d8', - // _source: { - // section: 'Unit 4: Functions', - // activity_name: 'Lesson 4.14: Sequences', - // lesson_page: '4.14.2: What Is a Sequence?', - // teacher_only: false - // }, - // highlight: { - // lesson_page: [ - // '4.14.2: What Is a Sequence?' - // ], - // visible_content: [ - // 'What is the smallest number of moves in which you are able to complete the puzzle with 3 discs?', - // 'Enter your answer here:\n\n\nCompare your answer:\n15 moves\n\n\n\n\n\n\n\n\n\nJada says she used the solution for', - // 'Enter your answer here:\n\n\nCompare your answer:\nJada moved the tower of discs 1\u20133, then moved disc 4,', - // 'The number of moves is 31 because \\(2\\cdot15+1=31\\).', - // 'The term (of a sequence) is one of the numbers in a sequence.' - // ] - // } - // } - // ] - // } - // } - setSearchResults(data) + const data: SearchResults = await response.json() + setSearchResults(data) + } catch (error) { + console.error('Error fetching search results:', error) + } } return (
    @@ -111,20 +66,21 @@ export const SearchBlock = ({ versionId, filter }: SearchBlockProps): JSX.Elemen initialValues={{ response: '' }} onSubmit={fetchContent} > - {({ setFieldValue }) => ( + {({ setFieldValue, isSubmitting }) => (
    ) => { setQuery(e.target.value) + setErrorMessage('') void setFieldValue('response', e.target.value) }} /> -
    - +
    + {errorMessage !== '' &&

    {errorMessage}

    } )} @@ -143,15 +99,14 @@ export const SearchBlock = ({ versionId, filter }: SearchBlockProps): JSX.Elemen
    {/* The keys for each item below are generated using the item's index in the array */} -

    Results

    -

    {hit._source.teacher_only && `This is ${filter} content`}

    - {hit.highlight.visible_content.map((content: string) => ( +

    {hit._source.teacher_only ? 'Teacher Content' : 'Content'}

    + {hit.highlight.visible_content?.map((content: string) => (

    ))} {hit.highlight.lesson_page?.map((page: string) => (

    ))} - {hit.highlight.activity_name.map((activity: string) => ( + {hit.highlight.activity_name?.map((activity: string) => (

    ))}
    @@ -162,7 +117,7 @@ export const SearchBlock = ({ versionId, filter }: SearchBlockProps): JSX.Elemen } {searchResults !== undefined && searchResults.hits.total.value === 0 &&
    -

    Your query did not produce any results. Please try again.

    +

    Your query did not produce any results. Please try again.

    } From aa1d67178d8338124ddf82d173e526a7be3cc9d7 Mon Sep 17 00:00:00 2001 From: Michael Reyna Date: Fri, 19 Apr 2024 12:25:27 -0500 Subject: [PATCH 12/14] Update button and no results message styling --- src/styles/interactives.scss | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/styles/interactives.scss b/src/styles/interactives.scss index f3982781..59a38968 100644 --- a/src/styles/interactives.scss +++ b/src/styles/interactives.scss @@ -184,11 +184,6 @@ math-field::part(menu-toggle) { padding: 1rem } -.os-search-button { - padding: 4px 16px; - margin-bottom: 1rem -} - .os-search-results-container { margin-top: 1rem; } @@ -214,3 +209,8 @@ math-field::part(menu-toggle) { color: $os-wrong-answer-border-color; text-align: center; } + +.os-search-no-results-message { + color: $os-selected-focus-border-color; + text-align: center; +} From 02dd35224643a9fdabdee2c0f0d8c6ce0bf3e6e6 Mon Sep 17 00:00:00 2001 From: Michael Reyna Date: Fri, 19 Apr 2024 12:25:44 -0500 Subject: [PATCH 13/14] Remove redundant undefined --- src/lib/blocks.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/lib/blocks.tsx b/src/lib/blocks.tsx index a7cb10b8..88335bac 100644 --- a/src/lib/blocks.tsx +++ b/src/lib/blocks.tsx @@ -373,7 +373,7 @@ export const parseSearchBlock = (element: HTMLElement): JSX.Element | null => { if (!element.classList.contains(OS_RAISE_SEARCH_CLASS)) { return null } - const maybeFilter = element.dataset.filter ?? undefined + const maybeFilter = element.dataset.filter return Date: Fri, 19 Apr 2024 12:26:38 -0500 Subject: [PATCH 14/14] Add new submit handler and disabled form state --- src/components/SearchBlock.tsx | 32 ++++++++++++++++++++++++++------ 1 file changed, 26 insertions(+), 6 deletions(-) diff --git a/src/components/SearchBlock.tsx b/src/components/SearchBlock.tsx index b71e937c..a63fdffc 100644 --- a/src/components/SearchBlock.tsx +++ b/src/components/SearchBlock.tsx @@ -50,21 +50,31 @@ export const SearchBlock = ({ versionId, filter }: SearchBlockProps): JSX.Elemen : await fetch(`${ENV.OS_RAISE_SEARCHAPI_URL_PREFIX}/v1/search?q=${query}&version=${versionId}`) if (!response.ok) { - setErrorMessage('Failed to get search results, please try again.') throw new Error('Failed to get search results') } const data: SearchResults = await response.json() setSearchResults(data) } catch (error) { + setErrorMessage('Failed to get search results, please try again.') console.error('Error fetching search results:', error) } } + + const handleSubmit = async (): Promise => { + if (query.trim() === '') { + setErrorMessage('Input cannot be empty') + return + } + setSearchResults(undefined) + setErrorMessage('') + await fetchContent() + } return (
    {({ setFieldValue, isSubmitting }) => (
    @@ -76,10 +86,20 @@ export const SearchBlock = ({ versionId, filter }: SearchBlockProps): JSX.Elemen setErrorMessage('') void setFieldValue('response', e.target.value) }} + disabled={isSubmitting} /> -
    - -
    + {isSubmitting + ?
    +
    +
    + Searching... +
    +
    +
    + :
    + +
    + } {errorMessage !== '' &&

    {errorMessage}

    }
    )} @@ -117,7 +137,7 @@ export const SearchBlock = ({ versionId, filter }: SearchBlockProps): JSX.Elemen } {searchResults !== undefined && searchResults.hits.total.value === 0 &&
    -

    Your query did not produce any results. Please try again.

    +

    Your query did not produce any results. Please try again.

    }