All Downloads are FREE. Search and download functionalities are using the official Maven repository.

hooks.process.ts Maven / Gradle / Ivy

There is a newer version: 0.80.3
Show newest version
import _ from 'lodash';
import { toPathsObject, translateDefinitions } from 'components/process/utils/process';
import Report from 'helpers/report';
import Settings from 'helpers/settings';
import Translator, { T } from 'helpers/translator';
import { Notifier, useNotifier } from 'hooks/notification';
import { SubmitHandler, useSubmitHandler } from 'hooks/submit';
import { useGearsTranslation } from 'hooks/translation';
import {
    GET_MY_PROCESS_INSTANCES, GET_PROCESS_DEFINITIONS, GET_PROCESS_DEFINITION_BY_KEY, START_PROCESS, STOP_PROCESS
} from 'queries/process';
import { CLAIM_TASK } from 'queries/task';
import { useEffect, useState } from 'react';
import { NavigateFunction, useNavigate } from 'react-router';
import { LinkKind, ProcessDefinition, Task } from 'types/graphql';
import { bannerQueries } from 'utils/banner-utils';
import { reformQuery, removerById } from 'utils/utils';

import { ApolloClient, useApolloClient, useMutation, useQuery } from '@apollo/client';

import { useLocale } from './locale';
import { useTranslator } from './translator';

export type ProcessRendering  = "standard" | "tiles" | "table";
export type PathObject     = { [key: string]: T | PathObject; };
export type PathOrder         = { [key: string]: number; };
export type PathEntry      = [string, T];
export type ProcessPathObject = PathObject;

export type ProcessGroupBy = "category" | "subcategory" | "none";

export interface TranslatedProcessDefinition extends ProcessDefinition { title: string }

export type ProcessInfo = {
  processes: TranslatedProcessDefinition[];
  paths: ProcessPathObject;
  order: PathOrder;
};

export type FormValues           = Promise
export type StartProcessFunction = (e: MouseEvent | null, processDefinition: Partial, kind: LinkKind, values?: FormValues) => void

type GraphqlMutationVariables = {
  variables: object
}

interface StartProcessOptions extends StartLinkProcessOptions,OpenFormOptions,OpenLinkFormOptions,GoToOptions,ClaimOptions {}

interface StartLinkProcessOptions extends ClaimOptions {
  doStartProcess: (variables: GraphqlMutationVariables) => Promise
}

interface ClaimLinkOptions extends ClaimOptions,OpenLinkFormOptions {}

interface OpenLinkFormOptions extends OpenFormOptions {
  handleSubmit:   SubmitHandler
}

interface ClaimOptions extends OpenFormOptions {
  doClaimTask:  (variables: GraphqlMutationVariables) => Promise
  translator: T
}

type OpenFormOptions = {
  register?:   boolean
  navigate :   NavigateFunction
  notifier :   Notifier
  translator : Translator
}

type GoToOptions = {
  navigate: NavigateFunction
}

export const useTranslatedProcesses = () => {
  const processDefinitionsResult = useQuery(GET_PROCESS_DEFINITIONS)
  const processInfo              = useProcessTranslator(processDefinitionsResult.data?.processDefinitions)

  return {result: processDefinitionsResult, loading: processDefinitionsResult.loading || Boolean(!processInfo), processInfo}
}

export const useProcessTranslator = (processes?: ProcessDefinition[]): ProcessInfo | undefined => {
  const { language }                  = useLocale()
  const { translator }                = useTranslator()
  const [processInfo, setProcessInfo] = useState()

  useEffect(() => {
    if (processes) {
      const sortedProcesses = _.sortBy(processes, def => def.key)
      const translatedProcesses       = translateDefinitions(translator, language, sortedProcesses)
      const { pathObject, pathOrder } = toPathsObject(translatedProcesses)

      setProcessInfo({
        processes: translatedProcesses,
        paths: pathObject,
        order: pathOrder
      })
    }
  }, [processes])

  return processInfo
}

// a hook that helps to start a process and go to the initial form
const useStartProcess = (): StartProcessFunction => {
  const navigate         = useNavigate()
  const [doStartProcess] = useMutation(START_PROCESS, {refetchQueries: bannerQueries})
  const [doClaimTask]    = useMutation(CLAIM_TASK,    {refetchQueries: bannerQueries})
  const { translator }   = useGearsTranslation()
  const {handleSubmit}   = useSubmitHandler()
  const notifier         = useNotifier()
  const client           = useApolloClient()
  const options          = {notifier, navigate, translator, handleSubmit, doStartProcess, doClaimTask}

  return (e: MouseEvent | null, partialProcessDefinition: Partial, kind: LinkKind, values?: FormValues) => {
    e?.preventDefault()
    e?.stopPropagation()

    console.log("startProcess: partialProcessDefinition=%o", partialProcessDefinition)
  
    const eventualProcessDefinition = partialProcessDefinition.hasStartForm !== undefined
      ? Promise.resolve(partialProcessDefinition) 
      : resolveProcessDefinition(client, partialProcessDefinition.key || 'unknown');

    eventualProcessDefinition
      .then(pd => {
        if (pd.hasStartForm)
          openLinkForm({register: true, ...options}, e, true, pd.key!, pd.id!, kind, values)
        else
          startProcessAndOpenForm({register: true, ...options}, e, pd.key!, kind, values)
      })
      .catch(e => notifier.error("Unable to start process: " + e))
  }
}

function resolveProcessDefinition(client: ApolloClient, key: string): Promise {
  return client.query({ query: GET_PROCESS_DEFINITION_BY_KEY, variables: { key } })
    .then(res => res.data.processDefinitionByKey)
}

export const useStopProcess = (deleteLine?: (id: any) => {}): (id: any) => Promise => {
  const notifier = useNotifier()
  const [doStopProcess] = useMutation(STOP_PROCESS, {
    // update the cache
    update: (cache, { data: { stopProcess: id } }) => {
      reformQuery(cache, GET_MY_PROCESS_INSTANCES, { instances: removerById(id) });
    },
    refetchQueries: bannerQueries
  })

  const stopProcess = (id: number): Promise => {
    return doStopProcess({ variables: { id }})
      .then(result => {
        deleteLine?.(id)
        notifier.success("Stopped process successfully")
      }, error => {
        notifier.error("Could not stop process: " + error)
        console.error("stopping processes has failed: %o", error)
      })
      .catch(reason => {
        notifier.error("Could not stop process")
        console.error("the frontend has an issue with stopping the process: " + reason)
      })
  }

  return stopProcess
}

// simply open a (start) form 
function openLinkForm(options: OpenLinkFormOptions, e: MouseEvent | null, isStartForm: boolean, key: string, id: string, kind: LinkKind, values?: FormValues) {
  const { handleSubmit } = options

  console.log('openForm: id=%o, key=%o, kind=%s, isStartform=%s', id, key, kind, isStartForm)
  if (options.register)
    setProcessOrigin()

  switch (kind) {
    case "PROCESS_SUBMIT": 
      if (values)
        values.then(values => handleSubmit(values, {} as any, { id, isStartForm}))
      else {
        window.alert("submit to a startform (without values)")
      }
      break

    case "PROCESS_FILL":
      // values are stored in FORM.VALUES (local storage)

    default:
      openForm(options, isStartForm, e, key, id )
  }
}

// first start a process, then claim the only task en open the form. Usefull if a process has no start form
function startProcessAndOpenForm (options: StartProcessOptions, e: MouseEvent | null, processKey: string, kind: LinkKind, values?: FormValues) {
  const open = !e?.ctrlKey
  const {translator, doStartProcess, notifier} = options

  // start the process, claim the only task, and go to the form
  doStartProcess({ variables: { key: processKey } })
    .then(result => {
      const { startProcessByKey: { id: instanceId, tasks } } = result.data
      console.log("startProcess: key=%o, open=%o, instanceId=%o, tasks=%o kind=%o", processKey, open, instanceId, tasks, kind)

      if (!open) {
        notifier.info(`Process ${translator.toProcessTitle(processKey)} has started`)
        return
      }

      if (tasks.length > 1) {
        notifier.info(`Process ${translator.toProcessTitle(processKey)} has create ${tasks.length} tasks`)
        console.log(`The ${translator.toProcessTitle(processKey)} form has been prevented from opening, because there are ${tasks.length} tasks to choose from`)
        return 
      }

      if (tasks.length == 0) {
        notifier.info(`Process ${translator.toProcessTitle(processKey)} has not resulted in tasks for you`)
        return 
      }

      return claimLinkTask(options, e, tasks[0], kind, values)
    })
    .catch(reason => {
      console.error(reason)
      notifier.error("Unable to start the process: " + translator.toProcessTitle(processKey))
    })
}

export function claimLinkTask(options: ClaimLinkOptions, e: MouseEvent | null, task: Partial, kind: LinkKind, values?: FormValues){
  const key = task.processDefinition?.key
  const id  = task.id

  return claim(options, e, task, result => openLinkForm(options, e, false, key || "?", id || "?", kind, values))
}

export function claimTask(options: ClaimOptions, e: MouseEvent | null, task: Partial ) {
  return claim(options, e, task, result => openTaskForm(options, e, task))
}

function claim(options: ClaimOptions, e: MouseEvent | null, task: Partial, success: (result: any) => void){
  const { notifier, doClaimTask, translator } = options

  return doClaimTask({ variables: { id: task.id } })
    .then(result => {
      console.log("claimTask: result=%o", result)
      return success(result)
    }, error => {
      const report = Report.from(error, translator, { category: Report.backend })
      report.addToNotifier(notifier)
      return Promise.reject(report.message)
    })
}

const processOrigin = "PROCESS_ORIGIN"

export function setProcessOrigin(origin?: string){
  const pathname = window.location.pathname;
  const hash     = window.location.hash;
  const path     = pathname == "/" ? hash.replace('^#', '') : pathname

  return Settings.session.write(processOrigin, origin || path)
}

export function getProcessOrigin() {
  return Settings.session.read(processOrigin, '/gears/processes/start').replace(/^#/, '')
}

export function goToProcessOrigin(navigate: NavigateFunction) { 
  const url = getProcessOrigin()
  if (url)
    navigate(url)
  else
    navigate(-1)
}

export function openTasksForm(options: OpenFormOptions, tasks: Partial[]) {
  const {navigate, notifier} = options
  switch (tasks.length) {
    case 0:
      goToProcessOrigin(navigate)
      break
    case 1:
      openTaskForm(options, null, tasks[0])
      break
    default:
      notifier.info("There are multiple tasks to choose from. Redirecting momentarily...") 
      setTimeout(() => { navigate('/gears/tasks/assigned') }, 3000)
      break
  }
}

export function openTaskForm(options: OpenFormOptions, e: MouseEvent | null, task: Partial) {
  const key = task?.processDefinition?.key
  const id  = task?.id
  openForm(options, false, e, key, id)
}

export function openForm(options: OpenFormOptions, isStartForm: boolean, e: MouseEvent | null, key: string | undefined, id: string | undefined) {
  const {translator, notifier, navigate} = options
  if (!key || !id) {
    notifier.error(`Could not go to form of ${key ? `${translator.toProcessTitle(key)} ` : `key ${key} `}with id ${id}`)
    return
  }
 
  const open = !e?.ctrlKey
  if (!open) {
    notifier.info(`Ctrl-click has prevented the ${translator.toProcessTitle(key)} ${isStartForm ? "start " : ""}form to open`)
    return
  }

  if (options.register)
    setProcessOrigin()

  if (isStartForm)
    goToUrl(options, e, `/gears/start/form/${key}/${id}`)
  else
    goToUrl(options, e, `/gears/tasks/${key}/${id}`)
}

function goToUrl({navigate}: GoToOptions, e: MouseEvent | null, url: String) { 
  if (e?.ctrlKey) {
    const uri = `http://${window.location.host}/#${url}`
    window.open(uri)
  } else {
    // @ts-ignore
    navigate(url)
  }
}

export default useStartProcess