ui_migration fix(deps): --preliminary-- added stronger type checking on things properties

This commit is contained in:
Antonio Anaya 2026-02-13 19:23:07 -06:00
parent f4a10ec897
commit 5bbe88c9f7
6 changed files with 75 additions and 40 deletions

View file

@ -236,19 +236,24 @@ export default {
* *
*/ */
async checkExistingTasks() { async checkExistingTasks() {
console.log("Checking for existing tasks for action ", this.action);
let response = await this.findOngoingActions(this.thing, this.action); let response = await this.findOngoingActions(this.thing, this.action);
// Exit if response is null, due to an error. // Exit if response is null, due to an error.
if (response == null) return; if (response == null) return;
// Check for a task that is ongoing. // Check for a task that is ongoing.
// We can't handle multiple tasks ongoing, so this picks the first. // We can't handle multiple tasks ongoing, so this picks the first.
const ongoingTask = response.data.find((t) => ["pending", "running"].includes(t.status)); const ongoingTask = response.data.find((t) => ["pending", "running"].includes(t.status));
console.log("Existing tasks: ", response.data, "Ongoing task: ", ongoingTask);
if (ongoingTask) { if (ongoingTask) {
// There is a started task // There is a started task
console.log("Found ongoing task, resuming polling: ", ongoingTask);
this.taskStarted = true; this.taskStarted = true;
this.$emit("taskStarted"); this.$emit("taskStarted");
// Find its URL // Find its URL
console.log("Task links: ", ongoingTask.links);
const taskUrl = ongoingTask.links.find((t) => t.rel == "self").href; const taskUrl = ongoingTask.links.find((t) => t.rel == "self").href;
this.startPollingTask(ongoingTask.id, taskUrl); this.startPollingTask(ongoingTask.id, taskUrl);
console.log("Resumed polling on existing task with URL: ", taskUrl);
} }
}, },
@ -324,6 +329,7 @@ export default {
this.taskUrl = null; this.taskUrl = null;
this.taskRunning = false; this.taskRunning = false;
this.taskStarted = false; this.taskStarted = false;
console.log("Task ended with status: ", this.taskStatus);
this.$emit("finished"); this.$emit("finished");
}, },

View file

@ -50,7 +50,7 @@
</label> </label>
<label v-if="dataType == 'number_object'" class="uk-form-label" <label v-if="dataType == 'number_object'" class="uk-form-label"
>{{ label }} >{{ label }}
<div v-for="(val, key) in value" :key="key"> <div v-for="(val, key) in modelValue" :key="key">
<label>{{ internalLabels[key] }}</label> <label>{{ internalLabels[key] }}</label>
<div class="input-and-buttons-container"> <div class="input-and-buttons-container">
<input <input
@ -109,6 +109,7 @@ export default {
syncPropertyButton syncPropertyButton
}, },
compatConfig: { COMPONENT_V_MODEL: false },
props: { props: {
dataSchema: { dataSchema: {
type: Object, type: Object,
@ -125,14 +126,14 @@ export default {
}, },
animate: { animate: {
type: Boolean, type: Boolean,
default: false, default: null,
}, },
}, },
data() { data() {
return { return {
// Initialise with a copy to try to prevent the this.value prop being mutated if // Initialise with a copy to try to prevent the this.modelValue prop being mutated if
// the value is an array or object. For future updates we stringify and parse // the modelValue is an array or object. For future updates we stringify and parse
// (see resetInternalValue). If we do this here there is a chance we get errors // (see resetInternalValue). If we do this here there is a chance we get errors
// as internalValue is still null when rendering starts. // as internalValue is still null when rendering starts.
internalValue: Array.isArray(this.modelValue) internalValue: Array.isArray(this.modelValue)
@ -142,7 +143,7 @@ export default {
: this.modelValue, : this.modelValue,
// Is edited can't be computed as we mutate internalValue // Is edited can't be computed as we mutate internalValue
isEdited: false, isEdited: false,
animateUpdate: false, animateUpdate: null,
}; };
}, },
computed: { computed: {
@ -210,8 +211,8 @@ export default {
watch: { watch: {
modelValue: { modelValue: {
handler() { handler() {
// Fire updateIsEdited on both value and internal value change, // Fire updateIsEdited on both modelValue and internal modelValue change,
// as change in value may not causse internalValue to change. // as change in modelValue may not causse internalValue to change.
this.updateIsEdited(); this.updateIsEdited();
this.resetInternalValue(); this.resetInternalValue();
}, },
@ -236,8 +237,8 @@ export default {
methods: { methods: {
resetInternalValue: function () { resetInternalValue: function () {
// Whenever updatirng th internal value stringify and parse as a form of deepcopy. // Whenever updatirng th internal modelValue stringify and parse as a form of deepcopy.
// This ensure that the this.value prop is not mutated for when elements of arrays // This ensure that the this.modelValue prop is not mutated for when elements of arrays
// or objects are updated. // or objects are updated.
this.internalValue = JSON.parse(JSON.stringify(this.modelValue)); this.internalValue = JSON.parse(JSON.stringify(this.modelValue));
}, },
@ -254,11 +255,11 @@ export default {
} }
}, },
focusIn: function (event) { focusIn: function (event) {
this.valueOnEnter = event.target.value; this.valueOnEnter = event.target.modelValue;
}, },
focusOut: function (event) { focusOut: function (event) {
if (this.valueOnEnter != event.target.value) { if (this.valueOnEnter != event.target.modelValue) {
this.sendValue(event.target.value); this.sendValue(event.target.modelValue);
} }
}, },
keyDown: function (event) { keyDown: function (event) {
@ -271,12 +272,12 @@ export default {
this.isEdited = this.deepStringify(this.internalValue) !== this.deepStringify(this.modelValue); this.isEdited = this.deepStringify(this.internalValue) !== this.deepStringify(this.modelValue);
}, },
animationEnd: function () { animationEnd: function () {
this.animateUpdate = false; this.animateUpdate = null;
this.$emit("animationShown"); this.$emit("animationShown");
}, },
deepStringify: function (val) { deepStringify: function (val) {
// Create a json string where all internal numbers are also JSON strings. This is // Create a json string where all internal numbers are also JSON strings. This is
// needed to robustly check if the value is updated because the raw value may be a // needed to robustly check if the modelValue is updated because the raw modelValue may be a
// number but anything typed in the input is a string. In the case of arrays or // number but anything typed in the input is a string. In the case of arrays or
// objects even with JSON.stringify we end up comparing ["1", 3] with [1, 3] and // objects even with JSON.stringify we end up comparing ["1", 3] with [1, 3] and
// find them as not equal. // find them as not equal.

View file

@ -1,17 +1,17 @@
<template> <template>
<div uk-grid class="uk-height-1-1 uk-margin-remove uk-padding-remove"> <div uk-grid class="uk-height-1-1 uk-margin-remove uk-padding-remove">
<div class="control-component uk-padding-small"> <div class="control-component uk-padding-small">
<div v-show="!scanning" v-observe-visibility="visibilityChanged" class="uk-padding-small"> <div v-show="!scanning" ref="slideScanView" class="uk-padding-small">
<!-- Workflow Selection Dropdown --> <!-- Workflow Selection Dropdown -->
<div class="uk-margin"> <div class="uk-margin">
<label class="uk-form-label">Workflow</label> <label class="uk-form-label">Workflow</label>
<select <select
class="uk-select uk-form-small" class="uk-select uk-form-small"
:value="workflowName" :modelValue="workflowName"
@change="setWorkflow($event.target.value)" @change="setWorkflow($event.target.modelValue)"
> >
<option v-for="(label, name) in workflowOptions" :key="name" :value="name"> <option v-for="(label, name) in workflowOptions" :key="name" :modelValue="name">
{{ label }} {{ label }}
</option> </option>
</select> </select>
@ -176,12 +176,12 @@ export default {
async created() { async created() {
this.readSettings(); this.readSettings();
this.workflowOptions = await this.readThingProperty("smart_scan", "workflow_display_names"); this.workflowOptions = await this.readThingProperty("smart_scan", "workflow_display_names", true);
}, },
mounted() { mounted() {
useIntersectionObserver( useIntersectionObserver(
this.$refs.slideScanContent, this.$refs.slideScanView,
([{ isIntersecting }]) => { ([{ isIntersecting }]) => {
this.visibilityChanged(isIntersecting); this.visibilityChanged(isIntersecting);
}, },
@ -198,13 +198,22 @@ export default {
} }
}, },
async readSettings() { async readSettings() {
this.workflowName = await this.readThingProperty("smart_scan", "workflow_name"); this.workflowOptions = await this.readThingProperty("smart_scan", "workflow_display_names", true) || {};
this.workflowName = await this.readThingProperty("smart_scan", "workflow_name", false);
if (!this.workflowName) {
console.warn("Could not read workflow_name, using default");
this.workflowName = "histo_scan_workflow";
}
if (this.workflowName) { if (this.workflowName) {
this.ready = await this.readThingProperty(this.workflowName, "ready"); console.log("Current workflow name: ", this.workflowName);
this.workflowSettings = await this.readThingProperty(this.workflowName, "settings_ui"); this.ready = await this.readThingProperty(this.workflowName, "ready", true);
this.workflowSettings = await this.readThingProperty(this.workflowName, "settings_ui", true) || [];
console.log(this.workflowSettings); console.log(this.workflowSettings);
this.workflowDisplayName = await this.readThingProperty(this.workflowName, "display_name"); this.workflowDisplayName = await this.readThingProperty(this.workflowName, "display_name", true);
this.workflowBlurb = await this.readThingProperty(this.workflowName, "ui_blurb"); this.workflowBlurb = await this.readThingProperty(this.workflowName, "ui_blurb", true);
} }
}, },
onScanError: function (error) { onScanError: function (error) {
@ -231,34 +240,47 @@ export default {
* - starts the polling loop that fetches scan progress and preview images * - starts the polling loop that fetches scan progress and preview images
*/ */
startScanning() { startScanning() {
console.log("Starting scan...");
this.lastStitchedImage = null; this.lastStitchedImage = null;
this.scanning = true; this.scanning = true;
setTimeout(this.pollScan, 1000); setTimeout(this.pollScan, 1000);
}, },
async pollScan() { async pollScan() {
console.log("Polling for scan progress...");
if (this.cancellable) { if (this.cancellable) {
// while the scan is running // while the scan is running
let mtime = await this.readThingProperty("smart_scan", "latest_preview_stitch_time"); console.log("Polling for scan updates...");
let mtime = await this.readThingProperty("smart_scan", "latest_preview_stitch_time", true);
console.log("smart scan latest preview stitch time: ", mtime);
if (mtime !== null) { if (mtime !== null) {
this.lastStitchedImage = `${this.$store.getters.baseUri}/smart_scan/latest_preview_stitch.jpg?t=${mtime}`; this.lastStitchedImage = `${this.$store.getters.baseUri}/smart_scan/latest_preview_stitch.jpg?t=${mtime}`;
console.log("Updated preview image URL: ", this.lastStitchedImage);
} }
this.lastScanName = await this.readThingProperty("smart_scan", "latest_scan_name");
this.lastScanName = await this.readThingProperty("smart_scan", "latest_scan_name", true);
setTimeout(this.pollScan, 1000); // keep rescheduling until it's stopped setTimeout(this.pollScan, 1000); // keep rescheduling until it's stopped
} }
}, },
async setWorkflow(name) { async setWorkflow(name) {
console.log("Setting workflow to ", name);
try { try {
this.workflowName = name; this.workflowName = name;
if (!this.workflowName) {
console.warn("Could not read workflow_name, using default");
this.workflowName = "histo_scan_workflow";
}
await this.writeThingProperty("smart_scan", "workflow_name", name); await this.writeThingProperty("smart_scan", "workflow_name", name);
// refresh UI // refresh UI
await this.readSettings(); await this.readSettings();
console.log("readsettings values: ", this.workflowName, this.workflowSettings, this.workflowDisplayName, this.workflowBlurb);
} catch (err) { } catch (err) {
this.modalError(err); this.modalError(err);
// revert if server rejected // revert if server rejected
this.workflowName = await this.readThingProperty("smart_scan", "workflow_name"); this.workflowName = await this.readThingProperty("smart_scan", "workflow_name", true);
} }
}, },
async downloadZipFile(response) { async downloadZipFile(response) {

View file

@ -3,13 +3,12 @@ import App from "./App.vue";
import store from "./store"; import store from "./store";
import UIkit from "uikit"; import UIkit from "uikit";
import VueObserveVisibility from "vue-observe-visibility";
// Import MD icons // Import MD icons
import "material-symbols/outlined.css"; import "material-symbols/outlined.css";
import queryMixin from "@/mixins/labThingsMixins.js"; //import queryMixin from "@/mixins/labThingsMixins.js";
import modalMixin from "@/mixins/modalMixins.js"; import modalMixin from "@/mixins/modalMixins.js";
import labThingsMixins from './mixins/labThingsMixins';
// UIKit overrides // UIKit overrides
UIkit.mixin( UIkit.mixin(
@ -25,11 +24,12 @@ UIkit.mixin(
const app = createApp(App); const app = createApp(App);
// Use visibility observer // Use visibility observer
app.use(VueObserveVisibility); //app.use(VueObserveVisibility);
// Use global mixins // Use global mixins
app.mixin(queryMixin); // app.mixin(queryMixin);
app.mixin(modalMixin); app.mixin(modalMixin);
app.mixin(labThingsMixins);
// Use Vuex store // Use Vuex store
app.use(store); app.use(store);

View file

@ -59,7 +59,7 @@ export default {
// `false` fails because axios somehow eats it! // `false` fails because axios somehow eats it!
// Other values should not be stringified or pydantic // Other values should not be stringified or pydantic
// can't parse them. // can't parse them.
if ((value === false) | (value === true)) { if ((value === false) || (value === true)) {
value = JSON.stringify(value); value = JSON.stringify(value);
} }
await axios.put(url, value); await axios.put(url, value);
@ -91,7 +91,7 @@ export default {
response = await axios.get(taskUrl, { baseURL: this.$store.getters.baseUri }); response = await axios.get(taskUrl, { baseURL: this.$store.getters.baseUri });
const result = response.data.status; const result = response.data.status;
if ((result == "running") | (result == "pending")) { if ((result === "running") || (result === "pending")) {
ongoingMethod?.(response); ongoingMethod?.(response);
this.pollTimers[taskUrl] = setTimeout(() => { this.pollTimers[taskUrl] = setTimeout(() => {
this.pollUntilComplete(taskUrl, ongoingMethod, finalMethod, interval); this.pollUntilComplete(taskUrl, ongoingMethod, finalMethod, interval);

View file

@ -28,7 +28,7 @@ export const wotStoreModule = {
// Deduplication should be done elsewhere. // Deduplication should be done elsewhere.
let response = await axios.get(uri); let response = await axios.get(uri);
let td = response.data; let td = response.data;
let thing_name = name | uri.replace(/\/$/, "").split("/").pop(); let thing_name = name || uri.replace(/\/$/, "").split("/").pop();
commit("addThingDescription", { commit("addThingDescription", {
thingName: thing_name, thingName: thing_name,
thingDescription: td, thingDescription: td,
@ -37,7 +37,7 @@ export const wotStoreModule = {
async fetchThingDescriptions({ commit }, uri) { async fetchThingDescriptions({ commit }, uri) {
// Fetch thing descriptions from the given URI // Fetch thing descriptions from the given URI
let response = await axios.get(uri); let response = await axios.get(uri);
if (response.status != 200) throw "Could not retrieve thing descriptions"; if (response.status !== 200) throw "Could not retrieve thing descriptions";
for (const k in response.data) { for (const k in response.data) {
let thing_name = k.replace(/\/$/, "").replace(/^\//, ""); let thing_name = k.replace(/\/$/, "").replace(/^\//, "");
commit("addThingDescription", { commit("addThingDescription", {
@ -77,8 +77,14 @@ export const wotStoreModule = {
throw `Could not find form for ${affordanceType} ${thing}/${affordance} with op ${op}`; throw `Could not find form for ${affordanceType} ${thing}/${affordance} with op ${op}`;
} }
let affordances = td[affordanceType]; let affordances = td[affordanceType];
if (!affordances || !(affordance in affordances)) {
if (allowUndefined) return undefined;
throw `Could not find form for ${affordanceType} ${thing}/${affordance} with op ${op}`;
}
let href = findFormHref(affordances[affordance], op); let href = findFormHref(affordances[affordance], op);
if (href == undefined) { if (href === undefined) {
if (allowUndefined) return undefined; if (allowUndefined) return undefined;
throw `Could not find form for ${affordanceType} ${thing}/${affordance} with op ${op}`; throw `Could not find form for ${affordanceType} ${thing}/${affordance} with op ${op}`;
} }
@ -105,10 +111,10 @@ export const wotStoreModule = {
export function findFormHref(affordance, op) { export function findFormHref(affordance, op) {
// Find the form in the affordance that matches the given operation type // Find the form in the affordance that matches the given operation type
if (affordance == undefined) return undefined; if (affordance === undefined) return undefined;
let forms = affordance.forms; let forms = affordance.forms;
let matchingForm = forms.find((f) => f.op == op || f.op.includes(op)); let matchingForm = forms.find((f) => f.op == op || f.op.includes(op));
if (matchingForm == undefined) return undefined; if (matchingForm === undefined) return undefined;
return matchingForm.href; return matchingForm.href;
} }