Config UI: replace Monaco editor with bundled CodeMirror 6 (#30855)
This commit is contained in:
parent
7c98d6db25
commit
81a84fcf81
8 changed files with 387 additions and 208 deletions
|
|
@ -1,41 +1,12 @@
|
|||
<template>
|
||||
<VueMonacoEditor
|
||||
v-if="active"
|
||||
ref="editor"
|
||||
class="editor"
|
||||
language="yaml"
|
||||
:theme="theme"
|
||||
:options="options"
|
||||
:value="modelValue"
|
||||
data-testid="yaml-editor"
|
||||
@update:value="$emit('update:modelValue', $event)"
|
||||
@mount="ready"
|
||||
>
|
||||
<template #default> {{ $t("config.editor.loading") }} </template>
|
||||
<template #failure>
|
||||
<textarea
|
||||
class="form-control"
|
||||
:rows="lines"
|
||||
:value="modelValue"
|
||||
:disabled="disabled"
|
||||
data-testid="yaml-editor-fallback"
|
||||
@input="$emit('update:modelValue', $event.target.value)"
|
||||
/>
|
||||
</template>
|
||||
</VueMonacoEditor>
|
||||
<div ref="host" class="editor" data-testid="yaml-editor">
|
||||
<div v-if="loading" class="editor-loading">{{ $t("config.editor.loading") }}</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { VueMonacoEditor, loader } from "@guolao/vue-monaco-editor";
|
||||
import { cleanYaml } from "@/utils/cleanYaml.js";
|
||||
// don't bundle monaco-editor but ensure that it get updated regularly
|
||||
import { packages } from "../../../../package-lock.json";
|
||||
const monacoVersion = packages["node_modules/monaco-editor"].version;
|
||||
|
||||
const $html = document.querySelector("html");
|
||||
export default {
|
||||
name: "YamlEditor",
|
||||
components: { VueMonacoEditor },
|
||||
props: {
|
||||
modelValue: String,
|
||||
errorLine: Number,
|
||||
|
|
@ -45,101 +16,117 @@ export default {
|
|||
emits: ["update:modelValue"],
|
||||
data() {
|
||||
return {
|
||||
theme: "vs",
|
||||
defaultOptions: {
|
||||
automaticLayout: true,
|
||||
formatOnType: true,
|
||||
formatOnPaste: true,
|
||||
minimap: { enabled: false },
|
||||
showFoldingControls: "always",
|
||||
scrollBeyondLastLine: false,
|
||||
wordWrap: "off",
|
||||
wrappingStrategy: "advanced",
|
||||
overviewRulerLanes: 0,
|
||||
scrollbar: {
|
||||
alwaysConsumeMouseWheel: false,
|
||||
},
|
||||
},
|
||||
active: true,
|
||||
pasteDisposable: null,
|
||||
loading: true,
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
options() {
|
||||
return { ...this.defaultOptions, readOnly: this.disabled };
|
||||
},
|
||||
lines() {
|
||||
return (this.modelValue || "").split("\n").length;
|
||||
},
|
||||
},
|
||||
watch: {
|
||||
errorLine() {
|
||||
// force rerender to update decorations
|
||||
this.active = false;
|
||||
this.$nextTick(() => (this.active = true));
|
||||
modelValue(value) {
|
||||
this.editor?.setDoc(value || "");
|
||||
},
|
||||
errorLine(line) {
|
||||
this.editor?.setErrorLine(line || null);
|
||||
},
|
||||
disabled(value) {
|
||||
this.editor?.setReadOnly(value);
|
||||
},
|
||||
},
|
||||
mounted() {
|
||||
this.updateTheme();
|
||||
$html.addEventListener("themechange", this.updateTheme);
|
||||
created() {
|
||||
// kept off the reactive data() so Vue doesn't proxy the EditorView
|
||||
this.editor = null;
|
||||
this.destroyed = false;
|
||||
},
|
||||
beforeMount() {
|
||||
loader.config({
|
||||
paths: { vs: `https://cdn.jsdelivr.net/npm/monaco-editor@${monacoVersion}/min/vs` },
|
||||
async mounted() {
|
||||
const { createEditor } = await import("./codemirror");
|
||||
// component may have been torn down while the chunk loaded
|
||||
if (this.destroyed || !this.$refs.host) return;
|
||||
|
||||
this.editor = createEditor({
|
||||
parent: this.$refs.host,
|
||||
doc: this.modelValue || "",
|
||||
readOnly: this.disabled,
|
||||
onChange: (value) => this.$emit("update:modelValue", value),
|
||||
getRemoveKey: () => this.removeKey,
|
||||
});
|
||||
loader.init();
|
||||
this.loading = false;
|
||||
|
||||
if (this.errorLine) this.editor.setErrorLine(this.errorLine);
|
||||
},
|
||||
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();
|
||||
if (this.errorLine) {
|
||||
decorations = editor.createDecorationsCollection([
|
||||
{
|
||||
range: new monaco.Range(this.errorLine, 1, this.errorLine, 1),
|
||||
options: {
|
||||
isWholeLine: true,
|
||||
className: "error",
|
||||
lineNumberClassName: "error",
|
||||
marginClassName: "error",
|
||||
},
|
||||
},
|
||||
]);
|
||||
}
|
||||
};
|
||||
editor.onDidChangeModelContent(() => highlight());
|
||||
highlight();
|
||||
},
|
||||
this.destroyed = true;
|
||||
this.editor?.destroy();
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.editor :global(.monaco-editor) {
|
||||
--vscode-editor-background: var(--evcc-box) !important;
|
||||
--vscode-editorGutter-background: var(--evcc-box-border) !important;
|
||||
}
|
||||
.editor :global(.error) {
|
||||
background-color: var(--bs-danger-50) !important;
|
||||
}
|
||||
.editor {
|
||||
border: 1px solid var(--bs-border-color);
|
||||
min-height: 150px;
|
||||
}
|
||||
.editor-loading {
|
||||
padding: 0.5rem 0.75rem;
|
||||
color: var(--bs-secondary-color);
|
||||
}
|
||||
|
||||
/* editor chrome */
|
||||
.editor :global(.cm-editor) {
|
||||
height: 100%;
|
||||
font-size: 13px;
|
||||
background-color: var(--evcc-box);
|
||||
color: var(--bs-body-color);
|
||||
}
|
||||
.editor :global(.cm-editor.cm-focused) {
|
||||
outline: none;
|
||||
}
|
||||
.editor :global(.cm-content) {
|
||||
font-family: var(--bs-font-monospace, monospace);
|
||||
}
|
||||
/* override CM's built-in light gutter colors (we don't load CM's dark chrome) */
|
||||
.editor :global(.cm-gutters) {
|
||||
background-color: var(--evcc-box-border) !important;
|
||||
color: var(--bs-secondary-color) !important;
|
||||
border: none;
|
||||
padding-left: 6px;
|
||||
}
|
||||
.editor :global(.cm-activeLine),
|
||||
.editor :global(.cm-activeLineGutter) {
|
||||
background-color: transparent;
|
||||
}
|
||||
.editor :global(.cm-cursor) {
|
||||
border-left-color: var(--bs-body-color);
|
||||
}
|
||||
.editor :global(.cm-selectionBackground),
|
||||
.editor :global(.cm-focused .cm-selectionBackground) {
|
||||
background-color: var(--evcc-gray-50);
|
||||
}
|
||||
.editor :global(.cm-errorLine) {
|
||||
background-color: var(--bs-danger) !important;
|
||||
}
|
||||
/* keep error text readable over the red regardless of syntax colors */
|
||||
.editor :global(.cm-errorLine),
|
||||
.editor :global(.cm-errorLine span) {
|
||||
color: var(--bs-white) !important;
|
||||
}
|
||||
|
||||
/* syntax tokens (classHighlighter), bootstrap palette reads well on light and dark */
|
||||
.editor :global(.tok-comment),
|
||||
.editor :global(.tok-meta) {
|
||||
color: var(--bs-gray-medium);
|
||||
}
|
||||
.editor :global(.tok-propertyName),
|
||||
.editor :global(.tok-keyword) {
|
||||
color: var(--bs-orange);
|
||||
}
|
||||
.editor :global(.tok-string),
|
||||
.editor :global(.tok-string2) {
|
||||
color: var(--bs-green);
|
||||
}
|
||||
.editor :global(.tok-bool),
|
||||
.editor :global(.tok-atom),
|
||||
.editor :global(.tok-number) {
|
||||
color: var(--bs-purple);
|
||||
}
|
||||
.editor :global(.tok-invalid) {
|
||||
color: var(--bs-red);
|
||||
}
|
||||
</style>
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
<template>
|
||||
<div class="editor-container" :style="{ height: computedHeight }">
|
||||
<div class="editor-container">
|
||||
<YamlEditor
|
||||
v-if="!hidden"
|
||||
v-model="localValue"
|
||||
|
|
@ -28,11 +28,6 @@ export default {
|
|||
localValue: this.modelValue,
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
computedHeight() {
|
||||
return Math.max(150, (this.localValue || "").split("\n").length * 18) + 22 + "px";
|
||||
},
|
||||
},
|
||||
watch: {
|
||||
modelValue: {
|
||||
handler(newVal) {
|
||||
|
|
|
|||
127
assets/js/components/Config/codemirror.ts
Normal file
127
assets/js/components/Config/codemirror.ts
Normal file
|
|
@ -0,0 +1,127 @@
|
|||
// Separate module so YamlEditor.vue can dynamic-import it, keeping CodeMirror out of the main chunk.
|
||||
import { EditorState, Compartment, StateField, StateEffect } from "@codemirror/state";
|
||||
import { EditorView, lineNumbers, keymap, Decoration, type DecorationSet } from "@codemirror/view";
|
||||
import {
|
||||
indentOnInput,
|
||||
foldGutter,
|
||||
syntaxHighlighting,
|
||||
bracketMatching,
|
||||
} from "@codemirror/language";
|
||||
import { defaultKeymap, history, historyKeymap, indentWithTab } from "@codemirror/commands";
|
||||
import { yaml } from "@codemirror/lang-yaml";
|
||||
import { classHighlighter } from "@lezer/highlight";
|
||||
import { cleanYaml } from "@/utils/cleanYaml";
|
||||
|
||||
// 1-based line, null clears
|
||||
const setErrorLineEffect = StateEffect.define<number | null>();
|
||||
|
||||
const errorLineField = StateField.define<number | null>({
|
||||
create: () => null,
|
||||
update(value, tr) {
|
||||
let next = value;
|
||||
for (const e of tr.effects) {
|
||||
if (e.is(setErrorLineEffect)) next = e.value;
|
||||
}
|
||||
return next;
|
||||
},
|
||||
});
|
||||
|
||||
function errorLineFrom(state: EditorState) {
|
||||
const line = state.field(errorLineField);
|
||||
if (!line || line < 1 || line > state.doc.lines) return null;
|
||||
return state.doc.line(line);
|
||||
}
|
||||
|
||||
const errorLineExtension = [
|
||||
errorLineField,
|
||||
EditorView.decorations.compute([errorLineField, "doc"], (state): DecorationSet => {
|
||||
const line = errorLineFrom(state);
|
||||
if (!line) return Decoration.none;
|
||||
return Decoration.set([Decoration.line({ class: "cm-errorLine" }).range(line.from)]);
|
||||
}),
|
||||
];
|
||||
|
||||
export interface EditorOptions {
|
||||
parent: HTMLElement;
|
||||
doc: string;
|
||||
readOnly: boolean;
|
||||
onChange: (value: string) => void;
|
||||
getRemoveKey: () => string | undefined;
|
||||
}
|
||||
|
||||
export interface EditorController {
|
||||
view: EditorView;
|
||||
setDoc(text: string): void;
|
||||
setErrorLine(line: number | null): void;
|
||||
setReadOnly(readOnly: boolean): void;
|
||||
destroy(): void;
|
||||
}
|
||||
|
||||
export function createEditor(opts: EditorOptions): EditorController {
|
||||
const readOnlyCompartment = new Compartment();
|
||||
const readOnlyExt = (readOnly: boolean) => [
|
||||
EditorState.readOnly.of(readOnly),
|
||||
EditorView.editable.of(!readOnly),
|
||||
];
|
||||
|
||||
const holder: { view?: EditorView } = {};
|
||||
|
||||
const state = EditorState.create({
|
||||
doc: opts.doc ?? "",
|
||||
extensions: [
|
||||
lineNumbers(),
|
||||
foldGutter(),
|
||||
history(),
|
||||
bracketMatching(),
|
||||
yaml(),
|
||||
indentOnInput(),
|
||||
keymap.of([indentWithTab, ...defaultKeymap, ...historyKeymap]),
|
||||
errorLineExtension,
|
||||
// emits stable tok-* classes so colors/theme live in YamlEditor.vue CSS
|
||||
syntaxHighlighting(classHighlighter),
|
||||
readOnlyCompartment.of(readOnlyExt(opts.readOnly)),
|
||||
EditorView.updateListener.of((update) => {
|
||||
if (!update.docChanged) return;
|
||||
const value = update.state.doc.toString();
|
||||
// strip a pasted wrapper key (e.g. "power:") and unindent, matching the editor's removeKey
|
||||
const pasted = update.transactions.some((tr) => tr.isUserEvent("input.paste"));
|
||||
const removeKey = pasted ? opts.getRemoveKey() : undefined;
|
||||
if (removeKey) {
|
||||
const cleaned = cleanYaml(value, removeKey);
|
||||
if (cleaned !== value) {
|
||||
// dispatch outside the update cycle to avoid a re-entrant transaction
|
||||
queueMicrotask(() => {
|
||||
const view = holder.view;
|
||||
if (!view) return;
|
||||
view.dispatch({
|
||||
changes: { from: 0, to: view.state.doc.length, insert: cleaned },
|
||||
});
|
||||
});
|
||||
return;
|
||||
}
|
||||
}
|
||||
opts.onChange(value);
|
||||
}),
|
||||
],
|
||||
});
|
||||
|
||||
const view = new EditorView({ state, parent: opts.parent });
|
||||
holder.view = view;
|
||||
|
||||
return {
|
||||
view,
|
||||
setDoc(text: string) {
|
||||
if (view.state.doc.toString() === text) return;
|
||||
view.dispatch({ changes: { from: 0, to: view.state.doc.length, insert: text } });
|
||||
},
|
||||
setErrorLine(line: number | null) {
|
||||
view.dispatch({ effects: setErrorLineEffect.of(line || null) });
|
||||
},
|
||||
setReadOnly(readOnly: boolean) {
|
||||
view.dispatch({ effects: readOnlyCompartment.reconfigure(readOnlyExt(readOnly)) });
|
||||
},
|
||||
destroy() {
|
||||
view.destroy();
|
||||
},
|
||||
};
|
||||
}
|
||||
200
package-lock.json
generated
200
package-lock.json
generated
|
|
@ -7,9 +7,14 @@
|
|||
"name": "evcc",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@codemirror/commands": "^6.8.1",
|
||||
"@codemirror/lang-yaml": "^6.1.2",
|
||||
"@codemirror/language": "^6.11.0",
|
||||
"@codemirror/state": "^6.5.2",
|
||||
"@codemirror/view": "^6.36.5",
|
||||
"@formkit/drag-and-drop": "^0.6.0",
|
||||
"@guolao/vue-monaco-editor": "1.5.5",
|
||||
"@h2d2/shopicons": "^1.9.1",
|
||||
"@lezer/highlight": "^1.2.1",
|
||||
"@popperjs/core": "^2.11.8",
|
||||
"@unhead/vue": "^2.1.15",
|
||||
"axios": "^1.17.0",
|
||||
|
|
@ -21,7 +26,6 @@
|
|||
"countup.js": "^2.10.0",
|
||||
"dayjs": "^1.11.21",
|
||||
"echarts": "^6.1.0",
|
||||
"monaco-editor": "0.52.2",
|
||||
"qrcode": "^1.5.4",
|
||||
"smoothscroll-polyfill": "^0.4.4",
|
||||
"snarkdown": "^2.0.0",
|
||||
|
|
@ -1684,6 +1688,80 @@
|
|||
"node": ">=6.9.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@codemirror/autocomplete": {
|
||||
"version": "6.20.3",
|
||||
"resolved": "https://registry.npmjs.org/@codemirror/autocomplete/-/autocomplete-6.20.3.tgz",
|
||||
"integrity": "sha512-tlosUqb+3BbxCxZdu4tKeRghPFC+QM7q4X5YhKV2eCmPG+1r2F3f4AaSz5sCrFqUtX4Jh20VFTKecl16MgiV9g==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@codemirror/language": "^6.0.0",
|
||||
"@codemirror/state": "^6.0.0",
|
||||
"@codemirror/view": "^6.17.0",
|
||||
"@lezer/common": "^1.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@codemirror/commands": {
|
||||
"version": "6.10.3",
|
||||
"resolved": "https://registry.npmjs.org/@codemirror/commands/-/commands-6.10.3.tgz",
|
||||
"integrity": "sha512-JFRiqhKu+bvSkDLI+rUhJwSxQxYb759W5GBezE8Uc8mHLqC9aV/9aTC7yJSqCtB3F00pylrLCwnyS91Ap5ej4Q==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@codemirror/language": "^6.0.0",
|
||||
"@codemirror/state": "^6.6.0",
|
||||
"@codemirror/view": "^6.27.0",
|
||||
"@lezer/common": "^1.1.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@codemirror/lang-yaml": {
|
||||
"version": "6.1.3",
|
||||
"resolved": "https://registry.npmjs.org/@codemirror/lang-yaml/-/lang-yaml-6.1.3.tgz",
|
||||
"integrity": "sha512-AZ8DJBuXGVHybpBQhmZtgew5//4hv3tdkXnr3vDmOUMJRuB6vn/uuwtmTOTlqEaQFg3hQSVeA90NmvIQyUV6FQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@codemirror/autocomplete": "^6.0.0",
|
||||
"@codemirror/language": "^6.0.0",
|
||||
"@codemirror/state": "^6.0.0",
|
||||
"@lezer/common": "^1.2.0",
|
||||
"@lezer/highlight": "^1.2.0",
|
||||
"@lezer/lr": "^1.0.0",
|
||||
"@lezer/yaml": "^1.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@codemirror/language": {
|
||||
"version": "6.12.3",
|
||||
"resolved": "https://registry.npmjs.org/@codemirror/language/-/language-6.12.3.tgz",
|
||||
"integrity": "sha512-QwCZW6Tt1siP37Jet9Tb02Zs81TQt6qQrZR2H+eGMcFsL1zMrk2/b9CLC7/9ieP1fjIUMgviLWMmgiHoJrj+ZA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@codemirror/state": "^6.0.0",
|
||||
"@codemirror/view": "^6.23.0",
|
||||
"@lezer/common": "^1.5.0",
|
||||
"@lezer/highlight": "^1.0.0",
|
||||
"@lezer/lr": "^1.0.0",
|
||||
"style-mod": "^4.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@codemirror/state": {
|
||||
"version": "6.6.0",
|
||||
"resolved": "https://registry.npmjs.org/@codemirror/state/-/state-6.6.0.tgz",
|
||||
"integrity": "sha512-4nbvra5R5EtiCzr9BTHiTLc+MLXK2QGiAVYMyi8PkQd3SR+6ixar/Q/01Fa21TBIDOZXgeWV4WppsQolSreAPQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@marijn/find-cluster-break": "^1.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@codemirror/view": {
|
||||
"version": "6.43.1",
|
||||
"resolved": "https://registry.npmjs.org/@codemirror/view/-/view-6.43.1.tgz",
|
||||
"integrity": "sha512-+BIjw/AG3tDQ4pJgTLPYdAW25eDE66YsvM4LKyVPgGzVgZ4a9Wj1SRX8kPVKgBDdPt8oHtZ15F0qx7p0oOHdHw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@codemirror/state": "^6.6.0",
|
||||
"crelt": "^1.0.6",
|
||||
"style-mod": "^4.1.0",
|
||||
"w3c-keyname": "^2.2.4"
|
||||
}
|
||||
},
|
||||
"node_modules/@emnapi/core": {
|
||||
"version": "1.10.0",
|
||||
"resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.10.0.tgz",
|
||||
|
|
@ -2309,52 +2387,6 @@
|
|||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@guolao/vue-monaco-editor": {
|
||||
"version": "1.5.5",
|
||||
"resolved": "https://registry.npmjs.org/@guolao/vue-monaco-editor/-/vue-monaco-editor-1.5.5.tgz",
|
||||
"integrity": "sha512-NFGImQ8dBYj6ehIxy1DngPRkctB9b6GbxvCm6aXZztNsgm/TtM4u+YM9ZwZHQPlXt7a4IODXoKCcTYEVycBSyA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@monaco-editor/loader": "^1.5.0",
|
||||
"vue-demi": "latest"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@vue/composition-api": "^1.7.1",
|
||||
"monaco-editor": ">=0.43.0",
|
||||
"vue": "^2.6.14 || >=3.0.0"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@vue/composition-api": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@guolao/vue-monaco-editor/node_modules/vue-demi": {
|
||||
"version": "0.14.10",
|
||||
"resolved": "https://registry.npmjs.org/vue-demi/-/vue-demi-0.14.10.tgz",
|
||||
"integrity": "sha512-nMZBOwuzabUO0nLgIcc6rycZEebF6eeUfaiQx9+WSk8e29IbLvPU9feI6tqW4kTo3hvoYAJkMh8n8D0fuISphg==",
|
||||
"hasInstallScript": true,
|
||||
"license": "MIT",
|
||||
"bin": {
|
||||
"vue-demi-fix": "bin/vue-demi-fix.js",
|
||||
"vue-demi-switch": "bin/vue-demi-switch.js"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/antfu"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@vue/composition-api": "^1.0.0-rc.1",
|
||||
"vue": "^3.0.0-0 || ^2.6.0"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@vue/composition-api": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@h2d2/shopicons": {
|
||||
"version": "1.9.1",
|
||||
"resolved": "https://registry.npmjs.org/@h2d2/shopicons/-/shopicons-1.9.1.tgz",
|
||||
|
|
@ -2661,15 +2693,47 @@
|
|||
"integrity": "sha512-M5UknZPHRu3DEDWoipU6sE8PdkZ6Z/S+v4dD+Ke8IaNlpdSQah50lz1KtcFBa2vsdOnwbbnxJwVM4wty6udA5w==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@monaco-editor/loader": {
|
||||
"version": "1.5.0",
|
||||
"resolved": "https://registry.npmjs.org/@monaco-editor/loader/-/loader-1.5.0.tgz",
|
||||
"integrity": "sha512-hKoGSM+7aAc7eRTRjpqAZucPmoNOC4UUbknb/VNoTkEIkCPhqV8LfbsgM1webRM7S/z21eHEx9Fkwx8Z/C/+Xw==",
|
||||
"node_modules/@lezer/common": {
|
||||
"version": "1.5.2",
|
||||
"resolved": "https://registry.npmjs.org/@lezer/common/-/common-1.5.2.tgz",
|
||||
"integrity": "sha512-sxQE460fPZyU3sdc8lafxiPwJHBzZRy/udNFynGQky1SePYBdhkBl1kOagA9uT3pxR8K09bOrmTUqA9wb/PjSQ==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@lezer/highlight": {
|
||||
"version": "1.2.3",
|
||||
"resolved": "https://registry.npmjs.org/@lezer/highlight/-/highlight-1.2.3.tgz",
|
||||
"integrity": "sha512-qXdH7UqTvGfdVBINrgKhDsVTJTxactNNxLk7+UMwZhU13lMHaOBlJe9Vqp907ya56Y3+ed2tlqzys7jDkTmW0g==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"state-local": "^1.0.6"
|
||||
"@lezer/common": "^1.3.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@lezer/lr": {
|
||||
"version": "1.4.10",
|
||||
"resolved": "https://registry.npmjs.org/@lezer/lr/-/lr-1.4.10.tgz",
|
||||
"integrity": "sha512-rnCpTIBafOx4mRp43xOxDJbFipJm/c0cia/V5TiGlhmMa+wsSdoGmUN3w5Bqrks/09Q/D4tNAmWaT8p6NRi77A==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@lezer/common": "^1.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@lezer/yaml": {
|
||||
"version": "1.0.4",
|
||||
"resolved": "https://registry.npmjs.org/@lezer/yaml/-/yaml-1.0.4.tgz",
|
||||
"integrity": "sha512-2lrrHqxalACEbxIbsjhqGpSW8kWpUKuY6RHgnSAFZa6qK62wvnPxA8hGOwOoDbwHcOFs5M4o27mjGu+P7TvBmw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@lezer/common": "^1.2.0",
|
||||
"@lezer/highlight": "^1.0.0",
|
||||
"@lezer/lr": "^1.4.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@marijn/find-cluster-break": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/@marijn/find-cluster-break/-/find-cluster-break-1.0.2.tgz",
|
||||
"integrity": "sha512-l0h88YhZFyKdXIFNfSWpyjStDjGHwZ/U7iobcK1cQQD8sejsONdQtTVU+1wVN1PBw40PiiHB1vA5S7VTfQiP9g==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@napi-rs/wasm-runtime": {
|
||||
"version": "1.1.4",
|
||||
"resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-1.1.4.tgz",
|
||||
|
|
@ -6253,6 +6317,12 @@
|
|||
"integrity": "sha512-QQpZx7oYxsR+OeITlZe46fY/OQjV11oBqjY8wgIXzLU2jIz8GzOrbMhqKLysGY8bWI3T1ZNrYkwGzKb4JNgyzg==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/crelt": {
|
||||
"version": "1.0.6",
|
||||
"resolved": "https://registry.npmjs.org/crelt/-/crelt-1.0.6.tgz",
|
||||
"integrity": "sha512-VQ2MBenTq1fWZUH9DJNGti7kKv6EeAuYr3cLwxUWhIu1baTaXh4Ib5W2CqHVqib4/MqbYGJqiL3Zb8GJZr3l4g==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/cross-env": {
|
||||
"version": "10.1.0",
|
||||
"resolved": "https://registry.npmjs.org/cross-env/-/cross-env-10.1.0.tgz",
|
||||
|
|
@ -9546,12 +9616,6 @@
|
|||
"pathe": "^2.0.1"
|
||||
}
|
||||
},
|
||||
"node_modules/monaco-editor": {
|
||||
"version": "0.52.2",
|
||||
"resolved": "https://registry.npmjs.org/monaco-editor/-/monaco-editor-0.52.2.tgz",
|
||||
"integrity": "sha512-GEQWEZmfkOGLdd3XK8ryrfWz3AIP8YymVXiPHEdewrUq7mh0qrKrfHLNCXcbB6sTnMLnOZ3ztSiKcciFUkIJwQ==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/mqtt": {
|
||||
"version": "5.15.1",
|
||||
"resolved": "https://registry.npmjs.org/mqtt/-/mqtt-5.15.1.tgz",
|
||||
|
|
@ -11689,12 +11753,6 @@
|
|||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/state-local": {
|
||||
"version": "1.0.7",
|
||||
"resolved": "https://registry.npmjs.org/state-local/-/state-local-1.0.7.tgz",
|
||||
"integrity": "sha512-HTEHMNieakEnoe33shBYcZ7NX83ACUjCu8c40iOGEZsngj9zRnkqS9j1pqQPXwobB0ZcVTk27REb7COQ0UR59w==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/statuses": {
|
||||
"version": "2.0.2",
|
||||
"resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.2.tgz",
|
||||
|
|
@ -12010,6 +12068,12 @@
|
|||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/style-mod": {
|
||||
"version": "4.1.3",
|
||||
"resolved": "https://registry.npmjs.org/style-mod/-/style-mod-4.1.3.tgz",
|
||||
"integrity": "sha512-i/n8VsZydrugj3Iuzll8+x/00GH2vnYsk1eomD8QiRrSAeW6ItbCQDtfXCeJHd0iwiNagqjQkvpvREEPtW3IoQ==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/supports-color": {
|
||||
"version": "7.2.0",
|
||||
"resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz",
|
||||
|
|
@ -13882,6 +13946,12 @@
|
|||
"url": "https://github.com/sponsors/jonschlinkert"
|
||||
}
|
||||
},
|
||||
"node_modules/w3c-keyname": {
|
||||
"version": "2.2.8",
|
||||
"resolved": "https://registry.npmjs.org/w3c-keyname/-/w3c-keyname-2.2.8.tgz",
|
||||
"integrity": "sha512-dpojBhNsCNN7T82Tm7k26A6G9ML3NkhDsnw9n/eoxSRlVBB4CEtIQ/KTCLI2Fwf3ataSXRhYFkQi3SlnFwPvPQ==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/wait-on": {
|
||||
"version": "9.0.10",
|
||||
"resolved": "https://registry.npmjs.org/wait-on/-/wait-on-9.0.10.tgz",
|
||||
|
|
|
|||
11
package.json
11
package.json
|
|
@ -20,9 +20,14 @@
|
|||
"type": "module",
|
||||
"main": "index.js",
|
||||
"dependencies": {
|
||||
"@codemirror/commands": "^6.8.1",
|
||||
"@codemirror/lang-yaml": "^6.1.2",
|
||||
"@codemirror/language": "^6.11.0",
|
||||
"@codemirror/state": "^6.5.2",
|
||||
"@codemirror/view": "^6.36.5",
|
||||
"@formkit/drag-and-drop": "^0.6.0",
|
||||
"@guolao/vue-monaco-editor": "1.5.5",
|
||||
"@h2d2/shopicons": "^1.9.1",
|
||||
"@lezer/highlight": "^1.2.1",
|
||||
"@popperjs/core": "^2.11.8",
|
||||
"@unhead/vue": "^2.1.15",
|
||||
"axios": "^1.17.0",
|
||||
|
|
@ -34,7 +39,6 @@
|
|||
"countup.js": "^2.10.0",
|
||||
"dayjs": "^1.11.21",
|
||||
"echarts": "^6.1.0",
|
||||
"monaco-editor": "0.52.2",
|
||||
"qrcode": "^1.5.4",
|
||||
"smoothscroll-polyfill": "^0.4.4",
|
||||
"snarkdown": "^2.0.0",
|
||||
|
|
@ -43,9 +47,6 @@
|
|||
"vue-i18n": "^11.4.5",
|
||||
"vue-router": "^5.1.0"
|
||||
},
|
||||
"overrides": {
|
||||
"@monaco-editor/loader": "1.5.0"
|
||||
},
|
||||
"engines": {
|
||||
"npm": ">=10.0.0",
|
||||
"node": ">=24.0.0"
|
||||
|
|
|
|||
|
|
@ -174,13 +174,13 @@ energy:
|
|||
);
|
||||
|
||||
// no errors
|
||||
await expect(editor.locator(".line-numbers.error")).toHaveCount(0);
|
||||
await expect(editor.locator(".cm-errorLine")).toHaveCount(0);
|
||||
const restResult = modal.getByTestId("test-result");
|
||||
await expect(restResult).toContainText("Status: unknown");
|
||||
await restResult.getByRole("link", { name: "validate" }).click();
|
||||
await expect(restResult).toContainText("Status: failed");
|
||||
await expect(restResult).toContainText("mapping values are not allowed in this context");
|
||||
await expect(editor.locator(".line-numbers.error")).toHaveCount(1);
|
||||
await expect(editor.locator(".cm-errorLine")).toHaveCount(1);
|
||||
|
||||
// invalid field error
|
||||
await editorClear(editor);
|
||||
|
|
@ -194,7 +194,7 @@ energy:
|
|||
await restResult.getByRole("link", { name: "validate" }).click();
|
||||
await expect(restResult).toContainText("Status: failed");
|
||||
await expect(restResult).toContainText("has invalid keys: apower");
|
||||
await expect(editor.locator(".line-numbers.error")).toHaveCount(0);
|
||||
await expect(editor.locator(".cm-errorLine")).toHaveCount(0);
|
||||
|
||||
// unknown source error
|
||||
await editorClear(editor);
|
||||
|
|
@ -208,7 +208,7 @@ energy:
|
|||
await restResult.getByRole("link", { name: "validate" }).click();
|
||||
await expect(restResult).toContainText("Status: failed");
|
||||
await expect(restResult).toContainText("invalid plugin type: unknown");
|
||||
await expect(editor.locator(".line-numbers.error")).toHaveCount(0);
|
||||
await expect(editor.locator(".cm-errorLine")).toHaveCount(0);
|
||||
|
||||
// missing required field error
|
||||
await editorClear(editor);
|
||||
|
|
@ -222,6 +222,6 @@ energy:
|
|||
await restResult.getByRole("link", { name: "validate" }).click();
|
||||
await expect(restResult).toContainText("Status: failed");
|
||||
await expect(restResult).toContainText("power: missing plugin source");
|
||||
await expect(editor.locator(".line-numbers.error")).toHaveCount(0);
|
||||
await expect(editor.locator(".cm-errorLine")).toHaveCount(0);
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -42,6 +42,11 @@ test.describe("messaging", async () => {
|
|||
start:
|
||||
title: Charge started
|
||||
msg: Started charging`;
|
||||
// differs from the stored config so saving is an actual change
|
||||
const changedYaml = `events:
|
||||
start:
|
||||
title: Charge started
|
||||
msg: Charging has begun`;
|
||||
|
||||
// default content
|
||||
const editor = modal.getByTestId("yaml-editor");
|
||||
|
|
@ -55,7 +60,7 @@ test.describe("messaging", async () => {
|
|||
|
||||
// clear and enter valid yaml
|
||||
await editorClear(editor);
|
||||
await editorPaste(editor, page, validYaml);
|
||||
await editorPaste(editor, page, changedYaml);
|
||||
|
||||
await page.getByRole("button", { name: "Save" }).click();
|
||||
await expect(modal.getByTestId("error")).not.toBeVisible();
|
||||
|
|
|
|||
|
|
@ -18,25 +18,19 @@ export async function expectModalHidden(modal: Locator): Promise<void> {
|
|||
}
|
||||
|
||||
export async function editorClear(editor: Locator): Promise<void> {
|
||||
const firstLine = editor.locator(".view-line").nth(0);
|
||||
const content = editor.locator(".view-lines");
|
||||
const content = editor.locator(".cm-content");
|
||||
// wait for the async content load, otherwise clearing races the arriving text
|
||||
await expect(firstLine).not.toHaveText("");
|
||||
// retry until the editor is verified empty, keystrokes are dropped while monaco initializes
|
||||
const deadline = Date.now() + 10_000;
|
||||
do {
|
||||
await firstLine.click();
|
||||
await editor.page().keyboard.press("ControlOrMeta+KeyA", { delay: 50 });
|
||||
await editor.page().keyboard.press("Backspace", { delay: 50 });
|
||||
if ((await content.textContent())?.trim() === "") return;
|
||||
} while (Date.now() < deadline);
|
||||
await expect(content).not.toHaveText("");
|
||||
await content.click();
|
||||
await editor.page().keyboard.press("ControlOrMeta+KeyA");
|
||||
await editor.page().keyboard.press("Backspace");
|
||||
await expect(content, "editor should be empty after clearing").toHaveText("");
|
||||
}
|
||||
|
||||
export async function editorPaste(editor: Locator, page: Page, text: string): Promise<void> {
|
||||
await editor.locator(".view-line").nth(0).click();
|
||||
await editor.locator(".cm-content").click();
|
||||
await page.evaluate((text) => navigator.clipboard.writeText(text), text);
|
||||
await page.keyboard.press("ControlOrMeta+KeyV", { delay: 100 });
|
||||
await page.keyboard.press("ControlOrMeta+KeyV");
|
||||
}
|
||||
|
||||
export enum LoadpointType {
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue