Skip to content

Commit cab83b2

Browse files
Harden Compose editor interactions
1 parent c27df5b commit cab83b2

5 files changed

Lines changed: 59 additions & 4 deletions

File tree

web/src/components/applications/ComposeCodeEditor.vue

Lines changed: 33 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,8 +23,10 @@ const props = withDefaults(
2323
const emit = defineEmits<{
2424
"update:modelValue": [value: string];
2525
validate: [];
26+
error: [message: string];
2627
}>();
2728
const editorHost = ref<HTMLElement | null>(null);
29+
const fileInput = ref<HTMLInputElement | null>(null);
2830
let view: EditorView | null = null;
2931
3032
const example = `services:
@@ -93,12 +95,32 @@ function clear() {
9395
function handleDrop(event: DragEvent) {
9496
event.preventDefault();
9597
const file = event.dataTransfer?.files?.[0];
96-
if (!file || !/\.ya?ml$/i.test(file.name)) return;
98+
if (!file) return;
99+
readFile(file);
100+
}
101+
function readFile(file: File) {
102+
if (!/\.ya?ml$/i.test(file.name)) {
103+
emit("error", "Selecione um arquivo .yml ou .yaml.");
104+
return;
105+
}
106+
if (file.size > 1024 * 1024) {
107+
emit("error", "O arquivo Compose deve ter no máximo 1 MB.");
108+
return;
109+
}
97110
file.text().then((value) => {
98111
setValue(value);
99112
emit("update:modelValue", value);
100113
});
101114
}
115+
function chooseFile() {
116+
fileInput.value?.click();
117+
}
118+
function handleFileInput(event: Event) {
119+
const input = event.target as HTMLInputElement;
120+
const file = input.files?.[0];
121+
if (file) readFile(file);
122+
input.value = "";
123+
}
102124
onMounted(() => {
103125
if (editorHost.value) {
104126
view = new EditorView({
@@ -115,11 +137,21 @@ watch(() => props.modelValue, setValue);
115137
<div class="compose-editor-shell">
116138
<div class="compose-toolbar">
117139
<span class="compose-file">compose.yml</span>
140+
<button type="button" class="ghost" @click="chooseFile">
141+
Carregar arquivo
142+
</button>
118143
<button type="button" class="ghost" @click="loadExample">
119144
Inserir exemplo
120145
</button>
121146
<button type="button" class="ghost" @click="clear">Limpar</button>
122147
</div>
148+
<input
149+
ref="fileInput"
150+
class="visually-hidden"
151+
type="file"
152+
accept=".yml,.yaml,text/yaml"
153+
@change="handleFileInput"
154+
/>
123155
<div class="compose-dropzone" @dragover.prevent @drop="handleDrop">
124156
<div
125157
ref="editorHost"

web/src/style.css

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -345,6 +345,17 @@ h2 {
345345
color: #747c8c;
346346
font-size: 11px;
347347
}
348+
.visually-hidden {
349+
position: absolute;
350+
width: 1px;
351+
height: 1px;
352+
padding: 0;
353+
margin: -1px;
354+
overflow: hidden;
355+
clip: rect(0, 0, 0, 0);
356+
white-space: nowrap;
357+
border: 0;
358+
}
348359
.compose-config-grid {
349360
display: grid;
350361
grid-template-columns: minmax(0, 1.8fr) minmax(190px, 1fr);

web/src/views/ApplicationDetail.test.ts

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,12 @@ describe("ApplicationDetail", () => {
3838
});
3939
await router.push("/projects/2/applications/1?tab=source");
4040
await router.isReady();
41-
const wrapper = mount(ApplicationDetail, { global: { plugins: [router] } });
41+
const wrapper = mount(ApplicationDetail, {
42+
global: {
43+
plugins: [router],
44+
stubs: { ComposeCodeEditor: { template: "<div />" } },
45+
},
46+
});
4247
await new Promise((resolve) => setTimeout(resolve, 0));
4348
expect(wrapper.text()).toContain("Frontend");
4449
expect(wrapper.text()).toContain("Docker Compose");

web/src/views/ApplicationDetail.vue

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -408,7 +408,11 @@ onMounted(load);
408408
</div>
409409
<div v-if="sourceType === 'compose'">
410410
<label>Docker Compose</label>
411-
<ComposeCodeEditor v-model="composeYaml" @validate="validateSource" />
411+
<ComposeCodeEditor
412+
v-model="composeYaml"
413+
@validate="validateSource"
414+
@error="toast.error"
415+
/>
412416
</div>
413417
<div v-else-if="sourceType === 'image'" class="form-grid">
414418
<label

web/src/views/ProjectDetail.vue

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -522,7 +522,10 @@ onMounted(load);
522522
<div v-if="newApp.source_type === 'compose'">
523523
<h3>Docker Compose</h3>
524524
<div class="compose-config-grid">
525-
<ComposeCodeEditor v-model="newSource.compose_yaml" />
525+
<ComposeCodeEditor
526+
v-model="newSource.compose_yaml"
527+
@error="toast.error"
528+
/>
526529
<aside class="source-summary">
527530
<h3>Resumo detectado</h3>
528531
<p v-if="!newSource.compose_yaml" class="muted">

0 commit comments

Comments
 (0)