Merge branch 'dropdown-gui' into 'v3'

Optional options in UI property control become dropdown

See merge request openflexure/openflexure-microscope-server!477
This commit is contained in:
Joe Knapper 2026-02-18 16:43:45 +00:00
commit a6b95f0948
4 changed files with 55 additions and 7 deletions

View file

@ -1,6 +1,6 @@
"""Functionality for communicating the required user interface for a thing.""" """Functionality for communicating the required user interface for a thing."""
from typing import Any from typing import Any, Optional
from pydantic import BaseModel from pydantic import BaseModel
@ -90,6 +90,13 @@ class PropertyControl(BaseModel):
read_back_delay: int = 1000 read_back_delay: int = 1000
"""The delay in ms before reading back the property.""" """The delay in ms before reading back the property."""
options: Optional[dict[str, str | int | float | bool]] = None
"""A mapping of UI display name to value, used for creating a dropdown.
These options aren't validated here in any way. Any invalid values will be rejected
when selected.
"""
def property_control_for( def property_control_for(
thing: lt.Thing, property_name: str, **kwargs: Any thing: lt.Thing, property_name: str, **kwargs: Any

View file

@ -1,6 +1,21 @@
<template> <template>
<div> <div>
<label v-if="dataType == 'number'" class="uk-form-label" <label v-if="useDropdown" class="uk-form-label">
{{ label }}
<div class="input-and-buttons-container">
<select
v-model="internalValue"
class="uk-form-small numeric-setting-line-input dropdown"
@change="sendValue"
>
<option v-for="(value, display) in options" :key="value" :value="value">
{{ display }}
</option>
</select>
<sync-property-button @click="requestUpdate" />
</div>
</label>
<label v-if="!useDropdown && dataType == 'number'" class="uk-form-label"
>{{ label }} >{{ label }}
<div class="input-and-buttons-container"> <div class="input-and-buttons-container">
<input <input
@ -18,7 +33,7 @@
<sync-property-button @click="requestUpdate" /> <sync-property-button @click="requestUpdate" />
</div> </div>
</label> </label>
<div v-if="dataType == 'boolean'" class="input-and-buttons-container"> <div v-if="!useDropdown && dataType == 'boolean'" class="input-and-buttons-container">
<label class="uk-form-label numeric-setting-line-input"> <label class="uk-form-label numeric-setting-line-input">
<input <input
ref="checkbox" ref="checkbox"
@ -32,7 +47,7 @@
</label> </label>
<sync-property-button @click="requestUpdate" /> <sync-property-button @click="requestUpdate" />
</div> </div>
<label v-if="dataType == 'number_array'" class="uk-form-label" <label v-if="!useDropdown && dataType == 'number_array'" class="uk-form-label"
>{{ label }} >{{ label }}
<div class="input-and-buttons-container"> <div class="input-and-buttons-container">
<input <input
@ -51,7 +66,7 @@
<sync-property-button @click="requestUpdate" /> <sync-property-button @click="requestUpdate" />
</div> </div>
</label> </label>
<label v-if="dataType == 'number_object'" class="uk-form-label" <label v-if="!useDropdown && dataType == 'number_object'" class="uk-form-label"
>{{ label }} >{{ label }}
<div v-for="(val, key) in modelValue" :key="key"> <div v-for="(val, key) in modelValue" :key="key">
<label>{{ internalLabels[key] }}</label> <label>{{ internalLabels[key] }}</label>
@ -71,7 +86,7 @@
</div> </div>
</div> </div>
</label> </label>
<label v-if="dataType == 'string'" class="uk-form-label" <label v-if="!useDropdown && dataType == 'string'" class="uk-form-label"
>{{ label }} >{{ label }}
<div class="input-and-buttons-container"> <div class="input-and-buttons-container">
<input <input
@ -87,7 +102,7 @@
<sync-property-button @click="requestUpdate" /> <sync-property-button @click="requestUpdate" />
</div> </div>
</label> </label>
<label v-if="dataType == 'other'" class="uk-form-label" <label v-if="!useDropdown && dataType == 'other'" class="uk-form-label"
>{{ label }} >{{ label }}
<div class="input-and-buttons-container"> <div class="input-and-buttons-container">
<input <input
@ -128,6 +143,12 @@ export default {
type: Boolean, type: Boolean,
default: null, default: null,
}, },
options: {
type: Object,
// Default is Null as None is passed from the server.
default: null,
required: false,
},
}, },
emits: ["requestUpdate", "sendValue", "animationShown"], emits: ["requestUpdate", "sendValue", "animationShown"],
@ -176,6 +197,14 @@ export default {
form.op?.includes('writeproperty') form.op?.includes('writeproperty')
); );
}, },
useDropdown: function () {
if (this.options === null) return false;
if (["number", "string", "boolean"].includes(this.dataType)) {
return true;
}
console.warn(`Cannot support dropdown for datatype of ${this.dataType}`);
return false;
},
dataType: function () { dataType: function () {
let prop = this.dataSchema; let prop = this.dataSchema;
if (prop == undefined) { if (prop == undefined) {
@ -328,6 +357,11 @@ export default {
margin-right: 5px; margin-right: 5px;
width: 6em; width: 6em;
} }
.dropdown {
background-color: #fff;
border: 1px solid #ccc;
opacity: 1;
}
.edited { .edited {
background-color: #fff3cd; background-color: #fff3cd;
} }

View file

@ -4,6 +4,7 @@
:data-schema="propertyDescription" :data-schema="propertyDescription"
:label="label" :label="label"
:animate="animate" :animate="animate"
:options="options"
@request-update="readProperty" @request-update="readProperty"
@send-value="writeProperty" @send-value="writeProperty"
@animation-shown="resetAnimate" @animation-shown="resetAnimate"
@ -44,6 +45,11 @@ export default {
default: 1000, default: 1000,
required: false, required: false,
}, },
options: {
type: Object,
default: null,
required: false,
},
}, },
data() { data() {

View file

@ -6,6 +6,7 @@
:label="propertyData.label" :label="propertyData.label"
:read-back="propertyData.read_back" :read-back="propertyData.read_back"
:read-back-delay="propertyData.read_back_delay" :read-back-delay="propertyData.read_back_delay"
:options="propertyData.options"
/> />
</template> </template>