Skip to content

Commit 3e05d7e

Browse files
committed
Language Selector and File Upload for UDF Code
1 parent ffd0328 commit 3e05d7e

2 files changed

Lines changed: 178 additions & 20 deletions

File tree

src/components/ParameterDataType.vue

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -47,7 +47,7 @@
4747
<!-- Budget -->
4848
<Kernel v-else-if="type === 'kernel'" v-model="state" :editable="editable" />
4949
<!-- UDF-Code -->
50-
<TextEditor class="fieldValue textarea" v-else-if="type === 'udf-code'" :id="name" :editable="editable" v-model="state" :language="dependency" />
50+
<TextEditor class="fieldValue textarea" v-else-if="type === 'udf-code'" :id="name" :editable="editable" v-model="state" :language="dependency" languageChooser />
5151
<!-- CommonMark -->
5252
<TextEditor class="fieldValue textarea" v-else-if="type === 'commonmark'" :id="name" :editable="editable" v-model="state" language="markdown" />
5353
<!-- WKT / PROJ -->
@@ -205,11 +205,11 @@ export default {
205205
},
206206
newValue() {
207207
if (this.type === 'number') {
208-
var num = Number.parseFloat(this.state);
208+
const num = Number.parseFloat(this.state);
209209
return Number.isNaN(num) ? null : num;
210210
}
211211
else if (this.type === 'integer') {
212-
var num = Number.parseInt(this.state);
212+
const num = Number.parseInt(this.state);
213213
return Number.isNaN(num) ? null : num;
214214
}
215215
else if (this.type === 'null') {

src/components/TextEditor.vue

Lines changed: 175 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -3,10 +3,22 @@
33
<div class="sourceHeader">
44
<strong v-if="title">{{ title }}</strong>
55
<div class="sourceToolbar" ref="sourceToolbar">
6-
<span class="group">
7-
<BButton v-if="editable" @click="confirmClear" title="Start from scratch - Clears the current script">
6+
<select v-if="languageChooser" class="language-chooser" :value="editorLanguage"
7+
@change="changeLanguage($event.target.value)" title="Select language">
8+
<option value="">Plain Text</option>
9+
<option value="javascript">JavaScript</option>
10+
<option value="json">JSON</option>
11+
<option value="markdown">Markdown</option>
12+
<option value="math">Math</option>
13+
<option value="python">Python</option>
14+
<option value="r">R</option>
15+
<option value="yaml">YAML</option>
16+
</select>
17+
<span class="group" v-if="editable">
18+
<BButton @click="confirmClear" title="Start from scratch - Clears the current script">
819
<i class="fas fa-file"></i> <span class="text">New</span>
920
</BButton>
21+
<AsyncButton :fn="upload" fa icon="fas fa-cloud-upload-alt" title="Upload a file from your computer - Clears the current script"><span class="text">Upload</span></AsyncButton>
1022
<slot name="file-toolbar"></slot>
1123
</span>
1224
<span class="group" v-if="editable">
@@ -30,6 +42,7 @@
3042
import Utils from '../utils.js';
3143
import FullscreenButton from './FullscreenButton.vue';
3244
import ToolbarCompactMixin from './ToolbarCompactMixin.js';
45+
import AsyncButton from '@openeo/vue-components/components/internal/AsyncButton.vue';
3346
import BButton from '@openeo/vue-components/components/internal/BButton.vue';
3447
import { ProcessGraph } from '@openeo/js-processgraphs';
3548
@@ -58,6 +71,7 @@ export default {
5871
name: 'TextEditor',
5972
mixins: [ToolbarCompactMixin],
6073
components: {
74+
AsyncButton,
6175
BButton,
6276
FullscreenButton
6377
},
@@ -80,12 +94,28 @@ export default {
8094
},
8195
title: {
8296
type: String
97+
},
98+
languageChooser: {
99+
type: Boolean,
100+
default: false
83101
}
84102
},
85103
computed: {
86104
...Utils.mapGetters(['processes']),
87105
languageString() {
88-
return typeof this.language === 'string' ? this.language.toLowerCase() : '';
106+
return typeof this.activeLanguage === 'string' ? this.activeLanguage.toLowerCase() : '';
107+
},
108+
editorLanguage() {
109+
// Some additional aliases for some languages
110+
switch (this.languageString) {
111+
case 'processgraph':
112+
return 'json';
113+
case 'cwl':
114+
case 'eoap-cwl':
115+
return 'yaml';
116+
default:
117+
return this.languageString;
118+
}
89119
},
90120
editorOptions() {
91121
let options = {
@@ -95,9 +125,13 @@ export default {
95125
matchBrackets: true,
96126
autoCloseBrackets: true,
97127
readOnly: !this.editable,
98-
placeholder: this.placeholder
128+
placeholder: this.placeholder,
129+
mode: null,
130+
gutters: [],
131+
lint: false,
132+
lineWrapping: false
99133
};
100-
switch(this.languageString) {
134+
switch(this.editorLanguage) {
101135
case 'r':
102136
options.mode = 'text/x-rsrc';
103137
break;
@@ -115,17 +149,13 @@ export default {
115149
options.mode = 'text/javascript';
116150
break;
117151
case 'json':
118-
case 'processgraph':
119152
options.mode = 'application/json';
120153
options.gutters = ['CodeMirror-lint-markers'];
121154
options.lint = true;
122155
break;
123156
case 'yaml':
124-
case 'yaml':
125-
case 'cwl':
126157
options.mode = 'text/x-yaml';
127158
options.gutters = ['CodeMirror-lint-markers'];
128-
options.lint = true;
129159
break;
130160
}
131161
return options;
@@ -137,7 +167,8 @@ export default {
137167
canRedo: false,
138168
editor: null,
139169
emitValue: this.value,
140-
element: null
170+
element: null,
171+
activeLanguage: this.language
141172
}
142173
},
143174
watch: {
@@ -147,11 +178,13 @@ export default {
147178
this.editor.clearHistory();
148179
}
149180
},
150-
editorOptions() {
151-
for(var key in this.editorOptions) {
152-
this.editor.setOption(key, this.editorOptions[key]);
181+
language(newLang) {
182+
this.activeLanguage = newLang;
183+
},
184+
editorOptions(newOptions) {
185+
for(var key in newOptions) {
186+
this.editor.setOption(key, newOptions[key]);
153187
}
154-
this.updateContent();
155188
}
156189
},
157190
mounted() {
@@ -177,12 +210,125 @@ export default {
177210
this.element = this.$el;
178211
},
179212
methods: {
213+
async upload() {
214+
// Create temporary file input element to trigger file selection dialog
215+
const fileInput = document.createElement('input');
216+
fileInput.type = 'file';
217+
fileInput.style.display = 'none';
218+
document.body.appendChild(fileInput);
219+
try {
220+
// Click the file input to open the file dialog
221+
fileInput.click();
222+
// Wait for the user to select a file (or cancel)
223+
const file = await new Promise(resolve => {
224+
fileInput.addEventListener('change', () => {
225+
resolve(fileInput.files[0] || null);
226+
}, { once: true });
227+
// Handle cancel: when focus returns without a change event
228+
window.addEventListener('focus', () => {
229+
setTimeout(() => resolve(null), 300);
230+
}, { once: true });
231+
});
232+
if (!file) {
233+
return false;
234+
}
235+
// Read the file content as text
236+
const content = await new Promise((resolve, reject) => {
237+
const reader = new FileReader();
238+
reader.onload = () => resolve(reader.result);
239+
reader.onerror = () => reject(reader.error);
240+
reader.readAsText(file);
241+
});
242+
if (!this.confirmClear()) {
243+
return false;
244+
}
245+
this.insert(content);
246+
if (this.languageChooser) {
247+
const lang = this.detectLanguage(file);
248+
if (lang !== null) {
249+
this.activeLanguage = lang;
250+
this.$emit('update:language', lang);
251+
}
252+
}
253+
this.commit();
254+
return true;
255+
} catch (error) {
256+
Utils.exception(this, error, "Upload failed");
257+
return false;
258+
} finally {
259+
// Clean up the temporary file input element
260+
document.body.removeChild(fileInput);
261+
}
262+
},
263+
detectLanguage(file) {
264+
// Try to detect language from MIME type
265+
if (typeof file.type === 'string' && file.type.includes('/')) {
266+
switch (file.type.toLowerCase().replace(/;.*$/, '').trim()) {
267+
case 'application/json':
268+
case 'application/geo+json':
269+
case 'text/json':
270+
return 'json';
271+
case 'application/javascript':
272+
case 'text/javascript':
273+
return 'javascript';
274+
case 'text/markdown':
275+
return 'markdown';
276+
case 'text/x-python':
277+
case 'application/x-python':
278+
return 'python';
279+
case 'application/x-yaml':
280+
case 'text/yaml':
281+
case 'text/x-yaml':
282+
return 'yaml';
283+
case 'text/plain':
284+
break; // fall through to extension detection
285+
default:
286+
return null;
287+
}
288+
}
289+
// Fallback: detect by file extension
290+
if (typeof file.name === 'string') {
291+
const ext = file.name.split('.').pop().trim().toLowerCase();
292+
switch (ext) {
293+
case 'json':
294+
case 'geojson':
295+
return 'json';
296+
case 'js':
297+
case 'mjs':
298+
case 'cjs':
299+
return 'javascript';
300+
case 'md':
301+
case 'markdown':
302+
return 'markdown';
303+
case 'py':
304+
return 'python';
305+
case 'r':
306+
return 'r';
307+
case 'yml':
308+
case 'yaml':
309+
case 'cwl':
310+
return 'yaml';
311+
}
312+
}
313+
return null;
314+
},
315+
changeLanguage(lang) {
316+
this.activeLanguage = lang;
317+
this.$emit('update:language', lang);
318+
this.$nextTick(() => {
319+
this.editor.refresh();
320+
});
321+
},
180322
confirmClear() {
181-
var confirmed = confirm("Do you really want to clear the existing code?");
323+
if (this.editor.getValue().trim() === "") {
324+
return true;
325+
}
326+
const confirmed = confirm("Do you really want to clear the existing code?");
182327
if (confirmed) {
183328
this.insert("");
184329
this.emit(null);
185330
}
331+
return confirmed;
186332
},
187333
updateState() {
188334
// Don't lint empty values
@@ -214,7 +360,10 @@ export default {
214360
return this.emit(updateContext ? null : "");
215361
case 'json':
216362
if (value) {
217-
return this.emit(JSON.parse(value));
363+
if (typeof value === 'string') {
364+
value = JSON.parse(value);
365+
}
366+
return this.emit(value);
218367
}
219368
else {
220369
return this.emit(null);
@@ -256,7 +405,11 @@ export default {
256405
}
257406
break;
258407
case 'json':
259-
this.insert(JSON.stringify(this.value, null, this.editorOptions.indentUnit));
408+
let value = this.value;
409+
if (typeof this.value !== 'string') {
410+
value = JSON.stringify(this.value, null, this.editorOptions.indentUnit);
411+
}
412+
this.insert(value);
260413
break;
261414
default:
262415
this.insert(this.value);
@@ -293,6 +446,11 @@ export default {
293446
height: 100%;
294447
overflow: hidden;
295448
}
449+
.language-chooser {
450+
padding: 2px 4px;
451+
font-size: 0.9em;
452+
width: auto !important;
453+
}
296454
</style>
297455
<style>
298456
.textEditor.math .cm-operator {

0 commit comments

Comments
 (0)