Source: components/ol-map-props.vue

<template>
    <!-- <q-card dense class="dialog q-pa-none q-ma-none max-width" flat> -->
    <div class="max-width">

        <q-tabs dense v-model="tabToShow" align="justify" class="q-pa-none q-ma-none max-width" narrow-indicator
            @before-transition="tabChange" indicator-color="primary" active-color="primary">
            <q-tab name="general" :label="$t('General')" no-caps />
            <q-tab v-if="custom_geometry_id != null" name="times" :label="$t('Properties')" no-caps />
            <q-tab v-if="custom_geometry_id != null" name="files" :label="$t('Images')" no-caps />
        </q-tabs>

        <q-tab-panels dense v-model="tabToShow" animated>

            <q-tab-panel dense name="general">
                <q-form greedy @submit="saveGeneral" @validationError="valid = false" @validationSuccess="valid = true">
                    <autocomplete class="q-ml-xs" :options="$store.geometryProps" v-model="propsEdit[0].value"
                        emit-value map-options style="width:300px;" :clearable="false" :label="$t('Type')" />
                    <div class="row">
                        <q-input class="q-ml-md q-pb-sm" dense v-model="propsEdit[1].value" :label="$t('Name') + ' *'"
                            style="width:250px;" :rules="[val => val && val.length > 0 || $t('Required')]" />
                    </div>
                    <q-checkbox dense class="q-ml-md" v-model="propsEdit[2].value" :label="$t('Public')" />
                    <q-card-actions align="right">
                        <q-btn v-if="hasFeatureStatsButton" flat dense @click="showFeatureStats">{{ $t('Statistics')
                            }}</q-btn>
                        <q-btn v-if="custom_geometry_id != null && !isChanged" flat color="negative"
                            @click="deleteGeneral" :label="$t('Delete')"></q-btn>
                        <q-btn flat color="primary" @click="cancelGeneral"
                            :label="isChanged || custom_geometry_id == null ? $t('Cancel') : $t('Close')"></q-btn>
                        <q-btn type="submit" v-if="isChanged" flat color="positive" :label="$t('Save')"></q-btn>
                    </q-card-actions>
                </q-form>
            </q-tab-panel>

            <q-tab-panel dense name="times">
                <q-form greedy @submit="saveTime" @validationError="valid = false" @validationSuccess="valid = true">
                    <div class="row" style="align-items: center; justify-content: space-between; ">
                        <q-input class="q-ml-md" type="date" v-model="propsEdit[0].value" :label="$t('Date')"
                            style="width:200px;" />
                    </div>
                    <div class="row q-pb-sm" v-for="p of propsEdit.filter(x => x.property_id)" :key="p.property_id"
                        style="display: flex; align-items: flex-end;">
                        <autocomplete v-if="p.indicator_id && !p.numerical" class="q-ml-xs"
                            :options="$store.catalogs.descriptions.filter(x => x.indicator_id == p.indicator_id)"
                            :label="p.name + req(p)" emit-value map-options v-model="p.value" style="width:300px;" />
                        <q-checkbox v-else-if="p.data_type_id == 41" dense class="q-ml-md" v-model="p.value"
                            :label="p.name + req(p)" />
                        <q-input class="q-ml-md" v-else dense v-model="p.value" :label="p.name" style="width:250px;"
                            :type="buildType(p)" :rules="buildRules(p)" hide-bottom-space>
                            <template v-slot:label>
                                <label for="my-input" v-html="p.name + req(p)"></label>
                            </template>
                        </q-input>
                        <span style="color:gray; padding-bottom: 5px;" v-html="p.unit" />
                    </div>
                    <q-card-actions align="between">
                        <span>
                            <q-btn round flat :color="buttonColor(indexTime <= 0)" icon="chevron_left" @click="back" />

                            <span v-if="feature.times[indexTime].id">
                                {{ (indexTime + 1) + "/" + feature.times.length }}
                            </span>
                            <span v-else> {{ $t('New') }} </span>
                            <q-btn round flat :color="buttonColor(indexTime >= feature.times.length - 1)"
                                icon="chevron_right" @click="forward" />
                            <q-btn round flat icon="add" :color="buttonColor(!feature.times[indexTime].id)"
                                @click="add" /></span> <span>

                            <q-btn v-if="feature.times.length > 0 && feature.times[indexTime].id && !isChanged" flat
                                color="negative" @click="deleteTime" :label="$t('Delete')"></q-btn>
                            <q-btn flat color="primary" @click="cancelTime"
                                :label="isChanged || custom_geometry_id == null ? $t('Cancel') : $t('Close')"></q-btn>
                            <q-btn type="submit" v-if="isChanged" flat color="positive" :label="$t('Save')"></q-btn>
                        </span>
                    </q-card-actions>
                </q-form>
            </q-tab-panel>

            <q-tab-panel name="files">
                <div v-if="files.length == 0">{{ $t("No images") }}</div>
                <q-markup-table class="q-pa-none q-ma-none" dense flat v-if="files.length > 0">
                    <template v-for="(f, index) of files" :key="f.id">
                        <tr>
                            <td><q-btn class="q-pa-none" dense flat icon="edit" @click="editImage(index)">
                                    <q-tooltip>{{ $t("Edit properties") }}</q-tooltip>
                                </q-btn></td>
                            <td><q-btn class="q-pa-none" dense flat icon="visibility" @click="showImage(index)">
                                    <q-tooltip>{{ $t("Show image") }}</q-tooltip>
                                </q-btn></td>
                            <td><q-btn class="q-pa-none" dense flat color="negative" @click="deleteFile(f)"
                                    icon="delete">
                                    <q-tooltip>{{ $t("Delete image") }}</q-tooltip>
                                </q-btn></td>
                            <td @click="showImage(index)" class="q-pa-none"
                                style="max-width:300px; overflow: hidden; cursor: pointer;">
                                {{ f.name }}</td>
                        </tr>
                        <tr v-if="editIndex == index">
                            <td colspan="4">
                                <q-input v-model="f.name" dense :label="$t('Name')" style="width:300px;" />
                                <q-input v-model="f.compass" dense :label="$t('Compass')" style="width:300px;" />
                                <div class="row">
                                    <q-input v-model="f.lon" dense :label="$t('Longitude')" style="width:300px;" />
                                    <q-btn no-caps flat :icon="$icons.my_location" @click="setGPSPosition"
                                        :label="$t('Set GPS position')"></q-btn>
                                </div>
                                <div class="row">
                                    <q-input v-model="f.lat" dense :label="$t('Latitude')" style="width:300px;" />
                                    <q-btn no-caps flat icon="location_on" @click="setLocationPosition"
                                        :label="$t('Set location position')"></q-btn>
                                </div>
                                <q-btn flat color="positive" @click="saveImage" :label="$t('Save')"></q-btn>
                                <q-btn flat color="negative" @click="cancelImage" :label="$t('Cancel')"></q-btn>
                            </td>
                        </tr>
                    </template>
                </q-markup-table>
                <q-uploader style="width:100%" ref="uploader" class="q-pt-md" flat bordered color="primary"
                    :label="$t('Upload')" multiple accept=".jpg, .jpeg, .png" @failed="filesFailed"
                    @uploaded="filesUploaded" @added="filesAdded" :url="uploadURL" :form-fields="formFields"
                    :headers="[{ name: 'Authorization', value: 'Bearer ' + this.$keycloak.token }]" />
                <div><span class="q-ml-sm">{{ $t("Set position of uploaded image to") }}</span>
                    <q-radio v-model="coordType" val="gps" :label="$t('GPS')" />
                    <q-radio v-model="coordType" val="loc" :label="$t('Location')" />
                </div>
            </q-tab-panel>
        </q-tab-panels>
        <ImageViewer ref="imageViewer" />
    </div>
</template>
<script>

/**
 * Form for editing properties of a feature.
 *  
 * @component
 * @name OlMapProps
 * @example
 * <OlMapProps />
 */

import Autocomplete from "./autocomplete.vue";
import FileUploader from "./file-uploader.vue";
import ImageViewer from "./image-viewer.vue";
import { MapEditingMixin } from "../mixins/ol-map-editing";
import dayjs from "dayjs";
import { toLonLat } from "ol/proj";

export default {
    name: "OlMapProps",
    mixins: [MapEditingMixin],
    components: {
        Autocomplete,
        FileUploader,
        ImageViewer
    },
    props: {
        caption: null,
        loadFromServer: false,
    },
    data: () => ({
        options: {},
        feature: null,
        custom_geometry_id: null,
        props: [],
        propsEdit: [],
        existingTime: false,
        parent: null,
        valid: false,
        files: [],
        newFiles: [],
        file: null,
        tabToShow: null,
        loaded: false,
        indexTime: 0,
        editIndex: -1,
        fileSave: {},
        coordType: "gps"
    }),
    computed: {

        /**
        * Checks should we display feature stats button
        * 
        * @returns {boolean} True if the feature has a history button, false otherwise.
        */
        hasFeatureStatsButton() {
            return this.parent && this.parent.selectedTitle && this.parent.selectedFeature.get("custom_geometry_id") && this.parent.selectedFeature.getGeometry().getType() != "Point";
        },

        /**
         * Checks if any property has been changed.
         * @returns {boolean} True if any property has been changed, false otherwise.
         */
        isChanged: function () {
            return this.propsEdit.length > 0 && !this.equalArrays(this.props, this.propsEdit);
        },

        /**
         * Returns the upload URL for file picker.
         * 
         * @returns {string} The upload URL.
         */
        uploadURL: function () {
            return this.axios.API.defaults.baseURL + 'User/UploadFiles/' + this.custom_geometry_id;
        }
    },

    watch: {

        /**
         * Watches for changes in the tabToShow
         *
         * @param {any} val - The value to set the tab to show.
         */
        tabToShow: function (val) {
            if (val == "general") {
                this.initGeneral();
            } else if (val == "times") {
                this.showTime(Math.max(0, this.feature.times.length - 1));
            }
        },

        /**
         * Watches for changes in the `isChanged`.
         * 
         * @param {bool} val - The new value of the `isChanged` 
         */
        isChanged: function (val) {
            this.$store.formChanged = this.isChanged;
        }
    },

    /**
     * Initializes the component
     */
    async mounted() {

        this.loaded = false;
        this.parent = this.$store.popup.props.parent;

        // load catalog if not loaded
        if (this.$store.geometryProps.length == 0) {
            this.$store.geometryProps = await this.get("Home/GetGeometryTypeProperties", null, true);
        }

        if (this.$store.popup.props.custom_geometry_id != null) { // if called from the map
            this.custom_geometry_id = this.$store.popup.props.custom_geometry_id;
        } else if (this.$store.popup.props.row) { // if called from the table	
            this.custom_geometry_id = this.$store.popup.props.row.id;
        }

        if (this.custom_geometry_id) { // if editing existing feature, load it
            this.feature = await this.get("User/GetCustomGeometryProps", {
                id: this.custom_geometry_id
            }, true);
        } else { // create new feature
            this.feature = {
                general: {
                    geometry_type_id: (this.$store.popup.props.geometry_type == "Point" ? 1 : 2),
                    name: "",
                    public: false
                },
                times: [],
                files: []
            };
        }

        this.geometry_type_id = this.feature.general.geometry_type_id;
        this.files = this.feature.files ?? [];

        window.addEventListener('deviceorientationabsolute', this.getCompass);

        this.tabToShow = this.$store.popup.props.tabToShow ?? "general";
        this.loaded = true;
    },

    /**
     * Removes event listener for deleteorientationabsolute.
     */
    unmounted() {
        window.removeEventListener('deviceorientationabsolute', this.getCompass);
    },

    methods: {
        showFeatureStats() {
            this.parent.showFeatureStats();
        },

        /**
         * Checks if the tab has changed.
         * 
         * @param {string} oldValue - The old value of the tab.
         * @param {string} newValue - The new value of the tab.
         * @returns {boolean} True if the tab has changed, false otherwise.
         */
        tabChange(oldValue, newValue) {
            if (this.isChanged()) return false;
            return true;
        },

        /**
         * 
         * Closes properties window.
         */
        closePopup() {
            this.$store.popup.show = false;
            if (this.parent && this.parent.map) this.parent.map.updateSize();
        },

        /**
         * Adds a new item.
         */
        add() {
            if (this.feature.times[this.indexTime].id) {
                this.showTime(this.indexTime + 1);
            }
        },

        /**
         * Shows the previous time.
         */
        back() {
            if (this.indexTime > 0) {
                this.showTime(this.indexTime - 1);
            }
        },

        /**
         * Shows the next time.
         */
        forward() {
            if (this.indexTime < this.feature.times.length - 1) {
                this.showTime(this.indexTime + 1);
            }
        },

        /**
         * Retrieves the compass value
         *
         * @param {Event} event - The event object.
         * @returns {Compass} The compass value.
         */
        getCompass(event) {
            if (event.alpha == null) this.$store.compass = null;
            else this.$store.compass = Math.abs(event.alpha - 360);
        },

        /**
        * Returns the color of the left/right (or add) button.
        * 
        * @returns {string} The color of the button.
        */
        buttonColor(disabled) {
            return (this.isChanged || disabled) ? "red" : "green";
        },

        /**
            * Returns URLs of the files for download from server.
            * 
            * @returns {array} URLs of the files.
        */
        filesUrls() {
            console.log(this.files);
            return this.files.map(x => ({ url: this.axios.API.defaults.baseURL + "User/GetFile/" + this.custom_geometry_id + "/" + x.id + "/" + x.extension, compass: x.compass, file_id: x.id }));
        },

        /**
         * Builds the input type for the given property.
         *
         * @param {any} p - The parameter to build the type for.
         */
        buildType(p) {
            return p.numerical ? "number" : "text";
        },

        /**
         * Builds validation rules based on the provided parameter.
         *
         * @param {any} p - The property on wich the rules are built
         */
        buildRules(p) {
            let rules = [];
            if (p.required) {
                rules.push(val => val && val.length > 0 || this.$t("Required"));
            }
            if (p.value_from) {
                rules.push(val => val >= p.value_from && val <= p.value_to || this.$t("Allowed interval:") + " " + p.value_from + " - " + p.value_to);
            }
            return rules;
        },

        /**
         * Appends asterisk to label if the field is required.
         * 
         * @param {any} p - The field passed to the `req` function.
         */
        req(p) {
            return p.required ? " *" : "";
        },

        /**
         * Initializes the general properties of the feature.
         */
        initGeneral() {
            this.propsEdit = [
                { name: "geometry_type_id", value: this.feature.general.geometry_type_id },
                { name: "name", value: this.feature.general.name },
                { name: "public", value: this.feature.general.public }];
            this.copyArray(this.propsEdit, this.props);
        },

        /**
         * Cancels edits or closes popup.
         */
        cancelGeneral() {
            if (this.custom_geometry_id == null) {
                if (this.parent && this.parent.editableSource) {
                    this.parent.editableSource.removeFeature(this.parent.selectedFeature);
                    this.parent.selectedFeature = null;
                }
                this.$store.formChanged = false;
                this.closePopup();
            } else {
                if (!this.isChanged) {
                    this.closePopup();
                } else {
                    this.initGeneral();
                }
            }
        },

        /**
         * Deletes entire feature.
         * 
         * @async
         */
        async deleteGeneral() {
            if (await this.confirmDialog(this.$t("Delete location?"))) {
                await this.delete("User/DeleteCustomGeometry/" + this.custom_geometry_id);
                if (this.parent && this.parent.editableSource) {
                    for (let file of this.files) {
                        let f = this.parent.editableSource.getFeatureById('f' + file.id);
                        if (f) this.parent.editableSource.removeFeature(f);
                    }
                    this.parent.editableSource.removeFeature(this.parent.selectedFeature);
                    this.parent.selectedFeature = null;
                }
                this.$store.formChanged = false;
                this.closePopup();
            }
        },

        /**
        * Save changes in general properties of a feature.
        */
        async saveGeneral() {
            this.feature.general.geometry_type_id = this.propsEdit[0].value;
            this.feature.general.name = this.propsEdit[1].value;
            this.feature.general.public = this.propsEdit[2].value;
            let ret = await this.post("User/SetCustomGeometry", {
                id: this.custom_geometry_id ? this.custom_geometry_id.toString() : null,
                geometry_type_id: this.propsEdit[0].value.toString(),
                name: this.propsEdit[1].value,
                public: this.propsEdit[2].value.toString()
            });
            if (ret) {
                if (this.custom_geometry_id == null) {
                    this.parent.selectedFeature.set("custom_geometry_id", ret);
                    this.parent.saveFeature(this.parent.selectedFeature, this.parent.format, this.parent.projection, this.parent.projectionForSave);
                    this.custom_geometry_id = ret;
                }
                this.copyArray(this.propsEdit, this.props);

                if (this.parent && this.parent.selectedFeature) {
                    this.parent.selectedFeature.set("name", this.propsEdit[1].value);
                    this.parent.infoTip.innerText = this.propsEdit[1].value;
                }

                if (this.parent && this.parent.updateRow) {
                    let label = this.$store.geometryProps.find(x => x.value == this.propsEdit[0].value).label;
                    this.parent.updateRow({
                        name: this.propsEdit[1].value,
                        public: this.propsEdit[2].value,
                        type: label
                    });
                }
            }
        },

        /**
            * show properties for the selected time
        */
        showTime(index) {
            // prevent changing time if changes not saved
            if (index != this.indexTime && this.isChanged) return;

            this.indexTime = index;
            if (this.indexTime >= this.feature.times.length) { // add new time
                this.feature.times.push({ id: null, observation_time: dayjs(new Date()).format('YYYY-MM-DD'), props: [], files: [] });
            }

            let selectedType = this.$store.geometryProps.find(x => x.value == this.feature.general.geometry_type_id);

            this.copyArray(selectedType.properties.sort((a, b) => a.order_no - b.order_no), this.propsEdit);

            for (let p of this.propsEdit) {
                let v = this.feature.times[this.indexTime].props.find(x => x.property_id == p.property_id);

                if (!v || v.value == null) {
                    p.value = null;
                } else {
                    p.value = p.indicator_id && !p.numerical ? parseInt(v.value) : v.value;
                }
            }

            this.propsEdit.unshift({ name: "Date", value: dayjs(this.feature.times[this.indexTime].observation_time).format('YYYY-MM-DD') });
            this.copyArray(this.propsEdit, this.props);

            // set area if not set
            let area = this.propsEdit.find(x => x.property_id == 1);
            if (area && this.parent && this.parent.selectedFeature && (area.value == null || area.value == "")) {
                area.value = this.parent.getFeatureArea(this.parent.selectedFeature);
                this.props.find(x => x.property_id == 1).value = area.value;
            }
        },

        /**
         * Saves properties for the selected time.
         * 
         * @returns {Promise<void>} A promise that resolves when the time is saved.
         */
        async saveTime() {
            let time = this.feature.times[this.indexTime];
            time.observation_time = this.propsEdit[0].value;
            time.props = this.propsEdit.filter(x => x.property_id).map(x => {
                return {
                    property_id: x.property_id,
                    value: x.value
                }
            });
            let ret = await this.post("User/SetCustomGeometryTime", {
                id: time.id ? time.id.toString() : null,
                custom_geometry_id: this.custom_geometry_id.toString(),
                observation_time: time.observation_time.toString(),
                props: JSON.stringify(time.props)
            });
            if (ret) {
                time.id = ret;
                this.copyArray(this.propsEdit, this.props);
            }

        },

        /**
         * Deletes properties for the selected time.
         * 
         * @async
         * @returns {Promise<void>} A promise that resolves when the time is deleted.
         */
        async deleteTime() {
            if (await this.confirmDialog(this.$t("Delete this observation?"))) {
                if (await this.delete("User/DeleteCustomGeometryTime/" + this.feature.times[this.indexTime].id) != null) {
                    this.feature.times.splice(this.indexTime, 1);
                    this.showTime(Math.max(this.indexTime - 1, 0));
                }
            }
        },

        /**
         * Cancels changes for the selected time or closes the popup.
         */
        cancelTime() {
            if (this.isChanged) {
                this.showTime(this.indexTime);
            } else {
                this.closePopup();
            }
        },

        /**
         * Handles the event when files fail to load.
         * 
         * @param {Object} info - Information about the failed files.
         */
        filesFailed(info) {
            this.showError(this.$t("File upload failed") + "<br>" + JSON.stringify(info));
        },

        /**
         * Callback function that is called when files are uploaded.
         * 
         * @param {any} response - The response object returned after the files are uploaded.
         */
        filesUploaded(response) {
            let newFiles = JSON.parse(response.xhr.response);
            this.files = [...this.files, ...newFiles];
            if (this.parent && this.parent.editableSource) {
                for (let file of newFiles) {
                    this.parent.addFileFeature(file);
                }
            }
        },

        /**
         * Gets the GPS position.
         */
        async getGPSPosition() {
            let coords = { latitude: null, longitude: null };
            if (this.parent && this.parent.trackPosition) {
                let c = toLonLat(this.parent.geolocation.getPosition(), this.parent.projection);
                coords = { latitude: c[1], longitude: c[0] };
            } else {
                let position = await this.getCurrentPosition();
                coords = position.coords;
            }
            return coords;
        },

        /**
         * Gets the position of selected feature.
         */
        getLocationPosition() {
            let coords = { latitude: null, longitude: null }, f, c;
            if (this.parent && this.parent.selectedFeature) {
                if (this.parent.selectedFeature.getGeometry().getType() == "Polygon") {
                    f = this.parent.selectedFeature.getGeometry().getInteriorPoint().getCoordinates();
                } else {
                    f = this.parent.selectedFeature.getGeometry().getCoordinates();
                }
                c = toLonLat(f, this.parent.projection);
            } else {
                c = [this.feature.general.lon, this.feature.general.lat];
            }
            coords = { latitude: c[1], longitude: c[0] };
            return coords;
        },

        /**
         * Sets the coordinates of a file to GPS position.
         */
        async setGPSPosition() {
            let coords = await this.getGPSPosition();
            this.files[this.editIndex].lat = coords.latitude;
            this.files[this.editIndex].lon = coords.longitude;
        },

        /**
         * Sets the coordinates of a file to feature position.
        */
        setLocationPosition() {
            let coords = this.getLocationPosition();
            this.files[this.editIndex].lat = coords.latitude;
            this.files[this.editIndex].lon = coords.longitude;
        },

        /**
        * Callback function that is called when files are added to the list. Adds compass and position values to files.
        * 
        * @param {any} files - List of files added.
        */
        async filesAdded(files) {
            let coords = { latitude: null, longitude: null };
            if (this.coordType == "gps") {
                coords = await this.getGPSPosition();
            }
            if (coords.latitude == null || coords.longitude == null) {
                // get location of the feature, or inner point of the polygon
                if (this.parent && this.parent.selectedFeature) {
                    coords = this.getLocationPosition();
                }
            }
            for (let file of files) {
                file.lat = coords.latitude;
                file.lon = coords.longitude;
                file.compass = this.$store.compass;
            }
            this.$refs.uploader.upload();
        },

        /**
         * Deletes a single attached file asynchronously.
         *
         * @param {Object} file - The file to be deleted.
         * @returns {Promise} A promise that resolves when the file is successfully deleted.
         */
        async deleteFile(file) {
            if (await this.confirmDialog(this.$t("Delete image?"))) {
                if (await this.delete("User/DeleteFile/" + this.custom_geometry_id + "/" + file.id) != null) {
                    this.files = this.files.filter(x => x.id != file.id);
                    if (this.parent && this.parent.editableSource) {
                        let f = this.parent.editableSource.getFeatureById('f' + file.id);
                        if (f) this.parent.editableSource.removeFeature(f);
                    }
                }
            }
        },

        /**
         * Opens ImageViewer at a given index.
         * 
         * @param {number} index - The index of the image to show.
         */
        showImage(index) {
            console.log(this.filesUrls());
            this.$refs.imageViewer.show(this.filesUrls(), index, true);
        },

        /**
         * Creates array of additional atrributes to be uploaded with files
         * 
         * @returns {Array} The form fields.
         */
        formFields(files) {
            return files.map(x => {
                return {
                    name: x.name,
                    value: JSON.stringify({ compass: x.compass, lat: x.lat, lon: x.lon })
                }
            });
        },

        /**
         * Starts editing the properties of an image.
         * 
         * @param {number} index - The index of the image to edit.
         */
        editImage(index) {
            this.editIndex = index;
            this.copyObject(this.files[index], this.fileSave);
        },

        /**
         * Cancels editing the properties of an image.
         */
        cancelImage() {
            this.copyObject(this.fileSave, this.files[this.editIndex]);
            this.editIndex = -1;
        },

        /**
         * Save modified properties of an image.
         */
        async saveImage() {
            await this.put("User/UpdateFile", this.files[this.editIndex]);
            if (this.parent && this.parent.editableSource) {
                let f = this.parent.editableSource.getFeatureById('f' + this.files[this.editIndex].id);
                if (f) {
                    this.parent.editableSource.removeFeature(f);
                    this.parent.addFileFeature(this.files[this.editIndex]);
                }
            }
            this.editIndex = -1;
        },
    },
}
</script>
<style scoped>
.dialog {
    background-color: white;
    box-shadow: 0 0 10px rgba(0, 0, 0, 0.5);
}

.btn-fixed-width {
    width: 100px;
}
</style>