From fe8890013e94ed12bcf946988560f86073350166 Mon Sep 17 00:00:00 2001 From: SpliiT Date: Mon, 27 Apr 2026 17:37:26 +0200 Subject: [PATCH 01/14] feat(CSVPreview): A preview of a csv file --- app/components/FileSelector.vue | 6 +- app/components/FileUploader.vue | 105 +++++++- app/components/MissingFilesSelector.vue | 25 +- app/components/ObjectSelector.vue | 5 + app/components/csv-preview/CsvPreviewer.vue | 255 ++++++++++++++++++++ app/components/csv-preview/CsvSettings.vue | 150 ++++++++++++ app/components/csv-preview/CsvTable.vue | 107 ++++++++ 7 files changed, 643 insertions(+), 10 deletions(-) create mode 100644 app/components/csv-preview/CsvPreviewer.vue create mode 100644 app/components/csv-preview/CsvSettings.vue create mode 100644 app/components/csv-preview/CsvTable.vue diff --git a/app/components/FileSelector.vue b/app/components/FileSelector.vue index bbbf593a..bdd0c0db 100644 --- a/app/components/FileSelector.vue +++ b/app/components/FileSelector.vue @@ -48,7 +48,11 @@ async function get_allowed_files() { toggle_loading(); const geodeStore = useGeodeStore(); const response = await geodeStore.request(schema, {}); - accept.value = response.extensions.map((extension) => `.${extension}`).join(","); + const extensions = response.extensions.map((extension) => `.${extension}`); + if (!extensions.includes(".csv")) { + extensions.push(".csv"); + } + accept.value = extensions.join(","); toggle_loading(); } diff --git a/app/components/FileUploader.vue b/app/components/FileUploader.vue index 83712a4d..476ef069 100644 --- a/app/components/FileUploader.vue +++ b/app/components/FileUploader.vue @@ -3,6 +3,7 @@ import { useGeodeStore } from "@ogw_front/stores/geode"; import { useTemplateRef } from "vue"; import DragAndDrop from "@ogw_front/components/DragAndDrop"; +import CsvPreviewer from "@ogw_front/components/csv-preview/CsvPreviewer"; const emit = defineEmits(["files_uploaded", "decrement_step", "reset_values"]); @@ -13,6 +14,7 @@ const { auto_upload, mini, show_overlay: showOverlay, + allow_csv_config: allowCsvConfig, } = defineProps({ multiple: { type: Boolean, required: true }, accept: { type: String, required: true }, @@ -20,6 +22,7 @@ const { auto_upload: { type: Boolean, required: false, default: false }, mini: { type: Boolean, required: false, default: false }, show_overlay: { type: Boolean, required: false, default: true }, + allow_csv_config: { type: Boolean, required: false, default: false }, }); const geodeStore = useGeodeStore(); @@ -29,8 +32,38 @@ const loading = ref(false); const files_uploaded = ref(false); const dragAndDropRef = useTemplateRef("dragAndDropRef"); +const csv_dialog = ref(false); +const current_csv_file = ref(null); +const current_csv_index = ref(-1); + const toggle_loading = useToggle(loading); +function isCsv(file) { + return allowCsvConfig && file.name.toLowerCase().endsWith(".csv"); +} + +function openCsvPreviewer(file, index) { + current_csv_file.value = file; + current_csv_index.value = index; + csv_dialog.value = true; +} + +function onCsvConfirm(result) { + const json_content = JSON.stringify(result, null, 2); + const blob = new Blob([json_content], { type: "application/json" }); + const json_filename = `${current_csv_file.value.name}.json`; + const json_file = new File([blob], json_filename, { + type: "application/json", + }); + + // Keep original CSV and append JSON config file + internal_files.value.push(json_file); + csv_dialog.value = false; + + // Mark CSV as configured (for UI purposes) + current_csv_file.value.isConfigured = true; +} + function processSelectedFiles(selected_files) { if (multiple) { internal_files.value = [...internal_files.value, ...selected_files]; @@ -48,6 +81,27 @@ function removeFile(index) { } async function upload_files() { + const hasUnconfiguredCsv = internal_files.value.some((file) => isCsv(file) && !file.isConfigured); + if (hasUnconfiguredCsv) { + const index = internal_files.value.findIndex((file) => isCsv(file) && !file.isConfigured); + openCsvPreviewer(internal_files.value[index], index); + return; + } + + // Dev-only: download config JSON files + internal_files.value.forEach((file) => { + if (file.name.endsWith(".csv.json")) { + const url = URL.createObjectURL(file); + const link = document.createElement("a"); + link.href = url; + link.download = file.name; + document.body.appendChild(link); + link.click(); + document.body.removeChild(link); + URL.revokeObjectURL(url); + } + }); + toggle_loading(); const promise_array = internal_files.value.map((file) => geodeStore.upload(file)); await Promise.all(promise_array); @@ -75,7 +129,10 @@ watch( watch(internal_files, (value) => { files_uploaded.value = false; if (auto_upload && value.length > 0) { - upload_files(); + const hasUnconfiguredCsv = value.some((file) => isCsv(file)); + if (!hasUnconfiguredCsv) { + upload_files(); + } } }); @@ -143,8 +200,25 @@ watch(internal_files, (value) => { style="background: rgba(255, 255, 255, 0.05) !important" @click:close="removeFile(index)" > - mdi-file-outline + + {{ file.name.endsWith(".json") ? "mdi-file-code" : "mdi-file-outline" }} + {{ file.name }} + + + + + @@ -167,6 +241,13 @@ watch(internal_files, (value) => { Upload {{ internal_files.length }} files + + diff --git a/app/components/MissingFilesSelector.vue b/app/components/MissingFilesSelector.vue index f81a3b89..c8eef8f6 100644 --- a/app/components/MissingFilesSelector.vue +++ b/app/components/MissingFilesSelector.vue @@ -9,10 +9,11 @@ const schema = schemas.opengeodeweb_back.missing_files; const emit = defineEmits(["update_values", "increment_step", "decrement_step"]); -const { multiple, geode_object_type, filenames } = defineProps({ +const { multiple, geode_object_type, filenames, files } = defineProps({ multiple: { type: Boolean, required: true }, geode_object_type: { type: String, required: true }, filenames: { type: Array, required: true }, + files: { type: Array, required: false, default: () => [] }, }); const accept = ref(""); @@ -35,6 +36,9 @@ async function missing_files() { const geodeStore = useGeodeStore(); const promise_array = filenames.map(async (filename) => { + if (filename.toLowerCase().endsWith(".csv")) { + return { has_missing_files: false, mandatory_files: [], additional_files: [] }; + } const response = await geodeStore.request(schema, { geode_object_type, filename, @@ -49,11 +53,15 @@ async function missing_files() { mandatory_files.value = [...mandatory_files.value, ...value.mandatory_files]; additional_files.value = [...additional_files.value, ...value.additional_files]; } - if (has_missing_files.value) { - accept.value = [...mandatory_files.value, ...additional_files.value] - .map((filename) => `.${filename.split(".").pop()}`) - .join(","); - } else { + const main_csvs = files.filter((f) => f.name.toLowerCase().endsWith(".csv")); + if (main_csvs.length > 0) { + has_missing_files.value = true; + if (accept.value === "") { + accept.value = ".json"; + } + } + + if (!has_missing_files.value) { emit("increment_step"); } toggle_loading(); @@ -85,7 +93,10 @@ await missing_files(); - + diff --git a/app/components/ObjectSelector.vue b/app/components/ObjectSelector.vue index d06b8af8..4f53ce48 100644 --- a/app/components/ObjectSelector.vue +++ b/app/components/ObjectSelector.vue @@ -68,6 +68,11 @@ async function get_allowed_objects() { final_object[key].object_priority = Math.max(...priorities); } } + const isCsv = filenames.some((f) => f.toLowerCase().endsWith(".csv")); + if (isCsv && !final_object["PointSet3D"]) { + final_object["PointSet3D"] = { is_loadable: 1, object_priority: 100 }; + } + allowed_objects.value = final_object; const selected_object = select_geode_object(final_object); if (selected_object) { diff --git a/app/components/csv-preview/CsvPreviewer.vue b/app/components/csv-preview/CsvPreviewer.vue new file mode 100644 index 00000000..c2c90617 --- /dev/null +++ b/app/components/csv-preview/CsvPreviewer.vue @@ -0,0 +1,255 @@ + + + + + diff --git a/app/components/csv-preview/CsvSettings.vue b/app/components/csv-preview/CsvSettings.vue new file mode 100644 index 00000000..48ae612e --- /dev/null +++ b/app/components/csv-preview/CsvSettings.vue @@ -0,0 +1,150 @@ + + + + + diff --git a/app/components/csv-preview/CsvTable.vue b/app/components/csv-preview/CsvTable.vue new file mode 100644 index 00000000..1d67b31d --- /dev/null +++ b/app/components/csv-preview/CsvTable.vue @@ -0,0 +1,107 @@ + + + + + From 20708464a56ea4c9e9a9a2e8e7ef1b0f2fa61a36 Mon Sep 17 00:00:00 2001 From: SpliiT Date: Tue, 12 May 2026 14:00:19 +0200 Subject: [PATCH 02/14] csv preview & import json --- app/components/FileSelector.vue | 4 +- app/components/FileUploader.vue | 288 ++++++++++---------- app/components/MissingFilesSelector.vue | 10 +- app/components/ObjectSelector.vue | 7 +- app/components/csv-preview/CsvPreviewer.vue | 146 +++++----- app/components/csv-preview/CsvSettings.vue | 28 +- app/components/csv-preview/CsvTable.vue | 30 +- app/utils/import_workflow.js | 4 - 8 files changed, 272 insertions(+), 245 deletions(-) diff --git a/app/components/FileSelector.vue b/app/components/FileSelector.vue index bdd0c0db..a283b82c 100644 --- a/app/components/FileSelector.vue +++ b/app/components/FileSelector.vue @@ -9,11 +9,12 @@ const schema = schemas.opengeodeweb_back.allowed_files; const emit = defineEmits(["update_values", "increment_step", "decrement_step"]); -const { multiple, files, auto_upload, show_overlay } = defineProps({ +const { multiple, files, auto_upload, show_overlay, allow_csv_config } = defineProps({ multiple: { type: Boolean, required: true }, files: { type: Array, default: () => [] }, auto_upload: { type: Boolean, default: true }, show_overlay: { type: Boolean, default: true }, + allow_csv_config: { type: Boolean, default: false }, }); const internal_files = ref(files); @@ -69,6 +70,7 @@ await get_allowed_files(); files: internal_files, auto_upload: internal_auto_upload, show_overlay, + allow_csv_config, }" @files_uploaded="files_uploaded_event" /> diff --git a/app/components/FileUploader.vue b/app/components/FileUploader.vue index 476ef069..7d693629 100644 --- a/app/components/FileUploader.vue +++ b/app/components/FileUploader.vue @@ -1,45 +1,29 @@