diff --git a/Makefile b/Makefile index 0aa47756d72dcfcaff88efb673b5fa5044f8e05a..4c64a38d01f0273059e29f6dac0448967b005b86 100644 --- a/Makefile +++ b/Makefile @@ -43,7 +43,7 @@ LIBS_DIR = $(abspath libs) TEST_CORE_DIR = $(abspath test/core/) TEST_EXT_DIR = $(abspath test/ext) TEST_SSS_DIR =$(abspath test/server_side_scripting) -LIBS = fonts css/fonts css/bootstrap.css css/bootstrap-icons.css js/state-machine.js js/jquery.js js/showdown.js js/dropzone.js css/dropzone.css js/loglevel.js js/leaflet.js css/leaflet.css css/images js/leaflet-latlng-graticule.js js/proj4.js js/proj4leaflet.js js/leaflet-coordinates.js css/leaflet-coordinates.css js/leaflet-graticule.js js/bootstrap-select.js css/bootstrap-select.css js/bootstrap-autocomplete.min.js js/plotly.js js/pako.js js/utif.js js/bootstrap.js +LIBS = fonts css/fonts css/bootstrap.css css/bootstrap-icons.css js/state-machine.js js/jquery.js js/showdown.js js/dropzone.js css/dropzone.css js/loglevel.js js/leaflet.js css/leaflet.css css/images js/leaflet-latlng-graticule.js js/proj4.js js/proj4leaflet.js js/leaflet-coordinates.js css/leaflet-coordinates.css js/leaflet-graticule.js js/bootstrap-select.js css/bootstrap-select.css js/bootstrap-autocomplete.min.js js/plotly.js js/pako.js js/utif.js js/bootstrap.js js/qrcode.js TEST_LIBS = $(LIBS) js/qunit.js css/qunit.css $(subst $(TEST_CORE_DIR)/,,$(shell find $(TEST_CORE_DIR)/)) @@ -299,6 +299,9 @@ $(LIBS_DIR)/js/pako.js: unzip $(LIBS_DIR)/js $(LIBS_DIR)/js/utif.js: unzip $(LIBS_DIR)/js ln -s $(LIBS_DIR)/UTIF-8205c1f/UTIF.js $@ +$(LIBS_DIR)/js/qrcode.js: unzip $(LIBS_DIR)/js + ln -s $(LIBS_DIR)/qrcode-1.4.4/qrcode.min.js $@ + $(addprefix $(LIBS_DIR)/, js css): mkdir $@ || true diff --git a/build.properties.d/00_default.properties b/build.properties.d/00_default.properties index 386eb8bd22bd3f32b94d9b9ea4714b80e355ea8e..e0432268041a49be4799193b36abf68cb2518abe 100644 --- a/build.properties.d/00_default.properties +++ b/build.properties.d/00_default.properties @@ -152,4 +152,6 @@ MODULE_DEPENDENCIES=( ext_trigger_crawler_form.js ext_bookmarks.js ext_cosmetics.js + qrcode.js + ext_qrcode.js ) diff --git a/libs/qrcode-1.4.4.zip b/libs/qrcode-1.4.4.zip new file mode 100644 index 0000000000000000000000000000000000000000..ed1e0f854985cfcec8cd4f419fe27c195f39c22c Binary files /dev/null and b/libs/qrcode-1.4.4.zip differ diff --git a/src/core/js/ext_map.js b/src/core/js/ext_map.js index 719d4f77943fdfca562961838dc02d95d5fd5cb1..c20cdefc2e9de73a21db81e3a7d5ebacebe73416 100644 --- a/src/core/js/ext_map.js +++ b/src/core/js/ext_map.js @@ -1470,7 +1470,7 @@ var caosdb_map = new function () { */ this.make_entity_name_label = function (entity) { const name = getEntityName(entity); - const id = getEntityId(entity); + const id = getEntityID(entity); const entity_on_page = $(`#${id}`).length > 0; const href = entity_on_page ? `#${id}` : connection.getBasePath() + `Entity/${id}` diff --git a/src/core/js/ext_qrcode.js b/src/core/js/ext_qrcode.js new file mode 100644 index 0000000000000000000000000000000000000000..a3aefe3ae892ffe02b99a17664003d6b8965baa9 --- /dev/null +++ b/src/core/js/ext_qrcode.js @@ -0,0 +1,188 @@ +/* + * This file is a part of the CaosDB Project. + * + * Copyright (C) 2021 IndiScale GmbH <info@indiscale.com> + * Copyright (C) 2021 Timm Fitschen <t.fitschen@indiscale.com> + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see <https://www.gnu.org/licenses/>. + */ + +"use strict"; + +/** + * Adds QR-Code generation to entities. + * + * @author Timm Fitschen + */ +var ext_qrcode = function ($, connection, getEntityVersion, getEntityID, QRCode, logger) { + + const _buttons_list_class = "caosdb-v-entity-header-buttons-list"; + const _qrcode_button_class = "caosdb-f-entity-qrcode-button"; + const _qrcode_canvas_container = "caosdb-f-entity-qrcode"; + const _qrcode_link_container = "caosdb-f-entity-qrcode-link"; + const _qrcode_icon = `<i class="bi bi-upc"></i>`; + + /** + * Create a new QR Code and a caption with a link, either linking to the + * entity head or to the exact version of the entity, based on the selected + * radio buttons and insert it into the modal. + * + * @param {HTMLElement} modal + * @param {string} entity_id + * @param {string} entity_version + */ + var update_qrcode = function(modal, entity_id, entity_version) { + modal = $(modal); + const uri = modal.find("input[name=entity-qrcode-versioned]:checked").val(); + var display_version = ""; + if(uri.indexOf("@") > -1) { + display_version = `@${entity_version.substring(0,8)}`; + } + const description = `Entity <a href="${uri}">${entity_id}${display_version}</a>`; + modal.find(`.${_qrcode_canvas_container}`).empty(); + modal.find(`.${_qrcode_link_container}`).empty().append(description); + QRCode.toCanvas(uri, {"scale": 6}).then((canvas) => { + modal.find(`.${_qrcode_canvas_container}`).empty().append(canvas); + }).catch(logger.error); + } + + /** + * Create modal which shows the QR Code and a form where the user can choose + * whether the QR Code links to the entity head or the exact version of the + * entity. + * + * @param {string} modal_id - id of the resulting HTMLElement + * @param {string} entity_id + * @param {string} entity_version + * @return {HTMLElement} the resulting modal. + */ + var create_qrcode_modal = function(modal_id, entity_id, entity_version) { + const uri = `${connection.getEntityUri([entity_id])}`; + const short_version = entity_version.substring(0,8); + const modal = $(`<div class="modal fade" id="${modal_id}" tabindex="-1" aria-labelledby="${modal_id}-label" aria-hidden="true"> + <div class="modal-dialog"> + <div class="modal-content"> + <div class="modal-header"> + <h4 class="modal-title" id="${modal_id}-label">QR Code</h5> + <button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button> + </div> + <div class="modal-body text-center"> + <div class="${_qrcode_canvas_container}"></div> + <div class="${_qrcode_link_container}"></div> + </div> + <div class="modal-footer justify-content-start"> + <form> + <div class="form-check"> + <label class="form-check-label"> + <input value="${uri}" class="form-check-input" type="radio" name="entity-qrcode-versioned" checked> + Link to this entity. + </label> + </div> + <div class="form-check"> + <label class="form-check-label" for="flexRadioDefault1"> + <input value="${uri}@${entity_version}" class="form-check-input" type="radio" name="entity-qrcode-versioned"> + Link to this exact version of this entity. + </label> + </div> + </form> + </div> + </div> + </div> + </div>`); + modal.find("form").change(() => {update_qrcode(modal, entity_id, entity_version);}); + return modal[0]; + } + + /** + * Click handler of the QR Code button. The click event opens a modal showing + * the QR Code and a form where the user can choose whether the QR Code links + * to the entity head or the exact version of the entity. + * + * @param {string} entity_id + * @param {string} entity_version + */ + var qrcode_button_click_handler = function(entity_id, entity_version) { + const modal_id = `qrcode-modal-${entity_id}-${entity_version}`; + var modal_element = document.getElementById(modal_id); + if(modal_element) { + // toggle modal + const modal = bootstrap.Modal.getInstance(modal_element); + modal.toggle(); + } else { + modal_element = create_qrcode_modal(modal_id, entity_id, entity_version); + update_qrcode(modal_element, entity_id, entity_version); + $("body").append(modal_element); + const options = {}; + const modal = new bootstrap.Modal(modal_element, options); + modal.show(); + } + } + + /** + * Create a button which opens the QR Code modal on click. + * + * @param {string} entity_id + * @param {string} entity_version + * @return {HTMLElement} the newly created button. + */ + var create_qrcode_button = function (entity_id, entity_version) { + const button = $(`<button title="Create QR Code" type="button" class="${_qrcode_button_class} caosdb-v-entity-qrcode-button btn">${_qrcode_icon}</button>`); + button.click(() => {qrcode_button_click_handler(entity_id, entity_version);}); + return button[0]; + } + + /** + * Add a qrcode button to a given entity. + * @param {HTMLElement} entity + */ + var add_qrcode_to_entity = function (entity) { + const entity_id = getEntityID(entity); + const entity_version = getEntityVersion(entity); + + $(entity).find(`.${_buttons_list_class}`).append(create_qrcode_button(entity)); + } + + var remove_qrcode_button = function (entity) { + $(entity).find(`.${_buttons_list_class} .${_qrcode_button_class}`).remove(); + } + + /** + * Initialize this module and append a QR Code button to all entities panels on the page. + * + * Removes all respective buttons if present before adding a new one. + */ + var init = function () { + for(let entity of $(".caosdb-entity-panel")) { + remove_qrcode_button(entity); + add_qrcode_to_entity(entity); + } + }; + + return { + update_qrcode: update_qrcode, + add_qrcode_to_entity: add_qrcode_to_entity, + remove_qrcode_button: remove_qrcode_button, + create_qrcode_button: create_qrcode_button, + create_qrcode_modal: create_qrcode_modal, + qrcode_button_click_handler: qrcode_button_click_handler, + init: init + }; + +}($, connection, getEntityVersion, getEntityID, QRCode, console); + +$(document).ready(function () { + if("${BUILD_MODULE_EXT_QRCODE}"=="ENABLED") { + caosdb_modules.register(ext_qrcode); + } +}); diff --git a/src/core/js/fileupload.js b/src/core/js/fileupload.js index 31d86589286f1761f481cc9d9bb6a557a63cbce1..8a988e86a6a9b5bbb39f39a37d63b32b144c748d 100644 --- a/src/core/js/fileupload.js +++ b/src/core/js/fileupload.js @@ -156,7 +156,7 @@ var fileupload = new function() { // get property-value input element (in case of FILE property) var input = $(property).find(".caosdb-f-property-value input"); var set_value = function(entity) { - input.val(getEntityId(entity)); + input.val(getEntityID(entity)); } if (input.length == 0) { @@ -207,7 +207,7 @@ var fileupload = new function() { getEntityName(entity) + `</code> has been uploaded.</div>`); input.after(`<a class="btn btn-secondary btn-sm" - href="` + connection.getEntityUri([getEntityId(entity)]) + `" target= "_blank">` + + href="` + connection.getEntityUri([getEntityID(entity)]) + `" target= "_blank">` + getEntityName(entity) + `</a>`); }; diff --git a/src/core/js/webcaosdb.js b/src/core/js/webcaosdb.js index 74dc62be15551f987707253ca201777f7c3929ac..dbb4e269a247cbbc40f1ea58623ec7b515dc2d57 100644 --- a/src/core/js/webcaosdb.js +++ b/src/core/js/webcaosdb.js @@ -812,7 +812,7 @@ this.transaction = new function () { $(updatePanel).insertBefore(entity); // create and add waiting notification updatePanel.appendChild(transaction.update.createWaitRetrieveNotification()); - let entityId = getEntityId(entity); + let entityId = getEntityID(entity); transaction.update.retrieveOldEntityXmlString(entityId).then(xmlstr => { app.openForm(xmlstr); }, err => { @@ -1524,19 +1524,6 @@ function removeAllWaitingNotifications(elem) { return elem; } -/** - * Extract the ID of an entity by parsing the textContent of the first occuring element with - * class `caosdb-id`. - * - * @param {HTMLElement} entity - * @returns {Number} ID of entity. - */ -function getEntityId(entity) { - let id = Number.parseInt(entity.getElementsByClassName("caosdb-id")[0].textContent); - if (isNaN(id)) throw new Error("id was NaN"); - return id; -} - // TODO remove and use connection.post /** * Post an xml document to basepath/Entity diff --git a/test/core/js/modules/ext_qrcode.js.js b/test/core/js/modules/ext_qrcode.js.js new file mode 100644 index 0000000000000000000000000000000000000000..35707cc83c9c46a4f235ed2e3a8dfd429e6db33e --- /dev/null +++ b/test/core/js/modules/ext_qrcode.js.js @@ -0,0 +1,75 @@ +/* + * This file is a part of the CaosDB Project. + * + * Copyright (C) 2021 IndiScale GmbH <info@indiscale.com> + * Copyright (C) 2021 Timm Fitschen <t.fitschen@indiscale.com> + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see <https://www.gnu.org/licenses/>. + */ + +'use strict'; + +QUnit.module("ext_qrcode.js", { + before: function (assert) { + // setup before module + }, + beforeEach: function (assert) { + // setup before each test + $(document.body).append('<div data-entity-id="eid123" data-version-id="vid234" id="ext-qrcode-test-entity" class="caosdb-entity-panel"><div class="caosdb-v-entity-header-buttons-list"></div></div>'); + }, + afterEach: function (assert) { + // teardown after each test + const modal = bootstrap.Modal.getInstance($(".modal")[0]); + if(modal) modal.dispose(); + $("#ext-qrcode-test-entity").remove(); + $(".modal").remove(); + }, + after: function (assert) { + // teardown after module + } +}); + +QUnit.test("init", function (assert) { + assert.ok(ext_qrcode.init, "init available"); + assert.equal($(".caosdb-f-entity-qrcode-button").length, 0, "no button before."); + ext_qrcode.init(); + assert.equal($(".caosdb-f-entity-qrcode-button").length, 1, "button has been added."); +}); + +QUnit.test("create_qrcode_button", function(assert) { + assert.equal(ext_qrcode.create_qrcode_button("entityid", "versionid").tagName, "BUTTON", "create_qrcode_button creates a button"); +}); + + +QUnit.test("qrcode_button_click_handler", function(assert) { + var done = assert.async(); + assert.equal($("#qrcode-modal-entityid-versionid").length, 0, "no modal before first click"); + ext_qrcode.qrcode_button_click_handler("entityid", "versionid") + $("#qrcode-modal-entityid-versionid").on("shown.bs.modal", done); + assert.equal($("#qrcode-modal-entityid-versionid").length, 1, "first click added the modal"); +}); + +QUnit.test("update_qrcode", function(assert) { + // create modal + const entity_id = "eid456"; + const entity_version = "vid3564"; + const modal_id = `qrcode-modal-${entity_id}-${entity_version}`; + const modal_element = ext_qrcode.create_qrcode_modal(modal_id, entity_id, entity_version); + $(document.body).append(modal_element); + + assert.equal($("#" + modal_id).find(".caosdb-f-entity-qrcode *").length, 0); + assert.equal($("#" + modal_id).find(".caosdb-f-entity-qrcode-link *").length, 0); + + // TODO call update_qrcode +}); diff --git a/test/core/js/modules/webcaosdb.js.js b/test/core/js/modules/webcaosdb.js.js index 52bf4ada52d2ce59b59d8615c89ca5796343622b..ad26e8455fb7ca16a9e6e9687606b6aba1d0e7e6 100644 --- a/test/core/js/modules/webcaosdb.js.js +++ b/test/core/js/modules/webcaosdb.js.js @@ -91,28 +91,6 @@ QUnit.test("injectTemplate", async function (assert) { assert.equal(xml2str(result_xml), "<newroot xmlns=\"http://www.w3.org/1999/xhtml\">content</newroot>"); }); -QUnit.test("getEntityId", function (assert) { - assert.ok(getEntityId, "function available"); - let okElem = $('<div><div class="caosdb-id">1234</div></div>')[0]; - let notOkElem = $('<div><div class="caosdb-id">asdf</div></div>')[0]; - let emptyElem = $('<div></div>')[0]; - - assert.throws(() => { - getEntityId(); - }, "no parameter throws"); - assert.throws(() => { - getEntityId(null); - }, "null parameter throws"); - assert.throws(() => { - getEntityId(notOkElem); - }, "on-integer ID throws"); - assert.throws(() => { - getEntityId(empty); - }, "empty elem throws"); - - assert.equal("1234", getEntityId(okElem), "ID found"); -}); - QUnit.test("asyncXslt", function (assert) { let xml_str = '<root/>'; let xsl_str = '<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform"><xsl:output method="html" /><xsl:template match="root"><newroot/></xsl:template></xsl:stylesheet>'; @@ -920,7 +898,7 @@ QUnit.test("createCarouselNav", function (assert) { assert.equal($(carousel).find("." + preview.classNamePreviewCarouselNav).length, 1, "carousel has nav"); assert.equal($(carousel).find(".carousel-inner").length, 1, "carousel has inner"); for (let i = 0; i < correct_order_id.length; i++) { - assert.equal(getEntityId($(carousel).find('.carousel-item')[i]), correct_order_id[i], "entities ids are in order") + assert.equal(getEntityID($(carousel).find('.carousel-item')[i]), correct_order_id[i], "entities ids are in order") } assert.ok(carousel.id, "has id");