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.
This commit is contained in:
parent
23287ece95
commit
4a98cc8af8
|
@ -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);
|
|
||||||
},
|
|
||||||
});
|
|
|
@ -6,6 +6,8 @@ import {
|
||||||
default as computed,
|
default as computed,
|
||||||
observes,
|
observes,
|
||||||
} from "discourse-common/utils/decorators";
|
} from "discourse-common/utils/decorators";
|
||||||
|
import I18n from "I18n";
|
||||||
|
import { Promise } from "rsvp";
|
||||||
|
|
||||||
const NoQuery = Query.create({ name: "No queries", fake: true, group_ids: [] });
|
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"],
|
sortBy: ["last_run_at:desc"],
|
||||||
sortedQueries: Ember.computed.sort("model", "sortBy"),
|
sortedQueries: Ember.computed.sort("model", "sortBy"),
|
||||||
|
|
||||||
|
@computed
|
||||||
|
acceptedImportFileTypes() {
|
||||||
|
return ["application/json"];
|
||||||
|
},
|
||||||
|
|
||||||
@computed("search", "sortBy")
|
@computed("search", "sortBy")
|
||||||
filteredContent(search) {
|
filteredContent(search) {
|
||||||
const regexp = new RegExp(search, "i");
|
const regexp = new RegExp(search, "i");
|
||||||
|
@ -117,6 +124,36 @@ export default Ember.Controller.extend({
|
||||||
.finally(() => this.set("loading", false));
|
.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: {
|
actions: {
|
||||||
dummy() {},
|
dummy() {},
|
||||||
|
|
||||||
|
@ -124,9 +161,27 @@ export default Ember.Controller.extend({
|
||||||
this.set("hideSchema", false);
|
this.set("hideSchema", false);
|
||||||
},
|
},
|
||||||
|
|
||||||
importQuery() {
|
import(files) {
|
||||||
showModal("import-query");
|
this.set("loading", true);
|
||||||
this.set("showCreate", false);
|
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() {
|
showCreate() {
|
||||||
|
|
|
@ -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);
|
|
||||||
},
|
|
||||||
},
|
|
||||||
});
|
|
|
@ -6,7 +6,12 @@
|
||||||
<div class="query-list">
|
<div class="query-list">
|
||||||
{{text-field value=search placeholderKey="explorer.search_placeholder"}}
|
{{text-field value=search placeholderKey="explorer.search_placeholder"}}
|
||||||
{{d-button action=(action "showCreate") icon="plus" class="no-text btn-right"}}
|
{{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")}}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{{#if showCreate}}
|
{{#if showCreate}}
|
||||||
|
|
|
@ -1,16 +0,0 @@
|
||||||
<div class="jsfu-shade-container">
|
|
||||||
<div class="jsfu-file">
|
|
||||||
<input id="js-file-input" type="file" style="display:none;" accept={{accept}}>
|
|
||||||
{{d-button class="fileSelect" action=(action "selectFile") icon="upload" label="upload_selector.select_file"}}
|
|
||||||
{{conditional-loading-spinner condition=loading size="small"}}
|
|
||||||
</div>
|
|
||||||
<div class="jsfu-separator">
|
|
||||||
<small>
|
|
||||||
{{i18n "explorer.or"}}
|
|
||||||
</small>
|
|
||||||
</div>
|
|
||||||
<div class="jsfu-paste">
|
|
||||||
{{textarea value=value}}
|
|
||||||
</div>
|
|
||||||
<div class="jsfu-shade {{if hover "" "hidden"}}"><span class="text">{{d-icon "upload"}}</span></div>
|
|
||||||
</div>
|
|
|
@ -1,10 +0,0 @@
|
||||||
{{#d-modal-body title="explorer.import.modal"}}
|
|
||||||
<form {{action "dummy" on="submit"}}>
|
|
||||||
<div class="modal-body">
|
|
||||||
{{json-file-uploader value=queryFile extension=".dcquery.json"}}
|
|
||||||
</div>
|
|
||||||
<div class="modal-footer">
|
|
||||||
{{d-button class="btn-primary" action="doImport" type="submit" disabled=notReady icon="plus" label="explorer.import.label"}}
|
|
||||||
</div>
|
|
||||||
</form>
|
|
||||||
{{/d-modal-body}}
|
|
|
@ -315,6 +315,9 @@ table.group-reports {
|
||||||
li.none {
|
li.none {
|
||||||
display: none;
|
display: none;
|
||||||
}
|
}
|
||||||
|
.import-btn {
|
||||||
|
display: flex;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.recent-queries {
|
.recent-queries {
|
||||||
|
|
|
@ -15,6 +15,8 @@ en:
|
||||||
import:
|
import:
|
||||||
label: "Import"
|
label: "Import"
|
||||||
modal: "Import A Query"
|
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:
|
help:
|
||||||
label: "Help"
|
label: "Help"
|
||||||
modal_title: "Data Explorer Help"
|
modal_title: "Data Explorer Help"
|
||||||
|
|
Loading…
Reference in New Issue