diff --git a/app/react/hooks/useDebounce.ts b/app/react/hooks/useDebounce.ts index 3e413417e..271b0158c 100644 --- a/app/react/hooks/useDebounce.ts +++ b/app/react/hooks/useDebounce.ts @@ -1,17 +1,56 @@ -import _ from 'lodash'; +import { debounce } from 'lodash'; import { useState, useRef, useCallback, useEffect } from 'react'; +// `useRef` to keep the debouncer function (result of the _.debounce call) between rerenders. +// +// debouncer func is (value, onChange) => { onChange(value) }; +// +// Previously written and used as +// const onChangeDebouncer = useRef(debounce(onChange, 300)); +// onChangeDebouncer.current(value) +// +// The issue with the previous syntax is that it was holding the initial state of the `onChange` function passed to `useDebounce()`. +// When the `onChange` function was using a dynamic context (vars in parent scope/not in its parameters) +// then invoking the debouncer was producing a result of `onChange` computed uppon the initial state of the function, not the current state. +// +// Example of the issue +// +// function Component({ value }: { value: string; }) { +// +// function onChange(v: string) { +// // This will always print the first value of the "value" prop + the updated value of "v" +// // when called from "handleChange". +// // This is an issue when the `onChange` is a prop of the component and the real function performs state mutations upflow based on +// // values that are in the parent component, as `setDebouncedValue` will only use the initial instance of the `onChange` prop, thus +// // the initial state of the parent component. +// console.log(value, v) +// } +// +// const [debouncedValue, setDebouncedValue] = useDebounce(value, onChange); +// +// function handleChange(newValue: string) { +// setDebouncedValue(newValue); +// } +// +// return ( handleChange(e.target.value)} />) +// } export function useDebounce(value: string, onChange: (value: string) => void) { const [debouncedValue, setDebouncedValue] = useState(value); - const onChangeDebounces = useRef(_.debounce(onChange, 300)); + // Do not change. See notes above + const onChangeDebouncer = useRef( + debounce( + (value: string, onChangeFunc: (v: string) => void) => onChangeFunc(value), + 300 + ) + ); const handleChange = useCallback( (value: string) => { setDebouncedValue(value); - onChangeDebounces.current(value); + onChangeDebouncer.current(value, onChange); }, - [onChangeDebounces, setDebouncedValue] + [onChangeDebouncer, setDebouncedValue, onChange] ); useEffect(() => { diff --git a/app/react/portainer/gitops/RelativePathFieldset/RelativePathFieldset.tsx b/app/react/portainer/gitops/RelativePathFieldset/RelativePathFieldset.tsx index 6c643a1b4..4b4dac139 100644 --- a/app/react/portainer/gitops/RelativePathFieldset/RelativePathFieldset.tsx +++ b/app/react/portainer/gitops/RelativePathFieldset/RelativePathFieldset.tsx @@ -1,9 +1,9 @@ +import { useState } from 'react'; import { FormikErrors } from 'formik'; import { GitFormModel } from '@/react/portainer/gitops/types'; import { PathSelector } from '@/react/portainer/gitops/ComposePathField/PathSelector'; import { dummyGitForm } from '@/react/portainer/gitops/RelativePathFieldset/utils'; -import { useEnableFsPath } from '@/react/portainer/gitops/RelativePathFieldset/useEnableFsPath'; import { SwitchField } from '@@/form-components/SwitchField'; import { TextTip } from '@@/Tip/TextTip'; @@ -30,17 +30,20 @@ export function RelativePathFieldset({ hideEdgeConfigs, errors, }: Props) { - const { enableFsPath0, enableFsPath1, toggleFsPath } = useEnableFsPath(value); + const [relativePathManuallyEnabled, setRelativePathManuallyEnabled] = + useState(value.SupportRelativePath); + + const [relativePathForcedEnabled, setRelativePathForcedEnabled] = useState( + value.SupportPerDeviceConfigs + ); const gitoptsEdgeConfigDocUrl = useDocsUrl( '/user/edge/stacks/add#gitops-edge-configurations' ); - const pathTip0 = + const pathTipSwarm = 'For relative path volumes use with Docker Swarm, you must have a network filesystem which all of your nodes can access.'; - const pathTip1 = - 'Relative path is active. When you set the ‘local filesystem path’, it will also be utilzed for GitOps Edge configuration.'; - const pathTip2 = + const pathTipGitopsActive = 'GitOps Edge configurations is active. When you set the ‘local filesystem path’, it will also be utilized for relative paths.'; return ( @@ -53,10 +56,10 @@ export function RelativePathFieldset({ label="Enable relative path volumes" labelClass="col-sm-3 col-lg-2" tooltip="Enabling this means you can specify relative path volumes in your Compose files, with Portainer pulling the content from your git repository to the environment the stack is deployed to." - disabled={isEditing} + disabled={isEditing || relativePathForcedEnabled} checked={value.SupportRelativePath} onChange={(value) => { - toggleFsPath(0, value); + setRelativePathManuallyEnabled(value); handleChange({ SupportRelativePath: value }); }} /> @@ -68,31 +71,33 @@ export function RelativePathFieldset({