Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Course specific research form #1167

Merged
merged 15 commits into from
Aug 22, 2023
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
/* eslint-disable i18next/no-literal-string */
import { css } from "@emotion/css"
import { RichText } from "@wordpress/block-editor"
import { BlockEditProps } from "@wordpress/blocks"
import React from "react"

import CheckBox from "../../shared-module/components/InputFields/CheckBox"
import BlockWrapper from "../BlockWrapper"

import { CheckBoxAttributes } from "."

const ResearchConsentCheckBoxEditor: React.FC<
React.PropsWithChildren<BlockEditProps<CheckBoxAttributes>>
> = ({ clientId, attributes, isSelected, setAttributes }) => {
const { content } = attributes

return (
<BlockWrapper id={clientId}>
<div
className={css`
display: flex;
flex-direction: rox;
align-items: baseline;
padding: 1rem;
`}
>
<CheckBox label={" "} checked={isSelected} />

<RichText
className="paragraph"
tagName="span"
value={content}
onChange={(value: string) => setAttributes({ content: value })}
placeholder={"Add question here"}
/>
</div>
</BlockWrapper>
)
}

export default ResearchConsentCheckBoxEditor
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import { InnerBlocks } from "@wordpress/block-editor"

const ResearchConsentCheckBoxSave: React.FC<unknown> = () => {
return (
<div>
<InnerBlocks.Content />
</div>
)
}

export default ResearchConsentCheckBoxSave
28 changes: 28 additions & 0 deletions services/cms/src/blocks/ResearchConsentCheckbox/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
/* eslint-disable i18next/no-literal-string */
import { BlockConfiguration } from "@wordpress/blocks"
import { formatLtr } from "@wordpress/icons"

import ResearchConsentCheckBoxEditor from "./ResearchConsentCheckBoxEditor"
import ResearchConsentCheckBoxSave from "./ResearchConsentCheckBoxSave"

export interface CheckBoxAttributes {
content: string
}

const CheckBoxConfiguration: BlockConfiguration<CheckBoxAttributes> = {
title: "CheckBox",
description: "Checkbox block, only used for the teacher editable research questions",
category: "text",
attributes: {
content: {
type: "string",
source: "html",
selector: "span",
},
},
icon: formatLtr,
edit: ResearchConsentCheckBoxEditor,
save: ResearchConsentCheckBoxSave,
}

export default CheckBoxConfiguration
8 changes: 8 additions & 0 deletions services/cms/src/blocks/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ import LearningObjectives from "./LearningObjectives"
import Map from "./Map"
import PagesInChapter from "./PagesInChapter"
import PartnersBlock from "./Partners"
import ResearchConsentCheckBox from "./ResearchConsentCheckbox"
import TableBox from "./TableBox"
import TopLevelPage from "./TopLevelPage"
import UnsupportedBlock from "./UnsupportedBlock"
Expand Down Expand Up @@ -78,3 +79,10 @@ export const blockTypeMapForTopLevelPages = [
["moocfi/hero-section", HeroSection],
// eslint-disable-next-line @typescript-eslint/no-explicit-any
] as Array<[string, BlockConfiguration<Record<string, any>>]>

export const blockTypeMapForResearchConsentForm = [
["moocfi/research-consent-checkbox", ResearchConsentCheckBox],
] as Array<
// eslint-disable-next-line @typescript-eslint/no-explicit-any
[string, BlockConfiguration<Record<string, any>>]
>
7 changes: 7 additions & 0 deletions services/cms/src/blocks/supportedGutenbergBlocks.ts
Original file line number Diff line number Diff line change
Expand Up @@ -61,3 +61,10 @@ export const allowedExamInstructionsCoreBlocks: string[] = [
"core/list-item",
"core/table",
]

export const allowedResearchFormCoreBlocks: string[] = [
"core/paragraph",
"core/heading",
"core/list",
"core/list-item",
]
176 changes: 176 additions & 0 deletions services/cms/src/components/editors/ResearchConsentFormEditor.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,176 @@
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 { 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>
}

const EditorLoading = <Spinner variant="medium" />

const GutenbergEditor = dynamic(() => import("./GutenbergEditor"), {
ssr: false,
loading: () => EditorLoading,
})

const ResearchFormEditor: React.FC<React.PropsWithChildren<ResearchFormEditorProps>> = ({
data,
handleSave,
needToRunMigrationsAndValidations,
setNeedToRunMigrationsAndValidations,
}) => {
const { t } = useTranslation()

const [content, setContent] = useState<BlockInstance[]>(
modifyBlocks((data.content ?? []) as BlockInstance[], [
...allowedResearchFormCoreBlocks,
"moocfi/research-consent-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/research-consent-checkbox",
]) as BlockInstance[],
)

if (!currentContent) {
if (!isBlockInstanceArray(data.content)) {
throw new Error("content is not block instance")
} else {
setCurrentContent(data.content)
}
}

const handleOnSave = async () => {
setSaving(true)
try {
const res = await handleSave({
content: removeUnsupportedBlockType(content),
course_id: assertNotNullOrUndefined(courseId),
})
setContent(res.content as BlockInstance[])
} catch (e: unknown) {
if (!(e instanceof Error)) {
throw e
}
} finally {
setSaving(false)
setCurrentContent(content)
}
}
const saveAndReset = (
<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>
)
return (
<>
<BreakFromCentered sidebar={false}>
<div className="editor__top-button-wrapper">{saveAndReset}</div>
</BreakFromCentered>

<div>
{courseId && (
<GutenbergEditor
content={content}
onContentChange={setContent}
allowedBlocks={allowedResearchFormCoreBlocks}
customBlocks={blockTypeMapForResearchConsentForm}
mediaUpload={mediaUploadBuilder({ courseId: courseId })}
needToRunMigrationsAndValidations={needToRunMigrationsAndValidations}
setNeedToRunMigrationsAndValidations={setNeedToRunMigrationsAndValidations}
inspectorButtons={saveAndReset}
/>
)}
</div>
<div className="editor__component">
<div
className={css`
margin-top: 1rem;
margin-bottom: 1rem;
`}
>
<div
className={css`
margin-bottom: 0.5rem;
`}
>
<SerializeGutenbergModal content={content} />
</div>
</div>
</div>
</>
)
}
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
Loading
Loading