From c37dcccc4df1e704adbf7c7f3be403e0da16f774 Mon Sep 17 00:00:00 2001
From: dblythy <daniel-blyth@live.com.au>
Date: Wed, 18 Jan 2023 18:20:19 +1100
Subject: [PATCH 1/4] feat: add import dialog

---
 src/components/FileEditor/FileEditor.react.js |  2 +-
 src/components/Modal/Modal.scss               |  2 +-
 src/dashboard/Data/Browser/Browser.react.js   | 19 +++-
 .../Data/Browser/BrowserToolbar.react.js      |  6 ++
 .../Data/Browser/ImportDialog.react.js        | 97 +++++++++++++++++++
 5 files changed, 123 insertions(+), 3 deletions(-)
 create mode 100644 src/dashboard/Data/Browser/ImportDialog.react.js

diff --git a/src/components/FileEditor/FileEditor.react.js b/src/components/FileEditor/FileEditor.react.js
index abc87b654b..b2adc35fd6 100644
--- a/src/components/FileEditor/FileEditor.react.js
+++ b/src/components/FileEditor/FileEditor.react.js
@@ -80,7 +80,7 @@ export default class FileEditor extends React.Component {
     return (
       <div ref={this.inputRef} style={{ minWidth: this.props.width, display: 'none' }} className={styles.editor}>
         <a className={styles.upload}>
-          <input ref={this.fileInputRef} id='fileInput' type='file' onChange={this.handleChange.bind(this)} />
+          <input ref={this.fileInputRef} id='fileInput' type='file' onChange={this.handleChange.bind(this)} accept={this.props.accept} />
           <span>{file ? 'Replace file' : 'Upload file'}</span>
         </a>
       </div>
diff --git a/src/components/Modal/Modal.scss b/src/components/Modal/Modal.scss
index 171c32e4cf..ad34f3123b 100644
--- a/src/components/Modal/Modal.scss
+++ b/src/components/Modal/Modal.scss
@@ -43,7 +43,7 @@
   position: absolute;
   font-size: 14px;
   color: white;
-  top: 52px;
+  top: 56px;
   left: 28px;
 }
 
diff --git a/src/dashboard/Data/Browser/Browser.react.js b/src/dashboard/Data/Browser/Browser.react.js
index 8e35357351..8a20eb098c 100644
--- a/src/dashboard/Data/Browser/Browser.react.js
+++ b/src/dashboard/Data/Browser/Browser.react.js
@@ -16,6 +16,7 @@ import DeleteRowsDialog                   from 'dashboard/Data/Browser/DeleteRow
 import DropClassDialog                    from 'dashboard/Data/Browser/DropClassDialog.react';
 import EmptyState                         from 'components/EmptyState/EmptyState.react';
 import ExportDialog                       from 'dashboard/Data/Browser/ExportDialog.react';
+import ImportDialog                       from 'dashboard/Data/Browser/ImportDialog.react';
 import AttachRowsDialog                   from 'dashboard/Data/Browser/AttachRowsDialog.react';
 import AttachSelectedRowsDialog           from 'dashboard/Data/Browser/AttachSelectedRowsDialog.react';
 import CloneSelectedRowsDialog            from 'dashboard/Data/Browser/CloneSelectedRowsDialog.react';
@@ -56,6 +57,7 @@ class Browser extends DashboardView {
       showRemoveColumnDialog: false,
       showDropClassDialog: false,
       showExportDialog: false,
+      showImportDialog: false,
       showAttachRowsDialog: false,
       showEditRowDialog: false,
       showPointerKeyDialog: false,
@@ -101,6 +103,7 @@ class Browser extends DashboardView {
     this.showDeleteRows = this.showDeleteRows.bind(this);
     this.showDropClass = this.showDropClass.bind(this);
     this.showExport = this.showExport.bind(this);
+    this.showImport = this.showImport.bind(this);
     this.login = this.login.bind(this);
     this.logout = this.logout.bind(this);
     this.toggleMasterKeyUsage = this.toggleMasterKeyUsage.bind(this);
@@ -277,6 +280,10 @@ class Browser extends DashboardView {
     this.setState({ showExportDialog: true });
   }
 
+  showImport() {
+    this.setState({ showImportDialog: true });
+  }
+
   async login(username, password) {
     if (Parse.User.current()) {
       await Parse.User.logOut();
@@ -1089,6 +1096,7 @@ class Browser extends DashboardView {
       this.state.showRemoveColumnDialog ||
       this.state.showDropClassDialog ||
       this.state.showExportDialog ||
+      this.state.showImportDialog ||
       this.state.rowsToDelete ||
       this.state.showAttachRowsDialog ||
       this.state.showAttachSelectedRowsDialog ||
@@ -1539,6 +1547,7 @@ class Browser extends DashboardView {
             onExport={this.showExport}
             onChangeCLP={this.handleCLPChange}
             onRefresh={this.refresh}
+            onImport={this.showImport}
             onAttachRows={this.showAttachRowsDialog}
             onAttachSelectedRows={this.showAttachSelectedRowsDialog}
             onCloneSelectedRows={this.showCloneSelectedRowsDialog}
@@ -1659,7 +1668,15 @@ class Browser extends DashboardView {
           onCancel={() => this.setState({ showExportDialog: false })}
           onConfirm={() => this.exportClass(className)} />
       );
-    } else if (this.state.showAttachRowsDialog) {
+    }
+    else if (this.state.showImportDialog) {
+      extras = (
+        <ImportDialog
+          className={className}
+          onCancel={() => this.setState({ showImportDialog: false })}
+          onConfirm={() => this.exportClass(className)} />
+      );
+    }else if (this.state.showAttachRowsDialog) {
       extras = (
         <AttachRowsDialog
           relation={this.state.relation}
diff --git a/src/dashboard/Data/Browser/BrowserToolbar.react.js b/src/dashboard/Data/Browser/BrowserToolbar.react.js
index 8079c48a41..a3c09c420c 100644
--- a/src/dashboard/Data/Browser/BrowserToolbar.react.js
+++ b/src/dashboard/Data/Browser/BrowserToolbar.react.js
@@ -46,6 +46,7 @@ let BrowserToolbar = ({
   onDropClass,
   onChangeCLP,
   onRefresh,
+  onImport,
   onEditPermissions,
   hidePerms,
   isUnique,
@@ -260,6 +261,11 @@ let BrowserToolbar = ({
         </BrowserMenu>
       )}
       {onAddRow && <div className={styles.toolbarSeparator} />}
+      <a className={classes.join(' ')} onClick={isPendingEditCloneRows ? null : onImport}>
+        <Icon name="up-solid" width={14} height={14} />
+        <span>Import</span>
+      </a>
+      <div className={styles.toolbarSeparator} />
       <a className={classes.join(' ')} onClick={isPendingEditCloneRows ? null : onRefresh}>
         <Icon name="refresh-solid" width={14} height={14} />
         <span>Refresh</span>
diff --git a/src/dashboard/Data/Browser/ImportDialog.react.js b/src/dashboard/Data/Browser/ImportDialog.react.js
new file mode 100644
index 0000000000..4c2d1df97d
--- /dev/null
+++ b/src/dashboard/Data/Browser/ImportDialog.react.js
@@ -0,0 +1,97 @@
+/*
+ * Copyright (c) 2016-present, Parse, LLC
+ * All rights reserved.
+ *
+ * This source code is licensed under the license found in the LICENSE file in
+ * the root directory of this source tree.
+ */
+import Modal          from 'components/Modal/Modal.react';
+import FileEditor     from 'components/FileEditor/FileEditor.react';
+import React          from 'react';
+import Pill from 'components/Pill/Pill.react';
+import getFileName from 'lib/getFileName';
+import { CurrentApp } from 'context/currentApp';
+
+export default class ImportDialog extends React.Component {
+  static contextType = CurrentApp;
+  constructor() {
+    super();
+    this.state = {
+      progress: undefined,
+      file: null,
+      showFileEditor: false
+    };
+  }
+
+  componentWillMount() {
+    this.context.getExportProgress().then((progress) => {
+      this.setState({ progress });
+    });
+  }
+
+  inProgress() {
+    if (this.state.progress === undefined) {
+      return false;
+    }
+    let found = false;
+    if (Array.isArray(this.state.progress)) {
+      this.state.progress.forEach((obj) => {
+        if (obj.id === this.props.className) {
+          found = true;
+        }
+      });
+    }
+    return found;
+  }
+
+  openFileEditor() {
+    this.setState({
+      showFileEditor: true
+    });
+  }
+
+  hideFileEditor(file) {
+    this.setState({
+      showFileEditor: false,
+      file
+    });
+  }
+
+  render() {
+    let inProgress = this.inProgress();
+    return (
+      <div>
+      <Modal
+        type={Modal.Types.INFO}
+        icon='up-outline'
+        iconSize={40}
+        title={`Import Data into ${this.props.className}`}
+        subtitle='Note: If rows have a className, they will be imported into that class.'
+        confirmText='Import'
+        cancelText='Cancel'
+        disabled={!this.state.file}
+        buttonsInCenter={true}
+        onCancel={this.props.onCancel}
+        onConfirm={this.props.onConfirm}>
+          <div style={{ padding: '25px' }}>
+              {this.state.file && <Pill value={getFileName(this.state.file) }/>}
+              <div style={{ cursor: 'pointer' }}>
+                <Pill
+                  value={this.state.file ? 'Change file' : 'Select file'}
+                  onClick={() => this.openFileEditor()}
+                />
+                {this.state.showFileEditor && (
+                  <FileEditor
+                    value={this.state.file}
+                    accept='.json,.csv'
+                    onCommit={(file) => this.hideFileEditor(file)}
+                    onCancel={() => this.hideFileEditor()}
+                  />
+                )}
+              </div>
+            </div>
+      </Modal>
+        </div>
+    );
+  }
+}

From b5af939b529d4dfefcf8a960beb8c5af69267dcd Mon Sep 17 00:00:00 2001
From: dblythy <daniel-blyth@live.com.au>
Date: Thu, 19 Jan 2023 14:04:39 +1100
Subject: [PATCH 2/4] wip

---
 .../BrowserCell/BrowserCell.react.js          |  4 +-
 src/dashboard/Data/Browser/Browser.react.js   | 94 ++++++++++++++++++-
 .../Data/Browser/ImportDialog.react.js        |  4 +-
 3 files changed, 97 insertions(+), 5 deletions(-)

diff --git a/src/components/BrowserCell/BrowserCell.react.js b/src/components/BrowserCell/BrowserCell.react.js
index 96d40b8fcc..c5eb12379e 100644
--- a/src/components/BrowserCell/BrowserCell.react.js
+++ b/src/components/BrowserCell/BrowserCell.react.js
@@ -127,8 +127,8 @@ export default class BrowserCell extends Component {
     } else if (this.props.type === 'Object' || this.props.type === 'Bytes') {
       this.copyableValue = content = JSON.stringify(this.props.value);
     } else if (this.props.type === 'File') {
-      const fileName = this.props.value.url() ? getFileName(this.props.value) : 'Uploading\u2026';
-      content = <Pill value={fileName} fileDownloadLink={this.props.value.url()} shrinkablePill />;
+      const fileName = this.props.value.url?.() ? getFileName(this.props.value) : 'Uploading\u2026';
+      content = <Pill value={fileName} fileDownloadLink={this.props.value.url?.()} shrinkablePill />;
       this.copyableValue = fileName;
     } else if (this.props.type === 'ACL') {
       let pieces = [];
diff --git a/src/dashboard/Data/Browser/Browser.react.js b/src/dashboard/Data/Browser/Browser.react.js
index 8a20eb098c..e28a401650 100644
--- a/src/dashboard/Data/Browser/Browser.react.js
+++ b/src/dashboard/Data/Browser/Browser.react.js
@@ -1352,6 +1352,98 @@ class Browser extends DashboardView {
     document.body.removeChild(element);
   }
 
+  async confirmImport(file) {
+    this.setState({ showImportDialog: null });
+    const className = this.props.params.className;
+    const classColumns = this.getClassColumns(className, false);
+    const columnsObject = {};
+    classColumns.forEach((column) => {
+      columnsObject[column.name] = column;
+    });
+    const { base64, type}  = file._source;
+    if (type === 'text/csv') {
+      const csvToArray =(text) => {
+        let p = '', row = [''], ret = [row], i = 0, r = 0, s = !0, l;
+        for (l of text) {
+            if ('"' === l) {
+                if (s && l === p) row[i] += l;
+                s = !s;
+            } else if (',' === l && s) l = row[++i] = '';
+            else if ('\n' === l && s) {
+                if ('\r' === p) row[i] = row[i].slice(0, -1);
+                row = ret[++r] = [l = '']; i = 0;
+            } else row[i] += l;
+            p = l;
+        }
+        return ret;
+      };
+      const csv = atob(base64);
+      const [columns, ...rows] = csvToArray(csv);
+      await Parse.Object.saveAll(rows.filter(row => row.join() !== '').map(row => {
+        const json = {className};
+        for (let i = 1; i < row.length; i++) {
+          const column = columns[i];
+          const value = row[i];
+          if (value === 'null') {
+            continue;
+          }
+          const {type, targetClass, name} = columnsObject[column] || {};
+          if (type === 'Relation') {
+            json[column] = {
+              __type: 'Relation',
+              className: targetClass,
+            };
+            continue;
+          }
+          if (type === 'Pointer') {
+            json[column] = {
+              __type: 'Pointer',
+              className: targetClass,
+              objectId: value,
+            };
+            continue;
+          }
+          if (name === 'ACL') {
+            json.ACL = new Parse.ACL(JSON.parse(value));
+            continue;
+          }
+          if (type === 'Date') {
+            json[column] = new Date(value);
+            continue;
+          }
+          if (type === 'Boolean') {
+            json[column] = value === 'true';
+            continue;
+          }
+          if (type === 'String') {
+            json[column] = value;
+            continue;
+          }
+          if (type === 'Number') {
+            json[column] = Number(value);
+            continue;
+          }
+          try {
+            json[column] = JSON.parse(value);
+          } catch (e) {
+            /* */
+          }
+        }
+        return Parse.Object.fromJSON(json, false, true);
+      }), {useMasterKey: true});
+    }
+    this.refresh();
+
+    // Deliver to browser to download file
+    // const element = document.createElement('a');
+    // const file = new Blob([csvString], { type: 'text/csv' });
+    // element.href = URL.createObjectURL(file);
+    // element.download = `${className}.csv`;
+    // document.body.appendChild(element); // Required for this to work in FireFox
+    // element.click();
+    // document.body.removeChild(element);
+  }
+
   getClassRelationColumns(className) {
     const currentClassName = this.props.params.className;
     return this.getClassColumns(className, false)
@@ -1674,7 +1766,7 @@ class Browser extends DashboardView {
         <ImportDialog
           className={className}
           onCancel={() => this.setState({ showImportDialog: false })}
-          onConfirm={() => this.exportClass(className)} />
+          onConfirm={(file) => this.confirmImport(file)} />
       );
     }else if (this.state.showAttachRowsDialog) {
       extras = (
diff --git a/src/dashboard/Data/Browser/ImportDialog.react.js b/src/dashboard/Data/Browser/ImportDialog.react.js
index 4c2d1df97d..6683c48bae 100644
--- a/src/dashboard/Data/Browser/ImportDialog.react.js
+++ b/src/dashboard/Data/Browser/ImportDialog.react.js
@@ -66,13 +66,13 @@ export default class ImportDialog extends React.Component {
         icon='up-outline'
         iconSize={40}
         title={`Import Data into ${this.props.className}`}
-        subtitle='Note: If rows have a className, they will be imported into that class.'
+        subtitle='Note: Please make sure columns are defined in SCHEMA to import.'
         confirmText='Import'
         cancelText='Cancel'
         disabled={!this.state.file}
         buttonsInCenter={true}
         onCancel={this.props.onCancel}
-        onConfirm={this.props.onConfirm}>
+        onConfirm={() => this.props.onConfirm(this.state.file)}>
           <div style={{ padding: '25px' }}>
               {this.state.file && <Pill value={getFileName(this.state.file) }/>}
               <div style={{ cursor: 'pointer' }}>

From ae40a11cfcd84bec8d35e602125521e3c12e745e Mon Sep 17 00:00:00 2001
From: dblythy <daniel-blyth@live.com.au>
Date: Thu, 19 Jan 2023 16:36:12 +1100
Subject: [PATCH 3/4] Update ImportDialog.react.js

---
 src/dashboard/Data/Browser/ImportDialog.react.js | 16 ----------------
 1 file changed, 16 deletions(-)

diff --git a/src/dashboard/Data/Browser/ImportDialog.react.js b/src/dashboard/Data/Browser/ImportDialog.react.js
index 6683c48bae..3b4155d5ae 100644
--- a/src/dashboard/Data/Browser/ImportDialog.react.js
+++ b/src/dashboard/Data/Browser/ImportDialog.react.js
@@ -29,21 +29,6 @@ export default class ImportDialog extends React.Component {
     });
   }
 
-  inProgress() {
-    if (this.state.progress === undefined) {
-      return false;
-    }
-    let found = false;
-    if (Array.isArray(this.state.progress)) {
-      this.state.progress.forEach((obj) => {
-        if (obj.id === this.props.className) {
-          found = true;
-        }
-      });
-    }
-    return found;
-  }
-
   openFileEditor() {
     this.setState({
       showFileEditor: true
@@ -58,7 +43,6 @@ export default class ImportDialog extends React.Component {
   }
 
   render() {
-    let inProgress = this.inProgress();
     return (
       <div>
       <Modal

From 67817356394307c1a341fe681ff1447850da2450 Mon Sep 17 00:00:00 2001
From: dblythy <daniel-blyth@live.com.au>
Date: Fri, 20 Jan 2023 15:40:27 +1100
Subject: [PATCH 4/4] wip

---
 src/dashboard/Data/Browser/Browser.react.js     |  9 ---------
 .../Data/Browser/ImportDialog.react.js          | 17 +----------------
 2 files changed, 1 insertion(+), 25 deletions(-)

diff --git a/src/dashboard/Data/Browser/Browser.react.js b/src/dashboard/Data/Browser/Browser.react.js
index e28a401650..d3577bdfe5 100644
--- a/src/dashboard/Data/Browser/Browser.react.js
+++ b/src/dashboard/Data/Browser/Browser.react.js
@@ -1433,15 +1433,6 @@ class Browser extends DashboardView {
       }), {useMasterKey: true});
     }
     this.refresh();
-
-    // Deliver to browser to download file
-    // const element = document.createElement('a');
-    // const file = new Blob([csvString], { type: 'text/csv' });
-    // element.href = URL.createObjectURL(file);
-    // element.download = `${className}.csv`;
-    // document.body.appendChild(element); // Required for this to work in FireFox
-    // element.click();
-    // document.body.removeChild(element);
   }
 
   getClassRelationColumns(className) {
diff --git a/src/dashboard/Data/Browser/ImportDialog.react.js b/src/dashboard/Data/Browser/ImportDialog.react.js
index 3b4155d5ae..40a3199052 100644
--- a/src/dashboard/Data/Browser/ImportDialog.react.js
+++ b/src/dashboard/Data/Browser/ImportDialog.react.js
@@ -1,10 +1,3 @@
-/*
- * Copyright (c) 2016-present, Parse, LLC
- * All rights reserved.
- *
- * This source code is licensed under the license found in the LICENSE file in
- * the root directory of this source tree.
- */
 import Modal          from 'components/Modal/Modal.react';
 import FileEditor     from 'components/FileEditor/FileEditor.react';
 import React          from 'react';
@@ -13,22 +6,14 @@ import getFileName from 'lib/getFileName';
 import { CurrentApp } from 'context/currentApp';
 
 export default class ImportDialog extends React.Component {
-  static contextType = CurrentApp;
   constructor() {
     super();
     this.state = {
-      progress: undefined,
       file: null,
       showFileEditor: false
     };
   }
 
-  componentWillMount() {
-    this.context.getExportProgress().then((progress) => {
-      this.setState({ progress });
-    });
-  }
-
   openFileEditor() {
     this.setState({
       showFileEditor: true
@@ -67,7 +52,7 @@ export default class ImportDialog extends React.Component {
                 {this.state.showFileEditor && (
                   <FileEditor
                     value={this.state.file}
-                    accept='.json,.csv'
+                    accept='.csv'
                     onCommit={(file) => this.hideFileEditor(file)}
                     onCancel={() => this.hideFileEditor()}
                   />