@geode/opengeodeweb-front 10.18.0-rc.2 → 10.18.0-rc.3

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.
@@ -1,36 +1,59 @@
1
1
  <script setup>
2
2
  import { useGeodeStore } from "@ogw_front/stores/geode";
3
- import { useTemplateRef } from "vue";
4
3
 
4
+ import CsvPreviewer from "@ogw_front/components/csv-preview/CsvPreviewer";
5
5
  import DragAndDrop from "@ogw_front/components/DragAndDrop";
6
6
 
7
7
  const emit = defineEmits(["files_uploaded", "decrement_step", "reset_values"]);
8
8
 
9
- const {
10
- multiple,
11
- accept,
12
- files,
13
- auto_upload,
14
- mini,
15
- show_overlay: showOverlay,
16
- } = defineProps({
17
- multiple: { type: Boolean, required: true },
18
- accept: { type: String, required: true },
19
- files: { type: Array, required: false, default: [] },
20
- auto_upload: { type: Boolean, required: false, default: false },
21
- mini: { type: Boolean, required: false, default: false },
22
- show_overlay: { type: Boolean, required: false, default: true },
9
+ const { multiple, accept, files, auto_upload, showOverlay, mini } = defineProps({
10
+ multiple: { type: Boolean, default: false },
11
+ accept: { type: String, default: "" },
12
+ files: { type: Array, default: () => [] },
13
+ auto_upload: { type: Boolean, default: true },
14
+ showOverlay: { type: Boolean, default: false },
15
+ mini: { type: Boolean, default: false },
23
16
  });
24
17
 
25
18
  const geodeStore = useGeodeStore();
26
-
27
19
  const internal_files = ref(files);
20
+ const dragAndDropRef = useTemplateRef("dragAndDropRef");
21
+ const csv_dialog = ref(false);
22
+ const current_csv_file = ref(undefined);
23
+ const current_csv_index = ref(-1);
28
24
  const loading = ref(false);
29
25
  const files_uploaded = ref(false);
30
- const dragAndDropRef = useTemplateRef("dragAndDropRef");
31
-
32
26
  const toggle_loading = useToggle(loading);
33
27
 
28
+ function isCsv(file) {
29
+ return file.name.toLowerCase().endsWith(".csv");
30
+ }
31
+
32
+ function openCsvPreviewer(file, index) {
33
+ current_csv_file.value = file;
34
+ current_csv_index.value = index;
35
+ csv_dialog.value = true;
36
+ }
37
+
38
+ async function onCsvConfirm(result) {
39
+ const json_content = JSON.stringify(result, undefined, 2);
40
+ const base_name = current_csv_file.value.name.slice(
41
+ 0,
42
+ current_csv_file.value.name.lastIndexOf("."),
43
+ );
44
+ const json_filename = `${base_name}.json`;
45
+
46
+ const blob = new Blob([json_content], { type: "application/json" });
47
+ const json_file = new File([blob], json_filename, {
48
+ type: "application/json",
49
+ });
50
+
51
+ current_csv_file.value.isConfigured = true;
52
+ await geodeStore.upload(json_file);
53
+ internal_files.value = [...internal_files.value];
54
+ csv_dialog.value = false;
55
+ }
56
+
34
57
  function processSelectedFiles(selected_files) {
35
58
  if (multiple) {
36
59
  internal_files.value = [...internal_files.value, ...selected_files];
@@ -52,11 +75,32 @@ async function upload_files() {
52
75
  const promise_array = internal_files.value.map((file) => geodeStore.upload(file));
53
76
  await Promise.all(promise_array);
54
77
  files_uploaded.value = true;
55
- emit("files_uploaded", internal_files.value);
56
-
57
78
  toggle_loading();
79
+ emit("files_uploaded", internal_files.value);
58
80
  }
59
81
 
82
+ watch(
83
+ () => internal_files.value,
84
+ async (newFiles) => {
85
+ if (newFiles.length === 0) {
86
+ return;
87
+ }
88
+ const unconfiguredCsv = newFiles.find((file) => isCsv(file) && !file.isConfigured);
89
+
90
+ if (unconfiguredCsv) {
91
+ openCsvPreviewer(unconfiguredCsv, internal_files.value.indexOf(unconfiguredCsv));
92
+ return;
93
+ }
94
+
95
+ const allConfigured = newFiles.every((file) => !isCsv(file) || file.isConfigured);
96
+
97
+ if (auto_upload && allConfigured) {
98
+ await upload_files();
99
+ }
100
+ },
101
+ { deep: true },
102
+ );
103
+
60
104
  if (files.length > 0) {
61
105
  internal_files.value = files;
62
106
  if (auto_upload) {
@@ -71,33 +115,26 @@ watch(
71
115
  },
72
116
  { deep: true },
73
117
  );
74
-
75
- watch(internal_files, (value) => {
76
- files_uploaded.value = false;
77
- if (auto_upload && value.length > 0) {
78
- upload_files();
79
- }
80
- });
81
118
  </script>
82
119
 
83
120
  <template>
84
- <DragAndDrop
85
- v-if="!internal_files.length"
86
- ref="dragAndDropRef"
87
- :multiple
88
- :accept
89
- :loading
90
- :show-extensions="false"
91
- :inline="true"
92
- :show-overlay="showOverlay"
93
- :texts="{
94
- idle: 'Select files',
95
- drop: 'Drop files here',
96
- loading: 'Loading...',
97
- }"
98
- @files-selected="processSelectedFiles"
99
- />
100
-
121
+ <template v-if="mini">
122
+ <v-btn
123
+ icon="mdi-plus"
124
+ variant="text"
125
+ color="primary"
126
+ :loading="loading"
127
+ class="mt-2"
128
+ @click="dragAndDropRef?.triggerFileDialog"
129
+ />
130
+ <DragAndDrop
131
+ ref="dragAndDropRef"
132
+ class="d-none"
133
+ :multiple
134
+ :accept
135
+ @files-selected="processSelectedFiles"
136
+ />
137
+ </template>
101
138
  <DragAndDrop
102
139
  v-else
103
140
  ref="dragAndDropRef"
@@ -105,7 +142,7 @@ watch(internal_files, (value) => {
105
142
  :accept
106
143
  :loading
107
144
  :show-extensions="false"
108
- :inline="false"
145
+ :inline="!internal_files.length"
109
146
  :show-overlay="showOverlay"
110
147
  @files-selected="processSelectedFiles"
111
148
  />
@@ -132,23 +169,50 @@ watch(internal_files, (value) => {
132
169
  </v-sheet>
133
170
 
134
171
  <v-sheet class="d-flex flex-wrap ga-2" color="transparent">
135
- <v-chip
136
- v-for="(file, index) in internal_files"
137
- :key="index"
138
- closable
139
- size="default"
140
- color="white"
141
- variant="outlined"
142
- class="font-weight-medium glass-ui border-opacity-10 px-4"
143
- style="background: rgba(255, 255, 255, 0.05) !important"
144
- @click:close="removeFile(index)"
145
- >
146
- <v-icon start size="18" color="primary">mdi-file-outline</v-icon>
147
- <span class="text-white">{{ file.name }}</span>
148
- <template #close>
149
- <v-icon size="16" class="ml-2 opacity-60 hover-opacity-100">mdi-close-circle</v-icon>
150
- </template>
151
- </v-chip>
172
+ <template v-for="(file, index) in internal_files" :key="index">
173
+ <v-chip
174
+ closable
175
+ size="default"
176
+ color="white"
177
+ variant="outlined"
178
+ class="font-weight-medium glass-ui border-opacity-10 px-4"
179
+ style="background: rgba(255, 255, 255, 0.05) !important"
180
+ @click:close="removeFile(index)"
181
+ >
182
+ <v-icon start size="18" :color="isCsv(file) && file.isConfigured ? 'success' : 'primary'">
183
+ {{
184
+ isCsv(file)
185
+ ? file.isConfigured
186
+ ? "mdi-file-check"
187
+ : "mdi-file-table"
188
+ : "mdi-file-outline"
189
+ }}
190
+ </v-icon>
191
+ <span class="text-white">{{ file.displayName || file.name }}</span>
192
+
193
+ <v-tooltip v-if="isCsv(file)" text="Configure CSV" location="bottom">
194
+ <template #activator="{ props: tooltipProps }">
195
+ <v-btn
196
+ v-bind="tooltipProps"
197
+ icon="mdi-cog"
198
+ variant="flat"
199
+ :color="file.isConfigured ? 'success' : 'primary'"
200
+ :class="['ml-2', { 'pulse-animation': !file.isConfigured }]"
201
+ width="24"
202
+ height="24"
203
+ density="compact"
204
+ @click.stop="openCsvPreviewer(file, index)"
205
+ >
206
+ <v-icon size="14">mdi-cog</v-icon>
207
+ </v-btn>
208
+ </template>
209
+ </v-tooltip>
210
+
211
+ <template #close>
212
+ <v-icon size="16" class="ml-2 opacity-60 hover-opacity-100">mdi-close-circle</v-icon>
213
+ </template>
214
+ </v-chip>
215
+ </template>
152
216
  </v-sheet>
153
217
  </v-card-text>
154
218
 
@@ -164,24 +228,40 @@ watch(internal_files, (value) => {
164
228
  @click="upload_files"
165
229
  >
166
230
  <v-icon start size="22">mdi-cloud-upload</v-icon>
167
- Upload {{ internal_files.length }} file<span v-if="internal_files.length > 1">s</span>
231
+ Upload
232
+ {{ internal_files.length }}
233
+ file<span v-if="internal_files.length > 1">s</span>
168
234
  </v-btn>
169
235
  </v-card-actions>
236
+
237
+ <CsvPreviewer
238
+ v-if="current_csv_file"
239
+ v-model="csv_dialog"
240
+ :file="current_csv_file"
241
+ @confirm="onCsvConfirm"
242
+ />
170
243
  </template>
171
244
 
172
245
  <style scoped>
173
- .border-dashed {
174
- border: 2px dashed rgba(255, 255, 255, 0.1) !important;
175
- transition: all 0.3s ease;
246
+ .glass-ui {
247
+ background: rgba(255, 255, 255, 0.05) !important;
248
+ backdrop-filter: blur(10px);
176
249
  }
177
-
178
- .border-dashed:hover {
179
- border-color: rgba(var(--v-theme-primary), 0.4) !important;
180
- background: rgba(var(--v-theme-primary), 0.02) !important;
250
+ .hover-opacity-100:hover {
251
+ opacity: 1 !important;
181
252
  }
182
-
183
- .custom-upload-btn {
184
- letter-spacing: 0.5px;
185
- box-shadow: 0 4px 15px rgba(var(--v-theme-primary), 0.3);
253
+ .pulse-animation {
254
+ animation: pulse 2s infinite;
255
+ }
256
+ @keyframes pulse {
257
+ 0% {
258
+ box-shadow: 0 0 0 0 rgba(var(--v-theme-primary), 0.4);
259
+ }
260
+ 70% {
261
+ box-shadow: 0 0 0 10px rgba(var(--v-theme-primary), 0);
262
+ }
263
+ 100% {
264
+ box-shadow: 0 0 0 0 rgba(var(--v-theme-primary), 0);
265
+ }
186
266
  }
187
267
  </style>
@@ -9,10 +9,11 @@ const schema = schemas.opengeodeweb_back.missing_files;
9
9
 
10
10
  const emit = defineEmits(["update_values", "increment_step", "decrement_step"]);
11
11
 
12
- const { multiple, geode_object_type, filenames } = defineProps({
12
+ const { multiple, geode_object_type, filenames, files } = defineProps({
13
13
  multiple: { type: Boolean, required: true },
14
14
  geode_object_type: { type: String, required: true },
15
15
  filenames: { type: Array, required: true },
16
+ files: { type: Array, required: false, default: () => [] },
16
17
  });
17
18
 
18
19
  const accept = ref("");
@@ -34,12 +35,17 @@ async function missing_files() {
34
35
  additional_files.value = [];
35
36
  const geodeStore = useGeodeStore();
36
37
 
37
- const promise_array = filenames.map(async (filename) => {
38
- const response = await geodeStore.request(schema, {
39
- geode_object_type,
40
- filename,
41
- });
42
- return response;
38
+ const promise_array = filenames.map((filename) => {
39
+ const isCsvFile =
40
+ filename.toLowerCase().endsWith(".csv") || filename.toLowerCase().endsWith(".csv.json");
41
+ if (isCsvFile) {
42
+ return Promise.resolve({
43
+ has_missing_files: false,
44
+ mandatory_files: [],
45
+ additional_files: [],
46
+ });
47
+ }
48
+ return geodeStore.request(schema, { geode_object_type, filename });
43
49
  });
44
50
  const values = await Promise.all(promise_array);
45
51
  for (const value of values) {
@@ -49,11 +55,19 @@ async function missing_files() {
49
55
  mandatory_files.value = [...mandatory_files.value, ...value.mandatory_files];
50
56
  additional_files.value = [...additional_files.value, ...value.additional_files];
51
57
  }
52
- if (has_missing_files.value) {
53
- accept.value = [...mandatory_files.value, ...additional_files.value]
54
- .map((filename) => `.${filename.split(".").pop()}`)
55
- .join(",");
56
- } else {
58
+ const unconfigured_csvs = files.filter(
59
+ (file) =>
60
+ (file.name.toLowerCase().endsWith(".csv") || file.name.toLowerCase().endsWith(".csv.json")) &&
61
+ !file.isConfigured,
62
+ );
63
+ if (unconfigured_csvs.length > 0) {
64
+ has_missing_files.value = true;
65
+ if (accept.value === "") {
66
+ accept.value = ".json";
67
+ }
68
+ }
69
+
70
+ if (!has_missing_files.value) {
57
71
  emit("increment_step");
58
72
  }
59
73
  toggle_loading();
@@ -85,7 +99,10 @@ await missing_files();
85
99
  </v-row>
86
100
  <v-row>
87
101
  <v-col cols="12">
88
- <FileUploader v-bind="{ multiple, accept }" @files_uploaded="files_uploaded_event" />
102
+ <FileUploader
103
+ v-bind="{ multiple, accept, files, auto_upload: false }"
104
+ @files_uploaded="files_uploaded_event"
105
+ />
89
106
  </v-col>
90
107
  </v-row>
91
108
  <v-row>
@@ -62,12 +62,20 @@ async function get_allowed_objects() {
62
62
  const load_scores = allowed_objects_list.map((obj) => obj[key].is_loadable);
63
63
  const priorities = allowed_objects_list
64
64
  .map((obj) => obj[key].object_priority)
65
- .filter((priority) => priority !== undefined && priority !== null);
65
+ .filter((priority) => priority !== undefined);
66
66
  final_object[key] = { is_loadable: Math.min(...load_scores) };
67
67
  if (priorities.length > 0) {
68
68
  final_object[key].object_priority = Math.max(...priorities);
69
69
  }
70
70
  }
71
+ const isCsv = filenames.some(
72
+ (filename) =>
73
+ filename.toLowerCase().endsWith(".csv") || filename.toLowerCase().endsWith(".csv.json"),
74
+ );
75
+ if (isCsv && !final_object["PointSet3D"]) {
76
+ final_object["PointSet3D"] = { is_loadable: 1, object_priority: 100 };
77
+ }
78
+
71
79
  allowed_objects.value = final_object;
72
80
  const selected_object = select_geode_object(final_object);
73
81
  if (selected_object) {
@@ -0,0 +1,277 @@
1
+ <script setup>
2
+ import CsvSettings from "./CsvSettings.vue";
3
+ import CsvTable from "./CsvTable.vue";
4
+ import { useToggle } from "@vueuse/core";
5
+
6
+ const { file, modelValue } = defineProps({
7
+ file: { type: Object, required: true },
8
+ modelValue: { type: Boolean, default: false },
9
+ });
10
+
11
+ const MAX_CONTENT_SLICE = 1000;
12
+ const MAX_LINES_FOR_DETECTION = 5;
13
+ const MIN_AVG_COUNT = 1.5;
14
+ const MAX_VARIANCE = 0.5;
15
+ const PREVIEW_ROWS_LIMIT = 101;
16
+
17
+ const emit = defineEmits(["update:modelValue", "confirm"]);
18
+
19
+ const separator = ref(",");
20
+ const headerRow = ref(0);
21
+ const firstRow = ref(1);
22
+
23
+ const xColumn = ref(undefined);
24
+ const yColumn = ref(undefined);
25
+ const zColumn = ref(undefined);
26
+
27
+ const rawContent = ref("");
28
+ const previewRows = ref([]);
29
+ const previewHeaders = ref([]);
30
+ const loading = ref(false);
31
+ const toggleLoading = useToggle(loading);
32
+
33
+ function autoDetectSeparator(content) {
34
+ const lines = content
35
+ .slice(0, MAX_CONTENT_SLICE)
36
+ .split(/\r?\n/u)
37
+ .slice(0, MAX_LINES_FOR_DETECTION);
38
+ const candidates = [",", ";", "\t", "|"];
39
+ let best = ",";
40
+ let maxCount = -1;
41
+
42
+ for (const candidate of candidates) {
43
+ const counts = lines.map((line) => line.split(candidate).length);
44
+ const average = counts.reduce((total, count) => total + count, 0) / counts.length;
45
+ const variance =
46
+ counts.reduce((total, count) => total + (count - average) ** 2, 0) / counts.length;
47
+
48
+ if (average > MIN_AVG_COUNT && variance < MAX_VARIANCE && average > maxCount) {
49
+ maxCount = average;
50
+ best = candidate;
51
+ }
52
+ }
53
+ return best;
54
+ }
55
+
56
+ function readAndParse() {
57
+ if (!file) {
58
+ return;
59
+ }
60
+ toggleLoading(true);
61
+
62
+ const reader = new FileReader();
63
+ reader.addEventListener("load", (event) => {
64
+ rawContent.value = event.target.result;
65
+ if (!separator.value || separator.value === ",") {
66
+ separator.value = autoDetectSeparator(rawContent.value);
67
+ }
68
+ parseContent();
69
+ toggleLoading(false);
70
+ });
71
+ reader.addEventListener("error", () => {
72
+ toggleLoading(false);
73
+ });
74
+ reader.readAsText(file, "utf8");
75
+ }
76
+
77
+ function parseContent() {
78
+ if (!rawContent.value) {
79
+ return;
80
+ }
81
+
82
+ const allLines = rawContent.value.split(/\r?\n/u).filter((line) => line.trim() !== "");
83
+
84
+ function splitLine(line) {
85
+ if (!separator.value) {
86
+ return [line];
87
+ }
88
+ const result = [];
89
+ let current = "";
90
+ let inQuotes = false;
91
+ for (let index = 0; index < line.length; index += 1) {
92
+ const char = line[index];
93
+ if (char === '"') {
94
+ inQuotes = !inQuotes;
95
+ } else if (char === separator.value && !inQuotes) {
96
+ result.push(current.trim());
97
+ current = "";
98
+ } else {
99
+ current += char;
100
+ }
101
+ }
102
+ result.push(current.trim());
103
+ return result;
104
+ }
105
+
106
+ const headerLine = allLines[headerRow.value];
107
+ const rawHeaders = headerLine ? splitLine(headerLine) : [];
108
+
109
+ previewHeaders.value = rawHeaders.map((header, index) => ({
110
+ title: header || `Column ${index + 1}`,
111
+ key: `col${index}`,
112
+ align: "start",
113
+ sortable: true,
114
+ }));
115
+
116
+ const dataLines = allLines.slice(firstRow.value, firstRow.value + PREVIEW_ROWS_LIMIT);
117
+ previewRows.value = dataLines.map((line) => {
118
+ const row = splitLine(line);
119
+ const obj = {};
120
+ for (let index = 0; index < row.length; index += 1) {
121
+ obj[`col${index}`] = row[index];
122
+ }
123
+ return obj;
124
+ });
125
+ }
126
+ const computedResult = computed(() => {
127
+ const xIndex = previewHeaders.value.findIndex((header) => header.key === xColumn.value);
128
+ const yIndex = previewHeaders.value.findIndex((header) => header.key === yColumn.value);
129
+ const zIndex = previewHeaders.value.findIndex((header) => header.key === zColumn.value);
130
+
131
+ return {
132
+ firstRow: firstRow.value,
133
+ headerRow: headerRow.value,
134
+ separator: separator.value,
135
+ xColumn: xIndex === -1 ? 0 : xIndex,
136
+ yColumn: yIndex === -1 ? 1 : yIndex,
137
+ zColumn: zIndex === -1 ? 2 : zIndex,
138
+ };
139
+ });
140
+
141
+ watch([separator, headerRow, firstRow], () => {
142
+ parseContent();
143
+ xColumn.value = undefined;
144
+ yColumn.value = undefined;
145
+ zColumn.value = undefined;
146
+ });
147
+
148
+ watch(
149
+ () => modelValue,
150
+ (val) => {
151
+ if (val) {
152
+ readAndParse();
153
+ }
154
+ },
155
+ { immediate: true },
156
+ );
157
+
158
+ watch(
159
+ () => file,
160
+ (newFile) => {
161
+ if (newFile && modelValue) {
162
+ readAndParse();
163
+ }
164
+ },
165
+ );
166
+
167
+ function onConfirm() {
168
+ emit("confirm", computedResult.value);
169
+ emit("update:modelValue", false);
170
+ }
171
+ </script>
172
+
173
+ <template>
174
+ <v-dialog
175
+ :model-value="modelValue"
176
+ @update:model-value="emit('update:modelValue', $event)"
177
+ max-width="1200px"
178
+ >
179
+ <v-card class="glass-ui rounded-xl overflow-hidden border-opacity-10" color="grey-darken-4">
180
+ <v-toolbar color="transparent" flat class="px-4">
181
+ <v-icon icon="mdi-file-table" size="32" color="primary" class="ml-1" />
182
+ <v-toolbar-title class="text-h6 font-weight-bold text-white">
183
+ CSV Previewer & Configuration
184
+ </v-toolbar-title>
185
+ <v-spacer />
186
+ <v-btn
187
+ icon="mdi-close"
188
+ variant="text"
189
+ color="white"
190
+ @click="emit('update:modelValue', false)"
191
+ />
192
+ </v-toolbar>
193
+
194
+ <v-divider class="border-opacity-10" />
195
+
196
+ <div class="previewer-grid">
197
+ <CsvSettings
198
+ v-model:separator="separator"
199
+ v-model:header-row="headerRow"
200
+ v-model:first-row="firstRow"
201
+ v-model:x-column="xColumn"
202
+ v-model:y-column="yColumn"
203
+ v-model:z-column="zColumn"
204
+ :headers="previewHeaders"
205
+ />
206
+
207
+ <CsvTable
208
+ :headers="previewHeaders"
209
+ :rows="previewRows"
210
+ :loading="loading"
211
+ :coordinates="{ x: xColumn, y: yColumn, z: zColumn }"
212
+ :separator="separator"
213
+ :header-row="headerRow"
214
+ :first-row="firstRow"
215
+ />
216
+ </div>
217
+
218
+ <v-divider class="border-opacity-10" />
219
+
220
+ <v-card-actions class="pa-4 bg-white-opacity-5">
221
+ <v-spacer />
222
+ <v-btn
223
+ variant="text"
224
+ class="text-none px-6"
225
+ color="white"
226
+ @click="emit('update:modelValue', false)"
227
+ >
228
+ Cancel
229
+ </v-btn>
230
+ <v-btn
231
+ variant="flat"
232
+ color="primary"
233
+ class="text-none px-8 rounded-lg font-weight-bold"
234
+ @click="onConfirm"
235
+ :disabled="!previewHeaders.length"
236
+ >
237
+ Confirm
238
+ </v-btn>
239
+ </v-card-actions>
240
+ </v-card>
241
+ </v-dialog>
242
+ </template>
243
+
244
+ <style scoped>
245
+ .glass-ui {
246
+ background: rgba(30, 30, 30, 0.8) !important;
247
+ backdrop-filter: blur(20px);
248
+ -webkit-backdrop-filter: blur(20px);
249
+ border: 1px solid rgba(255, 255, 255, 0.1);
250
+ }
251
+
252
+ .previewer-grid {
253
+ display: grid;
254
+ grid-template-columns: 350px 1fr;
255
+ height: 70vh;
256
+ overflow: hidden;
257
+ }
258
+
259
+ .bg-white-opacity-5 {
260
+ background: rgba(255, 255, 255, 0.03);
261
+ }
262
+
263
+ /* Custom scrollbar */
264
+ ::-webkit-scrollbar {
265
+ width: 6px;
266
+ }
267
+ ::-webkit-scrollbar-track {
268
+ background: transparent;
269
+ }
270
+ ::-webkit-scrollbar-thumb {
271
+ background: rgba(255, 255, 255, 0.1);
272
+ border-radius: 10px;
273
+ }
274
+ ::-webkit-scrollbar-thumb:hover {
275
+ background: rgba(255, 255, 255, 0.2);
276
+ }
277
+ </style>
@@ -0,0 +1,170 @@
1
+ <script setup>
2
+ const { separator, headerRow, firstRow, xColumn, yColumn, zColumn, headers } = defineProps({
3
+ separator: { type: String, required: true },
4
+ headerRow: { type: Number, required: true },
5
+ firstRow: { type: Number, required: true },
6
+ xColumn: { type: String, default: undefined },
7
+ yColumn: { type: String, default: undefined },
8
+ zColumn: { type: String, default: undefined },
9
+ headers: { type: Array, default: () => [] },
10
+ });
11
+
12
+ const emit = defineEmits([
13
+ "update:separator",
14
+ "update:headerRow",
15
+ "update:firstRow",
16
+ "update:xColumn",
17
+ "update:yColumn",
18
+ "update:zColumn",
19
+ ]);
20
+
21
+ const separators = [
22
+ { title: "Comma (,)", value: "," },
23
+ { title: "Semicolon (;)", value: ";" },
24
+ { title: "Tab (\\t)", value: "\t" },
25
+ { title: "Pipe (|)", value: "|" },
26
+ ];
27
+
28
+ const internalSeparator = computed({
29
+ get: () => separator,
30
+ set: (value) => emit("update:separator", value),
31
+ });
32
+
33
+ const internalHeaderRow = computed({
34
+ get: () => headerRow,
35
+ set: (value) => emit("update:headerRow", value),
36
+ });
37
+
38
+ const internalFirstRow = computed({
39
+ get: () => firstRow,
40
+ set: (value) => emit("update:firstRow", value),
41
+ });
42
+
43
+ const internalXColumn = computed({
44
+ get: () => xColumn,
45
+ set: (value) => emit("update:xColumn", value),
46
+ });
47
+
48
+ const internalYColumn = computed({
49
+ get: () => yColumn,
50
+ set: (value) => emit("update:yColumn", value),
51
+ });
52
+
53
+ const internalZColumn = computed({
54
+ get: () => zColumn,
55
+ set: (value) => emit("update:zColumn", value),
56
+ });
57
+ </script>
58
+
59
+ <template>
60
+ <div class="pa-6 overflow-y-auto border-e border-opacity-10 bg-white-opacity-5">
61
+ <div class="text-overline mb-4 text-primary font-weight-bold">Parser Settings</div>
62
+
63
+ <v-select
64
+ v-model="internalSeparator"
65
+ :items="separators"
66
+ label="Separator"
67
+ variant="outlined"
68
+ density="compact"
69
+ class="mb-4"
70
+ rounded="lg"
71
+ />
72
+
73
+ <v-text-field
74
+ v-if="!separators.find((s) => s.value === separator)"
75
+ v-model="internalSeparator"
76
+ label="Custom Separator"
77
+ variant="outlined"
78
+ density="compact"
79
+ placeholder="Enter character"
80
+ class="mb-4"
81
+ rounded="lg"
82
+ />
83
+
84
+ <v-divider class="my-6 border-opacity-10" />
85
+
86
+ <div class="text-overline mb-4 text-primary font-weight-bold">Row Configuration</div>
87
+
88
+ <v-text-field
89
+ v-model.number="internalHeaderRow"
90
+ type="number"
91
+ label="Header Row"
92
+ variant="outlined"
93
+ density="compact"
94
+ hint="Index of the row containing headers"
95
+ persistent-hint
96
+ class="mb-4"
97
+ min="0"
98
+ rounded="lg"
99
+ />
100
+
101
+ <v-text-field
102
+ v-model.number="internalFirstRow"
103
+ type="number"
104
+ label="First Data Row"
105
+ variant="outlined"
106
+ density="compact"
107
+ hint="Index of the first row containing data"
108
+ persistent-hint
109
+ class="mb-6"
110
+ min="0"
111
+ rounded="lg"
112
+ />
113
+
114
+ <v-divider class="my-6 border-opacity-10" />
115
+
116
+ <div class="text-overline mb-4 text-primary font-weight-bold">Spatial Mapping</div>
117
+
118
+ <v-select
119
+ v-model="internalXColumn"
120
+ :items="headers"
121
+ item-title="title"
122
+ item-value="key"
123
+ label="X Column"
124
+ variant="outlined"
125
+ density="compact"
126
+ prepend-inner-icon="mdi-axis-x-arrow"
127
+ color="light-blue-accent-3"
128
+ class="mb-2"
129
+ clearable
130
+ rounded="lg"
131
+ />
132
+
133
+ <v-select
134
+ v-model="internalYColumn"
135
+ :items="headers"
136
+ item-title="title"
137
+ item-value="key"
138
+ label="Y Column"
139
+ variant="outlined"
140
+ density="compact"
141
+ prepend-inner-icon="mdi-axis-y-arrow"
142
+ color="light-green-accent-3"
143
+ class="mb-2"
144
+ clearable
145
+ rounded="lg"
146
+ />
147
+
148
+ <v-select
149
+ v-model="internalZColumn"
150
+ :items="headers"
151
+ item-title="title"
152
+ item-value="key"
153
+ label="Z Column"
154
+ variant="outlined"
155
+ density="compact"
156
+ prepend-inner-icon="mdi-axis-z-arrow"
157
+ color="red-accent-3"
158
+ class="mb-8"
159
+ clearable
160
+ rounded="lg"
161
+ />
162
+ <div class="pb-12" />
163
+ </div>
164
+ </template>
165
+
166
+ <style scoped>
167
+ .bg-white-opacity-5 {
168
+ background: rgba(255, 255, 255, 0.03);
169
+ }
170
+ </style>
@@ -0,0 +1,112 @@
1
+ <script setup>
2
+ const { headers, rows, loading, coordinates, separator, headerRow, firstRow } = defineProps({
3
+ headers: { type: Array, required: true },
4
+ rows: { type: Array, required: true },
5
+ loading: { type: Boolean, default: false },
6
+ coordinates: { type: Object, default: () => ({ x: undefined, y: undefined, z: undefined }) },
7
+ separator: { type: String, default: "," },
8
+ headerRow: { type: Number, default: 0 },
9
+ firstRow: { type: Number, default: 1 },
10
+ });
11
+
12
+ function getColumnClass(key) {
13
+ if (key === coordinates.x) {
14
+ return "x-col-highlight";
15
+ }
16
+ if (key === coordinates.y) {
17
+ return "y-col-highlight";
18
+ }
19
+ if (key === coordinates.z) {
20
+ return "z-col-highlight";
21
+ }
22
+ return "";
23
+ }
24
+ </script>
25
+
26
+ <template>
27
+ <div class="d-flex flex-column bg-black-opacity-20 overflow-hidden">
28
+ <div class="pa-4 d-flex align-center border-b border-opacity-10">
29
+ <v-icon icon="mdi-table-eye" size="small" class="mr-2 opacity-60" />
30
+ <span class="text-subtitle-2 font-weight-medium opacity-80">
31
+ Live Preview (First 100 rows)
32
+ </span>
33
+ <v-spacer />
34
+ <v-chip v-if="loading" size="x-small" color="primary" variant="flat">
35
+ <v-progress-circular indeterminate size="12" width="2" class="mr-2" />
36
+ Parsing...
37
+ </v-chip>
38
+ </div>
39
+
40
+ <v-data-table
41
+ :key="`${separator}-${headerRow}-${firstRow}`"
42
+ :headers="headers"
43
+ :items="rows"
44
+ class="bg-transparent"
45
+ density="compact"
46
+ hover
47
+ hide-default-footer
48
+ :loading="loading"
49
+ fixed-header
50
+ height="100%"
51
+ >
52
+ <template v-for="header in headers" v-slot:[`item.${header.key}`]="{ item }">
53
+ <div :class="getColumnClass(header.key)" class="px-2 py-1 rounded text-truncate">
54
+ {{ item[header.key] }}
55
+ </div>
56
+ </template>
57
+
58
+ <template #no-data>
59
+ <div class="d-flex flex-column align-center justify-center h-100 py-12 opacity-40">
60
+ <v-icon size="64" icon="mdi-table-off" />
61
+ <div class="text-h6 mt-2">No preview available</div>
62
+ <div class="text-caption">Check your parser settings</div>
63
+ </div>
64
+ </template>
65
+ </v-data-table>
66
+ </div>
67
+ </template>
68
+
69
+ <style scoped>
70
+ .bg-black-opacity-20 {
71
+ background: rgba(0, 0, 0, 0.2);
72
+ }
73
+
74
+ :deep(.v-data-table__th) {
75
+ background: #1e1e1e !important; /* Solid background to prevent overlap transparency */
76
+ color: rgba(var(--v-theme-primary), 0.9) !important;
77
+ font-weight: bold !important;
78
+ border-bottom: 1px solid rgba(255, 255, 255, 0.1) !important;
79
+ text-transform: uppercase;
80
+ font-size: 0.75rem;
81
+ letter-spacing: 0.5px;
82
+ min-width: 150px !important;
83
+ white-space: nowrap !important;
84
+ }
85
+
86
+ :deep(.v-data-table__td) {
87
+ border-bottom: 1px solid rgba(255, 255, 255, 0.05) !important;
88
+ color: rgba(255, 255, 255, 0.8) !important;
89
+ }
90
+
91
+ :deep(.v-data-table__tr:hover td) {
92
+ background: rgba(var(--v-theme-primary), 0.05) !important;
93
+ }
94
+
95
+ .x-col-highlight {
96
+ background: rgba(0, 176, 255, 0.2) !important;
97
+ color: #00b0ff !important;
98
+ font-weight: bold;
99
+ }
100
+
101
+ .y-col-highlight {
102
+ background: rgba(100, 221, 23, 0.2) !important;
103
+ color: #64dd17 !important;
104
+ font-weight: bold;
105
+ }
106
+
107
+ .z-col-highlight {
108
+ background: rgba(255, 23, 68, 0.2) !important;
109
+ color: #ff1744 !important;
110
+ font-weight: bold;
111
+ }
112
+ </style>
@@ -9,26 +9,16 @@ import { useGeodeStore } from "@ogw_front/stores/geode";
9
9
  import { useHybridViewerStore } from "@ogw_front/stores/hybrid_viewer";
10
10
  import { useTreeviewStore } from "@ogw_front/stores/treeview";
11
11
 
12
- const SECOND = 1000;
13
-
14
12
  async function importWorkflow(files) {
15
- console.log("importWorkflow", { files });
16
- const start = Date.now();
17
- const promise_array = [];
18
- for (const file of files) {
19
- const { filename, geode_object_type } = file;
20
- console.log({ filename }, { geode_object_type });
21
- promise_array.push(importFile(filename, geode_object_type));
22
- }
23
- const results = await Promise.all(promise_array);
13
+ const results = await Promise.all(
14
+ files.map(({ filename, geode_object_type }) => importFile(filename, geode_object_type)),
15
+ );
24
16
  const hybridViewerStore = useHybridViewerStore();
25
17
  hybridViewerStore.remoteRender();
26
- console.log("importWorkflow completed in", (Date.now() - start) / SECOND);
27
18
  return results;
28
19
  }
29
20
 
30
21
  function buildImportItemFromPayloadApi(value, geode_object_type) {
31
- console.log("buildImportItemFromPayloadApi", { value, geode_object_type });
32
22
  return {
33
23
  geode_object_type,
34
24
  ...value,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@geode/opengeodeweb-front",
3
- "version": "10.18.0-rc.2",
3
+ "version": "10.18.0-rc.3",
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": {
@@ -17,7 +17,7 @@ describe("crs selector", () => {
17
17
  beforeEach(() => {
18
18
  pinia = setupActivePinia();
19
19
  geodeStore = useGeodeStore();
20
- geodeStore.base_url = "";
20
+ geodeStore.base_url = "/";
21
21
  });
22
22
 
23
23
  test("default behavior", async () => {
@@ -21,7 +21,7 @@ const geodeStore = useGeodeStore();
21
21
 
22
22
  describe("extension selector", () => {
23
23
  beforeEach(() => {
24
- geodeStore.base_url = "";
24
+ geodeStore.base_url = "/";
25
25
 
26
26
  geodeStore.request = vi.fn(() => {
27
27
  const response = {
@@ -21,7 +21,7 @@ const upload_file_schema = schemas.opengeodeweb_back.upload_file;
21
21
  describe("file selector", () => {
22
22
  const pinia = setupActivePinia();
23
23
  const geodeStore = useGeodeStore();
24
- geodeStore.base_url = "";
24
+ geodeStore.base_url = "/";
25
25
 
26
26
  test("select file", async () => {
27
27
  registerEndpoint(allowed_files_schema.$id, {
@@ -52,6 +52,7 @@ describe("file selector", () => {
52
52
  writable: true,
53
53
  });
54
54
  await v_file_input.trigger("change");
55
+ await flushPromises();
55
56
  const v_btn = wrapper.findComponent(components.VBtn);
56
57
  await v_btn.trigger("click");
57
58
  await flushPromises();
@@ -18,7 +18,7 @@ const upload_file_schema = schemas.opengeodeweb_back.upload_file;
18
18
  describe("file uploader", () => {
19
19
  const pinia = setupActivePinia();
20
20
  const geodeStore = useGeodeStore();
21
- geodeStore.base_url = "";
21
+ geodeStore.base_url = "/";
22
22
 
23
23
  registerEndpoint(upload_file_schema.$id, {
24
24
  method: upload_file_schema.methods[FIRST_INDEX],
@@ -37,7 +37,7 @@ describe("file uploader", () => {
37
37
  global: {
38
38
  plugins: [vuetify, pinia],
39
39
  },
40
- props: { multiple: false, accept: "*.txt" },
40
+ props: { multiple: false, accept: "*.txt", auto_upload: false },
41
41
  });
42
42
 
43
43
  const v_file_input = wrapper.find('input[type="file"]');
@@ -46,6 +46,7 @@ describe("file uploader", () => {
46
46
  writable: true,
47
47
  });
48
48
  await v_file_input.trigger("change");
49
+ await flushPromises();
49
50
  const v_btn = wrapper.findComponent(components.VBtn);
50
51
 
51
52
  await v_btn.trigger("click");
@@ -12,7 +12,7 @@ import { useGeodeStore } from "@ogw_front/stores/geode";
12
12
  describe("inspector inspection button", () => {
13
13
  const pinia = setupActivePinia();
14
14
  const geodeStore = useGeodeStore();
15
- geodeStore.base_url = "";
15
+ geodeStore.base_url = "/";
16
16
 
17
17
  test("with issues", async () => {
18
18
  const inspection_result = {
@@ -20,7 +20,7 @@ const upload_file_schema = schemas.opengeodeweb_back.upload_file;
20
20
  describe("missing files selector", () => {
21
21
  const pinia = setupActivePinia();
22
22
  const geodeStore = useGeodeStore();
23
- geodeStore.base_url = "";
23
+ geodeStore.base_url = "/";
24
24
 
25
25
  test("select file", async () => {
26
26
  geodeStore.request = vi.fn((schema, params, callbacks) => {
@@ -57,6 +57,7 @@ describe("missing files selector", () => {
57
57
  writable: true,
58
58
  });
59
59
  await v_file_input.trigger("change");
60
+ await flushPromises();
60
61
  const v_btn = file_uploader.findComponent(components.VBtn);
61
62
 
62
63
  registerEndpoint(upload_file_schema.$id, {
@@ -19,7 +19,7 @@ const { allowed_objects } = schemas.opengeodeweb_back;
19
19
  describe("object selector", () => {
20
20
  const pinia = setupActivePinia();
21
21
  const geodeStore = useGeodeStore();
22
- geodeStore.base_url = "";
22
+ geodeStore.base_url = "/";
23
23
 
24
24
  test("loadable with one class", async () => {
25
25
  const response = {
@@ -11,7 +11,7 @@ describe("packages versions", () => {
11
11
  test("mount", async () => {
12
12
  const pinia = setupActivePinia();
13
13
  const geodeStore = useGeodeStore();
14
- geodeStore.base_url = "";
14
+ geodeStore.base_url = "/";
15
15
 
16
16
  const schema = {
17
17
  $id: "/versions",