From 2c2a21c8c6bf05e9a37384e101fee0b78d18f0db Mon Sep 17 00:00:00 2001 From: "kamil.orwat" Date: Wed, 23 Apr 2025 16:01:07 +0200 Subject: [PATCH 1/7] custom component for path input --- .../es/aem/acm/core/code/ArgumentType.java | 3 +- .../vml/es/aem/acm/core/code/Arguments.java | 10 + .../aem/acm/core/code/arg/PathArgument.java | 10 + .../components/CodeArgumentInput.module.css | 14 +- .../src/components/CodeArgumentInput.tsx | 31 +++- .../src/components/PathInput.module.css | 9 + ui.frontend/src/components/PathInput.tsx | 173 ++++++++++++++++++ ui.frontend/src/utils/api.types.ts | 20 +- 8 files changed, 253 insertions(+), 17 deletions(-) create mode 100644 core/src/main/java/com/vml/es/aem/acm/core/code/arg/PathArgument.java create mode 100644 ui.frontend/src/components/PathInput.module.css create mode 100644 ui.frontend/src/components/PathInput.tsx diff --git a/core/src/main/java/com/vml/es/aem/acm/core/code/ArgumentType.java b/core/src/main/java/com/vml/es/aem/acm/core/code/ArgumentType.java index eeb3badf..ff56ed66 100644 --- a/core/src/main/java/com/vml/es/aem/acm/core/code/ArgumentType.java +++ b/core/src/main/java/com/vml/es/aem/acm/core/code/ArgumentType.java @@ -7,5 +7,6 @@ public enum ArgumentType { STRING, TEXT, SELECT, - MULTISELECT + MULTISELECT, + PATH } diff --git a/core/src/main/java/com/vml/es/aem/acm/core/code/Arguments.java b/core/src/main/java/com/vml/es/aem/acm/core/code/Arguments.java index f2d23b20..967c33e6 100644 --- a/core/src/main/java/com/vml/es/aem/acm/core/code/Arguments.java +++ b/core/src/main/java/com/vml/es/aem/acm/core/code/Arguments.java @@ -106,6 +106,16 @@ public void text(String name, Closure options) { add(argument); } + public void path(String name) { + path(name, null); + } + + public void path(String name, Closure options) { + PathArgument argument = new PathArgument(name); + GroovyUtils.with(argument, options); + add(argument); + } + public void select(String name) { select(name, null); } diff --git a/core/src/main/java/com/vml/es/aem/acm/core/code/arg/PathArgument.java b/core/src/main/java/com/vml/es/aem/acm/core/code/arg/PathArgument.java new file mode 100644 index 00000000..09f03757 --- /dev/null +++ b/core/src/main/java/com/vml/es/aem/acm/core/code/arg/PathArgument.java @@ -0,0 +1,10 @@ +package com.vml.es.aem.acm.core.code.arg; + +import com.vml.es.aem.acm.core.code.Argument; +import com.vml.es.aem.acm.core.code.ArgumentType; + +public class PathArgument extends Argument { + public PathArgument(String name) { + super(name, ArgumentType.PATH); + } +} diff --git a/ui.frontend/src/components/CodeArgumentInput.module.css b/ui.frontend/src/components/CodeArgumentInput.module.css index 15dfe7e1..4e1556a1 100644 --- a/ui.frontend/src/components/CodeArgumentInput.module.css +++ b/ui.frontend/src/components/CodeArgumentInput.module.css @@ -1,9 +1,9 @@ /*Custom error state since react spectrum doesn't provide one for switch and checkbox component*/ .error { - color: var(--spectrum-red-900); - font-size: var(--spectrum-global-dimension-font-size-75); - line-height: var(--spectrum-global-font-line-height-small); - letter-spacing: var(--spectrum-global-font-letter-spacing-none); - margin-inline-end: var(--spectrum-global-dimension-size-100); - font-family: var(--spectrum-global-font-family-base), sans-serif; -} \ No newline at end of file + color: var(--spectrum-red-900); + font-size: var(--spectrum-global-dimension-font-size-75); + line-height: var(--spectrum-global-font-line-height-small); + letter-spacing: var(--spectrum-global-font-letter-spacing-none); + margin-inline-end: var(--spectrum-global-dimension-size-100); + font-family: var(--spectrum-global-font-family-base), sans-serif; +} diff --git a/ui.frontend/src/components/CodeArgumentInput.tsx b/ui.frontend/src/components/CodeArgumentInput.tsx index 226d8a4f..57948e9a 100644 --- a/ui.frontend/src/components/CodeArgumentInput.tsx +++ b/ui.frontend/src/components/CodeArgumentInput.tsx @@ -4,9 +4,10 @@ import { Field } from '@react-spectrum/label'; import React from 'react'; import { Controller, useFormContext } from 'react-hook-form'; import useFormCrossFieldValidation from '../hooks/form.ts'; -import { Argument, ArgumentValue, isBoolArgument, isMultiSelectArgument, isNumberArgument, isSelectArgument, isStringArgument, isTextArgument } from '../utils/api.types.ts'; +import { Argument, ArgumentValue, isBoolArgument, isMultiSelectArgument, isNumberArgument, isPathArgument, isSelectArgument, isStringArgument, isTextArgument } from '../utils/api.types.ts'; import { Strings } from '../utils/strings.ts'; -import styles from "./CodeArgumentInput.module.css" +import styles from './CodeArgumentInput.module.css'; +import { PathInput } from './PathInput.tsx'; interface CodeArgumentInputProps { arg: Argument; @@ -211,6 +212,32 @@ const CodeArgumentInput: React.FC = ({ arg }) => { )} /> ); + } else if (isPathArgument(arg)) { + return ( + ( + + + + {fieldState.error &&

{fieldState.error.message}

} +
+
+ )} + /> + ); } else { return null; } diff --git a/ui.frontend/src/components/PathInput.module.css b/ui.frontend/src/components/PathInput.module.css new file mode 100644 index 00000000..32bf807d --- /dev/null +++ b/ui.frontend/src/components/PathInput.module.css @@ -0,0 +1,9 @@ +.label { + color: var(--spectrum-gray-700); + padding-top: var(--spectrum-global-dimension-size-50); + padding-bottom: var(--spectrum-global-dimension-size-65); + font-size: var(--spectrum-global-dimension-font-size-75); + font-weight: var(--spectrum-global-font-weight-regular); + line-height: var(--spectrum-global-font-line-height-small); + font-family: var(--spectrum-global-font-family-base), sans-serif; +} diff --git a/ui.frontend/src/components/PathInput.tsx b/ui.frontend/src/components/PathInput.tsx new file mode 100644 index 00000000..0e60dc32 --- /dev/null +++ b/ui.frontend/src/components/PathInput.tsx @@ -0,0 +1,173 @@ +import { SpectrumTreeViewProps, Text, TreeView, TreeViewItem, TreeViewItemContent } from '@adobe/react-spectrum'; +import { Selection } from '@react-types/shared/src/selection'; +import { useEffect, useState } from 'react'; +import { apiRequest } from '../utils/api.ts'; +import { AssistCodeOutput, AssistCodeOutputSuggestion } from '../utils/api.types.ts'; +import styles from './PathInput.module.css'; + +interface Node { + name: string; + children: Node[] | null; +} + +interface IPathInput extends SpectrumTreeViewProps { + rootPath: string; + onChange: (paths: string) => void; + errorMessage: string | undefined; + isInvalid: boolean; + label: string; + value: string; +} + +export function PathInput({ rootPath = '', onChange, label, value, ...props }: IPathInput) { + const { getPathCompletion } = useRequestPathCompletion(); + const [selected, setSelected] = useState>(new Set()); + const [expanded, setExpanded] = useState>(new Set()); + const [items, setItems] = useState({ + name: rootPath, + children: null, + }); + + const onSelectionChange = (keys: Selection) => { + setSelected((prevState) => { + const newSelection = new Set([...keys].map((key) => key.toString())); + if ((prevState.size === newSelection.size && [...newSelection].every((value) => prevState.has(value))) || keys == 'all') { + onChange([...prevState][0]); + return prevState; + } + onChange([...newSelection][0]); + return newSelection; + }); + }; + + const translateSuggestion = (suggestion: AssistCodeOutputSuggestion): Node => { + const name = suggestion.it.split('/').at(-1); + return { name: name ? name : suggestion.it, children: null }; + }; + + const findAndUpdateNodeFromPath = (path: string, newChildren: Node[]) => { + const updateTree = (node: Node, pathParts: string[]): Node => { + if (pathParts.length === 0) { + return { ...node, children: newChildren }; + } + const [currentPart, ...remainingParts] = pathParts; + const children = node.children || []; + let child = children.find((child) => child.name === currentPart); + if (!child) { + child = { name: currentPart, children: [] }; + children.push(child); + } + + return { + ...node, + children: children.map((c) => (c.name === currentPart ? updateTree(c, remainingParts) : c)), + }; + }; + const rootName = items.name; + const normalizedPath = path.startsWith(rootName) ? path.slice(rootName.length) : path; + const parts = normalizedPath.split('/').filter(Boolean); + setItems((prevItems) => updateTree(prevItems, parts)); + }; + + const loadSuggestions = async (path: string) => { + getPathCompletion(path) + .then((data) => data.suggestions.map(translateSuggestion)) + .then((nodes) => { + findAndUpdateNodeFromPath(path, nodes); + }); + }; + + const loadedPaths = new Set([rootPath]); + const onExpandedChange = (keys: Selection) => { + const newKeys = new Set([...keys].map((key) => key.toString())); + const differenceToAdd = [...newKeys].filter((key) => !expanded.has(key)); + const differenceToRemove = [...expanded].filter((key) => !newKeys.has(key)); + + const updatedExpanded = new Set(expanded); + + differenceToAdd.forEach(async (key) => { + if (!loadedPaths.has(key)) { + loadedPaths.add(key); + await loadSuggestions(key + '/'); + } + updatedExpanded.add(key); + }); + + differenceToRemove.forEach((key) => { + updatedExpanded.delete(key); + }); + + setExpanded(updatedExpanded); + }; + + useEffect(() => { + const initializePathInput = async () => { + if (!loadedPaths.has(rootPath)) { + loadedPaths.add(rootPath); + await loadSuggestions(rootPath); + } + const expandPathToValue = async (path: string) => { + const pathParts = path.split('/').filter(Boolean); + let currentPath = rootPath; + const expandedKeys = new Set(); + expandedKeys.add(rootPath); + for (const part of pathParts) { + currentPath += `/${part}`; + expandedKeys.add(currentPath); + if (!loadedPaths.has(currentPath)) { + loadedPaths.add(currentPath); + await loadSuggestions(currentPath); + } + } + return expandedKeys; + }; + const expandedKeys = await expandPathToValue(value); + setExpanded(expandedKeys); + setSelected(new Set([value])); + }; + + initializePathInput().catch((err) => { + console.error('Error initializing PathInput:', err); + }); + }, [rootPath, value]); + + return ( + <> +

{label}

+ + {items && } + + + ); +} + +const TreeItem = ({ node, path }: { node: Node; path: string }) => { + return ( + + + {node.name} + + {node.children && node.children.length > 0 ? ( + node.children.map((child: Node) => ) + ) : node.children ? null : ( + + )} + + ); +}; + +const useRequestPathCompletion = () => { + async function getPathCompletion(path: string) { + if (!path.endsWith('/')) { + path += '/'; + } + const response = await apiRequest({ + method: 'GET', + url: `/apps/acm/api/assist-code.json?type=resource&word=${encodeURIComponent(path)}`, + operation: 'Code assistance', + }); + return response.data.data; + } + + return { getPathCompletion }; +}; diff --git a/ui.frontend/src/utils/api.types.ts b/ui.frontend/src/utils/api.types.ts index cac5f825..7647274d 100644 --- a/ui.frontend/src/utils/api.types.ts +++ b/ui.frontend/src/utils/api.types.ts @@ -23,7 +23,7 @@ export type Description = { }; }; -export type ArgumentType = 'BOOL' | 'STRING' | 'TEXT' | 'SELECT' | 'MULTISELECT' | 'INTEGER' | 'DECIMAL'; +export type ArgumentType = 'BOOL' | 'STRING' | 'TEXT' | 'SELECT' | 'MULTISELECT' | 'INTEGER' | 'DECIMAL' | 'PATH'; export type ArgumentValue = string | string[] | number | number[] | boolean | null | undefined; export type ArgumentValues = Record; @@ -86,6 +86,10 @@ export function isMultiSelectArgument(arg: Argument): arg is Mult return arg.type === 'MULTISELECT'; } +export function isPathArgument(arg: Argument): arg is MultiSelectArgument { + return arg.type === 'PATH'; +} + export type Execution = { id: string; userId: string; @@ -145,14 +149,16 @@ export type ExecutionOutput = { list: E[]; }; +export type AssistCodeOutputSuggestion = { + k: string; // kind + l: string; // label + it: string; // insert text + i: string; +}; + export type AssistCodeOutput = { code: string; - suggestions: { - k: string; // kind - l: string; // label - it: string; // insert text - i: string; // info - }[]; + suggestions: AssistCodeOutputSuggestion[]; }; export type SnippetOutput = { From 76be5757360faef3ffb458c5de2c7c72874e0398 Mon Sep 17 00:00:00 2001 From: "kamil.orwat" Date: Thu, 24 Apr 2025 13:40:47 +0200 Subject: [PATCH 2/7] code cleanup and small fixes --- .../snippet/available/core/argument/path.yml | 12 ++++++++++++ 1 file changed, 12 insertions(+) create mode 100644 ui.content/src/main/content/jcr_root/conf/acm/settings/snippet/available/core/argument/path.yml diff --git a/ui.content/src/main/content/jcr_root/conf/acm/settings/snippet/available/core/argument/path.yml b/ui.content/src/main/content/jcr_root/conf/acm/settings/snippet/available/core/argument/path.yml new file mode 100644 index 00000000..dca39c49 --- /dev/null +++ b/ui.content/src/main/content/jcr_root/conf/acm/settings/snippet/available/core/argument/path.yml @@ -0,0 +1,12 @@ +group: Argument +name: argument_path +content: | + args.path("${1:name}") { label = "${2:label}"; value = ${3:value}; rootPath = "${4:rootPath}" } +documentation: | + An argument that allows the user to select a path.
+ The path can be relative to the current resource or absolute. +

+ Example: +
+        args.path("path") { label = "Path"; value = "/content"; rootPath = "/content" }
+    
From ac3baf5747f338f78e318cecde93d57f98cbea08 Mon Sep 17 00:00:00 2001 From: "kamil.orwat" Date: Thu, 24 Apr 2025 13:41:15 +0200 Subject: [PATCH 3/7] code cleanup and small fixes --- .../aem/acm/core/code/arg/PathArgument.java | 11 ++++ .../src/components/CodeArgumentInput.tsx | 3 +- ui.frontend/src/components/PathInput.tsx | 53 ++++++++++--------- 3 files changed, 40 insertions(+), 27 deletions(-) diff --git a/core/src/main/java/com/vml/es/aem/acm/core/code/arg/PathArgument.java b/core/src/main/java/com/vml/es/aem/acm/core/code/arg/PathArgument.java index 09f03757..c15c5d2c 100644 --- a/core/src/main/java/com/vml/es/aem/acm/core/code/arg/PathArgument.java +++ b/core/src/main/java/com/vml/es/aem/acm/core/code/arg/PathArgument.java @@ -4,7 +4,18 @@ import com.vml.es.aem.acm.core.code.ArgumentType; public class PathArgument extends Argument { + + private String rootPath; + public PathArgument(String name) { super(name, ArgumentType.PATH); } + + public String getRootPath() { + return rootPath; + } + + public void setRootPath(String rootPath) { + this.rootPath = rootPath; + } } diff --git a/ui.frontend/src/components/CodeArgumentInput.tsx b/ui.frontend/src/components/CodeArgumentInput.tsx index 57948e9a..16e1ed52 100644 --- a/ui.frontend/src/components/CodeArgumentInput.tsx +++ b/ui.frontend/src/components/CodeArgumentInput.tsx @@ -224,10 +224,9 @@ const CodeArgumentInput: React.FC = ({ arg }) => { { value: string; } -export function PathInput({ rootPath = '', onChange, label, value, ...props }: IPathInput) { +export const PathInput = forwardRef(function PathInput({ rootPath = '', onChange, label, value, ...props }: IPathInput, ref: DOMRef) { const { getPathCompletion } = useRequestPathCompletion(); + const [loadedPaths, setLoadedPaths] = useState>(new Set()); const [selected, setSelected] = useState>(new Set()); const [expanded, setExpanded] = useState>(new Set()); const [items, setItems] = useState({ - name: rootPath, + name: rootPath.replace(/(?!^)\//g, ""), children: null, }); @@ -48,7 +50,7 @@ export function PathInput({ rootPath = '', onChange, label, value, ...props }: I const findAndUpdateNodeFromPath = (path: string, newChildren: Node[]) => { const updateTree = (node: Node, pathParts: string[]): Node => { if (pathParts.length === 0) { - return { ...node, children: newChildren }; + return { ...node, children: newChildren.length > 0 ? newChildren : [] }; } const [currentPart, ...remainingParts] = pathParts; const children = node.children || []; @@ -70,24 +72,22 @@ export function PathInput({ rootPath = '', onChange, label, value, ...props }: I }; const loadSuggestions = async (path: string) => { - getPathCompletion(path) + await getPathCompletion(path) .then((data) => data.suggestions.map(translateSuggestion)) .then((nodes) => { findAndUpdateNodeFromPath(path, nodes); }); }; - const loadedPaths = new Set([rootPath]); const onExpandedChange = (keys: Selection) => { const newKeys = new Set([...keys].map((key) => key.toString())); const differenceToAdd = [...newKeys].filter((key) => !expanded.has(key)); const differenceToRemove = [...expanded].filter((key) => !newKeys.has(key)); const updatedExpanded = new Set(expanded); - differenceToAdd.forEach(async (key) => { if (!loadedPaths.has(key)) { - loadedPaths.add(key); + setLoadedPaths(prevPaths => new Set(prevPaths).add(key)); await loadSuggestions(key + '/'); } updatedExpanded.add(key); @@ -101,51 +101,54 @@ export function PathInput({ rootPath = '', onChange, label, value, ...props }: I }; useEffect(() => { + // Expand the root path when the component mounts const initializePathInput = async () => { if (!loadedPaths.has(rootPath)) { - loadedPaths.add(rootPath); + setLoadedPaths(prevPaths => new Set(prevPaths).add(rootPath)); await loadSuggestions(rootPath); } const expandPathToValue = async (path: string) => { - const pathParts = path.split('/').filter(Boolean); - let currentPath = rootPath; - const expandedKeys = new Set(); - expandedKeys.add(rootPath); - for (const part of pathParts) { - currentPath += `/${part}`; - expandedKeys.add(currentPath); - if (!loadedPaths.has(currentPath)) { - loadedPaths.add(currentPath); + const pathParts = path.split("/").filter(Boolean); + let currentPath = ""; + const tempExpanded = new Set(); + const tempLoadedPaths = new Set(loadedPaths); + // Expand each part of the path except for the last oen + for (const part of pathParts.slice(0, -1)) { + currentPath = `${currentPath}/${part}`.replace(/\/+/g, '/'); + if (!tempLoadedPaths.has(currentPath)) { await loadSuggestions(currentPath); + tempLoadedPaths.add(currentPath); + tempExpanded.add(currentPath); } } - return expandedKeys; + + setLoadedPaths(new Set(tempLoadedPaths)); + setExpanded(new Set(tempExpanded)); }; - const expandedKeys = await expandPathToValue(value); - setExpanded(expandedKeys); + await expandPathToValue(value); setSelected(new Set([value])); }; initializePathInput().catch((err) => { console.error('Error initializing PathInput:', err); }); - }, [rootPath, value]); + }, []); return ( <>

{label}

- + {items && } ); -} +}) const TreeItem = ({ node, path }: { node: Node; path: string }) => { return ( - {node.name} + {node.name.replace("/", "")} {node.children && node.children.length > 0 ? ( node.children.map((child: Node) => ) From 4e72bb848f092725d79556bb0d84c6385b19c70f Mon Sep 17 00:00:00 2001 From: "kamil.orwat" Date: Thu, 24 Apr 2025 14:46:42 +0200 Subject: [PATCH 4/7] cleanup and optimization --- .../snippet/available/core/argument/path.yml | 2 + .../src/components/CodeArgumentInput.tsx | 3 +- ui.frontend/src/components/PathInput.tsx | 55 ++++++++++--------- ui.frontend/src/utils/api.types.ts | 6 +- 4 files changed, 37 insertions(+), 29 deletions(-) diff --git a/ui.content/src/main/content/jcr_root/conf/acm/settings/snippet/available/core/argument/path.yml b/ui.content/src/main/content/jcr_root/conf/acm/settings/snippet/available/core/argument/path.yml index dca39c49..1858c178 100644 --- a/ui.content/src/main/content/jcr_root/conf/acm/settings/snippet/available/core/argument/path.yml +++ b/ui.content/src/main/content/jcr_root/conf/acm/settings/snippet/available/core/argument/path.yml @@ -10,3 +10,5 @@ documentation: |
         args.path("path") { label = "Path"; value = "/content"; rootPath = "/content" }
     
+
+ Note the fact that both rootpath and value can't end with '/' diff --git a/ui.frontend/src/components/CodeArgumentInput.tsx b/ui.frontend/src/components/CodeArgumentInput.tsx index 16e1ed52..71e55727 100644 --- a/ui.frontend/src/components/CodeArgumentInput.tsx +++ b/ui.frontend/src/components/CodeArgumentInput.tsx @@ -225,8 +225,7 @@ const CodeArgumentInput: React.FC = ({ arg }) => { {...field} selectionMode={'single'} label={argLabel(arg)} - // This will be used to set the value of the field - rootPath={'/content'} + rootPath={arg.rootPath ?? ""} errorMessage={fieldState.error ? fieldState.error.message : undefined} isInvalid={!!fieldState.error} aria-label={`Argument ${arg.name}`} diff --git a/ui.frontend/src/components/PathInput.tsx b/ui.frontend/src/components/PathInput.tsx index 15d42a88..8e517767 100644 --- a/ui.frontend/src/components/PathInput.tsx +++ b/ui.frontend/src/components/PathInput.tsx @@ -1,7 +1,6 @@ import { SpectrumTreeViewProps, Text, TreeView, TreeViewItem, TreeViewItemContent } from '@adobe/react-spectrum'; import { Selection } from '@react-types/shared/src/selection'; -import { DOMRef } from '@react-types/shared/src/refs'; -import React, {forwardRef, useEffect, useState} from 'react'; +import React, {forwardRef, Ref, useCallback, useEffect, useState} from 'react'; import { apiRequest } from '../utils/api.ts'; import { AssistCodeOutput, AssistCodeOutputSuggestion } from '../utils/api.types.ts'; import styles from './PathInput.module.css'; @@ -20,26 +19,26 @@ interface IPathInput extends SpectrumTreeViewProps { value: string; } -export const PathInput = forwardRef(function PathInput({ rootPath = '', onChange, label, value, ...props }: IPathInput, ref: DOMRef) { +export const PathInput = forwardRef(function PathInput({ rootPath = "", onChange, label, value, ...props }: IPathInput, ref: Ref) { const { getPathCompletion } = useRequestPathCompletion(); const [loadedPaths, setLoadedPaths] = useState>(new Set()); const [selected, setSelected] = useState>(new Set()); const [expanded, setExpanded] = useState>(new Set()); const [items, setItems] = useState({ - name: rootPath.replace(/(?!^)\//g, ""), + name: rootPath, children: null, }); const onSelectionChange = (keys: Selection) => { - setSelected((prevState) => { - const newSelection = new Set([...keys].map((key) => key.toString())); - if ((prevState.size === newSelection.size && [...newSelection].every((value) => prevState.has(value))) || keys == 'all') { - onChange([...prevState][0]); - return prevState; - } - onChange([...newSelection][0]); - return newSelection; - }); + const newSelection = new Set([...keys].map((key) => key.toString())); + const selectedValue = [...newSelection][0]; + if (selectedValue != undefined) { + setSelected(new Set([selectedValue])); + // Offload the onChange call to avoid blocking the UI + setTimeout(() => { + onChange(selectedValue.length > 0 ? selectedValue : '/'); + }, 0) + } }; const translateSuggestion = (suggestion: AssistCodeOutputSuggestion): Node => { @@ -71,13 +70,13 @@ export const PathInput = forwardRef(function PathInput({ rootPath = '', onChang setItems((prevItems) => updateTree(prevItems, parts)); }; - const loadSuggestions = async (path: string) => { + const loadSuggestions = useCallback(async (path: string) => { await getPathCompletion(path) .then((data) => data.suggestions.map(translateSuggestion)) .then((nodes) => { findAndUpdateNodeFromPath(path, nodes); }); - }; + }, []) const onExpandedChange = (keys: Selection) => { const newKeys = new Set([...keys].map((key) => key.toString())); @@ -110,7 +109,7 @@ export const PathInput = forwardRef(function PathInput({ rootPath = '', onChang const expandPathToValue = async (path: string) => { const pathParts = path.split("/").filter(Boolean); let currentPath = ""; - const tempExpanded = new Set(); + const tempExpanded = new Set([rootPath]); const tempLoadedPaths = new Set(loadedPaths); // Expand each part of the path except for the last oen for (const part of pathParts.slice(0, -1)) { @@ -128,27 +127,31 @@ export const PathInput = forwardRef(function PathInput({ rootPath = '', onChang await expandPathToValue(value); setSelected(new Set([value])); }; - - initializePathInput().catch((err) => { - console.error('Error initializing PathInput:', err); - }); - }, []); + if (value != null && value.length > 0) { + // Offload the initialization to avoid blocking the UI + setTimeout(async () => { + await initializePathInput() + }, 0) + } + }, [rootPath, loadSuggestions]); return ( <>

{label}

- - {items && } - +
+ + {items && } + +
); }) const TreeItem = ({ node, path }: { node: Node; path: string }) => { return ( - + 0 ? node.name : "/"}> - {node.name.replace("/", "")} + {node.name.length > 0 ? node.name.replace("/", "") : '/'} {node.children && node.children.length > 0 ? ( node.children.map((child: Node) => ) diff --git a/ui.frontend/src/utils/api.types.ts b/ui.frontend/src/utils/api.types.ts index 7647274d..80fa991e 100644 --- a/ui.frontend/src/utils/api.types.ts +++ b/ui.frontend/src/utils/api.types.ts @@ -62,6 +62,10 @@ export type MultiSelectArgument = Argument & { display: 'AUTO' | 'CHECKBOX' | 'DROPDOWN'; }; +export type PathArgument = Argument & { + rootPath: string; +}; + export function isStringArgument(arg: Argument): arg is Argument { return arg.type === 'STRING'; } @@ -86,7 +90,7 @@ export function isMultiSelectArgument(arg: Argument): arg is Mult return arg.type === 'MULTISELECT'; } -export function isPathArgument(arg: Argument): arg is MultiSelectArgument { +export function isPathArgument(arg: Argument): arg is PathArgument { return arg.type === 'PATH'; } From 3a9fcea5d503033abd4c0bbf8c8bb95ec9a2a872 Mon Sep 17 00:00:00 2001 From: "kamil.orwat" Date: Thu, 24 Apr 2025 15:23:33 +0200 Subject: [PATCH 5/7] fixed margin and height --- ui.frontend/src/components/CodeArgumentInput.tsx | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/ui.frontend/src/components/CodeArgumentInput.tsx b/ui.frontend/src/components/CodeArgumentInput.tsx index 71e55727..57859b2e 100644 --- a/ui.frontend/src/components/CodeArgumentInput.tsx +++ b/ui.frontend/src/components/CodeArgumentInput.tsx @@ -219,11 +219,12 @@ const CodeArgumentInput: React.FC = ({ arg }) => { control={control} rules={controllerRules(arg)} render={({ field, fieldState }) => ( - + Date: Thu, 24 Apr 2025 15:48:02 +0200 Subject: [PATCH 6/7] allowing users to enter rootPath = '/' --- ui.frontend/src/components/PathInput.tsx | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/ui.frontend/src/components/PathInput.tsx b/ui.frontend/src/components/PathInput.tsx index 8e517767..f0d93471 100644 --- a/ui.frontend/src/components/PathInput.tsx +++ b/ui.frontend/src/components/PathInput.tsx @@ -25,7 +25,7 @@ export const PathInput = forwardRef(function PathInput({ rootPath = "", onChang const [selected, setSelected] = useState>(new Set()); const [expanded, setExpanded] = useState>(new Set()); const [items, setItems] = useState({ - name: rootPath, + name: rootPath == "/" ? "" : rootPath, children: null, }); @@ -100,6 +100,9 @@ export const PathInput = forwardRef(function PathInput({ rootPath = "", onChang }; useEffect(() => { + if (rootPath == '/') { + rootPath = ""; + } // Expand the root path when the component mounts const initializePathInput = async () => { if (!loadedPaths.has(rootPath)) { From 4ae1e633640f84c80a7af65a2913bd276958ddf0 Mon Sep 17 00:00:00 2001 From: "kamil.orwat" Date: Fri, 25 Apr 2025 10:12:08 +0200 Subject: [PATCH 7/7] Fix proposition for unknown userId --- ui.frontend/src/pages/ExecutionView.tsx | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/ui.frontend/src/pages/ExecutionView.tsx b/ui.frontend/src/pages/ExecutionView.tsx index fcf03f9f..e9a41792 100644 --- a/ui.frontend/src/pages/ExecutionView.tsx +++ b/ui.frontend/src/pages/ExecutionView.tsx @@ -31,7 +31,6 @@ const ExecutionView = () => { const [autoscrollOutput, setAutoscrollOutput] = useState(true); const { execution, setExecution, loading } = useExecutionPolling(executionId, appState.spaSettings.executionPollInterval); const [selectedTab, handleTabChange] = useNavigationTab('details'); - if (loading) { return ( @@ -87,7 +86,7 @@ const ExecutionView = () => { - +