diff --git a/core/src/main/java/dev/vml/es/acm/core/code/ArgumentType.java b/core/src/main/java/dev/vml/es/acm/core/code/ArgumentType.java index 5a0f1d0c..3b2d26af 100644 --- a/core/src/main/java/dev/vml/es/acm/core/code/ArgumentType.java +++ b/core/src/main/java/dev/vml/es/acm/core/code/ArgumentType.java @@ -12,5 +12,6 @@ public enum ArgumentType { SELECT, MULTISELECT, COLOR, - NUMBER_RANGE + NUMBER_RANGE, + PATH } diff --git a/core/src/main/java/dev/vml/es/acm/core/code/Arguments.java b/core/src/main/java/dev/vml/es/acm/core/code/Arguments.java index 33b1dafe..a7e25867 100644 --- a/core/src/main/java/dev/vml/es/acm/core/code/Arguments.java +++ b/core/src/main/java/dev/vml/es/acm/core/code/Arguments.java @@ -7,11 +7,13 @@ import dev.vml.es.acm.core.util.TypeUtils; import dev.vml.es.acm.core.util.TypeValueMap; import groovy.lang.Closure; + import java.io.Serializable; import java.util.HashMap; import java.util.LinkedHashMap; import java.util.Map; import java.util.Optional; + import org.apache.sling.api.resource.ValueMap; public class Arguments implements Serializable { @@ -221,4 +223,14 @@ public void decimalNumber(String name, Closure options) { GroovyUtils.with(argument, options); add(argument); } + + public void path(String path) { + text(path, null); + } + + public void path(String path, Closure options) { + PathArgument argument = new PathArgument(path); + GroovyUtils.with(argument, options); + add(argument); + } } diff --git a/core/src/main/java/dev/vml/es/acm/core/code/arg/PathArgument.java b/core/src/main/java/dev/vml/es/acm/core/code/arg/PathArgument.java new file mode 100644 index 00000000..ab74d737 --- /dev/null +++ b/core/src/main/java/dev/vml/es/acm/core/code/arg/PathArgument.java @@ -0,0 +1,20 @@ +package dev.vml.es.acm.core.code.arg; + +import dev.vml.es.acm.core.code.Argument; +import dev.vml.es.acm.core.code.ArgumentType; + +public class PathArgument extends Argument { + private String root; + + public String getRoot() { + return root; + } + + public void setRoot(String root) { + this.root = root; + } + + public PathArgument(String name) { + super(name, ArgumentType.PATH, String.class); + } +} diff --git a/ui.content.example/src/main/content/jcr_root/conf/acm/settings/script/manual/example/ACME-201_arguments.groovy b/ui.content.example/src/main/content/jcr_root/conf/acm/settings/script/manual/example/ACME-201_arguments.groovy index 0215e5e2..f2875012 100644 --- a/ui.content.example/src/main/content/jcr_root/conf/acm/settings/script/manual/example/ACME-201_arguments.groovy +++ b/ui.content.example/src/main/content/jcr_root/conf/acm/settings/script/manual/example/ACME-201_arguments.groovy @@ -6,19 +6,23 @@ * @author Krystian Panek */ void describeRun() { - args.string("animalName") { value = "Whiskers"; validator = "(v, a) => a.animalType === 'cat' ? (v && v.startsWith('W') || 'Cat name must start with W!') : true" } - args.select("animalType") { value = "cat"; options = ["cat", "dog", "bird", "fish", "hamster", "rabbit", "turtle", "lizard", "snake", "frog"] } + args.string("animalName") { value = "Whiskers"; + validator = "(v, a) => a.animalType === 'cat' ? (v && v.startsWith('W') || 'Cat name must start with W!') : true" } + args.select("animalType") { value = "cat"; + options = ["cat", "dog", "bird", "fish", "hamster", "rabbit", "turtle", "lizard", "snake", "frog"] } args.bool("allergicToDogs") { label = "Allergic to Dogs?"; value = false; checkbox() } args.integerNumber("napTime") { min = 1; value = 5; group = "Behavior" } - args.select("activity") { label = "Activity"; options = ["Sleeping": "sleep", "Playing": "play", "Eating": "eat"]; value = "play"; group = "Behavior" } + args.select("activity") { label = "Activity"; options = ["Sleeping": "sleep", "Playing": "play", "Eating": "eat"]; + value = "play"; group = "Behavior" } args.decimalNumber("hungerLevel") { min = 0.1d; max = 1.0d; value = 0.5d; group = "Behavior" } - args.text("favoriteFoods") { label = "Favorite Foods"; language = "json"; value = """["milk", "mice"]"""; group = "Data" } + args.text("favoriteFoods") { label = "Favorite Foods"; language = "json"; value = """["milk", "mice"]"""; + group = "Data" } args.date("birthDate") { label = "Birth Date"; value = "2023-01-01"; group = "Details" } args.time("feedingTime") { label = "Feeding Time"; value = "12:00"; group = "Details" } args.dateTime("lastVetVisit") { label = "Last Vet Visit"; value = "2025-05-01T10:10:10"; group = "Details" } args.string("secretCode") { label = "Secret Code"; value = "1234"; password(); group = "Security" } args.color("favoriteColor") { label = "Favorite Color"; value = "#ffcc00"; group = "Preferences" } - // args.path("profilePicture") { label = "Profile Picture"; group = "Media"; root = "/content/dam" } + args.path("profilePicture") { label = "Profile Picture"; group = "Media"; root = "/content/dam" } } boolean canRun() { 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..09ac3801 --- /dev/null +++ b/ui.content/src/main/content/jcr_root/conf/acm/settings/snippet/available/core/argument/path.yml @@ -0,0 +1,13 @@ +group: Argument +name: argument_path +content: | + args.path("${1:name}") { label = "${2:label}"; root = "${3:value}" } +documentation: | + The path picker allows users to browse and select paths from the JCR repository. + + For example: + ```groovy + args.path("contentPath") { label = "Content Path" } + args.path("assetPath") { label = "Asset Path"; root = "/content/dam" } + args.path("templatePath") { label = "Template Path"; root = "/conf/acme/settings/wcm/templates" } + ``` diff --git a/ui.frontend/src/App.tsx b/ui.frontend/src/App.tsx index 3cbec3fc..02b58c86 100644 --- a/ui.frontend/src/App.tsx +++ b/ui.frontend/src/App.tsx @@ -69,6 +69,7 @@ function App() { fetchState(); const intervalId = setInterval(fetchState, state.spaSettings.appStateInterval); return () => clearInterval(intervalId); + // eslint-disable-next-line react-hooks/exhaustive-deps }, [state.spaSettings.appStateInterval]); return ( diff --git a/ui.frontend/src/components/CodeArgumentInput.tsx b/ui.frontend/src/components/CodeArgumentInput.tsx index 276d5e20..6b9ef033 100644 --- a/ui.frontend/src/components/CodeArgumentInput.tsx +++ b/ui.frontend/src/components/CodeArgumentInput.tsx @@ -28,10 +28,24 @@ import { Field } from '@react-spectrum/label'; import React from 'react'; import { Controller } from 'react-hook-form'; import { useArgumentInput } from '../hooks/form'; -import { Argument, ArgumentValue, isBoolArgument, isColorArgument, isDateTimeArgument, isMultiSelectArgument, isNumberArgument, isRangeArgument, isSelectArgument, isStringArgument, isTextArgument } from '../utils/api.types.ts'; +import { + Argument, + ArgumentValue, + isBoolArgument, + isColorArgument, + isDateTimeArgument, + isMultiSelectArgument, + isNumberArgument, + isPathArgument, + isRangeArgument, + isSelectArgument, + isStringArgument, + isTextArgument, +} from '../utils/api.types.ts'; import { Dates } from '../utils/dates'; import { Strings } from '../utils/strings'; import styles from './CodeArgumentInput.module.css'; +import PathField from './PathPicker.tsx'; interface CodeArgumentInputProps { arg: Argument; @@ -242,6 +256,10 @@ const CodeArgumentInput: React.FC = ({ arg }) => { {fieldState.error &&

{fieldState.error.message}

} ); + } else if (isPathArgument(arg)) { + return ( + + ); } else { throw new Error(`Unsupported argument type: ${arg.type}`); } diff --git a/ui.frontend/src/components/CodeExecuteButton.tsx b/ui.frontend/src/components/CodeExecuteButton.tsx index 6a6104d8..4f0d5f02 100644 --- a/ui.frontend/src/components/CodeExecuteButton.tsx +++ b/ui.frontend/src/components/CodeExecuteButton.tsx @@ -1,8 +1,6 @@ -import { Button, ButtonGroup, Content, Dialog, DialogContainer, Divider, Footer, Form, Heading, Item, TabList, TabPanels, Tabs, Text, View } from '@adobe/react-spectrum'; +import { Button, ButtonGroup, Content, Dialog, DialogContainer, Divider, Form, Heading, Item, TabList, TabPanels, Tabs, Text, View } from '@adobe/react-spectrum'; import Checkmark from '@spectrum-icons/workflow/Checkmark'; import Close from '@spectrum-icons/workflow/Close'; -import Copy from '@spectrum-icons/workflow/Copy'; -import FolderOpen from '@spectrum-icons/workflow/FolderOpen'; import Gears from '@spectrum-icons/workflow/Gears'; import React, { useState } from 'react'; import { FormProvider, useForm } from 'react-hook-form'; @@ -12,7 +10,6 @@ import { Objects } from '../utils/objects'; import { ToastTimeoutLong } from '../utils/spectrum.ts'; import { Strings } from '../utils/strings'; import CodeArgumentInput from './CodeArgumentInput'; -import PathPicker from './PathPicker.tsx'; interface CodeExecuteButtonProps { code: string; @@ -26,7 +23,6 @@ const CodeExecuteButton: React.FC = ({ code, onDescribeF const [description, setDescription] = useState(null); const [dialogOpen, setDialogOpen] = useState(false); const [described, setDescribed] = useState(false); - const [pathPickerOpened, setPathPickerOpened] = useState(false); const methods = useForm({ mode: 'onChange', reValidateMode: 'onChange', @@ -92,32 +88,13 @@ const CodeExecuteButton: React.FC = ({ code, onDescribeF onExecute(description!, data); }; - const handlePathSelect = (path: string) => { - setPathPickerOpened(false); - navigator.clipboard.writeText(path); - }; - const descriptionArguments: Argument[] = Object.values(description?.arguments || []); const groups = Array.from(new Set(descriptionArguments.map((arg) => arg.group))); const shouldRenderTabs = groups.length > 1 || (groups.length === 1 && groups[0] !== ArgumentGroupDefault); const validationFailed = Object.keys(formState.errors).length > 0; - const textFieldExists = descriptionArguments.some((arg) => arg.type === 'STRING' || arg.type === 'TEXT'); return ( <> - {textFieldExists && ( - setPathPickerOpened(false)} - confirmButtonLabel={ - <> - - Copy to clipboard - - } - open={pathPickerOpened} - /> - )} - - )} + setPathPickerOpened(false)} confirmButtonLabel="Choose" root={root} open={pathPickerOpened} /> + + ); +}; + +export default PathField; diff --git a/ui.frontend/src/utils/api.types.ts b/ui.frontend/src/utils/api.types.ts index 4176bb26..58400827 100644 --- a/ui.frontend/src/utils/api.types.ts +++ b/ui.frontend/src/utils/api.types.ts @@ -27,7 +27,7 @@ export type Description = { }; }; -export type ArgumentType = 'BOOL' | 'STRING' | 'TEXT' | 'SELECT' | 'MULTISELECT' | 'INTEGER' | 'DECIMAL' | 'DATETIME' | 'DATE' | 'TIME' | 'COLOR' | 'NUMBER_RANGE'; +export type ArgumentType = 'BOOL' | 'STRING' | 'TEXT' | 'SELECT' | 'MULTISELECT' | 'INTEGER' | 'DECIMAL' | 'DATETIME' | 'DATE' | 'TIME' | 'COLOR' | 'NUMBER_RANGE' | 'PATH'; export type ArgumentValue = string | string[] | number | number[] | boolean | null | undefined | RangeValue; export type ArgumentValues = Record; @@ -92,6 +92,10 @@ export type MultiSelectArgument = Argument & { display: 'AUTO' | 'CHECKBOX' | 'DROPDOWN'; }; +export type PathArgument = Argument & { + root: string; +}; + export function isStringArgument(arg: Argument): arg is StringArgument { return arg.type === 'STRING'; } @@ -124,6 +128,10 @@ export function isRangeArgument(arg: Argument): arg is NumberRang return arg.type === 'NUMBER_RANGE'; } +export function isPathArgument(arg: Argument): arg is PathArgument { + return arg.type === 'PATH'; +} + export function isMultiSelectArgument(arg: Argument): arg is MultiSelectArgument { return arg.type === 'MULTISELECT'; }