diff --git a/src/context/ActionViewContext.tsx b/src/context/ActionViewContext.tsx index fe22c6a7..134ec5be 100644 --- a/src/context/ActionViewContext.tsx +++ b/src/context/ActionViewContext.tsx @@ -276,7 +276,7 @@ export const useActionViewContext = () => { searchTreeNameSearch: undefined, goToResourceId: async () => {}, limit: DEFAULT_SEARCH_LIMIT, - isActive: false, + isActive: undefined, formIsSaving: false, setFormIsSaving: () => {}, formHasChanges: false, diff --git a/src/hooks/useAutorefreshableFormFields.ts b/src/hooks/useAutorefreshableFormFields.ts new file mode 100644 index 00000000..62534037 --- /dev/null +++ b/src/hooks/useAutorefreshableFormFields.ts @@ -0,0 +1,122 @@ +import { ConnectionProvider } from ".."; +import { useNetworkRequest } from "./useNetworkRequest"; +import { useDeepCompareEffect } from "use-deep-compare"; +import { useRef, useState, useCallback, useEffect } from "react"; +import { useBrowserVisibility } from "./useBrowserVisibility"; + +const AUTOREFRESH_INTERVAL_SECONDS = 3 * 1000; + +export type UseAutorefreshableFormFieldsOpts = { + model: string; + id?: number; + context: any; + autorefreshableFields?: string[]; + fieldDefs: any; + onAutorefreshableFieldsChange: (newValues: any) => void; + isActive?: boolean; +}; + +export const useAutorefreshableFormFields = ( + opts: UseAutorefreshableFormFieldsOpts, +) => { + const { + model, + id, + context, + autorefreshableFields, + fieldDefs, + onAutorefreshableFieldsChange, + isActive, + } = opts; + + const intervalRef = useRef(null); + const [internalIsActive, setInternalIsActive] = useState(true); + + const [fetchRequest, cancelRequest] = useNetworkRequest( + ConnectionProvider.getHandler().readObjects, + ); + + const tabOrWindowIsVisible = useBrowserVisibility(); + + useEffect(() => { + if (isActive === false) { + pause(); + } + if ( + (isActive === undefined || isActive === true) && + !tabOrWindowIsVisible + ) { + pause(); + } + if ((isActive === undefined || isActive === true) && tabOrWindowIsVisible) { + resume(); + } + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [isActive, tabOrWindowIsVisible]); + + const refresh = useCallback(async () => { + if (!id || !autorefreshableFields?.length || !internalIsActive) return; + + try { + const [result] = await fetchRequest({ + model, + ids: [id], + fields: fieldDefs, + fieldsToRetrieve: autorefreshableFields, + context, + }); + onAutorefreshableFieldsChange(result); + } catch (err) { + console.error(err); + } + }, [ + id, + autorefreshableFields, + internalIsActive, + fetchRequest, + model, + fieldDefs, + context, + onAutorefreshableFieldsChange, + ]); + + useDeepCompareEffect(() => { + const shouldStart = id && autorefreshableFields?.length && internalIsActive; + + if (shouldStart) { + refresh(); + intervalRef.current = setInterval(refresh, AUTOREFRESH_INTERVAL_SECONDS); + } + + return () => { + cancelRequest(); + if (intervalRef.current) { + clearInterval(intervalRef.current); + intervalRef.current = null; + } + }; + }, [ + autorefreshableFields, + fetchRequest, + fieldDefs, + model, + id, + context, + internalIsActive, + ]); + + const pause = useCallback(() => { + setInternalIsActive(false); + if (intervalRef.current) { + clearInterval(intervalRef.current); + intervalRef.current = null; + } + cancelRequest(); + }, [cancelRequest]); + + const resume = useCallback(() => { + setInternalIsActive(true); + }, []); + + return { pause, resume }; +}; diff --git a/src/hooks/useAutorefreshableTreeFields.ts b/src/hooks/useAutorefreshableTreeFields.ts new file mode 100644 index 00000000..68267dae --- /dev/null +++ b/src/hooks/useAutorefreshableTreeFields.ts @@ -0,0 +1,123 @@ +import { ConnectionProvider } from ".."; +import { useNetworkRequest } from "./useNetworkRequest"; +import { useDeepCompareEffect } from "use-deep-compare"; +import { useRef, useState, useCallback, useEffect } from "react"; +import { InfiniteTableRef } from "@gisce/react-formiga-table"; +import { useBrowserVisibility } from "./useBrowserVisibility"; + +const AUTOREFRESH_INTERVAL_SECONDS = 3 * 1000; + +export type UseAutorefreshableTreeFieldsOpts = { + tableRef: React.RefObject; + model: string; + context: any; + autorefreshableFields?: string[]; + fieldDefs: any; + isActive?: boolean; +}; + +export const useAutorefreshableTreeFields = ( + opts: UseAutorefreshableTreeFieldsOpts, +) => { + const { + tableRef, + model, + context, + autorefreshableFields, + fieldDefs, + isActive, + } = opts; + + const intervalRef = useRef(null); + const [internalIsActive, setInternalIsActive] = useState(true); + + const [fetchRequest, cancelRequest] = useNetworkRequest( + ConnectionProvider.getHandler().readObjects, + ); + + const tabOrWindowIsVisible = useBrowserVisibility(); + + useEffect(() => { + if (isActive === false) { + pause(); + } + if ( + (isActive === undefined || isActive === true) && + !tabOrWindowIsVisible + ) { + pause(); + } + if ((isActive === undefined || isActive === true) && tabOrWindowIsVisible) { + resume(); + } + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [isActive, tabOrWindowIsVisible]); + + const refresh = useCallback(async () => { + if (!autorefreshableFields?.length || !internalIsActive) return; + + const ids = tableRef.current?.getVisibleRowIds(); + + if (!ids) return; + + try { + const results = await fetchRequest({ + model, + ids, + fields: fieldDefs, + fieldsToRetrieve: autorefreshableFields, + context, + }); + tableRef.current?.updateRows(results); + } catch (err) { + console.error(err); + } + }, [ + autorefreshableFields, + internalIsActive, + tableRef, + fetchRequest, + model, + fieldDefs, + context, + ]); + + useDeepCompareEffect(() => { + const shouldStart = autorefreshableFields?.length && internalIsActive; + + if (shouldStart) { + refresh(); + intervalRef.current = setInterval(refresh, AUTOREFRESH_INTERVAL_SECONDS); + } + + return () => { + cancelRequest(); + if (intervalRef.current) { + clearInterval(intervalRef.current); + intervalRef.current = null; + } + }; + }, [ + autorefreshableFields, + fetchRequest, + fieldDefs, + model, + context, + internalIsActive, + ]); + + const pause = useCallback(() => { + setInternalIsActive(false); + if (intervalRef.current) { + clearInterval(intervalRef.current); + intervalRef.current = null; + } + cancelRequest(); + }, [cancelRequest]); + + const resume = useCallback(() => { + setInternalIsActive(true); + }, []); + + return { pause, resume }; +}; diff --git a/src/hooks/useBrowserVisibility.ts b/src/hooks/useBrowserVisibility.ts new file mode 100644 index 00000000..bf53d3d9 --- /dev/null +++ b/src/hooks/useBrowserVisibility.ts @@ -0,0 +1,19 @@ +import { useState, useEffect } from "react"; + +export const useBrowserVisibility = (): boolean => { + const [isVisible, setIsVisible] = useState(!document.hidden); + + useEffect(() => { + const handleVisibilityChange = (): void => { + setIsVisible(!document.hidden); + }; + + document.addEventListener("visibilitychange", handleVisibilityChange); + + return (): void => { + document.removeEventListener("visibilitychange", handleVisibilityChange); + }; + }, []); + + return isVisible; +}; diff --git a/src/hooks/useSearchTreeState.ts b/src/hooks/useSearchTreeState.ts index f42d949f..afbd6927 100644 --- a/src/hooks/useSearchTreeState.ts +++ b/src/hooks/useSearchTreeState.ts @@ -26,6 +26,7 @@ export type SearchTreeState = { setSearchQuery: (value: SearchQueryParams) => void; totalItems: number; setTotalItems: (value: number) => void; + isActive?: boolean; }; export function useSearchTreeState({ @@ -76,6 +77,7 @@ export function useSearchTreeState({ setSearchQuery: actionViewContext.setSearchQuery ?? (() => {}), totalItems: actionViewContext.totalItems ?? 0, setTotalItems: actionViewContext.setTotalItems ?? (() => {}), + isActive: actionViewContext.isActive, } : { treeIsLoading: localTreeIsLoading, @@ -98,5 +100,6 @@ export function useSearchTreeState({ setSearchQuery: setLocalSearchQuery, totalItems: localTotalItems, setTotalItems: setLocalTotalItems, + isActive: undefined, }; } diff --git a/src/views/CurrentTabContent.tsx b/src/views/CurrentTabContent.tsx index 090a7c06..1f5ce4d8 100644 --- a/src/views/CurrentTabContent.tsx +++ b/src/views/CurrentTabContent.tsx @@ -1,4 +1,4 @@ -import React, { useContext } from "react"; +import { useContext } from "react"; import { TabManagerContext, diff --git a/src/widgets/views/Form.tsx b/src/widgets/views/Form.tsx index a0d05320..b569038b 100644 --- a/src/widgets/views/Form.tsx +++ b/src/widgets/views/Form.tsx @@ -1,10 +1,12 @@ -import React, { +import { useState, forwardRef, useImperativeHandle, useEffect, useRef, useContext, + useCallback, + useMemo, } from "react"; import { Form as FormOoui, parseContext } from "@gisce/ooui"; import { @@ -60,6 +62,7 @@ import { } from "@/helpers/one2manyHelper"; import { ErrorAlert } from "@/ui/ErrorAlert"; import { mergeFieldsContext } from "@/helpers/fieldsHelper"; +import { useAutorefreshableFormFields } from "@/hooks/useAutorefreshableFormFields"; export type FormProps = { model: string; @@ -161,6 +164,7 @@ function Form(props: FormProps, ref: any) { setAttachments = undefined, title = undefined, setTitle = undefined, + isActive = undefined, } = (rootForm ? actionViewContext : {}) || {}; const contentRootContext = useContext( @@ -247,9 +251,21 @@ function Form(props: FormProps, ref: any) { propsOnSubmitError?.(error); }; - function getCurrentId() { + const getCurrentId = useCallback(() => { return id || createdId.current; - } + }, [id]); + const [refId, setRefId] = useState(() => createdId.current); + + useEffect(() => { + if (createdId.current !== refId) { + setRefId(createdId.current); + } + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [createdId.current]); + + const currentId = useMemo(() => { + return id || refId; + }, [id, refId]); function getFields() { return fields; @@ -330,7 +346,7 @@ function Form(props: FormProps, ref: any) { return { active_id: getCurrentId()!, active_ids: [getCurrentId()!] }; } - function getAdditionalValues() { + const getAdditionalValues = useCallback(() => { return { id: getCurrentId()!, active_id: getCurrentId()!, @@ -338,7 +354,7 @@ function Form(props: FormProps, ref: any) { parent_id: parentId, ...globalValues, }; - } + }, [getCurrentId, parentId, globalValues]); const getDefaultValues = async (fields: any) => { const formContext = getCurrentId() ? formOoui?.context : {}; @@ -514,38 +530,38 @@ function Form(props: FormProps, ref: any) { })) as FormView; }; - const assignNewValuesToForm = ({ - values: newValues, - fields, - reset, - isDefaultGet = false, - }: { - values: any; - fields: any; - reset: boolean; - isDefaultGet?: boolean; - }) => { - const currentValues = reset ? {} : antForm.getFieldsValue(true); - const mergedValues = { ...currentValues, ...newValues }; - const valuesProcessed = processValues(mergedValues, fields); - const fieldsToUpdate = Object.keys(fields).map((fieldName) => { - const fieldValue = - valuesProcessed[fieldName] !== undefined - ? valuesProcessed[fieldName] - : undefined; - return { + const assignNewValuesToForm = useCallback( + ({ + values: newValues, + fields, + reset, + isDefaultGet = false, + }: { + values: any; + fields: any; + reset: boolean; + isDefaultGet?: boolean; + }) => { + const currentValues = reset ? {} : antForm.getFieldsValue(true); + const mergedValues = { ...currentValues, ...newValues }; + const valuesProcessed = processValues(mergedValues, fields); + const fieldsToUpdate = Object.keys(fields).map((fieldName) => ({ name: fieldName, touched: false, - value: fieldValue, - }; - }); - - if (!isDefaultGet) { - lastAssignedValues.current = valuesProcessed; - } + value: + valuesProcessed[fieldName] !== undefined + ? valuesProcessed[fieldName] + : undefined, + })); + + if (!isDefaultGet) { + lastAssignedValues.current = valuesProcessed; + } - antForm.setFields(fieldsToUpdate); - }; + antForm.setFields(fieldsToUpdate); + }, + [antForm], + ); const fetchValuesFromApi = async ({ fields, @@ -729,59 +745,66 @@ function Form(props: FormProps, ref: any) { return { succeed: submitSucceed, id: getCurrentId()! }; }; - const getFormOoui = ({ - fields, - arch, - values, - operationInProgress = false, - }: { - arch: string; - fields: any; - values: any; - operationInProgress?: boolean; - }) => { - const ooui = new FormOoui(fields); - // Here we must inject `values` to the ooui parser in order to evaluate arch+values and get the new form container - ooui.parse(arch, { - readOnly: readOnly || operationInProgress, - values: convertToPlain2ManyValues( - { - ...values, - ...getAdditionalValues(), - }, - fields, - ), - }); - return ooui; - }; - - const parseForm = ({ - fields, - arch, - values, - operationInProgress = false, - }: { - arch: string; - fields: any; - values: any; - operationInProgress?: boolean; - }) => { - const ooui = getFormOoui({ + const getFormOoui = useCallback( + ({ + fields, arch, + values, + operationInProgress = false, + }: { + arch: string; + fields: any; + values: any; + operationInProgress?: boolean; + }) => { + const ooui = new FormOoui(fields); + // Here we must inject `values` to the ooui parser in order to evaluate arch+values and get the new form container + ooui.parse(arch, { + readOnly: readOnly || operationInProgress, + values: convertToPlain2ManyValues( + { + ...values, + ...getAdditionalValues(), + }, + fields, + ), + }); + return ooui; + }, + [getAdditionalValues, readOnly], + ); + + const parseForm = useCallback( + ({ fields, + arch, values, - operationInProgress, - }); + operationInProgress = false, + }: { + arch: string; + fields: any; + values: any; + operationInProgress?: boolean; + }) => { + const ooui = getFormOoui({ + arch, + fields, + values, + operationInProgress, + }); - setFormOoui(ooui); + setFormOoui(ooui); - if (ooui.string && ooui.string !== title) { - setTitle?.(ooui.string); - } + if (ooui.string && ooui.string !== title) { + setTitle?.(ooui.string); + } - if (formModalContext && ooui.string) - formModalContext.setTitle?.(ooui.string); - }; + if (formModalContext && ooui.string) { + formModalContext.setTitle?.(ooui.string); + } + }, + [formModalContext, getFormOoui, setTitle, title], + ); const checkFieldsChanges = async ({ elementHasLostFocus = false, @@ -1052,6 +1075,39 @@ function Form(props: FormProps, ref: any) { }); } + const onAutorefreshableFieldsChange = useCallback( + (newValues: any) => { + if (!arch) { + return; + } + + const values = { ...getCurrentValues(fields), ...newValues }; + + originalFormValues.current = { + ...originalFormValues.current, + ...newValues, + }; + + parseForm({ fields, arch, values }); + assignNewValuesToForm({ + values, + fields, + reset: false, + }); + }, + [arch, assignNewValuesToForm, fields, getCurrentValues, parseForm], + ); + + useAutorefreshableFormFields({ + model, + id: currentId, + context: parentContext, + autorefreshableFields: formOoui?.autorefreshableFields, + fieldDefs: fields, + onAutorefreshableFieldsChange, + isActive, + }); + async function executeButtonAction({ type, action, diff --git a/src/widgets/views/SearchTreeInfinite.tsx b/src/widgets/views/SearchTreeInfinite.tsx index a8321e4b..4cf798bb 100644 --- a/src/widgets/views/SearchTreeInfinite.tsx +++ b/src/widgets/views/SearchTreeInfinite.tsx @@ -45,6 +45,8 @@ import deepEqual from "deep-equal"; import { useShowErrorDialog } from "@/ui/GenericErrorDialog"; import SearchFilter from "./searchFilter/SearchFilter"; import { useSearchTreeState } from "@/hooks/useSearchTreeState"; +import { Tree as TreeOoui } from "@gisce/ooui"; +import { useAutorefreshableTreeFields } from "@/hooks/useAutorefreshableTreeFields"; export const HEIGHT_OFFSET = 10; export const MAX_ROWS_TO_SELECT = 200; @@ -127,6 +129,7 @@ function SearchTreeInfiniteComp(props: SearchTreeInfiniteProps, ref: any) { results: actionViewResults, setSearchQuery, setTotalItems: setTotalItemsActionView, + isActive, } = useSearchTreeState({ useLocalState: !rootTree }); const nameSearch = nameSearchProps || searchTreeNameSearch; @@ -153,13 +156,24 @@ function SearchTreeInfiniteComp(props: SearchTreeInfiniteProps, ref: any) { // eslint-disable-next-line react-hooks/exhaustive-deps }, [nameSearch]); - const treeOoui = useMemo(() => { + const treeOoui: TreeOoui | undefined = useMemo(() => { if (!treeView) { return; } return getTree(treeView); }, [treeView]); + useAutorefreshableTreeFields({ + model, + tableRef, + autorefreshableFields: treeOoui?.autorefreshableFields, + fieldDefs: treeView?.field_parent + ? { ...treeView?.fields, [treeView?.field_parent]: {} } + : treeView?.fields, + context: parentContext, + isActive, + }); + const columns = useDeepCompareMemo(() => { if (!treeOoui) { return;