Config UI: clean yaml on paste (#19148)
This commit is contained in:
parent
d405ee4ba6
commit
af8a57544a
12 changed files with 113 additions and 5 deletions
|
|
@ -5,6 +5,7 @@
|
|||
:description="$t('config.circuits.description')"
|
||||
docs="/docs/features/loadmanagement"
|
||||
:defaultYaml="defaultYaml"
|
||||
removeKey="circuits"
|
||||
endpoint="/config/circuits"
|
||||
@changed="$emit('changed')"
|
||||
/>
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@
|
|||
:description="$t('config.eebus.description')"
|
||||
docs="/docs/reference/configuration/eebus"
|
||||
:defaultYaml="defaultYaml"
|
||||
removeKey="eebus"
|
||||
endpoint="/config/eebus"
|
||||
@changed="$emit('changed')"
|
||||
/>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@
|
|||
docs="/docs/reference/configuration/hems"
|
||||
:defaultYaml="defaultYaml"
|
||||
endpoint="/config/hems"
|
||||
removeKey="hems"
|
||||
size="md"
|
||||
@changed="$emit('changed')"
|
||||
/>
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@
|
|||
docs="/docs/reference/configuration/messaging"
|
||||
:defaultYaml="defaultYaml"
|
||||
endpoint="/config/messaging"
|
||||
removeKey="messaging"
|
||||
data-testid="messaging-modal"
|
||||
@changed="$emit('changed')"
|
||||
/>
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@
|
|||
docs="/docs/reference/configuration/modbusproxy"
|
||||
:defaultYaml="defaultYaml"
|
||||
endpoint="/config/modbusproxy"
|
||||
removeKey="modbusproxy"
|
||||
size="md"
|
||||
@changed="$emit('changed')"
|
||||
/>
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
@ -53,6 +53,7 @@
|
|||
rows="5"
|
||||
spellcheck="false"
|
||||
class="form-control"
|
||||
@paste="handlePaste"
|
||||
/>
|
||||
</FormRow>
|
||||
</div>
|
||||
|
|
@ -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 },
|
||||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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')"
|
||||
|
|
|
|||
|
|
@ -25,6 +25,8 @@
|
|||
|
||||
<script>
|
||||
import { VueMonacoEditor, loader } from "@guolao/vue-monaco-editor";
|
||||
import { cleanYaml } from "../../utils/cleanYaml";
|
||||
|
||||
const $html = document.querySelector("html");
|
||||
export default {
|
||||
name: "YamlEditor",
|
||||
|
|
@ -32,6 +34,7 @@ export default {
|
|||
props: {
|
||||
modelValue: String,
|
||||
errorLine: Number,
|
||||
removeKey: String,
|
||||
disabled: Boolean,
|
||||
},
|
||||
emits: ["update:modelValue"],
|
||||
|
|
@ -50,6 +53,7 @@ export default {
|
|||
overviewRulerLanes: 0,
|
||||
},
|
||||
active: true,
|
||||
pasteDisposable: null,
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
|
|
@ -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();
|
||||
|
|
|
|||
|
|
@ -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">
|
||||
|
|
@ -54,6 +59,7 @@ export default {
|
|||
docs: String,
|
||||
endpoint: String,
|
||||
defaultYaml: String,
|
||||
removeKey: String,
|
||||
size: { type: String, default: "xl" },
|
||||
},
|
||||
emits: ["changed"],
|
||||
|
|
|
|||
31
assets/js/utils/cleanYaml.test.ts
Normal file
31
assets/js/utils/cleanYaml.test.ts
Normal file
|
|
@ -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
assets/js/utils/cleanYaml.ts
Normal file
43
assets/js/utils/cleanYaml.ts
Normal file
|
|
@ -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");
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue