@geode/opengeodeweb-front 10.40.0-rc.1 → 10.40.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.
@@ -1,7 +1,7 @@
1
1
  <script setup lang="ts">
2
- import { onMounted, onUnmounted, ref } from "vue";
3
2
  import DragAndDropInline from "./DragAndDropInternal/DragAndDropInline.vue";
4
3
  import DragAndDropOverlay from "./DragAndDropInternal/DragAndDropOverlay.vue";
4
+ import type { UploadFile } from "@ogw_front/utils/upload_path";
5
5
 
6
6
  interface DragAndDropTexts {
7
7
  idle: string;
@@ -18,6 +18,7 @@ interface Props {
18
18
  inline?: boolean;
19
19
  showOverlay?: boolean;
20
20
  texts?: DragAndDropTexts;
21
+ directory?: boolean;
21
22
  }
22
23
 
23
24
  const {
@@ -33,10 +34,20 @@ const {
33
34
  drop: "Drop files here",
34
35
  loading: "Loading...",
35
36
  },
37
+ directory = false,
38
+ // oxlint-disable-next-line vue/max-props
36
39
  } = defineProps<Props>();
37
40
 
41
+ const displayed_texts = computed<DragAndDropTexts>(() =>
42
+ directory
43
+ ? { ...texts, idle: "Click or drag and drop a folder", drop: "Drop the folder here" }
44
+ : texts,
45
+ );
46
+
38
47
  const emit = defineEmits<{
39
- "files-selected": [files: File[]];
48
+ "files-selected": [files: UploadFile[]];
49
+ "folders-ignored": [];
50
+ "folder-read-error": [message: string];
40
51
  }>();
41
52
 
42
53
  const isDragging = ref(false);
@@ -115,10 +126,70 @@ function onDragOver(event: DragEvent): void {
115
126
  }
116
127
  }
117
128
 
129
+ async function readAllEntries(reader: FileSystemDirectoryReader): Promise<FileSystemEntry[]> {
130
+ // oxlint-disable-next-line promise/avoid-new
131
+ const batch = await new Promise<FileSystemEntry[]>((resolve, reject) => {
132
+ reader.readEntries(resolve, reject);
133
+ });
134
+ if (batch.length === 0) {
135
+ return [];
136
+ }
137
+ return [...batch, ...(await readAllEntries(reader))];
138
+ }
139
+
140
+ async function filesFromEntry(entry: FileSystemEntry, path: string): Promise<UploadFile[]> {
141
+ if (entry.isFile) {
142
+ // oxlint-disable-next-line promise/avoid-new
143
+ const file = await new Promise<File>((resolve, reject) => {
144
+ // oxlint-disable-next-line no-unsafe-type-assertion -- isFile guarantees a FileSystemFileEntry
145
+ (entry as FileSystemFileEntry).file(resolve, reject);
146
+ });
147
+ return [Object.assign(file, { relativePath: path })];
148
+ }
149
+ // oxlint-disable-next-line no-unsafe-type-assertion -- not a file, so a FileSystemDirectoryEntry
150
+ const children = await readAllEntries((entry as FileSystemDirectoryEntry).createReader());
151
+ const nested = await Promise.all(
152
+ children.map((child) => filesFromEntry(child, `${path}/${child.name}`)),
153
+ );
154
+ return nested.flat();
155
+ }
156
+
157
+ async function emitDroppedEntries(entries: FileSystemEntry[]): Promise<void> {
158
+ try {
159
+ const groups = await Promise.all(
160
+ entries.map(async (entry) => {
161
+ const files = await filesFromEntry(entry, entry.name);
162
+ // Files inside a dropped folder are all needed: never filtered out.
163
+ return entry.isDirectory ? files : files.filter((file) => isFileAccepted(file, accept));
164
+ }),
165
+ );
166
+ const files = groups.flat();
167
+ if (files.length > 0) {
168
+ emit("files-selected", files);
169
+ }
170
+ } catch (error) {
171
+ const message = error instanceof Error ? error.message : String(error);
172
+ emit("folder-read-error", message);
173
+ }
174
+ }
175
+
118
176
  function onDrop(event: DragEvent): void {
119
177
  event.preventDefault();
120
178
  dragCounter.value = 0;
121
179
  isDragging.value = false;
180
+ // Entries must be read synchronously: the DataTransfer is emptied once the event returns.
181
+ const entries = [...(event.dataTransfer?.items ?? [])]
182
+ .map((item) => item.webkitGetAsEntry?.())
183
+ .filter((entry): entry is FileSystemEntry => entry !== null && entry !== undefined);
184
+ if (entries.some((entry) => entry.isDirectory)) {
185
+ if (directory) {
186
+ void emitDroppedEntries(entries);
187
+ return;
188
+ }
189
+ emit("folders-ignored");
190
+ void emitDroppedEntries(entries.filter((entry) => entry.isFile));
191
+ return;
192
+ }
122
193
  const files = [...(event.dataTransfer?.files ?? [])].filter((file) =>
123
194
  isFileAccepted(file, accept),
124
195
  );
@@ -181,7 +252,7 @@ defineExpose({ triggerFileDialog });
181
252
  v-if="inline"
182
253
  :is-dragging
183
254
  :loading
184
- :texts
255
+ :texts="displayed_texts"
185
256
  :accept
186
257
  :show-extensions
187
258
  @click="triggerFileDialog"
@@ -193,13 +264,21 @@ defineExpose({ triggerFileDialog });
193
264
  :show-overlay
194
265
  :fullscreen
195
266
  :loading
196
- :texts
267
+ :texts="displayed_texts"
197
268
  :multiple
198
269
  :accept
199
270
  :show-extensions
200
271
  />
201
272
 
202
- <input ref="fileInput" type="file" class="d-none" :multiple :accept @change="handleFileSelect" />
273
+ <input
274
+ ref="fileInput"
275
+ type="file"
276
+ class="d-none"
277
+ :multiple
278
+ :accept
279
+ :webkitdirectory="directory"
280
+ @change="handleFileSelect"
281
+ />
203
282
  </template>
204
283
 
205
284
  <style>
@@ -8,7 +8,7 @@ import { useBackStore } from "@ogw_front/stores/back";
8
8
  const schema = schemas.opengeodeweb_back.allowed_files;
9
9
 
10
10
  interface Emits {
11
- update_values: [values: { files: unknown[]; autoUpload: boolean }];
11
+ update_values: [values: { files: unknown[]; autoUpload: boolean; time_series?: string[] }];
12
12
  increment_step: [];
13
13
  decrement_step: [];
14
14
  }
@@ -20,13 +20,21 @@ interface Props {
20
20
  files?: File[];
21
21
  autoUpload?: boolean;
22
22
  showOverlay?: boolean;
23
+ timeSeries?: boolean;
23
24
  }
24
25
 
25
- const { multiple, files = [], autoUpload = true, showOverlay = true } = defineProps<Props>();
26
+ const {
27
+ multiple,
28
+ files = [],
29
+ autoUpload = true,
30
+ showOverlay = true,
31
+ timeSeries = false,
32
+ } = defineProps<Props>();
26
33
 
27
34
  const internal_files = ref<File[]>(files);
28
35
  const internal_auto_upload = ref<boolean>(autoUpload);
29
36
  const accept = ref<string>("");
37
+ const time_series = ref<string[]>([]);
30
38
  const loading = ref<boolean>(false);
31
39
 
32
40
  watch(
@@ -47,7 +55,12 @@ const toggle_loading = useToggle(loading);
47
55
 
48
56
  function files_uploaded_event(value: unknown[]): void {
49
57
  if (value.length > 0) {
50
- emit("update_values", { files: value, autoUpload: false });
58
+ emit(
59
+ "update_values",
60
+ timeSeries
61
+ ? { files: value, autoUpload: false, time_series: time_series.value }
62
+ : { files: value, autoUpload: false },
63
+ );
51
64
  emit("increment_step");
52
65
  }
53
66
  }
@@ -56,7 +69,10 @@ async function get_allowed_files(): Promise<void> {
56
69
  toggle_loading();
57
70
  const backStore = useBackStore();
58
71
  const response = await backStore.request({ schema });
59
- accept.value = response.extensions.map((extension) => `.${extension}`).join(",");
72
+ time_series.value = timeSeries ? response.time_series_extensions : [];
73
+ accept.value = [...response.data_extensions, ...time_series.value]
74
+ .map((extension) => `.${extension}`)
75
+ .join(",");
60
76
  toggle_loading();
61
77
  }
62
78
 
@@ -1,5 +1,7 @@
1
1
  <script setup lang="ts">
2
+ import { foldersIgnoredWarning } from "@ogw_front/utils/upload_path";
2
3
  import { useBackStore } from "@ogw_front/stores/back";
4
+ import { useFeedbackStore } from "@ogw_front/stores/feedback";
3
5
 
4
6
  import CsvPreviewer from "@ogw_front/components/csv-preview/CsvPreviewer.vue";
5
7
  import DragAndDrop from "@ogw_front/components/DragAndDrop.vue";
@@ -72,6 +74,10 @@ async function onCsvConfirm(result: unknown): Promise<void> {
72
74
  csv_dialog.value = false;
73
75
  }
74
76
 
77
+ function warnFoldersIgnored(): void {
78
+ useFeedbackStore().add_warning(foldersIgnoredWarning);
79
+ }
80
+
75
81
  function processSelectedFiles(selected_files: UploadFile[]): void {
76
82
  if (multiple) {
77
83
  internal_files.value = [...internal_files.value, ...selected_files];
@@ -154,6 +160,7 @@ watch(
154
160
  :accept
155
161
  :inline="false"
156
162
  @files-selected="processSelectedFiles"
163
+ @folders-ignored="warnFoldersIgnored"
157
164
  />
158
165
  </template>
159
166
  <DragAndDrop
@@ -166,6 +173,7 @@ watch(
166
173
  :inline="!internal_files.length"
167
174
  :show-overlay="showOverlay"
168
175
  @files-selected="processSelectedFiles"
176
+ @folders-ignored="warnFoldersIgnored"
169
177
  />
170
178
 
171
179
  <v-card-text v-if="internal_files.length" class="mt-6 pa-0">
@@ -1,19 +1,23 @@
1
1
  <script setup lang="ts">
2
2
  import schemas from "@geode/opengeodeweb-back/opengeodeweb_back_typed_schemas.js";
3
3
 
4
+ import { matchExpectedFiles, uploadPath } from "@ogw_front/utils/upload_path";
5
+ import DragAndDrop from "@ogw_front/components/DragAndDrop.vue";
4
6
  import FetchingData from "@ogw_front/components/FetchingData.vue";
5
7
  import FileUploader from "@ogw_front/components/FileUploader.vue";
6
8
  import { useBackStore } from "@ogw_front/stores/back";
9
+ import { useFeedbackStore } from "@ogw_front/stores/feedback";
7
10
 
8
11
  // Files carry extra app-specific bookkeeping fields once picked up here.
9
- type UploadFile = File & { isConfigured?: boolean };
12
+ type UploadFile = File & { isConfigured?: boolean; relativePath?: string };
10
13
  interface FilePlan {
11
14
  has_missing_files: boolean;
12
15
  mandatory_files: string[];
13
16
  additional_files: string[];
14
17
  }
15
18
 
16
- const schema = schemas.opengeodeweb_back.missing_files;
19
+ const schema = schemas.opengeodeweb_back.data_missing_files;
20
+ const time_series_schema = schemas.opengeodeweb_back.time_series_missing_files;
17
21
 
18
22
  interface Emits {
19
23
  update_values: [value: { additional_files: UploadFile[] }];
@@ -28,9 +32,16 @@ interface Props {
28
32
  geodeObjectType: string;
29
33
  filenames: string[];
30
34
  files?: UploadFile[];
35
+ timeSeries?: boolean;
31
36
  }
32
37
 
33
- const { multiple, geodeObjectType, filenames, files = [] } = defineProps<Props>();
38
+ const {
39
+ multiple,
40
+ geodeObjectType,
41
+ filenames,
42
+ files = [],
43
+ timeSeries = false,
44
+ } = defineProps<Props>();
34
45
 
35
46
  const accept = ref<string>("");
36
47
  const loading = ref<boolean>(false);
@@ -38,6 +49,7 @@ const has_missing_files = ref<boolean>(false);
38
49
  const mandatory_files = ref<string[]>([]);
39
50
  const additional_files = ref<string[]>([]);
40
51
  const toggle_loading = useToggle(loading);
52
+ const uploading = ref<boolean>(false);
41
53
 
42
54
  function files_uploaded_event(value: UploadFile[]): void {
43
55
  emit("update_values", { additional_files: value });
@@ -63,6 +75,9 @@ async function missing_files(): Promise<void> {
63
75
  additional_files: [],
64
76
  });
65
77
  }
78
+ if (timeSeries) {
79
+ return backStore.request({ schema: time_series_schema, params: { filename } });
80
+ }
66
81
  const params = { geode_object_type: geodeObjectType, filename };
67
82
  return backStore.request({ schema, params });
68
83
  });
@@ -88,12 +103,61 @@ async function missing_files(): Promise<void> {
88
103
  toggle_loading();
89
104
  }
90
105
 
106
+ // Time series: uploads the missing files found in the selected folder, then the files they
107
+ // Reference in turn, until nothing is missing. The folder layout does not matter.
108
+ async function upload_missing_from(
109
+ folder_files: UploadFile[],
110
+ uploaded: UploadFile[],
111
+ ): Promise<void> {
112
+ const expected = [...mandatory_files.value, ...additional_files.value];
113
+ const match = matchExpectedFiles(
114
+ folder_files.map((file) => uploadPath(file)),
115
+ expected,
116
+ );
117
+ if (match.status === "ambiguous") {
118
+ useFeedbackStore().add_warning(
119
+ `Several matching files for ${match.path}, select a more specific folder`,
120
+ );
121
+ return;
122
+ }
123
+ const found = expected.flatMap((expected_path, index) => {
124
+ const file = folder_files[match.indices[index] ?? -1];
125
+ return file && !uploaded.includes(file)
126
+ ? [Object.assign(file, { relativePath: expected_path })]
127
+ : [];
128
+ });
129
+ if (found.length === 0) {
130
+ useFeedbackStore().add_warning(`The selected folder does not contain: ${expected.join(", ")}`);
131
+ return;
132
+ }
133
+ const backStore = useBackStore();
134
+ await Promise.all(found.map((file) => backStore.upload(file)));
135
+ emit("update_values", { additional_files: [...uploaded, ...found] });
136
+ await missing_files();
137
+ if (has_missing_files.value) {
138
+ await upload_missing_from(folder_files, [...uploaded, ...found]);
139
+ }
140
+ }
141
+
142
+ async function upload_folder(folder_files: UploadFile[]): Promise<void> {
143
+ uploading.value = true;
144
+ try {
145
+ await upload_missing_from(folder_files, []);
146
+ } finally {
147
+ uploading.value = false;
148
+ }
149
+ }
150
+
151
+ function warn_folder_read_error(message: string): void {
152
+ useFeedbackStore().add_warning(`Could not read the dropped folder: ${message}`);
153
+ }
154
+
91
155
  // oxlint-disable-next-line no-top-level-await
92
156
  await missing_files();
93
157
  </script>
94
158
 
95
159
  <template>
96
- <FetchingData v-if="loading" />
160
+ <FetchingData v-if="loading || uploading" />
97
161
  <v-container v-else-if="has_missing_files">
98
162
  <v-row v-if="mandatory_files.length" align="center">
99
163
  <v-col cols="auto" class="pa-0">
@@ -115,7 +179,15 @@ await missing_files();
115
179
  </v-row>
116
180
  <v-row>
117
181
  <v-col cols="12">
182
+ <DragAndDrop
183
+ v-if="timeSeries"
184
+ directory
185
+ :show-extensions="false"
186
+ @files-selected="upload_folder"
187
+ @folder-read-error="warn_folder_read_error"
188
+ />
118
189
  <FileUploader
190
+ v-else
119
191
  v-bind="{ multiple, accept, files, autoUpload: false }"
120
192
  @files_uploaded="files_uploaded_event"
121
193
  />
@@ -0,0 +1,70 @@
1
+ <script setup lang="ts">
2
+ import schemas from "@geode/opengeodeweb-back/opengeodeweb_back_typed_schemas.js";
3
+
4
+ import FetchingData from "@ogw_front/components/FetchingData.vue";
5
+ import { useBackStore } from "@ogw_front/stores/back";
6
+ import { useDataStore } from "@ogw_front/stores/data";
7
+
8
+ const schema = schemas.opengeodeweb_back.time_series_allowed_objects;
9
+
10
+ interface Target {
11
+ id: string;
12
+ name: string;
13
+ geode_object_type: string;
14
+ }
15
+
16
+ const emit = defineEmits<{
17
+ update_values: [values: { target_id: string; geode_object_type: string }];
18
+ increment_step: [];
19
+ decrement_step: [];
20
+ }>();
21
+
22
+ const { filename } = defineProps<{ filename: string }>();
23
+
24
+ const loading = ref(true);
25
+ const targets = ref<Target[]>([]);
26
+
27
+ async function get_targets(): Promise<void> {
28
+ const backStore = useBackStore();
29
+ const dataStore = useDataStore();
30
+ const [{ allowed_objects }, items] = await Promise.all([
31
+ backStore.request({ schema, params: { filename } }),
32
+ dataStore.allItems(),
33
+ ]);
34
+ targets.value = items
35
+ .filter((item) => allowed_objects.includes(item.geode_object_type))
36
+ .map(({ id, name, geode_object_type }) => ({ id, name, geode_object_type }));
37
+ loading.value = false;
38
+ }
39
+
40
+ function select(target: Target): void {
41
+ emit("update_values", { target_id: target.id, geode_object_type: target.geode_object_type });
42
+ emit("increment_step");
43
+ }
44
+
45
+ // oxlint-disable-next-line no-top-level-await
46
+ await get_targets();
47
+ </script>
48
+
49
+ <template>
50
+ <FetchingData v-if="loading" />
51
+ <v-alert
52
+ v-else-if="targets.length === 0"
53
+ data-testid="noTimeSeriesTarget"
54
+ type="warning"
55
+ variant="tonal"
56
+ >
57
+ No compatible model loaded. Import a model (BRep) first, then drop the time series again.
58
+ </v-alert>
59
+ <v-list v-else bg-color="transparent">
60
+ <v-list-item
61
+ v-for="target in targets"
62
+ :key="target.id"
63
+ data-testid="timeSeriesTarget"
64
+ :title="target.name"
65
+ :subtitle="target.geode_object_type"
66
+ prepend-icon="mdi-cube-outline"
67
+ @click="select(target)"
68
+ />
69
+ </v-list>
70
+ </template>
@@ -150,4 +150,29 @@ async function importWorkflowFromSnapshot(items: readonly NewDataItem[]): Promis
150
150
  });
151
151
  return ids;
152
152
  }
153
- export { importFile, importWorkflow, importWorkflowFromSnapshot, importItem };
153
+
154
+ // The time series only adds attributes to the model, so the viewer object is reloaded under the same id with its stored style.
155
+ async function applyTimeSeriesWorkflow(filename: string, id: string): Promise<string> {
156
+ const backStore = useBackStore();
157
+ const dataStore = useDataStore();
158
+ const dataStyleStore = useDataStyleStore();
159
+ const hybridViewerStore = useHybridViewerStore();
160
+ await backStore.request({
161
+ schema: back_schemas.opengeodeweb_back.apply_time_series,
162
+ params: { id, filename },
163
+ });
164
+ const item = await dataStore.item(id);
165
+ await dataStore.deregisterObject(id);
166
+ await dataStore.registerObject(id, item.name ?? id);
167
+ await dataStyleStore.applyDefaultStyle(id);
168
+ await hybridViewerStore.remoteRender();
169
+ return id;
170
+ }
171
+
172
+ export {
173
+ importFile,
174
+ importWorkflow,
175
+ importWorkflowFromSnapshot,
176
+ importItem,
177
+ applyTimeSeriesWorkflow,
178
+ };
@@ -0,0 +1,75 @@
1
+ // Files carry extra app-specific bookkeeping fields once picked up by the uploader.
2
+ type UploadFile = File & { relativePath?: string; isConfigured?: boolean; displayName?: string };
3
+
4
+ // Path of the file inside the back upload folder: keeps the folder tree of a dropped directory.
5
+ function uploadPath(file: UploadFile): string {
6
+ for (const candidate of [file.relativePath, file.webkitRelativePath]) {
7
+ if (candidate !== undefined && candidate !== "") {
8
+ return candidate;
9
+ }
10
+ }
11
+ return file.name;
12
+ }
13
+
14
+ function fileExtension(path: string): string {
15
+ const name = path.slice(path.lastIndexOf("/") + 1);
16
+ const index = name.lastIndexOf(".");
17
+ return index === -1 ? "" : name.slice(index + 1).toLowerCase();
18
+ }
19
+
20
+ type FileMatch =
21
+ | { status: "matched"; indices: (number | undefined)[] }
22
+ | { status: "ambiguous"; path: string };
23
+
24
+ // Number of folders the two paths have in common, in order: 0 when the file names differ.
25
+ function matchScore(path: string, expected_path: string): number {
26
+ const segments = path.split("/");
27
+ const expected_segments = expected_path.split("/");
28
+ if (segments.at(-1) !== expected_segments.at(-1)) {
29
+ return 0;
30
+ }
31
+ // Longest common subsequence of the path segments
32
+ let previous = Array.from({ length: expected_segments.length + 1 }, () => 0);
33
+ for (const segment of segments) {
34
+ const current = [0];
35
+ for (const [index, expected_segment] of expected_segments.entries()) {
36
+ current.push(
37
+ segment === expected_segment
38
+ ? (previous[index] ?? 0) + 1
39
+ : Math.max(previous[index + 1] ?? 0, current[index] ?? 0),
40
+ );
41
+ }
42
+ previous = current;
43
+ }
44
+ return previous.at(-1) ?? 0;
45
+ }
46
+
47
+ // Finds each expected file in a selected folder, whatever the folder layout: a selected file is
48
+ // Kept for an expected path when each is the other's closest match (most folders in common).
49
+ // Gives, for each expected path, the index of its selected path (undefined when not found).
50
+ function matchExpectedFiles(paths: readonly string[], expected: readonly string[]): FileMatch {
51
+ const scores = expected.map((expected_path) =>
52
+ paths.map((path) => matchScore(path, expected_path)),
53
+ );
54
+ const path_best_scores = paths.map((_path, index) =>
55
+ Math.max(0, ...scores.map((expected_scores) => expected_scores[index] ?? 0)),
56
+ );
57
+ const indices: (number | undefined)[] = [];
58
+ for (const [expected_index, expected_scores] of scores.entries()) {
59
+ const best = Math.max(0, ...expected_scores);
60
+ const candidates = expected_scores.flatMap((score, index) =>
61
+ best > 0 && score === best && path_best_scores[index] === best ? [index] : [],
62
+ );
63
+ if (candidates.length > 1) {
64
+ return { status: "ambiguous", path: expected[expected_index] ?? "" };
65
+ }
66
+ indices.push(candidates[0]);
67
+ }
68
+ return { status: "matched", indices };
69
+ }
70
+
71
+ // Shared by every drop zone that refuses folders.
72
+ const foldersIgnoredWarning = "Folders can't be dropped here";
73
+
74
+ export { fileExtension, foldersIgnoredWarning, matchExpectedFiles, uploadPath };
75
+ export type { UploadFile };
@@ -3,6 +3,7 @@ import { CHUNK_SIZE_BYTES } from "@ogw_shared/utils/file.js";
3
3
  import type { Microservice } from "./api_fetch.js";
4
4
  import { consola } from "consola";
5
5
  import { fetchRaw } from "@ogw_shared/utils/fetch_raw.js";
6
+ import { uploadPath } from "@ogw_front/utils/upload_path";
6
7
  import { useFeedbackStore } from "@ogw_front/stores/feedback.js";
7
8
 
8
9
  interface UploadFileParams {
@@ -86,7 +87,7 @@ async function upload_file(
86
87
  const chunk = file.slice(start, start + CHUNK_SIZE_BYTES);
87
88
  const query = new URLSearchParams({
88
89
  ...params,
89
- filename: file.name,
90
+ filename: uploadPath(file),
90
91
  chunk_index: String(chunkIndex),
91
92
  total_chunks: String(totalChunks),
92
93
  });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@geode/opengeodeweb-front",
3
- "version": "10.40.0-rc.1",
3
+ "version": "10.40.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": {
@@ -64,9 +64,9 @@
64
64
  "prepublishOnly": "npm run build"
65
65
  },
66
66
  "dependencies": {
67
- "@geode/cloud-api": "next",
68
- "@geode/opengeodeweb-back": "next",
69
- "@geode/opengeodeweb-viewer": "next",
67
+ "@geode/cloud-api": "latest",
68
+ "@geode/opengeodeweb-back": "latest",
69
+ "@geode/opengeodeweb-viewer": "latest",
70
70
  "@kitware/vtk.js": "33.3.0",
71
71
  "@mdi/font": "7.4.47",
72
72
  "@msgpack/msgpack": "2.8.0",