Skip to content

Commit 933ce30

Browse files
Merge pull request #171 from wttech/script-delete-and-sync
Script save, sync, delete
2 parents 321fa67 + f42b1b7 commit 933ce30

File tree

8 files changed

+319
-92
lines changed

8 files changed

+319
-92
lines changed

core/src/main/java/dev/vml/es/acm/core/script/ScriptRepository.java

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
import java.util.List;
1111
import java.util.Optional;
1212
import java.util.stream.Stream;
13+
import org.apache.commons.collections.CollectionUtils;
1314
import org.apache.sling.api.resource.PersistenceException;
1415
import org.apache.sling.api.resource.Resource;
1516
import org.apache.sling.api.resource.ResourceResolver;
@@ -75,4 +76,22 @@ public Script save(String id, Object data) throws AcmException {
7576
resource.saveFile(ScriptUtils.MIME_TYPE, data);
7677
return read(id).orElseThrow(() -> new AcmException(String.format("Cannot read script '%s' after saving!", id)));
7778
}
79+
80+
public void deleteAll(List<String> ids) {
81+
if (CollectionUtils.isEmpty(ids)) {
82+
return;
83+
}
84+
ids.forEach(this::delete);
85+
}
86+
87+
public void delete(String id) {
88+
if (!ScriptType.byPath(id).isPresent()) {
89+
throw new AcmException(String.format("Cannot delete script '%s' at unsupported path!", id));
90+
}
91+
RepoResource resource = RepoResource.of(resourceResolver, id);
92+
if (!resource.exists()) {
93+
throw new AcmException(String.format("Cannot delete script '%s' as it does not exist!", id));
94+
}
95+
resource.delete();
96+
}
7897
}

core/src/main/java/dev/vml/es/acm/core/servlet/ScriptServlet.java

Lines changed: 71 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -3,18 +3,22 @@
33
import static dev.vml.es.acm.core.util.ServletResult.*;
44
import static dev.vml.es.acm.core.util.ServletUtils.*;
55

6+
import com.day.cq.replication.Replicator;
67
import dev.vml.es.acm.core.code.Code;
78
import dev.vml.es.acm.core.gui.SpaSettings;
9+
import dev.vml.es.acm.core.replication.Activator;
810
import dev.vml.es.acm.core.script.Script;
911
import dev.vml.es.acm.core.script.ScriptRepository;
1012
import dev.vml.es.acm.core.script.ScriptStats;
1113
import dev.vml.es.acm.core.script.ScriptType;
1214
import dev.vml.es.acm.core.util.JsonUtils;
1315
import java.io.IOException;
16+
import java.util.Arrays;
1417
import java.util.List;
1518
import java.util.Optional;
1619
import java.util.stream.Collectors;
1720
import javax.servlet.Servlet;
21+
import org.apache.commons.collections.CollectionUtils;
1822
import org.apache.sling.api.SlingHttpServletRequest;
1923
import org.apache.sling.api.SlingHttpServletResponse;
2024
import org.apache.sling.api.servlets.ServletResolverConstants;
@@ -42,8 +46,25 @@ public class ScriptServlet extends SlingAllMethodsServlet {
4246

4347
private static final String TYPE_PARAM = "type";
4448

49+
private static final String ACTION_PARAM = "action";
50+
4551
private static final String STATS_LIMIT_PARAM = "statsLimit";
4652

53+
private enum Action {
54+
SAVE,
55+
DELETE,
56+
SYNC;
57+
58+
public static Optional<Action> of(String name) {
59+
return Arrays.stream(Action.values())
60+
.filter(a -> a.name().equalsIgnoreCase(name))
61+
.findFirst();
62+
}
63+
}
64+
65+
@Reference
66+
private Replicator replicator;
67+
4768
@Reference
4869
private SpaSettings spaSettings;
4970

@@ -92,23 +113,57 @@ protected void doGet(SlingHttpServletRequest request, SlingHttpServletResponse r
92113

93114
@Override
94115
protected void doPost(SlingHttpServletRequest request, SlingHttpServletResponse response) throws IOException {
95-
try {
96-
ScriptInput input = JsonUtils.read(request.getInputStream(), ScriptInput.class);
97-
if (input == null) {
98-
respondJson(response, badRequest("Script input is not specified!"));
99-
return;
100-
}
101-
102-
Code code = input.getCode();
103-
104-
ScriptRepository repository = new ScriptRepository(request.getResourceResolver());
105-
Script script = repository.save(code);
116+
Optional<Action> action = Action.of(stringParam(request, ACTION_PARAM));
117+
if (!action.isPresent()) {
118+
respondJson(response, error("Invalid action parameter! Must be either 'save', 'delete' or 'sync'"));
119+
return;
120+
}
106121

107-
ScriptOutput output = new ScriptOutput(script);
108-
respondJson(response, ok("Script saved successfully", output));
109-
} catch (Exception e) {
110-
LOG.error("Script cannot be saved!", e);
111-
respondJson(response, error("Script cannot be saved! " + e.getMessage()));
122+
ScriptRepository repository = new ScriptRepository(request.getResourceResolver());
123+
Activator activator = new Activator(request.getResourceResolver(), replicator);
124+
125+
switch (action.get()) {
126+
case SAVE:
127+
try {
128+
ScriptInput input = JsonUtils.read(request.getInputStream(), ScriptInput.class);
129+
if (input == null) {
130+
respondJson(response, badRequest("Script input is not specified!"));
131+
return;
132+
}
133+
134+
Code code = input.getCode();
135+
Script script = repository.save(code);
136+
137+
ScriptOutput output = new ScriptOutput(script);
138+
respondJson(response, ok("Script saved successfully", output));
139+
} catch (Exception e) {
140+
LOG.error("Script cannot be saved!", e);
141+
respondJson(response, error("Script cannot be saved! " + e.getMessage()));
142+
}
143+
break;
144+
case DELETE:
145+
try {
146+
List<String> ids = stringsParam(request, ID_PARAM);
147+
if (CollectionUtils.isEmpty(ids)) {
148+
respondJson(response, error("Script 'id' parameter is not specified!"));
149+
return;
150+
}
151+
repository.deleteAll(ids);
152+
} catch (Exception e) {
153+
LOG.error("Script(s) cannot be deleted!", e);
154+
respondJson(response, error("Script(s) cannot be deleted! " + e.getMessage()));
155+
}
156+
break;
157+
case SYNC:
158+
try {
159+
activator.reactivateTree(ScriptRepository.ROOT);
160+
} catch (Exception e) {
161+
LOG.error("Script(s) cannot be synchronized!", e);
162+
respondJson(response, error("Script(s) cannot be synchronized! " + e.getMessage()));
163+
}
164+
break;
165+
default:
166+
break;
112167
}
113168
}
114169
}

ui.frontend/src/components/CodeExecutor.tsx

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -55,8 +55,6 @@ const CodeExecutor = () => {
5555
return () => clearInterval(intervalId);
5656
}, [appState.spaSettings.appStateInterval]);
5757

58-
console.log('executions', executions);
59-
6058
const [selectedKeys, setSelectedKeys] = useState<Selection>(new Set<Key>());
6159
const selectedIds = (selectedKeys: Selection): string[] => {
6260
if (selectedKeys === 'all') {

ui.frontend/src/components/CodeSaveButton.tsx

Lines changed: 101 additions & 61 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,12 @@
1-
import { Button, ButtonGroup, Content, Dialog, DialogContainer, Divider, Form, Heading, Radio, RadioGroup, Text, TextField } from '@adobe/react-spectrum';
2-
import { ToastQueue } from '@react-spectrum/toast';
1+
import { Button, ButtonGroup, Checkbox, Content, Dialog, DialogContainer, Divider, Form, Heading, InlineAlert, Radio, RadioGroup, Text, TextField } from '@adobe/react-spectrum';
32
import Checkmark from '@spectrum-icons/workflow/Checkmark';
43
import Close from '@spectrum-icons/workflow/Close';
54
import FileAdd from '@spectrum-icons/workflow/FileAdd';
65
import Hand from '@spectrum-icons/workflow/Hand';
76
import Launch from '@spectrum-icons/workflow/Launch';
7+
import UploadToCloud from '@spectrum-icons/workflow/UploadToCloud';
88
import React, { useState } from 'react';
9+
import { Controller, FormProvider, useForm } from 'react-hook-form';
910
import { toastRequest } from '../utils/api';
1011
import { ScriptRoots, ScriptType } from '../utils/api.types';
1112
import { Strings } from '../utils/strings';
@@ -14,43 +15,53 @@ interface CodeSaveButtonProps extends React.ComponentProps<typeof Button> {
1415
code: string;
1516
}
1617

18+
interface CodeFormValues {
19+
scriptName: string;
20+
scriptType: ScriptType;
21+
sync: boolean;
22+
}
23+
24+
function getFormDefaults(code: string): CodeFormValues {
25+
return {
26+
scriptName: '',
27+
scriptType: detectScriptType(code),
28+
sync: true,
29+
};
30+
}
31+
1732
function detectScriptType(code: string): ScriptType {
1833
const automaticPattern = /(def|Schedule)\s+scheduleRun\s*(\(\s*\))?\s*\{/;
1934
return automaticPattern.test(code) ? ScriptType.AUTOMATIC : ScriptType.MANUAL;
2035
}
2136

2237
const CodeSaveButton: React.FC<CodeSaveButtonProps> = ({ code, ...buttonProps }) => {
2338
const [dialogOpen, setDialogOpen] = useState(false);
24-
const [scriptType, setScriptType] = useState<ScriptType>(detectScriptType(code));
25-
const [scriptName, setScriptName] = useState('');
2639
const [saving, setSaving] = useState(false);
2740

28-
const scriptNameValid = Strings.checkFilePath(scriptName);
29-
const scriptId = ScriptRoots[scriptType] + '/' + (Strings.removeEnd(scriptName.trim(), '.groovy') || '{name}') + '.groovy';
41+
const methods = useForm<CodeFormValues>({ defaultValues: getFormDefaults(code) });
42+
const { control, handleSubmit, formState, reset, watch } = methods;
43+
const scriptName = watch('scriptName');
44+
const scriptType = watch('scriptType');
45+
const sync = watch('sync');
46+
const scriptId = ScriptRoots[scriptType] + '/' + (Strings.removeEnd(scriptName?.trim(), '.groovy') || '{name}') + '.groovy';
3047

31-
const handleOpen = () => setDialogOpen(true);
48+
const handleOpen = () => {
49+
reset(getFormDefaults(code));
50+
setDialogOpen(true);
51+
};
3252

3353
const handleClose = () => {
3454
setDialogOpen(false);
35-
setScriptName('');
3655
setSaving(false);
56+
reset(getFormDefaults(code));
3757
};
3858

39-
const handleSave = async () => {
40-
if (!scriptName.trim()) {
41-
ToastQueue.negative('Script name is required!');
42-
return;
43-
}
44-
if (!scriptNameValid) {
45-
ToastQueue.negative('Script name is invalid!');
46-
return;
47-
}
48-
59+
const onSubmit = async (data: CodeFormValues) => {
4960
setSaving(true);
5061
try {
5162
await toastRequest({
5263
operation: 'Save script',
53-
url: '/apps/acm/api/script.json',
64+
url: '/apps/acm/api/script.json?action=save',
5465
method: 'POST',
5566
data: {
5667
code: {
@@ -59,17 +70,19 @@ const CodeSaveButton: React.FC<CodeSaveButtonProps> = ({ code, ...buttonProps })
5970
},
6071
},
6172
});
73+
if (scriptType === ScriptType.AUTOMATIC && data.sync) {
74+
await toastRequest({
75+
method: 'POST',
76+
url: `/apps/acm/api/script.json?action=sync`,
77+
operation: `Synchronize scripts`,
78+
});
79+
}
6280
handleClose();
6381
} finally {
6482
setSaving(false);
6583
}
6684
};
6785

68-
const handleFormSubmit = async (e: React.FormEvent) => {
69-
e.preventDefault();
70-
await handleSave();
71-
};
72-
7386
return (
7487
<>
7588
<Button onPress={handleOpen} {...buttonProps}>
@@ -79,43 +92,70 @@ const CodeSaveButton: React.FC<CodeSaveButtonProps> = ({ code, ...buttonProps })
7992
<DialogContainer onDismiss={handleClose}>
8093
{dialogOpen && (
8194
<Dialog minWidth="40vw">
82-
<Heading>Save Script</Heading>
83-
<Divider />
84-
<Content>
85-
<Form validationBehavior="native" onSubmit={handleFormSubmit}>
86-
<RadioGroup label="Type" isRequired value={scriptType} onChange={(value) => setScriptType(value as ScriptType)} orientation="horizontal">
87-
<Radio value={ScriptType.MANUAL}>
88-
<Hand size="XS" />
89-
<Text marginStart="size-50">Manual</Text>
90-
</Radio>
91-
<Radio value={ScriptType.AUTOMATIC}>
92-
<Launch size="XS" />
93-
<Text marginStart="size-50">Automatic</Text>
94-
</Radio>
95-
</RadioGroup>
96-
<TextField
97-
label="Name"
98-
width="100%"
99-
value={scriptName}
100-
onChange={setScriptName}
101-
isRequired
102-
marginTop="size-200"
103-
validationState={scriptName && !scriptNameValid ? 'invalid' : undefined}
104-
errorMessage={scriptName && !scriptNameValid ? 'Invalid file path' : undefined}
105-
/>
106-
<TextField label="ID" width="100%" value={scriptId} isDisabled marginTop="size-200" />
107-
</Form>
108-
</Content>
109-
<ButtonGroup>
110-
<Button variant="secondary" onPress={handleClose}>
111-
<Close size="XS" />
112-
<Text>Cancel</Text>
113-
</Button>
114-
<Button variant="cta" type="submit" isPending={saving} isDisabled={!scriptNameValid || saving}>
115-
<Checkmark size="XS" />
116-
<Text>Save</Text>
117-
</Button>
118-
</ButtonGroup>
95+
<FormProvider {...methods}>
96+
<Heading>Save Script</Heading>
97+
<Divider />
98+
<Content>
99+
<Form onSubmit={handleSubmit(onSubmit)}>
100+
<Controller
101+
name="scriptType"
102+
control={control}
103+
render={({ field }) => (
104+
<RadioGroup {...field} label="Type" isRequired value={field.value} onChange={field.onChange} orientation="horizontal">
105+
<Radio value={ScriptType.MANUAL}>
106+
<Hand size="XS" />
107+
<Text marginStart="size-50">Manual</Text>
108+
</Radio>
109+
<Radio value={ScriptType.AUTOMATIC}>
110+
<Launch size="XS" />
111+
<Text marginStart="size-50">Automatic</Text>
112+
</Radio>
113+
</RadioGroup>
114+
)}
115+
/>
116+
<Controller
117+
name="scriptName"
118+
control={control}
119+
rules={{
120+
required: 'Value is required',
121+
validate: (value) => Strings.checkFilePath(value) || 'Value has invalid format',
122+
}}
123+
render={({ field }) => (
124+
<TextField {...field} label="Name" width="100%" isRequired marginTop="size-200" validationState={formState.errors.scriptName ? 'invalid' : undefined} errorMessage={formState.errors.scriptName?.message} />
125+
)}
126+
/>
127+
<TextField label="ID" width="100%" value={scriptId} isDisabled marginTop="size-200" />
128+
<Controller
129+
name="sync"
130+
control={control}
131+
render={({ field: { value, onChange, onBlur, name, ref } }) => (
132+
<Checkbox isHidden={scriptType === ScriptType.MANUAL} isSelected={value} onChange={onChange} onBlur={onBlur} name={name} ref={ref} marginTop="size-200">
133+
<UploadToCloud size="XS" />
134+
<Text marginStart="size-50">Synchronize</Text>
135+
</Checkbox>
136+
)}
137+
/>
138+
</Form>
139+
{scriptType === ScriptType.AUTOMATIC && (
140+
<InlineAlert width="100%" variant="notice" marginTop="size-100" UNSAFE_style={{ padding: '8px' }}>
141+
<Heading>Warning</Heading>
142+
<Content UNSAFE_style={{ padding: '6px', marginTop: '6px' }}>
143+
<Text>{sync ? 'This action may cause immediate execution on the author and publish instances.' : 'This action may cause immediate execution on the author instance.'}</Text>
144+
</Content>
145+
</InlineAlert>
146+
)}
147+
</Content>
148+
<ButtonGroup>
149+
<Button variant="secondary" onPress={handleClose}>
150+
<Close size="XS" />
151+
<Text>Cancel</Text>
152+
</Button>
153+
<Button variant="cta" isPending={saving} isDisabled={saving} onPress={() => handleSubmit(onSubmit)()} type="button">
154+
<Checkmark size="XS" />
155+
<Text>Save</Text>
156+
</Button>
157+
</ButtonGroup>
158+
</FormProvider>
119159
</Dialog>
120160
)}
121161
</DialogContainer>

0 commit comments

Comments
 (0)