mirror of
https://gitlab.com/wgp/dougal/software.git
synced 2025-12-06 06:37:07 +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-subtitle>Line name decoding configuration for real-time data</v-card-subtitle>
|
||||||
<v-card-text>
|
<v-card-text>
|
||||||
<v-form>
|
<v-form>
|
||||||
<v-text-field
|
<dougal-fixed-string-encoder
|
||||||
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
|
|
||||||
title="Line name format"
|
title="Line name format"
|
||||||
subtitle="Format of line names as configured in the navigation system"
|
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"
|
:multiline="true"
|
||||||
:text="cwo.lineNameInfo.example"
|
:properties="properties"
|
||||||
:fields="cwo.lineNameInfo.fields"
|
:fields.sync="fields_"
|
||||||
></dougal-fixed-string-decoder>
|
:values.sync="values_"
|
||||||
|
></dougal-fixed-string-encoder>
|
||||||
</v-form>
|
</v-form>
|
||||||
</v-card-text>
|
</v-card-text>
|
||||||
<v-card-actions>
|
<v-card-actions>
|
||||||
@@ -50,16 +46,19 @@
|
|||||||
|
|
||||||
<script>
|
<script>
|
||||||
import { deepSet } from '@/lib/utils';
|
import { deepSet } from '@/lib/utils';
|
||||||
import DougalFixedStringDecoder from '@/components/decoder/fixed-string-decoder';
|
import DougalFixedStringEncoder from '@/components/encoder/fixed-string-encoder';
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: "DougalProjectSettingsOnlineLineNameFormat",
|
name: "DougalProjectSettingsOnlineLineNameFormat",
|
||||||
|
|
||||||
components: {
|
components: {
|
||||||
DougalFixedStringDecoder
|
DougalFixedStringEncoder
|
||||||
},
|
},
|
||||||
|
|
||||||
props: {
|
props: {
|
||||||
|
fields: Array,
|
||||||
|
values: Object,
|
||||||
|
properties: Object,
|
||||||
value: Object
|
value: Object
|
||||||
},
|
},
|
||||||
|
|
||||||
@@ -68,53 +67,27 @@ export default {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
|
watch: {
|
||||||
|
},
|
||||||
|
|
||||||
computed: {
|
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 () {
|
get () {
|
||||||
if (this.value) {
|
return this.fields;
|
||||||
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 {};
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
|
|
||||||
set (v) {
|
set (v) {
|
||||||
if (this.value) {
|
this.$emit("update", structuredClone({values: this.values, fields: v}));
|
||||||
deepSet(this.value, [ "online", "line" ], v);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
values_: {
|
||||||
|
get () {
|
||||||
|
return this.values;
|
||||||
|
},
|
||||||
|
set (v) {
|
||||||
|
this.$emit("update", structuredClone({values: v, fields: this.fields}));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|||||||
@@ -67,6 +67,7 @@
|
|||||||
:is="activeComponent"
|
:is="activeComponent"
|
||||||
v-bind="activeValues"
|
v-bind="activeValues"
|
||||||
v-model.sync="configuration"
|
v-model.sync="configuration"
|
||||||
|
@update="activeUpdateHandler"
|
||||||
@close="deselect"
|
@close="deselect"
|
||||||
></component>
|
></component>
|
||||||
</v-col>
|
</v-col>
|
||||||
@@ -279,6 +280,7 @@ export default {
|
|||||||
data () {
|
data () {
|
||||||
return {
|
return {
|
||||||
configuration: null,
|
configuration: null,
|
||||||
|
settings: null,
|
||||||
active: [],
|
active: [],
|
||||||
open: [],
|
open: [],
|
||||||
files: [],
|
files: [],
|
||||||
@@ -420,8 +422,17 @@ export default {
|
|||||||
id: "line_name_format",
|
id: "line_name_format",
|
||||||
name: "Line name format",
|
name: "Line name format",
|
||||||
values: (obj) => ({
|
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",
|
id: "planner_settings",
|
||||||
@@ -518,6 +529,12 @@ export default {
|
|||||||
this.activeItem.values(this.configuration);
|
this.activeItem.values(this.configuration);
|
||||||
},
|
},
|
||||||
|
|
||||||
|
activeUpdateHandler () {
|
||||||
|
return this.activeItem?.update ?? ((obj) => {
|
||||||
|
console.warn("Unhandled update event on", this.activeItem?.id, obj);
|
||||||
|
})
|
||||||
|
},
|
||||||
|
|
||||||
surveyState: {
|
surveyState: {
|
||||||
get () {
|
get () {
|
||||||
return !this.configuration?.archived;
|
return !this.configuration?.archived;
|
||||||
@@ -559,6 +576,35 @@ export default {
|
|||||||
|
|
||||||
methods: {
|
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 () {
|
async getConfiguration () {
|
||||||
this.configuration = null;
|
this.configuration = null;
|
||||||
const url = `/project/${this.$route.params.project}/configuration`;
|
const url = `/project/${this.$route.params.project}/configuration`;
|
||||||
@@ -571,6 +617,21 @@ export default {
|
|||||||
this.dirty = false;
|
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) {
|
makeTree (obj, id=0) {
|
||||||
const isObject = typeof obj === "object" && !Array.isArray(obj) && obj !== null;
|
const isObject = typeof obj === "object" && !Array.isArray(obj) && obj !== null;
|
||||||
return isObject
|
return isObject
|
||||||
@@ -681,6 +742,7 @@ export default {
|
|||||||
},
|
},
|
||||||
|
|
||||||
async mounted () {
|
async mounted () {
|
||||||
|
this.getSettings();
|
||||||
this.getConfiguration();
|
this.getConfiguration();
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user