Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -12,5 +12,6 @@ public enum ArgumentType {
SELECT,
MULTISELECT,
COLOR,
NUMBER_RANGE
NUMBER_RANGE,
PATH
}
12 changes: 12 additions & 0 deletions core/src/main/java/dev/vml/es/acm/core/code/Arguments.java
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -221,4 +223,14 @@ public void decimalNumber(String name, Closure<DecimalArgument> options) {
GroovyUtils.with(argument, options);
add(argument);
}

public void path(String path) {
text(path, null);
}

public void path(String path, Closure<PathArgument> options) {
PathArgument argument = new PathArgument(path);
GroovyUtils.with(argument, options);
add(argument);
}
}
20 changes: 20 additions & 0 deletions core/src/main/java/dev/vml/es/acm/core/code/arg/PathArgument.java
Original file line number Diff line number Diff line change
@@ -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<String> {
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);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -6,19 +6,23 @@
* @author Krystian Panek <krystian.panek@vml.com>
*/
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() {
Expand Down
Original file line number Diff line number Diff line change
@@ -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" }
```
1 change: 1 addition & 0 deletions ui.frontend/src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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 (
Expand Down
20 changes: 19 additions & 1 deletion ui.frontend/src/components/CodeArgumentInput.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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<ArgumentValue>;
Expand Down Expand Up @@ -242,6 +256,10 @@ const CodeArgumentInput: React.FC<CodeArgumentInputProps> = ({ arg }) => {
{fieldState.error && <p className={styles.error}>{fieldState.error.message}</p>}
</Flex>
);
} else if (isPathArgument(arg)) {
return (
<PathField label={label} root={arg.root} onSelect={field.onChange} value={field.value ?? ''} errorMessage={fieldState.error ? fieldState.error.message : undefined} validationState={fieldState.error ? 'invalid' : 'valid'} />
);
} else {
throw new Error(`Unsupported argument type: ${arg.type}`);
}
Expand Down
33 changes: 1 addition & 32 deletions ui.frontend/src/components/CodeExecuteButton.tsx
Original file line number Diff line number Diff line change
@@ -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';
Expand All @@ -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;
Expand All @@ -26,7 +23,6 @@ const CodeExecuteButton: React.FC<CodeExecuteButtonProps> = ({ code, onDescribeF
const [description, setDescription] = useState<Description | null>(null);
const [dialogOpen, setDialogOpen] = useState(false);
const [described, setDescribed] = useState(false);
const [pathPickerOpened, setPathPickerOpened] = useState(false);
const methods = useForm<ArgumentValues>({
mode: 'onChange',
reValidateMode: 'onChange',
Expand Down Expand Up @@ -92,32 +88,13 @@ const CodeExecuteButton: React.FC<CodeExecuteButtonProps> = ({ code, onDescribeF
onExecute(description!, data);
};

const handlePathSelect = (path: string) => {
setPathPickerOpened(false);
navigator.clipboard.writeText(path);
};

const descriptionArguments: Argument<ArgumentValue>[] = 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 && (
<PathPicker
onSelect={handlePathSelect}
onCancel={() => setPathPickerOpened(false)}
confirmButtonLabel={
<>
<Copy size="XS" marginEnd="size-100" />
Copy to clipboard
</>
}
open={pathPickerOpened}
/>
)}
<Button aria-label="Execute" variant="accent" onPress={handleExecute} isPending={isPending || described} isDisabled={isDisabled}>
<Gears />
<Text>Execute</Text>
Expand Down Expand Up @@ -156,14 +133,6 @@ const CodeExecuteButton: React.FC<CodeExecuteButtonProps> = ({ code, onDescribeF
)}
</Form>
</Content>
{textFieldExists && (
<Footer>
<Button aria-label="Browse" variant="secondary" onPress={() => setPathPickerOpened(true)}>
<FolderOpen size="XS" />
<Text>Browse</Text>
</Button>
</Footer>
)}
<ButtonGroup>
<Button aria-label="Cancel" variant="secondary" onPress={handleCloseDialog}>
<Close size="XS" />
Expand Down
Loading