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

Merge branch '647-refact' into 'main'

Resolve "Refactor inject specification (editor) without the changed state"

Closes #647

See merge request inject/frontend!590
parents de8ab292 eb5f3eef
Loading
Loading
Loading
Loading
+1 −2
Original line number Diff line number Diff line
@@ -64,7 +64,6 @@ const DefinitionImportDialog: FC<DefinitionImportDialogProps> = ({
        intent: 'danger',
      })
      await clearDb()
      clearDb()
      setConfig({})
      setGitlabConfig(prev => ({
        ...prev,
@@ -72,7 +71,7 @@ const DefinitionImportDialog: FC<DefinitionImportDialogProps> = ({
        branchFrom: undefined,
      }))
    }
  }, [onAdd])
  }, [nav, onAdd, setConfig, setGitlabConfig])

  return (
    <>
+5 −0
Original line number Diff line number Diff line
@@ -3,6 +3,11 @@ import { useMemo } from 'react'
import { db } from '../../indexeddb/db'
import { validateExpression } from '../../utils'

export const validateExpressionStatic = async (expression?: number[]) => {
  const milestones = await db.milestones.toArray()
  return validateExpression(expression || [], milestones)
}

const useValidateExpression = (expression?: number[]) => {
  const milestones = useLiveQuery(() => db.milestones.toArray(), [], [])

+76 −80
Original line number Diff line number Diff line
import { NumericInput } from '@blueprintjs/core'
import { notify } from '@inject/shared'
import { ShallowGet, ShallowGetSet, useShallowStore } from '@inject/shared'
import { useLiveQuery } from 'dexie-react-hooks'
import { memo, useCallback, useEffect, useState, type FC } from 'react'
import { memo, useCallback, useEffect, type FC } from 'react'
import { INJECT_CONNECTIONS_FORM } from '../../assets/pageContent/injectSpecification'
import {
  addInjectControl,
  getInjectControlByInjectInfoId,
  updateInjectControl,
} from '../../indexeddb/operations'
import { getInjectControlByInjectInfoId } from '../../indexeddb/operations'
import type { InjectControl } from '../../indexeddb/types'
import { InjectType } from '../../indexeddb/types'
import { InjectsSpecificationRoute } from '../../routes/create/inject-specification.index'
@@ -15,19 +11,24 @@ import ExpressionBuilder from '../ExpressionBuilder'
import useValidateExpression from '../ExpressionBuilder/useValidateExpression'
import SaveButtonGroup from '../SaveButtonGroup'
import TooltipLabel from '../Tooltips/TooltipLabel'
import type { InjectSpecState } from './store'

interface ConnectionsFormProps {
  injectInfoId: number
  injectType: InjectType
  changed: boolean
  onChangedChange: (value: boolean) => void
export interface ConnectionsInjectData {
  start: number
  delay: number
  milestoneCondition: number[]
}

const ConnectionsForm: FC<ConnectionsFormProps> = ({
type ConnectionsFormProps<T extends InjectType = InjectType> = FC<{
  injectInfoId: number
  injectType: T
  state: InjectSpecState<T>
}>

const ConnectionsForm: ConnectionsFormProps = ({
  injectInfoId,
  injectType,
  changed,
  onChangedChange,
  state,
}) => {
  const injectControl = useLiveQuery(
    () => getInjectControlByInjectInfoId(injectInfoId),
@@ -35,83 +36,78 @@ const ConnectionsForm: FC<ConnectionsFormProps> = ({
    null
  ) as InjectControl

  const [start, setStart] = useState<number>(0)
  const [delay, setDelay] = useState<number>(0)
  const [milestoneCondition, setMilestoneCondition] = useState<number[]>([])
  const { isValid } = useValidateExpression(milestoneCondition)

  const milestoneConditions = useShallowStore(
    state,
    state => state.milestoneCondition
  )
  const { isValid } = useValidateExpression(milestoneConditions)
  useEffect(() => {
    setStart(injectControl?.start || 0)
    // eslint-disable-next-line @typescript-eslint/no-unused-expressions
    injectType === InjectType.QUESTIONNAIRE
      ? setDelay(0)
      : setDelay(injectControl?.delay || 0)
    setMilestoneCondition(injectControl?.milestoneCondition || [])
  }, [injectControl, injectType])

  const update = useCallback(
    async (newInjectControl: InjectControl) => {
      try {
    if (injectControl) {
          await updateInjectControl({
            ...newInjectControl,
          })
        } else {
          await addInjectControl(newInjectControl)
        }
      } catch (error) {
        notify(
          `Failed to update inject control: ${error}`,
          JSON.stringify(error),
          {
            intent: 'danger',
      state.setState({
        start: injectControl.start,
        milestoneCondition: injectControl.milestoneCondition,
        ...(injectType === InjectType.QUESTIONNAIRE
          ? {
              delay: 0,
            }
        )
          : {
              delay: injectControl.delay,
            }),
      })
    }
    },
    [injectControl]
  )
  }, [injectControl, injectType, state])

  const handleUpdate = useCallback(async () => {
    if (!changed) onChangedChange(true)
    await update({
      injectInfoId,
      start,
      delay,
      milestoneCondition: isValid ? milestoneCondition : [],
    })
    onChangedChange(false)
  }, [changed, injectInfoId, start, delay, milestoneCondition, isValid, update])

  useEffect(() => {
    if (changed) handleUpdate()
  }, [changed, handleUpdate])
    state.getState().update(injectInfoId)
  }, [injectInfoId, state])

  return (
    <div>
      <TooltipLabel label={INJECT_CONNECTIONS_FORM.time}>
        <ShallowGetSet
          store={state}
          get={state => state.start}
          set={state => state.set}
        >
          {(value, setValue) => (
            <NumericInput
              placeholder='Input number'
              min={0}
          value={start}
          onValueChange={(value: number) => setStart(value)}
              value={value}
              onValueChange={(value: number) => setValue('start', value)}
            />
          )}
        </ShallowGetSet>
      </TooltipLabel>
      {injectType !== InjectType.QUESTIONNAIRE && (
        <TooltipLabel label={INJECT_CONNECTIONS_FORM.delay}>
          <ShallowGetSet
            store={state}
            get={state => state.delay}
            set={state => state.set}
          >
            {(value, setValue) => (
              <NumericInput
                placeholder='Input number'
                min={0}
            value={delay}
            onValueChange={(value: number) => setDelay(value)}
                value={value}
                onValueChange={(value: number) => setValue('delay', value)}
              />
            )}
          </ShallowGetSet>
        </TooltipLabel>
      )}
      <ShallowGet store={state} get={state => state.set}>
        {setValue => (
          <ExpressionBuilder
            label={INJECT_CONNECTIONS_FORM.condition}
            initExpression={injectControl?.milestoneCondition}
        onExpressionChange={expression => setMilestoneCondition(expression)}
            onExpressionChange={expression =>
              setValue('milestoneCondition', expression)
            }
          />
        )}
      </ShallowGet>
      <SaveButtonGroup
        isValid={isValid}
        handleUpdate={() => handleUpdate()}
+117 −117
Original line number Diff line number Diff line
import { InputGroup, NumericInput, TextArea } from '@blueprintjs/core'
import { notify } from '@inject/shared'
import { ShallowGetSet } from '@inject/shared'
import { useLiveQuery } from 'dexie-react-hooks'
import { memo, useCallback, useEffect, useState, type FC } from 'react'
import { useCallback, useEffect, type FC } from 'react'
import { EMAIL_ADDRESS_FORM } from '../../assets/pageContent/emails'
import { EMAIL_INJECT_FORM } from '../../assets/pageContent/injectSpecification'
import {
  addEmailInject,
  getEmailInjectByInjectInfoId,
  updateEmailInject,
} from '../../indexeddb/operations'
import type { EmailInject } from '../../indexeddb/types'
import { getEmailInjectByInjectInfoId } from '../../indexeddb/operations'
import type { EmailInject, InjectType } from '../../indexeddb/types'
import { InjectsSpecificationRoute } from '../../routes/create/inject-specification.index'
import { SelectChangeShared } from '../../utils'
import EmailAddressSelector from '../EmailAddressSelector'
import FileSelector from '../FileSelector'
import SaveButtonGroup from '../SaveButtonGroup'
import TooltipLabel from '../Tooltips/TooltipLabel'
import type { InjectSpecState } from './store'

interface EmailInjectFormProps {
  injectInfoId: number
  changed: boolean
  onChangedChange: (value: boolean) => void
export interface EmailInjectData {
  subject: string
  content: string
  selectedAddressId: number
  extraCopies: number
  fileId: number
}

const EmailInjectForm: FC<EmailInjectFormProps> = ({
  injectInfoId,
  changed,
  onChangedChange,
}) => {
const EmailInjectForm: FC<{
  injectInfoId: number
  state: InjectSpecState<InjectType.EMAIL>
}> = ({ injectInfoId, state }) => {
  const emailInject = useLiveQuery(
    () => getEmailInjectByInjectInfoId(injectInfoId),
    [injectInfoId],
    null
  ) as EmailInject

  const [subject, setSubject] = useState<string>('')
  const [content, setContent] = useState<string>('')
  const [selectedAddressId, setSelectedAddressId] = useState<number>(0)
  const [extraCopies, setExtraCopies] = useState<number>(0)
  const [fileId, setFileId] = useState<number>(0)

  useEffect(() => {
    setSubject(emailInject?.subject || '')
    setContent(emailInject?.content || '')
    setSelectedAddressId(emailInject?.emailAddressId || 0)
    setExtraCopies(emailInject?.extraCopies || 0)
    setFileId(emailInject?.fileId || 0)
  }, [emailInject])

  const updateInject = useCallback(
    async (newEmailInject: EmailInject) => {
      try {
    if (emailInject) {
          await updateEmailInject({ ...newEmailInject })
        } else {
          await addEmailInject(newEmailInject)
        }
      } catch (error) {
        notify(
          `Failed to update email inject: ${error}`,
          JSON.stringify(error),
          {
            intent: 'danger',
          }
        )
      }
    },
    [emailInject]
  )

  const handleUpdate = useCallback(async () => {
    if (!changed) onChangedChange(true)
    await updateInject({
      injectInfoId,
      subject,
      content,
      emailAddressId: selectedAddressId,
      extraCopies,
      fileId,
      state.setState({
        subject: emailInject.subject,
        content: emailInject.content,
        selectedAddressId: emailInject.emailAddressId,
        extraCopies: emailInject.extraCopies,
        fileId: emailInject.fileId,
      })
    onChangedChange(false)
  }, [
    changed,
    injectInfoId,
    subject,
    content,
    selectedAddressId,
    extraCopies,
    fileId,
    updateInject,
  ])
    }
  }, [emailInject, state])

  useEffect(() => {
    if (changed) handleUpdate()
  }, [changed, handleUpdate])
  const handleUpdate = useCallback(() => {
    state.getState().update(injectInfoId)
  }, [state, injectInfoId])

  return (
    <div>
      <ShallowGetSet
        store={state}
        get={state => state.selectedAddressId}
        set={state => state.set}
      >
        {(value, setValue) => (
          <EmailAddressSelector
            label={EMAIL_ADDRESS_FORM.address}
        emailAddressId={selectedAddressId}
        onChange={id =>
          SelectChangeShared({ setValue: setSelectedAddressId, value: id })
            emailAddressId={value}
            onChange={id => {
              if (value === id) {
                setValue('selectedAddressId', 0)
              }
              setValue('selectedAddressId', id)
            }}
          />
        )}
      </ShallowGetSet>
      <ShallowGetSet
        store={state}
        get={state => state.subject}
        set={state => state.set}
      >
        {(value, setValue) => (
          <TooltipLabel label={EMAIL_INJECT_FORM.subject}>
            <InputGroup
              placeholder='Input text'
          value={subject}
          onChange={e => setSubject(e.target.value)}
              value={value}
              onChange={e => setValue('subject', e.target.value)}
            />
          </TooltipLabel>
        )}
      </ShallowGetSet>
      <ShallowGetSet
        store={state}
        get={state => state.content}
        set={state => state.set}
      >
        {(value, setValue) => (
          <TooltipLabel label={EMAIL_INJECT_FORM.content}>
            <TextArea
          value={content}
              value={value}
              style={{
                width: '100%',
                height: '10rem',
@@ -121,22 +98,45 @@ const EmailInjectForm: FC<EmailInjectFormProps> = ({
                overflowY: 'auto',
              }}
              placeholder='Input text'
          onChange={e => setContent(e.target.value)}
              onChange={e => setValue('content', e.target.value)}
            />
          </TooltipLabel>
        )}
      </ShallowGetSet>
      <ShallowGetSet
        store={state}
        get={state => state.extraCopies}
        set={state => state.set}
      >
        {(value, setValue) => (
          <TooltipLabel label={EMAIL_INJECT_FORM.extraCopies}>
            <NumericInput
              placeholder='Input number'
              min={0}
          value={extraCopies}
          onValueChange={(value: number) => setExtraCopies(value)}
              value={value}
              onValueChange={(value: number) => setValue('extraCopies', value)}
            />
          </TooltipLabel>
        )}
      </ShallowGetSet>
      <ShallowGetSet
        store={state}
        get={state => state.fileId}
        set={state => state.set}
      >
        {(value, setValue) => (
          <FileSelector
            label={EMAIL_INJECT_FORM.file}
        fileId={fileId}
        onChange={id => SelectChangeShared({ setValue: setFileId, value: id })}
            fileId={value}
            onChange={id => {
              if (value === id) {
                setValue('fileId', 0)
              }
              setValue('fileId', id)
            }}
          />
        )}
      </ShallowGetSet>
      <SaveButtonGroup
        isValid
        handleUpdate={() => handleUpdate()}
@@ -146,4 +146,4 @@ const EmailInjectForm: FC<EmailInjectFormProps> = ({
  )
}

export default memo(EmailInjectForm)
export default EmailInjectForm
+62 −80
Original line number Diff line number Diff line
import { TextArea } from '@blueprintjs/core'
import { notify } from '@inject/shared'
import { ShallowGetSet } from '@inject/shared'
import { useLiveQuery } from 'dexie-react-hooks'
import { memo, useCallback, useEffect, useState, type FC } from 'react'
import { useCallback, useEffect, type FC } from 'react'
import { INFORMATION_INJECT_FORM } from '../../assets/pageContent/injectSpecification'
import {
  addInformationInject,
  getInformationInjectByInjectInfoId,
  updateInformationInject,
} from '../../indexeddb/operations'
import type { InformationInject } from '../../indexeddb/types'
import { getInformationInjectByInjectInfoId } from '../../indexeddb/operations'
import type { InjectType } from '../../indexeddb/types'
import { InjectsSpecificationRoute } from '../../routes/create/inject-specification.index'
import { SelectChangeShared } from '../../utils'
import FileSelector from '../FileSelector'
import SaveButtonGroup from '../SaveButtonGroup'
import TooltipLabel from '../Tooltips/TooltipLabel'
import type { InjectSpecState } from './store'

interface InformationInjectFormProps {
  injectInfoId: number
  changed: boolean
  onChangedChange: (value: boolean) => void
export interface InformationInjectData {
  content: string
  fileId: number
}

const InformationInjectForm: FC<InformationInjectFormProps> = ({
  injectInfoId,
  changed,
  onChangedChange,
}) => {
const InformationInjectForm: FC<{
  injectInfoId: number
  state: InjectSpecState<InjectType.INFORMATION>
}> = ({ injectInfoId, state }) => {
  const informationInject = useLiveQuery(
    () => getInformationInjectByInjectInfoId(injectInfoId),
    [injectInfoId],
    null
  ) as InformationInject

  const [content, setContent] = useState<string>('')
  const [fileId, setFileId] = useState<number>(0)
    [injectInfoId]
  )

  useEffect(() => {
    setContent(informationInject?.content || '')
    setFileId(informationInject?.fileId || 0)
  }, [informationInject])

  const updateInject = useCallback(
    async (newInformationInject: InformationInject) => {
      try {
    if (informationInject) {
          await updateInformationInject({
            ...newInformationInject,
      state.setState({
        content: informationInject?.content,
        fileId: informationInject?.fileId,
      })
        } else {
          await addInformationInject(newInformationInject)
        }
      } catch (error) {
        notify(
          `Failed to update information inject: ${error}`,
          JSON.stringify(error),
          {
            intent: 'danger',
          }
        )
    }
    },
    [informationInject]
  )
  }, [informationInject, state])

  const handleUpdate = useCallback(async () => {
    if (!changed) onChangedChange(true)
    await updateInject({
      injectInfoId,
      content,
      fileId,
    })
    onChangedChange(false)
  }, [changed, injectInfoId, content, fileId, updateInject])

  useEffect(() => {
    if (changed) handleUpdate()
  }, [changed, handleUpdate])
  const handleUpdate = useCallback(() => {
    state.getState().update(injectInfoId)
  }, [state, injectInfoId])

  return (
    <div>
      <TooltipLabel label={INFORMATION_INJECT_FORM.content}>
        <ShallowGetSet
          store={state}
          get={state => state.content}
          set={state => state.set}
        >
          {(value, setValue) => (
            <TextArea
          value={content}
              value={value}
              style={{
                width: '100%',
                height: '10rem',
@@ -89,14 +56,29 @@ const InformationInjectForm: FC<InformationInjectFormProps> = ({
                overflowY: 'auto',
              }}
              placeholder='Input text'
          onChange={e => setContent(e.target.value)}
              onChange={e => setValue('content', e.target.value)}
            />
          )}
        </ShallowGetSet>
      </TooltipLabel>
      <ShallowGetSet
        store={state}
        get={state => state.fileId}
        set={state => state.set}
      >
        {(value, setValue) => (
          <FileSelector
            label={INFORMATION_INJECT_FORM.file}
        fileId={fileId}
        onChange={id => SelectChangeShared({ setValue: setFileId, value: id })}
            fileId={value}
            onChange={id => {
              if (value === id) {
                setValue('fileId', 0)
              }
              setValue('fileId', id)
            }}
          />
        )}
      </ShallowGetSet>
      <SaveButtonGroup
        isValid
        handleUpdate={() => handleUpdate()}
@@ -106,4 +88,4 @@ const InformationInjectForm: FC<InformationInjectFormProps> = ({
  )
}

export default memo(InformationInjectForm)
export default InformationInjectForm
Loading