@geode/opengeodeweb-front 10.40.1-rc.1 → 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
@@ -26,7 +26,7 @@ const emit = defineEmits<Emits>();
26
26
  const separators = [
27
27
  { title: "Comma (,)", value: "," },
28
28
  { title: "Semicolon (;)", value: ";" },
29
- { title: "Tab (\\t)", value: "\t" },
29
+ { title: String.raw`Tab (\t)`, value: "\t" },
30
30
  { title: "Space ( )", value: " " },
31
31
  { title: "Pipe (|)", value: "|" },
32
32
  { title: "Custom", value: "custom" },
@@ -96,18 +96,26 @@ async function importWorkflow(files: readonly FileToImport[]): Promise<string[]>
96
96
  }
97
97
 
98
98
  const results: string[] = [];
99
+ const errors: unknown[] = [];
99
100
  async function processChunk(chunkIndex: number): Promise<void> {
100
101
  if (chunkIndex >= chunks.length) {
101
102
  return;
102
103
  }
103
104
  const chunk = chunks[chunkIndex] ?? [];
104
- const chunk_results = await Promise.all(
105
+ // A failing file does not prevent the other files from being imported
106
+ const chunk_results = await Promise.allSettled(
105
107
  chunk.map(async ({ filename, geode_object_type }) => {
106
108
  const id = await importFile(filename, geode_object_type);
107
109
  return id;
108
110
  }),
109
111
  );
110
- results.push(...chunk_results);
112
+ for (const result of chunk_results) {
113
+ if (result.status === "fulfilled") {
114
+ results.push(result.value);
115
+ } else {
116
+ errors.push(result.reason);
117
+ }
118
+ }
111
119
  await processChunk(chunkIndex + 1);
112
120
  }
113
121
  await processChunk(0);
@@ -116,6 +124,9 @@ async function importWorkflow(files: readonly FileToImport[]): Promise<string[]>
116
124
  hybridViewerStore.resetCamera();
117
125
  }
118
126
  await hybridViewerStore.remoteRender();
127
+ if (errors.length > 0) {
128
+ throw new AggregateError(errors, `${errors.length} file(s) could not be imported`);
129
+ }
119
130
  return results;
120
131
  }
121
132
 
@@ -44,7 +44,7 @@ async function upload_file(
44
44
  const feedbackStore = useFeedbackStore();
45
45
 
46
46
  if (!(file instanceof File)) {
47
- throw new Error("file must be an instance of File");
47
+ throw new TypeError("file must be an instance of File");
48
48
  }
49
49
 
50
50
  const route = schema.$id;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@geode/opengeodeweb-front",
3
- "version": "10.40.1-rc.1",
3
+ "version": "10.41.0-rc.1",
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": {
@@ -10,7 +10,7 @@ const CHUNK_SIZE_BYTES = CHUNK_SIZE_MEBIBYTES * KIBIBYTES_PER_MEBIBYTE * BYTES_P
10
10
  function assertFile(file: unknown): asserts file is File {
11
11
  consola.debug("[ASSERT_FILE] Asserting file", { file });
12
12
  if (!(file instanceof File)) {
13
- throw new Error("file must be an instance of File");
13
+ throw new TypeError("file must be an instance of File");
14
14
  }
15
15
  }
16
16
 
@@ -106,4 +106,40 @@ function resolveAllowedObjects(
106
106
  };
107
107
  }
108
108
 
109
- export { resolveAllowedObjects, getFileExtension };
109
+ interface FileAllowedObjects {
110
+ readonly filename: string;
111
+ // Loadable types only, sorted by best is_loadable then best object_priority
112
+ readonly candidates: string[];
113
+ readonly selectedGeodeObject: string | undefined;
114
+ }
115
+
116
+ function sortCandidates(objectMap: AllowedObjectMap): string[] {
117
+ return Object.keys(objectMap)
118
+ .filter((key) => getObject(objectMap, key).is_loadable > 0)
119
+ .toSorted((keyA, keyB) => {
120
+ const objectA = getObject(objectMap, keyA);
121
+ const objectB = getObject(objectMap, keyB);
122
+ if (objectA.is_loadable !== objectB.is_loadable) {
123
+ return objectB.is_loadable - objectA.is_loadable;
124
+ }
125
+ return (objectB.object_priority ?? -Infinity) - (objectA.object_priority ?? -Infinity);
126
+ });
127
+ }
128
+
129
+ // Each file gets its own geode object type; files are no longer required to share one
130
+ function resolveAllowedObjectsPerFile(
131
+ filenames: readonly string[],
132
+ allowedObjectsList: readonly AllowedObjectMap[],
133
+ ): FileAllowedObjects[] {
134
+ return filenames.map((filename, index) => {
135
+ const objectMap = allowedObjectsList[index] ?? {};
136
+ return {
137
+ filename,
138
+ candidates: sortCandidates(objectMap),
139
+ selectedGeodeObject: selectGeodeObject(objectMap),
140
+ };
141
+ });
142
+ }
143
+
144
+ export { resolveAllowedObjects, resolveAllowedObjectsPerFile, getFileExtension };
145
+ export type { FileAllowedObjects };