mirror of https://github.com/lissy93/dashy
359 lines
12 KiB
Vue
359 lines
12 KiB
Vue
<template>
|
|
<modal
|
|
:name="modalName"
|
|
:resizable="true"
|
|
width="50%"
|
|
height="80%"
|
|
classes="dashy-modal edit-item"
|
|
@closed="modalClosed"
|
|
>
|
|
<div class="edit-item-inner" v-if="allowViewConfig">
|
|
<!-- Title and Item ID -->
|
|
<h3 class="title">Edit Item</h3>
|
|
<p class="sub-title">Editing {{item.title}} (ID: {{itemId}})</p>
|
|
<!-- If no elements added to form, show info message -->
|
|
<p class="warning-note" v-if="formData.length === 0">
|
|
No data configured yet. Click an attribute in the list below to add the field to the form.
|
|
</p>
|
|
<!-- For each data attribute, render the correct type of input field -->
|
|
<div class="row" v-for="(row, index) in formData" :key="row.name">
|
|
<!-- Text box, for text/ number/ raw input elements -->
|
|
<Input
|
|
v-if="row.type === 'text' || row.type === 'number'"
|
|
v-model="formData[index].value"
|
|
:description="row.description"
|
|
:label="row.title || row.name"
|
|
:type="row.type"
|
|
layout="horizontal"
|
|
/>
|
|
<!-- Radio button, used for True or False input -->
|
|
<Radio
|
|
v-else-if="row.type === 'boolean'"
|
|
v-model="formData[index].value"
|
|
:description="row.description"
|
|
:label="row.title || row.name"
|
|
:options="[ ...boolRadioOptions ]"
|
|
:initialOption="boolToStr(formData[index].value)"
|
|
/>
|
|
<!-- Select/ dropdown for enum multiple-choice input -->
|
|
<Select
|
|
v-else-if="row.type === 'select'"
|
|
v-model="formData[index].value"
|
|
:options="formData[index].enum"
|
|
:description="row.description"
|
|
:initialOption="formData[index].value"
|
|
:label="row.title || row.name"
|
|
class="edit-item-select"
|
|
/>
|
|
<!-- Warning note, for any other data types, that aren't yet supported -->
|
|
<div v-else>
|
|
{{ row.name }} cannot currently be edited through the UI.
|
|
</div>
|
|
<BinIcon @click="() => removeField(row.name)" />
|
|
</div>
|
|
<!-- Show Add chips, for adding more data elements to the form -->
|
|
<div class="add-more-inputs" v-if="additionalFormData.length > 0">
|
|
<h4>More Fields</h4>
|
|
<div class="more-fields">
|
|
<span
|
|
v-for="row in additionalFormData"
|
|
:key="row.name"
|
|
@click="() => appendNewField(row.name)"
|
|
class="add-field-tag">
|
|
<AddIcon /> {{ row.title || row.name }}
|
|
</span>
|
|
</div>
|
|
</div>
|
|
<!-- Save to state button -->
|
|
<SaveCancelButtons :saveClick="saveItem" :cancelClick="modalClosed" />
|
|
</div>
|
|
<AccessError v-else />
|
|
</modal>
|
|
</template>
|
|
|
|
<script>
|
|
import AddIcon from '@/assets/interface-icons/interactive-editor-add.svg';
|
|
import BinIcon from '@/assets/interface-icons/interactive-editor-remove.svg';
|
|
import SaveCancelButtons from '@/components/InteractiveEditor/SaveCancelButtons';
|
|
import AccessError from '@/components/Configuration/AccessError';
|
|
import Input from '@/components/FormElements/Input';
|
|
import Radio from '@/components/FormElements/Radio';
|
|
import Select from '@/components/FormElements/Select';
|
|
import StoreKeys from '@/utils/StoreMutations';
|
|
import DashySchema from '@/utils/ConfigSchema';
|
|
import { modalNames } from '@/utils/defaults';
|
|
|
|
export default {
|
|
name: 'EditItem',
|
|
data() {
|
|
return {
|
|
modalName: modalNames.EDIT_ITEM,
|
|
schema: DashySchema.properties.sections.items.properties.items.items.properties,
|
|
formData: [], // Array of form fields
|
|
additionalFormData: [], // Array of not-yet-used form fields
|
|
item: {},
|
|
boolRadioOptions: [
|
|
{ label: 'true', value: 'true' },
|
|
{ label: 'false', value: 'false' },
|
|
],
|
|
};
|
|
},
|
|
props: {
|
|
itemId: String,
|
|
isNew: Boolean,
|
|
parentSectionTitle: String, // If adding new item, which section to add it under
|
|
},
|
|
computed: {
|
|
allowViewConfig() {
|
|
return this.$store.getters.permissions.allowViewConfig;
|
|
},
|
|
},
|
|
components: {
|
|
Input,
|
|
Radio,
|
|
Select,
|
|
AddIcon,
|
|
BinIcon,
|
|
AccessError,
|
|
SaveCancelButtons,
|
|
},
|
|
mounted() {
|
|
if (!this.isNew) { // Get existing item data
|
|
this.item = this.getItemFromState(this.itemId);
|
|
}
|
|
this.formData = this.makeInitialFormData();
|
|
this.$modal.show(modalNames.EDIT_ITEM);
|
|
},
|
|
methods: {
|
|
/* For a given item ID, return the item obj from store */
|
|
getItemFromState(id) {
|
|
return this.$store.getters.getItemById(id);
|
|
},
|
|
/* Using the schema, make data structure for the UI form fields to use */
|
|
makeRowData(property) {
|
|
return {
|
|
name: property,
|
|
description: this.schema[property].description,
|
|
value: this.item[property],
|
|
type: this.getInputType(this.schema[property]),
|
|
enum: this.schema[property].enum,
|
|
title: this.schema[property].title,
|
|
};
|
|
},
|
|
/* Make formatted data structure to be rendered as form elements */
|
|
makeInitialFormData() {
|
|
const formData = [];
|
|
const requiredFields = ['title', 'description', 'url', 'icon', 'target'];
|
|
const unneededFields = ['id'];
|
|
const isPrimaryField = (property) => (
|
|
this.item[property] || requiredFields.includes(property)
|
|
) && !unneededFields.includes(property);
|
|
Object.keys(this.schema).forEach((property) => {
|
|
const singleRow = this.makeRowData(property);
|
|
if (isPrimaryField(property)) {
|
|
formData.push(singleRow);
|
|
} else {
|
|
this.additionalFormData.push(singleRow);
|
|
}
|
|
});
|
|
return formData;
|
|
},
|
|
/* Convert boolean to string */
|
|
boolToStr(bool) {
|
|
if (bool) return 'true';
|
|
if (bool === false) return 'false';
|
|
return undefined;
|
|
},
|
|
/* Adds field from extras list to main form, then removes from extras list */
|
|
appendNewField(fieldId) {
|
|
Object.keys(this.schema).forEach((property) => {
|
|
if (property === fieldId) {
|
|
this.formData.push(this.makeRowData(property));
|
|
}
|
|
});
|
|
this.additionalFormData.forEach((elem, index) => {
|
|
if (elem.name === fieldId) {
|
|
this.additionalFormData.splice(index, 1);
|
|
}
|
|
});
|
|
},
|
|
/* On Remove Field click, removes field from main form, and adds to chip list */
|
|
removeField(fieldId) {
|
|
this.formData.forEach((elem, index) => {
|
|
if (elem.name === fieldId) {
|
|
this.formData.splice(index, 1);
|
|
this.additionalFormData.push(elem);
|
|
}
|
|
});
|
|
},
|
|
/* Use schema to determine type of form element to render, for a given attribute */
|
|
getInputType(schemaItem) {
|
|
const definedType = schemaItem.type;
|
|
if (definedType === 'text') {
|
|
return 'text';
|
|
} else if (definedType === 'number') {
|
|
return 'number';
|
|
} else if (definedType === 'boolean') {
|
|
return 'boolean';
|
|
} else if (schemaItem.enum) {
|
|
return 'select';
|
|
}
|
|
return 'text';
|
|
},
|
|
/* Saves the updated item to VueX Store */
|
|
saveItem() {
|
|
// Convert form data back into section.item data structure
|
|
const structured = {};
|
|
this.formData.forEach((row) => { structured[row.name] = row.value; });
|
|
if (!structured.title) { // Missing title, show error and don't proceed
|
|
this.$toasted.show(
|
|
this.$t('interactive-editor.edit-item.missing-title-err'),
|
|
{ className: 'toast-error' },
|
|
);
|
|
} else {
|
|
// Some attributes need a little extra formatting
|
|
const newItem = this.formatBeforeSave(structured);
|
|
if (this.isNew) { // Insert new item into data store
|
|
newItem.id = `temp_${newItem.title}`;
|
|
const payload = { newItem, targetSection: this.parentSectionTitle };
|
|
this.$store.commit(StoreKeys.INSERT_ITEM, payload);
|
|
} else { // Update existing item from form data, in the store
|
|
this.$store.commit(StoreKeys.UPDATE_ITEM, { newItem, itemId: this.itemId });
|
|
}
|
|
// If we're not already in edit mode, enable it now
|
|
this.$store.commit(StoreKeys.SET_EDIT_MODE, true);
|
|
// Close edit menu
|
|
this.$emit('closeEditMenu');
|
|
}
|
|
},
|
|
/* Some fields require a bit of extra processing before they're saved */
|
|
formatBeforeSave(item) {
|
|
const newItem = item;
|
|
newItem.id = this.itemId;
|
|
if (newItem.hotkey) newItem.hotkey = parseInt(newItem.hotkey, 10);
|
|
const strToTags = (str) => {
|
|
const tagArr = str.split(',');
|
|
return tagArr.map((tag) => tag.trim().toLowerCase().replace(/[^a-z0-9]+/, ''));
|
|
};
|
|
const strToBool = (str) => {
|
|
if (str === undefined) return undefined;
|
|
return str === 'true';
|
|
};
|
|
if (newItem.tags) newItem.tags = strToTags(newItem.tags);
|
|
if (newItem.statusCheck) newItem.statusCheck = strToBool(newItem.statusCheck);
|
|
if (newItem.statusCheckAllowInsecure) {
|
|
newItem.statusCheckAllowInsecure = strToBool(newItem.statusCheckAllowInsecure);
|
|
}
|
|
// if (newItem.hotkey) newItem.hotkey = parseInt(newItem.hotkey, 10);
|
|
return newItem;
|
|
},
|
|
/* Clean up work, triggered when modal closed */
|
|
modalClosed() {
|
|
this.$store.commit(StoreKeys.SET_MODAL_OPEN, false);
|
|
this.$emit('closeEditMenu');
|
|
},
|
|
},
|
|
};
|
|
</script>
|
|
|
|
<style lang="scss">
|
|
@import '@/styles/style-helpers.scss';
|
|
@import '@/styles/media-queries.scss';
|
|
|
|
.edit-item-inner {
|
|
padding: 1rem;
|
|
background: var(--interactive-editor-background);
|
|
color: var(--interactive-editor-color);
|
|
height: 100%;
|
|
overflow-y: auto;
|
|
@extend .svg-button;
|
|
h3.title {
|
|
font-size: 1.5rem;
|
|
margin: 0.25rem 0;
|
|
}
|
|
p.sub-title {
|
|
margin: 0.25rem 0;
|
|
font-size: 0.8rem;
|
|
font-style: italic;
|
|
opacity: var(--dimming-factor);
|
|
}
|
|
p.warning-note {
|
|
color: var(--warning);
|
|
}
|
|
.row {
|
|
display: flex;
|
|
padding: 0.5rem 0.25rem;
|
|
&:not(:last-child) {
|
|
border-bottom: 1px dotted var(--interactive-editor-color);
|
|
}
|
|
.input-container, .select-container {
|
|
width: 100%;
|
|
input.input-field {
|
|
font-size: 1rem;
|
|
padding: 0.35rem 0.5rem;
|
|
}
|
|
}
|
|
}
|
|
.more-fields {
|
|
display: flex;
|
|
flex-wrap: wrap;
|
|
span.add-field-tag {
|
|
margin: 0.2rem;
|
|
padding: 0.2rem 0.5rem;;
|
|
min-width: 2rem;
|
|
display: flex;
|
|
align-items: center;
|
|
cursor: pointer;
|
|
text-align: center;
|
|
border: 1px solid var(--interactive-editor-color);
|
|
border-radius: var(--curve-factor);
|
|
&:hover {
|
|
background: var(--interactive-editor-color);
|
|
color: var(--interactive-editor-background);
|
|
svg {
|
|
background: var(--interactive-editor-color);
|
|
path { fill: var(--interactive-editor-background); }
|
|
}
|
|
}
|
|
svg {
|
|
margin-right: 0.25rem;
|
|
border: none;
|
|
}
|
|
}
|
|
}
|
|
|
|
/* Override form element colors, with local CSS variables */
|
|
div.input-container input.input-field,
|
|
.radio-container div.radio-wrapper,
|
|
.form-dropdown div.vs__dropdown-toggle {
|
|
color: var(--interactive-editor-color);
|
|
border-color: var(--interactive-editor-color);
|
|
background: var(--interactive-editor-background);
|
|
}
|
|
svg {
|
|
path { fill: var(--interactive-editor-color); }
|
|
background: var(--interactive-editor-background);
|
|
&:hover, &.selected {
|
|
path { fill: var(--interactive-editor-background); }
|
|
background: var(--interactive-editor-color);
|
|
}
|
|
}
|
|
.edit-item-select .v-select {
|
|
input.vs__search { color: var(--interactive-editor-color); }
|
|
div.vs__dropdown-toggle {
|
|
border-color: var(--interactive-editor-color);
|
|
background: var(--interactive-editor-background);
|
|
span.vs__selected { color: var(--interactive-editor-color); }
|
|
.vs__actions svg {
|
|
background: var(--interactive-editor-background);
|
|
path { fill: var(--interactive-editor-color); }
|
|
&:hover {
|
|
background: var(--interactive-editor-color);
|
|
path { fill: var(--interactive-editor-background); }
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
</style>
|