@geode/opengeodeweb-front 10.38.0-rc.2 → 10.38.0-rc.4

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.
@@ -1,6 +1,5 @@
1
1
  <script setup lang="ts">
2
2
  import Loading from "@ogw_front/components/Loading.vue";
3
- import Recaptcha from "@ogw_front/components/Recaptcha.vue";
4
3
  import { Status } from "@ogw_front/utils/status";
5
4
  import { appMode } from "@ogw_shared/app_mode";
6
5
  import { getCloudUrlParam } from "@ogw_front/utils/cloud";
@@ -8,22 +7,28 @@ import { useInfraStore } from "@ogw_front/stores/infra";
8
7
 
9
8
  interface Props {
10
9
  appName: string;
11
- email?: string;
10
+ // Firebase ID token for the cloud launch; required in cloud mode.
11
+ authToken?: string;
12
12
  isUserAuthenticated?: boolean;
13
13
  logo?: string;
14
14
  }
15
15
 
16
- const { appName, email = undefined, isUserAuthenticated = false, logo = "" } = defineProps<Props>();
16
+ const {
17
+ appName,
18
+ authToken = undefined,
19
+ isUserAuthenticated = false,
20
+ logo = "",
21
+ } = defineProps<Props>();
17
22
 
18
23
  const infraStore = useInfraStore();
19
24
  // In cloud mode, wait for the user to authenticate and load the app, unless a `?cloud_url=` link
20
25
  // Points at an already running service: that one is connected straight away, without authentication.
21
26
  if (infraStore.app_mode !== appMode.CLOUD || getCloudUrlParam() !== undefined) {
22
- infraStore.create_backend(email);
27
+ infraStore.create_backend(authToken);
23
28
  }
24
29
 
25
30
  function cloudCreateBackend(): Promise<void> {
26
- return infraStore.create_backend(email);
31
+ return infraStore.create_backend(authToken);
27
32
  }
28
33
  </script>
29
34
 
@@ -284,9 +284,7 @@ function onVerticalResizeStart(event: MouseEvent, index: number): void {
284
284
  >
285
285
  <ModelTree
286
286
  data-testid="modelComponentsObjectTree"
287
- :id="view.modelId || view.id"
288
- :view-id="view.id"
289
- :view-type="view.viewType"
287
+ :id="view.id"
290
288
  @show-menu="emit('show-menu', $event)"
291
289
  />
292
290
  </ViewerObjectTreeBox>
@@ -8,7 +8,6 @@ import CommonTreeView from "@ogw_front/components/Viewer/ObjectTree/Base/CommonT
8
8
  import type { DisplayItem } from "@ogw_front/composables/virtual_tree";
9
9
  import ObjectTreeControls from "@ogw_front/components/Viewer/ObjectTree/Base/Controls.vue";
10
10
  import ObjectTreeItemLabel from "@ogw_front/components/Viewer/ObjectTree/Base/ItemLabel.vue";
11
- import { consola } from "consola";
12
11
  import { useDataStore } from "@ogw_front/stores/data";
13
12
  import { useDataStyleStore } from "@ogw_front/stores/data_style";
14
13
  import { useHoverhighlight } from "@ogw_front/composables/hover_highlight";
@@ -110,31 +109,6 @@ function isModel(item: TreeGroupItem): boolean {
110
109
  );
111
110
  }
112
111
 
113
- const hasCollectionsMap = reactive<Record<string, boolean>>({});
114
-
115
- watch(
116
- () => treeviewStore.items,
117
- async (newItems) => {
118
- const models = newItems
119
- .flatMap((group) => group.children || [])
120
- .filter((item) => isModel(item));
121
- const fetchPromises = models.map(async (model) => {
122
- if (hasCollectionsMap[model.id] === undefined) {
123
- hasCollectionsMap[model.id] = false;
124
- try {
125
- const hasCollections = await dataStore.hasCollectionComponents(model.id);
126
- hasCollectionsMap[model.id] = hasCollections;
127
- } catch (error) {
128
- consola.error("Failed to check collections", error);
129
- }
130
- }
131
- });
132
-
133
- await Promise.all(fetchPromises);
134
- },
135
- { immediate: true, deep: true },
136
- );
137
-
138
112
  function handleHoverEnter({
139
113
  item,
140
114
  immediate = false,
@@ -245,29 +219,11 @@ function expandAll(): void {
245
219
  item.id as string,
246
220
  item.title as string | undefined,
247
221
  item.geode_object_type as string,
248
- 'model_components',
249
222
  )
250
223
  "
251
224
  >
252
225
  <v-icon size="18">mdi-magnify-expand</v-icon>
253
226
  </v-btn>
254
- <v-btn
255
- v-if="isModel(item as unknown as TreeGroupItem) && hasCollectionsMap[item.id as string]"
256
- data-testid="expandModelCollectionsButton"
257
- icon="mdi-format-list-group"
258
- size="medium"
259
- class="ml-2"
260
- variant="text"
261
- v-tooltip="'Model\'s collections'"
262
- @click.stop="
263
- treeviewStore.displayAdditionalTree(
264
- item.id as string,
265
- item.title as string | undefined,
266
- item.geode_object_type as string,
267
- 'model_collections',
268
- )
269
- "
270
- />
271
227
  </template>
272
228
  </template>
273
229
  </CommonTreeView>
@@ -3,24 +3,19 @@ import { sortAndFormatItems, useTreeFilter } from "@ogw_front/composables/tree_f
3
3
  import CommonTreeView from "@ogw_front/components/Viewer/ObjectTree/Base/CommonTreeView.vue";
4
4
  import type { DisplayItem } from "@ogw_front/composables/virtual_tree";
5
5
  import FetchingData from "@ogw_front/components/FetchingData.vue";
6
- import { MESH_COMPONENT_TYPES } from "@ogw_front/utils/default_styles";
7
6
  import type { ModelComponentMenuPayload } from "@ogw_front/utils/treeview";
8
7
  import ObjectTreeControls from "@ogw_front/components/Viewer/ObjectTree/Base/Controls.vue";
9
8
  import ObjectTreeItemLabel from "@ogw_front/components/Viewer/ObjectTree/Base/ItemLabel.vue";
10
9
  import { useHoverhighlight } from "@ogw_front/composables/hover_highlight";
11
10
  import { useHybridViewerStore } from "@ogw_front/stores/hybrid_viewer";
12
- import { useModelCollections } from "@ogw_front/composables/model_collections";
13
- import { useModelComponents } from "@ogw_front/composables/model_components";
11
+ import { useModelTree } from "@ogw_front/composables/model_tree";
14
12
  import { useTreeviewStore } from "@ogw_front/stores/treeview";
15
13
 
16
14
  interface Props {
17
15
  id: string;
18
- viewId?: string;
19
- viewType?: string;
20
16
  }
21
17
 
22
- const { id, viewId = undefined, viewType = "model_components" } = defineProps<Props>();
23
- const actualViewId = viewId || id;
18
+ const { id } = defineProps<Props>();
24
19
 
25
20
  interface CollectionTreeItem {
26
21
  raw?: CollectionTreeItem;
@@ -41,29 +36,20 @@ const emit = defineEmits<Emits>();
41
36
 
42
37
  const treeviewStore = useTreeviewStore();
43
38
 
44
- const isCollections = computed(() => viewType === "model_collections");
45
-
46
- const composable = isCollections.value ? useModelCollections(id) : useModelComponents(id);
47
39
  const {
48
- items: rawItems,
40
+ isLoading,
49
41
  localCategories,
42
+ cache,
43
+ collectionTypes,
50
44
  selection: visibleComponents,
51
45
  updateVisibility,
52
- } = composable;
53
-
54
- const cache = computed(
55
- () =>
56
- ("componentsCache" in composable ? composable.componentsCache.value : undefined) ??
57
- ("collectionsCache" in composable ? composable.collectionsCache.value : undefined),
58
- );
46
+ } = useModelTree(id);
59
47
 
60
- const currentView = computed(() =>
61
- treeviewStore.opened_views.find((view) => view.id === actualViewId),
62
- );
48
+ const currentView = computed(() => treeviewStore.opened_views.find((view) => view.id === id));
63
49
 
64
50
  const opened = computed<string[]>({
65
51
  get: () => currentView.value?.opened || [],
66
- set: (val) => treeviewStore.setOpened(actualViewId, val),
52
+ set: (val) => treeviewStore.setOpened(id, val),
67
53
  });
68
54
 
69
55
  const {
@@ -84,25 +70,37 @@ function onUpdateSelection(newSelection: string[]): void {
84
70
 
85
71
  const visibleSelection = computed<string[]>(() => applySearchFilter(visibleComponents.value, []));
86
72
 
73
+ function matchesSearch(item: CollectionTreeItem, query: string): boolean {
74
+ return (
75
+ (item.title ?? "").toLowerCase().includes(query) ||
76
+ item.id.toLowerCase().includes(query) ||
77
+ (item.children ?? []).some((child) => matchesSearch(child, query))
78
+ );
79
+ }
80
+
81
+ function searchGroups(query: string): CollectionTreeItem[] {
82
+ const groups = (cache.value ?? {}) as Record<string, CollectionTreeItem[]>;
83
+ const result: CollectionTreeItem[] = [];
84
+ for (const [type, components] of Object.entries(groups)) {
85
+ const matches = components.filter((component) => matchesSearch(component, query));
86
+ if (matches.length > 0) {
87
+ result.push({
88
+ id: type,
89
+ title: `${type}s (${matches.length})`,
90
+ children: sortAndFormatItems(matches, sortType.value) as unknown as CollectionTreeItem[],
91
+ });
92
+ }
93
+ }
94
+ return result;
95
+ }
96
+
87
97
  const itemsForTreeView = computed<CollectionTreeItem[]>(() => {
88
- if (search.value && cache.value) {
98
+ if (search.value) {
89
99
  const query = search.value.toLowerCase();
90
- const result: CollectionTreeItem[] = [];
91
- for (const type of Object.keys(cache.value)) {
92
- const matches = (cache.value[type] ?? []).filter(
93
- (component: { title: string; id: string }) =>
94
- component.title.toLowerCase().includes(query) ||
95
- component.id.toLowerCase().includes(query),
96
- );
97
- if (matches.length > 0) {
98
- result.push({
99
- id: type,
100
- title: `${type}s (${matches.length})`,
101
- children: sortAndFormatItems(matches, sortType.value) as unknown as CollectionTreeItem[],
102
- });
103
- }
104
- }
105
- return result;
100
+ return sortAndFormatItems(
101
+ searchGroups(query),
102
+ sortType.value,
103
+ ) as unknown as CollectionTreeItem[];
106
104
  }
107
105
 
108
106
  const result: CollectionTreeItem[] = [];
@@ -127,9 +125,13 @@ function extractComponentIds(node: CollectionTreeItem): string[] {
127
125
  return [node.id];
128
126
  }
129
127
 
128
+ function isCollectionNode(item: CollectionTreeItem): boolean {
129
+ return collectionTypes.value.has(item.category ?? item.id);
130
+ }
131
+
130
132
  function showContextMenu(event: MouseEvent, item: CollectionTreeItem): void {
131
133
  const actualItem = item.raw || item;
132
- if (isCollections.value && !MESH_COMPONENT_TYPES.includes(actualItem.category ?? "")) {
134
+ if (isCollectionNode(actualItem)) {
133
135
  emit("show-menu", {
134
136
  event,
135
137
  itemId: id,
@@ -217,9 +219,10 @@ function getLeafViewerIds(item: CollectionTreeItem): number[] {
217
219
  @expand-all="expandAll"
218
220
  />
219
221
 
220
- <FetchingData v-if="rawItems === undefined" :size="48" :width="4" text="" />
222
+ <FetchingData v-if="isLoading" :size="48" :width="4" text="" />
221
223
 
222
224
  <CommonTreeView
225
+ v-else
223
226
  :selected="visibleSelection"
224
227
  v-model:opened="opened"
225
228
  v-model:active="treeviewStore.activeItems"
@@ -233,7 +236,7 @@ function getLeafViewerIds(item: CollectionTreeItem): number[] {
233
236
  class="transparent-treeview virtual-tree-height"
234
237
  @update:selected="(val) => onUpdateSelection(val as string[])"
235
238
  @click:item="onUpdateSelection([$event.id as string, ...visibleComponents])"
236
- @update:scroll-top="treeviewStore.setScrollTop(actualViewId, $event)"
239
+ @update:scroll-top="treeviewStore.setScrollTop(id, $event)"
237
240
  @hover:enter="({ item }) => handleHoverEnter({ item: item as unknown as CollectionTreeItem })"
238
241
  @hover:leave="handleHoverLeave"
239
242
  @contextmenu="showContextMenu($event.event, $event.item as unknown as CollectionTreeItem)"
@@ -0,0 +1,82 @@
1
+ import type {
2
+ CollectionComponent,
3
+ CollectionComponentGroup,
4
+ } from "@ogw_front/stores/data_helpers/collections";
5
+ import type {
6
+ FormattedComponent,
7
+ FormattedComponentGroup,
8
+ } from "@ogw_front/stores/data_helpers/mesh";
9
+ import { compareSelections } from "@ogw_front/utils/treeview";
10
+ import { useDataStore } from "@ogw_front/stores/data";
11
+ import { useDataStyleStore } from "@ogw_front/stores/data_style";
12
+ import { useHybridViewerStore } from "@ogw_front/stores/hybrid_viewer";
13
+
14
+ type ModelTreeCategory = FormattedComponentGroup | CollectionComponentGroup;
15
+ type ModelTreeCache = Record<string, (FormattedComponent | CollectionComponent)[]>;
16
+
17
+ function useModelVisibility(modelId: string): {
18
+ selection: typeof selection;
19
+ updateVisibility: typeof updateVisibility;
20
+ } {
21
+ const dataStyleStore = useDataStyleStore();
22
+ const hybridViewerStore = useHybridViewerStore();
23
+ const selection = dataStyleStore.visibleMeshComponents(modelId);
24
+
25
+ async function updateVisibility(current: readonly string[]): Promise<void> {
26
+ const { added, removed } = compareSelections([...current], selection.value);
27
+ if (added.length > 0) {
28
+ await dataStyleStore.setModelComponentsVisibility(modelId, added, true);
29
+ }
30
+ if (removed.length > 0) {
31
+ await dataStyleStore.setModelComponentsVisibility(modelId, removed, false);
32
+ }
33
+ if (added.length > 0 || removed.length > 0) {
34
+ await hybridViewerStore.remoteRender();
35
+ }
36
+ }
37
+
38
+ return { selection, updateVisibility };
39
+ }
40
+
41
+ export function useModelTree(modelId: string): {
42
+ isLoading: ComputedRef<boolean>;
43
+ localCategories: Ref<ModelTreeCategory[]>;
44
+ cache: Ref<ModelTreeCache | undefined>;
45
+ collectionTypes: Ref<Set<string>>;
46
+ } & ReturnType<typeof useModelVisibility> {
47
+ const dataStore = useDataStore();
48
+
49
+ const meshGroups = dataStore.refFormatedMeshComponents(modelId);
50
+ const collectionGroups = dataStore.refFormatedCollectionComponents(modelId);
51
+ const cache = ref<ModelTreeCache | undefined>(undefined);
52
+ const localCategories = ref<ModelTreeCategory[]>([]);
53
+ const collectionTypes = ref<Set<string>>(new Set());
54
+
55
+ // The mesh cache also groups collection components by their type; the collections cache overrides those keys with the real collection -> mesh components hierarchy.
56
+ watch(
57
+ [meshGroups, collectionGroups],
58
+ async ([mesh, collections]) => {
59
+ if (!mesh || !collections) {
60
+ return;
61
+ }
62
+ const [meshByType, collectionsByType] = await Promise.all([
63
+ dataStore.fetchAllMeshComponents(modelId),
64
+ dataStore.fetchAllCollectionComponents(modelId),
65
+ ]);
66
+ cache.value = markRaw({ ...meshByType, ...collectionsByType });
67
+ localCategories.value = [...mesh, ...collections];
68
+ collectionTypes.value = new Set(Object.keys(collectionsByType));
69
+ },
70
+ { immediate: true },
71
+ );
72
+
73
+ const isLoading = computed(() => cache.value === undefined);
74
+
75
+ return {
76
+ isLoading,
77
+ localCategories,
78
+ cache,
79
+ collectionTypes,
80
+ ...useModelVisibility(modelId),
81
+ };
82
+ }
package/app/stores/api.ts CHANGED
@@ -1,4 +1,4 @@
1
- import type { RequestHandlers } from "@ogw_shared/utils/types";
1
+ import type { ParamsOf, RequestHandlers, ResponseOf } from "@ogw_shared/utils/types";
2
2
  import { api_fetch } from "@ogw_internal/utils/api_fetch";
3
3
  import { consola } from "consola";
4
4
 
@@ -8,7 +8,7 @@ interface ApiSchema {
8
8
  [key: string]: unknown;
9
9
  }
10
10
 
11
- type ApiCallbacks = RequestHandlers & {
11
+ type ApiCallbacks<Response = unknown> = RequestHandlers<Response> & {
12
12
  skip_feedback_error?: boolean;
13
13
  };
14
14
 
@@ -26,21 +26,21 @@ export const useAPIStore = defineStore("api", () => {
26
26
  request_counter.value -= 1;
27
27
  }
28
28
 
29
- // `TResult` is asserted, not verified: the response is only checked against `schema` at runtime
30
- async function request<TResult = unknown>(
29
+ // The response type comes from the schema's generated `response` type (see the Cloud API typed schemas), asserted at the API boundary below, not verified
30
+ async function request<Schema extends ApiSchema>(
31
31
  {
32
32
  schema,
33
- params = {},
33
+ params,
34
34
  headers = {},
35
- }: { schema: ApiSchema; params?: Record<string, unknown>; headers?: Record<string, string> },
36
- callbacks: ApiCallbacks = {},
37
- ): Promise<TResult> {
35
+ }: { schema: Schema; params?: ParamsOf<Schema>; headers?: Record<string, string> },
36
+ callbacks: ApiCallbacks<ResponseOf<Schema>> = {},
37
+ ): Promise<ResponseOf<Schema>> {
38
38
  consola.info("[API] Request:", schema.$id);
39
39
  const start = Date.now();
40
40
 
41
41
  const result = await api_fetch(
42
42
  { $id: schema.$id, base_url: base_url.value, start_request, stop_request },
43
- { schema, params, headers },
43
+ { schema, params: params ?? {}, headers },
44
44
  {
45
45
  ...callbacks,
46
46
  response_function: async (response: unknown) => {
@@ -52,13 +52,14 @@ export const useAPIStore = defineStore("api", () => {
52
52
  "s",
53
53
  );
54
54
  if (callbacks.response_function) {
55
- await callbacks.response_function(response);
55
+ // oxlint-disable-next-line no-unsafe-type-assertion -- trusted API boundary; see comment above.
56
+ await callbacks.response_function(response as ResponseOf<Schema>);
56
57
  }
57
58
  },
58
59
  },
59
60
  );
60
61
  // oxlint-disable-next-line no-unsafe-type-assertion -- trusted API boundary; see comment above.
61
- return result as TResult;
62
+ return result as ResponseOf<Schema>;
62
63
  }
63
64
  return {
64
65
  base_url,
@@ -1,9 +1,8 @@
1
1
  import { clearCloudUrlParam, getCloudUrlParam } from "@ogw_front/utils/cloud";
2
- import type { RunCloudResponse } from "@geode/cloud-api/types";
3
2
  import { Status } from "@ogw_front/utils/status";
4
3
  import { api_fetch } from "@ogw_internal/utils/api_fetch";
5
4
  import back_schemas from "@geode/opengeodeweb-back/opengeodeweb_back_typed_schemas.js";
6
- import cloud_api_schemas from "@geode/cloud-api/cloud_api_schemas.json";
5
+ import cloud_api_schemas from "@geode/cloud-api/cloud_api_typed_schemas.js";
7
6
  import { setAppBaseUrl } from "@ogw_shared/scripts";
8
7
  import { useAPIStore } from "@ogw_front/stores/api";
9
8
  import { useFeedbackStore } from "@ogw_front/stores/feedback";
@@ -17,10 +16,13 @@ export const useCloudStore = defineStore("cloud", {
17
16
  actions: {
18
17
  // Reuses the running service given by `?cloud_url=` when present, otherwise launches a new one.
19
18
  // The param is single use: it is cleared whatever the outcome, so a retry launches a new service.
20
- async start(email: string) {
19
+ async start(authToken?: string) {
21
20
  const existing_host = getCloudUrlParam();
22
21
  if (existing_host === undefined) {
23
- await this.launch(email);
22
+ if (authToken === undefined) {
23
+ throw new Error("Launching a cloud service requires an authenticated user");
24
+ }
25
+ await this.launch(authToken);
24
26
  return;
25
27
  }
26
28
  try {
@@ -29,29 +31,23 @@ export const useCloudStore = defineStore("cloud", {
29
31
  clearCloudUrlParam();
30
32
  }
31
33
  },
32
- async launch(email: string) {
34
+ // `authToken` is the user's Firebase ID token, checked by the Cloud API
35
+ async launch(authToken: string) {
33
36
  this.status = Status.CONNECTING;
34
37
  const { PROJECT, BRANCH } = useRuntimeConfig().public;
35
- const params = { email, project: PROJECT, branch: BRANCH };
38
+ const params = { project: PROJECT, branch: BRANCH };
39
+ const headers = { Authorization: `Bearer ${authToken}` };
36
40
  const feedbackStore = useFeedbackStore();
37
41
  const APIStore = useAPIStore();
38
- const result = await APIStore.request<RunCloudResponse>(
39
- { schema: run_cloud_schema, params },
42
+ const result = await APIStore.request(
43
+ { schema: run_cloud_schema, params, headers },
40
44
  {
41
45
  request_error_function: () => {
42
46
  feedbackStore.$patch({ server_error: true });
43
47
  this.status = Status.NOT_CONNECTED;
44
48
  },
45
- response_function: async (response: unknown) => {
46
- if (
47
- typeof response !== "object" ||
48
- response === null ||
49
- !("url" in response) ||
50
- typeof response.url !== "string"
51
- ) {
52
- return;
53
- }
54
- await this.on_connected(response.url);
49
+ response_function: async ({ url }) => {
50
+ await this.on_connected(url);
55
51
  },
56
52
  response_error_function: () => {
57
53
  feedbackStore.$patch({ server_error: true });
@@ -132,6 +132,7 @@ export const useDataStyleStore = defineStore("dataStyle", () => {
132
132
  ...component_style_promises,
133
133
  ...model_component_type_style_promises,
134
134
  ]);
135
+ await dataStyleState.loadFromDatabase();
135
136
  }
136
137
 
137
138
  async function applyAllStylesFromState(): Promise<void> {
@@ -44,7 +44,7 @@ export const useInfraStore = defineStore("infra", {
44
44
  (microservice: Microservice) => microservice.$id !== microserviceId,
45
45
  );
46
46
  },
47
- async create_backend(email?: string) {
47
+ async create_backend(authToken?: string) {
48
48
  if (this.status === Status.CREATED) {
49
49
  return undefined;
50
50
  }
@@ -56,7 +56,7 @@ export const useInfraStore = defineStore("infra", {
56
56
  if (this.app_mode === appMode.CLOUD) {
57
57
  const cloudStore = useCloudStore();
58
58
  try {
59
- await cloudStore.start(email ?? "");
59
+ await cloudStore.start(authToken);
60
60
  } catch (error) {
61
61
  // Back to the "Load the app" button so the user is not stuck on the loading screen.
62
62
  this.status = Status.NOT_CREATED;
@@ -6,7 +6,6 @@ import { defineStore } from "pinia";
6
6
 
7
7
  import type { Table } from "dexie";
8
8
  import { ref, watch } from "vue";
9
- import { compareSelections } from "@ogw_front/utils/treeview";
10
9
  import { database } from "@ogw_internal/database/database";
11
10
 
12
11
  const PANEL_WIDTH = 300;
@@ -17,8 +16,6 @@ interface OpenedView {
17
16
  title: string;
18
17
  scrollTop: number;
19
18
  opened: string[];
20
- modelId?: string;
21
- viewType?: string;
22
19
  geode_object_type?: string;
23
20
  }
24
21
 
@@ -144,18 +141,6 @@ export const useTreeviewStore = defineStore("treeview", () => {
144
141
  opened_views.value = opened_views.value.filter((view: ReadonlyOpenedView) => view.id !== id);
145
142
  }
146
143
 
147
- watch(selection, (current: readonly string[], previous: readonly string[]) => {
148
- const { removed } = compareSelections(current, previous);
149
- for (const id of removed) {
150
- const index = opened_views.value.findIndex(
151
- (view: ReadonlyOpenedView) => view.type === "component" && view.id === id,
152
- );
153
- if (index !== -1) {
154
- closeView(id);
155
- }
156
- }
157
- });
158
-
159
144
  function toggleView(id: string): void {
160
145
  const index = opened_views.value.findIndex((view: ReadonlyOpenedView) => view.id === id);
161
146
  if (index !== -1) {
@@ -229,20 +214,16 @@ export const useTreeviewStore = defineStore("treeview", () => {
229
214
  id: string,
230
215
  title: string | undefined,
231
216
  geodeObjectType: string,
232
- viewType = "model_components",
233
217
  ): void {
234
- const viewId = `${id}_${viewType}`;
235
- const index = opened_views.value.findIndex((view: ReadonlyOpenedView) => view.id === viewId);
218
+ const index = opened_views.value.findIndex((view: ReadonlyOpenedView) => view.id === id);
236
219
  if (index !== -1) {
237
- closeView(viewId);
220
+ closeView(id);
238
221
  return;
239
222
  }
240
223
  additionalPanelWidth.value = panelWidth.value;
241
224
  opened_views.value.push({
242
225
  type: "component",
243
- id: viewId,
244
- modelId: id,
245
- viewType,
226
+ id,
246
227
  title: title ?? id,
247
228
  geode_object_type: geodeObjectType,
248
229
  scrollTop: 0,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@geode/opengeodeweb-front",
3
- "version": "10.38.0-rc.2",
3
+ "version": "10.38.0-rc.4",
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,89 +0,0 @@
1
- <script setup lang="ts">
2
- import { useInfraStore } from "@ogw_front/stores/infra";
3
-
4
- interface Props {
5
- buttonLabel?: string;
6
- buttonColor?: string;
7
- color?: string;
8
- }
9
-
10
- const {
11
- buttonLabel = "Load the app",
12
- buttonColor = "white",
13
- color = undefined,
14
- } = defineProps<Props>();
15
-
16
- const name = ref("");
17
- const email = ref("");
18
- const launch = ref(false);
19
- const valid = ref(false);
20
- const emailRules = [
21
- (value: string): boolean | string => {
22
- if (value) {
23
- return true;
24
- }
25
- return "E-mail is required.";
26
- },
27
- (value: string): boolean | string => {
28
- if (/.+@.+\..+/u.test(value)) {
29
- return true;
30
- }
31
- return "E-mail must be valid.";
32
- },
33
- ];
34
-
35
- function submit(): Promise<void> {
36
- const infraStore = useInfraStore();
37
- return infraStore.create_backend(email.value);
38
- }
39
- </script>
40
-
41
- <template>
42
- <VRow align="center" justify="center" style="display: none">
43
- <VCol cols="4">
44
- <VForm v-model="valid">
45
- <VContainer>
46
- <VRow>
47
- <VCol>
48
- <VTextField v-model="name" label="Name" required />
49
- </VCol>
50
- </VRow>
51
- <VRow>
52
- <VCol>
53
- <VTextField v-model="email" :rules="emailRules" label="E-mail" required />
54
- </VCol>
55
- </VRow>
56
- <VRow>
57
- <VCol>
58
- <VCheckbox label="Load the app" v-model="launch" />
59
- </VCol>
60
- </VRow>
61
- </VContainer>
62
- </VForm>
63
- </VCol>
64
- </VRow>
65
- <VRow align="center" justify="center">
66
- <VCol cols="auto" class="d-flex justify-center align-center">
67
- <VBtn class="load-btn" :text="buttonLabel" :color="color || buttonColor" @click="submit" />
68
- </VCol>
69
- </VRow>
70
- </template>
71
-
72
- <style scoped>
73
- .load-btn {
74
- padding: 0 40px !important;
75
- height: 50px !important;
76
- border-radius: 8px;
77
- text-transform: none !important;
78
- font-weight: 600;
79
- letter-spacing: 0.5px;
80
- transition:
81
- transform 0.2s ease,
82
- box-shadow 0.2s ease !important;
83
- }
84
-
85
- .load-btn:hover {
86
- transform: translateY(-2px);
87
- box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15) !important;
88
- }
89
- </style>
@@ -1,91 +0,0 @@
1
- import type {
2
- CollectionComponent,
3
- CollectionComponentGroup,
4
- } from "@ogw_front/stores/data_helpers/collections";
5
- import { compareSelections } from "@ogw_front/utils/treeview";
6
- import { useDataStore } from "@ogw_front/stores/data";
7
- import { useDataStyleStore } from "@ogw_front/stores/data_style";
8
- import { useHybridViewerStore } from "@ogw_front/stores/hybrid_viewer";
9
-
10
- // The `watch` callback receives the freshly-fetched collection groups purely for reading (matching against and copying into `localCategories`, never mutated in place), so it's typed with its own fully-readonly mirror of `CollectionComponentGroup` rather than that (intentionally mutable, see `existing.title` below) store type directly.
11
- interface ReadonlyCollectionComponentGroup {
12
- readonly id: string;
13
- readonly title: string;
14
- readonly children: readonly CollectionComponent[];
15
- }
16
-
17
- export function useModelCollections(viewId: string): {
18
- items: typeof items;
19
- collectionsCache: typeof collectionsCache;
20
- localCategories: typeof localCategories;
21
- selection: typeof selection;
22
- updateVisibility: typeof updateVisibility;
23
- } {
24
- const dataStore = useDataStore();
25
- const dataStyleStore = useDataStyleStore();
26
- const hybridViewerStore = useHybridViewerStore();
27
-
28
- const items = dataStore.refFormatedCollectionComponents(viewId);
29
- const collectionsCache = ref<Record<string, CollectionComponent[]> | undefined>(undefined);
30
- const localCategories = ref<CollectionComponentGroup[]>([]);
31
-
32
- onMounted(async () => {
33
- const data = await dataStore.fetchAllCollectionComponents(viewId);
34
- collectionsCache.value = markRaw(data);
35
- });
36
-
37
- watch(
38
- items,
39
- async (newItems: readonly ReadonlyCollectionComponentGroup[] | undefined) => {
40
- if (!newItems) {
41
- localCategories.value = [];
42
- return;
43
- }
44
-
45
- const data = await dataStore.fetchAllCollectionComponents(viewId);
46
- collectionsCache.value = markRaw(data);
47
-
48
- localCategories.value = newItems.map((newCategory: ReadonlyCollectionComponentGroup) => {
49
- const existing = localCategories.value.find(
50
- (category: ReadonlyCollectionComponentGroup) => category.id === newCategory.id,
51
- );
52
- if (existing) {
53
- existing.title = newCategory.title || newCategory.id;
54
- return existing;
55
- }
56
- return reactive({
57
- ...newCategory,
58
- title: newCategory.title || newCategory.id,
59
- });
60
- });
61
- },
62
- { immediate: true },
63
- );
64
-
65
- const selection = dataStyleStore.visibleMeshComponents(viewId);
66
-
67
- async function updateVisibility(current: readonly string[]): Promise<void> {
68
- const previous = selection.value;
69
- const { added, removed } = compareSelections([...current], previous);
70
-
71
- if (added.length === 0 && removed.length === 0) {
72
- return;
73
- }
74
-
75
- if (added.length > 0) {
76
- await dataStyleStore.setModelComponentsVisibility(viewId, added, true);
77
- }
78
- if (removed.length > 0) {
79
- await dataStyleStore.setModelComponentsVisibility(viewId, removed, false);
80
- }
81
- await hybridViewerStore.remoteRender();
82
- }
83
-
84
- return {
85
- items,
86
- collectionsCache,
87
- localCategories,
88
- selection,
89
- updateVisibility,
90
- };
91
- }
@@ -1,91 +0,0 @@
1
- import type {
2
- FormattedComponent,
3
- FormattedComponentGroup,
4
- } from "@ogw_front/stores/data_helpers/mesh";
5
- import { compareSelections } from "@ogw_front/utils/treeview";
6
- import { useDataStore } from "@ogw_front/stores/data";
7
- import { useDataStyleStore } from "@ogw_front/stores/data_style";
8
- import { useHybridViewerStore } from "@ogw_front/stores/hybrid_viewer";
9
-
10
- // The `watch` callback receives the freshly-fetched component groups purely for reading (matching against and copying into `localCategories`, never mutated in place), so it's typed with its own fully-readonly mirror of `FormattedComponentGroup` rather than that (intentionally mutable, see `existing.title` below) store type directly.
11
- interface ReadonlyFormattedComponentGroup {
12
- readonly id: string;
13
- readonly title: string;
14
- readonly children: readonly FormattedComponent[];
15
- }
16
-
17
- export function useModelComponents(viewId: string): {
18
- items: typeof items;
19
- componentsCache: typeof componentsCache;
20
- localCategories: typeof localCategories;
21
- selection: typeof selection;
22
- updateVisibility: typeof updateVisibility;
23
- } {
24
- const dataStore = useDataStore();
25
- const dataStyleStore = useDataStyleStore();
26
- const hybridViewerStore = useHybridViewerStore();
27
-
28
- const items = dataStore.refFormatedMeshComponents(viewId);
29
- const componentsCache = ref<Record<string, FormattedComponent[]> | undefined>(undefined);
30
- const localCategories = ref<FormattedComponentGroup[]>([]);
31
-
32
- onMounted(async () => {
33
- const data = await dataStore.fetchAllMeshComponents(viewId);
34
- componentsCache.value = markRaw(data);
35
- });
36
-
37
- watch(
38
- items,
39
- async (newItems: readonly ReadonlyFormattedComponentGroup[] | undefined) => {
40
- if (!newItems) {
41
- localCategories.value = [];
42
- return;
43
- }
44
-
45
- const data = await dataStore.fetchAllMeshComponents(viewId);
46
- componentsCache.value = markRaw(data);
47
-
48
- localCategories.value = newItems.map((newCategory: ReadonlyFormattedComponentGroup) => {
49
- const existing = localCategories.value.find(
50
- (category: ReadonlyFormattedComponentGroup) => category.id === newCategory.id,
51
- );
52
- if (existing) {
53
- existing.title = newCategory.title || newCategory.id;
54
- return existing;
55
- }
56
- return reactive({
57
- ...newCategory,
58
- title: newCategory.title || newCategory.id,
59
- });
60
- });
61
- },
62
- { immediate: true },
63
- );
64
-
65
- const selection = dataStyleStore.visibleMeshComponents(viewId);
66
-
67
- async function updateVisibility(current: readonly string[]): Promise<void> {
68
- const previous = selection.value;
69
- const { added, removed } = compareSelections([...current], previous);
70
-
71
- if (added.length === 0 && removed.length === 0) {
72
- return;
73
- }
74
-
75
- if (added.length > 0) {
76
- await dataStyleStore.setModelComponentsVisibility(viewId, added, true);
77
- }
78
- if (removed.length > 0) {
79
- await dataStyleStore.setModelComponentsVisibility(viewId, removed, false);
80
- }
81
- await hybridViewerStore.remoteRender();
82
- }
83
-
84
- return {
85
- items,
86
- componentsCache,
87
- localCategories,
88
- selection,
89
- updateVisibility,
90
- };
91
- }