evcc-io/assets/js/components/Config/YamlModal.vue
2026-06-25 17:47:30 +02:00

183 lines
4.4 KiB
Vue

<template>
<GenericModal
:id="`${name}Modal`"
ref="modal"
:size="size"
:title="title"
:data-testid="`${name}-modal`"
:config-modal-name="name"
@open="open"
@close="close"
>
<p v-if="description || docsLink">
<span v-if="description">{{ description + " " }}</span>
<a v-if="docsLink" :href="docsLink" target="_blank">
{{ $t("config.general.docsLink") }}
</a>
</p>
<slot name="afterDescription" />
<ErrorMessage :error="error" data-testid="error" />
<form ref="form" class="container mx-0 px-0">
<div v-if="!noYamlEditor" class="editor-container">
<YamlEditorContainer
v-model="yaml"
:errorLine="errorLine"
:removeKey="removeKey"
:hidden="!modalVisible"
/>
</div>
<slot name="extra" />
<AdminPasswordPrompt
v-if="adminPasswordRequired"
v-model:password="adminPassword"
:invalid="adminPasswordInvalid"
/>
<div class="mt-4 d-flex justify-content-between">
<button
type="button"
class="btn btn-link text-muted btn-cancel"
data-bs-dismiss="modal"
>
{{ $t("config.general.cancel") }}
</button>
<button
v-if="!disableSave"
type="submit"
class="btn btn-primary"
:disabled="saving || nothingChanged"
@click.prevent="save"
>
<span
v-if="saving"
class="spinner-border spinner-border-sm"
role="status"
aria-hidden="true"
></span>
{{ $t("config.general.save") }}
</button>
</div>
</form>
</GenericModal>
</template>
<script>
import GenericModal from "../Helper/GenericModal.vue";
import ErrorMessage from "../Helper/ErrorMessage.vue";
import AdminPasswordPrompt from "@/components/Auth/AdminPasswordPrompt.vue";
import api from "@/api";
import { docsPrefix } from "@/i18n";
import YamlEditorContainer from "./YamlEditorContainer.vue";
import { ADMIN_PASSWORD_REQUIRED } from "./DeviceModal/index";
export default {
name: "YamlModal",
components: { GenericModal, ErrorMessage, YamlEditorContainer, AdminPasswordPrompt },
props: {
title: String,
description: String,
docs: String,
endpoint: String,
defaultYaml: String,
removeKey: String,
size: { type: String, default: "xl" },
noYamlEditor: Boolean,
disableSave: Boolean,
name: String,
},
emits: ["changed", "open"],
data() {
return {
saving: false,
error: "",
errorLine: undefined,
yaml: "",
serverYaml: "",
modalVisible: false,
adminPassword: "",
adminPasswordRequired: false,
adminPasswordInvalid: false,
};
},
computed: {
docsLink() {
return `${docsPrefix()}${this.docs}`;
},
nothingChanged() {
return this.yaml === this.serverYaml && this.yaml !== "";
},
},
watch: {
adminPassword() {
this.adminPasswordInvalid = false;
},
},
methods: {
reset() {
this.yaml = "";
this.serverYaml = "";
this.error = "";
this.saving = false;
this.errorLine = undefined;
// keep adminPassword across reopens (like BaseDeviceModal), cleared on reload
this.adminPasswordRequired = false;
this.adminPasswordInvalid = false;
},
async open() {
this.reset();
this.modalVisible = true;
this.$emit("open");
await this.load();
},
close() {
this.modalVisible = false;
},
async load() {
try {
const { data } = await api.get(this.endpoint);
this.serverYaml = data;
this.yaml = data || this.defaultYaml;
} catch (e) {
console.error(e);
}
},
async save() {
this.saving = true;
this.error = "";
this.errorLine = undefined;
try {
const data = this.yaml === this.defaultYaml ? "" : this.yaml;
const res = await api.post(this.endpoint, data, {
headers: this.adminPassword ? { "X-Admin-Password": this.adminPassword } : {},
validateStatus: (code) => [200, 400, ADMIN_PASSWORD_REQUIRED].includes(code),
});
if (res.status === ADMIN_PASSWORD_REQUIRED) {
this.adminPasswordRequired = true;
this.adminPasswordInvalid = !!this.adminPassword;
this.saving = false;
return;
}
if (res.status === 200) {
this.$emit("changed");
this.$refs.modal.close();
}
if (res.status === 400) {
this.error = res.data.error;
this.errorLine = res.data.line;
}
} catch (e) {
console.error(e);
}
this.saving = false;
},
},
};
</script>
<style scoped>
.container {
margin-left: calc(var(--bs-gutter-x) * -0.5);
margin-right: calc(var(--bs-gutter-x) * -0.5);
padding-right: 0;
}
</style>