Commit b62f7f90 authored by Adam Parák's avatar Adam Parák 💬
Browse files

Checkout: feat, support 0.15

parent 8be4370e
Loading
Loading
Loading
Loading
+2 −2
Original line number Diff line number Diff line
{
  "name": "@inject/editor",
  "version": "0.14.0",
  "description": "Editor module for Inject Exercise Platform",
  "version": "0.15.0",
  "description": "Editor module to Inject Exercise Platform",
  "main": "index.js",
  "license": "MIT",
  "peerDependencies": {
+10 −0
Original line number Diff line number Diff line
@@ -104,4 +104,14 @@ export const QUESTIONNAIRE_QUESTION_FORM: Form = {
    tooltip: '',
    optional: true,
  },
  multiline: {
    label: 'Multiline',
    tooltip: '',
    optional: true,
  },
  relatedMilestones: {
    label: 'Related Milestones',
    tooltip: '',
    optional: true,
  },
}
+200 −0
Original line number Diff line number Diff line
import type { ButtonProps } from '@blueprintjs/core'
import {
  Button,
  Dialog,
  DialogBody,
  DialogFooter,
  InputGroup,
} from '@blueprintjs/core'
import type { ZustandSetterSingle } from '@inject/shared'
import {
  compute,
  computed,
  create,
  notify,
  ShallowGet,
  ShallowGetSet,
} from '@inject/shared'
import type { FC } from 'react'
import { memo, useCallback, useEffect, useState } from 'react'
import { GENERIC_CONTENT } from '../../../assets/generalContent'
import { QUESTIONNAIRE_QUESTION_FORM } from '../../../assets/pageContent/injectSpecification'
import { db } from '../../../indexeddb/db'
import {
  patchQuestionnaireMilestone,
  updateQuestionnaireQuestion,
} from '../../../indexeddb/operations'
import type {
  QuestionnaireQuestion,
  QuestionnaireQuestionFreeForm,
} from '../../../indexeddb/types'
import TooltipLabel from '../../Tooltips/TooltipLabel'
import TooltipSwitch from '../../Tooltips/TooltipSwitch'
import RelatedMilestoneForm from './RelatedMilestoneForm'

interface QuestionnaireQuestionFormProps {
  questionnaireQuestion?: QuestionnaireQuestion
  freeformQuestion?: QuestionnaireQuestionFreeForm
  injectInfoId: number
  buttonProps: ButtonProps
}

type State<
  S = {
    text: string
    valid: boolean
    multiline: boolean
    update: (id?: number) => Promise<void>
  },
> = S & ZustandSetterSingle<S>

const FreeformForm: FC<QuestionnaireQuestionFormProps> = ({
  questionnaireQuestion,
  freeformQuestion,
  injectInfoId,
  buttonProps,
}) => {
  const [isOpen, setIsOpen] = useState(false)
  const state = create<State>(
    computed((set, get) => ({
      text: '',
      multiline: false,
      ...compute(get, state => ({
        valid: state.text !== '',
      })),
      set: (item, value) => set({ [item]: value }),
      update: async id => {
        const { multiline, text } = get()

        const relatedMilestones = (
          await db.questionnaireMilestones
            .where({ injectInfoId })
            .and(ms => ms.questionId === id || ms.questionId === undefined)
            .toArray()
        ).map(it => it.id)
        const newId = await updateQuestionnaireQuestion(
          {
            id,
            injectInfoId,
            type: 'free-form',
            text,
          },
          {
            multiline,
            relatedMilestones,
          }
        )

        await patchQuestionnaireMilestone(
          relatedMilestones,
          injectInfoId,
          newId
        )
      },
    }))
  )

  useEffect(() => {
    state.setState({
      text: questionnaireQuestion?.text,
      multiline: freeformQuestion?.multiline,
    })
  }, [isOpen, state, questionnaireQuestion, freeformQuestion])

  const handleUpdateButton = useCallback(async () => {
    try {
      state.getState().update(questionnaireQuestion?.id)
      setIsOpen(false)
    } catch (error) {
      const { text } = state.getState()
      notify(
        `Failed to update questionnaire question '${text}': ${error}`,
        JSON.stringify(error),
        {
          intent: 'danger',
        }
      )
    }
  }, [questionnaireQuestion?.id, state])

  return (
    <>
      <Button {...buttonProps} onClick={() => setIsOpen(true)} />
      <Dialog
        isOpen={isOpen}
        onClose={() => setIsOpen(false)}
        icon={questionnaireQuestion ? 'edit' : 'plus'}
        title={
          questionnaireQuestion
            ? 'Edit questionnaire question'
            : 'New questionnaire question'
        }
      >
        <DialogBody>
          <TooltipLabel label={QUESTIONNAIRE_QUESTION_FORM.text}>
            <ShallowGetSet
              store={state}
              get={({ text }) => text}
              set={({ set }) => set}
            >
              {(text, set) => (
                <InputGroup
                  placeholder='Input text'
                  value={text}
                  onChange={e => set('text', e.target.value)}
                />
              )}
            </ShallowGetSet>
          </TooltipLabel>
          <ShallowGetSet
            store={state}
            get={({ multiline }) => multiline}
            set={({ set }) => set}
          >
            {(multiline, set) => (
              <TooltipSwitch
                label={QUESTIONNAIRE_QUESTION_FORM.multiline}
                switchProps={{
                  checked: multiline,
                  onChange: () => set('multiline', !multiline),
                }}
              />
            )}
          </ShallowGetSet>

          <RelatedMilestoneForm
            questionId={freeformQuestion?.id}
            injectInfoId={injectInfoId}
          />
        </DialogBody>
        <DialogFooter
          actions={
            <ShallowGet store={state} get={({ valid }) => valid}>
              {isValid =>
                questionnaireQuestion ? (
                  <Button
                    disabled={!isValid}
                    onClick={() => handleUpdateButton()}
                    intent='primary'
                    icon='edit'
                    text={GENERIC_CONTENT.buttons.save}
                  />
                ) : (
                  <Button
                    disabled={!isValid}
                    onClick={() => handleUpdateButton()}
                    intent='primary'
                    icon='plus'
                    text={GENERIC_CONTENT.buttons.add}
                  />
                )
              }
            </ShallowGet>
          }
        />
      </Dialog>
    </>
  )
}

export default memo(FreeformForm)
+109 −15
Original line number Diff line number Diff line
import { Button, ButtonGroup, Card } from '@blueprintjs/core'
import { notify } from '@inject/shared'
import { useLiveQuery } from 'dexie-react-hooks'
import { range } from 'lodash'
import type { FC } from 'react'
import { memo, useCallback, useEffect, useState } from 'react'
import { memo, useCallback, useMemo } from 'react'
import { db } from '../../../indexeddb/db'
import { deleteQuestionnaireQuestion } from '../../../indexeddb/operations'
import { type QuestionnaireQuestion } from '../../../indexeddb/types'
import QuestionnaireQuestionForm from './QuestionnaireQuestionForm'
import FreeformForm from './FreeformForm'
import RadioForm from './RadioForm'

interface QuestionnaireQuestionProps {
  questionnaireQuestion: QuestionnaireQuestion
}

const QuestionnaireQuestionItem: FC<QuestionnaireQuestionProps> = ({
const QuestionnaireQuestionRadio: FC<QuestionnaireQuestionProps> = ({
  questionnaireQuestion,
}) => {
  const [answers, setAnswers] = useState<string[]>([])
  const radioQuestion = useLiveQuery(
    () =>
      db.questionnaireQuestionRadio.get({
        id: questionnaireQuestion.id,
        injectInfoId: questionnaireQuestion.injectInfoId,
      }),
    [questionnaireQuestion.id]
  )
  const answers = useMemo(
    () =>
      radioQuestion?.labels
        ? radioQuestion?.labels.split(', ')
        : range(1, (radioQuestion?.max || 1) + 1).map(value =>
            value.toString()
          ),
    [radioQuestion?.labels, radioQuestion?.max]
  )

  const handleDeleteButton = useCallback(
    async (question: QuestionnaireQuestion) => {
@@ -33,14 +52,6 @@ const QuestionnaireQuestionItem: FC<QuestionnaireQuestionProps> = ({
    []
  )

  useEffect(() => {
    setAnswers(
      questionnaireQuestion.labels
        ? questionnaireQuestion.labels.split(', ')
        : range(1, questionnaireQuestion.max + 1).map(value => value.toString())
    )
  }, [questionnaireQuestion])

  return (
    <Card
      style={{ display: 'flex', flexDirection: 'column', alignItems: 'start' }}
@@ -55,8 +66,9 @@ const QuestionnaireQuestionItem: FC<QuestionnaireQuestionProps> = ({
      >
        <span style={{ flexGrow: 1 }}>{questionnaireQuestion.text}</span>
        <ButtonGroup>
          <QuestionnaireQuestionForm
          <RadioForm
            questionnaireQuestion={questionnaireQuestion}
            radioQuestion={radioQuestion}
            injectInfoId={questionnaireQuestion.injectInfoId}
            buttonProps={{
              minimal: true,
@@ -76,8 +88,7 @@ const QuestionnaireQuestionItem: FC<QuestionnaireQuestionProps> = ({
          <li
            key={i}
            style={{
              fontWeight:
                questionnaireQuestion.correct === i + 1 ? 'bold' : 'normal',
              fontWeight: radioQuestion?.correct === i + 1 ? 'bold' : 'normal',
            }}
          >
            {answer}
@@ -88,4 +99,87 @@ const QuestionnaireQuestionItem: FC<QuestionnaireQuestionProps> = ({
  )
}

const QuestionnaireQuestionFreeform: FC<QuestionnaireQuestionProps> = ({
  questionnaireQuestion,
}) => {
  const freeformQuestion = useLiveQuery(
    () =>
      db.questionnaireQuestionFreeform.get({
        id: questionnaireQuestion.id,
        injectInfoId: questionnaireQuestion.injectInfoId,
      }),
    [questionnaireQuestion.id]
  )

  const handleDeleteButton = useCallback(
    async (question: QuestionnaireQuestion) => {
      try {
        await deleteQuestionnaireQuestion(question.id)
      } catch (error) {
        notify(
          `Failed to delete question '${question.text}': ${error}`,
          JSON.stringify(error),
          {
            intent: 'danger',
          }
        )
      }
    },
    []
  )

  return (
    <Card
      style={{ display: 'flex', flexDirection: 'column', alignItems: 'start' }}
    >
      <div
        style={{
          display: 'flex',
          justifyContent: 'space-between',
          alignItems: 'center',
          width: '100%',
        }}
      >
        <span style={{ flexGrow: 1 }}>{questionnaireQuestion.text}</span>
        <ButtonGroup>
          <FreeformForm
            questionnaireQuestion={questionnaireQuestion}
            injectInfoId={questionnaireQuestion.injectInfoId}
            freeformQuestion={freeformQuestion}
            buttonProps={{
              minimal: true,
              icon: 'edit',
              style: { marginRight: '1rem' },
            }}
          />
          <Button
            minimal
            icon='cross'
            onClick={() => handleDeleteButton(questionnaireQuestion)}
          />
        </ButtonGroup>
      </div>
    </Card>
  )
}

const QuestionnaireQuestionItem: FC<QuestionnaireQuestionProps> = ({
  questionnaireQuestion,
}) => {
  switch (questionnaireQuestion.type) {
    case 'radio':
      return (
        <QuestionnaireQuestionRadio
          questionnaireQuestion={questionnaireQuestion}
        />
      )
    case 'free-form':
      return (
        <QuestionnaireQuestionFreeform
          questionnaireQuestion={questionnaireQuestion}
        />
      )
  }
}

export default memo(QuestionnaireQuestionItem)
+0 −205
Original line number Diff line number Diff line
import type { ButtonProps } from '@blueprintjs/core'
import {
  Button,
  Dialog,
  DialogBody,
  DialogFooter,
  InputGroup,
} from '@blueprintjs/core'
import { notify } from '@inject/shared'
import type { FC } from 'react'
import { memo, useCallback, useEffect, useState } from 'react'
import { GENERIC_CONTENT } from '../../../assets/generalContent'
import { QUESTIONNAIRE_QUESTION_FORM } from '../../../assets/pageContent/injectSpecification'
import {
  addQuestionnaireQuestion,
  updateQuestionnaireQuestion,
} from '../../../indexeddb/operations'
import type { QuestionnaireQuestion } from '../../../indexeddb/types'
import TooltipLabel from '../../Tooltips/TooltipLabel'
import TooltipSwitch from '../../Tooltips/TooltipSwitch'
import QuestionCustomLabels from './QuestionCustomLabels'
import QuestionRangeLabels from './QuestionRangeLabels'

interface QuestionnaireQuestionFormProps {
  questionnaireQuestion?: QuestionnaireQuestion
  injectInfoId: number
  buttonProps: ButtonProps
}

const QuestionnaireQuestionForm: FC<QuestionnaireQuestionFormProps> = ({
  questionnaireQuestion,
  injectInfoId,
  buttonProps,
}) => {
  const [isOpen, setIsOpen] = useState(false)
  const [isValid, setIsValid] = useState(false)
  const [text, setText] = useState<string>('')
  const [max, setMax] = useState<number>(1)
  const [correct, setCorrect] = useState<number>(0)
  const [customLabels, setCustomLabels] = useState<boolean>(false)
  const [labels, setLabels] = useState<string>('')

  useEffect(() => {
    setText(questionnaireQuestion?.text || '')
    setMax(questionnaireQuestion?.max || 1)
    setCorrect(questionnaireQuestion?.correct || 0)
    setCustomLabels(
      (questionnaireQuestion?.labels !== '' &&
        questionnaireQuestion?.labels !== undefined) ||
        false
    )
    setLabels(questionnaireQuestion?.labels || '')
  }, [questionnaireQuestion, isOpen])

  useEffect(() => {
    setIsValid(
      text !== '' &&
        ((customLabels && labels !== '') || (!customLabels && max > 0))
    )
  }, [text, customLabels, labels, max])

  const clearInput = useCallback(() => {
    setText('')
    setMax(1)
    setCorrect(0)
    setCustomLabels(false)
    setLabels('')
  }, [])

  const handleAddButton = useCallback(
    async (questionnaireQuestion: Omit<QuestionnaireQuestion, 'id'>) => {
      try {
        await addQuestionnaireQuestion(questionnaireQuestion)
        clearInput()
        setIsOpen(false)
      } catch (error) {
        notify(
          `Failed to add questionnaire question '${questionnaireQuestion.text}': ${error}`,
          JSON.stringify(error),
          {
            intent: 'danger',
          }
        )
      }
    },
    []
  )

  const handleUpdateButton = useCallback(
    async (questionnaireQuestion: QuestionnaireQuestion) => {
      try {
        await updateQuestionnaireQuestion(questionnaireQuestion)
        setIsOpen(false)
      } catch (error) {
        notify(
          `Failed to update questionnaire question '${questionnaireQuestion.text}': ${error}`,
          JSON.stringify(error),
          {
            intent: 'danger',
          }
        )
      }
    },
    []
  )

  const onCorrectChange = useCallback(
    (newCorrect: number) => {
      if (correct === newCorrect) {
        setCorrect(0)
      } else {
        setCorrect(newCorrect)
      }
    },
    [correct]
  )

  return (
    <>
      <Button {...buttonProps} onClick={() => setIsOpen(true)} />
      <Dialog
        isOpen={isOpen}
        onClose={() => setIsOpen(false)}
        icon={questionnaireQuestion ? 'edit' : 'plus'}
        title={
          questionnaireQuestion
            ? 'Edit questionnaire question'
            : 'New questionnaire question'
        }
      >
        <DialogBody>
          <TooltipLabel label={QUESTIONNAIRE_QUESTION_FORM.text}>
            <InputGroup
              placeholder='Input text'
              value={text}
              onChange={e => setText(e.target.value)}
            />
          </TooltipLabel>
          <TooltipSwitch
            label={QUESTIONNAIRE_QUESTION_FORM.customLabels}
            switchProps={{
              checked: customLabels,
              onChange: () => setCustomLabels(!customLabels),
            }}
          />
          {customLabels ? (
            <QuestionCustomLabels
              labels={labels}
              correct={correct}
              onLabelsChange={(value: string) => setLabels(value)}
              onCorrectChange={(value: number) => onCorrectChange(value)}
            />
          ) : (
            <QuestionRangeLabels
              max={max}
              correct={correct}
              onMaxChange={(value: number) => setMax(value)}
              onCorrectChange={(value: number) => onCorrectChange(value)}
            />
          )}
        </DialogBody>
        <DialogFooter
          actions={
            questionnaireQuestion ? (
              <Button
                disabled={!isValid}
                onClick={() =>
                  handleUpdateButton({
                    id: questionnaireQuestion.id,
                    injectInfoId,
                    text,
                    max: customLabels ? labels.split(', ').length : max,
                    correct,
                    labels: customLabels ? labels : '',
                  })
                }
                intent='primary'
                icon='edit'
                text={GENERIC_CONTENT.buttons.save}
              />
            ) : (
              <Button
                disabled={!isValid}
                onClick={() =>
                  handleAddButton({
                    injectInfoId,
                    text,
                    max: customLabels ? labels.split(', ').length : max,
                    correct,
                    labels: customLabels ? labels : '',
                  })
                }
                intent='primary'
                icon='plus'
                text={GENERIC_CONTENT.buttons.add}
              />
            )
          }
        />
      </Dialog>
    </>
  )
}

export default memo(QuestionnaireQuestionForm)
Loading