<template>
<div class="content yaml-item">
<b-field :label="field.name"
:type="status"
>
<template #message>
<div>{{ field.help }}.</div>
<div v-if="status === 'is-danger'">This field is required!</div>
</template>
<div v-if="field.type === 'filename'">
<b-checkbox v-for="f in fileList"
:key="f"
v-model="value"
:native-value="f"
:name="field.key"
@input="save"
>{{ f }}</b-checkbox>
</div>
<YAMLFieldInputArray v-else-if="field.is_array"
:field="field"
:data="fileList"
v-model="value"
@blur="save"
/>
<YAMLFieldInput v-else
:field="field"
:data="field.type === 'filename'? fileList : []"
v-model="value"
@blur="save"
/>
</b-field>
<b-loading :active="loadingFileList" :is-full-page="false"/>
</div>
</template>
<script>
import YAMLFieldInput from "./YAMLFieldInput";
import YAMLFieldInputArray from "./YAMLFieldInputArray";
/**
* @description The YAMLField component displays a single YAML field for editing using the appropriate input option.
*
* @vue-prop field {Field} YAML Field to display.
* @vue-prop [saveStatusLinger=1000] {Number} Display duration (ms) for the save indicator.
*
* @vue-data currentValue=null {any} Current value of the YAML field.
* @vue-data valueChanged=false {Boolean} Whether the current value matches the value saved in the store.
* @vue-data loadingFileList=true {Boolean} Whether the list of files required for the field's options is being fetched.
* @vue-data [fileList=[]] {Array<String>} List of filenames for files required for the field's options.
* @vue-data saveStatus='' {String} Description of the current save status. "clean" = unchanged, "saved" = freshly saved to store, "dirty" = changes pending save
* @vue-data saveStatusTimeout=null {Number} Timeout handle for the event cancelling the 'recently saved' display.
*
* @vue-computed value {any} Value of the YAML field in the store.
* @vue-computed status='' {String} The Buefy style class for displaying the field's save status.
*
* @vue-event save {{key: String, value: any}} Indicate that a new value should be saved as this field's value.
*/
export default {
name: "YAMLField",
components: {YAMLFieldInput, YAMLFieldInputArray},
props: {
field: {type: Object, required: true},
saveStatusLinger: {type: Number, required: false, default: 1000}
},
data: function() {
return {
currentValue: null,
valueChanged: false,
loadingFileList: true,
fileList: [],
saveStatus: '',
saveStatusTimeout: null
}
},
computed: {
value: {
get() {
try {
if(this.valueChanged)
return this.currentValue;
if(this.field.type !== 'time')
return this.field.value;
return this.field.value.map(x => x.replace(/\|/, ''));
} catch (e) {return this.field.is_array? [] : ''}
},
// Set by key, value
set(v) {
this.valueChanged = true;
this.setSaveStatus('dirty');
this.currentValue = v;
}
},
status() {
if(this.field.is_required &&
(this.value === '' || this.value === null))
return 'is-danger';
switch(this.saveStatus) {
case "saved": return 'is-success';
case "dirty": return 'is-info is-light';
}
return '';
}
},
methods: {
/**
* Convert values to the format in which they are saved in the store.
* @param v {any} Value to convert.
* @return {any} Value in the format expected by the back end.
*/
toBackendValue(v) {
if(v === null)
return v;
if(this.field.is_array && v.length)
v = v.filter(x => x !== '');
if(this.field.type === 'time')
v = v.map(x => `${x.substr(0, 2)}|${x.substr(2,2)}`);
return v;
},
/**
* Save the value of the field to the store.
*/
save() {
console.log(`Save: ${this.field.value} -> ${this.value}`)
let input = this.toBackendValue(this.value);
if(input === this.field.value)
return;
this.setSaveStatus('saved');
this.$emit('save', {key: this.field.key, value: input});
},
/**
* Check whether a string is a valid 12h60m format string.
* @param tag {String} Time string to check.
* @return {boolean}
*/
validTime(tag) {
const match = /^(?<h>[0-9]{2})(?<m>[0-9]{2})$/.exec(tag);
if(!match || !match.groups) return false;
if(match.groups.h >= 24) return false;
return match.groups.m < 60;
},
/**
* Search through a repository's path to determine the files that are valid completion values for the field. This is triggered when the component is mounted.
* @return {Array<String>|null}
*/
getFileList() {
if(!this.field.special || this.field.type !== 'filename') {
this.loadingFileList = false;
return;
}
this.fileList = [];
const self = this;
return this.field.special.forEach(path => {
this.$store.dispatch('workshop/pullURL', {
url: `${self.$store.getters['workshop/Repository']().url}/contents/${path}`
})
.then(r => {
r.forEach(f => {
const match = /^(.+)\.[^.]+$/.exec(f.name);
if(!match || match[1][0] === '_')
return;
const entry = match[1];
if(self.fileList.includes(entry))
return;
self.fileList.push(entry)
});
self.loadingFileList = false;
})
.catch(() => null)
})
},
/**
* Set the save status of the field. Setting the status to "saved" will set the status back to "clean" after a delay.
* @param status {String} Status ("clean", "dirty", or "saved") to set the save status to.
*/
setSaveStatus(status) {
if(this.saveStatusTimeout)
clearTimeout(this.saveStatusTimeout);
this.saveStatus = status;
if(status === 'saved')
this.saveStatusTimeout = setTimeout(
() => this.setSaveStatus('clean'),
this.saveStatusLinger
)
}
},
mounted() {return this.getFileList()}
}
</script>
<style scoped>
</style>