From fc449fc7bb1603b961476ea026e228becd865419 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Katar=C3=ADna=20Platkov=C3=A1?= <xplatkov@fi.muni.cz> Date: Fri, 28 Jun 2024 12:11:23 +0200 Subject: [PATCH] Editor - injects page --- frontend/src/editor/InjectForm/index.tsx | 149 +++++++++++++++++++ frontend/src/editor/Injects/Inject.tsx | 58 ++++++++ frontend/src/editor/Injects/index.tsx | 20 +++ frontend/src/editor/indexeddb/db.tsx | 4 +- frontend/src/editor/indexeddb/operations.tsx | 14 +- frontend/src/editor/indexeddb/types.tsx | 12 ++ frontend/src/editor/utils.tsx | 3 + frontend/src/pages/editor/create/injects.tsx | 60 ++++++++ 8 files changed, 318 insertions(+), 2 deletions(-) create mode 100644 frontend/src/editor/InjectForm/index.tsx create mode 100644 frontend/src/editor/Injects/Inject.tsx create mode 100644 frontend/src/editor/Injects/index.tsx create mode 100644 frontend/src/editor/utils.tsx diff --git a/frontend/src/editor/InjectForm/index.tsx b/frontend/src/editor/InjectForm/index.tsx new file mode 100644 index 000000000..fec430db0 --- /dev/null +++ b/frontend/src/editor/InjectForm/index.tsx @@ -0,0 +1,149 @@ +import type { ButtonProps } from '@blueprintjs/core' +import { + Button, + Dialog, + DialogBody, + DialogFooter, + HTMLSelect, + InputGroup, + Label, + TextArea, +} from '@blueprintjs/core' +import { useNotifyContext } from '@inject/shared/notification/contexts/NotifyContext' +import type { FC } from 'react' +import { memo, useCallback, useEffect, useState } from 'react' +import { addInjectInfo, updateInjectInfo } from '../indexeddb/operations' +import type { InjectInfo } from '../indexeddb/types' +import { InjectType } from '../indexeddb/types' +import { INJECT_TYPES } from '../utils' + +interface InjectFormProps { + inject?: InjectInfo + buttonProps: ButtonProps +} + +const InjectForm: FC<InjectFormProps> = ({ inject, buttonProps }) => { + const [isOpen, setIsOpen] = useState(false) + const [name, setName] = useState<string>('') + const [description, setDescription] = useState<string>('') + const [type, setType] = useState<InjectType>(InjectType.INFORMATION) + + const { notify } = useNotifyContext() + + const clearInput = useCallback(() => { + setName('') + setDescription('') + }, []) + + const handleAddButton = useCallback( + async (inject: Omit<InjectInfo, 'id'>) => { + try { + await addInjectInfo(inject) + clearInput() + setIsOpen(false) + } catch (err) { + notify(`Failed to add inject '${inject.name}': ${err}`, { + intent: 'danger', + }) + } + }, + [notify] + ) + + const handleUpdateButton = useCallback( + async (inject: InjectInfo) => { + try { + await updateInjectInfo(inject) + setIsOpen(false) + } catch (err) { + notify(`Failed to update inject '${inject.name}': ${err}`, { + intent: 'danger', + }) + } + }, + [notify] + ) + + useEffect(() => { + setName(inject?.name || '') + setDescription(inject?.description || '') + setType(inject?.type || InjectType.INFORMATION) + }, [inject]) + + return ( + <> + <Button {...buttonProps} onClick={() => setIsOpen(true)} /> + <Dialog + isOpen={isOpen} + onClose={() => setIsOpen(false)} + icon={inject ? 'edit' : 'plus'} + title={inject ? 'Edit inject' : 'New inject'} + > + <DialogBody> + <Label style={{ width: '100%' }}> + Title + <InputGroup + placeholder='Input text' + value={name} + onChange={e => setName(e.target.value)} + /> + </Label> + <Label style={{ width: '100%' }}> + What is this inject about? (optional) + <TextArea + value={description} + style={{ + width: '100%', + height: '5rem', + resize: 'none', + overflowY: 'auto', + }} + placeholder='Input text' + onChange={e => setDescription(e.target.value)} + /> + </Label> + <Label style={{ width: '100%' }}> + Channel + <HTMLSelect + options={INJECT_TYPES} + value={type} + onChange={event => + setType(event.currentTarget.value as InjectType) + } + /> + </Label> + </DialogBody> + <DialogFooter + actions={ + inject ? ( + <Button + disabled={!name} + onClick={() => + handleUpdateButton({ + id: inject.id, + name, + description, + type, + }) + } + intent='primary' + icon='edit' + text='Save changes' + /> + ) : ( + <Button + disabled={!name} + onClick={() => handleAddButton({ name, description, type })} + intent='primary' + icon='plus' + text='Add' + /> + ) + } + /> + </Dialog> + </> + ) +} + +export default memo(InjectForm) diff --git a/frontend/src/editor/Injects/Inject.tsx b/frontend/src/editor/Injects/Inject.tsx new file mode 100644 index 000000000..9b3e50a58 --- /dev/null +++ b/frontend/src/editor/Injects/Inject.tsx @@ -0,0 +1,58 @@ +import { Button, ButtonGroup, Card, Icon } from '@blueprintjs/core' +import { useNotifyContext } from '@inject/shared/notification/contexts/NotifyContext' +import type { FC } from 'react' +import { memo, useCallback } from 'react' +import InjectForm from '../InjectForm' +import { deleteInjectInfo } from '../indexeddb/operations' +import type { InjectInfo } from '../indexeddb/types' +import { InjectType } from '../indexeddb/types' + +interface InjectItemProps { + inject: InjectInfo +} + +const InjectItem: FC<InjectItemProps> = ({ inject }) => { + const { notify } = useNotifyContext() + + const handleDeleteButton = useCallback( + async (inject: InjectInfo) => { + try { + await deleteInjectInfo(inject.id) + } catch (err) { + notify(`Failed to delete inject '${inject.name}': ${err}`, { + intent: 'danger', + }) + } + }, + [notify] + ) + + return ( + <Card style={{ display: 'flex', justifyContent: 'space-between' }}> + <span style={{ height: '100%', flexGrow: 1 }}> + <Icon + icon={inject.type === InjectType.EMAIL ? 'envelope' : 'clipboard'} + style={{ marginRight: '1rem' }} + /> + {inject.name} + </span> + <ButtonGroup> + <InjectForm + inject={inject} + buttonProps={{ + minimal: true, + icon: 'edit', + style: { marginRight: '1rem' }, + }} + /> + <Button + minimal + icon='cross' + onClick={() => handleDeleteButton(inject)} + /> + </ButtonGroup> + </Card> + ) +} + +export default memo(InjectItem) diff --git a/frontend/src/editor/Injects/index.tsx b/frontend/src/editor/Injects/index.tsx new file mode 100644 index 000000000..dac5dd0fa --- /dev/null +++ b/frontend/src/editor/Injects/index.tsx @@ -0,0 +1,20 @@ +import Inject from '@/editor/Injects/Inject' +import { CardList } from '@blueprintjs/core' +import { useLiveQuery } from 'dexie-react-hooks' +import { memo } from 'react' +import { db } from '../indexeddb/db' +import type { InjectInfo } from '../indexeddb/types' + +const Injects = () => { + const injects = useLiveQuery(() => db.injectInfos.toArray(), [], []) + + return ( + <CardList> + {injects?.map((inject: InjectInfo) => ( + <Inject key={inject.id} inject={inject} /> + ))} + </CardList> + ) +} + +export default memo(Injects) diff --git a/frontend/src/editor/indexeddb/db.tsx b/frontend/src/editor/indexeddb/db.tsx index 6a0c3973f..be0cb235b 100644 --- a/frontend/src/editor/indexeddb/db.tsx +++ b/frontend/src/editor/indexeddb/db.tsx @@ -1,15 +1,17 @@ import Dexie, { type EntityTable } from 'dexie' -import type { LearningObjectiveInfo } from './types' +import type { InjectInfo, LearningObjectiveInfo } from './types' const dbName = 'EditorDatabase' const dbVersion = 1 const db = new Dexie(dbName) as Dexie & { learningObjectives: EntityTable<LearningObjectiveInfo, 'id'> + injectInfos: EntityTable<InjectInfo, 'id'> } db.version(dbVersion).stores({ learningObjectives: '++id, &name', + injectInfos: '++id, &name, description, type', }) export { db } diff --git a/frontend/src/editor/indexeddb/operations.tsx b/frontend/src/editor/indexeddb/operations.tsx index 8ee3e1e19..4ac05600c 100644 --- a/frontend/src/editor/indexeddb/operations.tsx +++ b/frontend/src/editor/indexeddb/operations.tsx @@ -1,5 +1,5 @@ import { db } from './db' -import type { LearningObjectiveInfo } from './types' +import type { InjectInfo, LearningObjectiveInfo } from './types' // learning objectives operations export const addLearningObjective = async ( @@ -11,3 +11,15 @@ export const addLearningObjective = async ( export const deleteLearningObjective = async (id: string) => await db.learningObjectives.delete(id) + +// inject info operations +export const addInjectInfo = async (injectInfo: Omit<InjectInfo, 'id'>) => + await db.transaction('rw', db.injectInfos, async () => { + await db.injectInfos.add(injectInfo) + }) + +export const updateInjectInfo = async (injectInfo: InjectInfo) => + await db.injectInfos.put(injectInfo) + +export const deleteInjectInfo = async (id: number) => + await db.injectInfos.delete(id) diff --git a/frontend/src/editor/indexeddb/types.tsx b/frontend/src/editor/indexeddb/types.tsx index abc5b0479..e5a58b70f 100644 --- a/frontend/src/editor/indexeddb/types.tsx +++ b/frontend/src/editor/indexeddb/types.tsx @@ -1,3 +1,15 @@ import type { LearningObjective } from '@inject/graphql/fragments/LearningObjective.generated' export type LearningObjectiveInfo = Pick<LearningObjective, 'id' | 'name'> + +export enum InjectType { + INFORMATION = 'Information', + EMAIL = 'Email', +} + +export type InjectInfo = { + id: number + name: string + description: string + type: InjectType +} diff --git a/frontend/src/editor/utils.tsx b/frontend/src/editor/utils.tsx new file mode 100644 index 000000000..8c9629c81 --- /dev/null +++ b/frontend/src/editor/utils.tsx @@ -0,0 +1,3 @@ +import { InjectType } from './indexeddb/types' + +export const INJECT_TYPES = Object.values(InjectType) diff --git a/frontend/src/pages/editor/create/injects.tsx b/frontend/src/pages/editor/create/injects.tsx index e69de29bb..f122c1885 100644 --- a/frontend/src/pages/editor/create/injects.tsx +++ b/frontend/src/pages/editor/create/injects.tsx @@ -0,0 +1,60 @@ +import InjectForm from '@/editor/InjectForm' +import Injects from '@/editor/Injects' +import { useNavigate } from '@/router' +import { Button } from '@blueprintjs/core' +import { css } from '@emotion/css' +import { memo } from 'react' + +const injectsPage = css` + display: grid; + grid-template-rows: auto auto 1fr auto; + height: 100vh; +` + +const InjectsPage = () => { + const nav = useNavigate() + + return ( + <div className={injectsPage}> + <h1>Define injects</h1> + <p style={{ marginBottom: '1rem' }}>Description.</p> + <div style={{ overflowY: 'auto' }}> + <Injects /> + <InjectForm + buttonProps={{ + minimal: true, + text: 'Add new inject', + alignText: 'left', + icon: 'plus', + style: { padding: '1rem', width: '100%' }, + }} + /> + </div> + <div + style={{ + display: 'flex', + justifyContent: 'center', + padding: '0.5rem 0', + }} + > + <Button + type='button' + onClick={() => nav('/editor/create/learning-objectives')} + text='Back' + icon='arrow-left' + style={{ + marginRight: '0.5rem', + }} + /> + <Button + type='button' + text='Continue' + intent='primary' + rightIcon='arrow-right' + /> + </div> + </div> + ) +} + +export default memo(InjectsPage) -- GitLab