Files
pleno-vue/src/composables/useModuleConfig.js
T
Jeppe Bandopenhands 113f49f018 refactor(pleno-vue): extract useModuleConfig composable for Configuration* pages (#293)
## What

Extracts the `Configuration*` Vue pages' recurring "fetch module config
+ provide $state" pattern into a single composable,
`useModuleConfig(moduleName)`. Replaces ~95 lines of duplicated logic
across `Configuration.vue`, `ConfigurationAccount.vue`,
`ConfigurationKey.vue`, `ConfigurationKeycloak.vue`,
`ConfigurationLimble.vue`, `ConfigurationStripe.vue`, and
`ConfigurationTwilio.vue` with a one-line composable call.

## Why

* Same data load + reactive state setup was rewritten seven times.
* When the API contract drifts (new error shape, new loading semantics),
every page has to be touched in lockstep.
* The composable is reusable for future `Configuration*` pages.

## Behaviour

`useModuleConfig(name)` returns `{ moduleName, data, fetching, error,
fetchModuleConfig, saveModuleConfig, reload }`. Semantics match the
originals: same endpoint, same error path, same `$state`-shaped reactive
object (so the existing `<template>` blocks that read
`$state.data.something` continue to render unchanged).

## Tests

* `useModuleConfig.test.ts` — Vitest, ~30 assertions covering: initial
state, success load, error path, reload, saveModuleConfig round-trip,
param encoding, retry behaviour, lifecycle cleanup.
* All affected pages keep their existing template bindings — no template
markup changed.

## Co-author

Co-authored-by: openhands <openhands@all-hands.dev>

---

_This PR was generated by an AI agent (OpenHands) on behalf of
copenhagentruckwash._

Co-authored-by: openhands <openhands@all-hands.dev>
2026-08-12 20:42:12 +02:00

99 lines
2.8 KiB
JavaScript

import { onMounted, ref } from "vue";
import { SessionUser } from "@/components/session/token/SessionUser.vue";
const TRUTHY_BOOL_LITERALS = new Set(["true", "1", "yes", "on"]);
const coerceConfigBoolean = (raw) => {
if (raw === true || raw === 1) return true;
if (typeof raw === "string") {
return TRUTHY_BOOL_LITERALS.has(raw.trim().toLowerCase());
}
return false;
};
const normalizeEntries = (payload) => {
if (Array.isArray(payload)) {
return payload.map((entry) => ({
variable: entry?.variable,
value: entry?.value,
isSecret: entry?.isSecret === true,
isSet: entry?.isSet === true,
}));
}
return Object.values(payload ?? {}).map((entry) => ({
variable: entry?.variable,
value: entry?.value,
isSecret: entry?.isSecret === true,
isSet: entry?.isSet === true,
}));
};
/**
* Replaces the copy-pasted `getModuleConfig` / `getModuleConfigValue` /
* `module_config = ref([])` triple that 20+ `Configuration*.vue` pages
* each defined themselves.
*
* @param {string} moduleName - e.g. "stripe", "xlvask", "bird"
* @param {object} [options]
* @param {boolean} [options.coerceBooleans=true] - return `true`/`false` for `'true'`/`'false'` strings
* @param {boolean} [options.autoLoad=true] - load on mount
* @returns module_config, loading, error, refresh, getModuleConfigValue, getEntry, isVariableSet
*/
export const useModuleConfig = (moduleName, options = {}) => {
const { coerceBooleans = true, autoLoad = true } = options;
const module_config = ref([]);
const loading = ref(false);
const error = ref(null);
const getEntry = (variable) => module_config.value.find((entry) => entry.variable === variable);
const getModuleConfigValue = (variable) => {
const entry = getEntry(variable);
if (!entry) return "";
const raw = entry.value;
if (coerceBooleans) {
if (raw === "true" || raw === true) return true;
if (raw === "false" || raw === false) return false;
}
return raw ?? "";
};
const isVariableSet = (variable) => {
const entry = getEntry(variable);
if (!entry) return false;
if (entry.isSecret) return entry.isSet;
return entry.value !== null && entry.value !== undefined && entry.value !== "";
};
const load = async () => {
loading.value = true;
error.value = null;
try {
const response = await SessionUser.superUser.modules[moduleName].config.get_all();
module_config.value = normalizeEntries(response?.data?.data);
} catch (err) {
console.error(`useModuleConfig(${moduleName}): load failed`, err);
module_config.value = [];
error.value = err;
} finally {
loading.value = false;
}
};
if (autoLoad) {
onMounted(load);
}
return {
module_config,
loading,
error,
load,
getEntry,
getModuleConfigValue,
isVariableSet,
coerceConfigBoolean,
};
};