Skip to content

Commit

Permalink
Config UI: clean yaml on paste (#19148)
Browse files Browse the repository at this point in the history
  • Loading branch information
naltatis authored Feb 24, 2025
1 parent d405ee4 commit af8a575
Show file tree
Hide file tree
Showing 12 changed files with 113 additions and 5 deletions.
1 change: 1 addition & 0 deletions assets/js/components/Config/CircuitsModal.vue
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
:description="$t('config.circuits.description')"
docs="/docs/features/loadmanagement"
:defaultYaml="defaultYaml"
removeKey="circuits"
endpoint="/config/circuits"
@changed="$emit('changed')"
/>
Expand Down
1 change: 1 addition & 0 deletions assets/js/components/Config/EebusModal.vue
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
:description="$t('config.eebus.description')"
docs="/docs/reference/configuration/eebus"
:defaultYaml="defaultYaml"
removeKey="eebus"
endpoint="/config/eebus"
@changed="$emit('changed')"
/>
Expand Down
4 changes: 2 additions & 2 deletions assets/js/components/Config/FormRow.vue
Original file line number Diff line number Diff line change
Expand Up @@ -19,9 +19,9 @@
<div v-if="example" class="hyphenate">
{{ $t("config.form.example") }}: {{ example }}
</div>
<div v-if="help">
<div v-if="help" class="d-flex gap-1">
<Markdown :markdown="help" class="text-gray hyphenate" />
<a v-if="link" class="ms-1 text-gray" :href="link" target="_blank">
<a v-if="link" class="text-gray" :href="link" target="_blank">
{{ $t("config.general.docsLink") }}
</a>
</div>
Expand Down
1 change: 1 addition & 0 deletions assets/js/components/Config/HemsModal.vue
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
docs="/docs/reference/configuration/hems"
:defaultYaml="defaultYaml"
endpoint="/config/hems"
removeKey="hems"
size="md"
@changed="$emit('changed')"
/>
Expand Down
1 change: 1 addition & 0 deletions assets/js/components/Config/MessagingModal.vue
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
docs="/docs/reference/configuration/messaging"
:defaultYaml="defaultYaml"
endpoint="/config/messaging"
removeKey="messaging"
data-testid="messaging-modal"
@changed="$emit('changed')"
/>
Expand Down
1 change: 1 addition & 0 deletions assets/js/components/Config/ModbusProxyModal.vue
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
docs="/docs/reference/configuration/modbusproxy"
:defaultYaml="defaultYaml"
endpoint="/config/modbusproxy"
removeKey="modbusproxy"
size="md"
@changed="$emit('changed')"
/>
Expand Down
11 changes: 9 additions & 2 deletions assets/js/components/Config/SponsorModal.vue
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@
:help="
$t('config.sponsor.descriptionToken', { url: 'https://sponsor.evcc.io/' })
"
docs-link="/docs/sponsorship"
docs-link="/docs/sponsorship#trial"
class="mt-4"
>
<textarea
Expand All @@ -53,6 +53,7 @@
rows="5"
spellcheck="false"
class="form-control"
@paste="handlePaste"
/>
</FormRow>
</div>
Expand All @@ -67,7 +68,7 @@ import Sponsor, { VICTRON_DEVICE } from "../Sponsor.vue";
import SponsorTokenExpires from "../SponsorTokenExpires.vue";
import store from "../../store";
import { docsPrefix } from "../../i18n";
import { cleanYaml } from "../../utils/cleanYaml";
export default {
name: "SponsorModal",
components: { FormRow, JsonModal, Sponsor, SponsorTokenExpires },
Expand Down Expand Up @@ -96,6 +97,12 @@ export default {
transformReadValues() {
return { token: "" };
},
handlePaste(event) {
event.preventDefault();
const text = event.clipboardData.getData("text");
const cleaned = cleanYaml(text, "sponsortoken");
event.target.value = cleaned;
},
},
};
</script>
Expand Down
1 change: 1 addition & 0 deletions assets/js/components/Config/TariffsModal.vue
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
:description="$t('config.tariffs.description')"
docs="/docs/reference/configuration/tariffs"
:defaultYaml="defaultYaml"
removeKey="tariffs"
endpoint="/config/tariffs"
data-testid="tariffs-modal"
@changed="$emit('changed')"
Expand Down
15 changes: 15 additions & 0 deletions assets/js/components/Config/YamlEditor.vue
Original file line number Diff line number Diff line change
Expand Up @@ -25,13 +25,16 @@

<script>
import { VueMonacoEditor, loader } from "@guolao/vue-monaco-editor";
import { cleanYaml } from "../../utils/cleanYaml";
const $html = document.querySelector("html");
export default {
name: "YamlEditor",
components: { VueMonacoEditor },
props: {
modelValue: String,
errorLine: Number,
removeKey: String,
disabled: Boolean,
},
emits: ["update:modelValue"],
Expand All @@ -50,6 +53,7 @@ export default {
overviewRulerLanes: 0,
},
active: true,
pasteDisposable: null,
};
},
computed: {
Expand Down Expand Up @@ -79,12 +83,23 @@ export default {
},
unmounted() {
$html.removeEventListener("themechange", this.updateTheme);
this.pasteDisposable?.dispose();
},
methods: {
updateTheme() {
this.theme = $html.classList.contains("dark") ? "vs-dark" : "vs";
},
ready(editor, monaco) {
const disposable = editor.onDidPaste(async () => {
if (!this.removeKey) return;
await this.$nextTick();
const model = editor.getModel();
const cleaned = cleanYaml(model.getValue(), this.removeKey);
model.setValue(cleaned);
});
this.pasteDisposable = disposable;
let decorations = null;
const highlight = () => {
decorations?.clear();
Expand Down
8 changes: 7 additions & 1 deletion assets/js/components/Config/YamlModal.vue
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,12 @@
<p v-if="error" class="text-danger" data-testid="error">{{ error }}</p>
<form ref="form" class="container mx-0 px-0">
<div class="editor-container" :style="{ height }">
<YamlEditor v-model="yaml" class="editor" :errorLine="errorLine" />
<YamlEditor
v-model="yaml"
class="editor"
:errorLine="errorLine"
:removeKey="removeKey"
/>
</div>

<div class="mt-4 d-flex justify-content-between">
Expand Down Expand Up @@ -54,6 +59,7 @@ export default {
docs: String,
endpoint: String,
defaultYaml: String,
removeKey: String,
size: { type: String, default: "xl" },
},
emits: ["changed"],
Expand Down
31 changes: 31 additions & 0 deletions assets/js/utils/cleanYaml.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import { describe, expect, test } from "vitest";
import { cleanYaml } from "./cleanYaml";

describe("cleanYaml", () => {
test("removes key from single line", () => {
const result = cleanYaml("key: value", "key");
expect(result).toBe("value");
});
test("removes key from multi-line yaml", () => {
const input = `key:\n nested: value\n another: thing`;
const expected = `nested: value\nanother: thing`;
const result = cleanYaml(input, "key");
expect(result).toBe(expected);
});
test("keep untouched if key not found", () => {
const result = cleanYaml("key: value", "not-found");
expect(result).toBe("key: value");
});
test("trim whitespace at the end of the line", () => {
const result = cleanYaml("key: \n - foo \n - bar ", "key");
expect(result).toBe("- foo\n- bar");
});
test("should remove leading comment lines", () => {
const result = cleanYaml("# this is\n# a comment\nkey: value", "key");
expect(result).toBe("value");
});
test("should note remove leading comment lines if key is not found", () => {
const result = cleanYaml("# this is\n# a comment\nnot-found: value", "key");
expect(result).toBe("# this is\n# a comment\nnot-found: value");
});
});
43 changes: 43 additions & 0 deletions assets/js/utils/cleanYaml.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
// accepts a multiline yaml string. if it starts with a key, the key will be removed, and the indent level will be reduced by one
// example 1: "key: value" -> "value"
// example 2:
// """
// key:
// foo: bar
// """
// will be transformed into:
// """
// foo: bar
// """
export function cleanYaml(text: string, removeKey: string) {
if (!removeKey) return text;
const result: string[] = [];

const prefix = removeKey + ":";
const lines = text.split("\n");

// remove first comment lines
while (lines[0].startsWith("#")) lines.shift();

const [firstLine, ...restLines] = lines;

if (!firstLine.startsWith(prefix)) {
// does not start with key, skip
return text;
} else {
const first = firstLine.slice(prefix.length).trim();
if (first) {
result.push(first);
}
}

if (restLines.length > 0) {
const indentChars = restLines[0].match(/^(\s+)/)?.[0] || "";
restLines
.map((l) => (l.startsWith(indentChars) ? l.slice(indentChars.length) : l))
.map((l) => l.trimEnd())
.forEach((l) => result.push(l));
}

return result.join("\n");
}

0 comments on commit af8a575

Please sign in to comment.