diff --git a/frontend/src/editor/EditorPage/index.tsx b/frontend/src/editor/EditorPage/index.tsx index 63d77e9f4da525405bcdb182fb343b4acba6b422..afb081b541258176aeea30d123bafee6165c492c 100644 --- a/frontend/src/editor/EditorPage/index.tsx +++ b/frontend/src/editor/EditorPage/index.tsx @@ -53,8 +53,8 @@ const EditorPage: FC<EditorPageProps> = ({ style={{ marginRight: '0.5rem', }} - />{' '} - {nextVisible && ( + /> + {(nextVisible === undefined || nextVisible) && ( <Button type='button' onClick={() => nav(nextPath || '/')} @@ -69,10 +69,4 @@ const EditorPage: FC<EditorPageProps> = ({ ) } -EditorPage.defaultProps = { - nextPath: '/', - nextDisabled: false, - nextVisible: true, -} - export default memo(EditorPage) diff --git a/frontend/src/editor/EmailAddressForm/index.tsx b/frontend/src/editor/EmailAddressForm/index.tsx index 35cc8d43c88b85408b4b7152a2047e77720d6a98..dde59bee902289831893839bf6ac1c0c1b6028b5 100644 --- a/frontend/src/editor/EmailAddressForm/index.tsx +++ b/frontend/src/editor/EmailAddressForm/index.tsx @@ -130,7 +130,7 @@ const EmailAddressForm: FC<EmailAddressFormProps> = ({ actions={ emailAddressInfo ? ( <Button - disabled={!address || !description} + disabled={!address} onClick={() => handleUpdateButton({ id: emailAddressInfo.id, @@ -146,7 +146,7 @@ const EmailAddressForm: FC<EmailAddressFormProps> = ({ /> ) : ( <Button - disabled={!address || !description} + disabled={!address} onClick={() => handleAddButton({ address, diff --git a/frontend/src/editor/LearningActivitySpecification/EmailAddressSelector.tsx b/frontend/src/editor/EmailAddressSelector/index.tsx similarity index 86% rename from frontend/src/editor/LearningActivitySpecification/EmailAddressSelector.tsx rename to frontend/src/editor/EmailAddressSelector/index.tsx index bd3d25631cc135d0620a86502934402c6e884765..5c9e53c9f26e5cb8102478cb6b34136bf8494082 100644 --- a/frontend/src/editor/LearningActivitySpecification/EmailAddressSelector.tsx +++ b/frontend/src/editor/EmailAddressSelector/index.tsx @@ -1,7 +1,7 @@ import type { OptionProps } from '@blueprintjs/core' import { HTMLSelect, Label } from '@blueprintjs/core' import { useLiveQuery } from 'dexie-react-hooks' -import { memo, useMemo, type FC } from 'react' +import { memo, useEffect, useMemo, type FC } from 'react' import EmailAddressForm from '../EmailAddressForm' import { db } from '../indexeddb/db' import type { EmailAddressInfo } from '../indexeddb/types' @@ -34,6 +34,12 @@ const EmailAddressSelector: FC<EmailAddressFormProps> = ({ })) }, [emailAddresses]) + useEffect(() => { + if (!emailAddressId && emailAddresses && emailAddresses.length > 0) { + onChange(Number(emailAddresses[0].id)) + } + }, [emailAddresses, emailAddressId]) + return ( <div style={{ display: 'flex', width: '100%' }}> <Label style={{ flexGrow: '1' }}> diff --git a/frontend/src/editor/EmailAddresses/EmailAddress.tsx b/frontend/src/editor/EmailAddresses/EmailAddress.tsx new file mode 100644 index 0000000000000000000000000000000000000000..f01aff35fd413cee880785df799ba995d417d328 --- /dev/null +++ b/frontend/src/editor/EmailAddresses/EmailAddress.tsx @@ -0,0 +1,81 @@ +import { Button, ButtonGroup, Card } from '@blueprintjs/core' +import { useNotifyContext } from '@inject/shared/notification/contexts/NotifyContext' +import type { FC } from 'react' +import { memo, useCallback } from 'react' +import EmailAddressForm from '../EmailAddressForm' +import EmailTemplateFormDialog from '../EmailTemplateFormDialog' +import EmailTemplates from '../EmailTemplates' +import { deleteEmailAddress } from '../indexeddb/operations' +import type { EmailAddressInfo } from '../indexeddb/types' + +interface EmailAddressProps { + emailAddress: EmailAddressInfo +} + +const EmailAddressItem: FC<EmailAddressProps> = ({ emailAddress }) => { + const { notify } = useNotifyContext() + + const handleDeleteButton = useCallback( + async (emailAddressInfo: EmailAddressInfo) => { + try { + await deleteEmailAddress(emailAddressInfo.id) + } catch (err) { + notify( + `Failed to delete email address '${emailAddressInfo.address}': ${err}`, + { + intent: 'danger', + } + ) + } + }, + [notify] + ) + + return ( + <Card style={{ flexDirection: 'column' }}> + <div + style={{ + display: 'flex', + justifyContent: 'space-between', + alignItems: 'center', + width: '100%', + padding: '0rem 1rem 1rem 0', + }} + > + <span style={{ height: '100%', flexGrow: 1 }}> + {emailAddress.address} + </span> + <ButtonGroup> + <EmailAddressForm + emailAddressInfo={emailAddress} + buttonProps={{ + minimal: true, + icon: 'edit', + style: { marginRight: '1rem' }, + }} + /> + <Button + minimal + icon='cross' + onClick={() => handleDeleteButton(emailAddress)} + /> + </ButtonGroup> + </div> + <div style={{ width: '100%', paddingLeft: '2rem' }}> + <EmailTemplates emailAddressId={Number(emailAddress.id)} /> + <EmailTemplateFormDialog + emailAddressId={Number(emailAddress.id)} + buttonProps={{ + minimal: true, + text: 'Add template', + alignText: 'left', + icon: 'plus', + style: { padding: '1rem', width: '100%' }, + }} + /> + </div> + </Card> + ) +} + +export default memo(EmailAddressItem) diff --git a/frontend/src/editor/EmailAddresses/index.tsx b/frontend/src/editor/EmailAddresses/index.tsx new file mode 100644 index 0000000000000000000000000000000000000000..56e82bab1f1f6549ba2de59fb44eda76cedfa8f5 --- /dev/null +++ b/frontend/src/editor/EmailAddresses/index.tsx @@ -0,0 +1,20 @@ +import { db } from '@/editor/indexeddb/db' +import type { EmailAddressInfo } from '@/editor/indexeddb/types' +import { CardList } from '@blueprintjs/core' +import { useLiveQuery } from 'dexie-react-hooks' +import { memo } from 'react' +import EmailAddress from './EmailAddress' + +const EmailAddresses = () => { + const emailAddresses = useLiveQuery(() => db.emailAddresses.toArray(), [], []) + + return ( + <CardList> + {emailAddresses?.map((emailAddress: EmailAddressInfo) => ( + <EmailAddress key={emailAddress.id} emailAddress={emailAddress} /> + ))} + </CardList> + ) +} + +export default memo(EmailAddresses) diff --git a/frontend/src/editor/EmailTemplateFormContent/index.tsx b/frontend/src/editor/EmailTemplateFormContent/index.tsx new file mode 100644 index 0000000000000000000000000000000000000000..00b9f9e4d877add6e6431efb6b523b989a28c283 --- /dev/null +++ b/frontend/src/editor/EmailTemplateFormContent/index.tsx @@ -0,0 +1,52 @@ +import { InputGroup, Label, TextArea } from '@blueprintjs/core' +import { memo, type FC } from 'react' +import EmailAddressSelector from '../EmailAddressSelector' + +interface EmailTemplateFormProps { + context: string + onContextChange: (value: string) => void + content: string + onContentChange: (value: string) => void + emailAddressId: number + onEmailAddressIdChange: (value: number) => void +} + +const EmailTemplateForm: FC<EmailTemplateFormProps> = ({ + context, + onContextChange, + content, + onContentChange, + emailAddressId, + onEmailAddressIdChange, +}) => ( + <div> + <EmailAddressSelector + emailAddressId={emailAddressId} + onChange={id => onEmailAddressIdChange(id)} + /> + <Label> + Context + <InputGroup + placeholder='Input text' + value={context} + onChange={e => onContextChange(e.target.value)} + /> + </Label> + <Label> + Content + <TextArea + value={content} + style={{ + width: '100%', + height: '10rem', + resize: 'none', + overflowY: 'auto', + }} + placeholder='Input text' + onChange={e => onContentChange(e.target.value)} + /> + </Label> + </div> +) + +export default memo(EmailTemplateForm) diff --git a/frontend/src/editor/EmailTemplateFormDialog/index.tsx b/frontend/src/editor/EmailTemplateFormDialog/index.tsx new file mode 100644 index 0000000000000000000000000000000000000000..604aa52541796344b2ccf3438324cecf04108940 --- /dev/null +++ b/frontend/src/editor/EmailTemplateFormDialog/index.tsx @@ -0,0 +1,127 @@ +import type { ButtonProps } from '@blueprintjs/core' +import { Button, Dialog, DialogBody, DialogFooter } from '@blueprintjs/core' +import { useNotifyContext } from '@inject/shared/notification/contexts/NotifyContext' +import { memo, useCallback, useEffect, useState, type FC } from 'react' +import EmailTemplateFormContent from '../EmailTemplateFormContent' +import { addEmailTemplate, updateEmailTemplate } from '../indexeddb/operations' +import type { EmailTemplate } from '../indexeddb/types' + +interface EmailTemplateFormDialogProps { + template?: EmailTemplate + emailAddressId: number + buttonProps: ButtonProps +} + +const EmailTemplateFormDialog: FC<EmailTemplateFormDialogProps> = ({ + template, + emailAddressId, + buttonProps, +}) => { + const { notify } = useNotifyContext() + const [isOpen, setIsOpen] = useState(false) + + const [context, setContext] = useState<string>('') + const [content, setContent] = useState<string>('') + const [selectedAddressId, setSelectedAddressId] = useState<number>(0) + + useEffect(() => { + setContext(template?.context || '') + setContent(template?.content || '') + setSelectedAddressId(template?.emailAddressId || emailAddressId) + }, [template, isOpen]) + + const clearInput = useCallback(() => { + setContext('') + setContent('') + }, []) + + const handleAddButton = useCallback( + async (emailTemplate: Omit<EmailTemplate, 'id'>) => { + try { + await addEmailTemplate(emailTemplate) + clearInput() + setIsOpen(false) + } catch (err) { + notify(`Failed to add template '${emailTemplate.context}': ${err}`, { + intent: 'danger', + }) + } + }, + [notify] + ) + + const handleUpdateButton = useCallback( + async (emailTemplate: EmailTemplate) => { + try { + await updateEmailTemplate(emailTemplate) + setIsOpen(false) + } catch (err) { + notify(`Failed to update template '${emailTemplate.context}': ${err}`, { + intent: 'danger', + }) + } + }, + [notify] + ) + + return ( + <> + <Button {...buttonProps} onClick={() => setIsOpen(true)} /> + <Dialog + isOpen={isOpen} + onClose={() => setIsOpen(false)} + icon={template ? 'edit' : 'plus'} + title={template ? 'Edit template' : 'New template'} + > + <DialogBody> + <EmailTemplateFormContent + context={context} + content={content} + emailAddressId={selectedAddressId} + onContextChange={(value: string) => setContext(value)} + onContentChange={(value: string) => setContent(value)} + onEmailAddressIdChange={(value: number) => + setSelectedAddressId(value) + } + /> + </DialogBody> + <DialogFooter + actions={ + template ? ( + <Button + disabled={!context || !selectedAddressId} + onClick={() => + handleUpdateButton({ + id: template.id, + context, + content, + emailAddressId: selectedAddressId, + }) + } + intent='primary' + icon='edit' + text='Save changes' + /> + ) : ( + <Button + disabled={!context || !selectedAddressId} + onClick={() => + handleAddButton({ + context, + content, + emailAddressId: selectedAddressId, + }) + } + intent='primary' + icon='plus' + text='Add' + /> + ) + } + /> + </Dialog> + </> + ) +} + +export default memo(EmailTemplateFormDialog) diff --git a/frontend/src/editor/EmailTemplates/EmailTemplate.tsx b/frontend/src/editor/EmailTemplates/EmailTemplate.tsx new file mode 100644 index 0000000000000000000000000000000000000000..f61b34673e9de4eea7d2044c06a5ebcfdbd6cf5f --- /dev/null +++ b/frontend/src/editor/EmailTemplates/EmailTemplate.tsx @@ -0,0 +1,54 @@ +import { Button, ButtonGroup, Card } from '@blueprintjs/core' +import { useNotifyContext } from '@inject/shared/notification/contexts/NotifyContext' +import type { FC } from 'react' +import { memo, useCallback } from 'react' +import EmailTemplateFormDialog from '../EmailTemplateFormDialog' +import { deleteEmailTemplate } from '../indexeddb/operations' +import type { EmailTemplate } from '../indexeddb/types' + +interface EmailTemplateProps { + emailTemplate: EmailTemplate +} + +const EmailTemplateItem: FC<EmailTemplateProps> = ({ emailTemplate }) => { + const { notify } = useNotifyContext() + + const handleDeleteButton = useCallback( + async (template: EmailTemplate) => { + try { + await deleteEmailTemplate(template.id) + } catch (err) { + notify(`Failed to delete template '${template.context}': ${err}`, { + intent: 'danger', + }) + } + }, + [notify] + ) + + return ( + <Card style={{ display: 'flex', justifyContent: 'space-between' }}> + <span style={{ height: '100%', flexGrow: 1 }}> + {emailTemplate.context} + </span> + <ButtonGroup> + <EmailTemplateFormDialog + template={emailTemplate} + emailAddressId={emailTemplate.emailAddressId} + buttonProps={{ + minimal: true, + icon: 'edit', + style: { marginRight: '1rem' }, + }} + /> + <Button + minimal + icon='cross' + onClick={() => handleDeleteButton(emailTemplate)} + /> + </ButtonGroup> + </Card> + ) +} + +export default memo(EmailTemplateItem) diff --git a/frontend/src/editor/EmailTemplates/index.tsx b/frontend/src/editor/EmailTemplates/index.tsx new file mode 100644 index 0000000000000000000000000000000000000000..93c4b0e08b8313e1f26e5e2508b67b2771b2632e --- /dev/null +++ b/frontend/src/editor/EmailTemplates/index.tsx @@ -0,0 +1,35 @@ +import { db } from '@/editor/indexeddb/db' +import type { EmailTemplate } from '@/editor/indexeddb/types' +import { CardList } from '@blueprintjs/core' +import { useLiveQuery } from 'dexie-react-hooks' +import type { FC } from 'react' +import { memo } from 'react' +import EmailTemplateItem from './EmailTemplate' + +interface EmailTemplatesProps { + emailAddressId: number +} + +const EmailTemplates: FC<EmailTemplatesProps> = ({ emailAddressId }) => { + const emailTemplates = useLiveQuery( + () => + db.emailTemplates + .where({ emailAddressId: Number(emailAddressId) }) + .toArray(), + [emailAddressId], + [] + ) + + return ( + <CardList> + {emailTemplates?.map((emailTemplate: EmailTemplate) => ( + <EmailTemplateItem + key={emailTemplate.id} + emailTemplate={emailTemplate} + /> + ))} + </CardList> + ) +} + +export default memo(EmailTemplates) diff --git a/frontend/src/editor/InjectSpecification/EmailInjectForm.tsx b/frontend/src/editor/InjectSpecification/EmailInjectForm.tsx index c9bc5881801b7892f92a70506782340981f02ff9..0163856bf502eceeabd7a5baf89c25f7e9bbd360 100644 --- a/frontend/src/editor/InjectSpecification/EmailInjectForm.tsx +++ b/frontend/src/editor/InjectSpecification/EmailInjectForm.tsx @@ -8,7 +8,7 @@ import { import { useNotifyContext } from '@inject/shared/notification/contexts/NotifyContext' import { useLiveQuery } from 'dexie-react-hooks' import { memo, useCallback, useEffect, useState, type FC } from 'react' -import EmailAddressSelector from '../LearningActivitySpecification/EmailAddressSelector' +import EmailAddressSelector from '../EmailAddressSelector' import { addEmailInject, getEmailInjectByInjectInfoId, diff --git a/frontend/src/editor/LearningActivitySpecification/EmailTemplateForm.tsx b/frontend/src/editor/LearningActivitySpecification/EmailTemplateForm.tsx index cbd4c24d6c6f8817719272efa6c961a0af1e9f5b..a9ba887d60daf3508e2fc9879c8cad0d65cb144f 100644 --- a/frontend/src/editor/LearningActivitySpecification/EmailTemplateForm.tsx +++ b/frontend/src/editor/LearningActivitySpecification/EmailTemplateForm.tsx @@ -1,14 +1,14 @@ -import { Button, InputGroup, Label, TextArea } from '@blueprintjs/core' +import { Button } from '@blueprintjs/core' import { useNotifyContext } from '@inject/shared/notification/contexts/NotifyContext' import { useLiveQuery } from 'dexie-react-hooks' import { memo, useCallback, useEffect, useState, type FC } from 'react' +import EmailTemplateFormContent from '../EmailTemplateFormContent' import { addEmailTemplate, getEmailTemplateByActivityId, updateEmailTemplate, } from '../indexeddb/operations' import type { EmailTemplate } from '../indexeddb/types' -import EmailAddressSelector from './EmailAddressSelector' interface EmailTemplateFormProps { learningActivityId: number @@ -54,33 +54,16 @@ const EmailTemplateForm: FC<EmailTemplateFormProps> = ({ return ( <div> - <EmailAddressSelector + <EmailTemplateFormContent + context={context} + content={content} emailAddressId={selectedAddressId} - onChange={id => setSelectedAddressId(id)} + onContextChange={(value: string) => setContext(value)} + onContentChange={(value: string) => setContent(value)} + onEmailAddressIdChange={(value: number) => setSelectedAddressId(value)} /> - <Label> - Context - <InputGroup - placeholder='Input text' - value={context} - onChange={e => setContext(e.target.value)} - /> - </Label> - <Label> - Content - <TextArea - value={content} - style={{ - width: '100%', - height: '10rem', - resize: 'none', - overflowY: 'auto', - }} - placeholder='Input text' - onChange={e => setContent(e.target.value)} - /> - </Label> <Button + disabled={!context || !selectedAddressId} onClick={() => handleUpdateButton({ learningActivityId, diff --git a/frontend/src/editor/LearningActivitySpecification/ToolResponseForm.tsx b/frontend/src/editor/LearningActivitySpecification/ToolResponseForm.tsx index 324c7d1094376c371e825d5136b36873abf782d7..4387df86f20de1759fc8b1decbed9b7b5fe41d85 100644 --- a/frontend/src/editor/LearningActivitySpecification/ToolResponseForm.tsx +++ b/frontend/src/editor/LearningActivitySpecification/ToolResponseForm.tsx @@ -1,20 +1,14 @@ -import { - Button, - Checkbox, - InputGroup, - Label, - TextArea, -} from '@blueprintjs/core' +import { Button } from '@blueprintjs/core' import { useNotifyContext } from '@inject/shared/notification/contexts/NotifyContext' import { useLiveQuery } from 'dexie-react-hooks' import { memo, useCallback, useEffect, useState, type FC } from 'react' +import ToolResponseFormContent from '../ToolResponseFormContent' import { addToolResponse, getToolResponseByActivityId, updateToolResponse, } from '../indexeddb/operations' import type { ToolResponse } from '../indexeddb/types' -import ToolSelector from './ToolSelector' interface ToolResponseFormProps { learningActivityId: number @@ -62,38 +56,18 @@ const ToolResponseForm: FC<ToolResponseFormProps> = ({ return ( <div> - <ToolSelector + <ToolResponseFormContent + parameter={parameter} + content={content} + isRegex={isRegex} toolId={selectedToolId} - onChange={id => setSelectedToolId(id)} - /> - <Label> - Parameter - <InputGroup - placeholder='Input text' - value={parameter} - onChange={e => setParameter(e.target.value)} - /> - </Label> - <Checkbox - label='Is parameter regex?' - checked={isRegex} - onChange={e => setIsRegex(e.target.checked)} + onParameterChange={(value: string) => setParameter(value)} + onContentChange={(value: string) => setContent(value)} + onIsRegexChange={(value: boolean) => setIsRegex(value)} + onToolIdChange={(value: number) => setSelectedToolId(value)} /> - <Label> - Response - <TextArea - value={content} - style={{ - width: '100%', - height: '10rem', - resize: 'none', - overflowY: 'auto', - }} - placeholder='Input text' - onChange={e => setContent(e.target.value)} - /> - </Label> <Button + disabled={!parameter || !selectedToolId} onClick={() => handleUpdateButton({ learningActivityId, diff --git a/frontend/src/editor/Navbar/index.tsx b/frontend/src/editor/Navbar/index.tsx index 2bc48d11dc542de80dcb967bbef194dcf1feb2a1..0b6ae3b55aff6630afa58d5af84f92ee6d0f6df1 100644 --- a/frontend/src/editor/Navbar/index.tsx +++ b/frontend/src/editor/Navbar/index.tsx @@ -20,6 +20,7 @@ const Navbar = () => ( path='/editor/create/inject-specification' name='Injects specification' /> + <NavbarButton path='/editor/create/other' name='Other' /> <NavbarButton path='/editor/create/final-information' name='Final Information' diff --git a/frontend/src/editor/ToolForm/index.tsx b/frontend/src/editor/ToolForm/index.tsx index 1b0b96a9e88813450f20b3e0cf4d8edc140ac774..56899e07d97b08f25f2471e3056a1804e0827194 100644 --- a/frontend/src/editor/ToolForm/index.tsx +++ b/frontend/src/editor/ToolForm/index.tsx @@ -137,13 +137,7 @@ const ToolForm: FC<ToolFormProps> = ({ toolInfo, buttonProps, onAdd }) => { actions={ toolInfo ? ( <Button - disabled={ - !name || - !category || - !tooltipDescription || - !hint || - !defaultResponse - } + disabled={!name || !defaultResponse} onClick={() => handleUpdateButton({ id: toolInfo.id, @@ -160,13 +154,7 @@ const ToolForm: FC<ToolFormProps> = ({ toolInfo, buttonProps, onAdd }) => { /> ) : ( <Button - disabled={ - !name || - !category || - !tooltipDescription || - !hint || - !defaultResponse - } + disabled={!name || !defaultResponse} onClick={() => handleAddButton({ name, diff --git a/frontend/src/editor/ToolResponseFormContent/index.tsx b/frontend/src/editor/ToolResponseFormContent/index.tsx new file mode 100644 index 0000000000000000000000000000000000000000..167139611f816c2295bb72caa9d3cadb6c8cba4e --- /dev/null +++ b/frontend/src/editor/ToolResponseFormContent/index.tsx @@ -0,0 +1,58 @@ +import { Checkbox, InputGroup, Label, TextArea } from '@blueprintjs/core' +import { memo, type FC } from 'react' +import ToolSelector from '../ToolSelector' + +interface ToolResponseFormProps { + parameter: string + onParameterChange: (value: string) => void + content: string + onContentChange: (value: string) => void + isRegex: boolean + onIsRegexChange: (value: boolean) => void + toolId: number + onToolIdChange: (value: number) => void +} + +const ToolResponseForm: FC<ToolResponseFormProps> = ({ + parameter, + onParameterChange, + content, + onContentChange, + isRegex, + onIsRegexChange, + toolId, + onToolIdChange, +}) => ( + <div> + <ToolSelector toolId={toolId} onChange={id => onToolIdChange(id)} /> + <Label> + Parameter + <InputGroup + placeholder='Input text' + value={parameter} + onChange={e => onParameterChange(e.target.value)} + /> + </Label> + <Checkbox + label='Is parameter regex?' + checked={isRegex} + onChange={e => onIsRegexChange(e.target.checked)} + /> + <Label> + Response + <TextArea + value={content} + style={{ + width: '100%', + height: '10rem', + resize: 'none', + overflowY: 'auto', + }} + placeholder='Input text' + onChange={e => onContentChange(e.target.value)} + /> + </Label> + </div> +) + +export default memo(ToolResponseForm) diff --git a/frontend/src/editor/ToolResponseFormDialog/index.tsx b/frontend/src/editor/ToolResponseFormDialog/index.tsx new file mode 100644 index 0000000000000000000000000000000000000000..be96fc4c7ba12cc2bbb77c368ccb0c7ef958d381 --- /dev/null +++ b/frontend/src/editor/ToolResponseFormDialog/index.tsx @@ -0,0 +1,138 @@ +import type { ButtonProps } from '@blueprintjs/core' +import { Button, Dialog, DialogBody, DialogFooter } from '@blueprintjs/core' +import { useNotifyContext } from '@inject/shared/notification/contexts/NotifyContext' +import { memo, useCallback, useEffect, useState, type FC } from 'react' +import ToolResponseForm from '../ToolResponseFormContent' +import { addToolResponse, updateToolResponse } from '../indexeddb/operations' +import type { ToolResponse } from '../indexeddb/types' + +interface ToolResponseFormDialogProps { + response?: ToolResponse + toolId: number + buttonProps: ButtonProps +} + +const ToolResponseFormDialog: FC<ToolResponseFormDialogProps> = ({ + response, + toolId, + buttonProps, +}) => { + const { notify } = useNotifyContext() + const [isOpen, setIsOpen] = useState(false) + + const [parameter, setParameter] = useState<string>('') + const [content, setContent] = useState<string>('') + const [isRegex, setIsRegex] = useState<boolean>(false) + const [selectedToolId, setSelectedToolId] = useState<number>(0) + + const clearInput = useCallback(() => { + setParameter('') + setContent('') + setIsRegex(false) + }, []) + + useEffect(() => { + setParameter(response?.parameter || '') + setContent(response?.content || '') + setIsRegex(response?.isRegex || false) + setSelectedToolId(response?.toolId || toolId) + }, [response, isOpen]) + + const handleAddButton = useCallback( + async (toolResponse: Omit<ToolResponse, 'id'>) => { + try { + await addToolResponse(toolResponse) + clearInput() + setIsOpen(false) + } catch (err) { + notify( + `Failed to add tool response '${toolResponse.parameter}': ${err}`, + { + intent: 'danger', + } + ) + } + }, + [notify] + ) + + const handleUpdateButton = useCallback( + async (toolResponse: ToolResponse) => { + try { + await updateToolResponse(toolResponse) + setIsOpen(false) + } catch (err) { + notify( + `Failed to update tool response '${toolResponse.parameter}': ${err}`, + { + intent: 'danger', + } + ) + } + }, + [notify] + ) + + return ( + <> + <Button {...buttonProps} onClick={() => setIsOpen(true)} /> + <Dialog + isOpen={isOpen} + onClose={() => setIsOpen(false)} + icon={response ? 'edit' : 'plus'} + title={response ? 'Edit tool response' : 'New tool response'} + > + <DialogBody> + <ToolResponseForm + parameter={parameter} + content={content} + isRegex={isRegex} + toolId={selectedToolId} + onParameterChange={(value: string) => setParameter(value)} + onContentChange={(value: string) => setContent(value)} + onIsRegexChange={(value: boolean) => setIsRegex(value)} + onToolIdChange={(value: number) => setSelectedToolId(value)} + /> + </DialogBody> + <DialogFooter + actions={ + response ? ( + <Button + disabled={!parameter} + onClick={() => + handleUpdateButton({ + id: response.id, + parameter, + content, + isRegex, + toolId: selectedToolId, + }) + } + intent='primary' + icon='edit' + text='Save changes' + /> + ) : ( + <Button + disabled={!parameter} + onClick={() => + handleAddButton({ + parameter, + content, + isRegex, + toolId: selectedToolId, + }) + } + intent='primary' + icon='plus' + text='Add' + /> + ) + } + /> + </Dialog> + </> + ) +} + +export default memo(ToolResponseFormDialog) diff --git a/frontend/src/editor/ToolResponses/ToolResponse.tsx b/frontend/src/editor/ToolResponses/ToolResponse.tsx new file mode 100644 index 0000000000000000000000000000000000000000..025d57952441f9b5807503c41335603f4ad6ecb6 --- /dev/null +++ b/frontend/src/editor/ToolResponses/ToolResponse.tsx @@ -0,0 +1,57 @@ +import { Button, ButtonGroup, Card } from '@blueprintjs/core' +import { useNotifyContext } from '@inject/shared/notification/contexts/NotifyContext' +import type { FC } from 'react' +import { memo, useCallback } from 'react' +import ToolResponseFormDialog from '../ToolResponseFormDialog' +import { deleteToolResponse } from '../indexeddb/operations' +import type { ToolResponse } from '../indexeddb/types' + +interface ToolResponseProps { + toolResponse: ToolResponse +} + +const ToolResponseItem: FC<ToolResponseProps> = ({ toolResponse }) => { + const { notify } = useNotifyContext() + + const handleDeleteButton = useCallback( + async (response: ToolResponse) => { + try { + await deleteToolResponse(response.id) + } catch (err) { + notify( + `Failed to delete tool response '${response.parameter}': ${err}`, + { + intent: 'danger', + } + ) + } + }, + [notify] + ) + + return ( + <Card style={{ display: 'flex', justifyContent: 'space-between' }}> + <span style={{ height: '100%', flexGrow: 1 }}> + {toolResponse.parameter} + </span> + <ButtonGroup> + <ToolResponseFormDialog + response={toolResponse} + toolId={toolResponse.toolId} + buttonProps={{ + minimal: true, + icon: 'edit', + style: { marginRight: '1rem' }, + }} + /> + <Button + minimal + icon='cross' + onClick={() => handleDeleteButton(toolResponse)} + /> + </ButtonGroup> + </Card> + ) +} + +export default memo(ToolResponseItem) diff --git a/frontend/src/editor/ToolResponses/index.tsx b/frontend/src/editor/ToolResponses/index.tsx new file mode 100644 index 0000000000000000000000000000000000000000..1c96632cc93a4b63eec6a6c5fb001626bf54e23d --- /dev/null +++ b/frontend/src/editor/ToolResponses/index.tsx @@ -0,0 +1,29 @@ +import { db } from '@/editor/indexeddb/db' +import type { ToolResponse } from '@/editor/indexeddb/types' +import { CardList } from '@blueprintjs/core' +import { useLiveQuery } from 'dexie-react-hooks' +import type { FC } from 'react' +import { memo } from 'react' +import ToolResponseItem from './ToolResponse' + +interface ToolResponsesProps { + toolId: number +} + +const ToolResponses: FC<ToolResponsesProps> = ({ toolId }) => { + const toolResponses = useLiveQuery( + () => db.toolResponses.where({ toolId: Number(toolId) }).toArray(), + [toolId], + [] + ) + + return ( + <CardList> + {toolResponses?.map((toolResponse: ToolResponse) => ( + <ToolResponseItem key={toolResponse.id} toolResponse={toolResponse} /> + ))} + </CardList> + ) +} + +export default memo(ToolResponses) diff --git a/frontend/src/editor/LearningActivitySpecification/ToolSelector.tsx b/frontend/src/editor/ToolSelector/index.tsx similarity index 88% rename from frontend/src/editor/LearningActivitySpecification/ToolSelector.tsx rename to frontend/src/editor/ToolSelector/index.tsx index e3a194a2db68e4b5ac8145556654f4bcbcff16ff..e6a2d7bddb23d3564db15004b36a0a7abca60092 100644 --- a/frontend/src/editor/LearningActivitySpecification/ToolSelector.tsx +++ b/frontend/src/editor/ToolSelector/index.tsx @@ -1,7 +1,7 @@ import type { OptionProps } from '@blueprintjs/core' import { HTMLSelect, Label } from '@blueprintjs/core' import { useLiveQuery } from 'dexie-react-hooks' -import { memo, useMemo, type FC } from 'react' +import { memo, useEffect, useMemo, type FC } from 'react' import ToolForm from '../ToolForm' import { db } from '../indexeddb/db' import type { ToolInfo } from '../indexeddb/types' @@ -31,6 +31,12 @@ const ToolSelector: FC<ToolFormProps> = ({ toolId, onChange }) => { })) }, [tools]) + useEffect(() => { + if (!toolId && tools && tools.length > 0) { + onChange(tools[0].id) + } + }, [tools, toolId]) + return ( <div style={{ display: 'flex', width: '100%' }}> <Label style={{ flexGrow: '1' }}> diff --git a/frontend/src/editor/Tools/Tool.tsx b/frontend/src/editor/Tools/Tool.tsx new file mode 100644 index 0000000000000000000000000000000000000000..a9effbb8154ca201c16f8ac94b1846065800aa7b --- /dev/null +++ b/frontend/src/editor/Tools/Tool.tsx @@ -0,0 +1,76 @@ +import { Button, ButtonGroup, Card } from '@blueprintjs/core' +import { useNotifyContext } from '@inject/shared/notification/contexts/NotifyContext' +import type { FC } from 'react' +import { memo, useCallback } from 'react' +import ToolForm from '../ToolForm' +import ToolResponseFormDialog from '../ToolResponseFormDialog' +import ToolResponses from '../ToolResponses' +import { deleteTool } from '../indexeddb/operations' +import type { ToolInfo } from '../indexeddb/types' + +interface ToolProps { + tool: ToolInfo +} + +const ToolItem: FC<ToolProps> = ({ tool }) => { + const { notify } = useNotifyContext() + + const handleDeleteButton = useCallback( + async (toolInfo: ToolInfo) => { + try { + await deleteTool(toolInfo.id) + } catch (err) { + notify(`Failed to delete tool '${toolInfo.name}': ${err}`, { + intent: 'danger', + }) + } + }, + [notify] + ) + + return ( + <Card style={{ flexDirection: 'column' }}> + <div + style={{ + display: 'flex', + justifyContent: 'space-between', + alignItems: 'center', + width: '100%', + padding: '0rem 1rem 1rem 0', + }} + > + <span style={{ height: '100%', flexGrow: 1 }}>{tool.name}</span> + <ButtonGroup> + <ToolForm + toolInfo={tool} + buttonProps={{ + minimal: true, + icon: 'edit', + style: { marginRight: '1rem' }, + }} + /> + <Button + minimal + icon='cross' + onClick={() => handleDeleteButton(tool)} + /> + </ButtonGroup> + </div> + <div style={{ width: '100%', paddingLeft: '2rem' }}> + <ToolResponses toolId={tool.id} /> + <ToolResponseFormDialog + toolId={tool.id} + buttonProps={{ + minimal: true, + text: 'Add tool response', + alignText: 'left', + icon: 'plus', + style: { padding: '1rem', width: '100%' }, + }} + /> + </div> + </Card> + ) +} + +export default memo(ToolItem) diff --git a/frontend/src/editor/Tools/index.tsx b/frontend/src/editor/Tools/index.tsx new file mode 100644 index 0000000000000000000000000000000000000000..ef1a973428e1d84f5a8b945602c10b5bd56c80fa --- /dev/null +++ b/frontend/src/editor/Tools/index.tsx @@ -0,0 +1,18 @@ +import { db } from '@/editor/indexeddb/db' +import type { ToolInfo } from '@/editor/indexeddb/types' +import { CardList } from '@blueprintjs/core' +import { useLiveQuery } from 'dexie-react-hooks' +import { memo } from 'react' +import Tool from './Tool' + +const Tools = () => { + const tools = useLiveQuery(() => db.tools.toArray(), [], []) + + return ( + <CardList> + {tools?.map((tool: ToolInfo) => <Tool key={tool.id} tool={tool} />)} + </CardList> + ) +} + +export default memo(Tools) diff --git a/frontend/src/editor/indexeddb/types.tsx b/frontend/src/editor/indexeddb/types.tsx index d80b9e54c7c24d72787eb4844a593e894c5082b2..d308ae7f2a3526e4f3e42cd2eeb3039b2a65e71c 100644 --- a/frontend/src/editor/indexeddb/types.tsx +++ b/frontend/src/editor/indexeddb/types.tsx @@ -41,7 +41,7 @@ export type ToolInfo = { export type ToolResponse = { id: number - learningActivityId: number + learningActivityId?: number toolId: number parameter: string isRegex: boolean @@ -52,7 +52,7 @@ export type EmailAddressInfo = Omit<EmailAddress, 'control'> export type EmailTemplate = { id: number - learningActivityId: number + learningActivityId?: number emailAddressId: number context: string content: string diff --git a/frontend/src/pages/editor/create/emails.tsx b/frontend/src/pages/editor/create/emails.tsx new file mode 100644 index 0000000000000000000000000000000000000000..afac8110e56815c6bd125a5a172e6d89a4bbe9ba --- /dev/null +++ b/frontend/src/pages/editor/create/emails.tsx @@ -0,0 +1,26 @@ +import EditorPage from '@/editor/EditorPage' +import EmailAddressForm from '@/editor/EmailAddressForm' +import EmailAddresses from '@/editor/EmailAddresses' +import { memo } from 'react' + +const EmailAddressesPage = () => ( + <EditorPage + title='Define email addresses' + description='Description.' + prevPath='/editor/create/other' + nextVisible={false} + > + <EmailAddresses /> + <EmailAddressForm + buttonProps={{ + minimal: true, + text: 'Add email address', + alignText: 'left', + icon: 'plus', + style: { padding: '1rem', width: '100%' }, + }} + /> + </EditorPage> +) + +export default memo(EmailAddressesPage) diff --git a/frontend/src/pages/editor/create/final-information.tsx b/frontend/src/pages/editor/create/final-information.tsx index 5140d3c6f49c8c40b772a0457cfecbbd95a8dcb3..44e199ef37b7be6ad8b1c31c8b7b4a9c87e15809 100644 --- a/frontend/src/pages/editor/create/final-information.tsx +++ b/frontend/src/pages/editor/create/final-information.tsx @@ -9,7 +9,7 @@ const FinalInformationPage = () => { <EditorPage title='Final information' description='Description.' - prevPath='/editor/create/activity-specification' + prevPath='/editor/create/other' nextPath='/editor/create/conclusion' nextDisabled={nextDisabled} > diff --git a/frontend/src/pages/editor/create/inject-specification/index.tsx b/frontend/src/pages/editor/create/inject-specification/index.tsx index 8dc412ee9fb6db54c9f5526889b7d87083fd7d65..a76a44b364e5e847fdb55fd59ebd54e2020731e3 100644 --- a/frontend/src/pages/editor/create/inject-specification/index.tsx +++ b/frontend/src/pages/editor/create/inject-specification/index.tsx @@ -7,8 +7,7 @@ const ActivitiesSpecificationPage = () => ( title='Define injects' description='Description.' prevPath='/editor/create/activity-specification' - nextPath='/editor' - nextDisabled + nextPath='/editor/create/other' > <InjectsOverview /> </EditorPage> diff --git a/frontend/src/pages/editor/create/other.tsx b/frontend/src/pages/editor/create/other.tsx new file mode 100644 index 0000000000000000000000000000000000000000..ce24da83b0eca966d7d2331d836f006d83267c13 --- /dev/null +++ b/frontend/src/pages/editor/create/other.tsx @@ -0,0 +1,28 @@ +import EditorPage from '@/editor/EditorPage' +import { useNavigate } from '@/router' +import { Card, CardList } from '@blueprintjs/core' +import { memo } from 'react' + +const OtherPage = () => { + const nav = useNavigate() + + return ( + <EditorPage + title='Define other information' + description='Add new tools and email addresses to your exercise.' + prevPath='/editor/create/inject-specification' + nextPath='/editor/create/final-information' + > + <CardList> + <Card interactive onClick={() => nav(`/editor/create/tools`)}> + Tools + </Card> + <Card interactive onClick={() => nav(`/editor/create/emails`)}> + Email addresses + </Card> + </CardList> + </EditorPage> + ) +} + +export default memo(OtherPage) diff --git a/frontend/src/pages/editor/create/tools.tsx b/frontend/src/pages/editor/create/tools.tsx new file mode 100644 index 0000000000000000000000000000000000000000..c5ba6977ef15f6782631934887e1234e4995e5f1 --- /dev/null +++ b/frontend/src/pages/editor/create/tools.tsx @@ -0,0 +1,26 @@ +import EditorPage from '@/editor/EditorPage' +import ToolForm from '@/editor/ToolForm' +import Tools from '@/editor/Tools' +import { memo } from 'react' + +const ToolsPage = () => ( + <EditorPage + title='Define tools' + description='Description.' + prevPath='/editor/create/other' + nextVisible={false} + > + <Tools /> + <ToolForm + buttonProps={{ + minimal: true, + text: 'Add tool', + alignText: 'left', + icon: 'plus', + style: { padding: '1rem', width: '100%' }, + }} + /> + </EditorPage> +) + +export default memo(ToolsPage) diff --git a/frontend/src/router.ts b/frontend/src/router.ts index bc19fedf15b0407dd2cd0f91200b8b92f3e4976e..b2ef20e1bc2dc9120f49a2d42ea62fa910b99470 100644 --- a/frontend/src/router.ts +++ b/frontend/src/router.ts @@ -16,6 +16,7 @@ export type Path = | `/editor/create/activity-specification` | `/editor/create/activity-specification/:activityId` | `/editor/create/conclusion` + | `/editor/create/emails` | `/editor/create/exercise-information` | `/editor/create/final-information` | `/editor/create/inject-specification` @@ -23,6 +24,8 @@ export type Path = | `/editor/create/injects` | `/editor/create/introduction` | `/editor/create/learning-objectives` + | `/editor/create/other` + | `/editor/create/tools` | `/exercise-panel` | `/exercise-panel/definition/:definitionId` | `/exercise-panel/exercise/:exerciseId`