Loading editor/src/components/Select/index.tsx +3 −0 Original line number Diff line number Diff line Loading @@ -56,6 +56,7 @@ type Props<T extends string | number> = { buttonClassName?: string fill?: boolean placeholder?: string disabled?: boolean } const SharedSelect = <T extends string | number>({ Loading @@ -66,6 +67,7 @@ const SharedSelect = <T extends string | number>({ buttonClassName, fill = false, placeholder, disabled, }: Props<T>) => ( <Select<OptionProps<T>> items={items} Loading @@ -78,6 +80,7 @@ const SharedSelect = <T extends string | number>({ onItemSelect={onItemSelect} className={selectClassname} fill={fill} disabled={disabled} popoverProps={{ enforceFocus: false, autoFocus: false, Loading editor/src/components/Visualisation/AddNodeDialog.tsx +28 −9 Original line number Diff line number Diff line Loading @@ -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; Loading Loading @@ -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 = () => { Loading @@ -67,6 +73,7 @@ const AddNodeDialog: FC<AddNodeDialogProps> = ({ `} > {parentNode.type !== 'Start' && parentNode.type !== 'Milestone' && ( <> <Button text='Milestone' fill Loading @@ -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') && ( Loading Loading @@ -146,6 +162,9 @@ const AddNodeDialog: FC<AddNodeDialogProps> = ({ } /> )} {selectedMode === 'Inject pattern' && ( <AddPatternForm onClose={onClose} parentNode={parentNode} /> )} </Dialog> ) } Loading editor/src/components/Visualisation/InjectPatterns/AddPatternForm.tsx 0 → 100644 +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} /> } /> </> ) } editor/src/components/Visualisation/InjectPatterns/AssignPatterns.tsx 0 → 100644 +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> ) } editor/src/components/Visualisation/Node/EditorNode.tsx +6 −24 Original line number Diff line number Diff line Loading @@ -27,6 +27,7 @@ import { CONTENT_HEIGHT, getControlLinks, getFilteredQuestions, getNodeName, getQuestionLinks, isControlUsed, QUESTION_HEIGHT, Loading Loading @@ -81,6 +82,10 @@ const AdditionalNodeData: FC<{ CONTENT_HEIGHT * templates.length + (templates.length - 1) * TEMPLATE_GAP ) break } default: { onHeightCalculated(0) } } }, [node, onHeightCalculated]) Loading Loading @@ -224,7 +229,6 @@ const AdditionalNodeData: FC<{ ) ) ) return ( <> {links.map((link, index) => ( Loading Loading @@ -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>> Loading @@ -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 Loading
editor/src/components/Select/index.tsx +3 −0 Original line number Diff line number Diff line Loading @@ -56,6 +56,7 @@ type Props<T extends string | number> = { buttonClassName?: string fill?: boolean placeholder?: string disabled?: boolean } const SharedSelect = <T extends string | number>({ Loading @@ -66,6 +67,7 @@ const SharedSelect = <T extends string | number>({ buttonClassName, fill = false, placeholder, disabled, }: Props<T>) => ( <Select<OptionProps<T>> items={items} Loading @@ -78,6 +80,7 @@ const SharedSelect = <T extends string | number>({ onItemSelect={onItemSelect} className={selectClassname} fill={fill} disabled={disabled} popoverProps={{ enforceFocus: false, autoFocus: false, Loading
editor/src/components/Visualisation/AddNodeDialog.tsx +28 −9 Original line number Diff line number Diff line Loading @@ -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; Loading Loading @@ -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 = () => { Loading @@ -67,6 +73,7 @@ const AddNodeDialog: FC<AddNodeDialogProps> = ({ `} > {parentNode.type !== 'Start' && parentNode.type !== 'Milestone' && ( <> <Button text='Milestone' fill Loading @@ -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') && ( Loading Loading @@ -146,6 +162,9 @@ const AddNodeDialog: FC<AddNodeDialogProps> = ({ } /> )} {selectedMode === 'Inject pattern' && ( <AddPatternForm onClose={onClose} parentNode={parentNode} /> )} </Dialog> ) } Loading
editor/src/components/Visualisation/InjectPatterns/AddPatternForm.tsx 0 → 100644 +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} /> } /> </> ) }
editor/src/components/Visualisation/InjectPatterns/AssignPatterns.tsx 0 → 100644 +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> ) }
editor/src/components/Visualisation/Node/EditorNode.tsx +6 −24 Original line number Diff line number Diff line Loading @@ -27,6 +27,7 @@ import { CONTENT_HEIGHT, getControlLinks, getFilteredQuestions, getNodeName, getQuestionLinks, isControlUsed, QUESTION_HEIGHT, Loading Loading @@ -81,6 +82,10 @@ const AdditionalNodeData: FC<{ CONTENT_HEIGHT * templates.length + (templates.length - 1) * TEMPLATE_GAP ) break } default: { onHeightCalculated(0) } } }, [node, onHeightCalculated]) Loading Loading @@ -224,7 +229,6 @@ const AdditionalNodeData: FC<{ ) ) ) return ( <> {links.map((link, index) => ( Loading Loading @@ -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>> Loading @@ -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