Skip to content

Commit

Permalink
Research form and its editor
Browse files Browse the repository at this point in the history
  • Loading branch information
Maija Y committed Aug 7, 2023
1 parent afa185e commit 5459f59
Show file tree
Hide file tree
Showing 18 changed files with 939 additions and 88 deletions.
148 changes: 107 additions & 41 deletions services/cms/src/components/editors/ResearchConsentFormEditor.tsx
Original file line number Diff line number Diff line change
@@ -1,26 +1,27 @@
import SaveIcon from "@mui/icons-material/Save"
import LoadingButton from "@mui/lab/LoadingButton"
import { css } from "@emotion/css"
import { BlockInstance } from "@wordpress/blocks"
import dynamic from "next/dynamic"
import React, { useContext, useState } from "react"
import { useTranslation } from "react-i18next"

import { allowerdResearchFormCoreBlocks } from "../../blocks/supportedGutenbergBlocks"
import { blockTypeMapForResearchConsentForm } from "../../blocks"
import { allowedResearchFormCoreBlocks } from "../../blocks/supportedGutenbergBlocks"
import CourseContext from "../../contexts/CourseContext"
import mediaUploadBuilder from "../../services/backend/media/mediaUpload"
import { NewResearchForm, ResearchForm } from "../../shared-module/bindings"
import Button from "../../shared-module/components/Button"
import BreakFromCentered from "../../shared-module/components/Centering/BreakFromCentered"
import Spinner from "../../shared-module/components/Spinner"
import { assertNotNullOrUndefined } from "../../shared-module/utils/nullability"
import { modifyBlocks } from "../../utils/Gutenberg/modifyBlocks"
import { removeUnsupportedBlockType } from "../../utils/Gutenberg/removeUnsupportedBlockType"
import SerializeGutenbergModal from "../SerializeGutenbergModal"

interface ResearchFormEditorProps {
data: ResearchForm
handleSave: (updatedTemplate: NewResearchForm) => Promise<ResearchForm>
needToRunMigrationsAndValidations: boolean
setNeedToRunMigrationsAndValidations: React.Dispatch<boolean>

courseId?: string
}

const EditorLoading = <Spinner variant="medium" />
Expand All @@ -39,13 +40,27 @@ const ResearchFormEditor: React.FC<React.PropsWithChildren<ResearchFormEditorPro
const { t } = useTranslation()

const [content, setContent] = useState<BlockInstance[]>(
modifyBlocks(
(data.content ?? []) as BlockInstance[],
allowerdResearchFormCoreBlocks,
) as BlockInstance[],
modifyBlocks((data.content ?? []) as BlockInstance[], [
...allowedResearchFormCoreBlocks,
"moocfi/checkbox",
]) as BlockInstance[],
)
const courseId = useContext(CourseContext)?.courseId
const [saving, setSaving] = useState(false)
const [currentContent, setCurrentContent] = useState<BlockInstance[]>(
modifyBlocks((data.content ?? []) as BlockInstance[], [
...allowedResearchFormCoreBlocks,
"moocfi/checkbox",
]) as BlockInstance[],
)

if (!currentContent) {
if (!isBlockInstanceArray(data.content)) {
throw new Error("content is not block instance")
} else {
setCurrentContent(data.content)
}
}
const [error, setError] = useState<string | null>(null)

const handleOnSave = async () => {
Expand All @@ -64,48 +79,99 @@ const ResearchFormEditor: React.FC<React.PropsWithChildren<ResearchFormEditorPro
setError(e.toString())
} finally {
setSaving(false)
setCurrentContent(content)
}
}
console.log(
"aaa",
t,
content,
data,
handleSave,
needToRunMigrationsAndValidations,
setNeedToRunMigrationsAndValidations,
saving,
error,
handleOnSave,
)

return (
<>
<BreakFromCentered sidebar={false}>
<div>
<div
className={css`
display: flex;
justify-content: center;
background: #f5f6f7;
padding: 1rem;
`}
>
<Button
variant="primary"
size="medium"
className={css`
margin-right: 1rem;
border: 1px black solid;
pointer-events: auto;
`}
onClick={handleOnSave}
disabled={saving}
>
{t("save")}
</Button>
<Button
variant="secondary"
size="medium"
className={css`
margin-left: 1rem;
border: 1px black solid;
pointer-events: auto;
`}
onClick={() => {
const res = confirm(t("are-you-sure-you-want-to-discard-changes"))
if (res) {
setContent(currentContent)
}
}}
disabled={saving}
>
{t("reset")}
</Button>
</div>
</div>
</BreakFromCentered>
<div className="editor__component">
<div>
{error && <pre>{error}</pre>}
<LoadingButton
// eslint-disable-next-line i18next/no-literal-string
loadingPosition="start"
startIcon={<SaveIcon />}
loading={saving}
onClick={handleOnSave}
{courseId && (
<ResearchFormGutenbergEditor
content={content}
onContentChange={setContent}
allowedBlocks={allowedResearchFormCoreBlocks}
customBlocks={blockTypeMapForResearchConsentForm}
mediaUpload={mediaUploadBuilder({ courseId: courseId })}
needToRunMigrationsAndValidations={needToRunMigrationsAndValidations}
setNeedToRunMigrationsAndValidations={setNeedToRunMigrationsAndValidations}
/>
)}
</div>
</div>
<div className="editor__component">
<div
className={css`
margin-top: 1rem;
margin-bottom: 1rem;
`}
>
<div
className={css`
margin-bottom: 0.5rem;
`}
>
{t("save")}
</LoadingButton>
<SerializeGutenbergModal content={content} />
</div>
</div>
</div>
{courseId && (
<ResearchFormGutenbergEditor
content={content}
onContentChange={setContent}
allowedBlocks={allowerdResearchFormCoreBlocks}
mediaUpload={mediaUploadBuilder({ courseId: courseId })}
needToRunMigrationsAndValidations={needToRunMigrationsAndValidations}
setNeedToRunMigrationsAndValidations={setNeedToRunMigrationsAndValidations}
/>
)}
</>
)
}

function isBlockInstanceArray(obj: unknown): obj is BlockInstance[] {
if (!Array.isArray(obj)) {
return false
}
for (const o of obj) {
if (typeof o.name !== "string" || typeof o.clientId !== "string") {
return false
}
}
return true
}
export default ResearchFormEditor
81 changes: 60 additions & 21 deletions services/cms/src/pages/courses/[id]/research-form-edit.tsx
Original file line number Diff line number Diff line change
@@ -1,15 +1,21 @@
import { useQuery } from "@tanstack/react-query"
import { BlockInstance } from "@wordpress/blocks"
import dynamic from "next/dynamic"
import React, { useContext, useState } from "react"
import React, { useState } from "react"

import { CheckBoxAttributes } from "../../../blocks/Checkbox"
import CourseContext from "../../../contexts/CourseContext"
import {
fetchResearchFormWithCourseId,
upsertResearchForm,
upsertResearchFormQuestion,
} from "../../../services/backend/courses"
import { NewResearchForm, ResearchForm } from "../../../shared-module/bindings"
import {
NewResearchForm,
NewResearchFormQuestion,
ResearchForm,
} from "../../../shared-module/bindings"
import Button from "../../../shared-module/components/Button"
import ErrorBanner from "../../../shared-module/components/ErrorBanner"
import Spinner from "../../../shared-module/components/Spinner"
import { withSignedIn } from "../../../shared-module/contexts/LoginStateContext"
import dontRenderUntilQueryParametersReady, {
Expand All @@ -22,6 +28,11 @@ interface ResearchFormProps {
query: SimplifiedUrlQuery<"id">
}

interface ResearchContent {
name: string
clientId: string
attributes: { content: string }
}
const EditorLoading = <Spinner variant="medium" />

const ResearchFormEditor = dynamic(
Expand All @@ -32,18 +43,18 @@ const ResearchFormEditor = dynamic(
},
)

const ResearchForms = ({ query }: ResearchFormProps) => {
const { id } = query
const ResearchForms: React.FC<React.PropsWithChildren<ResearchFormProps>> = ({ query }) => {
const [needToRunMigrationsAndValidations, setNeedToRunMigrationsAndValidations] = useState(false)
const courseId = useContext(CourseContext)?.courseId
const courseId = query.id

const getResearchForm = useQuery(
[`courses-${id}-research-consent-form`],
() => fetchResearchFormWithCourseId(id),
[`courses-${courseId}-research-consent-form`],
() => fetchResearchFormWithCourseId(courseId),
{
select: (data) => {
const form: ResearchForm = {
...data,
content: data.content,
content: data.content as ResearchContent,
}
return form
},
Expand All @@ -52,30 +63,43 @@ const ResearchForms = ({ query }: ResearchFormProps) => {
},
},
)
console.log("aaa")

const handleSave = async (form: NewResearchForm): Promise<ResearchForm> => {
const res = await upsertResearchForm(assertNotNullOrUndefined(id), {
...form,
const handleCreateNewForm = async () => {
await upsertResearchForm(assertNotNullOrUndefined(courseId), {
course_id: assertNotNullOrUndefined(courseId),
content: [],
})
await getResearchForm.refetch()
return res
}
console.log(courseId, id)
const handleCreateNewForm = async () => {
const res = await upsertResearchForm(assertNotNullOrUndefined(id), {
course_id: assertNotNullOrUndefined(id),
content: [],

const handleSave = async (form: NewResearchForm): Promise<ResearchForm> => {
const researchForm = await upsertResearchForm(assertNotNullOrUndefined(courseId), {
...form,
})

if (!isBlockInstanceArray(form.content)) {
throw new Error("content is not block instance")
}
form.content.forEach((block) => {
if (isMoocfiCheckbox(block)) {
const newResearchQuestion: NewResearchFormQuestion = {
question_id: block.clientId,
course_id: researchForm.course_id,
research_consent_form_id: researchForm.id,
question: block.attributes.content,
}
upsertResearchFormQuestion(researchForm.id, newResearchQuestion)
}
})
await getResearchForm.refetch()
console.log(res)
return researchForm
}

return (
<>
{getResearchForm.isLoading && <Spinner variant={"medium"} />}
{getResearchForm.isSuccess && (
<CourseContext.Provider value={{ courseId: assertNotNullOrUndefined(id) }}>
<CourseContext.Provider value={{ courseId: assertNotNullOrUndefined(courseId) }}>
<ResearchFormEditor
data={getResearchForm.data}
handleSave={handleSave}
Expand All @@ -94,4 +118,19 @@ const ResearchForms = ({ query }: ResearchFormProps) => {
)
}

function isBlockInstanceArray(obj: unknown): obj is BlockInstance[] {
if (!Array.isArray(obj)) {
return false
}
for (const o of obj) {
if (typeof o.name !== "string" || typeof o.clientId !== "string") {
return false
}
}
return true
}

function isMoocfiCheckbox(obj: BlockInstance): obj is BlockInstance<CheckBoxAttributes> {
return obj.name === "moocfi/checkbox"
}
export default withErrorBoundary(withSignedIn(dontRenderUntilQueryParametersReady(ResearchForms)))
22 changes: 21 additions & 1 deletion services/cms/src/services/backend/courses.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,15 @@
import {
CmsPeerReviewConfiguration,
NewResearchForm,
NewResearchFormQuestion,
ResearchForm,
ResearchFormQuestion,
} from "../../shared-module/bindings"
import { isCmsPeerReviewConfiguration, isResearchForm } from "../../shared-module/bindings.guard"
import {
isCmsPeerReviewConfiguration,
isResearchForm,
isResearchFormQuestion,
} from "../../shared-module/bindings.guard"
import { validateResponse } from "../../shared-module/utils/fetching"

import { cmsClient } from "./cmsClient"
Expand Down Expand Up @@ -39,3 +45,17 @@ export const upsertResearchForm = async (
})
return validateResponse(response, isResearchForm)
}

export const upsertResearchFormQuestion = async (
courseId: string,
data: NewResearchFormQuestion,
): Promise<ResearchFormQuestion> => {
const response = await cmsClient.put(
`/courses/${courseId}/research-consent-form-question`,
data,
{
responseType: "json",
},
)
return validateResponse(response, isResearchFormQuestion)
}
3 changes: 2 additions & 1 deletion services/course-material/src/components/Page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ import NavigationContainer from "./ContentRenderer/moocfi/NavigationContainer"
import FeedbackHandler from "./FeedbackHandler"
import HeadingsNavigation from "./HeadingsNavigation"
import ReferenceList from "./ReferencesList"
import SelectResearchConsentForm from "./forms/SelectResearchConsentForm"
import CourseSettingsModal from "./modals/CourseSettingsModal"
import UserOnWrongCourseNotification from "./notifications/UserOnWrongCourseNotification"

Expand Down Expand Up @@ -102,7 +103,7 @@ const Page: React.FC<React.PropsWithChildren<Props>> = ({ onRefresh, organizatio
/>
)}
{courseId && <CourseSettingsModal onClose={onRefresh} />}

{courseId && <SelectResearchConsentForm />}
{getPageAudioFiles.isSuccess && tracks.length !== 0 && (
<AudioNotification>
<p>{t("audio-notification-description")}</p>
Expand Down
Loading

0 comments on commit 5459f59

Please sign in to comment.