Skip to content
Open
Show file tree
Hide file tree
Changes from 23 commits
Commits
Show all changes
27 commits
Select commit Hold shift + click to select a range
aa86c91
feat(save_and_load): add save/load methods to all stores for project …
MaxNumerique Oct 27, 2025
5b2b4f3
Apply prepare changes
MaxNumerique Oct 28, 2025
f4ef491
cartStore becomes geodeStore
MaxNumerique Oct 28, 2025
5f3e0c6
Merge branch 'feat/save_and_load' of https://github.com/Geode-solutio…
MaxNumerique Oct 28, 2025
e6c54b5
Apply prepare changes
MaxNumerique Oct 28, 2025
01a9732
renames and move tests
MaxNumerique Oct 28, 2025
0b70454
Merge branch 'next' of https://github.com/Geode-solutions/OpenGeodeWe…
MaxNumerique Oct 28, 2025
d577721
persist upload folder
MaxNumerique Oct 29, 2025
df419ec
renamed app_store store to app
MaxNumerique Oct 29, 2025
bb9d02d
rm unused var
MaxNumerique Oct 29, 2025
a93225c
importStore & exportStore refacto
MaxNumerique Oct 30, 2025
af41f9d
Apply prepare changes
MaxNumerique Oct 30, 2025
ea63e2b
testes with import/export
MaxNumerique Oct 30, 2025
97072b5
Merge branch 'feat/save_and_load' of https://github.com/Geode-solutio…
MaxNumerique Oct 30, 2025
63464b7
Apply prepare changes
MaxNumerique Oct 30, 2025
98991cf
test
MaxNumerique Oct 30, 2025
6321c8f
Merge branch 'feat/save_and_load' of https://github.com/Geode-solutio…
MaxNumerique Oct 30, 2025
35c75dd
Apply prepare changes
MaxNumerique Oct 30, 2025
22a67fe
test
MaxNumerique Oct 30, 2025
caf5a39
Merge branch 'feat/save_and_load' of https://github.com/Geode-solutio…
MaxNumerique Oct 30, 2025
18fc885
Apply prepare changes
MaxNumerique Oct 30, 2025
d4a1cd6
exportStores/importStores and createTestingPinia
MaxNumerique Oct 31, 2025
8aec8b8
Apply prepare changes
MaxNumerique Oct 31, 2025
6a282e4
export working
MaxNumerique Oct 31, 2025
196be52
Merge branch 'feat/save_and_load' of https://github.com/Geode-solutio…
MaxNumerique Oct 31, 2025
fdf547e
nothing appear in camera
MaxNumerique Nov 1, 2025
c208b30
Apply prepare changes
MaxNumerique Nov 1, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
61 changes: 61 additions & 0 deletions composables/project_manager.js
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@JulienChampagnol que penses tu de cette proposition ?

Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
import back_schemas from "@geode/opengeodeweb-back/opengeodeweb_back_schemas.json"
import viewer_schemas from "@geode/opengeodeweb-viewer/opengeodeweb_viewer_schemas.json"

export function useProjectManager() {
const appStore = useAppStore()
const geode = useGeodeStore()

async function exportProject() {
geode.start_request()
try {
await useInfraStore().create_connection()
const snapshot = appStore.exportStores()

const schema = back_schemas.opengeodeweb_back.project.export_project
const url = `${geode.base_url}${schema.$id}`
const method = schema.methods[0]

const response = await fetch(url, {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pk ne pas utiliser api_fetch?

method,
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ snapshot }),
})
if (!response.ok) {
throw new Error(`Export failed: ${response.statusText}`)
}
const blob = await response.blob()
const filename = response.headers.get("new-file-name")
const urlObject = URL.createObjectURL(blob)
const a = document.createElement("a")
a.href = urlObject
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@JulienChampagnol tu n'avais pas utilisé un truc pour faire ça simplement ?

a.download = filename
a.click()
URL.revokeObjectURL(urlObject)
} finally {
geode.stop_request()
}
}

async function importProjectFile(file) {
geode.start_request()
try {
const snapshot = JSON.parse(await file.text())
await useInfraStore().create_connection()

await viewer_call({
schema: viewer_schemas.opengeodeweb_viewer.utils.import_project,
params: {},
})
await viewer_call({
schema: viewer_schemas.opengeodeweb_viewer.viewer.reset_visualization,
params: {},
})

await appStore.importStores(snapshot)
} finally {
geode.stop_request()
}
}

return { exportProject, importProjectFile }
}
File renamed without changes.
46 changes: 17 additions & 29 deletions stores/app_store.js → stores/app.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,81 +5,69 @@ 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) {
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`)
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
}

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,
}
})
17 changes: 17 additions & 0 deletions stores/data_base.js
Original file line number Diff line number Diff line change
Expand Up @@ -134,6 +134,21 @@ export const useDataBaseStore = defineStore("dataBase", () => {
return flat_indexes.filter((index) => index !== null)
}

function exportStores() {
return { db: JSON.parse(JSON.stringify(db)) }
}

async function importStores(snapshot) {
const entries = snapshot?.db || {}
const hybrid_store = useHybridViewerStore()
await hybrid_store.initHybridViewer()
hybrid_store.clear()
for (const [id, item] of Object.entries(entries)) {
await registerObject(id)
await addItem(id, item)
}
}

return {
db,
itemMetaDatas,
Expand All @@ -148,5 +163,7 @@ export const useDataBaseStore = defineStore("dataBase", () => {
getSurfacesUuids,
getBlocksUuids,
getFlatIndexes,
exportStores,
importStores,
}
})
10 changes: 10 additions & 0 deletions stores/data_style.js
Original file line number Diff line number Diff line change
Expand Up @@ -46,12 +46,22 @@ export const useDataStyleStore = defineStore("dataStyle", () => {
return modelStyleStore.modelMeshComponentVisibility(id, "Edge", null)
}

function exportStores() {
return { styles: dataStyleState.styles }
}

async function importStores(snapshot) {
dataStyleState.styles = snapshot?.styles || {}
}

return {
...dataStyleState,
addDataStyle,
setVisibility,
setModelEdgesVisibility,
modelEdgesVisibility,
exportStores,
importStores,
...meshStyleStore,
...modelStyleStore,
}
Expand Down
22 changes: 22 additions & 0 deletions stores/hybrid_viewer.js
Original file line number Diff line number Diff line change
Expand Up @@ -184,6 +184,25 @@ 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() {
return { zScale: zScale.value }
}

async function importStores(snapshot) {
const z_scale = snapshot?.zScale
if (z_scale != null) {
await setZScaling(z_scale)
}
}

return {
db,
genericRenderWindow,
Expand All @@ -195,5 +214,8 @@ export const useHybridViewerStore = defineStore("hybridViewer", () => {
resize,
setContainer,
zScale,
clear,
exportStores,
importStores,
}
})
23 changes: 23 additions & 0 deletions stores/treeview.js
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,27 @@ 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,
selection: selection.value,
}
}

async function importStores(snapshot) {
selection.value = snapshot?.selection || []
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
}

return {
items,
selection,
Expand All @@ -60,5 +81,7 @@ export const useTreeviewStore = defineStore("treeview", () => {
displayFileTree,
toggleTreeView,
setPanelWidth,
exportStores,
importStores,
}
})
85 changes: 85 additions & 0 deletions tests/unit/composables/ProjectManager.nuxt.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
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: {
project: {
export_project: { $id: "/project/export_project", methods: ["POST"] },
},
},
},
}))
vi.mock("@geode/opengeodeweb-viewer/opengeodeweb_viewer_schemas.json", () => ({
default: {
opengeodeweb_viewer: {
utils: { import_project: { rpc: "utils.import_project" } },
viewer: { reset_visualization: { rpc: "viewer.reset_visualization" } },
},
},
}))

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(() => {})
const fetchSpy = vi.spyOn(globalThis, "fetch").mockResolvedValue({
ok: true,
blob: async () => new Blob(["zipcontent"], { type: "application/zip" }),
headers: { get: () => 'attachment; filename="project_123.zip"' },
statusText: "OK",
})

const { exportProject } = useProjectManager()
await exportProject()

const app_store = useAppStore()
expect(app_store.exportStores).toHaveBeenCalled()
expect(fetchSpy).toHaveBeenCalledTimes(1)
expect(clickSpy).toHaveBeenCalled()
})

test("importProjectFile loads snapshot", async () => {
const { importProjectFile } = useProjectManager()

const file = { text: () => Promise.resolve('{"dataBase":{"db":{}}}') }
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()
})
})
Loading