diff --git a/composables/project_manager.js b/composables/project_manager.js new file mode 100644 index 00000000..02540b2b --- /dev/null +++ b/composables/project_manager.js @@ -0,0 +1,151 @@ +import back_schemas from "@geode/opengeodeweb-back/opengeodeweb_back_schemas.json" +import fileDownload from "js-file-download" +import viewer_schemas from "@geode/opengeodeweb-viewer/opengeodeweb_viewer_schemas.json" + +export function useProjectManager() { + const geode = useGeodeStore() + const appStore = useAppStore() + + const exportProject = function () { + geode.start_request() + const infraStore = useInfraStore() + const snapshot = appStore.exportStores() + const schema = back_schemas.opengeodeweb_back.export_project + const defaultName = "project.zip" + + return infraStore + .create_connection() + .then(function () { + return api_fetch( + { schema, params: { snapshot, filename: defaultName } }, + { + response_function: function (response) { + const data = response._data + let downloadName = defaultName + if ( + response && + response.headers && + typeof response.headers.get === "function" + ) { + const name = response.headers.get("new-file-name") + if (name) { + downloadName = name + } + } + fileDownload(data, downloadName) + }, + }, + ) + }) + .finally(function () { + geode.stop_request() + }) + } + + const importProjectFile = function (file) { + geode.start_request() + + const viewerStore = useViewerStore() + const dataBaseStore = useDataBaseStore() + const treeviewStore = useTreeviewStore() + const hybridViewerStore = useHybridViewerStore() + const infraStore = useInfraStore() + + return infraStore + .create_connection() + .then(function () { + return viewerStore.ws_connect() + }) + .then(function () { + return viewer_call({ + schema: viewer_schemas.opengeodeweb_viewer.import_project, + params: {}, + }) + }) + .then(function () { + return viewer_call({ + schema: viewer_schemas.opengeodeweb_viewer.viewer.reset_visualization, + params: {}, + }) + }) + .then(function () { + treeviewStore.clear() + dataBaseStore.clear() + hybridViewerStore.clear() + + const schemaImport = back_schemas.opengeodeweb_back.import_project + const form = new FormData() + const fileName = file && file.name ? file.name : "file" + form.append("file", file, fileName) + + return $fetch(schemaImport.$id, { + baseURL: geode.base_url, + method: "POST", + body: form, + }) + }) + .then(function (result) { + const snapshot = result && result.snapshot ? result.snapshot : {} + + treeviewStore.isImporting = true + + return Promise.resolve() + .then(function () { + return treeviewStore.importStores(snapshot.treeview) + }) + .then(function () { + return hybridViewerStore.initHybridViewer() + }) + .then(function () { + return hybridViewerStore.importStores(snapshot.hybridViewer) + }) + .then(function () { + const snapshotDataBase = + snapshot && snapshot.dataBase && snapshot.dataBase.db + ? snapshot.dataBase.db + : {} + const items = Object.entries(snapshotDataBase).map(function (pair) { + const id = pair[0] + const item = pair[1] + const binaryLightViewable = + item && item.vtk_js && item.vtk_js.binary_light_viewable + ? item.vtk_js.binary_light_viewable + : undefined + return { + id: id, + object_type: item.object_type, + geode_object: item.geode_object, + native_filename: item.native_filename, + viewable_filename: item.viewable_filename, + displayed_name: item.displayed_name, + vtk_js: { binary_light_viewable: binaryLightViewable }, + } + }) + + return importWorkflowFromSnapshot(items) + }) + .then(function () { + return hybridViewerStore.importStores(snapshot.hybridViewer) + }) + .then(function () { + const dataStyleStore = useDataStyleStore() + return dataStyleStore.importStores(snapshot.dataStyle) + }) + .then(function () { + const dataStyleStore = useDataStyleStore() + return dataStyleStore.applyAllStylesFromState() + }) + .then(function () { + treeviewStore.finalizeImportSelection() + treeviewStore.isImporting = false + }) + }) + .finally(function () { + geode.stop_request() + }) + } + + return { exportProject, importProjectFile } +} + +export default useProjectManager diff --git a/composables/viewer_call.js b/composables/viewer_call.js index 6a91d36a..76a7d6fb 100644 --- a/composables/viewer_call.js +++ b/composables/viewer_call.js @@ -18,8 +18,9 @@ export function viewer_call( const client = viewer_store.client return new Promise((resolve, reject) => { - if (!client) { - reject() + if (!client?.getConnection) { + resolve() + return } viewer_store.start_request() client @@ -37,7 +38,7 @@ export function viewer_call( if (request_error_function) { request_error_function(reason) } - reject() + reject(reason) }, ) .catch((error) => { @@ -50,7 +51,7 @@ export function viewer_call( if (response_error_function) { response_error_function(error) } - reject() + reject(error) }) .finally(() => { viewer_store.stop_request() diff --git a/plugins/autoStoreRegister.js b/plugins/auto_store_register.js similarity index 100% rename from plugins/autoStoreRegister.js rename to plugins/auto_store_register.js diff --git a/stores/app_store.js b/stores/app.js similarity index 55% rename from stores/app_store.js rename to stores/app.js index fde1a59a..4fb94817 100644 --- a/stores/app_store.js +++ b/stores/app.js @@ -5,81 +5,72 @@ export const useAppStore = defineStore("app", () => { const isAlreadyRegistered = stores.some( (registeredStore) => registeredStore.$id === store.$id, ) - if (isAlreadyRegistered) { console.log( `[AppStore] Store "${store.$id}" already registered, skipping`, ) return } - console.log("[AppStore] Registering store", store.$id) stores.push(store) } - function save() { + function exportStores() { const snapshot = {} - let savedCount = 0 + let exportCount = 0 for (const store of stores) { - if (!store.save) { - continue - } + if (!store.exportStores) continue const storeId = store.$id try { - snapshot[storeId] = store.save() - savedCount++ + snapshot[storeId] = store.exportStores() + exportCount++ } catch (error) { - console.error(`[AppStore] Error saving store "${storeId}":`, error) + console.error(`[AppStore] Error exporting store "${storeId}":`, error) } } - - console.log(`[AppStore] Saved ${savedCount} stores`) + console.log( + `[AppStore] Exported ${exportCount} stores; snapshot keys:`, + Object.keys(snapshot), + ) return snapshot } - function load(snapshot) { + async function importStores(snapshot) { if (!snapshot) { - console.warn("[AppStore] load called with invalid snapshot") + console.warn("[AppStore] import called with invalid snapshot") return } + console.log("[AppStore] Import snapshot keys:", Object.keys(snapshot || {})) - let loadedCount = 0 + let importedCount = 0 const notFoundStores = [] - for (const store of stores) { - if (!store.load) { - continue - } - + if (!store.importStores) continue const storeId = store.$id - if (!snapshot[storeId]) { notFoundStores.push(storeId) continue } - try { - store.load(snapshot[storeId]) - loadedCount++ + await store.importStores(snapshot[storeId]) + importedCount++ } catch (error) { - console.error(`[AppStore] Error loading store "${storeId}":`, error) + console.error(`[AppStore] Error importing store "${storeId}":`, error) } } - if (notFoundStores.length > 0) { console.warn( `[AppStore] Stores not found in snapshot: ${notFoundStores.join(", ")}`, ) } - - console.log(`[AppStore] Loaded ${loadedCount} stores`) + console.log(`[AppStore] Imported ${importedCount} stores`) } return { stores, registerStore, - save, - load, + exportStores, + importStores, } }) diff --git a/stores/data_base.js b/stores/data_base.js index cc4668d8..4180a624 100644 --- a/stores/data_base.js +++ b/stores/data_base.js @@ -48,6 +48,9 @@ export const useDataBaseStore = defineStore("dataBase", () => { }) } + // const treeviewStore = useTreeviewStore() + const hybridViewerStore = useHybridViewerStore() + async function addItem( id, value = { @@ -59,7 +62,17 @@ export const useDataBaseStore = defineStore("dataBase", () => { vtk_js: { binary_light_viewable }, }, ) { + // console.log("[DataBase] addItem start", { + // id, + // object_type: value.object_type, + // geode_object: value.geode_object, + // }) db[id] = value + + if (value.object_type === "model") { + await fetchMeshComponents(id) + await fetchUuidToFlatIndexDict(id) + } } async function fetchMeshComponents(id) { @@ -120,6 +133,43 @@ export const useDataBaseStore = defineStore("dataBase", () => { return flat_indexes } + function exportStores() { + const snapshotDb = {} + for (const [id, item] of Object.entries(db)) { + if (!item) continue + snapshotDb[id] = { + object_type: item.object_type, + geode_object: item.geode_object, + native_filename: item.native_filename, + viewable_filename: item.viewable_filename, + displayed_name: item.displayed_name, + vtk_js: { + binary_light_viewable: item?.vtk_js?.binary_light_viewable, + }, + } + } + return { db: snapshotDb } + } + + async function importStores(snapshot) { + await hybridViewerStore.initHybridViewer() + hybridViewerStore.clear() + console.log( + "[DataBase] importStores entries:", + Object.keys(snapshot?.db || {}), + ) + for (const [id, item] of Object.entries(snapshot?.db || {})) { + await registerObject(id) + await addItem(id, item) + } + } + + function clear() { + for (const id of Object.keys(db)) { + delete db[id] + } + } + return { db, itemMetaDatas, @@ -134,5 +184,8 @@ export const useDataBaseStore = defineStore("dataBase", () => { getSurfacesUuids, getBlocksUuids, getFlatIndexes, + exportStores, + importStores, + clear, } }) diff --git a/stores/data_style.js b/stores/data_style.js index 994f306b..2bd5d044 100644 --- a/stores/data_style.js +++ b/stores/data_style.js @@ -1,6 +1,7 @@ import useDataStyleState from "../internal_stores/data_style_state.js" import useMeshStyle from "../internal_stores/mesh/index.js" import useModelStyle from "../internal_stores/model/index.js" +import { getDefaultStyle } from "../utils/default_styles.js" export const useDataStyleStore = defineStore("dataStyle", () => { const dataStyleState = useDataStyleState() @@ -9,15 +10,17 @@ export const useDataStyleStore = defineStore("dataStyle", () => { const dataBaseStore = useDataBaseStore() function addDataStyle(id, geode_object) { - dataStyleState.styles[id] = getDefaultStyle(geode_object) + const style = getDefaultStyle(geode_object) + dataStyleState.styles[id] = style } function setVisibility(id, visibility) { - console.log( - "dataBaseStore.itemMetaDatas(id)", - dataBaseStore.itemMetaDatas(id), - ) - const object_type = dataBaseStore.itemMetaDatas(id).object_type + const meta = dataBaseStore.itemMetaDatas(id) + if (!meta) { + console.warn("[DataStyle] setVisibility skipped: unknown id", id) + return Promise.resolve([]) + } + const object_type = meta.object_type if (object_type === "mesh") { return Promise.all([meshStyleStore.setMeshVisibility(id, visibility)]) } else if (object_type === "model") { @@ -32,8 +35,43 @@ export const useDataStyleStore = defineStore("dataStyle", () => { return meshStyleStore.applyMeshStyle(id) } else if (object_type === "model") { return modelStyleStore.applyModelStyle(id) - } else { - throw new Error("Unknown object_type: " + object_type) + } + return Promise.resolve([]) + } + + function setModelEdgesVisibility(id, visibility) { + return modelStyleStore.setModelEdgesVisibility(id, visibility) + } + + function modelEdgesVisibility(id) { + return modelStyleStore.modelEdgesVisibility(id) + } + + function exportStores() { + return { styles: dataStyleState.styles } + } + + async function importStores(snapshot) { + const stylesSnapshot = snapshot?.styles || {} + for (const id of Object.keys(dataStyleState.styles)) + delete dataStyleState.styles[id] + for (const [id, style] of Object.entries(stylesSnapshot)) { + dataStyleState.styles[id] = style + } + } + + async function applyAllStylesFromState() { + const ids = Object.keys(dataStyleState.styles || {}) + for (const id of ids) { + const meta = dataBaseStore.itemMetaDatas(id) + const objectType = meta?.object_type + const style = dataStyleState.styles[id] + if (!style || !objectType) continue + if (objectType === "mesh") { + await meshStyleStore.applyMeshStyle(id) + } else if (objectType === "model") { + await modelStyleStore.applyModelStyle(id) + } } } @@ -44,5 +82,10 @@ export const useDataStyleStore = defineStore("dataStyle", () => { addDataStyle, applyDefaultStyle, setVisibility, + setModelEdgesVisibility, + modelEdgesVisibility, + exportStores, + importStores, + applyAllStylesFromState, } }) diff --git a/stores/hybrid_viewer.js b/stores/hybrid_viewer.js index 56cb35ee..a6012d61 100644 --- a/stores/hybrid_viewer.js +++ b/stores/hybrid_viewer.js @@ -100,7 +100,6 @@ export const useHybridViewerStore = defineStore("hybridViewer", () => { } function syncRemoteCamera() { - console.log("syncRemoteCamera") const renderer = genericRenderWindow.value.getRenderer() const camera = renderer.getActiveCamera() const params = { @@ -110,7 +109,6 @@ export const useHybridViewerStore = defineStore("hybridViewer", () => { position: camera.getPosition(), view_angle: camera.getViewAngle(), clipping_range: camera.getClippingRange(), - distance: camera.getDistance(), }, } viewer_call( @@ -198,6 +196,73 @@ export const useHybridViewerStore = defineStore("hybridViewer", () => { remoteRender() } + function clear() { + const renderer = genericRenderWindow.value.getRenderer() + const actors = renderer.getActors() + actors.forEach((actor) => renderer.removeActor(actor)) + genericRenderWindow.value.getRenderWindow().render() + Object.keys(db).forEach((id) => delete db[id]) + } + + function exportStores() { + const renderer = genericRenderWindow.value?.getRenderer?.() + const camera = renderer?.getActiveCamera?.() + const cameraSnapshot = camera + ? { + focal_point: Array.from(camera.getFocalPoint()), + view_up: Array.from(camera.getViewUp()), + position: Array.from(camera.getPosition()), + view_angle: camera.getViewAngle(), + clipping_range: Array.from(camera.getClippingRange()), + distance: camera.getDistance(), + } + : camera_options + return { zScale: zScale.value, camera_options: cameraSnapshot } + } + + async function importStores(snapshot) { + const z_scale = snapshot?.zScale + if (z_scale != null) { + await setZScaling(z_scale) + } + + const camera_options = snapshot?.camera_options + if (!camera_options) return + + const renderer = genericRenderWindow.value.getRenderer() + const camera = renderer.getActiveCamera() + + camera.setFocalPoint(...camera_options.focal_point) + camera.setViewUp(...camera_options.view_up) + camera.setPosition(...camera_options.position) + camera.setViewAngle(camera_options.view_angle) + camera.setClippingRange(...camera_options.clipping_range) + + genericRenderWindow.value.getRenderWindow().render() + + const payload = { + camera_options: { + focal_point: camera_options.focal_point, + view_up: camera_options.view_up, + position: camera_options.position, + view_angle: camera_options.view_angle, + clipping_range: camera_options.clipping_range, + }, + } + viewer_call( + { + schema: viewer_schemas.opengeodeweb_viewer.viewer.update_camera, + params: payload, + }, + { + response_function: () => { + remoteRender() + Object.assign(camera_options, payload.camera_options) + }, + }, + ) + } + return { db, genericRenderWindow, @@ -210,5 +275,8 @@ export const useHybridViewerStore = defineStore("hybridViewer", () => { resize, setContainer, zScale, + clear, + exportStores, + importStores, } }) diff --git a/stores/treeview.js b/stores/treeview.js index f9a89caf..56e17e59 100644 --- a/stores/treeview.js +++ b/stores/treeview.js @@ -1,7 +1,4 @@ export const useTreeviewStore = defineStore("treeview", () => { - const dataStyleStore = useDataStyleStore() - const dataBaseStore = useDataBaseStore() - const items = ref([]) const selection = ref([]) const components_selection = ref([]) @@ -10,6 +7,8 @@ export const useTreeviewStore = defineStore("treeview", () => { const model_id = ref("") const isTreeCollection = ref(false) const selectedTree = ref(null) + const isImporting = ref(false) + const pendingSelectionIds = ref([]) // /** Functions **/ function addItem(geodeObject, displayed_name, id, object_type) { @@ -24,12 +23,16 @@ export const useTreeviewStore = defineStore("treeview", () => { sensitivity: "base", }), ) - selection.value.push(child) + if (!isImporting.value) { + selection.value.push(child) + } return } } items.value.push({ title: geodeObject, children: [child] }) - selection.value.push(child) + if (!isImporting.value) { + selection.value.push(child) + } } function displayAdditionalTree(id) { @@ -53,6 +56,60 @@ export const useTreeviewStore = defineStore("treeview", () => { panelWidth.value = width } + function exportStores() { + return { + isAdditionnalTreeDisplayed: isAdditionnalTreeDisplayed.value, + panelWidth: panelWidth.value, + model_id: model_id.value, + isTreeCollection: isTreeCollection.value, + selectedTree: selectedTree.value, + selectionIds: selection.value.map((c) => c.id), + } + } + + async function importStores(snapshot) { + isAdditionnalTreeDisplayed.value = + snapshot?.isAdditionnalTreeDisplayed || false + panelWidth.value = snapshot?.panelWidth || 300 + model_id.value = snapshot?.model_id || "" + isTreeCollection.value = snapshot?.isTreeCollection || false + selectedTree.value = snapshot?.selectedTree || null + + pendingSelectionIds.value = + snapshot?.selectionIds || + (snapshot?.selection || []).map((c) => c.id) || + [] + } + + function finalizeImportSelection() { + const ids = pendingSelectionIds.value || [] + const rebuilt = [] + if (ids.length === 0) { + for (const group of items.value) { + for (const child of group.children) { + rebuilt.push(child) + } + } + } else { + for (const group of items.value) { + for (const child of group.children) { + if (ids.includes(child.id)) rebuilt.push(child) + } + } + } + selection.value = rebuilt + pendingSelectionIds.value = [] + } + + function clear() { + items.value = [] + selection.value = [] + components_selection.value = [] + pendingSelectionIds.value = [] + model_id.value = "" + selectedTree.value = null + } + return { items, selection, @@ -61,10 +118,15 @@ export const useTreeviewStore = defineStore("treeview", () => { panelWidth, model_id, selectedTree, + isImporting, addItem, displayAdditionalTree, displayFileTree, toggleTreeView, setPanelWidth, + exportStores, + importStores, + finalizeImportSelection, + clear, } }) diff --git a/tests/integration/microservices/back/requirements.txt b/tests/integration/microservices/back/requirements.txt index 0c0e8387..bd3a3ef5 100644 --- a/tests/integration/microservices/back/requirements.txt +++ b/tests/integration/microservices/back/requirements.txt @@ -5,4 +5,3 @@ # pip-compile --output-file=tests/integration/microservices/back/requirements.txt tests/integration/microservices/back/requirements.in # -opengeodeweb-back==5.*,>=5.12.0 diff --git a/tests/integration/microservices/viewer/requirements.txt b/tests/integration/microservices/viewer/requirements.txt index 5930c6c0..4d097394 100644 --- a/tests/integration/microservices/viewer/requirements.txt +++ b/tests/integration/microservices/viewer/requirements.txt @@ -5,4 +5,3 @@ # pip-compile --output-file=tests/integration/microservices/viewer/requirements.txt tests/integration/microservices/viewer/requirements.in # -opengeodeweb-viewer==1.*,>=1.11.9rc1 diff --git a/tests/unit/composables/ProjectManager.nuxt.test.js b/tests/unit/composables/ProjectManager.nuxt.test.js new file mode 100644 index 00000000..a81539ff --- /dev/null +++ b/tests/unit/composables/ProjectManager.nuxt.test.js @@ -0,0 +1,129 @@ +import { beforeEach, describe, expect, test, vi } from "vitest" +import { setActivePinia } from "pinia" +import { createTestingPinia } from "@pinia/testing" +import { useProjectManager } from "@/composables/project_manager.js" + +// Mocks +const mockAppStore = { + exportStores: vi.fn(() => ({ projectName: "mockedProject" })), + importStores: vi.fn(), +} +const mockInfraStore = { create_connection: vi.fn() } + +vi.mock("@/stores/app.js", () => ({ useAppStore: () => mockAppStore })) +vi.mock("@ogw_f/stores/infra", () => ({ useInfraStore: () => mockInfraStore })) +vi.mock("@/composables/viewer_call.js", () => ({ + viewer_call: vi.fn(), +})) + +beforeEach(async () => { + const pinia = createTestingPinia({ + stubActions: false, + createSpy: vi.fn, + }) + setActivePinia(pinia) + const geode_store = useGeodeStore() + await geode_store.$reset() + geode_store.base_url = "" +}) + +vi.mock("@geode/opengeodeweb-back/opengeodeweb_back_schemas.json", () => ({ + default: { + opengeodeweb_back: { + export_project: { + $id: "opengeodeweb_back/export_project", + route: "/export_project", + methods: ["POST"], + type: "object", + properties: { + snapshot: { type: "object" }, + filename: { type: "string", minLength: 1 }, + }, + required: ["snapshot", "filename"], + additionalProperties: false, + }, + import_project: { + $id: "opengeodeweb_back/import_project", + route: "/import_project", + methods: ["POST"], + type: "object", + properties: {}, + required: [], + additionalProperties: false, + }, + }, + }, +})) +vi.mock("@geode/opengeodeweb-viewer/opengeodeweb_viewer_schemas.json", () => ({ + default: { + opengeodeweb_viewer: { + import_project: { rpc: "utils.import_project" }, + viewer: { + reset_visualization: { rpc: "viewer.reset_visualization" }, + update_data: { rpc: "viewer.update_data" }, + }, + mesh: { + register: { rpc: "mesh.register" }, + points: {}, + }, + model: { + register: { rpc: "model.register" }, + surfaces: {}, + }, + }, + }, +})) +vi.mock("@/composables/api_fetch.js", () => ({ + api_fetch: vi.fn(async (_req, options = {}) => { + const response = { + _data: new Blob(["zipcontent"], { type: "application/zip" }), + headers: { + get: (k) => (k === "new-file-name" ? "project_123.zip" : null), + }, + } + if (options.response_function) { + await options.response_function(response) + } + return response + }), +})) +vi.stubGlobal( + "$fetch", + vi.fn(async () => ({ snapshot: {} })), +) +vi.stubGlobal("useDataBaseStore", () => ({ items: {} })) + +// Mock du store base de données pour éviter Object.entries(undefined) +vi.mock("@/stores/data_base.js", () => ({ + useDataBaseStore: () => ({ items: {} }), +})) +vi.mock("@ogw_f/stores/data_base", () => ({ + useDataBaseStore: () => ({ items: {} }), +})) + +describe("ProjectManager composable", () => { + test("exportProject triggers download", async () => { + const clickSpy = vi + .spyOn(HTMLAnchorElement.prototype, "click") + .mockImplementation(() => {}) + vi.spyOn(URL, "createObjectURL").mockReturnValue("blob:url") + vi.spyOn(URL, "revokeObjectURL").mockImplementation(() => {}) + }) + + test("importProjectFile loads snapshot", async () => { + const { importProjectFile } = useProjectManager() + + const file = new Blob(['{"dataBase":{"db":{}}}'], { + type: "application/json", + }) + await importProjectFile(file) + + const infra_store = useInfraStore() + const app_store = useAppStore() + const { viewer_call } = await import("@/composables/viewer_call.js") + + expect(infra_store.create_connection).toHaveBeenCalled() + expect(viewer_call).toHaveBeenCalled() + expect(app_store.importStores).toHaveBeenCalled() + }) +}) diff --git a/tests/unit/plugins/ProjectLoad.nuxt.test.js b/tests/unit/plugins/ProjectLoad.nuxt.test.js new file mode 100644 index 00000000..fdcb6606 --- /dev/null +++ b/tests/unit/plugins/ProjectLoad.nuxt.test.js @@ -0,0 +1,102 @@ +import { beforeEach, describe, expect, test, vi } from "vitest" +import { setActivePinia } from "pinia" +import { createTestingPinia } from "@pinia/testing" +import { useAppStore } from "@/stores/app.js" +import { useDataBaseStore } from "@/stores/data_base.js" +import { useTreeviewStore } from "@/stores/treeview.js" +import { useDataStyleStore } from "@/stores/data_style.js" +import { useHybridViewerStore } from "@/stores/hybrid_viewer.js" +import { viewer_call } from "@/composables/viewer_call.js" + +vi.mock("@/composables/viewer_call.js", () => ({ + default: vi.fn(() => Promise.resolve({})), + viewer_call: vi.fn(() => Promise.resolve({})), +})) +vi.mock("@/stores/hybrid_viewer.js", () => ({ + useHybridViewerStore: () => ({ + $id: "hybridViewer", + initHybridViewer: vi.fn(), + clear: vi.fn(), + addItem: vi.fn(), + setZScaling: vi.fn(), + save: vi.fn(), + load: vi.fn(), + }), +})) + +beforeEach(() => { + setActivePinia( + createTestingPinia({ + stubActions: false, + createSpy: vi.fn, + }), + ) +}) + +describe("Project import", () => { + test("app.importStores restores stores", async () => { + const stores = { + app: useAppStore(), + dataBase: useDataBaseStore(), + treeview: useTreeviewStore(), + dataStyle: useDataStyleStore(), + hybrid: useHybridViewerStore(), + } + Object.values(stores) + .slice(1) + .forEach((store) => stores.app.registerStore(store)) + + const snapshot = { + dataBase: { + db: { + abc123: { + object_type: "mesh", + geode_object: "PointSet2D", + native_filename: "native.ext", + viewable_filename: "viewable.ext", + displayed_name: "My Data", + vtk_js: { binary_light_viewable: "VGxpZ2h0RGF0YQ==" }, + }, + }, + }, + treeview: { + items: [{ title: "PointSet2D", children: [] }], + selection: [], + components_selection: [], + isAdditionnalTreeDisplayed: false, + panelWidth: 320, + model_id: "", + isTreeCollection: false, + selectedTree: null, + }, + dataStyle: { styles: { abc123: { some: "style" } } }, + hybridViewer: { zScale: 1.5 }, + } + + console.log("[TEST ProjectImport] Snapshot keys:", Object.keys(snapshot)) + console.log( + "[TEST ProjectImport] treeview snapshot:", + JSON.stringify(snapshot.treeview, null, 2), + ) + console.log( + "[TEST ProjectImport] dataStyle snapshot:", + JSON.stringify(snapshot.dataStyle, null, 2), + ) + + await stores.app.importStores(snapshot) + + console.log( + "[TEST ProjectImport] Treeview items after import:", + JSON.stringify(stores.treeview.items, null, 2), + ) + console.log( + "[TEST ProjectImport] Styles after import:", + JSON.stringify(stores.dataStyle.styles, null, 2), + ) + + expect(stores.dataBase.db.abc123).toBeDefined() + expect(stores.treeview.items.length).toBe(1) + expect(stores.dataStyle.styles.abc123).toBeDefined() + expect(viewer_call).toHaveBeenCalled() + }) +}) diff --git a/tests/unit/stores/Appstore.nuxt.test.js b/tests/unit/stores/Appstore.nuxt.test.js index 4f4a7155..ee37ecda 100644 --- a/tests/unit/stores/Appstore.nuxt.test.js +++ b/tests/unit/stores/Appstore.nuxt.test.js @@ -1,6 +1,6 @@ import { beforeEach, describe, expect, expectTypeOf, test, vi } from "vitest" import { createTestingPinia } from "@pinia/testing" -import { useAppStore } from "@/stores/app_store.js" +import { useAppStore } from "@/stores/app.js" import { setActivePinia } from "pinia" beforeEach(async () => { @@ -16,8 +16,8 @@ describe("App Store", () => { test("initial state", () => { const app_store = useAppStore() expectTypeOf(app_store.stores).toBeArray() - expectTypeOf(app_store.save).toBeFunction() - expectTypeOf(app_store.load).toBeFunction() + expectTypeOf(app_store.exportStores).toBeFunction() + expectTypeOf(app_store.importStores).toBeFunction() expectTypeOf(app_store.registerStore).toBeFunction() }) }) @@ -46,7 +46,7 @@ describe("App Store", () => { load: vi.fn().mockImplementation(() => {}), } const mock_store_2 = { - $id: "cartStore", + $id: "geodeStore", save: vi.fn().mockImplementation(() => {}), load: vi.fn().mockImplementation(() => {}), } @@ -56,58 +56,60 @@ describe("App Store", () => { expect(app_store.stores.length).toBe(2) expect(app_store.stores[0].$id).toBe("userStore") - expect(app_store.stores[1].$id).toBe("cartStore") + expect(app_store.stores[1].$id).toBe("geodeStore") }) }) - describe("save", () => { - test("save stores with save method", () => { + describe("Export", () => { + test("export stores with exportStores method", () => { const app_store = useAppStore() const mock_store_1 = { $id: "userStore", - save: vi.fn().mockImplementation(() => ({ + exportStores: vi.fn().mockImplementation(() => ({ name: "toto", email: "toto@titi.com", })), - load: vi.fn().mockImplementation(() => {}), + importStores: vi.fn().mockImplementation(() => {}), } const mock_store_2 = { - $id: "cartStore", - save: vi.fn().mockImplementation(() => ({ items: [], total: 0 })), - load: vi.fn().mockImplementation(() => {}), + $id: "geodeStore", + exportStores: vi + .fn() + .mockImplementation(() => ({ items: [], total: 0 })), + importStores: vi.fn().mockImplementation(() => {}), } app_store.registerStore(mock_store_1) app_store.registerStore(mock_store_2) - const snapshot = app_store.save() + const snapshot = app_store.exportStores() - expect(mock_store_1.save).toHaveBeenCalledTimes(1) - expect(mock_store_2.save).toHaveBeenCalledTimes(1) + expect(mock_store_1.exportStores).toHaveBeenCalledTimes(1) + expect(mock_store_2.exportStores).toHaveBeenCalledTimes(1) expect(snapshot).toEqual({ userStore: { name: "toto", email: "toto@titi.com" }, - cartStore: { items: [], total: 0 }, + geodeStore: { items: [], total: 0 }, }) }) - test("skip stores without save method", () => { + test("skip stores without exportSave method", () => { const app_store = useAppStore() const mock_store_1 = { $id: "withSave", - save: vi.fn().mockImplementation(() => ({ data: "test" })), - load: vi.fn().mockImplementation(() => {}), + exportStores: vi.fn().mockImplementation(() => ({ data: "test" })), + importStores: vi.fn().mockImplementation(() => {}), } const mock_store_2 = { $id: "withoutSave", - load: vi.fn().mockImplementation(() => {}), + importStores: vi.fn().mockImplementation(() => {}), } app_store.registerStore(mock_store_1) app_store.registerStore(mock_store_2) - const snapshot = app_store.save() + const snapshot = app_store.exportStores() - expect(mock_store_1.save).toHaveBeenCalledTimes(1) + expect(mock_store_1.exportStores).toHaveBeenCalledTimes(1) expect(snapshot).toEqual({ withSave: { data: "test" }, }) @@ -116,71 +118,53 @@ describe("App Store", () => { test("return empty snapshot when no stores registered", () => { const app_store = useAppStore() - const snapshot = app_store.save() + const snapshot = app_store.exportStores() expect(snapshot).toEqual({}) }) }) describe("load", () => { - test("load stores with load method", () => { - const app_store = useAppStore() - const mock_store_1 = { + test("App Store > actions > importStores > import stores with importStores method", async () => { + const appStore = useAppStore() + const userStore = { $id: "userStore", - save: vi.fn().mockImplementation(() => {}), - load: vi.fn().mockImplementation(() => {}), + importStores: vi.fn().mockResolvedValue(), } - const mock_store_2 = { - $id: "cartStore", - save: vi.fn().mockImplementation(() => {}), - load: vi.fn().mockImplementation(() => {}), + const geodeStore = { + $id: "geodeStore", + importStores: vi.fn().mockResolvedValue(), } - - app_store.registerStore(mock_store_1) - app_store.registerStore(mock_store_2) - + appStore.registerStore(userStore) + appStore.registerStore(geodeStore) const snapshot = { - userStore: { name: "tata", email: "tata@tutu.com" }, - cartStore: { items: [{ id: 1 }], total: 50 }, + userStore: { some: "data" }, + geodeStore: { other: "data" }, } - - app_store.load(snapshot) - - expect(mock_store_1.load).toHaveBeenCalledTimes(1) - expect(mock_store_1.load).toHaveBeenCalledWith({ - name: "tata", - email: "tata@tutu.com", - }) - expect(mock_store_2.load).toHaveBeenCalledTimes(1) - expect(mock_store_2.load).toHaveBeenCalledWith({ - items: [{ id: 1 }], - total: 50, - }) + await appStore.importStores(snapshot) + expect(userStore.importStores).toHaveBeenCalledTimes(1) + expect(geodeStore.importStores).toHaveBeenCalledTimes(1) }) - test("skip stores without load method", () => { + test("skip stores without importStores method", () => { const app_store = useAppStore() const mock_store_1 = { - $id: "withLoad", + $id: "withImport", save: vi.fn().mockImplementation(() => {}), - load: vi.fn().mockImplementation(() => {}), + importStores: vi.fn().mockImplementation(() => {}), } const mock_store_2 = { - $id: "withoutLoad", + $id: "withoutImport", save: vi.fn().mockImplementation(() => {}), } - app_store.registerStore(mock_store_1) app_store.registerStore(mock_store_2) - const snapshot = { - withLoad: { data: "test" }, - withoutLoad: { data: "ignored" }, + withImport: { data: "test" }, + withoutImport: { data: "ignored" }, } - - app_store.load(snapshot) - - expect(mock_store_1.load).toHaveBeenCalledTimes(1) - expect(mock_store_2.load).toBeUndefined() + app_store.importStores(snapshot) + expect(mock_store_1.importStores).toHaveBeenCalledTimes(1) + expect(mock_store_2.importStores).toBeUndefined() }) test("warn when store not found in snapshot", () => { @@ -190,12 +174,10 @@ describe("App Store", () => { .mockImplementation(() => {}) const mock_store = { $id: "testStore", - load: vi.fn().mockImplementation(() => {}), + importStores: vi.fn().mockImplementation(() => {}), } - app_store.registerStore(mock_store) - app_store.load({}) - + app_store.importStores({}) expect(console_warn_spy).toHaveBeenCalledWith( expect.stringContaining("Stores not found in snapshot: testStore"), ) diff --git a/utils/default_styles.js b/utils/default_styles.js index eede29e7..6f071350 100644 --- a/utils/default_styles.js +++ b/utils/default_styles.js @@ -16,9 +16,9 @@ const corners_defaultColor = { r: 20, g: 20, b: 20 } const lines_defaultVisibility = true const lines_defaultColor = { r: 20, g: 20, b: 20 } const surfaces_defaultVisibility = true -const surfaces_defaultColor = { r: 20, g: 20, b: 20 } +const surfaces_defaultColor = { r: 255, g: 255, b: 255 } const blocks_defaultVisibility = true -const blocks_defaultColor = { r: 20, g: 20, b: 20 } +const blocks_defaultColor = { r: 255, g: 255, b: 255 } // Mesh functions const meshPointsDefaultStyle = ( diff --git a/utils/file_import_workflow.js b/utils/file_import_workflow.js index 998d23d8..66182cf4 100644 --- a/utils/file_import_workflow.js +++ b/utils/file_import_workflow.js @@ -1,18 +1,17 @@ // Third party imports import back_schemas from "@geode/opengeodeweb-back/opengeodeweb_back_schemas.json" -import { useHybridViewerStore } from "../stores/hybrid_viewer" // Local imports async function importWorkflow(files) { console.log("importWorkflow", { files }) const promise_array = [] - for (const file of files) { - const { filename, geode_object } = file + for (const { filename, geode_object } of files) { console.log({ filename }, { geode_object }) - promise_array.push(importFile(filename, geode_object)) + const id = await importFile(filename, geode_object) + promise_array.push(id) } - return Promise.all(promise_array) + return promise_array } async function importFile(filename, geode_object) { @@ -37,11 +36,14 @@ async function importFile(filename, geode_object) { binary_light_viewable, } = data._value - console.log("data._value", data._value) + console.log("[importFile] response", { + id, + geode_object, + input_file: data._value?.input_file, + name, + }) - console.log("data._value.id", data._value.id) - await dataBaseStore.registerObject(data._value.id) - console.log("after dataBaseStore.registerObject") + await dataBaseStore.registerObject(id) await dataBaseStore.addItem(id, { object_type: object_type, geode_object: geode_object, @@ -65,14 +67,11 @@ async function importFile(filename, geode_object) { data._value.geode_object, data._value.object_type, ) - console.log("after dataStyleStore.addDataStyle") if (data._value.object_type === "model") { await Promise.all([ dataBaseStore.fetchMeshComponents(id), dataBaseStore.fetchUuidToFlatIndexDict(id), ]) - console.log("after dataBaseStore.fetchMeshComponents") - console.log("after dataBaseStore.fetchUuidToFlatIndexDict") } await dataStyleStore.applyDefaultStyle(id) console.log("after dataStyleStore.applyDefaultStyle") @@ -80,4 +79,74 @@ async function importFile(filename, geode_object) { return data._value.id } -export { importFile, importWorkflow } +async function importWorkflowFromSnapshot(items) { + console.log("[importWorkflowFromSnapshot] start", { count: items?.length }) + const dataBaseStore = useDataBaseStore() + const treeviewStore = useTreeviewStore() + const dataStyleStore = useDataStyleStore() + const hybridViewerStore = useHybridViewerStore() + const ids = [] + for (const item of items) { + console.log("[importWorkflowFromSnapshot] item", { + id: item.id, + object_type: item.object_type, + geode_object: item.geode_object, + displayed_name: item.displayed_name, + }) + + await dataBaseStore.registerObject(item.id) + console.log("[importWorkflowFromSnapshot] registerObject ok", item.id) + + await dataBaseStore.addItem(item.id, { + object_type: item.object_type, + geode_object: item.geode_object, + native_filename: item.native_filename, + viewable_filename: item.viewable_filename, + displayed_name: item.displayed_name, + vtk_js: item.vtk_js, + }) + console.log("[importWorkflowFromSnapshot] addItem ok", item.id) + + await treeviewStore.addItem( + item.geode_object, + item.displayed_name, + item.id, + item.object_type, + ) + console.log("[importWorkflowFromSnapshot] treeview.addItem ok", item.id) + + await hybridViewerStore.addItem(item.id) + console.log("[importWorkflowFromSnapshot] hybridViewer.addItem ok", item.id) + + await dataStyleStore.addDataStyle(item.id, item.geode_object) + console.log( + "[importWorkflowFromSnapshot] dataStyle.addDataStyle ok", + item.id, + ) + + if (item.object_type === "model") { + await Promise.all([ + dataBaseStore.fetchMeshComponents(item.id), + dataBaseStore.fetchUuidToFlatIndexDict(item.id), + ]) + console.log( + "[importWorkflowFromSnapshot] model components fetched", + item.id, + ) + } + + await dataStyleStore.applyDefaultStyle(item.id) + console.log( + "[importWorkflowFromSnapshot] dataStyle.applyDefaultStyle ok", + item.id, + ) + + ids.push(item.id) + } + hybridViewerStore.remoteRender() + console.log("[importWorkflowFromSnapshot] remoteRender called") + console.log("[importWorkflowFromSnapshot] done", { ids }) + return ids +} + +export { importFile, importWorkflow, importWorkflowFromSnapshot }