From c2c09ddb6920cb2d9f95b5d2adf51b86c4eb8f3c Mon Sep 17 00:00:00 2001 From: Ashar Fuadi Date: Mon, 4 Sep 2023 09:34:54 +0200 Subject: [PATCH] feat(client,server): revamp course chapter submission mechanism --- .../bundle/ItemSubmissionResource.java | 8 +- .../ContentWithTopbar/ContentWithTopbar.jsx | 25 +-- .../FullWidthPageLayout.scss | 1 - .../ProblemStatementCard.jsx | 8 +- .../ProblemSubmissionCard.jsx | 24 +++ .../Bundle/ProblemWorksheetCard.jsx | 12 +- .../Programming/ProblemWorksheetCard.jsx | 3 + .../SubmissionDetails.jsx} | 6 +- .../SubmissionDetails.scss} | 2 +- .../Programming/SubmissionDetails.jsx | 32 +-- .../src/components/Topbar/Topbar.scss | 7 +- .../modules/api/jerahmeel/submissionBundle.js | 4 +- .../src/modules/api/sandalphon/lesson.js | 2 +- .../src/modules/api/sandalphon/problem.js | 2 +- .../ContestSubmissionSummaryPage.jsx | 4 +- .../CourseChaptersSidebar.jsx | 40 ++-- .../CourseChaptersSidebar.scss | 25 ++- .../single/CourseOverview/CourseOverview.scss | 6 +- .../single/SingleCourseContentRoutes.jsx | 10 +- .../courses/single/SingleCourseRoutes.jsx | 8 +- .../courses/single/SingleCourseRoutes.scss | 15 +- .../single/MainSingleCourseChapterRoutes.jsx | 4 +- .../single/SingleCourseChapterRoutes.jsx | 58 +----- .../single/SingleCourseChapterRoutes.scss | 14 -- .../single/lessons/ChapterLessonRoutes.jsx | 7 +- .../ChapterLessonsPage/ChapterLessonsPage.jsx | 121 ------------ .../ChapterLessonsPage.test.jsx | 115 ----------- .../lessons/modules/chapterLessonActions.js | 15 -- .../modules/chapterLessonActions.test.js | 15 -- .../ChapterLessonPage/ChapterLessonPage.jsx | 40 +++- .../ChapterLessonPage/ChapterLessonPage.scss | 25 +++ .../ChapterProblemCard.scss | 0 .../single/problems/ChapterProblemRoutes.jsx | 7 +- .../ChapterProblemsPage.jsx | 114 ----------- .../ChapterProblemsPage.test.jsx | 98 --------- .../problems/modules/chapterProblemActions.js | 7 - .../modules/chapterProblemActions.test.js | 15 -- .../single/Bundle/ChapterProblemPage.jsx | 98 ++------- .../single/Bundle/ChapterProblemPage.scss | 11 ++ .../ChapterProblemStatementPage.jsx | 100 ++++++++++ .../ChapterProblemStatementPage.scss | 4 + .../ChapterProblemSubmissionRoutes.jsx | 15 ++ .../ChapterProblemSubmissionsPage.jsx | 108 ++++++++++ .../ChapterProblemSubmissionsPage.scss | 3 + .../chapterProblemSubmissionActions.js} | 28 +-- .../ChapterProblemPage/ChapterProblemPage.jsx | 41 +++- .../ChapterProblemPage.scss | 23 +++ .../single/Programming/ChapterProblemPage.jsx | 97 +-------- .../Programming/ChapterProblemPage.scss | 10 + .../ChapterProblemStatementPage.jsx | 48 +++++ .../ChapterProblemStatementPage.scss | 4 + .../ChapterProblemStatementRoutes.jsx | 26 +++ .../ChapterProblemStatementRoutes.scss | 3 + .../ChapterProblemWorkspacePage.jsx | 64 ++++++ .../ChapterProblemSubmissionRoutes.jsx | 28 +++ .../ChapterProblemSubmissionRoutes.scss | 3 + .../ChapterProblemSubmissionsPage.jsx | 156 +++++++++++++++ .../ChapterProblemSubmissionsPage.test.jsx} | 38 ++-- .../ChapterProblemSubmissionsTable.jsx} | 18 +- .../chapterProblemSubmissionActions.js} | 28 +-- .../chapterProblemSubmissionActions.test.js} | 14 +- .../ChapterProblemSubmissionPage.jsx} | 50 +++-- .../ChapterProblemSubmissionPage.test.jsx} | 26 +-- .../ChapterLessonCard/ChapterLessonCard.jsx | 9 +- .../ChapterLessonCard/ChapterLessonCard.scss | 13 ++ .../ChapterProblemCard/ChapterProblemCard.jsx | 3 + .../ChapterProblemCard.scss | 13 ++ .../ChapterResourcesPage.jsx | 109 ++++++++++ .../ChapterResourcesPage.scss | 25 +++ .../ChapterResourcesPage.test.jsx | 125 ++++++++++++ .../modules/chapterResourceActions.js | 13 ++ .../modules/chapterResourceActions.test.js | 40 ++++ .../results/ChapterItemSubmissionRoutes.jsx | 22 --- .../ChapterSubmissionSummaryPage.jsx | 107 ---------- .../ChapterSubmissionsPage.jsx | 180 ----------------- .../submissions/ChapterSubmissionRoutes.jsx | 30 --- .../ChapterSubmissionsPage.jsx | 187 ------------------ .../single/SingleProblemSetRoutes.scss | 9 - .../ProblemSubmissionSummaryPage.jsx | 4 +- judgels-client/src/styles/index.scss | 14 ++ 80 files changed, 1332 insertions(+), 1514 deletions(-) create mode 100644 judgels-client/src/components/ProblemWorksheetCard/Bundle/ProblemSubmissionCard/ProblemSubmissionCard.jsx rename judgels-client/src/components/SubmissionDetails/Bundle/{ProblemSubmissionsCard/ProblemSubmissionCard.jsx => SubmissionDetails/SubmissionDetails.jsx} (95%) rename judgels-client/src/components/SubmissionDetails/Bundle/{ProblemSubmissionsCard/ProblemSubmissionCard.scss => SubmissionDetails/SubmissionDetails.scss} (94%) delete mode 100644 judgels-client/src/routes/courses/courses/single/chapters/single/SingleCourseChapterRoutes.scss delete mode 100644 judgels-client/src/routes/courses/courses/single/chapters/single/lessons/ChapterLessonsPage/ChapterLessonsPage.jsx delete mode 100644 judgels-client/src/routes/courses/courses/single/chapters/single/lessons/ChapterLessonsPage/ChapterLessonsPage.test.jsx create mode 100644 judgels-client/src/routes/courses/courses/single/chapters/single/lessons/single/ChapterLessonPage/ChapterLessonPage.scss delete mode 100644 judgels-client/src/routes/courses/courses/single/chapters/single/problems/ChapterProblemCard/ChapterProblemCard.scss delete mode 100644 judgels-client/src/routes/courses/courses/single/chapters/single/problems/ChapterProblemsPage/ChapterProblemsPage.jsx delete mode 100644 judgels-client/src/routes/courses/courses/single/chapters/single/problems/ChapterProblemsPage/ChapterProblemsPage.test.jsx create mode 100644 judgels-client/src/routes/courses/courses/single/chapters/single/problems/single/Bundle/ChapterProblemPage.scss create mode 100644 judgels-client/src/routes/courses/courses/single/chapters/single/problems/single/Bundle/ChapterProblemStatementPage/ChapterProblemStatementPage.jsx create mode 100644 judgels-client/src/routes/courses/courses/single/chapters/single/problems/single/Bundle/ChapterProblemStatementPage/ChapterProblemStatementPage.scss create mode 100644 judgels-client/src/routes/courses/courses/single/chapters/single/problems/single/Bundle/submissions/ChapterProblemSubmissionRoutes.jsx create mode 100644 judgels-client/src/routes/courses/courses/single/chapters/single/problems/single/Bundle/submissions/ChapterProblemSubmissionsPage/ChapterProblemSubmissionsPage.jsx create mode 100644 judgels-client/src/routes/courses/courses/single/chapters/single/problems/single/Bundle/submissions/ChapterProblemSubmissionsPage/ChapterProblemSubmissionsPage.scss rename judgels-client/src/routes/courses/courses/single/chapters/single/{results/modules/chapterSubmissionActions.js => problems/single/Bundle/submissions/modules/chapterProblemSubmissionActions.js} (52%) create mode 100644 judgels-client/src/routes/courses/courses/single/chapters/single/problems/single/ChapterProblemPage/ChapterProblemPage.scss create mode 100644 judgels-client/src/routes/courses/courses/single/chapters/single/problems/single/Programming/ChapterProblemPage.scss create mode 100644 judgels-client/src/routes/courses/courses/single/chapters/single/problems/single/Programming/ChapterProblemStatementPage/ChapterProblemStatementPage.jsx create mode 100644 judgels-client/src/routes/courses/courses/single/chapters/single/problems/single/Programming/ChapterProblemStatementPage/ChapterProblemStatementPage.scss create mode 100644 judgels-client/src/routes/courses/courses/single/chapters/single/problems/single/Programming/ChapterProblemStatementRoutes.jsx create mode 100644 judgels-client/src/routes/courses/courses/single/chapters/single/problems/single/Programming/ChapterProblemStatementRoutes.scss create mode 100644 judgels-client/src/routes/courses/courses/single/chapters/single/problems/single/Programming/ChapterProblemWorkspacePage/ChapterProblemWorkspacePage.jsx create mode 100644 judgels-client/src/routes/courses/courses/single/chapters/single/problems/single/Programming/submissions/ChapterProblemSubmissionRoutes.jsx create mode 100644 judgels-client/src/routes/courses/courses/single/chapters/single/problems/single/Programming/submissions/ChapterProblemSubmissionRoutes.scss create mode 100644 judgels-client/src/routes/courses/courses/single/chapters/single/problems/single/Programming/submissions/ChapterProblemSubmissionsPage/ChapterProblemSubmissionsPage.jsx rename judgels-client/src/routes/courses/courses/single/chapters/single/{submissions/ChapterSubmissionsPage/ChapterSubmissionsPage.test.jsx => problems/single/Programming/submissions/ChapterProblemSubmissionsPage/ChapterProblemSubmissionsPage.test.jsx} (78%) rename judgels-client/src/routes/courses/courses/single/chapters/single/{submissions/ChapterSubmissionsTable/ChapterSubmissionsTable.jsx => problems/single/Programming/submissions/ChapterProblemSubmissionsTable/ChapterProblemSubmissionsTable.jsx} (76%) rename judgels-client/src/routes/courses/courses/single/chapters/single/{submissions/modules/chapterSubmissionActions.js => problems/single/Programming/submissions/modules/chapterProblemSubmissionActions.js} (79%) rename judgels-client/src/routes/courses/courses/single/chapters/single/{submissions/modules/chapterSubmissionActions.test.js => problems/single/Programming/submissions/modules/chapterProblemSubmissionActions.test.js} (68%) rename judgels-client/src/routes/courses/courses/single/chapters/single/{submissions/single/ChapterSubmissionPage/ChapterSubmissionPage.jsx => problems/single/Programming/submissions/single/ChapterProblemSubmissionPage/ChapterProblemSubmissionPage.jsx} (51%) rename judgels-client/src/routes/courses/courses/single/chapters/single/{submissions/single/ChapterSubmissionPage/ChapterSubmissionPage.test.jsx => problems/single/Programming/submissions/single/ChapterProblemSubmissionPage/ChapterProblemSubmissionPage.test.jsx} (66%) rename judgels-client/src/routes/courses/courses/single/chapters/single/{lessons => resources}/ChapterLessonCard/ChapterLessonCard.jsx (73%) create mode 100644 judgels-client/src/routes/courses/courses/single/chapters/single/resources/ChapterLessonCard/ChapterLessonCard.scss rename judgels-client/src/routes/courses/courses/single/chapters/single/{problems => resources}/ChapterProblemCard/ChapterProblemCard.jsx (95%) create mode 100644 judgels-client/src/routes/courses/courses/single/chapters/single/resources/ChapterProblemCard/ChapterProblemCard.scss create mode 100644 judgels-client/src/routes/courses/courses/single/chapters/single/resources/ChapterResourcesPage/ChapterResourcesPage.jsx create mode 100644 judgels-client/src/routes/courses/courses/single/chapters/single/resources/ChapterResourcesPage/ChapterResourcesPage.scss create mode 100644 judgels-client/src/routes/courses/courses/single/chapters/single/resources/ChapterResourcesPage/ChapterResourcesPage.test.jsx create mode 100644 judgels-client/src/routes/courses/courses/single/chapters/single/resources/modules/chapterResourceActions.js create mode 100644 judgels-client/src/routes/courses/courses/single/chapters/single/resources/modules/chapterResourceActions.test.js delete mode 100644 judgels-client/src/routes/courses/courses/single/chapters/single/results/ChapterItemSubmissionRoutes.jsx delete mode 100644 judgels-client/src/routes/courses/courses/single/chapters/single/results/ChapterSubmissionSummaryPage/ChapterSubmissionSummaryPage.jsx delete mode 100644 judgels-client/src/routes/courses/courses/single/chapters/single/results/ChapterSubmissionsPage/ChapterSubmissionsPage.jsx delete mode 100644 judgels-client/src/routes/courses/courses/single/chapters/single/submissions/ChapterSubmissionRoutes.jsx delete mode 100644 judgels-client/src/routes/courses/courses/single/chapters/single/submissions/ChapterSubmissionsPage/ChapterSubmissionsPage.jsx diff --git a/judgels-backends/judgels-server-app/src/main/java/judgels/jerahmeel/submission/bundle/ItemSubmissionResource.java b/judgels-backends/judgels-server-app/src/main/java/judgels/jerahmeel/submission/bundle/ItemSubmissionResource.java index 2c599fd44..266cc33b0 100644 --- a/judgels-backends/judgels-server-app/src/main/java/judgels/jerahmeel/submission/bundle/ItemSubmissionResource.java +++ b/judgels-backends/judgels-server-app/src/main/java/judgels/jerahmeel/submission/bundle/ItemSubmissionResource.java @@ -195,6 +195,7 @@ public SubmissionSummaryResponse getSubmissionSummary( @QueryParam("containerJid") String containerJid, @QueryParam("problemJid") Optional problemJid, @QueryParam("username") Optional username, + @QueryParam("problemAlias") Optional problemAlias, @QueryParam("language") Optional language) { String actorJid = actorChecker.check(authHeader); @@ -211,8 +212,11 @@ public SubmissionSummaryResponse getSubmissionSummary( List problemJids; if (problemJid.isPresent()) { problemJids = ImmutableList.of(problemJid.get()); - submissions = submissionStore - .getLatestSubmissionsByUserForProblemInContainer(containerJid, problemJid.get(), userJid); + submissions = submissionStore.getLatestSubmissionsByUserForProblemInContainer(containerJid, problemJid.get(), userJid); + } else if (problemAlias.isPresent()) { + String problemJidFromAlias = getProblemJidByAlias(containerJid, problemAlias.get()).orElse(""); + problemJids = List.of(problemJidFromAlias); + submissions = submissionStore.getLatestSubmissionsByUserForProblemInContainer(containerJid, problemJidFromAlias, userJid); } else { problemJids = chapterProblemStore.getBundleProblemJids(containerJid); submissions = submissionStore.getLatestSubmissionsByUserInContainer(containerJid, userJid); diff --git a/judgels-client/src/components/ContentWithTopbar/ContentWithTopbar.jsx b/judgels-client/src/components/ContentWithTopbar/ContentWithTopbar.jsx index b4e0507b5..d6bf1875b 100644 --- a/judgels-client/src/components/ContentWithTopbar/ContentWithTopbar.jsx +++ b/judgels-client/src/components/ContentWithTopbar/ContentWithTopbar.jsx @@ -3,12 +3,10 @@ import { Redirect, Switch, withRouter } from 'react-router'; import { Topbar } from '../Topbar/Topbar'; -function ContentAndTopbar({ className, contentHeader, topbarElement, contentElement }) { +function ContentAndTopbar({ className, topbarElement, contentElement }) { return (
- {contentHeader} {topbarElement} -
{contentElement}
); @@ -19,7 +17,7 @@ function resolveUrl(parentPath, childPath) { return (parentPath + '/' + actualChildPath).replace(/\/\/+/g, '/'); } -function ContentWithTopbar({ match, location, className, contentHeader, items }) { +function ContentWithTopbar({ match, location, className, items }) { const renderTopbar = () => { const topbarItems = items .filter(item => !item.disabled) @@ -46,12 +44,10 @@ function ContentWithTopbar({ match, location, className, contentHeader, items }) const redirect = items[0].id !== '@' && ; return ( -
- - {redirect} - {components} - -
+ + {redirect} + {components} + ); }; @@ -69,14 +65,7 @@ function ContentWithTopbar({ match, location, className, contentHeader, items }) return currentPath.substring(match.url.length + 1, nextSlashPos); }; - return ( - - ); + return ; } export default withRouter(ContentWithTopbar); diff --git a/judgels-client/src/components/FullWidthPageLayout/FullWidthPageLayout.scss b/judgels-client/src/components/FullWidthPageLayout/FullWidthPageLayout.scss index 0f0f781e9..108fb53aa 100644 --- a/judgels-client/src/components/FullWidthPageLayout/FullWidthPageLayout.scss +++ b/judgels-client/src/components/FullWidthPageLayout/FullWidthPageLayout.scss @@ -1,6 +1,5 @@ @import '../../styles/base'; .layout-full-width-page { - max-width: 1260px; min-height: 100vh; } diff --git a/judgels-client/src/components/ProblemWorksheetCard/Bundle/ProblemStatementCard/ProblemStatementCard.jsx b/judgels-client/src/components/ProblemWorksheetCard/Bundle/ProblemStatementCard/ProblemStatementCard.jsx index f23f6fb3d..8bbb0ace8 100644 --- a/judgels-client/src/components/ProblemWorksheetCard/Bundle/ProblemStatementCard/ProblemStatementCard.jsx +++ b/judgels-client/src/components/ProblemWorksheetCard/Bundle/ProblemStatementCard/ProblemStatementCard.jsx @@ -17,7 +17,7 @@ export function ProblemStatementCard({ statement, onAnswerItem, latestSubmissions, - reasonNotAllowedToSubmit, + disabled, }) { const generateOnAnswer = itemJid => { return async answer => { @@ -40,7 +40,7 @@ export function ProblemStatementCard({ {...item} itemNumber={item.number} initialAnswer={initialAnswer} - disabled={!!reasonNotAllowedToSubmit} + disabled={disabled} /> ); }; @@ -56,7 +56,7 @@ export function ProblemStatementCard({ {...item} itemNumber={item.number} initialAnswer={initialAnswer} - disabled={!!reasonNotAllowedToSubmit} + disabled={disabled} /> ); }; @@ -72,7 +72,7 @@ export function ProblemStatementCard({ {...item} itemNumber={item.number} initialAnswer={initialAnswer} - disabled={!!reasonNotAllowedToSubmit} + disabled={disabled} /> ); }; diff --git a/judgels-client/src/components/ProblemWorksheetCard/Bundle/ProblemSubmissionCard/ProblemSubmissionCard.jsx b/judgels-client/src/components/ProblemWorksheetCard/Bundle/ProblemSubmissionCard/ProblemSubmissionCard.jsx new file mode 100644 index 000000000..a570524c4 --- /dev/null +++ b/judgels-client/src/components/ProblemWorksheetCard/Bundle/ProblemSubmissionCard/ProblemSubmissionCard.jsx @@ -0,0 +1,24 @@ +import { Callout, Intent } from '@blueprintjs/core'; +import { BanCircle } from '@blueprintjs/icons'; + +import { ContentCard } from '../../../ContentCard/ContentCard'; +import { ButtonLink } from '../../../ButtonLink/ButtonLink'; + +export function ProblemSubmissionCard({ reasonNotAllowedToSubmit, resultsUrl }) { + const renderSubmissionForm = () => { + if (reasonNotAllowedToSubmit) { + return ( + } className="secondary-info"> + {reasonNotAllowedToSubmit} + + ); + } + return ( + + Finish and show results + + ); + }; + + return {renderSubmissionForm()}; +} diff --git a/judgels-client/src/components/ProblemWorksheetCard/Bundle/ProblemWorksheetCard.jsx b/judgels-client/src/components/ProblemWorksheetCard/Bundle/ProblemWorksheetCard.jsx index e93575921..f1a44d10d 100644 --- a/judgels-client/src/components/ProblemWorksheetCard/Bundle/ProblemWorksheetCard.jsx +++ b/judgels-client/src/components/ProblemWorksheetCard/Bundle/ProblemWorksheetCard.jsx @@ -1,13 +1,10 @@ import { ProblemStatementCard } from './ProblemStatementCard/ProblemStatementCard'; +import { ProblemSubmissionCard } from './ProblemSubmissionCard/ProblemSubmissionCard'; import './ProblemWorksheetCard.scss'; -export function ProblemWorksheetCard({ - alias, - worksheet: { statement, items, reasonNotAllowedToSubmit }, - latestSubmissions, - onAnswerItem, -}) { +export function ProblemWorksheetCard({ alias, worksheet, latestSubmissions, onAnswerItem, resultsUrl, disabled }) { + const { statement, items, reasonNotAllowedToSubmit } = worksheet; return (
+
); } diff --git a/judgels-client/src/components/ProblemWorksheetCard/Programming/ProblemWorksheetCard.jsx b/judgels-client/src/components/ProblemWorksheetCard/Programming/ProblemWorksheetCard.jsx index 4f1fbe01d..6736633e8 100644 --- a/judgels-client/src/components/ProblemWorksheetCard/Programming/ProblemWorksheetCard.jsx +++ b/judgels-client/src/components/ProblemWorksheetCard/Programming/ProblemWorksheetCard.jsx @@ -15,6 +15,9 @@ export function ProblemWorksheetCard({ }; const renderSubmission = () => { + if (!onSubmit) { + return null; + } return ( +

{alias ? `${alias} . ` : ''} diff --git a/judgels-client/src/components/SubmissionDetails/Bundle/ProblemSubmissionsCard/ProblemSubmissionCard.scss b/judgels-client/src/components/SubmissionDetails/Bundle/SubmissionDetails/SubmissionDetails.scss similarity index 94% rename from judgels-client/src/components/SubmissionDetails/Bundle/ProblemSubmissionsCard/ProblemSubmissionCard.scss rename to judgels-client/src/components/SubmissionDetails/Bundle/SubmissionDetails/SubmissionDetails.scss index 7fc18980c..f95ad86df 100644 --- a/judgels-client/src/components/SubmissionDetails/Bundle/ProblemSubmissionsCard/ProblemSubmissionCard.scss +++ b/judgels-client/src/components/SubmissionDetails/Bundle/SubmissionDetails/SubmissionDetails.scss @@ -1,6 +1,6 @@ @import '../../../../styles/table'; -.bundle-problem-submission { +.bundle-submission-details { .card-header { display: flex; align-items: baseline; diff --git a/judgels-client/src/components/SubmissionDetails/Programming/SubmissionDetails.jsx b/judgels-client/src/components/SubmissionDetails/Programming/SubmissionDetails.jsx index e381af038..305e468d0 100644 --- a/judgels-client/src/components/SubmissionDetails/Programming/SubmissionDetails.jsx +++ b/judgels-client/src/components/SubmissionDetails/Programming/SubmissionDetails.jsx @@ -80,20 +80,24 @@ export function SubmissionDetails({ - - Problem - - {!!problemUrl ? ( - {constructProblemName(problemName, problemAlias)} - ) : ( - constructProblemName(problemName, problemAlias) - )} - - - - {containerTitle} - {containerName} - + {problemName && ( + + Problem + + {!!problemUrl ? ( + {constructProblemName(problemName, problemAlias)} + ) : ( + constructProblemName(problemName, problemAlias) + )} + + + )} + {containerName && ( + + {containerTitle} + {containerName} + + )} Language {getGradingLanguageName(gradingLanguage)} diff --git a/judgels-client/src/components/Topbar/Topbar.scss b/judgels-client/src/components/Topbar/Topbar.scss index 113aedc40..72a68528c 100644 --- a/judgels-client/src/components/Topbar/Topbar.scss +++ b/judgels-client/src/components/Topbar/Topbar.scss @@ -2,10 +2,5 @@ .topbar { font-family: $accent-font; -} - -@media only screen and (max-width: 600px) { - .topbar__item { - display: none; - } + margin-bottom: 10px; } diff --git a/judgels-client/src/modules/api/jerahmeel/submissionBundle.js b/judgels-client/src/modules/api/jerahmeel/submissionBundle.js index dcf522721..6e6d8b837 100644 --- a/judgels-client/src/modules/api/jerahmeel/submissionBundle.js +++ b/judgels-client/src/modules/api/jerahmeel/submissionBundle.js @@ -15,8 +15,8 @@ export const submissionBundleAPI = { return post(`${baseSubmissionsURL}/`, token, data); }, - getSubmissionSummary: (token, containerJid, problemJid, username, language) => { - const params = stringify({ containerJid, problemJid, username, language }); + getSubmissionSummary: (token, containerJid, problemJid, username, problemAlias, language) => { + const params = stringify({ containerJid, problemJid, username, problemAlias, language }); return get(`${baseSubmissionsURL}/summary?${params}`, token); }, diff --git a/judgels-client/src/modules/api/sandalphon/lesson.js b/judgels-client/src/modules/api/sandalphon/lesson.js index 08b03249f..0658ee17d 100644 --- a/judgels-client/src/modules/api/sandalphon/lesson.js +++ b/judgels-client/src/modules/api/sandalphon/lesson.js @@ -1,5 +1,5 @@ export function getLessonName(lesson, language) { - return lesson.titlesByLanguage[language] || lesson.titlesByLanguage[lesson.defaultLanguage]; + return (language && lesson.titlesByLanguage[language]) || lesson.titlesByLanguage[lesson.defaultLanguage]; } export function constructLessonName(title, alias) { diff --git a/judgels-client/src/modules/api/sandalphon/problem.js b/judgels-client/src/modules/api/sandalphon/problem.js index 6100bf133..248e93d6e 100644 --- a/judgels-client/src/modules/api/sandalphon/problem.js +++ b/judgels-client/src/modules/api/sandalphon/problem.js @@ -4,7 +4,7 @@ export const ProblemType = { }; export function getProblemName(problem, language) { - return problem.titlesByLanguage[language] || problem.titlesByLanguage[problem.defaultLanguage]; + return (language && problem.titlesByLanguage[language]) || problem.titlesByLanguage[problem.defaultLanguage]; } export function constructProblemName(title, alias) { diff --git a/judgels-client/src/routes/contests/contests/single/submissions/Bundle/ContestSubmissionSummaryPage/ContestSubmissionSummaryPage.jsx b/judgels-client/src/routes/contests/contests/single/submissions/Bundle/ContestSubmissionSummaryPage/ContestSubmissionSummaryPage.jsx index 43bfa1bd5..1cf20553d 100644 --- a/judgels-client/src/routes/contests/contests/single/submissions/Bundle/ContestSubmissionSummaryPage/ContestSubmissionSummaryPage.jsx +++ b/judgels-client/src/routes/contests/contests/single/submissions/Bundle/ContestSubmissionSummaryPage/ContestSubmissionSummaryPage.jsx @@ -7,7 +7,7 @@ import { UserRef } from '../../../../../../../components/UserRef/UserRef'; import { selectStatementLanguage } from '../../../../../../../modules/webPrefs/webPrefsSelectors'; import { selectContest } from '../../../../modules/contestSelectors'; -import { ProblemSubmissionCard } from '../../../../../../../components/SubmissionDetails/Bundle/ProblemSubmissionsCard/ProblemSubmissionCard'; +import { SubmissionDetails } from '../../../../../../../components/SubmissionDetails/Bundle/SubmissionDetails/SubmissionDetails'; import * as contestSubmissionActions from '../modules/contestSubmissionActions'; class SubmissionSummaryPage extends Component { @@ -51,7 +51,7 @@ class SubmissionSummaryPage extends Component { Summary for {this.state.problemSummaries.map(props => ( - + ))} ); diff --git a/judgels-client/src/routes/courses/courses/single/CourseChaptersSidebar/CourseChaptersSidebar.jsx b/judgels-client/src/routes/courses/courses/single/CourseChaptersSidebar/CourseChaptersSidebar.jsx index 171e32ad2..b2ee25c1f 100644 --- a/judgels-client/src/routes/courses/courses/single/CourseChaptersSidebar/CourseChaptersSidebar.jsx +++ b/judgels-client/src/routes/courses/courses/single/CourseChaptersSidebar/CourseChaptersSidebar.jsx @@ -1,4 +1,3 @@ -import { ChevronDown } from '@blueprintjs/icons'; import classNames from 'classnames'; import { Component } from 'react'; import { connect } from 'react-redux'; @@ -8,7 +7,6 @@ import { Link } from 'react-router-dom'; import { ProgressTag } from '../../../../../components/ProgressTag/ProgressTag'; import { ProgressBar } from '../../../../../components/ProgressBar/ProgressBar'; import { selectCourse } from '../../modules/courseSelectors'; -import { selectCourseChapter } from '../chapters/modules/courseChapterSelectors'; import * as courseChapterActions from '../chapters/modules/courseChapterActions'; import './CourseChaptersSidebar.scss'; @@ -24,44 +22,49 @@ class CourseChaptersSidebar extends Component { } render() { - const { course, chapter } = this.props; + const { course, match } = this.props; const { response } = this.state; if (!course || !response) { return null; } const { data: courseChapters, chaptersMap, chapterProgressesMap } = response; - const activeChapterJid = chapter && chapter.jid; return ( -
- - -

{course.name}

- +
{courseChapters.map(courseChapter => (
- {courseChapter.alias}. {chaptersMap[courseChapter.chapterJid].name} + {courseChapter.alias}{' '} + {this.isInProblemPath() ? <>  : <>. {chaptersMap[courseChapter.chapterJid].name}} {this.renderProgress(chapterProgressesMap[courseChapter.chapterJid])}
- {this.renderProgressBar(chapterProgressesMap[courseChapter.chapterJid])} + {!this.isInProblemPath() && this.renderProgressBar(chapterProgressesMap[courseChapter.chapterJid])} ))}
); } + isInChapterPath = chapterAlias => { + return (this.props.location.pathname + '/') + .replace('//', '/') + .startsWith(this.props.match.url + '/chapters/' + chapterAlias); + }; + + isInProblemPath = () => { + return this.props.location.pathname.includes('/problems/'); + }; + renderProgress = progress => { if (!progress || progress.totalProblems === 0) { return null; @@ -85,7 +88,6 @@ class CourseChaptersSidebar extends Component { const mapStateToProps = state => ({ course: selectCourse(state), - chapter: selectCourseChapter(state), }); const mapDispatchToProps = { diff --git a/judgels-client/src/routes/courses/courses/single/CourseChaptersSidebar/CourseChaptersSidebar.scss b/judgels-client/src/routes/courses/courses/single/CourseChaptersSidebar/CourseChaptersSidebar.scss index f52e2bcb9..e49b61bd5 100644 --- a/judgels-client/src/routes/courses/courses/single/CourseChaptersSidebar/CourseChaptersSidebar.scss +++ b/judgels-client/src/routes/courses/courses/single/CourseChaptersSidebar/CourseChaptersSidebar.scss @@ -1,15 +1,29 @@ @import '../../../../../styles/base'; .course-chapters-sidebar { + flex-grow: 1; + max-width: 300px; + margin-top: 56px; + margin-right: 10px; font-family: $accent-font; background-color: inherit !important; box-shadow: none !important; - border-right: 1px solid $border-color; + + transition: all 0.2s ease-in-out; + + hr { + margin: 0 !important; + } + + &--compact { + flex-grow: unset; + } } a.course-chapters-sidebar__item { display: block; padding: 10px; + margin-bottom: 1px; color: inherit; text-decoration: none; @@ -17,10 +31,12 @@ a.course-chapters-sidebar__item { &:hover { color: inherit; background-color: $tertiary-background-color; + border-radius: 3px; } &--selected { background-color: $tertiary-background-color; + border-radius: 3px; } } @@ -49,13 +65,14 @@ a.course-chapters-sidebar__item { } } - a.course-chapters-sidebar__item:hover.course-chapters-sidebar__item--selected{ + a.course-chapters-sidebar__item:hover.course-chapters-sidebar__item--selected { background-color: $dark-secondary-background-color; } } -@media only screen and (max-width: 780px) { +@media only screen and (max-width: 750px) { .course-chapters-sidebar { - border-right: none; + max-width: none; + margin-top: 0; } } diff --git a/judgels-client/src/routes/courses/courses/single/CourseOverview/CourseOverview.scss b/judgels-client/src/routes/courses/courses/single/CourseOverview/CourseOverview.scss index 20f505922..b2222f1c7 100644 --- a/judgels-client/src/routes/courses/courses/single/CourseOverview/CourseOverview.scss +++ b/judgels-client/src/routes/courses/courses/single/CourseOverview/CourseOverview.scss @@ -1,5 +1,5 @@ .course-overview { - h2 { - margin-top: -2px; - } + flex: 1 1; + max-width: 865px; + margin-left: 20px; } diff --git a/judgels-client/src/routes/courses/courses/single/SingleCourseContentRoutes.jsx b/judgels-client/src/routes/courses/courses/single/SingleCourseContentRoutes.jsx index 5ea1302bb..c316facce 100644 --- a/judgels-client/src/routes/courses/courses/single/SingleCourseContentRoutes.jsx +++ b/judgels-client/src/routes/courses/courses/single/SingleCourseContentRoutes.jsx @@ -5,12 +5,10 @@ import CourseOverview from './CourseOverview/CourseOverview'; function SingleCourseContentRoutes() { return ( -
- - - - -
+ + + + ); } diff --git a/judgels-client/src/routes/courses/courses/single/SingleCourseRoutes.jsx b/judgels-client/src/routes/courses/courses/single/SingleCourseRoutes.jsx index ead355b11..c628164c4 100644 --- a/judgels-client/src/routes/courses/courses/single/SingleCourseRoutes.jsx +++ b/judgels-client/src/routes/courses/courses/single/SingleCourseRoutes.jsx @@ -21,13 +21,9 @@ function SingleCourseRoutes({ match, course }) {
-
- -
+
-
- -
+
); diff --git a/judgels-client/src/routes/courses/courses/single/SingleCourseRoutes.scss b/judgels-client/src/routes/courses/courses/single/SingleCourseRoutes.scss index a0433dee6..19175fd3d 100644 --- a/judgels-client/src/routes/courses/courses/single/SingleCourseRoutes.scss +++ b/judgels-client/src/routes/courses/courses/single/SingleCourseRoutes.scss @@ -3,29 +3,18 @@ width: 100%; } -.single-course-routes__sidebar { - width: 300px; - margin-right: 25px; - margin-bottom: 15px; -} - -.single-course-routes__content { - flex-grow: 1; - margin-left: auto; - max-width: 865px; -} - .single-course-routes__divider { width: 100%; display: none; } -@media only screen and (max-width: 780px) { +@media only screen and (max-width: 750px) { .single-course-routes { flex-direction: column-reverse; } .single-course-routes__sidebar { + max-width: none; width: inherit; margin-right: 0; } diff --git a/judgels-client/src/routes/courses/courses/single/chapters/single/MainSingleCourseChapterRoutes.jsx b/judgels-client/src/routes/courses/courses/single/chapters/single/MainSingleCourseChapterRoutes.jsx index 589b1c6af..96ce5030f 100644 --- a/judgels-client/src/routes/courses/courses/single/chapters/single/MainSingleCourseChapterRoutes.jsx +++ b/judgels-client/src/routes/courses/courses/single/chapters/single/MainSingleCourseChapterRoutes.jsx @@ -5,10 +5,10 @@ import SingleCourseChapterDataRoute from './SingleCourseChapterDataRoute'; function MainSingleCourseChapterRoutes() { return ( -
+ <> -
+ ); } diff --git a/judgels-client/src/routes/courses/courses/single/chapters/single/SingleCourseChapterRoutes.jsx b/judgels-client/src/routes/courses/courses/single/chapters/single/SingleCourseChapterRoutes.jsx index d6c66f558..dcd5df16c 100644 --- a/judgels-client/src/routes/courses/courses/single/chapters/single/SingleCourseChapterRoutes.jsx +++ b/judgels-client/src/routes/courses/courses/single/chapters/single/SingleCourseChapterRoutes.jsx @@ -1,16 +1,11 @@ -import { Layers, Manual, ManuallyEnteredData, Presentation } from '@blueprintjs/icons'; import { connect } from 'react-redux'; -import { Route, withRouter } from 'react-router'; +import { Route, Switch, withRouter } from 'react-router'; -import ContentWithTopbar from '../../../../../../components/ContentWithTopbar/ContentWithTopbar'; import { LoadingState } from '../../../../../../components/LoadingState/LoadingState'; import { selectCourseChapter } from '../modules/courseChapterSelectors'; +import ChapterResourcesPage from './resources/ChapterResourcesPage/ChapterResourcesPage'; import ChapterLessonRoutes from './lessons/ChapterLessonRoutes'; import ChapterProblemRoutes from './problems/ChapterProblemRoutes'; -import ChapterSubmissionRoutes from './submissions/ChapterSubmissionRoutes'; -import ChapterItemSubmissionRoutes from './results/ChapterItemSubmissionRoutes'; - -import './SingleCourseChapterRoutes.scss'; function SingleCourseChapterRoutes({ chapter, match }) { // Optimization: @@ -19,48 +14,13 @@ function SingleCourseChapterRoutes({ chapter, match }) { return ; } - const topbarItems = [ - { - id: 'lessons', - titleIcon: , - title: 'Lessons', - routeComponent: Route, - component: ChapterLessonRoutes, - }, - { - id: 'problems', - titleIcon: , - title: 'Problems', - routeComponent: Route, - component: ChapterProblemRoutes, - }, - { - id: 'results', - titleIcon: , - title: 'Quiz Results', - routeComponent: Route, - component: ChapterItemSubmissionRoutes, - }, - { - id: 'submissions', - titleIcon: , - title: 'Submissions', - routeComponent: Route, - component: ChapterSubmissionRoutes, - }, - ]; - - const contentWithTopbarProps = { - className: 'single-course-chapter-routes', - contentHeader: ( -

- {chapter.alias}. {chapter.name} -

- ), - items: topbarItems, - }; - - return ; + return ( + + + + + + ); } const mapStateToProps = state => ({ diff --git a/judgels-client/src/routes/courses/courses/single/chapters/single/SingleCourseChapterRoutes.scss b/judgels-client/src/routes/courses/courses/single/chapters/single/SingleCourseChapterRoutes.scss deleted file mode 100644 index 6691496dd..000000000 --- a/judgels-client/src/routes/courses/courses/single/chapters/single/SingleCourseChapterRoutes.scss +++ /dev/null @@ -1,14 +0,0 @@ -.single-course-chapter-routes { - h2 { - margin-top: -2px; - } -} - -@media only screen and (max-width: 750px) { - .single-course-chapter-routes { - h2 { - font-size: 20px !important; - line-height: 22px !important; - } - } -} diff --git a/judgels-client/src/routes/courses/courses/single/chapters/single/lessons/ChapterLessonRoutes.jsx b/judgels-client/src/routes/courses/courses/single/chapters/single/lessons/ChapterLessonRoutes.jsx index e393268d8..61d103645 100644 --- a/judgels-client/src/routes/courses/courses/single/chapters/single/lessons/ChapterLessonRoutes.jsx +++ b/judgels-client/src/routes/courses/courses/single/chapters/single/lessons/ChapterLessonRoutes.jsx @@ -1,16 +1,11 @@ import { Route } from 'react-router'; import { withBreadcrumb } from '../../../../../../../components/BreadcrumbWrapper/BreadcrumbWrapper'; -import ChapterLessonsPage from './ChapterLessonsPage/ChapterLessonsPage'; import ChapterLessonPage from './single/ChapterLessonPage/ChapterLessonPage.jsx'; function ChapterLessonRoutes() { return ( -
- - - -
+ ); } diff --git a/judgels-client/src/routes/courses/courses/single/chapters/single/lessons/ChapterLessonsPage/ChapterLessonsPage.jsx b/judgels-client/src/routes/courses/courses/single/chapters/single/lessons/ChapterLessonsPage/ChapterLessonsPage.jsx deleted file mode 100644 index c381380d7..000000000 --- a/judgels-client/src/routes/courses/courses/single/chapters/single/lessons/ChapterLessonsPage/ChapterLessonsPage.jsx +++ /dev/null @@ -1,121 +0,0 @@ -import { Component } from 'react'; -import { connect } from 'react-redux'; -import { withRouter } from 'react-router'; - -import { ContentCard } from '../../../../../../../../components/ContentCard/ContentCard'; -import { LoadingContentCard } from '../../../../../../../../components/LoadingContentCard/LoadingContentCard'; -import StatementLanguageWidget from '../../../../../../../../components/LanguageWidget/StatementLanguageWidget'; -import { ChapterLessonCard } from '../ChapterLessonCard/ChapterLessonCard'; -import { consolidateLanguages } from '../../../../../../../../modules/api/sandalphon/language'; -import { getLessonName } from '../../../../../../../../modules/api/sandalphon/lesson'; -import { selectStatementLanguage } from '../../../../../../../../modules/webPrefs/webPrefsSelectors'; -import { selectCourse } from '../../../../../modules/courseSelectors'; -import { selectCourseChapter } from '../../../modules/courseChapterSelectors'; -import * as chapterLessonActions from '../modules/chapterLessonActions'; - -export class ChapterLessonsPage extends Component { - state = { - response: undefined, - defaultLanguage: undefined, - uniqueLanguages: undefined, - }; - - async componentDidMount() { - const lessonAliases = this.props.chapter.lessonAliases || []; - if (lessonAliases.length === 1) { - this.props.onRedirectToLesson(this.props.match.url, lessonAliases[0]); - } - - const response = await this.props.onGetLessons(this.props.chapter.jid); - - const { defaultLanguage, uniqueLanguages } = consolidateLanguages( - response.lessonsMap, - this.props.statementLanguage - ); - - this.setState({ - response, - defaultLanguage, - uniqueLanguages, - }); - } - - async componentDidUpdate(prevProps) { - const { response } = this.state; - if (this.props.statementLanguage !== prevProps.statementLanguage && response) { - const { defaultLanguage, uniqueLanguages } = consolidateLanguages( - response.lessonsMap, - this.props.statementLanguage - ); - - this.setState({ - defaultLanguage, - uniqueLanguages, - }); - } - } - - renderStatementLanguageWidget = () => { - const { defaultLanguage, uniqueLanguages } = this.state; - if (!defaultLanguage || !uniqueLanguages) { - return null; - } - - const props = { - defaultLanguage, - statementLanguages: uniqueLanguages, - }; - return ; - }; - - render() { - return ( - -

Lessons

-
- {this.renderStatementLanguageWidget()} - {this.renderLessons()} -
- ); - } - - renderLessons = () => { - const { response } = this.state; - if (!response) { - return ; - } - - const { data: lessons } = response; - - if (lessons.length === 0) { - return ( -

- No lessons. -

- ); - } - - return lessons.map(lesson => { - const props = { - course: this.props.course, - chapter: this.props.chapter, - lesson, - lessonName: getLessonName(this.state.response.lessonsMap[lesson.lessonJid], this.state.defaultLanguage), - }; - return ; - }); - }; -} - -const mapStateToProps = state => ({ - course: selectCourse(state), - chapter: selectCourseChapter(state), - statementLanguage: selectStatementLanguage(state), -}); - -const mapDispatchToProps = { - onGetLessons: chapterLessonActions.getLessons, - onRedirectToLesson: chapterLessonActions.redirectToLesson, -}; - -export default withRouter(connect(mapStateToProps, mapDispatchToProps)(ChapterLessonsPage)); diff --git a/judgels-client/src/routes/courses/courses/single/chapters/single/lessons/ChapterLessonsPage/ChapterLessonsPage.test.jsx b/judgels-client/src/routes/courses/courses/single/chapters/single/lessons/ChapterLessonsPage/ChapterLessonsPage.test.jsx deleted file mode 100644 index 18167f03a..000000000 --- a/judgels-client/src/routes/courses/courses/single/chapters/single/lessons/ChapterLessonsPage/ChapterLessonsPage.test.jsx +++ /dev/null @@ -1,115 +0,0 @@ -import { mount } from 'enzyme'; -import { Provider } from 'react-redux'; -import { MemoryRouter, Route } from 'react-router'; -import { applyMiddleware, combineReducers, createStore } from 'redux'; -import thunk from 'redux-thunk'; - -import ChapterLessonsPage from './ChapterLessonsPage'; -import webPrefsReducer, { PutStatementLanguage } from '../../../../../../../../modules/webPrefs/webPrefsReducer'; -import courseReducer, { PutCourse } from '../../../../../modules/courseReducer'; -import courseChapterReducer, { PutCourseChapter } from '../../../modules/courseChapterReducer'; -import * as chapterLessonActions from '../modules/chapterLessonActions'; - -jest.mock('../modules/chapterLessonActions'); - -describe('ChapterLessonsPage', () => { - let wrapper; - let lessons; - let store; - - const render = async () => { - chapterLessonActions.getLessons.mockReturnValue(() => - Promise.resolve({ - data: lessons, - lessonsMap: { - lessonJid1: { - slug: 'lesson-a', - titlesByLanguage: { en: 'Lesson A' }, - }, - lessonJid2: { - slug: 'lesson-b', - titlesByLanguage: { en: 'Lesson B' }, - }, - }, - }) - ); - chapterLessonActions.redirectToLesson.mockReturnValue(() => Promise.resolve()); - - store = createStore( - combineReducers({ - webPrefs: webPrefsReducer, - jerahmeel: combineReducers({ course: courseReducer, courseChapter: courseChapterReducer }), - }), - applyMiddleware(thunk) - ); - store.dispatch(PutCourse({ jid: 'courseJid', slug: 'courseSlug' })); - store.dispatch( - PutCourseChapter({ - jd: 'chapterJid', - name: 'Chapter 1', - lessonAliases: lessons.map(lesson => lesson.alias), - alias: 'chapter-1', - courseSlug: 'courseSlug', - }) - ); - store.dispatch(PutStatementLanguage('en')); - - wrapper = mount( - - - - - - ); - - await new Promise(resolve => setImmediate(resolve)); - wrapper.update(); - }; - - describe('when there are no lessons', () => { - beforeEach(async () => { - lessons = []; - await render(); - }); - - it('shows placeholder text and no lessons', () => { - expect(wrapper.text()).toContain('No lessons.'); - expect(wrapper.find('div.chapter-lesson-card')).toHaveLength(0); - }); - }); - - describe('when there is one lesson', () => { - beforeEach(async () => { - lessons = [{ lessonJid: 'lessonJid1', alias: 'A' }]; - await render(); - }); - - it('redirects to the only lesson', async () => { - await new Promise(resolve => setImmediate(resolve)); - wrapper.update(); - - expect(chapterLessonActions.redirectToLesson).toHaveBeenCalledWith( - '/courses/courseSlug/chapter/chapter-1/lessons', - 'A' - ); - }); - }); - - describe('when there are lessons', () => { - beforeEach(async () => { - lessons = [ - { lessonJid: 'lessonJid1', alias: 'A' }, - { lessonJid: 'lessonJid2', alias: 'B' }, - ]; - await render(); - }); - - it('shows the lessons', () => { - const cards = wrapper.find('div.chapter-lesson-card'); - expect(cards.map(card => [card.text(), card.find('a').props().href])).toEqual([ - ['A. Lesson A', '/courses/courseSlug/chapters/chapter-1/lessons/A'], - ['B. Lesson B', '/courses/courseSlug/chapters/chapter-1/lessons/B'], - ]); - }); - }); -}); diff --git a/judgels-client/src/routes/courses/courses/single/chapters/single/lessons/modules/chapterLessonActions.js b/judgels-client/src/routes/courses/courses/single/chapters/single/lessons/modules/chapterLessonActions.js index 61a892fe0..f5b615fb9 100644 --- a/judgels-client/src/routes/courses/courses/single/chapters/single/lessons/modules/chapterLessonActions.js +++ b/judgels-client/src/routes/courses/courses/single/chapters/single/lessons/modules/chapterLessonActions.js @@ -1,21 +1,6 @@ -import { replace } from 'connected-react-router'; - import { selectToken } from '../../../../../../../../modules/session/sessionSelectors'; import { chapterLessonAPI } from '../../../../../../../../modules/api/jerahmeel/chapterLesson'; -export function redirectToLesson(baseURL, lessonAlias) { - return async dispatch => { - dispatch(replace(`${baseURL}/${lessonAlias}`)); - }; -} - -export function getLessons(chapterJid) { - return async (dispatch, getState) => { - const token = selectToken(getState()); - return await chapterLessonAPI.getLessons(token, chapterJid); - }; -} - export function getLessonStatement(chapterJid, lessonAlias, language) { return async (dispatch, getState) => { const token = selectToken(getState()); diff --git a/judgels-client/src/routes/courses/courses/single/chapters/single/lessons/modules/chapterLessonActions.test.js b/judgels-client/src/routes/courses/courses/single/chapters/single/lessons/modules/chapterLessonActions.test.js index a9f2511b1..6ad5b8fab 100644 --- a/judgels-client/src/routes/courses/courses/single/chapters/single/lessons/modules/chapterLessonActions.test.js +++ b/judgels-client/src/routes/courses/courses/single/chapters/single/lessons/modules/chapterLessonActions.test.js @@ -20,21 +20,6 @@ describe('chapterLessonActions', () => { nock.cleanAll(); }); - describe('getLessons()', () => { - const responseBody = { - data: [], - }; - - it('calls API', async () => { - nockJerahmeel() - .get(`/chapters/${chapterJid}/lessons`) - .reply(200, responseBody); - - const response = await store.dispatch(chapterLessonActions.getLessons(chapterJid)); - expect(response).toEqual(responseBody); - }); - }); - describe('getLessonStatement()', () => { const language = 'id'; const responseBody = { diff --git a/judgels-client/src/routes/courses/courses/single/chapters/single/lessons/single/ChapterLessonPage/ChapterLessonPage.jsx b/judgels-client/src/routes/courses/courses/single/chapters/single/lessons/single/ChapterLessonPage/ChapterLessonPage.jsx index 9e0f3468d..b9e063709 100644 --- a/judgels-client/src/routes/courses/courses/single/chapters/single/lessons/single/ChapterLessonPage/ChapterLessonPage.jsx +++ b/judgels-client/src/routes/courses/courses/single/chapters/single/lessons/single/ChapterLessonPage/ChapterLessonPage.jsx @@ -1,14 +1,19 @@ +import { ChevronRight } from '@blueprintjs/icons'; import { Component } from 'react'; import { connect } from 'react-redux'; +import { Link } from 'react-router-dom'; import { LoadingState } from '../../../../../../../../../components/LoadingState/LoadingState'; import { ContentCard } from '../../../../../../../../../components/ContentCard/ContentCard'; import StatementLanguageWidget from '../../../../../../../../../components/LanguageWidget/StatementLanguageWidget'; import { LessonStatementCard } from '../../../../../../../../../components/LessonStatementCard/LessonStatementCard'; +import { selectCourse } from '../../../../../../modules/courseSelectors'; import { selectCourseChapter } from '../../../../modules/courseChapterSelectors'; import * as chapterLessonActions from '../../modules/chapterLessonActions'; import * as breadcrumbsActions from '../../../../../../../../../modules/breadcrumbs/breadcrumbsActions'; +import './ChapterLessonPage.scss'; + export class ChapterLessonPage extends Component { state = { response: undefined, @@ -42,13 +47,39 @@ export class ChapterLessonPage extends Component { render() { return ( - - {this.renderStatementLanguageWidget()} - {this.renderStatement()} - +
+ {this.renderHeader()} +
+ + {this.renderStatementLanguageWidget()} + {this.renderStatement()} + +
); } + renderHeader = () => { + const { course, chapter, match } = this.props; + + return ( +

+ + {course.name} + +   + +   + + {chapter.alias}. {chapter.name} + +   + +   + {match.params.lessonAlias} +

+ ); + }; + renderStatementLanguageWidget = () => { const { response } = this.state; if (!response) { @@ -77,6 +108,7 @@ export class ChapterLessonPage extends Component { } const mapStateToProps = state => ({ + course: selectCourse(state), chapter: selectCourseChapter(state), }); diff --git a/judgels-client/src/routes/courses/courses/single/chapters/single/lessons/single/ChapterLessonPage/ChapterLessonPage.scss b/judgels-client/src/routes/courses/courses/single/chapters/single/lessons/single/ChapterLessonPage/ChapterLessonPage.scss new file mode 100644 index 000000000..07b6fbbc1 --- /dev/null +++ b/judgels-client/src/routes/courses/courses/single/chapters/single/lessons/single/ChapterLessonPage/ChapterLessonPage.scss @@ -0,0 +1,25 @@ +@import '../../../../../../../../../styles/base'; + +.chapter-lesson-page { + flex: 1 1; + max-width: 865px; + margin-left: 20px; +} + +.chapter-lesson-page__title { + margin-bottom: 20px; + line-height: 20px !important; +} + +.chapter-lesson-page__title--link { + color: inherit !important; + + &:hover { + text-decoration: none; + color: $primary-color !important; + } +} + +.chapter-lesson-page__title--chevron { + color: $dark-secondary-background-color !important; +} diff --git a/judgels-client/src/routes/courses/courses/single/chapters/single/problems/ChapterProblemCard/ChapterProblemCard.scss b/judgels-client/src/routes/courses/courses/single/chapters/single/problems/ChapterProblemCard/ChapterProblemCard.scss deleted file mode 100644 index e69de29bb..000000000 diff --git a/judgels-client/src/routes/courses/courses/single/chapters/single/problems/ChapterProblemRoutes.jsx b/judgels-client/src/routes/courses/courses/single/chapters/single/problems/ChapterProblemRoutes.jsx index bd5289263..5e0291fa9 100644 --- a/judgels-client/src/routes/courses/courses/single/chapters/single/problems/ChapterProblemRoutes.jsx +++ b/judgels-client/src/routes/courses/courses/single/chapters/single/problems/ChapterProblemRoutes.jsx @@ -1,16 +1,11 @@ import { Route } from 'react-router'; import { withBreadcrumb } from '../../../../../../../components/BreadcrumbWrapper/BreadcrumbWrapper'; -import ChapterProblemsPage from './ChapterProblemsPage/ChapterProblemsPage'; import ChapterProblemPage from './single/ChapterProblemPage/ChapterProblemPage'; function ChapterProblemRoutes() { return ( -
- - - -
+ ); } diff --git a/judgels-client/src/routes/courses/courses/single/chapters/single/problems/ChapterProblemsPage/ChapterProblemsPage.jsx b/judgels-client/src/routes/courses/courses/single/chapters/single/problems/ChapterProblemsPage/ChapterProblemsPage.jsx deleted file mode 100644 index bfcba5fbd..000000000 --- a/judgels-client/src/routes/courses/courses/single/chapters/single/problems/ChapterProblemsPage/ChapterProblemsPage.jsx +++ /dev/null @@ -1,114 +0,0 @@ -import { Component } from 'react'; -import { connect } from 'react-redux'; - -import { ContentCard } from '../../../../../../../../components/ContentCard/ContentCard'; -import { LoadingContentCard } from '../../../../../../../../components/LoadingContentCard/LoadingContentCard'; -import StatementLanguageWidget from '../../../../../../../../components/LanguageWidget/StatementLanguageWidget'; -import { ChapterProblemCard } from '../ChapterProblemCard/ChapterProblemCard'; -import { consolidateLanguages } from '../../../../../../../../modules/api/sandalphon/language'; -import { getProblemName } from '../../../../../../../../modules/api/sandalphon/problem'; -import { selectCourse } from '../../../../../modules/courseSelectors'; -import { selectCourseChapter } from '../../../modules/courseChapterSelectors'; -import { selectStatementLanguage } from '../../../../../../../../modules/webPrefs/webPrefsSelectors'; -import * as chapterProblemActions from '../modules/chapterProblemActions'; - -export class ChapterProblemsPage extends Component { - state = { - response: undefined, - defaultLanguage: undefined, - uniqueLanguages: undefined, - }; - - async componentDidMount() { - const response = await this.props.onGetProblems(this.props.chapter.jid); - const { defaultLanguage, uniqueLanguages } = consolidateLanguages( - response.problemsMap, - this.props.statementLanguage - ); - - this.setState({ - response, - defaultLanguage, - uniqueLanguages, - }); - } - - async componentDidUpdate(prevProps) { - const { response } = this.state; - if (this.props.statementLanguage !== prevProps.statementLanguage && response) { - const { defaultLanguage, uniqueLanguages } = consolidateLanguages( - response.problemsMap, - this.props.statementLanguage - ); - - this.setState({ - defaultLanguage, - uniqueLanguages, - }); - } - } - - render() { - return ( - -

Problems

-
- {this.renderStatementLanguageWidget()} - {this.renderProblems()} -
- ); - } - - renderStatementLanguageWidget = () => { - const { defaultLanguage, uniqueLanguages } = this.state; - if (!defaultLanguage || !uniqueLanguages) { - return null; - } - - const props = { - defaultLanguage, - statementLanguages: uniqueLanguages, - }; - return ; - }; - - renderProblems = () => { - const { response } = this.state; - if (!response) { - return ; - } - - const { data: problems, problemsMap, problemProgressesMap } = response; - - if (problems.length === 0) { - return ( -

- No problems. -

- ); - } - - return problems.map(problem => { - const props = { - course: this.props.course, - chapter: this.props.chapter, - problem, - problemName: getProblemName(problemsMap[problem.problemJid], this.state.defaultLanguage), - progress: problemProgressesMap[problem.problemJid], - }; - return ; - }); - }; -} - -const mapStateToProps = state => ({ - course: selectCourse(state), - chapter: selectCourseChapter(state), - statementLanguage: selectStatementLanguage(state), -}); - -const mapDispatchToProps = { - onGetProblems: chapterProblemActions.getProblems, -}; - -export default connect(mapStateToProps, mapDispatchToProps)(ChapterProblemsPage); diff --git a/judgels-client/src/routes/courses/courses/single/chapters/single/problems/ChapterProblemsPage/ChapterProblemsPage.test.jsx b/judgels-client/src/routes/courses/courses/single/chapters/single/problems/ChapterProblemsPage/ChapterProblemsPage.test.jsx deleted file mode 100644 index 8da921000..000000000 --- a/judgels-client/src/routes/courses/courses/single/chapters/single/problems/ChapterProblemsPage/ChapterProblemsPage.test.jsx +++ /dev/null @@ -1,98 +0,0 @@ -import { mount } from 'enzyme'; -import { Provider } from 'react-redux'; -import { MemoryRouter, Route } from 'react-router'; -import { applyMiddleware, combineReducers, createStore } from 'redux'; -import thunk from 'redux-thunk'; - -import ChapterProblemsPage from './ChapterProblemsPage'; -import webPrefsReducer, { PutStatementLanguage } from '../../../../../../../../modules/webPrefs/webPrefsReducer'; -import courseReducer, { PutCourse } from '../../../../../modules/courseReducer'; -import courseChapterReducer, { PutCourseChapter } from '../../../modules/courseChapterReducer'; -import * as chapterProblemActions from '../modules/chapterProblemActions'; - -jest.mock('../modules/chapterProblemActions'); - -describe('ChapterProblemsPage', () => { - let wrapper; - let problems; - - const render = async () => { - chapterProblemActions.getProblems.mockReturnValue(() => - Promise.resolve({ - data: problems, - problemsMap: { - problemJid1: { - slug: 'problem-a', - titlesByLanguage: { en: 'Problem A' }, - }, - problemJid2: { - slug: 'problem-b', - titlesByLanguage: { en: 'Problem B' }, - }, - }, - problemProgressesMap: { - problemJid1: { verdict: 'AC', score: 100 }, - }, - }) - ); - - const store = createStore( - combineReducers({ - webPrefs: webPrefsReducer, - jerahmeel: combineReducers({ course: courseReducer, courseChapter: courseChapterReducer }), - }), - applyMiddleware(thunk) - ); - store.dispatch(PutCourse({ jid: 'courseJid', slug: 'courseSlug' })); - store.dispatch( - PutCourseChapter({ - jid: 'chapterJid', - name: 'Chapter 1', - alias: 'chapter-1', - courseSlug: 'courseSlug', - }) - ); - store.dispatch(PutStatementLanguage('en')); - - wrapper = mount( - - - - - - ); - - await new Promise(resolve => setImmediate(resolve)); - wrapper.update(); - }; - - describe('when there are no problems', () => { - beforeEach(async () => { - problems = []; - await render(); - }); - - it('shows placeholder text and no problems', () => { - expect(wrapper.text()).toContain('No problems.'); - expect(wrapper.find('div.chapter-problem-card')).toHaveLength(0); - }); - }); - - describe('when there are problems', () => { - beforeEach(async () => { - problems = [ - { problemJid: 'problemJid1', alias: 'A' }, - { problemJid: 'problemJid2', alias: 'B' }, - ]; - await render(); - }); - - it('shows the problems', () => { - const cards = wrapper.find('div.chapter-problem-card'); - expect(cards.map(card => [card.text(), card.find('a').props().href])).toEqual([ - ['A. Problem AAC 100', '/courses/courseSlug/chapters/chapter-1/problems/A'], - ['B. Problem B', '/courses/courseSlug/chapters/chapter-1/problems/B'], - ]); - }); - }); -}); diff --git a/judgels-client/src/routes/courses/courses/single/chapters/single/problems/modules/chapterProblemActions.js b/judgels-client/src/routes/courses/courses/single/chapters/single/problems/modules/chapterProblemActions.js index c45b8196b..2c5d83f05 100644 --- a/judgels-client/src/routes/courses/courses/single/chapters/single/problems/modules/chapterProblemActions.js +++ b/judgels-client/src/routes/courses/courses/single/chapters/single/problems/modules/chapterProblemActions.js @@ -1,13 +1,6 @@ import { selectToken } from '../../../../../../../../modules/session/sessionSelectors'; import { chapterProblemAPI } from '../../../../../../../../modules/api/jerahmeel/chapterProblem'; -export function getProblems(chapterJid) { - return async (dispatch, getState) => { - const token = selectToken(getState()); - return await chapterProblemAPI.getProblems(token, chapterJid); - }; -} - export function getProblemWorksheet(chapterJid, problemAlias, language) { return async (dispatch, getState) => { const token = selectToken(getState()); diff --git a/judgels-client/src/routes/courses/courses/single/chapters/single/problems/modules/chapterProblemActions.test.js b/judgels-client/src/routes/courses/courses/single/chapters/single/problems/modules/chapterProblemActions.test.js index 76f60b359..d92cd15e5 100644 --- a/judgels-client/src/routes/courses/courses/single/chapters/single/problems/modules/chapterProblemActions.test.js +++ b/judgels-client/src/routes/courses/courses/single/chapters/single/problems/modules/chapterProblemActions.test.js @@ -20,21 +20,6 @@ describe('chapterProblemActions', () => { nock.cleanAll(); }); - describe('getProblems()', () => { - const responseBody = { - data: [], - }; - - it('calls API', async () => { - nockJerahmeel() - .get(`/chapters/${chapterJid}/problems`) - .reply(200, responseBody); - - const response = await store.dispatch(chapterProblemActions.getProblems(chapterJid)); - expect(response).toEqual(responseBody); - }); - }); - describe('getProblemWorksheet()', () => { const language = 'id'; const responseBody = { diff --git a/judgels-client/src/routes/courses/courses/single/chapters/single/problems/single/Bundle/ChapterProblemPage.jsx b/judgels-client/src/routes/courses/courses/single/chapters/single/problems/single/Bundle/ChapterProblemPage.jsx index f77f394c8..e01c01bf3 100644 --- a/judgels-client/src/routes/courses/courses/single/chapters/single/problems/single/Bundle/ChapterProblemPage.jsx +++ b/judgels-client/src/routes/courses/courses/single/chapters/single/problems/single/Bundle/ChapterProblemPage.jsx @@ -1,87 +1,13 @@ -import { Component } from 'react'; -import { connect } from 'react-redux'; -import { withRouter } from 'react-router'; - -import { LoadingState } from '../../../../../../../../../components/LoadingState/LoadingState'; -import { ContentCard } from '../../../../../../../../../components/ContentCard/ContentCard'; -import StatementLanguageWidget from '../../../../../../../../../components/LanguageWidget/StatementLanguageWidget'; -import { ProblemWorksheetCard } from '../../../../../../../../../components/ProblemWorksheetCard/Bundle/ProblemWorksheetCard'; -import { selectCourseChapter } from '../../../../modules/courseChapterSelectors'; -import * as chapterSubmissionActions from '../../../results/modules/chapterSubmissionActions'; - -export class ChapterProblemPage extends Component { - state = { - latestSubmissions: undefined, - }; - - async componentDidMount() { - const latestSubmissions = await this.props.onGetLatestSubmissions( - this.props.chapter.jid, - this.props.worksheet.problem.alias - ); - this.setState({ - latestSubmissions, - }); - } - - render() { - return ( - - {this.renderStatementLanguageWidget()} - {this.renderStatement()} - - ); - } - - renderStatementLanguageWidget = () => { - const { defaultLanguage, languages } = this.props.worksheet; - if (!defaultLanguage || !languages) { - return null; - } - const props = { - defaultLanguage: defaultLanguage, - statementLanguages: languages, - }; - return ( -
- -
- ); - }; - - renderStatement = () => { - const { problem, worksheet } = this.props.worksheet; - if (!problem || !worksheet) { - return ; - } - - const { latestSubmissions } = this.state; - if (!latestSubmissions) { - return ; - } - - return ( - - ); - }; - - createSubmission = async (itemJid, answer) => { - const { problem } = this.props.worksheet; - return await this.props.onCreateSubmission(this.props.chapter.jid, problem.problemJid, itemJid, answer); - }; +import ChapterProblemStatementPage from './ChapterProblemStatementPage/ChapterProblemStatementPage'; +import ChapterProblemSubmissionRoutes from './submissions/ChapterProblemSubmissionRoutes'; + +import './ChapterProblemPage.scss'; + +export default function ChapterProblemPage({ worksheet }) { + return ( +
+ + +
+ ); } - -const mapStateToProps = state => ({ - chapter: selectCourseChapter(state), -}); -const mapDispatchToProps = { - onCreateSubmission: chapterSubmissionActions.createItemSubmission, - onGetLatestSubmissions: chapterSubmissionActions.getLatestSubmissions, -}; - -export default withRouter(connect(mapStateToProps, mapDispatchToProps)(ChapterProblemPage)); diff --git a/judgels-client/src/routes/courses/courses/single/chapters/single/problems/single/Bundle/ChapterProblemPage.scss b/judgels-client/src/routes/courses/courses/single/chapters/single/problems/single/Bundle/ChapterProblemPage.scss new file mode 100644 index 000000000..323901929 --- /dev/null +++ b/judgels-client/src/routes/courses/courses/single/chapters/single/problems/single/Bundle/ChapterProblemPage.scss @@ -0,0 +1,11 @@ +.chapter-bundle-problem-page { + display: flex; + column-gap: 5px; + justify-content: center; +} + +@media only screen and (max-width: 1024px) { + .chapter-bundle-problem-page { + display: block; + } +} diff --git a/judgels-client/src/routes/courses/courses/single/chapters/single/problems/single/Bundle/ChapterProblemStatementPage/ChapterProblemStatementPage.jsx b/judgels-client/src/routes/courses/courses/single/chapters/single/problems/single/Bundle/ChapterProblemStatementPage/ChapterProblemStatementPage.jsx new file mode 100644 index 000000000..8edfde8b9 --- /dev/null +++ b/judgels-client/src/routes/courses/courses/single/chapters/single/problems/single/Bundle/ChapterProblemStatementPage/ChapterProblemStatementPage.jsx @@ -0,0 +1,100 @@ +import { Component } from 'react'; +import { connect } from 'react-redux'; +import { withRouter } from 'react-router'; + +import { LoadingState } from '../../../../../../../../../../components/LoadingState/LoadingState'; +import { ContentCard } from '../../../../../../../../../../components/ContentCard/ContentCard'; +import StatementLanguageWidget from '../../../../../../../../../../components/LanguageWidget/StatementLanguageWidget'; +import { ProblemWorksheetCard } from '../../../../../../../../../../components/ProblemWorksheetCard/Bundle/ProblemWorksheetCard'; +import { selectCourseChapter } from '../../../../../modules/courseChapterSelectors'; +import * as chapterProblemSubmissionActions from '../submissions/modules/chapterProblemSubmissionActions'; + +import './ChapterProblemStatementPage.scss'; + +export class ChapterProblemStatementPage extends Component { + state = { + latestSubmissions: undefined, + }; + + async componentDidMount() { + const latestSubmissions = await this.props.onGetLatestSubmissions( + this.props.chapter.jid, + this.props.worksheet.problem.alias + ); + this.setState({ + latestSubmissions, + }); + } + + render() { + return ( + + {this.renderStatementLanguageWidget()} + {this.renderStatement()} + + ); + } + + renderStatementLanguageWidget = () => { + const { defaultLanguage, languages } = this.props.worksheet; + if (!defaultLanguage || !languages) { + return null; + } + const props = { + defaultLanguage: defaultLanguage, + statementLanguages: languages, + }; + return ( +
+ +
+ ); + }; + + renderStatement = () => { + const { problem, worksheet } = this.props.worksheet; + if (!problem || !worksheet) { + return ; + } + + const { latestSubmissions } = this.state; + if (!latestSubmissions) { + return ; + } + + const reasonNotAllowedToSubmit = this.isInSubmissionsPath() + ? 'Submission received.' + : worksheet.reasonNotAllowedToSubmit; + + const resultsUrl = (this.props.location.pathname + '/submissions').replace('//', '/'); + + return ( + + ); + }; + + createSubmission = async (itemJid, answer) => { + const { problem } = this.props.worksheet; + return await this.props.onCreateSubmission(this.props.chapter.jid, problem.problemJid, itemJid, answer); + }; + + isInSubmissionsPath = () => { + return (this.props.location.pathname + '/').includes('/submissions/'); + }; +} + +const mapStateToProps = state => ({ + chapter: selectCourseChapter(state), +}); +const mapDispatchToProps = { + onCreateSubmission: chapterProblemSubmissionActions.createItemSubmission, + onGetLatestSubmissions: chapterProblemSubmissionActions.getLatestSubmissions, +}; + +export default withRouter(connect(mapStateToProps, mapDispatchToProps)(ChapterProblemStatementPage)); diff --git a/judgels-client/src/routes/courses/courses/single/chapters/single/problems/single/Bundle/ChapterProblemStatementPage/ChapterProblemStatementPage.scss b/judgels-client/src/routes/courses/courses/single/chapters/single/problems/single/Bundle/ChapterProblemStatementPage/ChapterProblemStatementPage.scss new file mode 100644 index 000000000..023ccbbd7 --- /dev/null +++ b/judgels-client/src/routes/courses/courses/single/chapters/single/problems/single/Bundle/ChapterProblemStatementPage/ChapterProblemStatementPage.scss @@ -0,0 +1,4 @@ +.chapter-bundle-problem-statement-page { + flex: 1 1; + max-width: 865px; +} diff --git a/judgels-client/src/routes/courses/courses/single/chapters/single/problems/single/Bundle/submissions/ChapterProblemSubmissionRoutes.jsx b/judgels-client/src/routes/courses/courses/single/chapters/single/problems/single/Bundle/submissions/ChapterProblemSubmissionRoutes.jsx new file mode 100644 index 000000000..b314d4b8d --- /dev/null +++ b/judgels-client/src/routes/courses/courses/single/chapters/single/problems/single/Bundle/submissions/ChapterProblemSubmissionRoutes.jsx @@ -0,0 +1,15 @@ +import { Route, withRouter } from 'react-router'; + +import ChapterProblemSubmissionsPage from './ChapterProblemSubmissionsPage/ChapterProblemSubmissionsPage'; + +function ChapterProblemSubmissionRoutes() { + return ( + + ); +} + +export default withRouter(ChapterProblemSubmissionRoutes); diff --git a/judgels-client/src/routes/courses/courses/single/chapters/single/problems/single/Bundle/submissions/ChapterProblemSubmissionsPage/ChapterProblemSubmissionsPage.jsx b/judgels-client/src/routes/courses/courses/single/chapters/single/problems/single/Bundle/submissions/ChapterProblemSubmissionsPage/ChapterProblemSubmissionsPage.jsx new file mode 100644 index 000000000..69c623b25 --- /dev/null +++ b/judgels-client/src/routes/courses/courses/single/chapters/single/problems/single/Bundle/submissions/ChapterProblemSubmissionsPage/ChapterProblemSubmissionsPage.jsx @@ -0,0 +1,108 @@ +import { Component } from 'react'; +import { connect } from 'react-redux'; +import { withRouter } from 'react-router'; + +import { ButtonLink } from '../../../../../../../../../../../components/ButtonLink/ButtonLink'; +import { ScrollToTopOnMount } from '../../../../../../../../../../../components/ScrollToTopOnMount/ScrollToTopOnMount'; +import { LoadingState } from '../../../../../../../../../../../components/LoadingState/LoadingState'; +import { ContentCard } from '../../../../../../../../../../../components/ContentCard/ContentCard'; +import ItemSubmissionUserFilter from '../../../../../../../../../../../components/ItemSubmissionUserFilter/ItemSubmissionUserFilter'; +import { SubmissionDetails } from '../../../../../../../../../../../components/SubmissionDetails/Bundle/SubmissionDetails/SubmissionDetails'; +import { selectMaybeUserJid } from '../../../../../../../../../../../modules/session/sessionSelectors'; +import { selectCourse } from '../../../../../../../../modules/courseSelectors'; +import { selectCourseChapter } from '../../../../../../modules/courseChapterSelectors'; +import { selectStatementLanguage } from '../../../../../../../../../../../modules/webPrefs/webPrefsSelectors'; +import * as chapterProblemSubmissionActions from '../modules/chapterProblemSubmissionActions'; + +import './ChapterProblemSubmissionsPage.scss'; +import { Intent } from '@blueprintjs/core'; + +class ChapterProblemSubmissionsPage extends Component { + state = { + config: undefined, + profile: undefined, + problemSummaries: undefined, + }; + + async refreshSubmissions() { + const { userJid, chapter, match, language, onGetSubmissionSummary } = this.props; + if (!userJid) { + this.setState({ problemSummaries: [] }); + return; + } + + const response = await onGetSubmissionSummary(chapter.jid, match.params.problemAlias, language); + + const problemSummaries = response.config.problemJids.map(problemJid => ({ + name: response.problemNamesMap[problemJid] || '-', + alias: response.problemAliasesMap[chapter.jid + '-' + problemJid] || '-', + itemJids: response.itemJidsByProblemJid[problemJid], + submissionsByItemJid: response.submissionsByItemJid, + canViewGrading: true, + canManage: false, + itemTypesMap: response.itemTypesMap, + })); + + this.setState({ config: response.config, profile: response.profile, problemSummaries }); + } + + async componentDidMount() { + await this.refreshSubmissions(); + } + + render() { + const { course, chapter, match } = this.props; + + return ( + + +

Results

+ + Retake + +
+ {this.renderResults()} +
+ ); + } + + renderUserFilter = () => { + if (this.props.location.pathname.includes('/users/')) { + return null; + } + return ; + }; + + renderResults = () => { + const { problemSummaries } = this.state; + if (!problemSummaries) { + return ; + } + if (problemSummaries.length === 0) { + return No quizzes.; + } + return ( + <> + {this.state.problemSummaries.map(props => ( + + ))} + + ); + }; +} + +const mapStateToProps = state => ({ + userJid: selectMaybeUserJid(state), + course: selectCourse(state), + chapter: selectCourseChapter(state), + language: selectStatementLanguage(state), +}); + +const mapDispatchToProps = { + onGetSubmissionSummary: chapterProblemSubmissionActions.getSubmissionSummary, +}; + +export default withRouter(connect(mapStateToProps, mapDispatchToProps)(ChapterProblemSubmissionsPage)); diff --git a/judgels-client/src/routes/courses/courses/single/chapters/single/problems/single/Bundle/submissions/ChapterProblemSubmissionsPage/ChapterProblemSubmissionsPage.scss b/judgels-client/src/routes/courses/courses/single/chapters/single/problems/single/Bundle/submissions/ChapterProblemSubmissionsPage/ChapterProblemSubmissionsPage.scss new file mode 100644 index 000000000..abe85e581 --- /dev/null +++ b/judgels-client/src/routes/courses/courses/single/chapters/single/problems/single/Bundle/submissions/ChapterProblemSubmissionsPage/ChapterProblemSubmissionsPage.scss @@ -0,0 +1,3 @@ +.chapter-bundle-problem-submissions-page { + flex: 1 1; +} diff --git a/judgels-client/src/routes/courses/courses/single/chapters/single/results/modules/chapterSubmissionActions.js b/judgels-client/src/routes/courses/courses/single/chapters/single/problems/single/Bundle/submissions/modules/chapterProblemSubmissionActions.js similarity index 52% rename from judgels-client/src/routes/courses/courses/single/chapters/single/results/modules/chapterSubmissionActions.js rename to judgels-client/src/routes/courses/courses/single/chapters/single/problems/single/Bundle/submissions/modules/chapterProblemSubmissionActions.js index ca4f84dfc..f119d9d88 100644 --- a/judgels-client/src/routes/courses/courses/single/chapters/single/results/modules/chapterSubmissionActions.js +++ b/judgels-client/src/routes/courses/courses/single/chapters/single/problems/single/Bundle/submissions/modules/chapterProblemSubmissionActions.js @@ -1,6 +1,6 @@ -import { selectToken } from '../../../../../../../../modules/session/sessionSelectors'; -import { submissionBundleAPI } from '../../../../../../../../modules/api/jerahmeel/submissionBundle'; -import { toastActions } from '../../../../../../../../modules/toast/toastActions'; +import { selectToken } from '../../../../../../../../../../../modules/session/sessionSelectors'; +import { submissionBundleAPI } from '../../../../../../../../../../../modules/api/jerahmeel/submissionBundle'; +import { toastActions } from '../../../../../../../../../../../modules/toast/toastActions'; export function getSubmissions(chapterJid, username, problemAlias, page) { return async (dispatch, getState) => { @@ -24,10 +24,10 @@ export function createItemSubmission(chapterJid, problemJid, itemJid, answer) { }; } -export function getSubmissionSummary(chapterJid, username, language) { +export function getSubmissionSummary(chapterJid, problemAlias, language) { return async (dispatch, getState) => { const token = selectToken(getState()); - return submissionBundleAPI.getSubmissionSummary(token, chapterJid, undefined, username, language); + return submissionBundleAPI.getSubmissionSummary(token, chapterJid, undefined, undefined, problemAlias, language); }; } @@ -37,21 +37,3 @@ export function getLatestSubmissions(chapterJid, problemAlias) { return submissionBundleAPI.getLatestSubmissions(token, chapterJid, problemAlias); }; } - -export function regradeSubmission(submissionJid) { - return async (dispatch, getState) => { - const token = selectToken(getState()); - await submissionBundleAPI.regradeSubmission(token, submissionJid); - - toastActions.showSuccessToast('Submission regraded.'); - }; -} - -export function regradeSubmissions(chapterJid, userJid, problemJid) { - return async (dispatch, getState) => { - const token = selectToken(getState()); - await submissionBundleAPI.regradeSubmissions(token, chapterJid, userJid, problemJid); - - toastActions.showSuccessToast('Regrade in progress.'); - }; -} diff --git a/judgels-client/src/routes/courses/courses/single/chapters/single/problems/single/ChapterProblemPage/ChapterProblemPage.jsx b/judgels-client/src/routes/courses/courses/single/chapters/single/problems/single/ChapterProblemPage/ChapterProblemPage.jsx index 9b9a81792..12505e813 100644 --- a/judgels-client/src/routes/courses/courses/single/chapters/single/problems/single/ChapterProblemPage/ChapterProblemPage.jsx +++ b/judgels-client/src/routes/courses/courses/single/chapters/single/problems/single/ChapterProblemPage/ChapterProblemPage.jsx @@ -1,18 +1,22 @@ +import { ChevronRight } from '@blueprintjs/icons'; import { Component } from 'react'; import { connect } from 'react-redux'; import { withRouter } from 'react-router'; +import { Link } from 'react-router-dom'; import { sendGAEvent } from '../../../../../../../../../ga'; -import { ProblemType } from '../../../../../../../../../modules/api/sandalphon/problem'; import { LoadingState } from '../../../../../../../../../components/LoadingState/LoadingState'; import ChapterProblemProgrammingPage from '../Programming/ChapterProblemPage'; import ChapterProblemBundlePage from '../Bundle/ChapterProblemPage'; +import { ProblemType } from '../../../../../../../../../modules/api/sandalphon/problem'; import { selectCourse } from '../../../../../../modules/courseSelectors'; import { selectCourseChapter } from '../../../../modules/courseChapterSelectors'; import { selectStatementLanguage } from '../../../../../../../../../modules/webPrefs/webPrefsSelectors'; import * as chapterProblemActions from '../../modules/chapterProblemActions'; import * as breadcrumbsActions from '../../../../../../../../../modules/breadcrumbs/breadcrumbsActions'; +import './ChapterProblemPage.scss'; + export class ChapterProblemPage extends Component { state = { response: undefined, @@ -53,17 +57,50 @@ export class ChapterProblemPage extends Component { } render() { + return ( +
+ {this.renderHeader()} +
+ {this.renderContent()} +
+ ); + } + + renderHeader = () => { + const { course, chapter, match } = this.props; + + return ( +

+ + {course.name} + +   + +   + + {chapter.alias}. {chapter.name} + +   + +   + {match.params.problemAlias} +

+ ); + }; + + renderContent = () => { const { response } = this.state; if (!response) { return ; } + const { problem } = response; if (problem.type === ProblemType.Programming) { return ; } else { return ; } - } + }; } const mapStateToProps = state => ({ diff --git a/judgels-client/src/routes/courses/courses/single/chapters/single/problems/single/ChapterProblemPage/ChapterProblemPage.scss b/judgels-client/src/routes/courses/courses/single/chapters/single/problems/single/ChapterProblemPage/ChapterProblemPage.scss new file mode 100644 index 000000000..29d5259f0 --- /dev/null +++ b/judgels-client/src/routes/courses/courses/single/chapters/single/problems/single/ChapterProblemPage/ChapterProblemPage.scss @@ -0,0 +1,23 @@ +@import '../../../../../../../../../styles/base'; + +.chapter-problem-page { + flex: 1 1; +} + +.chapter-problem-page__title { + margin-bottom: 20px; + line-height: 20px !important; +} + +.chapter-problem-page__title--link { + color: inherit !important; + + &:hover { + text-decoration: none; + color: $primary-color !important; + } +} + +.chapter-problem-page__title--chevron { + color: $dark-secondary-background-color !important; +} diff --git a/judgels-client/src/routes/courses/courses/single/chapters/single/problems/single/Programming/ChapterProblemPage.jsx b/judgels-client/src/routes/courses/courses/single/chapters/single/problems/single/Programming/ChapterProblemPage.jsx index c8e7cbfc1..98f847a59 100644 --- a/judgels-client/src/routes/courses/courses/single/chapters/single/problems/single/Programming/ChapterProblemPage.jsx +++ b/judgels-client/src/routes/courses/courses/single/chapters/single/problems/single/Programming/ChapterProblemPage.jsx @@ -1,94 +1,13 @@ -import { connect } from 'react-redux'; -import { withRouter } from 'react-router'; +import ChapterProblemStatementPage from './ChapterProblemStatementPage/ChapterProblemStatementPage'; +import ChapterProblemStatementRoutes from './ChapterProblemStatementRoutes'; -import { sendGAEvent } from '../../../../../../../../../ga'; -import { ContentCard } from '../../../../../../../../../components/ContentCard/ContentCard'; -import StatementLanguageWidget from '../../../../../../../../../components/LanguageWidget/StatementLanguageWidget'; -import { getGradingLanguageFamily } from '../../../../../../../../../modules/api/gabriel/language.js'; -import { selectCourse } from '../../../../../../modules/courseSelectors'; -import { selectCourseChapter } from '../../../../modules/courseChapterSelectors'; -import { selectGradingLanguage } from '../../../../../../../../../modules/webPrefs/webPrefsSelectors'; -import { ProblemWorksheetCard } from '../../../../../../../../../components/ProblemWorksheetCard/Programming/ProblemWorksheetCard'; -import * as chapterSubmissionActions from '../../../submissions/modules/chapterSubmissionActions'; -import * as webPrefsActions from '../../../../../../../../../modules/webPrefs/webPrefsActions'; - -export function ChapterProblemPage({ - match, - course, - chapter, - worksheet, - gradingLanguage, - onCreateSubmission, - onUpdateGradingLanguage, -}) { - const renderStatementLanguageWidget = () => { - const { defaultLanguage, languages } = worksheet; - if (!defaultLanguage || !languages) { - return null; - } - const props = { - defaultLanguage: defaultLanguage, - statementLanguages: languages, - }; - return ( -
- -
- ); - }; - - const renderStatement = () => { - const { problem } = worksheet; - - return ( - - ); - }; - - const createSubmission = async data => { - const { problem } = worksheet; - - onUpdateGradingLanguage(data.gradingLanguage); - - sendGAEvent({ category: 'Courses', action: 'Submit course problem', label: course.name }); - sendGAEvent({ category: 'Courses', action: 'Submit chapter problem', label: chapter.name }); - sendGAEvent({ - category: 'Courses', - action: 'Submit problem', - label: chapter.name + ': ' + match.params.problemAlias, - }); - if (getGradingLanguageFamily(data.gradingLanguage)) { - sendGAEvent({ - category: 'Courses', - action: 'Submit language', - label: getGradingLanguageFamily(data.gradingLanguage), - }); - } - - return await onCreateSubmission(course.slug, chapter.jid, chapter.alias, problem.problemJid, data); - }; +import './ChapterProblemPage.scss'; +export default function ChapterProblemPage({ worksheet }) { return ( - - {renderStatementLanguageWidget()} - {renderStatement()} - +
+ + +
); } - -const mapStateToProps = state => ({ - course: selectCourse(state), - chapter: selectCourseChapter(state), - gradingLanguage: selectGradingLanguage(state), -}); -const mapDispatchToProps = { - onCreateSubmission: chapterSubmissionActions.createSubmission, - onUpdateGradingLanguage: webPrefsActions.updateGradingLanguage, -}; - -export default withRouter(connect(mapStateToProps, mapDispatchToProps)(ChapterProblemPage)); diff --git a/judgels-client/src/routes/courses/courses/single/chapters/single/problems/single/Programming/ChapterProblemPage.scss b/judgels-client/src/routes/courses/courses/single/chapters/single/problems/single/Programming/ChapterProblemPage.scss new file mode 100644 index 000000000..008c87b25 --- /dev/null +++ b/judgels-client/src/routes/courses/courses/single/chapters/single/problems/single/Programming/ChapterProblemPage.scss @@ -0,0 +1,10 @@ +.chapter-programming-problem-page { + display: flex; + column-gap: 5px; +} + +@media only screen and (max-width: 1024px) { + .chapter-programming-problem-page { + display: block; + } +} diff --git a/judgels-client/src/routes/courses/courses/single/chapters/single/problems/single/Programming/ChapterProblemStatementPage/ChapterProblemStatementPage.jsx b/judgels-client/src/routes/courses/courses/single/chapters/single/problems/single/Programming/ChapterProblemStatementPage/ChapterProblemStatementPage.jsx new file mode 100644 index 000000000..5f45eea49 --- /dev/null +++ b/judgels-client/src/routes/courses/courses/single/chapters/single/problems/single/Programming/ChapterProblemStatementPage/ChapterProblemStatementPage.jsx @@ -0,0 +1,48 @@ +import { connect } from 'react-redux'; +import { withRouter } from 'react-router'; + +import { ContentCard } from '../../../../../../../../../../components/ContentCard/ContentCard'; +import StatementLanguageWidget from '../../../../../../../../../../components/LanguageWidget/StatementLanguageWidget'; +import { selectCourse } from '../../../../../../../modules/courseSelectors'; +import { selectCourseChapter } from '../../../../../modules/courseChapterSelectors'; +import { ProblemWorksheetCard } from '../../../../../../../../../../components/ProblemWorksheetCard/Programming/ProblemWorksheetCard'; + +import './ChapterProblemStatementPage.scss'; + +export function ChapterProblemStatementPage({ worksheet }) { + const renderStatementLanguageWidget = () => { + const { defaultLanguage, languages } = worksheet; + if (!defaultLanguage || !languages) { + return null; + } + const props = { + defaultLanguage: defaultLanguage, + statementLanguages: languages, + }; + return ( +
+ +
+ ); + }; + + const renderStatement = () => { + const { problem } = worksheet; + + return ; + }; + + return ( + + {renderStatementLanguageWidget()} + {renderStatement()} + + ); +} + +const mapStateToProps = state => ({ + course: selectCourse(state), + chapter: selectCourseChapter(state), +}); + +export default withRouter(connect(mapStateToProps)(ChapterProblemStatementPage)); diff --git a/judgels-client/src/routes/courses/courses/single/chapters/single/problems/single/Programming/ChapterProblemStatementPage/ChapterProblemStatementPage.scss b/judgels-client/src/routes/courses/courses/single/chapters/single/problems/single/Programming/ChapterProblemStatementPage/ChapterProblemStatementPage.scss new file mode 100644 index 000000000..194fe204f --- /dev/null +++ b/judgels-client/src/routes/courses/courses/single/chapters/single/problems/single/Programming/ChapterProblemStatementPage/ChapterProblemStatementPage.scss @@ -0,0 +1,4 @@ +.chapter-programming-problem-statement-page { + flex: 1 1; + max-width: 865px; +} diff --git a/judgels-client/src/routes/courses/courses/single/chapters/single/problems/single/Programming/ChapterProblemStatementRoutes.jsx b/judgels-client/src/routes/courses/courses/single/chapters/single/problems/single/Programming/ChapterProblemStatementRoutes.jsx new file mode 100644 index 000000000..e304b3dc7 --- /dev/null +++ b/judgels-client/src/routes/courses/courses/single/chapters/single/problems/single/Programming/ChapterProblemStatementRoutes.jsx @@ -0,0 +1,26 @@ +import { Route } from 'react-router'; + +import ContentWithTopbar from '../../../../../../../../../components/ContentWithTopbar/ContentWithTopbar'; +import ChapterProblemWorkspacePage from './ChapterProblemWorkspacePage/ChapterProblemWorkspacePage'; +import ChapterProblemSubmissionRoutes from './submissions/ChapterProblemSubmissionRoutes'; + +import './ChapterProblemStatementRoutes.scss'; + +export default function ChapterProblemStatementRoutes({ worksheet }) { + const topbarItems = [ + { + id: '@', + title: 'Submit', + routeComponent: Route, + component: () => , + }, + { + id: 'submissions', + title: 'Submissions', + routeComponent: Route, + component: ChapterProblemSubmissionRoutes, + }, + ]; + + return ; +} diff --git a/judgels-client/src/routes/courses/courses/single/chapters/single/problems/single/Programming/ChapterProblemStatementRoutes.scss b/judgels-client/src/routes/courses/courses/single/chapters/single/problems/single/Programming/ChapterProblemStatementRoutes.scss new file mode 100644 index 000000000..9be80a431 --- /dev/null +++ b/judgels-client/src/routes/courses/courses/single/chapters/single/problems/single/Programming/ChapterProblemStatementRoutes.scss @@ -0,0 +1,3 @@ +.chapter-problem-statement-routes { + flex: 1; +} diff --git a/judgels-client/src/routes/courses/courses/single/chapters/single/problems/single/Programming/ChapterProblemWorkspacePage/ChapterProblemWorkspacePage.jsx b/judgels-client/src/routes/courses/courses/single/chapters/single/problems/single/Programming/ChapterProblemWorkspacePage/ChapterProblemWorkspacePage.jsx new file mode 100644 index 000000000..695fde9cf --- /dev/null +++ b/judgels-client/src/routes/courses/courses/single/chapters/single/problems/single/Programming/ChapterProblemWorkspacePage/ChapterProblemWorkspacePage.jsx @@ -0,0 +1,64 @@ +import { connect } from 'react-redux'; + +import { sendGAEvent } from '../../../../../../../../../../ga'; +import { ProblemSubmissionCard } from '../../../../../../../../../../components/ProblemWorksheetCard/Programming/ProblemSubmissionCard/ProblemSubmissionCard'; +import { getGradingLanguageFamily } from '../../../../../../../../../../modules/api/gabriel/language.js'; +import { selectCourse } from '../../../../../../../modules/courseSelectors'; +import { selectCourseChapter } from '../../../../../modules/courseChapterSelectors'; +import { selectGradingLanguage } from '../../../../../../../../../../modules/webPrefs/webPrefsSelectors'; +import * as chapterProblemSubmissionActions from '../submissions/modules/chapterProblemSubmissionActions'; +import * as webPrefsActions from '../../../../../../../../../../modules/webPrefs/webPrefsActions'; + +function ChapterProblemWorkspacePage({ + worksheet: { problem, worksheet }, + course, + chapter, + gradingLanguage, + onCreateSubmission, + onUpdateGradingLanguage, +}) { + const { submissionConfig, reasonNotAllowedToSubmit } = worksheet; + + const createSubmission = async data => { + onUpdateGradingLanguage(data.gradingLanguage); + + sendGAEvent({ category: 'Courses', action: 'Submit course problem', label: course.name }); + sendGAEvent({ category: 'Courses', action: 'Submit chapter problem', label: chapter.name }); + sendGAEvent({ + category: 'Courses', + action: 'Submit problem', + label: chapter.name + ': ' + problem.alis, + }); + if (getGradingLanguageFamily(data.gradingLanguage)) { + sendGAEvent({ + category: 'Courses', + action: 'Submit language', + label: getGradingLanguageFamily(data.gradingLanguage), + }); + } + + return await onCreateSubmission(course.slug, chapter.jid, chapter.alias, problem.problemJid, problem.alias, data); + }; + + return ( + + ); +} + +const mapStateToProps = state => ({ + course: selectCourse(state), + chapter: selectCourseChapter(state), + gradingLanguage: selectGradingLanguage(state), +}); + +const mapDispatchToProps = { + onCreateSubmission: chapterProblemSubmissionActions.createSubmission, + onUpdateGradingLanguage: webPrefsActions.updateGradingLanguage, +}; + +export default connect(mapStateToProps, mapDispatchToProps)(ChapterProblemWorkspacePage); diff --git a/judgels-client/src/routes/courses/courses/single/chapters/single/problems/single/Programming/submissions/ChapterProblemSubmissionRoutes.jsx b/judgels-client/src/routes/courses/courses/single/chapters/single/problems/single/Programming/submissions/ChapterProblemSubmissionRoutes.jsx new file mode 100644 index 000000000..63ad2d708 --- /dev/null +++ b/judgels-client/src/routes/courses/courses/single/chapters/single/problems/single/Programming/submissions/ChapterProblemSubmissionRoutes.jsx @@ -0,0 +1,28 @@ +import { Route, withRouter, Switch } from 'react-router'; + +import ChapterProblemSubmissionsPage from './ChapterProblemSubmissionsPage/ChapterProblemSubmissionsPage'; +import ChapterProblemSubmissionPage from './single/ChapterProblemSubmissionPage/ChapterProblemSubmissionPage'; + +function ChapterProblemSubmissionRoutes() { + return ( + + + + + + ); +} + +export default withRouter(ChapterProblemSubmissionRoutes); diff --git a/judgels-client/src/routes/courses/courses/single/chapters/single/problems/single/Programming/submissions/ChapterProblemSubmissionRoutes.scss b/judgels-client/src/routes/courses/courses/single/chapters/single/problems/single/Programming/submissions/ChapterProblemSubmissionRoutes.scss new file mode 100644 index 000000000..bfb8bbdc2 --- /dev/null +++ b/judgels-client/src/routes/courses/courses/single/chapters/single/problems/single/Programming/submissions/ChapterProblemSubmissionRoutes.scss @@ -0,0 +1,3 @@ +.chapter-problem-submissions-routes { + flex: 1 1; +} diff --git a/judgels-client/src/routes/courses/courses/single/chapters/single/problems/single/Programming/submissions/ChapterProblemSubmissionsPage/ChapterProblemSubmissionsPage.jsx b/judgels-client/src/routes/courses/courses/single/chapters/single/problems/single/Programming/submissions/ChapterProblemSubmissionsPage/ChapterProblemSubmissionsPage.jsx new file mode 100644 index 000000000..fb3e28589 --- /dev/null +++ b/judgels-client/src/routes/courses/courses/single/chapters/single/problems/single/Programming/submissions/ChapterProblemSubmissionsPage/ChapterProblemSubmissionsPage.jsx @@ -0,0 +1,156 @@ +import { Switch } from '@blueprintjs/core'; +import { push } from 'connected-react-router'; +import { parse } from 'query-string'; +import { Component } from 'react'; +import { connect } from 'react-redux'; +import { withRouter } from 'react-router'; + +import { reallyConfirm } from '../../../../../../../../../../../utils/confirmation'; +import { LoadingState } from '../../../../../../../../../../../components/LoadingState/LoadingState'; +import { ContentCard } from '../../../../../../../../../../../components/ContentCard/ContentCard'; +import { RegradeAllButton } from '../../../../../../../../../../../components/RegradeAllButton/RegradeAllButton'; +import Pagination from '../../../../../../../../../../../components/Pagination/Pagination'; +import { ChapterProblemSubmissionsTable } from '../ChapterProblemSubmissionsTable/ChapterProblemSubmissionsTable'; +import { + selectMaybeUserJid, + selectMaybeUsername, +} from '../../../../../../../../../../../modules/session/sessionSelectors'; +import { selectCourse } from '../../../../../../../../modules/courseSelectors'; +import { selectCourseChapter } from '../../../../../../modules/courseChapterSelectors'; +import * as chapterProblemSubmissionActions from '../modules/chapterProblemSubmissionActions'; + +class ChapterProblemSubmissionsPage extends Component { + static PAGE_SIZE = 20; + state = { + response: undefined, + }; + + render() { + return ( + + {this.renderFilter()} + {this.renderHeader()} + {this.renderSubmissions()} + {this.renderPagination()} + + ); + } + + renderHeader = () => { + return ( +
+
{this.renderRegradeAllButton()}
+
+
+ ); + }; + + renderFilter = () => { + return ( + this.props.userJid && ( + + ) + ); + }; + + isFilterShowAllChecked = () => { + return (this.props.location.pathname + '/').includes('/all/'); + }; + + onChangeFilterShowAll = ({ target }) => { + if (target.checked) { + this.props.push((this.props.location.pathname + '/all').replace('//', '/')); + } else { + const idx = this.props.location.pathname.lastIndexOf('/all'); + this.props.push(this.props.location.pathname.substr(0, idx)); + } + }; + + renderRegradeAllButton = () => { + if (!this.state.response || !this.state.response.config.canManage) { + return null; + } + return ; + }; + + renderSubmissions = () => { + const { response } = this.state; + if (!response) { + return ; + } + + const { data: submissions, config, profilesMap } = response; + if (submissions.page.length === 0) { + return ( +

+ No submissions. +

+ ); + } + + return ( + + ); + }; + + renderPagination = () => { + const key = '' + this.isFilterShowAllChecked(); + return ; + }; + + onChangePage = async nextPage => { + const data = await this.refreshSubmissions(nextPage); + return data.totalCount; + }; + + refreshSubmissions = async page => { + const username = this.isFilterShowAllChecked() ? undefined : this.props.username; + const problemAlias = this.props.match.params.problemAlias; + const response = await this.props.onGetSubmissions(this.props.chapter.jid, problemAlias, username, page); + this.setState({ response }); + return response.data; + }; + + onRegradeSubmission = async submissionJid => { + await this.props.onRegradeSubmission(submissionJid); + const queries = parse(this.props.location.search); + await this.refreshSubmissions(queries.page); + }; + + onRegradeSubmissions = async () => { + if (reallyConfirm('Regrade all submissions in all pages?')) { + const problemAlias = this.props.match.params.problemAlias; + await this.props.onRegradeSubmissions(this.props.chapter.jid, undefined, problemAlias); + const queries = parse(this.props.location.search); + await this.refreshSubmissions(queries.page); + } + }; +} + +const mapStateToProps = state => ({ + userJid: selectMaybeUserJid(state), + username: selectMaybeUsername(state), + course: selectCourse(state), + chapter: selectCourseChapter(state), +}); + +const mapDispatchToProps = { + onGetSubmissions: chapterProblemSubmissionActions.getSubmissions, + onRegradeSubmission: chapterProblemSubmissionActions.regradeSubmission, + onRegradeSubmissions: chapterProblemSubmissionActions.regradeSubmissions, + push, +}; + +export default withRouter(connect(mapStateToProps, mapDispatchToProps)(ChapterProblemSubmissionsPage)); diff --git a/judgels-client/src/routes/courses/courses/single/chapters/single/submissions/ChapterSubmissionsPage/ChapterSubmissionsPage.test.jsx b/judgels-client/src/routes/courses/courses/single/chapters/single/problems/single/Programming/submissions/ChapterProblemSubmissionsPage/ChapterProblemSubmissionsPage.test.jsx similarity index 78% rename from judgels-client/src/routes/courses/courses/single/chapters/single/submissions/ChapterSubmissionsPage/ChapterSubmissionsPage.test.jsx rename to judgels-client/src/routes/courses/courses/single/chapters/single/problems/single/Programming/submissions/ChapterProblemSubmissionsPage/ChapterProblemSubmissionsPage.test.jsx index 6893060e8..575750729 100644 --- a/judgels-client/src/routes/courses/courses/single/chapters/single/submissions/ChapterSubmissionsPage/ChapterSubmissionsPage.test.jsx +++ b/judgels-client/src/routes/courses/courses/single/chapters/single/problems/single/Programming/submissions/ChapterProblemSubmissionsPage/ChapterProblemSubmissionsPage.test.jsx @@ -4,21 +4,21 @@ import { MemoryRouter, Route } from 'react-router'; import { applyMiddleware, combineReducers, createStore } from 'redux'; import thunk from 'redux-thunk'; -import ChapterSubmissionsPage from './ChapterSubmissionsPage'; -import sessionReducer, { PutUser } from '../../../../../../../../modules/session/sessionReducer'; -import courseReducer, { PutCourse } from '../../../../../modules/courseReducer'; -import courseChapterReducer, { PutCourseChapter } from '../../../modules/courseChapterReducer'; -import * as chapterSubmissionActions from '../modules/chapterSubmissionActions'; +import ChapterProblemSubmissionsPage from './ChapterProblemSubmissionsPage'; +import sessionReducer, { PutUser } from '../../../../../../../../../../../modules/session/sessionReducer'; +import courseReducer, { PutCourse } from '../../../../../../../../modules/courseReducer'; +import courseChapterReducer, { PutCourseChapter } from '../../../../../../modules/courseChapterReducer'; +import * as chapterProblemSubmissionActions from '../modules/chapterProblemSubmissionActions'; -jest.mock('../modules/chapterSubmissionActions'); +jest.mock('../modules/chapterProblemSubmissionActions'); -describe('ChapterSubmissionsPage', () => { +describe('ChapterProblemSubmissionsPage', () => { let wrapper; let submissions; let canManage; const render = async () => { - chapterSubmissionActions.getSubmissions.mockReturnValue(() => + chapterProblemSubmissionActions.getSubmissions.mockReturnValue(() => Promise.resolve({ data: { page: submissions, @@ -29,17 +29,16 @@ describe('ChapterSubmissionsPage', () => { }, problemAliasesMap: { 'chapterJid-problemJid1': 'A', - 'chapterJid-problemJid2': 'B', }, config: { canManage, userJids: ['userJid1', 'userJid2'], - problemJids: ['problemJid1', 'problemJid2'], + problemJids: ['problemJid1'], }, }) ); - chapterSubmissionActions.getSubmissionSourceImage.mockReturnValue(() => Promise.resolve('image.url')); + chapterProblemSubmissionActions.getSubmissionSourceImage.mockReturnValue(() => Promise.resolve('image.url')); const store = createStore( combineReducers({ @@ -61,8 +60,11 @@ describe('ChapterSubmissionsPage', () => { wrapper = mount( - - + + ); @@ -138,7 +140,7 @@ describe('ChapterSubmissionsPage', () => { jid: 'submissionJid2', containerJid: 'chapterJid', userJid: 'userJid2', - problemJid: 'problemJid2', + problemJid: 'problemJid1', gradingLanguage: 'Cpp17', time: new Date(new Date().setDate(new Date().getDate() - 2)).getTime(), }, @@ -154,8 +156,8 @@ describe('ChapterSubmissionsPage', () => { it('shows the submissions', () => { expect(wrapper.find('tr').map(tr => tr.find('td').map(td => td.text().trim()))).toEqual([ [], - ['20', 'username1', 'A', 'C++17', 'AC', '100', '1 day ago', 'search'], - ['10', 'username2', 'B', 'C++17', '', '', '2 days ago', 'search'], + ['20', 'username1', 'C++17', 'AC', '100', '1 day ago', 'search'], + ['10', 'username2', 'C++17', '', '', '2 days ago', 'search'], ]); }); }); @@ -178,8 +180,8 @@ describe('ChapterSubmissionsPage', () => { ) ).toEqual([ [], - ['20 refresh', 'username1', 'A', 'C++17', 'AC', '100', '1 day ago', 'search'], - ['10 refresh', 'username2', 'B', 'C++17', '', '', '2 days ago', 'search'], + ['20 refresh', 'username1', 'C++17', 'AC', '100', '1 day ago', 'search'], + ['10 refresh', 'username2', 'C++17', '', '', '2 days ago', 'search'], ]); }); }); diff --git a/judgels-client/src/routes/courses/courses/single/chapters/single/submissions/ChapterSubmissionsTable/ChapterSubmissionsTable.jsx b/judgels-client/src/routes/courses/courses/single/chapters/single/problems/single/Programming/submissions/ChapterProblemSubmissionsTable/ChapterProblemSubmissionsTable.jsx similarity index 76% rename from judgels-client/src/routes/courses/courses/single/chapters/single/submissions/ChapterSubmissionsTable/ChapterSubmissionsTable.jsx rename to judgels-client/src/routes/courses/courses/single/chapters/single/problems/single/Programming/submissions/ChapterProblemSubmissionsTable/ChapterProblemSubmissionsTable.jsx index ec37df7e7..d659121bc 100644 --- a/judgels-client/src/routes/courses/courses/single/chapters/single/submissions/ChapterSubmissionsTable/ChapterSubmissionsTable.jsx +++ b/judgels-client/src/routes/courses/courses/single/chapters/single/problems/single/Programming/submissions/ChapterProblemSubmissionsTable/ChapterProblemSubmissionsTable.jsx @@ -2,20 +2,20 @@ import { HTMLTable } from '@blueprintjs/core'; import { Refresh, Search } from '@blueprintjs/icons'; import { Link } from 'react-router-dom'; -import { FormattedRelative } from '../../../../../../../../components/FormattedRelative/FormattedRelative'; -import { UserRef } from '../../../../../../../../components/UserRef/UserRef'; -import { VerdictTag } from '../../../../../../../../components/VerdictTag/VerdictTag'; -import { getGradingLanguageName } from '../../../../../../../../modules/api/gabriel/language.js'; +import { FormattedRelative } from '../../../../../../../../../../../components/FormattedRelative/FormattedRelative'; +import { UserRef } from '../../../../../../../../../../../components/UserRef/UserRef'; +import { VerdictTag } from '../../../../../../../../../../../components/VerdictTag/VerdictTag'; +import { getGradingLanguageName } from '../../../../../../../../../../../modules/api/gabriel/language.js'; -import '../../../../../../../../components/SubmissionsTable/Programming/SubmissionsTable.scss'; +import '../../../../../../../../../../../components/SubmissionsTable/Programming/SubmissionsTable.scss'; -export function ChapterSubmissionsTable({ +export function ChapterProblemSubmissionsTable({ course, chapter, + problemAlias, submissions, canManage, profilesMap, - problemAliasesMap, onRegrade, }) { const renderHeader = () => { @@ -24,7 +24,6 @@ export function ChapterSubmissionsTable({ ID User - Prob Lang Verdict Pts @@ -50,7 +49,6 @@ export function ChapterSubmissionsTable({ - {problemAliasesMap[submission.containerJid + '-' + submission.problemJid]} {getGradingLanguageName(submission.gradingLanguage)} {submission.latestGrading && } @@ -62,7 +60,7 @@ export function ChapterSubmissionsTable({ diff --git a/judgels-client/src/routes/courses/courses/single/chapters/single/submissions/modules/chapterSubmissionActions.js b/judgels-client/src/routes/courses/courses/single/chapters/single/problems/single/Programming/submissions/modules/chapterProblemSubmissionActions.js similarity index 79% rename from judgels-client/src/routes/courses/courses/single/chapters/single/submissions/modules/chapterSubmissionActions.js rename to judgels-client/src/routes/courses/courses/single/chapters/single/problems/single/Programming/submissions/modules/chapterProblemSubmissionActions.js index a38a0c6a4..9a281d540 100644 --- a/judgels-client/src/routes/courses/courses/single/chapters/single/submissions/modules/chapterSubmissionActions.js +++ b/judgels-client/src/routes/courses/courses/single/chapters/single/problems/single/Programming/submissions/modules/chapterProblemSubmissionActions.js @@ -1,11 +1,11 @@ import { push } from 'connected-react-router'; -import { selectToken } from '../../../../../../../../modules/session/sessionSelectors'; -import { selectIsDarkMode } from '../../../../../../../../modules/webPrefs/webPrefsSelectors'; -import { submissionProgrammingAPI } from '../../../../../../../../modules/api/jerahmeel/submissionProgramming'; -import { toastActions } from '../../../../../../../../modules/toast/toastActions'; +import { selectToken } from '../../../../../../../../../../../modules/session/sessionSelectors'; +import { submissionProgrammingAPI } from '../../../../../../../../../../../modules/api/jerahmeel/submissionProgramming'; +import { toastActions } from '../../../../../../../../../../../modules/toast/toastActions'; +import { selectIsDarkMode } from '../../../../../../../../../../../modules/webPrefs/webPrefsSelectors'; -export function getSubmissions(chapterJid, username, problemAlias, page) { +export function getSubmissions(chapterJid, problemAlias, username, page) { return async (dispatch, getState) => { const token = selectToken(getState()); return await submissionProgrammingAPI.getSubmissions(token, chapterJid, username, undefined, problemAlias, page); @@ -19,7 +19,14 @@ export function getSubmissionWithSource(submissionId, language) { }; } -export function createSubmission(courseSlug, chapterJid, chapterAlias, problemJid, data) { +export function getSubmissionSourceImage(submissionJid) { + return async (dispatch, getState) => { + const isDarkMode = selectIsDarkMode(getState()); + return await submissionProgrammingAPI.getSubmissionSourceImage(submissionJid, isDarkMode); + }; +} + +export function createSubmission(courseSlug, chapterJid, chapterAlias, problemJid, problemAlias, data) { return async (dispatch, getState) => { const token = selectToken(getState()); let sourceFiles = {}; @@ -32,7 +39,7 @@ export function createSubmission(courseSlug, chapterJid, chapterAlias, problemJi toastActions.showSuccessToast('Solution submitted.'); window.scrollTo(0, 0); - dispatch(push(`/courses/${courseSlug}/chapters/${chapterAlias}/submissions/mine`)); + dispatch(push(`/courses/${courseSlug}/chapters/${chapterAlias}/problems/${problemAlias}/submissions`)); }; } @@ -53,10 +60,3 @@ export function regradeSubmissions(chapterJid, username, problemAlias) { toastActions.showSuccessToast('Regrade in progress.'); }; } - -export function getSubmissionSourceImage(submissionJid) { - return async (dispatch, getState) => { - const isDarkMode = selectIsDarkMode(getState()); - return await submissionProgrammingAPI.getSubmissionSourceImage(submissionJid, isDarkMode); - }; -} diff --git a/judgels-client/src/routes/courses/courses/single/chapters/single/submissions/modules/chapterSubmissionActions.test.js b/judgels-client/src/routes/courses/courses/single/chapters/single/problems/single/Programming/submissions/modules/chapterProblemSubmissionActions.test.js similarity index 68% rename from judgels-client/src/routes/courses/courses/single/chapters/single/submissions/modules/chapterSubmissionActions.test.js rename to judgels-client/src/routes/courses/courses/single/chapters/single/problems/single/Programming/submissions/modules/chapterProblemSubmissionActions.test.js index 99bb6fea1..bcea002c3 100644 --- a/judgels-client/src/routes/courses/courses/single/chapters/single/submissions/modules/chapterSubmissionActions.test.js +++ b/judgels-client/src/routes/courses/courses/single/chapters/single/problems/single/Programming/submissions/modules/chapterProblemSubmissionActions.test.js @@ -2,14 +2,14 @@ import nock from 'nock'; import configureMockStore from 'redux-mock-store'; import thunk from 'redux-thunk'; -import { nockJerahmeel } from '../../../../../../../../utils/nock'; -import * as chapterSubmissionActions from './chapterSubmissionActions'; +import { nockJerahmeel } from '../../../../../../../../../../../utils/nock'; +import * as chapterProblemSubmissionActions from './chapterProblemSubmissionActions'; const chapterJid = 'chapter-jid'; const submissionId = 10; const mockStore = configureMockStore([thunk]); -describe('chapterSubmissionActions', () => { +describe('chapterProblemSubmissionActions', () => { let store; beforeEach(() => { @@ -31,11 +31,11 @@ describe('chapterSubmissionActions', () => { it('calls API', async () => { nockJerahmeel() .get(`/submissions/programming`) - .query({ containerJid: chapterJid, username, problemAlias, page }) + .query({ containerJid: chapterJid, problemAlias, username, page }) .reply(200, responseBody); const response = await store.dispatch( - chapterSubmissionActions.getSubmissions(chapterJid, username, problemAlias, page) + chapterProblemSubmissionActions.getSubmissions(chapterJid, problemAlias, username, page) ); expect(response).toEqual(responseBody); }); @@ -53,7 +53,9 @@ describe('chapterSubmissionActions', () => { .query({ language }) .reply(200, responseBody); - const response = await store.dispatch(chapterSubmissionActions.getSubmissionWithSource(submissionId, language)); + const response = await store.dispatch( + chapterProblemSubmissionActions.getSubmissionWithSource(submissionId, language) + ); expect(response).toEqual(responseBody); }); }); diff --git a/judgels-client/src/routes/courses/courses/single/chapters/single/submissions/single/ChapterSubmissionPage/ChapterSubmissionPage.jsx b/judgels-client/src/routes/courses/courses/single/chapters/single/problems/single/Programming/submissions/single/ChapterProblemSubmissionPage/ChapterProblemSubmissionPage.jsx similarity index 51% rename from judgels-client/src/routes/courses/courses/single/chapters/single/submissions/single/ChapterSubmissionPage/ChapterSubmissionPage.jsx rename to judgels-client/src/routes/courses/courses/single/chapters/single/problems/single/Programming/submissions/single/ChapterProblemSubmissionPage/ChapterProblemSubmissionPage.jsx index e75dfe543..4a86382ec 100644 --- a/judgels-client/src/routes/courses/courses/single/chapters/single/submissions/single/ChapterSubmissionPage/ChapterSubmissionPage.jsx +++ b/judgels-client/src/routes/courses/courses/single/chapters/single/problems/single/Programming/submissions/single/ChapterProblemSubmissionPage/ChapterProblemSubmissionPage.jsx @@ -1,28 +1,29 @@ +import { ChevronLeft } from '@blueprintjs/icons'; import { Component } from 'react'; import { connect } from 'react-redux'; import { withRouter } from 'react-router'; -import { LoadingState } from '../../../../../../../../../components/LoadingState/LoadingState'; -import { ContentCard } from '../../../../../../../../../components/ContentCard/ContentCard'; -import { SubmissionDetails } from '../../../../../../../../../components/SubmissionDetails/Programming/SubmissionDetails'; -import { selectStatementLanguage } from '../../../../../../../../../modules/webPrefs/webPrefsSelectors'; -import { selectCourse } from '../../../../../../modules/courseSelectors'; -import { selectCourseChapter } from '../../../../modules/courseChapterSelectors'; -import * as breadcrumbsActions from '../../../../../../../../../modules/breadcrumbs/breadcrumbsActions'; -import * as chapterSubmissionActions from '../../modules/chapterSubmissionActions'; +import { LoadingState } from '../../../../../../../../../../../../components/LoadingState/LoadingState'; +import { ContentCard } from '../../../../../../../../../../../../components/ContentCard/ContentCard'; +import { ButtonLink } from '../../../../../../../../../../../../components/ButtonLink/ButtonLink'; +import { SubmissionDetails } from '../../../../../../../../../../../../components/SubmissionDetails/Programming/SubmissionDetails'; +import { selectStatementLanguage } from '../../../../../../../../../../../../modules/webPrefs/webPrefsSelectors'; +import { selectCourse } from '../../../../../../../../../modules/courseSelectors'; +import { selectCourseChapter } from '../../../../../../../modules/courseChapterSelectors'; +import * as breadcrumbsActions from '../../../../../../../../../../../../modules/breadcrumbs/breadcrumbsActions'; +import * as chapterProblemSubmissionActions from '../../modules/chapterProblemSubmissionActions'; -export class ChapterSubmissionPage extends Component { +export class ChapterProblemSubmissionPage extends Component { state = { submissionWithSource: undefined, sourceImageUrl: undefined, profile: undefined, problemName: undefined, - problemAlias: undefined, containerName: undefined, }; async componentDidMount() { - const { data, profile, problemName, problemAlias, containerName } = await this.props.onGetSubmissionWithSource( + const { data, profile, problemName, containerName } = await this.props.onGetSubmissionWithSource( +this.props.match.params.submissionId, this.props.statementLanguage ); @@ -33,7 +34,6 @@ export class ChapterSubmissionPage extends Component { sourceImageUrl, profile, problemName, - problemAlias, containerName, }); } @@ -43,18 +43,30 @@ export class ChapterSubmissionPage extends Component { } render() { + const { course, chapter } = this.props; + const { problemAlias } = this.props.match.params; + return ( -

Submission #{this.props.match.params.submissionId}

+

Submission #{this.props.match.params.submissionId}

+ } + to={`/courses/${course.slug}/chapters/${chapter.alias}/problems/${problemAlias}/submissions`} + > + Back +
+ {this.renderSubmission()}
); } renderSubmission = () => { - const { submissionWithSource, profile, problemName, problemAlias, containerName, sourceImageUrl } = this.state; + const { submissionWithSource, profile, sourceImageUrl } = this.state; const { course, chapter } = this.props; + const { problemAlias } = this.props.match.params; if (!submissionWithSource) { return ; @@ -66,11 +78,7 @@ export class ChapterSubmissionPage extends Component { source={submissionWithSource.source} sourceImageUrl={sourceImageUrl} profile={profile} - problemName={problemName} - problemAlias={problemAlias} problemUrl={`/courses/${course.slug}/chapters/${chapter.alias}/problems/${problemAlias}`} - containerTitle="Chapter" - containerName={containerName} /> ); }; @@ -83,10 +91,10 @@ const mapStateToProps = state => ({ }); const mapDispatchToProps = { - onGetSubmissionWithSource: chapterSubmissionActions.getSubmissionWithSource, - onGetSubmissionSourceImage: chapterSubmissionActions.getSubmissionSourceImage, + onGetSubmissionWithSource: chapterProblemSubmissionActions.getSubmissionWithSource, + onGetSubmissionSourceImage: chapterProblemSubmissionActions.getSubmissionSourceImage, onPushBreadcrumb: breadcrumbsActions.pushBreadcrumb, onPopBreadcrumb: breadcrumbsActions.popBreadcrumb, }; -export default withRouter(connect(mapStateToProps, mapDispatchToProps)(ChapterSubmissionPage)); +export default withRouter(connect(mapStateToProps, mapDispatchToProps)(ChapterProblemSubmissionPage)); diff --git a/judgels-client/src/routes/courses/courses/single/chapters/single/submissions/single/ChapterSubmissionPage/ChapterSubmissionPage.test.jsx b/judgels-client/src/routes/courses/courses/single/chapters/single/problems/single/Programming/submissions/single/ChapterProblemSubmissionPage/ChapterProblemSubmissionPage.test.jsx similarity index 66% rename from judgels-client/src/routes/courses/courses/single/chapters/single/submissions/single/ChapterSubmissionPage/ChapterSubmissionPage.test.jsx rename to judgels-client/src/routes/courses/courses/single/chapters/single/problems/single/Programming/submissions/single/ChapterProblemSubmissionPage/ChapterProblemSubmissionPage.test.jsx index 81931d923..cd4472a09 100644 --- a/judgels-client/src/routes/courses/courses/single/chapters/single/submissions/single/ChapterSubmissionPage/ChapterSubmissionPage.test.jsx +++ b/judgels-client/src/routes/courses/courses/single/chapters/single/problems/single/Programming/submissions/single/ChapterProblemSubmissionPage/ChapterProblemSubmissionPage.test.jsx @@ -4,21 +4,23 @@ import { MemoryRouter, Route } from 'react-router'; import { applyMiddleware, combineReducers, createStore } from 'redux'; import thunk from 'redux-thunk'; -import ChapterSubmissionPage from './ChapterSubmissionPage'; -import { OutputOnlyOverrides } from '../../../../../../../../../modules/api/gabriel/language'; -import webPrefsReducer, { PutStatementLanguage } from '../../../../../../../../../modules/webPrefs/webPrefsReducer'; -import courseReducer, { PutCourse } from '../../../../../../modules/courseReducer'; -import courseChapterReducer, { PutCourseChapter } from '../../../../modules/courseChapterReducer'; -import * as chapterSubmissionActions from '../../modules/chapterSubmissionActions'; +import ChapterProblemSubmissionPage from './ChapterProblemSubmissionPage'; +import { OutputOnlyOverrides } from '../../../../../../../../../../../../modules/api/gabriel/language'; +import webPrefsReducer, { + PutStatementLanguage, +} from '../../../../../../../../../../../../modules/webPrefs/webPrefsReducer'; +import courseReducer, { PutCourse } from '../../../../../../../../../modules/courseReducer'; +import courseChapterReducer, { PutCourseChapter } from '../../../../../../../modules/courseChapterReducer'; +import * as chapterProblemSubmissionActions from '../../modules/chapterProblemSubmissionActions'; -jest.mock('../../modules/chapterSubmissionActions'); +jest.mock('../../modules/chapterProblemSubmissionActions'); -describe('ChapterSubmissionPage', () => { +describe('ChapterProblemSubmissionPage', () => { let wrapper; let source = {}; const render = async () => { - chapterSubmissionActions.getSubmissionWithSource.mockReturnValue(() => + chapterProblemSubmissionActions.getSubmissionWithSource.mockReturnValue(() => Promise.resolve({ data: { submission: { @@ -30,7 +32,7 @@ describe('ChapterSubmissionPage', () => { }, }) ); - chapterSubmissionActions.getSubmissionSourceImage.mockReturnValue(() => Promise.resolve('image url')); + chapterProblemSubmissionActions.getSubmissionSourceImage.mockReturnValue(() => Promise.resolve('image url')); const store = createStore( combineReducers({ @@ -55,7 +57,7 @@ describe('ChapterSubmissionPage', () => { @@ -80,7 +82,7 @@ describe('ChapterSubmissionPage', () => { }); test('get source image url', () => { - expect(chapterSubmissionActions.getSubmissionSourceImage).toHaveBeenCalledWith('submissionJid'); + expect(chapterProblemSubmissionActions.getSubmissionSourceImage).toHaveBeenCalledWith('submissionJid'); }); }); }); diff --git a/judgels-client/src/routes/courses/courses/single/chapters/single/lessons/ChapterLessonCard/ChapterLessonCard.jsx b/judgels-client/src/routes/courses/courses/single/chapters/single/resources/ChapterLessonCard/ChapterLessonCard.jsx similarity index 73% rename from judgels-client/src/routes/courses/courses/single/chapters/single/lessons/ChapterLessonCard/ChapterLessonCard.jsx rename to judgels-client/src/routes/courses/courses/single/chapters/single/resources/ChapterLessonCard/ChapterLessonCard.jsx index c0f0c7214..0ad67297c 100644 --- a/judgels-client/src/routes/courses/courses/single/chapters/single/lessons/ChapterLessonCard/ChapterLessonCard.jsx +++ b/judgels-client/src/routes/courses/courses/single/chapters/single/resources/ChapterLessonCard/ChapterLessonCard.jsx @@ -1,14 +1,19 @@ +import { Presentation } from '@blueprintjs/icons'; + import { ContentCardLink } from '../../../../../../../../components/ContentCardLink/ContentCardLink'; +import './ChapterLessonCard.scss'; + export function ChapterLessonCard({ course, chapter, lesson, lessonName }) { return ( - + +

{lesson.alias}. {lessonName} - +

); } diff --git a/judgels-client/src/routes/courses/courses/single/chapters/single/resources/ChapterLessonCard/ChapterLessonCard.scss b/judgels-client/src/routes/courses/courses/single/chapters/single/resources/ChapterLessonCard/ChapterLessonCard.scss new file mode 100644 index 000000000..3d78f876b --- /dev/null +++ b/judgels-client/src/routes/courses/courses/single/chapters/single/resources/ChapterLessonCard/ChapterLessonCard.scss @@ -0,0 +1,13 @@ +.chapter-lesson-card { + margin-bottom: 10px; + + h4 { + display: inline-block; + margin-bottom: 0; + } + + .bp4-icon { + padding-bottom: 2px; + margin-right: 10px; + } +} diff --git a/judgels-client/src/routes/courses/courses/single/chapters/single/problems/ChapterProblemCard/ChapterProblemCard.jsx b/judgels-client/src/routes/courses/courses/single/chapters/single/resources/ChapterProblemCard/ChapterProblemCard.jsx similarity index 95% rename from judgels-client/src/routes/courses/courses/single/chapters/single/problems/ChapterProblemCard/ChapterProblemCard.jsx rename to judgels-client/src/routes/courses/courses/single/chapters/single/resources/ChapterProblemCard/ChapterProblemCard.jsx index 69bf2026e..493bdd7c1 100644 --- a/judgels-client/src/routes/courses/courses/single/chapters/single/problems/ChapterProblemCard/ChapterProblemCard.jsx +++ b/judgels-client/src/routes/courses/courses/single/chapters/single/resources/ChapterProblemCard/ChapterProblemCard.jsx @@ -1,3 +1,5 @@ +import { Selection } from '@blueprintjs/icons'; + import { ContentCardLink } from '../../../../../../../../components/ContentCardLink/ContentCardLink'; import { VerdictProgressTag } from '../../../../../../../../components/VerdictProgressTag/VerdictProgressTag'; import { ProgressBar } from '../../../../../../../../components/ProgressBar/ProgressBar'; @@ -27,6 +29,7 @@ export function ChapterProblemCard({ course, chapter, problem, progress, problem className="chapter-problem-card" to={`/courses/${course.slug}/chapters/${chapter.alias}/problems/${problem.alias}`} > +

{problem.alias}. {problemName} {renderProgress()} diff --git a/judgels-client/src/routes/courses/courses/single/chapters/single/resources/ChapterProblemCard/ChapterProblemCard.scss b/judgels-client/src/routes/courses/courses/single/chapters/single/resources/ChapterProblemCard/ChapterProblemCard.scss new file mode 100644 index 000000000..acfc35f3e --- /dev/null +++ b/judgels-client/src/routes/courses/courses/single/chapters/single/resources/ChapterProblemCard/ChapterProblemCard.scss @@ -0,0 +1,13 @@ +.chapter-problem-card { + margin-bottom: 10px; + + h4 { + margin-bottom: 0; + } + + .bp4-icon { + float: left; + padding-top: 1px; + margin-right: 10px; + } +} diff --git a/judgels-client/src/routes/courses/courses/single/chapters/single/resources/ChapterResourcesPage/ChapterResourcesPage.jsx b/judgels-client/src/routes/courses/courses/single/chapters/single/resources/ChapterResourcesPage/ChapterResourcesPage.jsx new file mode 100644 index 000000000..0e464de6f --- /dev/null +++ b/judgels-client/src/routes/courses/courses/single/chapters/single/resources/ChapterResourcesPage/ChapterResourcesPage.jsx @@ -0,0 +1,109 @@ +import { ChevronRight } from '@blueprintjs/icons'; +import { Component } from 'react'; +import { connect } from 'react-redux'; +import { withRouter } from 'react-router'; +import { Link } from 'react-router-dom'; + +import { LoadingContentCard } from '../../../../../../../../components/LoadingContentCard/LoadingContentCard'; +import { ChapterLessonCard } from '../ChapterLessonCard/ChapterLessonCard'; +import { ChapterProblemCard } from '../ChapterProblemCard/ChapterProblemCard'; +import { getLessonName } from '../../../../../../../../modules/api/sandalphon/lesson'; +import { getProblemName } from '../../../../../../../../modules/api/sandalphon/problem'; +import { selectCourse } from '../../../../../modules/courseSelectors'; +import { selectCourseChapter } from '../../../modules/courseChapterSelectors'; +import * as chapterResourcesActions from '../modules/chapterResourceActions'; + +import './ChapterResourcesPage.scss'; + +export class ChapterResourcesPage extends Component { + state = { + response: undefined, + }; + + async componentDidMount() { + const response = await this.props.onGetResources(this.props.chapter.jid); + this.setState({ + response, + }); + } + + render() { + return ( +
+ {this.renderHeader()} +
+ {this.renderResources()} +
+ ); + } + + renderHeader = () => { + const { course, chapter } = this.props; + + return ( +

+ + {course.name} + +   + +   + {chapter.alias}. {chapter.name} +

+ ); + }; + + renderResources = () => { + const { response } = this.state; + if (!response) { + return ; + } + + const [lessonsResponse, problemsResponse] = response; + const { data: lessons, lessonsMap } = lessonsResponse; + const { data: problems, problemsMap, problemProgressesMap } = problemsResponse; + + if (lessons.length === 0 && problems.length === 0) { + return ( +

+ No resources. +

+ ); + } + + return ( + <> + {lessons.map(lesson => { + const props = { + course: this.props.course, + chapter: this.props.chapter, + lesson, + lessonName: getLessonName(lessonsMap[lesson.lessonJid], undefined), + }; + return ; + })} + {problems.map(problem => { + const props = { + course: this.props.course, + chapter: this.props.chapter, + problem, + problemName: getProblemName(problemsMap[problem.problemJid], undefined), + progress: problemProgressesMap[problem.problemJid], + }; + return ; + })} + + ); + }; +} + +const mapStateToProps = state => ({ + course: selectCourse(state), + chapter: selectCourseChapter(state), +}); + +const mapDispatchToProps = { + onGetResources: chapterResourcesActions.getResources, +}; + +export default withRouter(connect(mapStateToProps, mapDispatchToProps)(ChapterResourcesPage)); diff --git a/judgels-client/src/routes/courses/courses/single/chapters/single/resources/ChapterResourcesPage/ChapterResourcesPage.scss b/judgels-client/src/routes/courses/courses/single/chapters/single/resources/ChapterResourcesPage/ChapterResourcesPage.scss new file mode 100644 index 000000000..bea9de7ff --- /dev/null +++ b/judgels-client/src/routes/courses/courses/single/chapters/single/resources/ChapterResourcesPage/ChapterResourcesPage.scss @@ -0,0 +1,25 @@ +@import '../../../../../../../../styles/base'; + +.chapter-resources-page { + flex: 1 1; + max-width: 865px; + margin-left: 20px; +} + +.chapter-resources-page__title { + margin-bottom: 20px; + line-height: 20px !important; +} + +.chapter-resources-page__title--link { + color: inherit !important; + + &:hover { + text-decoration: none; + color: $primary-color !important; + } +} + +.chapter-resources-page__title--chevron { + color: $dark-secondary-background-color !important; +} diff --git a/judgels-client/src/routes/courses/courses/single/chapters/single/resources/ChapterResourcesPage/ChapterResourcesPage.test.jsx b/judgels-client/src/routes/courses/courses/single/chapters/single/resources/ChapterResourcesPage/ChapterResourcesPage.test.jsx new file mode 100644 index 000000000..d2bb09441 --- /dev/null +++ b/judgels-client/src/routes/courses/courses/single/chapters/single/resources/ChapterResourcesPage/ChapterResourcesPage.test.jsx @@ -0,0 +1,125 @@ +import { mount } from 'enzyme'; +import { Provider } from 'react-redux'; +import { MemoryRouter, Route } from 'react-router'; +import { applyMiddleware, combineReducers, createStore } from 'redux'; +import thunk from 'redux-thunk'; + +import ChapterResourcesPage from './ChapterResourcesPage'; +import webPrefsReducer, { PutStatementLanguage } from '../../../../../../../../modules/webPrefs/webPrefsReducer'; +import courseReducer, { PutCourse } from '../../../../../modules/courseReducer'; +import courseChapterReducer, { PutCourseChapter } from '../../../modules/courseChapterReducer'; +import * as chapterResourceActions from '../modules/chapterResourceActions'; + +jest.mock('../modules/chapterResourceActions'); + +describe('ChapterResourcesPage', () => { + let wrapper; + let lessons; + let problems; + + const render = async () => { + chapterResourceActions.getResources.mockReturnValue(() => + Promise.resolve([ + { + data: lessons, + lessonsMap: { + lessonJid1: { + slug: 'lesson-x', + titlesByLanguage: { en: 'Lesson X' }, + defaultLanguage: 'en', + }, + lessonJid2: { + slug: 'lesson-y', + titlesByLanguage: { en: 'Lesson Y' }, + defaultLanguage: 'en', + }, + }, + }, + { + data: problems, + problemsMap: { + problemJid1: { + slug: 'problem-a', + titlesByLanguage: { en: 'Problem A' }, + defaultLanguage: 'en', + }, + problemJid2: { + slug: 'problem-b', + titlesByLanguage: { en: 'Problem B' }, + defaultLanguage: 'en', + }, + }, + problemProgressesMap: { + problemJid1: { verdict: 'AC', score: 100 }, + }, + }, + ]) + ); + + const store = createStore( + combineReducers({ + webPrefs: webPrefsReducer, + jerahmeel: combineReducers({ course: courseReducer, courseChapter: courseChapterReducer }), + }), + applyMiddleware(thunk) + ); + store.dispatch(PutCourse({ jid: 'courseJid', slug: 'courseSlug' })); + store.dispatch( + PutCourseChapter({ + jid: 'chapterJid', + name: 'Chapter 1', + alias: 'chapter-1', + courseSlug: 'courseSlug', + }) + ); + store.dispatch(PutStatementLanguage('en')); + + wrapper = mount( + + + + + + ); + + await new Promise(resolve => setImmediate(resolve)); + wrapper.update(); + }; + + describe('when there are no resources', () => { + beforeEach(async () => { + lessons = []; + problems = []; + await render(); + }); + + it('shows placeholder text and no resources', () => { + expect(wrapper.text()).toContain('No resources.'); + expect(wrapper.find('a.content-card-link')).toHaveLength(0); + }); + }); + + describe('when there are resources', () => { + beforeEach(async () => { + lessons = [ + { lessonJid: 'lessonJid1', alias: 'X' }, + { lessonJid: 'lessonJid2', alias: 'Y' }, + ]; + problems = [ + { problemJid: 'problemJid1', alias: 'A' }, + { problemJid: 'problemJid2', alias: 'B' }, + ]; + await render(); + }); + + it('shows the resources', () => { + const cards = wrapper.find('a.content-card-link'); + expect(cards.map(card => [card.text(), card.find('a').props().href])).toEqual([ + ['presentationX. Lesson X', '/courses/courseSlug/chapters/chapter-1/lessons/X'], + ['presentationY. Lesson Y', '/courses/courseSlug/chapters/chapter-1/lessons/Y'], + ['selectionA. Problem AAC 100', '/courses/courseSlug/chapters/chapter-1/problems/A'], + ['selectionB. Problem B', '/courses/courseSlug/chapters/chapter-1/problems/B'], + ]); + }); + }); +}); diff --git a/judgels-client/src/routes/courses/courses/single/chapters/single/resources/modules/chapterResourceActions.js b/judgels-client/src/routes/courses/courses/single/chapters/single/resources/modules/chapterResourceActions.js new file mode 100644 index 000000000..fb344a97d --- /dev/null +++ b/judgels-client/src/routes/courses/courses/single/chapters/single/resources/modules/chapterResourceActions.js @@ -0,0 +1,13 @@ +import { selectToken } from '../../../../../../../../modules/session/sessionSelectors'; +import { chapterLessonAPI } from '../../../../../../../../modules/api/jerahmeel/chapterLesson'; +import { chapterProblemAPI } from '../../../../../../../../modules/api/jerahmeel/chapterProblem'; + +export function getResources(chapterJid) { + return async (dispatch, getState) => { + const token = selectToken(getState()); + return await Promise.all([ + chapterLessonAPI.getLessons(token, chapterJid), + chapterProblemAPI.getProblems(token, chapterJid), + ]); + }; +} diff --git a/judgels-client/src/routes/courses/courses/single/chapters/single/resources/modules/chapterResourceActions.test.js b/judgels-client/src/routes/courses/courses/single/chapters/single/resources/modules/chapterResourceActions.test.js new file mode 100644 index 000000000..0ee80d5ae --- /dev/null +++ b/judgels-client/src/routes/courses/courses/single/chapters/single/resources/modules/chapterResourceActions.test.js @@ -0,0 +1,40 @@ +import nock from 'nock'; +import configureMockStore from 'redux-mock-store'; +import thunk from 'redux-thunk'; + +import { nockJerahmeel } from '../../../../../../../../utils/nock'; +import * as chapterResourceActions from './chapterResourceActions'; + +const chapterJid = 'chapter-jid'; +const mockStore = configureMockStore([thunk]); + +describe('chapterResourceActions', () => { + let store; + + beforeEach(() => { + store = mockStore({}); + }); + + afterEach(function() { + nock.cleanAll(); + }); + + describe('getResources()', () => { + const responseBody = { + data: [], + }; + + it('calls APIs', async () => { + nockJerahmeel() + .get(`/chapters/${chapterJid}/lessons`) + .reply(200, responseBody); + + nockJerahmeel() + .get(`/chapters/${chapterJid}/problems`) + .reply(200, responseBody); + + const response = await store.dispatch(chapterResourceActions.getResources(chapterJid)); + expect(response).toEqual([responseBody, responseBody]); + }); + }); +}); diff --git a/judgels-client/src/routes/courses/courses/single/chapters/single/results/ChapterItemSubmissionRoutes.jsx b/judgels-client/src/routes/courses/courses/single/chapters/single/results/ChapterItemSubmissionRoutes.jsx deleted file mode 100644 index d75585dab..000000000 --- a/judgels-client/src/routes/courses/courses/single/chapters/single/results/ChapterItemSubmissionRoutes.jsx +++ /dev/null @@ -1,22 +0,0 @@ -import { Route, Switch } from 'react-router'; - -import { withBreadcrumb } from '../../../../../../../components/BreadcrumbWrapper/BreadcrumbWrapper'; -import ChapterSubmissionsPage from './ChapterSubmissionsPage/ChapterSubmissionsPage'; -import ChapterSubmissionSummaryPage from './ChapterSubmissionSummaryPage/ChapterSubmissionSummaryPage'; - -function ChapterItemSubmissionRoutes() { - return ( -
- - - - - -
- ); -} - -export default withBreadcrumb('Quiz results')(ChapterItemSubmissionRoutes); diff --git a/judgels-client/src/routes/courses/courses/single/chapters/single/results/ChapterSubmissionSummaryPage/ChapterSubmissionSummaryPage.jsx b/judgels-client/src/routes/courses/courses/single/chapters/single/results/ChapterSubmissionSummaryPage/ChapterSubmissionSummaryPage.jsx deleted file mode 100644 index aa5a0715c..000000000 --- a/judgels-client/src/routes/courses/courses/single/chapters/single/results/ChapterSubmissionSummaryPage/ChapterSubmissionSummaryPage.jsx +++ /dev/null @@ -1,107 +0,0 @@ -import { Component } from 'react'; -import { connect } from 'react-redux'; -import { withRouter } from 'react-router'; -import { LoadingState } from '../../../../../../../../components/LoadingState/LoadingState'; - -import { ContentCard } from '../../../../../../../../components/ContentCard/ContentCard'; -import { UserRef } from '../../../../../../../../components/UserRef/UserRef'; -import ItemSubmissionUserFilter from '../../../../../../../../components/ItemSubmissionUserFilter/ItemSubmissionUserFilter'; -import { selectMaybeUserJid } from '../../../../../../../../modules/session/sessionSelectors'; -import { selectCourseChapter } from '../../../modules/courseChapterSelectors'; -import { selectStatementLanguage } from '../../../../../../../../modules/webPrefs/webPrefsSelectors'; -import { ProblemSubmissionCard } from '../../../../../../../../components/SubmissionDetails/Bundle/ProblemSubmissionsCard/ProblemSubmissionCard'; -import * as chapterSubmissionActions from '../modules/chapterSubmissionActions'; - -class ChapterSubmissionSummaryPage extends Component { - state = { - config: undefined, - profile: undefined, - problemSummaries: undefined, - }; - - async refreshSubmissions() { - const { userJid, chapter, onGetSubmissionSummary } = this.props; - if (!userJid) { - this.setState({ problemSummaries: [] }); - return; - } - - const response = await onGetSubmissionSummary(chapter.jid, this.props.match.params.username, this.props.language); - - const problemSummaries = response.config.problemJids.map(problemJid => ({ - name: response.problemNamesMap[problemJid] || '-', - alias: response.problemAliasesMap[chapter.jid + '-' + problemJid] || '-', - itemJids: response.itemJidsByProblemJid[problemJid], - submissionsByItemJid: response.submissionsByItemJid, - canViewGrading: true, - canManage: response.config.canManage, - itemTypesMap: response.itemTypesMap, - onRegrade: () => this.regrade(problemJid), - })); - - this.setState({ config: response.config, profile: response.profile, problemSummaries }); - } - - async componentDidMount() { - await this.refreshSubmissions(); - } - - render() { - return ( - -

Quiz Results

-
- {this.renderUserFilter()} - {this.renderResults()} -
- ); - } - - renderUserFilter = () => { - if (this.props.location.pathname.includes('/users/')) { - return null; - } - return ; - }; - - renderResults = () => { - const { problemSummaries } = this.state; - if (!problemSummaries) { - return ; - } - if (problemSummaries.length === 0) { - return No quizzes.; - } - return ( - <> - - Summary for - - {this.state.problemSummaries.map(props => ( - - ))} - - ); - }; - - regrade = async problemJid => { - const { userJids } = this.state.config; - const userJid = userJids[0]; - - await this.props.onRegradeAll(this.props.chapter.jid, userJid, problemJid); - await this.refreshSubmissions(); - }; -} - -const mapStateToProps = state => ({ - userJid: selectMaybeUserJid(state), - chapter: selectCourseChapter(state), - language: selectStatementLanguage(state), -}); - -const mapDispatchToProps = { - onGetSubmissionSummary: chapterSubmissionActions.getSubmissionSummary, - onRegradeAll: chapterSubmissionActions.regradeSubmissions, -}; - -export default withRouter(connect(mapStateToProps, mapDispatchToProps)(ChapterSubmissionSummaryPage)); diff --git a/judgels-client/src/routes/courses/courses/single/chapters/single/results/ChapterSubmissionsPage/ChapterSubmissionsPage.jsx b/judgels-client/src/routes/courses/courses/single/chapters/single/results/ChapterSubmissionsPage/ChapterSubmissionsPage.jsx deleted file mode 100644 index d2a06da26..000000000 --- a/judgels-client/src/routes/courses/courses/single/chapters/single/results/ChapterSubmissionsPage/ChapterSubmissionsPage.jsx +++ /dev/null @@ -1,180 +0,0 @@ -import { Button, HTMLTable, Intent, ButtonGroup } from '@blueprintjs/core'; -import { Refresh, Search } from '@blueprintjs/icons'; -import { parse, stringify } from 'query-string'; -import { Component } from 'react'; -import { connect } from 'react-redux'; -import { withRouter, Link } from 'react-router-dom'; -import { push } from 'connected-react-router'; - -import { reallyConfirm } from '../../../../../../../../utils/confirmation'; -import { FormattedRelative } from '../../../../../../../../components/FormattedRelative/FormattedRelative'; -import { LoadingState } from '../../../../../../../../components/LoadingState/LoadingState'; -import { ContentCard } from '../../../../../../../../components/ContentCard/ContentCard'; -import { UserRef } from '../../../../../../../../components/UserRef/UserRef'; -import Pagination from '../../../../../../../../components/Pagination/Pagination'; -import ItemSubmissionUserFilter from '../../../../../../../../components/ItemSubmissionUserFilter/ItemSubmissionUserFilter'; -import { VerdictTag } from '../../../../../../../../components/SubmissionDetails/Bundle/VerdictTag/VerdictTag'; -import { FormattedAnswer } from '../../../../../../../../components/SubmissionDetails/Bundle/FormattedAnswer/FormattedAnswer'; -import { selectMaybeUserJid } from '../../../../../../../../modules/session/sessionSelectors'; -import { selectCourse } from '../../../../../modules/courseSelectors'; -import { selectCourseChapter } from '../../../modules/courseChapterSelectors'; -import * as chapterSubmissionActions from '../modules/chapterSubmissionActions'; - -import '../../../../../../../../components/SubmissionsTable/Bundle/ItemSubmissionsTable.scss'; - -export class ChapterSubmissionsPage extends Component { - static PAGE_SIZE = 20; - - state = { - response: undefined, - }; - - render() { - return ( - -

Quiz Results

-
- - {this.renderRegradeAllButton()} - {this.renderSubmissions()} - {this.renderPagination()} -
- ); - } - - renderSubmissions = () => { - const response = this.state.response; - if (!response) { - return ; - } - - const { data, profilesMap, problemAliasesMap, itemNumbersMap, itemTypesMap } = response; - const { userJid, course, chapter } = this.props; - const canManage = response.config.canManage; - - return ( - - - - User - Prob - No - Answer - {canManage && Verdict} - Time - - - - - {data.page.map(item => ( - - - - - {problemAliasesMap[this.props.chapter.jid + '-' + item.problemJid] || '-'} - {itemNumbersMap[item.itemJid] || '-'} - - - - {canManage && ( - {item.grading ? : '-'} - )} - - - - - - {(canManage || userJid === item.userJid) && ( - - - ); - }; -} - -const mapStateToProps = state => ({ - userJid: selectMaybeUserJid(state), - course: selectCourse(state), - chapter: selectCourseChapter(state), -}); - -const mapDispatchToProps = { - onGetSubmissions: chapterSubmissionActions.getSubmissions, - onRegrade: chapterSubmissionActions.regradeSubmission, - onRegradeAll: chapterSubmissionActions.regradeSubmissions, - onAppendRoute: queries => push({ search: stringify(queries) }), -}; - -export default withRouter(connect(mapStateToProps, mapDispatchToProps)(ChapterSubmissionsPage)); diff --git a/judgels-client/src/routes/courses/courses/single/chapters/single/submissions/ChapterSubmissionRoutes.jsx b/judgels-client/src/routes/courses/courses/single/chapters/single/submissions/ChapterSubmissionRoutes.jsx deleted file mode 100644 index 8a58f4639..000000000 --- a/judgels-client/src/routes/courses/courses/single/chapters/single/submissions/ChapterSubmissionRoutes.jsx +++ /dev/null @@ -1,30 +0,0 @@ -import { Route, Switch } from 'react-router'; - -import { withBreadcrumb } from '../../../../../../../components/BreadcrumbWrapper/BreadcrumbWrapper'; -import ChapterSubmissionsPage from './ChapterSubmissionsPage/ChapterSubmissionsPage'; -import ChapterSubmissionPage from './single/ChapterSubmissionPage/ChapterSubmissionPage'; - -function ChapterSubmissionRoutes() { - return ( -
- - - - - -
- ); -} - -export default withBreadcrumb('Submissions')(ChapterSubmissionRoutes); diff --git a/judgels-client/src/routes/courses/courses/single/chapters/single/submissions/ChapterSubmissionsPage/ChapterSubmissionsPage.jsx b/judgels-client/src/routes/courses/courses/single/chapters/single/submissions/ChapterSubmissionsPage/ChapterSubmissionsPage.jsx deleted file mode 100644 index 5d06d00f8..000000000 --- a/judgels-client/src/routes/courses/courses/single/chapters/single/submissions/ChapterSubmissionsPage/ChapterSubmissionsPage.jsx +++ /dev/null @@ -1,187 +0,0 @@ -import { push } from 'connected-react-router'; -import { parse, stringify } from 'query-string'; -import { Component } from 'react'; -import { connect } from 'react-redux'; -import { withRouter } from 'react-router'; - -import { reallyConfirm } from '../../../../../../../../utils/confirmation'; -import { LoadingState } from '../../../../../../../../components/LoadingState/LoadingState'; -import { ContentCard } from '../../../../../../../../components/ContentCard/ContentCard'; -import { RegradeAllButton } from '../../../../../../../../components/RegradeAllButton/RegradeAllButton'; -import Pagination from '../../../../../../../../components/Pagination/Pagination'; -import SubmissionUserFilter from '../../../../../../../../components/SubmissionUserFilter/SubmissionUserFilter'; -import { SubmissionFilterWidget } from '../../../../../../../../components/SubmissionFilterWidget/SubmissionFilterWidget'; -import { ChapterSubmissionsTable } from '../ChapterSubmissionsTable/ChapterSubmissionsTable'; -import { selectMaybeUserJid, selectMaybeUsername } from '../../../../../../../../modules/session/sessionSelectors'; -import { selectCourse } from '../../../../../modules/courseSelectors'; -import { selectCourseChapter } from '../../../modules/courseChapterSelectors'; -import * as chapterSubmissionActions from '../modules/chapterSubmissionActions'; - -export class ChapterSubmissionsPage extends Component { - static PAGE_SIZE = 20; - - state; - - constructor(props) { - super(props); - - const queries = parse(this.props.location.search); - const problemAlias = queries.problemAlias; - - this.state = { - response: undefined, - filter: { problemAlias }, - isFilterLoading: false, - }; - } - - componentDidUpdate() { - const queries = parse(this.props.location.search); - const problemAlias = queries.problemAlias; - - if (problemAlias !== this.state.filter.problemAlias) { - this.setState({ filter: { problemAlias }, isFilterLoading: true }); - } - } - - render() { - return ( - -

Submissions

-
- {this.renderUserFilter()} - {this.renderHeader()} - {this.renderSubmissions()} - {this.renderPagination()} -
- ); - } - - renderHeader = () => { - return ( -
-
{this.renderRegradeAllButton()}
- {this.renderFilterWidget()} -
-
- ); - }; - - renderUserFilter = () => { - return this.props.userJid && ; - }; - - isUserFilterMine = () => { - return (this.props.location.pathname + '/').includes('/mine/'); - }; - - renderRegradeAllButton = () => { - if (!this.state.response || !this.state.response.config.canManage) { - return null; - } - return ; - }; - - renderSubmissions = () => { - const { response } = this.state; - if (!response) { - return ; - } - - const { data: submissions, config, profilesMap, problemAliasesMap } = response; - if (submissions.page.length === 0) { - return ( -

- No submissions. -

- ); - } - - return ( - - ); - }; - - renderPagination = () => { - const { filter } = this.state; - - const key = '' + filter.problemAlias + this.isUserFilterMine(); - return ; - }; - - onChangePage = async nextPage => { - const { problemAlias } = this.state.filter; - const data = await this.refreshSubmissions(problemAlias, nextPage); - return data.totalCount; - }; - - refreshSubmissions = async (problemAlias, page) => { - const username = this.isUserFilterMine() ? this.props.username : undefined; - const response = await this.props.onGetProgrammingSubmissions(this.props.chapter.jid, username, problemAlias, page); - this.setState({ response, isFilterLoading: false }); - return response.data; - }; - - renderFilterWidget = () => { - const { response, filter, isFilterLoading } = this.state; - if (!response || !filter) { - return null; - } - const { config, problemAliasesMap } = response; - const { problemJids } = config; - - const { problemAlias } = filter; - return ( - problemAliasesMap[this.props.chapter.jid + '-' + jid])} - problemAlias={problemAlias} - onFilter={this.onFilter} - isLoading={!!isFilterLoading} - /> - ); - }; - - onFilter = async filter => { - this.props.onAppendRoute(filter); - }; - - onRegrade = async submissionJid => { - await this.props.onRegrade(submissionJid); - const { problemAlias } = this.state.filter; - const queries = parse(this.props.location.search); - await this.refreshSubmissions(problemAlias, queries.page); - }; - - onRegradeAll = async () => { - if (reallyConfirm('Regrade all submissions in all pages for the current filter?')) { - const { problemAlias } = this.state.filter; - await this.props.onRegradeAll(this.props.chapter.jid, undefined, problemAlias); - const queries = parse(this.props.location.search); - await this.refreshSubmissions(problemAlias, queries.page); - } - }; -} - -const mapStateToProps = state => ({ - userJid: selectMaybeUserJid(state), - username: selectMaybeUsername(state), - course: selectCourse(state), - chapter: selectCourseChapter(state), -}); - -const mapDispatchToProps = { - onGetProgrammingSubmissions: chapterSubmissionActions.getSubmissions, - onRegrade: chapterSubmissionActions.regradeSubmission, - onRegradeAll: chapterSubmissionActions.regradeSubmissions, - onAppendRoute: queries => push({ search: stringify(queries) }), -}; - -export default withRouter(connect(mapStateToProps, mapDispatchToProps)(ChapterSubmissionsPage)); diff --git a/judgels-client/src/routes/problems/problemsets/single/SingleProblemSetRoutes.scss b/judgels-client/src/routes/problems/problemsets/single/SingleProblemSetRoutes.scss index c46e12d25..ff490d29a 100644 --- a/judgels-client/src/routes/problems/problemsets/single/SingleProblemSetRoutes.scss +++ b/judgels-client/src/routes/problems/problemsets/single/SingleProblemSetRoutes.scss @@ -2,12 +2,3 @@ margin-top: -2px; padding-bottom: 8px; } - -@media only screen and (max-width: 750px) { - .single-problemset-routes__header { - h2 { - font-size: 20px !important; - line-height: 22px !important; - } - } -} diff --git a/judgels-client/src/routes/problems/problemsets/single/problems/single/results/ProblemSubmissionSummaryPage/ProblemSubmissionSummaryPage.jsx b/judgels-client/src/routes/problems/problemsets/single/problems/single/results/ProblemSubmissionSummaryPage/ProblemSubmissionSummaryPage.jsx index 10184838a..bd058b543 100644 --- a/judgels-client/src/routes/problems/problemsets/single/problems/single/results/ProblemSubmissionSummaryPage/ProblemSubmissionSummaryPage.jsx +++ b/judgels-client/src/routes/problems/problemsets/single/problems/single/results/ProblemSubmissionSummaryPage/ProblemSubmissionSummaryPage.jsx @@ -9,7 +9,7 @@ import { selectMaybeUserJid } from '../../../../../../../../modules/session/sess import { selectProblemSet } from '../../../../../modules/problemSetSelectors'; import { selectProblemSetProblem } from '../../../modules/problemSetProblemSelectors'; import { selectStatementLanguage } from '../../../../../../../../modules/webPrefs/webPrefsSelectors'; -import { ProblemSubmissionCard } from '../../../../../../../../components/SubmissionDetails/Bundle/ProblemSubmissionsCard/ProblemSubmissionCard'; +import { SubmissionDetails } from '../../../../../../../../components/SubmissionDetails/Bundle/SubmissionDetails/SubmissionDetails'; import * as problemSetSubmissionActions from '../modules/problemSetSubmissionActions'; class ProblemSubmissionSummaryPage extends Component { @@ -82,7 +82,7 @@ class ProblemSubmissionSummaryPage extends Component { Summary for {this.state.problemSummaries.map(props => ( - + ))} ); diff --git a/judgels-client/src/styles/index.scss b/judgels-client/src/styles/index.scss index 3d376d5a7..4c5988443 100644 --- a/judgels-client/src/styles/index.scss +++ b/judgels-client/src/styles/index.scss @@ -83,6 +83,13 @@ h5 { font-size: 14px !important; } +@media only screen and (max-width: 750px) { + h2 { + font-size: 20px !important; + line-height: 22px !important; + } +} + hr { margin-top: 15px !important; margin-bottom: 15px !important; @@ -225,3 +232,10 @@ blockquote { .normal-weight { font-weight: normal !important; } + +.heading-with-button-action { + display: inline-block; + margin-bottom: 0 !important; + margin-right: 10px; + vertical-align: middle; +}