From 4a98cc8af800295ffbf50a8e25a0c4830d947d09 Mon Sep 17 00:00:00 2001 From: Andrei Prigorshnev Date: Fri, 20 Aug 2021 14:26:48 +0400 Subject: [PATCH] FEATURE: get rid of the import a query modal (#127) This is the new version of #126. Now the pick-files-button moved to core. To import a query into Data Explorer, you need to push the button and then deal with the import modal. Instead, we want just to be triggering a system file picker directly. This PR makes it happen. --- .../components/json-file-uploader.js.es6 | 113 ------------------ .../controllers/admin-plugins-explorer.js.es6 | 61 +++++++++- .../discourse/controllers/import-query.js.es6 | 43 ------- .../templates/admin/plugins-explorer.hbs | 7 +- .../components/json-file-uploader.hbs | 16 --- .../templates/modal/import-query.hbs | 10 -- assets/stylesheets/explorer.scss | 3 + config/locales/client.en.yml | 2 + 8 files changed, 69 insertions(+), 186 deletions(-) delete mode 100644 assets/javascripts/discourse/components/json-file-uploader.js.es6 delete mode 100644 assets/javascripts/discourse/controllers/import-query.js.es6 delete mode 100644 assets/javascripts/discourse/templates/components/json-file-uploader.hbs delete mode 100644 assets/javascripts/discourse/templates/modal/import-query.hbs diff --git a/assets/javascripts/discourse/components/json-file-uploader.js.es6 b/assets/javascripts/discourse/components/json-file-uploader.js.es6 deleted file mode 100644 index ca124a9..0000000 --- a/assets/javascripts/discourse/components/json-file-uploader.js.es6 +++ /dev/null @@ -1,113 +0,0 @@ -import { - default as computed, - observes, - on, -} from "discourse-common/utils/decorators"; - -export default Ember.Component.extend({ - fileInput: null, - loading: false, - expectedRootObjectName: null, - hover: 0, - - classNames: ["json-uploader"], - - @on("didInsertElement") - _initialize() { - const $this = $(this.element); - const fileInput = this.element.querySelector("#js-file-input"); - this.set("fileInput", fileInput); - - $(fileInput).on("change", () => this.fileSelected(fileInput.files)); - - $this.on("dragover", (e) => { - if (e.preventDefault) { - e.preventDefault(); - } - return false; - }); - $this.on("dragenter", (e) => { - if (e.preventDefault) { - e.preventDefault(); - } - this.set("hover", this.hover + 1); - return false; - }); - $this.on("dragleave", (e) => { - if (e.preventDefault) { - e.preventDefault(); - } - this.set("hover", this.hover - 1); - return false; - }); - $this.on("drop", (e) => { - if (e.preventDefault) { - e.preventDefault(); - } - - this.set("hover", 0); - this.fileSelected(e.dataTransfer.files); - return false; - }); - }, - - @computed("extension") - accept(extension) { - return ( - ".json,application/json,application/x-javascript,text/json" + - (extension ? `,${extension}` : "") - ); - }, - - @observes("destination", "expectedRootObjectName") - setReady() { - let parsed; - try { - parsed = JSON.parse(this.value); - } catch (e) { - this.set("ready", false); - return; - } - - const rootObject = parsed[this.expectedRootObjectName]; - - if (rootObject !== null && rootObject !== undefined) { - this.set("ready", true); - } else { - this.set("ready", false); - } - }, - - actions: { - selectFile() { - $(this.fileInput).click(); - }, - }, - - fileSelected(fileList) { - let files = []; - for (let i = 0; i < fileList.length; i++) { - files[i] = fileList[i]; - } - const fileNameRegex = /\.(json|txt)$/; - files = files.filter((file) => { - if (fileNameRegex.test(file.name)) { - return true; - } - if (file.type === "text/plain") { - return true; - } - return false; - }); - const firstFile = files[0]; - - this.set("loading", true); - - const reader = new FileReader(); - reader.onload = (evt) => { - this.setProperties({ value: evt.target.result, loading: false }); - }; - - reader.readAsText(firstFile); - }, -}); diff --git a/assets/javascripts/discourse/controllers/admin-plugins-explorer.js.es6 b/assets/javascripts/discourse/controllers/admin-plugins-explorer.js.es6 index a276816..8075026 100644 --- a/assets/javascripts/discourse/controllers/admin-plugins-explorer.js.es6 +++ b/assets/javascripts/discourse/controllers/admin-plugins-explorer.js.es6 @@ -6,6 +6,8 @@ import { default as computed, observes, } from "discourse-common/utils/decorators"; +import I18n from "I18n"; +import { Promise } from "rsvp"; const NoQuery = Query.create({ name: "No queries", fake: true, group_ids: [] }); @@ -30,6 +32,11 @@ export default Ember.Controller.extend({ sortBy: ["last_run_at:desc"], sortedQueries: Ember.computed.sort("model", "sortBy"), + @computed + acceptedImportFileTypes() { + return ["application/json"]; + }, + @computed("search", "sortBy") filteredContent(search) { const regexp = new RegExp(search, "i"); @@ -117,6 +124,36 @@ export default Ember.Controller.extend({ .finally(() => this.set("loading", false)); }, + async _importQuery(file) { + const json = await this._readFileAsTextAsync(file); + const query = this._parseQuery(json); + const record = this.store.createRecord("query", query); + const response = await record.save(); + return response.target; + }, + + _parseQuery(json) { + const parsed = JSON.parse(json); + const query = parsed.query; + if (!query || !query.sql) { + throw new TypeError(); + } + query.id = 0; // 0 means no Id yet + return query; + }, + + _readFileAsTextAsync(file) { + return new Promise((resolve, reject) => { + const reader = new FileReader(); + reader.onload = () => { + resolve(reader.result); + }; + reader.onerror = reject; + + reader.readAsText(file); + }); + }, + actions: { dummy() {}, @@ -124,9 +161,27 @@ export default Ember.Controller.extend({ this.set("hideSchema", false); }, - importQuery() { - showModal("import-query"); - this.set("showCreate", false); + import(files) { + this.set("loading", true); + const file = files[0]; + this._importQuery(file) + .then((record) => this.addCreatedRecord(record)) + .catch((e) => { + if (e.jqXHR) { + popupAjaxError(e); + } else if (e instanceof SyntaxError) { + bootbox.alert(I18n.t("explorer.import.unparseable_json")); + } else if (e instanceof TypeError) { + bootbox.alert(I18n.t("explorer.import.wrong_json")); + } else { + bootbox.alert(I18n.t("errors.desc.unknown")); + // eslint-disable-next-line no-console + console.error(e); + } + }) + .finally(() => { + this.set("loading", false); + }); }, showCreate() { diff --git a/assets/javascripts/discourse/controllers/import-query.js.es6 b/assets/javascripts/discourse/controllers/import-query.js.es6 deleted file mode 100644 index 8cc3b5f..0000000 --- a/assets/javascripts/discourse/controllers/import-query.js.es6 +++ /dev/null @@ -1,43 +0,0 @@ -import { default as computed } from "discourse-common/utils/decorators"; -import ModalFunctionality from "discourse/mixins/modal-functionality"; -import { popupAjaxError } from "discourse/lib/ajax-error"; - -export default Ember.Controller.extend(ModalFunctionality, { - notReady: Ember.computed.not("ready"), - - adminPluginsExplorer: Ember.inject.controller(), - - @computed("queryFile") - ready(queryFile) { - let parsed; - try { - parsed = JSON.parse(queryFile); - } catch (e) { - return false; - } - - return !!parsed["query"]; - }, - - actions: { - doImport() { - const object = JSON.parse(this.queryFile).query; - - // Slight fixup before creating object - object.id = 0; // 0 means no Id yet - - this.set("loading", true); - this.store - .createRecord("query", object) - .save() - .then((query) => { - this.send("closeModal"); - this.set("loading", false); - - const parentController = this.adminPluginsExplorer; - parentController.addCreatedRecord(query.target); - }) - .catch(popupAjaxError); - }, - }, -}); diff --git a/assets/javascripts/discourse/templates/admin/plugins-explorer.hbs b/assets/javascripts/discourse/templates/admin/plugins-explorer.hbs index e200f29..6f50c87 100644 --- a/assets/javascripts/discourse/templates/admin/plugins-explorer.hbs +++ b/assets/javascripts/discourse/templates/admin/plugins-explorer.hbs @@ -6,7 +6,12 @@
{{text-field value=search placeholderKey="explorer.search_placeholder"}} {{d-button action=(action "showCreate") icon="plus" class="no-text btn-right"}} - {{d-button action=(action "importQuery") label="explorer.import.label" icon="upload"}} + {{pick-files-button + class="import-btn" + label="explorer.import.label" + icon="upload" + acceptedFileTypes=acceptedImportFileTypes + onFilesPicked=(action "import")}}
{{#if showCreate}} diff --git a/assets/javascripts/discourse/templates/components/json-file-uploader.hbs b/assets/javascripts/discourse/templates/components/json-file-uploader.hbs deleted file mode 100644 index f3baef5..0000000 --- a/assets/javascripts/discourse/templates/components/json-file-uploader.hbs +++ /dev/null @@ -1,16 +0,0 @@ -
-
- - {{d-button class="fileSelect" action=(action "selectFile") icon="upload" label="upload_selector.select_file"}} - {{conditional-loading-spinner condition=loading size="small"}} -
-
- - {{i18n "explorer.or"}} - -
-
- {{textarea value=value}} -
-
{{d-icon "upload"}}
-
diff --git a/assets/javascripts/discourse/templates/modal/import-query.hbs b/assets/javascripts/discourse/templates/modal/import-query.hbs deleted file mode 100644 index 29de9df..0000000 --- a/assets/javascripts/discourse/templates/modal/import-query.hbs +++ /dev/null @@ -1,10 +0,0 @@ -{{#d-modal-body title="explorer.import.modal"}} -
- - -
-{{/d-modal-body}} diff --git a/assets/stylesheets/explorer.scss b/assets/stylesheets/explorer.scss index e7cc2dc..7a9b980 100644 --- a/assets/stylesheets/explorer.scss +++ b/assets/stylesheets/explorer.scss @@ -315,6 +315,9 @@ table.group-reports { li.none { display: none; } + .import-btn { + display: flex; + } } .recent-queries { diff --git a/config/locales/client.en.yml b/config/locales/client.en.yml index ffb0df4..5b73d30 100644 --- a/config/locales/client.en.yml +++ b/config/locales/client.en.yml @@ -15,6 +15,8 @@ en: import: label: "Import" modal: "Import A Query" + unparseable_json: "Unparseable JSON file." + wrong_json: "Wrong JSON file. A JSON file should contain a 'query' object, which should at least have an 'sql' property." help: label: "Help" modal_title: "Data Explorer Help"