Skip to content

Commit

Permalink
Display all exemplar files (#1908)
Browse files Browse the repository at this point in the history
* Display all exemplar files

* Only show header if there's multiple files

* Fix failing tests

* Rename GitFile to MentoringSessionExemplarFile

* Use test_helper

* Inline exemplar files list

Co-authored-by: Jeremy Walker <jez.walker@gmail.com>
  • Loading branch information
kntsoriano and iHiD committed Sep 24, 2021
1 parent aacf716 commit c319e73
Show file tree
Hide file tree
Showing 22 changed files with 269 additions and 54 deletions.
14 changes: 14 additions & 0 deletions app/css/components/mentor/discussion.css
Expand Up @@ -527,6 +527,20 @@
}
& #panel-guidance {
@apply py-16 px-24;

& .exemplar-files {
& .filename {
@apply border-1 border-borderColor5;
@apply bg-backgroundColorE;
@apply text-15 font-mono font-semibold;
@apply py-4 px-20;
border-radius: 5px 5px 0 0;
}
& .filename + pre {
@apply border-t-0;
border-radius: 0 0 5px 5px;
}
}
}

& .mentoring-request-section {
Expand Down
17 changes: 16 additions & 1 deletion app/helpers/react_components/mentoring/session.rb
Expand Up @@ -31,7 +31,7 @@ def to_s
anonymous_mode: discussion&.anonymous_mode?
),
mentor_solution: mentor_solution,
exemplar_solution: exercise.exemplar_files.values.first,
exemplar_files: ExemplarFileList.new(exercise.exemplar_files),
notes: exercise.mentoring_notes_content,
out_of_date: solution.out_of_date?,
download_command: solution.mentor_download_cmd,
Expand Down Expand Up @@ -96,6 +96,21 @@ def student
def scratchpad
ScratchpadPage.new(about: exercise)
end

class ExemplarFileList
extend Mandate::InitializerInjector

initialize_with :files

def as_json
files.map do |filename, content|
{
filename: filename.gsub(%r{^\.meta/}, ''),
content: content
}
end
end
end
end
end
end
7 changes: 4 additions & 3 deletions app/javascript/components/mentoring/Session.tsx
Expand Up @@ -26,6 +26,7 @@ import {
MentorDiscussion as Discussion,
MentorSessionTrack as Track,
MentorSessionExercise as Exercise,
MentoringSessionExemplarFile,
} from '../types'

import { useIterationScrolling } from './session/useIterationScrolling'
Expand Down Expand Up @@ -60,7 +61,7 @@ export type SessionProps = {
notes: string
outOfDate: boolean
mentorSolution: CommunitySolution
exemplarSolution: string
exemplarFiles: readonly MentoringSessionExemplarFile[]
request: Request
scratchpad: Scratchpad
downloadCommand: string
Expand All @@ -86,7 +87,7 @@ export const Session = (props: SessionProps): JSX.Element => {
discussion,
notes,
mentorSolution,
exemplarSolution,
exemplarFiles,
outOfDate,
request,
scratchpad,
Expand Down Expand Up @@ -207,7 +208,7 @@ export const Session = (props: SessionProps): JSX.Element => {
<Guidance
notes={notes}
mentorSolution={mentorSolution}
exemplarSolution={exemplarSolution}
exemplarFiles={exemplarFiles}
language={track.highlightjsLanguage}
links={links}
/>
Expand Down
44 changes: 24 additions & 20 deletions app/javascript/components/mentoring/session/Guidance.tsx
@@ -1,9 +1,13 @@
import React, { useState, useCallback } from 'react'
import { Accordion } from '../../common/Accordion'
import { MentorNotes } from './MentorNotes'
import { CommunitySolution as CommunitySolutionProps } from '../../types'
import {
CommunitySolution as CommunitySolutionProps,
MentoringSessionExemplarFile,
} from '../../types'
import { CommunitySolution, GraphicalIcon } from '../../common'
import { useHighlighting } from '../../../utils/highlight'
import { ExemplarFilesList } from './guidance/ExemplarFilesList'

const AccordionHeader = ({
isOpen,
Expand All @@ -28,30 +32,32 @@ type Links = {
improveNotes: string
}

export type Props = {
notes: string
mentorSolution?: CommunitySolutionProps
exemplarFiles: readonly MentoringSessionExemplarFile[]
links: Links
language: string
feedback?: any
}

export const Guidance = ({
notes,
mentorSolution,
exemplarSolution,
exemplarFiles,
links,
language,
feedback = false,
}: {
notes: string
mentorSolution?: CommunitySolutionProps
exemplarSolution: string
links: Links
language: string
feedback?: any
}): JSX.Element => {
}: Props): JSX.Element => {
const ref = useHighlighting<HTMLDivElement>()
const [accordionState, setAccordionState] = useState([
{
id: 'exemplar-solution',
isOpen: exemplarSolution != null,
id: 'exemplar-files',
isOpen: exemplarFiles.length !== 0,
},
{
id: 'notes',
isOpen: exemplarSolution == null,
isOpen: exemplarFiles.length === 0,
},
{
id: 'mentor-solution',
Expand Down Expand Up @@ -93,14 +99,14 @@ export const Guidance = ({

return (
<div ref={ref}>
{exemplarSolution ? (
{exemplarFiles.length !== 0 ? (
<Accordion
id="exemplar-solution"
isOpen={isOpen('exemplar-solution')}
id="exemplar-files"
isOpen={isOpen('exemplar-files')}
onClick={handleClick}
>
<AccordionHeader
isOpen={isOpen('exemplar-solution')}
isOpen={isOpen('exemplar-files')}
title="The exemplar solution"
/>
<Accordion.Panel>
Expand All @@ -109,9 +115,7 @@ export const Guidance = ({
Try and guide the student towards this solution. It is the best
place for them to reach at this point during the Track.
</p>
<pre className="overflow-auto">
<code className={language}>{exemplarSolution}</code>
</pre>
<ExemplarFilesList files={exemplarFiles} language={language} />
</div>
</Accordion.Panel>
</Accordion>
Expand Down
@@ -0,0 +1,44 @@
import React from 'react'
import { MentoringSessionExemplarFile } from '../../../types'

export type Props = {
files: readonly MentoringSessionExemplarFile[]
language: string
}

export const ExemplarFilesList = ({ files, language }: Props): JSX.Element => {
return (
<React.Fragment>
{files.map((file) => (
<ExemplarFile key={file.filename}>
{files.length > 1 ? (
<ExemplarFile.Header filename={file.filename} />
) : null}
<ExemplarFile.Content content={file.content} language={language} />
</ExemplarFile>
))}
</React.Fragment>
)
}

const ExemplarFile = ({ children }: { children?: React.ReactNode }) => {
return <div className="exemplar-files">{children}</div>
}

ExemplarFile.Header = ({ filename }: { filename: string }) => {
return <div className="filename">{filename}</div>
}

ExemplarFile.Content = ({
content,
language,
}: {
content: string
language: string
}) => {
return (
<pre className="overflow-auto">
<code className={language}>{content}</code>
</pre>
)
}
5 changes: 5 additions & 0 deletions app/javascript/components/types.ts
Expand Up @@ -656,3 +656,8 @@ export type Notification = {
}

type NotificationImageType = 'icon' | 'avatar'

export type MentoringSessionExemplarFile = {
filename: string
content: string
}
5 changes: 4 additions & 1 deletion app/javascript/packs/internal.tsx
Expand Up @@ -22,6 +22,7 @@ import {
// SiteUpdate,
CommunicationPreferences,
User,
MentoringSessionExemplarFile,
// TrackContribution,
} from '../components/types'

Expand Down Expand Up @@ -137,7 +138,9 @@ initReact({
userHandle={data.user_handle}
discussion={camelizeKeysAs<MentorDiscussion>(data.discussion)}
mentorSolution={camelizeKeysAs<CommunitySolution>(data.mentor_solution)}
exemplarSolution={data.exemplar_solution}
exemplarFiles={camelizeKeysAs<readonly MentoringSessionExemplarFile[]>(
data.exemplar_files
)}
student={camelizeKeysAs<MentoringSessionStudent>(data.student)}
track={camelizeKeysAs<MentorSessionTrack>(data.track)}
exercise={camelizeKeysAs<MentorSessionExercise>(data.exercise)}
Expand Down
37 changes: 34 additions & 3 deletions test/helpers/react_components/mentoring/session_test.rb
Expand Up @@ -42,7 +42,12 @@ class SessionTest < ReactComponentTestCase
tests: solution.tests,
student: SerializeStudent.(student, mentor, relationship: nil, anonymous_mode: false, user_track: user_track),
mentor_solution: nil,
exemplar_solution: exercise.exemplar_files.values.first,
exemplar_files: [
{
filename: "exemplar.rb",
content: exercise.exemplar_files.values.first
}
],
notes: "<p>These are notes for lasagna.</p>\n",
out_of_date: false,
download_command: solution.mentor_download_cmd,
Expand Down Expand Up @@ -109,7 +114,12 @@ class SessionTest < ReactComponentTestCase
tests: solution.tests,
student: SerializeStudent.(student, mentor, relationship: nil, anonymous_mode: false, user_track: user_track),
mentor_solution: nil,
exemplar_solution: exercise.exemplar_files.values.first,
exemplar_files: [
{
filename: "exemplar.rb",
content: exercise.exemplar_files.values.first
}
],
notes: "<p>These are notes for lasagna.</p>\n",
out_of_date: false,
download_command: solution.mentor_download_cmd,
Expand Down Expand Up @@ -173,7 +183,12 @@ class SessionTest < ReactComponentTestCase
tests: solution.tests,
student: SerializeStudent.(student, mentor, relationship: nil, anonymous_mode: false, user_track: user_track),
mentor_solution: nil,
exemplar_solution: exercise.exemplar_files.values.first,
exemplar_files: [
{
filename: "exemplar.rb",
content: exercise.exemplar_files.values.first
}
],
notes: "<p>These are notes for lasagna.</p>\n",
out_of_date: false,
download_command: solution.mentor_download_cmd,
Expand All @@ -194,5 +209,21 @@ class SessionTest < ReactComponentTestCase
}
)
end

test "#as_json serializes files" do
files = {
".meta/exemplar1.rb" => "class Ruby\nend"
}

assert_equal(
[
{
filename: "exemplar1.rb",
content: "class Ruby\nend"
}
],
ReactComponents::Mentoring::Session::ExemplarFileList.new(files).as_json
)
end
end
end
7 changes: 7 additions & 0 deletions test/javascript/components/mentoring/Session.test.js
Expand Up @@ -71,6 +71,7 @@ test('highlights currently selected iteration', async () => {
student={student}
iterations={iterations}
discussion={discussion}
exemplarFiles={[]}
links={{}}
/>
)
Expand Down Expand Up @@ -134,6 +135,7 @@ test('shows back button', async () => {
iterations={iterations}
discussion={discussion}
scratchpad={scratchpad}
exemplarFiles={[]}
/>
)
queryCache.cancelQueries()
Expand Down Expand Up @@ -200,6 +202,7 @@ test('hides latest label if on old iteration', async () => {
iterations={iterations}
discussion={discussion}
scratchpad={scratchpad}
exemplarFiles={[]}
/>
)
await awaitPopper()
Expand Down Expand Up @@ -268,6 +271,7 @@ test('switches to posts tab when comment success', async () => {
iterations={iterations}
discussion={discussion}
scratchpad={scratchpad}
exemplarFiles={[]}
/>
)

Expand Down Expand Up @@ -346,6 +350,7 @@ test('switches tabs', async () => {
iterations={iterations}
discussion={discussion}
scratchpad={scratchpad}
exemplarFiles={[]}
/>
)
userEvent.click(screen.getByRole('tab', { name: 'Scratchpad' }))
Expand Down Expand Up @@ -420,6 +425,7 @@ test('go to previous iteration', async () => {
iterations={iterations}
discussion={discussion}
scratchpad={scratchpad}
exemplarFiles={[]}
/>
)
})
Expand Down Expand Up @@ -490,6 +496,7 @@ test('go to next iteration', async () => {
iterations={iterations}
discussion={discussion}
scratchpad={scratchpad}
exemplarFiles={[]}
/>
)
userEvent.click(screen.getByRole('button', { name: 'Go to iteration 1' }))
Expand Down

0 comments on commit c319e73

Please sign in to comment.