Skip to content

Commit a5bd9e7

Browse files
committed
Merge branch 'next' of https://github.com/Geode-solutions/OpenGeodeWeb-Front into feat/scalarbar_model
2 parents a03aed2 + 7573faa commit a5bd9e7

11 files changed

Lines changed: 506 additions & 43 deletions

File tree

‎app/components/MissingFilesSelector.vue‎

Lines changed: 84 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -15,12 +15,17 @@ interface FilePlan {
1515
mandatory_files: string[];
1616
additional_files: string[];
1717
}
18+
interface FileMissingFiles {
19+
filename: string;
20+
mandatory_files: string[];
21+
additional_files: string[];
22+
}
1823
1924
const schema = schemas.opengeodeweb_back.data_missing_files;
2025
const time_series_schema = schemas.opengeodeweb_back.time_series_missing_files;
2126
2227
interface Emits {
23-
update_values: [value: { additional_files: UploadFile[] }];
28+
update_values: [value: { additional_files?: UploadFile[]; skipped_files?: string[] }];
2429
increment_step: [];
2530
decrement_step: [];
2631
}
@@ -29,15 +34,16 @@ const emit = defineEmits<Emits>();
2934
3035
interface Props {
3136
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>;
3339
filenames: string[];
3440
files?: UploadFile[];
3541
timeSeries?: boolean;
3642
}
3743
3844
const {
3945
multiple,
40-
geodeObjectType,
46+
geodeObjectTypes = {},
4147
filenames,
4248
files = [],
4349
timeSeries = false,
@@ -48,11 +54,36 @@ const loading = ref<boolean>(false);
4854
const has_missing_files = ref<boolean>(false);
4955
const mandatory_files = ref<string[]>([]);
5056
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+
);
5174
const toggle_loading = useToggle(loading);
5275
const uploading = ref<boolean>(false);
5376
5477
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 });
5687
emit("increment_step");
5788
}
5889
@@ -65,9 +96,10 @@ async function missing_files(): Promise<void> {
6596
has_missing_files.value = false;
6697
mandatory_files.value = [];
6798
additional_files.value = [];
99+
missing_by_file.value = [];
68100
const backStore = useBackStore();
69101
70-
const promise_array: Promise<FilePlan>[] = filenames.map((filename): Promise<FilePlan> => {
102+
const promise_array = checked_filenames.value.map((filename): Promise<FilePlan> => {
71103
if (isCsvFile(filename)) {
72104
return Promise.resolve({
73105
has_missing_files: false,
@@ -78,13 +110,18 @@ async function missing_files(): Promise<void> {
78110
if (timeSeries) {
79111
return backStore.request({ schema: time_series_schema, params: { filename } });
80112
}
81-
const params = { geode_object_type: geodeObjectType, filename };
113+
const params = { geode_object_type: geodeObjectTypes[filename] ?? "", filename };
82114
return backStore.request({ schema, params });
83115
});
84116
const values = await Promise.all(promise_array);
85-
for (const value of values) {
117+
for (const [index, value] of values.entries()) {
86118
if (value.has_missing_files) {
87119
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+
});
88125
}
89126
mandatory_files.value = [...mandatory_files.value, ...value.mandatory_files];
90127
additional_files.value = [...additional_files.value, ...value.additional_files];
@@ -98,6 +135,7 @@ async function missing_files(): Promise<void> {
98135
}
99136
100137
if (!has_missing_files.value) {
138+
emit("update_values", { skipped_files: [] });
101139
emit("increment_step");
102140
}
103141
toggle_loading();
@@ -159,7 +197,40 @@ await missing_files();
159197
<template>
160198
<FetchingData v-if="loading || uploading" />
161199
<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">
163234
<v-col cols="auto" class="pa-0">
164235
<v-icon color="warning" icon="mdi-file-document-alert-outline" />
165236
</v-col>
@@ -168,7 +239,7 @@ await missing_files();
168239
<v-chip>{{ mandatory_file }}</v-chip>
169240
</v-col>
170241
</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">
172243
<v-col cols="auto" class="pa-0">
173244
<v-icon color="accent" icon="mdi-file-document-plus-outline" />
174245
</v-col>
@@ -194,8 +265,10 @@ await missing_files();
194265
</v-col>
195266
</v-row>
196267
<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>
199272
</v-col>
200273
</v-row>
201274
</v-container>

‎app/components/ObjectSelector.vue‎

Lines changed: 147 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,11 @@
11
<script setup lang="ts">
2+
import {
3+
type FileAllowedObjects,
4+
resolveAllowedObjects,
5+
resolveAllowedObjectsPerFile,
6+
} from "@ogw_shared/utils/response_handlers/load.js";
27
import FetchingData from "@ogw_front/components/FetchingData.vue";
38
import { geode_objects } from "@ogw_front/assets/geode_objects";
4-
import { resolveAllowedObjects } from "@ogw_shared/utils/response_handlers/load.js";
59
import schemas from "@geode/opengeodeweb-back/opengeodeweb_back_typed_schemas.js";
610
import { useBackStore } from "@ogw_front/stores/back";
711
@@ -14,8 +18,15 @@ type AllowedObjectMap = Record<string, AllowedObject>;
1418
1519
const schema = schemas.opengeodeweb_back.allowed_objects;
1620
21+
interface PendingGroup {
22+
filenames: string[];
23+
candidates: string[];
24+
selected: string;
25+
}
26+
1727
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> }];
1930
increment_step: [];
2031
}>();
2132
@@ -31,6 +42,14 @@ const loading = ref<boolean>(false);
3142
const allowedGeodeObjects = ref<AllowedObjectMap>({});
3243
const toggleLoading = useToggle(loading);
3344
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);
3453
3554
async function fetchAllowedObjectsList(): Promise<AllowedObjectMap[]> {
3655
const promiseArray = filenames.map((filename): Promise<{ allowed_objects: AllowedObjectMap }> => {
@@ -41,11 +60,43 @@ async function fetchAllowedObjectsList(): Promise<AllowedObjectMap[]> {
4160
return responses.map((response) => response.allowed_objects);
4261
}
4362
63+
function emitGeodeObjectTypes(geode_object_types: Record<string, string>): void {
64+
emit("update_values", { geode_object_types });
65+
emit("increment_step");
66+
}
67+
4468
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+
}
4898
}
99+
return [...groups.values()];
49100
}
50101
51102
// 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 {
59110
return geode_objects[key]?.image;
60111
}
61112
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+
62162
async function getAllowedGeodeObjects(): Promise<void> {
63163
toggleLoading();
64164
allowedGeodeObjects.value = {};
65165
multipleFilesNoCommon.value = false;
166+
showPerFile.value = false;
66167
67168
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);
75170
toggleLoading();
76171
}
77172
// oxlint-disable-next-line no-top-level-await
@@ -80,7 +175,46 @@ await getAllowedGeodeObjects();
80175

81176
<template>
82177
<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>
83214
<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>
84218
<v-col v-for="(value, key) in allowedGeodeObjects" :key="key" cols="3" md="4">
85219
<v-tooltip :text="geodeObjectTooltip(key, Boolean(value['is_loadable']))" location="bottom">
86220
<template v-slot:activator="{ props }">
@@ -112,7 +246,7 @@ await getAllowedGeodeObjects();
112246
</v-card-text>
113247
</v-card>
114248
</v-row>
115-
<v-row v-else class="pa-5">
249+
<v-row v-else-if="!autoResolved" class="pa-5">
116250
<v-card class="card" variant="tonal" rounded>
117251
<v-card-text>
118252
This file format isn't supported! Please check the

0 commit comments

Comments
 (0)