mirror of
https://gitlab.com/wgp/dougal/software.git
synced 2025-12-06 11:37:08 +00:00
Reimplement <dougal-project-settings-online-line-name-format/>.
Closes #297.
This commit is contained in:
@@ -0,0 +1,269 @@
|
||||
<template>
|
||||
<v-row dense no-gutters>
|
||||
|
||||
<v-col>
|
||||
<slot name="prepend"></slot>
|
||||
</v-col>
|
||||
|
||||
<v-col cols="2">
|
||||
<v-chip v-if="value.item && !readonly"
|
||||
outlined
|
||||
label
|
||||
small
|
||||
:color="colour"
|
||||
:title="description"
|
||||
>{{name}}</v-chip>
|
||||
<v-select v-else-if="items.length && !readonly"
|
||||
label="Item"
|
||||
:items=items
|
||||
v-model="value.item"
|
||||
dense
|
||||
title="Select an item to use as a field"
|
||||
></v-select>
|
||||
</v-col>
|
||||
|
||||
<v-col>
|
||||
<v-select v-if="type == 'boolean'"
|
||||
label="Condition"
|
||||
:items="[true, false]"
|
||||
v-model="value.when"
|
||||
dense
|
||||
title="Use this configuration only when the value of this item matches the selected state. This allows the user to configure different values for true and false conditions."
|
||||
></v-select>
|
||||
</v-col>
|
||||
|
||||
<v-col>
|
||||
<v-text-field v-if="type == 'boolean' || type == 'text'"
|
||||
class="ml-3"
|
||||
dense
|
||||
label="Value"
|
||||
v-model="value.value"
|
||||
title="This literal text will be inserted at the designated position"
|
||||
></v-text-field>
|
||||
<v-menu v-else-if="type == 'number'"
|
||||
max-width="600"
|
||||
:close-on-content-click="false"
|
||||
offset-y
|
||||
>
|
||||
<template v-slot:activator="{ on, attrs }">
|
||||
<v-chip
|
||||
class="ml-3"
|
||||
small
|
||||
:light="$vuetify.theme.isDark"
|
||||
:dark="!$vuetify.theme.isDark"
|
||||
:color="value.scale_offset != null || value.scale_multiplier != null ? 'primary' : ''"
|
||||
:title="`Number scaling${ value.scale_offset != null ? ('\nOffset: ' + value.scale_offset) : '' }${ value.scale_multiplier != null ? ('\nMultiplier: ' + value.scale_multiplier) : ''}`"
|
||||
v-bind="attrs"
|
||||
v-on="on"
|
||||
>
|
||||
<v-icon small>mdi-ruler</v-icon>
|
||||
</v-chip>
|
||||
</template>
|
||||
|
||||
<v-card rounded outlined>
|
||||
<v-card-text>
|
||||
<v-row dense no-gutters>
|
||||
<v-text-field
|
||||
type="number"
|
||||
dense
|
||||
clearable
|
||||
label="Offset"
|
||||
title="Offset the value by this amount (after scaling)"
|
||||
v-model.number="value.scale_offset"
|
||||
></v-text-field>
|
||||
</v-row>
|
||||
<v-row dense no-gutters>
|
||||
<v-text-field
|
||||
type="number"
|
||||
dense
|
||||
clearable
|
||||
label="Scale"
|
||||
title="Mutiply the value by this amount (before scaling)"
|
||||
v-model.number="value.scale_multiplier"
|
||||
></v-text-field>
|
||||
</v-row>
|
||||
</v-card-text>
|
||||
</v-card>
|
||||
</v-menu>
|
||||
</v-col>
|
||||
|
||||
<v-col>
|
||||
<v-text-field
|
||||
class="ml-3"
|
||||
dense
|
||||
label="From"
|
||||
type="number"
|
||||
min="0"
|
||||
v-model.number="value.offset"
|
||||
:readonly="readonly"
|
||||
></v-text-field>
|
||||
</v-col>
|
||||
|
||||
<v-col>
|
||||
<v-text-field
|
||||
class="ml-3"
|
||||
dense
|
||||
label="Length"
|
||||
type="number"
|
||||
min="0"
|
||||
v-model.number="value.length"
|
||||
:readonly="readonly"
|
||||
></v-text-field>
|
||||
</v-col>
|
||||
|
||||
<v-col>
|
||||
<v-menu v-if="value.length > 1"
|
||||
max-width="600"
|
||||
:close-on-content-click="false"
|
||||
offset-y
|
||||
:disabled="!(value.length>1)"
|
||||
>
|
||||
<template v-slot:activator="{ on, attrs }">
|
||||
<v-chip
|
||||
class="ml-3"
|
||||
small
|
||||
:light="$vuetify.theme.isDark"
|
||||
:dark="!$vuetify.theme.isDark"
|
||||
title="Text alignment"
|
||||
v-bind="attrs"
|
||||
v-on="on"
|
||||
:disabled="!(value.length>1)"
|
||||
>
|
||||
<v-icon small v-if="value.pad_side=='right'">mdi-format-align-left</v-icon>
|
||||
<v-icon small v-else-if="value.pad_side=='left'">mdi-format-align-right</v-icon>
|
||||
<v-icon small v-else>mdi-format-align-justify</v-icon>
|
||||
</v-chip>
|
||||
</template>
|
||||
|
||||
<v-card rounded outlined>
|
||||
<v-card-text>
|
||||
<v-row dense no-gutters>
|
||||
<v-select
|
||||
label="Alignment"
|
||||
clearable
|
||||
:items='[{text:"Left", value:"right"}, {text:"Right", value:"left"}]'
|
||||
v-model="value.pad_side"
|
||||
></v-select>
|
||||
</v-row>
|
||||
<v-row dense no-gutters v-if="value.pad_side">
|
||||
<v-text-field
|
||||
dense
|
||||
label="Pad character"
|
||||
title="Fill the width of the field on the opposite side by padding with this character"
|
||||
v-model="value.pad_string"
|
||||
></v-text-field>
|
||||
</v-row>
|
||||
</v-card-text>
|
||||
</v-card>
|
||||
</v-menu>
|
||||
</v-col>
|
||||
|
||||
<v-col>
|
||||
<slot name="append"></slot>
|
||||
</v-col>
|
||||
|
||||
</v-row>
|
||||
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
|
||||
.input {
|
||||
flex: 1 1 auto;
|
||||
line-height: 20px;
|
||||
padding: 8px 0 8px;
|
||||
min-height: 32px;
|
||||
max-height: 32px;
|
||||
max-width: 100%;
|
||||
min-width: 0px;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.input >>> .chunk {
|
||||
padding-inline: 1px;
|
||||
border: 1px solid;
|
||||
}
|
||||
|
||||
.input >>> .chunk-empty {
|
||||
padding-inline: 1px;
|
||||
}
|
||||
|
||||
.input >>> .chunk-overlap {
|
||||
padding-inline: 1px;
|
||||
border: 1px solid grey;
|
||||
color: grey;
|
||||
}
|
||||
</style>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: "DougalFixedStringEncoderField",
|
||||
|
||||
components: {
|
||||
},
|
||||
|
||||
props: {
|
||||
value: Object,
|
||||
properties: Object,
|
||||
colour: String,
|
||||
readonly: Boolean,
|
||||
},
|
||||
|
||||
data () {
|
||||
return {
|
||||
}
|
||||
},
|
||||
|
||||
watch: {
|
||||
"value.value": function (value, old) {
|
||||
if (value != null && String(value).length > this.value.length) {
|
||||
this.value.length = String(value).length;
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
computed: {
|
||||
|
||||
field: {
|
||||
get () {
|
||||
return this.value;
|
||||
},
|
||||
set (v) {
|
||||
console.log("input", v);
|
||||
this.$emit("input", v);
|
||||
}
|
||||
},
|
||||
|
||||
item () {
|
||||
return this.properties?.[this.value?.item] ?? {};
|
||||
},
|
||||
|
||||
items () {
|
||||
return Object.entries(this.properties).map(i => ({text: i[1].summary ?? i[0], value: i[0]}))
|
||||
},
|
||||
|
||||
name () {
|
||||
// TODO Use properties[item].summary or similar
|
||||
return this.item?.summary ?? this.value.item ?? "???";
|
||||
},
|
||||
|
||||
type () {
|
||||
return this.item?.type ?? typeof this.value?.item ?? "undefined";
|
||||
},
|
||||
|
||||
description () {
|
||||
return this.item?.description;
|
||||
}
|
||||
},
|
||||
|
||||
methods: {
|
||||
reset () {
|
||||
}
|
||||
},
|
||||
|
||||
mounted () {
|
||||
this.reset();
|
||||
}
|
||||
|
||||
}
|
||||
</script>
|
||||
@@ -0,0 +1,351 @@
|
||||
<template>
|
||||
<v-input
|
||||
class="v-text-field"
|
||||
:hint="hint"
|
||||
persistent-hint
|
||||
:value="text"
|
||||
>
|
||||
<label
|
||||
class="v-label"
|
||||
:class="[ $vuetify.theme.isDark && 'theme--dark', text && text.length && 'v-label--active' ]"
|
||||
style="left: 0px; right: auto; position: absolute;"
|
||||
>{{ label }}</label>
|
||||
<div class="input" slot="default"
|
||||
v-html="html"
|
||||
>
|
||||
</div>
|
||||
<template slot="append">
|
||||
<v-menu
|
||||
scrollable
|
||||
offset-y
|
||||
:close-on-content-click="false"
|
||||
>
|
||||
|
||||
<template v-slot:activator="{on, attrs}">
|
||||
<v-btn
|
||||
icon
|
||||
v-bind="attrs"
|
||||
v-on="on"
|
||||
>
|
||||
<v-icon title="Configure sample values">mdi-list-box-outline</v-icon>
|
||||
</v-btn>
|
||||
</template>
|
||||
|
||||
<v-card>
|
||||
<v-card-title>Sample values</v-card-title>
|
||||
<v-card-subtitle>Enter sample values to test your configuration</v-card-subtitle>
|
||||
|
||||
<v-divider></v-divider>
|
||||
|
||||
<v-card-text>
|
||||
|
||||
<v-container>
|
||||
<v-row v-for="(prop, key) in properties" :key="key">
|
||||
<template v-if="prop.type == 'boolean'">
|
||||
<v-col cols="6" align-self="center">
|
||||
<v-chip
|
||||
outlined
|
||||
label
|
||||
small
|
||||
:color="getHSLColourFor(key)"
|
||||
:title="prop.description"
|
||||
>{{prop.summary || key}}</v-chip>
|
||||
</v-col>
|
||||
<v-col cols="6" align-self="center">
|
||||
<v-simple-checkbox v-model="values[key]"></v-simple-checkbox>
|
||||
</v-col>
|
||||
</template>
|
||||
<template v-else-if="key != 'text'">
|
||||
<v-col cols="6" align-self="center">
|
||||
<v-chip
|
||||
outlined
|
||||
label
|
||||
small
|
||||
:color="getHSLColourFor(key)"
|
||||
:title="prop.description"
|
||||
>{{prop.summary || key}}</v-chip>
|
||||
</v-col>
|
||||
<v-col cols="6" align-self="center">
|
||||
<v-text-field v-if="prop.type == 'number'"
|
||||
:type="prop.type"
|
||||
:label="prop.summary || key"
|
||||
:hint="prop.description"
|
||||
v-model.number="values[key]"
|
||||
></v-text-field>
|
||||
<v-text-field v-else
|
||||
:type="prop.type"
|
||||
:label="prop.summary || key"
|
||||
:hint="prop.description"
|
||||
v-model="values[key]"
|
||||
></v-text-field>
|
||||
</v-col>
|
||||
</template>
|
||||
</v-row>
|
||||
</v-container>
|
||||
|
||||
</v-card-text>
|
||||
</v-card>
|
||||
|
||||
</v-menu>
|
||||
</template>
|
||||
<v-icon slot="prepend">mdi-list</v-icon>
|
||||
</v-input>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
|
||||
.input {
|
||||
flex: 1 1 auto;
|
||||
line-height: 20px;
|
||||
padding: 8px 0 8px;
|
||||
min-height: 32px;
|
||||
max-height: 32px;
|
||||
max-width: 100%;
|
||||
min-width: 0px;
|
||||
width: 100%;
|
||||
white-space-collapse: preserve;
|
||||
}
|
||||
|
||||
.multiline {
|
||||
font-family: mono;
|
||||
white-space: pre;
|
||||
overflow-x: auto;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.multiline >>> .line-number {
|
||||
display: inline-block;
|
||||
font-size: 75%;
|
||||
width: 5ex;
|
||||
margin-inline-end: 1ex;
|
||||
text-align: right;
|
||||
border: none;
|
||||
position: relative;
|
||||
top: -1px;
|
||||
}
|
||||
|
||||
.input, .multiline >>> .chunk-field {
|
||||
padding-inline: 1px;
|
||||
border: 1px solid;
|
||||
}
|
||||
|
||||
.input, .multiline >>> .chunk-fixed {
|
||||
padding-inline: 1px;
|
||||
border: 1px dashed;
|
||||
}
|
||||
|
||||
.input, .multiline >>> .chunk-empty {
|
||||
padding-inline: 1px;
|
||||
}
|
||||
|
||||
.input, .multiline >>> .chunk-overlap {
|
||||
padding-inline: 1px;
|
||||
border: 1px solid grey;
|
||||
color: grey;
|
||||
}
|
||||
|
||||
.input >>> .chunk-mismatch {
|
||||
padding-inline: 1px;
|
||||
border: 2px solid red !important;
|
||||
}
|
||||
|
||||
</style>
|
||||
|
||||
<script>
|
||||
import { getHSLColourFor } from '@/lib/hsl'
|
||||
|
||||
export default {
|
||||
name: "DougalFixedStringEncoderSample",
|
||||
|
||||
components: {
|
||||
},
|
||||
|
||||
mixins: [
|
||||
{
|
||||
methods: {
|
||||
getHSLColourFor
|
||||
}
|
||||
}
|
||||
],
|
||||
|
||||
props: {
|
||||
properties: { type: Object, default: () => ({}) },
|
||||
fields: { type: Array, default: () => [] },
|
||||
values: { type: Object, default: () => ({}) },
|
||||
readonly: Boolean,
|
||||
label: String,
|
||||
hint: String,
|
||||
},
|
||||
|
||||
data () {
|
||||
return {
|
||||
}
|
||||
},
|
||||
|
||||
computed: {
|
||||
|
||||
chunks () {
|
||||
const properties = this.properties;
|
||||
const fields = this.fields;
|
||||
const values = this.values;
|
||||
const str = "";
|
||||
const chunks = [];
|
||||
|
||||
for (const field of fields) {
|
||||
const value = this.fieldValue(properties, field, values);
|
||||
|
||||
if (value != null) {
|
||||
const chunk = {
|
||||
start: field.offset,
|
||||
end: field.offset + field.length - 1,
|
||||
colour: this.getHSLColourFor(field.item),
|
||||
class: field.item == "text" ? "fixed" : "field",
|
||||
text: value
|
||||
}
|
||||
chunks.push(chunk);
|
||||
}
|
||||
}
|
||||
|
||||
return chunks;
|
||||
},
|
||||
|
||||
text () {
|
||||
return this.sample(this.properties, this.fields, this.values);
|
||||
},
|
||||
|
||||
html () {
|
||||
return this.renderTextLine(this.text);
|
||||
}
|
||||
|
||||
},
|
||||
|
||||
watch: {
|
||||
},
|
||||
|
||||
methods: {
|
||||
|
||||
fieldValue (properties, field, values) {
|
||||
let value;
|
||||
|
||||
if (field.item == "text") {
|
||||
value = field.value;
|
||||
} else if (properties[field.item]?.type == "boolean") {
|
||||
if (values[field.item] === field.when) {
|
||||
value = field.value;
|
||||
}
|
||||
} else {
|
||||
value = values[field.item];
|
||||
}
|
||||
|
||||
if (value != null) {
|
||||
|
||||
if (properties[field.item]?.type == "number") {
|
||||
if (field.scale_multiplier != null) {
|
||||
value *= field.scale_multiplier;
|
||||
}
|
||||
if (field.scale_offset != null) {
|
||||
value += field.scale_offset;
|
||||
}
|
||||
|
||||
if (field.format == "integer") {
|
||||
value = Math.round(value);
|
||||
}
|
||||
}
|
||||
|
||||
value = String(value);
|
||||
if (field.pad_side == "left") {
|
||||
value = value.padStart(field.length, field.pad_string ?? " ");
|
||||
} else if (field.pad_side == "right") {
|
||||
value = value.padEnd(field.length, field.pad_string ?? " ");
|
||||
}
|
||||
|
||||
return value;
|
||||
}
|
||||
},
|
||||
|
||||
sample (properties, fields, values, str = "") {
|
||||
|
||||
const length = fields.reduce( (acc, cur) => (cur.offset + cur.length) > acc ? (cur.offset + cur.length) : acc, str.length )
|
||||
|
||||
str = str.padEnd(length);
|
||||
|
||||
for (const field of fields) {
|
||||
//console.log("FIELD", field);
|
||||
const value = this.fieldValue(properties, field, values);
|
||||
if (value != null) {
|
||||
str = str.slice(0, field.offset) + value + str.slice(field.offset + field.length);
|
||||
}
|
||||
}
|
||||
|
||||
return str;
|
||||
},
|
||||
|
||||
/** Return a `<span>` opening tag.
|
||||
*/
|
||||
style (name, colour) {
|
||||
return colour
|
||||
? `<span class="${name}" style="color:${colour};border-color:${colour}">`
|
||||
: `<span class="${name}">`;
|
||||
},
|
||||
|
||||
/** Return an array of the intervals that intersect `pos`.
|
||||
* May be empty.
|
||||
*/
|
||||
chunksFor (pos) {
|
||||
return this.chunks.filter( chunk =>
|
||||
pos >= chunk.start &&
|
||||
pos <= chunk.end
|
||||
)
|
||||
},
|
||||
|
||||
/*
|
||||
* Algorithm:
|
||||
*
|
||||
* Go through every character of one line of text and determine in which
|
||||
* part(s) it falls in, if any. Collect adjacent same parts into <span/>
|
||||
* elements.
|
||||
*/
|
||||
renderTextLine (text) {
|
||||
const parts = [];
|
||||
|
||||
let prevStyle;
|
||||
|
||||
for (const pos in text) {
|
||||
const chunks = this.chunksFor(pos);
|
||||
const isEmpty = chunks.length == 0;
|
||||
const isOverlap = chunks.length > 1;
|
||||
const isMismatch = chunks[0]?.text &&
|
||||
(text.substring(chunks[0].start, chunks[0].end+1) != chunks[0].text);
|
||||
|
||||
const style = isEmpty
|
||||
? this.style("chunk-empty")
|
||||
: isMismatch
|
||||
? this.style("chunk-mismatch", chunks[0].colour)
|
||||
: isOverlap
|
||||
? this.style("chunk-overlap")
|
||||
: this.style("chunk-"+chunks[0].class, chunks[0].colour);
|
||||
|
||||
if (style != prevStyle) {
|
||||
if (prevStyle) {
|
||||
parts.push("</span>");
|
||||
}
|
||||
parts.push(style);
|
||||
}
|
||||
parts.push(text[pos]);
|
||||
prevStyle = style;
|
||||
}
|
||||
|
||||
if (parts.length) {
|
||||
parts.push("</span>");
|
||||
}
|
||||
|
||||
return parts.join("");
|
||||
},
|
||||
|
||||
},
|
||||
|
||||
mounted () {
|
||||
}
|
||||
|
||||
}
|
||||
</script>
|
||||
@@ -0,0 +1,307 @@
|
||||
<template>
|
||||
<v-card flat elevation="0">
|
||||
<v-card-title v-if="title">{{ title }}</v-card-title>
|
||||
<v-card-subtitle v-if="subtitle">{{ subtitle }}</v-card-subtitle>
|
||||
<v-card-text>
|
||||
<v-form>
|
||||
|
||||
<!-- Sample text -->
|
||||
|
||||
<dougal-fixed-string-encoder-sample
|
||||
:label="label"
|
||||
:hint="hint"
|
||||
:properties="properties"
|
||||
:fields="fields"
|
||||
:values.sync="values"
|
||||
></dougal-fixed-string-encoder-sample>
|
||||
|
||||
<!-- Fields -->
|
||||
|
||||
<v-container>
|
||||
|
||||
<v-row no-gutters class="mb-2">
|
||||
<h4>Fields</h4>
|
||||
</v-row>
|
||||
|
||||
<dougal-fixed-string-encoder-field v-for="(field, key) in fields" :key="key"
|
||||
v-model="fields[key]"
|
||||
:properties="properties"
|
||||
:colour="getHSLColourFor(field.item)"
|
||||
:readonly="readonly"
|
||||
>
|
||||
<template v-slot:append v-if="editableFieldList && !readonly">
|
||||
<v-btn
|
||||
class="ml-3"
|
||||
fab
|
||||
text
|
||||
small
|
||||
title="Remove this field"
|
||||
>
|
||||
<v-icon
|
||||
color="error"
|
||||
@click="removeField(key)"
|
||||
>mdi-minus</v-icon>
|
||||
</v-btn>
|
||||
</template>
|
||||
</dougal-fixed-string-encoder-field>
|
||||
|
||||
<v-row no-gutters class="mb-2" v-if="editableFieldList && !readonly">
|
||||
<h4>Add new field</h4>
|
||||
</v-row>
|
||||
|
||||
<dougal-fixed-string-encoder-field v-if="editableFieldList && !readonly"
|
||||
v-model="newField"
|
||||
:properties="properties"
|
||||
:colour="getHSLColourFor(newField.item)"
|
||||
>
|
||||
<template v-slot:prepend>
|
||||
<v-btn v-if="isFieldDirty(newField)"
|
||||
top
|
||||
text
|
||||
small
|
||||
title="Reset"
|
||||
>
|
||||
<v-icon
|
||||
color="warning"
|
||||
@click="resetField(newField)"
|
||||
>mdi-backspace-reverse-outline</v-icon>
|
||||
</v-btn>
|
||||
</template>
|
||||
<template v-slot:append>
|
||||
<v-btn
|
||||
class="ml-3"
|
||||
fab
|
||||
text
|
||||
small
|
||||
title="Add field"
|
||||
:disabled="isFieldValid(newField) !== true"
|
||||
>
|
||||
<v-icon
|
||||
color="primary"
|
||||
@click="addField(newField)"
|
||||
>mdi-plus</v-icon>
|
||||
</v-btn>
|
||||
</template>
|
||||
</dougal-fixed-string-encoder-field>
|
||||
|
||||
</v-container>
|
||||
|
||||
|
||||
|
||||
</v-form>
|
||||
</v-card-text>
|
||||
<v-card-actions>
|
||||
</v-card-actions>
|
||||
</v-card>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
|
||||
.input {
|
||||
flex: 1 1 auto;
|
||||
line-height: 20px;
|
||||
padding: 8px 0 8px;
|
||||
min-height: 32px;
|
||||
max-height: 32px;
|
||||
max-width: 100%;
|
||||
min-width: 0px;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.input, .multiline >>> .chunk-field {
|
||||
padding-inline: 1px;
|
||||
border: 1px solid;
|
||||
}
|
||||
|
||||
.input, .multiline >>> .chunk-fixed {
|
||||
padding-inline: 1px;
|
||||
border: 1px dashed;
|
||||
}
|
||||
|
||||
.input, .multiline >>> .chunk-empty {
|
||||
padding-inline: 1px;
|
||||
}
|
||||
|
||||
.input, .multiline >>> .chunk-overlap {
|
||||
padding-inline: 1px;
|
||||
border: 1px solid grey;
|
||||
color: grey;
|
||||
}
|
||||
|
||||
.input >>> .chunk-mismatch {
|
||||
padding-inline: 1px;
|
||||
border: 2px solid red !important;
|
||||
}
|
||||
</style>
|
||||
|
||||
<script>
|
||||
import { getHSLColourFor } from '@/lib/hsl'
|
||||
import DougalFixedStringEncoderField from './fixed-string-encoder-field'
|
||||
import DougalFixedStringEncoderSample from './fixed-string-encoder-sample'
|
||||
|
||||
export default {
|
||||
name: "DougalFixedStringEncoder",
|
||||
|
||||
components: {
|
||||
DougalFixedStringEncoderField,
|
||||
DougalFixedStringEncoderSample
|
||||
},
|
||||
|
||||
mixins: [
|
||||
{
|
||||
methods: {
|
||||
getHSLColourFor
|
||||
}
|
||||
}
|
||||
],
|
||||
|
||||
props: {
|
||||
properties: { type: Object },
|
||||
fields: { type: Array },
|
||||
values: { type: Object },
|
||||
editableFieldList: { type: Boolean, default: true },
|
||||
readonly: Boolean,
|
||||
title: String,
|
||||
subtitle: String,
|
||||
label: String,
|
||||
hint: String,
|
||||
},
|
||||
|
||||
data () {
|
||||
return {
|
||||
//< The reason for not using this.text directly is that at some point
|
||||
//< we might extend this component to allow editing the sample text.
|
||||
text_: "",
|
||||
//< The value of a fixed string that should be always present at a specific position
|
||||
fixedName: "",
|
||||
fixedOffset: 0,
|
||||
//< The name of a new field to add.
|
||||
fieldName: "",
|
||||
newField: {
|
||||
item: null,
|
||||
when: null,
|
||||
offset: null,
|
||||
length: null,
|
||||
value: null,
|
||||
pad_side: null,
|
||||
pad_string: null
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
computed: {
|
||||
|
||||
chunks () {
|
||||
const properties = this.properties;
|
||||
const fields = this.fields;
|
||||
const values = this.values;
|
||||
const str = "";
|
||||
const chunks = [];
|
||||
|
||||
for (const field of fields) {
|
||||
|
||||
//console.log("FIELD", structuredClone(field));
|
||||
//console.log("VALUES DATA", values[field.item]);
|
||||
let value;
|
||||
|
||||
if (field.item == "text") {
|
||||
value = field.value;
|
||||
} else if (properties[field.item]?.type == "boolean") {
|
||||
if (values[field.item] === field.when) {
|
||||
value = field.value;
|
||||
}
|
||||
} else {
|
||||
value = values[field.item];
|
||||
}
|
||||
|
||||
if (value != null) {
|
||||
|
||||
value = String(value);
|
||||
if (field.pad_side == "left") {
|
||||
value = value.padStart(field.length, field.pad_string);
|
||||
} else {
|
||||
value = value.padEnd(field.length, field.pad_string);
|
||||
}
|
||||
|
||||
const chunk = {
|
||||
start: field.offset,
|
||||
end: field.offset + field.length - 1,
|
||||
colour: this.getHSLColourFor(field.item),
|
||||
class: field.item == "text" ? "fixed" : "field",
|
||||
text: value
|
||||
}
|
||||
|
||||
//console.log("CHUNK", chunk);
|
||||
chunks.push(chunk);
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
return chunks;
|
||||
},
|
||||
|
||||
html () {
|
||||
return this.renderTextLine(this.sample(this.properties, this.fields, this.values));
|
||||
//return this.sample(this.properties, this.fields, this.values);
|
||||
}
|
||||
|
||||
},
|
||||
|
||||
watch: {
|
||||
},
|
||||
|
||||
methods: {
|
||||
|
||||
isFieldDirty (field) {
|
||||
return Object.entries(field).reduce( (acc, cur) => cur[1] === null ? acc : true, false );
|
||||
},
|
||||
|
||||
isFieldValid (field) {
|
||||
if (!field.item) return "Missing item";
|
||||
if (typeof field.offset !== "number" || field.offset < 0) return "Missing offset";
|
||||
if (typeof field.length !== "number" || field.length < 1) return "Missing length";
|
||||
if (!this.properties[field.item]) return "Unrecognised property";
|
||||
if (this.properties[field.item].type == "text" && !field.value?.length) return "Missing value";
|
||||
if (this.properties[field.item].type == "boolean" && !field.value?.length) return "Missing value (boolean)";
|
||||
if(!!field.pad_side && !field.pad_string) return "Missing pad string";
|
||||
|
||||
return true;
|
||||
},
|
||||
|
||||
resetField (field) {
|
||||
field.item = null;
|
||||
field.when = null;
|
||||
field.offset = null;
|
||||
field.length = null;
|
||||
field.value = null;
|
||||
field.pad_side = null;
|
||||
field.pad_string = null;
|
||||
|
||||
return field;
|
||||
},
|
||||
|
||||
addField (field) {
|
||||
if (this.isFieldValid(field)) {
|
||||
const fields = structuredClone(this.fields);
|
||||
fields.push({...field});
|
||||
this.resetField(field);
|
||||
console.log("update:fields", fields);
|
||||
this.$emit("update:fields", fields);
|
||||
}
|
||||
},
|
||||
|
||||
removeField (key) {
|
||||
console.log("REMOVE", "update:fields", key, this.fields);
|
||||
const fields = structuredClone(this.fields);
|
||||
fields.splice(key, 1);
|
||||
this.$emit("update:fields", fields);
|
||||
},
|
||||
|
||||
},
|
||||
|
||||
mounted () {
|
||||
}
|
||||
|
||||
}
|
||||
</script>
|
||||
@@ -4,20 +4,16 @@
|
||||
<v-card-subtitle>Line name decoding configuration for real-time data</v-card-subtitle>
|
||||
<v-card-text>
|
||||
<v-form>
|
||||
<v-text-field
|
||||
label="Example file name"
|
||||
hint="Enter the name of a representative file to make it easier to visualise your configuration"
|
||||
persistent-hint
|
||||
v-model="cwo.lineNameInfo.example"
|
||||
></v-text-field>
|
||||
|
||||
<dougal-fixed-string-decoder
|
||||
<dougal-fixed-string-encoder
|
||||
title="Line name format"
|
||||
subtitle="Format of line names as configured in the navigation system"
|
||||
label="Example line name"
|
||||
hint="Visualise your line name configuration with example values"
|
||||
:multiline="true"
|
||||
:text="cwo.lineNameInfo.example"
|
||||
:fields="cwo.lineNameInfo.fields"
|
||||
></dougal-fixed-string-decoder>
|
||||
:properties="properties"
|
||||
:fields.sync="fields_"
|
||||
:values.sync="values_"
|
||||
></dougal-fixed-string-encoder>
|
||||
</v-form>
|
||||
</v-card-text>
|
||||
<v-card-actions>
|
||||
@@ -50,16 +46,19 @@
|
||||
|
||||
<script>
|
||||
import { deepSet } from '@/lib/utils';
|
||||
import DougalFixedStringDecoder from '@/components/decoder/fixed-string-decoder';
|
||||
import DougalFixedStringEncoder from '@/components/encoder/fixed-string-encoder';
|
||||
|
||||
export default {
|
||||
name: "DougalProjectSettingsOnlineLineNameFormat",
|
||||
|
||||
components: {
|
||||
DougalFixedStringDecoder
|
||||
DougalFixedStringEncoder
|
||||
},
|
||||
|
||||
props: {
|
||||
fields: Array,
|
||||
values: Object,
|
||||
properties: Object,
|
||||
value: Object
|
||||
},
|
||||
|
||||
@@ -68,53 +67,27 @@ export default {
|
||||
}
|
||||
},
|
||||
|
||||
watch: {
|
||||
},
|
||||
|
||||
computed: {
|
||||
// Current working object.
|
||||
// A shortcut so we don't have to specify the full path
|
||||
// on every input control. It also makes it easier to
|
||||
// change that path if necessary. Finally, it ensures that
|
||||
// the properties being modified are always available.
|
||||
cwo: {
|
||||
|
||||
fields_: {
|
||||
get () {
|
||||
if (this.value) {
|
||||
if (!this.value?.online?.line) {
|
||||
deepSet(this.value, [ "online", "line" ], {
|
||||
lineNameInfo: {
|
||||
example: "",
|
||||
fields: {
|
||||
line: {
|
||||
length: 4,
|
||||
type: "int"
|
||||
},
|
||||
sequence: {
|
||||
length: 3,
|
||||
type: "int"
|
||||
},
|
||||
incr: {
|
||||
length: 1,
|
||||
type: "bool"
|
||||
},
|
||||
attempt: {
|
||||
length: 1,
|
||||
type: "int"
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
return this.value.online.line;
|
||||
} else {
|
||||
return {};
|
||||
}
|
||||
return this.fields;
|
||||
},
|
||||
|
||||
set (v) {
|
||||
if (this.value) {
|
||||
deepSet(this.value, [ "online", "line" ], v);
|
||||
}
|
||||
this.$emit("update", structuredClone({values: this.values, fields: v}));
|
||||
}
|
||||
},
|
||||
|
||||
values_: {
|
||||
get () {
|
||||
return this.values;
|
||||
},
|
||||
set (v) {
|
||||
this.$emit("update", structuredClone({values: v, fields: this.fields}));
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
|
||||
@@ -67,6 +67,7 @@
|
||||
:is="activeComponent"
|
||||
v-bind="activeValues"
|
||||
v-model.sync="configuration"
|
||||
@update="activeUpdateHandler"
|
||||
@close="deselect"
|
||||
></component>
|
||||
</v-col>
|
||||
@@ -279,6 +280,7 @@ export default {
|
||||
data () {
|
||||
return {
|
||||
configuration: null,
|
||||
settings: null,
|
||||
active: [],
|
||||
open: [],
|
||||
files: [],
|
||||
@@ -420,8 +422,17 @@ export default {
|
||||
id: "line_name_format",
|
||||
name: "Line name format",
|
||||
values: (obj) => ({
|
||||
lineNameInfo: obj?.online?.line?.lineNameInfo
|
||||
})
|
||||
fields: this.makeSection(obj, "online.line.lineNameBuilder.fields", []),
|
||||
values: this.makeSection(obj, "online.line.lineNameBuilder.values",
|
||||
Object.fromEntries(
|
||||
Object.keys(this.settings.lineNameBuilder.properties ?? {}).map( k => [k, undefined] ))),
|
||||
properties: this.settings.lineNameBuilder.properties ?? {}
|
||||
}),
|
||||
update: (obj) => {
|
||||
const configuration = structuredClone(this.configuration);
|
||||
deepSet(configuration, ["online", "line", "lineNameBuilder"], obj);
|
||||
this.configuration = configuration;
|
||||
}
|
||||
},
|
||||
{
|
||||
id: "planner_settings",
|
||||
@@ -518,6 +529,12 @@ export default {
|
||||
this.activeItem.values(this.configuration);
|
||||
},
|
||||
|
||||
activeUpdateHandler () {
|
||||
return this.activeItem?.update ?? ((obj) => {
|
||||
console.warn("Unhandled update event on", this.activeItem?.id, obj);
|
||||
})
|
||||
},
|
||||
|
||||
surveyState: {
|
||||
get () {
|
||||
return !this.configuration?.archived;
|
||||
@@ -559,6 +576,35 @@ export default {
|
||||
|
||||
methods: {
|
||||
|
||||
makeSection (obj, path, defaultVaue) {
|
||||
|
||||
function reduced (obj = {}, path = []) {
|
||||
return path.reduce( (acc, cur) =>
|
||||
{
|
||||
if (!(cur in acc)) {
|
||||
acc[cur] = {} ;
|
||||
};
|
||||
return acc[cur]
|
||||
}, obj);
|
||||
}
|
||||
|
||||
if (!obj) {
|
||||
obj = {};
|
||||
}
|
||||
|
||||
if (typeof path == "string") {
|
||||
path = path.split(".");
|
||||
}
|
||||
|
||||
let value = reduced(obj, path);
|
||||
|
||||
const key = path.pop();
|
||||
if (!Object.keys(value ?? {}).length && defaultVaue !== undefined) {
|
||||
reduced(obj, path)[key] = defaultVaue;
|
||||
}
|
||||
return reduced(obj, path)[key];
|
||||
},
|
||||
|
||||
async getConfiguration () {
|
||||
this.configuration = null;
|
||||
const url = `/project/${this.$route.params.project}/configuration`;
|
||||
@@ -571,6 +617,21 @@ export default {
|
||||
this.dirty = false;
|
||||
},
|
||||
|
||||
async getSettings () {
|
||||
this.settings = null;
|
||||
let url = `/project/${this.$route.params.project}/linename/properties`;
|
||||
const init = {
|
||||
headers: {
|
||||
"If-None-Match": "" // Ensure we get a fresh response
|
||||
}
|
||||
};
|
||||
this.settings = {
|
||||
lineNameBuilder: {
|
||||
properties: await this.api([url, init])
|
||||
}
|
||||
};
|
||||
},
|
||||
|
||||
makeTree (obj, id=0) {
|
||||
const isObject = typeof obj === "object" && !Array.isArray(obj) && obj !== null;
|
||||
return isObject
|
||||
@@ -681,6 +742,7 @@ export default {
|
||||
},
|
||||
|
||||
async mounted () {
|
||||
this.getSettings();
|
||||
this.getConfiguration();
|
||||
},
|
||||
|
||||
|
||||
Reference in New Issue
Block a user