From c37dcccc4df1e704adbf7c7f3be403e0da16f774 Mon Sep 17 00:00:00 2001 From: dblythy 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 (
- + {file ? 'Replace file' : 'Upload file'}
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 = ( + this.setState({ showImportDialog: false })} + onConfirm={() => this.exportClass(className)} /> + ); + }else if (this.state.showAttachRowsDialog) { extras = ( )} {onAddRow &&
} + + + Import + +
Refresh 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 ( +
+ +
+ {this.state.file && } +
+ this.openFileEditor()} + /> + {this.state.showFileEditor && ( + this.hideFileEditor(file)} + onCancel={() => this.hideFileEditor()} + /> + )} +
+
+
+
+ ); + } +} From b5af939b529d4dfefcf8a960beb8c5af69267dcd Mon Sep 17 00:00:00 2001 From: dblythy 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 = ; + const fileName = this.props.value.url?.() ? getFileName(this.props.value) : 'Uploading\u2026'; + content = ; 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 { 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)}>
{this.state.file && }
From ae40a11cfcd84bec8d35e602125521e3c12e745e Mon Sep 17 00:00:00 2001 From: dblythy 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 (
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 && ( this.hideFileEditor(file)} onCancel={() => this.hideFileEditor()} />