From bffa7aaf9dd1064b3b182c4c62c44b1707136039 Mon Sep 17 00:00:00 2001 From: PupoSDC Date: Thu, 8 Feb 2024 08:24:39 +0000 Subject: [PATCH] feat: add components for question editor (#139) Adds most of the components for the question editor. Connections between them are still missing. --- .../questions/editor/[questionId].page.tsx | 107 +++--- .../questions/editor/index.page.tsx | 56 ++- libs/base/utils/src/index.ts | 1 + libs/base/utils/src/js/deep-clone.ts | 3 + .../src/entities/question-bank-question.ts | 16 +- .../src/questions/get-new-variant.ts | 15 +- .../src/questions/get-question-preview.ts | 16 +- .../src/question-bank/question-search.ts | 4 +- libs/react/components/src/index.ts | 1 + .../components/src/loading-button/index.ts | 1 + .../src/loading-button/loading-button.tsx | 26 ++ .../create-use-persistence-hook.ts | 2 +- .../src/hooks/use-persistence/index.ts | 1 + libs/react/containers/src/index.ts | 2 + .../layouts/layout-module/layout-module.tsx | 3 +- .../questions/hooks/use-question-editor.tsx | 354 ++++++++++++++++-- .../question-editor-annexes.tsx | 212 ++++++----- .../question-editor-diff-tool/index.ts | 1 + .../question-editor-diff-tool.tsx | 221 +++++++++++ .../question-editor-explanation.tsx | 68 ++-- .../question-editor-learning-objectives.tsx | 199 +++++----- .../question-editor-preview/index.ts | 1 + .../question-editor-preview.tsx | 127 +++++++ .../question-editor-related-questions.tsx | 247 ++++++------ .../question-editor-variant.tsx | 46 ++- .../question-manager-changes-list-item.tsx | 87 ----- .../question-manager-changes-list.tsx | 47 --- .../question-manager-question-search-item.tsx | 148 -------- .../question-manager-question-search.tsx | 61 --- .../hooks/use-question-editor-data.tsx | 13 - .../question-manager/question-manager.tsx | 328 ++++++++++++++-- .../src/routers/common/questions-router.ts | 3 +- .../routers/containers/questions-router.ts | 12 +- package.json | 2 + pnpm-lock.yaml | 55 ++- 35 files changed, 1602 insertions(+), 884 deletions(-) create mode 100644 libs/base/utils/src/js/deep-clone.ts create mode 100644 libs/react/components/src/loading-button/index.ts create mode 100644 libs/react/components/src/loading-button/loading-button.tsx create mode 100644 libs/react/containers/src/questions/question-editor-diff-tool/index.ts create mode 100644 libs/react/containers/src/questions/question-editor-diff-tool/question-editor-diff-tool.tsx create mode 100644 libs/react/containers/src/questions/question-editor-preview/index.ts create mode 100644 libs/react/containers/src/questions/question-editor-preview/question-editor-preview.tsx delete mode 100644 libs/react/containers/src/questions/question-manager/components/question-manager-changes-list-item.tsx delete mode 100644 libs/react/containers/src/questions/question-manager/components/question-manager-changes-list.tsx delete mode 100644 libs/react/containers/src/questions/question-manager/components/question-manager-question-search-item.tsx delete mode 100644 libs/react/containers/src/questions/question-manager/components/question-manager-question-search.tsx delete mode 100644 libs/react/containers/src/questions/question-manager/hooks/use-question-editor-data.tsx diff --git a/apps/next-app/pages/modules/[questionBank]/questions/editor/[questionId].page.tsx b/apps/next-app/pages/modules/[questionBank]/questions/editor/[questionId].page.tsx index 74634fdf3..1e56c8b26 100644 --- a/apps/next-app/pages/modules/[questionBank]/questions/editor/[questionId].page.tsx +++ b/apps/next-app/pages/modules/[questionBank]/questions/editor/[questionId].page.tsx @@ -1,11 +1,20 @@ -import { useRouter } from "next/router"; -import { Box, Tab, TabList, TabPanel, Tabs, tabClasses } from "@mui/joy"; +import { default as RightArrow } from "@mui/icons-material/ChevronRightOutlined"; +import { + Divider, + Tab, + TabList, + TabPanel, + Tabs, + tabClasses, + tabPanelClasses, +} from "@mui/joy"; import { AppHead } from "@chair-flight/react/components"; import { LayoutModule, QuestionEditorAnnexes, QuestionEditorExplanation, QuestionEditorLearningObjectives, + QuestionEditorPreview, QuestionEditorRelatedQuestions, QuestionEditorVariant, } from "@chair-flight/react/containers"; @@ -14,11 +23,7 @@ import type { QuestionBankName } from "@chair-flight/core/question-bank"; import type { Breadcrumbs } from "@chair-flight/react/containers"; import type { NextPage } from "next"; -type QueryParams = { - tab?: string; -}; - -type PageParams = QueryParams & { +type PageParams = { questionBank: QuestionBankName; questionId: string; }; @@ -26,26 +31,9 @@ type PageParams = QueryParams & { type PageProps = { questionBank: QuestionBankName; questionId: string; - tab: string; }; -const Page: NextPage = ({ - questionBank, - questionId, - tab: initialTab, -}) => { - const router = useRouter(); - const query = router.query as PageParams; - const tab = query.tab ?? initialTab; - - const updateQuery = (query: QueryParams) => { - router.push( - { ...router, query: { ...router.query, ...query } }, - undefined, - { shallow: true }, - ); - }; - +const Page: NextPage = ({ questionBank, questionId }) => { const crumbs = [ [questionBank.toUpperCase(), `/modules/${questionBank}`], ["Questions", `/modules/${questionBank}/questions`], @@ -59,76 +47,87 @@ const Page: NextPage = ({ breadcrumbs={crumbs} fixedHeight noPadding + sx={{ flexDirection: "row" }} > + updateQuery({ tab: v as string })} + defaultValue={"question"} sx={{ - backgroundColor: "transparent", - display: "flex", - flexDirection: "column", - height: "100%", + pl: 1, + flex: 1, + background: "transparent", + + [`& .${tabPanelClasses.root}`]: { + flex: 1, + overflow: "hidden", + my: 1, + py: 0, + }, + + [`& .${tabClasses.selected}`]: { + color: "primary.plainColor", + background: "transparent", + }, }} > - `calc(${theme.spacing(5)} + 2px)`, - - [`& .${tabClasses.selected}`]: { - color: "primary.plainColor", - }, - }} - > + Question Explanation - Learning Objectives - Related Questions + LOs Annexes + Related - `calc(${theme.spacing(5)} + 2px)` }} /> - + - + - - + - - + - - + + + + + ); }; diff --git a/apps/next-app/pages/modules/[questionBank]/questions/editor/index.page.tsx b/apps/next-app/pages/modules/[questionBank]/questions/editor/index.page.tsx index d18d22ea4..09b5427db 100644 --- a/apps/next-app/pages/modules/[questionBank]/questions/editor/index.page.tsx +++ b/apps/next-app/pages/modules/[questionBank]/questions/editor/index.page.tsx @@ -1,6 +1,11 @@ import * as fs from "node:fs/promises"; +import { Box, Tab, TabList, TabPanel, Tabs, tabClasses } from "@mui/joy"; import { AppHead } from "@chair-flight/react/components"; -import { LayoutModule, QuestionManager } from "@chair-flight/react/containers"; +import { + LayoutModule, + QuestionEditorDiffTool, + QuestionManager, +} from "@chair-flight/react/containers"; import { staticHandler } from "@chair-flight/trpc/server"; import type { QuestionBankName } from "@chair-flight/core/question-bank"; import type { Breadcrumbs } from "@chair-flight/react/containers"; @@ -22,9 +27,54 @@ const Page: NextPage = ({ questionBank }) => { ] as Breadcrumbs; return ( - + - + + `calc(${theme.spacing(5)} + 2px)`, + + [`& .${tabClasses.selected}`]: { + color: "primary.plainColor", + }, + }} + > + Intro + Pick Questions + Edit Questions + Submit Changes + + `calc(${theme.spacing(5)} + 2px)` }} /> + + + + + + + ); }; diff --git a/libs/base/utils/src/index.ts b/libs/base/utils/src/index.ts index 6c1c44c76..e50960c03 100644 --- a/libs/base/utils/src/index.ts +++ b/libs/base/utils/src/index.ts @@ -1,4 +1,5 @@ export * from "./assert/assert-type"; +export * from "./js/deep-clone"; export * from "./js/make-map"; export * from "./js/noop"; export * from "./random/random"; diff --git a/libs/base/utils/src/js/deep-clone.ts b/libs/base/utils/src/js/deep-clone.ts new file mode 100644 index 000000000..b19ab1030 --- /dev/null +++ b/libs/base/utils/src/js/deep-clone.ts @@ -0,0 +1,3 @@ +export const deepClone = (obj: T): T => { + return JSON.parse(JSON.stringify(obj)) as T; +}; diff --git a/libs/core/question-bank/src/entities/question-bank-question.ts b/libs/core/question-bank/src/entities/question-bank-question.ts index fb84ca177..f83340240 100644 --- a/libs/core/question-bank/src/entities/question-bank-question.ts +++ b/libs/core/question-bank/src/entities/question-bank-question.ts @@ -42,6 +42,14 @@ export type QuestionTemplate = { variant: QuestionVariant; }; +export const questionVariantSchema = z.union([ + questionVariantSimpleSchema, + questionVariantDefinitionSchema, + questionVariantTrueOrFalseSchema, + questionVariantMultipleCorrectSchema, + questionVariantOneTwoSchema, +]); + export const questionTemplateSchema = z.object({ id: z.string(), doc: z.string(), @@ -52,13 +60,7 @@ export const questionTemplateSchema = z.object({ learningObjectives: z.array(z.string()).min(1), explanation: z.string(), srcLocation: z.string().min(6), - variant: z.union([ - questionVariantSimpleSchema, - questionVariantDefinitionSchema, - questionVariantTrueOrFalseSchema, - questionVariantMultipleCorrectSchema, - questionVariantOneTwoSchema, - ]), + variant: questionVariantSchema, }); type IQuestionTemplate = z.infer; diff --git a/libs/core/question-bank/src/questions/get-new-variant.ts b/libs/core/question-bank/src/questions/get-new-variant.ts index 78fc7824c..f1289b80d 100644 --- a/libs/core/question-bank/src/questions/get-new-variant.ts +++ b/libs/core/question-bank/src/questions/get-new-variant.ts @@ -5,18 +5,9 @@ import type { } from "../entities/question-bank-question"; export const getNewVariant = (type: QuestionVariantType): QuestionVariant => { - const common = { - type, - id: getRandomId(), - annexes: [] as string[], - externalIds: [] as string[], - explanation: "", - }; - switch (type) { case "simple": return { - ...common, type: "simple", question: "", options: [1, 2, 3, 4].map((i) => ({ @@ -28,7 +19,6 @@ export const getNewVariant = (type: QuestionVariantType): QuestionVariant => { }; case "one-two": return { - ...common, type: "one-two", question: "", firstCorrectStatements: [""], @@ -38,14 +28,12 @@ export const getNewVariant = (type: QuestionVariantType): QuestionVariant => { }; case "true-or-false": return { - ...common, type: "true-or-false", question: "", answer: true, }; case "definition": return { - ...common, type: "definition", question: "${term}...", fakeOptions: [], @@ -57,13 +45,12 @@ export const getNewVariant = (type: QuestionVariantType): QuestionVariant => { }; case "multiple-correct": return { - ...common, + type: "multiple-correct", options: [1, 2, 3, 4].map((i) => ({ text: "", correct: i === 1, why: "", })), - type: "multiple-correct", question: "", }; } diff --git a/libs/core/question-bank/src/questions/get-question-preview.ts b/libs/core/question-bank/src/questions/get-question-preview.ts index 21aea49cd..b33e750dd 100644 --- a/libs/core/question-bank/src/questions/get-question-preview.ts +++ b/libs/core/question-bank/src/questions/get-question-preview.ts @@ -1,4 +1,4 @@ -import type { QuestionTemplate } from "../entities/question-bank-question"; +import type { QuestionVariant } from "../entities/question-bank-question"; import type { QuestionVariantDefinition } from "../entities/question-bank-question-definition"; import type { QuestionVariantMultipleCorrect } from "../entities/question-bank-question-multiple-correct"; import type { QuestionVariantOneTwo } from "../entities/question-bank-question-one-two"; @@ -65,17 +65,17 @@ const getQuestionMultipleCorrectPreview = ( ].join("\n"); }; -export const getQuestionPreview = (question: QuestionTemplate) => { - switch (question.variant.type) { +export const getQuestionPreview = (variant: QuestionVariant) => { + switch (variant.type) { case "simple": - return getQuestionVariantSimplePreview(question.variant); + return getQuestionVariantSimplePreview(variant); case "one-two": - return getQuestionVariantOneTwoPreview(question.variant); + return getQuestionVariantOneTwoPreview(variant); case "true-or-false": - return getQuestionVariantTrueOrFalsePreview(question.variant); + return getQuestionVariantTrueOrFalsePreview(variant); case "definition": - return getQuestionVariantDefinitionPreview(question.variant); + return getQuestionVariantDefinitionPreview(variant); case "multiple-correct": - return getQuestionMultipleCorrectPreview(question.variant); + return getQuestionMultipleCorrectPreview(variant); } }; diff --git a/libs/providers/search/src/question-bank/question-search.ts b/libs/providers/search/src/question-bank/question-search.ts index 51ec9959f..776477ca8 100644 --- a/libs/providers/search/src/question-bank/question-search.ts +++ b/libs/providers/search/src/question-bank/question-search.ts @@ -69,7 +69,7 @@ export class QuestionSearch extends QuestionBankSearchProvider< return { id: q.id, questionBank: bank.getName(), - text: getQuestionPreview(q), + text: getQuestionPreview(q.variant), subjects: uniqueSubjects, learningObjectives: q.learningObjectives.map((id) => ({ id, @@ -97,7 +97,7 @@ export class QuestionSearch extends QuestionBankSearchProvider< id: q.id, questionBank: bank.getName(), subjects: uniqueSubjects.join(", "), - text: getQuestionPreview(q), + text: getQuestionPreview(q.variant), learningObjectives: los, externalIds: q.externalIds.join(", "), }; diff --git a/libs/react/components/src/index.ts b/libs/react/components/src/index.ts index 4df292a26..60e4e92bb 100644 --- a/libs/react/components/src/index.ts +++ b/libs/react/components/src/index.ts @@ -15,6 +15,7 @@ export * from "./image-viewer"; export * from "./image-with-modal"; export * from "./input-slider"; export * from "./learning-objectives-list"; +export * from "./loading-button"; export * from "./markdown"; export * from "./markdown-client"; export * from "./module-selection-button"; diff --git a/libs/react/components/src/loading-button/index.ts b/libs/react/components/src/loading-button/index.ts new file mode 100644 index 000000000..05361a80a --- /dev/null +++ b/libs/react/components/src/loading-button/index.ts @@ -0,0 +1 @@ +export { LoadingButton } from "./loading-button"; diff --git a/libs/react/components/src/loading-button/loading-button.tsx b/libs/react/components/src/loading-button/loading-button.tsx new file mode 100644 index 000000000..7ebfaefeb --- /dev/null +++ b/libs/react/components/src/loading-button/loading-button.tsx @@ -0,0 +1,26 @@ +import { forwardRef, useCallback, useState } from "react"; +import { Button } from "@mui/joy"; +import type { ButtonProps } from "@mui/joy"; + +export const LoadingButton = forwardRef< + HTMLButtonElement, + Omit +>(({ onClick, ...props }, ref) => { + const [loading, setLoading] = useState(false); + + const newOnClick = useCallback( + async (event: React.MouseEvent) => { + setLoading(true); + try { + await onClick?.(event); + } finally { + setLoading(false); + } + }, + [onClick], + ); + + return