@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.
- package/.build/shared/utils/file.js +1 -1
- package/.build/shared/utils/response_handlers/load.js +19 -1
- package/app/components/MissingFilesSelector.vue +84 -11
- package/app/components/ObjectSelector.vue +147 -13
- package/app/components/csv-preview/CsvSettings.vue +1 -1
- package/app/utils/import_workflow.ts +13 -2
- package/internal/utils/upload_file.ts +1 -1
- package/package.json +1 -1
- package/shared/utils/file.ts +1 -1
- package/shared/utils/response_handlers/load.ts +37 -1
|
@@ -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
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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:
|
|
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
|
-
<
|
|
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="
|
|
198
|
-
<v-btn @click="
|
|
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
|
-
|
|
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
|
-
|
|
47
|
-
|
|
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
|
-
|
|
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:
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
package/shared/utils/file.ts
CHANGED
|
@@ -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
|
|
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
|
-
|
|
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 };
|