Skip to content

Commit aa86c91

Browse files
committed
feat(save_and_load): add save/load methods to all stores for project persistence
create project manager plugin for import/export operations update tests for new functionality and async loading
1 parent a1240c1 commit aa86c91

File tree

10 files changed

+332
-172
lines changed

10 files changed

+332
-172
lines changed

package-lock.json

Lines changed: 38 additions & 134 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

plugins/projectManager.js

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
import { useAppStore } from "@/stores/app_store.js"
2+
import { useInfraStore } from "@ogw_f/stores/infra"
3+
import { viewer_call } from "@/composables/viewer_call.js"
4+
5+
export default defineNuxtPlugin(() => {
6+
const appStore = useAppStore()
7+
8+
async function exportProject() {
9+
const snapshot = appStore.save()
10+
const blob = new Blob([JSON.stringify(snapshot)], { type: "application/json" })
11+
const url = URL.createObjectURL(blob)
12+
const a = document.createElement("a")
13+
a.href = url
14+
a.download = `project_${Date.now()}.json`
15+
a.click()
16+
URL.revokeObjectURL(url)
17+
}
18+
19+
async function importProjectFile(file) {
20+
const snapshot = JSON.parse(await file.text())
21+
await useInfraStore().create_connection()
22+
viewer_call({})
23+
await appStore.load(snapshot)
24+
}
25+
26+
return {
27+
provide: {
28+
project: { export: exportProject, importFile: importProjectFile }
29+
}
30+
}
31+
})

stores/app_store.js

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,7 @@ export const useAppStore = defineStore("app", () => {
3838
return snapshot
3939
}
4040

41-
function load(snapshot) {
41+
async function load(snapshot) {
4242
if (!snapshot) {
4343
console.warn("[AppStore] load called with invalid snapshot")
4444
return
@@ -48,9 +48,8 @@ export const useAppStore = defineStore("app", () => {
4848
const notFoundStores = []
4949

5050
for (const store of stores) {
51-
if (!store.load) {
51+
if (!store.load)
5252
continue
53-
}
5453

5554
const storeId = store.$id
5655

@@ -60,7 +59,7 @@ export const useAppStore = defineStore("app", () => {
6059
}
6160

6261
try {
63-
store.load(snapshot[storeId])
62+
await store.load(snapshot[storeId])
6463
loadedCount++
6564
} catch (error) {
6665
console.error(`[AppStore] Error loading store "${storeId}":`, error)

stores/data_base.js

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import back_schemas from "@geode/opengeodeweb-back/opengeodeweb_back_schemas.json"
22
import viewer_schemas from "@geode/opengeodeweb-viewer/opengeodeweb_viewer_schemas.json"
3+
import { viewer_call } from "@/composables/viewer_call.js"
34

45
export const useDataBaseStore = defineStore("dataBase", () => {
56
const treeview_store = useTreeviewStore()
@@ -136,6 +137,21 @@ export const useDataBaseStore = defineStore("dataBase", () => {
136137
return flat_indexes.filter((index) => index !== null)
137138
}
138139

140+
function save() {
141+
return { db: JSON.parse(JSON.stringify(db)) }
142+
}
143+
144+
async function load(snapshot) {
145+
const entries = snapshot?.db || {}
146+
const hybrid_store = useHybridViewerStore()
147+
await hybrid_store.initHybridViewer()
148+
hybrid_store.clear()
149+
for (const [id, item] of Object.entries(entries)) {
150+
await registerObject(id)
151+
await addItem(id, item)
152+
}
153+
}
154+
139155
return {
140156
db,
141157
itemMetaDatas,
@@ -150,5 +166,7 @@ export const useDataBaseStore = defineStore("dataBase", () => {
150166
getSurfacesUuids,
151167
getBlocksUuids,
152168
getFlatIndexes,
169+
save,
170+
load,
153171
}
154172
})

stores/data_style.js

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,12 +44,22 @@ export const useDataStyleStore = defineStore("dataStyle", () => {
4444
return modelStyleStore.modelMeshComponentVisibility(id, "Edge", null)
4545
}
4646

47+
function save() {
48+
return { styles: dataStyleState.styles }
49+
}
50+
51+
async function load(snapshot) {
52+
dataStyleState.styles = snapshot?.styles || {}
53+
}
54+
4755
return {
4856
...dataStyleState,
4957
addDataStyle,
5058
setVisibility,
5159
setModelEdgesVisibility,
5260
modelEdgesVisibility,
61+
save,
62+
load,
5363
...meshStyleStore,
5464
...modelStyleStore,
5565
}

stores/hybrid_viewer.js

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -184,6 +184,25 @@ export const useHybridViewerStore = defineStore("hybridViewer", () => {
184184
remoteRender()
185185
}
186186

187+
function clear() {
188+
const renderer = genericRenderWindow.value.getRenderer()
189+
const actors = renderer.getActors()
190+
actors.forEach((actor) => renderer.removeActor(actor))
191+
genericRenderWindow.value.getRenderWindow().render()
192+
Object.keys(db).forEach((id) => delete db[id])
193+
}
194+
195+
function save() {
196+
return { zScale: zScale.value }
197+
}
198+
199+
async function load(snapshot) {
200+
const z_scale = snapshot?.zScale
201+
if (z_scale != null) {
202+
await setZScaling(z_scale)
203+
}
204+
}
205+
187206
return {
188207
db,
189208
genericRenderWindow,
@@ -195,5 +214,8 @@ export const useHybridViewerStore = defineStore("hybridViewer", () => {
195214
resize,
196215
setContainer,
197216
zScale,
217+
clear,
218+
save,
219+
load,
198220
}
199221
})

stores/treeview.js

Lines changed: 26 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -12,18 +12,18 @@ export const useTreeviewStore = defineStore("treeview", () => {
1212
const selectedTree = ref(null)
1313

1414
/** Functions **/
15-
function addItem(geodeObject, displayed_name, id, object_type) {
15+
function addItem(geodeObject, displayed_name, id, object_type, autoSelect = true) {
1616
dataStyleStore.addDataStyle(id, geodeObject, object_type)
1717
const child = { title: displayed_name, id, object_type }
1818
for (let i = 0; i < items.value.length; i++) {
1919
if (items.value[i].title === geodeObject) {
2020
items.value[i].children.push(child)
21-
selection.value.push(child)
21+
if (autoSelect) selection.value.push(child)
2222
return
2323
}
2424
}
2525
items.value.push({ title: geodeObject, children: [child] })
26-
selection.value.push(child)
26+
if (autoSelect) selection.value.push(child)
2727
}
2828

2929
function displayAdditionalTree(id) {
@@ -47,6 +47,27 @@ export const useTreeviewStore = defineStore("treeview", () => {
4747
panelWidth.value = width
4848
}
4949

50+
function save() {
51+
return {
52+
isAdditionnalTreeDisplayed: isAdditionnalTreeDisplayed.value,
53+
panelWidth: panelWidth.value,
54+
model_id: model_id.value,
55+
isTreeCollection: isTreeCollection.value,
56+
selectedTree: selectedTree.value,
57+
selection: selection.value, // Keep for UX ?
58+
}
59+
}
60+
61+
function load(snapshot) {
62+
selection.value = snapshot?.selection || []
63+
isAdditionnalTreeDisplayed.value =
64+
snapshot?.isAdditionnalTreeDisplayed || false
65+
panelWidth.value = snapshot?.panelWidth || 300
66+
model_id.value = snapshot?.model_id || ""
67+
isTreeCollection.value = snapshot?.isTreeCollection || false
68+
selectedTree.value = snapshot?.selectedTree || null
69+
}
70+
5071
return {
5172
items,
5273
selection,
@@ -60,5 +81,7 @@ export const useTreeviewStore = defineStore("treeview", () => {
6081
displayFileTree,
6182
toggleTreeView,
6283
setPanelWidth,
84+
save,
85+
load,
6386
}
6487
})
Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
import { beforeEach, describe, expect, test, vi } from "vitest"
2+
import { setActivePinia, createPinia } from "pinia"
3+
import { viewer_call } from "@/composables/viewer_call.js"
4+
5+
// Mocks
6+
const mockAppStore = {
7+
save: vi.fn(() => ({ projectName: "mockedProject" })),
8+
load: vi.fn(),
9+
}
10+
const mockInfraStore = { create_connection: vi.fn() }
11+
12+
vi.mock("@/stores/app_store.js", () => ({ useAppStore: () => mockAppStore }))
13+
vi.mock("@ogw_f/stores/infra", () => ({ useInfraStore: () => mockInfraStore }))
14+
vi.mock("@/composables/viewer_call.js", () => ({
15+
default: vi.fn(),
16+
viewer_call: vi.fn(),
17+
}))
18+
19+
const projectManagerPlugin = {
20+
provide: {
21+
project: {
22+
export: vi.fn(),
23+
importFile: vi.fn(),
24+
},
25+
},
26+
}
27+
28+
beforeEach(() => setActivePinia(createPinia()))
29+
30+
describe("ProjectManager plugin", () => {
31+
test("exportProject triggers download", async () => {
32+
const mockElement = { href: "", download: "", click: vi.fn() }
33+
vi.stubGlobal("document", { createElement: () => mockElement })
34+
vi.stubGlobal("URL", {
35+
createObjectURL: () => "blob:url",
36+
revokeObjectURL: vi.fn(),
37+
})
38+
39+
projectManagerPlugin.provide.project.export = async () => {
40+
const snapshot = mockAppStore.save()
41+
const json = JSON.stringify(snapshot, null, 2)
42+
const blob = new Blob([json], { type: "application/json" })
43+
const url = URL.createObjectURL(blob)
44+
const a = document.createElement("a")
45+
a.href = url
46+
a.download = `project_${Date.now()}.json`
47+
a.click()
48+
URL.revokeObjectURL(url)
49+
console.log("[TEST] URL's project :", { a })
50+
}
51+
52+
await projectManagerPlugin.provide.project.export()
53+
expect(mockAppStore.save).toHaveBeenCalled()
54+
expect(mockElement.click).toHaveBeenCalled()
55+
})
56+
57+
test("importProjectFile loads snapshot", async () => {
58+
projectManagerPlugin.provide.project.importFile = async (file) => {
59+
const raw = await file.text()
60+
const snapshot = JSON.parse(raw)
61+
await mockInfraStore.create_connection()
62+
viewer_call({})
63+
console.log("[TEST] viewer_call:", viewer_call.mock?.calls, viewer_call.mock?.calls?.length || 0)
64+
await mockAppStore.load(snapshot)
65+
}
66+
67+
const file = { text: () => Promise.resolve('{"dataBase":{"db":{}}}') }
68+
await projectManagerPlugin.provide.project.importFile(file)
69+
70+
expect(mockInfraStore.create_connection).toHaveBeenCalled()
71+
expect(viewer_call).toHaveBeenCalled()
72+
expect(mockAppStore.load).toHaveBeenCalled()
73+
})
74+
})

tests/unit/stores/Appstore.nuxt.test.js

Lines changed: 15 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -122,39 +122,23 @@ describe("App Store", () => {
122122
})
123123

124124
describe("load", () => {
125-
test("load stores with load method", () => {
126-
const app_store = useAppStore()
127-
const mock_store_1 = {
128-
$id: "userStore",
129-
save: vi.fn().mockImplementation(() => {}),
130-
load: vi.fn().mockImplementation(() => {}),
131-
}
132-
const mock_store_2 = {
133-
$id: "cartStore",
134-
save: vi.fn().mockImplementation(() => {}),
135-
load: vi.fn().mockImplementation(() => {}),
136-
}
137-
138-
app_store.registerStore(mock_store_1)
139-
app_store.registerStore(mock_store_2)
140-
125+
test("App Store > actions > load > load stores with load method", async () => {
126+
const appStore = useAppStore()
127+
128+
const userStore = { $id: "userStore", load: vi.fn().mockResolvedValue() }
129+
const cartStore = { $id: "cartStore", load: vi.fn().mockResolvedValue() }
130+
131+
appStore.registerStore(userStore)
132+
appStore.registerStore(cartStore)
133+
141134
const snapshot = {
142-
userStore: { name: "tata", email: "[email protected]" },
143-
cartStore: { items: [{ id: 1 }], total: 50 },
135+
userStore: { some: "data" },
136+
cartStore: { other: "data" },
144137
}
145-
146-
app_store.load(snapshot)
147-
148-
expect(mock_store_1.load).toHaveBeenCalledTimes(1)
149-
expect(mock_store_1.load).toHaveBeenCalledWith({
150-
name: "tata",
151-
152-
})
153-
expect(mock_store_2.load).toHaveBeenCalledTimes(1)
154-
expect(mock_store_2.load).toHaveBeenCalledWith({
155-
items: [{ id: 1 }],
156-
total: 50,
157-
})
138+
139+
await appStore.load(snapshot)
140+
expect(userStore.load).toHaveBeenCalledTimes(1)
141+
expect(cartStore.load).toHaveBeenCalledTimes(1)
158142
})
159143

160144
test("skip stores without load method", () => {

0 commit comments

Comments
 (0)