diff --git a/koha-tmpl/intranet-tmpl/prog/js/vue/modules/islands-intranet.ts b/koha-tmpl/intranet-tmpl/prog/js/vue/modules/islands-intranet.ts new file mode 100644 index 0000000000..30f53fdaf2 --- /dev/null +++ b/koha-tmpl/intranet-tmpl/prog/js/vue/modules/islands-intranet.ts @@ -0,0 +1,90 @@ +export * from "./islands"; +import { + hydrate as hydrateIslands, + registerIsland, + IslandStoreDefinitions, + WebComponentDynamicImport, +} from "./islands"; +import { useMainStore } from "../stores/main"; +import { useNavigationStore } from "../stores/navigation"; +import { useVendorStore } from "../stores/vendors"; + +/** + * The islands available in the staff interface. + * @type {Map} + */ +const islands: Map = new Map([ + [ + "acquisitions-menu", + { + importFn: async () => { + const module = await import( + /* webpackChunkName: "acquisitions-menu" */ + "../components/Islands/AcquisitionsMenu.vue" + ); + return module.default; + }, + config: { + stores: ["vendorStore", "navigationStore"], + }, + }, + ], + [ + "vendor-menu", + { + importFn: async () => { + const module = await import( + /* webpackChunkName: "vendor-menu" */ + "../components/Islands/VendorMenu.vue" + ); + return module.default; + }, + config: { + stores: ["vendorStore", "navigationStore"], + }, + }, + ], + [ + "admin-menu", + { + importFn: async () => { + const module = await import( + /* webpackChunkName: "admin-menu" */ + "../components/Islands/AdminMenu.vue" + ); + return module.default; + }, + config: { + stores: [], + }, + }, + ], +]); + +/** + * The Pinia stores islands in the staff interface may request via config.stores. + * @type {IslandStoreDefinitions} + */ +const storeDefinitions: IslandStoreDefinitions = { + mainStore: useMainStore, + navigationStore: useNavigationStore, + vendorStore: useVendorStore, +}; + +// Core islands take the same registration path as plugin islands. +islands.forEach((entry, name) => registerIsland(name, entry)); + +/** + * Hydrates islands with the staff interface store set. + * + * This local export takes precedence over the star re-export from ./islands, + * so imports of hydrate from the built islands.esm.js get the stores baked in. + * @returns {void} + */ +export function hydrate(): void { + hydrateIslands(storeDefinitions); +} + +if (parseInt(document?.currentScript?.getAttribute("init") ?? "0", 10)) { + hydrate(); +} diff --git a/koha-tmpl/intranet-tmpl/prog/js/vue/modules/islands-opac.ts b/koha-tmpl/intranet-tmpl/prog/js/vue/modules/islands-opac.ts new file mode 100644 index 0000000000..780b6e65d3 --- /dev/null +++ b/koha-tmpl/intranet-tmpl/prog/js/vue/modules/islands-opac.ts @@ -0,0 +1,29 @@ +export * from "./islands"; +import { hydrate, registerIsland, WebComponentDynamicImport } from "./islands"; + +/** + * The islands available in the OPAC. + * @type {Map} + */ +const islands: Map = new Map([ + [ + "patron-self-renewal", + { + importFn: async () => { + const module = await import( + /* webpackChunkName: "patron-self-renewal" */ + "../components/Islands/PatronSelfRenewal/PatronSelfRenewal.vue" + ); + return module.default; + }, + config: {}, + }, + ], +]); + +// Core islands take the same registration path as plugin islands. +islands.forEach((entry, name) => registerIsland(name, entry)); + +if (parseInt(document?.currentScript?.getAttribute("init") ?? "0", 10)) { + hydrate(); +} diff --git a/koha-tmpl/intranet-tmpl/prog/js/vue/modules/islands.ts b/koha-tmpl/intranet-tmpl/prog/js/vue/modules/islands.ts index 64a16a5cea..f971bf8ee4 100644 --- a/koha-tmpl/intranet-tmpl/prog/js/vue/modules/islands.ts +++ b/koha-tmpl/intranet-tmpl/prog/js/vue/modules/islands.ts @@ -2,9 +2,6 @@ import { Component, defineCustomElement } from "vue"; export * from "vue"; import { createPinia } from "pinia"; import { $__ } from "../i18n"; -import { useMainStore } from "../stores/main"; -import { useNavigationStore } from "../stores/navigation"; -import { useVendorStore } from "../stores/vendors"; /** * Represents a web component with an import function and optional configuration. @@ -18,8 +15,21 @@ export type WebComponentDynamicImport = { config?: Record<"stores", Array>; }; +/** + * Pinia store definitions available to islands, keyed by the name components + * reference in their config.stores list. + * @typedef {Object.} IslandStoreDefinitions + */ +export type IslandStoreDefinitions = Record< + string, + (pinia: ReturnType) => unknown +>; + /** * A registry for Vue components. + * + * Starts empty; the per-application entry points (islands-intranet.ts, + * islands-opac.ts) and Koha plugins populate it via registerIsland(). * @type {Map} * @property {string} key - The name of the component. * @property {WebComponentDynamicImport} value - The configuration for the component. Includes the import function and optional configuration. @@ -42,66 +52,7 @@ export type WebComponentDynamicImport = { * ], */ export const componentRegistry: Map = - new Map([ - [ - "acquisitions-menu", - { - importFn: async () => { - const module = await import( - /* webpackChunkName: "acquisitions-menu" */ - "../components/Islands/AcquisitionsMenu.vue" - ); - return module.default; - }, - config: { - stores: ["vendorStore", "navigationStore"], - }, - }, - ], - [ - "vendor-menu", - { - importFn: async () => { - const module = await import( - /* webpackChunkName: "vendor-menu" */ - "../components/Islands/VendorMenu.vue" - ); - return module.default; - }, - config: { - stores: ["vendorStore", "navigationStore"], - }, - }, - ], - [ - "admin-menu", - { - importFn: async () => { - const module = await import( - /* webpackChunkName: "admin-menu" */ - "../components/Islands/AdminMenu.vue" - ); - return module.default; - }, - config: { - stores: [], - }, - }, - ], - [ - "patron-self-renewal", - { - importFn: async () => { - const module = await import( - /* webpackChunkName: "patron-self-renewal" */ - "../components/Islands/PatronSelfRenewal/PatronSelfRenewal.vue" - ); - return module.default; - }, - config: {}, - }, - ], - ]); + new Map(); /** * Registers an island component for hydration. @@ -143,16 +94,22 @@ export function registerIsland( /** * Hydrates custom elements by scanning the document and loading only necessary components. + * @param {IslandStoreDefinitions} [storeDefinitions] - The stores islands on this page may request via config.stores. * @returns {void} */ -export function hydrate(): void { +export function hydrate(storeDefinitions: IslandStoreDefinitions = {}): void { window.requestIdleCallback(async () => { + if (componentRegistry.size === 0) { + return; + } + const pinia = createPinia(); - const storesMatrix = { - mainStore: useMainStore(pinia), - navigationStore: useNavigationStore(pinia), - vendorStore: useVendorStore(pinia), - }; + const storesMatrix = Object.fromEntries( + Object.entries(storeDefinitions).map(([name, useStore]) => [ + name, + useStore(pinia), + ]) + ); const islandTagNames = Array.from(componentRegistry.keys()).join(", "); const requestedIslands = new Set( @@ -203,7 +160,3 @@ export function hydrate(): void { }); }); } - -if (parseInt(document?.currentScript?.getAttribute("init") ?? "0", 10)) { - hydrate(); -} diff --git a/rspack.config.js b/rspack.config.js index 422bff86f5..3e2bf49212 100644 --- a/rspack.config.js +++ b/rspack.config.js @@ -16,6 +16,7 @@ const islandsExport = application => { return { resolve: { + extensions: [".ts", "..."], alias: { "@fetch": path.resolve( __dirname, @@ -33,7 +34,7 @@ const islandsExport = application => { entry: { islands: [ "./koha-tmpl/intranet-tmpl/prog/js/vue/csp-nonce.js", - "./koha-tmpl/intranet-tmpl/prog/js/vue/modules/islands.ts", + `./koha-tmpl/intranet-tmpl/prog/js/vue/modules/islands-${application}.ts`, ], }, output: { @@ -101,6 +102,7 @@ const islandsExport = application => { module.exports = [ { resolve: { + extensions: [".ts", "..."], alias: { "@fetch": path.resolve( __dirname, @@ -132,7 +134,7 @@ module.exports = [ ], islands: [ "./koha-tmpl/intranet-tmpl/prog/js/vue/csp-nonce.js", - "./koha-tmpl/intranet-tmpl/prog/js/vue/modules/islands.ts", + "./koha-tmpl/intranet-tmpl/prog/js/vue/modules/islands-intranet.ts", ], sip2: [ "./koha-tmpl/intranet-tmpl/prog/js/vue/csp-nonce.js",