evcc-io/assets/js/components/Config/JsonModal.vue
Maschga 07bcca2925
Config UI: visualize Modbusproxy (#24015)
Co-authored-by: Michael Geers <michael@geers.tv>
2025-11-13 09:44:39 +01:00

196 lines
4.8 KiB
Vue

<template>
<GenericModal ref="modal" :title="title" :size="size" @open="open">
<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>
<p v-if="error" class="text-danger">
<span v-if="errorMessage" class="d-block">{{ errorMessage }}</span>
{{ error }}
</p>
<form ref="form" class="container mx-0 px-0" @submit.prevent="save">
<slot :values="values"></slot>
<div
v-if="!noButtons"
class="mt-4 d-flex justify-content-between gap-2 flex-column flex-sm-row"
>
<div
class="d-flex justify-content-between order-2 order-sm-1 gap-2 flex-grow-1 flex-sm-grow-0"
>
<button
v-if="!disableCancel"
type="button"
class="btn btn-link text-muted btn-cancel"
data-bs-dismiss="modal"
>
{{ $t("config.general.cancel") }}
</button>
<button
v-if="!disableRemove"
type="button"
class="btn btn-link text-danger"
:disabled="removing"
@click="remove"
>
<span
v-if="removing"
class="spinner-border spinner-border-sm"
role="status"
aria-hidden="true"
></span>
{{ $t("config.general.remove") }}
</button>
</div>
<button
type="submit"
class="btn btn-primary order-1 order-sm-2 flex-grow-1 flex-sm-grow-0 px-4"
:disabled="saving || nothingChanged"
>
<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 api from "@/api";
import { docsPrefix } from "@/i18n";
import store from "@/store";
import deepClone from "@/utils/deepClone";
export default {
name: "JsonModal",
components: { GenericModal },
props: {
title: String,
description: String,
errorMessage: String,
docs: String,
endpoint: String,
disableCancel: Boolean,
disableRemove: Boolean,
noButtons: Boolean,
transformReadValues: Function,
stateKey: String,
saveMethod: { type: String, default: "post" },
storeValuesInArray: Boolean,
size: { type: String },
},
emits: ["changed", "open"],
data() {
return {
saving: false,
removing: false,
error: "",
values: this.storeValuesInArray ? [] : {},
serverValues: this.storeValuesInArray ? [] : {},
};
},
computed: {
docsLink() {
return this.docs ? `${docsPrefix()}${this.docs}` : null;
},
nothingChanged() {
return JSON.stringify(this.values) === JSON.stringify(this.serverValues);
},
},
methods: {
reset() {
this.saving = false;
this.deleting = false;
this.error = "";
this.values = "";
this.serverValues = "";
},
async open() {
this.$emit("open");
this.reset();
await this.load();
},
async load() {
this.serverValues = this.stateKey ? store.state[this.stateKey] : store.state;
if (this.transformReadValues) {
this.serverValues = this.transformReadValues(this.serverValues);
}
// Handle null/undefined values when expecting an array or object
if (this.serverValues == null) {
this.serverValues = this.storeValuesInArray ? [] : {};
}
this.values = deepClone(this.serverValues);
},
async save() {
this.saving = true;
this.error = "";
try {
const values = this.trimValues(this.values);
const res = await api[this.saveMethod](this.endpoint, values, {
validateStatus: (code) => [200, 202, 400].includes(code),
});
if (res.status === 200 || res.status === 202) {
this.$emit("changed");
this.$refs.modal.close();
}
if (res.status === 400) {
this.error = res.data.error;
}
} catch (e) {
console.error(e);
}
this.saving = false;
},
async remove() {
this.removing = true;
this.error = "";
try {
const res = await api.delete(this.endpoint, {
validateStatus: (code) => [200, 400].includes(code),
});
if (res.status === 200) {
this.$emit("changed");
this.$refs.modal.close();
}
if (res.status === 400) {
this.error = res.data.error;
}
} catch (e) {
console.error(e);
}
this.removing = false;
},
trimValues(values) {
if (Array.isArray(values)) {
for (let index = 0; index < values.length; index++) {
values[index] = this.trimValues(values[index]);
}
return values;
} else {
return Object.fromEntries(
Object.entries(values).map(([key, value]) => [
key,
typeof value === "string" ? value.trim() : value,
])
);
}
},
},
};
</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>