@geode/opengeodeweb-front 10.40.0 → 10.41.0-rc.1

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.
@@ -5,7 +5,7 @@ const BYTES_PER_KIBIBYTE = 1024;
5
5
  const CHUNK_SIZE_BYTES = 8192 * BYTES_PER_KIBIBYTE;
6
6
  function assertFile(file) {
7
7
  consola.debug("[ASSERT_FILE] Asserting file", { file });
8
- if (!(file instanceof File)) throw new Error("file must be an instance of File");
8
+ if (!(file instanceof File)) throw new TypeError("file must be an instance of File");
9
9
  }
10
10
  function hasBody(params) {
11
11
  if (params instanceof FormData || params instanceof Blob) return true;
@@ -46,5 +46,23 @@ function resolveAllowedObjects(filenames, allowedObjectsList) {
46
46
  selectedGeodeObject: selectGeodeObject(mergedAllowedObjects)
47
47
  };
48
48
  }
49
+ function sortCandidates(objectMap) {
50
+ return Object.keys(objectMap).filter((key) => getObject(objectMap, key).is_loadable > 0).toSorted((keyA, keyB) => {
51
+ const objectA = getObject(objectMap, keyA);
52
+ const objectB = getObject(objectMap, keyB);
53
+ if (objectA.is_loadable !== objectB.is_loadable) return objectB.is_loadable - objectA.is_loadable;
54
+ return (objectB.object_priority ?? -Infinity) - (objectA.object_priority ?? -Infinity);
55
+ });
56
+ }
57
+ function resolveAllowedObjectsPerFile(filenames, allowedObjectsList) {
58
+ return filenames.map((filename, index) => {
59
+ const objectMap = allowedObjectsList[index] ?? {};
60
+ return {
61
+ filename,
62
+ candidates: sortCandidates(objectMap),
63
+ selectedGeodeObject: selectGeodeObject(objectMap)
64
+ };
65
+ });
66
+ }
49
67
  //#endregion
50
- export { getFileExtension, resolveAllowedObjects };
68
+ export { getFileExtension, resolveAllowedObjects, resolveAllowedObjectsPerFile };
@@ -15,12 +15,17 @@ interface FilePlan {
15
15
  mandatory_files: string[];
16
16
  additional_files: string[];
17
17
  }
18
+ interface FileMissingFiles {
19
+ filename: string;
20
+ mandatory_files: string[];
21
+ additional_files: string[];
22
+ }
18
23
 
19
24
  const schema = schemas.opengeodeweb_back.data_missing_files;
20
25
  const time_series_schema = schemas.opengeodeweb_back.time_series_missing_files;
21
26
 
22
27
  interface Emits {
23
- update_values: [value: { additional_files: UploadFile[] }];
28
+ update_values: [value: { additional_files?: UploadFile[]; skipped_files?: string[] }];
24
29
  increment_step: [];
25
30
  decrement_step: [];
26
31
  }
@@ -29,7 +34,8 @@ const emit = defineEmits<Emits>();
29
34
 
30
35
  interface Props {
31
36
  multiple: boolean;
32
- geodeObjectType: string;
37
+ // Filename -> geode object type, each file is checked with its own type (unused for time series)
38
+ geodeObjectTypes?: Record<string, string>;
33
39
  filenames: string[];
34
40
  files?: UploadFile[];
35
41
  timeSeries?: boolean;
@@ -37,7 +43,7 @@ interface Props {
37
43
 
38
44
  const {
39
45
  multiple,
40
- geodeObjectType,
46
+ geodeObjectTypes = {},
41
47
  filenames,
42
48
  files = [],
43
49
  timeSeries = false,
@@ -48,11 +54,36 @@ const loading = ref<boolean>(false);
48
54
  const has_missing_files = ref<boolean>(false);
49
55
  const mandatory_files = ref<string[]>([]);
50
56
  const additional_files = ref<string[]>([]);
57
+ const missing_by_file = ref<FileMissingFiles[]>([]);
58
+ // Files left out at the data type step are not checked
59
+ const checked_filenames = computed(() =>
60
+ timeSeries ? filenames : filenames.filter((filename) => filename in geodeObjectTypes),
61
+ );
62
+ // Files whose mandatory files are missing are skipped when the step is skipped
63
+ const files_without_mandatory = computed(() =>
64
+ checked_filenames.value.filter(
65
+ (filename) =>
66
+ !missing_by_file.value.some(
67
+ (missing) => missing.filename === filename && missing.mandatory_files.length > 0,
68
+ ),
69
+ ),
70
+ );
71
+ const can_skip = computed(
72
+ () => missing_by_file.value.length > 0 && files_without_mandatory.value.length > 0,
73
+ );
51
74
  const toggle_loading = useToggle(loading);
52
75
  const uploading = ref<boolean>(false);
53
76
 
54
77
  function files_uploaded_event(value: UploadFile[]): void {
55
- emit("update_values", { additional_files: value });
78
+ emit("update_values", { additional_files: value, skipped_files: [] });
79
+ emit("increment_step");
80
+ }
81
+
82
+ function skip_step(): void {
83
+ const skipped_files = checked_filenames.value.filter(
84
+ (filename) => !files_without_mandatory.value.includes(filename),
85
+ );
86
+ emit("update_values", { skipped_files });
56
87
  emit("increment_step");
57
88
  }
58
89
 
@@ -65,9 +96,10 @@ async function missing_files(): Promise<void> {
65
96
  has_missing_files.value = false;
66
97
  mandatory_files.value = [];
67
98
  additional_files.value = [];
99
+ missing_by_file.value = [];
68
100
  const backStore = useBackStore();
69
101
 
70
- const promise_array: Promise<FilePlan>[] = filenames.map((filename): Promise<FilePlan> => {
102
+ const promise_array = checked_filenames.value.map((filename): Promise<FilePlan> => {
71
103
  if (isCsvFile(filename)) {
72
104
  return Promise.resolve({
73
105
  has_missing_files: false,
@@ -78,13 +110,18 @@ async function missing_files(): Promise<void> {
78
110
  if (timeSeries) {
79
111
  return backStore.request({ schema: time_series_schema, params: { filename } });
80
112
  }
81
- const params = { geode_object_type: geodeObjectType, filename };
113
+ const params = { geode_object_type: geodeObjectTypes[filename] ?? "", filename };
82
114
  return backStore.request({ schema, params });
83
115
  });
84
116
  const values = await Promise.all(promise_array);
85
- for (const value of values) {
117
+ for (const [index, value] of values.entries()) {
86
118
  if (value.has_missing_files) {
87
119
  has_missing_files.value = true;
120
+ missing_by_file.value.push({
121
+ filename: checked_filenames.value[index] ?? "",
122
+ mandatory_files: value.mandatory_files,
123
+ additional_files: value.additional_files,
124
+ });
88
125
  }
89
126
  mandatory_files.value = [...mandatory_files.value, ...value.mandatory_files];
90
127
  additional_files.value = [...additional_files.value, ...value.additional_files];
@@ -98,6 +135,7 @@ async function missing_files(): Promise<void> {
98
135
  }
99
136
 
100
137
  if (!has_missing_files.value) {
138
+ emit("update_values", { skipped_files: [] });
101
139
  emit("increment_step");
102
140
  }
103
141
  toggle_loading();
@@ -159,7 +197,40 @@ await missing_files();
159
197
  <template>
160
198
  <FetchingData v-if="loading || uploading" />
161
199
  <v-container v-else-if="has_missing_files">
162
- <v-row v-if="mandatory_files.length" align="center">
200
+ <template v-if="checked_filenames.length > 1">
201
+ <div v-for="missing in missing_by_file" :key="missing.filename" class="mb-2">
202
+ <v-row align="center">
203
+ <p class="pa-1 font-weight-bold">{{ missing.filename }}</p>
204
+ <v-chip
205
+ v-if="missing.mandatory_files.length && files_without_mandatory.length"
206
+ class="ma-1"
207
+ color="warning"
208
+ size="small"
209
+ >
210
+ Skipped if mandatory files are not added
211
+ </v-chip>
212
+ </v-row>
213
+ <v-row v-if="missing.mandatory_files.length" align="center">
214
+ <v-col cols="auto" class="pa-0">
215
+ <v-icon color="warning" icon="mdi-file-document-alert-outline" />
216
+ </v-col>
217
+ <p class="pa-1">Mandatory files:</p>
218
+ <v-col v-for="mandatory_file in missing.mandatory_files" cols="auto" class="pa-0">
219
+ <v-chip>{{ mandatory_file }}</v-chip>
220
+ </v-col>
221
+ </v-row>
222
+ <v-row v-if="missing.additional_files.length" align="center">
223
+ <v-col cols="auto" class="pa-0">
224
+ <v-icon color="accent" icon="mdi-file-document-plus-outline" />
225
+ </v-col>
226
+ <p class="pa-1">Additional files:</p>
227
+ <v-col v-for="additional_file in missing.additional_files" cols="auto" class="pa-0">
228
+ <v-chip>{{ additional_file }}</v-chip>
229
+ </v-col>
230
+ </v-row>
231
+ </div>
232
+ </template>
233
+ <v-row v-else-if="mandatory_files.length" align="center">
163
234
  <v-col cols="auto" class="pa-0">
164
235
  <v-icon color="warning" icon="mdi-file-document-alert-outline" />
165
236
  </v-col>
@@ -168,7 +239,7 @@ await missing_files();
168
239
  <v-chip>{{ mandatory_file }}</v-chip>
169
240
  </v-col>
170
241
  </v-row>
171
- <v-row v-if="additional_files.length" align="center">
242
+ <v-row v-if="!(checked_filenames.length > 1) && additional_files.length" align="center">
172
243
  <v-col cols="auto" class="pa-0">
173
244
  <v-icon color="accent" icon="mdi-file-document-plus-outline" />
174
245
  </v-col>
@@ -194,8 +265,10 @@ await missing_files();
194
265
  </v-col>
195
266
  </v-row>
196
267
  <v-row>
197
- <v-col v-if="mandatory_files.length === 0 && additional_files.length > 0" cols="auto">
198
- <v-btn @click="emit('increment_step')" color="warning">Skip step</v-btn>
268
+ <v-col v-if="can_skip" cols="auto">
269
+ <v-btn @click="skip_step" color="warning" data-testid="skipMissingFilesButton">
270
+ Skip step
271
+ </v-btn>
199
272
  </v-col>
200
273
  </v-row>
201
274
  </v-container>
@@ -1,7 +1,11 @@
1
1
  <script setup lang="ts">
2
+ import {
3
+ type FileAllowedObjects,
4
+ resolveAllowedObjects,
5
+ resolveAllowedObjectsPerFile,
6
+ } from "@ogw_shared/utils/response_handlers/load.js";
2
7
  import FetchingData from "@ogw_front/components/FetchingData.vue";
3
8
  import { geode_objects } from "@ogw_front/assets/geode_objects";
4
- import { resolveAllowedObjects } from "@ogw_shared/utils/response_handlers/load.js";
5
9
  import schemas from "@geode/opengeodeweb-back/opengeodeweb_back_typed_schemas.js";
6
10
  import { useBackStore } from "@ogw_front/stores/back";
7
11
 
@@ -14,8 +18,15 @@ type AllowedObjectMap = Record<string, AllowedObject>;
14
18
 
15
19
  const schema = schemas.opengeodeweb_back.allowed_objects;
16
20
 
21
+ interface PendingGroup {
22
+ filenames: string[];
23
+ candidates: string[];
24
+ selected: string;
25
+ }
26
+
17
27
  const emit = defineEmits<{
18
- update_values: [value: { geode_object_type: string }];
28
+ // Filename -> geode object type, files that cannot be loaded are left out
29
+ update_values: [value: { geode_object_types: Record<string, string> }];
19
30
  increment_step: [];
20
31
  }>();
21
32
 
@@ -31,6 +42,14 @@ const loading = ref<boolean>(false);
31
42
  const allowedGeodeObjects = ref<AllowedObjectMap>({});
32
43
  const toggleLoading = useToggle(loading);
33
44
  const multipleFilesNoCommon = ref<boolean>(false);
45
+ // Types already known, files left to choose for, and files that cannot be loaded
46
+ const resolvedTypes = ref<Record<string, string>>({});
47
+ const pendingFilenames = ref<string[]>([]);
48
+ const pendingGroups = ref<PendingGroup[]>([]);
49
+ const skippedFiles = ref<string[]>([]);
50
+ const showPerFile = ref<boolean>(false);
51
+ // Every file got its type without the user, nothing to display
52
+ const autoResolved = ref<boolean>(false);
34
53
 
35
54
  async function fetchAllowedObjectsList(): Promise<AllowedObjectMap[]> {
36
55
  const promiseArray = filenames.map((filename): Promise<{ allowed_objects: AllowedObjectMap }> => {
@@ -41,11 +60,43 @@ async function fetchAllowedObjectsList(): Promise<AllowedObjectMap[]> {
41
60
  return responses.map((response) => response.allowed_objects);
42
61
  }
43
62
 
63
+ function emitGeodeObjectTypes(geode_object_types: Record<string, string>): void {
64
+ emit("update_values", { geode_object_types });
65
+ emit("increment_step");
66
+ }
67
+
44
68
  function setGeodeObject(geode_object_type: string | undefined): void {
45
- if (geode_object_type) {
46
- emit("update_values", { geode_object_type });
47
- emit("increment_step");
69
+ if (!geode_object_type) {
70
+ return;
71
+ }
72
+ const chosen = pendingFilenames.value.map((filename) => [filename, geode_object_type]);
73
+ emitGeodeObjectTypes({ ...resolvedTypes.value, ...Object.fromEntries(chosen) });
74
+ }
75
+
76
+ function confirmPerFile(): void {
77
+ const chosen = pendingGroups.value.flatMap((group) =>
78
+ group.filenames.map((filename) => [filename, group.selected]),
79
+ );
80
+ emitGeodeObjectTypes({ ...resolvedTypes.value, ...Object.fromEntries(chosen) });
81
+ }
82
+
83
+ // Files with the same candidate types share one choice
84
+ function groupPendingFiles(pending: readonly FileAllowedObjects[]): PendingGroup[] {
85
+ const groups = new Map<string, PendingGroup>();
86
+ for (const file of pending) {
87
+ const key = file.candidates.join(",");
88
+ const group = groups.get(key);
89
+ if (group) {
90
+ group.filenames.push(file.filename);
91
+ } else {
92
+ groups.set(key, {
93
+ filenames: [file.filename],
94
+ candidates: file.candidates,
95
+ selected: file.candidates[0] ?? "",
96
+ });
97
+ }
48
98
  }
99
+ return [...groups.values()];
49
100
  }
50
101
 
51
102
  // The geode_objects lookup is fixed and keyed by known object type names; a missing entry only happens if the backend reports a type this table doesn't know about.
@@ -59,19 +110,63 @@ function geodeObjectImage(key: string): string | undefined {
59
110
  return geode_objects[key]?.image;
60
111
  }
61
112
 
113
+ function showCommonGeodeObjects(
114
+ pendingNames: readonly string[],
115
+ allowedObjectsList: readonly AllowedObjectMap[],
116
+ ): string | undefined {
117
+ const resolved = resolveAllowedObjects(pendingNames, allowedObjectsList);
118
+ allowedGeodeObjects.value = resolved.mergedAllowedObjects;
119
+ multipleFilesNoCommon.value = resolved.multipleFilesNoCommon;
120
+ return resolved.selectedGeodeObject;
121
+ }
122
+
123
+ // Each file gets its own type. Returns true when the per file panel is needed, i.e. the files cannot be handled by a single choice
124
+ function resolvePerFile(allowedObjectsList: readonly AllowedObjectMap[]): boolean {
125
+ const files = resolveAllowedObjectsPerFile(filenames, allowedObjectsList);
126
+ const loadable = files.filter((file) => file.candidates.length > 0);
127
+ if (loadable.length === 0) {
128
+ // Nothing can be loaded: show why with the common types
129
+ showCommonGeodeObjects(filenames, allowedObjectsList);
130
+ return false;
131
+ }
132
+ const pending = loadable.filter((file) => file.selectedGeodeObject === undefined);
133
+ resolvedTypes.value = Object.fromEntries(
134
+ loadable.flatMap((file) =>
135
+ file.selectedGeodeObject === undefined ? [] : [[file.filename, file.selectedGeodeObject]],
136
+ ),
137
+ );
138
+ pendingFilenames.value = pending.map((file) => file.filename);
139
+ skippedFiles.value = files
140
+ .filter((file) => file.candidates.length === 0)
141
+ .map((file) => file.filename);
142
+ if (skippedFiles.value.length === 0) {
143
+ if (pending.length === 0) {
144
+ autoResolved.value = true;
145
+ emitGeodeObjectTypes(resolvedTypes.value);
146
+ return false;
147
+ }
148
+ const pendingAllowedObjects = pending.map(
149
+ (file) => allowedObjectsList[filenames.indexOf(file.filename)] ?? {},
150
+ );
151
+ // Pending files sharing a loadable type: one choice for all of them
152
+ const selected = showCommonGeodeObjects(pendingFilenames.value, pendingAllowedObjects);
153
+ if (Object.values(allowedGeodeObjects.value).some((object) => object.is_loadable > 0)) {
154
+ setGeodeObject(selected);
155
+ return false;
156
+ }
157
+ }
158
+ pendingGroups.value = groupPendingFiles(pending);
159
+ return true;
160
+ }
161
+
62
162
  async function getAllowedGeodeObjects(): Promise<void> {
63
163
  toggleLoading();
64
164
  allowedGeodeObjects.value = {};
65
165
  multipleFilesNoCommon.value = false;
166
+ showPerFile.value = false;
66
167
 
67
168
  const allowedGeodeObjectsList = await fetchAllowedObjectsList();
68
- const resolved = resolveAllowedObjects(filenames, allowedGeodeObjectsList);
69
-
70
- allowedGeodeObjects.value = resolved.mergedAllowedObjects;
71
- multipleFilesNoCommon.value = resolved.multipleFilesNoCommon;
72
- if (resolved.selectedGeodeObject) {
73
- setGeodeObject(resolved.selectedGeodeObject);
74
- }
169
+ showPerFile.value = resolvePerFile(allowedGeodeObjectsList);
75
170
  toggleLoading();
76
171
  }
77
172
  // oxlint-disable-next-line no-top-level-await
@@ -80,7 +175,46 @@ await getAllowedGeodeObjects();
80
175
 
81
176
  <template>
82
177
  <FetchingData v-if="loading" />
178
+ <v-container v-else-if="showPerFile" data-testid="perFileObjectSelector">
179
+ <v-row v-for="group in pendingGroups" :key="group.filenames.join()" align="center">
180
+ <v-col cols="auto" class="pa-1">
181
+ <v-chip v-for="filename in group.filenames" :key="filename" class="ma-1" size="small">
182
+ {{ filename }}
183
+ </v-chip>
184
+ </v-col>
185
+ <v-col class="pa-1">
186
+ <v-select
187
+ v-model="group.selected"
188
+ :items="group.candidates"
189
+ density="compact"
190
+ hide-details
191
+ variant="outlined"
192
+ />
193
+ </v-col>
194
+ </v-row>
195
+ <v-row v-for="(type, filename) in resolvedTypes" :key="filename" align="center">
196
+ <v-chip class="ma-1" size="small">{{ filename }}</v-chip>
197
+ <v-chip class="ma-1" size="small" variant="tonal">{{ type }}</v-chip>
198
+ </v-row>
199
+ <v-row v-if="skippedFiles.length" align="center">
200
+ <v-icon color="warning" icon="mdi-file-alert-outline" class="ma-1" />
201
+ <p class="pa-1">Not supported, will be skipped:</p>
202
+ <v-chip v-for="filename in skippedFiles" :key="filename" class="ma-1" size="small">
203
+ {{ filename }}
204
+ </v-chip>
205
+ </v-row>
206
+ <v-row>
207
+ <v-col cols="auto">
208
+ <v-btn color="primary" data-testid="confirmDataTypesButton" @click="confirmPerFile">
209
+ Continue
210
+ </v-btn>
211
+ </v-col>
212
+ </v-row>
213
+ </v-container>
83
214
  <v-row v-else-if="Object.keys(allowedGeodeObjects).length" class="justify-left">
215
+ <v-col v-if="pendingFilenames.length && pendingFilenames.length < filenames.length" cols="12">
216
+ Choose the data type for: {{ pendingFilenames.join(", ") }}
217
+ </v-col>
84
218
  <v-col v-for="(value, key) in allowedGeodeObjects" :key="key" cols="3" md="4">
85
219
  <v-tooltip :text="geodeObjectTooltip(key, Boolean(value['is_loadable']))" location="bottom">
86
220
  <template v-slot:activator="{ props }">
@@ -112,7 +246,7 @@ await getAllowedGeodeObjects();
112
246
  </v-card-text>
113
247
  </v-card>
114
248
  </v-row>
115
- <v-row v-else class="pa-5">
249
+ <v-row v-else-if="!autoResolved" class="pa-5">
116
250
  <v-card class="card" variant="tonal" rounded>
117
251
  <v-card-text>
118
252
  This file format isn't supported! Please check the
@@ -1,4 +1,5 @@
1
1
  <script setup lang="ts">
2
+ import { type AttributeSchemas, fetchAttributeRange } from "@ogw_front/utils/attribute_range";
2
3
  import {
3
4
  MESH_ELEMENT_KINDS,
4
5
  MODEL_COMPONENT_ATTRIBUTE_SCHEMAS,
@@ -26,9 +27,9 @@ const { escapeFunction = undefined } = defineProps<Props>();
26
27
 
27
28
  interface AttributeSource {
28
29
  title: string;
29
- schema: JsonRpcSchema;
30
+ schemas: AttributeSchemas;
30
31
  timeStepKey: string;
31
- params: Record<string, unknown>;
32
+ params: { id: string; component_ids?: string[] };
32
33
  location: "point" | "cell";
33
34
  }
34
35
 
@@ -86,23 +87,24 @@ const itemOptions = computed<{ title: string; value: number }[]>(() =>
86
87
  })),
87
88
  );
88
89
 
89
- function attributeSchema(kind: string, isModel: boolean): JsonRpcSchema {
90
+ function attributeSchema(kind: string, isModel: boolean, route: "names" | "range"): JsonRpcSchema {
90
91
  const prefix = isModel ? "model_component_" : "";
91
92
  return back_schemas.opengeodeweb_back[
92
- `${prefix}${kind}_attribute_names` as keyof typeof back_schemas.opengeodeweb_back
93
+ `${prefix}${kind}_attribute_${route}` as keyof typeof back_schemas.opengeodeweb_back
93
94
  ] as JsonRpcSchema;
94
95
  }
95
96
 
96
97
  function attributeSource(
97
98
  kind: string,
98
99
  title: string,
99
- params: Record<string, unknown>,
100
+ params: { id: string; component_ids?: string[] },
100
101
  modelType?: string,
101
102
  ): AttributeSource {
102
- const schema = attributeSchema(kind, modelType !== undefined);
103
+ const isModel = modelType !== undefined;
104
+ const schema = attributeSchema(kind, isModel, "names");
103
105
  return {
104
106
  title: `${title}${kind} attribute`,
105
- schema,
107
+ schemas: { names: schema, range: attributeSchema(kind, isModel, "range") },
106
108
  timeStepKey:
107
109
  modelType === undefined
108
110
  ? schema.$id
@@ -154,11 +156,17 @@ async function applyThreshold(): Promise<void> {
154
156
 
155
157
  const debouncedApply = useDebounceFn(() => applyThreshold(), DEBOUNCE_DELAY);
156
158
 
157
- function resetRange(): void {
158
- const { min, max } = getAttributeRange(
159
- currentAttribute.value as Parameters<typeof getAttributeRange>[0],
160
- attributeItem.value,
161
- );
159
+ async function resetRange(): Promise<void> {
160
+ const source = sources.value[selectedSourceIndex.value ?? -1];
161
+ const name = attributeName.value;
162
+ if (!source || name === undefined) {
163
+ return;
164
+ }
165
+ const range = await fetchAttributeRange(source.schemas.range, source.params, name);
166
+ if (name !== attributeName.value) {
167
+ return;
168
+ }
169
+ const { min, max } = getAttributeRange(range, attributeItem.value);
162
170
  minimum.value = min;
163
171
  maximum.value = max;
164
172
  }
@@ -186,19 +194,19 @@ watch(selectedSourceIndex, async (index) => {
186
194
  return;
187
195
  }
188
196
  const response = (await backStore.request({
189
- schema: source.schema,
197
+ schema: source.schemas.names,
190
198
  params: source.params,
191
199
  })) as { attributes: AttributeInfo[] };
192
200
  attributes.value = response.attributes;
193
201
  });
194
202
 
195
- watch(attributeName, () => {
203
+ watch(attributeName, async () => {
196
204
  attributeItem.value = 0;
197
- resetRange();
205
+ await resetRange();
198
206
  });
199
207
 
200
- watch(attributeItem, () => {
201
- resetRange();
208
+ watch(attributeItem, async () => {
209
+ await resetRange();
202
210
  });
203
211
 
204
212
  watch(currentTimeStep, () => {