@geode/opengeodeweb-front 10.12.0 → 10.13.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,113 @@
1
+ <script setup>
2
+ import ObjectTreeControls from "@ogw_front/components/Viewer/ObjectTree/Base/Controls.vue";
3
+ import ObjectTreeItemLabel from "@ogw_front/components/Viewer/ObjectTree/Base/ItemLabel.vue";
4
+ import { compareSelections } from "@ogw_front/utils/treeview";
5
+ import { useDataStore } from "@ogw_front/stores/data";
6
+ import { useDataStyleStore } from "@ogw_front/stores/data_style";
7
+ import { useHybridViewerStore } from "@ogw_front/stores/hybrid_viewer";
8
+ import { useTreeFilter } from "@ogw_front/composables/use_tree_filter";
9
+ import { useTreeviewStore } from "@ogw_front/stores/treeview";
10
+
11
+ const { id: viewId } = defineProps({ id: { type: String, required: true } });
12
+ const emit = defineEmits(["show-menu"]);
13
+
14
+ const dataStore = useDataStore();
15
+ const dataStyleStore = useDataStyleStore();
16
+ const hybridViewerStore = useHybridViewerStore();
17
+ const treeviewStore = useTreeviewStore();
18
+
19
+ const currentView = computed(() => treeviewStore.opened_views.find((view) => view.id === viewId));
20
+ const opened = computed({
21
+ get: () => currentView.value?.opened || [],
22
+ set: (val) => treeviewStore.setOpened(viewId, val),
23
+ });
24
+
25
+ const items = dataStore.refFormatedMeshComponents(toRef(() => viewId));
26
+ const mesh_components_selection = dataStyleStore.visibleMeshComponents(toRef(() => viewId));
27
+
28
+ const {
29
+ search,
30
+ sortType,
31
+ filterOptions,
32
+ processedItems,
33
+ availableFilterOptions,
34
+ toggleSort,
35
+ customFilter,
36
+ } = useTreeFilter(items);
37
+
38
+ async function onSelectionChange(current) {
39
+ const previous = mesh_components_selection.value;
40
+ const { added, removed } = compareSelections(current, previous);
41
+
42
+ if (added.length === 0 && removed.length === 0) {
43
+ return;
44
+ }
45
+
46
+ if (added.length > 0) {
47
+ await dataStyleStore.setModelComponentsVisibility(viewId, added, true);
48
+ }
49
+ if (removed.length > 0) {
50
+ await dataStyleStore.setModelComponentsVisibility(viewId, removed, false);
51
+ }
52
+ hybridViewerStore.remoteRender();
53
+ }
54
+ </script>
55
+
56
+ <template>
57
+ <div class="tree-view-container">
58
+ <ObjectTreeControls
59
+ v-model:search="search"
60
+ :sort-type="sortType"
61
+ :filter-options="filterOptions"
62
+ :available-filter-options="availableFilterOptions"
63
+ @toggle-sort="toggleSort"
64
+ />
65
+
66
+ <v-treeview
67
+ :selected="mesh_components_selection"
68
+ v-model:opened="opened"
69
+ :items="processedItems"
70
+ :search="search"
71
+ :custom-filter="customFilter"
72
+ class="transparent-treeview"
73
+ item-value="id"
74
+ select-strategy="independent"
75
+ selectable
76
+ @update:selected="onSelectionChange"
77
+ >
78
+ <template #title="{ item }">
79
+ <ObjectTreeItemLabel
80
+ :item="item"
81
+ show-tooltip
82
+ @contextmenu="
83
+ emit('show-menu', {
84
+ event: $event,
85
+ itemId: item.id,
86
+ context_type: 'model_component',
87
+ modelId: viewId,
88
+ })
89
+ "
90
+ />
91
+ </template>
92
+ </v-treeview>
93
+ </div>
94
+ </template>
95
+
96
+ <style scoped>
97
+ .transparent-treeview {
98
+ background-color: transparent;
99
+ margin: 4px 0;
100
+ }
101
+
102
+ :deep(.v-list-item) {
103
+ transition: background-color 0.2s ease;
104
+ }
105
+
106
+ :deep(.v-list-item--active > .v-list-item__overlay) {
107
+ opacity: 0 !important;
108
+ }
109
+
110
+ :deep(.v-list-item:hover > .v-list-item__overlay) {
111
+ opacity: 0.1 !important;
112
+ }
113
+ </style>
@@ -1,6 +1,6 @@
1
1
  <script setup>
2
2
  import ViewerContextMenu from "@ogw_front/components/Viewer/ContextMenu";
3
- import ViewerTreeObjectTree from "@ogw_front/components/Viewer/Tree/ObjectTree";
3
+ import ViewerObjectTreeLayout from "@ogw_front/components/Viewer/ObjectTree/Layout";
4
4
  import { useDataStore } from "@ogw_front/stores/data";
5
5
  import { useDataStyleStore } from "@ogw_front/stores/data_style";
6
6
  import { useMenuStore } from "@ogw_front/stores/menu";
@@ -44,7 +44,7 @@ defineExpose({ get_viewer_id });
44
44
  </script>
45
45
 
46
46
  <template>
47
- <ViewerTreeObjectTree @show-menu="(args) => emit('show-menu', args)" />
47
+ <ViewerObjectTreeLayout @show-menu="(args) => emit('show-menu', args)" />
48
48
  <ViewerContextMenu
49
49
  v-if="displayMenu"
50
50
  :id="menuStore.current_id"
@@ -0,0 +1,77 @@
1
+ function customFilter(value, searchQuery, item) {
2
+ if (!searchQuery) {
3
+ return true;
4
+ }
5
+ const query = searchQuery.toLowerCase();
6
+ const title = (item.raw.title || "").toLowerCase();
7
+ const idValue = String(value || "").toLowerCase();
8
+ return title.includes(query) || idValue.includes(query);
9
+ }
10
+
11
+ function sortAndFormatItems(items, sortType) {
12
+ const field = sortType === "name" ? "title" : "id";
13
+ return items.map((category) => {
14
+ const children = (category.children || []).toSorted((itemA, itemB) => {
15
+ const valueA = itemA[field] || "";
16
+ const valueB = itemB[field] || "";
17
+ return valueA.localeCompare(valueB, undefined, {
18
+ numeric: true,
19
+ sensitivity: "base",
20
+ });
21
+ });
22
+ return {
23
+ ...category,
24
+ id: category.id,
25
+ title: category.title || category.id,
26
+ children,
27
+ };
28
+ });
29
+ }
30
+
31
+ function useTreeFilter(rawItems, options = {}) {
32
+ const search = ref("");
33
+ const sortType = ref(options.defaultSort || "name");
34
+ const filterOptions = ref(options.defaultFilters || {});
35
+
36
+ const availableFilterOptions = computed(() =>
37
+ rawItems.value.map((category) => category.title || category.id),
38
+ );
39
+
40
+ watch(
41
+ availableFilterOptions,
42
+ (newOptions) => {
43
+ for (const option of newOptions) {
44
+ if (filterOptions.value[option] === undefined) {
45
+ filterOptions.value[option] = true;
46
+ }
47
+ }
48
+ },
49
+ { immediate: true },
50
+ );
51
+
52
+ const processedItems = computed(() =>
53
+ sortAndFormatItems(
54
+ rawItems.value.filter((category) => {
55
+ const key = category.title || category.id;
56
+ return filterOptions.value[key] !== false;
57
+ }),
58
+ sortType.value,
59
+ ),
60
+ );
61
+
62
+ function toggleSort() {
63
+ sortType.value = sortType.value === "name" ? "id" : "name";
64
+ }
65
+
66
+ return {
67
+ search,
68
+ sortType,
69
+ filterOptions,
70
+ processedItems,
71
+ availableFilterOptions,
72
+ toggleSort,
73
+ customFilter,
74
+ };
75
+ }
76
+
77
+ export { customFilter, useTreeFilter };
@@ -1,100 +1,153 @@
1
- export const useTreeviewStore = defineStore("treeview", () => {
2
- const PANEL_WIDTH = 300;
1
+ import { defineStore } from "pinia";
2
+
3
+ import { ref, toRaw, watch } from "vue";
4
+ import { database } from "@ogw_internal/database/database";
5
+ const PANEL_WIDTH = 300;
3
6
 
7
+ export const useTreeviewStore = defineStore("treeview", () => {
4
8
  const items = ref([]);
5
9
  const selection = ref([]);
6
- const components_selection = ref([]);
7
- const isAdditionnalTreeDisplayed = ref(false);
10
+ const opened_views = ref([
11
+ { type: "object", id: "main", title: "Objects", scrollTop: 0, opened: [] },
12
+ ]);
8
13
  const panelWidth = ref(PANEL_WIDTH);
9
- const model_id = ref("");
10
- const isTreeCollection = ref(false);
11
- const selectedTree = ref(undefined);
14
+ const additionalPanelWidth = ref(PANEL_WIDTH);
12
15
  const isImporting = ref(false);
13
16
  const pendingSelectionIds = ref([]);
17
+ const rowHeights = ref([]);
14
18
 
15
- // /** Functions **/
16
- function addItem(geode_object_type, name, id, viewer_type) {
17
- const child = { title: name, id, viewer_type };
18
-
19
- for (let i = 0; i < items.value.length; i += 1) {
20
- if (items.value[i].title === geode_object_type) {
21
- items.value[i].children.push(child);
22
- items.value[i].children.sort((element1, element2) =>
23
- element1.title.localeCompare(element2.title, undefined, {
24
- numeric: true,
25
- sensitivity: "base",
26
- }),
27
- );
28
- selection.value.push(id);
29
- return;
19
+ async function loadConfig() {
20
+ try {
21
+ const config = await database.treeview_config.get("main");
22
+ if (config?.opened_views) {
23
+ opened_views.value = config.opened_views;
30
24
  }
25
+ if (config?.panelWidth) {
26
+ panelWidth.value = config.panelWidth;
27
+ }
28
+ if (config?.additionalPanelWidth) {
29
+ additionalPanelWidth.value = config.additionalPanelWidth;
30
+ }
31
+ if (config?.selectionIds) {
32
+ selection.value = config.selectionIds;
33
+ }
34
+ if (config?.rowHeights) {
35
+ rowHeights.value = config.rowHeights;
36
+ }
37
+ } catch (error) {
38
+ console.error("Failed to load treeview config:", error);
31
39
  }
32
- items.value.push({ title: geode_object_type, children: [child] });
33
- items.value.sort((element1, element2) =>
34
- element1.title.localeCompare(element2.title, undefined, {
35
- numeric: true,
36
- sensitivity: "base",
37
- }),
38
- );
39
- selection.value.push(id);
40
40
  }
41
-
42
- function displayAdditionalTree(id) {
43
- isAdditionnalTreeDisplayed.value = true;
44
- model_id.value = id;
41
+ loadConfig();
42
+
43
+ watch(
44
+ [opened_views, panelWidth, additionalPanelWidth, selection, rowHeights],
45
+ () => {
46
+ database.treeview_config.put({
47
+ id: "main",
48
+ opened_views: toRaw(opened_views.value),
49
+ panelWidth: panelWidth.value,
50
+ additionalPanelWidth: additionalPanelWidth.value,
51
+ selectionIds: toRaw(selection.value),
52
+ rowHeights: toRaw(rowHeights.value),
53
+ });
54
+ },
55
+ { deep: true },
56
+ );
57
+
58
+ function closeView(index) {
59
+ if (index > 0) {
60
+ opened_views.value.splice(index, 1);
61
+ }
45
62
  }
46
63
 
47
- function displayFileTree() {
48
- isAdditionnalTreeDisplayed.value = false;
64
+ function addItem(geodeObjectType, name, id, viewer_type) {
65
+ const child = { title: name, id, viewer_type, geode_object_type: geodeObjectType };
66
+ let found = false;
67
+ for (const item of items.value) {
68
+ if (item.title === geodeObjectType) {
69
+ item.children.push(child);
70
+ const opt = { numeric: true, sensitivity: "base" };
71
+ item.children.sort((childA, childB) =>
72
+ childA.title.localeCompare(childB.title, undefined, opt),
73
+ );
74
+ found = true;
75
+ break;
76
+ }
77
+ }
78
+ if (!found) {
79
+ items.value.push({ id: geodeObjectType, title: geodeObjectType, children: [child] });
80
+ const sortOpt = { numeric: true, sensitivity: "base" };
81
+ items.value.sort((groupA, groupB) =>
82
+ groupA.title.localeCompare(groupB.title, undefined, sortOpt),
83
+ );
84
+ }
85
+ selection.value.push(id);
49
86
  }
50
87
 
51
- function toggleTreeView() {
52
- isTreeCollection.value = !isTreeCollection.value;
53
- console.log("Switched to", isTreeCollection.value ? "TreeCollection" : "TreeComponent");
88
+ function removeItem(id) {
89
+ for (let index = 0; index < items.value.length; index += 1) {
90
+ const group = items.value[index];
91
+ const childIndex = group.children.findIndex((child) => child.id === id);
92
+ if (childIndex !== -1) {
93
+ group.children.splice(childIndex, 1);
94
+ if (group.children.length === 0) {
95
+ items.value.splice(index, 1);
96
+ }
97
+ const selectionIndex = selection.value.indexOf(id);
98
+ if (selectionIndex !== -1) {
99
+ selection.value.splice(selectionIndex, 1);
100
+ }
101
+ return;
102
+ }
103
+ }
54
104
  }
55
105
 
56
- function setPanelWidth(width) {
57
- panelWidth.value = width;
106
+ function displayAdditionalTree(id, title, geodeObjectType) {
107
+ const index = opened_views.value.findIndex((view) => view.id === id);
108
+ if (index !== -1) {
109
+ return closeView(index);
110
+ }
111
+ additionalPanelWidth.value = panelWidth.value;
112
+ opened_views.value.push({
113
+ type: "component",
114
+ id,
115
+ title: title || id,
116
+ geode_object_type: geodeObjectType,
117
+ scrollTop: 0,
118
+ opened: [],
119
+ });
58
120
  }
59
121
 
60
- function exportStores() {
61
- const selectionIds = selection.value.map((store) => store.id);
62
- return {
63
- isAdditionnalTreeDisplayed: isAdditionnalTreeDisplayed.value,
64
- panelWidth: panelWidth.value,
65
- model_id: model_id.value,
66
- isTreeCollection: isTreeCollection.value,
67
- selectedTree: selectedTree.value,
68
- selectionIds,
69
- };
122
+ function moveView(fromIdx, toIdx) {
123
+ if (fromIdx !== 0 && toIdx !== 0) {
124
+ const [element] = opened_views.value.splice(fromIdx, 1);
125
+ opened_views.value.splice(toIdx, 0, element);
126
+ }
70
127
  }
71
128
 
72
129
  function importStores(snapshot) {
73
- isAdditionnalTreeDisplayed.value = snapshot?.isAdditionnalTreeDisplayed || false;
130
+ opened_views.value = snapshot?.opened_views || [
131
+ { type: "object", id: "main", title: "Objects", scrollTop: 0, opened: [] },
132
+ ];
74
133
  panelWidth.value = snapshot?.panelWidth || PANEL_WIDTH;
75
- model_id.value = snapshot?.model_id || "";
76
- isTreeCollection.value = snapshot?.isTreeCollection || false;
77
- selectedTree.value = snapshot?.selectedTree || undefined;
78
-
134
+ additionalPanelWidth.value = snapshot?.additionalPanelWidth || PANEL_WIDTH;
135
+ rowHeights.value = snapshot?.rowHeights || [];
79
136
  pendingSelectionIds.value =
80
- snapshot?.selectionIds || (snapshot?.selection || []).map((store) => store.id) || [];
137
+ snapshot?.selectionIds ||
138
+ (snapshot?.selection || []).map((selectionItem) => selectionItem.id || selectionItem) ||
139
+ [];
81
140
  }
82
141
 
83
142
  function finalizeImportSelection() {
84
- const ids = pendingSelectionIds.value || [];
85
143
  const rebuilt = [];
86
- if (ids.length === 0) {
87
- for (const group of items.value) {
88
- for (const child of group.children) {
89
- rebuilt.push(child);
90
- }
91
- }
92
- } else {
93
- for (const group of items.value) {
94
- for (const child of group.children) {
95
- if (ids.includes(child.id)) {
96
- rebuilt.push(child);
97
- }
144
+ for (const group of items.value) {
145
+ for (const child of group.children) {
146
+ if (
147
+ pendingSelectionIds.value.length === 0 ||
148
+ pendingSelectionIds.value.includes(child.id)
149
+ ) {
150
+ rebuilt.push(child.id || child);
98
151
  }
99
152
  }
100
153
  }
@@ -102,54 +155,78 @@ export const useTreeviewStore = defineStore("treeview", () => {
102
155
  pendingSelectionIds.value = [];
103
156
  }
104
157
 
105
- function removeItem(id) {
106
- for (let i = 0; i < items.value.length; i += 1) {
107
- const group = items.value[i];
108
- const childIndex = group.children.findIndex((child) => child.id === id);
158
+ function clear() {
159
+ items.value = [];
160
+ selection.value = [];
161
+ pendingSelectionIds.value = [];
162
+ opened_views.value = [
163
+ { type: "object", id: "main", title: "Objects", scrollTop: 0, opened: [] },
164
+ ];
165
+ }
109
166
 
110
- if (childIndex !== -1) {
111
- group.children.splice(childIndex, 1);
167
+ function displayFileTree() {
168
+ opened_views.value = [
169
+ { type: "object", id: "main", title: "Objects", scrollTop: 0, opened: [] },
170
+ ];
171
+ }
112
172
 
113
- if (group.children.length === 0) {
114
- items.value.splice(i, 1);
115
- }
173
+ function setPanelWidth(width) {
174
+ panelWidth.value = width;
175
+ }
116
176
 
117
- const selectionIndex = selection.value.findIndex((item) => item.id === id);
118
- if (selectionIndex !== -1) {
119
- selection.value.splice(selectionIndex, 1);
120
- }
177
+ function setAdditionalPanelWidth(width) {
178
+ additionalPanelWidth.value = width;
179
+ }
121
180
 
122
- return;
123
- }
181
+ function setScrollTop(viewId, scrollTop) {
182
+ const view = opened_views.value.find((openedView) => openedView.id === viewId);
183
+ if (view) {
184
+ view.scrollTop = scrollTop;
124
185
  }
125
186
  }
126
187
 
127
- function clear() {
128
- items.value = [];
129
- selection.value = [];
130
- components_selection.value = [];
131
- pendingSelectionIds.value = [];
132
- model_id.value = "";
133
- selectedTree.value = undefined;
188
+ function setOpened(viewId, opened) {
189
+ const view = opened_views.value.find((openedView) => openedView.id === viewId);
190
+ if (view) {
191
+ view.opened = opened;
192
+ }
193
+ }
194
+
195
+ function setRowHeights(heights) {
196
+ rowHeights.value = heights;
197
+ }
198
+
199
+ function exportStores() {
200
+ return {
201
+ opened_views: opened_views.value,
202
+ panelWidth: panelWidth.value,
203
+ additionalPanelWidth: additionalPanelWidth.value,
204
+ selectionIds: selection.value,
205
+ rowHeights: rowHeights.value,
206
+ };
134
207
  }
135
208
 
136
209
  return {
137
210
  items,
138
211
  selection,
139
- components_selection,
140
- isAdditionnalTreeDisplayed,
212
+ opened_views,
141
213
  panelWidth,
142
- model_id,
143
- selectedTree,
214
+ additionalPanelWidth,
144
215
  isImporting,
216
+ rowHeights,
145
217
  addItem,
146
218
  removeItem,
147
219
  displayAdditionalTree,
220
+ closeView,
221
+ moveView,
222
+ importStores,
148
223
  displayFileTree,
149
- toggleTreeView,
150
224
  setPanelWidth,
225
+ setAdditionalPanelWidth,
226
+ setScrollTop,
227
+ setOpened,
228
+ setRowHeights,
151
229
  exportStores,
152
- importStores,
153
230
  finalizeImportSelection,
154
231
  clear,
155
232
  };
@@ -13,6 +13,7 @@ export class BaseDatabase extends Dexie {
13
13
  [dataStyleTable.name]: dataStyleTable.schema,
14
14
  [modelComponentDataStyleTable.name]: modelComponentDataStyleTable.schema,
15
15
  [modelComponentsRelationTable.name]: modelComponentsRelationTable.schema,
16
+ treeview_config: "id",
16
17
  };
17
18
  }
18
19
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@geode/opengeodeweb-front",
3
- "version": "10.12.0",
3
+ "version": "10.13.0",
4
4
  "description": "OpenSource Vue/Nuxt/Pinia/Vuetify framework for web applications",
5
5
  "homepage": "https://github.com/Geode-solutions/OpenGeodeWeb-Front",
6
6
  "bugs": {
@@ -1,49 +0,0 @@
1
- <script setup>
2
- import { useDataStore } from "@ogw_front/stores/data";
3
- import { useTreeviewStore } from "@ogw_front/stores/treeview";
4
-
5
- const dataStore = useDataStore();
6
- const treeviewStore = useTreeviewStore();
7
-
8
- const selectedTree = computed(() => treeviewStore.selectedTree);
9
-
10
- function goBackToFileTree() {
11
- treeviewStore.displayFileTree();
12
- }
13
-
14
- const metaDatas = computed(() => dataStore.refItem(treeviewStore.model_id));
15
- </script>
16
-
17
- <template>
18
- <v-breadcrumbs class="mb-n10 breadcrumb-container">
19
- <div class="d-flex align-center gap-2 ml-2 mt-2 mb-1">
20
- <template v-if="treeviewStore.isAdditionnalTreeDisplayed">
21
- <v-btn icon variant="text" size="medium" @click.stop="goBackToFileTree">
22
- <v-icon size="large">mdi-file-tree</v-icon>
23
- </v-btn>
24
- <span class="text-h5 font-weight-bold">/</span>
25
- <v-icon size="large">
26
- {{ selectedTree && selectedTree.icon ? selectedTree.icon : "mdi-shape-outline" }}
27
- </v-icon>
28
- <span class="text-subtitle-1 font-weight-regular align-center mt-1">
29
- Model Explorer ({{ metaDatas.name }})
30
- </span>
31
- </template>
32
-
33
- <div v-else class="d-flex align-center gap-2">
34
- <v-icon size="large">mdi-file-tree</v-icon>
35
- <span class="text-subtitle-1 font-weight-regular align-center mt-1"> Objects </span>
36
- </div>
37
- </div>
38
- </v-breadcrumbs>
39
- </template>
40
-
41
- <style scoped>
42
- .breadcrumb-container {
43
- position: relative;
44
- z-index: 10;
45
- max-width: 100%;
46
- overflow: hidden;
47
- white-space: nowrap;
48
- }
49
- </style>