From d66cf51241d391ccd17f80767e4056f0b786c0e0 Mon Sep 17 00:00:00 2001
From: Kirill Stytsenko <kirill@styts.com>
Date: Tue, 1 Mar 2022 18:01:10 +0100
Subject: [PATCH] Various improvements in csv import. Closes #131

Former-commit-id: ee99e9141359abe2678a3cd1284092ff8a0f6098
---
 fda-ui/components/dialogs/CreateDB.vue        |  6 +--
 .../_database_id/table/_table_id/import.vue   | 19 +++++++--
 .../database/_database_id/table/import.vue    | 41 +++++++++++--------
 fda-ui/utils/index.js                         | 32 +++++++++++++++
 4 files changed, 74 insertions(+), 24 deletions(-)
 create mode 100644 fda-ui/utils/index.js

diff --git a/fda-ui/components/dialogs/CreateDB.vue b/fda-ui/components/dialogs/CreateDB.vue
index ee59c65cbf..ee5bcf87c8 100644
--- a/fda-ui/components/dialogs/CreateDB.vue
+++ b/fda-ui/components/dialogs/CreateDB.vue
@@ -68,6 +68,8 @@
 </template>
 
 <script>
+const { notEmpty } = require('@/utils')
+
 export default {
   data () {
     return {
@@ -120,9 +122,7 @@ export default {
         setTimeout(resolve, ms)
       })
     },
-    notEmpty (str) {
-      return typeof str === 'string' && str.trim().length > 0
-    },
+    notEmpty,
     async createDB () {
       let res
       // create a container
diff --git a/fda-ui/pages/container/_container_id/database/_database_id/table/_table_id/import.vue b/fda-ui/pages/container/_container_id/database/_database_id/table/_table_id/import.vue
index 08b02651cd..7fd8e3bb71 100644
--- a/fda-ui/pages/container/_container_id/database/_database_id/table/_table_id/import.vue
+++ b/fda-ui/pages/container/_container_id/database/_database_id/table/_table_id/import.vue
@@ -73,11 +73,14 @@
         </v-row>
       </v-card-text>
       <v-card-actions>
-        <v-col>
-          <v-btn :disabled="!file" :loading="loading" color="primary" @click="upload">Next</v-btn>
-        </v-col>
+        <v-btn :disabled="!file" :loading="loading" color="primary" @click="upload">Upload</v-btn>
+        <v-btn :to="`/container/${$route.params.container_id}/database/${$route.params.database_id}/table/${$route.params.table_id}`" outlined>
+          <v-icon>mdi-table</v-icon>
+          View
+        </v-btn>
       </v-card-actions>
     </v-card>
+    <v-breadcrumbs :items="items" class="pa-0 mt-2" />
   </div>
 </template>
 <script>
@@ -107,7 +110,15 @@ export default {
         false_element: null
       },
       file: null,
-      fileLocation: null
+      fileLocation: null,
+      items: [
+        { text: 'Databases', to: '/container', activeClass: '' },
+        {
+          text: `${this.$route.params.database_id}`,
+          to: `/container/${this.$route.params.container_id}/database/${this.$route.params.database_id}/info`,
+          activeClass: ''
+        }
+      ]
     }
   },
   computed: {
diff --git a/fda-ui/pages/container/_container_id/database/_database_id/table/import.vue b/fda-ui/pages/container/_container_id/database/_database_id/table/import.vue
index f9b0e65e51..881a42b638 100644
--- a/fda-ui/pages/container/_container_id/database/_database_id/table/import.vue
+++ b/fda-ui/pages/container/_container_id/database/_database_id/table/import.vue
@@ -14,7 +14,7 @@
             <v-col cols="8">
               <v-text-field
                 v-model="tableCreate.name"
-                required
+                :rules="[v => notEmpty(v) || $t('Required')]"
                 autocomplete="off"
                 label="Name *" />
             </v-col>
@@ -23,14 +23,14 @@
             <v-col cols="8">
               <v-text-field
                 v-model="tableCreate.description"
-                required
+                :rules="[v => notEmpty(v) || $t('Required')]"
                 autocomplete="off"
                 label="Description *" />
             </v-col>
           </v-row>
           <v-row dense>
             <v-col cols="8">
-              <v-btn :disabled="!step1Valid" color="primary" type="submit" @click="step = 2">
+              <v-btn :disabled="!validStep1" color="primary" type="submit" @click="step = 2">
                 Continue
               </v-btn>
             </v-col>
@@ -48,7 +48,7 @@
             <v-col cols="8">
               <v-select
                 v-model="tableCreate.separator"
-                :rules="[rules.required]"
+                :rules="[v => notEmpty(v) || $t('Required')]"
                 :items="separators"
                 required
                 hint="Character separating the values"
@@ -59,10 +59,12 @@
             <v-col cols="8">
               <v-text-field
                 v-model="tableCreate.skip_lines"
-                :rules="[rules.required, rules.positive]"
+                :rules="[
+                  v => notEmpty(v) || $t('Required'),
+                  v => isNonNegativeInteger(v) || $t('Number of lines to skip')]"
                 type="number"
                 required
-                hint="Skip n lines from the top"
+                hint="Skip n lines from the top. These may include comments or the header of column names."
                 label="Skip Lines *"
                 placeholder="e.g. 0" />
             </v-col>
@@ -96,7 +98,7 @@
           </v-row>
           <v-row dense>
             <v-col cols="6">
-              <v-btn :disabled="!tableCreate.separator || !tableCreate.skip_lines" :loading="loading" color="primary" type="submit" @click="step = 3">Next</v-btn>
+              <v-btn :disabled="!validStep2 || !tableCreate.separator || !tableCreate.skip_lines" :loading="loading" color="primary" type="submit" @click="step = 3">Next</v-btn>
             </v-col>
           </v-row>
         </v-form>
@@ -216,6 +218,8 @@
   </div>
 </template>
 <script>
+const { notEmpty, isNonNegativeInteger } = require('@/utils')
+
 export default {
   name: 'TableFromCSV',
   components: {
@@ -245,8 +249,7 @@ export default {
         }
       ],
       rules: {
-        required: value => !!value || 'Required',
-        positive: value => value >= 0 || 'Positive number'
+        required: value => !!value || 'Required'
       },
       dateFormats: [],
       tableCreate: {
@@ -257,7 +260,7 @@ export default {
         true_element: null,
         null_element: null,
         separator: ',',
-        skip_lines: 0
+        skip_lines: '1'
       },
       loading: false,
       file: null,
@@ -277,9 +280,6 @@ export default {
     }
   },
   computed: {
-    step1Valid () {
-      return this.tableCreate.name !== null && this.tableCreate.name.length > 0 && this.tableCreate.description !== null && this.tableCreate.description.length > 0
-    },
     token () {
       return this.$store.state.token
     }
@@ -288,6 +288,8 @@ export default {
     this.loadDateFormats()
   },
   methods: {
+    notEmpty,
+    isNonNegativeInteger,
     submit () {
       this.$refs.form.validate()
     },
@@ -343,19 +345,24 @@ export default {
     },
     async createTable () {
       /* make enum values to array */
-      this.tableCreate.columns.forEach((column) => {
+      const validColumns = this.tableCreate.columns.map((column) => {
         // validate `id` column: must be a PK
         if (column.name === 'id' && (!column.primary_key)) {
           this.$toast.error('Column `id` has to be a Primary Key')
-          return
+          return false
         }
-        if (column.enum_values == null) {
-          return
+        if (column.enum_values === null) {
+          return false
         }
         if (column.enum_values.length > 0) {
           column.enum_values = column.enum_values.split(',')
         }
+        return true
       })
+
+      // bail out if there is a problem with one of the columns
+      if (!validColumns.every(Boolean)) { return }
+
       const createUrl = `/api/container/${this.$route.params.container_id}/database/${this.$route.params.database_id}/table`
       let createResult
       try {
diff --git a/fda-ui/utils/index.js b/fda-ui/utils/index.js
new file mode 100644
index 0000000000..58c28e61d3
--- /dev/null
+++ b/fda-ui/utils/index.js
@@ -0,0 +1,32 @@
+function notEmpty (str) {
+  return typeof str === 'string' && str.trim().length > 0
+}
+
+/**
+ * From https://stackoverflow.com/questions/10834796/validate-that-a-string-is-a-positive-integer
+
+ Tests:
+
+ "0"                     : true
+ "23"                    : true
+ "-10"                   : false
+ "10.30"                 : false
+ "-40.1"                 : false
+ "string"                : false
+ "1234567890"            : true
+ "129000098131766699.1"  : false
+ "-1e7"                  : false
+ "1e7"                   : true
+ "1e10"                  : false
+ "1edf"                  : false
+ " "                     : false
+ ""                      : false
+ */
+function isNonNegativeInteger (str) {
+  return str >>> 0 === parseFloat(str)
+}
+
+module.exports = {
+  notEmpty,
+  isNonNegativeInteger
+}
-- 
GitLab