Skip to content
Snippets Groups Projects
Commit c26f76bd authored by Florian Spreckelsen's avatar Florian Spreckelsen
Browse files

Merge branch 'f-file-upload' into 'dev'

ENH: add file-upload.js

See merge request !5
parents 5753610b acfe25bb
No related branches found
No related tags found
2 merge requests!6Release 0.2.0,!5ENH: add file-upload.js
......@@ -8,6 +8,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
### Added ###
* File Upload component from @indiscale/caosdb-webui-core-components
* Map component @indiscale/caosdb-webui-ext-map
### Changed ###
......
import React from "react";
import ReactDOM from "react-dom/client";
import "regenerator-runtime/runtime";
import { FileUpload } from "@indiscale/caosdb-webui-core-components";
const get_filesystem_body = () => {
return document.querySelector("#caosdb-f-filesystem .card-body");
};
const get_filesystem_header = () => {
return document.querySelector("#caosdb-f-filesystem .card-header");
};
/**
* Return an array of the currently shown directory (file system view).
*/
const get_current_directory = () => {
const header = get_filesystem_header();
if (!header) {
return [];
}
return Array.from(header.getElementsByClassName("caosdb-fs-cwd")).map(
(el) => el.textContent,
);
};
/**
* Construct the XML payload for file insertion.
*/
const create_file_upload_request_xml = (files, targetDirectory, recordType) => {
const _parent = recordType ? `<Parent id="${recordType}"/>` : "";
const request =
"<Request>" +
files.map((f, idx) => {
return `<File upload="${idx}-${f.name}" path="${targetDirectory}${f.name}">${_parent}</File>`;
}) +
"</Request>";
return request;
};
/**
* Put all the XML payload and the files into a form data object (for upload).
*/
const create_form_data = (xml, files) => {
const formData = new FormData();
formData.append("FileRepresentation", xml);
files.forEach((f, idx) => {
const name = `${idx}-${f.name}`;
formData.append(name, f.file, name);
});
return formData;
};
/**
* Split up the link to the file in the entity view. The two new links point to
* the file (for download) and to the parent directory (for browsing the file
* system).
*/
const split_parent_dir = (node) => {
repair_uri(node, "href");
const path_old = node
.getAttribute("href")
.substring(connection.getFileSystemPath().length)
.split("/");
if (path_old.length > 1) {
const parent_dir = path_old.slice(0, path_old.length - 1);
const parent_dir_node = node.cloneNode();
parent_dir_node.setAttribute(
"href",
connection.getFileSystemPath() + parent_dir.join("/") + "/",
);
parent_dir_node.textContent = parent_dir.join("/") + "/";
parent_dir_node.title = "Go to parent directory.";
node.before(parent_dir_node);
node.dataset.entityPath = node.textContent;
node.textContent = node.textContent.split("/").slice(-1)[0];
node.title = "Download this file";
}
};
/**
* Repairs broken links (e.g. when files have strange names)
*/
const repair_uri = (node, attr) => {
const path_old = node
.getAttribute(attr)
.substring(connection.getFileSystemPath().length)
.split("/");
const path_new = path_old.map(encodeURIComponent);
node.setAttribute(attr, connection.getFileSystemPath() + path_new.join("/"));
};
/**
* Perform the actual AJAX request (returns Promise).
*/
const uploadRequest = (formData) => {
return $.ajax({
url: connection.getBasePath() + "Entity/",
method: "POST",
dataType: "xml",
contentType: false,
processData: false,
data: formData,
});
};
/**
* This onSubmit implementation works for the legacy REST API.
*/
const defaultOnSubmit = async (data) => {
document.querySelector("#collapseFileUpload~ul")?.remove();
var { files, directory, recordType } = data;
directory = directory || [];
directory = directory.length > 0 ? "/" + directory.join("/") + "/" : "/";
const xml = create_file_upload_request_xml(files, directory, recordType);
const formData = create_form_data(xml, files);
const response = await uploadRequest(formData);
const iterator = response.evaluate(
"/Response/File",
response,
null,
XPathResult.UNORDERED_NODE_ITERATOR_TYPE,
);
const newFiles = [];
var next = iterator.iterateNext();
var hasErrors = false;
while (next) {
var error = response
.evaluate("Error", next, null, XPathResult.UNORDERED_NODE_ITERATOR_TYPE)
?.iterateNext();
const path = response.evaluate(
"@path",
next,
null,
XPathResult.STRING_TYPE,
)?.stringValue;
const id = response.evaluate(
"@id",
next,
null,
XPathResult.STRING_TYPE,
)?.stringValue;
if (error) {
hasErrors = true;
error = response.evaluate(
"@description",
error,
null,
XPathResult.STRING_TYPE,
)?.stringValue;
}
newFiles.push({
error: error,
id: id,
path: path,
url: !id || connection.getBasePath() + "Entity/" + id,
});
next = iterator.iterateNext();
}
return {
hasErrors: hasErrors,
url: connection.getBasePath() + "FileSystem/" + directory,
directory: directory,
newFiles: newFiles,
};
};
/**
* Suitable for our purpose here means: Permission to USE:AS_PARENT is granted
* and the RecordType doesn't have any obligatory properties.
*/
const findSuitableRecordTypes = async () => {
const response = await connection.get("Entity/?query=FIND RECORDTYPE");
const iterator = response.evaluate(
"/Response/RecordType",
response,
null,
XPathResult.UNORDERED_NODE_ITERATOR_TYPE,
);
const results = [];
var n;
while (true) {
n = iterator.iterateNext();
if (!n) {
break;
}
const hasUsePermission = response
.evaluate(
"Permissions/Permission[@name='USE:AS_PARENT']",
n,
null,
XPathResult.UNORDERED_NODE_ITERATOR_TYPE,
)
.iterateNext();
if (!hasUsePermission) {
continue;
}
const hasObligatoryProperties = response
.evaluate(
"Property[@importance='OBLIGATORY']",
n,
null,
XPathResult.UNORDERED_NODE_ITERATOR_TYPE,
)
.iterateNext();
if (hasObligatoryProperties) {
continue;
}
results.push({
name: response.evaluate("@name", n, null, XPathResult.STRING_TYPE)
?.stringValue,
id: response.evaluate("@id", n, null, XPathResult.STRING_TYPE)
?.stringValue,
description: response.evaluate(
"@description",
n,
null,
XPathResult.STRING_TYPE,
)?.stringValue,
});
}
return results;
};
const file_upload = {
get_record_types: async function () {
// TODO add permissions to GRPC API, then use this again
//const service = new TransactionService();
//const results = await service.executeQuery("FIND RECORDTYPE");
//console.log(results);
return await findSuitableRecordTypes();
},
create_file_upload_widget: function (container, config) {
const root = ReactDOM.createRoot(container);
const _config = config || {};
if (!_config.hasOwnProperty("onSubmit")) {
_config.onSubmit = defaultOnSubmit;
}
if (!_config.hasOwnProperty("recordTypes")) {
_config.recordTypes = this.get_record_types().then((results) =>
results.map((rt) => {
return { label: rt.name, value: rt.id, title: rt.description };
}),
);
}
root.render(
<React.StrictMode>
<form>
<FileUpload {..._config} />
</form>
</React.StrictMode>,
);
},
/**
* This is the "+" button shown in the file system view.
*/
create_upload_button: () => {
const button = $(`
<button class="py-0 px-1 ms-2 btn btn-primary" type="button" data-bs-toggle="collapse" data-bs-target="#collapseFileUpload" aria-expanded="false" aria-controls="collapseFileUpload">
<span class="bi bi-plus-lg"/>
</button>`);
const _onClick = () => {
button[0].classList.add("d-none");
};
button.on("click", _onClick);
return button[0];
},
init_file_upload: function () {
const container = $(
`<div class="row collapse mb-5" id="collapseFileUpload"/>`,
);
const filesystem_header =
get_filesystem_header()?.querySelector("div.text-end");
const filesystem_body = get_filesystem_body();
if (filesystem_body && filesystem_header) {
const button = this.create_upload_button();
filesystem_header.appendChild(button);
$(filesystem_body).prepend(container);
const config = {
directoryReadOnly: false,
directoryBase: get_current_directory(),
onCancel: () => {
button.click();
button.classList.remove("d-none");
},
onFinish: () => {
window.location.reload();
},
};
this.create_file_upload_widget(container[0], config);
}
},
repair_file_system_links: function () {
// entity view
document
.querySelectorAll(".caosdb-entity-heading-attr a")
.forEach(split_parent_dir);
// filesystem view
document
.querySelectorAll("img.entity-image-preview")
.forEach((node) => repair_uri(node, "src"));
document
.querySelectorAll("a.caosdb-fs-file")
.forEach((node) => repair_uri(node, "href"));
document
.querySelectorAll("a.caosdb-fs-dir")
.forEach((node) => repair_uri(node, "href"));
},
init: async function () {
this.init_file_upload();
this.repair_file_system_links();
// this object can be used to create a widget, e.g. for creating custom
// upload forms.
window.caosdb_file_upload_widget = this;
},
};
$(document).ready(function () {
const build = window.BUILD_MODULE_EXT_FILE_UPLOAD || "${BUILD_MODULE_EXT_FILE_UPLOAD}"
if (build === "ENABLED") {
caosdb_modules.register(file_upload);
}
});
import React from "react";
import ReactDOM from "react-dom/client";
import "regenerator-runtime/runtime";
import { TransactionService } from "@indiscale/caosdb-webui-entity-service";
import { Map, ToggleMapButton } from "@indiscale/caosdb-webui-ext-map";
import { queryCallback } from "./queryCallback";
......
......@@ -6,6 +6,7 @@ module.exports = {
entry: {
'query-form' : './src/query-form.js',
'map' : './src/map.js',
'file-upload': './src/file-upload.js',
},
output: {
path: path.resolve(__dirname, 'build'),
......
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Please register or to comment