Commit 23f464ed authored by Filip Šenk's avatar Filip Šenk
Browse files

Merge branch '1060-editor-add-feature-to-upload-definition-to-exercise-panel' into 'main'

Added upload

Closes #1060

See merge request inject/frontend!903
parents d2ddfc17 d08056fc
Loading
Loading
Loading
Loading
+3 −0
Original line number Diff line number Diff line
@@ -56,6 +56,7 @@ type Props<T extends string | number> = {
  buttonClassName?: string
  fill?: boolean
  placeholder?: string
  disabled?: boolean
}

const SharedSelect = <T extends string | number>({
@@ -66,6 +67,7 @@ const SharedSelect = <T extends string | number>({
  buttonClassName,
  fill = false,
  placeholder,
  disabled,
}: Props<T>) => (
  <Select<OptionProps<T>>
    items={items}
@@ -78,6 +80,7 @@ const SharedSelect = <T extends string | number>({
    onItemSelect={onItemSelect}
    className={selectClassname}
    fill={fill}
    disabled={disabled}
    popoverProps={{
      enforceFocus: false,
      autoFocus: false,
+28 −9
Original line number Diff line number Diff line
@@ -8,6 +8,7 @@ import { InjectFormContent } from '../InjectForm/InjectFormContent'
import { LearningActivityFormContent } from '../LearningObjectives/LearningActivity/Form/LearningActivityFormContent'
import { MilestoneDialogContent } from '../MilestoneDialog/MilestoneDialogContent'
import { QuestionnaireFormContent } from '../Questionnaires/QuestionnaireFormButton/QuestionnaireFormContent'
import { AddPatternForm } from './InjectPatterns/AddPatternForm'

const backButtonClass = css`
  margin-left: 1.125rem;
@@ -41,7 +42,12 @@ const AddNodeDialog: FC<AddNodeDialogProps> = ({
  parentNode,
}) => {
  const [selectedMode, setSelectedMode] = useState<
    'milestone' | 'inject' | 'questionnaire' | 'Learning activity' | null
    | 'milestone'
    | 'inject'
    | 'questionnaire'
    | 'Learning activity'
    | 'Inject pattern'
    | null
  >(null)

  const onClose = () => {
@@ -67,6 +73,7 @@ const AddNodeDialog: FC<AddNodeDialogProps> = ({
            `}
          >
            {parentNode.type !== 'Start' && parentNode.type !== 'Milestone' && (
              <>
                <Button
                  text='Milestone'
                  fill
@@ -75,6 +82,15 @@ const AddNodeDialog: FC<AddNodeDialogProps> = ({
                    padding-block: 1rem;
                  `}
                />
                <Button
                  text='Inject pattern'
                  fill
                  onClick={() => setSelectedMode('Inject pattern')}
                  className={css`
                    padding-block: 1rem;
                  `}
                />
              </>
            )}
            {(parentNode.type === 'Start' ||
              parentNode.type === 'Milestone') && (
@@ -146,6 +162,9 @@ const AddNodeDialog: FC<AddNodeDialogProps> = ({
          }
        />
      )}
      {selectedMode === 'Inject pattern' && (
        <AddPatternForm onClose={onClose} parentNode={parentNode} />
      )}
    </Dialog>
  )
}
+76 −0
Original line number Diff line number Diff line
import {
  Button,
  Classes,
  DialogBody,
  DialogFooter,
  FileInput,
} from '@blueprintjs/core'
import { notify, TooltipLabel } from '@inject/shared'
import JSZip from 'jszip'
import type { ChangeEvent } from 'react'
import { useCallback, useState, type FC } from 'react'
import { GENERIC_CONTENT } from '../../../assets/generalContent'
import { loadInjectPattern } from '../../../importExport'
import type { TreeNode } from '../../../indexeddb/types'
type AddPatternFormProps = {
  onClose: () => void
  parentNode?: TreeNode
}

export const AddPatternForm: FC<AddPatternFormProps> = ({
  onClose,
  parentNode,
}) => {
  const [file, setFile] = useState<File | undefined>()

  const handleFileChange = useCallback((e: ChangeEvent<HTMLInputElement>) => {
    if (e.target.files) {
      setFile(e.target.files[0])
    }
  }, [])

  const handleAdd = async () => {
    if (!file) return

    try {
      const zip = await new JSZip().loadAsync(file)
      await loadInjectPattern(zip, parentNode)
      onClose()
    } catch (error) {
      notify((error as Error).message, JSON.stringify(error), {
        intent: 'danger',
      })
    }
  }

  return (
    <>
      <DialogBody>
        <TooltipLabel
          label={{
            label: 'Inject pattern',
          }}
        >
          <FileInput
            className={Classes.INPUT}
            fill
            hasSelection={file !== undefined}
            text={file ? file.name : 'Choose pattern...'}
            onInputChange={handleFileChange}
          />
        </TooltipLabel>
      </DialogBody>
      <DialogFooter
        actions={
          <Button
            disabled={!file}
            onClick={handleAdd}
            intent='primary'
            icon='plus'
            text={GENERIC_CONTENT.buttons.add}
          />
        }
      />
    </>
  )
}
+149 −0
Original line number Diff line number Diff line
import { Button, Popover } from '@blueprintjs/core'
import { css } from '@emotion/css'
import { notify } from '@inject/shared'
import type { Dispatch, SetStateAction } from 'react'
import { useEffect, useState, type FC } from 'react'
import { exportInjectPattern } from '../../../importExport'
import type { TreeNode } from '../../../indexeddb/types'
import type { InjectPattern } from '../../../routes/create/tree-view'
import { getNodeName } from '../Node/utils'
import { getSelectedRec } from '../utils'

const contentWrapper = css`
  min-width: 18rem;
  max-height: 20rem;
  padding: 1rem;
  overflow-y: auto;
  max-width: 25rem;
  z-index: 1;
  display: flex;
  flex-direction: column;
  gap: 0.2rem;
`
const buttonWrapper = css`
  padding-top: 0.5rem;
  display: flex;
  flex-direction: column;
  gap: 0.5rem;
`

type AssignPatternsProps = {
  onClick: () => void
  selected: boolean
  injectPattern: InjectPattern
  setInjectPattern: Dispatch<SetStateAction<InjectPattern>>
  selectedNodes: TreeNode[]
  setSelectedNodes: Dispatch<SetStateAction<TreeNode[]>>
}

export const AssignPatterns: FC<AssignPatternsProps> = ({
  onClick,
  selected,
  injectPattern,
  setInjectPattern,
  setSelectedNodes,
  selectedNodes,
}) => {
  const [opened, setOpened] = useState(false)
  const handleDownload = async () => {
    if (injectPattern.begin && injectPattern.end) {
      await exportInjectPattern(selectedNodes, injectPattern.begin)
    }
  }

  useEffect(() => {
    if (!selected) {
      setOpened(false)
    }
  }, [selected])

  useEffect(() => {
    if (injectPattern.begin && injectPattern.end) {
      const children = getSelectedRec(injectPattern.begin, injectPattern.end.id)

      if (children.length <= 1) {
        notify('Wrongly selected begin and end', JSON.stringify(''), {
          intent: 'warning',
        })
      } else {
        setSelectedNodes(children)
      }
    } else {
      setSelectedNodes([])
    }
  }, [injectPattern.begin, injectPattern.end, setSelectedNodes])

  return (
    <div onClick={onClick}>
      <Popover
        fill
        minimal
        position={'bottom-left'}
        autoFocus={false}
        enforceFocus={false}
        captureDismiss={false}
        content={
          <>
            {selected && (
              <div className={contentWrapper}>
                <Button
                  text={
                    injectPattern.begin
                      ? getNodeName(injectPattern.begin)
                      : 'Select a begin'
                  }
                  fill
                  active={injectPattern.beginSelect}
                  intent={injectPattern.beginSelect ? 'success' : 'none'}
                  onClick={() =>
                    setInjectPattern({ ...injectPattern, beginSelect: true })
                  }
                />
                <Button
                  text={
                    injectPattern.end
                      ? getNodeName(injectPattern.end)
                      : 'Select a end'
                  }
                  active={!injectPattern.beginSelect}
                  intent={!injectPattern.beginSelect ? 'success' : 'none'}
                  fill
                  onClick={() =>
                    setInjectPattern({ ...injectPattern, beginSelect: false })
                  }
                />

                <div className={buttonWrapper}>
                  <Button
                    intent={'primary'}
                    disabled={!injectPattern.begin || !injectPattern.end}
                    onClick={handleDownload}
                    icon='download'
                    alignText='left'
                  >
                    Export Inject Pattern
                  </Button>
                </div>
              </div>
            )}
          </>
        }
        isOpen={opened}
      >
        <Button
          title='Download Inject Pattern'
          active={opened}
          icon='download'
          alignText='left'
          fill
          minimal={!selected}
          onClick={() => {
            setOpened(prev => !prev)
          }}
        >
          Inject Pattern
        </Button>
      </Popover>
    </div>
  )
}
+6 −24
Original line number Diff line number Diff line
@@ -27,6 +27,7 @@ import {
  CONTENT_HEIGHT,
  getControlLinks,
  getFilteredQuestions,
  getNodeName,
  getQuestionLinks,
  isControlUsed,
  QUESTION_HEIGHT,
@@ -81,6 +82,10 @@ const AdditionalNodeData: FC<{
          CONTENT_HEIGHT * templates.length +
            (templates.length - 1) * TEMPLATE_GAP
        )
        break
      }
      default: {
        onHeightCalculated(0)
      }
    }
  }, [node, onHeightCalculated])
@@ -224,7 +229,6 @@ const AdditionalNodeData: FC<{
          )
        )
      )

      return (
        <>
          {links.map((link, index) => (
@@ -266,28 +270,6 @@ const AdditionalNodeData: FC<{
  return null
}

const getName = (node: TreeNode) => {
  switch (node.type) {
    case TreeNodeTypes.EMAIL_ADDRESS: {
      return node.data.address
    }
    case TreeNodeTypes.TOOL_RESPONSE: {
      return (
        node.data.toolName +
        (node.data.param.length > 0 ? ` - ${node.data.param}` : '')
      )
    }
    case TreeNodeTypes.INJECT:
    case TreeNodeTypes.QUESTIONNAIRE:
    case TreeNodeTypes.MILESTONE: {
      return node.data.display_name ?? node.data.name
    }
    case TreeNodeTypes.START: {
      return node.data.name
    }
  }
}

type EditorNodeProps = {
  node: HierarchyNode<TreeNode>
  nodeMap: Map<string, HierarchyNode<TreeNode>>
@@ -301,7 +283,7 @@ export const EditorNode: FC<EditorNodeProps> = ({
  selected,
  nodeMap,
}) => {
  const name = getName(node.data)
  const name = getNodeName(node.data)

  let currentY = NODE_PADDING_Y

Loading