diff --git a/libs/gui-elements b/libs/gui-elements index 47cdaefc57..6b87ea5259 160000 --- a/libs/gui-elements +++ b/libs/gui-elements @@ -1 +1 @@ -Subproject commit 47cdaefc57ccd76705366d7831227e6fe03c8302 +Subproject commit 6b87ea5259c585e407b0e2bae03ea3adb7c6c4c1 diff --git a/workspace/src/app/views/shared/TargetVocabularySelection/DefaultTargetVocabularySelection.tsx b/workspace/src/app/views/shared/TargetVocabularySelection/DefaultTargetVocabularySelection.tsx index 05153c43d7..bc2038a301 100644 --- a/workspace/src/app/views/shared/TargetVocabularySelection/DefaultTargetVocabularySelection.tsx +++ b/workspace/src/app/views/shared/TargetVocabularySelection/DefaultTargetVocabularySelection.tsx @@ -8,7 +8,7 @@ import { IInputAttributes } from "../modals/CreateArtefactModal/ArtefactForms/In /** Target vocabulary selection component that has static entries 'all installed vocabularies' and 'none'. * And alternatively allows to multi-select all available vocabularies (from the global vocabulary cache). */ -export function DefaultTargetVocabularySelection({ id, name, intent, defaultValue, onChange }: IInputAttributes) { +export function DefaultTargetVocabularySelection({ id, name, defaultValue, onChange }: IInputAttributes) { const [vocabularies, setVocabularies] = useState([]); const [loading, setLoading] = useState(true); const [t] = useTranslation(); diff --git a/workspace/src/app/views/shared/YamlEditor.tsx b/workspace/src/app/views/shared/YamlEditor.tsx index 14deef7740..6fe6bcf4ad 100644 --- a/workspace/src/app/views/shared/YamlEditor.tsx +++ b/workspace/src/app/views/shared/YamlEditor.tsx @@ -3,6 +3,7 @@ import React from "react"; import { CodeAutocompleteField } from "@eccenca/gui-elements"; import { CodeAutocompleteFieldPartialAutoCompleteResult, + CodeAutocompleteFieldProps, CodeAutocompleteFieldReplacementResult, CodeAutocompleteFieldValidationResult, } from "@eccenca/gui-elements/src/components/AutoSuggestion/AutoSuggestion"; @@ -34,6 +35,7 @@ interface YamlEditorProps { onChange: (currentValue: string) => any; /** The auto-completion config. */ autoCompletion?: IPropertyAutocomplete; + intent?: CodeAutocompleteFieldProps["intent"]; } export const YamlEditor: React.FC = ({ @@ -46,6 +48,7 @@ export const YamlEditor: React.FC = ({ autoCompletion, id, onChange, + intent, }) => { const { registerError } = useErrorHandler(); @@ -125,6 +128,7 @@ export const YamlEditor: React.FC = ({ initialValue={initialValue ?? ""} multiline={true} onChange={onChange} + intent={intent} fetchSuggestions={fetchSuggestions} autoCompletionRequestDelay={200} checkInput={checkYamlString} diff --git a/workspace/src/app/views/shared/modals/CreateArtefactModal/ArtefactForms/ArtefactFormParameter.tsx b/workspace/src/app/views/shared/modals/CreateArtefactModal/ArtefactForms/ArtefactFormParameter.tsx index 197c5ffe19..52e0196e8c 100644 --- a/workspace/src/app/views/shared/modals/CreateArtefactModal/ArtefactForms/ArtefactFormParameter.tsx +++ b/workspace/src/app/views/shared/modals/CreateArtefactModal/ArtefactForms/ArtefactFormParameter.tsx @@ -19,7 +19,10 @@ import { OptionallyLabelledParameter, optionallyLabelledParameterToValue, } from "../../../../taskViews/linking/linking.types"; -import { CodeAutocompleteFieldValidationResult } from "@eccenca/gui-elements/src/components/AutoSuggestion/AutoSuggestion"; +import { + CodeAutocompleteFieldProps, + CodeAutocompleteFieldValidationResult, +} from "@eccenca/gui-elements/src/components/AutoSuggestion/AutoSuggestion"; import { useSelector } from "react-redux"; import { commonSel } from "@ducks/common"; import { CreateArtefactModalContext } from "../CreateArtefactModalContext"; @@ -71,6 +74,7 @@ interface Props { defaultValue?: string | number | boolean | OptionallyLabelledParameter; }; parameterType?: string; + highlightForReview?: boolean; } /** Wrapper around the input element of a parameter. Supports switching to variable templates. */ @@ -88,6 +92,7 @@ export const ArtefactFormParameter = ({ disabled = false, tooltip, supportVariableTemplateElement, + highlightForReview = false, }: Props) => { const [t] = useTranslation(); const [toggledTemplateSwitchBefore, setToggledTemplateSwitchBefore] = React.useState(false); @@ -182,17 +187,20 @@ export const ArtefactFormParameter = ({ parameterType, ))) || isTemplateInputType; + const inputIntent: CodeAutocompleteFieldProps["intent"] = + infoMessageDanger || !!validationError ? "danger" : highlightForReview ? "warning" : undefined; return ( { const currentUB = React.useRef(ignoreUnboundVariables); currentUB.current = ignoreUnboundVariables; @@ -383,6 +394,7 @@ export const TemplateInputComponent = memo( checkInput={checkTemplate} autoCompletionRequestDelay={200} multiline={multiline} + intent={intent} outerDivAttributes={ { "data-test-id": "codemirror-wrapper", diff --git a/workspace/src/app/views/shared/modals/CreateArtefactModal/ArtefactForms/InputMapper.tsx b/workspace/src/app/views/shared/modals/CreateArtefactModal/ArtefactForms/InputMapper.tsx index efbafe17f5..0c558bf42e 100644 --- a/workspace/src/app/views/shared/modals/CreateArtefactModal/ArtefactForms/InputMapper.tsx +++ b/workspace/src/app/views/shared/modals/CreateArtefactModal/ArtefactForms/InputMapper.tsx @@ -21,7 +21,7 @@ interface IProps { projectId: string; parameter: ITaskParameter; // Blueprint intent - intent: Intent; + intent?: Intent; onChange: (value) => void; // Initial values in a flat form, e.g. "nestedParam.param1". This is either set for all parameters or not set for none. // The prefixed values can be addressed with help of the 'formParamId' parameter. @@ -40,7 +40,7 @@ export type RegisterForExternalChangesFn = ( export interface IInputAttributes { id: string; name: string; - intent: Intent; + intent?: Intent; onChange: (value) => void; value?: any; defaultValue?: any; diff --git a/workspace/src/app/views/shared/modals/CreateArtefactModal/ArtefactForms/ParameterAutoCompletion.tsx b/workspace/src/app/views/shared/modals/CreateArtefactModal/ArtefactForms/ParameterAutoCompletion.tsx index 6fd31a5dd9..52b0575321 100644 --- a/workspace/src/app/views/shared/modals/CreateArtefactModal/ArtefactForms/ParameterAutoCompletion.tsx +++ b/workspace/src/app/views/shared/modals/CreateArtefactModal/ArtefactForms/ParameterAutoCompletion.tsx @@ -42,7 +42,7 @@ export interface ParameterAutoCompletionProps { /** If a value is required. If true, a reset won't be possible. */ required: boolean; onChange: (value: IAutocompleteDefaultResponse) => any; - intent: Intent; + intent?: Intent; /** Show errors in the auto-completion list instead of the global error notification widget. */ showErrorsInline?: boolean; /** When set to true the auto-complete input field will be in read-only mode and cannot be edited. */ diff --git a/workspace/src/app/views/shared/modals/CreateArtefactModal/ArtefactForms/ParameterWidget.tsx b/workspace/src/app/views/shared/modals/CreateArtefactModal/ArtefactForms/ParameterWidget.tsx index 96ca7b9b3b..62606180d8 100644 --- a/workspace/src/app/views/shared/modals/CreateArtefactModal/ArtefactForms/ParameterWidget.tsx +++ b/workspace/src/app/views/shared/modals/CreateArtefactModal/ArtefactForms/ParameterWidget.tsx @@ -76,6 +76,7 @@ interface IProps { // Values that the auto-completion of other parameters depends on dependentValues: React.MutableRefObject>; parameterCallbacks: ExtendedParameterCallbacks; + isDependencyReviewHighlighted: (fullParameterId: string) => boolean; } /** Renders the errors message based on the error type. */ @@ -109,6 +110,7 @@ export const ParameterWidget = (props: IProps) => { initialValues, dependentValues, parameterCallbacks, + isDependencyReviewHighlighted, } = props; const parameterExtensions = pluginRegistry.pluginComponent( SUPPORTED_PLUGINS.DI_PARAMETER_EXTENSIONS, @@ -144,6 +146,10 @@ export const ParameterWidget = (props: IProps) => { : undefined; const detailedDocumentationAvailable = parameterCallbacks.namedAnchors.includes(formParamId); + const dependencyReviewHighlighted = isDependencyReviewHighlighted(formParamId); + const dependencyReviewMessage = dependencyReviewHighlighted + ? (t("form.taskForm.affectedParameterNeedsReview") as string) + : undefined; let propertyHelperText: JSX.Element | undefined = undefined; if ((description && description.length > MAXLENGTH_TOOLTIP) || detailedDocumentationAvailable) { let parameterDescription: JSX.Element = {description}; @@ -198,6 +204,7 @@ export const ParameterWidget = (props: IProps) => { return (
{ /> } helperText={propertyHelperText} + intent={!!errorText ? "danger" : dependencyReviewHighlighted ? "warning" : undefined} + messageText={errorText ? errorText : dependencyReviewMessage} > {Object.entries(propertyDetails.properties as Record).map( ([nestedParamId, nestedParam]) => { @@ -224,6 +233,7 @@ export const ParameterWidget = (props: IProps) => { initialValues={initialValues} dependentValues={dependentValues} parameterCallbacks={parameterCallbacks} + isDependencyReviewHighlighted={isDependencyReviewHighlighted} /> ); }, @@ -234,6 +244,7 @@ export const ParameterWidget = (props: IProps) => { return (
{ /> } helperText={propertyHelperText} - intent={!!errorMessage(title, errors) ? "danger" : undefined} - messageText={errorText ? errorText : infoHelperText} + intent={!!errorMessage(title, errors) ? "danger" : dependencyReviewHighlighted ? "warning" : undefined} + messageText={errorText ? errorText : dependencyReviewMessage ?? infoHelperText} > { required={required && propertyDetails.parameterType !== "boolean"} tooltip={description && description.length <= MAXLENGTH_TOOLTIP ? description : undefined} helperText={propertyHelperText} - infoMessage={errorText ? errorText : infoHelperText} + infoMessage={errorText ? errorText : dependencyReviewMessage ?? infoHelperText} infoMessageDanger={!!errorText} parameterType={propertyDetails.parameterType} + highlightForReview={dependencyReviewHighlighted} supportVariableTemplateElement={{ onChange: changeHandlers[formParamId], startWithTemplateView: isTemplateParameter, @@ -292,6 +304,7 @@ export const ParameterWidget = (props: IProps) => { projectId={projectId} pluginId={pluginId} autoCompletion={autoCompletion} + intent={errors ? "danger" : dependencyReviewHighlighted ? "warning" : undefined} id={formParamId} initialValue={initialValue?.value ?? initialValue} onChange={(value) => (onChange ? onChange(value) : changeHandlers[formParamId](value))} @@ -318,7 +331,7 @@ export const ParameterWidget = (props: IProps) => { : defaultValueAsJs(propertyDetails, true)) } autoCompletion={autoCompletion} - intent={errors ? Intent.DANGER : Intent.NONE} + intent={errors ? Intent.DANGER : dependencyReviewHighlighted ? Intent.WARNING : undefined} formParamId={formParamId} dependentValue={dependentValue} defaultValue={parameterCallbacks.defaultValue} @@ -335,7 +348,7 @@ export const ParameterWidget = (props: IProps) => { any; - /** Shows a warning notification with the following message in the dialog popup that can be removed by the user. */ - showWarningMessage: (message: string) => void; + /** Shows or clears a warning notification in the dialog popup. */ + showWarningMessage: (warning?: TaskFormReviewWarning) => void; +} + +export interface TaskFormReviewWarning { + message: React.JSX.Element | string; + onClearHighlightedValues?: () => void; + onDismiss?: () => void; } export interface UpdateTaskProps { @@ -144,8 +151,10 @@ export function TaskForm({ React.useRef>({}); const dependentParameters = React.useRef>>(new Map()); const [doChange, setDoChange] = useState(false); + const [staleDependentParameterIds, setStaleDependentParameterIds] = useState([]); const { registerError } = useErrorHandler(); const parameterDefaultValues = React.useRef>(new Map()); + const staleDependentParameterIdsRef = React.useRef([]); parameterDefaultValues.current = extractDefaultValues(artefact); const addDependentParameter = React.useCallback((dependentParameter: string, dependsOn: string) => { @@ -253,6 +262,17 @@ export function TaskForm({ } }, [doChange]); + const scrollParameterIntoView = React.useCallback((fullParameterId: string) => { + const parameterElement = window.document.querySelector( + `[data-test-id="task-form-parameter-${fullParameterId}"]`, + ) as HTMLElement | null; + parameterElement?.scrollIntoView({ behavior: "smooth", block: "center" }); + }, []); + + const dismissHighlightedDependentParameters = React.useCallback(() => { + setStaleDependentParameterIds([]); + }, []); + /** Initialize: register parameters, set default/existing values etc. */ useEffect(() => { // All keys (also nested ones are stores in here) @@ -381,6 +401,57 @@ export function TaskForm({ }; }, [properties, register, projectId]); + /** Change handler for a specific parameter. */ + const clearHighlightedDependentParameters = React.useCallback(async () => { + const highlightedParameterIds = staleDependentParameterIdsRef.current; + highlightedParameterIds.forEach((paramId) => { + const oldValue = getValues(paramId); + if (dependentValues.current[paramId] !== undefined) { + dependentValues.current[paramId] = { + value: "", + isTemplate: parameterCallbacks.templateFlag(paramId), + }; + } + setValue(paramId, ""); + detectChange(paramId, "", oldValue); + propagateExternallyChangedParameterValue(paramId, ""); + }); + await Promise.all(highlightedParameterIds.map((paramId) => triggerValidation(paramId))); + dismissHighlightedDependentParameters(); + }, [ + detectChange, + dismissHighlightedDependentParameters, + getValues, + propagateExternallyChangedParameterValue, + setValue, + triggerValidation, + ]); + + useEffect(() => { + staleDependentParameterIdsRef.current = staleDependentParameterIds; + if (staleDependentParameterIds.length) { + const parameterLabelsToReview = staleDependentParameterIds.map( + (paramId) => parameterLabels.current.get(paramId) ?? paramId, + ); + showWarningMessage({ + message: t("form.taskForm.dependentValuesNeedReview", { + count: parameterLabelsToReview.length, + parameters: parameterLabelsToReview.join(", "), + }), + onDismiss: dismissHighlightedDependentParameters, + onClearHighlightedValues: () => void clearHighlightedDependentParameters(), + }); + } else { + showWarningMessage(undefined); + } + }, [ + clearHighlightedDependentParameters, + dismissHighlightedDependentParameters, + staleDependentParameterIds, + showWarningMessage, + t, + ]); + /** Change handler for a specific parameter. */ const handleChange = useCallback( (key: string) => async (e) => { @@ -403,40 +474,36 @@ export function TaskForm({ if (!escapeKeyDisabled.current) { escapeKeyDisabled.current = true; } - if (dependentParameters.current.has(key)) { - // collect all dependent parameters - const dependentParametersTransitiveSet = new Set(); - // Dependent parameters that were actually reset - const resetDependentParameters: string[] = []; - const collect = (currentParamId: string) => { - const params = dependentParameters.current?.get(currentParamId) ?? []; - params.forEach((p: string) => { - if (!dependentParametersTransitiveSet.has(p)) { - dependentParametersTransitiveSet.add(p); - collect(p); - } - }); - }; - collect(key); - dependentParametersTransitiveSet.forEach((paramId) => { - const currentValue = getValues(paramId); - if (currentValue && paramId !== key) { - resetDependentParameters.push(parameterLabels.current.get(paramId) ?? paramId); - handleChange(paramId)(""); - propagateExternallyChangedParameterValue(paramId, ""); - } - }); - if (resetDependentParameters.length) { - showWarningMessage( - t("form.taskForm.resetMessage", { - parameters: resetDependentParameters.join(", "), - dependOn: parameterLabels.current.get(key) ?? key, - }), - ); - } + const populatedDependentParameters = dependentParameters.current.has(key) + ? collectPopulatedDependentParameters( + key, + dependentParameters.current, + (paramId) => getValues(paramId), + ) + : []; + if (populatedDependentParameters.length) { + scrollParameterIntoView(populatedDependentParameters[0]); } + setStaleDependentParameterIds((previousIds) => { + const nextIds = new Set(previousIds); + nextIds.delete(key); + populatedDependentParameters.forEach((paramId) => nextIds.add(paramId)); + return Array.from(nextIds); + }); }, - [], + [ + detectChange, + form, + getValues, + projectId, + propagateExternallyChangedParameterValue, + registerError, + setValue, + showWarningMessage, + scrollParameterIntoView, + t, + triggerValidation, + ], ); const handleTagSelectionChange = React.useCallback( @@ -458,7 +525,12 @@ export function TaskForm({ handlers[key] = handleChange(key); }); return handlers; - }, [formValueKeys]); + }, [formValueKeys, handleChange]); + + const isDependencyReviewHighlighted = React.useCallback( + (fullParameterId: string) => staleDependentParameterIds.includes(fullParameterId), + [staleDependentParameterIds], + ); const normalParams = visibleParams.filter(([k, param]) => !param.advanced); const advancedParams = visibleParams.filter(([k, param]) => param.advanced); @@ -561,6 +633,7 @@ export function TaskForm({ initialValues={initialValues} dependentValues={dependentValues} parameterCallbacks={extendedCallbacks} + isDependencyReviewHighlighted={isDependencyReviewHighlighted} /> ))} { @@ -618,6 +691,7 @@ export function TaskForm({ initialValues={initialValues} dependentValues={dependentValues} parameterCallbacks={extendedCallbacks} + isDependencyReviewHighlighted={isDependencyReviewHighlighted} /> ))} diff --git a/workspace/src/app/views/shared/modals/CreateArtefactModal/ArtefactForms/TaskForm.utils.test.ts b/workspace/src/app/views/shared/modals/CreateArtefactModal/ArtefactForms/TaskForm.utils.test.ts new file mode 100644 index 0000000000..0a5aaf2994 --- /dev/null +++ b/workspace/src/app/views/shared/modals/CreateArtefactModal/ArtefactForms/TaskForm.utils.test.ts @@ -0,0 +1,34 @@ +import { collectPopulatedDependentParameters, collectTransitiveDependentParameters } from "./TaskForm.utils"; + +describe("TaskForm utils", () => { + it("collects transitive dependent parameters", () => { + const dependencyGraph = new Map>([ + ["connection.password", new Set(["connection.database", "connection.schema"])], + ["connection.database", new Set(["connection.table"])], + ]); + + expect(collectTransitiveDependentParameters("connection.password", dependencyGraph)).toEqual([ + "connection.database", + "connection.table", + "connection.schema", + ]); + }); + + it("collects only populated dependent parameters and ignores the changed parameter in cycles", () => { + const dependencyGraph = new Map>([ + ["connection.password", new Set(["connection.database", "connection.schema", "connection.role"])], + ["connection.database", new Set(["connection.password", "connection.table"])], + ]); + const values: Record = { + "connection.password": "secret", + "connection.database": "analytics", + "connection.schema": "", + "connection.role": undefined, + "connection.table": "customers", + }; + + expect( + collectPopulatedDependentParameters("connection.password", dependencyGraph, (paramId) => values[paramId]), + ).toEqual(["connection.database", "connection.table"]); + }); +}); diff --git a/workspace/src/app/views/shared/modals/CreateArtefactModal/ArtefactForms/TaskForm.utils.ts b/workspace/src/app/views/shared/modals/CreateArtefactModal/ArtefactForms/TaskForm.utils.ts new file mode 100644 index 0000000000..d953a36941 --- /dev/null +++ b/workspace/src/app/views/shared/modals/CreateArtefactModal/ArtefactForms/TaskForm.utils.ts @@ -0,0 +1,31 @@ +export const collectTransitiveDependentParameters = ( + changedParameterId: string, + dependencyGraph: Map>, +): string[] => { + const visited = new Set([changedParameterId]); + const collected: string[] = []; + + const collect = (currentParamId: string) => { + const dependentParams = dependencyGraph.get(currentParamId) ?? []; + dependentParams.forEach((paramId: string) => { + if (!visited.has(paramId)) { + visited.add(paramId); + collected.push(paramId); + collect(paramId); + } + }); + }; + + collect(changedParameterId); + return collected; +}; + +export const collectPopulatedDependentParameters = ( + changedParameterId: string, + dependencyGraph: Map>, + getValue: (paramId: string) => any, +): string[] => { + return collectTransitiveDependentParameters(changedParameterId, dependencyGraph).filter((paramId) => { + return paramId !== changedParameterId && !!getValue(paramId); + }); +}; diff --git a/workspace/src/app/views/shared/modals/CreateArtefactModal/CreateArtefactModal.tsx b/workspace/src/app/views/shared/modals/CreateArtefactModal/CreateArtefactModal.tsx index 4c4d2ad433..6ac8f8eed3 100644 --- a/workspace/src/app/views/shared/modals/CreateArtefactModal/CreateArtefactModal.tsx +++ b/workspace/src/app/views/shared/modals/CreateArtefactModal/CreateArtefactModal.tsx @@ -40,7 +40,7 @@ import { } from "@ducks/common/typings"; import Loading from "../../Loading"; import { ProjectForm } from "./ArtefactForms/ProjectForm"; -import { TaskForm } from "./ArtefactForms/TaskForm"; +import { TaskForm, TaskFormReviewWarning } from "./ArtefactForms/TaskForm"; import { DATA_TYPES } from "../../../../constants"; import ArtefactTypesList from "./ArtefactTypesList"; import { SearchBar } from "../../SearchBar/SearchBar"; @@ -156,7 +156,7 @@ export function CreateArtefactModal() { }, []); const [taskActionResult, setTaskActionResult] = React.useState<{ label: string; message: string }>(); const [taskActionLoading, setTaskActionLoading] = React.useState(null); - const [taskFormGeneralWarning, setTaskFormGeneralWarning] = React.useState(); + const [taskFormGeneralWarning, setTaskFormGeneralWarning] = React.useState(); const generalWarningTimeout = React.useRef(); const projectAcl = React.useRef(); @@ -164,11 +164,15 @@ export function CreateArtefactModal() { projectAcl.current = newProjectAcl; }, []); - const taskFormWarning = React.useCallback((message: string) => { + const taskFormWarning = React.useCallback((warning?: TaskFormReviewWarning) => { if (generalWarningTimeout.current) { clearTimeout(generalWarningTimeout.current); } - generalWarningTimeout.current = window.setTimeout(() => setTaskFormGeneralWarning(message), 250); + if (!warning) { + setTaskFormGeneralWarning(undefined); + return; + } + generalWarningTimeout.current = window.setTimeout(() => setTaskFormGeneralWarning(warning), 250); }, []); React.useEffect(() => { @@ -956,7 +960,29 @@ export function CreateArtefactModal() { if (taskFormGeneralWarning) { notifications.push( - setTaskFormGeneralWarning(undefined)} />, + + {t("form.taskForm.clearHighlightedValues")} + , + ] + : undefined + } + onDismiss={() => { + taskFormGeneralWarning.onDismiss?.(); + setTaskFormGeneralWarning(undefined); + }} + />, ); } diff --git a/workspace/src/locales/manual/en.json b/workspace/src/locales/manual/en.json index d446f154e5..037ff68a46 100644 --- a/workspace/src/locales/manual/en.json +++ b/workspace/src/locales/manual/en.json @@ -481,7 +481,11 @@ "title": "Title" }, "taskForm": { - "resetMessage": "Following parameters have been reset because they depend on the changed parameter '{{dependOn}}': {{parameters}}" + "resetMessage": "Following parameters have been reset because they depend on the changed parameter '{{dependOn}}': {{parameters}}", + "dependentValuesNeedReview": "You have changed a parameter that {{count}} other parameter depends on. Following parameter needs to be checked: {{parameters}}", + "dependentValuesNeedReview_plural": "You have changed a parameter that {{count}} other parameters depend on. Following parameters need to be checked: {{parameters}}", + "affectedParameterNeedsReview": "This parameter depends on a changed parameter. Please check whether its value is still correct.", + "clearHighlightedValues": "Clear highlighted values" }, "validations": { "float": "must be a valid floating point number", diff --git a/workspace/test/integration/components/CreateArtefactModal/CreateArtefactModal.test.tsx b/workspace/test/integration/components/CreateArtefactModal/CreateArtefactModal.test.tsx index 4b6979af6a..4247ef7fb4 100644 --- a/workspace/test/integration/components/CreateArtefactModal/CreateArtefactModal.test.tsx +++ b/workspace/test/integration/components/CreateArtefactModal/CreateArtefactModal.test.tsx @@ -380,6 +380,113 @@ describe("Task creation widget", () => { }); }); + const createDependentParameterWrapper = async () => { + addDocumentCreateRangeMethod(); + const dependentParameterPluginDescription: IPluginDetails = { + ...mockPluginDescription, + required: [], + properties: { + password: atomicParamDescription({ + title: "password", + parameterType: INPUT_TYPES.PASSWORD, + }), + database: atomicParamDescription( + { + title: "database", + parameterType: INPUT_TYPES.STRING, + }, + { + allowOnlyAutoCompletedValues: false, + autoCompletionDependsOnParameters: ["password"], + }, + ), + }, + }; + const existingTaskWithDependentValue: RecursivePartial = { + projectId: PROJECT_ID, + taskId: TASK_ID, + taskPluginDetails: dependentParameterPluginDescription, + metaData: { + label: "Task label", + }, + currentParameterValues: { + password: value("old-secret"), + database: value("analytics"), + }, + currentTemplateValues: {}, + }; + const { element } = await createMockedListWrapper(existingTaskWithDependentValue); + await waitFor(() => { + expect(findElement(element, "#password")).toBeInTheDocument(); + expect(findElement(element, "#database")).toBeInTheDocument(); + }); + return { element }; + }; + + const databaseInput = (element: Element | RenderResult) => findElement(element, "#database") as HTMLInputElement; + const databaseInputGroupClassName = (element: Element | RenderResult) => + databaseInput(element).closest(`.${bluePrintClassPrefix}-input-group`)?.className ?? ""; + + const expectDependentDatabaseWarning = async ( + element: Element | RenderResult, + expectedValue: string = "analytics", + ) => { + await waitFor(() => { + expect(databaseInput(element).value).toBe(expectedValue); + expect(findElement(element, byTestId("task-form-dependent-values-warning"))).toBeInTheDocument(); + expect(databaseInputGroupClassName(element)).toContain(`${bluePrintClassPrefix}-intent-warning`); + }); + }; + + const expectDependentDatabaseWarningCleared = async ( + element: Element | RenderResult, + expectedValue: string, + ) => { + await waitFor(() => { + expect(databaseInput(element).value).toBe(expectedValue); + expect(("container" in element ? element.container : element).querySelector(byTestId("task-form-dependent-values-warning"))).not.toBeInTheDocument(); + expect(databaseInputGroupClassName(element)).not.toContain(`${bluePrintClassPrefix}-intent-warning`); + }); + }; + + const dismissDependentDatabaseWarning = (element: Element | RenderResult) => { + const warning = findElement(element, byTestId("task-form-dependent-values-warning")); + clickRenderedElement(within(warning).getByLabelText("Close")); + }; + + it("should keep dependent values, highlight them, and clear them only via the warning action", async () => { + const { element } = await createDependentParameterWrapper(); + changeInputValue(findElement(element, "#password") as HTMLInputElement, "new-secret"); + await expectDependentDatabaseWarning(element); + + clickRenderedElement(findElement(element, byTestId("task-form-clear-highlighted-dependent-values"))); + await expectDependentDatabaseWarningCleared(element, ""); + }); + + it("should remove dependent value highlighting but keep the value when dismissing the warning", async () => { + const { element } = await createDependentParameterWrapper(); + changeInputValue(findElement(element, "#password") as HTMLInputElement, "new-secret"); + await expectDependentDatabaseWarning(element); + + dismissDependentDatabaseWarning(element); + + await expectDependentDatabaseWarningCleared(element, "analytics"); + }); + + it("should highlight dependent values again after dismissing the warning and changing the dependency again", async () => { + const { element } = await createDependentParameterWrapper(); + const passwordInput = findElement(element, "#password") as HTMLInputElement; + + changeInputValue(passwordInput, "new-secret"); + await expectDependentDatabaseWarning(element); + + dismissDependentDatabaseWarning(element); + await expectDependentDatabaseWarningCleared(element, "analytics"); + + changeInputValue(passwordInput, "newer-secret"); + await expectDependentDatabaseWarning(element); + }); + const value = (value: string, label?: string) => { const result: { value: string; label?: string } = { value }; if (label) {