From 434663ab35a9282f0eb2a471528c91db18f0c881 Mon Sep 17 00:00:00 2001 From: Lokananda Prabhu <102503482+lokanandaprabhu@users.noreply.github.com> Date: Wed, 11 Mar 2026 18:56:20 +0530 Subject: [PATCH 1/2] feat(orchestrator): add card height mode config for workflow run page (#2386) * feat(orchestrator): add card height mode config Expose a workflow instance page option to switch between fixed card heights and content-based sizing, with a new hook and changeset entry. Co-authored-by: Cursor * fix(orchestrator): simplify layout and warn on invalid mode Refactor the workflow instance layout to reuse card components and rename the height mode flag for clarity, and warn when config values are unexpected before falling back to fixed mode. Made-with: Cursor --------- Co-authored-by: Cursor --- .../workflow-instance-card-height.md | 6 + .../plugins/orchestrator-common/config.d.ts | 14 ++ .../WorkflowInstancePageContent.tsx | 146 +++++++++++------- .../useWorkflowInstanceCardHeightMode.ts | 32 ++++ 4 files changed, 144 insertions(+), 54 deletions(-) create mode 100644 workspaces/orchestrator/.changeset/workflow-instance-card-height.md create mode 100644 workspaces/orchestrator/plugins/orchestrator/src/hooks/useWorkflowInstanceCardHeightMode.ts diff --git a/workspaces/orchestrator/.changeset/workflow-instance-card-height.md b/workspaces/orchestrator/.changeset/workflow-instance-card-height.md new file mode 100644 index 0000000000..6681021910 --- /dev/null +++ b/workspaces/orchestrator/.changeset/workflow-instance-card-height.md @@ -0,0 +1,6 @@ +--- +'@red-hat-developer-hub/backstage-plugin-orchestrator': patch +'@red-hat-developer-hub/backstage-plugin-orchestrator-common': patch +--- + +Add workflow instance card height mode config for fixed or content-based layouts. diff --git a/workspaces/orchestrator/plugins/orchestrator-common/config.d.ts b/workspaces/orchestrator/plugins/orchestrator-common/config.d.ts index 8702c756db..762d49ec03 100644 --- a/workspaces/orchestrator/plugins/orchestrator-common/config.d.ts +++ b/workspaces/orchestrator/plugins/orchestrator-common/config.d.ts @@ -103,5 +103,19 @@ export interface Config { value: string; }>; }; + /** + * UI configuration for the workflow instance page. + * @visibility frontend + */ + workflowInstancePage?: { + /** + * Controls card height behavior on the workflow instance page. + * "fixed" keeps the current fixed-height cards with internal scrolling. + * "content" lets cards expand to fit their content. + * Default: fixed + * @visibility frontend + */ + cardHeightMode?: 'fixed' | 'content'; + }; }; } diff --git a/workspaces/orchestrator/plugins/orchestrator/src/components/WorkflowInstancePage/WorkflowInstancePageContent.tsx b/workspaces/orchestrator/plugins/orchestrator/src/components/WorkflowInstancePage/WorkflowInstancePageContent.tsx index 02768bd278..75714fcfc6 100644 --- a/workspaces/orchestrator/plugins/orchestrator/src/components/WorkflowInstancePage/WorkflowInstancePageContent.tsx +++ b/workspaces/orchestrator/plugins/orchestrator/src/components/WorkflowInstancePage/WorkflowInstancePageContent.tsx @@ -36,6 +36,7 @@ import { import { orchestratorApiRef } from '../../api/api'; import { VALUE_UNAVAILABLE } from '../../constants'; import { useTranslation } from '../../hooks/useTranslation'; +import { useWorkflowInstanceCardHeightMode } from '../../hooks/useWorkflowInstanceCardHeightMode'; import { formatDuration } from '../../utils/DurationUtils'; import { WorkflowRunDetail } from '../types/WorkflowRunDetail'; import { VariablesDialog } from './VariablesDialog'; @@ -102,6 +103,11 @@ export const WorkflowInstancePageContent: React.FC<{ const { t } = useTranslation(); const { classes } = useStyles(); const orchestratorApi = useApi(orchestratorApiRef); + const cardHeightMode = useWorkflowInstanceCardHeightMode(); + const isFixedHeightMode = cardHeightMode !== 'content'; + const topRowClassName = isFixedHeightMode ? classes.topRowCard : ''; + const bottomRowClassName = isFixedHeightMode ? classes.bottomRowCard : ''; + const cardOverflowClassName = isFixedHeightMode ? classes.cardClassName : ''; const details = useMemo( () => mapProcessInstanceToDetails(instance, t), @@ -161,6 +167,61 @@ export const WorkflowInstancePageContent: React.FC<{ ); + const detailsCard = ( + + + {t('common.details')} + + {viewVariables} + + } + divider={false} + className={topRowClassName} + cardClassName={cardOverflowClassName} + > + + + ); + + const resultCard = ( + + ); + + const inputsCard = ( + + ); + + const progressCard = ( + + + + ); + return ( - - - - {t('common.details')} - - {viewVariables} - - } - divider={false} - className={classes.topRowCard} - cardClassName={classes.cardClassName} - > - - - - - - - - - - - - - - - - - + {isFixedHeightMode ? ( + <> + + {detailsCard} + + + {resultCard} + + + {inputsCard} + + + {progressCard} + + + ) : ( + <> + + + {detailsCard} + {inputsCard} + + + + + {resultCard} + {progressCard} + + + + )} ); diff --git a/workspaces/orchestrator/plugins/orchestrator/src/hooks/useWorkflowInstanceCardHeightMode.ts b/workspaces/orchestrator/plugins/orchestrator/src/hooks/useWorkflowInstanceCardHeightMode.ts new file mode 100644 index 0000000000..f37864545c --- /dev/null +++ b/workspaces/orchestrator/plugins/orchestrator/src/hooks/useWorkflowInstanceCardHeightMode.ts @@ -0,0 +1,32 @@ +/* + * Copyright Red Hat, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import { configApiRef, useApi } from '@backstage/core-plugin-api'; + +export type WorkflowInstanceCardHeightMode = 'fixed' | 'content'; + +export function useWorkflowInstanceCardHeightMode(): WorkflowInstanceCardHeightMode { + const config = useApi(configApiRef); + const value = config.getOptionalString( + 'orchestrator.workflowInstancePage.cardHeightMode', + ); + + if (value && value !== 'fixed' && value !== 'content') { + // eslint-disable-next-line no-console + console.warn(`Unknown cardHeightMode "${value}", falling back to "fixed"`); + } + + return value === 'content' ? 'content' : 'fixed'; +} From 33c14401d98d96bcdb64b5fd8921fb383b8f2e5b Mon Sep 17 00:00:00 2001 From: Lokananda Prabhu <102503482+lokanandaprabhu@users.noreply.github.com> Date: Thu, 26 Feb 2026 12:14:31 +0530 Subject: [PATCH 2/2] fix(orchestrator-form-widgets): show spinner immediately on ActiveText retrigger (#2279) * fix: show spinner on ActiveText retrigger * fix(orchestrator-form-widgets): keep spinner until ActiveText eval completes * Merge upstream/main Co-authored-by: Cursor * chore(changeset): mention clearOnRetrigger Document the new fetch:clearOnRetrigger behavior in the existing changeset for the ActiveText retrigger spinner update. Co-authored-by: Cursor * refactor(orchestrator-form-widgets): dedupe clearOnRetrigger Extract shared clear-on-retrigger behavior into a reusable hook and reuse it across ActiveTextInput, ActiveDropdown, and ActiveMultiSelect. Co-authored-by: Cursor * fix(orchestrator-form-widgets): guard retrigger races Ignore stale fetch responses when retrigger values change and avoid reapplying cached data while a retriggered fetch is loading. Use layout effect for clearOnRetrigger to reduce UI flicker. Co-authored-by: Cursor --------- Co-authored-by: Cursor --- .../active-text-retrigger-spinner.md | 5 ++ .../docs/orchestratorFormWidgets.md | 2 + .../src/uiPropTypes.ts | 1 + .../src/utils/index.ts | 1 + .../src/utils/useClearOnRetrigger.ts | 52 +++++++++++++ .../src/utils/useFetch.ts | 78 ++++++++++++++++++- .../src/utils/useFetchAndEvaluate.ts | 26 +++++++ .../src/widgets/ActiveDropdown.tsx | 12 +++ .../src/widgets/ActiveMultiSelect.tsx | 14 ++++ .../src/widgets/ActiveTextInput.tsx | 18 +++++ 10 files changed, 205 insertions(+), 4 deletions(-) create mode 100644 workspaces/orchestrator/.changeset/active-text-retrigger-spinner.md create mode 100644 workspaces/orchestrator/plugins/orchestrator-form-widgets/src/utils/useClearOnRetrigger.ts diff --git a/workspaces/orchestrator/.changeset/active-text-retrigger-spinner.md b/workspaces/orchestrator/.changeset/active-text-retrigger-spinner.md new file mode 100644 index 0000000000..8f53b66310 --- /dev/null +++ b/workspaces/orchestrator/.changeset/active-text-retrigger-spinner.md @@ -0,0 +1,5 @@ +--- +'@red-hat-developer-hub/backstage-plugin-orchestrator-form-widgets': patch +--- + +Show ActiveText spinner immediately on retrigger changes to avoid stale text during debounce. Add fetch:clearOnRetrigger to clear widget values when dependencies change. diff --git a/workspaces/orchestrator/docs/orchestratorFormWidgets.md b/workspaces/orchestrator/docs/orchestratorFormWidgets.md index ba6b6ca5d2..248c4a31bb 100644 --- a/workspaces/orchestrator/docs/orchestratorFormWidgets.md +++ b/workspaces/orchestrator/docs/orchestratorFormWidgets.md @@ -521,6 +521,7 @@ The widget supports the following `ui:props` (for detailed information on each, - `fetch:headers`: HTTP headers for the fetch request - `fetch:body`: HTTP body for the fetch request - `fetch:retrigger`: Array of field paths that trigger a refetch when their values change +- `fetch:clearOnRetrigger`: Clears the field value when retrigger dependencies change ## Content of `ui:props` @@ -540,6 +541,7 @@ Various selectors (like `fetch:response:*`) are processed by the [jsonata](https | fetch:method | HTTP method to use. The default is GET. | GET, POST (So far no identified use-case for PUT or DELETE) | | fetch:body | An object representing the body of an HTTP POST request. Not used with the GET method. Property value can be a string template or an array of strings. templates. | `{“foo”: “bar $${{identityApi.token}}”, "myArray": ["constant", "$${{current.solutionName}}"]}` | | fetch:retrigger | An array of keys/key families as described in the Backstage API Exposed Parts. If the value referenced by any key from this list is changed, the fetch is triggered. | `["current.solutionName", "identityApi.profileName"]` | +| fetch:clearOnRetrigger | When set to `true`, clears the field value as soon as any `fetch:retrigger` dependency changes, before the fetch completes. Useful to avoid stale values while refetching. | `true`, `false` (default: `false`) | | fetch:error:ignoreUnready | When set to `true`, suppresses fetch error display until all `fetch:retrigger` dependencies have non-empty values. This is useful when fetch depends on other fields that are not filled yet, preventing expected errors from being displayed during initial load. | `true`, `false` (default: `false`) | | fetch:error:silent | When set to `true`, suppresses fetch error display when the fetch request returns a non-OK status (4xx/5xx). Use this when you want to handle error states via conditional UI instead of showing the widget error. | `true`, `false` (default: `false`) | | fetch:skipInitialValue | When set to `true`, prevents applying the initial value from `fetch:response:value`, keeping the field empty until the user selects or types a value. | `true`, `false` (default: `false`) | diff --git a/workspaces/orchestrator/plugins/orchestrator-form-widgets/src/uiPropTypes.ts b/workspaces/orchestrator/plugins/orchestrator-form-widgets/src/uiPropTypes.ts index b74a2d5bf0..385f932d3f 100644 --- a/workspaces/orchestrator/plugins/orchestrator-form-widgets/src/uiPropTypes.ts +++ b/workspaces/orchestrator/plugins/orchestrator-form-widgets/src/uiPropTypes.ts @@ -25,6 +25,7 @@ export type UiProps = { 'fetch:headers'?: Record; 'fetch:body'?: Record; 'fetch:retrigger'?: string[]; + 'fetch:clearOnRetrigger'?: boolean; 'fetch:error:ignoreUnready'?: boolean; 'fetch:error:silent'?: boolean; 'fetch:skipInitialValue'?: boolean; diff --git a/workspaces/orchestrator/plugins/orchestrator-form-widgets/src/utils/index.ts b/workspaces/orchestrator/plugins/orchestrator-form-widgets/src/utils/index.ts index 5899c1b647..1984e8ee0b 100644 --- a/workspaces/orchestrator/plugins/orchestrator-form-widgets/src/utils/index.ts +++ b/workspaces/orchestrator/plugins/orchestrator-form-widgets/src/utils/index.ts @@ -24,3 +24,4 @@ export * from './useFetchAndEvaluate'; export * from './applySelector'; export * from './useProcessingState'; export * from './resolveDropdownDefault'; +export * from './useClearOnRetrigger'; diff --git a/workspaces/orchestrator/plugins/orchestrator-form-widgets/src/utils/useClearOnRetrigger.ts b/workspaces/orchestrator/plugins/orchestrator-form-widgets/src/utils/useClearOnRetrigger.ts new file mode 100644 index 0000000000..ef961535d4 --- /dev/null +++ b/workspaces/orchestrator/plugins/orchestrator-form-widgets/src/utils/useClearOnRetrigger.ts @@ -0,0 +1,52 @@ +/* + * Copyright Red Hat, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import { useLayoutEffect, useRef } from 'react'; +import isEqual from 'lodash/isEqual'; + +type UseClearOnRetriggerArgs = { + enabled: boolean; + retrigger: (string | undefined)[] | undefined; + onClear: () => void; +}; + +export const useClearOnRetrigger = ({ + enabled, + retrigger, + onClear, +}: UseClearOnRetriggerArgs) => { + const prevRetriggerRef = useRef<(string | undefined)[] | undefined>( + retrigger, + ); + + useLayoutEffect(() => { + if (!enabled) { + prevRetriggerRef.current = retrigger; + return; + } + + if (!retrigger) { + prevRetriggerRef.current = retrigger; + return; + } + + const prev = prevRetriggerRef.current; + if (prev && !isEqual(prev, retrigger)) { + onClear(); + } + + prevRetriggerRef.current = retrigger; + }, [enabled, retrigger, onClear]); +}; diff --git a/workspaces/orchestrator/plugins/orchestrator-form-widgets/src/utils/useFetch.ts b/workspaces/orchestrator/plugins/orchestrator-form-widgets/src/utils/useFetch.ts index 8d71e06250..8d16a8fb81 100644 --- a/workspaces/orchestrator/plugins/orchestrator-form-widgets/src/utils/useFetch.ts +++ b/workspaces/orchestrator/plugins/orchestrator-form-widgets/src/utils/useFetch.ts @@ -16,7 +16,7 @@ import { useApi, fetchApiRef } from '@backstage/core-plugin-api'; import { JsonObject } from '@backstage/types'; -import { useState } from 'react'; +import { useEffect, useRef, useState } from 'react'; import { UiProps } from '../uiPropTypes'; import { getErrorMessage } from './errorUtils'; import { useEvaluateTemplate } from './evaluateTemplate'; @@ -24,6 +24,7 @@ import { useRequestInit } from './useRequestInit'; import { useRetriggerEvaluate } from './useRetriggerEvaluate'; import { useDebounce } from 'react-use'; import { DEFAULT_DEBOUNCE_LIMIT } from '../widgets/constants'; +import isEqual from 'lodash/isEqual'; /** * Checks if all fetch:retrigger dependencies have non-empty values. @@ -55,6 +56,7 @@ export const useFetch = ( const fetchUrl = uiProps['fetch:url']; const skipErrorWhenDepsEmpty = uiProps['fetch:error:ignoreUnready'] === true; + const clearOnRetrigger = uiProps['fetch:clearOnRetrigger'] === true; const evaluatedRequestInit = useRequestInit({ uiProps, prefix: 'fetch', @@ -67,6 +69,63 @@ export const useFetch = ( formData, setError, }); + const prevRetriggerRef = useRef<(string | undefined)[] | undefined>( + retrigger, + ); + const latestRetriggerRef = useRef<(string | undefined)[] | undefined>( + retrigger, + ); + const requestIdRef = useRef(0); + + useEffect(() => { + latestRetriggerRef.current = retrigger; + }, [retrigger]); + + useEffect(() => { + if (!clearOnRetrigger) { + prevRetriggerRef.current = retrigger; + return; + } + + if (!retrigger) { + prevRetriggerRef.current = retrigger; + return; + } + + const prev = prevRetriggerRef.current; + if (prev && !isEqual(prev, retrigger)) { + setData(undefined); + setError(undefined); + } + + prevRetriggerRef.current = retrigger; + }, [clearOnRetrigger, retrigger]); + + const hasFetchInputs = + !!fetchUrl && !!evaluatedFetchUrl && !!evaluatedRequestInit && !!retrigger; + + // Set loading immediately on dependency changes so UI shows a spinner during debounce. + useEffect(() => { + if (!hasFetchInputs) { + setLoading(false); + return; + } + + if (!areRetriggerDependenciesSatisfied(retrigger)) { + setLoading(false); + return; + } + + // Mark loading immediately when a retrigger change is detected so widgets + // can show the spinner during the debounce window before the fetch starts. + setLoading(true); + }, [ + hasFetchInputs, + fetchUrl, + evaluatedFetchUrl, + evaluatedRequestInit, + retrigger, + ]); useDebounce( () => { @@ -81,6 +140,8 @@ export const useFetch = ( } const fetchData = async () => { + const requestId = ++requestIdRef.current; + const retriggerSnapshot = retrigger; try { setError(undefined); if (typeof evaluatedFetchUrl !== 'string') { @@ -116,14 +177,23 @@ export const useFetch = ( throw new Error('JSON object expected'); } - setData(responseData); + if ( + requestId === requestIdRef.current && + isEqual(retriggerSnapshot, latestRetriggerRef.current) + ) { + setData(responseData); + } } catch (err) { const prefix = `Failed to fetch data for url ${fetchUrl}.`; // eslint-disable-next-line no-console console.error(prefix, err); - setError(getErrorMessage(prefix, err)); + if (requestId === requestIdRef.current) { + setError(getErrorMessage(prefix, err)); + } } finally { - setLoading(false); + if (requestId === requestIdRef.current) { + setLoading(false); + } } }; fetchData(); diff --git a/workspaces/orchestrator/plugins/orchestrator-form-widgets/src/utils/useFetchAndEvaluate.ts b/workspaces/orchestrator/plugins/orchestrator-form-widgets/src/utils/useFetchAndEvaluate.ts index bf7f0681ac..9e915ff27b 100644 --- a/workspaces/orchestrator/plugins/orchestrator-form-widgets/src/utils/useFetchAndEvaluate.ts +++ b/workspaces/orchestrator/plugins/orchestrator-form-widgets/src/utils/useFetchAndEvaluate.ts @@ -59,6 +59,32 @@ export const useFetchAndEvaluate = ( const [error, setError] = useState(); const [loading, setLoading] = React.useState(true); const [resultText, setResultText] = React.useState(); + + // Keep spinner visible during the debounce window after fetch/retrigger changes. + useEffect(() => { + if (!hasRetrigger || !retrigger || waitingForRetrigger) { + return; + } + + if (fetchError) { + setLoading(false); + return; + } + + // Show spinner immediately on retrigger changes and after data updates, + // even before the debounced evaluation completes. + if (retriggerSatisfied || fetchLoading || data !== undefined) { + setLoading(true); + } + }, [ + hasRetrigger, + retrigger, + retriggerSatisfied, + waitingForRetrigger, + fetchError, + fetchLoading, + data, + ]); useDebounce( () => { const evaluate = async () => { diff --git a/workspaces/orchestrator/plugins/orchestrator-form-widgets/src/widgets/ActiveDropdown.tsx b/workspaces/orchestrator/plugins/orchestrator-form-widgets/src/widgets/ActiveDropdown.tsx index fb41b98a71..200c02e259 100644 --- a/workspaces/orchestrator/plugins/orchestrator-form-widgets/src/widgets/ActiveDropdown.tsx +++ b/workspaces/orchestrator/plugins/orchestrator-form-widgets/src/widgets/ActiveDropdown.tsx @@ -34,6 +34,7 @@ import { applySelectorArray, resolveDropdownDefault, useProcessingState, + useClearOnRetrigger, } from '../utils'; import { UiProps } from '../uiPropTypes'; import { ErrorText } from './ErrorText'; @@ -79,6 +80,7 @@ export const ActiveDropdown: Widget< const hasStaticDefault = typeof staticDefault === 'string'; const staticDefaultValue = hasStaticDefault ? staticDefault : undefined; const skipInitialValue = uiProps['fetch:skipInitialValue'] === true; + const clearOnRetrigger = uiProps['fetch:clearOnRetrigger'] === true; const [localError, setLocalError] = useState( !labelSelector || !valueSelector @@ -159,6 +161,16 @@ export const ActiveDropdown: Widget< [onChange, id, setIsChangedByUser], ); + const handleClear = useCallback(() => { + handleChange('', false); + }, [handleChange]); + + useClearOnRetrigger({ + enabled: clearOnRetrigger, + retrigger, + onClear: handleClear, + }); + // Set default value from fetched options // Priority: selector default (if valid option) > static default (if valid) > first fetched option // Note: Static defaults are applied at form initialization level (in OrchestratorForm) diff --git a/workspaces/orchestrator/plugins/orchestrator-form-widgets/src/widgets/ActiveMultiSelect.tsx b/workspaces/orchestrator/plugins/orchestrator-form-widgets/src/widgets/ActiveMultiSelect.tsx index 7653161bdd..5f505a4e26 100644 --- a/workspaces/orchestrator/plugins/orchestrator-form-widgets/src/widgets/ActiveMultiSelect.tsx +++ b/workspaces/orchestrator/plugins/orchestrator-form-widgets/src/widgets/ActiveMultiSelect.tsx @@ -16,6 +16,7 @@ import { KeyboardEvent, SyntheticEvent, + useCallback, useEffect, useMemo, useState, @@ -41,6 +42,7 @@ import { useFetch, useRetriggerEvaluate, useProcessingState, + useClearOnRetrigger, } from '../utils'; import { UiProps } from '../uiPropTypes'; import { ErrorText } from './ErrorText'; @@ -85,6 +87,7 @@ export const ActiveMultiSelect: Widget< const allowNewItems = uiProps['ui:allowNewItems'] === true; const staticDefault = uiProps['fetch:response:default']; const skipInitialValue = uiProps['fetch:skipInitialValue'] === true; + const clearOnRetrigger = uiProps['fetch:clearOnRetrigger'] === true; const staticDefaultValues = Array.isArray(staticDefault) ? (staticDefault as string[]) : undefined; @@ -145,6 +148,17 @@ export const ActiveMultiSelect: Widget< handleFetchEnded, ); + const handleClear = useCallback(() => { + setInProgressItem(''); + onChange([]); + }, [onChange]); + + useClearOnRetrigger({ + enabled: clearOnRetrigger, + retrigger, + onClear: handleClear, + }); + // Process fetch results // Note: Static defaults are applied at form initialization level (in OrchestratorForm) useEffect(() => { diff --git a/workspaces/orchestrator/plugins/orchestrator-form-widgets/src/widgets/ActiveTextInput.tsx b/workspaces/orchestrator/plugins/orchestrator-form-widgets/src/widgets/ActiveTextInput.tsx index 5af03f7542..8fab03bfa3 100644 --- a/workspaces/orchestrator/plugins/orchestrator-form-widgets/src/widgets/ActiveTextInput.tsx +++ b/workspaces/orchestrator/plugins/orchestrator-form-widgets/src/widgets/ActiveTextInput.tsx @@ -37,6 +37,7 @@ import { applySelectorArray, applySelectorString, useProcessingState, + useClearOnRetrigger, } from '../utils'; import { ErrorText } from './ErrorText'; import { UiProps } from '../uiPropTypes'; @@ -73,6 +74,7 @@ export const ActiveTextInput: Widget< const hasStaticDefault = typeof staticDefault === 'string'; const skipInitialValue = uiProps['fetch:skipInitialValue'] === true; const hasFetchUrl = !!uiProps['fetch:url']; + const clearOnRetrigger = uiProps['fetch:clearOnRetrigger'] === true; // If fetch:url is configured, either fetch:response:value OR fetch:response:default should be set // to provide meaningful behavior. Without fetch:url, the widget works as a plain text input. @@ -113,9 +115,23 @@ export const ActiveTextInput: Widget< [onChange, id, setIsChangedByUser], ); + const handleClear = useCallback(() => { + handleChange('', false); + }, [handleChange]); + + useClearOnRetrigger({ + enabled: clearOnRetrigger, + retrigger, + onClear: handleClear, + }); + // Process fetch results - only override if fetch returns a non-empty value // Static defaults are applied at form initialization level (in OrchestratorForm) useEffect(() => { + if (clearOnRetrigger && loading) { + return; + } + if (!data) { return; } @@ -159,6 +175,8 @@ export const ActiveTextInput: Widget< isChangedByUser, skipInitialValue, wrapProcessing, + clearOnRetrigger, + loading, ]); const shouldShowFetchError = uiProps['fetch:error:silent'] !== true;