@mundoit-lib/histrix-component-vue

Histrix Vue 3 client components (Quasar 2)
5,011
Install
npm install @mundoit-lib/histrix-component-vue
Latest Version:0.10.0
Vue:3
Older releases:2
License:MIT
Last Updated:Oct 14, 2026
Links: GitHub  ·  npm
Maintainer: pablo_bagliere

@mundoit-lib/histrix-component-vue

npm

Componentes Vue 3 + Quasar 2 para construir clientes del backend Histrix (ERP declarativo, schema-driven). El componente raíz <HistrixApp :path="..."/> pide el schema de un XML de Histrix al backend y monta la pantalla completa (tabla, formulario, calendario, dashboard, árbol, gráfico) sin programarla.

v0.1.0+ requiere Vue 3 nativo y Quasar 2. Apps Vue 2: usar la serie 0.0.x.

Instalación

npm install @mundoit-lib/histrix-component-vue

Distribución source-only: el paquete contiene los .vue sin compilar; los compila el bundler de tu app (Vite / Quasar CLI / webpack).

Peers requeridos en tu app:

npm install vue@^3 quasar@^2 @vuelidate/core @vuelidate/validators

Peers opcionales (declarados en peerDependenciesMeta): son los adaptadores por defecto de los contratos de la librería (ver Integración). Si están instalados se usan solos; si no, la app inyecta los suyos.

Paquete Contrato Se toma de
@mundoit-lib/plugin-vue-axios http $axios
@mundoit-lib/plugin-vue-auth auth (1.x/websanova o el AuthService nuevo) $auth
@mundoit-lib/plugin-vue-event bus de eventos hacia la app $events (sin él, bus interno)
vue-router@^4 || ^5 navegación (schema.redirect, menú, volver) $router

Componentes

38 componentes Vue 3. Salvo HistrixUnsupported (aviso interno de HistrixApp para tipos de pantalla sin componente), todos se registran con el plugin y se exportan desde la raíz y por subpath.

Grupo Componentes
Páginas y diálogos HistrixPage (página de app: path de la ruta, query y _title), HistrixAppDialog (app en un q-dialog con v-model; se cierra en process-finish/closepopup y emite finish)
Pantallas schema-driven HistrixApp (raíz: monta la pantalla según el schema), HistrixForm, HistrixTable, HistrixTree, HistrixList, HistrixCalendar, HistrixDashboard, HistrixChart
Piezas de pantalla HistrixField, HistrixCell, HistrixFilters, HistrixHelp (picker de ayudas), HistrixPdfViewer (visor de PDF nativo, ver PDF), ExportForm, HistrixUnsupported
Auth nativa (sin Quasar) HistrixLoginSplit, HistrixRegisterSplit, HistrixForgotPasswordSplit, HistrixResetPasswordSplit
Auth con Quasar LoginForm, FormLoginNotStyles, HistrixPasswordChange, InputPassword
Menú y shell HistrixMenu, HistrixExpansionMenu, HistrixSystemMenu (engranaje del menú de sistema), HistrixMenuSearch (buscador con Ctrl/⌘+K), FavoritItems, profileMenu, profileMenuItems, notificationMenu
Conexión y utilidades DatabaseSelector, HistrixConnectionSettings, HistrixFileManager, HistrixLog, HistrixNews, HistrixUsers

El detalle de props y eventos de cada uno está en docs/03-componentes.md del repo.

Uso

// Como plugin (registra todos los componentes globalmente):
import HistrixPlugin from '@mundoit-lib/histrix-component-vue/plugin';
app.use(HistrixPlugin);

// Named imports:
import { HistrixApp, HistrixTable, config } from '@mundoit-lib/histrix-component-vue';

// Por subpath (tree-shaking manual):
import HistrixForm from '@mundoit-lib/histrix-component-vue/components/HistrixForm';
import useApi from '@mundoit-lib/histrix-component-vue/services/histrixApi';

Configuración runtime (host del backend, base, credenciales OAuth):

import { config } from '@mundoit-lib/histrix-component-vue';

config.apiUrl = 'https://mi-backend-histrix.com'; // `fixApi` sigue andando, pero está deprecado
config.db = 'micliente';
config.clientId = '...';
config.clientSecret = '...';

config es la fuente viva de host y base: se puede cambiar en runtime (multi-tenant, cambio de conexión) y useApi() lo toma en el próximo pedido. Las claves host y database del storage (las escribe HistrixConnectionSettings) son sólo el fallback para cuando config.apiUrl/config.db están vacíos.

Desde 0.2.0 config gana sobre el storage (antes era al revés). Para una app:

  • Si seteaba localStorage.host/localStorage.database para que la lib tomara otro host o base (p. ej. un applyTenant), alcanza con config.apiUrl/config.db: sacar esas escrituras.
  • Si usa HistrixConnectionSettings y además setea config.apiUrl/config.db al arrancar (por .env), lo elegido en el widget vale para la sesión actual, pero al recargar vuelve a mandar la config. Para que persista, la app lee el storage al arrancar y lo vuelca en config, o no setea config y deja que la lib caiga al storage.
<!-- Montar cualquier pantalla declarada en un XML de Histrix: -->
<HistrixApp path="ventas/qry/listado.xml" :query="{ id: 123 }" />

Integración

La librería depende de contratos, no de paquetes: bus ({ emit, on, off }), auth ({ login, logout, user, setUser, getToken, check, onUserChange }), http (instancia tipo axios), storage ({ get, set, remove }) y navegación ((to, { replace, back }) => void). Los tipos están en types/integration.d.ts.

(a) App Mundo IT con los tres plugins: no hay que configurar nada más. La librería toma $axios, $auth y $events de la app, y $router si existe. Los eventos login-ok, loaded-user, update-favorit y el eventAfter del login siguen saliendo por $events, así que los events: { 'loaded-user'() {} } de las apps no cambian. Además salen como emits del componente: <HistrixLoginSplit @loaded-user="...">.

(b) App sin plugins: se inyecta lo que haga falta. Lo que no se pase usa el default:

app.use(HistrixPlugin, {
  http: miAxios,                    // o config.http
  auth: miAuth,                     // adaptador parcial; también acepta el AuthService nuevo
  bus: miBus,                       // opcional: sin él hay un bus interno
  storage: false,                   // false = no persistir; sin definir = localStorage
  onNavigate: (to, { replace, back }) => (back ? history.back() : miNavegar(to, replace))
});

Para la sesión, useHistrixSession() devuelve { user, isLogged, login(u, p), logout(), refresh() }. Es la única fuente de user y reemplaza a leer localStorage.user. Para las pantallas, HistrixPage reemplaza el pages/Histrix.vue de cada app y HistrixAppDialog, los HistrixAppCard. HistrixList ya no navega a la ruta form, que no existe: emite select con { path, query }. HistrixField.resetField(names) reemplaza al evento global reset-field.

PDF

Las pantallas PDF (schema.pdf o la prop pdf de HistrixApp) y el botón de imprimir se ven con HistrixPdfViewer, que usa el visor nativo del navegador en un <iframe>, con Descargar y Abrir en pestaña nueva. Si el navegador no muestra PDFs en línea (navigator.pdfViewerEnabled === false: Chrome Android, algunas PWA de iOS), en lugar del iframe ofrece Compartir (Web Share con el archivo, si el dispositivo lo permite), Abrir y Descargar.

<HistrixPdfViewer v-model="open" :src="blobUrl" :blob="blob" filename="factura.pdf" />
<HistrixPdfViewer inline :src="blobUrl" filename="factura.pdf" />

El blob URL es de quien lo crea (HistrixApp lo revoca al reemplazar el PDF y al desmontarse).

Desde 0.1.4 ya no hace falta @quasar/qpdfviewer ni public/pdfjs (12 MB que la PWA precacheaba). Para sacarlo de una app:

  1. Actualizar a @mundoit-lib/histrix-component-vue@^0.1.4.
  2. quasar ext remove @quasar/qpdfviewer (saca la extensión de package.json y quasar.extensions.json).
  3. Borrar public/pdfjs.
  4. Si la app usa <q-pdfviewer> propio, cambiarlo por HistrixPdfViewer (importado de la lib).
  5. Buildear la PWA y verificar que el precache del service worker ya no incluye pdfjs/.

Errores y notificaciones

Todos los métodos de useApi() rechazan con un HistrixApiError:

import { isHistrixApiError } from '@mundoit-lib/histrix-component-vue';

try {
  await api.insertAppData('ventas/ing/cliente.xml', data);
} catch (e) {
  // e.status: 400 | 401 | 404 | 500 | 0 (red)
  // e.kind: 'validation' | 'auth' | 'not_found' | 'server' | 'network' | 'unknown'
  // e.message: texto legible (sin HTML); e.raw: error original de axios
}

getAppData() resuelve siempre response.data.data como array: un 204 o una consulta vacía dan { data: [] }.

Ante un 401 la librería llama a config.onUnauthorized(error) (no toca el router):

app.use(HistrixPlugin, {
  onUnauthorized: () => router.push('/login')
});

Las notificaciones pasan por un notifier inyectable con la interfaz { success(msg), error(msg | HistrixApiError), info(msg), confirm(msg) → Promise<boolean> }. Por defecto el plugin registra uno con Quasar (Notify y Dialog, que tienen que estar habilitados en quasar.config). Para usar otro, pasalo al instalar el plugin (los métodos que falten caen a la consola):

app.use(HistrixPlugin, {
  notify: {
    success: (msg) => toast.success(msg),
    error: (msg, err) => toast.error(msg),
    info: (msg) => toast.info(msg),
    confirm: (msg) => miDialogo.confirmar(msg) // Promise<boolean>
  }
});

En los componentes:

import { useHistrixNotify } from '@mundoit-lib/histrix-component-vue';

const notify = useHistrixNotify();
notify.error(e); // acepta un HistrixApiError
if (await notify.confirm('¿Eliminar el registro?')) { /* … */ }

Idioma (i18n)

Los textos propios de la librería (botones, avisos, validaciones, paginador…) salen de un diccionario por locale. Sin configurar nada se ven en español, como siempre. Lo que manda el backend (labels, títulos, processButton) no pasa por acá: lo traduce Histrix.

app.use(HistrixPlugin, {
  locale: 'en',                     // default 'es'; acepta un ref para cambiarlo en caliente
  messages: {                       // se mezcla con los diccionarios de la lib: solo las claves a pisar
    en: { 'common.search': 'Find' },
    pt: { 'common.search': 'Pesquisar', 'common.cancel': 'Cancelar' }
  }
});

Las claves son planas, con namespace por componente (table.perPage, form.processFinished, login.title…). La lista completa está en src/locales/es.js (exportado como defaultMessages.es). La librería trae es y en. Si una clave falta en el locale elegido, se usa la de es, y si tampoco está ahí, se muestra la clave. Los parámetros se interpolan con {nombre}: t('export.title', { title }).

Si la app ya usa vue-i18n, puede pasarle su t. Para que funcione, tiene que sumar las claves de la lib a sus mensajes. Las que falten caen al es de la librería:

import { defaultMessages } from '@mundoit-lib/histrix-component-vue';

const i18n = createI18n({
  legacy: false,
  flatJson: true, // las claves de la lib son planas ('table.perPage')
  locale: 'es',
  messages: { es: { ...defaultMessages.es, ...misEs }, en: { ...defaultMessages.en, ...misEn } }
});
app.use(HistrixPlugin, { i18n: { t: i18n.global.t, locale: i18n.global.locale } });

Para cambiar el idioma en caliente o usar los textos en la app:

import { useHistrixI18n } from '@mundoit-lib/histrix-component-vue';

const { t, locale } = useHistrixI18n();
locale.value = 'en';
t('common.cancel'); // 'Cancel'

Los props de texto de las pantallas de auth (title, submitLabel, emailPlaceholder…) siguen funcionando: si se pasan, ganan sobre el diccionario.

Personalizar el menú

HistrixExpansionMenu separa la lógica del diseño: el pedido del menú (getMenu(level)), los destacados, los favoritos, el loading, la navegación y las secciones abiertas (guardadas en el storage) las maneja la librería; la app puede cambiar sólo el diseño. Sin slots ni variables se ve igual que antes.

Slots

Todos son scoped y tienen el diseño de la librería como default. Los de ramas y hojas aplican en todos los niveles del árbol.

Slot Props Reemplaza
#loading — los skeletons mientras carga
#section-header section ('featured' | 'favorites'), count, open el contenido de la cabecera de Destacados y de Favoritos
#featured-item node, to, label, subtitle, onClick, open cada destacado
#favorite-item favorite, to, label, onClick, open, toggleFavorite cada favorito
#branch-header node, label, subtitle el contenido de la cabecera de una rama (el q-expansion-item y sus hijos los sigue armando el menú)
#leaf node, to, label, subtitle, onClick, open, favoritesEnabled, isFavorite, toggleFavorite cada hoja

label y subtitle vienen con las entidades HTML decodificadas y sin pasar a minúsculas. Para navegar hay dos formas: :to="to" + @click="onClick" (link del router, mantiene la clase del item activo y cierra el drawer) o @click="open" (navega y cierra el drawer, para diseños sin :to).

<HistrixExpansionMenu level="phpmen" :is-favorite="true" @close-drawer="drawer = false">
  <template #leaf="{ to, label, onClick, favoritesEnabled, isFavorite, toggleFavorite }">
    <q-item :to="to" dense @click="onClick">
      <q-item-section>{{ label }}</q-item-section>
      <q-item-section v-if="favoritesEnabled" side>
        <q-btn flat round dense :icon="isFavorite ? 'favorite' : 'favorite_border'" @click.stop.prevent="toggleFavorite" />
      </q-item-section>
    </q-item>
  </template>
</HistrixExpansionMenu>

Los tipos de las props están en HistrixExpansionMenuSlots. Hay un ejemplo completo en apps/playground/src/AltMenu.vue.

Variables CSS

Se definen en :root o en un ancestro del menú. Sin definir, valen lo del diseño default.

Variable Default Uso
--histrix-menu-accent var(--q-primary) destacados, item activo
--histrix-menu-accent-contrast #fff ícono de los destacados
--histrix-menu-active-bg accent al 11% sobre blanco fondo del item activo
--histrix-menu-favorite #f59e0b estrella y cabecera de favoritos
--histrix-menu-star #b6c0cf estrella sin marcar
--histrix-menu-muted #64748b íconos y subtítulos
--histrix-menu-branch-color #1e293b texto de las ramas
--histrix-menu-leaf-color #334155 texto de las hojas
--histrix-menu-hover-bg rgba(15, 23, 42, 0.045) hover de ramas y hojas
--histrix-menu-dot / --histrix-menu-dot-hover #cbd5e1 / #94a3b8 punto de las hojas sin ícono
--histrix-menu-count-color / --histrix-menu-count-bg #94a3b8 / rgba(148, 163, 184, 0.16) contador de las secciones

Composable

Para un menú armado desde cero, useHistrixMenu da la misma lógica sin diseño:

import { useHistrixMenu } from '@mundoit-lib/histrix-component-vue';

const menu = useHistrixMenu({ level: 'phpmen', favorites: true, onClose: () => (drawer.value = false) });
// menu.tree, menu.featured, menu.favorites, menu.loading: refs
// menu.featuredOpen, menu.favoritesOpen: refs guardadas en el storage
// menu.isFavorite(menuId), menu.toggleFavorite(node), menu.nodeUri(node), menu.open(node),
// menu.onItemClick(node), menu.label(text), menu.reload()

level y favorites aceptan un valor, una ref o un getter. toggleFavorite llama a la API, notifica y emite update-favorit por el bus, como el menú. Llamado dentro de un componente, comparte su estado con los HistrixExpansionMenu anidados por provide/inject.

Menú de sistema (engranaje)

HistrixSystemMenu es el engranaje de Histrix: un botón con un popup que lista el menú de sistema (GET /menu/phpmen-fsm: usuarios, perfiles, plugins, impresoras…). Se dibuja sólo si el /me trae capabilities.systemMenu: true (administradores) y el menú se pide recién al abrirlo, una vez por usuario. Con systemMenu en false o sin capabilities (Histrix anterior a 2.0) no se dibuja ni se pide nada. Los ítems ocultos y los permisos por perfil los filtra el servidor: se dibuja lo que llega. Cada ítem se abre como cualquier ítem del menú, por su uri.

<HistrixSystemMenu />
<!-- otro ícono, o forzar mostrarlo/ocultarlo sin mirar capabilities -->
<HistrixSystemMenu icon="admin_panel_settings" :enabled="esAdmin" @close="drawer = false" />

Si el menú trae ramas (ítems con children), se dibujan como encabezados con sus ítems debajo.

Slots

Como en HistrixExpansionMenu, son scoped y tienen el diseño de la librería como default.

Slot Props Reemplaza
#button loading, open el ícono del botón
#header title el título del popup
#loading — los skeletons mientras carga
#empty — el aviso de menú vacío
#section node, label, depth el encabezado de una rama
#item node, to, label, subtitle, depth, onClick, open cada ítem

:to="to" + @click="onClick" o @click="open" navegan y cierran el popup, igual que en el menú lateral. Los tipos están en HistrixSystemMenuSlots.

<HistrixSystemMenu>
  <template #item="{ node, label, open }">
    <q-item clickable dense @click="open">
      <q-item-section avatar><q-icon :name="node.icon || 'build'" /></q-item-section>
      <q-item-section>{{ label }}</q-item-section>
    </q-item>
  </template>
</HistrixSystemMenu>

Composable

Para armarlo desde cero (otro tipo de popup, un drawer, una página), useHistrixSystemMenu da la lógica sin diseño:

import { useHistrixSystemMenu } from '@mundoit-lib/histrix-component-vue';

const system = useHistrixSystemMenu({ immediate: false, onClose: () => (open.value = false) });
// system.enabled: computed, capabilities.systemMenu del usuario de la sesión
// system.tree: ref con el árbol tal como llega; system.entries: computed aplanado ({ node, depth, header })
// system.loading: ref; system.load(): lo pide una vez por usuario; system.reload(): lo vuelve a pedir
// system.nodeUri(node), system.open(node), system.onItemClick(node), system.label(text)

enabled acepta un valor, una ref o un getter y pisa a capabilities.systemMenu. Con immediate: true (default) el menú se pide apenas está habilitado; con false, al llamar a load(). Al cambiar de usuario se descarta y se vuelve a pedir. Si el pedido falla o el menú viene vacío, el próximo load() lo reintenta.

Árboles (arbol)

Las pantallas type: "arbol" (p. ej. histrix/menu/menues.xml, el menú del sistema) las muestra HistrixTree sobre q-tree. Es una tabla autorreferenciada: cada fila cuelga del nodo cuyo schema.childField es igual a su schema.parentField (en el menú, hijo del padre = Id_menu del hijo), y la raíz es schema.values[parentField] (phpmen). El backend, filtrando por la raíz, ya devuelve las filas anidadas en children; una lista plana también se arma.

  • Mostrar: el texto del nodo es el primer campo visible de texto que no es clave (titulo) y al lado van los otros visibles (Orden: 10), con el estilo de celda que manda el backend. Buscador sin mayúsculas ni tildes (texto y valores de la fila, sin el campo padre), un botón que expande o contrae todo, y recargar/exportar/imprimir (en celular, en un menú ⋮ con texto, y sin el detalle al lado del texto); lo abierto se conserva al recargar.
  • Editar (con PUT en resources): botón o doble click en el nodo; abre HistrixForm en un diálogo y al grabar recarga.
  • Agregar (con POST): Nuevo agrega en la raíz y + en un nodo agrega adentro, con el parentField ya cargado con el childField del padre. Sólo se puede agregar dentro de un nodo que tiene childField (en el menú, un ítem que llama a un submenú).
  • Borrar (con DELETE): pide confirmación (avisa si el nodo tiene elementos adentro) y manda las claves (esClave) del nodo.

can_insert/can_update/can_delete: false o readonly ocultan la acción aunque el recurso exista. La lógica pura está en src/core/tree.js.

Atajos de teclado

HistrixApp replica los atajos del ERP legacy. Atiende las teclas la app más interna que tenga el foco, y si esa no resuelve F9 o Esc, los pasa a la app que la contiene (por ejemplo, del grid de renglones al comprobante).

Tecla Acción
F9 Procesa (igual que el botón Procesar: valida antes y sólo si el schema trae can_process).
Esc Cierra el diálogo abierto más reciente. Si la app es inner y está dentro de un popup, emite closepopup.
F2 Abre la ayuda (helpContainer) del campo que tiene el foco.
F4 Limpia los filtros de búsqueda, si el foco está en un filtro.
Enter Pasa al siguiente campo editable. En el último campo no graba, salvo que el form tenga enter-submits.
Ctrl+K / ⌘K, / Abre el buscador de programas (HistrixMenuSearch).

Al cargar, el form pone el foco en el campo que tenga autofocus en el schema o, si ninguno lo tiene, en el primero editable.

Dentro de un textarea, un contenteditable o un QEditor no se intercepta ninguna tecla.

<!-- Sin atajos ni foco automático (también en las apps y forms que contiene) -->
<HistrixApp path="/ventas/fac.xml" :keyboard="false" />

<!-- Enter en el último campo graba el form -->
<HistrixApp path="/ventas/fac.xml" enter-submits />

<!-- Otro atajo para el buscador, y abrirlo desde código -->
<HistrixMenuSearch ref="search" hotkey="alt+m" />
<!-- this.$refs.search.show() / this.$refs.search.focus() -->

Desarrollo

La librería vive en el monorepo mundoit-lib/monorepo (packages/histrix-component-vue). El playground está en apps/playground (Vite + Quasar 2) y consume esta librería y los tres plugins por workspace:*. Desde la raíz del monorepo:

pnpm install
cp apps/playground/.env.example apps/playground/.env   # completar host/db/credenciales
pnpm turbo run build --filter histrix-dev-playground   # buildea los plugins
pnpm --filter histrix-dev-playground dev
pnpm --filter @mundoit-lib/histrix-component-vue test

La documentación interna (arquitectura, componentes, servicios, plan de evolución) está en docs/.

Licencia

MIT (c) Luis M. Melgratti luis@mundoit.com.ar

Related Packages

quasar-crud

quasar crud. Uses Quasar Framework and Vue.

9,053
@bildvitta/store-adapter

Adaptador de "stores" para pinia e vuex.

6,215
quasar-form-builder

Form Builder. Uses Quasar Framework and Vue.

5,723

Version History

Version Released License
0.10.0 MIT
0.9.0 MIT
0.8.2 MIT
0.8.1 MIT
0.8.0 MIT