ui_migration refactor(store) migrate vuex to pinia

This commit is contained in:
Antonio Anaya 2026-04-20 18:32:39 -06:00
parent b580a63675
commit 2701ad0eba
3 changed files with 157 additions and 217 deletions

View file

@ -1,4 +1,6 @@
import { createApp } from "vue"; import { createApp } from "vue";
import { createPinia } from "pinia";
import piniaPluginPersistedstate from "pinia-plugin-persistedstate";
import App from "./App.vue"; import App from "./App.vue";
import store from "./store"; import store from "./store";
import UIkit from "uikit"; import UIkit from "uikit";
@ -21,14 +23,13 @@ UIkit.mixin(
// Create Vue app // Create Vue app
const app = createApp(App); const app = createApp(App);
const pinia = createPinia();
// Use visibility observer pinia.use(piniaPluginPersistedstate);
//app.use(VueObserveVisibility);
// Use global mixins // Use global mixins
app.mixin(modalMixin); app.mixin(modalMixin);
app.mixin(labThingsMixins); app.mixin(labThingsMixins);
// Use Vuex store // Use Vuex store
app.use(store); app.use(pinia);
app.mount("#app"); app.mount("#app");

View file

@ -1,5 +1,5 @@
import { createStore } from "vuex"; import { defineStore } from "pinia";
import wotStoreModule from "./wot-client"; import { ref, computed } from "vue";
function getOriginFromLocation() { function getOriginFromLocation() {
// This will default to the same origin that's serving // This will default to the same origin that's serving
@ -14,156 +14,98 @@ function getOriginFromLocation() {
} }
} }
/** // Define Pinia store
* Converts a Vuex state key (e.g. "appTheme") into a corresponding export const useDefaultStore = defineStore(
* Vuex mutation name (e.g. "changeAppTheme") using the `change<Key>` "default",
* convention. () => {
* // State
* @param {string} key - The Vuex state key to convert. const origin = ref(getOriginFromLocation());
* @returns {string} - The formatted mutation name. const available = ref(false);
*/ const waiting = ref(false);
function keyToMutationName(key) { const error = ref("");
return `change${key.charAt(0).toUpperCase() + key.slice(1)}`; const trackWindow = ref(true);
} const activeStreams = ref({});
const microscopeHostname = ref("");
/**
* Converts a Vuex mutation name (e.g. "changeAppTheme") back into
* the corresponding state key (e.g. "appTheme") using the
* `change<Key>` convention.
*
* @param {string} mutationName - The Vuex mutation name to reverse.
* @returns {string|null} - The derived state key, or null if the
* mutation name doesn't match the `change<Key>` convention.
*/
function mutationToKey(mutationName) {
const prefix = "change";
if (!mutationName.startsWith(prefix)) {
return null; // Not a mutation we care about
}
const key = mutationName.slice(prefix.length);
return key.charAt(0).toLowerCase() + key.slice(1);
}
const LOCALSTORAGE_KEYS = [
"appTheme",
"disableStream",
"overrideOrigin",
"navigationStepSize",
"navigationInvert",
];
export default createStore({
modules: {
wot: wotStoreModule,
},
state: {
origin: getOriginFromLocation(),
available: false,
waiting: false,
error: "",
trackWindow: true,
activeStreams: {},
microscopeHostname: "",
// Persistent items: // Persistent items:
// The app theme (e.g. light/dark) // The app theme (e.g. light/dark)
appTheme: "system", const appTheme = ref("system");
disableStream: false, const disableStream = ref(false);
// The origin to use if overriding with dev tools // The origin to use if overriding with dev tools
overrideOrigin: "http://microscope.local:5000", const overrideOrigin = ref("http://microscope.local:5000");
// The step sizes for navigation via control pane/keys presses // The step sizes for navigation via control pane/keys presses
navigationStepSize: { const navigationStepSize = ref({
x: 200, x: 200,
y: 200, y: 200,
z: 50, z: 50,
}, });
// The axis inversion for navigation via control pane/keys presses // The axis inversion for navigation via control pane/keys presses
navigationInvert: { const navigationInvert = ref({
x: false, x: false,
y: false, y: false,
z: false, z: false,
});
// Actions
function resetState() {
waiting.value = false;
available.value = false;
error.value = ""; // TODO: verify that "" is needed to match initial state
}
function setConnected() {
waiting.value = false;
available.value = true;
}
function addStream(id) {
activeStreams.value[id] = true;
}
function removeStream(id) {
activeStreams.value[id] = false;
}
// TODO: Replace for direct access to state.
// Getters
const baseUri = computed(() => origin.value);
const ready = computed(() => available.value);
// Export
return {
//State
origin,
available,
waiting,
error,
trackWindow,
activeStreams,
microscopeHostname,
appTheme,
disableStream,
overrideOrigin,
navigationStepSize,
navigationInvert,
//Getters
baseUri,
ready,
//Actions
resetState,
setConnected,
addStream,
removeStream,
};
},
{
// PiniaPluginPersistedState will now automatically persist ONLY these specific refs to localStorage
persist: {
paths: [
"appTheme",
"disableStream",
"overrideOrigin",
"navigationStepSize",
"navigationInvert",
],
}, },
}, },
);
mutations: {
changeOrigin(state, origin) {
state.origin = origin;
},
changeWaiting(state, waiting) {
state.waiting = waiting;
},
changeDisableStream(state, disabled) {
state.disableStream = disabled;
},
changeTrackWindow(state, enabled) {
state.trackWindow = enabled;
},
changeAppTheme(state, theme) {
state.appTheme = theme;
},
resetState(state) {
state.waiting = false;
state.available = false;
state.error = null;
},
setConnected(state) {
state.waiting = false;
state.available = true;
},
setErrorMessage(state, msg) {
state.error = msg;
},
addStream(state, id) {
state.activeStreams[id] = true;
},
removeStream(state, id) {
state.activeStreams[id] = false;
},
changeMicroscopeHostname(state, value) {
state.microscopeHostname = value;
},
changeOverrideOrigin(state, value) {
state.overrideOrigin = value;
},
changeNavigationStepSize(state, value) {
state.navigationStepSize = value;
},
changeNavigationInvert(state, value) {
state.navigationInvert = value;
},
},
actions: {},
getters: {
baseUri: (state) => state.origin,
ready: (state) => state.available,
},
plugins: [
(store) => {
// Load initial state from localStorage
LOCALSTORAGE_KEYS.forEach((key) => {
const saved = localStorage.getItem(key);
if (saved !== null) {
try {
const parsed = JSON.parse(saved);
const mutationName = keyToMutationName(key);
store.commit(mutationName, parsed);
} catch (e) {
console.warn(`Failed to parse localStorage key "${key}":`, e);
localStorage.removeItem(key);
}
}
});
// Subscribe to mutations
store.subscribe((mutation, state) => {
const key = mutationToKey(mutation.type);
// If the mutation is chacning a local storage key then update localStorage
if (key && LOCALSTORAGE_KEYS.includes(key)) {
localStorage.setItem(key, JSON.stringify(state[key]));
}
});
},
],
});

View file

@ -1,82 +1,65 @@
import { defineStore } from "pinia";
import { ref, computed } from "vue";
import axios from "axios"; import axios from "axios";
export const wotStoreModule = { export const useWotStore = defineStore("wot", () => {
namespaced: true, // State
state: () => ({ const thingDescriptions = ref({});
thingDescriptions: {}, const servient = ref(null);
servient: null, const helpers = ref(null);
helpers: null, // Actions
}), function addThingDescription(thingName, thingDescription) {
mutations: { thingDescriptions.value[thingName] = thingDescription;
addThingDescription(state, { thingName, thingDescription }) { }
state.thingDescriptions[thingName] = thingDescription;
}, function removeThingDescription(thingName) {
removeThingDescription(state, thingName) { delete thingDescriptions.value[thingName];
delete state.thingDescriptions[thingName]; }
},
removeAllThingDescriptions(state) { function removeAllThingDescriptions() {
state.thingDescriptions = {}; thingDescriptions.value = {};
}, }
},
actions: { async function fetchThingDescription(uri, name = null) {
async start() {
// Set up thing client - not currently used.
},
async fetchThingDescription({ commit }, { uri, name = null }) {
// Fetch the thing description from the given URI and consume it // Fetch the thing description from the given URI and consume it
// NB this should only be called once, or we'll duplicate effort. // NB this should only be called once, or we'll duplicate effort.
// Deduplication should be done elsewhere. // Deduplication should be done elsewhere.
let response = await axios.get(uri); const response = await axios.get(uri);
let td = response.data; const td = response.data;
let thing_name = name || uri.replace(/\/$/, "").split("/").pop(); const thingName = name || uri.replace(/\/$/, "").split("/").pop();
commit("addThingDescription", { addThingDescription(thingName, td);
thingName: thing_name, }
thingDescription: td,
}); async function fetchThingDescriptions(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); const 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 thingName = k.replace(/\/$/, "").replace(/^\//, "");
commit("addThingDescription", { addThingDescription(thingName, response.data[k]);
thingName: thing_name,
thingDescription: response.data[k],
});
} }
}, }
},
getters: { const thingList = computed(() => Object.keys(thingDescriptions.value));
thingDescriptions: (state) => {
return state.thingDescriptions; const thingAvailable = (thingName) => thingName in thingDescriptions.value;
},
thingList: (state) => { const thingAffordanceAvailable = (thing, affordanceType, affordance) => {
return Object.keys(state.thingDescriptions); const td = thingDescriptions.value[thing];
}, if (!td || !td[affordanceType]) {
thingDescription: (state) => (thingName) => {
return state.thingDescriptions[thingName];
},
thingAvailable: (state) => (thingName) => {
return thingName in state.thingDescriptions;
},
thingAffordanceAvailable: (state) => (thing, affordanceType, affordance) => {
let td = state.thingDescriptions[thing];
if (!td) {
return false; return false;
} }
return affordance in td[affordanceType]; return affordance in td[affordanceType];
}, };
thingFormUrl:
(state) => const thingFormUrl = (thing, affordanceType, affordance, op, allowUndefined = true) => {
(thing, affordanceType, affordance, op, allowUndefined = true) => {
// Find the URL for a particular operation // Find the URL for a particular operation
let td = state.thingDescriptions[thing]; const td = thingDescriptions.value[thing];
if (!td) { if (!td) {
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}`;
} }
let affordances = td[affordanceType]; const affordances = td[affordanceType];
if (!affordances || !(affordance in affordances)) { if (!affordances || !(affordance in affordances)) {
if (allowUndefined) return undefined; if (allowUndefined) return undefined;
@ -97,17 +80,33 @@ export const wotStoreModule = {
return base + href; return base + href;
} }
return href; return href;
}, };
thingPropertyUrl: (_state, getters) => (thing, property, op, allowUndefined) => {
const thingPropertyUrl = (thing, property, op, allowUndefined) =>
// Find the URL for a particular property // Find the URL for a particular property
return getters.thingFormUrl(thing, "properties", property, op, allowUndefined); thingFormUrl(thing, "properties", property, op, allowUndefined);
},
thingActionUrl: (_state, getters) => (thing, action, op, allowUndefined) => { const thingActionUrl = (thing, action, op, allowUndefined) =>
// Find the URL for a particular action // Find the URL for a particular action
return getters.thingFormUrl(thing, "actions", action, op, allowUndefined); thingFormUrl(thing, "actions", action, op, allowUndefined);
},
}, return {
thingDescriptions,
servient,
helpers, // State
thingList,
thingAvailable, // Helpers
thingFormUrl,
thingPropertyUrl,
thingActionUrl,
fetchThingDescription,
fetchThingDescriptions, // Actions
addThingDescription,
removeThingDescription,
removeAllThingDescriptions,
thingAffordanceAvailable,
}; };
});
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
@ -117,5 +116,3 @@ export function findFormHref(affordance, op) {
if (matchingForm === undefined) return undefined; if (matchingForm === undefined) return undefined;
return matchingForm.href; return matchingForm.href;
} }
export default wotStoreModule;