diff --git a/.gitignore b/.gitignore index cc9336b6256771f79eb104a8b1325a55352657e1..f69db87ad5a5226535559b6965e771d975ded103 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,5 @@ +# -*- mode:conf; -*- + # dot files .* !/.git* @@ -8,13 +10,19 @@ # the build dir /public +/sss_bin +/node_modules/ +/build +__pycache__ + +# auto-generated sources +/src/doc/api # screen logs screenlog.* xerr.log # extensions - conf/ext test/ext src/ext diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index 548f86457c3216f5eed7b75b4b81d7f048351248..6817020fcabf4b7551e264709fc89cc487e4f0a0 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -6,6 +6,7 @@ # Copyright (C) 2019 Henrik tom Wörden # Copyright (C) 2020 Timm Fitschen (t.fitschen@indiscale.com) # Copyright (C) 2020 IndiScale GmbH (info@indiscale.com) +# Copyright (C) 2020 Daniel Hornung <d.hornung@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 @@ -22,9 +23,7 @@ variables: DEPLOY_REF: dev - CI_REGISTRY_IMAGE: $CI_REGISTRY/caosdb/caosdb-webui/testenv - # When using dind, it's wise to use the overlayfs driver for - # improved performance. + CI_REGISTRY_IMAGE: $CI_REGISTRY/caosdb/src/caosdb-webui/testenv image: $CI_REGISTRY_IMAGE:latest @@ -58,15 +57,22 @@ test: script: - make run-qunit +test-server-side-scripting: + timeout: 10 minutes + tags: [ docker ] + stage: test + script: + - whereis pytest pytest3 py.test pytest-3 py.test-3 + - make test-sss + # Trigger building of server image and integration tests trigger_build: timeout: 15 minutes tags: [ docker ] stage: deploy script: - - echo $TOKEN - /usr/bin/curl -X POST - -F token=$DEPLOY_TRIGGER_TOKEN + -F token=$CI_JOB_TOKEN -F "variables[F_BRANCH]=$CI_COMMIT_REF_NAME" -F "variables[WEBUI]=$CI_COMMIT_REF_NAME" -F "variables[TriggerdBy]=WEBUI" @@ -78,13 +84,41 @@ build-testenv: tags: [ cached-dind ] image: docker:19.03 stage: setup + timeout: 3 h + only: + - web + - schedules script: - cd test/docker - docker login -u gitlab-ci-token -p $CI_JOB_TOKEN $CI_REGISTRY # use here general latest or specific branch latest... - - docker pull $CI_REGISTRY_IMAGE:latest || true - docker build --pull - --cache-from $CI_REGISTRY_IMAGE:latest -t $CI_REGISTRY_IMAGE:latest . - docker push $CI_REGISTRY_IMAGE:latest + +# Build the sphinx documentation and make it ready for deployment by Gitlab Pages +# documentation: +# stage: deploy + +# Special job for serving a static website. See https://docs.gitlab.com/ee/ci/yaml/README.html#pages +pages_prepare: &pages_prepare + tags: [ cached-dind ] + stage: deploy + only: + refs: + - /^release-.*$/i + script: + - npm install jsdoc + - npm install jsdoc-sphinx + - echo "Deploying" + - make doc + - rm -r public || true ; cp -r build/doc/html public + artifacts: + paths: + - public +pages: + <<: *pages_prepare + only: + refs: + - main diff --git a/CHANGELOG.md b/CHANGELOG.md index cdc0bbfb39e53824ff52ea968b8926f4eeb413b3..57472cad21ba770edb8f7a2eb0894936d1be0ed8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,14 +8,90 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Added (for new features, dependecies etc.) +* Displaying and interacting with the entity state. +* Change password functionality for users of the internal user source. +* Visually highlighted drop zones for properties and parents in the edit_mode. +* two new field types for the form_elements module, `file` and `select`. See + the module documentation for more information. +- Added a menu/toc for the tour +- Added a previous and next buttons for pages in the tour +- Added warnings to inform about minimum width when accessing tour and + edit mode on small screens. +- Added a tutorial for the edit mode to the documentation + ### Changed (for changes in existing functionality) +- The heading attributes datatype, path, checksum and size are now placed + in a `details` html element. ### Deprecated (for soon-to-be removed features) ### Removed (for now removed features) +* `ext_revisions` module. This module was only a work-around which had been + used for versioning functionality before the native versioning was + implemented. Also, the `BUILD_MODULE_EXT_REVISIONS` is no longer used and can + be removed from the config files in `build.properties.d/` +* `#subnav` element from navbar which was previously used for spacing +* `caosdb.form.ready` event + ### Fixed +* #214 - Paging panel is hidden. +* #156 - Edit mode for Safari 11 +* #160 - Entity preview for Safari 11 +* Several minor cosmetic flaws +* Fixed edit mode for Safari 11. +* Displaying issues with long lists in property values +* An issue whereby a grey container would appear above the map when + changing the map view. + +### Security (in case of vulnerabilities) + +## [0.3.0] - 2021-02-10 + +### Added (for new features, dependecies etc.) + +- The versioning model has a new styling and can show and tsv-export the full + version history now. +- Module `ext_bookmarks` which allows users to bookmark entities, store them in + a link or export them to TSV. +- table previews in the bottom line module +- added preview for tif images +* new function `form_elements.make_alert` which generates a proceed/cancel + dialog which intercepts a function call and asks the user for confirmation. +* Deleting entities prompts for user confirmation after hitting the "Delete" + button (edit_mode). +* Plotly preview has an additional parameter for a config object, + e.g., for disabling the plotly logo +- The map can now show entities that have no geo location but are related to + entities that have one. This also effects the search from the map. +* `getPropertyValues` function which generates a table of property values from + a xml representation of entities. +* After a SELECT statement now also all referenced files can be downloaded. +* Automated documentation builds: `make doc` +- documentation on queries + +### Changed (for changes in existing functionality) + +* ext_map version bumped to 0.4 +- enabled and enhanced autocompletion +* Login form is hidden behind another button. + +### Deprecated (for soon-to-be removed features) + +### Removed (for now removed features) + +### Fixed + +- #144 (Select with ANY VERSION OF). +- #136 (adding reference properties to entities in edit mode) +- exclude configuration files when reading files from build.properties.d +- summaries when opening preview +- #125 special characters like "\t", \"n", "#" are replaced in table + download +- #158 show preview if the entity is too large for the viewport if + bottom line is in view. + ### Security (in case of vulnerabilities) ## [v0.2.1] - 2020-09-07 diff --git a/DEPENDENCIES.md b/DEPENDENCIES.md new file mode 100644 index 0000000000000000000000000000000000000000..037dc9973705005ea2ee656e8381444c38ba5c60 --- /dev/null +++ b/DEPENDENCIES.md @@ -0,0 +1,2 @@ +* CaosDB Server == 0.3 +* Make 4.2.0 diff --git a/makefile b/Makefile similarity index 72% rename from makefile rename to Makefile index 50dd72b500724bbe40801292ab39c3c5cf7ce70f..2c12ee4e67becfd7713ae305c4ab7432bda84391 100644 --- a/makefile +++ b/Makefile @@ -33,14 +33,17 @@ SQ=\' ROOT_DIR = $(abspath .) MISC_DIR = $(abspath misc) PUBLIC_DIR = $(abspath public) +SSS_BIN_DIR = $(abspath sss_bin) CONF_CORE_DIR = $(abspath conf/core) CONF_EXT_DIR = $(abspath conf/ext) SRC_CORE_DIR = $(abspath src/core) SRC_EXT_DIR = $(abspath src/ext) +SRC_SSS_DIR = $(abspath src/server_side_scripting) LIBS_DIR = $(abspath libs) TEST_CORE_DIR = $(abspath test/core/) TEST_EXT_DIR = $(abspath test/ext) -LIBS = fonts css/bootstrap.css js/bootstrap.js 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 +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.bundle.min.js TEST_LIBS = $(LIBS) js/qunit.js css/qunit.css $(subst $(TEST_CORE_DIR)/,,$(shell find $(TEST_CORE_DIR)/)) @@ -49,9 +52,9 @@ LIBS_SUBDIRS = $(addprefix $(LIBS_DIR)/, js css fonts) ALL: install -install: clean cp-src cp-ext cp-conf $(addprefix $(PUBLIC_DIR)/, $(LIBS)) build_properties merge_xsl +install: clean-ignore-zips install-sss cp-src cp-ext cp-conf $(addprefix $(PUBLIC_DIR)/, $(LIBS)) build_properties merge_xsl merge_js -test: clean cp-src cp-ext cp-ext-test cp-conf $(addprefix $(PUBLIC_DIR)/, $(TEST_LIBS)) build_properties merge_xsl +test: clean-ignore-zips install-sss cp-src cp-ext cp-ext-test cp-conf $(addprefix $(PUBLIC_DIR)/, $(TEST_LIBS)) build_properties merge_xsl merge_js @for f in $(shell find $(TEST_EXT_DIR) -type f -iname *.js) ; do \ sed -i "/EXTENSIONS/a \<script src=\"$${f#$(TEST_EXT_DIR)/}\" ></script>" $(PUBLIC_DIR)/index.html ; \ echo include $$f; \ @@ -64,10 +67,16 @@ test: clean cp-src cp-ext cp-ext-test cp-conf $(addprefix $(PUBLIC_DIR)/, $(TEST merge_xsl: misc/merge_xsl.sh +merge_js: + for f in ${BUILDFILELIST} ; do source "$$f" ; done ; \ + misc/merge_js.sh $${MODULE_DEPENDENCIES[*]} + +EXCLUDE_EXPR = %~ %.backup +BUILDFILELIST = $(filter-out $(EXCLUDE_EXPR),$(wildcard build.properties.d/*)) build_properties: @set -a -e ; \ pushd build.properties.files ; \ - for f in ../build.properties.d/* ; do source "$$f" ; done ; \ + for f in ${BUILDFILELIST} ; do echo "processing ../$$f" && source "../$$f" ; done ; \ popd ; \ BUILD_NUMBER=$(BUILD_NUMBER) ; \ PROPS=$$(printenv | grep -e "^BUILD_") ; \ @@ -127,18 +136,33 @@ run-qunit: test exit 1; \ fi +install-sss: + @set -a -e ; \ + pushd build.properties.files ; \ + for f in ../build.properties.d/* ; do source "$$f" ; done ; \ + popd ; \ + ./install-sss.sh $(SRC_SSS_DIR) $(SSS_BIN_DIR) + +PYTEST ?= pytest +PIP ?= pip3 +test-sss: install-sss + $(PIP) freeze + $(PYTEST) -vv $(TEST_SSS_DIR) + + +CMD_COPY_EXT_FILES = cp -i -r -L cp-ext: # TODO FIXME Base path for not-XSL-expanded files mkdir -p $(PUBLIC_DIR)/html for f in $(wildcard $(SRC_EXT_DIR)/html/*) ; do \ - echo "y" | cp -i -r "$$(realpath "$$f")" $(PUBLIC_DIR)/html/ ; \ + echo "y" | $(CMD_COPY_EXT_FILES) "$$(realpath "$$f")" $(PUBLIC_DIR)/html/ ; \ done for f in $(wildcard $(SRC_EXT_DIR)/js/*) ; do \ - echo "y" | cp -i -r "$$f" $(PUBLIC_DIR)/js/ ; \ + echo "y" | $(CMD_COPY_EXT_FILES) "$$f" $(PUBLIC_DIR)/js/ ; \ sed -i "/JS_EXTENSIONS/a \<xsl:element name=\"script\"><xsl:attribute name=\"src\"><xsl:value-of select=\"concat\(\$$basepath, 'webinterface/${BUILD_NUMBER}$${f#$(SRC_EXT_DIR)}'\)\" /></xsl:attribute></xsl:element>" $(PUBLIC_DIR)/xsl/main.xsl ; \ done for f in $(wildcard $(SRC_EXT_DIR)/css/*) ; do \ - echo "y" | cp -i -r "$$f" $(PUBLIC_DIR)/css/ ; \ + echo "y" | $(CMD_COPY_EXT_FILES) "$$f" $(PUBLIC_DIR)/css/ ; \ sed -i "/CSS_EXTENSIONS/a \<xsl:element name=\"link\"><xsl:attribute name=\"rel\">stylesheet</xsl:attribute><xsl:attribute name=\"href\"><xsl:value-of select=\"concat\(\$$basepath, 'webinterface/${BUILD_NUMBER}$${f#$(SRC_EXT_DIR)}'\)\" /></xsl:attribute></xsl:element>" $(PUBLIC_DIR)/xsl/main.xsl ; \ for html in $(PUBLIC_DIR)/html/* ; do \ echo "$$html"; \ @@ -146,26 +170,26 @@ cp-ext: done \ done for f in $(wildcard $(SRC_EXT_DIR)/pics/*) ; do \ - echo "y" | cp -i -r "$$(realpath "$$f")" $(PUBLIC_DIR)/pics/ ; \ + echo "y" | $(CMD_COPY_EXT_FILES) "$$(realpath "$$f")" $(PUBLIC_DIR)/pics/ ; \ done for f in $(wildcard $(SRC_EXT_DIR)/xsl/*) ; do \ - echo "y" | cp -i -r "$$(realpath "$$f")" $(PUBLIC_DIR)/xsl/ ; \ + echo "y" | $(CMD_COPY_EXT_FILES) "$$(realpath "$$f")" $(PUBLIC_DIR)/xsl/ ; \ done cp-ext-test: for f in $(wildcard $(TEST_EXT_DIR)/js/*) ; do \ - echo "y" | cp -i -r "$$f" $(PUBLIC_DIR)/js/ ; \ + echo "y" | $(CMD_COPY_EXT_FILES) "$$f" $(PUBLIC_DIR)/js/ ; \ sed -i "/JS_EXTENSIONS/a \<xsl:element name=\"script\"><xsl:attribute name=\"src\"><xsl:value-of select=\"concat\(\$$basepath, 'webinterface/${BUILD_NUMBER}$${f#$(SRC_EXT_DIR)}'\)\" /></xsl:attribute></xsl:element>" $(PUBLIC_DIR)/xsl/main.xsl ; \ done mkdir -p $(PUBLIC_DIR)/html for f in $(wildcard $(TEST_EXT_DIR)/html/*) ; do \ - echo "y" | cp -i -r "$$(realpath "$$f")" $(PUBLIC_DIR)/html/ ; \ + echo "y" | $(CMD_COPY_EXT_FILES) "$$(realpath "$$f")" $(PUBLIC_DIR)/html/ ; \ done for f in $(wildcard $(TEST_EXT_DIR)/pics/*) ; do \ - echo "y" | cp -i -r "$$(realpath "$$f")" $(PUBLIC_DIR)/pics/ ; \ + echo "y" | $(CMD_COPY_EXT_FILES) "$$(realpath "$$f")" $(PUBLIC_DIR)/pics/ ; \ done for f in $(wildcard $(TEST_EXT_DIR)/xsl/*) ; do \ - echo "y" | cp -i -r "$$(realpath "$$f")" $(PUBLIC_DIR)/xsl/ ; \ + echo "y" | $(CMD_COPY_EXT_FILES) "$$(realpath "$$f")" $(PUBLIC_DIR)/xsl/ ; \ done cp-conf: convert-yaml @@ -191,22 +215,25 @@ $(PUBLIC_DIR)/%: $(TEST_EXT_DIR)/% cp -r $< $@ $(LIBS_DIR)/fonts: unzip - ln -s $(LIBS_DIR)/bootstrap-3.3.7-dist/fonts $@ + ln -s $(LIBS_DIR)/bootstrap-icons-1.4.1/fonts/ $@ $(LIBS_DIR)/js/bootstrap.js: unzip $(LIBS_DIR)/js - ln -s $(LIBS_DIR)/bootstrap-3.3.7-dist/js/bootstrap.min.js $@ + ln -s $(LIBS_DIR)/bootstrap-5.0.0-beta3-dist/js/bootstrap.min.js $@ $(LIBS_DIR)/css/bootstrap.css: unzip $(LIBS_DIR)/css - ln -s $(LIBS_DIR)/bootstrap-3.3.7-dist/css/bootstrap.min.css $@ + ln -s $(LIBS_DIR)/bootstrap-5.0.0-beta3-dist/css/bootstrap.min.css $@ + +$(LIBS_DIR)/css/bootstrap-icons.css: unzip $(LIBS_DIR)/css + ln -s $(LIBS_DIR)/bootstrap-icons-1.4.1/bootstrap-icons.css $@ $(LIBS_DIR)/js/bootstrap-select.js: unzip $(LIBS_DIR)/js - ln -s $(LIBS_DIR)/bootstrap-select-1.13.9/dist/js/bootstrap-select.min.js $@ + ln -s $(LIBS_DIR)/bootstrap-select-1.14.0-beta2/js/bootstrap-select.min.js $@ $(LIBS_DIR)/css/bootstrap-select.css: unzip $(LIBS_DIR)/css - ln -s $(LIBS_DIR)/bootstrap-select-1.13.9/dist/css/bootstrap-select.min.css $@ + ln -s $(LIBS_DIR)/bootstrap-select-1.14.0-beta2/css/bootstrap-select.min.css $@ $(LIBS_DIR)/js/jquery.js: unzip $(LIBS_DIR)/js - ln -s $(LIBS_DIR)/jquery-3.3.1/jquery-3.3.1.min.js $@ + ln -s $(LIBS_DIR)/jquery-3.6.0.min.js $@ $(LIBS_DIR)/js/showdown.js: unzip $(LIBS_DIR)/js ln -s $(LIBS_DIR)/showdown-1.8.6/dist/showdown.min.js $@ @@ -241,6 +268,9 @@ $(LIBS_DIR)/css/images: unzip $(LIBS_DIR)/css $(LIBS_DIR)/js/leaflet-graticule.js: unzip $(LIBS_DIR)/js ln -s $(LIBS_DIR)/L.Graticule.js $@ +$(LIBS_DIR)/js/bootstrap.bundle.min.js: unzip $(LIBS_DIR)/js + ln -s $(LIBS_DIR)/bootstrap.bundle.min.js $@ + $(LIBS_DIR)/js/leaflet-latlng-graticule.js: unzip $(LIBS_DIR)/js ln -s $(LIBS_DIR)/leaflet.latlng-graticule-20191007/leaflet.latlng-graticule.js $@ @@ -257,32 +287,47 @@ $(LIBS_DIR)/css/leaflet-coordinates.css: unzip $(LIBS_DIR)/css ln -s $(LIBS_DIR)/Leaflet.Coordinates-0.1.5/dist/Leaflet.Coordinates-0.1.5.css $@ $(LIBS_DIR)/js/bootstrap-autocomplete.min.js: unzip $(LIBS_DIR)/js - ln -s $(LIBS_DIR)/bootstrap-autocomplete-2.3.0/dist/latest/bootstrap-autocomplete.min.js $@ + ln -s $(LIBS_DIR)/bootstrap-autocomplete-2.3.5/dist/latest/bootstrap-autocomplete.min.js $@ $(LIBS_DIR)/js/plotly.js: unzip $(LIBS_DIR)/js ln -s $(LIBS_DIR)/plotly.js-1.52.2/dist/plotly.min.js $@ +$(LIBS_DIR)/js/pako.js: unzip $(LIBS_DIR)/js + ln -s $(LIBS_DIR)/pako-dummy/pako.js $@ + +$(LIBS_DIR)/js/utif.js: unzip $(LIBS_DIR)/js + ln -s $(LIBS_DIR)/UTIF-8205c1f/UTIF.js $@ + $(addprefix $(LIBS_DIR)/, js css): mkdir $@ || true -.PHONY: clean -clean: +$(LIBS_DIR)/css/fonts: $(LIBS_DIR)/css + ln -s $(LIBS_DIR)/fonts/ $(LIBS_DIR)/css/fonts + +.PHONY: clean-ignore-zips +clean-ignore-zips: + $(RM) -r $(SSS_BIN_DIR) $(RM) -r $(PUBLIC_DIR) for f in $(LIBS_SUBDIRS); do unlink $$f || $(RM) -r $$f || true; done - for f in $(patsubst %.zip,%/,$(LIBS_ZIP)); do $(RM) -r $$f; done $(RM) .server_done +.PHONY: clean +clean: clean-ignore-zips + for f in $(patsubst %.zip,%/,$(LIBS_ZIP)); do $(RM) -r $$f; done + .PHONY: unzip unzip: - for f in $(LIBS_ZIP); do unzip -q -o -d libs $$f; done + for f in $(LIBS_ZIP); do echo "unzip $$f" ; unzip -u -q -o -d libs $$f; done -PYLINT = pylint3 -d all -e E,F +PYLINT ?= pylint PYTHON_FILES = $(subst $(ROOT_DIR)/,,$(shell find $(ROOT_DIR)/ -iname "*.py")) pylint: $(PYTHON_FILES) - for f in $(PYTHON_FILES); do $(PYLINT) $$f || exit 1; done + for f in $(PYTHON_FILES); do $(PYLINT) -d all -e E,F $$f || exit 1; done + -PYLINT_LOCAL = /usr/bin/pylint3 -d all -e E,F -pylint-local: $(PYTHON_FILES) - for f in $(PYTHON_FILES); do $(PYLINT_LOCAL) $$f || exit 1; done +# Compile the standalone documentation +.PHONY: doc +doc: + $(MAKE) -C src/doc html diff --git a/README.md b/README.md index 3f144a30731dab4f425f3f8978248f730a4c392c..5abbfafe8f45ad430e91d66ddd50f055cad61a25 100644 --- a/README.md +++ b/README.md @@ -1,27 +1,47 @@ -<!--THIS FILE HAS BEEN GENERATED BY A SCRIPT. PLEASE DON'T CHANGE IT MANUALLY.--> -Project migrated to https://gitlab.com/caosdb +# README -# Welcome +## Welcome -This is the **CaosDB WebUI** repository and a part of the CaosDB project. +This is the **CaosDB Web User Interface** repository and a part of the +CaosDB project. -# Setup +## Setup Please read the [README_SETUP.md](README_SETUP.md) for instructions on how to setup this code. -# Further Reading +## Further Reading -Please refer to the [official gitlab repository of the CaosDB -project](https://gitlab.com/caosdb/caosdb) for more information. +Please refer to the [official documentation](https://docs.indiscale.com/caosdb-webui/) for more information. -# License +## Contributing -Copyright (C) 2018 Research Group Biomedical Physics, Max Planck Institute for -Dynamics and Self-Organization Göttingen. +Thank you very much to all contributers—[past, present](https://gitlab.com/caosdb/caosdb/-/blob/dev/HUMANS.md), and prospective ones. + +### Code of Conduct + +By participating, you are expected to uphold our [Code of Conduct](https://gitlab.com/caosdb/caosdb/-/blob/dev/CODE_OF_CONDUCT.md). + +### How to Contribute + +* You found a bug, have a question, or want to request a feature? Please +[create an issue](https://gitlab.com/caosdb/caosdb-webui/-/issues). +* You want to contribute code? Please fork the repository and create a merge +request in GitLab and choose this repository as target. Make sure to select +"Allow commits from members who can merge the target branch" under Contribution +when creating the merge request. This allows our team to work with you on your request. +- If you have a suggestion for the [documentation](https://docs.indiscale.com/caosdb-webui/), +the preferred way is also a merge request as describe above (the documentation resides in `src/doc`). +However, you can also create an issue for it. +- You can also contact us at **info (AT) caosdb.de**. + +## License + +* Copyright (C) 2018 Research Group Biomedical Physics, Max Planck Institute + for Dynamics and Self-Organization Göttingen. +* Copyright (C) 2020-2021 Indiscale GmbH <info@indiscale.com> All files in this repository are licensed under a [GNU Affero General Public License](LICENCE.md) (version 3 or later). - diff --git a/README_SETUP.md b/README_SETUP.md index c027fd7d9af49c33492122fc575254b8d6b00845..485835fdae10239cb4329b0f9b6708c1b020fa2a 100644 --- a/README_SETUP.md +++ b/README_SETUP.md @@ -21,12 +21,16 @@ * ** end header --> -# Folder Structure +# Getting Started with the Web Interface +Here, we document how to install and build the CaosDB Web Interface. If you are +only interested in how to use it, please continue [here](tutorials/first_steps.html) -* The `src` folder contains all source code for the webinterface. +## Folder Structure + +* The `src` folder contains all source code for the web interface. * The `libs` folder contains all necessary third-party libraries as zip files. -* The `test` folder contains the unittests for the webinterface. -* The `ext` folder contains extension for the webinterface. The make file will +* The `test` folder contains the unittests for the web interface. +* The `ext` folder contains extension for the web interface. The make file will copy all javascript files from `ext/js/` into the public folder and links the javascript in the `public/xsl/main.xsl`. * The `misc` folder contains a simple http server which is used for running the @@ -34,33 +38,49 @@ * The `build.properties.d/` folder contains configuration files for the build. -# Build Configuration +## Build Configuration The default configuration is defined in `build.properties.d/00_default.properties`. -This file defines default variables which will be replaced in the source files -during the build. +This file defines default variables which can be used in source files and +will be replaced with the defined values during the build. All files in that directory will be sourced during `make install` and `make test`. Thus any customized configuration can also be added to that folder by just placing files in there which override the default values from `00_default.properties`. -See `build.properties.d/00_default.properties` for more -information. +See `build.properties.d/00_default.properties` for more information. -# Setup +## Setup -* Run `make install` to compile/copy the webinterface to a newly created +* Run `make install` to compile/copy the web interface to a newly created `public` folder. +* Also, `make install` will copy the scripts from `src/server_side_scripting/` + to `sss_bin/`. If you want to make the server-side scripts callable for the + server as server-side scripts you need to include the `sss_bin/` directory + into the server property `SERVER_SIDE_SCRIPTING_BIN_DIRS`. -# Test +## Test -* Run `make test` to compile/copy the webinterface and the tests to a newly +* Run `make test` to compile/copy the web interface and the tests to a newly created `public` folder. * Run `make run-test-server` to start a python http server. * The test suite can be started with `firefox http://localhost:8000/`. -# Clean +## Clean * Run `make clean` to clean up everything. + +## Documentation # + +Build documentation in `build/` with `make doc`. + +### Requirements ## + +- sphinx +- sphinx-autoapi +- jsdoc (`npm install jsdoc`) +- jsdoc-sphinx (`npm install jsdoc-sphinx`) +- sphinx-js +- recommonmark diff --git a/RELEASE_GUIDELINES.md b/RELEASE_GUIDELINES.md index ce69c0fc24e2ea754589ffe4dd4b8bf2a2fadfaf..1dc6d11a442b9f095d7e80b02a3a9542e62b8e98 100644 --- a/RELEASE_GUIDELINES.md +++ b/RELEASE_GUIDELINES.md @@ -18,10 +18,10 @@ guidelines of the CaosDB Project 2. Check all general prerequisites. -3. Merge the release branch into the master branch. +3. Merge the release branch into the main branch. -4. Tag the latest commit of the master branch with `v<VERSION>`. +4. Tag the latest commit of the main branch with `v<VERSION>`. 5. Delete the release branch. -6. Merge the master branch back into the dev branch. +6. Merge the main branch back into the dev branch. diff --git a/References_button.png b/References_button.png new file mode 100644 index 0000000000000000000000000000000000000000..998ef42f7ccb17e32b0c88b8395c249b4529c97f Binary files /dev/null and b/References_button.png differ diff --git a/build.properties.d/00_default.properties b/build.properties.d/00_default.properties index 23b1e9ff929b030750f6cab0014f22cd0d6b7cf4..36d296789f6058d106b450f405af726bf0273b9d 100644 --- a/build.properties.d/00_default.properties +++ b/build.properties.d/00_default.properties @@ -45,6 +45,14 @@ BUILD_MODULE_EXT_PREVIEW=ENABLED BUILD_MODULE_EXT_RESOLVE_REFERENCES=ENABLED BUILD_MODULE_EXT_SSS_MARKDOWN=DISABLED BUILD_MODULE_EXT_TRIGGER_CRAWLER_FORM=DISABLED +BUILD_MODULE_EXT_AUTOCOMPLETE=ENABLED +BUILD_MODULE_EXT_BOTTOM_LINE=ENABLED +BUILD_MODULE_EXT_BOTTOM_LINE_TABLE_PREVIEW=DISABLED +BUILD_MODULE_EXT_BOTTOM_LINE_TIFF_PREVIEW=DISABLED +BUILD_MODULE_EXT_BOOKMARKS=ENABLED + +BUILD_MODULE_USER_MANAGEMENT=ENABLED +BUILD_MODULE_USER_MANAGEMENT_CHANGE_OWN_PASSWORD_REALM=CaosDB ############################################################################## # Navbar properties @@ -63,7 +71,7 @@ BUILD_FAVICON=pics/caosdb_logo_42.png ############################################################################## # Link to the data policy statement document. -BUILD_FOOTER_DATA_POLICY_HREF=https://indiscale.com/?page_id=156 +BUILD_FOOTER_DATA_POLICY_HREF=https://missing-domain.com/missing-page # Custom footer elements can be placed here (will be placed inside a <div> # element). @@ -82,3 +90,48 @@ BUILD_CUSTOM_IMPRINT='<p> Put an imprint note here </p>' # ext_trigger_crawler_form properties ############################################################################## BUILD_MODULE_EXT_TRIGGER_CRAWLER_FORM_TOOLBOX="Tools" + +############################################################################## +# Module dependencies +# Override or extend to specify the order of js files in the resulting +# bundled js file +############################################################################## +MODULE_DEPENDENCIES=( + jquery.js + bootstrap.bundle.min.js + bootstrap-autocomplete.min.js + bootstrap-select.js + state-machine.js + showdown.js + dropzone.js + loglevel.js + plotly.js + webcaosdb.js + pako.js + utif.js + caosdb.js + form_elements.js + ext_autocomplete.js + preview.js + ext_references.js + ext_table_preview.js + ext_xls_download.js + query_shortcuts.js + ext_jupyterdrag.js + annotation.js + edit_mode.js + ext_entity_state.js + ext_file_download.js + leaflet.js + leaflet-graticule.js + leaflet-latlng-graticule.js + leaflet-coordinates.js + proj4.js + proj4leaflet.js + ext_map.js + tour.js + ext_bottom_line.js + ext_sss_markdown.js + ext_trigger_crawler_form.js + ext_bookmarks.js +) diff --git a/install-sss.sh b/install-sss.sh new file mode 100755 index 0000000000000000000000000000000000000000..bb2db57649000ba1e701786f56dba575753110eb --- /dev/null +++ b/install-sss.sh @@ -0,0 +1,17 @@ +SRC_DIR=$1 +INSTALL_DIR=$2 + +mkdir -p $INSTALL_DIR + +# from here on do your module-wise installing + +# ext_table_preview +if [ "${BUILD_MODULE_EXT_BOTTOM_LINE_TABLE_PREVIEW}" == "ENABLED" ]; then + mkdir -p $INSTALL_DIR/ext_table_preview + cp $SRC_DIR/ext_table_preview/*.py $INSTALL_DIR/ext_table_preview/ + echo "installed all server-side scripts for ext_table_preview" +fi +# ext_file_download; should always be installed - No build variable +mkdir -p $INSTALL_DIR/ext_file_download +cp $SRC_DIR/ext_file_download/*.py $INSTALL_DIR/ext_file_download/ +echo "installed all server-side scripts for ext_file_download" diff --git a/libs/UTIF-8205c1f.zip b/libs/UTIF-8205c1f.zip new file mode 100644 index 0000000000000000000000000000000000000000..7069cd288d5d629a7fc3a03d8d92415e78aeb728 Binary files /dev/null and b/libs/UTIF-8205c1f.zip differ diff --git a/libs/bootstrap-3.3.7-dist.zip b/libs/bootstrap-3.3.7-dist.zip deleted file mode 100644 index 6fbb95ebaa3867ce196ef3b54951a732107d94d2..0000000000000000000000000000000000000000 Binary files a/libs/bootstrap-3.3.7-dist.zip and /dev/null differ diff --git a/libs/bootstrap-5.0.0-beta3-dist.zip b/libs/bootstrap-5.0.0-beta3-dist.zip new file mode 100644 index 0000000000000000000000000000000000000000..c10719c59c4b76cf71ad04985feee6b5cbf73bcd Binary files /dev/null and b/libs/bootstrap-5.0.0-beta3-dist.zip differ diff --git a/libs/bootstrap-autocomplete-2.3.0.zip b/libs/bootstrap-autocomplete-2.3.0.zip deleted file mode 100644 index 206c00f49c87794e996c46da2f086d0a1d118071..0000000000000000000000000000000000000000 Binary files a/libs/bootstrap-autocomplete-2.3.0.zip and /dev/null differ diff --git a/libs/bootstrap-autocomplete-2.3.5.zip b/libs/bootstrap-autocomplete-2.3.5.zip new file mode 100644 index 0000000000000000000000000000000000000000..8cc2e03067955193bb89af2972a35ee9e0260b35 Binary files /dev/null and b/libs/bootstrap-autocomplete-2.3.5.zip differ diff --git a/libs/bootstrap-icons-1.4.1.zip b/libs/bootstrap-icons-1.4.1.zip new file mode 100644 index 0000000000000000000000000000000000000000..dc9398e6d6e6ce3c96e01b4734a29a37432f43b1 Binary files /dev/null and b/libs/bootstrap-icons-1.4.1.zip differ diff --git a/libs/bootstrap-select-1.14.0-beta2.zip b/libs/bootstrap-select-1.14.0-beta2.zip new file mode 100644 index 0000000000000000000000000000000000000000..9d7a97280ee9825b6a23aaddb40d20256a892f85 Binary files /dev/null and b/libs/bootstrap-select-1.14.0-beta2.zip differ diff --git a/libs/bootstrap-select-v1.13.9.zip b/libs/bootstrap-select-v1.13.9.zip deleted file mode 100644 index f52eea6450673782ba84504b249d65f3e6f8fa14..0000000000000000000000000000000000000000 Binary files a/libs/bootstrap-select-v1.13.9.zip and /dev/null differ diff --git a/libs/bootstrap.bundle.min.js b/libs/bootstrap.bundle.min.js new file mode 100644 index 0000000000000000000000000000000000000000..2168d63301800ff5696abcb4ba522a6a0a5a7533 --- /dev/null +++ b/libs/bootstrap.bundle.min.js @@ -0,0 +1,7 @@ +/*! + * Bootstrap v5.0.0-beta3 (https://getbootstrap.com/) + * Copyright 2011-2021 The Bootstrap Authors (https://github.com/twbs/bootstrap/graphs/contributors) + * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE) + */ +!function(t,e){"object"==typeof exports&&"undefined"!=typeof module?module.exports=e():"function"==typeof define&&define.amd?define(e):(t="undefined"!=typeof globalThis?globalThis:t||self).bootstrap=e()}(this,(function(){"use strict";const t=t=>{do{t+=Math.floor(1e6*Math.random())}while(document.getElementById(t));return t},e=t=>{let e=t.getAttribute("data-bs-target");if(!e||"#"===e){let i=t.getAttribute("href");if(!i||!i.includes("#")&&!i.startsWith("."))return null;i.includes("#")&&!i.startsWith("#")&&(i="#"+i.split("#")[1]),e=i&&"#"!==i?i.trim():null}return e},i=t=>{const i=e(t);return i&&document.querySelector(i)?i:null},s=t=>{const i=e(t);return i?document.querySelector(i):null},n=t=>{if(!t)return 0;let{transitionDuration:e,transitionDelay:i}=window.getComputedStyle(t);const s=Number.parseFloat(e),n=Number.parseFloat(i);return s||n?(e=e.split(",")[0],i=i.split(",")[0],1e3*(Number.parseFloat(e)+Number.parseFloat(i))):0},o=t=>{t.dispatchEvent(new Event("transitionend"))},r=t=>(t[0]||t).nodeType,a=(t,e)=>{let i=!1;const s=e+5;t.addEventListener("transitionend",(function e(){i=!0,t.removeEventListener("transitionend",e)})),setTimeout(()=>{i||o(t)},s)},l=(t,e,i)=>{Object.keys(i).forEach(s=>{const n=i[s],o=e[s],a=o&&r(o)?"element":null==(l=o)?""+l:{}.toString.call(l).match(/\s([a-z]+)/i)[1].toLowerCase();var l;if(!new RegExp(n).test(a))throw new TypeError(t.toUpperCase()+": "+`Option "${s}" provided type "${a}" `+`but expected type "${n}".`)})},c=t=>{if(!t)return!1;if(t.style&&t.parentNode&&t.parentNode.style){const e=getComputedStyle(t),i=getComputedStyle(t.parentNode);return"none"!==e.display&&"none"!==i.display&&"hidden"!==e.visibility}return!1},d=t=>!t||t.nodeType!==Node.ELEMENT_NODE||!!t.classList.contains("disabled")||(void 0!==t.disabled?t.disabled:t.hasAttribute("disabled")&&"false"!==t.getAttribute("disabled")),h=t=>{if(!document.documentElement.attachShadow)return null;if("function"==typeof t.getRootNode){const e=t.getRootNode();return e instanceof ShadowRoot?e:null}return t instanceof ShadowRoot?t:t.parentNode?h(t.parentNode):null},f=()=>function(){},u=t=>t.offsetHeight,p=()=>{const{jQuery:t}=window;return t&&!document.body.hasAttribute("data-bs-no-jquery")?t:null},g=()=>"rtl"===document.documentElement.dir,m=(t,e)=>{var i;i=()=>{const i=p();if(i){const s=i.fn[t];i.fn[t]=e.jQueryInterface,i.fn[t].Constructor=e,i.fn[t].noConflict=()=>(i.fn[t]=s,e.jQueryInterface)}},"loading"===document.readyState?document.addEventListener("DOMContentLoaded",i):i()},_=new Map;var b={set(t,e,i){_.has(t)||_.set(t,new Map);const s=_.get(t);s.has(e)||0===s.size?s.set(e,i):console.error(`Bootstrap doesn't allow more than one instance per element. Bound instance: ${Array.from(s.keys())[0]}.`)},get:(t,e)=>_.has(t)&&_.get(t).get(e)||null,remove(t,e){if(!_.has(t))return;const i=_.get(t);i.delete(e),0===i.size&&_.delete(t)}};const v=/[^.]*(?=\..*)\.|.*/,y=/\..*/,w=/::\d+$/,E={};let T=1;const A={mouseenter:"mouseover",mouseleave:"mouseout"},L=new Set(["click","dblclick","mouseup","mousedown","contextmenu","mousewheel","DOMMouseScroll","mouseover","mouseout","mousemove","selectstart","selectend","keydown","keypress","keyup","orientationchange","touchstart","touchmove","touchend","touchcancel","pointerdown","pointermove","pointerup","pointerleave","pointercancel","gesturestart","gesturechange","gestureend","focus","blur","change","reset","select","submit","focusin","focusout","load","unload","beforeunload","resize","move","DOMContentLoaded","readystatechange","error","abort","scroll"]);function O(t,e){return e&&`${e}::${T++}`||t.uidEvent||T++}function k(t){const e=O(t);return t.uidEvent=e,E[e]=E[e]||{},E[e]}function D(t,e,i=null){const s=Object.keys(t);for(let n=0,o=s.length;n<o;n++){const o=t[s[n]];if(o.originalHandler===e&&o.delegationSelector===i)return o}return null}function x(t,e,i){const s="string"==typeof e,n=s?i:e;let o=t.replace(y,"");const r=A[o];return r&&(o=r),L.has(o)||(o=t),[s,n,o]}function C(t,e,i,s,n){if("string"!=typeof e||!t)return;i||(i=s,s=null);const[o,r,a]=x(e,i,s),l=k(t),c=l[a]||(l[a]={}),d=D(c,r,o?i:null);if(d)return void(d.oneOff=d.oneOff&&n);const h=O(r,e.replace(v,"")),f=o?function(t,e,i){return function s(n){const o=t.querySelectorAll(e);for(let{target:e}=n;e&&e!==this;e=e.parentNode)for(let r=o.length;r--;)if(o[r]===e)return n.delegateTarget=e,s.oneOff&&N.off(t,n.type,i),i.apply(e,[n]);return null}}(t,i,s):function(t,e){return function i(s){return s.delegateTarget=t,i.oneOff&&N.off(t,s.type,e),e.apply(t,[s])}}(t,i);f.delegationSelector=o?i:null,f.originalHandler=r,f.oneOff=n,f.uidEvent=h,c[h]=f,t.addEventListener(a,f,o)}function S(t,e,i,s,n){const o=D(e[i],s,n);o&&(t.removeEventListener(i,o,Boolean(n)),delete e[i][o.uidEvent])}const N={on(t,e,i,s){C(t,e,i,s,!1)},one(t,e,i,s){C(t,e,i,s,!0)},off(t,e,i,s){if("string"!=typeof e||!t)return;const[n,o,r]=x(e,i,s),a=r!==e,l=k(t),c=e.startsWith(".");if(void 0!==o){if(!l||!l[r])return;return void S(t,l,r,o,n?i:null)}c&&Object.keys(l).forEach(i=>{!function(t,e,i,s){const n=e[i]||{};Object.keys(n).forEach(o=>{if(o.includes(s)){const s=n[o];S(t,e,i,s.originalHandler,s.delegationSelector)}})}(t,l,i,e.slice(1))});const d=l[r]||{};Object.keys(d).forEach(i=>{const s=i.replace(w,"");if(!a||e.includes(s)){const e=d[i];S(t,l,r,e.originalHandler,e.delegationSelector)}})},trigger(t,e,i){if("string"!=typeof e||!t)return null;const s=p(),n=e.replace(y,""),o=e!==n,r=L.has(n);let a,l=!0,c=!0,d=!1,h=null;return o&&s&&(a=s.Event(e,i),s(t).trigger(a),l=!a.isPropagationStopped(),c=!a.isImmediatePropagationStopped(),d=a.isDefaultPrevented()),r?(h=document.createEvent("HTMLEvents"),h.initEvent(n,l,!0)):h=new CustomEvent(e,{bubbles:l,cancelable:!0}),void 0!==i&&Object.keys(i).forEach(t=>{Object.defineProperty(h,t,{get:()=>i[t]})}),d&&h.preventDefault(),c&&t.dispatchEvent(h),h.defaultPrevented&&void 0!==a&&a.preventDefault(),h}};class j{constructor(t){(t="string"==typeof t?document.querySelector(t):t)&&(this._element=t,b.set(this._element,this.constructor.DATA_KEY,this))}dispose(){b.remove(this._element,this.constructor.DATA_KEY),this._element=null}static getInstance(t){return b.get(t,this.DATA_KEY)}static get VERSION(){return"5.0.0-beta3"}}class P extends j{static get DATA_KEY(){return"bs.alert"}close(t){const e=t?this._getRootElement(t):this._element,i=this._triggerCloseEvent(e);null===i||i.defaultPrevented||this._removeElement(e)}_getRootElement(t){return s(t)||t.closest(".alert")}_triggerCloseEvent(t){return N.trigger(t,"close.bs.alert")}_removeElement(t){if(t.classList.remove("show"),!t.classList.contains("fade"))return void this._destroyElement(t);const e=n(t);N.one(t,"transitionend",()=>this._destroyElement(t)),a(t,e)}_destroyElement(t){t.parentNode&&t.parentNode.removeChild(t),N.trigger(t,"closed.bs.alert")}static jQueryInterface(t){return this.each((function(){let e=b.get(this,"bs.alert");e||(e=new P(this)),"close"===t&&e[t](this)}))}static handleDismiss(t){return function(e){e&&e.preventDefault(),t.close(this)}}}N.on(document,"click.bs.alert.data-api",'[data-bs-dismiss="alert"]',P.handleDismiss(new P)),m("alert",P);class I extends j{static get DATA_KEY(){return"bs.button"}toggle(){this._element.setAttribute("aria-pressed",this._element.classList.toggle("active"))}static jQueryInterface(t){return this.each((function(){let e=b.get(this,"bs.button");e||(e=new I(this)),"toggle"===t&&e[t]()}))}}function M(t){return"true"===t||"false"!==t&&(t===Number(t).toString()?Number(t):""===t||"null"===t?null:t)}function R(t){return t.replace(/[A-Z]/g,t=>"-"+t.toLowerCase())}N.on(document,"click.bs.button.data-api",'[data-bs-toggle="button"]',t=>{t.preventDefault();const e=t.target.closest('[data-bs-toggle="button"]');let i=b.get(e,"bs.button");i||(i=new I(e)),i.toggle()}),m("button",I);const B={setDataAttribute(t,e,i){t.setAttribute("data-bs-"+R(e),i)},removeDataAttribute(t,e){t.removeAttribute("data-bs-"+R(e))},getDataAttributes(t){if(!t)return{};const e={};return Object.keys(t.dataset).filter(t=>t.startsWith("bs")).forEach(i=>{let s=i.replace(/^bs/,"");s=s.charAt(0).toLowerCase()+s.slice(1,s.length),e[s]=M(t.dataset[i])}),e},getDataAttribute:(t,e)=>M(t.getAttribute("data-bs-"+R(e))),offset(t){const e=t.getBoundingClientRect();return{top:e.top+document.body.scrollTop,left:e.left+document.body.scrollLeft}},position:t=>({top:t.offsetTop,left:t.offsetLeft})},H={find:(t,e=document.documentElement)=>[].concat(...Element.prototype.querySelectorAll.call(e,t)),findOne:(t,e=document.documentElement)=>Element.prototype.querySelector.call(e,t),children:(t,e)=>[].concat(...t.children).filter(t=>t.matches(e)),parents(t,e){const i=[];let s=t.parentNode;for(;s&&s.nodeType===Node.ELEMENT_NODE&&3!==s.nodeType;)s.matches(e)&&i.push(s),s=s.parentNode;return i},prev(t,e){let i=t.previousElementSibling;for(;i;){if(i.matches(e))return[i];i=i.previousElementSibling}return[]},next(t,e){let i=t.nextElementSibling;for(;i;){if(i.matches(e))return[i];i=i.nextElementSibling}return[]}},W={interval:5e3,keyboard:!0,slide:!1,pause:"hover",wrap:!0,touch:!0},U={interval:"(number|boolean)",keyboard:"boolean",slide:"(boolean|string)",pause:"(string|boolean)",wrap:"boolean",touch:"boolean"},$="next",F="prev",z="left",K="right";class Y extends j{constructor(t,e){super(t),this._items=null,this._interval=null,this._activeElement=null,this._isPaused=!1,this._isSliding=!1,this.touchTimeout=null,this.touchStartX=0,this.touchDeltaX=0,this._config=this._getConfig(e),this._indicatorsElement=H.findOne(".carousel-indicators",this._element),this._touchSupported="ontouchstart"in document.documentElement||navigator.maxTouchPoints>0,this._pointerEvent=Boolean(window.PointerEvent),this._addEventListeners()}static get Default(){return W}static get DATA_KEY(){return"bs.carousel"}next(){this._isSliding||this._slide($)}nextWhenVisible(){!document.hidden&&c(this._element)&&this.next()}prev(){this._isSliding||this._slide(F)}pause(t){t||(this._isPaused=!0),H.findOne(".carousel-item-next, .carousel-item-prev",this._element)&&(o(this._element),this.cycle(!0)),clearInterval(this._interval),this._interval=null}cycle(t){t||(this._isPaused=!1),this._interval&&(clearInterval(this._interval),this._interval=null),this._config&&this._config.interval&&!this._isPaused&&(this._updateInterval(),this._interval=setInterval((document.visibilityState?this.nextWhenVisible:this.next).bind(this),this._config.interval))}to(t){this._activeElement=H.findOne(".active.carousel-item",this._element);const e=this._getItemIndex(this._activeElement);if(t>this._items.length-1||t<0)return;if(this._isSliding)return void N.one(this._element,"slid.bs.carousel",()=>this.to(t));if(e===t)return this.pause(),void this.cycle();const i=t>e?$:F;this._slide(i,this._items[t])}dispose(){N.off(this._element,".bs.carousel"),this._items=null,this._config=null,this._interval=null,this._isPaused=null,this._isSliding=null,this._activeElement=null,this._indicatorsElement=null,super.dispose()}_getConfig(t){return t={...W,...t},l("carousel",t,U),t}_handleSwipe(){const t=Math.abs(this.touchDeltaX);if(t<=40)return;const e=t/this.touchDeltaX;this.touchDeltaX=0,e&&this._slide(e>0?K:z)}_addEventListeners(){this._config.keyboard&&N.on(this._element,"keydown.bs.carousel",t=>this._keydown(t)),"hover"===this._config.pause&&(N.on(this._element,"mouseenter.bs.carousel",t=>this.pause(t)),N.on(this._element,"mouseleave.bs.carousel",t=>this.cycle(t))),this._config.touch&&this._touchSupported&&this._addTouchEventListeners()}_addTouchEventListeners(){const t=t=>{!this._pointerEvent||"pen"!==t.pointerType&&"touch"!==t.pointerType?this._pointerEvent||(this.touchStartX=t.touches[0].clientX):this.touchStartX=t.clientX},e=t=>{this.touchDeltaX=t.touches&&t.touches.length>1?0:t.touches[0].clientX-this.touchStartX},i=t=>{!this._pointerEvent||"pen"!==t.pointerType&&"touch"!==t.pointerType||(this.touchDeltaX=t.clientX-this.touchStartX),this._handleSwipe(),"hover"===this._config.pause&&(this.pause(),this.touchTimeout&&clearTimeout(this.touchTimeout),this.touchTimeout=setTimeout(t=>this.cycle(t),500+this._config.interval))};H.find(".carousel-item img",this._element).forEach(t=>{N.on(t,"dragstart.bs.carousel",t=>t.preventDefault())}),this._pointerEvent?(N.on(this._element,"pointerdown.bs.carousel",e=>t(e)),N.on(this._element,"pointerup.bs.carousel",t=>i(t)),this._element.classList.add("pointer-event")):(N.on(this._element,"touchstart.bs.carousel",e=>t(e)),N.on(this._element,"touchmove.bs.carousel",t=>e(t)),N.on(this._element,"touchend.bs.carousel",t=>i(t)))}_keydown(t){/input|textarea/i.test(t.target.tagName)||("ArrowLeft"===t.key?(t.preventDefault(),this._slide(z)):"ArrowRight"===t.key&&(t.preventDefault(),this._slide(K)))}_getItemIndex(t){return this._items=t&&t.parentNode?H.find(".carousel-item",t.parentNode):[],this._items.indexOf(t)}_getItemByOrder(t,e){const i=t===$,s=t===F,n=this._getItemIndex(e),o=this._items.length-1;if((s&&0===n||i&&n===o)&&!this._config.wrap)return e;const r=(n+(s?-1:1))%this._items.length;return-1===r?this._items[this._items.length-1]:this._items[r]}_triggerSlideEvent(t,e){const i=this._getItemIndex(t),s=this._getItemIndex(H.findOne(".active.carousel-item",this._element));return N.trigger(this._element,"slide.bs.carousel",{relatedTarget:t,direction:e,from:s,to:i})}_setActiveIndicatorElement(t){if(this._indicatorsElement){const e=H.findOne(".active",this._indicatorsElement);e.classList.remove("active"),e.removeAttribute("aria-current");const i=H.find("[data-bs-target]",this._indicatorsElement);for(let e=0;e<i.length;e++)if(Number.parseInt(i[e].getAttribute("data-bs-slide-to"),10)===this._getItemIndex(t)){i[e].classList.add("active"),i[e].setAttribute("aria-current","true");break}}}_updateInterval(){const t=this._activeElement||H.findOne(".active.carousel-item",this._element);if(!t)return;const e=Number.parseInt(t.getAttribute("data-bs-interval"),10);e?(this._config.defaultInterval=this._config.defaultInterval||this._config.interval,this._config.interval=e):this._config.interval=this._config.defaultInterval||this._config.interval}_slide(t,e){const i=this._directionToOrder(t),s=H.findOne(".active.carousel-item",this._element),o=this._getItemIndex(s),r=e||this._getItemByOrder(i,s),l=this._getItemIndex(r),c=Boolean(this._interval),d=i===$,h=d?"carousel-item-start":"carousel-item-end",f=d?"carousel-item-next":"carousel-item-prev",p=this._orderToDirection(i);if(r&&r.classList.contains("active"))this._isSliding=!1;else if(!this._triggerSlideEvent(r,p).defaultPrevented&&s&&r){if(this._isSliding=!0,c&&this.pause(),this._setActiveIndicatorElement(r),this._activeElement=r,this._element.classList.contains("slide")){r.classList.add(f),u(r),s.classList.add(h),r.classList.add(h);const t=n(s);N.one(s,"transitionend",()=>{r.classList.remove(h,f),r.classList.add("active"),s.classList.remove("active",f,h),this._isSliding=!1,setTimeout(()=>{N.trigger(this._element,"slid.bs.carousel",{relatedTarget:r,direction:p,from:o,to:l})},0)}),a(s,t)}else s.classList.remove("active"),r.classList.add("active"),this._isSliding=!1,N.trigger(this._element,"slid.bs.carousel",{relatedTarget:r,direction:p,from:o,to:l});c&&this.cycle()}}_directionToOrder(t){return[K,z].includes(t)?g()?t===K?F:$:t===K?$:F:t}_orderToDirection(t){return[$,F].includes(t)?g()?t===$?z:K:t===$?K:z:t}static carouselInterface(t,e){let i=b.get(t,"bs.carousel"),s={...W,...B.getDataAttributes(t)};"object"==typeof e&&(s={...s,...e});const n="string"==typeof e?e:s.slide;if(i||(i=new Y(t,s)),"number"==typeof e)i.to(e);else if("string"==typeof n){if(void 0===i[n])throw new TypeError(`No method named "${n}"`);i[n]()}else s.interval&&s.ride&&(i.pause(),i.cycle())}static jQueryInterface(t){return this.each((function(){Y.carouselInterface(this,t)}))}static dataApiClickHandler(t){const e=s(this);if(!e||!e.classList.contains("carousel"))return;const i={...B.getDataAttributes(e),...B.getDataAttributes(this)},n=this.getAttribute("data-bs-slide-to");n&&(i.interval=!1),Y.carouselInterface(e,i),n&&b.get(e,"bs.carousel").to(n),t.preventDefault()}}N.on(document,"click.bs.carousel.data-api","[data-bs-slide], [data-bs-slide-to]",Y.dataApiClickHandler),N.on(window,"load.bs.carousel.data-api",()=>{const t=H.find('[data-bs-ride="carousel"]');for(let e=0,i=t.length;e<i;e++)Y.carouselInterface(t[e],b.get(t[e],"bs.carousel"))}),m("carousel",Y);const q={toggle:!0,parent:""},V={toggle:"boolean",parent:"(string|element)"};class X extends j{constructor(t,e){super(t),this._isTransitioning=!1,this._config=this._getConfig(e),this._triggerArray=H.find(`[data-bs-toggle="collapse"][href="#${this._element.id}"],[data-bs-toggle="collapse"][data-bs-target="#${this._element.id}"]`);const s=H.find('[data-bs-toggle="collapse"]');for(let t=0,e=s.length;t<e;t++){const e=s[t],n=i(e),o=H.find(n).filter(t=>t===this._element);null!==n&&o.length&&(this._selector=n,this._triggerArray.push(e))}this._parent=this._config.parent?this._getParent():null,this._config.parent||this._addAriaAndCollapsedClass(this._element,this._triggerArray),this._config.toggle&&this.toggle()}static get Default(){return q}static get DATA_KEY(){return"bs.collapse"}toggle(){this._element.classList.contains("show")?this.hide():this.show()}show(){if(this._isTransitioning||this._element.classList.contains("show"))return;let t,e;this._parent&&(t=H.find(".show, .collapsing",this._parent).filter(t=>"string"==typeof this._config.parent?t.getAttribute("data-bs-parent")===this._config.parent:t.classList.contains("collapse")),0===t.length&&(t=null));const i=H.findOne(this._selector);if(t){const s=t.find(t=>i!==t);if(e=s?b.get(s,"bs.collapse"):null,e&&e._isTransitioning)return}if(N.trigger(this._element,"show.bs.collapse").defaultPrevented)return;t&&t.forEach(t=>{i!==t&&X.collapseInterface(t,"hide"),e||b.set(t,"bs.collapse",null)});const s=this._getDimension();this._element.classList.remove("collapse"),this._element.classList.add("collapsing"),this._element.style[s]=0,this._triggerArray.length&&this._triggerArray.forEach(t=>{t.classList.remove("collapsed"),t.setAttribute("aria-expanded",!0)}),this.setTransitioning(!0);const o="scroll"+(s[0].toUpperCase()+s.slice(1)),r=n(this._element);N.one(this._element,"transitionend",()=>{this._element.classList.remove("collapsing"),this._element.classList.add("collapse","show"),this._element.style[s]="",this.setTransitioning(!1),N.trigger(this._element,"shown.bs.collapse")}),a(this._element,r),this._element.style[s]=this._element[o]+"px"}hide(){if(this._isTransitioning||!this._element.classList.contains("show"))return;if(N.trigger(this._element,"hide.bs.collapse").defaultPrevented)return;const t=this._getDimension();this._element.style[t]=this._element.getBoundingClientRect()[t]+"px",u(this._element),this._element.classList.add("collapsing"),this._element.classList.remove("collapse","show");const e=this._triggerArray.length;if(e>0)for(let t=0;t<e;t++){const e=this._triggerArray[t],i=s(e);i&&!i.classList.contains("show")&&(e.classList.add("collapsed"),e.setAttribute("aria-expanded",!1))}this.setTransitioning(!0),this._element.style[t]="";const i=n(this._element);N.one(this._element,"transitionend",()=>{this.setTransitioning(!1),this._element.classList.remove("collapsing"),this._element.classList.add("collapse"),N.trigger(this._element,"hidden.bs.collapse")}),a(this._element,i)}setTransitioning(t){this._isTransitioning=t}dispose(){super.dispose(),this._config=null,this._parent=null,this._triggerArray=null,this._isTransitioning=null}_getConfig(t){return(t={...q,...t}).toggle=Boolean(t.toggle),l("collapse",t,V),t}_getDimension(){return this._element.classList.contains("width")?"width":"height"}_getParent(){let{parent:t}=this._config;r(t)?void 0===t.jquery&&void 0===t[0]||(t=t[0]):t=H.findOne(t);const e=`[data-bs-toggle="collapse"][data-bs-parent="${t}"]`;return H.find(e,t).forEach(t=>{const e=s(t);this._addAriaAndCollapsedClass(e,[t])}),t}_addAriaAndCollapsedClass(t,e){if(!t||!e.length)return;const i=t.classList.contains("show");e.forEach(t=>{i?t.classList.remove("collapsed"):t.classList.add("collapsed"),t.setAttribute("aria-expanded",i)})}static collapseInterface(t,e){let i=b.get(t,"bs.collapse");const s={...q,...B.getDataAttributes(t),..."object"==typeof e&&e?e:{}};if(!i&&s.toggle&&"string"==typeof e&&/show|hide/.test(e)&&(s.toggle=!1),i||(i=new X(t,s)),"string"==typeof e){if(void 0===i[e])throw new TypeError(`No method named "${e}"`);i[e]()}}static jQueryInterface(t){return this.each((function(){X.collapseInterface(this,t)}))}}N.on(document,"click.bs.collapse.data-api",'[data-bs-toggle="collapse"]',(function(t){("A"===t.target.tagName||t.delegateTarget&&"A"===t.delegateTarget.tagName)&&t.preventDefault();const e=B.getDataAttributes(this),s=i(this);H.find(s).forEach(t=>{const i=b.get(t,"bs.collapse");let s;i?(null===i._parent&&"string"==typeof e.parent&&(i._config.parent=e.parent,i._parent=i._getParent()),s="toggle"):s=e,X.collapseInterface(t,s)})})),m("collapse",X);var Q="top",G="bottom",Z="right",J="left",tt=[Q,G,Z,J],et=tt.reduce((function(t,e){return t.concat([e+"-start",e+"-end"])}),[]),it=[].concat(tt,["auto"]).reduce((function(t,e){return t.concat([e,e+"-start",e+"-end"])}),[]),st=["beforeRead","read","afterRead","beforeMain","main","afterMain","beforeWrite","write","afterWrite"];function nt(t){return t?(t.nodeName||"").toLowerCase():null}function ot(t){if(null==t)return window;if("[object Window]"!==t.toString()){var e=t.ownerDocument;return e&&e.defaultView||window}return t}function rt(t){return t instanceof ot(t).Element||t instanceof Element}function at(t){return t instanceof ot(t).HTMLElement||t instanceof HTMLElement}function lt(t){return"undefined"!=typeof ShadowRoot&&(t instanceof ot(t).ShadowRoot||t instanceof ShadowRoot)}var ct={name:"applyStyles",enabled:!0,phase:"write",fn:function(t){var e=t.state;Object.keys(e.elements).forEach((function(t){var i=e.styles[t]||{},s=e.attributes[t]||{},n=e.elements[t];at(n)&&nt(n)&&(Object.assign(n.style,i),Object.keys(s).forEach((function(t){var e=s[t];!1===e?n.removeAttribute(t):n.setAttribute(t,!0===e?"":e)})))}))},effect:function(t){var e=t.state,i={popper:{position:e.options.strategy,left:"0",top:"0",margin:"0"},arrow:{position:"absolute"},reference:{}};return Object.assign(e.elements.popper.style,i.popper),e.styles=i,e.elements.arrow&&Object.assign(e.elements.arrow.style,i.arrow),function(){Object.keys(e.elements).forEach((function(t){var s=e.elements[t],n=e.attributes[t]||{},o=Object.keys(e.styles.hasOwnProperty(t)?e.styles[t]:i[t]).reduce((function(t,e){return t[e]="",t}),{});at(s)&&nt(s)&&(Object.assign(s.style,o),Object.keys(n).forEach((function(t){s.removeAttribute(t)})))}))}},requires:["computeStyles"]};function dt(t){return t.split("-")[0]}function ht(t){var e=t.getBoundingClientRect();return{width:e.width,height:e.height,top:e.top,right:e.right,bottom:e.bottom,left:e.left,x:e.left,y:e.top}}function ft(t){var e=ht(t),i=t.offsetWidth,s=t.offsetHeight;return Math.abs(e.width-i)<=1&&(i=e.width),Math.abs(e.height-s)<=1&&(s=e.height),{x:t.offsetLeft,y:t.offsetTop,width:i,height:s}}function ut(t,e){var i=e.getRootNode&&e.getRootNode();if(t.contains(e))return!0;if(i&<(i)){var s=e;do{if(s&&t.isSameNode(s))return!0;s=s.parentNode||s.host}while(s)}return!1}function pt(t){return ot(t).getComputedStyle(t)}function gt(t){return["table","td","th"].indexOf(nt(t))>=0}function mt(t){return((rt(t)?t.ownerDocument:t.document)||window.document).documentElement}function _t(t){return"html"===nt(t)?t:t.assignedSlot||t.parentNode||(lt(t)?t.host:null)||mt(t)}function bt(t){return at(t)&&"fixed"!==pt(t).position?t.offsetParent:null}function vt(t){for(var e=ot(t),i=bt(t);i&>(i)&&"static"===pt(i).position;)i=bt(i);return i&&("html"===nt(i)||"body"===nt(i)&&"static"===pt(i).position)?e:i||function(t){for(var e=-1!==navigator.userAgent.toLowerCase().indexOf("firefox"),i=_t(t);at(i)&&["html","body"].indexOf(nt(i))<0;){var s=pt(i);if("none"!==s.transform||"none"!==s.perspective||"paint"===s.contain||-1!==["transform","perspective"].indexOf(s.willChange)||e&&"filter"===s.willChange||e&&s.filter&&"none"!==s.filter)return i;i=i.parentNode}return null}(t)||e}function yt(t){return["top","bottom"].indexOf(t)>=0?"x":"y"}var wt=Math.max,Et=Math.min,Tt=Math.round;function At(t,e,i){return wt(t,Et(e,i))}function Lt(t){return Object.assign({},{top:0,right:0,bottom:0,left:0},t)}function Ot(t,e){return e.reduce((function(e,i){return e[i]=t,e}),{})}var kt={name:"arrow",enabled:!0,phase:"main",fn:function(t){var e,i=t.state,s=t.name,n=t.options,o=i.elements.arrow,r=i.modifiersData.popperOffsets,a=dt(i.placement),l=yt(a),c=[J,Z].indexOf(a)>=0?"height":"width";if(o&&r){var d=function(t,e){return Lt("number"!=typeof(t="function"==typeof t?t(Object.assign({},e.rects,{placement:e.placement})):t)?t:Ot(t,tt))}(n.padding,i),h=ft(o),f="y"===l?Q:J,u="y"===l?G:Z,p=i.rects.reference[c]+i.rects.reference[l]-r[l]-i.rects.popper[c],g=r[l]-i.rects.reference[l],m=vt(o),_=m?"y"===l?m.clientHeight||0:m.clientWidth||0:0,b=p/2-g/2,v=d[f],y=_-h[c]-d[u],w=_/2-h[c]/2+b,E=At(v,w,y),T=l;i.modifiersData[s]=((e={})[T]=E,e.centerOffset=E-w,e)}},effect:function(t){var e=t.state,i=t.options.element,s=void 0===i?"[data-popper-arrow]":i;null!=s&&("string"!=typeof s||(s=e.elements.popper.querySelector(s)))&&ut(e.elements.popper,s)&&(e.elements.arrow=s)},requires:["popperOffsets"],requiresIfExists:["preventOverflow"]},Dt={top:"auto",right:"auto",bottom:"auto",left:"auto"};function xt(t){var e,i=t.popper,s=t.popperRect,n=t.placement,o=t.offsets,r=t.position,a=t.gpuAcceleration,l=t.adaptive,c=t.roundOffsets,d=!0===c?function(t){var e=t.x,i=t.y,s=window.devicePixelRatio||1;return{x:Tt(Tt(e*s)/s)||0,y:Tt(Tt(i*s)/s)||0}}(o):"function"==typeof c?c(o):o,h=d.x,f=void 0===h?0:h,u=d.y,p=void 0===u?0:u,g=o.hasOwnProperty("x"),m=o.hasOwnProperty("y"),_=J,b=Q,v=window;if(l){var y=vt(i),w="clientHeight",E="clientWidth";y===ot(i)&&"static"!==pt(y=mt(i)).position&&(w="scrollHeight",E="scrollWidth"),y=y,n===Q&&(b=G,p-=y[w]-s.height,p*=a?1:-1),n===J&&(_=Z,f-=y[E]-s.width,f*=a?1:-1)}var T,A=Object.assign({position:r},l&&Dt);return a?Object.assign({},A,((T={})[b]=m?"0":"",T[_]=g?"0":"",T.transform=(v.devicePixelRatio||1)<2?"translate("+f+"px, "+p+"px)":"translate3d("+f+"px, "+p+"px, 0)",T)):Object.assign({},A,((e={})[b]=m?p+"px":"",e[_]=g?f+"px":"",e.transform="",e))}var Ct={name:"computeStyles",enabled:!0,phase:"beforeWrite",fn:function(t){var e=t.state,i=t.options,s=i.gpuAcceleration,n=void 0===s||s,o=i.adaptive,r=void 0===o||o,a=i.roundOffsets,l=void 0===a||a,c={placement:dt(e.placement),popper:e.elements.popper,popperRect:e.rects.popper,gpuAcceleration:n};null!=e.modifiersData.popperOffsets&&(e.styles.popper=Object.assign({},e.styles.popper,xt(Object.assign({},c,{offsets:e.modifiersData.popperOffsets,position:e.options.strategy,adaptive:r,roundOffsets:l})))),null!=e.modifiersData.arrow&&(e.styles.arrow=Object.assign({},e.styles.arrow,xt(Object.assign({},c,{offsets:e.modifiersData.arrow,position:"absolute",adaptive:!1,roundOffsets:l})))),e.attributes.popper=Object.assign({},e.attributes.popper,{"data-popper-placement":e.placement})},data:{}},St={passive:!0},Nt={name:"eventListeners",enabled:!0,phase:"write",fn:function(){},effect:function(t){var e=t.state,i=t.instance,s=t.options,n=s.scroll,o=void 0===n||n,r=s.resize,a=void 0===r||r,l=ot(e.elements.popper),c=[].concat(e.scrollParents.reference,e.scrollParents.popper);return o&&c.forEach((function(t){t.addEventListener("scroll",i.update,St)})),a&&l.addEventListener("resize",i.update,St),function(){o&&c.forEach((function(t){t.removeEventListener("scroll",i.update,St)})),a&&l.removeEventListener("resize",i.update,St)}},data:{}},jt={left:"right",right:"left",bottom:"top",top:"bottom"};function Pt(t){return t.replace(/left|right|bottom|top/g,(function(t){return jt[t]}))}var It={start:"end",end:"start"};function Mt(t){return t.replace(/start|end/g,(function(t){return It[t]}))}function Rt(t){var e=ot(t);return{scrollLeft:e.pageXOffset,scrollTop:e.pageYOffset}}function Bt(t){return ht(mt(t)).left+Rt(t).scrollLeft}function Ht(t){var e=pt(t),i=e.overflow,s=e.overflowX,n=e.overflowY;return/auto|scroll|overlay|hidden/.test(i+n+s)}function Wt(t,e){var i;void 0===e&&(e=[]);var s=function t(e){return["html","body","#document"].indexOf(nt(e))>=0?e.ownerDocument.body:at(e)&&Ht(e)?e:t(_t(e))}(t),n=s===(null==(i=t.ownerDocument)?void 0:i.body),o=ot(s),r=n?[o].concat(o.visualViewport||[],Ht(s)?s:[]):s,a=e.concat(r);return n?a:a.concat(Wt(_t(r)))}function Ut(t){return Object.assign({},t,{left:t.x,top:t.y,right:t.x+t.width,bottom:t.y+t.height})}function $t(t,e){return"viewport"===e?Ut(function(t){var e=ot(t),i=mt(t),s=e.visualViewport,n=i.clientWidth,o=i.clientHeight,r=0,a=0;return s&&(n=s.width,o=s.height,/^((?!chrome|android).)*safari/i.test(navigator.userAgent)||(r=s.offsetLeft,a=s.offsetTop)),{width:n,height:o,x:r+Bt(t),y:a}}(t)):at(e)?function(t){var e=ht(t);return e.top=e.top+t.clientTop,e.left=e.left+t.clientLeft,e.bottom=e.top+t.clientHeight,e.right=e.left+t.clientWidth,e.width=t.clientWidth,e.height=t.clientHeight,e.x=e.left,e.y=e.top,e}(e):Ut(function(t){var e,i=mt(t),s=Rt(t),n=null==(e=t.ownerDocument)?void 0:e.body,o=wt(i.scrollWidth,i.clientWidth,n?n.scrollWidth:0,n?n.clientWidth:0),r=wt(i.scrollHeight,i.clientHeight,n?n.scrollHeight:0,n?n.clientHeight:0),a=-s.scrollLeft+Bt(t),l=-s.scrollTop;return"rtl"===pt(n||i).direction&&(a+=wt(i.clientWidth,n?n.clientWidth:0)-o),{width:o,height:r,x:a,y:l}}(mt(t)))}function Ft(t){return t.split("-")[1]}function zt(t){var e,i=t.reference,s=t.element,n=t.placement,o=n?dt(n):null,r=n?Ft(n):null,a=i.x+i.width/2-s.width/2,l=i.y+i.height/2-s.height/2;switch(o){case Q:e={x:a,y:i.y-s.height};break;case G:e={x:a,y:i.y+i.height};break;case Z:e={x:i.x+i.width,y:l};break;case J:e={x:i.x-s.width,y:l};break;default:e={x:i.x,y:i.y}}var c=o?yt(o):null;if(null!=c){var d="y"===c?"height":"width";switch(r){case"start":e[c]=e[c]-(i[d]/2-s[d]/2);break;case"end":e[c]=e[c]+(i[d]/2-s[d]/2)}}return e}function Kt(t,e){void 0===e&&(e={});var i=e,s=i.placement,n=void 0===s?t.placement:s,o=i.boundary,r=void 0===o?"clippingParents":o,a=i.rootBoundary,l=void 0===a?"viewport":a,c=i.elementContext,d=void 0===c?"popper":c,h=i.altBoundary,f=void 0!==h&&h,u=i.padding,p=void 0===u?0:u,g=Lt("number"!=typeof p?p:Ot(p,tt)),m="popper"===d?"reference":"popper",_=t.elements.reference,b=t.rects.popper,v=t.elements[f?m:d],y=function(t,e,i){var s="clippingParents"===e?function(t){var e=Wt(_t(t)),i=["absolute","fixed"].indexOf(pt(t).position)>=0&&at(t)?vt(t):t;return rt(i)?e.filter((function(t){return rt(t)&&ut(t,i)&&"body"!==nt(t)})):[]}(t):[].concat(e),n=[].concat(s,[i]),o=n[0],r=n.reduce((function(e,i){var s=$t(t,i);return e.top=wt(s.top,e.top),e.right=Et(s.right,e.right),e.bottom=Et(s.bottom,e.bottom),e.left=wt(s.left,e.left),e}),$t(t,o));return r.width=r.right-r.left,r.height=r.bottom-r.top,r.x=r.left,r.y=r.top,r}(rt(v)?v:v.contextElement||mt(t.elements.popper),r,l),w=ht(_),E=zt({reference:w,element:b,strategy:"absolute",placement:n}),T=Ut(Object.assign({},b,E)),A="popper"===d?T:w,L={top:y.top-A.top+g.top,bottom:A.bottom-y.bottom+g.bottom,left:y.left-A.left+g.left,right:A.right-y.right+g.right},O=t.modifiersData.offset;if("popper"===d&&O){var k=O[n];Object.keys(L).forEach((function(t){var e=[Z,G].indexOf(t)>=0?1:-1,i=[Q,G].indexOf(t)>=0?"y":"x";L[t]+=k[i]*e}))}return L}function Yt(t,e){void 0===e&&(e={});var i=e,s=i.placement,n=i.boundary,o=i.rootBoundary,r=i.padding,a=i.flipVariations,l=i.allowedAutoPlacements,c=void 0===l?it:l,d=Ft(s),h=d?a?et:et.filter((function(t){return Ft(t)===d})):tt,f=h.filter((function(t){return c.indexOf(t)>=0}));0===f.length&&(f=h);var u=f.reduce((function(e,i){return e[i]=Kt(t,{placement:i,boundary:n,rootBoundary:o,padding:r})[dt(i)],e}),{});return Object.keys(u).sort((function(t,e){return u[t]-u[e]}))}var qt={name:"flip",enabled:!0,phase:"main",fn:function(t){var e=t.state,i=t.options,s=t.name;if(!e.modifiersData[s]._skip){for(var n=i.mainAxis,o=void 0===n||n,r=i.altAxis,a=void 0===r||r,l=i.fallbackPlacements,c=i.padding,d=i.boundary,h=i.rootBoundary,f=i.altBoundary,u=i.flipVariations,p=void 0===u||u,g=i.allowedAutoPlacements,m=e.options.placement,_=dt(m),b=l||(_!==m&&p?function(t){if("auto"===dt(t))return[];var e=Pt(t);return[Mt(t),e,Mt(e)]}(m):[Pt(m)]),v=[m].concat(b).reduce((function(t,i){return t.concat("auto"===dt(i)?Yt(e,{placement:i,boundary:d,rootBoundary:h,padding:c,flipVariations:p,allowedAutoPlacements:g}):i)}),[]),y=e.rects.reference,w=e.rects.popper,E=new Map,T=!0,A=v[0],L=0;L<v.length;L++){var O=v[L],k=dt(O),D="start"===Ft(O),x=[Q,G].indexOf(k)>=0,C=x?"width":"height",S=Kt(e,{placement:O,boundary:d,rootBoundary:h,altBoundary:f,padding:c}),N=x?D?Z:J:D?G:Q;y[C]>w[C]&&(N=Pt(N));var j=Pt(N),P=[];if(o&&P.push(S[k]<=0),a&&P.push(S[N]<=0,S[j]<=0),P.every((function(t){return t}))){A=O,T=!1;break}E.set(O,P)}if(T)for(var I=function(t){var e=v.find((function(e){var i=E.get(e);if(i)return i.slice(0,t).every((function(t){return t}))}));if(e)return A=e,"break"},M=p?3:1;M>0&&"break"!==I(M);M--);e.placement!==A&&(e.modifiersData[s]._skip=!0,e.placement=A,e.reset=!0)}},requiresIfExists:["offset"],data:{_skip:!1}};function Vt(t,e,i){return void 0===i&&(i={x:0,y:0}),{top:t.top-e.height-i.y,right:t.right-e.width+i.x,bottom:t.bottom-e.height+i.y,left:t.left-e.width-i.x}}function Xt(t){return[Q,Z,G,J].some((function(e){return t[e]>=0}))}var Qt={name:"hide",enabled:!0,phase:"main",requiresIfExists:["preventOverflow"],fn:function(t){var e=t.state,i=t.name,s=e.rects.reference,n=e.rects.popper,o=e.modifiersData.preventOverflow,r=Kt(e,{elementContext:"reference"}),a=Kt(e,{altBoundary:!0}),l=Vt(r,s),c=Vt(a,n,o),d=Xt(l),h=Xt(c);e.modifiersData[i]={referenceClippingOffsets:l,popperEscapeOffsets:c,isReferenceHidden:d,hasPopperEscaped:h},e.attributes.popper=Object.assign({},e.attributes.popper,{"data-popper-reference-hidden":d,"data-popper-escaped":h})}},Gt={name:"offset",enabled:!0,phase:"main",requires:["popperOffsets"],fn:function(t){var e=t.state,i=t.options,s=t.name,n=i.offset,o=void 0===n?[0,0]:n,r=it.reduce((function(t,i){return t[i]=function(t,e,i){var s=dt(t),n=[J,Q].indexOf(s)>=0?-1:1,o="function"==typeof i?i(Object.assign({},e,{placement:t})):i,r=o[0],a=o[1];return r=r||0,a=(a||0)*n,[J,Z].indexOf(s)>=0?{x:a,y:r}:{x:r,y:a}}(i,e.rects,o),t}),{}),a=r[e.placement],l=a.x,c=a.y;null!=e.modifiersData.popperOffsets&&(e.modifiersData.popperOffsets.x+=l,e.modifiersData.popperOffsets.y+=c),e.modifiersData[s]=r}},Zt={name:"popperOffsets",enabled:!0,phase:"read",fn:function(t){var e=t.state,i=t.name;e.modifiersData[i]=zt({reference:e.rects.reference,element:e.rects.popper,strategy:"absolute",placement:e.placement})},data:{}},Jt={name:"preventOverflow",enabled:!0,phase:"main",fn:function(t){var e=t.state,i=t.options,s=t.name,n=i.mainAxis,o=void 0===n||n,r=i.altAxis,a=void 0!==r&&r,l=i.boundary,c=i.rootBoundary,d=i.altBoundary,h=i.padding,f=i.tether,u=void 0===f||f,p=i.tetherOffset,g=void 0===p?0:p,m=Kt(e,{boundary:l,rootBoundary:c,padding:h,altBoundary:d}),_=dt(e.placement),b=Ft(e.placement),v=!b,y=yt(_),w="x"===y?"y":"x",E=e.modifiersData.popperOffsets,T=e.rects.reference,A=e.rects.popper,L="function"==typeof g?g(Object.assign({},e.rects,{placement:e.placement})):g,O={x:0,y:0};if(E){if(o||a){var k="y"===y?Q:J,D="y"===y?G:Z,x="y"===y?"height":"width",C=E[y],S=E[y]+m[k],N=E[y]-m[D],j=u?-A[x]/2:0,P="start"===b?T[x]:A[x],I="start"===b?-A[x]:-T[x],M=e.elements.arrow,R=u&&M?ft(M):{width:0,height:0},B=e.modifiersData["arrow#persistent"]?e.modifiersData["arrow#persistent"].padding:{top:0,right:0,bottom:0,left:0},H=B[k],W=B[D],U=At(0,T[x],R[x]),$=v?T[x]/2-j-U-H-L:P-U-H-L,F=v?-T[x]/2+j+U+W+L:I+U+W+L,z=e.elements.arrow&&vt(e.elements.arrow),K=z?"y"===y?z.clientTop||0:z.clientLeft||0:0,Y=e.modifiersData.offset?e.modifiersData.offset[e.placement][y]:0,q=E[y]+$-Y-K,V=E[y]+F-Y;if(o){var X=At(u?Et(S,q):S,C,u?wt(N,V):N);E[y]=X,O[y]=X-C}if(a){var tt="x"===y?Q:J,et="x"===y?G:Z,it=E[w],st=it+m[tt],nt=it-m[et],ot=At(u?Et(st,q):st,it,u?wt(nt,V):nt);E[w]=ot,O[w]=ot-it}}e.modifiersData[s]=O}},requiresIfExists:["offset"]};function te(t,e,i){void 0===i&&(i=!1);var s,n,o=mt(e),r=ht(t),a=at(e),l={scrollLeft:0,scrollTop:0},c={x:0,y:0};return(a||!a&&!i)&&(("body"!==nt(e)||Ht(o))&&(l=(s=e)!==ot(s)&&at(s)?{scrollLeft:(n=s).scrollLeft,scrollTop:n.scrollTop}:Rt(s)),at(e)?((c=ht(e)).x+=e.clientLeft,c.y+=e.clientTop):o&&(c.x=Bt(o))),{x:r.left+l.scrollLeft-c.x,y:r.top+l.scrollTop-c.y,width:r.width,height:r.height}}var ee={placement:"bottom",modifiers:[],strategy:"absolute"};function ie(){for(var t=arguments.length,e=new Array(t),i=0;i<t;i++)e[i]=arguments[i];return!e.some((function(t){return!(t&&"function"==typeof t.getBoundingClientRect)}))}function se(t){void 0===t&&(t={});var e=t,i=e.defaultModifiers,s=void 0===i?[]:i,n=e.defaultOptions,o=void 0===n?ee:n;return function(t,e,i){void 0===i&&(i=o);var n,r,a={placement:"bottom",orderedModifiers:[],options:Object.assign({},ee,o),modifiersData:{},elements:{reference:t,popper:e},attributes:{},styles:{}},l=[],c=!1,d={state:a,setOptions:function(i){h(),a.options=Object.assign({},o,a.options,i),a.scrollParents={reference:rt(t)?Wt(t):t.contextElement?Wt(t.contextElement):[],popper:Wt(e)};var n,r,c=function(t){var e=function(t){var e=new Map,i=new Set,s=[];return t.forEach((function(t){e.set(t.name,t)})),t.forEach((function(t){i.has(t.name)||function t(n){i.add(n.name),[].concat(n.requires||[],n.requiresIfExists||[]).forEach((function(s){if(!i.has(s)){var n=e.get(s);n&&t(n)}})),s.push(n)}(t)})),s}(t);return st.reduce((function(t,i){return t.concat(e.filter((function(t){return t.phase===i})))}),[])}((n=[].concat(s,a.options.modifiers),r=n.reduce((function(t,e){var i=t[e.name];return t[e.name]=i?Object.assign({},i,e,{options:Object.assign({},i.options,e.options),data:Object.assign({},i.data,e.data)}):e,t}),{}),Object.keys(r).map((function(t){return r[t]}))));return a.orderedModifiers=c.filter((function(t){return t.enabled})),a.orderedModifiers.forEach((function(t){var e=t.name,i=t.options,s=void 0===i?{}:i,n=t.effect;if("function"==typeof n){var o=n({state:a,name:e,instance:d,options:s});l.push(o||function(){})}})),d.update()},forceUpdate:function(){if(!c){var t=a.elements,e=t.reference,i=t.popper;if(ie(e,i)){a.rects={reference:te(e,vt(i),"fixed"===a.options.strategy),popper:ft(i)},a.reset=!1,a.placement=a.options.placement,a.orderedModifiers.forEach((function(t){return a.modifiersData[t.name]=Object.assign({},t.data)}));for(var s=0;s<a.orderedModifiers.length;s++)if(!0!==a.reset){var n=a.orderedModifiers[s],o=n.fn,r=n.options,l=void 0===r?{}:r,h=n.name;"function"==typeof o&&(a=o({state:a,options:l,name:h,instance:d})||a)}else a.reset=!1,s=-1}}},update:(n=function(){return new Promise((function(t){d.forceUpdate(),t(a)}))},function(){return r||(r=new Promise((function(t){Promise.resolve().then((function(){r=void 0,t(n())}))}))),r}),destroy:function(){h(),c=!0}};if(!ie(t,e))return d;function h(){l.forEach((function(t){return t()})),l=[]}return d.setOptions(i).then((function(t){!c&&i.onFirstUpdate&&i.onFirstUpdate(t)})),d}}var ne=se(),oe=se({defaultModifiers:[Nt,Zt,Ct,ct]}),re=se({defaultModifiers:[Nt,Zt,Ct,ct,Gt,qt,Jt,kt,Qt]}),ae=Object.freeze({__proto__:null,popperGenerator:se,detectOverflow:Kt,createPopperBase:ne,createPopper:re,createPopperLite:oe,top:Q,bottom:G,right:Z,left:J,auto:"auto",basePlacements:tt,start:"start",end:"end",clippingParents:"clippingParents",viewport:"viewport",popper:"popper",reference:"reference",variationPlacements:et,placements:it,beforeRead:"beforeRead",read:"read",afterRead:"afterRead",beforeMain:"beforeMain",main:"main",afterMain:"afterMain",beforeWrite:"beforeWrite",write:"write",afterWrite:"afterWrite",modifierPhases:st,applyStyles:ct,arrow:kt,computeStyles:Ct,eventListeners:Nt,flip:qt,hide:Qt,offset:Gt,popperOffsets:Zt,preventOverflow:Jt});const le=new RegExp("ArrowUp|ArrowDown|Escape"),ce=g()?"top-end":"top-start",de=g()?"top-start":"top-end",he=g()?"bottom-end":"bottom-start",fe=g()?"bottom-start":"bottom-end",ue=g()?"left-start":"right-start",pe=g()?"right-start":"left-start",ge={offset:[0,2],boundary:"clippingParents",reference:"toggle",display:"dynamic",popperConfig:null},me={offset:"(array|string|function)",boundary:"(string|element)",reference:"(string|element|object)",display:"string",popperConfig:"(null|object|function)"};class _e extends j{constructor(t,e){super(t),this._popper=null,this._config=this._getConfig(e),this._menu=this._getMenuElement(),this._inNavbar=this._detectNavbar(),this._addEventListeners()}static get Default(){return ge}static get DefaultType(){return me}static get DATA_KEY(){return"bs.dropdown"}toggle(){if(this._element.disabled||this._element.classList.contains("disabled"))return;const t=this._element.classList.contains("show");_e.clearMenus(),t||this.show()}show(){if(this._element.disabled||this._element.classList.contains("disabled")||this._menu.classList.contains("show"))return;const t=_e.getParentFromElement(this._element),e={relatedTarget:this._element};if(!N.trigger(this._element,"show.bs.dropdown",e).defaultPrevented){if(this._inNavbar)B.setDataAttribute(this._menu,"popper","none");else{if(void 0===ae)throw new TypeError("Bootstrap's dropdowns require Popper (https://popper.js.org)");let e=this._element;"parent"===this._config.reference?e=t:r(this._config.reference)?(e=this._config.reference,void 0!==this._config.reference.jquery&&(e=this._config.reference[0])):"object"==typeof this._config.reference&&(e=this._config.reference);const i=this._getPopperConfig(),s=i.modifiers.find(t=>"applyStyles"===t.name&&!1===t.enabled);this._popper=re(e,this._menu,i),s&&B.setDataAttribute(this._menu,"popper","static")}"ontouchstart"in document.documentElement&&!t.closest(".navbar-nav")&&[].concat(...document.body.children).forEach(t=>N.on(t,"mouseover",null,(function(){}))),this._element.focus(),this._element.setAttribute("aria-expanded",!0),this._menu.classList.toggle("show"),this._element.classList.toggle("show"),N.trigger(this._element,"shown.bs.dropdown",e)}}hide(){if(this._element.disabled||this._element.classList.contains("disabled")||!this._menu.classList.contains("show"))return;const t={relatedTarget:this._element};N.trigger(this._element,"hide.bs.dropdown",t).defaultPrevented||(this._popper&&this._popper.destroy(),this._menu.classList.toggle("show"),this._element.classList.toggle("show"),B.removeDataAttribute(this._menu,"popper"),N.trigger(this._element,"hidden.bs.dropdown",t))}dispose(){N.off(this._element,".bs.dropdown"),this._menu=null,this._popper&&(this._popper.destroy(),this._popper=null),super.dispose()}update(){this._inNavbar=this._detectNavbar(),this._popper&&this._popper.update()}_addEventListeners(){N.on(this._element,"click.bs.dropdown",t=>{t.preventDefault(),this.toggle()})}_getConfig(t){if(t={...this.constructor.Default,...B.getDataAttributes(this._element),...t},l("dropdown",t,this.constructor.DefaultType),"object"==typeof t.reference&&!r(t.reference)&&"function"!=typeof t.reference.getBoundingClientRect)throw new TypeError("dropdown".toUpperCase()+': Option "reference" provided type "object" without a required "getBoundingClientRect" method.');return t}_getMenuElement(){return H.next(this._element,".dropdown-menu")[0]}_getPlacement(){const t=this._element.parentNode;if(t.classList.contains("dropend"))return ue;if(t.classList.contains("dropstart"))return pe;const e="end"===getComputedStyle(this._menu).getPropertyValue("--bs-position").trim();return t.classList.contains("dropup")?e?de:ce:e?fe:he}_detectNavbar(){return null!==this._element.closest(".navbar")}_getOffset(){const{offset:t}=this._config;return"string"==typeof t?t.split(",").map(t=>Number.parseInt(t,10)):"function"==typeof t?e=>t(e,this._element):t}_getPopperConfig(){const t={placement:this._getPlacement(),modifiers:[{name:"preventOverflow",options:{boundary:this._config.boundary}},{name:"offset",options:{offset:this._getOffset()}}]};return"static"===this._config.display&&(t.modifiers=[{name:"applyStyles",enabled:!1}]),{...t,..."function"==typeof this._config.popperConfig?this._config.popperConfig(t):this._config.popperConfig}}static dropdownInterface(t,e){let i=b.get(t,"bs.dropdown");if(i||(i=new _e(t,"object"==typeof e?e:null)),"string"==typeof e){if(void 0===i[e])throw new TypeError(`No method named "${e}"`);i[e]()}}static jQueryInterface(t){return this.each((function(){_e.dropdownInterface(this,t)}))}static clearMenus(t){if(t){if(2===t.button||"keyup"===t.type&&"Tab"!==t.key)return;if(/input|select|textarea|form/i.test(t.target.tagName))return}const e=H.find('[data-bs-toggle="dropdown"]');for(let i=0,s=e.length;i<s;i++){const s=b.get(e[i],"bs.dropdown"),n={relatedTarget:e[i]};if(t&&"click"===t.type&&(n.clickEvent=t),!s)continue;const o=s._menu;if(e[i].classList.contains("show")){if(t){if([s._element].some(e=>t.composedPath().includes(e)))continue;if("keyup"===t.type&&"Tab"===t.key&&o.contains(t.target))continue}N.trigger(e[i],"hide.bs.dropdown",n).defaultPrevented||("ontouchstart"in document.documentElement&&[].concat(...document.body.children).forEach(t=>N.off(t,"mouseover",null,(function(){}))),e[i].setAttribute("aria-expanded","false"),s._popper&&s._popper.destroy(),o.classList.remove("show"),e[i].classList.remove("show"),B.removeDataAttribute(o,"popper"),N.trigger(e[i],"hidden.bs.dropdown",n))}}}static getParentFromElement(t){return s(t)||t.parentNode}static dataApiKeydownHandler(t){if(/input|textarea/i.test(t.target.tagName)?"Space"===t.key||"Escape"!==t.key&&("ArrowDown"!==t.key&&"ArrowUp"!==t.key||t.target.closest(".dropdown-menu")):!le.test(t.key))return;if(t.preventDefault(),t.stopPropagation(),this.disabled||this.classList.contains("disabled"))return;const e=_e.getParentFromElement(this),i=this.classList.contains("show");if("Escape"===t.key)return(this.matches('[data-bs-toggle="dropdown"]')?this:H.prev(this,'[data-bs-toggle="dropdown"]')[0]).focus(),void _e.clearMenus();if(!i&&("ArrowUp"===t.key||"ArrowDown"===t.key))return void(this.matches('[data-bs-toggle="dropdown"]')?this:H.prev(this,'[data-bs-toggle="dropdown"]')[0]).click();if(!i||"Space"===t.key)return void _e.clearMenus();const s=H.find(".dropdown-menu .dropdown-item:not(.disabled):not(:disabled)",e).filter(c);if(!s.length)return;let n=s.indexOf(t.target);"ArrowUp"===t.key&&n>0&&n--,"ArrowDown"===t.key&&n<s.length-1&&n++,n=-1===n?0:n,s[n].focus()}}N.on(document,"keydown.bs.dropdown.data-api",'[data-bs-toggle="dropdown"]',_e.dataApiKeydownHandler),N.on(document,"keydown.bs.dropdown.data-api",".dropdown-menu",_e.dataApiKeydownHandler),N.on(document,"click.bs.dropdown.data-api",_e.clearMenus),N.on(document,"keyup.bs.dropdown.data-api",_e.clearMenus),N.on(document,"click.bs.dropdown.data-api",'[data-bs-toggle="dropdown"]',(function(t){t.preventDefault(),_e.dropdownInterface(this)})),m("dropdown",_e);const be={backdrop:!0,keyboard:!0,focus:!0},ve={backdrop:"(boolean|string)",keyboard:"boolean",focus:"boolean"};class ye extends j{constructor(t,e){super(t),this._config=this._getConfig(e),this._dialog=H.findOne(".modal-dialog",this._element),this._backdrop=null,this._isShown=!1,this._isBodyOverflowing=!1,this._ignoreBackdropClick=!1,this._isTransitioning=!1,this._scrollbarWidth=0}static get Default(){return be}static get DATA_KEY(){return"bs.modal"}toggle(t){return this._isShown?this.hide():this.show(t)}show(t){if(this._isShown||this._isTransitioning)return;this._isAnimated()&&(this._isTransitioning=!0);const e=N.trigger(this._element,"show.bs.modal",{relatedTarget:t});this._isShown||e.defaultPrevented||(this._isShown=!0,this._checkScrollbar(),this._setScrollbar(),this._adjustDialog(),this._setEscapeEvent(),this._setResizeEvent(),N.on(this._element,"click.dismiss.bs.modal",'[data-bs-dismiss="modal"]',t=>this.hide(t)),N.on(this._dialog,"mousedown.dismiss.bs.modal",()=>{N.one(this._element,"mouseup.dismiss.bs.modal",t=>{t.target===this._element&&(this._ignoreBackdropClick=!0)})}),this._showBackdrop(()=>this._showElement(t)))}hide(t){if(t&&t.preventDefault(),!this._isShown||this._isTransitioning)return;if(N.trigger(this._element,"hide.bs.modal").defaultPrevented)return;this._isShown=!1;const e=this._isAnimated();if(e&&(this._isTransitioning=!0),this._setEscapeEvent(),this._setResizeEvent(),N.off(document,"focusin.bs.modal"),this._element.classList.remove("show"),N.off(this._element,"click.dismiss.bs.modal"),N.off(this._dialog,"mousedown.dismiss.bs.modal"),e){const t=n(this._element);N.one(this._element,"transitionend",t=>this._hideModal(t)),a(this._element,t)}else this._hideModal()}dispose(){[window,this._element,this._dialog].forEach(t=>N.off(t,".bs.modal")),super.dispose(),N.off(document,"focusin.bs.modal"),this._config=null,this._dialog=null,this._backdrop=null,this._isShown=null,this._isBodyOverflowing=null,this._ignoreBackdropClick=null,this._isTransitioning=null,this._scrollbarWidth=null}handleUpdate(){this._adjustDialog()}_getConfig(t){return t={...be,...t},l("modal",t,ve),t}_showElement(t){const e=this._isAnimated(),i=H.findOne(".modal-body",this._dialog);this._element.parentNode&&this._element.parentNode.nodeType===Node.ELEMENT_NODE||document.body.appendChild(this._element),this._element.style.display="block",this._element.removeAttribute("aria-hidden"),this._element.setAttribute("aria-modal",!0),this._element.setAttribute("role","dialog"),this._element.scrollTop=0,i&&(i.scrollTop=0),e&&u(this._element),this._element.classList.add("show"),this._config.focus&&this._enforceFocus();const s=()=>{this._config.focus&&this._element.focus(),this._isTransitioning=!1,N.trigger(this._element,"shown.bs.modal",{relatedTarget:t})};if(e){const t=n(this._dialog);N.one(this._dialog,"transitionend",s),a(this._dialog,t)}else s()}_enforceFocus(){N.off(document,"focusin.bs.modal"),N.on(document,"focusin.bs.modal",t=>{document===t.target||this._element===t.target||this._element.contains(t.target)||this._element.focus()})}_setEscapeEvent(){this._isShown?N.on(this._element,"keydown.dismiss.bs.modal",t=>{this._config.keyboard&&"Escape"===t.key?(t.preventDefault(),this.hide()):this._config.keyboard||"Escape"!==t.key||this._triggerBackdropTransition()}):N.off(this._element,"keydown.dismiss.bs.modal")}_setResizeEvent(){this._isShown?N.on(window,"resize.bs.modal",()=>this._adjustDialog()):N.off(window,"resize.bs.modal")}_hideModal(){this._element.style.display="none",this._element.setAttribute("aria-hidden",!0),this._element.removeAttribute("aria-modal"),this._element.removeAttribute("role"),this._isTransitioning=!1,this._showBackdrop(()=>{document.body.classList.remove("modal-open"),this._resetAdjustments(),this._resetScrollbar(),N.trigger(this._element,"hidden.bs.modal")})}_removeBackdrop(){this._backdrop.parentNode.removeChild(this._backdrop),this._backdrop=null}_showBackdrop(t){const e=this._isAnimated();if(this._isShown&&this._config.backdrop){if(this._backdrop=document.createElement("div"),this._backdrop.className="modal-backdrop",e&&this._backdrop.classList.add("fade"),document.body.appendChild(this._backdrop),N.on(this._element,"click.dismiss.bs.modal",t=>{this._ignoreBackdropClick?this._ignoreBackdropClick=!1:t.target===t.currentTarget&&("static"===this._config.backdrop?this._triggerBackdropTransition():this.hide())}),e&&u(this._backdrop),this._backdrop.classList.add("show"),!e)return void t();const i=n(this._backdrop);N.one(this._backdrop,"transitionend",t),a(this._backdrop,i)}else if(!this._isShown&&this._backdrop){this._backdrop.classList.remove("show");const i=()=>{this._removeBackdrop(),t()};if(e){const t=n(this._backdrop);N.one(this._backdrop,"transitionend",i),a(this._backdrop,t)}else i()}else t()}_isAnimated(){return this._element.classList.contains("fade")}_triggerBackdropTransition(){if(N.trigger(this._element,"hidePrevented.bs.modal").defaultPrevented)return;const t=this._element.scrollHeight>document.documentElement.clientHeight;t||(this._element.style.overflowY="hidden"),this._element.classList.add("modal-static");const e=n(this._dialog);N.off(this._element,"transitionend"),N.one(this._element,"transitionend",()=>{this._element.classList.remove("modal-static"),t||(N.one(this._element,"transitionend",()=>{this._element.style.overflowY=""}),a(this._element,e))}),a(this._element,e),this._element.focus()}_adjustDialog(){const t=this._element.scrollHeight>document.documentElement.clientHeight;(!this._isBodyOverflowing&&t&&!g()||this._isBodyOverflowing&&!t&&g())&&(this._element.style.paddingLeft=this._scrollbarWidth+"px"),(this._isBodyOverflowing&&!t&&!g()||!this._isBodyOverflowing&&t&&g())&&(this._element.style.paddingRight=this._scrollbarWidth+"px")}_resetAdjustments(){this._element.style.paddingLeft="",this._element.style.paddingRight=""}_checkScrollbar(){const t=document.body.getBoundingClientRect();this._isBodyOverflowing=Math.round(t.left+t.right)<window.innerWidth,this._scrollbarWidth=this._getScrollbarWidth()}_setScrollbar(){this._isBodyOverflowing&&(this._setElementAttributes(".fixed-top, .fixed-bottom, .is-fixed, .sticky-top","paddingRight",t=>t+this._scrollbarWidth),this._setElementAttributes(".sticky-top","marginRight",t=>t-this._scrollbarWidth),this._setElementAttributes("body","paddingRight",t=>t+this._scrollbarWidth)),document.body.classList.add("modal-open")}_setElementAttributes(t,e,i){H.find(t).forEach(t=>{if(t!==document.body&&window.innerWidth>t.clientWidth+this._scrollbarWidth)return;const s=t.style[e],n=window.getComputedStyle(t)[e];B.setDataAttribute(t,e,s),t.style[e]=i(Number.parseFloat(n))+"px"})}_resetScrollbar(){this._resetElementAttributes(".fixed-top, .fixed-bottom, .is-fixed, .sticky-top","paddingRight"),this._resetElementAttributes(".sticky-top","marginRight"),this._resetElementAttributes("body","paddingRight")}_resetElementAttributes(t,e){H.find(t).forEach(t=>{const i=B.getDataAttribute(t,e);void 0===i&&t===document.body?t.style[e]="":(B.removeDataAttribute(t,e),t.style[e]=i)})}_getScrollbarWidth(){const t=document.createElement("div");t.className="modal-scrollbar-measure",document.body.appendChild(t);const e=t.getBoundingClientRect().width-t.clientWidth;return document.body.removeChild(t),e}static jQueryInterface(t,e){return this.each((function(){let i=b.get(this,"bs.modal");const s={...be,...B.getDataAttributes(this),..."object"==typeof t&&t?t:{}};if(i||(i=new ye(this,s)),"string"==typeof t){if(void 0===i[t])throw new TypeError(`No method named "${t}"`);i[t](e)}}))}}N.on(document,"click.bs.modal.data-api",'[data-bs-toggle="modal"]',(function(t){const e=s(this);"A"!==this.tagName&&"AREA"!==this.tagName||t.preventDefault(),N.one(e,"show.bs.modal",t=>{t.defaultPrevented||N.one(e,"hidden.bs.modal",()=>{c(this)&&this.focus()})});let i=b.get(e,"bs.modal");if(!i){const t={...B.getDataAttributes(e),...B.getDataAttributes(this)};i=new ye(e,t)}i.toggle(this)})),m("modal",ye);const we=()=>{const t=document.documentElement.clientWidth;return Math.abs(window.innerWidth-t)},Ee=(t,e,i)=>{const s=we();H.find(t).forEach(t=>{if(t!==document.body&&window.innerWidth>t.clientWidth+s)return;const n=t.style[e],o=window.getComputedStyle(t)[e];B.setDataAttribute(t,e,n),t.style[e]=i(Number.parseFloat(o))+"px"})},Te=(t,e)=>{H.find(t).forEach(t=>{const i=B.getDataAttribute(t,e);void 0===i&&t===document.body?t.style.removeProperty(e):(B.removeDataAttribute(t,e),t.style[e]=i)})},Ae={backdrop:!0,keyboard:!0,scroll:!1},Le={backdrop:"boolean",keyboard:"boolean",scroll:"boolean"};class Oe extends j{constructor(t,e){super(t),this._config=this._getConfig(e),this._isShown=!1,this._addEventListeners()}static get Default(){return Ae}static get DATA_KEY(){return"bs.offcanvas"}toggle(t){return this._isShown?this.hide():this.show(t)}show(t){this._isShown||N.trigger(this._element,"show.bs.offcanvas",{relatedTarget:t}).defaultPrevented||(this._isShown=!0,this._element.style.visibility="visible",this._config.backdrop&&document.body.classList.add("offcanvas-backdrop"),this._config.scroll||((t=we())=>{document.body.style.overflow="hidden",Ee(".fixed-top, .fixed-bottom, .is-fixed","paddingRight",e=>e+t),Ee(".sticky-top","marginRight",e=>e-t),Ee("body","paddingRight",e=>e+t)})(),this._element.classList.add("offcanvas-toggling"),this._element.removeAttribute("aria-hidden"),this._element.setAttribute("aria-modal",!0),this._element.setAttribute("role","dialog"),this._element.classList.add("show"),setTimeout(()=>{this._element.classList.remove("offcanvas-toggling"),N.trigger(this._element,"shown.bs.offcanvas",{relatedTarget:t}),this._enforceFocusOnElement(this._element)},n(this._element)))}hide(){this._isShown&&(N.trigger(this._element,"hide.bs.offcanvas").defaultPrevented||(this._element.classList.add("offcanvas-toggling"),N.off(document,"focusin.bs.offcanvas"),this._element.blur(),this._isShown=!1,this._element.classList.remove("show"),setTimeout(()=>{this._element.setAttribute("aria-hidden",!0),this._element.removeAttribute("aria-modal"),this._element.removeAttribute("role"),this._element.style.visibility="hidden",this._config.backdrop&&document.body.classList.remove("offcanvas-backdrop"),this._config.scroll||(document.body.style.overflow="auto",Te(".fixed-top, .fixed-bottom, .is-fixed","paddingRight"),Te(".sticky-top","marginRight"),Te("body","paddingRight")),N.trigger(this._element,"hidden.bs.offcanvas"),this._element.classList.remove("offcanvas-toggling")},n(this._element))))}_getConfig(t){return t={...Ae,...B.getDataAttributes(this._element),..."object"==typeof t?t:{}},l("offcanvas",t,Le),t}_enforceFocusOnElement(t){N.off(document,"focusin.bs.offcanvas"),N.on(document,"focusin.bs.offcanvas",e=>{document===e.target||t===e.target||t.contains(e.target)||t.focus()}),t.focus()}_addEventListeners(){N.on(this._element,"click.dismiss.bs.offcanvas",'[data-bs-dismiss="offcanvas"]',()=>this.hide()),N.on(document,"keydown",t=>{this._config.keyboard&&"Escape"===t.key&&this.hide()}),N.on(document,"click.bs.offcanvas.data-api",t=>{const e=H.findOne(i(t.target));this._element.contains(t.target)||e===this._element||this.hide()})}static jQueryInterface(t){return this.each((function(){const e=b.get(this,"bs.offcanvas")||new Oe(this,"object"==typeof t?t:{});if("string"==typeof t){if(void 0===e[t]||t.startsWith("_")||"constructor"===t)throw new TypeError(`No method named "${t}"`);e[t](this)}}))}}N.on(document,"click.bs.offcanvas.data-api",'[data-bs-toggle="offcanvas"]',(function(t){const e=s(this);if(["A","AREA"].includes(this.tagName)&&t.preventDefault(),d(this))return;N.one(e,"hidden.bs.offcanvas",()=>{c(this)&&this.focus()});const i=H.findOne(".offcanvas.show, .offcanvas-toggling");i&&i!==e||(b.get(e,"bs.offcanvas")||new Oe(e)).toggle(this)})),N.on(window,"load.bs.offcanvas.data-api",()=>{H.find(".offcanvas.show").forEach(t=>(b.get(t,"bs.offcanvas")||new Oe(t)).show())}),m("offcanvas",Oe);const ke=new Set(["background","cite","href","itemtype","longdesc","poster","src","xlink:href"]),De=/^(?:(?:https?|mailto|ftp|tel|file):|[^#&/:?]*(?:[#/?]|$))/i,xe=/^data:(?:image\/(?:bmp|gif|jpeg|jpg|png|tiff|webp)|video\/(?:mpeg|mp4|ogg|webm)|audio\/(?:mp3|oga|ogg|opus));base64,[\d+/a-z]+=*$/i,Ce=(t,e)=>{const i=t.nodeName.toLowerCase();if(e.includes(i))return!ke.has(i)||Boolean(De.test(t.nodeValue)||xe.test(t.nodeValue));const s=e.filter(t=>t instanceof RegExp);for(let t=0,e=s.length;t<e;t++)if(s[t].test(i))return!0;return!1};function Se(t,e,i){if(!t.length)return t;if(i&&"function"==typeof i)return i(t);const s=(new window.DOMParser).parseFromString(t,"text/html"),n=Object.keys(e),o=[].concat(...s.body.querySelectorAll("*"));for(let t=0,i=o.length;t<i;t++){const i=o[t],s=i.nodeName.toLowerCase();if(!n.includes(s)){i.parentNode.removeChild(i);continue}const r=[].concat(...i.attributes),a=[].concat(e["*"]||[],e[s]||[]);r.forEach(t=>{Ce(t,a)||i.removeAttribute(t.nodeName)})}return s.body.innerHTML}const Ne=new RegExp("(^|\\s)bs-tooltip\\S+","g"),je=new Set(["sanitize","allowList","sanitizeFn"]),Pe={animation:"boolean",template:"string",title:"(string|element|function)",trigger:"string",delay:"(number|object)",html:"boolean",selector:"(string|boolean)",placement:"(string|function)",offset:"(array|string|function)",container:"(string|element|boolean)",fallbackPlacements:"array",boundary:"(string|element)",customClass:"(string|function)",sanitize:"boolean",sanitizeFn:"(null|function)",allowList:"object",popperConfig:"(null|object|function)"},Ie={AUTO:"auto",TOP:"top",RIGHT:g()?"left":"right",BOTTOM:"bottom",LEFT:g()?"right":"left"},Me={animation:!0,template:'<div class="tooltip" role="tooltip"><div class="tooltip-arrow"></div><div class="tooltip-inner"></div></div>',trigger:"hover focus",title:"",delay:0,html:!1,selector:!1,placement:"top",offset:[0,0],container:!1,fallbackPlacements:["top","right","bottom","left"],boundary:"clippingParents",customClass:"",sanitize:!0,sanitizeFn:null,allowList:{"*":["class","dir","id","lang","role",/^aria-[\w-]*$/i],a:["target","href","title","rel"],area:[],b:[],br:[],col:[],code:[],div:[],em:[],hr:[],h1:[],h2:[],h3:[],h4:[],h5:[],h6:[],i:[],img:["src","srcset","alt","title","width","height"],li:[],ol:[],p:[],pre:[],s:[],small:[],span:[],sub:[],sup:[],strong:[],u:[],ul:[]},popperConfig:null},Re={HIDE:"hide.bs.tooltip",HIDDEN:"hidden.bs.tooltip",SHOW:"show.bs.tooltip",SHOWN:"shown.bs.tooltip",INSERTED:"inserted.bs.tooltip",CLICK:"click.bs.tooltip",FOCUSIN:"focusin.bs.tooltip",FOCUSOUT:"focusout.bs.tooltip",MOUSEENTER:"mouseenter.bs.tooltip",MOUSELEAVE:"mouseleave.bs.tooltip"};class Be extends j{constructor(t,e){if(void 0===ae)throw new TypeError("Bootstrap's tooltips require Popper (https://popper.js.org)");super(t),this._isEnabled=!0,this._timeout=0,this._hoverState="",this._activeTrigger={},this._popper=null,this.config=this._getConfig(e),this.tip=null,this._setListeners()}static get Default(){return Me}static get NAME(){return"tooltip"}static get DATA_KEY(){return"bs.tooltip"}static get Event(){return Re}static get EVENT_KEY(){return".bs.tooltip"}static get DefaultType(){return Pe}enable(){this._isEnabled=!0}disable(){this._isEnabled=!1}toggleEnabled(){this._isEnabled=!this._isEnabled}toggle(t){if(this._isEnabled)if(t){const e=this._initializeOnDelegatedTarget(t);e._activeTrigger.click=!e._activeTrigger.click,e._isWithActiveTrigger()?e._enter(null,e):e._leave(null,e)}else{if(this.getTipElement().classList.contains("show"))return void this._leave(null,this);this._enter(null,this)}}dispose(){clearTimeout(this._timeout),N.off(this._element,this.constructor.EVENT_KEY),N.off(this._element.closest(".modal"),"hide.bs.modal",this._hideModalHandler),this.tip&&this.tip.parentNode&&this.tip.parentNode.removeChild(this.tip),this._isEnabled=null,this._timeout=null,this._hoverState=null,this._activeTrigger=null,this._popper&&this._popper.destroy(),this._popper=null,this.config=null,this.tip=null,super.dispose()}show(){if("none"===this._element.style.display)throw new Error("Please use show on visible elements");if(!this.isWithContent()||!this._isEnabled)return;const e=N.trigger(this._element,this.constructor.Event.SHOW),i=h(this._element),s=null===i?this._element.ownerDocument.documentElement.contains(this._element):i.contains(this._element);if(e.defaultPrevented||!s)return;const o=this.getTipElement(),r=t(this.constructor.NAME);o.setAttribute("id",r),this._element.setAttribute("aria-describedby",r),this.setContent(),this.config.animation&&o.classList.add("fade");const l="function"==typeof this.config.placement?this.config.placement.call(this,o,this._element):this.config.placement,c=this._getAttachment(l);this._addAttachmentClass(c);const d=this._getContainer();b.set(o,this.constructor.DATA_KEY,this),this._element.ownerDocument.documentElement.contains(this.tip)||(d.appendChild(o),N.trigger(this._element,this.constructor.Event.INSERTED)),this._popper?this._popper.update():this._popper=re(this._element,o,this._getPopperConfig(c)),o.classList.add("show");const f="function"==typeof this.config.customClass?this.config.customClass():this.config.customClass;f&&o.classList.add(...f.split(" ")),"ontouchstart"in document.documentElement&&[].concat(...document.body.children).forEach(t=>{N.on(t,"mouseover",(function(){}))});const u=()=>{const t=this._hoverState;this._hoverState=null,N.trigger(this._element,this.constructor.Event.SHOWN),"out"===t&&this._leave(null,this)};if(this.tip.classList.contains("fade")){const t=n(this.tip);N.one(this.tip,"transitionend",u),a(this.tip,t)}else u()}hide(){if(!this._popper)return;const t=this.getTipElement(),e=()=>{this._isWithActiveTrigger()||("show"!==this._hoverState&&t.parentNode&&t.parentNode.removeChild(t),this._cleanTipClass(),this._element.removeAttribute("aria-describedby"),N.trigger(this._element,this.constructor.Event.HIDDEN),this._popper&&(this._popper.destroy(),this._popper=null))};if(!N.trigger(this._element,this.constructor.Event.HIDE).defaultPrevented){if(t.classList.remove("show"),"ontouchstart"in document.documentElement&&[].concat(...document.body.children).forEach(t=>N.off(t,"mouseover",f)),this._activeTrigger.click=!1,this._activeTrigger.focus=!1,this._activeTrigger.hover=!1,this.tip.classList.contains("fade")){const i=n(t);N.one(t,"transitionend",e),a(t,i)}else e();this._hoverState=""}}update(){null!==this._popper&&this._popper.update()}isWithContent(){return Boolean(this.getTitle())}getTipElement(){if(this.tip)return this.tip;const t=document.createElement("div");return t.innerHTML=this.config.template,this.tip=t.children[0],this.tip}setContent(){const t=this.getTipElement();this.setElementContent(H.findOne(".tooltip-inner",t),this.getTitle()),t.classList.remove("fade","show")}setElementContent(t,e){if(null!==t)return"object"==typeof e&&r(e)?(e.jquery&&(e=e[0]),void(this.config.html?e.parentNode!==t&&(t.innerHTML="",t.appendChild(e)):t.textContent=e.textContent)):void(this.config.html?(this.config.sanitize&&(e=Se(e,this.config.allowList,this.config.sanitizeFn)),t.innerHTML=e):t.textContent=e)}getTitle(){let t=this._element.getAttribute("data-bs-original-title");return t||(t="function"==typeof this.config.title?this.config.title.call(this._element):this.config.title),t}updateAttachment(t){return"right"===t?"end":"left"===t?"start":t}_initializeOnDelegatedTarget(t,e){const i=this.constructor.DATA_KEY;return(e=e||b.get(t.delegateTarget,i))||(e=new this.constructor(t.delegateTarget,this._getDelegateConfig()),b.set(t.delegateTarget,i,e)),e}_getOffset(){const{offset:t}=this.config;return"string"==typeof t?t.split(",").map(t=>Number.parseInt(t,10)):"function"==typeof t?e=>t(e,this._element):t}_getPopperConfig(t){const e={placement:t,modifiers:[{name:"flip",options:{altBoundary:!0,fallbackPlacements:this.config.fallbackPlacements}},{name:"offset",options:{offset:this._getOffset()}},{name:"preventOverflow",options:{boundary:this.config.boundary}},{name:"arrow",options:{element:`.${this.constructor.NAME}-arrow`}},{name:"onChange",enabled:!0,phase:"afterWrite",fn:t=>this._handlePopperPlacementChange(t)}],onFirstUpdate:t=>{t.options.placement!==t.placement&&this._handlePopperPlacementChange(t)}};return{...e,..."function"==typeof this.config.popperConfig?this.config.popperConfig(e):this.config.popperConfig}}_addAttachmentClass(t){this.getTipElement().classList.add("bs-tooltip-"+this.updateAttachment(t))}_getContainer(){return!1===this.config.container?document.body:r(this.config.container)?this.config.container:H.findOne(this.config.container)}_getAttachment(t){return Ie[t.toUpperCase()]}_setListeners(){this.config.trigger.split(" ").forEach(t=>{if("click"===t)N.on(this._element,this.constructor.Event.CLICK,this.config.selector,t=>this.toggle(t));else if("manual"!==t){const e="hover"===t?this.constructor.Event.MOUSEENTER:this.constructor.Event.FOCUSIN,i="hover"===t?this.constructor.Event.MOUSELEAVE:this.constructor.Event.FOCUSOUT;N.on(this._element,e,this.config.selector,t=>this._enter(t)),N.on(this._element,i,this.config.selector,t=>this._leave(t))}}),this._hideModalHandler=()=>{this._element&&this.hide()},N.on(this._element.closest(".modal"),"hide.bs.modal",this._hideModalHandler),this.config.selector?this.config={...this.config,trigger:"manual",selector:""}:this._fixTitle()}_fixTitle(){const t=this._element.getAttribute("title"),e=typeof this._element.getAttribute("data-bs-original-title");(t||"string"!==e)&&(this._element.setAttribute("data-bs-original-title",t||""),!t||this._element.getAttribute("aria-label")||this._element.textContent||this._element.setAttribute("aria-label",t),this._element.setAttribute("title",""))}_enter(t,e){e=this._initializeOnDelegatedTarget(t,e),t&&(e._activeTrigger["focusin"===t.type?"focus":"hover"]=!0),e.getTipElement().classList.contains("show")||"show"===e._hoverState?e._hoverState="show":(clearTimeout(e._timeout),e._hoverState="show",e.config.delay&&e.config.delay.show?e._timeout=setTimeout(()=>{"show"===e._hoverState&&e.show()},e.config.delay.show):e.show())}_leave(t,e){e=this._initializeOnDelegatedTarget(t,e),t&&(e._activeTrigger["focusout"===t.type?"focus":"hover"]=e._element.contains(t.relatedTarget)),e._isWithActiveTrigger()||(clearTimeout(e._timeout),e._hoverState="out",e.config.delay&&e.config.delay.hide?e._timeout=setTimeout(()=>{"out"===e._hoverState&&e.hide()},e.config.delay.hide):e.hide())}_isWithActiveTrigger(){for(const t in this._activeTrigger)if(this._activeTrigger[t])return!0;return!1}_getConfig(t){const e=B.getDataAttributes(this._element);return Object.keys(e).forEach(t=>{je.has(t)&&delete e[t]}),t&&"object"==typeof t.container&&t.container.jquery&&(t.container=t.container[0]),"number"==typeof(t={...this.constructor.Default,...e,..."object"==typeof t&&t?t:{}}).delay&&(t.delay={show:t.delay,hide:t.delay}),"number"==typeof t.title&&(t.title=t.title.toString()),"number"==typeof t.content&&(t.content=t.content.toString()),l("tooltip",t,this.constructor.DefaultType),t.sanitize&&(t.template=Se(t.template,t.allowList,t.sanitizeFn)),t}_getDelegateConfig(){const t={};if(this.config)for(const e in this.config)this.constructor.Default[e]!==this.config[e]&&(t[e]=this.config[e]);return t}_cleanTipClass(){const t=this.getTipElement(),e=t.getAttribute("class").match(Ne);null!==e&&e.length>0&&e.map(t=>t.trim()).forEach(e=>t.classList.remove(e))}_handlePopperPlacementChange(t){const{state:e}=t;e&&(this.tip=e.elements.popper,this._cleanTipClass(),this._addAttachmentClass(this._getAttachment(e.placement)))}static jQueryInterface(t){return this.each((function(){let e=b.get(this,"bs.tooltip");const i="object"==typeof t&&t;if((e||!/dispose|hide/.test(t))&&(e||(e=new Be(this,i)),"string"==typeof t)){if(void 0===e[t])throw new TypeError(`No method named "${t}"`);e[t]()}}))}}m("tooltip",Be);const He=new RegExp("(^|\\s)bs-popover\\S+","g"),We={...Be.Default,placement:"right",offset:[0,8],trigger:"click",content:"",template:'<div class="popover" role="tooltip"><div class="popover-arrow"></div><h3 class="popover-header"></h3><div class="popover-body"></div></div>'},Ue={...Be.DefaultType,content:"(string|element|function)"},$e={HIDE:"hide.bs.popover",HIDDEN:"hidden.bs.popover",SHOW:"show.bs.popover",SHOWN:"shown.bs.popover",INSERTED:"inserted.bs.popover",CLICK:"click.bs.popover",FOCUSIN:"focusin.bs.popover",FOCUSOUT:"focusout.bs.popover",MOUSEENTER:"mouseenter.bs.popover",MOUSELEAVE:"mouseleave.bs.popover"};class Fe extends Be{static get Default(){return We}static get NAME(){return"popover"}static get DATA_KEY(){return"bs.popover"}static get Event(){return $e}static get EVENT_KEY(){return".bs.popover"}static get DefaultType(){return Ue}isWithContent(){return this.getTitle()||this._getContent()}setContent(){const t=this.getTipElement();this.setElementContent(H.findOne(".popover-header",t),this.getTitle());let e=this._getContent();"function"==typeof e&&(e=e.call(this._element)),this.setElementContent(H.findOne(".popover-body",t),e),t.classList.remove("fade","show")}_addAttachmentClass(t){this.getTipElement().classList.add("bs-popover-"+this.updateAttachment(t))}_getContent(){return this._element.getAttribute("data-bs-content")||this.config.content}_cleanTipClass(){const t=this.getTipElement(),e=t.getAttribute("class").match(He);null!==e&&e.length>0&&e.map(t=>t.trim()).forEach(e=>t.classList.remove(e))}static jQueryInterface(t){return this.each((function(){let e=b.get(this,"bs.popover");const i="object"==typeof t?t:null;if((e||!/dispose|hide/.test(t))&&(e||(e=new Fe(this,i),b.set(this,"bs.popover",e)),"string"==typeof t)){if(void 0===e[t])throw new TypeError(`No method named "${t}"`);e[t]()}}))}}m("popover",Fe);const ze={offset:10,method:"auto",target:""},Ke={offset:"number",method:"string",target:"(string|element)"};class Ye extends j{constructor(t,e){super(t),this._scrollElement="BODY"===this._element.tagName?window:this._element,this._config=this._getConfig(e),this._selector=`${this._config.target} .nav-link, ${this._config.target} .list-group-item, ${this._config.target} .dropdown-item`,this._offsets=[],this._targets=[],this._activeTarget=null,this._scrollHeight=0,N.on(this._scrollElement,"scroll.bs.scrollspy",()=>this._process()),this.refresh(),this._process()}static get Default(){return ze}static get DATA_KEY(){return"bs.scrollspy"}refresh(){const t=this._scrollElement===this._scrollElement.window?"offset":"position",e="auto"===this._config.method?t:this._config.method,s="position"===e?this._getScrollTop():0;this._offsets=[],this._targets=[],this._scrollHeight=this._getScrollHeight(),H.find(this._selector).map(t=>{const n=i(t),o=n?H.findOne(n):null;if(o){const t=o.getBoundingClientRect();if(t.width||t.height)return[B[e](o).top+s,n]}return null}).filter(t=>t).sort((t,e)=>t[0]-e[0]).forEach(t=>{this._offsets.push(t[0]),this._targets.push(t[1])})}dispose(){super.dispose(),N.off(this._scrollElement,".bs.scrollspy"),this._scrollElement=null,this._config=null,this._selector=null,this._offsets=null,this._targets=null,this._activeTarget=null,this._scrollHeight=null}_getConfig(e){if("string"!=typeof(e={...ze,..."object"==typeof e&&e?e:{}}).target&&r(e.target)){let{id:i}=e.target;i||(i=t("scrollspy"),e.target.id=i),e.target="#"+i}return l("scrollspy",e,Ke),e}_getScrollTop(){return this._scrollElement===window?this._scrollElement.pageYOffset:this._scrollElement.scrollTop}_getScrollHeight(){return this._scrollElement.scrollHeight||Math.max(document.body.scrollHeight,document.documentElement.scrollHeight)}_getOffsetHeight(){return this._scrollElement===window?window.innerHeight:this._scrollElement.getBoundingClientRect().height}_process(){const t=this._getScrollTop()+this._config.offset,e=this._getScrollHeight(),i=this._config.offset+e-this._getOffsetHeight();if(this._scrollHeight!==e&&this.refresh(),t>=i){const t=this._targets[this._targets.length-1];this._activeTarget!==t&&this._activate(t)}else{if(this._activeTarget&&t<this._offsets[0]&&this._offsets[0]>0)return this._activeTarget=null,void this._clear();for(let e=this._offsets.length;e--;)this._activeTarget!==this._targets[e]&&t>=this._offsets[e]&&(void 0===this._offsets[e+1]||t<this._offsets[e+1])&&this._activate(this._targets[e])}}_activate(t){this._activeTarget=t,this._clear();const e=this._selector.split(",").map(e=>`${e}[data-bs-target="${t}"],${e}[href="${t}"]`),i=H.findOne(e.join(","));i.classList.contains("dropdown-item")?(H.findOne(".dropdown-toggle",i.closest(".dropdown")).classList.add("active"),i.classList.add("active")):(i.classList.add("active"),H.parents(i,".nav, .list-group").forEach(t=>{H.prev(t,".nav-link, .list-group-item").forEach(t=>t.classList.add("active")),H.prev(t,".nav-item").forEach(t=>{H.children(t,".nav-link").forEach(t=>t.classList.add("active"))})})),N.trigger(this._scrollElement,"activate.bs.scrollspy",{relatedTarget:t})}_clear(){H.find(this._selector).filter(t=>t.classList.contains("active")).forEach(t=>t.classList.remove("active"))}static jQueryInterface(t){return this.each((function(){let e=b.get(this,"bs.scrollspy");if(e||(e=new Ye(this,"object"==typeof t&&t)),"string"==typeof t){if(void 0===e[t])throw new TypeError(`No method named "${t}"`);e[t]()}}))}}N.on(window,"load.bs.scrollspy.data-api",()=>{H.find('[data-bs-spy="scroll"]').forEach(t=>new Ye(t,B.getDataAttributes(t)))}),m("scrollspy",Ye);class qe extends j{static get DATA_KEY(){return"bs.tab"}show(){if(this._element.parentNode&&this._element.parentNode.nodeType===Node.ELEMENT_NODE&&this._element.classList.contains("active")||d(this._element))return;let t;const e=s(this._element),i=this._element.closest(".nav, .list-group");if(i){const e="UL"===i.nodeName||"OL"===i.nodeName?":scope > li > .active":".active";t=H.find(e,i),t=t[t.length-1]}const n=t?N.trigger(t,"hide.bs.tab",{relatedTarget:this._element}):null;if(N.trigger(this._element,"show.bs.tab",{relatedTarget:t}).defaultPrevented||null!==n&&n.defaultPrevented)return;this._activate(this._element,i);const o=()=>{N.trigger(t,"hidden.bs.tab",{relatedTarget:this._element}),N.trigger(this._element,"shown.bs.tab",{relatedTarget:t})};e?this._activate(e,e.parentNode,o):o()}_activate(t,e,i){const s=(!e||"UL"!==e.nodeName&&"OL"!==e.nodeName?H.children(e,".active"):H.find(":scope > li > .active",e))[0],o=i&&s&&s.classList.contains("fade"),r=()=>this._transitionComplete(t,s,i);if(s&&o){const t=n(s);s.classList.remove("show"),N.one(s,"transitionend",r),a(s,t)}else r()}_transitionComplete(t,e,i){if(e){e.classList.remove("active");const t=H.findOne(":scope > .dropdown-menu .active",e.parentNode);t&&t.classList.remove("active"),"tab"===e.getAttribute("role")&&e.setAttribute("aria-selected",!1)}t.classList.add("active"),"tab"===t.getAttribute("role")&&t.setAttribute("aria-selected",!0),u(t),t.classList.contains("fade")&&t.classList.add("show"),t.parentNode&&t.parentNode.classList.contains("dropdown-menu")&&(t.closest(".dropdown")&&H.find(".dropdown-toggle").forEach(t=>t.classList.add("active")),t.setAttribute("aria-expanded",!0)),i&&i()}static jQueryInterface(t){return this.each((function(){const e=b.get(this,"bs.tab")||new qe(this);if("string"==typeof t){if(void 0===e[t])throw new TypeError(`No method named "${t}"`);e[t]()}}))}}N.on(document,"click.bs.tab.data-api",'[data-bs-toggle="tab"], [data-bs-toggle="pill"], [data-bs-toggle="list"]',(function(t){t.preventDefault(),(b.get(this,"bs.tab")||new qe(this)).show()})),m("tab",qe);const Ve={animation:"boolean",autohide:"boolean",delay:"number"},Xe={animation:!0,autohide:!0,delay:5e3};class Qe extends j{constructor(t,e){super(t),this._config=this._getConfig(e),this._timeout=null,this._setListeners()}static get DefaultType(){return Ve}static get Default(){return Xe}static get DATA_KEY(){return"bs.toast"}show(){if(N.trigger(this._element,"show.bs.toast").defaultPrevented)return;this._clearTimeout(),this._config.animation&&this._element.classList.add("fade");const t=()=>{this._element.classList.remove("showing"),this._element.classList.add("show"),N.trigger(this._element,"shown.bs.toast"),this._config.autohide&&(this._timeout=setTimeout(()=>{this.hide()},this._config.delay))};if(this._element.classList.remove("hide"),u(this._element),this._element.classList.add("showing"),this._config.animation){const e=n(this._element);N.one(this._element,"transitionend",t),a(this._element,e)}else t()}hide(){if(!this._element.classList.contains("show"))return;if(N.trigger(this._element,"hide.bs.toast").defaultPrevented)return;const t=()=>{this._element.classList.add("hide"),N.trigger(this._element,"hidden.bs.toast")};if(this._element.classList.remove("show"),this._config.animation){const e=n(this._element);N.one(this._element,"transitionend",t),a(this._element,e)}else t()}dispose(){this._clearTimeout(),this._element.classList.contains("show")&&this._element.classList.remove("show"),N.off(this._element,"click.dismiss.bs.toast"),super.dispose(),this._config=null}_getConfig(t){return t={...Xe,...B.getDataAttributes(this._element),..."object"==typeof t&&t?t:{}},l("toast",t,this.constructor.DefaultType),t}_setListeners(){N.on(this._element,"click.dismiss.bs.toast",'[data-bs-dismiss="toast"]',()=>this.hide())}_clearTimeout(){clearTimeout(this._timeout),this._timeout=null}static jQueryInterface(t){return this.each((function(){let e=b.get(this,"bs.toast");if(e||(e=new Qe(this,"object"==typeof t&&t)),"string"==typeof t){if(void 0===e[t])throw new TypeError(`No method named "${t}"`);e[t](this)}}))}}return m("toast",Qe),{Alert:P,Button:I,Carousel:Y,Collapse:X,Dropdown:_e,Modal:ye,Offcanvas:Oe,Popover:Fe,ScrollSpy:Ye,Tab:qe,Toast:Qe,Tooltip:Be}})); +//# sourceMappingURL=bootstrap.bundle.min.js.map \ No newline at end of file diff --git a/libs/jquery-3.3.1.zip b/libs/jquery-3.3.1.zip deleted file mode 100644 index 404fac0639caf20bd4cf55419aa9a5f5bb768029..0000000000000000000000000000000000000000 Binary files a/libs/jquery-3.3.1.zip and /dev/null differ diff --git a/libs/jquery-3.6.0.min.js b/libs/jquery-3.6.0.min.js new file mode 100644 index 0000000000000000000000000000000000000000..c4c6022f2982e8dae64cebd6b9a2b59f2547faad --- /dev/null +++ b/libs/jquery-3.6.0.min.js @@ -0,0 +1,2 @@ +/*! jQuery v3.6.0 | (c) OpenJS Foundation and other contributors | jquery.org/license */ +!function(e,t){"use strict";"object"==typeof module&&"object"==typeof module.exports?module.exports=e.document?t(e,!0):function(e){if(!e.document)throw new Error("jQuery requires a window with a document");return t(e)}:t(e)}("undefined"!=typeof window?window:this,function(C,e){"use strict";var t=[],r=Object.getPrototypeOf,s=t.slice,g=t.flat?function(e){return t.flat.call(e)}:function(e){return t.concat.apply([],e)},u=t.push,i=t.indexOf,n={},o=n.toString,v=n.hasOwnProperty,a=v.toString,l=a.call(Object),y={},m=function(e){return"function"==typeof e&&"number"!=typeof e.nodeType&&"function"!=typeof e.item},x=function(e){return null!=e&&e===e.window},E=C.document,c={type:!0,src:!0,nonce:!0,noModule:!0};function b(e,t,n){var r,i,o=(n=n||E).createElement("script");if(o.text=e,t)for(r in c)(i=t[r]||t.getAttribute&&t.getAttribute(r))&&o.setAttribute(r,i);n.head.appendChild(o).parentNode.removeChild(o)}function w(e){return null==e?e+"":"object"==typeof e||"function"==typeof e?n[o.call(e)]||"object":typeof e}var f="3.6.0",S=function(e,t){return new S.fn.init(e,t)};function p(e){var t=!!e&&"length"in e&&e.length,n=w(e);return!m(e)&&!x(e)&&("array"===n||0===t||"number"==typeof t&&0<t&&t-1 in e)}S.fn=S.prototype={jquery:f,constructor:S,length:0,toArray:function(){return s.call(this)},get:function(e){return null==e?s.call(this):e<0?this[e+this.length]:this[e]},pushStack:function(e){var t=S.merge(this.constructor(),e);return t.prevObject=this,t},each:function(e){return S.each(this,e)},map:function(n){return this.pushStack(S.map(this,function(e,t){return n.call(e,t,e)}))},slice:function(){return this.pushStack(s.apply(this,arguments))},first:function(){return this.eq(0)},last:function(){return this.eq(-1)},even:function(){return this.pushStack(S.grep(this,function(e,t){return(t+1)%2}))},odd:function(){return this.pushStack(S.grep(this,function(e,t){return t%2}))},eq:function(e){var t=this.length,n=+e+(e<0?t:0);return this.pushStack(0<=n&&n<t?[this[n]]:[])},end:function(){return this.prevObject||this.constructor()},push:u,sort:t.sort,splice:t.splice},S.extend=S.fn.extend=function(){var e,t,n,r,i,o,a=arguments[0]||{},s=1,u=arguments.length,l=!1;for("boolean"==typeof a&&(l=a,a=arguments[s]||{},s++),"object"==typeof a||m(a)||(a={}),s===u&&(a=this,s--);s<u;s++)if(null!=(e=arguments[s]))for(t in e)r=e[t],"__proto__"!==t&&a!==r&&(l&&r&&(S.isPlainObject(r)||(i=Array.isArray(r)))?(n=a[t],o=i&&!Array.isArray(n)?[]:i||S.isPlainObject(n)?n:{},i=!1,a[t]=S.extend(l,o,r)):void 0!==r&&(a[t]=r));return a},S.extend({expando:"jQuery"+(f+Math.random()).replace(/\D/g,""),isReady:!0,error:function(e){throw new Error(e)},noop:function(){},isPlainObject:function(e){var t,n;return!(!e||"[object Object]"!==o.call(e))&&(!(t=r(e))||"function"==typeof(n=v.call(t,"constructor")&&t.constructor)&&a.call(n)===l)},isEmptyObject:function(e){var t;for(t in e)return!1;return!0},globalEval:function(e,t,n){b(e,{nonce:t&&t.nonce},n)},each:function(e,t){var n,r=0;if(p(e)){for(n=e.length;r<n;r++)if(!1===t.call(e[r],r,e[r]))break}else for(r in e)if(!1===t.call(e[r],r,e[r]))break;return e},makeArray:function(e,t){var n=t||[];return null!=e&&(p(Object(e))?S.merge(n,"string"==typeof e?[e]:e):u.call(n,e)),n},inArray:function(e,t,n){return null==t?-1:i.call(t,e,n)},merge:function(e,t){for(var n=+t.length,r=0,i=e.length;r<n;r++)e[i++]=t[r];return e.length=i,e},grep:function(e,t,n){for(var r=[],i=0,o=e.length,a=!n;i<o;i++)!t(e[i],i)!==a&&r.push(e[i]);return r},map:function(e,t,n){var r,i,o=0,a=[];if(p(e))for(r=e.length;o<r;o++)null!=(i=t(e[o],o,n))&&a.push(i);else for(o in e)null!=(i=t(e[o],o,n))&&a.push(i);return g(a)},guid:1,support:y}),"function"==typeof Symbol&&(S.fn[Symbol.iterator]=t[Symbol.iterator]),S.each("Boolean Number String Function Array Date RegExp Object Error Symbol".split(" "),function(e,t){n["[object "+t+"]"]=t.toLowerCase()});var d=function(n){var e,d,b,o,i,h,f,g,w,u,l,T,C,a,E,v,s,c,y,S="sizzle"+1*new Date,p=n.document,k=0,r=0,m=ue(),x=ue(),A=ue(),N=ue(),j=function(e,t){return e===t&&(l=!0),0},D={}.hasOwnProperty,t=[],q=t.pop,L=t.push,H=t.push,O=t.slice,P=function(e,t){for(var n=0,r=e.length;n<r;n++)if(e[n]===t)return n;return-1},R="checked|selected|async|autofocus|autoplay|controls|defer|disabled|hidden|ismap|loop|multiple|open|readonly|required|scoped",M="[\\x20\\t\\r\\n\\f]",I="(?:\\\\[\\da-fA-F]{1,6}"+M+"?|\\\\[^\\r\\n\\f]|[\\w-]|[^\0-\\x7f])+",W="\\["+M+"*("+I+")(?:"+M+"*([*^$|!~]?=)"+M+"*(?:'((?:\\\\.|[^\\\\'])*)'|\"((?:\\\\.|[^\\\\\"])*)\"|("+I+"))|)"+M+"*\\]",F=":("+I+")(?:\\((('((?:\\\\.|[^\\\\'])*)'|\"((?:\\\\.|[^\\\\\"])*)\")|((?:\\\\.|[^\\\\()[\\]]|"+W+")*)|.*)\\)|)",B=new RegExp(M+"+","g"),$=new RegExp("^"+M+"+|((?:^|[^\\\\])(?:\\\\.)*)"+M+"+$","g"),_=new RegExp("^"+M+"*,"+M+"*"),z=new RegExp("^"+M+"*([>+~]|"+M+")"+M+"*"),U=new RegExp(M+"|>"),X=new RegExp(F),V=new RegExp("^"+I+"$"),G={ID:new RegExp("^#("+I+")"),CLASS:new RegExp("^\\.("+I+")"),TAG:new RegExp("^("+I+"|[*])"),ATTR:new RegExp("^"+W),PSEUDO:new RegExp("^"+F),CHILD:new RegExp("^:(only|first|last|nth|nth-last)-(child|of-type)(?:\\("+M+"*(even|odd|(([+-]|)(\\d*)n|)"+M+"*(?:([+-]|)"+M+"*(\\d+)|))"+M+"*\\)|)","i"),bool:new RegExp("^(?:"+R+")$","i"),needsContext:new RegExp("^"+M+"*[>+~]|:(even|odd|eq|gt|lt|nth|first|last)(?:\\("+M+"*((?:-\\d)?\\d*)"+M+"*\\)|)(?=[^-]|$)","i")},Y=/HTML$/i,Q=/^(?:input|select|textarea|button)$/i,J=/^h\d$/i,K=/^[^{]+\{\s*\[native \w/,Z=/^(?:#([\w-]+)|(\w+)|\.([\w-]+))$/,ee=/[+~]/,te=new RegExp("\\\\[\\da-fA-F]{1,6}"+M+"?|\\\\([^\\r\\n\\f])","g"),ne=function(e,t){var n="0x"+e.slice(1)-65536;return t||(n<0?String.fromCharCode(n+65536):String.fromCharCode(n>>10|55296,1023&n|56320))},re=/([\0-\x1f\x7f]|^-?\d)|^-$|[^\0-\x1f\x7f-\uFFFF\w-]/g,ie=function(e,t){return t?"\0"===e?"\ufffd":e.slice(0,-1)+"\\"+e.charCodeAt(e.length-1).toString(16)+" ":"\\"+e},oe=function(){T()},ae=be(function(e){return!0===e.disabled&&"fieldset"===e.nodeName.toLowerCase()},{dir:"parentNode",next:"legend"});try{H.apply(t=O.call(p.childNodes),p.childNodes),t[p.childNodes.length].nodeType}catch(e){H={apply:t.length?function(e,t){L.apply(e,O.call(t))}:function(e,t){var n=e.length,r=0;while(e[n++]=t[r++]);e.length=n-1}}}function se(t,e,n,r){var i,o,a,s,u,l,c,f=e&&e.ownerDocument,p=e?e.nodeType:9;if(n=n||[],"string"!=typeof t||!t||1!==p&&9!==p&&11!==p)return n;if(!r&&(T(e),e=e||C,E)){if(11!==p&&(u=Z.exec(t)))if(i=u[1]){if(9===p){if(!(a=e.getElementById(i)))return n;if(a.id===i)return n.push(a),n}else if(f&&(a=f.getElementById(i))&&y(e,a)&&a.id===i)return n.push(a),n}else{if(u[2])return H.apply(n,e.getElementsByTagName(t)),n;if((i=u[3])&&d.getElementsByClassName&&e.getElementsByClassName)return H.apply(n,e.getElementsByClassName(i)),n}if(d.qsa&&!N[t+" "]&&(!v||!v.test(t))&&(1!==p||"object"!==e.nodeName.toLowerCase())){if(c=t,f=e,1===p&&(U.test(t)||z.test(t))){(f=ee.test(t)&&ye(e.parentNode)||e)===e&&d.scope||((s=e.getAttribute("id"))?s=s.replace(re,ie):e.setAttribute("id",s=S)),o=(l=h(t)).length;while(o--)l[o]=(s?"#"+s:":scope")+" "+xe(l[o]);c=l.join(",")}try{return H.apply(n,f.querySelectorAll(c)),n}catch(e){N(t,!0)}finally{s===S&&e.removeAttribute("id")}}}return g(t.replace($,"$1"),e,n,r)}function ue(){var r=[];return function e(t,n){return r.push(t+" ")>b.cacheLength&&delete e[r.shift()],e[t+" "]=n}}function le(e){return e[S]=!0,e}function ce(e){var t=C.createElement("fieldset");try{return!!e(t)}catch(e){return!1}finally{t.parentNode&&t.parentNode.removeChild(t),t=null}}function fe(e,t){var n=e.split("|"),r=n.length;while(r--)b.attrHandle[n[r]]=t}function pe(e,t){var n=t&&e,r=n&&1===e.nodeType&&1===t.nodeType&&e.sourceIndex-t.sourceIndex;if(r)return r;if(n)while(n=n.nextSibling)if(n===t)return-1;return e?1:-1}function de(t){return function(e){return"input"===e.nodeName.toLowerCase()&&e.type===t}}function he(n){return function(e){var t=e.nodeName.toLowerCase();return("input"===t||"button"===t)&&e.type===n}}function ge(t){return function(e){return"form"in e?e.parentNode&&!1===e.disabled?"label"in e?"label"in e.parentNode?e.parentNode.disabled===t:e.disabled===t:e.isDisabled===t||e.isDisabled!==!t&&ae(e)===t:e.disabled===t:"label"in e&&e.disabled===t}}function ve(a){return le(function(o){return o=+o,le(function(e,t){var n,r=a([],e.length,o),i=r.length;while(i--)e[n=r[i]]&&(e[n]=!(t[n]=e[n]))})})}function ye(e){return e&&"undefined"!=typeof e.getElementsByTagName&&e}for(e in d=se.support={},i=se.isXML=function(e){var t=e&&e.namespaceURI,n=e&&(e.ownerDocument||e).documentElement;return!Y.test(t||n&&n.nodeName||"HTML")},T=se.setDocument=function(e){var t,n,r=e?e.ownerDocument||e:p;return r!=C&&9===r.nodeType&&r.documentElement&&(a=(C=r).documentElement,E=!i(C),p!=C&&(n=C.defaultView)&&n.top!==n&&(n.addEventListener?n.addEventListener("unload",oe,!1):n.attachEvent&&n.attachEvent("onunload",oe)),d.scope=ce(function(e){return a.appendChild(e).appendChild(C.createElement("div")),"undefined"!=typeof e.querySelectorAll&&!e.querySelectorAll(":scope fieldset div").length}),d.attributes=ce(function(e){return e.className="i",!e.getAttribute("className")}),d.getElementsByTagName=ce(function(e){return e.appendChild(C.createComment("")),!e.getElementsByTagName("*").length}),d.getElementsByClassName=K.test(C.getElementsByClassName),d.getById=ce(function(e){return a.appendChild(e).id=S,!C.getElementsByName||!C.getElementsByName(S).length}),d.getById?(b.filter.ID=function(e){var t=e.replace(te,ne);return function(e){return e.getAttribute("id")===t}},b.find.ID=function(e,t){if("undefined"!=typeof t.getElementById&&E){var n=t.getElementById(e);return n?[n]:[]}}):(b.filter.ID=function(e){var n=e.replace(te,ne);return function(e){var t="undefined"!=typeof e.getAttributeNode&&e.getAttributeNode("id");return t&&t.value===n}},b.find.ID=function(e,t){if("undefined"!=typeof t.getElementById&&E){var n,r,i,o=t.getElementById(e);if(o){if((n=o.getAttributeNode("id"))&&n.value===e)return[o];i=t.getElementsByName(e),r=0;while(o=i[r++])if((n=o.getAttributeNode("id"))&&n.value===e)return[o]}return[]}}),b.find.TAG=d.getElementsByTagName?function(e,t){return"undefined"!=typeof t.getElementsByTagName?t.getElementsByTagName(e):d.qsa?t.querySelectorAll(e):void 0}:function(e,t){var n,r=[],i=0,o=t.getElementsByTagName(e);if("*"===e){while(n=o[i++])1===n.nodeType&&r.push(n);return r}return o},b.find.CLASS=d.getElementsByClassName&&function(e,t){if("undefined"!=typeof t.getElementsByClassName&&E)return t.getElementsByClassName(e)},s=[],v=[],(d.qsa=K.test(C.querySelectorAll))&&(ce(function(e){var t;a.appendChild(e).innerHTML="<a id='"+S+"'></a><select id='"+S+"-\r\\' msallowcapture=''><option selected=''></option></select>",e.querySelectorAll("[msallowcapture^='']").length&&v.push("[*^$]="+M+"*(?:''|\"\")"),e.querySelectorAll("[selected]").length||v.push("\\["+M+"*(?:value|"+R+")"),e.querySelectorAll("[id~="+S+"-]").length||v.push("~="),(t=C.createElement("input")).setAttribute("name",""),e.appendChild(t),e.querySelectorAll("[name='']").length||v.push("\\["+M+"*name"+M+"*="+M+"*(?:''|\"\")"),e.querySelectorAll(":checked").length||v.push(":checked"),e.querySelectorAll("a#"+S+"+*").length||v.push(".#.+[+~]"),e.querySelectorAll("\\\f"),v.push("[\\r\\n\\f]")}),ce(function(e){e.innerHTML="<a href='' disabled='disabled'></a><select disabled='disabled'><option/></select>";var t=C.createElement("input");t.setAttribute("type","hidden"),e.appendChild(t).setAttribute("name","D"),e.querySelectorAll("[name=d]").length&&v.push("name"+M+"*[*^$|!~]?="),2!==e.querySelectorAll(":enabled").length&&v.push(":enabled",":disabled"),a.appendChild(e).disabled=!0,2!==e.querySelectorAll(":disabled").length&&v.push(":enabled",":disabled"),e.querySelectorAll("*,:x"),v.push(",.*:")})),(d.matchesSelector=K.test(c=a.matches||a.webkitMatchesSelector||a.mozMatchesSelector||a.oMatchesSelector||a.msMatchesSelector))&&ce(function(e){d.disconnectedMatch=c.call(e,"*"),c.call(e,"[s!='']:x"),s.push("!=",F)}),v=v.length&&new RegExp(v.join("|")),s=s.length&&new RegExp(s.join("|")),t=K.test(a.compareDocumentPosition),y=t||K.test(a.contains)?function(e,t){var n=9===e.nodeType?e.documentElement:e,r=t&&t.parentNode;return e===r||!(!r||1!==r.nodeType||!(n.contains?n.contains(r):e.compareDocumentPosition&&16&e.compareDocumentPosition(r)))}:function(e,t){if(t)while(t=t.parentNode)if(t===e)return!0;return!1},j=t?function(e,t){if(e===t)return l=!0,0;var n=!e.compareDocumentPosition-!t.compareDocumentPosition;return n||(1&(n=(e.ownerDocument||e)==(t.ownerDocument||t)?e.compareDocumentPosition(t):1)||!d.sortDetached&&t.compareDocumentPosition(e)===n?e==C||e.ownerDocument==p&&y(p,e)?-1:t==C||t.ownerDocument==p&&y(p,t)?1:u?P(u,e)-P(u,t):0:4&n?-1:1)}:function(e,t){if(e===t)return l=!0,0;var n,r=0,i=e.parentNode,o=t.parentNode,a=[e],s=[t];if(!i||!o)return e==C?-1:t==C?1:i?-1:o?1:u?P(u,e)-P(u,t):0;if(i===o)return pe(e,t);n=e;while(n=n.parentNode)a.unshift(n);n=t;while(n=n.parentNode)s.unshift(n);while(a[r]===s[r])r++;return r?pe(a[r],s[r]):a[r]==p?-1:s[r]==p?1:0}),C},se.matches=function(e,t){return se(e,null,null,t)},se.matchesSelector=function(e,t){if(T(e),d.matchesSelector&&E&&!N[t+" "]&&(!s||!s.test(t))&&(!v||!v.test(t)))try{var n=c.call(e,t);if(n||d.disconnectedMatch||e.document&&11!==e.document.nodeType)return n}catch(e){N(t,!0)}return 0<se(t,C,null,[e]).length},se.contains=function(e,t){return(e.ownerDocument||e)!=C&&T(e),y(e,t)},se.attr=function(e,t){(e.ownerDocument||e)!=C&&T(e);var n=b.attrHandle[t.toLowerCase()],r=n&&D.call(b.attrHandle,t.toLowerCase())?n(e,t,!E):void 0;return void 0!==r?r:d.attributes||!E?e.getAttribute(t):(r=e.getAttributeNode(t))&&r.specified?r.value:null},se.escape=function(e){return(e+"").replace(re,ie)},se.error=function(e){throw new Error("Syntax error, unrecognized expression: "+e)},se.uniqueSort=function(e){var t,n=[],r=0,i=0;if(l=!d.detectDuplicates,u=!d.sortStable&&e.slice(0),e.sort(j),l){while(t=e[i++])t===e[i]&&(r=n.push(i));while(r--)e.splice(n[r],1)}return u=null,e},o=se.getText=function(e){var t,n="",r=0,i=e.nodeType;if(i){if(1===i||9===i||11===i){if("string"==typeof e.textContent)return e.textContent;for(e=e.firstChild;e;e=e.nextSibling)n+=o(e)}else if(3===i||4===i)return e.nodeValue}else while(t=e[r++])n+=o(t);return n},(b=se.selectors={cacheLength:50,createPseudo:le,match:G,attrHandle:{},find:{},relative:{">":{dir:"parentNode",first:!0}," ":{dir:"parentNode"},"+":{dir:"previousSibling",first:!0},"~":{dir:"previousSibling"}},preFilter:{ATTR:function(e){return e[1]=e[1].replace(te,ne),e[3]=(e[3]||e[4]||e[5]||"").replace(te,ne),"~="===e[2]&&(e[3]=" "+e[3]+" "),e.slice(0,4)},CHILD:function(e){return e[1]=e[1].toLowerCase(),"nth"===e[1].slice(0,3)?(e[3]||se.error(e[0]),e[4]=+(e[4]?e[5]+(e[6]||1):2*("even"===e[3]||"odd"===e[3])),e[5]=+(e[7]+e[8]||"odd"===e[3])):e[3]&&se.error(e[0]),e},PSEUDO:function(e){var t,n=!e[6]&&e[2];return G.CHILD.test(e[0])?null:(e[3]?e[2]=e[4]||e[5]||"":n&&X.test(n)&&(t=h(n,!0))&&(t=n.indexOf(")",n.length-t)-n.length)&&(e[0]=e[0].slice(0,t),e[2]=n.slice(0,t)),e.slice(0,3))}},filter:{TAG:function(e){var t=e.replace(te,ne).toLowerCase();return"*"===e?function(){return!0}:function(e){return e.nodeName&&e.nodeName.toLowerCase()===t}},CLASS:function(e){var t=m[e+" "];return t||(t=new RegExp("(^|"+M+")"+e+"("+M+"|$)"))&&m(e,function(e){return t.test("string"==typeof e.className&&e.className||"undefined"!=typeof e.getAttribute&&e.getAttribute("class")||"")})},ATTR:function(n,r,i){return function(e){var t=se.attr(e,n);return null==t?"!="===r:!r||(t+="","="===r?t===i:"!="===r?t!==i:"^="===r?i&&0===t.indexOf(i):"*="===r?i&&-1<t.indexOf(i):"$="===r?i&&t.slice(-i.length)===i:"~="===r?-1<(" "+t.replace(B," ")+" ").indexOf(i):"|="===r&&(t===i||t.slice(0,i.length+1)===i+"-"))}},CHILD:function(h,e,t,g,v){var y="nth"!==h.slice(0,3),m="last"!==h.slice(-4),x="of-type"===e;return 1===g&&0===v?function(e){return!!e.parentNode}:function(e,t,n){var r,i,o,a,s,u,l=y!==m?"nextSibling":"previousSibling",c=e.parentNode,f=x&&e.nodeName.toLowerCase(),p=!n&&!x,d=!1;if(c){if(y){while(l){a=e;while(a=a[l])if(x?a.nodeName.toLowerCase()===f:1===a.nodeType)return!1;u=l="only"===h&&!u&&"nextSibling"}return!0}if(u=[m?c.firstChild:c.lastChild],m&&p){d=(s=(r=(i=(o=(a=c)[S]||(a[S]={}))[a.uniqueID]||(o[a.uniqueID]={}))[h]||[])[0]===k&&r[1])&&r[2],a=s&&c.childNodes[s];while(a=++s&&a&&a[l]||(d=s=0)||u.pop())if(1===a.nodeType&&++d&&a===e){i[h]=[k,s,d];break}}else if(p&&(d=s=(r=(i=(o=(a=e)[S]||(a[S]={}))[a.uniqueID]||(o[a.uniqueID]={}))[h]||[])[0]===k&&r[1]),!1===d)while(a=++s&&a&&a[l]||(d=s=0)||u.pop())if((x?a.nodeName.toLowerCase()===f:1===a.nodeType)&&++d&&(p&&((i=(o=a[S]||(a[S]={}))[a.uniqueID]||(o[a.uniqueID]={}))[h]=[k,d]),a===e))break;return(d-=v)===g||d%g==0&&0<=d/g}}},PSEUDO:function(e,o){var t,a=b.pseudos[e]||b.setFilters[e.toLowerCase()]||se.error("unsupported pseudo: "+e);return a[S]?a(o):1<a.length?(t=[e,e,"",o],b.setFilters.hasOwnProperty(e.toLowerCase())?le(function(e,t){var n,r=a(e,o),i=r.length;while(i--)e[n=P(e,r[i])]=!(t[n]=r[i])}):function(e){return a(e,0,t)}):a}},pseudos:{not:le(function(e){var r=[],i=[],s=f(e.replace($,"$1"));return s[S]?le(function(e,t,n,r){var i,o=s(e,null,r,[]),a=e.length;while(a--)(i=o[a])&&(e[a]=!(t[a]=i))}):function(e,t,n){return r[0]=e,s(r,null,n,i),r[0]=null,!i.pop()}}),has:le(function(t){return function(e){return 0<se(t,e).length}}),contains:le(function(t){return t=t.replace(te,ne),function(e){return-1<(e.textContent||o(e)).indexOf(t)}}),lang:le(function(n){return V.test(n||"")||se.error("unsupported lang: "+n),n=n.replace(te,ne).toLowerCase(),function(e){var t;do{if(t=E?e.lang:e.getAttribute("xml:lang")||e.getAttribute("lang"))return(t=t.toLowerCase())===n||0===t.indexOf(n+"-")}while((e=e.parentNode)&&1===e.nodeType);return!1}}),target:function(e){var t=n.location&&n.location.hash;return t&&t.slice(1)===e.id},root:function(e){return e===a},focus:function(e){return e===C.activeElement&&(!C.hasFocus||C.hasFocus())&&!!(e.type||e.href||~e.tabIndex)},enabled:ge(!1),disabled:ge(!0),checked:function(e){var t=e.nodeName.toLowerCase();return"input"===t&&!!e.checked||"option"===t&&!!e.selected},selected:function(e){return e.parentNode&&e.parentNode.selectedIndex,!0===e.selected},empty:function(e){for(e=e.firstChild;e;e=e.nextSibling)if(e.nodeType<6)return!1;return!0},parent:function(e){return!b.pseudos.empty(e)},header:function(e){return J.test(e.nodeName)},input:function(e){return Q.test(e.nodeName)},button:function(e){var t=e.nodeName.toLowerCase();return"input"===t&&"button"===e.type||"button"===t},text:function(e){var t;return"input"===e.nodeName.toLowerCase()&&"text"===e.type&&(null==(t=e.getAttribute("type"))||"text"===t.toLowerCase())},first:ve(function(){return[0]}),last:ve(function(e,t){return[t-1]}),eq:ve(function(e,t,n){return[n<0?n+t:n]}),even:ve(function(e,t){for(var n=0;n<t;n+=2)e.push(n);return e}),odd:ve(function(e,t){for(var n=1;n<t;n+=2)e.push(n);return e}),lt:ve(function(e,t,n){for(var r=n<0?n+t:t<n?t:n;0<=--r;)e.push(r);return e}),gt:ve(function(e,t,n){for(var r=n<0?n+t:n;++r<t;)e.push(r);return e})}}).pseudos.nth=b.pseudos.eq,{radio:!0,checkbox:!0,file:!0,password:!0,image:!0})b.pseudos[e]=de(e);for(e in{submit:!0,reset:!0})b.pseudos[e]=he(e);function me(){}function xe(e){for(var t=0,n=e.length,r="";t<n;t++)r+=e[t].value;return r}function be(s,e,t){var u=e.dir,l=e.next,c=l||u,f=t&&"parentNode"===c,p=r++;return e.first?function(e,t,n){while(e=e[u])if(1===e.nodeType||f)return s(e,t,n);return!1}:function(e,t,n){var r,i,o,a=[k,p];if(n){while(e=e[u])if((1===e.nodeType||f)&&s(e,t,n))return!0}else while(e=e[u])if(1===e.nodeType||f)if(i=(o=e[S]||(e[S]={}))[e.uniqueID]||(o[e.uniqueID]={}),l&&l===e.nodeName.toLowerCase())e=e[u]||e;else{if((r=i[c])&&r[0]===k&&r[1]===p)return a[2]=r[2];if((i[c]=a)[2]=s(e,t,n))return!0}return!1}}function we(i){return 1<i.length?function(e,t,n){var r=i.length;while(r--)if(!i[r](e,t,n))return!1;return!0}:i[0]}function Te(e,t,n,r,i){for(var o,a=[],s=0,u=e.length,l=null!=t;s<u;s++)(o=e[s])&&(n&&!n(o,r,i)||(a.push(o),l&&t.push(s)));return a}function Ce(d,h,g,v,y,e){return v&&!v[S]&&(v=Ce(v)),y&&!y[S]&&(y=Ce(y,e)),le(function(e,t,n,r){var i,o,a,s=[],u=[],l=t.length,c=e||function(e,t,n){for(var r=0,i=t.length;r<i;r++)se(e,t[r],n);return n}(h||"*",n.nodeType?[n]:n,[]),f=!d||!e&&h?c:Te(c,s,d,n,r),p=g?y||(e?d:l||v)?[]:t:f;if(g&&g(f,p,n,r),v){i=Te(p,u),v(i,[],n,r),o=i.length;while(o--)(a=i[o])&&(p[u[o]]=!(f[u[o]]=a))}if(e){if(y||d){if(y){i=[],o=p.length;while(o--)(a=p[o])&&i.push(f[o]=a);y(null,p=[],i,r)}o=p.length;while(o--)(a=p[o])&&-1<(i=y?P(e,a):s[o])&&(e[i]=!(t[i]=a))}}else p=Te(p===t?p.splice(l,p.length):p),y?y(null,t,p,r):H.apply(t,p)})}function Ee(e){for(var i,t,n,r=e.length,o=b.relative[e[0].type],a=o||b.relative[" "],s=o?1:0,u=be(function(e){return e===i},a,!0),l=be(function(e){return-1<P(i,e)},a,!0),c=[function(e,t,n){var r=!o&&(n||t!==w)||((i=t).nodeType?u(e,t,n):l(e,t,n));return i=null,r}];s<r;s++)if(t=b.relative[e[s].type])c=[be(we(c),t)];else{if((t=b.filter[e[s].type].apply(null,e[s].matches))[S]){for(n=++s;n<r;n++)if(b.relative[e[n].type])break;return Ce(1<s&&we(c),1<s&&xe(e.slice(0,s-1).concat({value:" "===e[s-2].type?"*":""})).replace($,"$1"),t,s<n&&Ee(e.slice(s,n)),n<r&&Ee(e=e.slice(n)),n<r&&xe(e))}c.push(t)}return we(c)}return me.prototype=b.filters=b.pseudos,b.setFilters=new me,h=se.tokenize=function(e,t){var n,r,i,o,a,s,u,l=x[e+" "];if(l)return t?0:l.slice(0);a=e,s=[],u=b.preFilter;while(a){for(o in n&&!(r=_.exec(a))||(r&&(a=a.slice(r[0].length)||a),s.push(i=[])),n=!1,(r=z.exec(a))&&(n=r.shift(),i.push({value:n,type:r[0].replace($," ")}),a=a.slice(n.length)),b.filter)!(r=G[o].exec(a))||u[o]&&!(r=u[o](r))||(n=r.shift(),i.push({value:n,type:o,matches:r}),a=a.slice(n.length));if(!n)break}return t?a.length:a?se.error(e):x(e,s).slice(0)},f=se.compile=function(e,t){var n,v,y,m,x,r,i=[],o=[],a=A[e+" "];if(!a){t||(t=h(e)),n=t.length;while(n--)(a=Ee(t[n]))[S]?i.push(a):o.push(a);(a=A(e,(v=o,m=0<(y=i).length,x=0<v.length,r=function(e,t,n,r,i){var o,a,s,u=0,l="0",c=e&&[],f=[],p=w,d=e||x&&b.find.TAG("*",i),h=k+=null==p?1:Math.random()||.1,g=d.length;for(i&&(w=t==C||t||i);l!==g&&null!=(o=d[l]);l++){if(x&&o){a=0,t||o.ownerDocument==C||(T(o),n=!E);while(s=v[a++])if(s(o,t||C,n)){r.push(o);break}i&&(k=h)}m&&((o=!s&&o)&&u--,e&&c.push(o))}if(u+=l,m&&l!==u){a=0;while(s=y[a++])s(c,f,t,n);if(e){if(0<u)while(l--)c[l]||f[l]||(f[l]=q.call(r));f=Te(f)}H.apply(r,f),i&&!e&&0<f.length&&1<u+y.length&&se.uniqueSort(r)}return i&&(k=h,w=p),c},m?le(r):r))).selector=e}return a},g=se.select=function(e,t,n,r){var i,o,a,s,u,l="function"==typeof e&&e,c=!r&&h(e=l.selector||e);if(n=n||[],1===c.length){if(2<(o=c[0]=c[0].slice(0)).length&&"ID"===(a=o[0]).type&&9===t.nodeType&&E&&b.relative[o[1].type]){if(!(t=(b.find.ID(a.matches[0].replace(te,ne),t)||[])[0]))return n;l&&(t=t.parentNode),e=e.slice(o.shift().value.length)}i=G.needsContext.test(e)?0:o.length;while(i--){if(a=o[i],b.relative[s=a.type])break;if((u=b.find[s])&&(r=u(a.matches[0].replace(te,ne),ee.test(o[0].type)&&ye(t.parentNode)||t))){if(o.splice(i,1),!(e=r.length&&xe(o)))return H.apply(n,r),n;break}}}return(l||f(e,c))(r,t,!E,n,!t||ee.test(e)&&ye(t.parentNode)||t),n},d.sortStable=S.split("").sort(j).join("")===S,d.detectDuplicates=!!l,T(),d.sortDetached=ce(function(e){return 1&e.compareDocumentPosition(C.createElement("fieldset"))}),ce(function(e){return e.innerHTML="<a href='#'></a>","#"===e.firstChild.getAttribute("href")})||fe("type|href|height|width",function(e,t,n){if(!n)return e.getAttribute(t,"type"===t.toLowerCase()?1:2)}),d.attributes&&ce(function(e){return e.innerHTML="<input/>",e.firstChild.setAttribute("value",""),""===e.firstChild.getAttribute("value")})||fe("value",function(e,t,n){if(!n&&"input"===e.nodeName.toLowerCase())return e.defaultValue}),ce(function(e){return null==e.getAttribute("disabled")})||fe(R,function(e,t,n){var r;if(!n)return!0===e[t]?t.toLowerCase():(r=e.getAttributeNode(t))&&r.specified?r.value:null}),se}(C);S.find=d,S.expr=d.selectors,S.expr[":"]=S.expr.pseudos,S.uniqueSort=S.unique=d.uniqueSort,S.text=d.getText,S.isXMLDoc=d.isXML,S.contains=d.contains,S.escapeSelector=d.escape;var h=function(e,t,n){var r=[],i=void 0!==n;while((e=e[t])&&9!==e.nodeType)if(1===e.nodeType){if(i&&S(e).is(n))break;r.push(e)}return r},T=function(e,t){for(var n=[];e;e=e.nextSibling)1===e.nodeType&&e!==t&&n.push(e);return n},k=S.expr.match.needsContext;function A(e,t){return e.nodeName&&e.nodeName.toLowerCase()===t.toLowerCase()}var N=/^<([a-z][^\/\0>:\x20\t\r\n\f]*)[\x20\t\r\n\f]*\/?>(?:<\/\1>|)$/i;function j(e,n,r){return m(n)?S.grep(e,function(e,t){return!!n.call(e,t,e)!==r}):n.nodeType?S.grep(e,function(e){return e===n!==r}):"string"!=typeof n?S.grep(e,function(e){return-1<i.call(n,e)!==r}):S.filter(n,e,r)}S.filter=function(e,t,n){var r=t[0];return n&&(e=":not("+e+")"),1===t.length&&1===r.nodeType?S.find.matchesSelector(r,e)?[r]:[]:S.find.matches(e,S.grep(t,function(e){return 1===e.nodeType}))},S.fn.extend({find:function(e){var t,n,r=this.length,i=this;if("string"!=typeof e)return this.pushStack(S(e).filter(function(){for(t=0;t<r;t++)if(S.contains(i[t],this))return!0}));for(n=this.pushStack([]),t=0;t<r;t++)S.find(e,i[t],n);return 1<r?S.uniqueSort(n):n},filter:function(e){return this.pushStack(j(this,e||[],!1))},not:function(e){return this.pushStack(j(this,e||[],!0))},is:function(e){return!!j(this,"string"==typeof e&&k.test(e)?S(e):e||[],!1).length}});var D,q=/^(?:\s*(<[\w\W]+>)[^>]*|#([\w-]+))$/;(S.fn.init=function(e,t,n){var r,i;if(!e)return this;if(n=n||D,"string"==typeof e){if(!(r="<"===e[0]&&">"===e[e.length-1]&&3<=e.length?[null,e,null]:q.exec(e))||!r[1]&&t)return!t||t.jquery?(t||n).find(e):this.constructor(t).find(e);if(r[1]){if(t=t instanceof S?t[0]:t,S.merge(this,S.parseHTML(r[1],t&&t.nodeType?t.ownerDocument||t:E,!0)),N.test(r[1])&&S.isPlainObject(t))for(r in t)m(this[r])?this[r](t[r]):this.attr(r,t[r]);return this}return(i=E.getElementById(r[2]))&&(this[0]=i,this.length=1),this}return e.nodeType?(this[0]=e,this.length=1,this):m(e)?void 0!==n.ready?n.ready(e):e(S):S.makeArray(e,this)}).prototype=S.fn,D=S(E);var L=/^(?:parents|prev(?:Until|All))/,H={children:!0,contents:!0,next:!0,prev:!0};function O(e,t){while((e=e[t])&&1!==e.nodeType);return e}S.fn.extend({has:function(e){var t=S(e,this),n=t.length;return this.filter(function(){for(var e=0;e<n;e++)if(S.contains(this,t[e]))return!0})},closest:function(e,t){var n,r=0,i=this.length,o=[],a="string"!=typeof e&&S(e);if(!k.test(e))for(;r<i;r++)for(n=this[r];n&&n!==t;n=n.parentNode)if(n.nodeType<11&&(a?-1<a.index(n):1===n.nodeType&&S.find.matchesSelector(n,e))){o.push(n);break}return this.pushStack(1<o.length?S.uniqueSort(o):o)},index:function(e){return e?"string"==typeof e?i.call(S(e),this[0]):i.call(this,e.jquery?e[0]:e):this[0]&&this[0].parentNode?this.first().prevAll().length:-1},add:function(e,t){return this.pushStack(S.uniqueSort(S.merge(this.get(),S(e,t))))},addBack:function(e){return this.add(null==e?this.prevObject:this.prevObject.filter(e))}}),S.each({parent:function(e){var t=e.parentNode;return t&&11!==t.nodeType?t:null},parents:function(e){return h(e,"parentNode")},parentsUntil:function(e,t,n){return h(e,"parentNode",n)},next:function(e){return O(e,"nextSibling")},prev:function(e){return O(e,"previousSibling")},nextAll:function(e){return h(e,"nextSibling")},prevAll:function(e){return h(e,"previousSibling")},nextUntil:function(e,t,n){return h(e,"nextSibling",n)},prevUntil:function(e,t,n){return h(e,"previousSibling",n)},siblings:function(e){return T((e.parentNode||{}).firstChild,e)},children:function(e){return T(e.firstChild)},contents:function(e){return null!=e.contentDocument&&r(e.contentDocument)?e.contentDocument:(A(e,"template")&&(e=e.content||e),S.merge([],e.childNodes))}},function(r,i){S.fn[r]=function(e,t){var n=S.map(this,i,e);return"Until"!==r.slice(-5)&&(t=e),t&&"string"==typeof t&&(n=S.filter(t,n)),1<this.length&&(H[r]||S.uniqueSort(n),L.test(r)&&n.reverse()),this.pushStack(n)}});var P=/[^\x20\t\r\n\f]+/g;function R(e){return e}function M(e){throw e}function I(e,t,n,r){var i;try{e&&m(i=e.promise)?i.call(e).done(t).fail(n):e&&m(i=e.then)?i.call(e,t,n):t.apply(void 0,[e].slice(r))}catch(e){n.apply(void 0,[e])}}S.Callbacks=function(r){var e,n;r="string"==typeof r?(e=r,n={},S.each(e.match(P)||[],function(e,t){n[t]=!0}),n):S.extend({},r);var i,t,o,a,s=[],u=[],l=-1,c=function(){for(a=a||r.once,o=i=!0;u.length;l=-1){t=u.shift();while(++l<s.length)!1===s[l].apply(t[0],t[1])&&r.stopOnFalse&&(l=s.length,t=!1)}r.memory||(t=!1),i=!1,a&&(s=t?[]:"")},f={add:function(){return s&&(t&&!i&&(l=s.length-1,u.push(t)),function n(e){S.each(e,function(e,t){m(t)?r.unique&&f.has(t)||s.push(t):t&&t.length&&"string"!==w(t)&&n(t)})}(arguments),t&&!i&&c()),this},remove:function(){return S.each(arguments,function(e,t){var n;while(-1<(n=S.inArray(t,s,n)))s.splice(n,1),n<=l&&l--}),this},has:function(e){return e?-1<S.inArray(e,s):0<s.length},empty:function(){return s&&(s=[]),this},disable:function(){return a=u=[],s=t="",this},disabled:function(){return!s},lock:function(){return a=u=[],t||i||(s=t=""),this},locked:function(){return!!a},fireWith:function(e,t){return a||(t=[e,(t=t||[]).slice?t.slice():t],u.push(t),i||c()),this},fire:function(){return f.fireWith(this,arguments),this},fired:function(){return!!o}};return f},S.extend({Deferred:function(e){var o=[["notify","progress",S.Callbacks("memory"),S.Callbacks("memory"),2],["resolve","done",S.Callbacks("once memory"),S.Callbacks("once memory"),0,"resolved"],["reject","fail",S.Callbacks("once memory"),S.Callbacks("once memory"),1,"rejected"]],i="pending",a={state:function(){return i},always:function(){return s.done(arguments).fail(arguments),this},"catch":function(e){return a.then(null,e)},pipe:function(){var i=arguments;return S.Deferred(function(r){S.each(o,function(e,t){var n=m(i[t[4]])&&i[t[4]];s[t[1]](function(){var e=n&&n.apply(this,arguments);e&&m(e.promise)?e.promise().progress(r.notify).done(r.resolve).fail(r.reject):r[t[0]+"With"](this,n?[e]:arguments)})}),i=null}).promise()},then:function(t,n,r){var u=0;function l(i,o,a,s){return function(){var n=this,r=arguments,e=function(){var e,t;if(!(i<u)){if((e=a.apply(n,r))===o.promise())throw new TypeError("Thenable self-resolution");t=e&&("object"==typeof e||"function"==typeof e)&&e.then,m(t)?s?t.call(e,l(u,o,R,s),l(u,o,M,s)):(u++,t.call(e,l(u,o,R,s),l(u,o,M,s),l(u,o,R,o.notifyWith))):(a!==R&&(n=void 0,r=[e]),(s||o.resolveWith)(n,r))}},t=s?e:function(){try{e()}catch(e){S.Deferred.exceptionHook&&S.Deferred.exceptionHook(e,t.stackTrace),u<=i+1&&(a!==M&&(n=void 0,r=[e]),o.rejectWith(n,r))}};i?t():(S.Deferred.getStackHook&&(t.stackTrace=S.Deferred.getStackHook()),C.setTimeout(t))}}return S.Deferred(function(e){o[0][3].add(l(0,e,m(r)?r:R,e.notifyWith)),o[1][3].add(l(0,e,m(t)?t:R)),o[2][3].add(l(0,e,m(n)?n:M))}).promise()},promise:function(e){return null!=e?S.extend(e,a):a}},s={};return S.each(o,function(e,t){var n=t[2],r=t[5];a[t[1]]=n.add,r&&n.add(function(){i=r},o[3-e][2].disable,o[3-e][3].disable,o[0][2].lock,o[0][3].lock),n.add(t[3].fire),s[t[0]]=function(){return s[t[0]+"With"](this===s?void 0:this,arguments),this},s[t[0]+"With"]=n.fireWith}),a.promise(s),e&&e.call(s,s),s},when:function(e){var n=arguments.length,t=n,r=Array(t),i=s.call(arguments),o=S.Deferred(),a=function(t){return function(e){r[t]=this,i[t]=1<arguments.length?s.call(arguments):e,--n||o.resolveWith(r,i)}};if(n<=1&&(I(e,o.done(a(t)).resolve,o.reject,!n),"pending"===o.state()||m(i[t]&&i[t].then)))return o.then();while(t--)I(i[t],a(t),o.reject);return o.promise()}});var W=/^(Eval|Internal|Range|Reference|Syntax|Type|URI)Error$/;S.Deferred.exceptionHook=function(e,t){C.console&&C.console.warn&&e&&W.test(e.name)&&C.console.warn("jQuery.Deferred exception: "+e.message,e.stack,t)},S.readyException=function(e){C.setTimeout(function(){throw e})};var F=S.Deferred();function B(){E.removeEventListener("DOMContentLoaded",B),C.removeEventListener("load",B),S.ready()}S.fn.ready=function(e){return F.then(e)["catch"](function(e){S.readyException(e)}),this},S.extend({isReady:!1,readyWait:1,ready:function(e){(!0===e?--S.readyWait:S.isReady)||(S.isReady=!0)!==e&&0<--S.readyWait||F.resolveWith(E,[S])}}),S.ready.then=F.then,"complete"===E.readyState||"loading"!==E.readyState&&!E.documentElement.doScroll?C.setTimeout(S.ready):(E.addEventListener("DOMContentLoaded",B),C.addEventListener("load",B));var $=function(e,t,n,r,i,o,a){var s=0,u=e.length,l=null==n;if("object"===w(n))for(s in i=!0,n)$(e,t,s,n[s],!0,o,a);else if(void 0!==r&&(i=!0,m(r)||(a=!0),l&&(a?(t.call(e,r),t=null):(l=t,t=function(e,t,n){return l.call(S(e),n)})),t))for(;s<u;s++)t(e[s],n,a?r:r.call(e[s],s,t(e[s],n)));return i?e:l?t.call(e):u?t(e[0],n):o},_=/^-ms-/,z=/-([a-z])/g;function U(e,t){return t.toUpperCase()}function X(e){return e.replace(_,"ms-").replace(z,U)}var V=function(e){return 1===e.nodeType||9===e.nodeType||!+e.nodeType};function G(){this.expando=S.expando+G.uid++}G.uid=1,G.prototype={cache:function(e){var t=e[this.expando];return t||(t={},V(e)&&(e.nodeType?e[this.expando]=t:Object.defineProperty(e,this.expando,{value:t,configurable:!0}))),t},set:function(e,t,n){var r,i=this.cache(e);if("string"==typeof t)i[X(t)]=n;else for(r in t)i[X(r)]=t[r];return i},get:function(e,t){return void 0===t?this.cache(e):e[this.expando]&&e[this.expando][X(t)]},access:function(e,t,n){return void 0===t||t&&"string"==typeof t&&void 0===n?this.get(e,t):(this.set(e,t,n),void 0!==n?n:t)},remove:function(e,t){var n,r=e[this.expando];if(void 0!==r){if(void 0!==t){n=(t=Array.isArray(t)?t.map(X):(t=X(t))in r?[t]:t.match(P)||[]).length;while(n--)delete r[t[n]]}(void 0===t||S.isEmptyObject(r))&&(e.nodeType?e[this.expando]=void 0:delete e[this.expando])}},hasData:function(e){var t=e[this.expando];return void 0!==t&&!S.isEmptyObject(t)}};var Y=new G,Q=new G,J=/^(?:\{[\w\W]*\}|\[[\w\W]*\])$/,K=/[A-Z]/g;function Z(e,t,n){var r,i;if(void 0===n&&1===e.nodeType)if(r="data-"+t.replace(K,"-$&").toLowerCase(),"string"==typeof(n=e.getAttribute(r))){try{n="true"===(i=n)||"false"!==i&&("null"===i?null:i===+i+""?+i:J.test(i)?JSON.parse(i):i)}catch(e){}Q.set(e,t,n)}else n=void 0;return n}S.extend({hasData:function(e){return Q.hasData(e)||Y.hasData(e)},data:function(e,t,n){return Q.access(e,t,n)},removeData:function(e,t){Q.remove(e,t)},_data:function(e,t,n){return Y.access(e,t,n)},_removeData:function(e,t){Y.remove(e,t)}}),S.fn.extend({data:function(n,e){var t,r,i,o=this[0],a=o&&o.attributes;if(void 0===n){if(this.length&&(i=Q.get(o),1===o.nodeType&&!Y.get(o,"hasDataAttrs"))){t=a.length;while(t--)a[t]&&0===(r=a[t].name).indexOf("data-")&&(r=X(r.slice(5)),Z(o,r,i[r]));Y.set(o,"hasDataAttrs",!0)}return i}return"object"==typeof n?this.each(function(){Q.set(this,n)}):$(this,function(e){var t;if(o&&void 0===e)return void 0!==(t=Q.get(o,n))?t:void 0!==(t=Z(o,n))?t:void 0;this.each(function(){Q.set(this,n,e)})},null,e,1<arguments.length,null,!0)},removeData:function(e){return this.each(function(){Q.remove(this,e)})}}),S.extend({queue:function(e,t,n){var r;if(e)return t=(t||"fx")+"queue",r=Y.get(e,t),n&&(!r||Array.isArray(n)?r=Y.access(e,t,S.makeArray(n)):r.push(n)),r||[]},dequeue:function(e,t){t=t||"fx";var n=S.queue(e,t),r=n.length,i=n.shift(),o=S._queueHooks(e,t);"inprogress"===i&&(i=n.shift(),r--),i&&("fx"===t&&n.unshift("inprogress"),delete o.stop,i.call(e,function(){S.dequeue(e,t)},o)),!r&&o&&o.empty.fire()},_queueHooks:function(e,t){var n=t+"queueHooks";return Y.get(e,n)||Y.access(e,n,{empty:S.Callbacks("once memory").add(function(){Y.remove(e,[t+"queue",n])})})}}),S.fn.extend({queue:function(t,n){var e=2;return"string"!=typeof t&&(n=t,t="fx",e--),arguments.length<e?S.queue(this[0],t):void 0===n?this:this.each(function(){var e=S.queue(this,t,n);S._queueHooks(this,t),"fx"===t&&"inprogress"!==e[0]&&S.dequeue(this,t)})},dequeue:function(e){return this.each(function(){S.dequeue(this,e)})},clearQueue:function(e){return this.queue(e||"fx",[])},promise:function(e,t){var n,r=1,i=S.Deferred(),o=this,a=this.length,s=function(){--r||i.resolveWith(o,[o])};"string"!=typeof e&&(t=e,e=void 0),e=e||"fx";while(a--)(n=Y.get(o[a],e+"queueHooks"))&&n.empty&&(r++,n.empty.add(s));return s(),i.promise(t)}});var ee=/[+-]?(?:\d*\.|)\d+(?:[eE][+-]?\d+|)/.source,te=new RegExp("^(?:([+-])=|)("+ee+")([a-z%]*)$","i"),ne=["Top","Right","Bottom","Left"],re=E.documentElement,ie=function(e){return S.contains(e.ownerDocument,e)},oe={composed:!0};re.getRootNode&&(ie=function(e){return S.contains(e.ownerDocument,e)||e.getRootNode(oe)===e.ownerDocument});var ae=function(e,t){return"none"===(e=t||e).style.display||""===e.style.display&&ie(e)&&"none"===S.css(e,"display")};function se(e,t,n,r){var i,o,a=20,s=r?function(){return r.cur()}:function(){return S.css(e,t,"")},u=s(),l=n&&n[3]||(S.cssNumber[t]?"":"px"),c=e.nodeType&&(S.cssNumber[t]||"px"!==l&&+u)&&te.exec(S.css(e,t));if(c&&c[3]!==l){u/=2,l=l||c[3],c=+u||1;while(a--)S.style(e,t,c+l),(1-o)*(1-(o=s()/u||.5))<=0&&(a=0),c/=o;c*=2,S.style(e,t,c+l),n=n||[]}return n&&(c=+c||+u||0,i=n[1]?c+(n[1]+1)*n[2]:+n[2],r&&(r.unit=l,r.start=c,r.end=i)),i}var ue={};function le(e,t){for(var n,r,i,o,a,s,u,l=[],c=0,f=e.length;c<f;c++)(r=e[c]).style&&(n=r.style.display,t?("none"===n&&(l[c]=Y.get(r,"display")||null,l[c]||(r.style.display="")),""===r.style.display&&ae(r)&&(l[c]=(u=a=o=void 0,a=(i=r).ownerDocument,s=i.nodeName,(u=ue[s])||(o=a.body.appendChild(a.createElement(s)),u=S.css(o,"display"),o.parentNode.removeChild(o),"none"===u&&(u="block"),ue[s]=u)))):"none"!==n&&(l[c]="none",Y.set(r,"display",n)));for(c=0;c<f;c++)null!=l[c]&&(e[c].style.display=l[c]);return e}S.fn.extend({show:function(){return le(this,!0)},hide:function(){return le(this)},toggle:function(e){return"boolean"==typeof e?e?this.show():this.hide():this.each(function(){ae(this)?S(this).show():S(this).hide()})}});var ce,fe,pe=/^(?:checkbox|radio)$/i,de=/<([a-z][^\/\0>\x20\t\r\n\f]*)/i,he=/^$|^module$|\/(?:java|ecma)script/i;ce=E.createDocumentFragment().appendChild(E.createElement("div")),(fe=E.createElement("input")).setAttribute("type","radio"),fe.setAttribute("checked","checked"),fe.setAttribute("name","t"),ce.appendChild(fe),y.checkClone=ce.cloneNode(!0).cloneNode(!0).lastChild.checked,ce.innerHTML="<textarea>x</textarea>",y.noCloneChecked=!!ce.cloneNode(!0).lastChild.defaultValue,ce.innerHTML="<option></option>",y.option=!!ce.lastChild;var ge={thead:[1,"<table>","</table>"],col:[2,"<table><colgroup>","</colgroup></table>"],tr:[2,"<table><tbody>","</tbody></table>"],td:[3,"<table><tbody><tr>","</tr></tbody></table>"],_default:[0,"",""]};function ve(e,t){var n;return n="undefined"!=typeof e.getElementsByTagName?e.getElementsByTagName(t||"*"):"undefined"!=typeof e.querySelectorAll?e.querySelectorAll(t||"*"):[],void 0===t||t&&A(e,t)?S.merge([e],n):n}function ye(e,t){for(var n=0,r=e.length;n<r;n++)Y.set(e[n],"globalEval",!t||Y.get(t[n],"globalEval"))}ge.tbody=ge.tfoot=ge.colgroup=ge.caption=ge.thead,ge.th=ge.td,y.option||(ge.optgroup=ge.option=[1,"<select multiple='multiple'>","</select>"]);var me=/<|&#?\w+;/;function xe(e,t,n,r,i){for(var o,a,s,u,l,c,f=t.createDocumentFragment(),p=[],d=0,h=e.length;d<h;d++)if((o=e[d])||0===o)if("object"===w(o))S.merge(p,o.nodeType?[o]:o);else if(me.test(o)){a=a||f.appendChild(t.createElement("div")),s=(de.exec(o)||["",""])[1].toLowerCase(),u=ge[s]||ge._default,a.innerHTML=u[1]+S.htmlPrefilter(o)+u[2],c=u[0];while(c--)a=a.lastChild;S.merge(p,a.childNodes),(a=f.firstChild).textContent=""}else p.push(t.createTextNode(o));f.textContent="",d=0;while(o=p[d++])if(r&&-1<S.inArray(o,r))i&&i.push(o);else if(l=ie(o),a=ve(f.appendChild(o),"script"),l&&ye(a),n){c=0;while(o=a[c++])he.test(o.type||"")&&n.push(o)}return f}var be=/^([^.]*)(?:\.(.+)|)/;function we(){return!0}function Te(){return!1}function Ce(e,t){return e===function(){try{return E.activeElement}catch(e){}}()==("focus"===t)}function Ee(e,t,n,r,i,o){var a,s;if("object"==typeof t){for(s in"string"!=typeof n&&(r=r||n,n=void 0),t)Ee(e,s,n,r,t[s],o);return e}if(null==r&&null==i?(i=n,r=n=void 0):null==i&&("string"==typeof n?(i=r,r=void 0):(i=r,r=n,n=void 0)),!1===i)i=Te;else if(!i)return e;return 1===o&&(a=i,(i=function(e){return S().off(e),a.apply(this,arguments)}).guid=a.guid||(a.guid=S.guid++)),e.each(function(){S.event.add(this,t,i,r,n)})}function Se(e,i,o){o?(Y.set(e,i,!1),S.event.add(e,i,{namespace:!1,handler:function(e){var t,n,r=Y.get(this,i);if(1&e.isTrigger&&this[i]){if(r.length)(S.event.special[i]||{}).delegateType&&e.stopPropagation();else if(r=s.call(arguments),Y.set(this,i,r),t=o(this,i),this[i](),r!==(n=Y.get(this,i))||t?Y.set(this,i,!1):n={},r!==n)return e.stopImmediatePropagation(),e.preventDefault(),n&&n.value}else r.length&&(Y.set(this,i,{value:S.event.trigger(S.extend(r[0],S.Event.prototype),r.slice(1),this)}),e.stopImmediatePropagation())}})):void 0===Y.get(e,i)&&S.event.add(e,i,we)}S.event={global:{},add:function(t,e,n,r,i){var o,a,s,u,l,c,f,p,d,h,g,v=Y.get(t);if(V(t)){n.handler&&(n=(o=n).handler,i=o.selector),i&&S.find.matchesSelector(re,i),n.guid||(n.guid=S.guid++),(u=v.events)||(u=v.events=Object.create(null)),(a=v.handle)||(a=v.handle=function(e){return"undefined"!=typeof S&&S.event.triggered!==e.type?S.event.dispatch.apply(t,arguments):void 0}),l=(e=(e||"").match(P)||[""]).length;while(l--)d=g=(s=be.exec(e[l])||[])[1],h=(s[2]||"").split(".").sort(),d&&(f=S.event.special[d]||{},d=(i?f.delegateType:f.bindType)||d,f=S.event.special[d]||{},c=S.extend({type:d,origType:g,data:r,handler:n,guid:n.guid,selector:i,needsContext:i&&S.expr.match.needsContext.test(i),namespace:h.join(".")},o),(p=u[d])||((p=u[d]=[]).delegateCount=0,f.setup&&!1!==f.setup.call(t,r,h,a)||t.addEventListener&&t.addEventListener(d,a)),f.add&&(f.add.call(t,c),c.handler.guid||(c.handler.guid=n.guid)),i?p.splice(p.delegateCount++,0,c):p.push(c),S.event.global[d]=!0)}},remove:function(e,t,n,r,i){var o,a,s,u,l,c,f,p,d,h,g,v=Y.hasData(e)&&Y.get(e);if(v&&(u=v.events)){l=(t=(t||"").match(P)||[""]).length;while(l--)if(d=g=(s=be.exec(t[l])||[])[1],h=(s[2]||"").split(".").sort(),d){f=S.event.special[d]||{},p=u[d=(r?f.delegateType:f.bindType)||d]||[],s=s[2]&&new RegExp("(^|\\.)"+h.join("\\.(?:.*\\.|)")+"(\\.|$)"),a=o=p.length;while(o--)c=p[o],!i&&g!==c.origType||n&&n.guid!==c.guid||s&&!s.test(c.namespace)||r&&r!==c.selector&&("**"!==r||!c.selector)||(p.splice(o,1),c.selector&&p.delegateCount--,f.remove&&f.remove.call(e,c));a&&!p.length&&(f.teardown&&!1!==f.teardown.call(e,h,v.handle)||S.removeEvent(e,d,v.handle),delete u[d])}else for(d in u)S.event.remove(e,d+t[l],n,r,!0);S.isEmptyObject(u)&&Y.remove(e,"handle events")}},dispatch:function(e){var t,n,r,i,o,a,s=new Array(arguments.length),u=S.event.fix(e),l=(Y.get(this,"events")||Object.create(null))[u.type]||[],c=S.event.special[u.type]||{};for(s[0]=u,t=1;t<arguments.length;t++)s[t]=arguments[t];if(u.delegateTarget=this,!c.preDispatch||!1!==c.preDispatch.call(this,u)){a=S.event.handlers.call(this,u,l),t=0;while((i=a[t++])&&!u.isPropagationStopped()){u.currentTarget=i.elem,n=0;while((o=i.handlers[n++])&&!u.isImmediatePropagationStopped())u.rnamespace&&!1!==o.namespace&&!u.rnamespace.test(o.namespace)||(u.handleObj=o,u.data=o.data,void 0!==(r=((S.event.special[o.origType]||{}).handle||o.handler).apply(i.elem,s))&&!1===(u.result=r)&&(u.preventDefault(),u.stopPropagation()))}return c.postDispatch&&c.postDispatch.call(this,u),u.result}},handlers:function(e,t){var n,r,i,o,a,s=[],u=t.delegateCount,l=e.target;if(u&&l.nodeType&&!("click"===e.type&&1<=e.button))for(;l!==this;l=l.parentNode||this)if(1===l.nodeType&&("click"!==e.type||!0!==l.disabled)){for(o=[],a={},n=0;n<u;n++)void 0===a[i=(r=t[n]).selector+" "]&&(a[i]=r.needsContext?-1<S(i,this).index(l):S.find(i,this,null,[l]).length),a[i]&&o.push(r);o.length&&s.push({elem:l,handlers:o})}return l=this,u<t.length&&s.push({elem:l,handlers:t.slice(u)}),s},addProp:function(t,e){Object.defineProperty(S.Event.prototype,t,{enumerable:!0,configurable:!0,get:m(e)?function(){if(this.originalEvent)return e(this.originalEvent)}:function(){if(this.originalEvent)return this.originalEvent[t]},set:function(e){Object.defineProperty(this,t,{enumerable:!0,configurable:!0,writable:!0,value:e})}})},fix:function(e){return e[S.expando]?e:new S.Event(e)},special:{load:{noBubble:!0},click:{setup:function(e){var t=this||e;return pe.test(t.type)&&t.click&&A(t,"input")&&Se(t,"click",we),!1},trigger:function(e){var t=this||e;return pe.test(t.type)&&t.click&&A(t,"input")&&Se(t,"click"),!0},_default:function(e){var t=e.target;return pe.test(t.type)&&t.click&&A(t,"input")&&Y.get(t,"click")||A(t,"a")}},beforeunload:{postDispatch:function(e){void 0!==e.result&&e.originalEvent&&(e.originalEvent.returnValue=e.result)}}}},S.removeEvent=function(e,t,n){e.removeEventListener&&e.removeEventListener(t,n)},S.Event=function(e,t){if(!(this instanceof S.Event))return new S.Event(e,t);e&&e.type?(this.originalEvent=e,this.type=e.type,this.isDefaultPrevented=e.defaultPrevented||void 0===e.defaultPrevented&&!1===e.returnValue?we:Te,this.target=e.target&&3===e.target.nodeType?e.target.parentNode:e.target,this.currentTarget=e.currentTarget,this.relatedTarget=e.relatedTarget):this.type=e,t&&S.extend(this,t),this.timeStamp=e&&e.timeStamp||Date.now(),this[S.expando]=!0},S.Event.prototype={constructor:S.Event,isDefaultPrevented:Te,isPropagationStopped:Te,isImmediatePropagationStopped:Te,isSimulated:!1,preventDefault:function(){var e=this.originalEvent;this.isDefaultPrevented=we,e&&!this.isSimulated&&e.preventDefault()},stopPropagation:function(){var e=this.originalEvent;this.isPropagationStopped=we,e&&!this.isSimulated&&e.stopPropagation()},stopImmediatePropagation:function(){var e=this.originalEvent;this.isImmediatePropagationStopped=we,e&&!this.isSimulated&&e.stopImmediatePropagation(),this.stopPropagation()}},S.each({altKey:!0,bubbles:!0,cancelable:!0,changedTouches:!0,ctrlKey:!0,detail:!0,eventPhase:!0,metaKey:!0,pageX:!0,pageY:!0,shiftKey:!0,view:!0,"char":!0,code:!0,charCode:!0,key:!0,keyCode:!0,button:!0,buttons:!0,clientX:!0,clientY:!0,offsetX:!0,offsetY:!0,pointerId:!0,pointerType:!0,screenX:!0,screenY:!0,targetTouches:!0,toElement:!0,touches:!0,which:!0},S.event.addProp),S.each({focus:"focusin",blur:"focusout"},function(e,t){S.event.special[e]={setup:function(){return Se(this,e,Ce),!1},trigger:function(){return Se(this,e),!0},_default:function(){return!0},delegateType:t}}),S.each({mouseenter:"mouseover",mouseleave:"mouseout",pointerenter:"pointerover",pointerleave:"pointerout"},function(e,i){S.event.special[e]={delegateType:i,bindType:i,handle:function(e){var t,n=e.relatedTarget,r=e.handleObj;return n&&(n===this||S.contains(this,n))||(e.type=r.origType,t=r.handler.apply(this,arguments),e.type=i),t}}}),S.fn.extend({on:function(e,t,n,r){return Ee(this,e,t,n,r)},one:function(e,t,n,r){return Ee(this,e,t,n,r,1)},off:function(e,t,n){var r,i;if(e&&e.preventDefault&&e.handleObj)return r=e.handleObj,S(e.delegateTarget).off(r.namespace?r.origType+"."+r.namespace:r.origType,r.selector,r.handler),this;if("object"==typeof e){for(i in e)this.off(i,t,e[i]);return this}return!1!==t&&"function"!=typeof t||(n=t,t=void 0),!1===n&&(n=Te),this.each(function(){S.event.remove(this,e,n,t)})}});var ke=/<script|<style|<link/i,Ae=/checked\s*(?:[^=]|=\s*.checked.)/i,Ne=/^\s*<!(?:\[CDATA\[|--)|(?:\]\]|--)>\s*$/g;function je(e,t){return A(e,"table")&&A(11!==t.nodeType?t:t.firstChild,"tr")&&S(e).children("tbody")[0]||e}function De(e){return e.type=(null!==e.getAttribute("type"))+"/"+e.type,e}function qe(e){return"true/"===(e.type||"").slice(0,5)?e.type=e.type.slice(5):e.removeAttribute("type"),e}function Le(e,t){var n,r,i,o,a,s;if(1===t.nodeType){if(Y.hasData(e)&&(s=Y.get(e).events))for(i in Y.remove(t,"handle events"),s)for(n=0,r=s[i].length;n<r;n++)S.event.add(t,i,s[i][n]);Q.hasData(e)&&(o=Q.access(e),a=S.extend({},o),Q.set(t,a))}}function He(n,r,i,o){r=g(r);var e,t,a,s,u,l,c=0,f=n.length,p=f-1,d=r[0],h=m(d);if(h||1<f&&"string"==typeof d&&!y.checkClone&&Ae.test(d))return n.each(function(e){var t=n.eq(e);h&&(r[0]=d.call(this,e,t.html())),He(t,r,i,o)});if(f&&(t=(e=xe(r,n[0].ownerDocument,!1,n,o)).firstChild,1===e.childNodes.length&&(e=t),t||o)){for(s=(a=S.map(ve(e,"script"),De)).length;c<f;c++)u=e,c!==p&&(u=S.clone(u,!0,!0),s&&S.merge(a,ve(u,"script"))),i.call(n[c],u,c);if(s)for(l=a[a.length-1].ownerDocument,S.map(a,qe),c=0;c<s;c++)u=a[c],he.test(u.type||"")&&!Y.access(u,"globalEval")&&S.contains(l,u)&&(u.src&&"module"!==(u.type||"").toLowerCase()?S._evalUrl&&!u.noModule&&S._evalUrl(u.src,{nonce:u.nonce||u.getAttribute("nonce")},l):b(u.textContent.replace(Ne,""),u,l))}return n}function Oe(e,t,n){for(var r,i=t?S.filter(t,e):e,o=0;null!=(r=i[o]);o++)n||1!==r.nodeType||S.cleanData(ve(r)),r.parentNode&&(n&&ie(r)&&ye(ve(r,"script")),r.parentNode.removeChild(r));return e}S.extend({htmlPrefilter:function(e){return e},clone:function(e,t,n){var r,i,o,a,s,u,l,c=e.cloneNode(!0),f=ie(e);if(!(y.noCloneChecked||1!==e.nodeType&&11!==e.nodeType||S.isXMLDoc(e)))for(a=ve(c),r=0,i=(o=ve(e)).length;r<i;r++)s=o[r],u=a[r],void 0,"input"===(l=u.nodeName.toLowerCase())&&pe.test(s.type)?u.checked=s.checked:"input"!==l&&"textarea"!==l||(u.defaultValue=s.defaultValue);if(t)if(n)for(o=o||ve(e),a=a||ve(c),r=0,i=o.length;r<i;r++)Le(o[r],a[r]);else Le(e,c);return 0<(a=ve(c,"script")).length&&ye(a,!f&&ve(e,"script")),c},cleanData:function(e){for(var t,n,r,i=S.event.special,o=0;void 0!==(n=e[o]);o++)if(V(n)){if(t=n[Y.expando]){if(t.events)for(r in t.events)i[r]?S.event.remove(n,r):S.removeEvent(n,r,t.handle);n[Y.expando]=void 0}n[Q.expando]&&(n[Q.expando]=void 0)}}}),S.fn.extend({detach:function(e){return Oe(this,e,!0)},remove:function(e){return Oe(this,e)},text:function(e){return $(this,function(e){return void 0===e?S.text(this):this.empty().each(function(){1!==this.nodeType&&11!==this.nodeType&&9!==this.nodeType||(this.textContent=e)})},null,e,arguments.length)},append:function(){return He(this,arguments,function(e){1!==this.nodeType&&11!==this.nodeType&&9!==this.nodeType||je(this,e).appendChild(e)})},prepend:function(){return He(this,arguments,function(e){if(1===this.nodeType||11===this.nodeType||9===this.nodeType){var t=je(this,e);t.insertBefore(e,t.firstChild)}})},before:function(){return He(this,arguments,function(e){this.parentNode&&this.parentNode.insertBefore(e,this)})},after:function(){return He(this,arguments,function(e){this.parentNode&&this.parentNode.insertBefore(e,this.nextSibling)})},empty:function(){for(var e,t=0;null!=(e=this[t]);t++)1===e.nodeType&&(S.cleanData(ve(e,!1)),e.textContent="");return this},clone:function(e,t){return e=null!=e&&e,t=null==t?e:t,this.map(function(){return S.clone(this,e,t)})},html:function(e){return $(this,function(e){var t=this[0]||{},n=0,r=this.length;if(void 0===e&&1===t.nodeType)return t.innerHTML;if("string"==typeof e&&!ke.test(e)&&!ge[(de.exec(e)||["",""])[1].toLowerCase()]){e=S.htmlPrefilter(e);try{for(;n<r;n++)1===(t=this[n]||{}).nodeType&&(S.cleanData(ve(t,!1)),t.innerHTML=e);t=0}catch(e){}}t&&this.empty().append(e)},null,e,arguments.length)},replaceWith:function(){var n=[];return He(this,arguments,function(e){var t=this.parentNode;S.inArray(this,n)<0&&(S.cleanData(ve(this)),t&&t.replaceChild(e,this))},n)}}),S.each({appendTo:"append",prependTo:"prepend",insertBefore:"before",insertAfter:"after",replaceAll:"replaceWith"},function(e,a){S.fn[e]=function(e){for(var t,n=[],r=S(e),i=r.length-1,o=0;o<=i;o++)t=o===i?this:this.clone(!0),S(r[o])[a](t),u.apply(n,t.get());return this.pushStack(n)}});var Pe=new RegExp("^("+ee+")(?!px)[a-z%]+$","i"),Re=function(e){var t=e.ownerDocument.defaultView;return t&&t.opener||(t=C),t.getComputedStyle(e)},Me=function(e,t,n){var r,i,o={};for(i in t)o[i]=e.style[i],e.style[i]=t[i];for(i in r=n.call(e),t)e.style[i]=o[i];return r},Ie=new RegExp(ne.join("|"),"i");function We(e,t,n){var r,i,o,a,s=e.style;return(n=n||Re(e))&&(""!==(a=n.getPropertyValue(t)||n[t])||ie(e)||(a=S.style(e,t)),!y.pixelBoxStyles()&&Pe.test(a)&&Ie.test(t)&&(r=s.width,i=s.minWidth,o=s.maxWidth,s.minWidth=s.maxWidth=s.width=a,a=n.width,s.width=r,s.minWidth=i,s.maxWidth=o)),void 0!==a?a+"":a}function Fe(e,t){return{get:function(){if(!e())return(this.get=t).apply(this,arguments);delete this.get}}}!function(){function e(){if(l){u.style.cssText="position:absolute;left:-11111px;width:60px;margin-top:1px;padding:0;border:0",l.style.cssText="position:relative;display:block;box-sizing:border-box;overflow:scroll;margin:auto;border:1px;padding:1px;width:60%;top:1%",re.appendChild(u).appendChild(l);var e=C.getComputedStyle(l);n="1%"!==e.top,s=12===t(e.marginLeft),l.style.right="60%",o=36===t(e.right),r=36===t(e.width),l.style.position="absolute",i=12===t(l.offsetWidth/3),re.removeChild(u),l=null}}function t(e){return Math.round(parseFloat(e))}var n,r,i,o,a,s,u=E.createElement("div"),l=E.createElement("div");l.style&&(l.style.backgroundClip="content-box",l.cloneNode(!0).style.backgroundClip="",y.clearCloneStyle="content-box"===l.style.backgroundClip,S.extend(y,{boxSizingReliable:function(){return e(),r},pixelBoxStyles:function(){return e(),o},pixelPosition:function(){return e(),n},reliableMarginLeft:function(){return e(),s},scrollboxSize:function(){return e(),i},reliableTrDimensions:function(){var e,t,n,r;return null==a&&(e=E.createElement("table"),t=E.createElement("tr"),n=E.createElement("div"),e.style.cssText="position:absolute;left:-11111px;border-collapse:separate",t.style.cssText="border:1px solid",t.style.height="1px",n.style.height="9px",n.style.display="block",re.appendChild(e).appendChild(t).appendChild(n),r=C.getComputedStyle(t),a=parseInt(r.height,10)+parseInt(r.borderTopWidth,10)+parseInt(r.borderBottomWidth,10)===t.offsetHeight,re.removeChild(e)),a}}))}();var Be=["Webkit","Moz","ms"],$e=E.createElement("div").style,_e={};function ze(e){var t=S.cssProps[e]||_e[e];return t||(e in $e?e:_e[e]=function(e){var t=e[0].toUpperCase()+e.slice(1),n=Be.length;while(n--)if((e=Be[n]+t)in $e)return e}(e)||e)}var Ue=/^(none|table(?!-c[ea]).+)/,Xe=/^--/,Ve={position:"absolute",visibility:"hidden",display:"block"},Ge={letterSpacing:"0",fontWeight:"400"};function Ye(e,t,n){var r=te.exec(t);return r?Math.max(0,r[2]-(n||0))+(r[3]||"px"):t}function Qe(e,t,n,r,i,o){var a="width"===t?1:0,s=0,u=0;if(n===(r?"border":"content"))return 0;for(;a<4;a+=2)"margin"===n&&(u+=S.css(e,n+ne[a],!0,i)),r?("content"===n&&(u-=S.css(e,"padding"+ne[a],!0,i)),"margin"!==n&&(u-=S.css(e,"border"+ne[a]+"Width",!0,i))):(u+=S.css(e,"padding"+ne[a],!0,i),"padding"!==n?u+=S.css(e,"border"+ne[a]+"Width",!0,i):s+=S.css(e,"border"+ne[a]+"Width",!0,i));return!r&&0<=o&&(u+=Math.max(0,Math.ceil(e["offset"+t[0].toUpperCase()+t.slice(1)]-o-u-s-.5))||0),u}function Je(e,t,n){var r=Re(e),i=(!y.boxSizingReliable()||n)&&"border-box"===S.css(e,"boxSizing",!1,r),o=i,a=We(e,t,r),s="offset"+t[0].toUpperCase()+t.slice(1);if(Pe.test(a)){if(!n)return a;a="auto"}return(!y.boxSizingReliable()&&i||!y.reliableTrDimensions()&&A(e,"tr")||"auto"===a||!parseFloat(a)&&"inline"===S.css(e,"display",!1,r))&&e.getClientRects().length&&(i="border-box"===S.css(e,"boxSizing",!1,r),(o=s in e)&&(a=e[s])),(a=parseFloat(a)||0)+Qe(e,t,n||(i?"border":"content"),o,r,a)+"px"}function Ke(e,t,n,r,i){return new Ke.prototype.init(e,t,n,r,i)}S.extend({cssHooks:{opacity:{get:function(e,t){if(t){var n=We(e,"opacity");return""===n?"1":n}}}},cssNumber:{animationIterationCount:!0,columnCount:!0,fillOpacity:!0,flexGrow:!0,flexShrink:!0,fontWeight:!0,gridArea:!0,gridColumn:!0,gridColumnEnd:!0,gridColumnStart:!0,gridRow:!0,gridRowEnd:!0,gridRowStart:!0,lineHeight:!0,opacity:!0,order:!0,orphans:!0,widows:!0,zIndex:!0,zoom:!0},cssProps:{},style:function(e,t,n,r){if(e&&3!==e.nodeType&&8!==e.nodeType&&e.style){var i,o,a,s=X(t),u=Xe.test(t),l=e.style;if(u||(t=ze(s)),a=S.cssHooks[t]||S.cssHooks[s],void 0===n)return a&&"get"in a&&void 0!==(i=a.get(e,!1,r))?i:l[t];"string"===(o=typeof n)&&(i=te.exec(n))&&i[1]&&(n=se(e,t,i),o="number"),null!=n&&n==n&&("number"!==o||u||(n+=i&&i[3]||(S.cssNumber[s]?"":"px")),y.clearCloneStyle||""!==n||0!==t.indexOf("background")||(l[t]="inherit"),a&&"set"in a&&void 0===(n=a.set(e,n,r))||(u?l.setProperty(t,n):l[t]=n))}},css:function(e,t,n,r){var i,o,a,s=X(t);return Xe.test(t)||(t=ze(s)),(a=S.cssHooks[t]||S.cssHooks[s])&&"get"in a&&(i=a.get(e,!0,n)),void 0===i&&(i=We(e,t,r)),"normal"===i&&t in Ge&&(i=Ge[t]),""===n||n?(o=parseFloat(i),!0===n||isFinite(o)?o||0:i):i}}),S.each(["height","width"],function(e,u){S.cssHooks[u]={get:function(e,t,n){if(t)return!Ue.test(S.css(e,"display"))||e.getClientRects().length&&e.getBoundingClientRect().width?Je(e,u,n):Me(e,Ve,function(){return Je(e,u,n)})},set:function(e,t,n){var r,i=Re(e),o=!y.scrollboxSize()&&"absolute"===i.position,a=(o||n)&&"border-box"===S.css(e,"boxSizing",!1,i),s=n?Qe(e,u,n,a,i):0;return a&&o&&(s-=Math.ceil(e["offset"+u[0].toUpperCase()+u.slice(1)]-parseFloat(i[u])-Qe(e,u,"border",!1,i)-.5)),s&&(r=te.exec(t))&&"px"!==(r[3]||"px")&&(e.style[u]=t,t=S.css(e,u)),Ye(0,t,s)}}}),S.cssHooks.marginLeft=Fe(y.reliableMarginLeft,function(e,t){if(t)return(parseFloat(We(e,"marginLeft"))||e.getBoundingClientRect().left-Me(e,{marginLeft:0},function(){return e.getBoundingClientRect().left}))+"px"}),S.each({margin:"",padding:"",border:"Width"},function(i,o){S.cssHooks[i+o]={expand:function(e){for(var t=0,n={},r="string"==typeof e?e.split(" "):[e];t<4;t++)n[i+ne[t]+o]=r[t]||r[t-2]||r[0];return n}},"margin"!==i&&(S.cssHooks[i+o].set=Ye)}),S.fn.extend({css:function(e,t){return $(this,function(e,t,n){var r,i,o={},a=0;if(Array.isArray(t)){for(r=Re(e),i=t.length;a<i;a++)o[t[a]]=S.css(e,t[a],!1,r);return o}return void 0!==n?S.style(e,t,n):S.css(e,t)},e,t,1<arguments.length)}}),((S.Tween=Ke).prototype={constructor:Ke,init:function(e,t,n,r,i,o){this.elem=e,this.prop=n,this.easing=i||S.easing._default,this.options=t,this.start=this.now=this.cur(),this.end=r,this.unit=o||(S.cssNumber[n]?"":"px")},cur:function(){var e=Ke.propHooks[this.prop];return e&&e.get?e.get(this):Ke.propHooks._default.get(this)},run:function(e){var t,n=Ke.propHooks[this.prop];return this.options.duration?this.pos=t=S.easing[this.easing](e,this.options.duration*e,0,1,this.options.duration):this.pos=t=e,this.now=(this.end-this.start)*t+this.start,this.options.step&&this.options.step.call(this.elem,this.now,this),n&&n.set?n.set(this):Ke.propHooks._default.set(this),this}}).init.prototype=Ke.prototype,(Ke.propHooks={_default:{get:function(e){var t;return 1!==e.elem.nodeType||null!=e.elem[e.prop]&&null==e.elem.style[e.prop]?e.elem[e.prop]:(t=S.css(e.elem,e.prop,""))&&"auto"!==t?t:0},set:function(e){S.fx.step[e.prop]?S.fx.step[e.prop](e):1!==e.elem.nodeType||!S.cssHooks[e.prop]&&null==e.elem.style[ze(e.prop)]?e.elem[e.prop]=e.now:S.style(e.elem,e.prop,e.now+e.unit)}}}).scrollTop=Ke.propHooks.scrollLeft={set:function(e){e.elem.nodeType&&e.elem.parentNode&&(e.elem[e.prop]=e.now)}},S.easing={linear:function(e){return e},swing:function(e){return.5-Math.cos(e*Math.PI)/2},_default:"swing"},S.fx=Ke.prototype.init,S.fx.step={};var Ze,et,tt,nt,rt=/^(?:toggle|show|hide)$/,it=/queueHooks$/;function ot(){et&&(!1===E.hidden&&C.requestAnimationFrame?C.requestAnimationFrame(ot):C.setTimeout(ot,S.fx.interval),S.fx.tick())}function at(){return C.setTimeout(function(){Ze=void 0}),Ze=Date.now()}function st(e,t){var n,r=0,i={height:e};for(t=t?1:0;r<4;r+=2-t)i["margin"+(n=ne[r])]=i["padding"+n]=e;return t&&(i.opacity=i.width=e),i}function ut(e,t,n){for(var r,i=(lt.tweeners[t]||[]).concat(lt.tweeners["*"]),o=0,a=i.length;o<a;o++)if(r=i[o].call(n,t,e))return r}function lt(o,e,t){var n,a,r=0,i=lt.prefilters.length,s=S.Deferred().always(function(){delete u.elem}),u=function(){if(a)return!1;for(var e=Ze||at(),t=Math.max(0,l.startTime+l.duration-e),n=1-(t/l.duration||0),r=0,i=l.tweens.length;r<i;r++)l.tweens[r].run(n);return s.notifyWith(o,[l,n,t]),n<1&&i?t:(i||s.notifyWith(o,[l,1,0]),s.resolveWith(o,[l]),!1)},l=s.promise({elem:o,props:S.extend({},e),opts:S.extend(!0,{specialEasing:{},easing:S.easing._default},t),originalProperties:e,originalOptions:t,startTime:Ze||at(),duration:t.duration,tweens:[],createTween:function(e,t){var n=S.Tween(o,l.opts,e,t,l.opts.specialEasing[e]||l.opts.easing);return l.tweens.push(n),n},stop:function(e){var t=0,n=e?l.tweens.length:0;if(a)return this;for(a=!0;t<n;t++)l.tweens[t].run(1);return e?(s.notifyWith(o,[l,1,0]),s.resolveWith(o,[l,e])):s.rejectWith(o,[l,e]),this}}),c=l.props;for(!function(e,t){var n,r,i,o,a;for(n in e)if(i=t[r=X(n)],o=e[n],Array.isArray(o)&&(i=o[1],o=e[n]=o[0]),n!==r&&(e[r]=o,delete e[n]),(a=S.cssHooks[r])&&"expand"in a)for(n in o=a.expand(o),delete e[r],o)n in e||(e[n]=o[n],t[n]=i);else t[r]=i}(c,l.opts.specialEasing);r<i;r++)if(n=lt.prefilters[r].call(l,o,c,l.opts))return m(n.stop)&&(S._queueHooks(l.elem,l.opts.queue).stop=n.stop.bind(n)),n;return S.map(c,ut,l),m(l.opts.start)&&l.opts.start.call(o,l),l.progress(l.opts.progress).done(l.opts.done,l.opts.complete).fail(l.opts.fail).always(l.opts.always),S.fx.timer(S.extend(u,{elem:o,anim:l,queue:l.opts.queue})),l}S.Animation=S.extend(lt,{tweeners:{"*":[function(e,t){var n=this.createTween(e,t);return se(n.elem,e,te.exec(t),n),n}]},tweener:function(e,t){m(e)?(t=e,e=["*"]):e=e.match(P);for(var n,r=0,i=e.length;r<i;r++)n=e[r],lt.tweeners[n]=lt.tweeners[n]||[],lt.tweeners[n].unshift(t)},prefilters:[function(e,t,n){var r,i,o,a,s,u,l,c,f="width"in t||"height"in t,p=this,d={},h=e.style,g=e.nodeType&&ae(e),v=Y.get(e,"fxshow");for(r in n.queue||(null==(a=S._queueHooks(e,"fx")).unqueued&&(a.unqueued=0,s=a.empty.fire,a.empty.fire=function(){a.unqueued||s()}),a.unqueued++,p.always(function(){p.always(function(){a.unqueued--,S.queue(e,"fx").length||a.empty.fire()})})),t)if(i=t[r],rt.test(i)){if(delete t[r],o=o||"toggle"===i,i===(g?"hide":"show")){if("show"!==i||!v||void 0===v[r])continue;g=!0}d[r]=v&&v[r]||S.style(e,r)}if((u=!S.isEmptyObject(t))||!S.isEmptyObject(d))for(r in f&&1===e.nodeType&&(n.overflow=[h.overflow,h.overflowX,h.overflowY],null==(l=v&&v.display)&&(l=Y.get(e,"display")),"none"===(c=S.css(e,"display"))&&(l?c=l:(le([e],!0),l=e.style.display||l,c=S.css(e,"display"),le([e]))),("inline"===c||"inline-block"===c&&null!=l)&&"none"===S.css(e,"float")&&(u||(p.done(function(){h.display=l}),null==l&&(c=h.display,l="none"===c?"":c)),h.display="inline-block")),n.overflow&&(h.overflow="hidden",p.always(function(){h.overflow=n.overflow[0],h.overflowX=n.overflow[1],h.overflowY=n.overflow[2]})),u=!1,d)u||(v?"hidden"in v&&(g=v.hidden):v=Y.access(e,"fxshow",{display:l}),o&&(v.hidden=!g),g&&le([e],!0),p.done(function(){for(r in g||le([e]),Y.remove(e,"fxshow"),d)S.style(e,r,d[r])})),u=ut(g?v[r]:0,r,p),r in v||(v[r]=u.start,g&&(u.end=u.start,u.start=0))}],prefilter:function(e,t){t?lt.prefilters.unshift(e):lt.prefilters.push(e)}}),S.speed=function(e,t,n){var r=e&&"object"==typeof e?S.extend({},e):{complete:n||!n&&t||m(e)&&e,duration:e,easing:n&&t||t&&!m(t)&&t};return S.fx.off?r.duration=0:"number"!=typeof r.duration&&(r.duration in S.fx.speeds?r.duration=S.fx.speeds[r.duration]:r.duration=S.fx.speeds._default),null!=r.queue&&!0!==r.queue||(r.queue="fx"),r.old=r.complete,r.complete=function(){m(r.old)&&r.old.call(this),r.queue&&S.dequeue(this,r.queue)},r},S.fn.extend({fadeTo:function(e,t,n,r){return this.filter(ae).css("opacity",0).show().end().animate({opacity:t},e,n,r)},animate:function(t,e,n,r){var i=S.isEmptyObject(t),o=S.speed(e,n,r),a=function(){var e=lt(this,S.extend({},t),o);(i||Y.get(this,"finish"))&&e.stop(!0)};return a.finish=a,i||!1===o.queue?this.each(a):this.queue(o.queue,a)},stop:function(i,e,o){var a=function(e){var t=e.stop;delete e.stop,t(o)};return"string"!=typeof i&&(o=e,e=i,i=void 0),e&&this.queue(i||"fx",[]),this.each(function(){var e=!0,t=null!=i&&i+"queueHooks",n=S.timers,r=Y.get(this);if(t)r[t]&&r[t].stop&&a(r[t]);else for(t in r)r[t]&&r[t].stop&&it.test(t)&&a(r[t]);for(t=n.length;t--;)n[t].elem!==this||null!=i&&n[t].queue!==i||(n[t].anim.stop(o),e=!1,n.splice(t,1));!e&&o||S.dequeue(this,i)})},finish:function(a){return!1!==a&&(a=a||"fx"),this.each(function(){var e,t=Y.get(this),n=t[a+"queue"],r=t[a+"queueHooks"],i=S.timers,o=n?n.length:0;for(t.finish=!0,S.queue(this,a,[]),r&&r.stop&&r.stop.call(this,!0),e=i.length;e--;)i[e].elem===this&&i[e].queue===a&&(i[e].anim.stop(!0),i.splice(e,1));for(e=0;e<o;e++)n[e]&&n[e].finish&&n[e].finish.call(this);delete t.finish})}}),S.each(["toggle","show","hide"],function(e,r){var i=S.fn[r];S.fn[r]=function(e,t,n){return null==e||"boolean"==typeof e?i.apply(this,arguments):this.animate(st(r,!0),e,t,n)}}),S.each({slideDown:st("show"),slideUp:st("hide"),slideToggle:st("toggle"),fadeIn:{opacity:"show"},fadeOut:{opacity:"hide"},fadeToggle:{opacity:"toggle"}},function(e,r){S.fn[e]=function(e,t,n){return this.animate(r,e,t,n)}}),S.timers=[],S.fx.tick=function(){var e,t=0,n=S.timers;for(Ze=Date.now();t<n.length;t++)(e=n[t])()||n[t]!==e||n.splice(t--,1);n.length||S.fx.stop(),Ze=void 0},S.fx.timer=function(e){S.timers.push(e),S.fx.start()},S.fx.interval=13,S.fx.start=function(){et||(et=!0,ot())},S.fx.stop=function(){et=null},S.fx.speeds={slow:600,fast:200,_default:400},S.fn.delay=function(r,e){return r=S.fx&&S.fx.speeds[r]||r,e=e||"fx",this.queue(e,function(e,t){var n=C.setTimeout(e,r);t.stop=function(){C.clearTimeout(n)}})},tt=E.createElement("input"),nt=E.createElement("select").appendChild(E.createElement("option")),tt.type="checkbox",y.checkOn=""!==tt.value,y.optSelected=nt.selected,(tt=E.createElement("input")).value="t",tt.type="radio",y.radioValue="t"===tt.value;var ct,ft=S.expr.attrHandle;S.fn.extend({attr:function(e,t){return $(this,S.attr,e,t,1<arguments.length)},removeAttr:function(e){return this.each(function(){S.removeAttr(this,e)})}}),S.extend({attr:function(e,t,n){var r,i,o=e.nodeType;if(3!==o&&8!==o&&2!==o)return"undefined"==typeof e.getAttribute?S.prop(e,t,n):(1===o&&S.isXMLDoc(e)||(i=S.attrHooks[t.toLowerCase()]||(S.expr.match.bool.test(t)?ct:void 0)),void 0!==n?null===n?void S.removeAttr(e,t):i&&"set"in i&&void 0!==(r=i.set(e,n,t))?r:(e.setAttribute(t,n+""),n):i&&"get"in i&&null!==(r=i.get(e,t))?r:null==(r=S.find.attr(e,t))?void 0:r)},attrHooks:{type:{set:function(e,t){if(!y.radioValue&&"radio"===t&&A(e,"input")){var n=e.value;return e.setAttribute("type",t),n&&(e.value=n),t}}}},removeAttr:function(e,t){var n,r=0,i=t&&t.match(P);if(i&&1===e.nodeType)while(n=i[r++])e.removeAttribute(n)}}),ct={set:function(e,t,n){return!1===t?S.removeAttr(e,n):e.setAttribute(n,n),n}},S.each(S.expr.match.bool.source.match(/\w+/g),function(e,t){var a=ft[t]||S.find.attr;ft[t]=function(e,t,n){var r,i,o=t.toLowerCase();return n||(i=ft[o],ft[o]=r,r=null!=a(e,t,n)?o:null,ft[o]=i),r}});var pt=/^(?:input|select|textarea|button)$/i,dt=/^(?:a|area)$/i;function ht(e){return(e.match(P)||[]).join(" ")}function gt(e){return e.getAttribute&&e.getAttribute("class")||""}function vt(e){return Array.isArray(e)?e:"string"==typeof e&&e.match(P)||[]}S.fn.extend({prop:function(e,t){return $(this,S.prop,e,t,1<arguments.length)},removeProp:function(e){return this.each(function(){delete this[S.propFix[e]||e]})}}),S.extend({prop:function(e,t,n){var r,i,o=e.nodeType;if(3!==o&&8!==o&&2!==o)return 1===o&&S.isXMLDoc(e)||(t=S.propFix[t]||t,i=S.propHooks[t]),void 0!==n?i&&"set"in i&&void 0!==(r=i.set(e,n,t))?r:e[t]=n:i&&"get"in i&&null!==(r=i.get(e,t))?r:e[t]},propHooks:{tabIndex:{get:function(e){var t=S.find.attr(e,"tabindex");return t?parseInt(t,10):pt.test(e.nodeName)||dt.test(e.nodeName)&&e.href?0:-1}}},propFix:{"for":"htmlFor","class":"className"}}),y.optSelected||(S.propHooks.selected={get:function(e){var t=e.parentNode;return t&&t.parentNode&&t.parentNode.selectedIndex,null},set:function(e){var t=e.parentNode;t&&(t.selectedIndex,t.parentNode&&t.parentNode.selectedIndex)}}),S.each(["tabIndex","readOnly","maxLength","cellSpacing","cellPadding","rowSpan","colSpan","useMap","frameBorder","contentEditable"],function(){S.propFix[this.toLowerCase()]=this}),S.fn.extend({addClass:function(t){var e,n,r,i,o,a,s,u=0;if(m(t))return this.each(function(e){S(this).addClass(t.call(this,e,gt(this)))});if((e=vt(t)).length)while(n=this[u++])if(i=gt(n),r=1===n.nodeType&&" "+ht(i)+" "){a=0;while(o=e[a++])r.indexOf(" "+o+" ")<0&&(r+=o+" ");i!==(s=ht(r))&&n.setAttribute("class",s)}return this},removeClass:function(t){var e,n,r,i,o,a,s,u=0;if(m(t))return this.each(function(e){S(this).removeClass(t.call(this,e,gt(this)))});if(!arguments.length)return this.attr("class","");if((e=vt(t)).length)while(n=this[u++])if(i=gt(n),r=1===n.nodeType&&" "+ht(i)+" "){a=0;while(o=e[a++])while(-1<r.indexOf(" "+o+" "))r=r.replace(" "+o+" "," ");i!==(s=ht(r))&&n.setAttribute("class",s)}return this},toggleClass:function(i,t){var o=typeof i,a="string"===o||Array.isArray(i);return"boolean"==typeof t&&a?t?this.addClass(i):this.removeClass(i):m(i)?this.each(function(e){S(this).toggleClass(i.call(this,e,gt(this),t),t)}):this.each(function(){var e,t,n,r;if(a){t=0,n=S(this),r=vt(i);while(e=r[t++])n.hasClass(e)?n.removeClass(e):n.addClass(e)}else void 0!==i&&"boolean"!==o||((e=gt(this))&&Y.set(this,"__className__",e),this.setAttribute&&this.setAttribute("class",e||!1===i?"":Y.get(this,"__className__")||""))})},hasClass:function(e){var t,n,r=0;t=" "+e+" ";while(n=this[r++])if(1===n.nodeType&&-1<(" "+ht(gt(n))+" ").indexOf(t))return!0;return!1}});var yt=/\r/g;S.fn.extend({val:function(n){var r,e,i,t=this[0];return arguments.length?(i=m(n),this.each(function(e){var t;1===this.nodeType&&(null==(t=i?n.call(this,e,S(this).val()):n)?t="":"number"==typeof t?t+="":Array.isArray(t)&&(t=S.map(t,function(e){return null==e?"":e+""})),(r=S.valHooks[this.type]||S.valHooks[this.nodeName.toLowerCase()])&&"set"in r&&void 0!==r.set(this,t,"value")||(this.value=t))})):t?(r=S.valHooks[t.type]||S.valHooks[t.nodeName.toLowerCase()])&&"get"in r&&void 0!==(e=r.get(t,"value"))?e:"string"==typeof(e=t.value)?e.replace(yt,""):null==e?"":e:void 0}}),S.extend({valHooks:{option:{get:function(e){var t=S.find.attr(e,"value");return null!=t?t:ht(S.text(e))}},select:{get:function(e){var t,n,r,i=e.options,o=e.selectedIndex,a="select-one"===e.type,s=a?null:[],u=a?o+1:i.length;for(r=o<0?u:a?o:0;r<u;r++)if(((n=i[r]).selected||r===o)&&!n.disabled&&(!n.parentNode.disabled||!A(n.parentNode,"optgroup"))){if(t=S(n).val(),a)return t;s.push(t)}return s},set:function(e,t){var n,r,i=e.options,o=S.makeArray(t),a=i.length;while(a--)((r=i[a]).selected=-1<S.inArray(S.valHooks.option.get(r),o))&&(n=!0);return n||(e.selectedIndex=-1),o}}}}),S.each(["radio","checkbox"],function(){S.valHooks[this]={set:function(e,t){if(Array.isArray(t))return e.checked=-1<S.inArray(S(e).val(),t)}},y.checkOn||(S.valHooks[this].get=function(e){return null===e.getAttribute("value")?"on":e.value})}),y.focusin="onfocusin"in C;var mt=/^(?:focusinfocus|focusoutblur)$/,xt=function(e){e.stopPropagation()};S.extend(S.event,{trigger:function(e,t,n,r){var i,o,a,s,u,l,c,f,p=[n||E],d=v.call(e,"type")?e.type:e,h=v.call(e,"namespace")?e.namespace.split("."):[];if(o=f=a=n=n||E,3!==n.nodeType&&8!==n.nodeType&&!mt.test(d+S.event.triggered)&&(-1<d.indexOf(".")&&(d=(h=d.split(".")).shift(),h.sort()),u=d.indexOf(":")<0&&"on"+d,(e=e[S.expando]?e:new S.Event(d,"object"==typeof e&&e)).isTrigger=r?2:3,e.namespace=h.join("."),e.rnamespace=e.namespace?new RegExp("(^|\\.)"+h.join("\\.(?:.*\\.|)")+"(\\.|$)"):null,e.result=void 0,e.target||(e.target=n),t=null==t?[e]:S.makeArray(t,[e]),c=S.event.special[d]||{},r||!c.trigger||!1!==c.trigger.apply(n,t))){if(!r&&!c.noBubble&&!x(n)){for(s=c.delegateType||d,mt.test(s+d)||(o=o.parentNode);o;o=o.parentNode)p.push(o),a=o;a===(n.ownerDocument||E)&&p.push(a.defaultView||a.parentWindow||C)}i=0;while((o=p[i++])&&!e.isPropagationStopped())f=o,e.type=1<i?s:c.bindType||d,(l=(Y.get(o,"events")||Object.create(null))[e.type]&&Y.get(o,"handle"))&&l.apply(o,t),(l=u&&o[u])&&l.apply&&V(o)&&(e.result=l.apply(o,t),!1===e.result&&e.preventDefault());return e.type=d,r||e.isDefaultPrevented()||c._default&&!1!==c._default.apply(p.pop(),t)||!V(n)||u&&m(n[d])&&!x(n)&&((a=n[u])&&(n[u]=null),S.event.triggered=d,e.isPropagationStopped()&&f.addEventListener(d,xt),n[d](),e.isPropagationStopped()&&f.removeEventListener(d,xt),S.event.triggered=void 0,a&&(n[u]=a)),e.result}},simulate:function(e,t,n){var r=S.extend(new S.Event,n,{type:e,isSimulated:!0});S.event.trigger(r,null,t)}}),S.fn.extend({trigger:function(e,t){return this.each(function(){S.event.trigger(e,t,this)})},triggerHandler:function(e,t){var n=this[0];if(n)return S.event.trigger(e,t,n,!0)}}),y.focusin||S.each({focus:"focusin",blur:"focusout"},function(n,r){var i=function(e){S.event.simulate(r,e.target,S.event.fix(e))};S.event.special[r]={setup:function(){var e=this.ownerDocument||this.document||this,t=Y.access(e,r);t||e.addEventListener(n,i,!0),Y.access(e,r,(t||0)+1)},teardown:function(){var e=this.ownerDocument||this.document||this,t=Y.access(e,r)-1;t?Y.access(e,r,t):(e.removeEventListener(n,i,!0),Y.remove(e,r))}}});var bt=C.location,wt={guid:Date.now()},Tt=/\?/;S.parseXML=function(e){var t,n;if(!e||"string"!=typeof e)return null;try{t=(new C.DOMParser).parseFromString(e,"text/xml")}catch(e){}return n=t&&t.getElementsByTagName("parsererror")[0],t&&!n||S.error("Invalid XML: "+(n?S.map(n.childNodes,function(e){return e.textContent}).join("\n"):e)),t};var Ct=/\[\]$/,Et=/\r?\n/g,St=/^(?:submit|button|image|reset|file)$/i,kt=/^(?:input|select|textarea|keygen)/i;function At(n,e,r,i){var t;if(Array.isArray(e))S.each(e,function(e,t){r||Ct.test(n)?i(n,t):At(n+"["+("object"==typeof t&&null!=t?e:"")+"]",t,r,i)});else if(r||"object"!==w(e))i(n,e);else for(t in e)At(n+"["+t+"]",e[t],r,i)}S.param=function(e,t){var n,r=[],i=function(e,t){var n=m(t)?t():t;r[r.length]=encodeURIComponent(e)+"="+encodeURIComponent(null==n?"":n)};if(null==e)return"";if(Array.isArray(e)||e.jquery&&!S.isPlainObject(e))S.each(e,function(){i(this.name,this.value)});else for(n in e)At(n,e[n],t,i);return r.join("&")},S.fn.extend({serialize:function(){return S.param(this.serializeArray())},serializeArray:function(){return this.map(function(){var e=S.prop(this,"elements");return e?S.makeArray(e):this}).filter(function(){var e=this.type;return this.name&&!S(this).is(":disabled")&&kt.test(this.nodeName)&&!St.test(e)&&(this.checked||!pe.test(e))}).map(function(e,t){var n=S(this).val();return null==n?null:Array.isArray(n)?S.map(n,function(e){return{name:t.name,value:e.replace(Et,"\r\n")}}):{name:t.name,value:n.replace(Et,"\r\n")}}).get()}});var Nt=/%20/g,jt=/#.*$/,Dt=/([?&])_=[^&]*/,qt=/^(.*?):[ \t]*([^\r\n]*)$/gm,Lt=/^(?:GET|HEAD)$/,Ht=/^\/\//,Ot={},Pt={},Rt="*/".concat("*"),Mt=E.createElement("a");function It(o){return function(e,t){"string"!=typeof e&&(t=e,e="*");var n,r=0,i=e.toLowerCase().match(P)||[];if(m(t))while(n=i[r++])"+"===n[0]?(n=n.slice(1)||"*",(o[n]=o[n]||[]).unshift(t)):(o[n]=o[n]||[]).push(t)}}function Wt(t,i,o,a){var s={},u=t===Pt;function l(e){var r;return s[e]=!0,S.each(t[e]||[],function(e,t){var n=t(i,o,a);return"string"!=typeof n||u||s[n]?u?!(r=n):void 0:(i.dataTypes.unshift(n),l(n),!1)}),r}return l(i.dataTypes[0])||!s["*"]&&l("*")}function Ft(e,t){var n,r,i=S.ajaxSettings.flatOptions||{};for(n in t)void 0!==t[n]&&((i[n]?e:r||(r={}))[n]=t[n]);return r&&S.extend(!0,e,r),e}Mt.href=bt.href,S.extend({active:0,lastModified:{},etag:{},ajaxSettings:{url:bt.href,type:"GET",isLocal:/^(?:about|app|app-storage|.+-extension|file|res|widget):$/.test(bt.protocol),global:!0,processData:!0,async:!0,contentType:"application/x-www-form-urlencoded; charset=UTF-8",accepts:{"*":Rt,text:"text/plain",html:"text/html",xml:"application/xml, text/xml",json:"application/json, text/javascript"},contents:{xml:/\bxml\b/,html:/\bhtml/,json:/\bjson\b/},responseFields:{xml:"responseXML",text:"responseText",json:"responseJSON"},converters:{"* text":String,"text html":!0,"text json":JSON.parse,"text xml":S.parseXML},flatOptions:{url:!0,context:!0}},ajaxSetup:function(e,t){return t?Ft(Ft(e,S.ajaxSettings),t):Ft(S.ajaxSettings,e)},ajaxPrefilter:It(Ot),ajaxTransport:It(Pt),ajax:function(e,t){"object"==typeof e&&(t=e,e=void 0),t=t||{};var c,f,p,n,d,r,h,g,i,o,v=S.ajaxSetup({},t),y=v.context||v,m=v.context&&(y.nodeType||y.jquery)?S(y):S.event,x=S.Deferred(),b=S.Callbacks("once memory"),w=v.statusCode||{},a={},s={},u="canceled",T={readyState:0,getResponseHeader:function(e){var t;if(h){if(!n){n={};while(t=qt.exec(p))n[t[1].toLowerCase()+" "]=(n[t[1].toLowerCase()+" "]||[]).concat(t[2])}t=n[e.toLowerCase()+" "]}return null==t?null:t.join(", ")},getAllResponseHeaders:function(){return h?p:null},setRequestHeader:function(e,t){return null==h&&(e=s[e.toLowerCase()]=s[e.toLowerCase()]||e,a[e]=t),this},overrideMimeType:function(e){return null==h&&(v.mimeType=e),this},statusCode:function(e){var t;if(e)if(h)T.always(e[T.status]);else for(t in e)w[t]=[w[t],e[t]];return this},abort:function(e){var t=e||u;return c&&c.abort(t),l(0,t),this}};if(x.promise(T),v.url=((e||v.url||bt.href)+"").replace(Ht,bt.protocol+"//"),v.type=t.method||t.type||v.method||v.type,v.dataTypes=(v.dataType||"*").toLowerCase().match(P)||[""],null==v.crossDomain){r=E.createElement("a");try{r.href=v.url,r.href=r.href,v.crossDomain=Mt.protocol+"//"+Mt.host!=r.protocol+"//"+r.host}catch(e){v.crossDomain=!0}}if(v.data&&v.processData&&"string"!=typeof v.data&&(v.data=S.param(v.data,v.traditional)),Wt(Ot,v,t,T),h)return T;for(i in(g=S.event&&v.global)&&0==S.active++&&S.event.trigger("ajaxStart"),v.type=v.type.toUpperCase(),v.hasContent=!Lt.test(v.type),f=v.url.replace(jt,""),v.hasContent?v.data&&v.processData&&0===(v.contentType||"").indexOf("application/x-www-form-urlencoded")&&(v.data=v.data.replace(Nt,"+")):(o=v.url.slice(f.length),v.data&&(v.processData||"string"==typeof v.data)&&(f+=(Tt.test(f)?"&":"?")+v.data,delete v.data),!1===v.cache&&(f=f.replace(Dt,"$1"),o=(Tt.test(f)?"&":"?")+"_="+wt.guid+++o),v.url=f+o),v.ifModified&&(S.lastModified[f]&&T.setRequestHeader("If-Modified-Since",S.lastModified[f]),S.etag[f]&&T.setRequestHeader("If-None-Match",S.etag[f])),(v.data&&v.hasContent&&!1!==v.contentType||t.contentType)&&T.setRequestHeader("Content-Type",v.contentType),T.setRequestHeader("Accept",v.dataTypes[0]&&v.accepts[v.dataTypes[0]]?v.accepts[v.dataTypes[0]]+("*"!==v.dataTypes[0]?", "+Rt+"; q=0.01":""):v.accepts["*"]),v.headers)T.setRequestHeader(i,v.headers[i]);if(v.beforeSend&&(!1===v.beforeSend.call(y,T,v)||h))return T.abort();if(u="abort",b.add(v.complete),T.done(v.success),T.fail(v.error),c=Wt(Pt,v,t,T)){if(T.readyState=1,g&&m.trigger("ajaxSend",[T,v]),h)return T;v.async&&0<v.timeout&&(d=C.setTimeout(function(){T.abort("timeout")},v.timeout));try{h=!1,c.send(a,l)}catch(e){if(h)throw e;l(-1,e)}}else l(-1,"No Transport");function l(e,t,n,r){var i,o,a,s,u,l=t;h||(h=!0,d&&C.clearTimeout(d),c=void 0,p=r||"",T.readyState=0<e?4:0,i=200<=e&&e<300||304===e,n&&(s=function(e,t,n){var r,i,o,a,s=e.contents,u=e.dataTypes;while("*"===u[0])u.shift(),void 0===r&&(r=e.mimeType||t.getResponseHeader("Content-Type"));if(r)for(i in s)if(s[i]&&s[i].test(r)){u.unshift(i);break}if(u[0]in n)o=u[0];else{for(i in n){if(!u[0]||e.converters[i+" "+u[0]]){o=i;break}a||(a=i)}o=o||a}if(o)return o!==u[0]&&u.unshift(o),n[o]}(v,T,n)),!i&&-1<S.inArray("script",v.dataTypes)&&S.inArray("json",v.dataTypes)<0&&(v.converters["text script"]=function(){}),s=function(e,t,n,r){var i,o,a,s,u,l={},c=e.dataTypes.slice();if(c[1])for(a in e.converters)l[a.toLowerCase()]=e.converters[a];o=c.shift();while(o)if(e.responseFields[o]&&(n[e.responseFields[o]]=t),!u&&r&&e.dataFilter&&(t=e.dataFilter(t,e.dataType)),u=o,o=c.shift())if("*"===o)o=u;else if("*"!==u&&u!==o){if(!(a=l[u+" "+o]||l["* "+o]))for(i in l)if((s=i.split(" "))[1]===o&&(a=l[u+" "+s[0]]||l["* "+s[0]])){!0===a?a=l[i]:!0!==l[i]&&(o=s[0],c.unshift(s[1]));break}if(!0!==a)if(a&&e["throws"])t=a(t);else try{t=a(t)}catch(e){return{state:"parsererror",error:a?e:"No conversion from "+u+" to "+o}}}return{state:"success",data:t}}(v,s,T,i),i?(v.ifModified&&((u=T.getResponseHeader("Last-Modified"))&&(S.lastModified[f]=u),(u=T.getResponseHeader("etag"))&&(S.etag[f]=u)),204===e||"HEAD"===v.type?l="nocontent":304===e?l="notmodified":(l=s.state,o=s.data,i=!(a=s.error))):(a=l,!e&&l||(l="error",e<0&&(e=0))),T.status=e,T.statusText=(t||l)+"",i?x.resolveWith(y,[o,l,T]):x.rejectWith(y,[T,l,a]),T.statusCode(w),w=void 0,g&&m.trigger(i?"ajaxSuccess":"ajaxError",[T,v,i?o:a]),b.fireWith(y,[T,l]),g&&(m.trigger("ajaxComplete",[T,v]),--S.active||S.event.trigger("ajaxStop")))}return T},getJSON:function(e,t,n){return S.get(e,t,n,"json")},getScript:function(e,t){return S.get(e,void 0,t,"script")}}),S.each(["get","post"],function(e,i){S[i]=function(e,t,n,r){return m(t)&&(r=r||n,n=t,t=void 0),S.ajax(S.extend({url:e,type:i,dataType:r,data:t,success:n},S.isPlainObject(e)&&e))}}),S.ajaxPrefilter(function(e){var t;for(t in e.headers)"content-type"===t.toLowerCase()&&(e.contentType=e.headers[t]||"")}),S._evalUrl=function(e,t,n){return S.ajax({url:e,type:"GET",dataType:"script",cache:!0,async:!1,global:!1,converters:{"text script":function(){}},dataFilter:function(e){S.globalEval(e,t,n)}})},S.fn.extend({wrapAll:function(e){var t;return this[0]&&(m(e)&&(e=e.call(this[0])),t=S(e,this[0].ownerDocument).eq(0).clone(!0),this[0].parentNode&&t.insertBefore(this[0]),t.map(function(){var e=this;while(e.firstElementChild)e=e.firstElementChild;return e}).append(this)),this},wrapInner:function(n){return m(n)?this.each(function(e){S(this).wrapInner(n.call(this,e))}):this.each(function(){var e=S(this),t=e.contents();t.length?t.wrapAll(n):e.append(n)})},wrap:function(t){var n=m(t);return this.each(function(e){S(this).wrapAll(n?t.call(this,e):t)})},unwrap:function(e){return this.parent(e).not("body").each(function(){S(this).replaceWith(this.childNodes)}),this}}),S.expr.pseudos.hidden=function(e){return!S.expr.pseudos.visible(e)},S.expr.pseudos.visible=function(e){return!!(e.offsetWidth||e.offsetHeight||e.getClientRects().length)},S.ajaxSettings.xhr=function(){try{return new C.XMLHttpRequest}catch(e){}};var Bt={0:200,1223:204},$t=S.ajaxSettings.xhr();y.cors=!!$t&&"withCredentials"in $t,y.ajax=$t=!!$t,S.ajaxTransport(function(i){var o,a;if(y.cors||$t&&!i.crossDomain)return{send:function(e,t){var n,r=i.xhr();if(r.open(i.type,i.url,i.async,i.username,i.password),i.xhrFields)for(n in i.xhrFields)r[n]=i.xhrFields[n];for(n in i.mimeType&&r.overrideMimeType&&r.overrideMimeType(i.mimeType),i.crossDomain||e["X-Requested-With"]||(e["X-Requested-With"]="XMLHttpRequest"),e)r.setRequestHeader(n,e[n]);o=function(e){return function(){o&&(o=a=r.onload=r.onerror=r.onabort=r.ontimeout=r.onreadystatechange=null,"abort"===e?r.abort():"error"===e?"number"!=typeof r.status?t(0,"error"):t(r.status,r.statusText):t(Bt[r.status]||r.status,r.statusText,"text"!==(r.responseType||"text")||"string"!=typeof r.responseText?{binary:r.response}:{text:r.responseText},r.getAllResponseHeaders()))}},r.onload=o(),a=r.onerror=r.ontimeout=o("error"),void 0!==r.onabort?r.onabort=a:r.onreadystatechange=function(){4===r.readyState&&C.setTimeout(function(){o&&a()})},o=o("abort");try{r.send(i.hasContent&&i.data||null)}catch(e){if(o)throw e}},abort:function(){o&&o()}}}),S.ajaxPrefilter(function(e){e.crossDomain&&(e.contents.script=!1)}),S.ajaxSetup({accepts:{script:"text/javascript, application/javascript, application/ecmascript, application/x-ecmascript"},contents:{script:/\b(?:java|ecma)script\b/},converters:{"text script":function(e){return S.globalEval(e),e}}}),S.ajaxPrefilter("script",function(e){void 0===e.cache&&(e.cache=!1),e.crossDomain&&(e.type="GET")}),S.ajaxTransport("script",function(n){var r,i;if(n.crossDomain||n.scriptAttrs)return{send:function(e,t){r=S("<script>").attr(n.scriptAttrs||{}).prop({charset:n.scriptCharset,src:n.url}).on("load error",i=function(e){r.remove(),i=null,e&&t("error"===e.type?404:200,e.type)}),E.head.appendChild(r[0])},abort:function(){i&&i()}}});var _t,zt=[],Ut=/(=)\?(?=&|$)|\?\?/;S.ajaxSetup({jsonp:"callback",jsonpCallback:function(){var e=zt.pop()||S.expando+"_"+wt.guid++;return this[e]=!0,e}}),S.ajaxPrefilter("json jsonp",function(e,t,n){var r,i,o,a=!1!==e.jsonp&&(Ut.test(e.url)?"url":"string"==typeof e.data&&0===(e.contentType||"").indexOf("application/x-www-form-urlencoded")&&Ut.test(e.data)&&"data");if(a||"jsonp"===e.dataTypes[0])return r=e.jsonpCallback=m(e.jsonpCallback)?e.jsonpCallback():e.jsonpCallback,a?e[a]=e[a].replace(Ut,"$1"+r):!1!==e.jsonp&&(e.url+=(Tt.test(e.url)?"&":"?")+e.jsonp+"="+r),e.converters["script json"]=function(){return o||S.error(r+" was not called"),o[0]},e.dataTypes[0]="json",i=C[r],C[r]=function(){o=arguments},n.always(function(){void 0===i?S(C).removeProp(r):C[r]=i,e[r]&&(e.jsonpCallback=t.jsonpCallback,zt.push(r)),o&&m(i)&&i(o[0]),o=i=void 0}),"script"}),y.createHTMLDocument=((_t=E.implementation.createHTMLDocument("").body).innerHTML="<form></form><form></form>",2===_t.childNodes.length),S.parseHTML=function(e,t,n){return"string"!=typeof e?[]:("boolean"==typeof t&&(n=t,t=!1),t||(y.createHTMLDocument?((r=(t=E.implementation.createHTMLDocument("")).createElement("base")).href=E.location.href,t.head.appendChild(r)):t=E),o=!n&&[],(i=N.exec(e))?[t.createElement(i[1])]:(i=xe([e],t,o),o&&o.length&&S(o).remove(),S.merge([],i.childNodes)));var r,i,o},S.fn.load=function(e,t,n){var r,i,o,a=this,s=e.indexOf(" ");return-1<s&&(r=ht(e.slice(s)),e=e.slice(0,s)),m(t)?(n=t,t=void 0):t&&"object"==typeof t&&(i="POST"),0<a.length&&S.ajax({url:e,type:i||"GET",dataType:"html",data:t}).done(function(e){o=arguments,a.html(r?S("<div>").append(S.parseHTML(e)).find(r):e)}).always(n&&function(e,t){a.each(function(){n.apply(this,o||[e.responseText,t,e])})}),this},S.expr.pseudos.animated=function(t){return S.grep(S.timers,function(e){return t===e.elem}).length},S.offset={setOffset:function(e,t,n){var r,i,o,a,s,u,l=S.css(e,"position"),c=S(e),f={};"static"===l&&(e.style.position="relative"),s=c.offset(),o=S.css(e,"top"),u=S.css(e,"left"),("absolute"===l||"fixed"===l)&&-1<(o+u).indexOf("auto")?(a=(r=c.position()).top,i=r.left):(a=parseFloat(o)||0,i=parseFloat(u)||0),m(t)&&(t=t.call(e,n,S.extend({},s))),null!=t.top&&(f.top=t.top-s.top+a),null!=t.left&&(f.left=t.left-s.left+i),"using"in t?t.using.call(e,f):c.css(f)}},S.fn.extend({offset:function(t){if(arguments.length)return void 0===t?this:this.each(function(e){S.offset.setOffset(this,t,e)});var e,n,r=this[0];return r?r.getClientRects().length?(e=r.getBoundingClientRect(),n=r.ownerDocument.defaultView,{top:e.top+n.pageYOffset,left:e.left+n.pageXOffset}):{top:0,left:0}:void 0},position:function(){if(this[0]){var e,t,n,r=this[0],i={top:0,left:0};if("fixed"===S.css(r,"position"))t=r.getBoundingClientRect();else{t=this.offset(),n=r.ownerDocument,e=r.offsetParent||n.documentElement;while(e&&(e===n.body||e===n.documentElement)&&"static"===S.css(e,"position"))e=e.parentNode;e&&e!==r&&1===e.nodeType&&((i=S(e).offset()).top+=S.css(e,"borderTopWidth",!0),i.left+=S.css(e,"borderLeftWidth",!0))}return{top:t.top-i.top-S.css(r,"marginTop",!0),left:t.left-i.left-S.css(r,"marginLeft",!0)}}},offsetParent:function(){return this.map(function(){var e=this.offsetParent;while(e&&"static"===S.css(e,"position"))e=e.offsetParent;return e||re})}}),S.each({scrollLeft:"pageXOffset",scrollTop:"pageYOffset"},function(t,i){var o="pageYOffset"===i;S.fn[t]=function(e){return $(this,function(e,t,n){var r;if(x(e)?r=e:9===e.nodeType&&(r=e.defaultView),void 0===n)return r?r[i]:e[t];r?r.scrollTo(o?r.pageXOffset:n,o?n:r.pageYOffset):e[t]=n},t,e,arguments.length)}}),S.each(["top","left"],function(e,n){S.cssHooks[n]=Fe(y.pixelPosition,function(e,t){if(t)return t=We(e,n),Pe.test(t)?S(e).position()[n]+"px":t})}),S.each({Height:"height",Width:"width"},function(a,s){S.each({padding:"inner"+a,content:s,"":"outer"+a},function(r,o){S.fn[o]=function(e,t){var n=arguments.length&&(r||"boolean"!=typeof e),i=r||(!0===e||!0===t?"margin":"border");return $(this,function(e,t,n){var r;return x(e)?0===o.indexOf("outer")?e["inner"+a]:e.document.documentElement["client"+a]:9===e.nodeType?(r=e.documentElement,Math.max(e.body["scroll"+a],r["scroll"+a],e.body["offset"+a],r["offset"+a],r["client"+a])):void 0===n?S.css(e,t,i):S.style(e,t,n,i)},s,n?e:void 0,n)}})}),S.each(["ajaxStart","ajaxStop","ajaxComplete","ajaxError","ajaxSuccess","ajaxSend"],function(e,t){S.fn[t]=function(e){return this.on(t,e)}}),S.fn.extend({bind:function(e,t,n){return this.on(e,null,t,n)},unbind:function(e,t){return this.off(e,null,t)},delegate:function(e,t,n,r){return this.on(t,e,n,r)},undelegate:function(e,t,n){return 1===arguments.length?this.off(e,"**"):this.off(t,e||"**",n)},hover:function(e,t){return this.mouseenter(e).mouseleave(t||e)}}),S.each("blur focus focusin focusout resize scroll click dblclick mousedown mouseup mousemove mouseover mouseout mouseenter mouseleave change select submit keydown keypress keyup contextmenu".split(" "),function(e,n){S.fn[n]=function(e,t){return 0<arguments.length?this.on(n,null,e,t):this.trigger(n)}});var Xt=/^[\s\uFEFF\xA0]+|[\s\uFEFF\xA0]+$/g;S.proxy=function(e,t){var n,r,i;if("string"==typeof t&&(n=e[t],t=e,e=n),m(e))return r=s.call(arguments,2),(i=function(){return e.apply(t||this,r.concat(s.call(arguments)))}).guid=e.guid=e.guid||S.guid++,i},S.holdReady=function(e){e?S.readyWait++:S.ready(!0)},S.isArray=Array.isArray,S.parseJSON=JSON.parse,S.nodeName=A,S.isFunction=m,S.isWindow=x,S.camelCase=X,S.type=w,S.now=Date.now,S.isNumeric=function(e){var t=S.type(e);return("number"===t||"string"===t)&&!isNaN(e-parseFloat(e))},S.trim=function(e){return null==e?"":(e+"").replace(Xt,"")},"function"==typeof define&&define.amd&&define("jquery",[],function(){return S});var Vt=C.jQuery,Gt=C.$;return S.noConflict=function(e){return C.$===S&&(C.$=Gt),e&&C.jQuery===S&&(C.jQuery=Vt),S},"undefined"==typeof e&&(C.jQuery=C.$=S),S}); diff --git a/libs/pako-dummy.zip b/libs/pako-dummy.zip new file mode 100644 index 0000000000000000000000000000000000000000..e493ee9d673c81a523ad8e488c509c392765da52 Binary files /dev/null and b/libs/pako-dummy.zip differ diff --git a/misc/entity_state_test_data.py b/misc/entity_state_test_data.py new file mode 100755 index 0000000000000000000000000000000000000000..72ffe278d0ad90cb5223966bcd52c79cd420f160 --- /dev/null +++ b/misc/entity_state_test_data.py @@ -0,0 +1,211 @@ +#!/usr/bin/env python3 +import sys +import caosdb as db + + +_PASSWORD = "password1A!" + + +def teardown(): + d = db.execute_query("FIND ENTITY WITH ID > 99") + if len(d) > 0: + d.delete(flags={"forceFinalState": "true"}) + + +def setup_users(): + for role in ["publisher", "normal", "external"]: + try: + db.administration._delete_user(name=role+"_user") + except BaseException: + pass + for role in ["publisher", "normal", "external"]: + try: + db.administration._delete_role(name=role) + except BaseException: + pass + for role in ["publisher", "normal", "external"]: + db.administration._insert_role(name=role, description="A test role") + + username = role + "_user" + db.administration._insert_user( + name=username, + password=_PASSWORD, + status="ACTIVE") + db.administration._set_roles(username=username, roles=[role]) + + db.administration._set_permissions( + role="external", permission_rules=[ + db.administration.PermissionRule( + "Grant", "TRANSACTION:RETRIEVE"), + ]) + + db.administration._set_permissions( + role="normal", permission_rules=[ + db.administration.PermissionRule( + "Grant", "TRANSACTION:*"), + db.administration.PermissionRule( + "Grant", "ACM:USER:UPDATE_PASSWORD:?REALM?:?USERNAME?"), + db.administration.PermissionRule( + "Grant", "STATE:TRANSITION:Edit"), + db.administration.PermissionRule( + "Grant", "STATE:TRANSITION:Start Review"), + ]) + + db.administration._set_permissions( + role="publisher", permission_rules=[ + db.administration.PermissionRule( + "Grant", "ACM:USER:UPDATE_PASSWORD:?REALM?:?USERNAME?"), + db.administration.PermissionRule( + "Grant", "TRANSACTION:*"), + db.administration.PermissionRule( + "Grant", "STATE:*"), + ]) + + +def freeze_and_hide(entity): + """ nobody owns this entity and nobody has any permissions""" + entity.acl = db.ACL() + entity.acl.deny(role="?OTHER?", permission="*") + entity.insert() + + +def setup_state_data_model(): + freeze_and_hide(db.RecordType("State")) + freeze_and_hide(db.RecordType("StateModel")) + freeze_and_hide(db.RecordType("Transition")) + freeze_and_hide(db.Property(name="from", datatype="State")) + freeze_and_hide(db.Property(name="to", datatype="State")) + freeze_and_hide(db.Property(name="initial", datatype="State")) + freeze_and_hide(db.Property(name="final", datatype="State")) + freeze_and_hide(db.Property(name="color", datatype=db.TEXT)) + + +def setup_state_model(): + unpublished_acl = db.ACL() + unpublished_acl.grant(role="publisher", permission="*") + unpublished_acl.grant(role="normal", permission="UPDATE:*") + unpublished_acl.grant(role="normal", permission="RETRIEVE:ENTITY") + unpublished_acl = db.State.create_state_acl(unpublished_acl) + + unpublished_state = db.Record( + "Unpublished", + description="Unpublished entries are only visible to the team and may be edited by any team member." + ).add_parent("State").add_property( + "color", + "#5bc0de") + unpublished_state.acl = unpublished_acl + unpublished_state.insert() + + + review_acl = db.ACL() + review_acl.grant(role="publisher", permission="*") + review_acl.grant(role="normal", permission="RETRIEVE:ENTITY") + + review_state = db.Record( + "Under Review", + description="Entries under review are not publicly available yet, but they can only be edited by the members of the publisher group." + ).add_parent("State").add_property( + "color", + "#FFCC33") + review_state.acl = db.State.create_state_acl(review_acl) + review_state.insert() + + + published_acl = db.ACL() + + published_state = db.Record( + "Published", + description="Published entries are publicly available and cannot be edited unless they are unpublished again." + ).add_parent("State").add_property( + "color", + "#333333") + published_state.acl = db.State.create_state_acl(published_acl) + published_state.insert() + + # 1->2 + db.Record( + "Start Review", + description="This transitions denies the permissions to edit an entry for anyone but the members of the publisher group. However, the entry is not yet publicly available." + ).add_parent("Transition").add_property( + "from", + "unpublished").add_property( + "to", + "under review").add_property( + "color", + "#FFCC33").insert() + + # 2->3 + db.Record( + "Publish", + description="Published entries are visible for the public and cannot be changed unless they are unpublished again. Only members of the publisher group can publish or unpublish entries." + ).add_parent("Transition").add_property( + "from", "under review").add_property( + "to", "published").add_property( + "color", + "red").insert() + + # 3->1 + db.Record("Unpublish", description="Unpublish this entry to hide it from the public. Unpublished entries can be edited by any team member.").add_parent( + "Transition").add_property("from", "published").add_property("to", "unpublished").insert() + + # 2->1 + db.Record("Reject", description="Reject the publishing of this entity. Afterwards, the entity is editable for any team member again.").add_parent( + "Transition").add_property("from", "under review").add_property("to", "unpublished").insert() + + # 1->1 + db.Record("Edit").add_parent( + "Transition", + description="Edit this entity. The changes are not publicly available until this entity will have been reviewed and published.").add_property( + "from", + "unpublished").add_property( + "to", + "unpublished").insert() + + db.Record("Publish Life-cycle", description="The publish life-cycle is a quality assurance tool. Database entries can be edited without being publicly available until the changes have been reviewed and explicitely published by an eligible user.").add_parent("StateModel").add_property( + "Transition", + datatype=db.LIST("Transition"), + value=[ + "Edit", + "Start Review", + "Reject", + "Publish", + "Unpublish", + ]).add_property( + "initial", + "Unpublished").add_property( + "final", + "Unpublished").insert() + + +def setup_test_data(): + # any record of this type will have the unpublished state + rt = db.RecordType("TestRT") + rt.state = db.State(model="Publish Life-cycle", name="Unpublished") + rt.insert() + + db.Property("TestProperty", datatype=db.TEXT).insert() + rec = db.Record().add_parent("TestRT") + rec.description = "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur." + rec.add_property("TestProperty", "TestValue") + rec.insert() + + +if __name__ == "__main__": + for call in sys.argv[1:]: + if call == "setup_test_data": + setup_test_data() + elif call == "setup_state_data_model": + setup_state_data_model() + elif call == "setup_state_model": + setup_state_model() + elif call == "setup_users": + setup_users() + elif call == "teardown": + teardown() + elif call == "all": + setup_users() + setup_state_data_model() + setup_state_model() + setup_test_data() + else: + print("unknown parameter") diff --git a/misc/install_comment_datamodel.py b/misc/install_comment_datamodel.py new file mode 100755 index 0000000000000000000000000000000000000000..b8204811d37756c805c9ae8104cd4e58c6f19ab6 --- /dev/null +++ b/misc/install_comment_datamodel.py @@ -0,0 +1,41 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +# +# ** header v3.0 +# This file is a part of the CaosDB Project. +# +# Copyright (C) 2020 IndiScale GmbH <info@indiscale.com> +# Copyright (C) 2020 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/>. +# +# ** end header + +import sys +import caosdb as db + + +if len(db.execute_query("FIND RecordType CommentAnnotation")) > 0: + print("RecordType CommentAnnotation does exist") + sys.exit(1) + +comment = db.Property("comment", datatype=db.TEXT).insert() +annotationOf = db.Property("annotationOf", datatype=db.REFERENCE).insert() +par = db.RecordType("Annotation") +par.add_property(annotationOf, importance=db.OBLIGATORY) +par.insert() +rt = db.RecordType("CommentAnnotation").add_parent("Annotation", + inheritance=db.OBLIGATORY) +rt.add_property(comment, importance=db.OBLIGATORY) +rt.insert() diff --git a/misc/merge_js.sh b/misc/merge_js.sh new file mode 100755 index 0000000000000000000000000000000000000000..375529fe472a05caafb12981a6fb17aef0ae8db9 --- /dev/null +++ b/misc/merge_js.sh @@ -0,0 +1,70 @@ +#!/bin/bash +# +# ** header v3.0 +# This file is a part of the CaosDB Project. +# +# Copyright (C) 2021 IndiScale GmbH +# 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/>. +# +# ** end header + +# This file can be used to merge js files together. +# +# call: `./merge_js.sh [JS_FILE]*` +# where the JS_FILE are the files which will be merged into the resulting +# `public/webcaosdb.dist.js` in the order they appear in the command line +# call. See `Makefile` for an example. +# +# All other files in `public/js` are appended to the resulting file in no +# particular order. + + +CORE_MODULES=$@ +SOURCE_DIR=public/js/ +TARGET=public/webcaosdb.dist.js + +function _merge () { + _SOURCE=$2 + _TARGET=$3 + + echo "merging $1 module ${_SOURCE} into ${_TARGET}" + + + echo "//COPIED FROM ${_SOURCE} (START)" >> ${_TARGET} + cat ${_SOURCE} >> ${_TARGET} + echo "//COPIED FROM ${_SOURCE} (END)" >> ${_TARGET} + + rm ${_SOURCE} +} + +# clean up old +rm $TARGET || true +touch $TARGET + +for _SOURCE in ${CORE_MODULES[@]} ; do + _merge "core" "${SOURCE_DIR}${_SOURCE}" $TARGET +done + +# load other js files but exclude any subdirectory +for _SOURCE in $(find ${SOURCE_DIR}* -prune -iname "*.js") ; do + _merge "extension" ${_SOURCE} $TARGET +done + +# for `make test` +for _SOURCE in $(find ${SOURCE_DIR} -ipath "${SOURCE_DIR}modules/*.js") ; do + _merge "extension" ${_SOURCE} $TARGET +done + diff --git a/misc/revision_test_data.py b/misc/revision_test_data.py deleted file mode 100755 index 0f41fa9c1b748be0cbc6c5b917dc6739d3c21d89..0000000000000000000000000000000000000000 --- a/misc/revision_test_data.py +++ /dev/null @@ -1,46 +0,0 @@ -#!/usr/bin/env python3 -# -*- coding: utf-8 -*- -""" -Copyright 2020 IndiScale GmbH <info@indiscale.com> -Copyright 2020 Timm Fitschen <t.fitschen@indiscale.com> -""" - -import caosdb -import random -import os - -# data model -c = caosdb.execute_query("FIND Test*") -if len(c) > 0: - print(c) - delete = input("Delete these entities?\nType `yes`:") - if delete == "yes": - c.delete(); - else: - print("You typed `{}`".format(delete)) - print("[Canceled]") - exit(0) - - -print("inserting test data") - -upload_file = open("test.dat", "w") -upload_file.write("hello world\n") -upload_file.close() - -testdata = caosdb.Container() -testdata.extend([ - caosdb.File("TestFile", - path="test.dat", - file="test.dat"), - caosdb.Property("TestRevisionOf", datatype="TestObsolete"), - caosdb.RecordType("TestObsolete"), - caosdb.RecordType("TestRecordType"), - caosdb.Property("TestProperty", datatype=caosdb.TEXT), - caosdb.Record("TestRecord" - ).add_parent("TestRecordType" - ).add_property("TestProperty", "this is a test"), -]) - -testdata.insert() -os.remove("test.dat") diff --git a/misc/versioning_test_data.py b/misc/versioning_test_data.py index eaa83e46f61ea2f20263b487e4bb42c37678c94f..5ec7073aeaffc894916ee8a6c4cfdc82bc25a4f1 100755 --- a/misc/versioning_test_data.py +++ b/misc/versioning_test_data.py @@ -91,3 +91,8 @@ else: str(rec1.id), str(rec1.id)]) rec4.insert() + +for i in range(4,11): + rec1.name = f"TestRecord1-{i}thVersion" + rec1.description = f"This is the {i}th version." + rec1.update() diff --git a/misc/yaml_to_json.py b/misc/yaml_to_json.py index a7d5bd62a7a1ccc50766b797ef6710466e9bee11..e77e5efc56b2cea39b0a7b6f90236fb5b39da24e 100755 --- a/misc/yaml_to_json.py +++ b/misc/yaml_to_json.py @@ -6,4 +6,4 @@ import json import yaml with open(sys.argv[1], 'r') as infile: - print(json.dumps(yaml.load(infile))) + print(json.dumps(yaml.safe_load(infile))) diff --git a/model.svg b/model.svg new file mode 100644 index 0000000000000000000000000000000000000000..2602cb43f15976305d48e6f2d5efeb3821e1d669 --- /dev/null +++ b/model.svg @@ -0,0 +1,632 @@ +<?xml version="1.0" encoding="UTF-8" standalone="no"?> +<svg + xmlns:dc="http://purl.org/dc/elements/1.1/" + xmlns:cc="http://creativecommons.org/ns#" + xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#" + xmlns:svg="http://www.w3.org/2000/svg" + xmlns="http://www.w3.org/2000/svg" + xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd" + xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape" + contentScriptType="application/ecmascript" + contentStyleType="text/css" + height="502" + preserveAspectRatio="none" + version="1.1" + viewBox="0 0 407 502" + width="407" + zoomAndPan="magnify" + id="svg233" + sodipodi:docname="model.svg" + inkscape:version="0.92.4 5da689c313, 2019-01-14"> + <metadata + id="metadata237"> + <rdf:RDF> + <cc:Work + rdf:about=""> + <dc:format>image/svg+xml</dc:format> + <dc:type + rdf:resource="http://purl.org/dc/dcmitype/StillImage" /> + <dc:title></dc:title> + </cc:Work> + </rdf:RDF> + </metadata> + <sodipodi:namedview + pagecolor="#ffffff" + bordercolor="#666666" + borderopacity="1" + objecttolerance="10" + gridtolerance="10" + guidetolerance="10" + inkscape:pageopacity="0" + inkscape:pageshadow="2" + inkscape:window-width="1920" + inkscape:window-height="1043" + id="namedview235" + showgrid="false" + fit-margin-top="0" + fit-margin-left="0" + fit-margin-right="0" + fit-margin-bottom="0" + inkscape:zoom="1.1817368" + inkscape:cx="112.55875" + inkscape:cy="257" + inkscape:window-x="1920" + inkscape:window-y="0" + inkscape:window-maximized="1" + inkscape:current-layer="svg233" /> + <defs + id="defs11"> + <filter + height="3" + id="f64vrt8w3qxjw" + width="3" + x="-1" + y="-1"> + <feGaussianBlur + result="blurOut" + stdDeviation="2.0" + id="feGaussianBlur2" /> + <feColorMatrix + in="blurOut" + result="blurOut2" + type="matrix" + values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 .4 0" + id="feColorMatrix4" /> + <feOffset + dx="4.0" + dy="4.0" + in="blurOut2" + result="blurOut3" + id="feOffset6" /> + <feBlend + in="SourceGraphic" + in2="blurOut3" + mode="normal" + id="feBlend8" /> + </filter> + </defs> + <rect + style="fill:#ffffff;fill-opacity:1;stroke:none;stroke-width:1.13385832;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" + id="rect4867" + width="407" + height="502" + x="0" + y="0" /> + <polygon + id="polygon13" + style="fill:#dddddd;stroke:#000000;stroke-width:1.5;filter:url(#f64vrt8w3qxjw)" + points="533.5,526 126.5,526 126.5,24 236.5,24 243.5,46.2969 533.5,46.2969 " + transform="translate(-126.5,-24)" /> + <line + id="line15" + y2="22.296902" + y1="22.296902" + x2="117" + x1="0" + style="stroke:#000000;stroke-width:1.5" /> + <text + style="font-weight:bold;font-size:14px;font-family:sans-serif;fill:#000000" + id="text17" + y="38.995098" + x="130.5" + textLength="104" + lengthAdjust="spacingAndGlyphs" + font-weight="bold" + font-size="14" + transform="translate(-126.5,-24)">RecordTypes</text> + <text + style="font-size:11px;font-family:sans-serif;fill:#000000" + id="text42" + y="144.7104" + x="461" + textLength="0" + lengthAdjust="spacingAndGlyphs" + font-size="11" + transform="translate(-126.5,-24)" /> + <rect + y="411" + x="16" + width="116" + style="fill:#fefece;stroke:#a80036;stroke-width:1.5;filter:url(#f64vrt8w3qxjw)" + id="Manufacturer" + height="60.804699" /> + <circle + r="11" + id="ellipse47" + style="fill:#ff1111;stroke:#a80036;stroke-width:1" + cy="427" + cx="31" /> + <path + inkscape:connector-curvature="0" + id="path49" + d="m 33.9688,432.6406 q -0.5782,0.2969 -1.2188,0.4375 -0.6406,0.1563 -1.3437,0.1563 -2.5,0 -3.8282,-1.6406 -1.3125,-1.6563 -1.3125,-4.7813 0,-3.125 1.3125,-4.7812 1.3282,-1.6563 3.8282,-1.6563 0.7031,0 1.3437,0.1563 0.6563,0.1562 1.2188,0.4531 v 2.7187 q -0.625,-0.5781 -1.2188,-0.8437 -0.5937,-0.2813 -1.2187,-0.2813 -1.3438,0 -2.0313,1.0782 -0.6875,1.0625 -0.6875,3.1562 0,2.0938 0.6875,3.1719 0.6875,1.0625 2.0313,1.0625 0.625,0 1.2187,-0.2656 0.5938,-0.2813 1.2188,-0.8594 z" /> + <text + style="font-size:12px;font-family:sans-serif;fill:#000000" + id="text51" + y="455.1543" + x="171.5" + textLength="84" + lengthAdjust="spacingAndGlyphs" + font-size="12" + transform="translate(-126.5,-24)">Manufacturer</text> + <line + id="line53" + y2="443" + y1="443" + x2="131" + x1="17" + style="stroke:#a80036;stroke-width:1.5" /> + <text + style="font-size:11px;font-family:sans-serif;fill:#000000" + id="text55" + y="481.21039" + x="152.5" + textLength="0" + lengthAdjust="spacingAndGlyphs" + font-size="11" + transform="translate(-126.5,-24)" /> + <line + id="line57" + y2="463.80469" + y1="463.80469" + x2="131" + x1="17" + style="stroke:#a80036;stroke-width:1;stroke-dasharray:1, 2" /> + <rect + y="235" + x="16" + width="174" + style="fill:#fefece;stroke:#a80036;stroke-width:1.5;filter:url(#f64vrt8w3qxjw)" + id="MusicalInstrument" + height="101.6211" /> + <circle + r="11" + id="ellipse60" + style="fill:#ff1111;stroke:#a80036;stroke-width:1" + cy="251" + cx="43.600006" /> + <path + inkscape:connector-curvature="0" + id="path62" + d="m 46.5688,256.6406 q -0.5782,0.2969 -1.2188,0.4375 -0.6406,0.1563 -1.3437,0.1563 -2.5,0 -3.8282,-1.6406 -1.3125,-1.6563 -1.3125,-4.7813 0,-3.125 1.3125,-4.7812 1.3282,-1.6563 3.8282,-1.6563 0.7031,0 1.3437,0.1563 0.6563,0.1562 1.2188,0.4531 v 2.7187 q -0.625,-0.5781 -1.2188,-0.8437 -0.5937,-0.2813 -1.2187,-0.2813 -1.3438,0 -2.0313,1.0782 -0.6875,1.0625 -0.6875,3.1562 0,2.0938 0.6875,3.1719 0.6875,1.0625 2.0313,1.0625 0.625,0 1.2187,-0.2656 0.5938,-0.2813 1.2188,-0.8594 z" /> + <text + style="font-size:12px;font-family:sans-serif;fill:#000000" + id="text64" + y="279.1543" + x="186.89999" + textLength="114" + lengthAdjust="spacingAndGlyphs" + font-size="12" + transform="translate(-126.5,-24)">MusicalInstrument</text> + <line + id="line66" + y2="267" + y1="267" + x2="189" + x1="17" + style="stroke:#a80036;stroke-width:1.5" /> + <line + id="line68" + y2="281.40231" + y1="281.40231" + x2="73.5" + x1="17" + style="stroke:#a80036;stroke-width:1.5" /> + <text + style="font-size:11px;font-family:sans-serif;fill:#000000" + id="text70" + y="308.71039" + x="200" + textLength="59" + lengthAdjust="spacingAndGlyphs" + font-size="11" + transform="translate(-126.5,-24)">Properties</text> + <line + id="line72" + y2="281.40231" + y1="281.40231" + x2="189" + x1="132.5" + style="stroke:#a80036;stroke-width:1.5" /> + <text + style="font-size:11px;font-family:sans-serif;fill:#000000" + id="text74" + y="341.2222" + x="148.5" + textLength="86" + lengthAdjust="spacingAndGlyphs" + font-size="11" + transform="translate(-126.5,-24)">price (DOUBLE)</text> + <text + style="font-size:11px;font-family:sans-serif;fill:#000000" + id="text76" + y="354.02689" + x="148.5" + textLength="162" + lengthAdjust="spacingAndGlyphs" + font-size="11" + transform="translate(-126.5,-24)">Manufacturer (Manufacturer)</text> + <line + id="line78" + y2="300.60941" + y1="300.60941" + x2="62" + x1="17" + style="stroke:#a80036;stroke-width:1;stroke-dasharray:1, 2" /> + <text + style="font-size:11px;font-family:sans-serif;fill:#000000" + id="text80" + y="327.91751" + x="188.5" + textLength="82" + lengthAdjust="spacingAndGlyphs" + font-size="11" + transform="translate(-126.5,-24)">recommended</text> + <line + id="line82" + y2="300.60941" + y1="300.60941" + x2="189" + x1="144" + style="stroke:#a80036;stroke-width:1;stroke-dasharray:1, 2" /> + <rect + y="411" + x="167.5" + width="65" + style="fill:#fefece;stroke:#a80036;stroke-width:1.5;filter:url(#f64vrt8w3qxjw)" + id="Violin" + height="60.804699" /> + <circle + r="11" + id="ellipse85" + style="fill:#ff1111;stroke:#a80036;stroke-width:1" + cy="427" + cx="182.5" /> + <path + inkscape:connector-curvature="0" + id="path87" + d="m 185.4688,432.6406 q -0.5782,0.2969 -1.2188,0.4375 -0.6406,0.1563 -1.3437,0.1563 -2.5,0 -3.8282,-1.6406 -1.3125,-1.6563 -1.3125,-4.7813 0,-3.125 1.3125,-4.7812 1.3282,-1.6563 3.8282,-1.6563 0.7031,0 1.3437,0.1563 0.6563,0.1562 1.2188,0.4531 v 2.7187 q -0.625,-0.5781 -1.2188,-0.8437 -0.5937,-0.2813 -1.2187,-0.2813 -1.3438,0 -2.0313,1.0782 -0.6875,1.0625 -0.6875,3.1562 0,2.0938 0.6875,3.1719 0.6875,1.0625 2.0313,1.0625 0.625,0 1.2187,-0.2656 0.5938,-0.2813 1.2188,-0.8594 z" /> + <text + style="font-size:12px;font-family:sans-serif;fill:#000000" + id="text89" + y="455.1543" + x="323" + textLength="33" + lengthAdjust="spacingAndGlyphs" + font-size="12" + transform="translate(-126.5,-24)">Violin</text> + <line + id="line91" + y2="443" + y1="443" + x2="231.5" + x1="168.5" + style="stroke:#a80036;stroke-width:1.5" /> + <text + style="font-size:11px;font-family:sans-serif;fill:#000000" + id="text93" + y="481.21039" + x="304" + textLength="0" + lengthAdjust="spacingAndGlyphs" + font-size="11" + transform="translate(-126.5,-24)" /> + <line + id="line95" + y2="463.80469" + y1="463.80469" + x2="231.5" + x1="168.5" + style="stroke:#a80036;stroke-width:1;stroke-dasharray:1, 2" /> + <rect + y="397" + x="267.5" + width="119" + style="fill:#fefece;stroke:#a80036;stroke-width:1.5;filter:url(#f64vrt8w3qxjw)" + id="Guitar" + height="88.816399" /> + <circle + r="11" + id="ellipse98" + style="fill:#ff1111;stroke:#a80036;stroke-width:1" + cy="413" + cx="304.54999" /> + <path + inkscape:connector-curvature="0" + id="path100" + d="m 307.5188,418.6406 q -0.5782,0.2969 -1.2188,0.4375 -0.6406,0.1563 -1.3437,0.1563 -2.5,0 -3.8282,-1.6406 -1.3125,-1.6563 -1.3125,-4.7813 0,-3.125 1.3125,-4.7812 1.3282,-1.6563 3.8282,-1.6563 0.7031,0 1.3437,0.1563 0.6563,0.1562 1.2188,0.4531 v 2.7187 q -0.625,-0.5781 -1.2188,-0.8437 -0.5937,-0.2813 -1.2187,-0.2813 -1.3438,0 -2.0313,1.0782 -0.6875,1.0625 -0.6875,3.1562 0,2.0938 0.6875,3.1719 0.6875,1.0625 2.0313,1.0625 0.625,0 1.2187,-0.2656 0.5938,-0.2813 1.2188,-0.8594 z" /> + <text + style="font-size:12px;font-family:sans-serif;fill:#000000" + id="text102" + y="441.1543" + x="449.95001" + textLength="38" + lengthAdjust="spacingAndGlyphs" + font-size="12" + transform="translate(-126.5,-24)">Guitar</text> + <line + id="line104" + y2="429" + y1="429" + x2="385.5" + x1="268.5" + style="stroke:#a80036;stroke-width:1.5" /> + <line + id="line106" + y2="443.40231" + y1="443.40231" + x2="297.5" + x1="268.5" + style="stroke:#a80036;stroke-width:1.5" /> + <text + style="font-size:11px;font-family:sans-serif;fill:#000000" + id="text108" + y="470.71039" + x="424" + textLength="59" + lengthAdjust="spacingAndGlyphs" + font-size="11" + transform="translate(-126.5,-24)">Properties</text> + <line + id="line110" + y2="443.40231" + y1="443.40231" + x2="385.5" + x1="356.5" + style="stroke:#a80036;stroke-width:1.5" /> + <text + style="font-size:11px;font-family:sans-serif;fill:#000000" + id="text112" + y="503.2222" + x="400" + textLength="107" + lengthAdjust="spacingAndGlyphs" + font-size="11" + transform="translate(-126.5,-24)">electric (BOOLEAN)</text> + <line + id="line114" + y2="462.60941" + y1="462.60941" + x2="286" + x1="268.5" + style="stroke:#a80036;stroke-width:1;stroke-dasharray:1, 2" /> + <text + style="font-size:11px;font-family:sans-serif;fill:#000000" + id="text116" + y="489.91751" + x="412.5" + textLength="82" + lengthAdjust="spacingAndGlyphs" + font-size="11" + transform="translate(-126.5,-24)">recommended</text> + <line + id="line118" + y2="462.60941" + y1="462.60941" + x2="385.5" + x1="368" + style="stroke:#a80036;stroke-width:1;stroke-dasharray:1, 2" /> + <rect + y="255.5" + x="225.5" + width="165" + style="fill:#fefece;stroke:#a80036;stroke-width:1.5;filter:url(#f64vrt8w3qxjw)" + id="SoundQualityAnalyzer" + height="60.804699" /> + <circle + r="11" + id="ellipse121" + style="fill:#ff1111;stroke:#a80036;stroke-width:1" + cy="271.5" + cx="240.5" /> + <path + inkscape:connector-curvature="0" + id="path123" + d="m 243.4688,277.1406 q -0.5782,0.2969 -1.2188,0.4375 -0.6406,0.1563 -1.3437,0.1563 -2.5,0 -3.8282,-1.6406 -1.3125,-1.6563 -1.3125,-4.7813 0,-3.125 1.3125,-4.7812 1.3282,-1.6563 3.8282,-1.6563 0.7031,0 1.3437,0.1563 0.6563,0.1562 1.2188,0.4531 v 2.7187 q -0.625,-0.5781 -1.2188,-0.8437 -0.5937,-0.2813 -1.2187,-0.2813 -1.3438,0 -2.0313,1.0782 -0.6875,1.0625 -0.6875,3.1562 0,2.0938 0.6875,3.1719 0.6875,1.0625 2.0313,1.0625 0.625,0 1.2187,-0.2656 0.5938,-0.2813 1.2188,-0.8594 z" /> + <text + style="font-size:12px;font-family:sans-serif;fill:#000000" + id="text125" + y="299.6543" + x="381" + textLength="133" + lengthAdjust="spacingAndGlyphs" + font-size="12" + transform="translate(-126.5,-24)">SoundQualityAnalyzer</text> + <line + id="line127" + y2="287.5" + y1="287.5" + x2="389.5" + x1="226.5" + style="stroke:#a80036;stroke-width:1.5" /> + <text + style="font-size:11px;font-family:sans-serif;fill:#000000" + id="text129" + y="325.71039" + x="362" + textLength="0" + lengthAdjust="spacingAndGlyphs" + font-size="11" + transform="translate(-126.5,-24)" /> + <line + id="line131" + y2="308.30469" + y1="308.30469" + x2="389.5" + x1="226.5" + style="stroke:#a80036;stroke-width:1;stroke-dasharray:1, 2" /> + <rect + y="35" + x="20" + width="268" + style="fill:#fefece;stroke:#a80036;stroke-width:1.5;filter:url(#f64vrt8w3qxjw)" + id="Analysis" + height="140.0352" /> + <circle + r="11" + id="ellipse134" + style="fill:#ff1111;stroke:#a80036;stroke-width:1" + cy="51" + cx="124.75" /> + <path + inkscape:connector-curvature="0" + id="path136" + d="m 127.7188,56.6406 q -0.5782,0.2969 -1.2188,0.4375 -0.6406,0.1563 -1.3437,0.1563 -2.5,0 -3.8282,-1.6406 -1.3125,-1.6563 -1.3125,-4.7813 0,-3.125 1.3125,-4.7812 1.3282,-1.6563 3.8282,-1.6563 0.7031,0 1.3437,0.1563 0.6563,0.1562 1.2188,0.4531 v 2.7187 q -0.625,-0.5781 -1.2188,-0.8437 -0.5937,-0.2813 -1.2187,-0.2813 -1.3438,0 -2.0313,1.0782 -0.6875,1.0625 -0.6875,3.1562 0,2.0938 0.6875,3.1719 0.6875,1.0625 2.0313,1.0625 0.625,0 1.2187,-0.2656 0.5938,-0.2813 1.2188,-0.8594 z" /> + <text + style="font-size:12px;font-family:sans-serif;fill:#000000" + id="text138" + y="79.154297" + x="271.75" + textLength="50" + lengthAdjust="spacingAndGlyphs" + font-size="12" + transform="translate(-126.5,-24)">Analysis</text> + <line + id="line140" + y2="67" + y1="67" + x2="287" + x1="21" + style="stroke:#a80036;stroke-width:1.5" /> + <line + id="line142" + y2="81.402298" + y1="81.402298" + x2="124.5" + x1="21" + style="stroke:#a80036;stroke-width:1.5" /> + <text + style="font-size:11px;font-family:sans-serif;fill:#000000" + id="text144" + y="108.7104" + x="251" + textLength="59" + lengthAdjust="spacingAndGlyphs" + font-size="11" + transform="translate(-126.5,-24)">Properties</text> + <line + id="line146" + y2="81.402298" + y1="81.402298" + x2="287" + x1="183.5" + style="stroke:#a80036;stroke-width:1.5" /> + <text + style="font-size:11px;font-family:sans-serif;fill:#000000" + id="text148" + y="141.2222" + x="152.5" + textLength="134" + lengthAdjust="spacingAndGlyphs" + font-size="11" + transform="translate(-126.5,-24)">quality_factor (DOUBLE)</text> + <text + style="font-size:11px;font-family:sans-serif;fill:#000000" + id="text150" + y="154.0269" + x="152.5" + textLength="92" + lengthAdjust="spacingAndGlyphs" + font-size="11" + transform="translate(-126.5,-24)">date (DATETIME)</text> + <text + style="font-size:11px;font-family:sans-serif;fill:#000000" + id="text152" + y="166.8315" + x="152.5" + textLength="111" + lengthAdjust="spacingAndGlyphs" + font-size="11" + transform="translate(-126.5,-24)">report (REFERENCE)</text> + <text + style="font-size:11px;font-family:sans-serif;fill:#000000" + id="text154" + y="179.6362" + x="152.5" + textLength="256" + lengthAdjust="spacingAndGlyphs" + font-size="11" + transform="translate(-126.5,-24)">SoundQualityAnalyzer (SoundQualityAnalyzer)</text> + <text + style="font-size:11px;font-family:sans-serif;fill:#000000" + id="text156" + y="192.4409" + x="152.5" + textLength="220" + lengthAdjust="spacingAndGlyphs" + font-size="11" + transform="translate(-126.5,-24)">MusicalInstrument (MusicalInstrument)</text> + <line + id="line158" + y2="100.6094" + y1="100.6094" + x2="113" + x1="21" + style="stroke:#a80036;stroke-width:1;stroke-dasharray:1, 2" /> + <text + style="font-size:11px;font-family:sans-serif;fill:#000000" + id="text160" + y="127.9175" + x="239.5" + textLength="82" + lengthAdjust="spacingAndGlyphs" + font-size="11" + transform="translate(-126.5,-24)">recommended</text> + <line + id="line162" + y2="100.6094" + y1="100.6094" + x2="287" + x1="195" + style="stroke:#a80036;stroke-width:1;stroke-dasharray:1, 2" /> + <path + inkscape:connector-curvature="0" + style="fill:none;stroke:#a80036;stroke-width:1" + id="MusicalInstrument-Violin" + d="m 145.51,354.27 c 12.48,19.76 25.51,40.37 35.69,56.48" /> + <polygon + id="polygon211" + style="fill:none;stroke:#a80036;stroke-width:1" + points="261.26,361.26 277.86,374.43 266.03,381.91 " + transform="translate(-126.5,-24)" /> + <path + inkscape:connector-curvature="0" + style="fill:none;stroke:#a80036;stroke-width:1" + id="MusicalInstrument-Guitar" + d="m 192.64,348.42 c 25.04,17.17 51.64,35.39 74.51,51.06" /> + <polygon + id="polygon214" + style="fill:none;stroke:#a80036;stroke-width:1" + points="302.54,361.05 322.99,366.58 315.08,378.13 " + transform="translate(-126.5,-24)" /> + <path + inkscape:connector-curvature="0" + style="fill:none;stroke:#a80036;stroke-width:1" + id="MusicalInstrument-Manufacturer" + d="m 91.08,350.09 c -3.97,21 -8.2,43.41 -11.46,60.66" /> + <polygon + id="polygon217" + style="fill:#a80036;stroke:#a80036;stroke-width:1" + points="220,361.26 214.9551,366.4126 217.771,373.0512 222.8159,367.8986 " + transform="translate(-126.5,-24)" /> + <path + inkscape:connector-curvature="0" + style="fill:none;stroke:#a80036;stroke-width:1" + id="Analysis-SoundQualityAnalyzer" + d="m 222.3,185.39 c 21.35,24.82 43.61,50.69 60.09,69.84" /> + <polygon + id="polygon220" + style="fill:#a80036;stroke:#a80036;stroke-width:1" + points="340.04,199.21 340.9231,206.3668 347.869,208.3043 346.9859,201.1475 " + transform="translate(-126.5,-24)" /> + <path + inkscape:connector-curvature="0" + style="fill:none;stroke:#a80036;stroke-width:1" + id="Analysis-MusicalInstrument" + d="m 130.66,187.93 c -4.56,16 -9.21,32.33 -13.37,46.92" /> + <polygon + id="polygon223" + style="fill:#a80036;stroke:#a80036;stroke-width:1" + points="260.78,199.21 255.287,203.8819 257.4868,210.7493 262.9798,206.0774 " + transform="translate(-126.5,-24)" /> +</svg> diff --git a/src/core/css/tour.css b/src/core/css/tour.css index 3bb471369b698981f49858b8127652e957a11f8d..beec314ff8f8aa944e1fbb1fd9efd20e1fb17aa5 100644 --- a/src/core/css/tour.css +++ b/src/core/css/tour.css @@ -29,40 +29,44 @@ width: 5em; /* line-height: 50px; */ } -.caosdb-v-tour-button.small { - height: 2em; - width: 2em; +.caosdb-v-tour-button { + height: 1.5em; + width: 1.5em; /* line-height: 30px; */ } - + .caosdb-v-tour-highlight { background-color: #ff593c !important; color: white !important; animation: color-change 1s 2; } + .caosdb-v-tour-highlighter { color: #ff593c; animation: color-change 1s 3; } +.caosdb-v-tour-highlighter a:hover { + color: #212529; +} + @keyframes color-change { - 0% { + 0% { box-shadow: 0 0 0 0 #ff593c; - } - 50% { + } + 50% { box-shadow: 0 0 1em 0.5em #ff593c; - } - 100% { + } + 100% { box-shadow: 0 0 0 0 #ff593c; - } + } } .caosdb-v-tour-menu-entry-highlight { - background-color: #ff593c !important; + background-color: #348187 !important; color: white !important; } .caosdb-f-tour-menu-entry { background-color: #ff593c; - color: white !important; } .caosdb-f-tour-menu-entry:hover { background-color: transparent; @@ -88,9 +92,11 @@ /*}*/ .caosdb-f-tour-overview-entry.caosdb-v-tour-overview-entry-pageset { } +/* .caosdb-f-tour-overview-entry.caosdb-v-tour-overview-entry-page { display: none; } +*/ li.list-group-item > ul.list-group { margin-bottom: 0px; } @@ -155,3 +161,97 @@ li.list-group-item > .btn { color: LightSeaGreen; } +.caosdb-v-tour-overview { + padding: 0px; +} + +.caosdb-v-tour-toc-sidebar { + height: 100%; + width: 200px; + position: fixed; + top: 0; + left: 0; + background-color: #1a4548; + color: #e1eff0; + overflow-x: hidden; + padding-top: 20px; + visibility: hidden; +} + +.caosdb-v-tour-toc-sidebar a { + color: #e1eff0; +} + +.caosdb-v-tour-toc-sidebar a:hover { + color: #212529; +} + +body.tour-sidebar-visible .caosdb-v-tour-toc-sidebar { + visibility: visible; +} + +body.tour-sidebar-visible { + margin-left: 200px; +} + +.caosdb-v-tour-toc-active-item { + background-color: #a1c4c6; +} + +.caosdb-v-tour-toc-active-item a { + color: #333; +} + +.caosdb-v-tour-toc-pageset { + width: 100%; + text-align: left; + color: #e1eff0; +} +.caosdb-v-tour-pn-btn { + width:6em; +} + +button.caosdb-v-tour-toc-show { + position: fixed; + top: calc(50vh - 12px); + left: -2px; + padding: 0; + margin: 0; + border: none; + transform: rotate(45deg); + width: 24px; + height: 24px; + background-color: #1a4548; + visibility: hidden; + z-index: 100000; +} + +div.caosdb-v-tour-toc-show { + background-color: #1a4548; + visibility: hidden; + width: 12px; + height: 100%; + position: fixed; + padding: 0; + margin: 0; + border: none; + z-index: 100000; +} + +.caosdb-v-tour-toc-detour { + font-size: 0.875em; + padding-left: 1em; +} + +.caosdb-v-tour-toc-cur { + border: 1px dashed #e1eff0; +} + +.caosdb-v-tour-toc-active-item.caosdb-v-tour-toc-cur { + border: none; +} + +/* For elements in popovers which are not for clicking but only illustrative. */ +.caosdb-v-tour-unclickable { + cursor: text !important; +} diff --git a/src/core/css/webcaosdb.css b/src/core/css/webcaosdb.css index 9d7a72e4cbef02e59cb8f0c0540fbd80549255d2..4e5a0b6fc3d149012668b30cc671c3c7f083eb7b 100644 --- a/src/core/css/webcaosdb.css +++ b/src/core/css/webcaosdb.css @@ -22,11 +22,85 @@ */ @CHARSET "UTF-8"; + +main { +} + body { - display: flex; - flex-direction: column; + background-color: lightgrey; +} + +.background { + background-color: #1a4548; + min-height: 60vh; +} + +@media screen and (min-height: 1150px) { + .background { + min-height: 80vh; + } +} + + +footer { + background-color: lightgrey; + width:100%; +} + +.caosdb-v-server-message strong { + margin-right: 8px; +} + +div.export-data { + display: none; +} + +tr:not(:hover) .caosdb-v-entity-version-hint-cur { + color: #DDD; +} + +tr:hover .caosdb-v-entity-version-hint { + color: unset; +} + +.caosdb-v-entity-version-hint { + color: #DDD; +} + +tbody:not(:hover) tr .caosdb-v-entity-version-hint-cur { + color: unset; } +.caosdb-v-entity-version-no-related { + color: #DDD; +} + +.caosdb-v-entity-version-no-related:hover { + color: unset; +} + +.caosdb-v-state-model-label .caosdb-v-state-label { + margin-left: .6em; font-size: 100%; +} + +.caosdb-v-state-label:hover, +.caosdb-v-state-label:active { + filter: brightness(110%); +} + +.caosdb-v-bookmark-button, +.caosdb-v-bookmark-button:focus, +.caosdb-v-bookmark-button:hover { + color: #333; + top: 3px; +} + +.caosdb-v-bookmark-button:active, +.caosdb-v-bookmark-button.active { + color: red; +} + + .caosdb-v-navbar-toolbox li a:hover, .caosdb-v-navbar-toolbox li input:hover, .caosdb-v-navbar-toolbox li button:hover { @@ -57,20 +131,12 @@ body { text-align: center; } -button.caosdb-v-entity-version-button { - height: 15px; -} - .caosdb-v-entity-header-buttons-list > * { - margin: 0; - margin-left: 8px; padding: 0; - vertical-align: middle; } -.caosdb-v-main-col { - flex-grow: 1; - max-width: 90vw; +.caosdb-v-entity-property-attributes { + font-size: 0.75em; } .caosdb-v-show-only-child { @@ -95,6 +161,7 @@ button.caosdb-v-entity-version-button { * .caosdb-v-property-text-value */ .caosdb-property-text-value { white-space: pre-line; + background-color: transparent !important; } .caosdb-clickable:hover { @@ -117,23 +184,12 @@ button.caosdb-v-entity-version-button { font-size: 16px; } -.caosdb-f-main { - display: flex; - width: unset; -} -.caosdb-f-main-entities { - width: calc(100% - 5px); - min-width: 50vw; -} - .caosdb-show-preview-button, .caosdb-hide-preview-button { position: absolute; - top: 2px; - left: -8px; -} - -.caosdb-entity-heading-attr { - overflow-x: auto; + left: -0.65rem; + padding-left: 0; + padding-right: 0; + background-color: transparent !important; } a.label.caosdb-id-button:hover { @@ -144,8 +200,12 @@ h5 { margin: auto; } -.caosdb-properties-heading { - display: none; +.caosdb-f-main-entities { + max-width: 100%; +} + +.caosdb-f-main-entities-edit { + max-width: calc(100% - 240px); } .caosdb-entity-preview .caosdb-entity-panel-body { @@ -163,31 +223,42 @@ h5 { .caosdb-preview-carousel-nav { position: relative; } + +.caosdb-query-form { + width: 100%; +} + .caosdb-query-panel { - padding-top: 20px; - padding-bottom: 20px; + padding-top: 20px; + padding-bottom: 20px; } -.navbar-default .btn-link:hover { +.navbar-light .btn-link:hover { text-decoration: none; } -.navbar-default .btn-link:focus { +.navbar-light .btn-link:focus { text-decoration: none; } -.navbar-fixed-top, .navbar-fixed-bottom { - position: sticky; +.caosdb-v-property-left-col { + min-height: 30px; + display: inline-block; +} + +.caosdb-v-property-left-col > * { + margin-top: 3px; + margin-bottom: 3px; + display: inline-block; } .caosdb-property-name { - font-weight: bold; - margin-left: 8px; + font-weight: bold; } .caosdb-square { - position: relative; - overflow: hidden; + position: relative; + overflow: hidden; } .caosdb-entity-actions-panel { @@ -208,14 +279,9 @@ h5 { left: 0px; } -.caosdb-parent-name { - margin: 0 0.4em; - font-size: 14px; - color: black; -} - .caosdb-label-name { font-weight: bold; + text-decoration: none; } /* lists of values */ @@ -251,18 +317,18 @@ h5 { /* single boolean values */ .caosdb-boolean-true { - font-weight: bold; - font-size: 90%; - border: 1px solid #bbb; - padding: 2px 8px; - border-radius: 8px; + font-weight: bold; + font-size: 90%; + border: 1px solid #bbb; + padding: 0 5px; + border-radius: 8px; } .caosdb-boolean-false { font-weight: bold; font-size: 90%; border: 1px solid #bbb; - padding: 2px 8px; + padding: 0 5px; border-radius: 8px; } @@ -279,16 +345,14 @@ h5 { border-bottom-right-radius: 3px; } -.caosdb-entity-panel-body { - padding: 0px 10px; -} - -.caosdb-entity-panel-body > :first-child { - margin-top: 15px; +.coasdb-entity-version-attr, +.caosdb-entity-heading-attr { + overflow-x: auto; } +.coasdb-entity-version-attr-name, .caosdb-entity-heading-attr-name { - color: #6c6c6c; + color: #6c6c6c; font-size: 90%; margin-right: 0.3em; } @@ -318,25 +382,11 @@ h5 { padding: 5px; } -.caosdb-v-edit-list { - padding-left: 0px; -} - -.caosdb-v-editmode-existing { - height: 320.7px; +.caosdb-v-editmode-existing ul { + height: 280.0px; overflow-y: auto; } -.caosdb-v-edit-panel { - position: sticky; - top: 57px; - padding: 0px; - margin-top: 5px; - margin-left: 5px; - width: unset; - height: 800px; -} - .caosdb-v-editmode-btngroup { padding-bottom: 15px; } @@ -367,33 +417,22 @@ h5 { .caosdb-label-record { background-color: #F92108; - margin: -4px 0.2em 0 0.4em; + border: 1px solid #F92108; } .caosdb-label-recordtype { background-color: #00A32E; - margin-right: 8px; + border: 1px solid #00A32E; } .caosdb-label-property { background-color: #496DAB; - margin-right: 8px; + border: 1px solid #496DAB; } .caosdb-label-file { background-color: #C92E86; - margin-right: 8px; -} - -.label.caosdb-id-button { - background-color: #4E5752; -} - -.caosdb-properties-heading { - padding-top: 2px; - padding-bottom: 2px; - background-color: #f5f5f5; - color: #7c7c7c; + border: 1px solid #C92E86; } .caosdb-parents-heading { @@ -412,13 +451,13 @@ h5 { .caosdb-parent-item { border: 1px solid #666; - border-radius: 1ex; - padding: .3ex .2em .3ex; - margin: 0 0.5em; + color: black; + text-decoration: none; } -.caosdb-f-parent-list { - margin: 0 0.5em 0 auto; +.caosdb-parent-item a { + color: black; + text-decoration: none; } .caosdb-unit { @@ -427,44 +466,10 @@ h5 { margin-left: 0.3em; } -.navbar-brand { - display: flex; - align-items: center; -} - -.navbar-brand>img { - padding: 0px 0px; - height: 100% -} - .caosdb-fs-cwd::before { content: " > "; } -.caosdb-fs-dir>.glyphicon::before { - content: "\e117"; -} - -.caosdb-fs-dir>.glyphicon { - margin-right: 8px; -} - -.caosdb-fs-dir:hover>.glyphicon::before { - content: "\e118"; -} - -.caosdb-fs-file>.glyphicon::before { - content: "\e022"; -} - -.caosdb-fs-file>.glyphicon { - margin-right: 8px; -} - -.caosdb-fs-file:hover>.glyphicon::before { - content: "\e025"; -} - .caosdb-fs-btn-file { padding: 0px; background-color: transparent; @@ -502,36 +507,6 @@ h5 { border-left: 0px solid #7c7c7c; } -.caosdb-paging-panel { - padding-left: 0px; - padding-right: 0px; -} - -.caosdb-pagination { - margin: 5px 15px; -} - -.caosdb-pagination-navbar { - padding-bottom: 5px; - position: fixed; - bottom: 0px; - width: 100%; - border: 0px; - border-top: 1px solid #e7e7e7; - z-index: 1000; - position: fixed; -} - -.caosdb-heading { - color: #5e5e5e; - background-color: #f8f8f8; - border-bottom: 1px solid #e7e7e7; -} - -.caosdb-heading>.container { - padding: 20px 0px; -} - .spinning { animation: spin 2s linear infinite; } @@ -543,7 +518,10 @@ h5 { .caosdb-v-property-row { animation: appear 0.5s 1; - padding: 0.3ex 1em; + padding-left: 2.4rem; + padding-right: 1rem; + padding-top: 0.3ex; + padding-bottom: 0.3ex; } @keyframes appear { @@ -577,16 +555,198 @@ input[type="file"] { min-height: 22px; } -footer { - background-color: lightgrey; - padding: 0.5em; +.caosdb-v-property-value-inputs > textarea { + width: 100%; +} + +.caosdb-v-property-value-inputs li > textarea { + width: calc(100% - 40px); +} + +.caosdb-v-edit-mode-property-dropzone { + list-style: none; + text-align: center; + color: #69c2df; + border: 2px dashed #69c2df; + padding: 25px 0; + margin: 1rem; + margin-top: 20px; + margin-bottom: 0px; +} + +.caosdb-v-edit-mode-property-dropzone:hover { + filter: brightness(80%); +} + +.caosdb-v-edit-mode-parent-dropzone { + position: relative; + display: block; + color: #69c2df; + border: 2px dashed #69c2df; + padding-top: 15px; + padding-bottom: 15px; + padding-left: 5px; + padding-right: 15px; + margin: 0; +} + +.caosdb-v-edit-mode-parent-dropzone:hover { + filter: brightness(80%); +} + +.caosdb-v-edit-mode-highlight { + color: #333; + background-color: #d3fdd3; + border: 2px solid #d3fdd3; } -.caosdb-footer-element { - margin: 1em; +.caosdb-v-edit-mode-parent-dropzone div:first-child { + font-size: 80%; + position: absolute; + top: 0px; + right: 0px; + margin: 0px; +} + +.caosdb-v-property-value-inputs .caosdb-v-edit-value-list-buttons > button { + padding: 1px; +} + +.caosdb-v-property-other-inputs * + * { + margin-left: 6px; +} + +.caosdb-v-property-other-inputs label * { + margin-left: 6px; +} + +.caosdb-v-property-other-inputs { + margin-top: 6px; + margin-bottom: 6px; } .caosdb-bulletsep { margin-left: 2ch; margin-right: 2ch; } + +details summary .dropdown { + display:none; +} +details[open] summary .dropdown { + display:block; +} + +.modal-dialog { + top:80px; +} + +details summary { + cursor: pointer; + display: list-item; +} + +details summary > * { + display: inline; +} + +details p { + margin-bottom: 0.2rem; +} + +.caosdb-v-tour-overview-entry-page a { + text-decoration: none; + padding: .1875rem .5rem; + margin-top: .125rem; + margin-left: 1.25rem; +} + +.caosdb-next-button, +.caosdb-prev-button { + visibility: hidden; +} + +.caosdb-next-button[href], +.caosdb-prev-button[href] { + visibility: visible; +} + +.caosdb-f-paging-panel { + display: none; + height: 31px; +} + +.caosdb-f-show-paging-panel .caosdb-f-paging-panel { + display: flex; + justify-content: space-between; +} + +.caosdb-properties { + margin-left: -1rem; + margin-right: -1rem; +} + +.caosdb-v-state-label { + background-color: #333; +} + +.caosdb-v-state-model-label { + background-color: #666; + padding-top: 0; + padding-bottom: 0; + padding-right: 0; + font-size: inherit; +} + +.caosdb-v-tour-popover-close-button { + padding: 0; + position: absolute; + top: 0.6rem; + right: 0.6rem; + font-size: 1rem; +} + +.caosdb-f-property-value { + position: relative; + margin-top: auto; + margin-bottom: auto; +} + +.caosdb-f-reference-value { + background-color: white; + padding: 0.2rem 0.5rem; +} + +.caosdb-f-edit { + display: none; + position: relative; + width: 240px; + flex-shrink: 0; +} + +.caosdb-v-edit-panel { + padding: 0px; + height: min-content; + position: fixed; + width: 240px; +} + +.caosdb-f-show-paging-panel .caosdb-v-edit-panel { + margin-top: 39px; +} + +.caosdb-query-response.card .card-header { + border: none; +} + +.caosdb-f-entity-state-transition-button:hover { + filter: brightness(110%); +} + +.caosdb-f-entity-state-transition-button { + border: none; +} + +.caosdb-f-map-panel .leaflet-container { + height: 500px; +} diff --git a/src/core/css/webcaosdb.less b/src/core/css/webcaosdb.less index 5ecb7fdf28a4cf3e5f93c78863faaef40e34e385..54e9113df34232234ed6814a4383cf7ba954e121 100644 --- a/src/core/css/webcaosdb.less +++ b/src/core/css/webcaosdb.less @@ -83,11 +83,11 @@ a.label.caosdb-id-button:hover { padding-bottom: 20px; } -.navbar-default .btn-link:hover { +.navbar-light .btn-link:hover { text-decoration: none; } -.navbar-default .btn-link:focus { +.navbar-light .btn-link:focus { text-decoration: none; } diff --git a/src/core/js/annotation.js b/src/core/js/annotation.js index 685930da76d73980985210484a60f51690e45a46..dd75a81180d2081fd992d168ff3e1b5f43492f06 100644 --- a/src/core/js/annotation.js +++ b/src/core/js/annotation.js @@ -36,12 +36,12 @@ this.annotation = new function() { this.createNewCommentForm = function(entityId) { var form = $('<form class="caosdb-new-comment-form">' + '<input type="hidden" name="annotationOf" value="' + entityId + '"> ' + - '<div class="form-group">' + + '<div class="form-control">' + '<label for="comment">Your new comment:</label>' + '<textarea class="form-control" rows="5" name="newComment" title="Your comment with 5 or more characters." pattern=".{5,}"></textarea>' + '</div>' + - '<button class="btn btn-default" title="Submit this comment." type="submit" name="submit" value="Submit">Submit</button>' + - '<button class="btn btn-default" title="Cancel this comment." type="reset" name="cancel" value="Cancel">Cancel</button>' + + '<button class="btn btn-secondary" title="Submit this comment." type="submit" name="submit" value="Submit">Submit</button>' + + '<button class="btn btn-secondary" title="Cancel this comment." type="reset" name="cancel" value="Cancel">Cancel</button>' + '</form>'); return form[0]; } @@ -117,7 +117,7 @@ this.annotation = new function() { this.createError = function(error) { var ret = $('<div class="alert alert-danger caosdb-new-comment-error alert-dismissable">' + - '<button class="close" data-dismiss="alert" aria-label="close">×</button>' + + '<button class="btn-close" data-bs-dismiss="alert" aria-label="close"></button>' + '<strong>' + error.name + '!</strong> ' + error.message + '<p class="small"><pre><code>' + (error.stack ? error.stack : "") + '</code></pre></p></div>')[0]; return ret; @@ -390,9 +390,15 @@ this.annotation = new function() { } this.loadComments = async function(annotationSection) { - var entityId = annotation.getEntityId(annotationSection); - var annotations = await annotation.getAnnotationsForEntity(entityId, annotation.queryAnnotation, annotation.loadAnnotationXsl(connection.getBasePath())); + const entityId = annotation.getEntityId(annotationSection); + const annotations = await annotation.getAnnotationsForEntity(entityId, annotation.queryAnnotation, annotation.loadAnnotationXsl(connection.getBasePath())); + const len = annotations.length; $(annotationSection).append(annotations); + if (len > 0) { + const button = $(`#${entityId} .caosdb-v-entity-header-buttons-list .caosdb-v-entity-comment-badge`); + button.empty(); + button.append(`<span class="badge bg-dark caosdb-v-entity-number-of-comments">${len}<i class="bi-chat-left-fill ms-1"></i></span>`); + } } /** @@ -427,4 +433,8 @@ this.annotation = new function() { } }; -$(document).ready(annotation.init); +$(document).ready(function() { + //if ("${BUILD_MODULE_EXT_ANNOTATION}" == "ENABLED") { + caosdb_modules.register(annotation); + //} +}); diff --git a/src/core/js/autocomplete.js b/src/core/js/autocomplete.js deleted file mode 100644 index 345d06b37bc6456645ef02947c70b3f2f5d6c44f..0000000000000000000000000000000000000000 --- a/src/core/js/autocomplete.js +++ /dev/null @@ -1,109 +0,0 @@ -/* - * ** header v3.0 - * This file is a part of the CaosDB Project. - * - * Copyright (C) 2019 IndiScale GmbH - * - * 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/>. - * - * ** end header - */ - -'use strict'; - -var autocomplete = new function() { - this.version = "0.1"; - - /** - * @type {Promise} promise for a list of entity names. - */ - this.candidates = undefined; - var logger = log.getLogger("autocomplete"); - this.init = function() { - this.candidates = undefined; - this.toggle_completion(); - logger.setLevel("trace"); - }; - - this.retrieve_names = async function () { - var response = $(await connection.get(transaction.generateEntitiesUri( - ["names"]))).find("Response [name]") - - return response.toArray().map(x=> $(x).attr("name")); - }; - - /** - * @return {Promise} promise for a list of entity names. - */ - this.get_candidates = function () { - - if (typeof this.candidates === "undefined"){ - - this.candidates = this.retrieve_names(); - }; - return this.candidates; - }; - - this.filter = function (x, qry){ - return x.toLowerCase().startsWith(qry.toLowerCase()) - }; - - this.search = async function (qry, callback, origJQElement) { - const names = await autocomplete.get_candidates(); - callback(names.filter(x => autocomplete.filter(x, qry))); - }; - - this.typed = function (newValue, origJQElement) { - var cursorpos = origJQElement[0].selectionEnd; - var beginning_of_word = newValue.slice(0, cursorpos).lastIndexOf(" "); - var word = newValue.slice(beginning_of_word+1, cursorpos); - return word; - }; - - this.searchPost = function (resultsFromServer, origJQElement) { - var cursorpos = origJQElement[0].selectionEnd; - var newValue = origJQElement[0].value - var beginning_of_word = newValue.slice(0, cursorpos).lastIndexOf( - " "); - var start = newValue.slice(0, beginning_of_word+1); - var end = origJQElement[0].value.slice(cursorpos); - var result = resultsFromServer.map( x => { - return { - text: start + x + end, - html: x - }}); - return result; - }; - - this.toggle_completion = function () { - var field = $("#caosdb-query-textarea"); - field.toggleClass("basicAutoComplete", true); - field.autoComplete({ - events: { - search: this.search, - typed: this.typed, - searchPost: this.searchPost, - } - } - ); - return field; - }; -}; - -// this will be replaced by require.js in the future. -$(document).ready(function() { - if ("${BUILD_MODULE_EXT_AUTOCOMPLETE}" === "ENABLED") { - caosdb_modules.register(autocomplete); - } -}); diff --git a/src/core/js/caosdb.js b/src/core/js/caosdb.js index 7fe4e9441822bf6c706a09c95aa65f0a2fef06a8..e5f77ab21d27de1e8a36d6b5ba1d09994934f343 100644 --- a/src/core/js/caosdb.js +++ b/src/core/js/caosdb.js @@ -1,30 +1,32 @@ /* -* ** header v3.0 -* This file is a part of the CaosDB Project. -* -* Copyright (C) 2019 IndiScale GmbH -* -* 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/>. -* -* ** end header -*/ + * ** header v3.0 + * This file is a part of the CaosDB Project. + * + * Copyright (C) 2018-2020 Alexander Schlemmer + * Copyright (C) 2018 Research Group Biomedical Physics, + * Max-Planck-Institute for Dynamics and Self-Organization Göttingen + * Copyright (C) 2019-2020 IndiScale GmbH (info@indiscale.com) + * Copyright (C) 2019-2020 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/>. + * + * ** end header + */ 'use strict'; /** * JavaScript client for CaosDB - * A. Schlemmer, 08/2018 - * T. Fitschen, 02/2019 * * Dependency: jquery * Dependency: webcaosdb @@ -69,8 +71,7 @@ function getUserRealm() { * @return Array containing the roles of the user. */ function getUserRoles() { - return Array.from(document.getElementsByClassName("caosdb-user-role") - ).map(el => el.innerText); + return Array.from(document.getElementsByClassName("caosdb-user-role")).map(el => el.innerText); } /** @@ -189,9 +190,9 @@ function getPropertyDatatype(element) { x => x.classList.contains("caosdb-property-datatype"), x => x.classList.contains("caosdb-preview-container")); - if(dt_elem.length == 1){ + if (dt_elem.length == 1) { return $(dt_elem[0]).text(); - } else if (dt_elem.length > 1){ + } else if (dt_elem.length > 1) { throw new Error("The datatype of this property could not uniquely be determined."); } @@ -219,14 +220,63 @@ function getEntityName(element) { } /** - * Return the path of element. + * Return the path of an entity. + * + * This attribute is always set for file entities. + * * If the corresponding label can not be found or the label is ambigious undefined is returned. - * @return A string containing the name of the element. + * + * @param {HTMLElement} entity - entity in HTML representation. + * @return A string containing the path of the entity. */ function getEntityPath(element) { + const path = $(element).find('.caosdb-f-entity-path').val(); + if (typeof path !== 'undefined') { + return path; + } + return getEntityHeadingAttribute(element, "path"); } +/** + * Return the checksum of an entity. + * + * This attribute is always set for file entities. + * + * If the corresponding label can not be found or the label is ambigious undefined is returned. + * + * @param {HTMLElement} entity - entity in HTML representation. + * @return A string containing the checksum of the entity. + */ +function getEntityChecksum(element) { + const checksum = $(element).find('.caosdb-f-entity-checksum').val(); + if (typeof checksum !== 'undefined') { + return checksum; + } + + return getEntityHeadingAttribute(element, "checksum"); +} + +/** + * Return the size of element. This attribute is always set for file entities. + * If the corresponding label can not be found or the label is ambigious undefined is returned. + * @return A string containing the size of the element. + */ +function getEntitySize(element) { + // TODO: check if this if block is needed + // it is analogous to getEntityDescription + // if ($(element).find('[data-entity-size]').length == 1) { + // return $(element).find('[data-entity-size]')[0].dataset.entitySize; + // } + + if (typeof $(element).find('.caosdb-f-entity-size').val() !== 'undefined') { + // This is needed for the edit mode to work properly: + return $(element).find('.caosdb-f-entity-size').val(); + } + + return getEntityHeadingAttribute(element, "size"); +} + /** * Return the id of an entity. * @param element The element holding the entity. @@ -286,6 +336,32 @@ function input2caosdbDate(date, time) { return date + "T" + time; } +/** + * Return true if the current user has the given permission for the given + * entity. + * + * @param {HTMLElement} entity + * @return {boolean} + */ +var hasEntityPermission = function (entity, permission) { + if (userHasRole("administration")) { + // administration is a special role. It has * permissions. + return true; + } + const permissions = getAllEntityPermissions(entity); + return permissions.indexOf(permission.toUpperCase()) > -1; +} + +/** + * Get all permissions the current user has for this entity. + * @param {HTMLElement} entity + * @return {string[]} array of permissions. + */ +var getAllEntityPermissions = function (entity) { + const permissions = $(entity).find("[data-permission]").toArray().map(x => x.getAttribute("data-permission")); + return permissions; +} + /** * Take a datetime from caosdb and return a date and a time * suitable for html inputs. @@ -355,6 +431,7 @@ function getEntityDescription(element) { if ($(element).find('[data-entity-description]').length == 1) { return $(element).find('[data-entity-description]')[0].dataset.entityDescription; } else if (typeof $(element).find('.caosdb-f-entity-description').val() !== 'undefined') { + // This is needed for the edit mode to work properly: return $(element).find('.caosdb-f-entity-description').val(); } @@ -423,7 +500,7 @@ function getEntityXML(ent_element) { function getPropertyName(element) { var name_element = element.getElementsByClassName("caosdb-property-name"); - if(name_element.length > 0) { + if (name_element.length > 0) { return name_element[0].textContent; } else if ($(element).is("[data-property-name]")) { return $(element).attr("data-property-name"); @@ -522,7 +599,7 @@ function getPropertyFromElement(propertyelement, names = undefined) { var value_string = undefined; if (valel && valel.textContent.length > 0) { value_string = valel.textContent; - } else if (valel && valel.value && valel.value.length > 0 ) { + } else if (valel && valel.value && valel.value.length > 0) { value_string = valel.value; } @@ -539,7 +616,7 @@ function getPropertyFromElement(propertyelement, names = undefined) { if (typeof value_string !== "undefined") { // This is set to true, when there is a reference or a list of references: - if(typeof property.reference === "undefined") { + if (typeof property.reference === "undefined") { property.reference = (valel.getElementsByClassName("caosdb-id").length > 0); } @@ -555,7 +632,7 @@ function getPropertyFromElement(propertyelement, names = undefined) { } } else { // list of anything but references - listel = findElementByConditions(valel, x => x.classList.contains("list-group-item"), + listel = findElementByConditions(valel, x => x.classList.contains("list-inline-item"), x => x.classList.contains("caosdb-preview-container")); for (var j = 0; j < listel.length; j++) { property.value.push(listel[j].textContent); @@ -620,6 +697,91 @@ function getProperties(element) { return list; } + +/** + * Construct XPath expression from selectors. + * + * Used by getPropertyValues. + * + * @param {String[][]} selectors + * @return {String[]} XPath expressions. + */ +var _constructXpaths = function (selectors) { + const xpaths = []; + for (let sel of selectors) { + var expr = "Property"; + if (sel[0] == "id") { + expr = ""; + } + for (let i = 0; i < sel.length; i++) { + const segment = sel[i]; + if (segment == "id") { + expr += `@id`; + } else if (segment) { + expr += `[@name='${segment}']`; + } + + if (i+1 < sel.length) { + expr += "//Property" + } + } + xpaths.push(expr); + } + return xpaths; +} + +/** + * Return a table where each row represents an entity and each column a property. + * + * This also works for entities from select queries, where the properties + * are deeply nested, e.g. when each entity references a "Geo Location" + * record which have latitude and longitude properties: + * + * `getPropertyValues(entities, [["Geo Location", "latitude"], ["Geo Location", "longitude"]])` + * + * Use empty strings for selector elements when the property name is irrelevant: + * + * `getPropertyValues(entities, [["", "latitude"], ["", "longitude"]])` + * + * Limitations: + * + * 1. Currently, this implementation assumes that properties (and subproperties + * for that matter) have unique names, entity-wide and do have a LIST + * datatype. + * + * 2. It only handles one of the many special cases, which is "id". Other + * special cases ("name", "description", "unit", etc.) are to be added when + * needed. + * + * @param {XMLElement[]) entities + * @param {String[][]} selectors + * @return {String[][]} A table of the property values for each entity. + */ +var getPropertyValues = function (entities, selectors) { + const entity_iter = entities.evaluate("/Response/Record", entities); + + const table = []; + const xpaths = _constructXpaths(selectors) + + var current_entity = entity_iter.iterateNext(); + while (current_entity) { + const row = []; + for (let expr of xpaths) { + const property = entities.evaluate(expr, current_entity).iterateNext(); + if (typeof property != "undefined" && property != null) { + row.push(property.textContent.trim()); + } else { + row.push(undefined) + } + + } + table.push(row); + current_entity = entity_iter.iterateNext(); + } + + return table; +} + /** * Sets a property with some basic type checking. * @@ -643,7 +805,7 @@ function setPropertySafe(valueelement, property, propold) { } else { finalstring = ''; for (var i = 0; i < property.value.length; i++) { - finalstring += '<a class="btn btn-default btn-sm caosdb-resolvable-reference" href="' + serverstring + property.value[i] + '"><span class="caosdb-id">' + property.value[i] + '</span><span class="caosdb-resolve-reference-target" /></a>'; + finalstring += '<a class="btn btn-secondary btn-sm caosdb-resolvable-reference" href="' + serverstring + property.value[i] + '"><span class="caosdb-id">' + property.value[i] + '</span><span class="caosdb-resolve-reference-target" /></a>'; } } valueelement.getElementsByClassName("caosdb-value-list")[0].getElementsByClassName("caosdb-overflow-content")[0].innerHTML = finalstring; @@ -657,14 +819,14 @@ function setPropertySafe(valueelement, property, propold) { ael.setAttribute("href", serverstring + property.value); ael.innerHTML = '<span class="caosdb-id">' + property.value + '</span><span class="caosdb-resolve-reference-target" />'; } else { - finalstring = '<a class="btn btn-default btn-sm caosdb-resolvable-reference" href="' + serverstring + property.value + '"><span class="caosdb-id">' + property.value + '</span><span class="caosdb-resolve-reference-target" /></a>'; + finalstring = '<a class="btn btn-secondary btn-sm caosdb-resolvable-reference" href="' + serverstring + property.value + '"><span class="caosdb-id">' + property.value + '</span><span class="caosdb-resolve-reference-target" /></a>'; valueelement.innerHTML = finalstring; preview.init(); } } else { /* DEPRECATED css class .caosdb-property-text-value - Use - * .caosdb-f-property-single-raw-value or introduce new - * .caosdb-v-property-text-value */ + * .caosdb-f-property-single-raw-value or introduce new + * .caosdb-v-property-text-value */ valueelement.innerHTML = "<span class='caosdb-property-text-value'>" + property.value + "</span>"; } } @@ -711,7 +873,7 @@ function setProperty(element, property) { * equivalent). * @returns {string} The value of the the property with property_name or `undefined` when this property is not available for this entity. */ -function getProperty(element, property_name, case_sensitive=true) { +function getProperty(element, property_name, case_sensitive = true) { var props; if (case_sensitive) { props = getProperties(element).filter(el => el.name == property_name); @@ -802,23 +964,15 @@ function appendProperty(doc, element, property, append_datatype = false) { /** - * Return a new Document or DocumentFragment, depending on the availability of the latter. + * Return a new Document. * * Helper function. * * @param {string} root - the new root element. - * @returns {(Document|DocumentFragement)} the new document. - */ + * @returns {Document} the new document. + */ function _createDocument(root) { - var doc = undefined; - if (window.DocumentFragment) { - doc = new DocumentFragment(); - const rootNode = document.createElementNS(undefined, root); - doc.append(rootNode); - } else { - doc = document.implementation.createDocument(null, root, null); - } - return doc; + return document.implementation.createDocument(null, root, null); } @@ -827,17 +981,20 @@ function _createDocument(root) { * This function uses the object notation. * @see getProperties * @see getParents - * @param role Record, RecordType or Property + * @param role Record, RecordType, Property or File (in case of files the three file arguments must be used!) * @param name The name of the entity. Can be undefined. * @param id The id of the entity. Can be undefined. * @param properties A list of properties. * @param parents A list of parents. + * @param description A description for this entity. * @return {Document|DocumentFragment} - An xml document holding the newly * created entity. * */ function createEntityXML(role, name, id, properties, parents, - append_datatypes = false, datatype = undefined, description = undefined, unit = undefined) { + append_datatypes = false, datatype = undefined, description = undefined, + unit = undefined, + file_path = undefined, file_checksum = undefined, file_size = undefined) { var doc = _createDocument(role); var nelnode = doc.children[0]; @@ -869,9 +1026,49 @@ function createEntityXML(role, name, id, properties, parents, appendProperty(doc, nelnode, properties[i], append_datatypes); } } + + if (role.toLowerCase() == "file") { + /* + File path, checksum and size are needed for File entities. + + An error is raised when these arguments are not set. + */ + if (file_path === undefined || file_checksum === undefined || file_size === undefined) { + throw "Path, checksum and size must not be undefined in case of file entities."; + } + + $(nelnode).attr("path", file_path); + $(nelnode).attr("checksum", file_checksum); + $(nelnode).attr("size", file_size); + } return doc; } +/** + * Create an XML for a file entity. + * This is a convenience function for creating XML from file entities. + * This function uses the object notation. + * @see getProperties + * @see getParents + * @param name The name of the entity. Can be undefined. + * @param id The id of the entity. Can be undefined. + * @param parents A list of parents. + * @param file_path The path of the file in the CaosDB file system. + * @param file_checksum The checksum of the file. + * @param file_size The size of the file in bytes. + * @param description A description for this entity. + * @return {Document|DocumentFragment} - An xml document holding the newly + * created entity. + * + */ +function createFileXML(name, id, parents, + file_path, file_checksum, file_size, + description = undefined) { + return createEntityXML("File", name, id, {}, parents, + false, undefined, description, undefined, + file_path, file_checksum, file_size); +} + /** * Helper function to wrap xml documents into another node which could e.g. be * Update, Response, Delete. @@ -885,7 +1082,7 @@ function wrapXML(root, xmls) { caosdb_utils.assert_string(root, "param `root`"); var doc = _createDocument(root); - for (var i=0; i < xmls.length; i++) { + for (var i = 0; i < xmls.length; i++) { doc.firstElementChild.appendChild(xmls[i].firstElementChild); } diff --git a/src/core/js/edit_mode.js b/src/core/js/edit_mode.js index 0c52f403d5d9b2f0b2feb60a9dc21403c12dc0b2..8644fff20894cc775919c8dfe76f49cec96460fb 100644 --- a/src/core/js/edit_mode.js +++ b/src/core/js/edit_mode.js @@ -42,6 +42,11 @@ var edit_mode = new function() { */ this.start_edit = new Event("caosdb.edit_mode.start_edit") + /** + * Fired by the entity element after editing (canceled or saved). + */ + this.end_edit = new Event("caosdb.edit_mode.end_edit") + /** * Fired by a list-property when an (input) element is added to the list during editing. */ @@ -58,20 +63,31 @@ var edit_mode = new function() { */ this.property_data_type_changed = new Event("caosdb.edit_mode.property_data_type_changed"); + /** + * Initialize this module + */ this.init = function() { if (isAuthenticated()) { - var target = $("#top-navbar").find("ul").first(); - this.add_edit_mode_button(target, edit_mode.toggle_edit_mode); - if (this.is_edit_mode()) { - edit_mode.enter_edit_mode(); - edit_mode.toggle_edit_panel(); - } - $('.caosdb-f-edit').css("transition", "top 1s"); + this._init(); } else { window.localStorage.removeItem("edit_mode"); } } + this._init = function () { + var target = $("#top-navbar").find("ul").first(); + this.add_edit_mode_button(target, edit_mode.toggle_edit_mode); + if (this.is_edit_mode()) { + edit_mode.enter_edit_mode(); + edit_mode.toggle_edit_panel(); + // This is for the very specific case of reloading the + // page while the edit mode is active on small screens + $(".caosdb-edit-min-width-warning").removeClass("d-none"); + $(".caosdb-edit-min-width-warning").addClass("d-block"); + } + $('.caosdb-f-edit').css("transition", "top 1s"); + } + this.dragstart = function(e) { e.dataTransfer.setData("text/plain", e.target.id); @@ -101,34 +117,47 @@ var edit_mode = new function() { if (typeof new_prop === "undefined" || !(new_prop instanceof HTMLElement)) { throw new TypeError("new_prop must instantiate HTMLElement"); } - var rt = entity.getElementsByClassName("caosdb-properties")[0]; - rt.appendChild(new_prop); + const drop_zone = $(entity).find(".caosdb-properties").find(".caosdb-f-edit-mode-property-dropzone"); + drop_zone.before(new_prop); make_property_editable_cb(new_prop); new_prop.dispatchEvent(edit_mode.property_added); } - this.add_dropped_property = function(e, panel) { + + /** + * Add a dropped property to the entity. + * + * @param {Event} e - the drop event. + * @param {HTMLElement} entity - the entity. + */ + this.add_dropped_property = function(e, entity) { var propsrcid = e.dataTransfer.getData("text/plain"); var tmp_id = propsrcid.split("-"); var prop_id = tmp_id[tmp_id.length - 1]; var entity_type = tmp_id[tmp_id.length - 2]; if (entity_type == "p") { retrieve_dragged_property(prop_id).then(new_prop_doc => { - edit_mode.add_new_property(panel, new_prop_doc.firstChild); + edit_mode.add_new_property(entity, new_prop_doc.firstChild); }, edit_mode.handle_error); } else if (entity_type == "rt") { var name = $("#" + propsrcid).text(); var dragged_rt = str2xml('<Response><Property id="' + prop_id + '" name="' + name + '" datatype="' + name + '"></Property></Response>'); transformation.transformProperty(dragged_rt).then(new_prop_doc => { - edit_mode.add_new_property(panel, new_prop_doc.firstChild); + edit_mode.add_new_property(entity, new_prop_doc.firstChild); }, edit_mode.handle_error); } } - this.add_dropped_parent = function(e, panel) { + /** + * Add a dropped parent to the entity. + * + * @param {Event} e - the drop event. + * @param {HTMLElement} entity - the entity. + */ + this.add_dropped_parent = function(e, entity) { var propsrcid = e.dataTransfer.getData("text/plain"); - var parent_list = panel.getElementsByClassName("caosdb-f-parent-list")[0] + var parent_list = entity.getElementsByClassName("caosdb-f-parent-list")[0] var tmp_id = propsrcid.split("-"); var prop_id = tmp_id[tmp_id.length - 1]; var entity_type = tmp_id[tmp_id.length - 2]; @@ -139,20 +168,14 @@ var edit_mode = new function() { var dragged_rt = str2xml('<Response><RecordType id="' + prop_id + '" name="' + name + '"></RecordType></Response>'); transformation.transformParent(dragged_rt).then(new_prop => { parent_list.appendChild(new_prop); - /* - edit_mode.add_one_delete_button( - parent_list.children[parent_list.children.length-1], - is_parent=true - ); - */ - edit_mode.add_parent_delete_buttons(panel); + edit_mode.add_parent_delete_buttons(entity); }, edit_mode.handle_error); } } this.property_drop_listener = function(e) { - edit_mode._drop_listener.call(this, e, edit_mode.add_dropped_property); + edit_mode._drop_listener.call(this, e, edit_mode.add_dropped_property); } this.parent_drop_listener = function(e) { @@ -302,7 +325,7 @@ var edit_mode = new function() { * @return {undefined} */ this.add_trash_button = function(appendable, deletable, className, callback = undefined, title = undefined) { - var button = $('<button class="btn btn-link ' + className + ' caosdb-f-entity-trash-button"><span class="glyphicon glyphicon-trash"></span></button>'); + var button = $('<button class="btn btn-link p-0 ' + className + ' caosdb-f-entity-trash-button"><i class="bi-trash"></i></button>'); if(title) { button.attr("title", title); } @@ -406,9 +429,21 @@ var edit_mode = new function() { * entity in XML representation. */ this.form_to_xml = function(entity_form) { - const obj = form_elements.form_to_object($(entity_form).find("form")[0]); + const obj = form_elements.form_to_object($(entity_form).find("form")[0])[0]; + var entityRole = getEntityRole(entity_form); + var file_path = undefined; + var file_checksum = undefined; + var file_size = undefined; + if (entityRole.toLowerCase() == "file") { + file_path = getEntityPath(entity_form); + file_checksum = getEntityChecksum(entity_form); + file_size = getEntitySize(entity_form); + console.log(file_path); + console.log(file_checksum); + console.log(file_size); + } return createEntityXML( - getEntityRole(entity_form), + entityRole, getEntityName(entity_form), getEntityID(entity_form), edit_mode.getProperties(entity_form), @@ -417,6 +452,7 @@ var edit_mode = new function() { edit_mode.get_datatype_str(obj), getEntityDescription(entity_form), obj.unit, + file_path, file_checksum, file_size ); } @@ -502,19 +538,22 @@ var edit_mode = new function() { this.update = update; this.add_edit_mode_button = function(target, toggle_function) { - var edit_mode_li = $('<li><button class="navbar-btn btn btn-link caosdb-f-btn-toggle-edit-mode">Edit Mode</button></li>'); + var edit_mode_li = $('<li class="nav-item"><a class="nav-link caosdb-f-btn-toggle-edit-mode" role="button">Edit Mode</a></li>'); $(target).append(edit_mode_li); $(".caosdb-f-btn-toggle-edit-mode").click(toggle_function); + var min_width_warning = $('<div class="alert alert-warning caosdb-edit-min-width-warning d-lg-none d-none" role="alert"><strong>Warning</strong> The edit mode is optimized for screens wider than 992px. If you have trouble using it, please try accessing it on a larger screen.</div>'); + $(".navbar").append(min_width_warning); + return edit_mode_li[0]; } - this.toggle_edit_mode = function() { + this.toggle_edit_mode = async function() { edit_mode.toggle_edit_panel(); if (edit_mode.is_edit_mode()) { - edit_mode.leave_edit_mode(); + await edit_mode.leave_edit_mode(); } else { - edit_mode.enter_edit_mode(); + await edit_mode.enter_edit_mode(); } } @@ -525,15 +564,15 @@ var edit_mode = new function() { this.leave_edit_mode = function() {} - this.enter_edit_mode = function(editApp = undefined) { + this.enter_edit_mode = async function(editApp = undefined) { window.localStorage.edit_mode = "true"; var editPanel = edit_mode.get_edit_panel(); removeAllWaitingNotifications(editPanel); this.add_wait_datamodel_info(); - // TODO make enter_edit_mode ayncronous? - return edit_mode.retrieve_data_model().then(model => { + try { + const model = await edit_mode.retrieve_data_model(); $(".caosdb-f-btn-toggle-edit-mode").text("Leave Edit Mode"); edit_mode.init_tool_box(model); @@ -549,7 +588,9 @@ var edit_mode = new function() { }; return nextEditApp; - }, edit_mode.handle_error); + } catch (err) { + edit_mode.handle_error(err); + } } @@ -600,8 +641,16 @@ var edit_mode = new function() { roleElem.detach(); var parentsElem = $(header).find('.caosdb-f-parent-list'); parentsElem.detach(); - const parentsSection = $('<div class="form-group"><label class="col-sm-2 control-label">parents</label><div class="col-sm-10"></div></div>'); - parentsSection.find("div.col-sm-10").append(parentsElem); + const parentsSection = $(` + <div class="row"> + <div class="col-2 text-end"> + <label class="col-form-label">parents</label> + </div> + <div class="col caosdb-f-parents-form-element"> + </div> + </div> + `); + parentsSection.find("div.caosdb-f-parents-form-element").append(parentsElem); header.attr("title", "Drop parents from the right panel here."); header.data("toggle", "tooltip"); @@ -625,11 +674,14 @@ var edit_mode = new function() { header.attr("title", ""); } else if (getEntityRole(roleElem[0]) == "File") { inputs.push(this.make_input("path", getEntityPath(entity))); + inputs.push(this.make_input("checksum", getEntityChecksum(entity))); + inputs.push(this.make_input("size", getEntitySize(entity))); } // remove other stuff header.children().remove(); const form = $('<form class="form-horizontal"></form>').append(inputs); header.append(form); + edit_mode.add_parent_dropzone(entity); edit_mode.make_datatype_input_logic(form[0]); edit_mode.add_parent_delete_buttons(header[0]); @@ -709,7 +761,7 @@ var edit_mode = new function() { label: "unit", value: unit, })); - unit_input.toggleClass("form-group", true); + unit_input.toggleClass("form-control", true); unit_input.find(".col-sm-3").toggleClass("col-sm-2", true).toggleClass("col-sm-3", false); unit_input.find(".col-sm-9").toggleClass("col-sm-2", true).toggleClass("col-sm-9", false); return unit_input[0]; @@ -728,7 +780,7 @@ var edit_mode = new function() { /** * Make three input elements which contain all necessary parts of a datatype. * - * The three input elements are wrapped in a single DIV.form-group. + * The three input elements are wrapped in a single DIV.form-control. * * @param {string} [datatype] - defaults to TEXT if undefined. * @returns {HTMLElement} @@ -783,11 +835,10 @@ var edit_mode = new function() { .make_checkbox_input(list_checkbox_config); // styling - $(list_checkbox).children().toggleClass("col-sm-3",false).toggleClass("col-sm-9", false).toggleClass("col-sm-1", true); - - const form_group = $('<div class="form-group">').append([datatype_selector, ref_selector, list_checkbox]); - form_group.find(".form-group").toggleClass("form-group", false); - form_group.find(".col-sm-3").toggleClass("col-sm-2", true).toggleClass("col-sm-3", false); + //$(list_checkbox).children().toggleClass("col-sm-3",false).toggleClass("col-sm-9", false).toggleClass("col-sm-1", true); + $(list_checkbox).find(".caosdb-f-property-value").toggleClass("my-auto",true) + const form_group = $('<div class="">').append([datatype_selector, ref_selector, list_checkbox]); + form_group.find(".col-sm-3").toggleClass("text-end", true).toggleClass("col-sm-2", true).toggleClass("col-sm-3", false); form_group.find(".col-sm-9").toggleClass("col-sm-3", true).toggleClass("col-sm-9", false); @@ -795,7 +846,7 @@ var edit_mode = new function() { } this.make_input = function(label, value) { - return $('<div class="form-group"><label class="col-sm-2 control-label">' + label + '</label><div class="col-sm-10"><input type="text" class="form-control caosdb-f-entity-' + label + '" value="' + (typeof value == 'undefined' ? "" : value) + '"></input></div></div>')[0]; + return $('<div class="row"> <div class="col-2 text-end"> <label class="col-form-label">' + label +' </label> </div> <div class="col caosdb-f-parents-form-element"> <input type="text" class="form-control caosdb-f-entity-' + label + '" value="' + (typeof value == 'undefined' ? "" : value) + '"></input> </div> </div>')[0]; } this.smooth_replace = function(from, to) { @@ -819,7 +870,7 @@ var edit_mode = new function() { this.createElementForProperty = function(property, options) { var result; if (property.datatype == "TEXT") { - result = "<textarea>" + ( property.value || "" ) + "</textarea>"; + result = `<textarea>${property.value || ""}</textarea>`; } else if (property.datatype == "DATETIME") { var dateandtime = [""]; if(property.value) { @@ -861,7 +912,7 @@ var edit_mode = new function() { */ this.generate_list_item_control_panel = function(property, options) { // Add list delete buttons: - var deleteButton = $('<button title="Delete this list element." class="btn btn-link caosdb-update-entity-button caosdb-f-list-item-button"><span class="glyphicon glyphicon-trash"></span></button>'); + var deleteButton = $('<button title="Delete this list element." class="btn btn-link caosdb-update-entity-button caosdb-f-list-item-button"><i class="bi-trash"></i></button>'); $(deleteButton).click(function() { var ol = this.parentElement.parentElement.parentElement; @@ -871,7 +922,7 @@ var edit_mode = new function() { // Add list insert buttons: - var insertButton = $('<button title="Insert a new list element before this element." class="btn btn-link caosdb-update-entity-button caosdb-f-list-item-button"><span class="glyphicon glyphicon-plus"></span></button>'); + var insertButton = $('<button title="Insert a new list element before this element." class="btn btn-link caosdb-update-entity-button caosdb-f-list-item-button"><i class="bi-plus"></i></button>'); $(insertButton).click(function() { // TODO MERGE WITH OTHER PLACES WHERE THIS STRING APPEARS var proptemp = { @@ -892,7 +943,9 @@ var edit_mode = new function() { this.parentElement.parentElement.parentElement.dispatchEvent(edit_mode.list_value_input_added); }); - return $("<span></span>").append(deleteButton).append(insertButton)[0]; + return $('<span class="caosdb-v-edit-value-list-buttons"></span>') + .append(deleteButton) + .append(insertButton)[0]; } @@ -951,7 +1004,7 @@ var edit_mode = new function() { } // PLUS-button for appending inputs to the list. - var insertButton = $('<button title="Append a new field at the end." class="btn btn-link caosdb-update-entity-button caosdb-f-list-item-button"><span class="glyphicon glyphicon-plus"></span></button>'); + var insertButton = $('<button title="Append a new field at the end." class="btn btn-link caosdb-update-entity-button caosdb-f-list-item-button"><i class="bi-plus"></i></button>'); $(insertButton).click(function() { // TODO MERGE WITH OTHER PLACES WHERE THIS STRING APPEARS var proptemp = { @@ -1075,9 +1128,9 @@ var edit_mode = new function() { */ this.add_toggle_list_checkbox = function (element, list, datatype) { var editfield = $(element).find(".caosdb-f-property-value"); - var label = "List "; - var checkbox = $('<input type="checkbox" class="caosdb-f-entity-is-list"/>'); - $(element).find(".caosdb-property-edit").prepend(checkbox).prepend(label); + var label = $('<label>List</label>'); + var checkbox = $('<input type="checkbox" class="form-check-input caosdb-f-entity-is-list"/>'); + $(element).find(".caosdb-property-edit").prepend(label.append(checkbox)); checkbox.prop("checked", list); @@ -1142,13 +1195,16 @@ var edit_mode = new function() { this.make_property_editable = function(element) { caosdb_utils.assert_html_element(element, "param 'element'"); - var editfield = $(element).find(".caosdb-f-property-value"); + var editfield = $(element).find(".caosdb-f-property-value") + .removeClass("col-sm-8") + .addClass("col-sm-6") + .addClass("caosdb-v-property-value-inputs") + .after(`<div class="col-sm-2 caosdb-v-property-other-inputs caosdb-property-edit" style="text-align: right;"/>`); var property = getPropertyFromElement(element); // create inputs - var inputs = edit_mode - .create_value_inputs(property); + var inputs = edit_mode.create_value_inputs(property); editfield.children().remove(); editfield.append(inputs); @@ -1188,7 +1244,7 @@ var edit_mode = new function() { */ this.delete_action = async function(entity) { var app = edit_mode.app; - // this is the callback of the delete button + // show waiting notification edit_mode.smooth_replace(entity, app.waiting); // send delete request @@ -1383,6 +1439,8 @@ var edit_mode = new function() { init_drag_n_drop(); } app.onEnterInitial = async function(e) { + $(".caosdb-f-edit-mode-existing").toggleClass("d-none", true); + $(".caosdb-f-edit-mode-create-buttons").toggleClass("d-none", false); app.old = undefined; app.errorHandler(() => { // make entities dropable and freezable @@ -1455,6 +1513,10 @@ var edit_mode = new function() { }, edit_mode.handle_error); }; app.onEnterChanged = function(e) { + // show existing entities in toolbox + $(".caosdb-f-edit-mode-existing").toggleClass("d-none", false); + $(".caosdb-f-edit-mode-create-buttons").toggleClass("d-none", true); + edit_mode.unhighlight(); hintMessages.removeMessages(app.old); edit_mode.make_header_editable(app.entity); @@ -1467,6 +1529,9 @@ var edit_mode = new function() { for (var element of prop_elements) { edit_mode.make_property_editable(element); } + if(getEntityRole(app.entity) != "Property") { + edit_mode.add_property_dropzone(app.entity); + } app.entity.dispatchEvent(edit_mode.start_edit); } app.onEnterWait = function(e) { @@ -1533,6 +1598,7 @@ var edit_mode = new function() { } hintMessages.hintMessages(app.entity); edit_mode.unfreeze(); + app.entity.dispatchEvent(edit_mode.end_edit); resolve_references.init(); preview.init(); } @@ -1559,6 +1625,18 @@ var edit_mode = new function() { }); } + this.add_property_dropzone = function (entity) { + $(entity).find("ul.caosdb-properties") + .append('<li class="caosdb-v-edit-mode-dropzone caosdb-f-edit-mode-property-dropzone caosdb-v-edit-mode-property-dropzone">Drag and drop Properties and RecordTypes from the Edit Mode Toolbox here.</li>'); + } + + this.add_parent_dropzone = function (entity) { + $(entity).find(".caosdb-f-parent-list") + .addClass("caosdb-v-edit-mode-parent-dropzone") + .addClass("caosdb-v-edit-mode-dropzone") + .prepend('<div>Drag and drop RecordTypes from the Edit Mode Toolbox here.</div>'); + } + this.unfreeze = function() { $('.caosdb-f-main-entities').children().each(function(index) { edit_mode.unfreeze_entity(this); @@ -1644,11 +1722,13 @@ var edit_mode = new function() { } this.highlight = function(entity) { - $(entity).addClass("caosdb-v-edit-mode-highlight").css("background-color", "lightgreen"); + $(entity).find(".caosdb-v-edit-mode-dropzone") + .addClass("caosdb-v-edit-mode-highlight"); } this.unhighlight = function() { - $('.caosdb-v-edit-mode-highlight').removeClass("caosdb-v-edit-mode-highlight").css("background-color", ""); + $('.caosdb-v-edit-mode-highlight') + .removeClass("caosdb-v-edit-mode-highlight"); } this.handle_error = function(err) { @@ -1664,9 +1744,18 @@ var edit_mode = new function() { } this.toggle_edit_panel = function() { - $(".caosdb-f-main").toggleClass("container-fluid").toggleClass("container"); - $(".caosdb-f-main-entities").toggleClass("col-xs-8"); - $(".caosdb-f-edit").toggleClass("hidden").toggleClass("col-xs-4"); + //$(".caosdb-f-main").toggleClass("container-fluid").toggleClass("container"); + $(".caosdb-f-main-entities").toggleClass("caosdb-f-main-entities-edit"); + $(".caosdb-f-edit").toggle();//.toggleClass("col-xs-4"); + this._toggle_min_width_warning(); + } + + this._toggle_min_width_warning = function() { + // Somewhat counter-intuitive, but when we're not in edit mode + // and toggle the panel, we're entering and the warning should + // be shown on small screens and vice-versa. + $(".caosdb-edit-min-width-warning").toggleClass("d-none", edit_mode.is_edit_mode()); + $(".caosdb-edit-min-width-warning").toggleClass("d-block", !(edit_mode.is_edit_mode())); } this.leave_edit_mode_template = function(app) { @@ -1745,8 +1834,68 @@ var edit_mode = new function() { } } + /** + * Programatically start editing an entity. + * + * Throws an error if the action cannot be performed (this or another + * entity is already being edited or the edit_mode is not active). + * + * @param {HTMLElement} entity - the entity which is to be changed. + * @return {HTMLElement} the entity form + */ + this.edit = function (entity) { + if (!this.is_edit_mode()) { + throw Error("edit_mode is not active"); + } + if (!edit_mode.app.can("startEdit")) { + throw Error("edit_mode.app does not allow to start the edit"); + } + edit_mode.app.startEdit(entity); + return edit_mode.app.entity; + } + + /** + * List of all permissions which indicate that the edit button should be + * visible. + */ + const UPDATE_PERMISSIONS = [ + "UPDATE:DESCRIPTION", + "UPDATE:VALUE", + "UPDATE:ROLE", + "UPDATE:PARENT:REMOVE", + "UPDATE:PARENT:ADD", + "UPDATE:PROPERTY:REMOVE", + "UPDATE:PROPERTY:ADD", + "UPDATE:NAME", + "UPDATE:DATA_TYPE", + "UPDATE:FILE:REMOVE", + "UPDATE:FILE:ADD", + "UPDATE:FILE:MOVE", + "UPDATE:QUERY_TEMPLATE_DEFINITION", + ]; + /** + * Add a button labeled "Edit" to the entity which opens the edit form for + * this entity. + * + * The button is added only when any of the `UPDATE:...` permissions are + * there. + * + * @param {HTMLElement} entity - the entity which gets the button. + * @parma {function} callback - the function which initializes and opens + * the edit form. + */ this.add_start_edit_button = function(entity, callback) { + var has_any_update_permission = false; + for (let permission of UPDATE_PERMISSIONS) { + if (hasEntityPermission(entity, permission)) { + has_any_update_permission = true; + break; + } + } + if (!has_any_update_permission) { + return; + } edit_mode.remove_start_edit_button(entity); var button = $('<button title="Edit this ' + getEntityRole(entity) + '." class="btn btn-link caosdb-update-entity-button caosdb-f-entity-start-edit-button">Edit</button>'); @@ -1761,6 +1910,9 @@ var edit_mode = new function() { this.add_new_record_button = function(entity, callback) { + if (!hasEntityPermission(entity, "USE:AS_PARENT")) { + return; + } edit_mode.remove_new_record_button(entity); var button = $('<button title="Create a new Record from this RecordType." class="btn btn-link caosdb-update-entity-button caosdb-f-entity-new-record-button">+Record</button>'); @@ -1774,13 +1926,33 @@ var edit_mode = new function() { } this.add_delete_button = function(entity, callback) { + if (!hasEntityPermission(entity, "DELETE")) { + return; + } edit_mode.remove_delete_button(entity); var button = $('<button title="Delete this ' + getEntityRole(entity) + '." class="btn btn-link caosdb-update-entity-button caosdb-f-entity-delete-button">Delete</button>'); $(entity).find(".caosdb-f-edit-mode-entity-actions-panel").append(button); $(button).click(() => { - edit_mode.delete_action(entity); + // hide other elements + const _alert = form_elements.make_alert({ + title: "Warning", + message: "You are going to delete this entity permanently. This cannot be undone.", + proceed_callback: () => { + edit_mode.delete_action(entity) + }, + cancel_callback: () => { + $(_alert).remove(); + $(entity).find(".caosdb-f-edit-mode-entity-actions-panel").show(); + }, + proceed_text: "Yes, delete!", + remember_my_decision_id : "delete_entity", + }); + $(_alert).addClass("text-end"); + + $(entity).find(".caosdb-f-edit-mode-entity-actions-panel").after(_alert).hide(); + }); } diff --git a/src/core/js/ext_autocomplete.js b/src/core/js/ext_autocomplete.js new file mode 100644 index 0000000000000000000000000000000000000000..cd145ee9e29bf370fd44f035f26d477a9cc0d478 --- /dev/null +++ b/src/core/js/ext_autocomplete.js @@ -0,0 +1,161 @@ +/* + * ** header v3.0 + * This file is a part of the CaosDB Project. + * + * Copyright (C) 2019 IndiScale GmbH, Henrik tom Wörden + * + * 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/>. + * + * ** end header + */ + +/* + * This module uses and extends bootstrap-autocomplete to provide + * autocompletion to the query field. + * Currently, completion exists for names that are stored in CaosDB + */ +'use strict'; + +var ext_autocomplete = new function () { + this.CQL_WORDS = [ + "FIND", + "FILE", + "ENTITY", + "SELECT", + "COUNT", + "RECORD", + "PROPERTY", + "RECORDTYPE", + "REFERENCES", + "REFERENCED BY", + "WHICH", + "WITH", + "CREATED BY", + "CREATED ON", + "SOMEONE", + "STORED AT", + "HAS A PROPERTY", + "HAS BEEN", + "ANY VERSION OF", + ]; + this.version = "0.1"; + + + /** + * configure logging + */ + var logger = log.getLogger("ext_autocomplete"); + + /** + * Initialize this module. + * + * Fetch candidates and prepare bootstrap-autocomplete. + */ + this.init = async function () { + this.candidates = await this.retrieve_names(); + this.switch_on_completion(); + }; + + + /** + * creates a list of names from the server resource Entity/names + */ + this.retrieve_names = async function () { + var response = $(await connection.get(transaction.generateEntitiesUri( + ["names"]))).find("Property[name],RecordType[name],Record[name]") + + response = response.toArray().map(x => $(x).attr("name")); + response = response.concat(ext_autocomplete.CQL_WORDS); + + return response + }; + + /** + * case insensitive filter that returns elements that start with qry + */ + this.starts_with_filter = function (x, qry) { + return x.toLowerCase().startsWith(qry.toLowerCase()) + }; + + /** + * Overwrites the default search function of bootstrap-autocomplete + * uses candidates and filters the list using filter + */ + this.search = async function (qry, callback, origJQElement) { + callback(ext_autocomplete.candidates.filter( + x => ext_autocomplete.starts_with_filter(x, qry))); + }; + + /** + * Overwrites the default typed function of bootstrap-autocomplete + * This assures that only the word before the cursor is used for completion + */ + this.typed = function (newValue, origJQElement) { + var cursorpos = origJQElement[0].selectionEnd; + var beginning_of_word = newValue.slice(0, cursorpos).lastIndexOf(" "); + var word = newValue.slice(beginning_of_word + 1, cursorpos); + return word; + }; + + /** + * Overwrites the default searchPost function of bootstrap-autocomplete + * This assures that only the word before the cursor is replaced. + * It is achieved by returning not a simple list, but an object where each + * element has "text" and "html" fields, where "html" is what is visible + * in the dropdown and "text" will be inserted. + */ + this.searchPost = function (resultsFromServer, origJQElement) { + var cursorpos = origJQElement[0].selectionEnd; + var newValue = origJQElement[0].value + var beginning_of_word = newValue.slice(0, cursorpos).lastIndexOf( + " "); + var start = newValue.slice(0, beginning_of_word + 1); + var end = origJQElement[0].value.slice(cursorpos); + var result = resultsFromServer.map(x => { + return { + text: start + x + end, + html: x + } + }); + return result; + }; + + /** + * enables autocompletion in the query field + */ + this.switch_on_completion = function () { + var field = $("#caosdb-query-textarea"); + field.attr("autocomplete", "off"); + field.toggleClass("basicAutoComplete", true); + field.autoComplete({ + events: { + search: this.search, + typed: this.typed, + searchPost: this.searchPost, + }, + noResultsText: 'No autocompletion suggestions', + bootstrapVersion: "4", + + }); + + return field; + }; +}; + +// this will be replaced by require.js in the future. +$(document).ready(function () { + if ("${BUILD_MODULE_EXT_AUTOCOMPLETE}" == "ENABLED") { + caosdb_modules.register(ext_autocomplete); + } +}); diff --git a/src/core/js/ext_bookmarks.js b/src/core/js/ext_bookmarks.js new file mode 100644 index 0000000000000000000000000000000000000000..d99ff362d8a26ee4818a76a624c80f55f757c419 --- /dev/null +++ b/src/core/js/ext_bookmarks.js @@ -0,0 +1,730 @@ +/* + * ** header v3.0 + * This file is a part of the CaosDB Project. + * + * Copyright (C) 2020 IndiScale GmbH <info@indiscale.com> + * Copyright (C) 2020 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/>. + * + * ** end header + */ + +'use strict'; + + +/** + * Keep track of bookmarked entities and provide functions for export, viewing + * all entities and resetting the bookmarks. + * + * @module ext_bookmarks + * @version 0.2 + * + * @param jQuery - well-known library. + * @param log - singleton from loglevel library or javascript console. + * @param {ext_bookmarks_config} config + * + * @type ext_bookmarks_config + * @property {function} [get_context_root] - param-less function which returns the + * uri of the collection resource which returns the bookmarked entities. + * @property {string} [data_attribute=data-bmval] - name of the data-attribute + * which contains the bookmark key. + * @property {Storage} [bookmark_storage=localStorage] - the Storage + * implementation which is used for storing bookmarks. + * @property {function} [set_collection_link] - a function which sets the + * collection link. + * @property {function} [set_counter] - a function which sets the counter + * @property {function} [getPaging] - a function which returns the paging string + * @property {function} [set_export_button_click] - a function which sets the click + * event handler of the export button. + * @property {function} [set_clear_button_click] - a function which sets the click + * event handler of the clear button. + * @property {string[]} [tsv_columns=["URI"]] + * @property {function} [get_export_table_row] - function which receives the + * bookmarked id and must return an array of columns. + * @property {object} [data_getters={}] - a dictionary of functions which + * retrieve bookmark data for a given bookmark id and a data_key. + * @property {string[]} [data_no_cache] - an array of data_keys which are not + * to be cached. + */ +var ext_bookmarks = function ($, logger, config) { + + config = config || {}; + + /** + * This counter is used as a cache for the number of current bookmarks. It + * is used to quickly change the counter in the bookmarks menu and lazily + * updated. + */ + var counter = 0; + + /** + * Currently this is mainly usefull for testing, but in the future it might + * be desirable to have multiple bookmark collection at the same time. It + * would be easy to extent this class for this because the collection_id is + * used in the generated links and as part of the storage key in the + * bookmark_storage. + */ + var collection_id = config["collection_id"] || 0; + + const data_getters = config["data_getters"] || { + "URI": (id) => get_context_root() + id + }; + const data_no_cache = config["data_no_cache"] || ["URI"]; + + const data_attribute = config["data_attribute"] || "data-bmval"; + + /** + * Return all bookmark buttons on this page or which are children of scope. + * + * @param {HTMLElement|string} [scope='body'] + * @return {HTMLElement[]} array of bookmark buttons. + */ + const get_bookmark_buttons = function (scope) { + return $(scope || "body").find(`[${data_attribute}]`).toArray(); + } + + /** + * Sets the click event handler of the clear button. + * + * @param {function} cb - event handler. + */ + const set_clear_button_click = config["set_clear_button_click"] || function (cb) { + $("#caosdb-f-bookmarks-clear") + .toggleClass("disabled", !cb) + .on("click", cb); + } + + /** + * Sets the click event handler of the export button. + * + * @param {function} cb - event handler. + */ + const set_export_button_click = config["set_export_button_click"] || function (cb) { + $("#caosdb-f-bookmarks-export-link") + .toggleClass("disabled", !cb) + .on("click", cb); + } + + const getPaging = config["getPaging"] || (() => "?P=0L10"); + + /** + * The storage backend for the bookmarks. + */ + const bookmark_storage = config["bookmark_storage"] || window.localStorage; + + /** + * Set the href attribute of the bookmark collection link. + * + * @param {string} uri + */ + const set_collection_link = config["set_collection_link"] || function (uri) { + const link = $("#caosdb-f-bookmarks-collection-link") + .toggleClass("disabled", !uri) + .find("a"); + if (uri) { + link.attr("href", uri); + } else { + link.removeAttr("href"); + } + } + + /** + * Set the counter badge in the bookmark menu. + */ + const update_counter = config["set_counter"] || function (counter) { + $("#caosdb-f-bookmarks-collection-counter").text(counter); + } + + const get_context_root = config["get_context_root"] || (() => ""); + + /** + * This is used as a prefix of the key in the bookmark_storage. + */ + const key_prefix = "_bm_"; + + /** + * This marker is used to identify uris which specify a bookmark collection + * which should be reloaded. + */ + const uri_marker = "_bm_"; + + /** + * Extract the bookmark id from the bookmark button. + * + * @param {HTMLElement} button - the bookmark button. + * @return {string} the bookmark id. + */ + const get_value = function (button) { + const result = $(button).attr(data_attribute); + return result; + } + + /** + * Construct the prefix of the key for the bookmark_storage. + * + * This can be used to construct the item key and the data key + * to delete all storage keys which belong to the current bookmark + * collection. + * + * @param {string} + */ + const get_collection_prefix = function () { + return key_prefix + collection_id; + } + + /** + * Generate the key for the bookmark_storage. + * + * @param {string} val - the value which is used to generate the key. + */ + const get_key = function (val) { + return get_collection_prefix() + '_it_' + val; + } + + + /** + * These will be the columns in the TSV file. For each column there should + * exist a data_getter. + */ + const tsv_columns = config["tsv_columns"] || ["URI"]; + + /** + * Generate a single TSV row + * + * @return {string[]} array of row columns + */ + const get_export_table_row = async function (id) { + const row = []; + for (var col of tsv_columns) { + row.push(await get_bookmark_data(id, col)); + } + return row; + } + + /** + * Generate the TSV data for the export callback with all current + * bookmarks. + * + * TODO merge with caosdb_utils.create_tsv_table. + * + * @param {string[]} bookmarks - array of ids. + * @param {string} [preamble="data:text/csv;charset=utf-8,"] - the preamble + * which is used for generating tables which can be downloaded by + * browsers. + * @param {string} [tab="%09"] - the tab string. + * @param {string} [newline="%0A"] - the newline string. + * @param {string[]} [leading_comments] - comment lines which are to be put + * even before the header line. They should be appropriately escaped (e.g. + * with "%23"). + */ + const get_export_table = async function (bookmarks, preamble, tab, newline, leading_comments) { + // TODO merge with related code in the module "caosdb_table_export". + preamble = ((typeof preamble == 'undefined') ? "data:text/csv;charset=utf-8,": preamble); + tab = tab || "%09"; + newline = newline || "%0A"; + leading_comments = (leading_comments ? leading_comments.join(newline) + newline : ""); + const header = leading_comments + tsv_columns.join(tab) + newline; + const rows = []; + for (let i = 0; i < bookmarks.length; i++) { + rows.push((await get_export_table_row(bookmarks[i])).join(tab)); + } + return `${preamble}${header}${rows.join(newline)}`; + } + + /** + * Download the table with a given filename. + * + * This method adds a temporay <A> element to the dom tree and triggers + * "click" because otherwise the filename cannot be set. + * + * See also: + * https://stackoverflow.com/questions/21177078/javascript-download-csv-as-file + */ + const download = function (table, filename) { + console.log("download"); + const link = $(`<a style="display: none" download="${filename}" href="${table}"/>`); + $("body").append(link); + link[0].click(); + link.remove(); + } + + /** + * Trigger the download of the TSV table with all current bookmarks. + * + * This is the call-back for the export button. + */ + const export_bookmarks = async function () { + const ids = get_bookmarks(); + const versioned_bookmarks = [] + for (let id of ids) { + if (id.indexOf("@") > -1) { + versioned_bookmarks.push(id); + } else { + versioned_bookmarks.push(id + "@" + (await get_bookmark_data(id, "Version"))); + } + } + const uri = get_collection_link(ids); + const leading_comments = [encodeURIComponent(`#Link to all entities: ${uri}`)]; + const export_table = await get_export_table(ids, undefined, undefined, undefined, leading_comments); + download(export_table, "bookmarked_entities.tsv"); + } + + /** + * Return all current bookmarks. + * + * @return {string[]} array of bookmarked ids. + */ + const get_bookmarks = function () { + const result = []; + + const storage_key_prefix = get_key(""); + for (let i = 0; i < bookmark_storage.length; i++) { + const key = bookmark_storage.key(i); + if (key.indexOf(storage_key_prefix) > -1) { + result.push(bookmark_storage[key]); + } + } + return result; + } + + /** + * Update the clear button (i.e. add a click handler which resets the bookmarks.) + * + * @param {string[]} bookmarks - array of ids. + */ + const update_clear_button = function (bookmarks) { + if (bookmarks.length > 0) { + set_clear_button_click(clear_bookmark_storage); + } else { + set_clear_button_click(false); + } + } + + /** + * Update the export button (i.e. add a click handler which generates the + * tsv file.) + * + * @param {string[]} bookmarks - array of ids. + */ + const update_export_link = function (bookmarks) { + if (bookmarks.length > 0) { + set_export_button_click(export_bookmarks); + } else { + set_export_button_click(false); + } + } + + /** + * Generate the uri for the collection of all bookmarked entities. + * + * @param {string[]} bookmarks - array of ids. + * @return {string} uri + */ + const get_collection_link = function (bookmarks) { + const uri_segment = bookmarks.join("&"); + return get_context_root() + uri_segment + getPaging() + + "#" + uri_marker + collection_id; + } + + /** + * Update the link of the collection of bookmarks in the bookmark drop down + * menu. + * + * @param {string[]} bookmarks - array of ids. + */ + const update_collection_link = function (bookmarks) { + if (bookmarks.length > 0) { + const link = get_collection_link(bookmarks); + set_collection_link(link); + } else { + set_collection_link(false); + } + } + + /** + * Syncronize the bookmark_storage and currently visible bookmark button and + * update all buttons and other visible elements and the bookmarks drop + * down menu. + * + * @param {string[]} [bookmarks] - array of ids. If omitted, the + * get_bookmarks function is being called. + */ + const update_collection = function (bookmarks) { + bookmarks = bookmarks || get_bookmarks(); + update_counter(bookmarks.length); + update_collection_link(bookmarks); + update_export_link(bookmarks); + update_clear_button(bookmarks); + } + + /** + * Toggle the active class of the button and change the title of the button + * accordingly. + * + * @param {HTMLElement} button - bookmark button + * @param {boolean} is_active - whether the new state is active or not. + */ + const set_button_state = function (button, is_active) { + $(button).toggleClass("active", is_active); + if (is_active) { + $(button).attr("title", "Remove bookmark"); + } else { + $(button).attr("title", "Add bookmark"); + } + } + + /** + * Event handler for the click event of the bookmark buttons. + * + * Toggles the buttons state and adds or removes the bookmark. + * + * @param {Event} e - the click event; + */ + const toggle_bookmark_active = function (e) { + const button = $(this); + + const new_is_active = !button.is(".active"); + set_button_state(button, new_is_active); + + const value = get_value(button[0]); + const key = get_key(value); + if (new_is_active) { + bookmark_storage.setItem(key, value); + update_counter(++counter); + + // fill the cache immediately. This is a good idea, because many + // data_getters can work on the DOM tree when the bookmark is being + // selected. Later, when the user has left the current page, the + // getters might need to request the database. We want to prevent + // that. + collect_bookmark_data(value); + } else { + bookmark_storage.removeItem(key); + update_counter(--counter); + remove_bookmark_data(value); + } + update_collection(); + } + + /** + * Fill the cache with data for the export for a bookmark. + * + * @param {string} id - bookmark id. + */ + const collect_bookmark_data = function (id) { + for (let data_key in data_getters) { + if (data_no_cache.indexOf(data_key) == -1) { + // do nothing, only trigger the fetching + get_bookmark_data(id, data_key) + } + } + } + + /** + * Remove all data item which belong to a bookmark. + * + * @param {string} id - bookmark id. + */ + const remove_bookmark_data = function (id) { + const data_key_prefix = get_data_key(id, ""); + remove_from_storage_by_prefix(data_key_prefix); + } + + /** + * Initialize a single bookmark button. + * + * Fetch the state from the bookmark_storage and set the bookmark button to + * active or inactive. Also add the onclick handler which toggles the + * bookmark state. + * + * @param {HTMLElement} button - The bookmark button + */ + const init_button = function (button) { + // load state + const key = get_key(get_value(button)); + const is_bookmarked = !!key && !!bookmark_storage[key]; + set_button_state(button, is_bookmarked); + + // onlick handler + button.onclick = toggle_bookmark_active; + } + + /** + * Remove all items in the bookmark_storage by a prefix. + * + * Useful for resetting the whole bookmark_storage or just deleting a + * single item along with its data items. + * + * @param {string} prefix + */ + const remove_from_storage_by_prefix = function (prefix) { + const keys = []; + for (let i = 0; i < bookmark_storage.length; i++) { + const key = bookmark_storage.key(i); + if (key.indexOf(prefix) > -1) { + keys.push(key); + } + } + for (let i = 0; i < keys.length; i++) { + bookmark_storage.removeItem(keys[i]); + } + } + + /** + * Remove all bookmarks, clear the counter and reset the buttons + */ + const clear_bookmark_storage = function () { + counter = 0; + update_collection([]); + + // reset all buttons + get_bookmark_buttons().forEach((x) => { + set_button_state(x, false); + }); + + const storage_key_prefix = get_collection_prefix() + remove_from_storage_by_prefix(storage_key_prefix); + } + + /** + * Add all bookmarks to storage. + * + * @param {string[]} ids - an array of ids. + */ + const add_all_bookmarks_to_storage = function (ids) { + counter = counter + ids.length; + for (let i = 0; i < ids.length; i++) { + const id = ids[i]; + if (id) { + bookmark_storage[get_key(id)] = id + } + } + update_collection(); + } + + /** + * Parse a list of bookmarked entities and the collection_id from and URI. + * + * @param {string} uri + * @return {object} dict with two keys: + * {string[]} bookmarks + * {string} _collection_id + */ + const parse_uri = function (uri) { + var cut_index = uri.indexOf("#" + uri_marker); + if (cut_index > -1) { + // get collection id + const _collection_id = uri + .substring(cut_index + uri_marker.length + 1) + // remove query + const query_marker = uri.indexOf("?"); + if (query_marker > -1) { + cut_index = query_marker; + } + const uri_segments = uri.substring(0, cut_index).split("/") + const ids = uri_segments[uri_segments.length - 1].split("&"); + logger.debug("found ids in uri", ids); + return { + bookmarks: ids, + collection_id: _collection_id + }; + } + return undefined; + } + + + /** + * Initialize all bookmark buttons which are children of scope. + * + * @param {HTMLElement|string} [scope="body"] - element or jquery selector. + */ + const init_bookmark_buttons = function (scope) { + logger.trace("enter init_bookmark_buttons", scope); + $(get_bookmark_buttons(scope)).each((idx, button) => { + init_button(button); + }); + } + + /** + * Setter for the collection_id. + * + * @param {string} id + */ + const set_collection_id = function (id) { + collection_id = id; + } + + /** + * Initialize this module. + */ + const init = async function (scope) { + logger.info("init ext_bookmarks"); + //add_bookmark_buttons(); + counter = 0; + const parsed_uri = parse_uri(window.location.href); + if (typeof parsed_uri != "undefined") { + // this hack removes the "#_bm" marker from the uri without + // reloading the page. + window.location.href = "#"; + + clear_bookmark_storage(); + collection_id = parsed_uri["collection_id"]; + add_all_bookmarks_to_storage(parsed_uri["bookmarks"]); + } + + init_bookmark_buttons(scope); + update_collection(); + if (edit_mode) { + window.document.body.addEventListener(edit_mode.end_edit.type, (e) => { + init_bookmark_buttons(e.target); + }, true); + } + } + + /** + * Construct the key for data items in the bookmark_storage. + */ + const get_data_key = function (id, data_key) { + return get_collection_prefix() + '_da_' + id + "_" + data_key; + } + + /** + * Get a specific data item which belongs to a bookmark. + * + * This is currently prominently used by the tsv-export. + * + * @param {string} id - the bookmarked id + * @param {string} data_key - an identifier for the data item to be + * retrieved. + * @returns {string} the `data_key` of bookmark `id`. + */ + const get_bookmark_data = async function (id, data_key) { + // get full key (needed for the cache) + const full_data_key = get_data_key(id, data_key); + + // retrieve from cache + const cached = bookmark_storage[full_data_key]; + if (typeof cached != "undefined") { + return cached; + } + + // not in cache, try the data_getters + var uncached = undefined + if (data_getters[data_key]) { + uncached = (await data_getters[data_key](id)) + } + + // don't cache if getting the information is trivial or there are other + // reasons why this is in the data_no_cache array. + if (data_no_cache.indexOf(data_key) == -1) { + bookmark_storage[full_data_key] = uncached || ""; + } + return uncached; + } + + return { + init: init, + parse_uri: parse_uri, + get_bookmarks: get_bookmarks, + get_key: get_key, + get_value: get_value, + set_collection_id: set_collection_id, + bookmark_storage: bookmark_storage, + get_export_table: get_export_table, + clear_bookmark_storage, + clear_bookmark_storage, + update_clear_button: update_clear_button, + update_export_link: update_export_link, + update_collection_link: update_collection_link, + get_collection_link: get_collection_link, + get_bookmark_buttons: get_bookmark_buttons, + init_button: init_button, + get_bookmark_data: get_bookmark_data, + } +}; + + + +$(document).ready(function () { + if ("${BUILD_MODULE_EXT_BOOKMARKS}" == "ENABLED") { + // The following is the configuration for the CaosDB WebUI. + const get_context_root = (() => connection.getBasePath() + "Entity/"); + + // This getter retrieves a file's path from the page or, if necessary, + // from the server. + const get_path = async function (id) { + if (id.indexOf("@") > -1) { + const entity = $(`[data-bmval='${id}']`); + if (entity.length > 0) { + return getEntityPath(entity[0]) || ""; + } + } + return $(await transaction.retrieveEntityById(id)).attr("path"); + } + + // This retrieves the head version id + const get_version = async function (id) { + return $(await transaction.retrieveEntityById(id)).find("Version").attr("id"); + } + + const get_name = async function (id) { + if (id.indexOf("@") > -1) { + const entity = $(`[data-bmval='${id}']`); + if (entity.length > 0) { + return getEntityName(entity[0]) || ""; + } + } + return $(await transaction.retrieveEntityById(id)).attr("name"); + } + + const get_rt = async function (id) { + if (id.indexOf("@") > -1) { + const entity = $(`[data-bmval='${id}']`); + if (entity.length > 0) { + return getParents(entity[0]).join("/"); + } + } + const parent_names = $(await transaction.retrieveEntityById(id)) + .find("Parent").toArray().map(x => x.getAttribute("name")) + return parent_names.join("/"); + } + + // these columns will be in the export + const tsv_columns = ["ID", "Version", "URI", "Path", "Name", "RecordType"]; + // functions for collecting the export data for a particular bookmarked id. + const data_getters = { + "ID": (id) => id.indexOf("@") > -1 ? id.split("@")[0] : id, + "Version": async (id) => id.indexOf("@") > -1 ? id.split("@")[1] : await get_version(id), + "Path": get_path, + "URI": async (id) => get_context_root() + id + (id.indexOf("@") > -1 ? "" : ("@" + await get_version(id))), + "Name": get_name, + "RecordType": get_rt, + }; + + // we cannot cache these because the the values might change unnoticed + // when the head moves to a newer version. + const data_no_cache = ["ID", "Version", "URI", "Path", "Name", "RecordType"]; + + const config = { + get_context_root: get_context_root, + tsv_columns: tsv_columns, + data_getters: data_getters, + data_no_cache: data_no_cache, + }; + + ext_bookmarks = ext_bookmarks($, log.getLogger("ext_bookmarks"), config); + caosdb_modules.register(ext_bookmarks); + } +}); diff --git a/src/core/js/ext_bottom_line.js b/src/core/js/ext_bottom_line.js index 1bcf73dea17889bda9858ed78a2f6848e1dc21ee..2f40cd5c822c7153ef9fe8c40e7b06ffe70a3996 100644 --- a/src/core/js/ext_bottom_line.js +++ b/src/core/js/ext_bottom_line.js @@ -23,6 +23,28 @@ 'use strict'; +/** + * @typedef {BottomLineConfig} + * @property {string|HTMLElement} fallback - Fallback content if none of + * the creators are applicable. + * @property {string} version - the version of the configuration which must + * match this module's version. + * @property {CreatorConfig[]} creators - an array of creators. + */ + +/** + * @typedef {CreatorConfig} + * @property {string} [id] - a unique id for the creator. optional, for + * debuggin purposes. + * @property {function|string} is_applicable - If this is a string this has + * to be valid javascript! An asynchronous function which accepts one + * parameter, an entity in html representation, and which returns true + * iff this creator is applicable for the given entity. + * @property {string} create - This has to be valid javascript! An + * asynchronous function which accepts one parameter, an entity in html + * representation. It returns a HTMLElement or text node which will be + * shown in the bottom line container iff the creator is applicable. + */ /** * Add a special section to each entity one the current page where a thumbnail, @@ -42,36 +64,31 @@ * @requires load_config (function from caosdb.js) * @requires getEntityPath (function from caosdb.js) * @requires connection (module from webcaosdb.js) + * @requires UTIF (from utif.js library) + * @requires ext_table_preview (module from ext_table_preview.js) */ -var ext_bottom_line = function($, logger, is_in_view_port, load_config, getEntityPath, connection) { + +/** + * Helper function analogous to ext_references isOutOfViewport + * + * Check whether the bottom of an entity is within the viewport. + * Returns true when this is the case and false otherwise. + * + */ +function is_bottom_in_viewport(entity) { + var bounding = entity.getBoundingClientRect(); + return bounding.bottom > 0 && bounding.bottom < (window.innerHeight || + document.documentElement.clientHeight); +} + +var ext_bottom_line = function($, logger, is_in_view_port, load_config, getEntityPath, connection, UTIF, ext_table_preview) { /** * @property {string|function} create - a function with one parameter * (entity) Note: This property can as well be a * javascript string which evaluates to a function. */ - /** - * @type {BottomLineConfig} - * @property {string|HTMLElement} fallback - Fallback content if none of - * the creators are applicable. - * @property {string} version - the version of the configuration which must - * match this module's version. - * @property {CreatorConfig[]} creators - an array of creators. - */ - /** - * @type {CreatorConfig} - * @property {string} [id] - a unique id for the creator. optional, for - * debuggin purposes. - * @property {function|string} is_applicable - If this is a string this has - * to be valid javascript! An asynchronous function which accepts one - * parameter, an entity in html representation, and which returns true - * iff this creator is applicable for the given entity. - * @property {string} create - This has to be valid javascript! An - * asynchronous function which accepts one parameter, an entity in html - * representation. It returns a HTMLElement or text node which will be - * shown in the bottom line container iff the creator is applicable. - */ /** * Check if an entity has a path attribute and one of a set of extensions. @@ -103,7 +120,91 @@ var ext_bottom_line = function($, logger, is_in_view_port, load_config, getEntit */ const _create_video_preview = function(entity) { var path = connection.getFileSystemPath() + getEntityPath(entity); - return $(`<div class="caosdb-v-bottom-line-video-preview"><video controls="controls"><source src="${path}"/></video></div>`)[0]; + return $(`<div class="caosdb-v-bottom-line-video-preview"> + <video controls="controls"><source src="${path}"/></video></div>`)[0]; + } + + /** + * Error class which has the special to_html method. + * + * The to_html method creates a html representation of the error which is + * intended for displaying in the bottom_line container. + */ + const BottomLineError = function(arg) { + this._is_bottom_line_error = true; + + if (arg.message) { + // arg is an Error object + this.message = arg.message; + this.stack = arg.stack; + } else { + this.message = arg; + } + + this.to_html = function() { + return $(`<div><p>An error occured while loading this preview.<p>${ + this.message}<div>`)[0]; + } + } + + const BottomLineWarning = function (arg) { + this._is_bottom_line_error = true; + + if (arg.message) { + // arg is an Error object + this.message = arg.message; + this.stack = arg.stack; + } else { + this.message = arg; + } + + this.to_html = function() { + return $(`<div>${this.message}<div>`)[0]; + } + } + + /** + * Create a preview for tiff files. + * + * Tiff files are decompressed if necessary and converted into png by UTIF library. + * + * @param {HTMLElement} entity + * @return {Promise | HTMLElement} Promise for an IMG element. + */ + const _create_tiff_preview = function(entity) { + const path = connection.getFileSystemPath() + getEntityPath(entity); + const result = $(`<div class="caosdb-v-bottom-line-image-preview"></div>`); + const img = $(`<img src="${path}"/>`)[0]; + result.append(img); + + /** + * Promise which retrieves the tiff file and calls the UTIF library for + * decompression and conversion into png. + */ + return new Promise((resolve, reject) => { + const xhr = new XMLHttpRequest(); + UTIF._xhrs.push(xhr); + UTIF._imgs.push(img); + xhr.open("GET", path); + xhr.responseType = "arraybuffer"; + xhr.onload = (e) => { + try { + // decompress and convert tiff file + UTIF._imgLoaded(e); + + // return the result if no error occured. + resolve(result); + } catch(err) { + // throw errors from UTIF to the awaiting caller. + reject(new BottomLineError(err)); + } + } + // throw http errors to the awaiting caller. + xhr.onerror = reject; + + // this finally triggers the retrieval + xhr.send(); + }); } /** @@ -119,6 +220,8 @@ var ext_bottom_line = function($, logger, is_in_view_port, load_config, getEntit var fallback_preview = undefined; + const _tiff_preview_enabled = "${BUILD_MODULE_EXT_BOTTOM_LINE_TIFF_PREVIEW}" == "ENABLED"; + /** * Default creators. * @@ -128,23 +231,33 @@ var ext_bottom_line = function($, logger, is_in_view_port, load_config, getEntit id: "_default_creators.pictures", is_applicable: (entity) => _path_has_file_extension( entity, ["jpg", "png", "gif", "svg"]), - create: _create_picture_preview + create: _create_picture_preview, + }, { + id: "_default_creators.tiff_images", + is_applicable: (entity) => _tiff_preview_enabled && _path_has_file_extension( + entity, ["tif", "tiff","dng","cr2","nef"]), + create: _create_tiff_preview, }, { // videos id: "_default_creators.videos", is_applicable: (entity) => _path_has_file_extension( entity, ["mp4", "mov", "webm"]), create: _create_video_preview, + }, { // tables + id: "_default_creators.table_preview", + is_applicable: (e) => ext_table_preview.is_table(e), + create: (e) => ext_table_preview.get_preview(e), }, { // fallback id: "_default_creators.fallback", is_applicable: (entity) => true, create: (entity) => fallback_preview, - }, + } ]; const previewShownEvent = new Event("ext_bottom_line.preview.shown"); const previewReadyEvent = new Event("ext_bottom_line.preview.ready"); + const previewHiddenEvent = new Event("ext_bottom_line.preview.hidden"); const _css_class_preview_container = "caosdb-f-ext_bottom_line-container"; const _css_class_preview_container_resolvable = "caosdb-f-ext_bottom_line-container-resolvable"; @@ -191,14 +304,10 @@ var ext_bottom_line = function($, logger, is_in_view_port, load_config, getEntit preview_container.empty(); var buttons = preview_container.siblings(`.${_css_class_preview_container_button}`); if (element) { - buttons.css({ - "visibility": "initial" - }); + buttons.toggleClass("d-none", false); preview_container.append(element); } else { - buttons.css({ - "visibility": "hidden" - }); + buttons.toggleClass("d-none", true); } } else { logger.error(new Error("Could not find the preview container.")); @@ -231,8 +340,10 @@ var ext_bottom_line = function($, logger, is_in_view_port, load_config, getEntit } } catch (err) { logger.error(err); - const err_msg = "An error occured while loading this preview"; - set_preview_container(entity, err_msg); + if (!err._is_bottom_line_error) { + err = new BottomLineError(err); + } + set_preview_container(entity, err.to_html()); } } @@ -285,14 +396,16 @@ var ext_bottom_line = function($, logger, is_in_view_port, load_config, getEntit * @return {HTMLElement} the newly created container. */ var add_preview_container = function(entity) { - const button_show = $('<button class="btn btn-xs"><span class="glyphicon glyphicon-menu-down"/> Show Preview</button>') + const button_show = $('<button class="btn btn-sm card-footer"><i class="bi bi-chevron-down"></i> Show Preview</button>') .css({ - width: "100%" + width: "100%", + padding: "0", }) - .addClass(_css_class_preview_container_button); - const button_hide = $('<button class="btn btn-xs"><span class="glyphicon glyphicon-menu-up"/> Hide Preview</button>') + .addClass(_css_class_preview_container_button) + const button_hide = $('<button class="btn btn-sm card-footer"><i class="bi bi-chevron-up"></i> Hide Preview</button>') .css({ - width: "100%" + width: "100%", + padding: "0", }) .addClass(_css_class_preview_container_button) .hide(); @@ -318,6 +431,9 @@ var ext_bottom_line = function($, logger, is_in_view_port, load_config, getEntit container.on("shown.bs.collapse", () => { container[0].dispatchEvent(previewShownEvent); }); + container.on("hidden.bs.collapse", () => { + container[0].dispatchEvent(previewHiddenEvent); + }); $(entity).append(container); $(entity).append(button_show); $(entity).append(button_hide); @@ -448,6 +564,9 @@ var ext_bottom_line = function($, logger, is_in_view_port, load_config, getEntit } + /** + * @exports ext_bottom_line + */ return { previewShownEvent: previewShownEvent, previewReadyEvent: previewReadyEvent, @@ -460,8 +579,12 @@ var ext_bottom_line = function($, logger, is_in_view_port, load_config, getEntit _css_class_preview_container, _css_class_preview_container_button, _css_class_preview_container_resolvable, + BottomLineError: BottomLineError, + BottomLineWarning: BottomLineWarning, } -}($, log.getLogger("ext_bottom_line"), resolve_references.is_in_viewport_vertically, load_config, getEntityPath, connection); +}($, log.getLogger("ext_bottom_line"), + is_bottom_in_viewport, load_config, getEntityPath, + connection, UTIF, ext_table_preview); /** @@ -484,20 +607,23 @@ var plotly_preview = function(logger, ext_bottom_line, plotly) { * to be plotted. * @param {object[]} layout - dictionary of settings defining the layout of * the plot. + * @param {object[]} settings - object containing additional + * settings for the plot. * @returns {HTMLElement} the element which contains the plot. */ const create_plot = function(data, - layout = { - margin: { - t: 0 - }, - height: 400, - widht: 400 - }) { + layout = { + margin: { + t: 0 + }, + height: 400, + widht: 400 + }, + settings = { + responsive: true + }) { var div = $('<div/>')[0]; - plotly.newPlot(div, data, layout, { - responsive: true - }); + plotly.newPlot(div, data, layout, settings); return div; } @@ -525,7 +651,7 @@ var plotly_preview = function(logger, ext_bottom_line, plotly) { // this will be replaced by require.js in the future. $(document).ready(function() { - if ("${BUILD_MODULE_EXT_BOTTOM_LINE}" === "ENABLED") { + if ("${BUILD_MODULE_EXT_BOTTOM_LINE}" == "ENABLED") { caosdb_modules.register(plotly_preview); caosdb_modules.register(ext_bottom_line); } diff --git a/src/core/js/ext_cosmetics.js b/src/core/js/ext_cosmetics.js index 3913a37b6645e88d3a357b9c4c124cf296ccfcad..e70f8e5779bb75ebd08a47ec080fc57dc1348dc4 100644 --- a/src/core/js/ext_cosmetics.js +++ b/src/core/js/ext_cosmetics.js @@ -14,7 +14,7 @@ var cosmetics = new function() { $(this).parent().css("overflow", "hidden"); $(this).parent().css("text-overflow", "ellipsis"); - $(this).html('<a href="' + uri + '"><span class="glyphicon glyphicon-new-window"></span> ' + text + '</a>'); + $(this).html('<a href="' + uri + '"><i class="bi-window"></i> ' + text + '</a>'); } }); } diff --git a/src/core/js/ext_entity_state.js b/src/core/js/ext_entity_state.js new file mode 100644 index 0000000000000000000000000000000000000000..101c3800fc16632f2e36778a9e0468feae43f677 --- /dev/null +++ b/src/core/js/ext_entity_state.js @@ -0,0 +1,236 @@ +/* + * ** header v3.0 + * This file is a part of the CaosDB Project. + * + * Copyright (C) 2020 IndiScale GmbH <info@indiscale.com> + * Copyright (C) 2020 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/>. + * + * ** end header + */ + +'use strict'; + +const ext_entity_state = function ($, logger, edit_mode, update_state, getEntityXML) { + + const entity_state_transition_ready_event = new Event("entity_state.transition.ready"); + + /** + * Return an xml representation of an entity's state + * + * @param {State} state - the entity state + * @return {XMLElement} + */ + const state_to_xml = function (state) { + logger.trace("enter state_to_xml", state); + const name = state.name ? ` name="${state.name}"` : ""; + const model = state.model ? ` model="${state.model}"` : ""; + const id = state.id ? ` id="${state.id}"` : ""; + return str2xml(`<State${name}${model}${id}/>`).firstElementChild; + } + + /** + * Generate a function which replaces the edit_mode.form_to_xml function and + * includes the entity state into the output xml. + * + * Internally, the function calls the original function (proxy pattern). + * + * @pararm {function} original - the original edit_mode.form_to_xml function. + * @return {function} + */ + const create_edit_mode_form_to_xml_function = function (original) { + const result = function (entity_form) { + const entity_xml = original(entity_form); + const state = ext_entity_state.get_entity_state(entity_form); + if (state) { + const state_xml = ext_entity_state.state_to_xml(state); + entity_xml.firstElementChild.appendChild(state_xml); + } + return entity_xml; + } + return result; + } + + /** + * Patch the edit mode to include the entity state (unchanged) into the xml + * when inserting or updating an entity. + */ + const init_edit_mode_patch = function () { + edit_mode.form_to_xml = create_edit_mode_form_to_xml_function( + edit_mode.form_to_xml); + } + + /** + * Return the state of an entity. + * + * @param {HTMLElement} entity - an entity in HTML representation. + * @return {State} + */ + const get_entity_state = function (entity) { + const result = { + "id": entity.getAttribute("data-state-id"), + "name": entity.getAttribute("data-state-name"), + "model": entity.getAttribute("data-state-model"), + }; + if (result.id || (result.name && result.model)) { + return result; + } + return undefined; + } + + /** + * Represents an entity state. + * + * @typedef {Object} State + * @property {string} id - the entity id of the state. + * @property {string} name - the name of the state. + * @property {string} model - the name of the state model. + + /** + * Set a new entity state + * + * @param {HTMLElement} entity - an entity in HTML representation. + * @param {State} state - the new state + */ + const set_entity_state = function (entity, state) { + entity.removeAttribute("data-state-name"); + entity.removeAttribute("data-state-model"); + entity.removeAttribute("data-state-id"); + if (state.id) + entity.setAttribute("data-state-id", state.id); + if (state.name) + entity.setAttribute("data-state-name", state.name); + if (state.model) + entity.setAttribute("data-state-model", state.model); + } + + /** + * Perform or start a state transition. + * + * If the transition is the (special) 'Edit' transition this function + * triggers the edit_mode and opens the entity in the edit_mode form. + * + * Otherwise, the entity is set to the new entity state and then the update + * requests is performed. Apart from that, the entity is left unchanged. + * + * For a reinitialization of relevant clients after the transition, the + * `entity_state_transition_ready_event` is dispatched after the transition + * has been performed. + * + * @param {HTMLElement} entity - entity in HTML representation. + * @param {Transition} transition - the transition + */ + const perform_transition = async function (entity, transition) { + logger.trace("enter perform_transition", entity, transition); + const next_state = ext_entity_state.get_entity_state(entity); + next_state.id = undefined; + next_state.name = transition.to_state; + if (transition.name.toLowerCase() == "edit") { + if(!edit_mode.is_edit_mode()) { + // switch on edit_mode + await edit_mode.toggle_edit_mode(); + } + try { + const entity_form = edit_mode.edit(entity); + ext_entity_state.set_entity_state(entity_form, next_state); + } catch (err) { + logger.warn(err); + } + } else { + const entity_xml = getEntityXML(entity); + const state_xml = ext_entity_state.state_to_xml(next_state); + entity_xml.firstElementChild.appendChild(state_xml); + const response = await ext_entity_state.update_state(entity_xml); + const updated_entity = await transformation.transformEntities(response); + // remove warnings and info messages + $(updated_entity).find(".alert-warning, .alert-info").remove(); + edit_mode.smooth_replace(entity, updated_entity[0]); + updated_entity[0].dispatchEvent(entity_state_transition_ready_event); + resolve_references.init(); + preview.init(); + } + } + + /** + * Represents a entity state transition + * + * @typedef {object} Transition + * @property {string} name - name of the transition. + * @property {string} to_state - name of the target state of the transition. + + /** + * Return the transition which a button stands for. + * + * @param {HTMLElement} button - a transition button from the state's modal + * popover. + * @return {Transition} + * + */ + const get_transition = function (button) { + return { + "name": button.getAttribute("data-transition-name"), + "to_state": button.getAttribute("data-to-state"), + }; + } + + /** + * @param {HTMLElement} entity + */ + const init_state_transitions = function (entity) { + $(entity) + .find(".caosdb-f-entity-state-transition-button") + .click(function() { + const transition = ext_entity_state.get_transition(this); + ext_entity_state.perform_transition(entity, transition); + $(entity).find(".caosdb-f-entity-state-info").modal("hide"); + }); + + } + + const init = async function () { + logger.info("init ext_entity_state"); + const entities = $(".caosdb-entity-panel"); + + document.body.addEventListener(edit_mode.end_edit.type, (e) => { + // reinitialization after an entity has been changed in the edit mode + init_state_transitions(e.target); + }, true); + + document.body.addEventListener(entity_state_transition_ready_event.type, (e) => { + // reinitialization after a state transition has been performed + init_state_transitions(e.target); + }, true); + + for (let entity of entities) { + // initialize all entities on the page which have a state + init_state_transitions(entity); + } + + init_edit_mode_patch(); + } + + return { + init: init, + state_to_xml: state_to_xml, + get_entity_state: get_entity_state, + perform_transition: perform_transition, + get_transition: get_transition, + update_state: update_state, + } +}($, log.getLogger("ext_entity_state"), edit_mode, update, getEntityXML); + +$(document).ready(function () { + caosdb_modules.register(ext_entity_state); +}); diff --git a/src/core/js/ext_file_download.js b/src/core/js/ext_file_download.js new file mode 100644 index 0000000000000000000000000000000000000000..477349b421e03d74aa28fddf6b4748f64ffa9f7b --- /dev/null +++ b/src/core/js/ext_file_download.js @@ -0,0 +1,186 @@ +/* + * ** header v3.0 + * This file is a part of the CaosDB Project. + * + * Copyright (C) 2020 IndiScale GmbH <info@indiscale.com> + * Copyright (C) 2020 Henrik tom Wörden <h.tomwoerden@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/>. + * + * ** end header + */ + +'use strict'; + +/** + * The ext_file_download module provides a very basic preview for table files. + * + * The preview is generated using a server side script. + * + * @module ext_file_download + * @version 0.1 + * + * @requires jQuery + * @requires log + */ +var ext_file_download = function ($, logger) { + + /** + * collect the file ids that will be passed to the zip script + * + * @return {string[]} array of entity ids + */ + const collect_ids = function(){ + var properties = $(".caosdb-f-property-value").find( + ".caosdb-id.caosdb-f-property-single-raw-value"); + var id_list = properties.toArray().map(x=>x.textContent); + var entities = $("tr[data-entity-id]") + id_list = id_list.concat(entities.toArray().map( + x=>x.attributes['data-entity-id'].value)); + return id_list + }; + + /** + * chunk list to smaller pieces. + * + * @param {string[]} list - array of (usually) ids + * @param {Number} size - chunk size (use integer) + * @return {string[][]} array of array of string from the original list. + */ + const chunk_list = function(list, size){ + size = size || 20; + var pieces = []; + var index = 0; + while (index < list.length){ + pieces.push(list.slice(index,index+size)); + index+=size; + } + + return pieces + }; + + + /** + * create select statement to find files. + * + * @param {string[]} id_list - array of ids + * @return {string} a query string + */ + const query_files_str = function(id_list){ + var povs = id_list.map(x=>` id=${x} `); + const query_str="SELECT ID FROM FILE WITH " + povs.join("or"); + return query_str; + }; + + /** + * Reduce id list to files. Throw away all ids which do not belong to a + * file entity. + * + * @param {string[]} id_list - array of ids + * @return {string[]} array of file ids. + */ + const reduce_ids = async function(id_list){ + var file_ids = [] + for (var part of chunk_list(id_list)) { + // query for files + var result = await query(query_files_str(part)); + file_ids=file_ids.concat(result.map(x => getEntityID(x))); + } + return file_ids; + }; + + + /** + * Callback function for the download files link. + * + * Collects all the file entities and sends them to a server-side script + * which then puts the files into a zip. + * + * @param {HTMLElement} zip_link - the link element which triggered this + * call and which will be disabled during the execution of this + * function in order to prevent the user from triggering the process + * twice. + */ + const download_files = async function (zip_link) { + const onClickValue = zip_link.getAttribute("onClick"); + try { + // remove click event handler which called this function in the first place + zip_link.removeAttribute("onClick"); + + // add loading info. TODO make an animated one + $("#downloadModal").find(".caosdb-f-modal-footer-left").append( + createWaitingNotification("Collecting files...") + ); + + var ids = collect_ids(); + ids = await reduce_ids(ids); + if (ids.length == 0){ + alert ("There are no file entities in this table."); + return; + } + + var table = await ext_bookmarks.get_export_table(ids, "", "\t", "\n"); + + const result = await connection.runScript( + "ext_file_download/zip_files.py", + { + "-p0": ids, + "-p1": table, + } + ); + const code = result.getElementsByTagName("script")[0].getAttribute("code"); + if (parseInt(code) > 0) { + throw ("An error occurred during execution of the server-side script:\n" + + result.getElementsByTagName("script")[0].outerHTML); + } + const filename = result.getElementsByTagName("stdout")[0].textContent; + if (filename.length == 0) { + throw("Server-side script produced no file or did not return the file name: \n" + + result.getElementsByTagName("script")[0].outerHTML); + } + + + // trigger download of generated file + caosdb_table_export.go_to_script_results(filename); + + //close modal + $("#downloadModal").find(".modal-footer").find(".btn")[0].click(); + } catch (e) { + globalError(e); + } finally { + removeAllWaitingNotifications($("#downloadModal")[0]); + // restore the old click handler - hence a new file is generated with each click. + zip_link.setAttribute("onClick", onClickValue); + } + + }; + + const init = function () { + // only enable when init is being called + logger.info("init ext_file_download"); + if (userIsAnonymous()) { + $("#caosdb-f-query-select-files").parent().hide(); + } + }; + + return { + init: init, + download_files: download_files, + collect_ids: collect_ids, + chunk_list: chunk_list, + }; + +}($, log.getLogger("ext_file_download")); + +// This module is registered by caosdb_table_export. diff --git a/src/core/js/ext_jupyterdrag.js b/src/core/js/ext_jupyterdrag.js index 87d2f3a964ced1411df66da04ff4ddd4d7db2f15..0d055170076e23dfecc3d282ffeee03886b1d15c 100644 --- a/src/core/js/ext_jupyterdrag.js +++ b/src/core/js/ext_jupyterdrag.js @@ -72,5 +72,7 @@ var ext_jupyterdrag = function($, logger, getEntityRole, getEntityID) { $(document).ready(function() { - caosdb_modules.register(ext_jupyterdrag); + if ("${BUILD_MODULE_EXT_JUPYTERDRAG}" == "ENABLED") { + caosdb_modules.register(ext_jupyterdrag); + } }); diff --git a/src/core/js/ext_map.js b/src/core/js/ext_map.js index 67c7bfe799c8c96eb945ec9334ad87fa3f7d70f9..719d4f77943fdfca562961838dc02d95d5fd5cb1 100644 --- a/src/core/js/ext_map.js +++ b/src/core/js/ext_map.js @@ -25,7 +25,7 @@ /** * @module caosdb_map - * @version 0.3 + * @version 0.4 * * For displaying a geographical map which shows entities at their associated * geolocation. @@ -34,7 +34,7 @@ * `conf/ext/json/ext_map.json` and comply with the {@link MapConfig} type * which is described below. * - * The current version is 0.3. It is not considered to be stable because + * The current version is 0.4. It is not considered to be stable because * implementation of the graticule still is not satisfactory. * * Apart from that the specification of the configuration and the @@ -43,12 +43,19 @@ var caosdb_map = new function () { var logger = log.getLogger("caosdb_map"); - this.version = "0.3"; + this.version = "0.4"; this.dependencies = ["log", { "L": ["latlngGraticule", "Proj"] }, "navbar", "caosdb_utils"]; this.logger = logger; + /** + * Map is initialized, map button is visible in the menu. + * + * @event caosdb_map#map_ready + */ + this.map_ready = new Event("caosdb.caosdb_map.map_ready"); + /** * The MapConfig object is used to define all relevant parameters for the * map including the tiling servers, different views and CRSs of the map, @@ -84,10 +91,14 @@ var caosdb_map = new function () { /** * The SelectConfig object configures the custom {@link select_handler} * plugin for the Leaflet.js module, especially the query generation (for - * searching for Entities in the selected area). + * searching for Entities in the selected area) and when retrieving + * entities to be shown. * - * The generated query has the pattern <code>FIND {@link query.role} {@link - * query.entity} WITH ...<code>. The dots stand for the area filter here. + * The generated query for a selected area has the pattern + * <code>FIND {@link query.role} {@link query.entity} WITH PATH AREA<code>. + * PATH can be empty or represent a configured path to some entity, e.g. + * `WITH RT1 WITH RT2`. + * AREA stand for the area filter here. * * The default values of the {@link query} result in queries for any Record * in the selected map area. @@ -98,6 +109,8 @@ var caosdb_map = new function () { * are to be searched in the selected ares. * @property {string} [query.entity] The (parent) entity to be searched * for in the area. Defaults to empty string. + * @property {object} [paths] - A dictionary of paths that define from + * which entities the geographic location shall be taken. */ /** @@ -115,7 +128,7 @@ var caosdb_map = new function () { * {@link view_change_handler} plugin. * * Note: Leaflet comes with a few pre-defined coordinate reference systems - * (cf. {@link https://leafletjs.com/reference-1.5.0.html#crs Defined + * (cf. {@link https://leafletjs.com/reference-1.5.1.html#crs Defined * CRSs}). By default, the default CRS of leaflet will be used for the map * (which is currently EPSG:3857, the Sperical Mercator). If {@link crs} is * a string, e.g. "EPSG:3395" or "Simple" that matches the pre-defined CRS, @@ -127,7 +140,7 @@ var caosdb_map = new function () { * preserving the active view and view configuration across reloads of * the page. * @property {string} name - the name is shown in the views menu. - * @property {string} descriptoin - a short discription of the views + * @property {string} description - a short discription of the views * purpose and properties. Also shown in the views menu when mouse * hovers over the name. * @property {number} zoom - Initial zoom level. Must be an integer and @@ -145,9 +158,9 @@ var caosdb_map = new function () { /** * The TileLayerConfig is a thin extension wrapper around the {@link - * https://leafletjs.com/reference-1.5.0.html#tilelayer-wms-option + * https://leafletjs.com/reference-1.5.1.html#tilelayer-wms-option * TileLayer.WMS options} and the {@link - * https://leafletjs.com/reference-1.5.0.html#tilelayer-option TileLayer + * https://leafletjs.com/reference-1.5.1.html#tilelayer-option TileLayer * options}. * * Only three properties are defined by the wrapper {@link type}, {@link @@ -168,9 +181,9 @@ var caosdb_map = new function () { * * The {@link options} is an object which has all properties of the * respective tileLayer as defined by {@link - * https://leafletjs.com/reference-1.5.0.html#tilelayer-option + * https://leafletjs.com/reference-1.5.1.html#tilelayer-option * TileLayer options} when {@link type} = "osm" or {@link - * https://leafletjs.com/reference-1.5.0.html#tilelayer-wms-option + * https://leafletjs.com/reference-1.5.1.html#tilelayer-wms-option * TileLayer.WMS options} when {@link type} = "wms". * * @example <caption>Example for a TileLayerConfig with OSM</caption> @@ -199,9 +212,9 @@ var caosdb_map = new function () { * "wms" * @property {string} url - the url of the OSM or WMS server. * @property {object} options - the {@link - * https://leafletjs.com/reference-1.5.0.html#tilelayer-option + * https://leafletjs.com/reference-1.5.1.html#tilelayer-option * TileLayer options} or {@link - * https://leafletjs.com/reference-1.5.0.html#tilelayer-wms-option + * https://leafletjs.com/reference-1.5.1.html#tilelayer-wms-option * TileLayer.WMS options}. */ @@ -332,6 +345,7 @@ var caosdb_map = new function () { "role": "RECORD", "entity": "", }, + "paths": {}, }, } @@ -373,21 +387,190 @@ var caosdb_map = new function () { */ /** - * Implements {@link mapEntityGetter}. + * Generates a Property Operator Value (POV) expression by chaining the + * provided arguments with "WITH". + * + * @param {string[]} props - array with the names of RecordTypes + * @returns {string} string with the the filter + */ + this._get_with_POV = function (props) { + var pov = "" + for (let p of props) { + pov = pov + ` WITH ${p} `; + } + return pov; + } + + /** + * Generates a Property Operator Value (POV) by joining ids with OR. + * + * @param {number[]} ids - array of ids for the filter + * @returns {string} string with the the filter + */ + this._get_id_POV = function (ids) { + ids = ids.map(x => "id=" + x); + return "WITH " + ids.join(" or ") + } + + /** + * Generates a SELECT query string that applies the provided path of + * properties as POV and as selector + * + * If ids is provided, the condition is not created from the path, but + * from ids. + * + * @param {DataModelConfig} datamodel - datamodel of the entities to be returned. + * @param {string[]} path - array with the names of RecordTypes + * @param {number[]} ids - array of ids for the filter + * @returns {string} query string + */ + this._get_select_with_path = function (datamodel, path, ids) { + if (typeof datamodel === "undefined") { + throw new Error("Supply the datamodel.") + } + if (typeof path === "undefined" || path.length == 0) { + throw new Error("Supply at least a RecordType.") + } + const recordtype = path[0]; + const props = path.slice(1, path.length) + var selector = props.join(".") + if (selector != "") { + selector = selector + "." + } + var pov = undefined; + if (typeof ids === "undefined") { + pov = (caosdb_map._get_with_POV(props) + + ` WITH ${datamodel.lat} AND ${datamodel.lng}`); + + } else { + pov = caosdb_map._get_id_POV(ids); + } + return `SELECT parent,${selector}${datamodel.lat},${selector}${datamodel.lng} FROM ENTITY ${recordtype} ${pov} `; + + } + + + /** + * Returns a dictionary where for each top level record in the xmldoc + * The long and lat is assigned. + * + * depth: the depth of the tree including the top level record type: + * e.g. RT1->prop1->prop2->lat/long would be a depth=3 + * + * @param {XMLDocument} xmldoc - xml document containing the entities + * @param {number} depth - the depth at which the properties shall be taken + * @param {DataModelConfig} datamodel - datamodel of the entities to be returned. + * @returns {Object} a dictionary where for each id as key the location ist + * stored as [lat, lng] + */ + this._get_leaf_prop = function (xmldoc, depth, datamodel) { + const paths = [ + ["id"], + // The following creates a list: ["", "", ... (depth times), lat/long] + ("__split__".repeat(depth) + datamodel.lat).split("__split__"), + ("__split__".repeat(depth) + datamodel.lng).split("__split__"), + ]; + const propertyValues = getPropertyValues(xmldoc, paths); + + const leaves = {}; + for (let row of propertyValues) { + leaves[row[0]] = [row[1], row[2]]; + } + return leaves; + } + + /** + * Template for {@link mapEntityGetter}. + * + * This implementation has a single additional parameter which is not + * defined by {@link mapEntityGetter}: + * + * @param {string[]} path - array of strings defining the path to the + * related entity */ - this._get_current_page_entities = function ( - datamodel, north, south, west, east) { - const container = $(".caosdb-f-main-entities")[0]; + this._generic_get_current_page_entities = async function ( + datamodel, north, south, west, east, path) { + var container = $(".caosdb-f-main-entities")[0]; + + if (typeof path !== "undefined" && path.length) { + var ids = [] + for (let rec of getEntities(container)) { + ids.push(getEntityID(rec)) + } + if (ids.length) { + const qs = caosdb_map._get_select_with_path(datamodel, path, ids); + let entities = await connection.get("Entity/?query=" + qs); + caosdb_map._set_subprops_at_top(entities, path.length - 1, datamodel); + let results = await transformation.transformEntities(entities); + container = $('<div>').append(results)[0]; + } else { + return []; + } + } + // it is possible, the the page contains entities which do not have + // lat/lng and there doesn't exist any related entity with lat/lng. return caosdb_map.get_map_entities(container, datamodel); } /** - * Implements {@link mapEntityGetter}. + * Returns a top level record entity from xml. + * + * @param {XMLDocument} entities - xml document containing the entities + * @param {number} rec_id - id of the record to be returned + * @returns {XMLDocument} the corresponding record + */ + this._get_toplvl_rec_with_id = function (entities, rec_id) { + let tmp = $(entities).find(`Response >[id='${rec_id}']`); + if (tmp.length != 1) { + throw new Error("There should be exactly one result record. Not " + + tmp.length) + } + return tmp[0]; + } + + /** + * Set the longitude/latitude from subproperties to the top level + * records in the xml and convert everything to html. + * + * @param {XMLDocument} entities - xml document containing the entities + * @param {number} depth - the depth of the path (full: including the first + * recordtype) + */ + this._set_subprops_at_top = function (entities, depth, datamodel) { + var latlong = caosdb_map._get_leaf_prop(entities, depth, datamodel); + + for (let rec_id in latlong) { + let tmp_rec = caosdb_map._get_toplvl_rec_with_id(entities, rec_id); + tmp_rec.append(str2xml(`<Property name="${datamodel.lat}">${latlong[rec_id][0]}</Property>`).firstElementChild); + tmp_rec.append(str2xml(`<Property name="${datamodel.lng}">${latlong[rec_id][1]}</Property>`).firstElementChild); + } + } + + /** + * Template for {@link mapEntityGetter}. + * + * This implementation has a single additional parameter which is not + * defined by {@link mapEntityGetter}: + * + * @param {string[]} path - array of strings defining the path to the + * related entity */ - this._query_all_entities = async function ( - datamodel, north, south, west, east) { - const results = await caosdb_map.query(`FIND ENTITY WITH ${datamodel.lat} AND ${datamodel.lng}`); + this._generic_query_all_entities = async function ( + datamodel, north, south, west, east, path) { + var results = undefined; + if (typeof path !== "undefined" && path.length) { + const qs = caosdb_map._get_select_with_path(datamodel, path); + let entities = await connection.get("Entity/?query=" + qs); + caosdb_map._set_subprops_at_top(entities, path.length - 1, datamodel); + results = await transformation.transformEntities(entities); + } else { + results = await caosdb_map.query( + `FIND ENTITY WITH ${datamodel.lat} AND ${datamodel.lng}`); + } const container = $('<div>').append(results)[0]; + + // As soon as the SELECT query can handle subtyping, the results don't + // have to filtered anymore. return caosdb_map.get_map_entities(container, datamodel); } @@ -410,7 +593,12 @@ var caosdb_map = new function () { const name = caosdb_map.make_entity_name_label(entity); const dms_lat = L.NumberFormatter.toDMS(lat); const dms_lng = L.NumberFormatter.toDMS(lng); - const loc = $(`<div class="small text-muted"> + let extra_loc_hint = ""; + let path = caosdb_map._get_current_path(); + if (path && path.length > 1) { + extra_loc_hint = `<div>Location of related ${path[path.length-1]}<div>`; + } + const loc = $(`<div class="small text-muted">${extra_loc_hint} Lat: ${dms_lat} Lng: ${dms_lng} </div>`); const ret = $('<div/>') @@ -421,6 +609,18 @@ var caosdb_map = new function () { return ret[0]; } + /** + * Returns the path from the config corresponding to the value stored in + * the session storage (i.e. the storage should be updated before calling + * this method if the value changes). + * + * @returns {string[]} path - array of strings defining the path to the + * related entity + */ + this._get_current_path = function () { + return caosdb_map.config.select.paths[sessionStorage["caosdb_map.display_path"]]; + } + /** * Default entities layers configuration with two layers: @@ -430,43 +630,52 @@ var caosdb_map = new function () { * * @type {EntityLayerConfig[]} */ - this._default_entity_layer_config = [{ - "id": "current_page_entities", - "name": "Entities on the current page.", - "description": "Show all entities on the current page.", - "icon": { - html: '<span style="color: #00F; font-size: 20px" class="glyphicon glyphicon-map-marker"></span>', - iconAnchor: [10, 19], - className: "", - }, - "zIndexOffset": 1000, - "datamodel": { - "lat": "latitude", - "lng": "longitude", - "role": "ENTITY", - "entity": "", - }, - "get_entities": this._get_current_page_entities, - "make_popup": this._make_map_popup, - }, { - "id": "all_map_entities", - "name": "All entities", - "description": "Show all entities with coordinates.", - "icon": { - html: '<span style="color: #F00; font-size: 20px" class="glyphicon glyphicon-map-marker"></span>', - iconAnchor: [10, 19], - className: "", + this._default_entity_layer_config = { + "current_page_entities": { + "name": "Entities on the current page.", + "description": "Show all entities on the current page.", + "icon": { + html: '<i class="bi-geo-alt-fill" style="font-size: 20px; color: #00F;"></i>', + iconAnchor: [10, 19], + className: "", + }, + "zIndexOffset": 1000, + "datamodel": { + "lat": "latitude", + "lng": "longitude", + "role": "ENTITY", + "entity": "", + }, + "get_entities": (datamodel, north, south, west, east) => { + let path = caosdb_map._get_current_path() + return caosdb_map._generic_get_current_page_entities( + datamodel, north, south, west, east, path) + }, + "make_popup": this._make_map_popup, }, - "zIndexOffset": 0, - "datamodel": { - "lat": "latitude", - "lng": "longitude", - "role": "ENTITY", - "entity": "", + "all_map_entities": { + "name": "All entities", + "description": "Show all entities with coordinates.", + "icon": { + html: '<i class="bi-geo-alt-fill" style="font-size: 20px; color: #F00;"></i>', + iconAnchor: [10, 19], + className: "", + }, + "zIndexOffset": 0, + "datamodel": { + "lat": "latitude", + "lng": "longitude", + "role": "ENTITY", + "entity": "", + }, + "get_entities": (datamodel, north, south, west, east) => { + let path = caosdb_map._get_current_path() + return caosdb_map._generic_query_all_entities( + datamodel, north, south, west, east, path) + }, + "make_popup": this._make_map_popup, }, - "get_entities": this._query_all_entities, - "make_popup": this._make_map_popup, - }, ]; + }; /** @@ -525,7 +734,7 @@ var caosdb_map = new function () { this.create_toggle_map_button = function (content = "Map") { logger.trace("enter create_toggle_map_button"); let button = $( - `<button class="navbar-btn btn btn-link"/>`); + `<a class="nav-link" role="button"></a>`); button.toggleClass("caosdb-f-toggle-map-button", true); button.text(content); logger.trace("leave create_toggle_map_button"); @@ -540,13 +749,7 @@ var caosdb_map = new function () { panel.toggleClass("caosdb-f-map-panel", true); // for centered and responsive display - panel.toggleClass("container", true); - - // TODO move to css file - $(panel).css({ - "height": "500px" - }); - + panel.toggleClass(["container", "mb-2"], true); return panel[0]; } @@ -595,7 +798,9 @@ var caosdb_map = new function () { config.tileLayer.type); } - var map = L.map(container, config); + const wrapped = $("<div/>"); + $(container).append(wrapped); + var map = L.map(wrapped[0], config); map._crs = config.crs; tileLayer.addTo(map); @@ -665,6 +870,13 @@ var caosdb_map = new function () { } + this.show_map = function () { + logger.trace("enter show_map"); + $(".caosdb-f-map-panel").show(900, () => this + ._toggle_cb()); + } + + /** * To be called after the map panel has been toggled. */ @@ -730,6 +942,22 @@ var caosdb_map = new function () { throw new Error("Could not find view " + id); } + /** + * Reload layers. + */ + this._reload_layers = function () { + caosdb_map._show_load_info() + const promises = [] + for (const layer of caosdb_map.layers) { + promises.push(caosdb_map._fill_layer(layer.layer_group, + caosdb_map._default_entity_layer_config[layer.id])); + } + Promise.all(promises).then((val) => { + caosdb_map._hide_load_info() + }) + } + + /** Initialize the caosdb_map module. * @@ -738,6 +966,8 @@ var caosdb_map = new function () { * 3) initialize the container panel * 4) inttialize the map itself * 5) add the map toggle button to the navbar + * + * @fires caosdb_map#map_ready */ this.init = async function () { logger.trace("enter init"); @@ -760,6 +990,7 @@ var caosdb_map = new function () { // TODO split in smaller pieces and move callback to separate function this.change_map_view = (view) => { if (this._map) { + this._map._container.remove(); this._map.remove(); } @@ -807,16 +1038,36 @@ var caosdb_map = new function () { view_config); // init entity layers - var layers = this.init_entity_layers(this._default_entity_layer_config); + this.layers = this.init_entity_layers(this._default_entity_layer_config); var layerControl = L.control.layers(); - for (const layer of layers) { + + const promises = [] + for (const layer of this.layers) { + + promises.push(caosdb_map._fill_layer(layer.layer_group, + this._default_entity_layer_config[layer.id])); layerControl.addOverlay(layer.layer_group, layer.chooser_html.outerHTML); layer.layer_group.addTo(this._map); } + Promise.all(promises).then((val) => { + caosdb_map._hide_load_info() + }) layerControl.addTo(this._map); // initialize handlers this.add_select_handler(this._map); + + + this.path_ddm = this._get_path_ddm( + (event) => { + sessionStorage["caosdb_map.display_path"] = event.target.value; + caosdb_map._reload_layers(); + }, + this.config.select.paths + ); + this._map.addControl(this.path_ddm); + + this.add_view_change_handler( this._map, config.views, @@ -827,6 +1078,12 @@ var caosdb_map = new function () { this.change_map_view(); toggle_button = this.init_toggle_map_button(); + + // indicate that the map is ready: map button is present and + // map is hidden or shown but initialized in either case. + this._map.whenReady(()=>{ + document.body.dispatchEvent(caosdb_map.map_ready); + }); } catch (err) { logger.error("Could not initialize the map.", err); @@ -858,12 +1115,33 @@ var caosdb_map = new function () { */ this.init_entity_layers = function (configs) { var ret = [] - for (const conf of configs) { - ret.push(this.init_entity_layer(conf)); + for (let name in configs) { + configs[name]["id"] = name; + ret.push(this._init_single_entity_layer(configs[name])); } return ret; } + /** + * Initialize an entity layer. + * + * @param {EntityLayerConfig} config + * @return {_EntityLayer} + */ + this._fill_layer = async function (layer_group, config) { + // in case load is called on a filled layer: clear first + layer_group.clearLayers(); + + var entities = await config.get_entities(config.datamodel); + layer_group.entities = entities; + var markers = caosdb_map.create_entity_markers( + entities, config.datamodel, config.make_popup, + config.zIndexOffset, config.icon); + + for (const marker of markers) { + layer_group.addLayer(marker); + } + }; /** * Initialize an entity layer. @@ -871,24 +1149,11 @@ var caosdb_map = new function () { * @param {EntityLayerConfig} config * @return {_EntityLayer} */ - this.init_entity_layer = function (config) { - logger.trace("enter init_entity_layer", config); + this._init_single_entity_layer = function (config) { + logger.trace("enter _init_single_entity_layer", config); var layer_group = L.layerGroup(); - // load all entities into layer group - var _load = async function (layer_group, config) { - var entities = await config.get_entities(config.datamodel); - var markers = caosdb_map.create_entitiy_markers( - entities, config.datamodel, config.make_popup, - config.zIndexOffset, config.icon); - - for (const marker of markers) { - layer_group.addLayer(marker); - } - }; - _load(layer_group, config); - var ret = { "id": config.id, "active": typeof config.active === "undefined" || config.active, @@ -975,7 +1240,6 @@ var caosdb_map = new function () { L.Handler.extend(this.select_handler); } - /** * Show the query panel if not visible, collapse the query shortcuts * if visible and fill the query string into the text input of the @@ -998,7 +1262,7 @@ var caosdb_map = new function () { ".caosdb-f-shortcuts-panel-toggle-button:not('.caosdb-f-shortcuts-panel-hidden')" ).click(); - query_panel + $("#caosdb-query-panel-collapsible") .collapse("show"); // fill query into text field @@ -1048,9 +1312,18 @@ var caosdb_map = new function () { this.generate_query_from_bounds = function (north, south, west, east) { const role = this.config.select.query.role; - const entity = this.config.select.query.entity; + var entity = this.config.select.query.entity; const lat = this.config.datamodel.lat; const lng = this.config.datamodel.lng; + let path = caosdb_map._get_current_path(); + if (path && path.length > 0 && entity == "") { + entity = path[0]; + } + var additional_path = "" + if (path && path.length > 1) { + additional_path = caosdb_map._get_with_POV( + path.slice(1, path.length)) + } const query_filter = " ( " + lat + " < '" + north + "' AND " + lat + @@ -1058,7 +1331,7 @@ var caosdb_map = new function () { "' AND " + lng + " < '" + east + "' ) "; - const query = "FIND " + role + " " + entity + + const query = "FIND " + role + " " + entity + additional_path + " WITH " + query_filter; return query } @@ -1172,7 +1445,7 @@ var caosdb_map = new function () { var parents = getParents(entity); var ret = []; for (const par of parents) { - var label = $('<span class="label">' + par.name + + var label = $('<span class="badge">' + par.name + '</span>') // TODO move to global css .css({ @@ -1201,10 +1474,10 @@ var caosdb_map = new function () { const entity_on_page = $(`#${id}`).length > 0; const href = entity_on_page ? `#${id}` : connection.getBasePath() + `Entity/${id}` - const link_title = entity_on_page ? "Jump to this entitiy." : "Browse to this entity."; + const link_title = entity_on_page ? "Jump to this entity." : "Browse to this entity."; const link = $(`<a title="${link_title}" href="${href}"/>`) .addClass("pull-right") - .append(`<span class="glyphicon glyphicon-share-alt"/></a>`); + .append(`<i class="bi bi-box-arrow-up-right"></i></a>`); const name_label = $('<div/>') // TODO move to global css @@ -1239,8 +1512,8 @@ var caosdb_map = new function () { * @param {DivIcon_options} icon_options * @returns {L.Marker[]} an array of markers for the map. */ - this.create_entitiy_markers = function (entities, datamodel, make_popup, zIndexOffset, icon_options) { - logger.trace("enter create_entitiy_markers", entities, datamodel, zIndexOffset, icon_options); + this.create_entity_markers = function (entities, datamodel, make_popup, zIndexOffset, icon_options) { + logger.trace("enter create_entity_markers", entities, datamodel, zIndexOffset, icon_options); var ret = [] for (const map_entity of entities) { @@ -1411,7 +1684,7 @@ var caosdb_map = new function () { // TODO refactor and extract function for map controls and // merge with similar code from the select_handler. var button = L.DomUtil.create("div", - "leaflet-bar leaflet-control leaflet-control-custom" + "leaflet-bar leaflet-control leaflet-control-custom caosdb-f-map-change-view-btn" ); button.title = "Change the view"; // TODO move to css @@ -1421,7 +1694,7 @@ var caosdb_map = new function () { button.style.textAlign = "center"; button.style.marginTop = "2px"; button.innerHTML = - '<span style="margin-top: 5px; font-size: 15px" class="glyphicon glyphicon-option-vertical"></span>'; + '<i style="font-size: 20px" class="bi-three-dots-vertical"></i>'; button.addEventListener("click", click); $(button).prepend(view_menu); @@ -1440,6 +1713,83 @@ var caosdb_map = new function () { }, } + /** + * Shows the loading information div of the map + */ + this._show_load_info = function () { + $(".caosdb-f-map-loading").attr("style", "display:inherit"); + } + + /** + * Hides the loading information div of the map + */ + this._hide_load_info = function () { + $(".caosdb-f-map-loading").attr("style", "display:None"); + } + + /** + * Return a new leaflet control for setting paths to use for geo location + * + * @param {function} callback - a callback applies the effect of a + * changed path + * @returns {L.Control} the drop down menu button. + */ + this._get_path_ddm = function (callback, paths) { + + // TODO flatten the structure of the code and possibly merge it with the query_button code. + var path_ddm = L.Control.extend({ + options: { + position: "bottomright" + }, + + onAdd: function (m) { + return this.button; + }, + + button: function () { + // TODO refactor to make_map_control function + var button = L.DomUtil + .create("div", + "leaflet-bar leaflet-control leaflet-control-custom" + ); + button.title = `Show the location of related entities. +By default ('Same Entity') entities are shown that have +a geographic location. The other options allow to show +entities on the map using the location of a related +entity.`; + button.style.backgroundColor = "white"; + button.style.textAlign = "center"; + // Distance to zoom buttons: + button.style.marginTop = "10px"; + // TODO implement helper for pictures + let tmp_html = ('<div class="caosdb-f-map-loading" style="display:inherit">Loading Entities...</div><select><option value="same">Same Entity</option>'); + for (let pa in paths) { + tmp_html += `<option value="${pa}">${pa}</option>`; + } + tmp_html += '</select>'; + button.innerHTML = tmp_html; + const select = $(button).find('select'); + select.on("change", callback); + + const current_path = sessionStorage["caosdb_map.display_path"] || "same"; + sessionStorage["caosdb_map.display_path"] = current_path; + select[0].value = current_path + + $(button).on("mousedown", ( + event) => { + event + .stopPropagation(); + }); + $(button).on("mouseup", ( + event) => { + event + .stopPropagation(); + }); + return button; + }(), + }); + return new path_ddm(); + } /** * Plug-in for leaflet which lets the user select an area in the map @@ -1650,7 +2000,7 @@ var caosdb_map = new function () { button.style.marginTop = "2px"; button.innerHTML = - '<span style="margin-top: 5px; font-size: 15px" class="glyphicon glyphicon-search"></span>'; + '<i style="margin-top: 5px; font-size: 15px" class="bi-search"></i>'; button.onclick = callback; $(button).on("mousedown", ( diff --git a/src/core/js/ext_references.js b/src/core/js/ext_references.js index 8a3bc50852381e5e9e9c6221b97a54f2f6841cfb..7cd597e128e8c09da9134f42f542898fb84a4e53 100644 --- a/src/core/js/ext_references.js +++ b/src/core/js/ext_references.js @@ -190,6 +190,10 @@ var resolve_references = new function () { var _scroll_timeout = undefined; + // bind global function to local context for unit testing + this.retrieve = retrieve; + this.getParents = getParents; + /** * This event is dispatched on the summary container after the summary has * been generated and appended to the container. @@ -292,18 +296,15 @@ var resolve_references = new function () { * {string} par - parent name. @return {boolean} */ this.is_child = function (entity, par) { - var pars = getParents(entity); - for (const par of pars) { - if (par.name === par) { + var pars = resolve_references.getParents(entity); + for (const thispar of pars) { + if (thispar.name === par) { return true; } } return false; } - - this.retrieve = retrieve; - /** * @typedef {reference_info} * @property {string} text @@ -333,7 +334,7 @@ var resolve_references = new function () { const entity = (await resolve_references.retrieve(id))[0]; // TODO handle multiple parents - const par = getParents(entity)[0] || {}; + const par = resolve_references.getParents(entity)[0] || {}; var ret = { "text": id @@ -391,8 +392,8 @@ var resolve_references = new function () { const target = resolve_references.add_target(rs); const id = getEntityID(rs); target.textContent = id; - const resolved_entity_info = await resolve_references - .resolve_reference(id); + const resolved_entity_info = ( + await resolve_references.resolve_reference(id)); target.textContent = resolved_entity_info.text; return resolved_entity_info; } @@ -449,9 +450,11 @@ var resolve_references = new function () { // Loop over all property values in the container element. Note: each property_value can be a single reference or a list of references. for (const property_value of property_values) { - const lists = $(property_value).find( - ".caosdb-value-list").has( - `.${_unresolved_class_name}`); + var lists = findElementByConditions( + property_value, + x => x.classList.contains("caosdb-value-list"), + x => x.classList.contains("caosdb-preview-container")) + lists = $(lists).has(`.${_unresolved_class_name}`); if (lists.length > 0) { logger.debug("processing list of references", lists); @@ -524,8 +527,10 @@ var resolve_references = new function () { // Load all remaining references. These are single reference values // and those references from lists which are left for lazy loading. - const rs = $(property_value).find( - `.${_unresolved_class_name}`); + const rs = findElementByConditions( + property_value, + x => x.classList.contains(`${_unresolved_class_name}`), + x => x.classList.contains("caosdb-preview-container")); for (var i = 0; i < rs.length; i++) { if (resolve_references.is_in_viewport_vertically( rs[i]) && diff --git a/src/core/js/ext_revisions.js b/src/core/js/ext_revisions.js deleted file mode 100644 index 3ee086e60ed34ac659c5bb92de71d78b38f30e2b..0000000000000000000000000000000000000000 --- a/src/core/js/ext_revisions.js +++ /dev/null @@ -1,229 +0,0 @@ -/* - * ** header v3.0 - * This file is a part of the CaosDB Project. - * - * Copyright (C) 2020 IndiScale GmbH <info@indiscale.com> - * Copyright (C) 2020 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/>. - * - * ** end header - */ - -'use strict'; - -/** - * The ext_revisions module extends the edit_mode update functionality. - * - * The edit_mode.update_entity function is overridden by this module with a - * proxy pattern. That means, that the original function is still called, but a - * proxy (or wrapper) function adds further functionality. - * - * The extended update function creates a back-up version of the updated entity - * and adds a revisionOf property to the updated entity which references - * the back-up. The back-up entity loses all of its original parents and gets - * an "Obsolete" as only parent instead. - * - * Per default, the module assumes two Entities to be present in the - * database. A RecordType named "Obsolete" and a Property named - * "revisionOf". The initialization is aborted if these entities cannot be - * found and the module remains inactive. - * - * @module ext_revisions - * @version 0.1 - * - * @requires jQuery - * @requires log - * @requires edit_mode - * @requires getEntityID - * @requires transaction - * @requires _createDocument - */ -var ext_revisions = function ($, logger, edit_mode, getEntityID, transaction, _createDocument) { - - - /** - * Default names for the two entities which are required by this module. - */ - var _datamodel = { obsolete: "Obsolete", revisionOf: "revisionOf" }; - - /** - * Generate and insert the back-up entity which stores the old state of the - * entity which is to be updated. - * - * The obsolete entity has only one parent named "Obsolete". - * Apart from that, the obsolete entity has all the properties, name, - * description and so on from the original entity before the update. - * - * @param {string} id - the id of the entity which is to be updated. - * @returns {string} the id of the newly created obsolete entity. - */ - var _insert_obsolete = async function (id) { - logger.debug("insert obsolete", id); - - // create new obsolete entity from the original - const obsolete = await transaction.retrieveEntityById(id); - $(obsolete).attr("id", "-1"); - $(obsolete).find("Permissions").remove(); - $(obsolete).find("Parent").remove(); - $(obsolete).append(`<Parent name="${_datamodel.obsolete}"/>`); - - const doc = _createDocument("Request"); - doc.firstElementChild.appendChild(obsolete); - const result = await transaction.insertEntitiesXml(doc); - const obsolete_id = $(result.firstElementChild).find("[id]").first().attr("id"); - logger.trace("leave _insert_obsolete", obsolete_id); - return obsolete_id; - }; - - /** - * Generate a HTML string which represents a new "revisionOf" property - * which references the newly created obsolete entity. The property is - * meant to be appended to the property section of the entity which is to - * be updated. - * - * @param {string} obsolete_id - the id of the newly created obsolete - * entity. - * @returns {string} A HTML represesentation of an entity property. - */ - var _make_revision_of_property = async function (obsolete_id) { - logger.trace("enter _make_revision_of_property", obsolete_id); - const ret = (await transformation.transformProperty(str2xml(`<Response><Property id="${_datamodel._revisionOfId}" name="${_datamodel.revisionOf}" datatype="${_datamodel.obsolete}"></Property></Response>`))).firstElementChild; - - $(ret).find(".caosdb-f-property-value").append(`<div class="caosdb-property-edit-value"><select><option value="${obsolete_id}" selected="selected"></option></select></div>`); - - logger.trace("leave _make_revision_of_property", ret); - return ret; - } - - /** - * Remove all properties from ent_element with the id of the "revisionOf" - * property. - * - * @param {HTMLElement} ent_element - entity in HTML representation. - */ - var _remove_old_revision_of_property = function (ent_element) { - $(ent_element) - .find(".caosdb-f-entity-property") - .filter(function(index, property) { - if(_datamodel._revisionOfId === $(property) - .find(".caosdb-property-id").text()) { - return true; - } - return false; - }).remove(); - } - - /** - * Main functionality of this module is in here. - * - * This method is called by the (overridden) edit_mode.update_entity - * function before the actual update. It inserts a new obsolete entity - * which represesents the old state of the entity, deletes any revisionOf - * properties of the entity (if present) and adds a new revisionOf property - * which references the (newly inserted) obsolete entity. - * - * @param {HTMLElement} ent_element - The entity form which has been - * generated by the edit_mode with the changes by the user. - */ - var _create_revision = async function (ent_element) { - logger.debug("create revision", ent_element); - var id = getEntityID(ent_element); - - var obsolete_id = await _insert_obsolete(id); - - // remove old revision of and add new one - _remove_old_revision_of_property(ent_element); - var revision_of_property = await _make_revision_of_property(obsolete_id); - var properties_section = ent_element.getElementsByClassName("caosdb-properties")[0]; - properties_section.appendChild(revision_of_property); - }; - - /** - * Test whether the necessary entities exist ("revisionOf" and "Obsolete"). - */ - var _check_datamodel = async function() { - var results = Promise.all([ - query(`FIND RecordType ${_datamodel.obsolete}`), - query(`FIND Property ${_datamodel.revisionOf}`) - ]); - - for (let result of (await results)) { - if (result.length !== 1) { - throw new Error("Invalid datamodel"); - } - - var name = getEntityName(result[0]); - if (name && name.toLowerCase() === _datamodel.revisionOf.toLowerCase()) { - _datamodel._revisionOfId = getEntityID(result[0]); - _datamodel.revisionOf = name; - } else if (name && name.toLowerCase() === _datamodel.obsolete.toLowerCase()) { - _datamodel.obsolete = name; - } - } - }; - - /** - * Initialize the ext_revisions module. - * - * Per default, the module assumes two Entities to be present in the - * database. A RecordType named "Obsolete" and a Property named - * "revisionOf". The initialization is aborted if these entities cannot be - * found and the module remains inactive. For testing purposes the names of - * these entities can be set to different values via the respective - * parameters. - * - * @param {string} [obsolete] - The name of the obsolete RecordType. - * @param {string} [revisionOf] - The name of the revisionOf Property. - */ - var init = async function (obsolete, revisionOf) { - if (typeof obsolete === "string") { - _datamodel.obsolete = obsolete; - } - if (typeof revisionOf === "string") { - _datamodel.revisionOf = revisionOf; - } - - try { - await _check_datamodel(); - } catch (err) { - logger.error("could not init ext_revisions", err); - return; - } - - (function(proxied) { - edit_mode.update_entity = async function(ent_element) { - await _create_revision(ent_element); - return await proxied.apply(this, arguments) - }; - })(edit_mode.update_entity); - } - - return { - // public members, part of the API - init: init, - // private members, exposed for testing - _make_revision_of_property: _make_revision_of_property, - _datamodel: _datamodel, - _logger: logger, - } -}($, log.getLogger("ext_revisions"), edit_mode, getEntityID, transaction, _createDocument); - - -// this will be replaced by require.js in the future. -$(document).ready(function () { - if ("${BUILD_MODULE_EXT_REVISIONS}" == "ENABLED") { - caosdb_modules.register(ext_revisions); - } -}); diff --git a/src/core/js/ext_table_preview.js b/src/core/js/ext_table_preview.js new file mode 100644 index 0000000000000000000000000000000000000000..708d4da4b69602b4824b46e2aac082e4037b3102 --- /dev/null +++ b/src/core/js/ext_table_preview.js @@ -0,0 +1,97 @@ +/* + * ** header v3.0 + * This file is a part of the CaosDB Project. + * + * Copyright (C) 2020 IndiScale GmbH <info@indiscale.com> + * Copyright (C) 2020 Henrik tom Wörden <h.tomwoerden@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/>. + * + * ** end header + */ + +'use strict'; + +/** + * The ext_table_preview module provides a very basic preview for table files. + * + * The preview is generated using a server side script. + * + * @module ext_table_preview + * @version 0.1 + * + * @requires jQuery + * @requires log + * @requires getEntityPath + * @requires getEntityID + * @requires markdown + */ +var ext_table_preview = function ($, logger, connection, getEntityPath, getEntityID, markdown) { + + const get_preview = async function (entity) { + try { + const script_result = await connection.runScript("ext_table_preview/pandas_table_preview.py", + {"-p0": getEntityID(entity)} + ); + + const code = script_result.getElementsByTagName("script")[0].getAttribute("code"); + if (parseInt(code) > 1) { + return script_result.getElementsByTagName("stderr")[0] + } else if (parseInt(code) != 0) { + throw ("An error occurred during execution of the server-side " + + "script:\n" + + script_result.getElementsByTagName("stderr")[0]); + } else { + const tablecontent = script_result.getElementsByTagName("stdout")[0]; + const unformatted = markdown.textToHtml(tablecontent.textContent) + const formatted = $('<div/>').append(unformatted); + formatted.find("table").addClass("table table-responsive table-bordered table-sm").removeAttr("border"); + return formatted[0]; + } + } catch (err) { + if (err.message && err.message.indexOf && err.message.indexOf("HTTP status 403") > -1) { + throw new ext_bottom_line.BottomLineWarning("You are not allowed to generate the table preview. Please log in."); + } else { + throw err; + } + } + }; + + const is_table = function (entity) { + const path = getEntityPath(entity); + return path && (path.toLowerCase().endsWith('.xls') + || path.toLowerCase().endsWith('.xlsx') + || path.toLowerCase().endsWith('.csv') + || path.toLowerCase().endsWith('.tsv')); + }; + + const init = function () { + // only enable when init is being called + ext_table_preview.is_table = is_table; + }; + + return { + init: init, + get_preview: get_preview, + is_table: () => false, + }; + +}($, log.getLogger("ext_table_preview"), connection, getEntityPath, getEntityID, markdown); + +// this will be replaced by require.js in the future. +$(document).ready(function () { + if ("${BUILD_MODULE_EXT_BOTTOM_LINE_TABLE_PREVIEW}" == "ENABLED") { + caosdb_modules.register(ext_table_preview); + } +}); diff --git a/src/core/js/ext_trigger_crawler_form.js b/src/core/js/ext_trigger_crawler_form.js index 94ea6feb672cc3a2be8c87cb6bd6f5c8a5087d3f..0796ef77da36e730b05d70dbbd2e8728c6e65c79 100644 --- a/src/core/js/ext_trigger_crawler_form.js +++ b/src/core/js/ext_trigger_crawler_form.js @@ -74,8 +74,8 @@ var ext_trigger_crawler_form = function () { <div class="modal-content"> <div class="modal-header"> <button type="button" - class="close" - data-dismiss="modal" + class="btn-close" + data-bs-dismiss="modal" aria-label="Close"> <span aria-hidden="true">×</span> </button> @@ -108,7 +108,7 @@ var ext_trigger_crawler_form = function () { <form method="POST" action="/scripting"> <input type="hidden" name="call" value="${script}"/> <input type="hidden" name="-p0" value=""/> - <div class="form-group"> + <div class="form-control"> <input type="submit" class="form-control btn btn-primary" value="${button_name}"/> </div> diff --git a/src/core/js/ext_xls_download.js b/src/core/js/ext_xls_download.js index 0f607a459b8e502e1d4c451b1636d99ceb01657c..5ea29ba38698b7c14ad4329201b2f3104dfe57ab 100644 --- a/src/core/js/ext_xls_download.js +++ b/src/core/js/ext_xls_download.js @@ -24,7 +24,7 @@ /** * @module caosdb_table_export - * @version 0.1 + * @version 0.2 * * Convert Entities for TSV and XLS export. * @@ -35,8 +35,6 @@ var caosdb_table_export = new function () { var logger = log.getLogger("caosdb_table_export"); - var TAB = "%09"; - var NEWLINE = "%0A"; /** * Hide "Download XLS File" link if the user is not authenticted (i.e. @@ -44,11 +42,7 @@ var caosdb_table_export = new function () { */ this.init = function() { logger.info("init caosdb_table_export"); - // TODO with AMD, use userIsAnonymous() - if (Array.from( - document.getElementsByClassName("caosdb-user-role")).map( - el => el.innerText - ).filter(el => el == "anonymous").length > 0) { + if (userIsAnonymous()) { $(".caosdb-v-query-select-data-xsl").parent().hide(); } } @@ -62,19 +56,64 @@ var caosdb_table_export = new function () { * @return {string} */ this.get_tsv_string = function(raw) { + const columns = this._get_column_header(); + const entities = this._get_table_content(); + return this._create_tsv_string(entities, columns, raw); + } + + /** + * In order to create a valid tsv table, characters that have a special + * meaning (line break and tab) need to be removed. + * + * Both characters will be replaced by a single white space + * + * @param {string} raw - the cell content. + * @return {string} cleaned up content + */ + this._clean_cell = function(raw) { + return raw.replace(/\t/g," ").replace(/\n/g," ").replace(/\r/g," ").replace(/\x1E/g," ").replace(/\x15/g," ") + } + + /** + * Finds the table in the DOM tree and returns the column names as array + * + * @return {string[]} + */ + this._get_column_header = function() { + const table = $('.caosdb-select-table'); + return table.find("th").toArray() + .map(e => caosdb_table_export._clean_cell(e.textContent)) + .filter(e => e.length > 0 && e.toLowerCase() != "id" && e.toLowerCase() != "version"); + } + + /** + * Finds the table in the DOM tree and returns the table content as array + * + * @return {HTMLElement[]} + */ + this._get_table_content = function() { const table = $('.caosdb-select-table'); - const columns = table.find("th").toArray() - .map(e => e.textContent) - .filter(e => e.length > 0); // TODO use entity-panel class in table as well (change in query.xsl // and then here) - const entities = table.find("tbody tr").toArray(); - const csv_string = this._get_tsv_string(entities, columns, raw); - return csv_string; + return table.find("tbody tr").toArray(); + } + + /** + * Encode tsv string + * + * @param {string} main - the tsv table string. + * @return {string} the same with prefix and URL encoded + */ + this._encode_tsv_string = function(main) { + const preamble = "data:text/csv;charset=utf-8,"; + main = encodeURIComponent(main); + return `${preamble}${main}`; } /** - * Convert all entities to a tsv string with the given columns. + * Convert all entities to an encoded tsv string with the given columns. + * + * TODO merge with caosdb_utils.create_tsv_table. * * @param {HTMLElement[]} entities - entities which are converted to rows * of the tsv string. @@ -83,36 +122,56 @@ var caosdb_table_export = new function () { * cells. Otherwise, the displayed data is used instead. * @return {string} */ - this._get_tsv_string = function (entities, columns, raw) { - logger.trace("enter get_tsv_string", entities, columns); - var preamble = "data:text/csv;charset=utf-8,"; - var header = "ID" + TAB + columns.join(TAB) + NEWLINE - var rows = caosdb_table_export._get_tsv_rows(entities, columns, raw).join(NEWLINE); - - const ret = `${preamble}${header}${rows}`; - logger.trace("leave get_tsv_string", ret); - return ret; + this._create_tsv_string = function (entities, columns, raw) { + logger.trace("enter _create_tsv_string ", entities, columns); + var header = "ID\tVersion\t" + columns.join("\t") + "\n" + var rows = []; + for (const table_row of entities) { + rows.push(caosdb_table_export._get_entity_row(table_row, columns, raw).join("\t")); + } + var rows = rows.join("\n"); + logger.trace("leave _create_tsv_string ", rows); + return `${header}${rows}`; } /** - * Return an array of rows with the given columns of the tsv table, one per - * entity. + * Return an array of cells, one per column, which contain a string + * representation of the value of the properties with the same name (as the + * column). * - * @param {HTMLElement[]} entities - entities which are converted to rows - * of the tsv string. + * @param {HTMLElement} entity - entity from which the cells are extracted. * @param {string[]} columns - array of property names. * @param {boolean} raw - if true, the raw entity ids are put into the * cells. Otherwise, the displayed data is used instead. * @return {string[]} */ - this._get_tsv_rows = function (entities, columns, raw) { - var rows = []; - for (const entity of entities) { - rows.push(this._get_entity_row(entity, columns, raw)); + this._get_entity_row = function (entity, columns, raw) { + var cells = [getEntityID(entity), getEntityVersion(entity)]; + var properties = getProperties(entity); + + for (const column of columns) { + var cell = ""; + for (const property of properties) { + if(property.name.toLowerCase() === column.toLowerCase()) { + var value = caosdb_table_export + ._get_property_value(property.html); + if (raw) { + cell = value.raw; + } else if (value.summary) { + cell = value.summary; + } else if (value.pretty) { + cell = value.pretty; + } else { + cell = value.raw; + } + } + } + cells.push(caosdb_table_export._clean_cell(cell)); } - return rows; - } + logger.trace("leave _get_entity_row", cells); + return cells; + } /** * Return different string representations of the property's value. @@ -187,73 +246,15 @@ var caosdb_table_export = new function () { } } - - /** - * Return an array of cells, one per column, which contain a string - * representation of the value of the properties with the same name (as the - * column). - * - * @param {HTMLElement} entity - entity from which the cells are extracted. - * @param {string[]} columns - array of property names. - * @param {boolean} raw - if true, the raw entity ids are put into the - * cells. Otherwise, the displayed data is used instead. - * @return {string[]} - */ - this._get_entity_row = function (entity, columns, raw) { - var cells = [getEntityID(entity)]; - var properties = getProperties(entity); - - for (const column of columns) { - var cell = ""; - for (const property of properties) { - if(property.name.toLowerCase() === column.toLowerCase()) { - var value = caosdb_table_export - ._get_property_value(property.html); - if (raw) { - cell = value.raw; - } else if (value.summary) { - cell = value.summary; - } else if (value.pretty) { - cell = value.pretty; - } else { - cell = value.raw; - } - } - } - cells.push(cell); - } - - logger.trace("leave _get_entity_row", cells); - return cells.join(TAB); - } - - /** * Open the resulting xls file by setting href to the location of the resulting * file in the server's `Shared` resource and imitate a click. */ - this._go_to_script_results = function (xls_link, filename) { - xls_link.setAttribute( - "href", - location.protocol + "//" +location.host + "/Shared/" + filename); - xls_link.click(); - } - - - this._get_csv_string = function (){ - const raw = $("input#caosdb-table-export-raw-flag-xls").is(":checked"); - const csv_string = caosdb_table_export.get_tsv_string(raw); - //const csv_string = document.getElementById("caosdb-f-query-select-data-tsv").getAttribute( - //"href"); - if (!csv_string) { - return undefined; - } - - return decodeURIComponent(csv_string.replace(/^data.*utf-8,/, "")); + this.go_to_script_results = function (filename) { + window.location.href = connection.getBasePath() + "Shared/" + filename; } } - /** * This function is called on click by the link button which says "Download TSV * File". @@ -264,12 +265,12 @@ var caosdb_table_export = new function () { */ function downloadTSV(tsv_link) { const raw = $("input#caosdb-table-export-raw-flag-tsv").is(":checked"); - const tsv_string = caosdb_table_export.get_tsv_string(raw); + var tsv_string = caosdb_table_export.get_tsv_string(raw); + tsv_string = caosdb_table_export._encode_tsv_string(tsv_string); $(tsv_link).attr("href", tsv_string); return true; } - /** * This function is called on click by the link button which says "Download XLS * File". @@ -279,15 +280,24 @@ function downloadTSV(tsv_link) { * resulting file. */ async function downloadXLS(xls_link) { - const csv_string = caosdb_table_export._get_csv_string(); - - // remove click event handler which called this function in the first place const onClickValue = xls_link.getAttribute("onClick"); - xls_link.removeAttribute("onClick"); - try { + // remove click event handler which called this function in the first place + xls_link.removeAttribute("onClick"); + + // add loading info. TODO make an animated one + $("#downloadModal").find(".caosdb-f-modal-footer-left").append( + createWaitingNotification("Exporting table...") + ); + + const raw = $("input#caosdb-table-export-raw-flag-xls").is(":checked"); + const tsv_string = caosdb_table_export.get_tsv_string(raw); + // TODO This existed previously. I do not know why. + if (!tsv_string) { + tsv_string = undefined; + } const xls_result = await connection.runScript("xls_from_csv.py", - {"-p0": {"filename": "selected.tsv", "blob": new Blob([csv_string], {type: "text/tab-separated-values;charset=utf-8"})}}); + {"-p0": {"filename": "selected.tsv", "blob": new Blob([tsv_string], {type: "text/tab-separated-values;charset=utf-8"})}}); const code = xls_result.getElementsByTagName("script")[0].getAttribute("code"); if (parseInt(code) > 0) { throw ("An error occurred during execution of the server-side script:\n" @@ -299,11 +309,14 @@ async function downloadXLS(xls_link) { + xls_result.getElementsByTagName("script")[0].outerHTML); } - // set the href in order to download the file and simulate a click. - caosdb_table_export._go_to_script_results(xls_link, filename); + // trigger download of generated file + caosdb_table_export.go_to_script_results(filename); + + } catch (e) { globalError(e); } finally { + removeAllWaitingNotifications($("#downloadModal")[0]); // restore the old click handler - hence a new file is generated with each click. xls_link.setAttribute("onClick", onClickValue); } @@ -314,4 +327,5 @@ async function downloadXLS(xls_link) { $(document).ready(function () { caosdb_modules.register(caosdb_table_export); + caosdb_modules.register(ext_file_download); }); diff --git a/src/core/js/footer.js b/src/core/js/footer.js deleted file mode 100644 index 79f3ed0ecb382534d7c997ce0bc230178792f677..0000000000000000000000000000000000000000 --- a/src/core/js/footer.js +++ /dev/null @@ -1,60 +0,0 @@ -/* - * ** header v3.0 - * This file is a part of the CaosDB Project. - * - * Copyright (C) 2019 IndiScale GmbH (info@indiscale.com) - * Copyright (C) 2019 Daniel Hornung (d.hornung@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/>. - * - * ** end header - */ -'use strict'; - -/** - * Call initially. - * - * TODO refactor to async function for better readability. - * @return - */ -function footer_initOnDocumentReady() { - - var xhr = new XMLHttpRequest() - - // TODO Refactor and use transformation.retrieveXsltScript, - // - // Event better use the transformation module for injecting an entry point, - // because this implementation does not allow for non-HTML content in the - // caosdb-footer template. - xhr.open("GET", "/webinterface/${BUILD_NUMBER}/xsl/footer.xsl"); - xhr.addEventListener('load', function() { - if (this.status != 200) { - // TODO use proper logging framework (log.getLogger("footer.js");) - console.log(this.status); - return; - } - var footer_xsl = this.responseXML; - - var foot_content = $('[name = "caosdb-footer"]', footer_xsl)[0]; - var fragment = document.createRange().createContextualFragment( - foot_content.innerHTML); - var footer = $("footer")[0]; - footer.appendChild(fragment); - - }); - xhr.send(); - -} - -$(document).ready(footer_initOnDocumentReady); diff --git a/src/core/js/form_elements.js b/src/core/js/form_elements.js index 0014f4db452a55839edaa860d7fa49f98529434e..94607ab01a906ded9999bbfef00340d5818838f4 100644 --- a/src/core/js/form_elements.js +++ b/src/core/js/form_elements.js @@ -9,8 +9,7 @@ * 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 + * 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. * @@ -26,8 +25,6 @@ * form_elements module for reusable form elemenst which already have a basic * css styling. * - * @version 0.2 - * * IMPORTANCE CONCEPTS * * FIELD - an HTMLElement which wraps a LABEL element (the fields name) and the @@ -49,31 +46,62 @@ * SUBFORM - an HTMLElement which contains FIELDS and other SUBFORMS. SUBFORMS * can be used to nest FIELDS, which is not supported by HTML5 but allows only * for flat key-value pairs. - */ - -/** - * The configuration for double, integer, date input elements. - * - * @typedef {object} input_config - * @property {string} name - * @property {string} type - * @property {string} label - */ - -/** - * The configuration for reference_select input fields - * - * TODO * + * @version 0.2 + * @exports form_elements */ var form_elements = new function () { + /** + * Config for an alert + * + * @typedef {object} AlertConfig + * @property {string} [title] - an optional title for the alert. + * @property {string} [severity="danger"] - a bootstrap class suffix. Other + * examples: warning, info + * @property {string} message - informs the user what they are about to do. + * @property {function} proceed_callback - the function which is called + * then the user hits the "Proceed" button. + * @property {function} [cancel_callback] - a callback which is called then + * the cancel button is clicked. By default, only the alert is being + * closed an nothing happens. + * @property {string} [proceed_text="Proceed"] - the text on the proceed button. + * @property {string} [cancel_text="Cancel"] - the text on the cancel button. + * @property {string} [remember_my_decision_id] - if this parameter is + * present, a checkbox is appended to the alert ("Don't ask me + * again."). If the checkbox is checked the next time the make_alert + * function is called with the same remember_my_decision_id is created, + * the alert won't show up and the proceed_callback is called without + * any user interaction. + * @property {string} [remember_my_decision_text="Don't ask me again."] - + * label text for the checkbox. + * @property {HTMLElement} [proceed_button] - an optional custom proceed + * button. + * @property {HTMLElement} [cancel_button] - an optional custom cancel + * button. + */ + + + /** + * The configuration for double, integer, date input elements. + * + * There are several specializations of this configuration object. + * {@link ReferenceDropDownConfig}, {@link RangeFieldConfig}, {@link SelectFieldConfig}, {@link FileFieldConfig} + * + * @typedef {object} FieldConfig + * + * @property {string} name + * @property {string} type + * @property {string} [label] + * @property {string} [help] + * @property {boolean} [required=false] + * @property {boolean} [cached=false] + */ this.version = "0.1"; - this.dependencies = ["log", "caosdb_utils", "markdown"]; + this.dependencies = ["log", "caosdb_utils", "markdown", "bootstrap"]; this.logger = log.getLogger("form_elements"); this.cancel_form_event = new Event("caosdb.form.cancel"); this.submit_form_event = new Event("caosdb.form.submit"); - this.form_ready_event = new Event("caosdb.form.ready"); this.field_changed_event = new Event("caosdb.field.changed"); this.field_enabled_event = new Event("caosdb.field.enabled"); this.field_disabled_event = new Event("caosdb.field.disabled"); @@ -164,37 +192,136 @@ var form_elements = new function () { } + this._get_alert_decision = function (key) { + return localStorage["form_elements.alert_decision." + key]; + } + + this._set_alert_decision = function (key, val) { + localStorage["form_elements.alert_decision." + key] = val; + } + /** - * (Re-)set this module's functions to standard implementation. + * Make an alert, that is a dialog which can intercept a function call and + * asks the user to proceed or cancel. + * + * @param {AlertConfig} config + * @return {HTMLElement} */ - this._init_functions = function () { + this.make_alert = function (config) { + caosdb_utils.assert_string(config.message, "config param `message`"); + caosdb_utils.assert_type(config.proceed_callback, "function", + "config param `proceed_callback`"); + + // define some defaults. + const title = config.title ? `<h4>${config.title}</h4>` : ""; + const proceed_text = config.proceed_text || "Proceed"; + const cancel_text = config.cancel_text || "Cancel"; + const severity = config.severity || "danger"; + const remember = !!config.remember_my_decision_id || false; + + // check if alert should be created at all + if (remember) { + var result = this._get_alert_decision(config.remember_my_decision_id); + if (result == "proceed") { + // call callback asyncronously and return + (async function () { + config.proceed_callback(); + })(); + return undefined; + } + } - this.init = function () { - this.logger.trace("enter init"); + // create the alert + const _alert = $(`<div class="alert alert-${severity} + alert-dismissible caosdb-f-form-elements-alert" role="alert">${title} + <p>${config.message}</p> + </div>`); + + // create the "Don't ask me again" checkbox + var checkbox = undefined; + if (remember) { + const remember_my_decision_text = config.remember_my_decision_text || + "Don't ask me again."; + checkbox = $(`<p class="form-check"><label> + <input type="checkbox"/> ${remember_my_decision_text}</label></p>`); + _alert.append(checkbox); } - /** - * Return an OPTION element with entity reference. - * - * The OPTION element for a SELECT form input shows a short - * summary/description of an entity and has the entity's id as value. - * - * If the `desc` parameter is undefined, the entity_id is shown - * instead. - * - * @param {string} entity_id - the entity's id. - * @param {string} [desc] - the description for the entity. - * @returns {HTMLElement} OPTION element. - */ - this.make_reference_option = function (entity_id, desc) { - caosdb_utils.assert_string(entity_id, "param `entity_id`"); - if (typeof desc == "undefined") { - desc = entity_id; + + // create buttons ... + const cancel_button = config.cancel_button || $(`<button type="button" class="btn btn-secondary caosdb-f-btn-alert-cancel">${cancel_text}</button>`); + const proceed_button = config.proceed_button || $(`<button type="button" class="btn btn-${severity} caosdb-f-btn-alert-proceed">${proceed_text}</button>`); + _alert.append($("<p/>").append([proceed_button, cancel_button])); + + + // ... and bind callbacks to the buttons. + cancel_button.click(() => { + var alert = bootstrap.Alert.getInstance(_alert[0]); + alert.close() + if (typeof config.cancel_callback == "function") { + config.cancel_callback(); + } + }); + proceed_button.click(() => { + if (remember && checkbox.find("input[type='checkbox']").is(":checked")) { + // store this decision. + form_elements._set_alert_decision(config.remember_my_decision_id, + "proceed"); } - var opt_str = '<option value="' + entity_id + '">' + desc + - "</option>"; - return $(opt_str)[0]; + var alert = bootstrap.Alert.getInstance(_alert[0]); + alert.close() + config.proceed_callback(); + }); + + new bootstrap.Alert(_alert[0]); + + return _alert[0]; + } + + + this.init = function () { + this.logger.trace("enter init"); + } + + /** + * Return an OPTION element with entity reference. + * + * The OPTION element for a SELECT form input shows a short + * summary/description of an entity and has the entity's id as value. + * + * If the `desc` parameter is undefined, the entity_id is shown + * instead. + * + * @param {string} entity_id - the entity's id. + * @param {string} [desc] - the description for the entity. + * @returns {HTMLElement} OPTION element. + */ + this.make_reference_option = function (entity_id, desc) { + caosdb_utils.assert_string(entity_id, "param `entity_id`"); + if (typeof desc == "undefined") { + desc = entity_id; } + return form_elements._make_option(entity_id, desc); + } + + /** + * Return an `option` element for a `select`. + * + * @param {string} value - the actual value of the option element. + * @param {string} label - the string which is shown for this option in the + * drop-down menu of the select input. + * @return {HTMLElement} + */ + this._make_option = function (value, label) { + const opt_str = '<option value="' + value + '">' + label + + "</option>"; + return $(opt_str)[0]; + } + + /** + * (Re-)set this module's functions to standard implementation. + */ + this._init_functions = function () { /** * Return SELECT form element with entity references. @@ -212,10 +339,11 @@ var form_elements = new function () { * parameter which is an entity in HTML representation. * @param {boolean} [multiple] - whether the select allows multiple * options to be selected. + * @param {string} name - the name of the select element * @returns {HTMLElement} SELECT element with entity options. */ this.make_reference_select = async function (entities, make_desc, - make_value, multiple) { + make_value, name, multiple) { caosdb_utils.assert_array(entities, "param `entities`", false); if (typeof make_desc !== "undefined") { caosdb_utils.assert_type(make_desc, "function", @@ -225,12 +353,7 @@ var form_elements = new function () { caosdb_utils.assert_type(make_value, "function", "param `make_value`"); } - const ret = $('<select class="selectpicker form-control" title="Nothing selected"/>'); - if (multiple) { - ret.attr("multiple", ""); - } else { - ret.append('<option style="display: none" selected="selected" value="" disabled="disabled"></option>'); - } + const ret = $(form_elements._make_select(multiple, name)); for (let entity of entities) { this.logger.trace("add option", entity); let entity_id = getEntityID(entity); @@ -244,8 +367,30 @@ var form_elements = new function () { } /** - * @typedef {option} ReferenceDropDownConfig + * Return a new select element. + * + * This function is mainly used by other factory functions, e.g. {@link + * make_reference_select} and {@link make_select_input}. * + * @param {boolean} multiple - the `multiple` attribute of the select element. + * @param {string} name - the name of the select element. + * @return {HTMLElement} + */ + this._make_select = function (multiple, name) { + const ret = $(`<select class="selectpicker form-control" name="${name}" title="Nothing selected"/>`); + if (typeof name !== "undefined") { + caosdb_utils.assert_string(name, "param `name`"); + ret.attr("name", name); + } + if (multiple) { + ret.attr("multiple", ""); + } else { + ret.append('<option style="display: none" selected="selected" value="" disabled="disabled"></option>'); + } + return ret[0]; + } + + /** * Configuration object for a drop down menu for selecting references. * `make_reference_drop_down` generates such a drop down menu using a * SELECT input with the references as its OPTION elements. @@ -263,6 +408,9 @@ var form_elements = new function () { * defined by `label`. If the `label` property is undefined, the `name` * is shown instead. * + * @typedef {option} ReferenceDropDownConfig + * + * @augments FieldConfig * @property {string} name - The name of the select input. * @property {string} query - Query for entities. * @property {function} [make_value] - Call-back for the generation of @@ -276,129 +424,7 @@ var form_elements = new function () { * @property {string} [type] - This should be "reference_drop_down" or * undefined. This property is used by `make_form_field` to decide * which type of field is to be generated. - * - */ - - /** - * Search and retrieve entities and create a SELECT from element. - * - * @param {ReferenceDropDownConfig} config - all necessary parameters - * for the configuration. - * @returns {HTMLElement} SELECT element. - */ - this.make_reference_drop_down = function (config) { - let ret = $(this._make_field_wrapper(config.name)); - let label = this._make_input_label_str(config); - let loading = $('<div class="caosdb-f-field-not-ready">loading...</div>'); - let input_col = $('<div class="col-sm-9"/>'); - - input_col.append(loading); - this._query(config.query).then(async function (entities) { - let select = $(await form_elements.make_reference_select( - entities, config.make_desc, config.make_value, config.multiple, - config.value)); - select.attr("name", config.name); - loading.remove(); - input_col.append(select); - form_elements.init_select_picker(ret[0], config.value); - ret[0].dispatchEvent(form_elements.field_ready_event); - select.change(function () { - ret[0].dispatchEvent(form_elements.field_changed_event); - }); - }).catch(err => { - form_elements.logger.error(err); - loading.remove(); - input_col.append(err); - ret[0].dispatchEvent(form_elements.field_error_event); - }); - - return ret.append(label, input_col)[0]; - } - - - this.init_select_picker = function (field, value) { - caosdb_utils.assert_html_element(field, "parameter `field`"); - const select = $(field).find("select")[0]; - const select_picker_options = {}; - if ($(select).prop("multiple")) { - select_picker_options["actionsBox"] = true; - } - if ($(select).find("option").length > 8) { - select_picker_options["liveSearch"] = true; - select_picker_options["liveSearchNormalize"] = true; - select_picker_options["liveSearchPlaceholder"] = "search..."; - } - $(select).selectpicker(select_picker_options); - $(select).selectpicker("val", value); - this.init_actions_box(field); - } - - - this.init_actions_box = function (field) { - this.logger.trace("enter init_actions_box", field); - caosdb_utils.assert_html_element(field, "parameter `field`"); - const select = $(field).find("select"); - var actions_box = select.siblings().find(".bs-actionsbox"); - if (actions_box.length === 0) { - actions_box = $(`<div class="bs-actionsbox"> - <div class="btn-group btn-group-sm btn-block"> - <button type="button" class="actions-btn btn-default bs-deselect-all btn btn-light">None</button> - </div> - </div>`) - .hide(); - - select - .siblings(".dropdown-menu") - .prepend(actions_box); - - field.addEventListener( - form_elements.field_changed_event.type, - (e) => { - if (form_elements.is_set(field)) { - actions_box.show(); - } else { - actions_box.hide(); - } - }, true); - - actions_box - .find(".bs-deselect-all") - .click((e) => { - select.val(null) - .selectpicker("render") - .parent().toggleClass("open", false); - select[0].dispatchEvent(form_elements.field_changed_event); - }); - } - } - - /** - * Return a promise which resolves with the field when the field is ready. - * - * This function is especially useful if the caller can not be sure if - * the field_ready_event has been dispatched already and the field is - * ready or if the fields creation is still pending. - * - * @param {HTMLElement} field - * @return {Promise} the field-ready promise */ - this.field_ready = function (field) { - // TODO add support for field name (string) as field parameter - // TODO check type of param field (not an array!) - caosdb_utils.assert_html_element(field, "parameter `field`"); - return new Promise(function (resolve, reject) { - try { - if (!$(field).hasClass("caosdb-f-field-not-ready") && $(field).find(".caosdb-f-field-not-ready").length === 0) { - resolve(field); - } else { - field.addEventListener(form_elements.field_ready_event.type, - (e) => resolve(e.target), true); - } - } catch (err) { - reject(err); - } - }); - } this._query = async function (q) { const result = await query(q); @@ -406,8 +432,23 @@ var form_elements = new function () { return result; } + /** + * Call a server-side script with the content of the given form and + * return the results. + * + * Note that the form should be one generated by this form_elements + * module. Otherwise it cannot be guaranteed that the form will be + * serialized (to json) correctly. + * + * @param {string} script - the path of the script + * @param {HTMLElements} form - a form generated by this module. + * @return {ScriptingResult} the results of the call. + */ this._run_script = async function (script, form) { - const json_str = JSON.stringify(form_elements.form_to_object(form[0])); + const form_objects = form_elements.form_to_object(form[0]); + const json_str = JSON.stringify(form_objects[0]); + + // append non-file form fields to the request const params = { "-p0": { "filename": "form.json", @@ -416,807 +457,1146 @@ var form_elements = new function () { }) } }; + + // append files to the request + const files = form_objects[1]; + for (let i = 0; i < files.length; i++) { + params[`file_${i}`] = { + "filename": `${files[i]["fieldname"]}_${files[i]["filename"]}`, + "blob": files[i]["blob"] + }; + } + const result = await connection.runScript(script, params); this.logger.debug("server-side script returned", result); return this.parse_script_result(result); } - this.parse_script_result = function (result) { - console.log(result); - const scriptNode = result.evaluate("/Response/script", result, null, XPathResult.FIRST_ORDERED_NODE_TYPE, null).singleNodeValue; - const code = result.evaluate("@code", scriptNode, null, XPathResult.STRING_TYPE, null).stringValue; + } - const call = result.evaluate("call", scriptNode, null, XPathResult.STRING_TYPE, null).stringValue; - const stderr = result.evaluate("stderr", scriptNode, null, XPathResult.STRING_TYPE, null).stringValue; - const stdout = result.evaluate("stdout", scriptNode, null, XPathResult.STRING_TYPE, null).stringValue; + /** + * @typedef {object} ScriptingResult + * @property {string} code + * @property {string} call + * @property {string} stdout + * @property {string} stderr + */ - return { - "code": code, - "call": call, - "stdout": stdout, - "stderr": stderr - }; + /** + * Convert the reponse of a server-side scripting call into a {@link + * ScriptingResult} object. + * + * @param {XMLDocument} result + * @return {ScriptingResult} + */ + this.parse_script_result = function (result) { + const scriptNode = result.evaluate("/Response/script", result, null, XPathResult.FIRST_ORDERED_NODE_TYPE, null).singleNodeValue; + const code = result.evaluate("@code", scriptNode, null, XPathResult.STRING_TYPE, null).stringValue; + + const call = result.evaluate("call", scriptNode, null, XPathResult.STRING_TYPE, null).stringValue; + const stderr = result.evaluate("stderr", scriptNode, null, XPathResult.STRING_TYPE, null).stringValue; + const stdout = result.evaluate("stdout", scriptNode, null, XPathResult.STRING_TYPE, null).stringValue; + + const ret = { + "code": code, + "call": call, + "stdout": stdout, + "stderr": stderr + }; + + return ret; + } + + /** + * Search and retrieve entities and create a SELECT from element. + * + * @param {ReferenceDropDownConfig} config - all necessary parameters + * for the configuration. + * @returns {HTMLElement} SELECT element. + */ + this.make_reference_drop_down = function (config) { + let ret = $(this._make_field_wrapper(config.name)); + let label = this._make_input_label_str(config); + let loading = $(createWaitingNotification("loading...")) + .addClass("caosdb-f-field-not-ready"); + let input_col = $('<div class="col-sm-9"/>'); + + input_col.append(loading); + this._query(config.query).then(async function (entities) { + let select = $(await form_elements.make_reference_select( + entities, config.make_desc, config.make_value, config.name, config.multiple, + config.value)); + loading.remove(); + input_col.append(select); + form_elements.init_select_picker(ret[0], config.value); + ret[0].dispatchEvent(form_elements.field_ready_event); + select.change(function () { + ret[0].dispatchEvent(form_elements.field_changed_event); + }); + }).catch(err => { + form_elements.logger.error(err); + loading.remove(); + input_col.append(err); + ret[0].dispatchEvent(form_elements.field_error_event); + }); + + return ret.append(label, input_col)[0]; + } + + + /** + * Test 16 + */ + this.init_select_picker = function (field, value) { + caosdb_utils.assert_html_element(field, "parameter `field`"); + const select = $(field).find("select")[0]; + const select_picker_options = {}; + if ($(select).prop("multiple")) { + select_picker_options["actionsBox"] = true; + } + if ($(select).find("option").length > 8) { + select_picker_options["liveSearch"] = true; + select_picker_options["liveSearchNormalize"] = true; + select_picker_options["liveSearchPlaceholder"] = "search..."; } + console.log(select) + $(select).selectpicker(select_picker_options); + $(select).selectpicker("val", value); + this.init_actions_box(field); + } - /** - * generate a java script object representation of a form - */ - this.form_to_object = function (form) { - this.logger.trace("entity form_to_json", form); - caosdb_utils.assert_html_element(form, "parameter `form`"); - const _to_json = (element, data) => { - this.logger.trace("enter element_to_json", element, data); + /** + * Test 17 + */ + this.init_actions_box = function (field) { + this.logger.trace("enter init_actions_box", field); + caosdb_utils.assert_html_element(field, "parameter `field`"); + const select = $(field).find("select"); + var actions_box = select.siblings().find(".bs-actionsbox"); + if (actions_box.length === 0) { + actions_box = $(`<div class="bs-actionsbox"> + <div class="btn-group btn-group-sm d-grid"> + <button type="button" class="actions-btn btn-secondary bs-deselect-all btn btn-light">None</button> + </div> + </div>`) + .hide(); + + select + .siblings(".dropdown-menu") + .prepend(actions_box); + + field.addEventListener( + form_elements.field_changed_event.type, + (e) => { + if (form_elements.is_set(field)) { + actions_box.show(); + } else { + actions_box.hide(); + } + }, true); + + actions_box + .find(".bs-deselect-all") + .click((e) => { + select.val(null) + .selectpicker("render") + .parent().toggleClass("open", false); + select[0].dispatchEvent(form_elements.field_changed_event); + }); + } + } + + /** + * Return a promise which resolves with the field when the field is ready. + * + * This function is especially useful if the caller can not be sure if + * the field_ready_event has been dispatched already and the field is + * ready or if the fields creation is still pending. + * + * @param {HTMLElement} field + * @return {Promise} the field-ready promise + */ + this.field_ready = function (field) { + // TODO add support for field name (string) as field parameter + // TODO check type of param field (not an array!) + caosdb_utils.assert_html_element(field, "parameter `field`"); + return new Promise(function (resolve, reject) { + try { + if (!$(field).hasClass("caosdb-f-field-not-ready") && $(field).find(".caosdb-f-field-not-ready").length === 0) { + resolve(field); + } else { + field.addEventListener(form_elements.field_ready_event.type, + (e) => resolve(e.target), true); + } + } catch (err) { + reject(err); + } + }); + } + + /** + * Generate a java script object representation of a form and extract the + * files from the form. + * + * The property names (aka keys) are the names of the form fields and + * subforms. The values are single strings or arrays of strings. If the + * field was had a file-type input, the value is a string identifying the + * file blob which belongs to this key. + * + * Subforms lead to nested objects of the same structure. + * + * @param {HTMLElement} form - a form generated by this module. + * @return {object[]} - an array of length 2. The first element is an + * object representing the fields of the form. The second contains a + * list of file blobs resulting from file inputs in the form. + */ + this.form_to_object = function (form) { + this.logger.trace("entity form_to_json", form); + caosdb_utils.assert_html_element(form, "parameter `form`"); - for (const child of element.children) { - // ignore disabled fields and subforms - if ($(child).hasClass("caosdb-f-field-disabled")) { - continue; + const _to_json = (element, data, files) => { + this.logger.trace("enter element_to_json", element, data, files); + + for (const child of element.children) { + // ignore disabled fields and subforms + if ($(child).hasClass("caosdb-f-field-disabled")) { + continue; + } + const name = $(child).attr("name"); + const is_subform = $(child).hasClass("caosdb-f-form-elements-subform"); + if (is_subform) { + const subform = $(child).data("subform-name"); + // recursive + var subform_obj = _to_json(child, {}, files)[0]; + if (typeof data[subform] === "undefined") { + data[subform] = subform_obj; + } else if (Array.isArray(data[subform])) { + data[subform].push(subform_obj); + } else { + data[subform] = [data[subform], subform_obj] } - const name = $(child).attr("name"); - const is_subform = $(child).hasClass("caosdb-f-form-elements-subform"); - if (is_subform) { - const subform = $(child).data("subform-name"); - // recursive - var subform_obj = _to_json(child, {}); - if (typeof data[subform] === "undefined") { - data[subform] = subform_obj; - } else if (Array.isArray(data[subform])) { - data[subform].push(subform_obj); - } else { - data[subform] = [data[subform], subform_obj] - } - } else if (name && name !== "") { - // input elements - const not_checkbox = !$(child).is(":checkbox"); - if (not_checkbox || $(child).is(":checked")) { - // checked or not a checkbox - var value = $(child).val(); - if (typeof data[name] === "undefined") { - data[name] = value; - } else if (Array.isArray(data[name])) { - data[name].push(value); - } else { - data[name] = [data[name], value] + } else if (name && name !== "") { + // input elements + const not_checkbox = !$(child).is(":checkbox"); + const is_file = $(child).is("input:file"); + if (is_file) { + var fileList = child.files; + if (fileList.length > 0) { + for (let i = 0; i < fileList.length; i++) { + // generate an identifyer for the file(s) of this input + value = name + "_" + fileList[i].name; + if (typeof data[name] === "undefined") { + // first and possibly only value + data[name] = value + } else if (Array.isArray(data[name])) { + data[name].push(value); + } else { + // there is a value present yet - convert to array. + data[name] = [data[name], value] + } + files.push({ + "fieldname": name, + "filename": fileList[i].name, + "blob": fileList[i] + }); } + } + } else if (not_checkbox || $(child).is(":checked")) { + // checked or not a checkbox + var value = $(child).val(); + if (typeof data[name] === "undefined") { + data[name] = value; + } else if (Array.isArray(data[name])) { + data[name].push(value); } else { - // TODO checkbox + data[name] = [data[name], value] } - } else if (child.children.length > 0) { - // recursive - _to_json(child, data); + } else { + // TODO checkbox } + } else if (child.children.length > 0) { + // recursive + _to_json(child, data, files); } + } - this.logger.trace("leave element_to_json", element, data); - return data; - }; + this.logger.trace("leave element_to_json", element, data, files); + return [data, files]; + }; - const ret = _to_json(form, {}); - this.logger.trace("leave form_to_json", ret); - return ret; - } - - this.make_submit_button = function () { - var ret = $('<button class="caosdb-f-form-elements-submit-button btn btn-primary" type="submit">Submit</button>'); - return ret[0]; - } + const ret = _to_json(form, {}, []); + this.logger.trace("leave form_to_json", ret); + return ret; + } - this.make_cancel_button = function (form) { - var ret = $('<button class="caosdb-f-form-elements-cancel-button btn btn-primary" type="button">Cancel</button>'); - ret.on("click", e => { - this.logger.debug("cancel form", e, form); - form.dispatchEvent(this.cancel_form_event); - }); - return ret[0]; - } + this.make_submit_button = function () { + var ret = $('<button class="caosdb-f-form-elements-submit-button btn btn-primary" type="submit">Submit</button>'); + return ret[0]; + } - /** - * TODO make syncronous - */ - this.make_form_field = async function (config) { - caosdb_utils.assert_type(config, "object", "param `config`"); - caosdb_utils.assert_string(config.type, "`config.type` of param `config`"); - - var field = undefined; - const type = config.type; - if (type === "date") { - field = this.make_date_input(config); - } else if (type === "checkbox") { - field = this.make_checkbox_input(config); - } else if (type === "text") { - field = this.make_text_input(config); - } else if (type === "double") { - field = this.make_double_input(config); - } else if (type === "integer") { - field = this.make_integer_input(config); - } else if (type === "range") { - field = await this.make_range_input(config); - } else if (type === "reference_drop_down") { - field = this.make_reference_drop_down(config); - } else if (type === "subform") { - // TODO handle cache and required for subforms - return await this.make_subform(config); - } else { - throw new TypeError("undefined field type `" + type + "`"); - } + this.make_cancel_button = function (form) { + var ret = $('<button class="caosdb-f-form-elements-cancel-button btn btn-primary" type="button">Cancel</button>'); + ret.on("click", e => { + this.logger.debug("cancel form", e, form); + form.dispatchEvent(this.cancel_form_event); + }); + return ret[0]; + } - if (config.required) { - this.set_required(field); - } - if (config.cached) { - this.set_cached(field); - } - if (config.help) { - this.add_help(field, config.help); - } + /** + * Return a new form field (or a subform). + * + * This function is intended to be called by make_form and recursively by + * other make_* functions which create subforms or other deeper structured + * form fields. + * + * This function also configures the caching, whether a form field is + * 'required' or not, and the help for each field. + * + * @param {FieldConfig} config - the configuration of the form field + * @return {HTMLElement} + */ + this.make_form_field = function (config) { + caosdb_utils.assert_type(config, "object", "param `config`"); + caosdb_utils.assert_string(config.type, "`config.type` of param `config`"); + + var field = undefined; + const type = config.type; + if (type === "date") { + field = this.make_date_input(config); + } else if (type === "file") { + field = this.make_file_input(config); + } else if (type === "checkbox") { + field = this.make_checkbox_input(config); + } else if (type === "text") { + field = this.make_text_input(config); + } else if (type === "double") { + field = this.make_double_input(config); + } else if (type === "integer") { + field = this.make_integer_input(config); + } else if (type === "range") { + field = this.make_range_input(config); + } else if (type === "reference_drop_down") { + field = this.make_reference_drop_down(config); + } else if (type === "select") { + field = this.make_select_input(config); + } else if (type === "subform") { + return this.make_subform(config); + } else { + throw new TypeError("undefined field type `" + type + "`"); + } - return field; + if (config.required) { + this.set_required(field); + } + if (config.cached) { + this.set_cached(field); + } + if (config.help) { + this.add_help(field, config.help); } + return field; + } - this.add_help = function (field, config) { - var help_button = $('<span data-trigger="click focus" data-toggle="popover" class="caosdb-f-form-help pull-right glyphicon glyphicon-info-sign"/>') - .css({ - "cursor": "pointer" - }); - if (typeof config === "string" || config instanceof String) { - help_button.attr("data-content", config); - help_button.popover(); - } else { - help_button.popover(config); - } + this.add_help = function (field, config) { + var help_button = $('<a tabindex="0" role="button" data-bs-trigger="focus" data-bs-toggle="popover"><i class="caosdb-f-form-help pull-right bi-info-circle-fill"></i></a>') + .css({ + "cursor": "pointer" + }); + if (typeof config === "string" || config instanceof String) { + help_button.attr("data-bs-content", config); + help_button.popover(); + } else { + help_button.popover(config); + } - var label = $(field).children("label"); - if (label.length > 0) { - help_button.css({ - "margin-left": "4px" - }); - label.first().append(help_button); - } else { - $(field).append(help_button); - } + + var label = $(field).children("label"); + if (label.length > 0) { + help_button.css({ + "margin-left": "4px" + }); + label.first().append(help_button); + } else { + $(field).append(help_button); } + } - this.make_heading = function (config) { - if (typeof config.header === "undefined") { - return; - } else if (typeof config.header === "string" || config.header instanceof String) { - return $('<div class="caosdb-f-form-header h3">' + config.header + '</div>')[0]; - } - caosdb_utils.assert_html_element(config.header, "member `header` of parameter `config`"); - return config.header; + this.make_heading = function (config) { + if (typeof config.header === "undefined") { + return; + } else if (typeof config.header === "string" || config.header instanceof String) { + return $('<div class="caosdb-f-form-header h3">' + config.header + '</div>')[0]; } + caosdb_utils.assert_html_element(config.header, "member `header` of parameter `config`"); + return config.header; + } - this.make_form_wrapper = function (form, config) { - var wrapper = $('<div class="caosdb-f-form-wrapper"/>'); + this.make_form_wrapper = function (form, config) { + var wrapper = $('<div class="caosdb-f-form-wrapper"/>'); - var header = this.make_heading(config); - wrapper.append(header); - var loading = $('<div>loading...</div>'); - var logger = this.logger; - var cancel = (e) => { - logger.trace("cancel form", e); - wrapper.remove(); - }; + var cancel = (e) => { + form_elements.logger.trace("cancel form", e); + wrapper.remove(); + }; - wrapper.append(loading); + wrapper[0].addEventListener(this.cancel_form_event.type, cancel, true); - Promise.resolve(form).then(form => { - // form ready - loading.remove(); - wrapper.append(form); - wrapper[0].dispatchEvent(this.form_ready_event); - }).catch(err => { - logger.error("form loading error", err); - loading.remove(); - wrapper.append(err); - }); + var header = this.make_heading(config); + wrapper.append(header); + wrapper.append(form); - wrapper[0].addEventListener(this.cancel_form_event.type, cancel, true); + return wrapper[0]; + } - return wrapper[0]; - } + /** + * Configuration objects which are passed to {@link make_form}. + * + * Note: either the `script` or the `name` property must be defined. If the former is defined, the latter will be overriden. + * + * @typedef {object} FormConfig + * + * @property {FieldConfig[]} fields - array of fields. The order is the + * order in which they appear in the resulting form. + * @property {string} [script] - if present the form will call a + * server-side script on submission. + * @property {string} [name] - The name of the form. This is being + * overridden by the `script` parameter if present. + * @property {function} [submit] - a callback which handles the submission + * of the form. This parameter is being overridden if the `script` + * parameter is present. + */ - this.make_form = function (config) { - var form = undefined; + /** + * Create a form. + * + * The returned element is a container which contains a HTML form element. + * The fields are ready or they will emit a {@link field_ready_event} when + * they are. + * + * @param {FormConfig} config + * @return {HTMLElement} + */ + this.make_form = function (config) { + var form = undefined; - if (config.script) { - form = this.make_script_form(config, config.script); - } else { - form = this.make_generic_form(config); - } - var wrapper = this.make_form_wrapper(form, config); - return wrapper; + if (config.script) { + form = this.make_script_form(config, config.script); + } else { + form = this.make_generic_form(config); } + var wrapper = this.make_form_wrapper(form, config); + return wrapper; + } - /** - * TODO make syncronous - */ - this.make_subform = async function (config) { - this.logger.trace("enter make_subform"); - caosdb_utils.assert_type(config, "object", "param `config`"); - caosdb_utils.assert_string(config.name, "`config.name` of param `config`"); - caosdb_utils.assert_array(config.fields, "`config.fields` of param `config`"); - - const name = config.name; - var form = $('<div data-subform-name="' + name + '" class="caosdb-f-form-elements-subform"/>'); - - for (let field of config.fields) { - this.logger.trace("add subform field", field); - let elem = await this.make_form_field(field); - form.append(elem); - } + /** + * @typedef {object} SubFormConfig + * + * @augments FieldConfig + * @property {FieldConfig[]} fields - array of fields. The order is the + * order in which they appear in the resulting subform. + */ - this.logger.trace("leave make_subform", form[0]); - return form[0]; + /** + * Return a new subform. + * + * @param {SubFormConfig} config - the configuration of the subform. + * @return {HTMLElement} + */ + this.make_subform = function (config) { + this.logger.trace("enter make_subform"); + caosdb_utils.assert_type(config, "object", "param `config`"); + caosdb_utils.assert_string(config.name, "`config.name` of param `config`"); + caosdb_utils.assert_array(config.fields, "`config.fields` of param `config`"); + + const name = config.name; + var form = $('<div data-subform-name="' + name + '" class="caosdb-f-form-elements-subform"/>'); + + for (let field of config.fields) { + this.logger.trace("add subform field", field); + let elem = this.make_form_field(field); + form.append(elem); } - this.dismiss_form = function (form) { - if (form.tagName === "FORM") { - form.dispatchEvent(this.cancel_form_event); - } - var _form = $(form).find("form"); - if (_form.length > 0) { - _form[0].dispatchEvent(this.cancel_form_event); - } - } + this.logger.trace("leave make_subform", form[0]); + return form[0]; + } - this.enable_group = function (form, group) { - this.enable_fields(this.get_group_fields(form, group)); + this.dismiss_form = function (form) { + if (form.tagName === "FORM") { + form.dispatchEvent(this.cancel_form_event); } - - this.disable_group = function (form, group) { - this.disable_fields(this.get_group_fields(form, group)); + var _form = $(form).find("form"); + if (_form.length > 0) { + _form[0].dispatchEvent(this.cancel_form_event); } + } - this.get_group_fields = function (form, group) { - return $(form).find(".caosdb-f-field[data-groups*='(" + group + ")']").toArray(); - } + this.enable_group = function (form, group) { + this.enable_fields(this.get_group_fields(form, group)); + } - /** - * Return an array of field with name - * - * @param {string} name - the field name - * @return {HTMLElement[]} array of fields - */ - this.get_fields = function (form, name) { - caosdb_utils.assert_html_element(form, "parameter `form`"); - caosdb_utils.assert_string(name, "parameter `name`"); - return $(form).find(".caosdb-f-field[data-field-name='" + name + "']").toArray(); - } + this.disable_group = function (form, group) { + this.disable_fields(this.get_group_fields(form, group)); + } - this.add_field_to_group = function (field, group) { - this.logger.trace("enter add_field_to_group", field, group); - var groups = ($(field).attr("data-groups") ? $(field).attr("data-groups") : "") + "(" + group + ")"; - $(field).attr("data-groups", groups); - } + this.get_group_fields = function (form, group) { + return $(form).find(".caosdb-f-field[data-groups*='(" + group + ")']").toArray(); + } - this.disable_fields = function (fields) { - $(fields).toggleClass("caosdb-f-field-disabled", true).hide(); - for (const field of $(fields)) { - field.dispatchEvent(this.field_disabled_event); - } - } + /** + * Return an array of field with name + * + * @param {string} name - the field name + * @return {HTMLElement[]} array of fields + */ + this.get_fields = function (form, name) { + caosdb_utils.assert_html_element(form, "parameter `form`"); + caosdb_utils.assert_string(name, "parameter `name`"); + return $(form).find(".caosdb-f-field[data-field-name='" + name + "']").toArray(); + } - this.enable_fields = function (fields) { - $(fields).toggleClass("caosdb-f-field-disabled", false).show(); - for (const field of $(fields)) { - field.dispatchEvent(this.field_enabled_event); - } - } + this.add_field_to_group = function (field, group) { + this.logger.trace("enter add_field_to_group", field, group); + var groups = ($(field).attr("data-groups") ? $(field).attr("data-groups") : "") + "(" + group + ")"; + $(field).attr("data-groups", groups); + } - this.enable_name = function (form, name) { - this.enable_fields($(form).find(".caosdb-f-field[data-field-name='" + name + "']").toArray()); + this.disable_fields = function (fields) { + $(fields).toggleClass("caosdb-f-field-disabled", true).hide(); + $(fields).find(":input").prop("required", false); + for (const field of $(fields)) { + field.dispatchEvent(this.field_disabled_event); } + } - this.disable_name = function (form, name) { - this.disable_fields($(form).find(".caosdb-f-field[data-field-name='" + name + "']").toArray()); + this.enable_fields = function (fields) { + $(fields).toggleClass("caosdb-f-field-disabled", false).show(); + $(fields).filter(".caosdb-f-form-field-required").find("input.caosdb-f-property-single-raw-value, select.selectpicker").prop("required", true); + for (const field of $(fields)) { + field.dispatchEvent(this.field_enabled_event); } + } + + this.enable_name = function (form, name) { + this.enable_fields($(form).find(".caosdb-f-field[data-field-name='" + name + "']").toArray()); + } - this.make_script_form = async function (config, script) { - this.logger.trace("enter make_script_form"); + this.disable_name = function (form, name) { + this.disable_fields($(form).find(".caosdb-f-field[data-field-name='" + name + "']").toArray()); + } - const submit_callback = async function (form) { - form = $(form); + this.make_script_form = function (config, script) { + this.logger.trace("enter make_script_form"); + const submit_callback = async function (form) { + form = $(form); - // actually submit the form - var response = await form_elements._run_script(script, form); - var result = []; - if (response.code === "0") { - // handle success - result.push(form_elements.make_success_message(response.stdout)); - return result; + // actually submit the form + var response = await form_elements._run_script(script, form); + var result = []; - } else { - // handle scripting error - result.push(form_elements.make_error_message(response.call)); - result.push(form_elements.make_error_message(response.stderr)); - throw result; - } - }; + if (response.code === "0") { + // handle success + result.push(form_elements.make_success_message(response.stdout)); + return result; - this.logger.trace("leave make_script_form"); - const new_config = $.extend({}, { - name: script, - submit: submit_callback - }, config); - return await this.make_generic_form(new_config); - } + } else { + // handle scripting error + result.push(form_elements.make_error_message(response.call)); + result.push(form_elements.make_error_message(response.stderr)); + throw result; + } + }; + + this.logger.trace("leave make_script_form"); + const new_config = $.extend({}, { + name: script, + submit: submit_callback + }, config); + return this.make_generic_form(new_config); + } - /** - * Return a generic form, bind the config.submit to the submit event - * of the form. - * - * The `config.fields` array may contain `form_elements.field_config` - * objects or HTMLElements. - * - * TODO - */ - this.make_generic_form = async function (config) { - this.logger.trace("enter make_generic_form"); + /** + * Return a generic form, bind the config.submit to the submit event + * of the form. + * + * The `config.fields` array may contain `form_elements.field_config` + * objects or HTMLElements. + * + * @return {HTMLElement} + */ + this.make_generic_form = function (config) { + this.logger.trace("enter make_generic_form"); - caosdb_utils.assert_type(config, "object", "param `config`"); - caosdb_utils.assert_string(config.name, "`config.name` of param `config`", true); - caosdb_utils.assert_array(config.fields, "`config.fields` of param `config`"); + caosdb_utils.assert_type(config, "object", "param `config`"); + caosdb_utils.assert_string(config.name, "`config.name` of param `config`", true); + caosdb_utils.assert_array(config.fields, "`config.fields` of param `config`"); - const form = $('<form class="form-horizontal" action="#" method="post" />'); + const form = $('<form class="form-horizontal" action="#" method="post" />'); - // set name - if (config.name) { - form.attr("name", config.name); - } + // set name + if (config.name) { + form.attr("name", config.name); + } - // add fields - for (let field of config.fields) { - this.logger.trace("add field", field); - if (field instanceof HTMLElement) { - form.append(field); - } else { - let elem = await this.make_form_field(field); - form.append(elem); - } + // add fields + for (let field of config.fields) { + this.logger.trace("add field", field); + if (field instanceof HTMLElement) { + form.append(field); + } else { + let elem = this.make_form_field(field); + form.append(elem); } + } - // set groups - if (config.groups) { - for (let group of config.groups) { - this.logger.trace("add group", group); - for (let fieldname of group.fields) { - let field = form.find(".caosdb-f-field[data-field-name='" + fieldname + "']"); - this.logger.trace("set group", field, group); - this.add_field_to_group(field, group.name) + // set groups + if (config.groups) { + for (let group of config.groups) { + this.logger.trace("add group", group); + for (let fieldname of group.fields) { + let field = form.find(".caosdb-f-field[data-field-name='" + fieldname + "']"); + this.logger.trace("set group", field, group); + this.add_field_to_group(field, group.name) - } - // disable if necessary - if (typeof group.enabled === "undefined" || group.enabled) { - this.enable_group(form, group.name); - } else { - this.disable_group(form, group.name); - } + } + // disable if necessary + if (typeof group.enabled === "undefined" || group.enabled) { + this.enable_group(form, group.name); + } else { + this.disable_group(form, group.name); } } + } - const footer = this.make_footer(); - form.append(footer); + const footer = this.make_footer(); + form.append(footer); - if (!(typeof config.submit === 'boolean' && config.submit === false)) { - // add submit button unless config.submit is false - footer.append(this.make_submit_button()); + if (!(typeof config.submit === 'boolean' && config.submit === false)) { + // add submit button unless config.submit is false + footer.append(this.make_submit_button()); + } + form[0].addEventListener("submit", (e) => { + e.preventDefault(); + e.stopPropagation(); + if (form.find(".caosdb-f-form-submitting").length > 0) { + // do not submit twice + return; } - form[0].addEventListener("submit", (e) => { - e.preventDefault(); - e.stopPropagation(); - if (form.find(".caosdb-f-form-submitting").length > 0) { - // do not submit twice - return; - } - this.logger.debug("submit form", e); + this.logger.debug("submit form", e); - form[0].dispatchEvent(this.submit_form_event); + form[0].dispatchEvent(this.submit_form_event); - form.find(":input").prop("disabled", true); - var submitting = form_elements.make_submitting_info(); - form.find(".caosdb-f-form-elements-footer").before(submitting); + form.find(":input").prop("disabled", true); + var submitting = form_elements.make_submitting_info(); + form.find(".caosdb-f-form-elements-footer").before(submitting); - form[0].addEventListener(this.form_success_event.type, (e) => { - submitting.remove(); - }, true); - form[0].addEventListener(this.form_error_event.type, (e) => { - submitting.remove(); - }, true); + form[0].addEventListener(this.form_success_event.type, (e) => { + submitting.remove(); + }, true); + form[0].addEventListener(this.form_error_event.type, (e) => { + submitting.remove(); + }, true); - // remove old messages - const error_handler = config.error; - const success_handler = config.success; - const submit_callback = config.submit; - form.find(".caosdb-f-form-elements-message").remove(); - if (typeof config.submit === "function") { - // wrap callback in async function - const _wrap_callback = async function () { - try { - var results = await submit_callback(form[0]); - - // success_handler - if (typeof success_handler === "function") { - var processed = await success_handler(form[0], results); - if (typeof processed !== "undefined") { - form_elements.show_results(form[0], processed); - } - } else { - form_elements.show_results(form[0], results); + // remove old messages + const error_handler = config.error; + const success_handler = config.success; + const submit_callback = config.submit; + form.find(".caosdb-f-form-elements-message").remove(); + if (typeof config.submit === "function") { + // wrap callback in async function + const _wrap_callback = async function () { + try { + var results = await submit_callback(form[0]); + + // success_handler + if (typeof success_handler === "function") { + var processed = await success_handler(form[0], results); + if (typeof processed !== "undefined") { + form_elements.show_results(form[0], processed); } + } else { + form_elements.show_results(form[0], results); + } - form[0].dispatchEvent(form_elements.form_success_event); - } catch (err) { + form[0].dispatchEvent(form_elements.form_success_event); + } catch (err) { - // error_handler - if (typeof error_handler === "function") { - var processed = await error_handler(form[0], err); - if (typeof processed !== "undefined") { - form_elements.show_results(form[0], processed); - } - } else { - form_elements.show_errors(form[0], err); + // error_handler + if (typeof error_handler === "function") { + var processed = await error_handler(form[0], err); + if (typeof processed !== "undefined") { + form_elements.show_results(form[0], processed); } - - form[0].dispatchEvent(form_elements.form_error_event); + } else { + form_elements.show_errors(form[0], err); } - }(); - } - return false; + form[0].dispatchEvent(form_elements.form_error_event); + } + }(); + } + return false; - }, true); - form[0].addEventListener(this.form_success_event.type, function (e) { - // remove submit button, show ok button - form.find("button[type='submit']").remove(); - form.find("button:contains('Cancel')").text("Ok").prop("disabled", false); - }, true); - form[0].addEventListener(this.form_error_event.type, function (e) { - // reenable inputs - form.find(":input").prop("disabled", false); - }, true); + }, true); - // add cancel button - $(footer).append(this.make_cancel_button(form[0])); + form[0].addEventListener(this.form_success_event.type, function (e) { + // remove submit button, show ok button + form.find("button[type='submit']").remove(); + form.find("button:contains('Cancel')").text("Ok").prop("disabled", false); + }, true); + form[0].addEventListener(this.form_error_event.type, function (e) { + // reenable inputs + form.find(":input").prop("disabled", false); + }, true); - // init caching for this form - form_elements.init_form_caching(config, form[0]); + // add cancel button + $(footer).append(this.make_cancel_button(form[0])); - // init validation - form_elements.init_validator(form[0]); + // init caching for this form + form_elements.init_form_caching(config, form[0]); - this.logger.trace("leave make_generic_form"); - return form[0]; - } + // init validation + form_elements.init_validator(form[0]); - this.init_form_caching = function (config, form) { - var default_config = { - "cache_event": form_elements.submit_form_event.type, - "cache_storage": localStorage - }; - var lconfig = $.extend({}, default_config, config); + this.logger.trace("leave make_generic_form"); + return form[0]; + } - this.logger.trace("init_form_caching", lconfig, form); + this.init_form_caching = function (config, form) { + var default_config = { + "cache_event": form_elements.submit_form_event.type, + "cache_storage": localStorage + }; + var lconfig = $.extend({}, default_config, config); - form.addEventListener(lconfig.cache_event, (e) => { - form_elements.cache_form(lconfig.cache_storage, form); - }, true); - form_elements.load_cached(lconfig.cache_storage, form); - } + this.logger.trace("init_form_caching", lconfig, form); - this.show_results = function (form, results) { - $(form).append(results); - } + form.addEventListener(lconfig.cache_event, (e) => { + form_elements.cache_form(lconfig.cache_storage, form); + }, true); + form_elements.load_cached(lconfig.cache_storage, form); + } - this.show_errors = function (form, errors) { - $(form).append(errors); - } + this.show_results = function (form, results) { + $(form).append(results); + } - this.make_footer = function () { - return $('<div class="text-right caosdb-f-form-elements-footer"/>') - .css({ - "margin": "20px", - }).append(this.make_required_marker()) - .append('<span style="margin-right: 4px; font-size: 11px">required field</span>')[0]; - } + this.show_errors = function (form, errors) { + $(form).append(errors); + } - this.make_error_message = function (message) { - return this.make_message(message, "error"); - } + this.make_footer = function () { + return $('<div class="text-end caosdb-f-form-elements-footer"/>') + .css({ + "margin": "20px", + }).append(this.make_required_marker()) + .append('<span style="margin-right: 4px; font-size: 11px">required field</span>')[0]; + } - this.make_success_message = function (message) { - return this.make_message(message, "success"); - } + this.make_error_message = function (message) { + return this.make_message(message, "error"); + } + + this.make_success_message = function (message) { + return this.make_message(message, "success"); + } + + this.make_submitting_info = function () { + // TODO styling + return $(this.make_message("Submitting... please wait. This might take some time.", "info")) + .toggleClass("h3", true) + .toggleClass("caosdb-f-form-submitting", true) + .toggleClass("text-right", true)[0]; + } - this.make_submitting_info = function () { - // TODO styling - return $(this.make_message("Submitting... please wait. This might take some time.", "info")) - .toggleClass("h3", true) - .toggleClass("caosdb-f-form-submitting", true) - .toggleClass("text-right", true)[0]; + this.make_message = function (message, type) { + var ret = $('<div class="caosdb-f-form-elements-message"/>'); + if (type) { + ret.addClass("caosdb-f-form-elements-message-" + type); } + return ret.append(markdown.textToHtml(message))[0]; + } - this.make_message = function (message, type) { - var ret = $('<div class="caosdb-f-form-elements-message"/>'); - if (type) { - ret.addClass("caosdb-f-form-elements-message-" + type); - } - return ret.append(markdown.textToHtml(message))[0]; + /** + * @typedef {object} RangeFieldConfig + * + * @augments FieldConfig + * @property {FieldConfig} from - the start point of the range. This is + * usually an integer or double input field. + * @property {FieldConfig] to - the end point of the range. This is + * usually an integer or a double input field. + */ + + /** + * Return a new form field representing a range of numbers. + * + * @param {RangeFieldConfig} config + * @return {HTMLElement} + */ + this.make_range_input = function (config) { + + // TODO + // 1. wrapp both inputs to separate it from the label into a container + // 2. make two rows for each input + // 3. make inline-block for all included elements + const from_config = $.extend({}, { + cached: config.cached, + required: config.required, + type: "double" + }, config.from); + const to_config = $.extend({}, { + cached: config.cached, + required: config.required, + type: "double" + }, config.to); + + const from_input = this.make_form_field(from_config); + const to_input = this.make_form_field(to_config); + + const ret = $(this._make_field_wrapper(config.name)); + if (config.label) { + ret.append(this._make_input_label_str(config)); } - /** - * TODO make syncronous - */ - this.make_range_input = async function (config) { - - // TODO - // 1. wrapp both inputs to separate it from the label into a container - // 2. make two rows for each input - // 3. make inline-block for all included elements - const from_config = $.extend({}, { - cached: config.cached, - required: config.required, - type: "double" - }, config.from); - const to_config = $.extend({}, { - cached: config.cached, - required: config.required, - type: "double" - }, config.to); - - const from_input = await this.make_form_field(from_config); - const to_input = await this.make_form_field(to_config); - - const ret = $(this._make_field_wrapper(config.name)); - if (config.label) { - ret.append(this._make_input_label_str(config)); - } + ret.append(from_input); + ret.append(to_input); - ret.append(from_input); - ret.append(to_input); + // styling + $(from_input).toggleClass("form-control", false); + $(from_input).find(".col-sm-3").toggleClass("col-sm-3", false).toggleClass("col-sm-1"); + $(from_input).find(".col-sm-9").toggleClass("col-sm-9", false).toggleClass("col-sm-3"); + $(to_input).toggleClass("form-control", false); + $(to_input).find(".col-sm-3").toggleClass("col-sm-3", false).toggleClass("col-sm-1").toggleClass("col-sm-offset-1"); + $(to_input).find(".col-sm-9").toggleClass("col-sm-9", false).toggleClass("col-sm-3"); - // styling - $(from_input).toggleClass("form-group", false); - $(from_input).find(".col-sm-3").toggleClass("col-sm-3", false).toggleClass("col-sm-1"); - $(from_input).find(".col-sm-9").toggleClass("col-sm-9", false).toggleClass("col-sm-3"); - $(to_input).toggleClass("form-group", false); - $(to_input).find(".col-sm-3").toggleClass("col-sm-3", false).toggleClass("col-sm-1").toggleClass("col-sm-offset-1"); - $(to_input).find(".col-sm-9").toggleClass("col-sm-9", false).toggleClass("col-sm-3"); + return ret[0]; + } - return ret[0]; - } + /** + * Return a DIV with class `caosdb-f-field` and a data attribute + * `data-field-name` which contains the name. + * + * The DIV is used to wrap LABEL and INPUT elements of a form together. + * + * @param {string} name - the name of the field. + * @returns {HTMLElement} a DIV. + */ + this._make_field_wrapper = function (name) { + caosdb_utils.assert_string(name, "param `name`"); + return $('<div class="row caosdb-f-field" data-field-name="' + name + '" />')[0]; + } - /** - * Return a DIV with class `caosdb-f-field` and a data attribute - * `data-field-name` which contains the name. - * - * The DIV is used to wrap LABEL and INPUT elements of a form together. - * - * @param {string} name - the name of the field. - * @returns {HTMLElement} a DIV. - */ - this._make_field_wrapper = function (name) { - caosdb_utils.assert_string(name, "param `name`"); - return $('<div class="form-group caosdb-f-field" data-field-name="' + name + '" />') - .css({"padding": "0"})[0]; - } + /** + * Return a new date field. + * + * @param {FieldConfig} config + * @return {HTMLElement} + */ + this.make_date_input = function (config) { + return this._make_input(config); + } - this.make_date_input = function (config) { - return this._make_input(config); - } + /** + * Return a new text field. + * + * @param {FieldConfig} config + * @return {HTMLElement} + */ + this.make_text_input = function (config) { + return this._make_input(config); + } - this.make_text_input = function (config) { - return this._make_input(config); + + /** + * Return an input field which accepts double values. + * + * `config.type` is set to "number" and overrides any other type. + * + * @param {FieldConfig} config. + * @returns {HTMLElement} a double form field. + */ + this.make_double_input = function (config) { + const _config = $.extend({}, config, { + type: "number" + }); + const ret = $(this._make_input(_config)) + if (typeof config.min !== "undefined") { + ret.find("input").attr("min", config.min); + } + if (typeof config.max !== "undefined") { + ret.find("input").attr("max", config.max); } + ret.find("input").attr("step", "any"); + return ret[0]; + } - /** - * Return an input field which accepts double values. - * - * `config.type` is set to "number" and overrides any other type. - * - * @param {form_elements.input_config} config. - * @returns {HTMLElement} a double form field. - */ - this.make_double_input = function (config) { - var clone = $.extend({}, config, { - type: "number" + /** + * Return an input field which accepts integers. + * + * `config.type` is set to "number" and overrides any other type. + * + * @param {FieldConfig} config. + * @returns {HTMLElement} an integer form field. + */ + this.make_integer_input = function (config) { + var ret = $(this.make_double_input(config)); + ret.find("input").attr("step", "1"); + return ret[0]; + } + + /** + * @typedef {object} FileFieldConfig + * + * @augments FieldConfig + * @property {boolean} [multiple=false] - whether to accept multiple files. + * @property {string} [accept] - a comma separated list of file extensions + * which are accepted (exclusively). + */ + + /** + * Return a new form field for a file upload. + * + * @param {FileFieldConfig} config - configuration for this form field. + * @return {HTMLElement} + */ + this.make_file_input = function (config) { + const ret = this._make_input(config); + $(ret) + .find(":input") + .prop("multiple", !!config.multiple) + .css({ + "display": "block" }); - var ret = $(this._make_input(clone)) - ret.find("input").attr("step", "any"); - return ret[0]; + if (config.accept) { + $(ret) + .find(":input") + .attr("accept", config.accept); } + return ret; + } - /** - * Return an input field which accepts integers. - * - * `config.type` is set to "number" and overrides any other type. - * - * @param {form_elements.input_config} config. - * @returns {HTMLElement} an integer form field. - */ - this.make_integer_input = function (config) { - var ret = $(this.make_double_input(config)); - ret.find("input").attr("step", "1"); - return ret[0]; - } + /** + * @typedef {object} SelectOptionConfig + * + * @property {string} value - the value of the select option. + * @property {string} [label] - a visible representation (think: + * description) of the value of the select option. defaults to the + * value itself. + */ + /** + * @typedef {object} SelectFieldConfig + * + * @augments {FieldConfig} + * @property {SelectOptionConfig} - options + */ - /** - * Return a checkbox input field. - * - * @param {form_elements.checkbox_config} config. - * @returns {HTMLElement} a checkbox form field. - */ - this.make_checkbox_input = function (config) { - var clone = $.extend({}, config, { - type: "checkbox" - }); - var ret = $(this._make_input(clone)); - ret.find("input:checkbox").prop("checked", false); - ret.find("input:checkbox").toggleClass("form-control", false); - if (config.checked) { - ret.find("input:checkbox").prop("checked", true); - ret.find("input:checkbox").attr("checked", "checked"); - } - if (config.value) { - ret.find("input:checkbox").attr("value", config.value); - } - return ret[0]; + /** + * Return a select field. + * + * @param {SelectFieldConfig} config + * @returns {HTMLElement} a select field. + */ + this.make_select_input = function (config) { + const options = config.options; + const select = $(form_elements._make_select(config.multiple, config.name)); + + for (let option of options) { + select.append(form_elements._make_option(option.value, option.label)); } + const ret = form_elements._make_input(config, select[0]); + // Here, the bootstrap-select features should be activated for the new + // select element. However, up until now, this only works when the + // select element is already a part of the dom tree - which is not the + // case when this method is called and is controlled by the client. So + // there is currently no other work-around than to call + // init_select_picker after the form creation explicitely :( + //form_elements.init_select_picker(select[0], config.value); + + return ret; + } - /** - * Add `caosdb-f-form-field-required` class to form field. - * - * @param {HTMLElement} field - the required form field. - */ - this.set_required = function (field) { - $(field).toggleClass("caosdb-f-form-field-required", true); - $(field).find(":input").prop("required", true); - $(field).find("label").prepend(this.make_required_marker()); + /** + * Return a checkbox input field. + * + * @param {form_elements.checkbox_config} config. + * @returns {HTMLElement} a checkbox form field. + */ + this.make_checkbox_input = function (config) { + var clone = $.extend({}, config, { + type: "checkbox" + }); + var ret = $(this._make_input(clone)); + ret.find("input:checkbox").prop("checked", false); + ret.find("input:checkbox").toggleClass("form-control", false); + if (config.checked) { + ret.find("input:checkbox").prop("checked", true); + ret.find("input:checkbox").attr("checked", "checked"); } - - /** - * Return a span which is to be inserted before a field's label text - * and which marks that field as required. - * - * @returns {HTMLElement} span element. - */ - this.make_required_marker = function () { - // TODO create class and move to css file - return $('<span>*</span>') - .css({ - "font-size": "10px", - "color": "red", - "margin-right": "4px", - "font-weight": "100", - })[0]; + if (config.value) { + ret.find("input:checkbox").attr("value", config.value); } + return ret[0]; + } - this.get_enabled_required_fields = function (form) { - return $(this.get_enabled_fields(form)) - .filter(".caosdb-f-form-field-required") - .toArray(); - } + /** + * Add `caosdb-f-form-field-required` class to form field. + * + * @param {HTMLElement} field - the required form field. + */ + this.set_required = function (field) { + $(field).toggleClass("caosdb-f-form-field-required", true); + $(field).find(":input").prop("required", true); + $(field).find("label").prepend(this.make_required_marker()); + } + /** + * Return a span which is to be inserted before a field's label text + * and which marks that field as required. + * + * @returns {HTMLElement} span element. + */ + this.make_required_marker = function () { + // TODO create class and move to css file + return $('<span>*</span>') + .css({ + "font-size": "10px", + "color": "red", + "margin-right": "4px", + "font-weight": "100", + })[0]; + } - this.get_enabled_fields = function (form) { - return $(form) - .find(".caosdb-f-field") - .filter(function (idx) { - // remove disabled fields from results - return !$(this).hasClass("caosdb-f-field-disabled"); - }) - .toArray(); - } + this.get_enabled_required_fields = function (form) { + return $(this.get_enabled_fields(form)) + .filter(".caosdb-f-form-field-required") + .toArray(); + } - this.all_required_fields_set = function (form) { - const req = form_elements.get_enabled_required_fields(form); - for (const field of req) { - if (!form_elements.is_set(field)) { - return false; - } - } - return true; - } - /** - * @param {HTMLElement} form - the form be validated. - */ - this.is_valid = function (form) { - return form_elements.all_required_fields_set(form); - } + this.get_enabled_fields = function (form) { + return $(form) + .find(".caosdb-f-field") + .filter(function (idx) { + // remove disabled fields from results + return !$(this).hasClass("caosdb-f-field-disabled"); + }) + .toArray(); + } - this.toggle_submit_button_form_valid = function (form, submit) { - // TODO do not change the submit button directly. change the - // `submittable` state of the form and handle the case where a form - // is submitting when this function is called. - if (form_elements.is_valid(form)) { - $(submit).prop("disabled", false); - } else { - $(submit).prop("disabled", true); + this.all_required_fields_set = function (form) { + const req = form_elements.get_enabled_required_fields(form); + for (const field of req) { + if (!form_elements.is_set(field)) { + return false; } } + return true; + } + /** + * @param {HTMLElement} form - the form be validated. + */ + this.is_valid = function (form) { + return form_elements.all_required_fields_set(form); + } - this.init_validator = function (form) { - const submit = $(form).find(":input[type='submit']")[0]; - if (submit) { - form.addEventListener("caosdb.field.changed", (e) => form_elements.toggle_submit_button_form_valid(form, submit), true); - form.addEventListener("caosdb.field.enabled", (e) => form_elements.toggle_submit_button_form_valid(form, submit), true); - form.addEventListener("input", (e) => form_elements.toggle_submit_button_form_valid(form, submit), true); - } + + this.toggle_submit_button_form_valid = function (form, submit) { + // TODO do not change the submit button directly. change the + // `submittable` state of the form and handle the case where a form + // is submitting when this function is called. + if (form_elements.is_valid(form)) { + $(submit).prop("disabled", false); + } else { + $(submit).prop("disabled", true); } + } - /** - * Return an input and a label, wrapped in a div with class - * `caosdb-f-field`. - * - * @param {object} config - config object with `name`, `type` and - * optional `label` - * @returns {HTMLElement} a form field. - */ - this._make_input = function (config) { - caosdb_utils.assert_string(config.name, "the name of a form field"); - let ret = $(this._make_field_wrapper(config.name)); - let name = config.name; - let label = this._make_input_label_str(config); - let type = config.type || "text"; - let value = config.value; - let input = $('<input class="form-control caosdb-f-property-single-raw-value" type="' + type + - '" name="' + name + - '" />'); - input.change(function () { - ret[0].dispatchEvent(form_elements.field_changed_event); - }); - let input_col = $('<div class="caosdb-f-property-value col-sm-9"/>'); - input_col.append(input); - if (value) { - input.val(value); - } - return ret.append(label, input_col)[0]; + this.init_validator = function (form) { + const submit = $(form).find(":input[type='submit']")[0]; + if (submit) { + form.addEventListener("caosdb.field.changed", (e) => form_elements.toggle_submit_button_form_valid(form, submit), true); + form.addEventListener("caosdb.field.enabled", (e) => form_elements.toggle_submit_button_form_valid(form, submit), true); + form.addEventListener("input", (e) => form_elements.toggle_submit_button_form_valid(form, submit), true); } + } - /** - * Return a string representation of a LABEL element, ready for parsing. - * - * This function is used by other functions to generate a LABEL element. - * - * The config's `name` goes to the `for` attribute, the `label` is the - * text node of the resulting LABEL element. - * - * @param {object} config - a config object with `name` and `label`. - * @returns {string} a html string for a LABEL element. - */ - this._make_input_label_str = function (config) { - let name = config.name; - let label = config.label; - return label ? '<label for="' + name + - '" data-property-name="' + name + - '" class="control-label col-sm-3">' + label + - '</label>' : ""; + + /** + * Return an input and a label, wrapped in a div with class + * `caosdb-f-field`. + * + * @param {object} config - config object with `name`, `type` and + * optional `label` + * @param {string} input - optional specification of the HTML input element. + * `<input class="form-control caosdb-f-property-single-raw-value" type="' + type + '" name="' + name + '" />` + * is used as default where `name` and `type` stem from the config + * object. + * @returns {HTMLElement} a form field. + */ + this._make_input = function (config, input) { + caosdb_utils.assert_string(config.name, "the name of a form field"); + let ret = $(this._make_field_wrapper(config.name)); + let name = config.name; + let label = this._make_input_label_str(config); + let type = config.type || "text"; + let value = config.value; + const _input = $(input || + '<input class="form-control caosdb-f-property-single-raw-value" type="' + + type + '" name="' + name + '" />'); + _input.change(function () { + ret[0].dispatchEvent(form_elements.field_changed_event); + }); + let input_col = $('<div class="caosdb-f-property-value col-sm-9"/>'); + input_col.append(_input); + if (value) { + _input.val(value); } + return ret.append(label, input_col)[0]; + } + /** + * Return a string representation of a LABEL element, ready for parsing. + * + * This function is used by other functions to generate a LABEL element. + * + * The config's `name` goes to the `for` attribute, the `label` is the + * text node of the resulting LABEL element. + * + * @param {object} config - a config object with `name` and `label`. + * @returns {string} a html string for a LABEL element. + */ + this._make_input_label_str = function (config) { + let name = config.name; + let label = config.label; + return label ? '<label for="' + name + + '" data-property-name="' + name + + '" class="col-form-label col-sm-3">' + label + + '</label>' : ""; } + this._init_functions(); } diff --git a/src/core/js/preview.js b/src/core/js/preview.js index f74fd13ffd2fe373543c025866e91cfe6b6fd9eb..c5b7752b5070796ab01fb3d007065d2a4fc6b425 100644 --- a/src/core/js/preview.js +++ b/src/core/js/preview.js @@ -211,8 +211,8 @@ var preview = new function() { /** * Transform the raw xml response of the server into an array of entities for preview. * - * @param {Promise for XMLDocument} xml - A Promise for the servers xml response. - * @return {Promise for HTMLElement[]} A Promise for an array of entities. + * @param {Promise | XMLDocument} xml - A Promise for the servers xml response. + * @return {Promise | HTMLElement[]} A Promise for an array of entities. */ this.processPreviewResponse = function(xml) { let xsl = preview.getEntityXsl(); @@ -222,7 +222,7 @@ var preview = new function() { /** * Retrieve the XSL script for entities from the server. * - * @return {Promise for XMLDocument} A Promise for the XSL script. + * @return {Promise | XMLDocument} A Promise for the XSL script. */ this.getEntityXsl = async function _getEntityXsl() { return transformation.retrieveEntityXsl(); @@ -245,7 +245,7 @@ var preview = new function() { * @return {HTMLElement} A button for showing the preview carousel. */ this.createShowPreviewButton = function() { - return $('<button class="' + preview.classNameShowPreviewButton + ' btn btn-link btn-xs" title="Show preview of the referenced entities."><span class="glyphicon glyphicon-eye-open"></button>')[0]; + return $('<button class="' + preview.classNameShowPreviewButton + ' align-self-start btn btn-link btn-sm" title="Show preview of the referenced entities."><i class="bi-eye-fill"></i></button>')[0]; } /** @@ -253,7 +253,7 @@ var preview = new function() { * @return {HTMLElement} A button for hiding the preview carousel. */ this.createHidePreviewButton = function() { - return $('<button class="' + preview.classNameHidePreviewButton + ' btn btn-link btn-xs" title="Hide preview and show links."><span class="glyphicon glyphicon-eye-close"></button>')[0]; + return $('<button class="' + preview.classNameHidePreviewButton + ' align-self-start btn btn-link btn-sm" title="Hide preview and show links."><i class="bi-eye-slash-fill"></i></button>')[0]; } /** @@ -274,7 +274,8 @@ var preview = new function() { * @return {HTMLElement} parameter `ref_property_elem` */ this.addShowPreviewButton = function(ref_property_elem, button_elem) { - ref_property_elem.getElementsByClassName("caosdb-f-property-value")[0].appendChild(button_elem); + caosdb_utils.assert_html_element(button_elem, "param `button_elem`"); + $(ref_property_elem.getElementsByClassName("caosdb-f-property-value")[0]).prepend(button_elem); return ref_property_elem; } @@ -381,15 +382,16 @@ var preview = new function() { /** * Create a preview carousel from an array of entity elements. * - * A carousel consists of the main div with class `carousel slide` and a unique ID which will - * be generated here. Inside there are the navigation bar with class - * `caosdb-preview-carousel-nav`, and the inner div which contains and show the several slides - * with class `carousel-inner`. + * A carousel consists of the main div with class `carousel slide` and a + * unique ID which will be generated here. Inside there are the navigation + * bar with class `caosdb-preview-carousel-nav`, and the inner div which + * contains and show the several slides with class `carousel-inner`. * * The refLinksContainer are cloned and modified such that they trigger the - * sliding and added to the navigation bar. Then a set of empty slides is added to the inner - * div. The entities are put into the correct slide using the data-slide-to attributes and the - * entity id of each selector button. + * sliding and added to the navigation bar. Then a set of empty slides is + * added to the inner div. The entities are put into the correct slide + * using the data-bs-slide-to attributes and the entity id of each selector + * button. * * @param {HTMLElement[]} entities - The array of entity elements. * @param {HTMLElement} refLinksContainer - The original reference links. @@ -401,19 +403,19 @@ var preview = new function() { } let carouselId = ("previewCarousel" + preview.carouselId++); let nav = preview.createCarouselNav(refLinksContainer, carouselId); //preserves order, first is active - let N = $(nav).find('[data-slide-to]').length; + let N = $(nav).find('[data-bs-slide-to]').length; let inner = preview.createEmptyInner(N) //no content, first is active let selectorButtons = preview.getSelectorButtons(nav); selectorButtons.each((index, button) => { - let slide_id = button.getAttribute("data-slide-to"); + let slide_id = button.getAttribute("data-bs-slide-to"); let entity_id_version = preview.getEntityRef(button); let entity = preview.getEntityByIdVersion(entities, entity_id_version); if (entity == null) throw new Error("Entity with ID " + entity_id_version + " could not be found!"); inner.children[slide_id].appendChild(preview.preparePreviewEntity(entity)); }); - let mainDiv = $('<div class="carousel slide" data-interval="false"></div>')[0]; + let mainDiv = $('<div data-bs-interval="false" class="carousel slide"></div>')[0]; mainDiv.appendChild(nav); mainDiv.appendChild(inner); mainDiv.id = carouselId; @@ -424,14 +426,15 @@ var preview = new function() { } /** - * Get the selector buttons from a div which contains them or return the single selector button - * if the `refLinksContainer` parameter is itself the selector button. + * Get the selector buttons from a div which contains them or return the + * single selector button if the `refLinksContainer` parameter is itself + * the selector button. * * @param {HTMLElement} refLinksContainer * @return {jQuery} A collection of selector buttons. */ this.getSelectorButtons = function(refLinksContainer) { - return $(refLinksContainer).find('[data-slide-to]').addBack('[data-slide-to]'); + return $(refLinksContainer).find('[data-bs-slide-to]'); } /** @@ -472,26 +475,16 @@ var preview = new function() { * @return {HTMLElement} The prepared entity. */ this.preparePreviewEntity = function(entity) { - // move version modal into body because otherwise it would be displayed - // inside the caroussel. That would make sense but there is simply not - // enough space. - $(entity).find(".caosdb-f-entity-version-info").appendTo(document.body); - + const preparedEntity = entity.cloneNode(true); - // make backref button smaller - $(entity).find(".caosdb-backref-link > .hidden-xs").hide(); + const href = connection.getBasePath() + transaction.generateEntitiesUri([preview.getEntityRef(entity)]); - var preparedEntity = entity.cloneNode(true); + const link = $('<a title="Load this entity in a new window." href="' + href + '" class="btn" target="_blank"></a>'); + link.append('<i class="bi bi-box-arrow-up-right"></i>'); - // header is clickable: - let href = connection.getBasePath() + transaction.generateEntitiesUri([preview.getEntityRef(entity)]); - let link = $('<a title="Load this entity in a new window." href="' + href + '" class="label caosdb-id caosdb-id-button" target="_blank"></a>'); - let entityIdElem = $(preparedEntity).find('.label.caosdb-id'); - link.insertAfter(entityIdElem); - link.append(entityIdElem.text() + " "); - link.append('<span class="glyphicon glyphicon-new-window"/>'); - // TODO this link is not visible due to webcaosdb.css (caosdb-id) - entityIdElem.remove(); + const buttonsList = $(preparedEntity).find(".caosdb-v-entity-header-buttons-list"); + buttonsList.children().hide(); + buttonsList.append(link); return preparedEntity; } @@ -509,9 +502,9 @@ var preview = new function() { if (carouselId == null) { throw new Error("carouselId must not be null."); } - let prevButton = $('<a role="button" style="z-index: 5;position:absolute; top: 0;left:0px" class="btn btn-default btn-sm" href="#' + carouselId + '" data-slide="prev"></a>')[0]; + let prevButton = $('<a role="button" style="z-index: 5;position:absolute; top: 0;left:0px" class="btn btn-secondary btn-sm" href="#' + carouselId + '" data-bs-slide="prev"></a>')[0]; prevButton.innerHTML = preview.carouselPrevButtonInnerHTML; - let nextButton = $('<a role="button" style="z-index: 5;position:absolute; top: 0;right:0px" class="btn btn-default btn-sm" href="#' + carouselId + '" data-slide="next"></a>')[0]; + let nextButton = $('<a role="button" style="z-index: 5;position:absolute; top: 0;right:0px" class="btn btn-secondary btn-sm" href="#' + carouselId + '" data-bs-slide="next"></a>')[0]; nextButton.innerHTML = preview.carouselNextButtonInnerHTML; let nav = $('<div class="' + preview.classNamePreviewCarouselNav + '"></div>')[0]; let selectors = refLinksContainer.cloneNode(true); @@ -519,11 +512,11 @@ var preview = new function() { // resolvable-reference class but did not resolve it yet. $(selectors).show(); - $(selectors).find('a,button,.btn').each((index, button) => { + $(selectors).find('a.caosdb-f-reference-value').each((index, button) => { $(button).toggleClass("active", index === 0); button.removeAttribute("href"); - button.setAttribute("data-slide-to", index); - button.setAttribute("data-target", "#" + carouselId); + button.setAttribute("data-bs-slide-to", index); + button.setAttribute("data-bs-target", "#" + carouselId); }); nav.appendChild(prevButton); nav.appendChild(nextButton); @@ -532,12 +525,13 @@ var preview = new function() { return nav; }; - this.carouselPrevButtonInnerHTML = '<span class="glyphicon glyphicon-chevron-left" aria-hidden="true"></span></span><span class="sr-only">Previous</span>'; - this.carouselNextButtonInnerHTML = '<span class="glyphicon glyphicon-chevron-right" aria-hidden="true"></span></span><span class="sr-only">Next</span>'; + this.carouselPrevButtonInnerHTML = '<i class="bi-chevron-left" aria-hidden="true"></i><span class="visually-hidden">Previous</span>'; + this.carouselNextButtonInnerHTML = '<i class="bi-chevron-right" aria-hidden="true"></i><span class="visually-hidden">Next</span>'; /** - * Create a div with class `carousel-inner` which contains N divs with class `item` while - * the first also has class `active`. These item divs are empty. + * Create a div with class `carousel-inner` which contains N divs with + * class `carousel-item` while the first also has class `active`. These + * item divs are empty. * * @param {Number} N - An integer > 0. * @return {HTMLElement} A Div with class `carousel-inner`. @@ -546,8 +540,8 @@ var preview = new function() { if (N == null || isNaN(N) || N < 1) { throw new Error("N is to be an integer > 0"); } - let innerDiv = $('<div class="carousel-inner"><div class="item active"></div></div>')[0]; - let item = $('<div class="item"></div>')[0]; + let innerDiv = $('<div class="carousel-inner"><div class="carousel-item active"></div></div>')[0]; + let item = $('<div class="carousel-item"></div>')[0]; for (let i = 1; i < N; i++) { innerDiv.appendChild(item.cloneNode()); } @@ -654,7 +648,7 @@ var preview = new function() { * @returns {HTMLElement} The ith slide item selector. */ this.getSlideItemSelector = function(carousel, i) { - let items = $(carousel).find('.' + preview.classNamePreviewCarouselNav).find('[data-slide-to]'); + let items = $(carousel).find('.' + preview.classNamePreviewCarouselNav).find('[data-bs-slide-to]'); if (items.length <= i) { throw new Error("Index out of bounds."); } @@ -679,7 +673,7 @@ var preview = new function() { * Retrieve a list of entities from the server. * * @param {String[]} entityIds - The ids of the entities which are to be retrieved. - * @return {Promise for HTMLElement[]} A Promise for an array of entities. + * @return {Promise | HTMLElement[]} A Promise for an array of entities. */ this.retrievePreviewEntities = async function _rPE(entityIds) { try { @@ -711,9 +705,9 @@ var preview = new function() { /** * Transform the xml to an array of entities. * - * @param {Promise XMLDocument} xml - The server response. - * @param {Promise XMLDocument} xsl - The xsl script. - * @return {Promise HTMLElement[]} A promise for an Array of HTMLElements. + * @param {Promise | XMLDocument} xml - The server response. + * @param {Promise | XMLDocument} xsl - The xsl script. + * @return {Promise | HTMLElement[]} A promise for an Array of HTMLElements. */ this.transformXmlToPreviews = async function _tXTP(xml, xsl) { let html = await asyncXslt(xml, xsl); @@ -751,8 +745,11 @@ var preview = new function() { * @return {HTMLElement[]} A collection of links. */ this.getReferenceLinks = function(refLinksContainer) { - return $(refLinksContainer) - .find('a').addBack('a').has('.caosdb-id').toArray(); + var cont = $(refLinksContainer); + if (cont.is("a.caosdb-f-reference-value")) { + return cont.toArray(); + } + return cont.find('a.caosdb-f-reference-value').toArray(); } }; diff --git a/src/core/js/query_shortcuts.js b/src/core/js/query_shortcuts.js index 426a6db4ea3b25d10e0bb6c0289caa664b34e71f..af75516767b7a354562d7fdea744bc458047e500 100644 --- a/src/core/js/query_shortcuts.js +++ b/src/core/js/query_shortcuts.js @@ -70,14 +70,14 @@ var query_shortcuts = new function() { */ this.make_toolbox_button = function() { var ret = $( - `<div class="col-md-2 dropdown text-right caosdb-f-shortcuts-toolbox-button"> - <button title="Shortcuts Toolbox" class="btn dropdown-toggle" type="button" - data-toggle="dropdown"><span class="glyphicon glyphicon-wrench"></span></button> + `<div class="dropdown text-end caosdb-f-shortcuts-toolbox-button"> + <button title="Shortcuts Toolbox" class="btn dropdown-bs-toggle" type="button" + data-bs-toggle="dropdown"><i class="bi-wrench"></i></button> <ul class="dropdown-menu"> - <li class="dropdown-header">Shortcuts Tools</li> - <li><button class="btn" type="button" data-tool="create">Create</button></li> - <li><button class="btn" type="button" data-tool="edit">Edit</button></li> - <li><button class="btn" type="button" data-tool="delete">Delete</button></li> + <li class="dropdown-header dropdown-item">Shortcuts Tools</li> + <li><button class="btn dropdown-item" type="button" data-tool="create">Create</button></li> + <li><button class="btn dropdown-item" type="button" data-tool="edit">Edit</button></li> + <li><button class="btn dropdown-item" type="button" data-tool="delete">Delete</button></li> </ul> </div>`); @@ -144,12 +144,12 @@ var query_shortcuts = new function() { this.init = async function() { this.init_datamodel(); - var header = $('<div class="caosdb-f-shortcuts-panel-header row h3"><span class="caosdb-f-shortcuts-panel-header-title col-md-10">Shortcuts</span></div>') - .append(this.make_toolbox_button()); + var header = $('<details class="caosdb-f-shortcuts-panel-header"><summary class="caosdb-f-shortcuts-panel-header-title">Shortcuts</summary></details>') + header.find("summary").append(this.make_toolbox_button()); var body = $('<div class="caosdb-f-shortcuts-panel-body"/>'); + header.append(body) var shortcuts_panel = $('<div class="container caosdb-shortcuts-container"></div>') .append(header) - .append(body); body.append(await this.retrieve_global_shortcuts()); @@ -162,28 +162,11 @@ var query_shortcuts = new function() { $("#caosdb-query-panel").append(shortcuts_panel); } - // make toggle button - var toggle_button = this - .make_shortcuts_panel_toggle_button(shortcuts_panel); - - header.find("span.caosdb-f-shortcuts-panel-header-title") - .prepend(toggle_button); - - // initially hide panel or restore old visibility - if(sessionStorage[this._cache_visibility_key] !== "true") { - toggle_button.click(); - } - return shortcuts_panel[0]; } - this.make_shortcuts_panel_toggle_button = function(panel) { - var button = $('<span title="Toggle Shortcuts Panel" class="caosdb-f-shortcuts-panel-toggle-button glyphicon glyphicon-menu-down"/>'); - button.click((e) => this.toggle_shortcuts_panel(panel)); - return button[0]; - } /** @@ -296,7 +279,7 @@ var query_shortcuts = new function() { <div class="col-md-10"> <span class="caosdb-f-query-shortcut-form">` + preparedstr + `</span> </div> - <div class="caosdb-f-query-shortcut-right-col col-md-2 text-right"> + <div class="position-relative caosdb-f-query-shortcut-right-col col-md-2 text-end"> </div> </div>` ); @@ -495,7 +478,7 @@ var query_shortcuts = new function() { cloned.children().show(); cloned.find(".caosdb-f-query-shortcut").each((idx, item) => { - var wrapper = $(item).find(".col-md-2.text-right"); + var wrapper = $(item).find(".col-md-2.text-end"); // disable the inputs of the query shortcut $(item).find(":input").prop("disabled", true); @@ -506,7 +489,7 @@ var query_shortcuts = new function() { // user shortcut // insert "UPDATE" button var entity_id = $(item).attr("data-entity-id"); - var input = $('<button type="button" class="btn btn-default" name="update-' + entity_id + '">Edit</button>') + var input = $('<button type="button" class="btn btn-secondary" name="update-' + entity_id + '">Edit</button>') .attr("title", "Edit this shortcut.") .click(() => init_update_form(panel, entity_id)); wrapper.append(input); @@ -528,7 +511,7 @@ var query_shortcuts = new function() { this.init_cud_shortcut_form = function(panel, form) { // hide content $(panel).children().hide(); - var query_panel = $(".caosdb-query-panel").hide(); + var query_panel = $(".caosdb-query-form").hide(); // show original again on cancel form.addEventListener("caosdb.form.cancel", function(e) { @@ -538,12 +521,7 @@ var query_shortcuts = new function() { // show results in query panel form.addEventListener("caosdb.form.success", function(e) { - form.addEventListener("caosdb.form.cancel", function(e) { - // reset shortcuts - query_panel.show(); - query_shortcuts.reset(); - }, true); - + query_panel.show(); }, true); form.addEventListener("caosdb.form.submit", function(e) { @@ -630,8 +608,7 @@ var query_shortcuts = new function() { this.make_dismissible_alert = function(type, content) { var ret = $( `<div class="alert alert-` + type + ` alert-dismissible" role="alert"> - <button type="button" class="close" data-dismiss="alert" aria-label="Close"> - <span aria-hidden="true">×</span> + <button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"> </button> </div>`); ret.append(content); @@ -669,10 +646,10 @@ var query_shortcuts = new function() { this.make_shortcut_buttons = function(execute, customize) { - const execute_button = $('<button class="btn btn-primary" type="button" title="Execute query."><span class="glyphicon glyphicon-search"></span></button>') + const execute_button = $('<button class="btn btn-primary" type="button" title="Execute query."><i class="bi-search"></i></button>') .css({"font-size": "12px"}) .click(execute); - const customize_button = $('<button class="btn btn-primary" type="button" title="Write to the Query Panel for customization."><span class="glyphicon glyphicon-pencil"></span></button>') + const customize_button = $('<button class="btn btn-primary" type="button" title="Write to the Query Panel for customization."><i class="bi-pencil"></i></button>') .css({"font-size": "12px"}) .click(customize) .mouseenter(function(e) { @@ -688,26 +665,21 @@ var query_shortcuts = new function() { $('#caosdb-query-textarea').attr("style", ""); }); const button_group = $('<div class="btn-group"/>') - .css({"font-size": "12px", position: "absolute", top: 0, right: "15px"}) + .css({"font-size": "12px", position: "absolute", top: 0, right: "12px"}) .append(customize_button, execute_button) .hide() .mouseleave(function(e) { $(this).hide(); }); - const hover = $('<button class="btn btn-default caosdb-button-search"><span class="glyphicon glyphicon-search" aria-hidden="true"></span></button>') + const hover = $('<button class="btn btn-secondary caosdb-button-search"><i class="bi-search" aria-hidden="true"></i></button>') .css({"font-size": "12px"}) .click(execute) .mouseenter(() => { button_group.fadeIn(150); }); - const ret = $('<span/>') - .css({"text-align": "right"}) - .append(hover, button_group); - - //return [hover[0], button_group[0]]; - return ret[0]; + return [hover[0], button_group[0]]; } /** @@ -734,7 +706,7 @@ var query_shortcuts = new function() { // make form fields manually, only use the form_elements to wrap it. cloned.find(".caosdb-f-query-shortcut").each((idx, item) => { - var wrapper = $(item).find(".col-md-2.text-right"); + var wrapper = $(item).find(".col-md-2.text-end"); // disable the inputs of the query shortcut $(item).find(":input").prop("disabled", true); @@ -774,7 +746,7 @@ var query_shortcuts = new function() { this.make_form_entity = function(entity_id) { var entity = $(` - <div class="hidden" data-entity-role="Record"> + <div class="invisible" data-entity-role="Record"> </div>`); if(typeof entity_id === "string" || entity instanceof String) { entity.attr("data-entity-id", entity_id); @@ -959,20 +931,12 @@ var query_shortcuts = new function() { this._cache_visibility_key = "caosdb.query-shortcuts-panel.visible"; + /* TODO hide the toolbox when not visible this.toggle_shortcuts_panel = function(panel) { - var toggle_button = $(panel).find(".caosdb-f-shortcuts-panel-toggle-button") - .toggleClass("caosdb-f-shortcuts-panel-hidden") - .toggleClass("glyphicon-menu-right") - .toggleClass("glyphicon-menu-down"); - - - // remember visibility for page reloads - sessionStorage[this._cache_visibility_key] = !toggle_button - .hasClass("caosdb-f-shortcuts-panel-hidden"); - $(panel).find(".caosdb-f-shortcuts-toolbox-button").toggle(); $(panel).find(".caosdb-f-shortcuts-panel-body").toggle() } + */ // deps @@ -1103,12 +1067,12 @@ var query_shortcuts = new function() { text-align: left; } + .caosdb-f-query-shortcut button, .caosdb-f-query-shortcut div { min-height: 32px; - vertical-align: middle; } - div.text-right ul.dropdown-menu { + div.text-end ul.dropdown-menu { left: unset; right: 0; } diff --git a/src/core/js/tour.js b/src/core/js/tour.js index 7b47dd37239c29b5c5e7edb67f1d16fe43bf8ad6..b8ee39732eb0542e74ca4bced38a9203469b5aea 100644 --- a/src/core/js/tour.js +++ b/src/core/js/tour.js @@ -30,10 +30,11 @@ * defines chapters, sections and pages. Each page corresponds to a popover * window with some content, which can be activated by hint buttons. * - * Because it is easier to write for humans, this documentation will talk about - * a corresponding yaml file instead, which needs to be converted to a json file - * before usage. Please also consult `tour.example.yaml` stored in the `doc` - * folder. + * For the sake of simplicity, in this documentation we will talk about + * a corresponding yaml file (instead of json), which needs to be converted to + * a json file before usage. + * The `tour.yaml` shall be placed in `conf/ext/json` + * Please also consult `tour.example.yaml` stored in the `doc` folder. * * # Structure of the tour.yaml file # * @@ -73,6 +74,7 @@ * One of `top`, `bottom`, `left`, `right` or a combination thereof. * - button_size :: One of `small`, `medium`, `large`. If not given, the button * will have an automatic, not necessarily circular, size. + * Currently deactivated! * - title :: Title of the tour page. * - content :: The content of the page. Rendered as Markdown. * - highlighters :: Makes HTML elements of the content highlight other @@ -92,35 +94,42 @@ var INFO = 3 var DEBUG = 4 var TRACE = 5 -var tour = new function() { +var tour = new function () { /////////////////////////////////////////////////////////////////////////// // Improving a bit on jquery // /////////////////////////////////////////////////////////////////////////// // :Contains is case insensitive - jQuery.expr[':'].Contains = function(a, i, m) { + jQuery.expr[':'].Contains = function (a, i, m) { return jQuery(a).text().toUpperCase() .indexOf(m[3].toUpperCase()) >= 0; }; + var logger = log.getLogger("tour"); + this.PageSet = class { - constructor(parent_set, config) { - if(typeof parent_set === "undefined") { + constructor(parent_set, config, idx) { + if (typeof parent_set === "undefined") { throw new Error("param `parent_set` must not be undefined"); } - if(typeof config === "undefined") { + if (typeof config === "undefined") { throw new Error("param `config` must not be undefined"); } + this.isPageSet = true; this.parent_set = parent_set; this.config = config; this._elements = new Array(); this.active = false; - for (const element of config.elements) { - const next = tour.add_tour_element(element, this); - this._elements.push(next); + this.id = config.id || parent_set.id + "-psid-" + idx; + + if (config.elements) { + for (const element of config.elements) { + const next = tour.add_tour_element(element, this, this._elements.length); + this._elements.push(next); + } } // set some defaults @@ -138,12 +147,16 @@ var tour = new function() { } } + _tour_active () { + return this.parent_set._tour_active(); + } + _activate_by_id(id) { this.parent_set._activate_by_id(id); } set old_state_active(value) { - if(this.config.old_state_active != value) { + if (this.config.old_state_active != value) { this.config.old_state_active = value; this.update(); } @@ -169,20 +182,23 @@ var tour = new function() { return this._elements; } + /** + * Create menu entry for a PageSet + */ create_menu_entry() { - var page_set_entry = $("<li class='list-group-item caosdb-f-tour-overview-entry caosdb-v-tour-overview-entry-pageset'/>"); - var link = $("<a class='btn btn-link'>" + this.name + "</a>")[0]; - this.menu_entry = link; - link.addEventListener("click", () => {this.activate();}); + var menuid = 'tour-submenu-' + this.name.replace(/ /g, ""); + var page_set_entry = $("<li class='mb-1 caosdb-f-tour-overview-entry caosdb-v-tour-overview-entry-pageset'/><button class='caosdb-v-tour-toc-pageset btn' data-bs-toggle='collapse' data-bs-target='#" + menuid + "' aria-expanded='false'>" + this.name + "</button><div id='" + menuid + "'class='collapse'><ul class='btn-toggle-nav list-unstyled fw-normal pb-1'></ul></div></li>"); + var elements_list = page_set_entry.find("ul")[0]; + + // store menu_entry for highlightng opened pages/chapters. + this.menu_entry = page_set_entry[0]; - var elements_list = $('<ul class="list-group"/>'); for (const element of this.elements) { const next = element.create_menu_entry(); - elements_list.append(next); + if (next) { + elements_list.append(next); + } } - - page_set_entry.append(link); - page_set_entry.append(elements_list); return page_set_entry; } @@ -191,7 +207,7 @@ var tour = new function() { */ activate() { if (!this.active) { - tour.info("PageSet.activate: tour element '" + this.full_name + "'."); + logger.info("PageSet.activate: tour element '" + this.full_name + "'."); this._activate(); @@ -207,15 +223,20 @@ var tour = new function() { highlight_menu_entry() { $(this.menu_entry).toggleClass("caosdb-v-tour-menu-entry-highlight", true); + $(this.menu_entry).find(".collapse").toggleClass("show", true); } unhighlight_menu_entry() { $(this.menu_entry).toggleClass("caosdb-v-tour-menu-entry-highlight", false); + $(this.menu_entry).find(".collapse").toggleClass("show", false); } + /** + * _activate PageSet + */ _activate() { if (!this.active) { - tour.debug("PageSet._activate tour element '" + this.full_name + "'."); + logger.debug("PageSet._activate tour element '" + this.full_name + "'."); this.active = true; this.old_state_active = true; this.highlight_menu_entry(); @@ -226,22 +247,25 @@ var tour = new function() { } } + /** + * _on_activation PageSet + */ _on_activation() { let hiding = this.config.on_activation_hide; if (hiding) { - console.log("Tour page set hiding:"); + logger.debug("Tour page set hiding:"); hiding = tour.assert_array(hiding); hiding.forEach((selector) => { - console.log(selector); $(selector).addClass("caosdb-f-tour-hidden"); }); } } deactivate_other(trigger) { - if(this.config.deactivate_other) { - for (const element of this.elements) { - if(element instanceof tour.PageSet && element !== trigger) { + logger.debug("Close pages other than '" + trigger.id + "'."); + if (this.config.deactivate_other) { + for (const element of this._elements) { + if (element.isPage && element !== trigger) { element.deactivate(); } } @@ -251,8 +275,8 @@ var tour = new function() { /** * Initialize activation of PageSet. */ - init_activation(restore_old_state=false) { - tour.debug("PageSet.init_activation tour element '" + this.full_name + "'."); + init_activation(restore_old_state = false) { + logger.debug("PageSet.init_activation tour element '" + this.full_name + "'."); if (restore_old_state) { if (this.old_state_active) { this.activate(); @@ -264,7 +288,7 @@ var tour = new function() { deactivate() { if (this.active) { - tour.info("PageSet.deactivate tour element '" + this.full_name + "'."); + logger.info("PageSet.deactivate tour element '" + this.full_name + "'."); this.old_state_active = false; this._deactivate(); } @@ -272,7 +296,7 @@ var tour = new function() { _deactivate() { if (this.active) { - tour.debug("PageSet._deactivate tour element '" + this.full_name + "'."); + logger.debug("PageSet._deactivate tour element '" + this.full_name + "'."); this.active = false; this.unhighlight_menu_entry(); @@ -286,57 +310,288 @@ var tour = new function() { } } + get_previous_tour_page(id) { + return this.parent_set.get_previous_tour_page(id); + } + + get_next_tour_page(id) { + return this.parent_set.get_next_tour_page(id); + } + + get_tour_page_by_id(id) { + return this.parent_set.get_tour_page_by_id(id) + } + } this.Page = class { - constructor(parent_set, config) { - if(typeof parent_set === "undefined") { + constructor(parent_set, config, idx) { + if (typeof parent_set === "undefined") { throw new Error("param `parent_set` must not be undefined"); } - if(typeof config === "undefined") { + if (typeof config === "undefined") { throw new Error("param `config` must not be undefined"); } - this.active = false; - this.parent_set = parent_set; this.config = config; - this.button = this._create_tour_button(config.page, config.content, config.title, config.id, config.button_size, config.css); + this.isPage = true; + this.initialized = false; - this._activate_button(this.button, config.activation, config.deactivation); - this._position_button(this.button, config.target, config.button_position); - if (config.button_css) { - for (let key in config.button_css) { - this.button.style.setProperty(key, config.button_css[key]); - } + this.parent_set = parent_set; + this.active = false; + if (typeof this.config.href === "undefined") { + this.config.href = "/"; + } + if (typeof this.config.id === "undefined") { + this.config.id = (this.parent_set.id + "-p" + idx).replace(/ /g, ""); + } + if (typeof this.config.show_button === "undefined") { + this.config.show_button = false; + } + this.id = config.id + + // the argument button_size currently deactivated + this.button = this._create_tour_button(config.page, config.content, config.title, config.id, config.button_size, this, config.css); + if (! this.config.show_button){ + $(this.button).hide() } - this._apply_highlighters(this.button, config.highlighters); - this._apply_activation_links(this.button); - this._apply_hiding(this.button); + this._init_on_trigger(this.button, config); + this._deinit_on_trigger(this.button, config); + + this._setup_activation_listeners(this.button, this.config.activation, this.config.deactivation); // set some defaults if (typeof this.config.active === "undefined") { - this.config.active = true; + this.config.active = false; } if (typeof this.config.old_state_active === "undefined") { this.config.old_state_active = this.config.active; } } + _on_popover_open() { + caosdb_utils.assert_not_undefined(this.popover, "this.popover"); + caosdb_utils.assert_not_undefined(this.popover.tip, "this.popover.tip"); + this._scroll_into_view(this.popover.tip); + + // initialize close button + const cb = $(this.popover.tip).find(".caosdb-f-tour-popover-close-button") + .on("click", (e) => { + this.get_page_popover().hide(); + }); + + // TODO move to styling/popover_template + if (this.config.detour){ + const head = $(this.popover.tip).find(".popover-header") + head.toggleClass("bg-warning", true); + } + + // initialize detour button + $(this.popover.tip).find('a[data-detour]').each((idx, element) => { + const detour_start_page_id = $(element).data("detour"); + const detour_page = this.get_tour_page_by_id( + detour_start_page_id); + $(element) + .attr("href", detour_page.config.href+`#${detour_start_page_id}`) + .toggleClass( + ["btn", "btn-sm", "btn-warning", "fw-bold"], true); + $(element).click((e) =>{ + this._activate_by_id(detour_start_page_id); + const id_anchor = `#${detour_start_page_id}`; + if ($(id_anchor).length > 0) { + detour_page._open(); + return false; + } else { + sessionStorage["tour-page-open-next"] = detour_start_page_id; + // follow link + } + }); + }); + + // initialize next/prev buttons + var nb = $(this.popover.tip).find("button[data-role=next]") + if (this.get_next()) { + if (this.config.force_manual_action){ + nb.toggleClass("disabled", true); + nb.parent().attr("title","Manual action required!") + } else { + nb.on("click", (e) => { + const pn = this.get_next() + pn.activate(true); + if ($("#" + pn.config.id).length == 0) { + sessionStorage["tour-page-open-next"] = pn.config.id; + window.location = pn.config.href + } else { + pn._open(); + } + }); + } + } else { + nb.toggleClass("invisible", true); + } + var pb = $(this.popover.tip).find("button[data-role=prev]") + if (this.get_previous()) { + pb.on("click", (e) => { + const pp = this.get_previous() + pp.activate(true); + if ($("#" + pp.config.id).length == 0) { + sessionStorage["tour-page-open-next"] = pp.config.id; + window.location = pp.config.href + } else { + pp._open(); + } + }); + } else { + pb.toggleClass("invisible", true); + } + } + + _init_on_trigger(button, config) { + const id = this.config.id; + // Place in the dom tree at a temporary position. + // This is necessary because otherwise any referencing tour + // page would not find this button during the initialization of + // the listeners. + $(document.body).append(button); + $(button).hide(); + + if (config.init) { + + // now set up the triggering of the initialization + const ev = config.init["event"]; + const always = config.init["always"] + + var final_target = this._get_body_or_target(config.init["target"]) + var call_init = () => { + if (!always) { + logger.debug("remove init event handler", ev, final_target); + final_target.removeEventListener(ev, call_init, true); + } + this._init(button, config); + if (this.config.init["open"]) { + // open immediately + sessionStorage["tour-page-open-next"] = this.config.id; + } + if (this.active && sessionStorage["tour-page-open-next"] == this.config.id) { + if (this.config.show_button){ + $(this.button).show(); + } + this._open(); + }; + }; + + logger.debug("add init event handler", ev, call_init, final_target); + final_target.addEventListener(ev, call_init, true); + + } else { // trigger immediately + this._init(button, config); + } + } + + _deinit_on_trigger(button, config){ + if (config.deinit) { + // now set up the triggering of the deinitialization + const ev = config.deinit["event"]; + + const final_target = this._get_body_or_target(config.deinit["target"]) + const call_init = () => { + this._deinit(button, config); + $(this.button).hide(); + if (this.popover){ + this.popover.hide(); + } + }; + + logger.debug("add init event handler", ev, call_init, final_target); + final_target.addEventListener(ev, call_init, true); + } + } + + /* + * returns the document body if target is undefined, HTMLElement of + * target otherwise + */ + _get_body_or_target(target){ + if (typeof target == "undefined") { + return document.body; + } + + const final_target = $(target); + if (final_target.length < 1) { + throw new Error("could not find the target"); + } + return final_target[0]; + } + + /** + * Close popover and set page to initialized=false + */ + _deinit(button, config) { + this.initialized = false; + } + + /** + * Intialize the button and the popover of this page. + * + * This means positioning it on the page, adding all necessary event + * listeners, and more. + **/ + _init(button, config) { + logger.trace("enter Page._init", this.id, button, config); + this._position_button(button, config.target, config.button_position); + if (config.button_css) { + for (let key in config.button_css) { + button.style.setProperty(key, + config.button_css[key]); + } + } + this._apply_highlighters(button, config.highlighters); + this._apply_hiding(button); + this.initialized = true; + } + _activate_by_id(id) { this.parent_set._activate_by_id(id); } + /** create_menu_enrty for a Page */ create_menu_entry() { - var entry = $("<li class='list-group-item caosdb-f-tour-overview-entry caosdb-v-tour-overview-entry-page' />"); - var link = $("<a class='btn btn-link'>" + this.name + "</a>")[0]; - link.addEventListener("click", () => {this.activate();}); + const id_anchor = `#${this.config.id}`; + + if (this.config.do_not_show_in_toc) { + return; + } + var classes = "" + if (this.config.detour){ + classes = " caosdb-v-tour-toc-detour" + } + if (sessionStorage["tour-page-open-cur"] == this.config.id) { + classes = classes + " caosdb-v-tour-toc-cur"; + } + var entry = $("<li class='caosdb-f-tour-overview-entry caosdb-v-tour-overview-entry-page" + classes + "' />"); + var link = $("<a href='" + this.config.href + id_anchor + "'>" + this.name + "</a>")[0]; + + $(link).click((e) => { + this.activate(true); + + if ($(id_anchor).length > 0) { + this._open(); + return false; + } else { + sessionStorage["tour-page-open-next"] = this.config.id; + // follow link + } + + }); entry.append(link); + + this.menu_entry = entry[0]; return entry[0]; } set old_state_active(value) { - if(this.config.old_state_active != value) { + if (this.config.old_state_active != value) { this.config.old_state_active = value; this.update(); } @@ -358,33 +613,194 @@ var tour = new function() { return this.parent_set.full_name + ":" + this.name; } - init_activation(restore_old_state=false) { - tour.debug("Page.init_activation button '" + this.full_name + "'."); + /* + * Page init_activation + */ + init_activation(restore_old_state = false) { + logger.debug("Page.init_activation button '" + this.full_name + "'."); if (restore_old_state) { if (this.old_state_active) { - this.activate(); + this.activate(false); } } else if (this.config.active) { - this.activate(); + this.activate(false); } } - activate() { + _highlight_menu() { + $(".caosdb-v-tour-toc-active-item").toggleClass("caosdb-v-tour-toc-active-item", false); + $(".caosdb-v-tour-toc-cur").toggleClass("caosdb-v-tour-toc-cur", false); + $(this.menu_entry).toggleClass("caosdb-v-tour-toc-active-item", true); + } + + _unhighlight_menu() { + $(this.menu_entry).toggleClass("caosdb-v-tour-toc-active-item", false); + } + + _close_other() { + // hide all other popover, close all other tour pages. + const tour_inst = this.parent_set.parent_set + for (var index in tour_inst._elements_by_id ){ + const page = tour_inst._elements_by_id[index] + if (page.popover){ + page.popover.hide(); + } + if (page.button){ + // TODO close only when f-tour-open-page class is there? + $(page.button).toggleClass("caosdb-f-tour-open-page", false) + page.button.dispatchEvent(tour.close_page_event); + } + } + } + + get_page_popover() { + const target = $(this.config["target"])[0] + if (!this.popover) { + // not initialized yet. + this.popover = new bootstrap.Popover(target, this.popover_options); + + const events = [ + "ext_bottom_line.preview.ready", + "caosdb.preview.ready", + "caosdb.preview.show", + "caosdb.preview.hide", + "shown.bs.collapse", + "hidden.bs.collapse", + ]; + for (let ev of events) { + document.body.addEventListener(ev, () => { + this.popover.update(); + }, true); + } + target.addEventListener("shown.bs.popover", (e) => { + if (this.active){ + this._on_popover_open(); + this.button.dispatchEvent(tour.open_page_event); + } + + }); + target.addEventListener("hidden.bs.popover", (e) => { + $(this.button).toggleClass("caosdb-f-tour-open-page", false) + }); + + } + return this.popover; + } + + _before_open() { + if (this.config["before_open"]) { + const f = new Function(this.config["before_open"]); + f(); + } + } + + _open() { + this._before_open(); + const button = $(this.button); + const target = $(this.config["target"]) + sessionStorage["tour-page-open-cur"] = this.config.id; + if (button.is(".caosdb-f-tour-open-page")) { + return; // already open + } + if (! target[0]) { + return; // element to attach to not available; can't open + } + if (this.get_next()) { + if ($(`#${this.get_next().config.id}`).length == 0) { + logger.debug(`Set next page in session store to `, this.get_next().config.id); + sessionStorage["tour-page-open-next"] = this.get_next().config.id + } + } + target.on('hidden.bs.popover', (e) => { + button[0].dispatchEvent(tour.close_page_event); + }); + this._close_other(); + + // open this one + const popover = this.get_page_popover(); + button.toggleClass("caosdb-f-tour-open-page", true); + popover.show(); + + this._highlight_menu(); + } + + _scroll_into_view(popover) { + if (typeof popover == "undefined") { + return; + } + const box = popover.getBoundingClientRect(); + const viewport_height = window.innerHeight || document.documentElement.clientHeight; + var yscroll = 0; + + if (box.bottom > viewport_height) { + // element's bottom is hidden down there + // align top of popover with top of viewport or bottom of + // popover with bottom of viewport, whichever involves the + // least scrolling. + yscroll = box.bottom - viewport_height; + } + + if (box.top < yscroll) { + // element's top is hidden up there + // align top of popover with top of viewport + yscroll += box.top + } + + window.scrollBy(0, yscroll); + } + + _close() { + logger.debug(`Page.close ${this.config.id}`); + $(this.button) + .toggleClass("caosdb-f-tour-open-page", false) + if (this.popover) { + this.popover.hide(); + } + this._unhighlight_menu(); + } + + /* + * Page activate + */ + activate(deactivate_others) { + if (deactivate_others) { + this.parent_set.deactivate_other(this) + } if (!this.active) { - tour.info("Page.activate button '" + this.full_name + "'."); + logger.info("Page.activate button '" + this.full_name + "'."); this.old_state_active = true; this.active = true; + // TODO Why is the following line necessary? "References" + // Chapter of files does not open without it (when the + // "References" is used to get to the correct page) + if (this.config.show_button){ + $(this.button).show(); + } + // activation propagates to the parents this.parent_set._activate() + if (this.initialized && sessionStorage["tour-page-open-next"] == this.config.id) { + if ($(this.config["target"])[0]) { + this._open(); + } else { + tour._post_init_cb.push(() => { + this._open(); + }); + } + } + // Set the natural "next" as next tour page in session store as + // default. This value will be overwritten by navigation. + } + if (this.config.show_button){ $(this.button).show(); } } deactivate() { if (this.active) { - tour.info("Page.deactivate button '" + this.full_name + "'."); + logger.info("Page.deactivate button '" + this.full_name + "'."); this.old_state_active = false; this._deactivate(); @@ -394,47 +810,63 @@ var tour = new function() { _deactivate() { if (this.active) { - tour.debug("Page._deactivate button '" + this.full_name + "'."); + logger.debug("Page._deactivate button '" + this.full_name + "'."); this.active = false; + this._close(); $(this.button).hide(); - $(this.button).popover("hide"); $(this.button).toggleClass("caosdb-f-tour-open-page", false); } } + _tour_active () { + return this.parent_set._tour_active(); + } + /** * Hook the button to (de)activation events. */ - _activate_button(button, activation, deactivation) { - $(button).hide(); + _setup_activation_listeners(button, activation, deactivation) { + //$(button).hide(); if (typeof activation !== "undefined") { activation = tour.assert_array(activation); activation.forEach( (act) => { - if (act === null) { return; } + if (act === null) { + return; + } tour._post_init_cb.push(() => { var target = $(act.target)[0]; if (target) { + logger.debug("Added listener:", target, act.event) target.addEventListener( act.event, - (e) => {this.activate();} + (e) => { + if (this._tour_active()) { + logger.debug("Activate", this.config.id, "due to ", act.event) + this.activate(); + } + } ); } }); } ) } - if (typeof deactivation !== "undefined" ) { + if (typeof deactivation !== "undefined") { deactivation = tour.assert_array(deactivation); deactivation.forEach( (deact) => { - if (deact === null) { return; } + if (deact === null) { + return; + } tour._post_init_cb.push(() => { var target = $(deact.target)[0]; if (target) { target.addEventListener( deact.event, - (e) => {this.deactivate();} + (e) => { + this.deactivate(); + } ); } }); @@ -446,8 +878,9 @@ var tour = new function() { _position_button(button, target, position) { var sel = $(target).first() - if (typeof sel.css("position") === "undefined" - || sel.css("position") === "static") { + logger.debug("positioning button", button, target, sel, position); + if (typeof sel.css("position") === "undefined" || + sel.css("position") === "static") { sel.css("position", "relative"); } @@ -457,111 +890,118 @@ var tour = new function() { $(button).css("position", "absolute"); switch (position) { - case "top": - sel.prepend(wrapper); - $(button).css("top", - Math.abs($(button).outerHeight()) / 2); - //$(button).css("left", "50%"); - break; - case "top-right": - sel.prepend(wrapper); - $(button).css("top", - Math.abs($(button).outerHeight()) / 2); - $(button).css("right", - Math.abs($(button).outerWidth()) / 2); - break; - case "right": - wrapper.css("top", "50%"); - wrapper.css("right", sel.css("padding-right")); - wrapper.css("position", "absolute"); - sel.prepend(wrapper); - $(button).css("right", - Math.abs($(button).outerWidth()) / 2); - $(button).css("top", - Math.abs($(button).outerHeight()) / 2); - break; - case "bottom-right": - sel.append(wrapper); - $(button).css("bottom", - Math.abs($(button).outerHeight()) / 2); - $(button).css("right", - Math.abs($(button).outerWidth()) / 2); - break; - case "bottom": - wrapper.css("top", "100%"); - wrapper.css("left", "50%"); - wrapper.css("position", "absolute"); - sel.append(wrapper); - $(button).css("margin-top", "5px"); - $(button).css("top", 0); - $(button).css("left", 0); - $(button).css("transform", "translate(-50%, 0)"); - break; - case "bottom-left": - sel.append(wrapper); - $(button).css("bottom", - Math.abs($(button).outerHeight()) / 2); - $(button).css("left", - Math.abs($(button).outerWidth()) / 2); - break; - case "left": - wrapper.css("top", "50%"); - wrapper.css("position", "absolute"); - sel.prepend(wrapper); - $(button).css("margin-right", "5px"); - $(button).css("top", 0); - $(button).css("right", 0); - $(button).css("transform", "translate(0, -50%)"); - break; - default: - // top-left - sel.prepend(wrapper); - $(button).css("top", - Math.abs($(button).outerHeight()) / 2); - $(button).css("left", - Math.abs($(button).outerWidth()) / 2); - break; + case "top": + sel.prepend(wrapper); + $(button).css("top", -Math.abs($(button).outerHeight()) / 2); + //$(button).css("left", "50%"); + break; + case "top-right": + sel.prepend(wrapper); + $(button).css("top", -Math.abs($(button).outerHeight()) / 2); + $(button).css("right", -Math.abs($(button).outerWidth()) / 2); + break; + case "right": + wrapper.css("top", "50%"); + wrapper.css("right", sel.css("padding-right")); + wrapper.css("position", "absolute"); + sel.prepend(wrapper); + $(button).css("right", -Math.abs($(button).outerWidth()) / 2); + $(button).css("top", -Math.abs($(button).outerHeight()) / 2); + break; + case "bottom-right": + sel.append(wrapper); + $(button).css("bottom", -Math.abs($(button).outerHeight()) / 2); + $(button).css("right", -Math.abs($(button).outerWidth()) / 2); + break; + case "bottom": + wrapper.css("top", "100%"); + wrapper.css("left", "50%"); + wrapper.css("position", "absolute"); + sel.append(wrapper); + $(button).css("margin-top", "5px"); + $(button).css("top", 0); + $(button).css("left", 0); + $(button).css("transform", "translate(-50%, 0)"); + break; + case "bottom-left": + sel.append(wrapper); + $(button).css("bottom", -Math.abs($(button).outerHeight()) / 2); + $(button).css("left", -Math.abs($(button).outerWidth()) / 2); + break; + case "left": + wrapper.css("top", "50%"); + wrapper.css("position", "absolute"); + sel.prepend(wrapper); + $(button).css("margin-right", "5px"); + $(button).css("top", 0); + $(button).css("right", 0); + $(button).css("transform", "translate(0, -50%)"); + break; + default: + // top-left + sel.prepend(wrapper); + $(button).css("top", -Math.abs($(button).outerHeight()) / 2); + $(button).css("left", -Math.abs($(button).outerWidth()) / 2); + break; } + // initially hide the button + $(button).hide(); } _apply_highlighter(highlighter, highlightable) { $(highlighter).hover( - ()=>{ + () => { highlightable.toggleClass("caosdb-v-tour-highlight", true); }, - ()=>{ + () => { highlightable.toggleClass("caosdb-v-tour-highlight", false); } ); } - /** - * Hook click events for `data-tour-activate` <a> elements. - */ - _apply_activation_links(button) { - $(button).on('shown.bs.popover', (e) => { - $('a[data-tour-activate]').each((idx, element) => { - element.addEventListener("click", () => { - this._activate_by_id($(element).data("tour-activate")); - }); - }); - }); - } - _apply_hiding(button) { $(button).on('shown.bs.popover', (e) => { let hiding = this.config.on_activation_hide; if (hiding) { - console.log("Tour page hiding:"); + logger.debug("Tour page hiding:"); hiding = tour.assert_array(hiding); hiding.forEach((selector) => { - console.log(selector); $(selector).addClass("caosdb-f-tour-hidden"); }); } let unhiding = this.config.on_activation_unhide; if (unhiding) { - console.log("Tour page unhiding:"); + logger.debug("Tour page unhiding:"); unhiding = tour.assert_array(unhiding); unhiding.forEach((selector) => { - console.log(selector); $(selector).removeClass("caosdb-f-tour-hidden"); }); } }) } + get_previous() { + if (this.config.previous){ + return this.parent_set.parent_set._elements_by_id[this.config.previous]; + } else { + return this.parent_set.get_previous_tour_page(this.id); + } + } + + get_next() { + if (this.config.next){ + return this.parent_set.parent_set._elements_by_id[this.config.next]; + } else { + return this.parent_set.get_next_tour_page(this.id); + } + } + + get_tour_page_by_id(id) { + return this.parent_set.get_tour_page_by_id(id) + } + _apply_highlighters(button, highlighters) { if (typeof button === "undefined") { throw new Error("button was undefined"); @@ -581,17 +1021,18 @@ var tour = new function() { this._apply_highlighter(button, highlightable[id]); } else { $(button).on('shown.bs.popover', (e) => { - console.log("Highlighting:") - console.log(highlighters[id]); - console.log(highlightable[id]); + // TODO check if this popover is already initialized + logger.debug("Highlighting:", + highlighters[id], highlightable[id]); this._apply_highlighter("#" + id, highlightable[id]); }); } } } - _create_tour_button(name, content, title, id, size, css={}, - placement="auto") { + // currently deactivated + _create_tour_button(name, content, title, id, size, page, css = {}, + placement = "auto") { if (typeof name === "undefined") { throw new Error("name was undefined"); } @@ -599,10 +1040,10 @@ var tour = new function() { throw new Error("content was undefined"); } - var button = $('<button class="caosdb-v-tour-button">' + name + '</button>'); + var button = $('<button class="caosdb-v-tour-button"></button>'); var markdown_content = tour.markdown_to_html(content); - tour.debug(markdown_content); + //logger.debug("Page's markdown content: ", markdown_content); // Apply custom style immediately at popover creation. // Alternatively, we could create a custom style element which sets @@ -612,75 +1053,75 @@ var tour = new function() { css["max-width"] = "120em"; } if (!("width" in css)) { - css["width"] = "50em"; + css["width"] = "30em"; } - // console.log(css) let popover_style = ""; for (let key in css) { popover_style += key + ": " + css[key] + "; "; } - // Bootstrap 3.x: .popover-content, 4.x: .popover-body - let popover_template = '<div class="popover" role="tooltip" style="' - + popover_style - + '"><div class="arrow"></div><h3 class="popover-header"></h3>' - + '<div class="popover-body popover-content"></div></div>'; + let popover_template = '<div class="popover" role="tooltip" style="z-index:20000; ' + + popover_style + + '"><div class="popover-arrow"></div><button class="btn btn-close caosdb-f-tour-popover-close-button caosdb-v-tour-popover-close-button"></button><h3 class="popover-header"></h3><div class="popover-body popover-content"></div><div class="p-3 pt-0 d-flex justify-content-between" ><span><button class="btn btn-sm btn-secondary caosdb-v-tour-pn-btn me-auto" data-role="prev">Previous</button></span><span><button class="btn btn-sm btn-secondary caosdb-v-tour-pn-btn" data-role="next">Next</button></span></div></div>'; - button.popover({ + button.attr("title", title); + + if (typeof title === "undefined") { + title = "" + }; + this.popover_options = { title: title, content: markdown_content, + container: "body", placement: placement, html: true, + sanitize: false, trigger: 'manual', template: popover_template, - }); + }; - button.on("click", function(e) { - if($(this).hasClass("caosdb-f-tour-open-page")) { - this.dispatchEvent(tour.close_page_event); + button.on("click", (e) => { + if (button.hasClass("caosdb-f-tour-open-page")) { + this._close(); } else { - this.dispatchEvent(tour.open_page_event); + this._open(); } - $(this).toggleClass("caosdb-f-tour-open-page"); // clicks on the tour button should not trigger any other action e.preventDefault(); e.stopPropagation(); }); - button[0].addEventListener("close.tour.page", function() { - button.popover("hide"); - }); - - button[0].addEventListener("open.tour.page", function() { - var p_id = button.popover("show")[0].getAttribute("aria-describedby"); - var popover = $("#" + p_id); - - // hide all other popovers - $(".caosdb-v-tour-button").each(function(index) { - if (this != button[0]) { - $(this).popover("hide"); - } - }); - }); - if(id) { + if (id) { button.attr("id", id); } + /* currently deactivated if(size) $(button).toggleClass(size, true); + */ return button[0]; } } this.config = undefined; + this.popover_options = {}; this.close_page_event = new Event("close.tour.page"); this.open_page_event = new Event("open.tour.page"); this._post_init_cb = [] - this.post_init = function() { + /** + * Post init is called after the initialization. + * + * It is mainly used to initialize event listeners which needed to wait + * until all the tour buttons are actually present in the DOC tree. + * + * All functions which need to be called after the intialization of the + * tour may be appended to the {@link tour#_post_init_cb} array. + */ + this.post_init = function () { for (const fn of this._post_init_cb) { fn(); } @@ -699,13 +1140,14 @@ var tour = new function() { /** * Initialize the tour. * - * The `refresh` argument is currently only used interactively on the debugging console. + * The `refresh` argument is currently only used interactively on the + * debugging console. */ this.init = async function _in(refresh) { try { - tour.debug("initializing tour module, refresh: " + refresh); + logger.debug("initializing tour module, refresh: " + refresh); if (refresh === true) { - tour.info("Refreshing tour state."); + logger.info("Refreshing tour state."); localStorage.removeItem("tour_state"); } await tour.load_tour(); @@ -720,61 +1162,80 @@ var tour = new function() { try { var old_state = JSON.parse(localStorage.getItem("tour_state")); if (old_state) { - config = {tour: old_state} + config = { + tour: old_state + } }; } catch (error) { if (error instanceof SyntaxError) { - tour.warning("Parsing old tour state failed with SyntaxError. Old tour state: '" + localStorage.getItem("tour_state") + "'."); + logger.warning("Parsing old tour state failed with SyntaxError. Old tour state: '" + localStorage.getItem("tour_state") + "'."); } else { globalError(error); } } - if(!config || config.length == 0 || config.tour.length == 0) { - tour.info("No old tour state in the localStorage."); + + // reset if build number changed + if (config && config.tour && config.tour._build_number != "${BUILD_NUMBER}") { + config = undefined; + } + if (!config || config.length == 0 || config.tour.length == 0) { + logger.info("No old tour state in the localStorage."); // try to fetch config = await load_config("tour.json"); - console.log("Loaded tour.json:"); - console.log(config); + logger.debug("Loaded tour.json", config); } if (!config || config.length == 0 || config.tour.length == 0) { localStorage.setItem("tour_state", "[]"); - tour.info("Tour config is empty."); + logger.info("Tour config is empty."); return; } + // store build number in tour config + config.tour._build_number = "${BUILD_NUMBER}"; tour.configure(config.tour); } - - - - this.add_tour_element = function(element, parent_set) { + this.add_tour_element = function (element, parent_set, idx) { if (element.page_set) { // it's a page_set - return tour.add_tour_page_set(element, parent_set); + return tour.add_tour_page_set(element, parent_set, idx); + } else if (typeof element.separator != "undefined") { + // it's a separator + return tour.add_tour_menu_separator(); } else { // it's a page - return tour.add_tour_page(element, parent_set); + return tour.add_tour_page(element, parent_set, idx); } } + this.add_tour_page_set = function (config, parent_set, idx) { + return new tour.PageSet(parent_set, config, idx); + } - this.add_tour_page_set = function(config, parent_set) { - return new tour.PageSet(parent_set, config); + this.add_tour_menu_separator = function (element) { + return { + create_menu_entry: () => $("<hr>")[0], + init_activation: function() {}, + _deactivate: function() {}, + } } this.Tour = class { constructor(config) { this.full_name = "Tour"; + this.id = config.id || "tour0"; this.config = config; this.elements = new Array(); - this.leave_tour_button = $('<a class="btn btn-link">Leave Tour</a>'); - this.leave_tour_button.hide(); - this.reset_tour_button = $('<a class="btn btn-link">Reset Tour</a>'); - this.reset_tour_button.hide(); this.active = false; + + var menuitem = $('<li class="nav-item" id="caosdb-navbar-tour"><a href="#" class="d-none nav-link caosdb-f-start-tour-btn" title="Start a Tour">Tour</a><a href="#" title="Leave the Tour" class="d-none caosdb-f-leave-tour-btn nav-link">Tour</a></li>') + $(".caosdb-navbar").append(menuitem); + + var min_width_warning = $('<div class="alert alert-warning caosdb-tour-min-width-warning d-lg-none" role="alert"><strong>Warning</strong> This tour is optimized for screens wider than 992px. If you have trouble displaying elements of this tour, please try accessing it on a larger screen.</div>'); + $(".navbar").append(min_width_warning); + for (const element of this.config.elements) { - const next = tour.add_tour_element(element, this); + const next = tour.add_tour_element(element, this, this.elements.length); this.elements.push(next); } @@ -790,37 +1251,96 @@ var tour = new function() { this.config.deactivate_other = true; } + $(".caosdb-f-leave-tour-btn").click((e) => { + this.deactivate(); + }); + $(".caosdb-f-start-tour-btn").click((e) => { + this.activate(); + }); + this.update(); - this.panel = this.create_tour_overview_panel(); - this.init_activation(this.config.persistent_state); + this.create_tour_overview_panel(); this._elements_by_id = {}; - this._index_elements(this._elements_by_id, this); + this._tour_pages = []; + this._index_elements(this._elements_by_id, this._tour_pages, this); + this._toggle_tour_buttons(); + this._toggle_width_warning(); + } + + _toggle_tour_buttons() { + $(".caosdb-f-leave-tour-btn").toggleClass("d-none", !this.active); + $(".caosdb-f-start-tour-btn").toggleClass("d-none", this.active); + } + + _toggle_width_warning() { + $(".caosdb-tour-min-width-warning").toggleClass("d-block", this.active); + $(".caosdb-tour-min-width-warning").toggleClass("d-none", !this.active); + } + + _tour_active () { + return this.active; + } + + /** + * @param {string} id + * @return {tour.Page} + */ + get_next_tour_page(id) { + const index_old = this._tour_pages.indexOf(id); + if (index_old < 0) { + throw new Error("Tour page not in _tour_pages list"); + } + const index_new = index_old + 1; + if (index_new >= this._tour_pages.length) { + return null; + } + return this._elements_by_id[this._tour_pages[index_new]]; + } + get_tour_page_by_id(id) { + return this._elements_by_id[id]; } + /** + * @param {string} id + * @return {tour.Page} + */ + get_previous_tour_page(id) { + const index_old = this._tour_pages.indexOf(id); + if (index_old < 0) { + throw new Error("Tour page not in _tour_pages list"); + } + const index_new = index_old - 1; + if (index_new < 0) { + return null; + } + return this._elements_by_id[this._tour_pages[index_new]]; + } - _index_elements(index, element) { - if(element.elements) { + _index_elements(index, pages, element) { + if (element.elements) { for (const sub of element.elements) { - this._index_elements(index, sub); - if (sub.config.id) { - index[sub.config.id] = sub; + this._index_elements(index, pages, sub); + if (sub.id) { + index[sub.id] = sub; + } + if (sub.isPage) { + pages.push(sub.id); } } } } - _activate_by_id(id) { var element = this._elements_by_id[id]; if (element) { - element.activate(); + element.activate(true); } } set old_state_active(value) { - if(this.config.old_state_active != value) { + if (this.config.old_state_active != value) { this.config.old_state_active = value; this.update(); } @@ -833,8 +1353,8 @@ var tour = new function() { /** * Start tour activation. */ - init_activation(restore_old_state=false) { - tour.debug("Tour.init_activation '" + this.full_name + "'."); + init_activation(restore_old_state = false) { + logger.debug("Tour.init_activation '" + this.full_name + "'."); if (restore_old_state) { if (this.old_state_active) { this.activate(); @@ -845,55 +1365,63 @@ var tour = new function() { } deactivate() { - tour.info("Tour.deactivate tour"); + logger.info("Tour.deactivate tour"); this._deactivate(); // deactivation propagates to the children for (const element of this.elements) { element._deactivate(); } + + sessionStorage.removeItem("tour-page-open-next") } /** * Reset the tour state. Mainly useful for development and debugging. */ async reset_tour() { - // console.log("Resetting the tour"); + logger.info("Resetting the tour"); + sessionStorage.removeItem("tour-page-open-next") this.deactivate(); localStorage.removeItem("tour_state"); var config = await load_config("tour.json") if (!config || config.length == 0 || config.tour.length == 0) { localStorage.setItem("tour_state", "[]"); - tour.info("Tour config is empty."); + logger.info("Tour config is empty."); } else { tour.configure(config.tour); } - // A bit ugly, but the reset button will be removed or completely - // changed for production anyways. location.reload(); } _deactivate() { - if(this.active) { - this.set_tour_button_text("Start A Tour"); - tour.debug("Tour._deactivate tour"); + if (this.active) { + logger.debug("Tour._deactivate tour"); this.old_state_active = false; this.active = false; - this.leave_tour_button.hide(); - this.reset_tour_button.hide(); + this._hide_tour_sidebar(); + this._toggle_tour_buttons(); + this._toggle_width_warning(); this.update(); } } + _hide_tour_sidebar() { + $("body").toggleClass("tour-sidebar-visible", false); + } + + _show_tour_sidebar() { + $("body").toggleClass("tour-sidebar-visible", true); + } _activate() { - if(!this.active) { - this.set_tour_button_text("Tour"); - tour.debug("Tour._activate tour"); + if (!this.active) { + logger.debug("Tour._activate tour"); this.old_state_active = true; this.active = true; - this.leave_tour_button.show(); - this.reset_tour_button.show(); + this._show_tour_sidebar(); + this._toggle_tour_buttons(); + this._toggle_width_warning(); this.update(); } } @@ -902,7 +1430,7 @@ var tour = new function() { * Activate tour: initialize activation of elements. */ activate() { - tour.info("Tour.activate tour"); + logger.info("Tour.activate tour"); this._activate(); for (const element of this.elements) { @@ -912,38 +1440,25 @@ var tour = new function() { } deactivate_other(trigger) { - if(this.config.deactivate_other) { + if (this.config.deactivate_other) { + logger.debug("Close pagesets other than '" + trigger.id + "'."); for (const element of this.elements) { - if(element instanceof tour.PageSet && element !== trigger) { + if (element.isPageSet && element !== trigger) { element.deactivate(); } } } } - set_tour_button_text(text) { - $("button.caosdb-f-tour-button").text(text); - } - - create_tour_overview_panel() { - var panel = $('<div class="collapse" id="caosdb-f-tour-overview-panel" /></div>'); - var tour_overview = $('<ul class="list-inline caosdb-v-tour-overview"/>'); + var tour_overview = $('<ul class="list-unstyled caosdb-v-tour-overview"/>'); for (const element of this.elements) { const next = element.create_menu_entry(); - tour_overview.append(next); + if (next) { + tour_overview.append(next); + } } - - panel.hover(undefined, ()=>{panel.collapse('hide');}); - - panel.append(tour_overview); - - this.leave_tour_button.on("click", () => {this.deactivate();}); - this.reset_tour_button.on("click", () => {this.reset_tour();}); - panel.append(this.leave_tour_button); - panel.append(this.reset_tour_button); - return panel[0]; - + $("#tour-toc .caosdb-f-tour-toc-body").empty().append(tour_overview); } update() { @@ -953,87 +1468,55 @@ var tour = new function() { } - this.configure = function(config) { - tour.info({"configure tour": config}); + this.configure = function (config) { + logger.info({ + "configure tour": config + }); // clean up old tour elements (after reload) $("#caosdb-f-tour-overview-panel").remove(); $("#caosdb-navbar-tour").remove(); $(".caosdb-f-tour-button-wrapper").remove(); - // new stuff following - var tour_button = $(` -<li id="caosdb-navbar-tour"> - <button class="navbar-btn btn btn-link caosdb-f-tour-button" data-toggle="collapse" data-target="#caosdb-f-tour-overview-panel"> - Start A Tour - </button> -</li>`); - $('.caosdb-navbar').append(tour_button[0]); - tour._instance = new tour.Tour(config); + // TODO how can this be done better? + $("#caosdb-navbar-tour>a").each(function (index) { + this.addEventListener("click", () => { + $(".caosdb-v-left-panel").toggleClass("invisible"); + }); + }) + const instance = new tour.Tour(config); if (config.reload) { - $(config.reload.target).each(function(index) { - this.addEventListener(config.reload.event, (e) => {tour.configure(tour._instance.config);}, true); + $(config.reload.target).each(function (index) { + this.addEventListener(config.reload.event, (e) => { + tour.configure(instance.config); + }, true); }); } - if(tour._instance.active) { - tour._instance.set_tour_button_text("Tour"); + if (typeof sessionStorage["tour-page-open-next"] === "undefined") { + const next = sessionStorage["tour-page-open-cur"] || instance._tour_pages[0]; + sessionStorage["tour-page-open-next"] = next } - $('#caosdb-query-panel').before(tour._instance.panel); - } + instance.init_activation(config.persistent_state); + } - /** - * Send a message of a certain level to the logger. - * - * @param {string} message - The message which is to be send to the logger. - * @param {integer} [level=TRACE] - The level from 1 (Error) to 5 (Trace). - */ - this.log = function(message, level=TRACE) { - if (level <= this.verbosity_level) { - var names = ["Error","Warning", "Info", "Debug", "Trace"]; - if (typeof message === "string" || message instanceof String) { - console.log({level: names[level-1], logger: "tour", message: message}); - } else { - console.log({level: names[level-1], logger: "tour", object: message}); - } - } + this.add_tour_page = function (config, parent_set, idx) { + return new tour.Page(parent_set, config, idx); } /** - * Send a debug message to the logger. - * - * @param {string} message - The message which is to be send to the logger. + * TODO replace with function of the markdown module. */ - this.debug = function(message) { - this.log(message, DEBUG); - } - - this.warning = function(message) { - this.log(message, WARNING); - } - - this.error = function(message) { - this.log(message, ERROR); - } - - this.info = function(message) { - this.log(message, INFO); - } - - this.add_tour_page = function(config, parent_set) { - return new tour.Page(parent_set, config); - } - - this.markdown_to_html = function(content) { + this.markdown_to_html = function (content) { let converter = new showdown.Converter(); return converter.makeHtml(content.trim()); } - this.assert_array = function(content) { - if (! Array.isArray(content)) { - return [content]; + this.assert_array = function (content) { + if (!Array.isArray(content)) { + return [content]; } return content; } @@ -1041,13 +1524,13 @@ var tour = new function() { /** * Calls server-side script `scriptname`. */ - this.run_script = async function(scriptname) { + this.run_script = async function (scriptname) { try { const script_result = await connection.runScript(scriptname); const retcode = script_result.getElementsByTagName("script")[0].getAttribute("code"); if (parseInt(retcode) > 0) { - throw ("An error occurred during execution of the server-side script:\n" - + script_result.getElementsByTagName("script")[0].outerHTML); + throw ("An error occurred during execution of the server-side script:\n" + + script_result.getElementsByTagName("script")[0].outerHTML); } } catch (e) { globalError(e); diff --git a/src/core/js/webcaosdb.js b/src/core/js/webcaosdb.js index df93703d83fafde15908efb40c3ae578824980d9..221b8b5a12da92395efcc4e52bfec0d7d6561ce9 100644 --- a/src/core/js/webcaosdb.js +++ b/src/core/js/webcaosdb.js @@ -116,12 +116,13 @@ this.navbar = new function () { } // wrapp button - let wrapper = $("<li></li>").append(button_elem); + let wrapper = $("<li class='nav-item'></li>").append(button_elem); // menu defaults to the navbar const menu = _options["menu"] || this.get_navbar(); + /* TODO is this still needed? if ($(menu).is("ul.caosdb-navbar")) { // special styling for buttons which are added directly to the // navbar @@ -130,6 +131,7 @@ this.navbar = new function () { .toggleClass("btn", true) .toggleClass("btn-link", true); } + */ logger.debug("add", wrapper, "to", menu); $(menu).append(wrapper); @@ -143,9 +145,73 @@ this.navbar = new function () { .on("shown.bs.collapse", function (e) { logger.trace("navbar expands", e); }) - .on("hidden.bs.collapse", function (e) { + .on("invisible.bs.collapse", function (e) { logger.trace("navbar shrinks", e); }); + this.init_login_show_button(); + } + + + /** + * Initialize the hiding/showing of the input form. + * + * If the viewport is xs (width <= 768px) the login form is hidden in the + * a menu anyways. + * + * If the viewport is greater then the folloging applies: + * + * When loading the page, the login form is hidden behind a "Login" button. + * When the user clicks the Login button the form appears and the other + * button disappears. After a timeout of 10 seconds of inactivity the form + * hides again. When the user starts typing, the timeout is canceled. + * + * If the user leaves the input fields ("blur" event) the timeout is + * reestablished and the form hides after 20 seconds. + */ + this.init_login_show_button = function () { + const form = $("#caosdb-f-login-form"); + const show_button = $("#caosdb-f-login-show-button"); + var timeout = undefined; + + // show form and hide the show_button + const _in = () => { + // xs means viewport <= 768px + // TODO is there a better option + form.removeClass("d-none"); + form.addClass("d-xs-inline-block"); + show_button.removeClass("d-inline-block"); + show_button.addClass("d-none"); + } + // hide form and show the show_button + const _out = () => { + // xs means viewport <= 768px + // TODO is there a better option + form.removeClass("d-xs-inline-block"); + form.addClass("d-none"); + show_button.removeClass("d-none"); + show_button.addClass("d-inline-block"); + } + show_button.on("click", () => { + // show form... + _in(); + + // and hide it after ten seconds if nothing happens + timeout = setTimeout(_out, 10000) + }); + form.find("input,button").on("blur", () => { + if (timeout) { + // cancel existing timeout (e.g. from showing) + clearTimeout(timeout); + } + // hide after 20 seconds if nothing happens + timeout = setTimeout(_out, 20000) + }); + form.find("input,button").on("input", () => { + // something happens! + if (timeout) { + clearTimeout(timeout); + } + }); } @@ -159,9 +225,9 @@ this.navbar = new function () { * @return {HTMLElement} the dropdown-menu. */ this.init_toolbox = function (name) { - var button = $(`<a class="dropdown-toggle" - data-toggle="dropdown" href="#">${name} - <span class="caret"></span></a>`)[0]; + var button = $(`<a class="nav-link dropdown-bs-toggle" + data-bs-toggle="dropdown" href="#">${name} + </a>`)[0]; var menu = $(`<ul class="caosdb-v-navbar-toolbox @@ -254,6 +320,13 @@ this.caosdb_utils = new function () { return obj; } + this.assert_not_undefined = function (obj, name) { + if (typeof obj == "undefined" || obj == null) { + throw new TypeError(name + " must not be undefined") + } + return obj; + } + this.assert_html_element = function (obj, name) { if (typeof obj === "undefined" || !(obj instanceof HTMLElement)) { throw new TypeError(name + " is expected to be an HTMLElement, was " + typeof obj); @@ -269,6 +342,30 @@ this.caosdb_utils = new function () { } throw new TypeError(name + " is expected to be an array, was " + typeof obj); } + + /** + * Create a tsv table as string. + * + * The data must be appropriately encoded (e.g. urlencoded). + * + * With `tab=","` it is also possible to create csv tables. + * + * @param {string[][]} data - An array of rows which contain arrays of + * cells. + * @param {string} [preamble="data:text/csv;charset=utf-8,"] - a prefix for + * the the resulting string. The default is suitable for creating + * downloadable href attributes of links. + * @param {string} [tab="%09"] - the cell separator. + * @param {string} [newline="%0A"] - the row separator. + * @return {string} a tsv table as a string. + */ + this.create_tsv_table = function (data, preamble, tab, newline) { + preamble = ((typeof preamble == 'undefined') ? "data:text/csv;charset=utf-8," : preamble); + tab = tab || "%09"; + newline = newline || "%0A"; + const rows = data.map(x => x.join(tab)) + return `${preamble}${rows.join(newline)}`; + } } /** @@ -533,7 +630,7 @@ this.transformation = new function () { * @return {XMLDocument} xslt script */ this.retrieveEntityXsl = async function _rEX(root_template) { - const _root = root_template || '<xsl:template match="/"><div class="root"><xsl:apply-templates select="Response/*" mode="entities"/></div></xsl:template>'; + const _root = root_template || '<xsl:template match="/" xmlns="http://www.w3.org/1999/xhtml"><div class="root"><xsl:apply-templates select="Response/*" mode="entities"/></div></xsl:template>'; var entityXsl = await transformation.retrieveXsltScript("entity.xsl"); var commonXsl = await transformation.retrieveXsltScript("common.xsl"); var errorXsl = await transformation.retrieveXsltScript('messages.xsl'); @@ -592,7 +689,8 @@ this.transaction = new function () { * @return {Element[]} array of xml elements. */ this.retrieveEntitiesById = async function _rEBIs(entityIds) { - return $(await connection.get(this.generateEntitiesUri(entityIds))).find('Response [id]').toArray(); + const response = await connection.get(this.generateEntitiesUri(entityIds)); + return $(response).find('Response > [id]').toArray(); } /** Sends a PUT request with an xml representation of entities and @@ -663,11 +761,11 @@ this.transaction = new function () { } // create the form element by element - let textarea = $('<div class="form-group"><textarea rows="8" style="width: 100%;" name="updateXml"/></div>'); + let textarea = $('<div class="form-control"><textarea rows="8" style="width: 100%;" name="updateXml"/></div>'); textarea.find('textarea').val(entityXmlStr); - let submitButton = $('<button class="btn btn-default" type="submit">Update</button>'); - let resetButton = $('<button class="btn btn-default" type="reset">Reset</button>'); - let form = $('<form class="panel-body"></form>'); + let submitButton = $('<button class="btn btn-secondary" type="submit">Update</button>'); + let resetButton = $('<button class="btn btn-secondary" type="reset">Reset</button>'); + let form = $('<form class="card-body"></form>'); form.toggleClass(transaction.classNameUpdateForm, true); form.append(textarea); form.append(submitButton); @@ -785,7 +883,7 @@ this.transaction = new function () { // if there is an <Error> tag in the response, show // the response in a new form. app.openForm(xml2str(xml)); - transaction.update.addErrorNotification($(updatePanel).find('.panel-heading'), transaction.update.createErrorInUpdatedEntityNotification()); + transaction.update.addErrorNotification($(updatePanel).find('.card-header'), transaction.update.createErrorInUpdatedEntityNotification()); } else { // if there are no errors show the XSL-transformed // updated entity. @@ -812,10 +910,10 @@ this.transaction = new function () { app.init(entity); app.updatePanel = updatePanel; - let closeButton = transaction.update.createCloseButton('.panel', () => { + let closeButton = transaction.update.createCloseButton('.card', () => { app.resetApp(); }); - $(updatePanel).find('.panel-heading').prepend(closeButton); + $(updatePanel).find('.card-header').prepend(closeButton); return app; } @@ -855,7 +953,7 @@ this.transaction = new function () { * @return {HTMLElement} A div. */ this.createUpdateEntityPanel = function (heading) { - let panel = $('<div class="panel panel-default" style="border-color: blue;"/>'); + let panel = $('<div class="card" style="border-color: blue;"/>'); panel.append(heading); return panel[0]; }; @@ -895,7 +993,7 @@ this.transaction = new function () { } this.createCloseButton = function (close, callback) { - let button = $('<button title="Cancel update" class="btn btn-link close" aria-label="Cancel update">×</button>'); + let button = $('<button title="Cancel update" class="btn btn-link btn-close" aria-label="Cancel update">×</button>'); button.bind('click', function () { $(this).closest(close).hide(); callback(); @@ -905,6 +1003,117 @@ this.transaction = new function () { } } +/** + * This module provides the functionality to load the full version history (for + * privileged users) and export it to tsv. + */ +var version_history = new function () { + + this._get = connection.get; + /** + * Retrieve the version history of an entity and return a table with the + * history. + * + * @param {string} entity - the entity id with or without version id. + * @return {HTMLElement} A table with the version history. + */ + this.retrieve_history = async function (entity) { + const xml = this._get(transaction + .generateEntitiesUri([entity]) + "?H"); + const html = (await transformation.transformEntities(xml))[0]; + const history_table = $(html).find(".caosdb-f-entity-version-history"); + return history_table[0]; + } + + /** + * Initalize the buttons for loading the version history. + * + * The buttons are visible when the entity has only the normal version info + * attached and the current user has the permissions to retrieve the + * version history. + * + * The buttons trigger the retrieval of the version history and append the + * version history to the version info modal. + */ + this.init_load_history_buttons = function () { + for (let entity of $(".caosdb-entity-panel")) { + const is_permitted = hasEntityPermission(entity, "RETRIEVE:HISTORY"); + if (!is_permitted) { + continue; + } + const entity_id_version = getEntityIdVersion(entity); + const version_info = $(entity) + .find(".caosdb-f-entity-version-info"); + const button = $(version_info) + .find(".caosdb-f-entity-version-load-history-btn"); + button.show(); + button + .click(async () => { + button.prop("disabled", true); + const wait = createWaitingNotification("Retrieving full history. Please wait."); + const sparse = $(version_info) + .find(".caosdb-f-entity-version-history"); + sparse.find(".modal-body *").replaceWith(wait); + + const history_table = await version_history + .retrieve_history(entity_id_version); + sparse.replaceWith(history_table); + version_history.init_export_history_buttons(entity); + }); + } + } + + /** + * Transform the HTML table with the version history to tsv. + * + * @param {HTMLElement} history_table - the HTML representation of the + * version history. + * @return {string} the version history as downloadable tsv string, + * suitable for the href attribute of a link or window.location. + */ + this.get_history_tsv = function (history_table) { + const rows = []; + for (let row of $(history_table).find("tr")) { + const cells = $(row).find(".export-data").toArray().map(x => x.textContent); + rows.push(cells); + } + return caosdb_utils.create_tsv_table(rows); + } + + /** + * Initialize the export buttons of `entity`. + * + * The buttons are only visible when the version history is visible and + * trigger a download of a tsv file which contains the version history. + * + * The buttons trigger the download of a tsv file with the version history. + * + * @param {HTMLElement} [entity] - if undefined, the export buttons of all + * page entities are initialized. + */ + this.init_export_history_buttons = function (entity) { + entity = entity || $(".caosdb-entity-panel"); + for (let version_info of $(entity) + .find(".caosdb-f-entity-version-info")) { + $(version_info).find(".caosdb-f-entity-version-export-history-btn") + .click(async () => { + const html_table = $(version_info).find("table")[0]; + const history_tsv = this.get_history_tsv(html_table); + version_history._download_tsv(history_tsv); + }); + } + } + + this._download_tsv = function (tsv_link) { + window.location.href = tsv_link; + } + + + this.init = function () { + this.init_load_history_buttons(); + this.init_export_history_buttons(); + } +} var paging = new function () { @@ -940,27 +1149,22 @@ var paging = new function () { if (nextHref != null) { // set href and show next button $('.caosdb-next-button').attr("href", nextHref); - $('.caosdb-next-button').show(); - $('.caosdb-paging-panel').show(); - } else { - $('.caosdb-next-button').hide(); } if (prevHref != null) { // set href and show prev button $('.caosdb-prev-button').attr("href", prevHref); - $('.caosdb-prev-button').show(); - $('.caosdb-paging-panel').show(); - } else { - if (prevHref == nextHref) { - $('.caosdb-paging-panel').hide(); - } - $('.caosdb-prev-button').hide(); } + paging.toggle_paging_panel(!!prevHref || !!nextHref); + return true; } + this.toggle_paging_panel = function (on) { + $(".caosdb-f-main").toggleClass("caosdb-f-show-paging-panel", on); + } + /** * Replace the old page string in the given uri or concat it if there was no * page string. If page is null return null. @@ -979,6 +1183,9 @@ var paging = new function () { return null; } + // remove fragment + uri_old = uri_old.split("#")[0]; + var pattern = /(\?(.*&)?)P=([^&]*)/; if (pattern.test(uri_old)) { // replace existing P=... @@ -1003,7 +1210,7 @@ var paging = new function () { if (uri == null) { throw new Error("uri was null."); } - var pattern = /\?(.*&)?P=([^&]*)/; + var pattern = /\?(.*&)?P=([^&#]*)/; var ret = pattern.exec(uri); return (ret != null ? ret[2] : null); } @@ -1078,6 +1285,13 @@ var paging = new function () { } return index + "L" + length; } + + this.init = function () { + var response_count = document.body.getAttribute("data-response-count"); + if (parseInt(response_count) >= 0) { + paging.initPaging(window.location.href, response_count); + } + } }; var queryForm = new function () { @@ -1121,7 +1335,6 @@ var queryForm = new function () { See https://developer.mozilla.org/en-US/docs/Web/Events/submit why this is necessary. */ var submithandler = function () { - // store current query var queryField = form.query; var value = queryField.value.toUpperCase(); @@ -1141,6 +1354,13 @@ var queryForm = new function () { queryForm.redirect(queryField.value, paging); }; + $("#caosdb-query-textarea").on("keydown", (e) => { + // prevent submit on enter + if (e.originalEvent.which == 13) { + e.originalEvent.preventDefault(); + } + }) + // handler for the form form.onsubmit = function (e) { @@ -1288,9 +1508,9 @@ var hintMessages = new function () { }); } - // moves all badges into one div with text-right + // moves all badges into one div with text-end if ($(entity).find(".caosdb-messages > .caosdb-f-message-badge").length > 0) { - var div = $('<div class="text-right" style="padding: 5px 16px;"/>'); + var div = $('<div class="text-end" style="padding: 5px 16px;"/>'); div.prependTo($(entity).find(".caosdb-messages")); var messageBadges = $(entity).find(".caosdb-messages > .caosdb-f-message-badge"); messageBadges.detach(); @@ -1458,16 +1678,25 @@ function xslt(xml, xsl, params) { } } } - if (typeof xsltProcessor.transformDocument == 'function') { - // old FFs - var retDoc = document.implementation.createDocument("", "", null); - xsltProcessor.transformDocument(xml, xsl, retDoc, null); - return retDoc.documentElement; - } else { - // modern browsers - xsltProcessor.importStylesheet(xsl); - return xsltProcessor.transformToFragment(xml, document); + var result = null; + try { + if (typeof xsltProcessor.transformDocument == 'function') { + // old FFs + var retDoc = document.implementation.createDocument("", "", null); + xsltProcessor.transformDocument(xml, xsl, retDoc, null); + result = retDoc.documentElement; + } else { + // modern browsers + xsltProcessor.importStylesheet(xsl); + result = xsltProcessor.transformToFragment(xml, document); + } + } catch (error) { + throw new Error(`XSL Transformation terminated with error: ${error.message}`); } + if (!result) { + throw new Error("XSL Transformation did not return any results"); + } + return result; } /** @@ -1478,13 +1707,18 @@ function getXSLScriptClone(source) { } /** - * TODO + * Add a template rule to a XSL style sheet. + * + * The original document is cloned (copy-on-change) before the template rule is + * appended. + * + * @param {XMLDocument} orig_xsl - the original xsl style sheet + * @param {string} templateStr - the new template rule (an xml string) + * @return {XMLDocument} new xsl style sheet with one more rule. */ -function injectTemplate(orig_xsl, template) { +function injectTemplate(orig_xsl, templateStr) { var xsl = getXSLScriptClone(orig_xsl); - var entry_t = xsl.createElement("xsl:template"); - xsl.firstElementChild.appendChild(entry_t); - entry_t.outerHTML = template; + xsl.documentElement.insertAdjacentHTML("beforeend", templateStr); return xsl; } @@ -1500,18 +1734,148 @@ function insertParam(xsl, name, value = null) { xsl.firstElementChild.append(param); } -/** - * When the page is scrolled down 100 pixels, the scroll-back button appears. - * - * @return - */ + +this.user_management = function ($, connection, createWaitingNotification, createErrorNotification) { + + const set_new_password = function (realm, username, password) { + return $.ajax({ + type: "PUT", + url: connection.getBasePath() + `User/${realm}/${username}`, + dataType: "xml", + data: { + password: password, + }, + }); + } + + /** + * Get the modal with the password form, if present. + * + * @return {HTMLElement} + */ + const get_change_password_form = function () { + const modal = $("#caosdb-f-change-password-form"); + return modal[0]; + } + + const init_change_password_form = function () { + var modal = get_change_password_form(); + if (typeof modal == "undefined") { + return; + } + modal = $(modal); + + const form = modal.find("form"); + const password_input = form[0]["password"]; + const password_input2 = form[0]["password2"]; + const realm_input = form[0]["realm"]; + const username_input = form[0]["username"]; + const checkbox = form.find("[type='checkbox']"); + const reset_button = modal.find("[type='reset']"); + + reset_button.click(() => { + // hide form + modal.modal("hide"); + $(password_input).attr("type", "password"); + $(password_input2).attr("type", "password"); + checkbox.checked = false; + }); + + checkbox.change((e) => { + if (checkbox[0].checked) { + $(password_input).attr("type", "text"); + $(password_input2).attr("type", "text"); + } else { + $(password_input).attr("type", "password"); + $(password_input2).attr("type", "password"); + } + }) + + form[0].onsubmit = function (e) { + e.preventDefault(); + if (password_input.value == password_input2.value) { + const password = password_input.value; + const username = username_input.value; + const realm = realm_input.value; + form[0].reset(); + form.find(".modal-body > *, .modal-footer > *").hide() + const wait = createWaitingNotification("Please wait..."); + form.find(".modal-body").append(wait); + user_management.set_new_password(realm, username, password) + .then((result) => { + wait.remove(); + + const msg = $('<p>Success! The new password has been stored.</p>'); + form.find(".modal-body") + .append(msg); + + const ok_button = $('<button type="reset">Ok</button>'); + form.find(".modal-footer") + .append(ok_button); + + ok_button.click(() => { + ok_button.remove(); + msg.remove(); + form.find(".modal-body > *, .modal-footer > *").show(1000); + modal.modal("hide"); + }); + }) + .catch((err) => { + wait.remove(); + console.error(err); + + var msg_text; + if (err.status == 403) { + msg_text = "You are not allowed to do this."; + } else if (err.status == 422) { + msg_text = "Your password was too weak."; + } else { + msg_text = "An unknown error occurred."; + } + const msg = createErrorNotification(msg_text) + form.find(".modal-body").append(msg); + + const ok_button = $('<button type="reset">Ok</button>'); + form.find(".modal-footer") + .append(ok_button); + + ok_button.click(() => { + ok_button.remove(); + msg.remove(); + form.find(".modal-body > *, .modal-footer > *").show(1000); + modal.modal("hide"); + }); + }) + .catch(globalError); + + return false; + } else { + password_input2.setCustomValidity('The second password must match the first one.'); + password_input2.reportValidity(); + password_input2.setCustomValidity(''); + } + return false; + }; + + } + + var init = function () { + init_change_password_form(); + } + + return { + init: init, + set_new_password: set_new_password, + get_change_password_form: get_change_password_form, + }; +}($, connection, createWaitingNotification, createErrorNotification); + /** - * Every initial function calling is done here. - * - * @return + * Initialize all the submodules. */ function initOnDocumentReady() { + paging.init(); hintMessages.init(); // init query form @@ -1522,19 +1886,24 @@ function initOnDocumentReady() { // show image 100% width $(".entity-image-preview").click(function () { - $(this).css('width', '100%'); - $(this).css('max-width', ""); + $(this).css('max-width', '100%'); $(this).css('max-height', ""); }); - if (typeof caosdb_modules.auto_init === "undefined") { + if (typeof _caosdb_modules_auto_init === "undefined") { // the test index.html sets this to false, // unset -> no tests caosdb_modules.auto_init = true; + } else { + caosdb_modules.auto_init = _caosdb_modules_auto_init; } caosdb_modules.init(); navbar.init(); + version_history.init(); + if ("${BUILD_MODULE_USER_MANAGEMENT}" == "ENABLED") { + caosdb_modules.register(user_management); + } } diff --git a/src/core/webcaosdb.xsl b/src/core/webcaosdb.xsl index cb1ad2966cb2006767309da96151196102c3af57..9d0dd2e235ca59ce6f4e7b75acd59e063e322751 100644 --- a/src/core/webcaosdb.xsl +++ b/src/core/webcaosdb.xsl @@ -37,6 +37,21 @@ <xsl:include href="xsl/common.xsl"/> <xsl:include href="xsl/welcome.xsl"/> + <xsl:template name="caosdb-tour-toc"> + <div class="caosdb-v-tour-toc-sidebar" id="tour-toc"> + <div class="caosdb-v-tour-toc-show"></div> + <button class="caosdb-v-tour-toc-show caosdb-f-tour-toc-toggle btn"></button> + <div class="caosdb-v-tour-toc-header"> + <h3>Tour</h3> + </div> + <div class="caosdb-f-tour-toc-body"></div> + <!--<button class="btn btn-light caosdb-f-leave-tour">Leave Tour</button>--> + <!--<div class="caosdb-v-tour-footer">--> + <!--<button class="caosdb-f-tour-toc-toggle btn btn-light">Hide</button>--> + <!--</div>--> + </div> + </xsl:template> + <xsl:template match="/"> <html lang="en"> <head> @@ -54,9 +69,21 @@ <xsl:call-template name="caosdb-head-js" /> </head> <body> - <xsl:call-template name="caosdb-top-navbar" /> - <xsl:call-template name="caosdb-data-container" /> - <footer> + <xsl:attribute name="data-response-count"> + <xsl:value-of select="/Response/@count"/> + </xsl:attribute> + <xsl:if test="count(/Response/*)<3 and not(/Response/Error|/Response/Info|/Response/Warning)"> + <xsl:attribute name="class">caosdb-welcome</xsl:attribute> + </xsl:if> + <div class="background d-flex flex-column"> + <xsl:call-template name="caosdb-tour-toc" /> + <xsl:call-template name="caosdb-top-navbar" /> + <xsl:call-template name="caosdb-data-container" /> + <xsl:if test="count(/Response/*)<3 and not(/Response/Error|/Response/Info|/Response/Warning)"> + <xsl:call-template name="welcome"/> + </xsl:if> + </div> + <footer class="py-5"> <xsl:call-template name="caosdb-footer"/> </footer> </body> diff --git a/src/core/xsl/annotation.xsl b/src/core/xsl/annotation.xsl index f41f6cbb47680bc9825300de645ae39c67c809cb..1ed1d28fe00cffaa2bde79cd7fef070b49771b38 100644 --- a/src/core/xsl/annotation.xsl +++ b/src/core/xsl/annotation.xsl @@ -23,31 +23,33 @@ --> <xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform"> <xsl:output method="html"/> - <xsl:template match="History" mode="comment-annotation-header"> - <h4 class="media-heading"> + <xsl:template match="Version" mode="comment-annotation-header"> + <div class="caosdb-f-comment-header"> <xsl:value-of select="@username"/> <small> <i> <xsl:text> posted on </xsl:text> - <xsl:value-of select="@datetime"/> + <xsl:value-of select="@date"/> </i> </small> - </h4> + </div> </xsl:template> <xsl:template match="Property" mode="comment-annotation-text"> - <p class="caosdb-comment-annotation-text"> - <xsl:value-of select="text()"/> - </p> + <div class="caosdb-f-comment-body"> + <small> + <p class="caosdb-comment-annotation-text"> + <xsl:value-of select="text()"/> + </p> + </small> + </div> </xsl:template> <xsl:template match="Record" mode="comment-annotation"> - <div class="media"> - <div class="media-left"> - <h3> - <xsl:text>»</xsl:text> - </h3> + <div class="d-flex"> + <div class="d-shrink-0"> + <xsl:text>»</xsl:text> </div> - <div class="media-body"> - <xsl:apply-templates mode="comment-annotation-header" select="History[translate(@transaction,'insert','INSERT')='INSERT']"/> + <div class="flex-grow-1 ms-3"> + <xsl:apply-templates mode="comment-annotation-header" select="Version[@head='true']"/> <xsl:apply-templates mode="comment-annotation-text" select="Property[@name='comment']"/> </div> </div> @@ -59,7 +61,7 @@ </xsl:template> <xsl:template match="Record" mode="error"> <div class="alert alert-danger caosdb-new-comment-error alert-dismissable"> - <button class="close" data-dismiss="alert" aria-label="close">×</button> + <button class="btn-close" data-bs-dismiss="alert" aria-label="close">×</button> <strong>Error!</strong> This comment has not been inserted. <p class="small"><pre><code><xsl:copy-of select="."/></code></pre></p></div> diff --git a/src/core/xsl/common.xsl b/src/core/xsl/common.xsl index 395c22063b8e2cdb6725ee2bd4cc859f9fb7be2c..abaa86e34f54e7e59b1df517c177018e4100dea5 100644 --- a/src/core/xsl/common.xsl +++ b/src/core/xsl/common.xsl @@ -26,12 +26,10 @@ <xsl:template name="make-filesystem-link"> <xsl:param name="href"/> <xsl:param name="display" select="$href"/> - <a> <xsl:attribute name="href"> <xsl:value-of select="concat($filesystempath,$href)"/> </xsl:attribute> <xsl:value-of select="$display"/> - </a> </xsl:template> <xsl:template name="trim"> diff --git a/src/core/xsl/entity.xsl b/src/core/xsl/entity.xsl index 22b63b842276513a2bd7258edfa6a3ff7f3618f8..d19a31509b49b859ee54d6ef80b241bee4a4b3b0 100644 --- a/src/core/xsl/entity.xsl +++ b/src/core/xsl/entity.xsl @@ -26,39 +26,41 @@ <!-- These little colored Rs, RTs, Ps, and Fs which hilite the beginning of a new entity. --> <xsl:template match="Property" mode="entity-heading-label"> - <span class="label caosdb-f-entity-role caosdb-label-property" + <span class="badge caosdb-f-entity-role caosdb-label-property me-1" data-entity-role="Property" title="This entity is a Property.">P</span> </xsl:template> <xsl:template match="Record" mode="entity-heading-label"> - <span class="label caosdb-f-entity-role caosdb-label-record" + <span class="badge caosdb-f-entity-role caosdb-label-record me-1" data-entity-role="Record" title="This entity is a Record.">R</span> </xsl:template> <xsl:template match="RecordType" mode="entity-heading-label"> - <span class="label caosdb-f-entity-role caosdb-label-recordtype" + <span class="badge caosdb-f-entity-role caosdb-label-recordtype me-1" data-entity-role="RecordType" title="This entity is a Record Type.">RT</span> </xsl:template> <xsl:template match="File" mode="entity-heading-label"> - <span class="label caosdb-f-entity-role caosdb-label-file" + <span class="badge caosdb-f-entity-role caosdb-label-file me-1" data-entity-role="File" title="This entity is a File.">F</span> </xsl:template> <xsl:template match="@id" mode="backreference-link"> - <a class="caosdb-backref-link label caosdb-id-button" title="Find all entities which reference this one."> + <a class="caosdb-backref-link btn caosdb-id-button" title="Find all entities which reference this one."> <xsl:attribute name="href"> <xsl:value-of select="concat($entitypath, '?P=0L10&query=FIND+Entity+which+references+', current())"/> </xsl:attribute> - <span class="glyphicon glyphicon-share-alt flipped-horiz-icon"/> - <span class="hidden-xs"> References</span> + <span class="bg-dark badge d-none d-sm-inline"> + <i class="bi-link"></i> References + </span> + <i class="bi-link d-inline d-sm-none"></i> </a> </xsl:template> <!-- special entity properties like type, checksum, path... --> <xsl:template match="@datatype" mode="entity-heading-attributes-datatype"> - <p class="caosdb-entity-heading-attr small text-justify"> + <p class="caosdb-entity-heading-attr small"> <em class="caosdb-entity-heading-attr-name">data type:</em> <xsl:value-of select="."/> </p> </xsl:template> <xsl:template match="@checksum" mode="entity-heading-attributes-checksum"> - <p class="caosdb-entity-heading-attr caosdb-overflow-box small text-justify"> + <p class="caosdb-entity-heading-attr caosdb-overflow-box small"> <em class="caosdb-entity-heading-attr-name"> <xsl:value-of select="concat(name(),':')"/> </em> @@ -68,18 +70,20 @@ </p> </xsl:template> <xsl:template match="@path" mode="entity-heading-attributes-path"> - <p class="caosdb-entity-heading-attr small text-justify"> + <p class="caosdb-entity-heading-attr small"> <em class="caosdb-entity-heading-attr-name"> <xsl:value-of select="concat(name(),':')"/> </em> - <xsl:call-template name="make-filesystem-link"> - <xsl:with-param name="href" select="."/> - </xsl:call-template> + <a> + <xsl:call-template name="make-filesystem-link"> + <xsl:with-param name="href" select="."/> + </xsl:call-template> + </a> </p> </xsl:template> <!-- Any further entity attributes --> <xsl:template match="@*" mode="entity-heading-attributes"> - <p class="caosdb-entity-heading-attr small text-justify"> + <p class="caosdb-entity-heading-attr small"> <em class="caosdb-entity-heading-attr-name"> <xsl:value-of select="concat(name(),':')"/> </em> @@ -87,7 +91,7 @@ </p> </xsl:template> <xsl:template match="*" mode="entity-action-panel"> - <div class="caosdb-entity-actions-panel text-right btn-group-xs"> + <div class="caosdb-entity-actions-panel text-end btn-group-sm"> <xsl:apply-templates select="Version/Successor" mode="entity-action-panel-version"> <xsl:with-param name="entityId" select="@id"/> </xsl:apply-templates> @@ -95,7 +99,7 @@ </xsl:template> <!-- Main entry for ENTITIES --> <xsl:template match="Property|Record|RecordType|File" mode="entities"> - <div class="panel panel-default caosdb-entity-panel"> + <div class="card caosdb-entity-panel mb-2"> <xsl:apply-templates select="Version" mode="entity-version-marker"/> <xsl:attribute name="id"> <xsl:value-of select="@id"/> @@ -103,22 +107,26 @@ <xsl:attribute name="data-entity-id"> <xsl:value-of select="@id"/> </xsl:attribute> + <xsl:if test="State"> + <xsl:attribute name="data-state-model"><xsl:value-of select="State/@model"/></xsl:attribute> + <xsl:attribute name="data-state-name"><xsl:value-of select="State/@name"/></xsl:attribute> + <xsl:attribute name="data-state-id"><xsl:value-of select="State/@id"/></xsl:attribute> + </xsl:if> + <xsl:apply-templates mode="entity-permissions" select="Permissions"/> <!-- A page-unique ID for this entity --> <xsl:variable name="entityid" select="concat('entity_',generate-id())"/> - <div class="panel-heading caosdb-entity-panel-heading"> + <div class="card-header caosdb-entity-panel-heading"> <xsl:attribute name="data-entity-datatype"> <xsl:value-of select="@datatype"/> </xsl:attribute> - <div class="row"> - <div class="col-sm-8"> - <h5> + <div class="d-flex flex-wrap align-items-baseline"> <xsl:apply-templates mode="entity-heading-label" select="."/> <!-- Parents --> <span class="caosdb-f-parent-list"> <xsl:if test="Parent"> <!-- <xsl:apply-templates select="Parent" mode="entity-body" /> --> <xsl:for-each select="Parent"> - <span class="caosdb-parent-item small"> + <span class="badge caosdb-parent-item me-1"> <!-- TODO lots of code duplication with parent.xsl --> <xsl:attribute name="id"> <xsl:value-of select="generate-id()"/> @@ -140,36 +148,51 @@ </xsl:attribute> <xsl:value-of select="@name"/> </a> - </h5> - </div> - <div class="col-sm-4 text-right"> - <h5 class="caosdb-v-entity-header-buttons-list"> + <div class="caosdb-v-entity-header-buttons-list ms-auto"> + <xsl:apply-templates mode="entity-heading-attributes-state" select="State"> + <xsl:with-param name="entityId" select="@id"/> + <xsl:with-param name="hasSuccessor" select="Version/Successor"/> + </xsl:apply-templates> + <xsl:apply-templates mode="backreference-link" select="@id"/> <!-- Button for expanding/collapsing the comments section--> - <span class="caosdb-clickable glyphicon glyphicon-comment" data-toggle="collapse" title="Toggle the comments section at the bottom of this entity."> - <xsl:attribute name="data-target"> + <button class="btn caosdb-v-entity-comment-badge" data-bs-toggle="collapse" title="Toggle the comments section at the bottom of this entity."> + <xsl:attribute name="data-bs-target"> <xsl:value-of select="concat('#', 'comment_', $entityid)"/> </xsl:attribute> - </span> - <span> - <xsl:apply-templates mode="backreference-link" select="@id"/> - </span> - <span class="label caosdb-id caosdb-id-button hidden"> + <i class="bi-chat-left-fill"/> + </button> + <span class="badge bg-dark caosdb-id caosdb-id-button d-none"> <xsl:value-of select="@id"/> </span> + <button class="btn caosdb-v-bookmark-button"> + <xsl:attribute name="data-bmval"> + <xsl:value-of select="@id"/> + <xsl:if test="Version/Successor"> + <!-- this is not the head --> + <xsl:value-of select="concat('@', Version/@id)"/> + </xsl:if> + </xsl:attribute> + <i class="bi-bookmark-fill"></i> + </button> <xsl:apply-templates mode="entity-heading-attributes-version" select="Version"> <xsl:with-param name="entityId" select="@id"/> </xsl:apply-templates> - </h5> - </div> + </div> </div> <xsl:apply-templates mode="entity-heading-attributes" select="@description"/> <xsl:apply-templates mode="entity-heading-attributes-datatype" select="@datatype"/> - <xsl:apply-templates mode="entity-heading-attributes-path" select="@path"/> - <xsl:apply-templates mode="entity-heading-attributes" select="@*[not(contains('+checksum+cuid+id+name+description+datatype+path+',concat('+',name(),'+')))]"/> - <xsl:apply-templates mode="entity-heading-attributes-checksum" select="@checksum"/> + <xsl:apply-templates mode="entity-heading-attributes" select="@*[not(contains('+checksum+size+cuid+id+name+description+datatype+path+',concat('+',name(),'+')))]"/> + <xsl:if test="@*[contains('+path+checksum+size+',concat('+',name(),'+'))]"> + <details> + <summary><small>File Details</small></summary> + <xsl:apply-templates mode="entity-heading-attributes-path" select="@path"/> + <xsl:apply-templates mode="entity-heading-attributes-checksum" select="@checksum"/> + <xsl:apply-templates mode="entity-heading-attributes" select="@size"/> + </details> + </xsl:if> </div> <xsl:apply-templates mode="entity-action-panel" select="."/> - <div class="panel-body caosdb-entity-panel-body"> + <div class="card-body caosdb-entity-panel-body"> <!-- Messages --> <div class="caosdb-messages"> <xsl:apply-templates select="Error"> @@ -185,9 +208,6 @@ <!-- Properties --> <ul class="list-group caosdb-properties"> <xsl:if test="Property"> - <li class="list-group-item caosdb-properties-heading"> - <strong class="small">Properties</strong> - </li> <xsl:apply-templates mode="entity-body" select="Property"/> </xsl:if> </ul> @@ -220,47 +240,38 @@ <xsl:apply-templates select="Info"> <xsl:with-param name="class" select="'alert-info'"/> </xsl:apply-templates> - <!-- collapsed data --> - <div class="collapse"> - <xsl:attribute name="id"> - <xsl:value-of select="$collapseid"/> - </xsl:attribute> - <hr class="caosdb-subproperty-divider"/> - <!-- <li> --> - <!-- <a class="caosdb-property-name"> --> - <!-- <xsl:attribute name="href"> --> - <!-- <xsl:value-of select="concat($entitypath,@id)" /></xsl:attribute> --> - <!-- </a> --> - <!-- </li> --> - - <!-- property attributes --> - <xsl:apply-templates mode="property-attributes" select="@description"/> - <xsl:apply-templates mode="property-attributes-id" select="@id"/> - <xsl:apply-templates mode="property-attributes-type" select="@datatype"/> - <xsl:apply-templates mode="property-attributes" select="@*[not(contains('+cuid+id+name+description+datatype+',concat('+',name(),'+')))]"/> - </div> </li> </xsl:template> <xsl:template match="Property" mode="property-collapsed"> <xsl:param name="collapseid"/> <div class="row"> - <div class="col-sm-4"> - <h5> + <div class="col-sm-6 col-md-4 caosdb-v-property-left-col"> <xsl:if test="@*[not(contains('+cuid+id+name+',concat('+',name(),'+')))]"> - <span class="glyphicon glyphicon-collapse-down caosdb-clickable" data-toggle="collapse" style="margin-right: 10px;"> - <xsl:attribute name="data-target"> + <i data-bs-toggle="collapse" style="position: absolute; left: 1rem;" class="bi-caret-down-square fs-6 caosdb-clickable"> + <xsl:attribute name="data-bs-target"> <xsl:value-of select="concat('#',$collapseid)"/> </xsl:attribute> - </span> + </i> </xsl:if> - <strong class="caosdb-property-name"> <xsl:value-of select="@name"/></strong> - </h5> + <span class="caosdb-property-name"> <xsl:value-of select="@name"/></span> + </div> + <!-- collapsed data --> + <div class="collapse order-sm-last"> + <xsl:attribute name="id"> + <xsl:value-of select="$collapseid"/> + </xsl:attribute> + <hr class="caosdb-subproperty-divider"/> + <dl class="row caosdb-v-entity-property-attributes"> + <xsl:apply-templates mode="property-attributes" select="@description"/> + <xsl:apply-templates mode="property-attributes-id" select="@id"/> + <xsl:apply-templates mode="property-attributes-type" select="@datatype"/> + <xsl:apply-templates mode="property-attributes" select="@*[not(contains('+cuid+id+name+description+datatype+',concat('+',name(),'+')))]"/> + </dl> </div> <!-- property value --> - <div class="col-sm-6 caosdb-f-property-value"> + <div class="col-sm-6 col-md-8 caosdb-f-property-value"> <xsl:apply-templates mode="property-value" select="."/> </div> - <div class="col-sm-2 caosdb-property-edit" style="text-align: right;"></div> </div> </xsl:template> <xsl:template name="single-value"> @@ -272,7 +283,7 @@ <xsl:choose> <xsl:when test="$reference='true' and normalize-space($value)!=''"> <!-- this is a reference --> - <a class="btn btn-default btn-sm caosdb-f-reference-value caosdb-resolvable-reference"> + <a class="btn btn-outline-dark btn-sm caosdb-f-reference-value caosdb-resolvable-reference"> <xsl:attribute name="href"> <xsl:value-of select="concat($entitypath,normalize-space($value))"/> </xsl:attribute> @@ -357,7 +368,7 @@ <xsl:attribute name="class">list-group list-inline</xsl:attribute> <xsl:for-each select="Value"> <xsl:element name="li"> - <xsl:attribute name="class">list-group-item</xsl:attribute> + <xsl:attribute name="class">list-inline-item</xsl:attribute> <xsl:call-template name="single-value"> <xsl:with-param name="reference"> <xsl:value-of select="'false'"/> @@ -392,8 +403,8 @@ <xsl:otherwise> <xsl:choose> <!-- the referenced entities have been returned. --> - <xsl:when test="*[@id]"> - <xsl:for-each select="*[@id]"> + <xsl:when test="Record|RecordType|Property|File"> + <xsl:for-each select="Record|RecordType|Property|File"> <xsl:call-template name="single-value"> <xsl:with-param name="reference"> <xsl:value-of select="'true'"/> @@ -444,43 +455,27 @@ </xsl:choose> </xsl:template> <xsl:template match="@*" mode="property-attributes"> - <div class="row"> - <div class="col-sm-3 col-sm-offset-1"> - <strong> - <xsl:value-of select="name()"/> - </strong> - </div> - <div class="col-sm-8"> - <xsl:value-of select="."/> - </div> - </div> + <dt class="col-6 col-md-4 mb-0"><xsl:value-of select="name()"/></dt> + <dd class="col-6 col-md-8 mb-0"> + <xsl:value-of select="."/> + </dd> </xsl:template> <xsl:template match="@datatype" mode="property-attributes-type"> - <div class="row"> - <div class="col-sm-3 col-sm-offset-1"> - <strong>data type</strong> - </div> - <div class="col-sm-8 caosdb-property-datatype"> - <xsl:value-of select="."/> - </div> - </div> + <dt class="col-6 col-md-4 mb-0">data type</dt> + <dd class="col-6 col-md-8 mb-0 caosdb-property-datatype"> + <xsl:value-of select="."/> + </dd> </xsl:template> <xsl:template match="@id" mode="property-attributes-id"> - <div class="row"> - <div class="col-sm-3 col-sm-offset-1"> - <strong> - <xsl:value-of select="name()"/> - </strong> - </div> - <div class="col-sm-8"> - <a class="caosdb-property-id"> - <xsl:attribute name="href"> - <xsl:value-of select="concat($entitypath,.)"/> - </xsl:attribute> - <xsl:value-of select="."/> - </a> - </div> - </div> + <dt class="col-6 col-md-4 mb-0">id</dt> + <dd class="col-6 col-md-8 mb-0"> + <a class="caosdb-property-id"> + <xsl:attribute name="href"> + <xsl:value-of select="concat($entitypath,.)"/> + </xsl:attribute> + <xsl:value-of select="."/> + </a> + </dd> </xsl:template> <!-- ANNOTATIONS --> <xsl:template name="annotation-section"> @@ -493,22 +488,101 @@ <xsl:attribute name="id"> <xsl:value-of select="$collapseId"/> </xsl:attribute> - <li class="list-group-item caosdb-comments-heading"> - <span class="glyphicon glyphicon-comment" style="margin-right: 1em;"/> + <li class="list-group-item caosdb-comments-heading d-flex"> + <i class="bi-chat-left-fill" style="margin-right: 1em;"/> <strong class="small">Comments</strong> - <button class="btn btn-link btn-xs pull-right caosdb-new-comment-button"> + <button class="btn btn-sm pull-right caosdb-new-comment-button ms-auto"> <strong>add new comment</strong> </button> </li> </ul> </xsl:template> + + <!-- ENTITY STATE --> + <xsl:template mode="entity-heading-attributes-state" match="State"> + <!-- creates a state button in the header of an entity which opens a modal with more information buttons for transitions --> + <xsl:param name="entityId"/> + <xsl:param name="hasSuccessor"/> + <xsl:param name="stateModalId">state-modal-<xsl:value-of select="generate-id()"/></xsl:param> + <button title="State Info" class="btn" data-bs-toggle="modal"> + <xsl:attribute name="data-bs-target">#<xsl:value-of select="$stateModalId"/></xsl:attribute> + <span class="badge label-info caosdb-v-state-label"> + <xsl:if test="@color"> + <xsl:attribute name="style">background-color: <xsl:value-of select="@color"/>;</xsl:attribute> + </xsl:if> + <xsl:value-of select="./@name"/> + </span> + </button> + + <!-- here comes the modal --> + <div class="caosdb-f-entity-state-info modal fade" tabindex="-1" role="dialog"> + <xsl:attribute name="id"><xsl:value-of select="$stateModalId"/></xsl:attribute> + <div class="modal-dialog" role="document"> + <div class="modal-content text-left"> + <div class="modal-header flex-wrap"> + <span class="modal-title"> + <span class="badge caosdb-v-state-model-label"><xsl:value-of select="@model"/> + <span title="State Info" class="badge badge-info caosdb-v-state-label"> + <xsl:attribute name="style"> + <xsl:if test="@color"> + background-color: <xsl:value-of select="@color"/>; + </xsl:if> + </xsl:attribute> + <xsl:value-of select="@name"/></span> + </span> + </span> + <button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close" title="Close"></button> + <div style="margin-top: 8px"><em><xsl:value-of select="@description"/></em></div> + </div> + <div class="modal-body"> + <xsl:choose> + <xsl:when test="parent::RecordType"> + Every newly inserted Record with type "<xsl:value-of select="parent::RecordType/@name"/>" will initially be in this state. + </xsl:when> + <xsl:when test="$hasSuccessor"> + <p>You are currently viewing an old versions of this entity. <a> + <xsl:attribute name="href"><xsl:value-of select="$entityId"/></xsl:attribute> + Go to the latest version.</a></p> + </xsl:when> + <xsl:otherwise> + <xsl:if test="not(Transition)"> + You cannot perform any transitions. Maybe this is due to lack of permissions. + </xsl:if> + <dl class="row caosdb-f-transition"> + <xsl:for-each select="Transition"> + <dt class="col-sm-4 mb-2"><button class="btn btn-secondary badge caosdb-f-entity-state-transition-button fs-6" type="button"> + <xsl:attribute name="data-to-state"><xsl:value-of select="ToState/@name"/></xsl:attribute> + <xsl:attribute name="data-transition-name"><xsl:value-of select="@name"/></xsl:attribute> + <xsl:attribute name="title">Transition to state '<xsl:value-of select="ToState/@name"/>'. <xsl:if test="ToState/@description"><xsl:value-of select="ToState/@description"/></xsl:if></xsl:attribute> + <xsl:if test="@color"> + <xsl:attribute name="style"> + background-color: <xsl:value-of select="@color"/>; + </xsl:attribute> + </xsl:if> + <xsl:value-of select="@name"/></button></dt> + <dd class="col-sm-8"><xsl:value-of select="@description"/></dd> + </xsl:for-each> + </dl> + </xsl:otherwise> + </xsl:choose> + </div> + <div class="modal-footer"> + <a href="?query=FIND Record StateModel WITH name = Model1"> + <xsl:attribute name="href">?query=FIND RECORD StateModel WITH name = "<xsl:value-of select="@model"/>"</xsl:attribute> + View state model</a> + </div> + </div> + </div> + </div> + </xsl:template> + <!--VERSIONING--> <xsl:template match="Version" mode="entity-heading-attributes-version"> <xsl:param name="entityId"/> <xsl:param name="versionModalId">version-modal-<xsl:value-of select="generate-id()"/></xsl:param> <!-- the clock button which opens the window with the versioning info --> - <button title="Versioning Info" type="button" data-toggle="modal"> - <xsl:attribute name="data-target">#<xsl:value-of select="$versionModalId"/></xsl:attribute> + <button title="Versioning Info" type="button" data-bs-toggle="modal"> + <xsl:attribute name="data-bs-target">#<xsl:value-of select="$versionModalId"/></xsl:attribute> <xsl:attribute name="class"> caosdb-f-entity-version-button caosdb-v-entity-version-button btn <xsl:if test="Successor"> @@ -516,95 +590,217 @@ <xsl:value-of select="' text-danger'"/> </xsl:if> </xsl:attribute> - <span class="glyphicon glyphicon-time"/> + <i class="bi-clock-history"/> </button> + <!-- the following div.modal is the window that pops up when the user clicks on the clock button --> <div class="caosdb-f-entity-version-info modal fade" tabindex="-1" role="dialog"> <xsl:attribute name="id"><xsl:value-of select="$versionModalId"/></xsl:attribute> + <xsl:attribute name="data-entity-versioned-id"><xsl:value-of select="concat($entityId, '@', @id)"/></xsl:attribute> <div class="modal-dialog modal-lg" role="document"> <div class="modal-content text-left"> + <!-- modal-header start --> <div> <xsl:attribute name="class"> modal-header - <xsl:if test="Successor"> + text-start + <xsl:if test="not(@head='true')"> <!-- indicate old version by color --> <xsl:value-of select="' bg-danger'"/> </xsl:if> </xsl:attribute> - <button type="button" class="close" data-dismiss="modal" aria-label="Close" title="Close"><span aria-hidden="true">×</span></button> - <h4 class="modal-title">Version Info</h4> - <p class="caosdb-entity-heading-attr"> - <em class="caosdb-entity-heading-attr-name"> - This is - <xsl:if test="Successor"><b>not</b></xsl:if> - the latest version of this entity. - </em> - </p> + <p class="caosdb-entity-version-attr"> + <h4 class="modal-title">Version Info</h4> + <em class="caosdb-entity-version-attr-name"> + This is + <xsl:if test="not(@head='true')"><b>not</b></xsl:if> + the latest version of this entity. + <xsl:apply-templates mode="entity-version-modal-head" select="Successor"> + <xsl:with-param name="entityId" select="$entityId"/> + </xsl:apply-templates> + </em> + </p> + <button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close" title="Close"></button> </div> - <div class="modal-body"> - <xsl:apply-templates mode="entity-version-modal-head" select="Successor"> + <!-- modal-header end --> + <div class="caosdb-f-entity-version-history"> + <!-- modal-body and modal-footer are added by this template --> + <xsl:apply-templates select="." mode="entity-version-history-table"> <xsl:with-param name="entityId" select="$entityId"/> </xsl:apply-templates> - <xsl:apply-templates mode="entity-version-modal-successor" select="Successor"> + </div> + </div> + </div> + </div> + </xsl:template> + + <xsl:template match="Version[@completeHistory='true']" mode="entity-version-history-table"> + <!-- contains the table of the full version history --> + <xsl:param name="entityId"/> + <div class="modal-body"> + <table class="table table-hover"> + <thead> + <tr><th><div class="export-data">Entity ID</div></th> + <th class="export-data">Version ID</th> + <th class="export-data">Date</th> + <th class="export-data">User</th> + <th class="invisible"><div class="export-data">URI</div></th> + </tr></thead> + <tbody> + <xsl:apply-templates mode="entity-version-modal-successor" select="Successor"> + <xsl:with-param name="entityId" select="$entityId"/> + </xsl:apply-templates> + <tr> + <td class="invisible"><div class="export-data"><xsl:value-of select="$entityId"/></div></td> + <td class="caosdb-v-entity-version-hint caosdb-v-entity-version-hint-cur">This Version</td> + <td><xsl:apply-templates select="@id" mode="entity-version-id"/> + </td><td> + <xsl:apply-templates select="@date" mode="entity-version-date"/> + </td><td class="export-data"> + <xsl:value-of select="@username"/>@<xsl:value-of select="@realm"/> + </td> + <td class="invisible"><div class="export-data"><xsl:value-of select="concat($entitypath, $entityId, '@', @id)"/></div></td> + </tr> + <xsl:apply-templates mode="entity-version-modal-predecessor" select="Predecessor"> + <xsl:with-param name="entityId" select="$entityId"/> + </xsl:apply-templates> + </tbody> + </table> + </div> + <div class="modal-footer"> + <button type="button" class="caosdb-f-entity-version-export-history-btn btn btn-secondary">Export history</button> + </div> + </xsl:template> + + <xsl:template match="Version[not(@completeHistory='true')]" mode="entity-version-history-table"> + <!-- contains the table of the simple version info (not the full history)--> + <xsl:param name="entityId"/> + <div class="modal-body"> + <table class="table"> + <thead><tr><th>Previous Version</th><th>This Version</th><th>Next Version</th></tr></thead> + <tbody> + <tr> + <td> + <xsl:if test="not(Predecessor)"> + <div class="caosdb-v-entity-version-no-related">No predecessor</div> + </xsl:if> + <xsl:apply-templates select="Predecessor/@id" mode="entity-version-link-to-other-version"> <xsl:with-param name="entityId" select="$entityId"/> </xsl:apply-templates> - <p class="caosdb-entity-heading-attr"> - <em class="caosdb-entity-heading-attr-name">This version:</em> - <xsl:value-of select="@id"/> (<xsl:value-of select="@date"/>) - </p> - <xsl:apply-templates mode="entity-version-modal-predecessor" select="Predecessor"> + </td> + <td> + <xsl:apply-templates select="@id" mode="entity-version-id"/> + </td> + <td> + <xsl:if test="not(Successor)"> + <div class="caosdb-v-entity-version-no-related">No successor</div> + </xsl:if> + <xsl:apply-templates select="Successor/@id" mode="entity-version-link-to-other-version"> <xsl:with-param name="entityId" select="$entityId"/> </xsl:apply-templates> - </div> - </div> - </div> + </td> + </tr> + </tbody> + </table> + </div> + <div class="modal-footer"> + <button type="button" style="display: none" class="caosdb-f-entity-version-load-history-btn btn btn-secondary">Load full history</button> </div> </xsl:template> + + <xsl:template match="@id" mode="entity-version-id"> + <!-- a versions'id (abbreviated) --> + <xsl:attribute name="title">Full Version ID: <xsl:value-of select="."/></xsl:attribute> + <xsl:value-of select="substring(.,1,8)"/> + <td class="invisible"><div class="export-data"><xsl:value-of select="."/></div></td> + </xsl:template> + + <xsl:template match="@date" mode="entity-version-date"> + <!-- a version's date (abbreviated)--> + <xsl:attribute name="title"><xsl:value-of select="."/></xsl:attribute> + <xsl:value-of select="substring(.,0,11)"/> + <xsl:value-of select="' '"/> + <xsl:value-of select="substring(.,12,8)"/> + <td class="invisible"><div class="export-data"><xsl:value-of select="."/></div></td> + </xsl:template> + + <xsl:template match="Predecessor|Successor" mode="entity-version-modal-single-history-item"> + <!-- a single row of the version history table --> + <xsl:param name="entityId"/> + <xsl:param name="hint"/> + <tr> + <td class="invisible"><div class="export-data"><xsl:value-of select="$entityId"/></div></td> + <td class="caosdb-v-entity-version-hint"><xsl:value-of select="$hint"/></td> + <td> + <xsl:apply-templates select="@id" mode="entity-version-link-to-other-version"> + <xsl:with-param name="entityId" select="$entityId"/> + </xsl:apply-templates> + </td><td> + <xsl:apply-templates select="@date" mode="entity-version-date"/> + </td><td class="export-data"> + <xsl:value-of select="@username"/>@<xsl:value-of select="@realm"/> + </td> + <td class="invisible"><div class="export-data"><xsl:value-of select="concat($entitypath, $entityId, '@', @id)"/></div></td> + </tr> + </xsl:template> + + <xsl:template match="@id" mode="entity-version-link-to-other-version"> + <!-- link to other version (used by both version tables)--> + <xsl:param name="entityId"/> + <a><xsl:attribute name="href"><xsl:value-of select="$entityId"/>@<xsl:value-of select="."/></xsl:attribute> + <xsl:apply-templates select="." mode="entity-version-id"/></a> + </xsl:template> + <xsl:template match="Predecessor" mode="entity-version-modal-predecessor"> - <!-- content of the versioning window --> + <!-- content of the versioning info (not the full history) --> <xsl:param name="entityId"/> - <p class="caosdb-entity-heading-attr"> - <em class="caosdb-entity-heading-attr-name">Previous version:</em> - <a><xsl:attribute name="href"><xsl:value-of select="$entityId"/>@<xsl:value-of select="@id"/></xsl:attribute> - <xsl:value-of select="@id"/> (<xsl:value-of select="@date"/>) - </a> - </p> + <xsl:apply-templates mode="entity-version-modal-single-history-item" select="."> + <xsl:with-param name="entityId" select="$entityId"/> + <xsl:with-param name="hint" select="'Older Version'"/> + </xsl:apply-templates> + <xsl:apply-templates mode="entity-version-modal-predecessor" select="Predecessor"> + <xsl:with-param name="entityId" select="$entityId"/> + </xsl:apply-templates> </xsl:template> + <xsl:template match="Successor" mode="entity-version-modal-head"> - <!-- content of the versioning window --> + <!-- content of the versioning modal's header (if a newer version exists) --> <xsl:param name="entityId"/> - <p class="caosdb-entity-heading-attr"> - <em class="caosdb-entity-heading-attr-name">Newest version:</em> - <a><xsl:attribute name="href"><xsl:value-of select="$entityId"/>@HEAD</xsl:attribute> - <xsl:value-of select="$entityId"/>@HEAD - </a> - </p> + View the newest version here: + <a><xsl:attribute name="href"><xsl:value-of select="$entityId"/>@HEAD</xsl:attribute> + <xsl:value-of select="$entityId"/>@HEAD + </a> </xsl:template> + <xsl:template match="Successor" mode="entity-version-modal-successor"> - <!-- content of the versioning window --> + <!-- content of the versioning info (not the full history) --> <xsl:param name="entityId"/> - <p class="caosdb-entity-heading-attr"> - <em class="caosdb-entity-heading-attr-name">Next version:</em> - <a><xsl:attribute name="href"><xsl:value-of select="$entityId"/>@<xsl:value-of select="@id"/></xsl:attribute> - <xsl:value-of select="@id"/> (<xsl:value-of select="@date"/>) - </a> - </p> + <xsl:apply-templates mode="entity-version-modal-successor" select="Successor"> + <xsl:with-param name="entityId" select="$entityId"/> + </xsl:apply-templates> + <xsl:apply-templates mode="entity-version-modal-single-history-item" select="."> + <xsl:with-param name="entityId" select="$entityId"/> + <xsl:with-param name="hint" select="'Newer Version'"/> + </xsl:apply-templates> </xsl:template> + <xsl:template match="Version/Successor" mode="entity-action-panel-version"> <!-- clickable warning message in the entity actions panel when there exists a newer version --> <xsl:param name="entityId"/> - <a class="caosdb-f-entity-version-old-warning alert-warning btn btn-link" title="Go to the latest version of this entity."> + <a class="caosdb-f-entity-version-old-warning alert-warning btn" title="Go to the latest version of this entity."> <xsl:attribute name="href"><xsl:value-of select="$entityId"/>@HEAD</xsl:attribute> <strong>Warning</strong> A newer version exists! </a> </xsl:template> + <xsl:template match="Version" mode="entity-version-marker"> <!-- content of the data-version-id attribute --> <xsl:attribute name="data-version-id"> - <xsl:value-of select="@id"/> + <xsl:value-of select="@id"/> </xsl:attribute> <xsl:apply-templates select="Successor" mode="entity-version-marker"/> </xsl:template> + <xsl:template match="Successor" mode="entity-version-marker"> <!-- content of the data-version-successor attribute This data-attribute marks entities which have a newer version. @@ -613,4 +809,17 @@ <xsl:value-of select="@id"/> </xsl:attribute> </xsl:template> + + <!-- PERMISSIONS --> + <xsl:template match="Permissions" mode="entity-permissions"> + <div style="display: none"> + <xsl:apply-templates select="Permission" mode="entity-permissions"/> + </div> + </xsl:template> + + <xsl:template match="Permission" mode="entity-permissions"> + <div> + <xsl:attribute name="data-permission"><xsl:value-of select="@name"/></xsl:attribute> + </div> + </xsl:template> </xsl:stylesheet> diff --git a/src/core/xsl/entity_palette.xsl b/src/core/xsl/entity_palette.xsl index 961a51dc51584c3fe87496e54f9837fcec9de0b0..e45364c80a4ba3849d5b9abaf29c5b85c1b76af1 100644 --- a/src/core/xsl/entity_palette.xsl +++ b/src/core/xsl/entity_palette.xsl @@ -3,42 +3,40 @@ <xsl:output method="html"/> <xsl:template match="/Response"> - <div class="btn-group-vertical caosdb-v-editmode-btngroup"> - <button type="button" class="btn btn-default caosdb-f-edit-panel-new-button new-property">Create new Property</button> - <button type="button" class="btn btn-default caosdb-f-edit-panel-new-button new-recordtype">Create new RecordType</button> + <div class="list-group list-group-flush"> + <div class="list-group-item btn-group-vertical caosdb-v-editmode-btngroup caosdb-f-edit-mode-create-buttons"> + <button type="button" class="btn btn-secondary caosdb-f-edit-panel-new-button new-property">Create Property</button> + <button type="button" class="btn btn-secondary caosdb-f-edit-panel-new-button new-recordtype">Create RecordType</button> </div> - <div title="Drag and drop Properties from this panel to the Entities on the left." class="panel panel-default caosdb-v-editmode-existing"> - <div class="panel-heading"> - <h5>Existing Properties</h5> + <div title="Drag and drop Properties from this panel to the Entities on the left." class="caosdb-v-editmode-existing caosdb-f-edit-mode-existing d-none"> + <div class="card-header"> + <h6>Existing Properties</h6> </div> - <div class="panel-body"> + <div class="card"> <div class="input-group" style="width: 100%;"> <input class="form-control" placeholder="filter..." title="Type a name (full or partial)." oninput="edit_mode.filter('properties');" id="caosdb-f-filter-properties" type="text"/> - <span class="input-group-btn"> - <button class="btn btn-default caosdb-f-edit-panel-new-button new-property caosdb-f-hide-on-empty-input" title="Create this Property." ><span class="glyphicon glyphicon-plus"></span></button> - </span> + <button class="btn btn-secondary caosdb-f-edit-panel-new-button new-property caosdb-f-hide-on-empty-input" title="Create this Property." ><i class="bi-plus"></i></button> </div> - <ul class="caosdb-v-edit-list"> + <ul class="caosdb-v-edit-list list-group"> <xsl:apply-templates select="./Property"/> </ul> </div> </div> - <div title="Drag and drop RecordTypes from this panel to the Entities on the left." class="panel panel-default caosdb-v-editmode-existing"> - <div class="panel-heading"> - <h5>Existing RecordTypes</h5> + <div title="Drag and drop RecordTypes from this panel to the Entities on the left." class="caosdb-v-editmode-existing caosdb-f-edit-mode-existing d-none"> + <div class="card-header"> + <h6>Existing RecordTypes</h6> </div> - <div class="panel-body"> + <div class="card"> <div class="input-group" style="width: 100%;"> <input class="form-control" placeholder="filter..." title="Type a name (full or partial)." oninput="edit_mode.filter('recordtypes');" id="caosdb-f-filter-recordtypes" type="text"/> - <span class="input-group-btn"> - <button class="btn btn-default caosdb-f-edit-panel-new-button new-recordtype caosdb-f-hide-on-empty-input" title="Create this RecordType"><span class="glyphicon glyphicon-plus"></span></button> - </span> + <button class="btn btn-secondary caosdb-f-edit-panel-new-button new-recordtype caosdb-f-hide-on-empty-input" title="Create this RecordType"><i class="bi-plus"></i></button> </div> - <ul class="caosdb-v-edit-list"> + <ul class="caosdb-v-edit-list list-group"> <xsl:apply-templates select="./RecordType"/> </ul> - </div> </div> + </div> + </div> </xsl:template> <xsl:template match="RecordType"> diff --git a/src/core/xsl/filesystem.xsl b/src/core/xsl/filesystem.xsl index 38a772209fb1e199cee8a81a3f02ac1cf7a5da44..a924900091ca2f2c2b00ed698e144c62d27511d6 100644 --- a/src/core/xsl/filesystem.xsl +++ b/src/core/xsl/filesystem.xsl @@ -69,7 +69,7 @@ <xsl:attribute name="href"> <xsl:value-of select="concat(/Response/dir/@url, @name)"/> </xsl:attribute> - <span class="glyphicon"></span> + <i class="bi-folder2 me-1"></i> <xsl:value-of select="@name"/> </a> </li> @@ -83,17 +83,17 @@ <xsl:attribute name="href"> <xsl:value-of select="$file-uri"/> </xsl:attribute> - <span class="glyphicon"></span> + <i class="bi-file-arrow-down me-1"></i> <xsl:value-of select="@name"/> </a> </div> - <div class="col-sm-6 text-right"> + <div class="col-sm-6 text-end"> <a class="btn caosdb-fs-btn-file"> <xsl:attribute name="href"> <xsl:value-of select="concat($entitypath, @id)"/> </xsl:attribute> - <span class="label caosdb-label-file">F</span> - <span class="label caosdb-id hidden"> + <span class="badge caosdb-label-file">F</span> + <span class="badge caosdb-id invisible"> <xsl:value-of select="@id"/> </span> </a> @@ -106,9 +106,9 @@ </xsl:template> <xsl:template match="/Response/dir" mode="top-level-data"> <div class="container"> - <div class="panel-group"> - <div class="panel panel-default"> - <div class="panel-heading"> + <div> + <div class="card" id="caosdb-f-filesystem"> + <div class="card-header"> <div class="row"> <div class="col-sm-8"> <a title="Go back to the root of the file system."> @@ -119,13 +119,13 @@ </a> <xsl:call-template name="filesystem-cwd"/> </div> - <div class="col-sm-4 text-right"> + <div class="col-sm-4 text-end"> <xsl:value-of select="count(dir)"/> Directories and <xsl:value-of select="count(file)"/> Files </div> </div> </div> - <div class="panel-body"> + <div class="card-body"> <ul class="list-group"> <xsl:apply-templates mode="filesystem-item" select="dir"/> <xsl:apply-templates mode="filesystem-item" select="file"/> diff --git a/src/core/xsl/footer.xsl b/src/core/xsl/footer.xsl index 0b034e9d5a3b170305b85567f34732ac04755f91..0231649d4ff4deda68822eecf25576175a5eed00 100644 --- a/src/core/xsl/footer.xsl +++ b/src/core/xsl/footer.xsl @@ -26,20 +26,24 @@ <xsl:output method="html"/> <xsl:template name="caosdb-footer"> - <div class="caosdb-footer-element" id="caosdb-footer-element-custom-1"> - ${BUILD_FOOTER_CUSTOM_ELEMENT_ONE} + <div class="container d-flex flex-lg-row flex-column justify-content-around"> + <div class="caosdb-footer-element" id="caosdb-footer-element-custom-1"> + ${BUILD_FOOTER_CUSTOM_ELEMENT_ONE} + </div> + <div class="caosdb-footer-element" id="caosdb-footer-element-custom-2"> + ${BUILD_FOOTER_CUSTOM_ELEMENT_TWO} + </div> </div> - <div class="caosdb-footer-element" id="AGPL-notice"> - This server runs free software licensed under the <a - href="https://www.gnu.org/licenses/agpl-3.0.en.html" - target="_blank">AGPL-v3</a>, you can obtain the sources <a - href="https://gitlab.com/caosdb" target="_blank">here</a>. - </div> - <div class="caosdb-footer-element" id="caosdb-footer-element-custom-2"> - ${BUILD_FOOTER_CUSTOM_ELEMENT_TWO} - </div> - <div class="caosdb-footer-element"> - <a href="${BUILD_FOOTER_DATA_POLICY_HREF}">Data Policy</a><span class="caosdb-bulletsep">•</span><a href="/webinterface/${BUILD_NUMBER}/html/imprint.html">Imprint/Impressum</a> + <div class="container d-flex flex-md-row flex-column justify-content-center"> + <a href="mailto:info@indiscale.com">Contact</a> + <span class="caosdb-bulletsep d-none d-md-inline">•</span> + <a href="https://www.indiscale.com/imprint/">Imprint/Impressum</a> + <span class="caosdb-bulletsep d-none d-md-inline">•</span> + <a href="${BUILD_FOOTER_DATA_POLICY_HREF}">Data Policy</a> + <span class="caosdb-bulletsep d-none d-md-inline">•</span> + <a href="https://www.gnu.org/licenses/agpl-3.0.en.html" target="_blank">License (AGPL-v3)</a> + <span class="caosdb-bulletsep d-none d-md-inline">•</span> + <a href="https://gitlab.com/caosdb" target="_blank">Sources</a> </div> </xsl:template> </xsl:stylesheet> diff --git a/src/core/xsl/main.xsl b/src/core/xsl/main.xsl index efbf2e6b6e4db0ad7b14cb1c2483157bc79001f1..238f790fdcd93816d36adf35886e162575df9aa3 100644 --- a/src/core/xsl/main.xsl +++ b/src/core/xsl/main.xsl @@ -85,182 +85,27 @@ <xsl:value-of select="concat($basepath,'webinterface/${BUILD_NUMBER}/css/bootstrap-select.css')"/> </xsl:attribute> </xsl:element> + <xsl:element name="link"> + <xsl:attribute name="rel">stylesheet</xsl:attribute> + <xsl:attribute name="href"> + <xsl:value-of select="concat($basepath,'webinterface/${BUILD_NUMBER}/css/bootstrap-icons.css')"/> + </xsl:attribute> + </xsl:element> <!--CSS_EXTENSIONS--> </xsl:template> <xsl:template name="caosdb-head-js"> <script> - var caosdb_webui_build_number = "${BUILD_NUMBER}"; window.sessionStorage.caosdbBasePath = "<xsl:value-of select="$basepath"/>"; </script> <xsl:element name="script"> <xsl:attribute name="src"> - <xsl:value-of select="concat($basepath,'webinterface/${BUILD_NUMBER}/js/jquery.js')"/> - </xsl:attribute> - </xsl:element> - <xsl:element name="script"> - <xsl:attribute name="src"> - <xsl:value-of select="concat($basepath,'webinterface/${BUILD_NUMBER}/js/bootstrap.js')"/> - </xsl:attribute> - </xsl:element> - <xsl:element name="script"> - <xsl:attribute name="src"> - <xsl:value-of select="concat($basepath,'webinterface/${BUILD_NUMBER}/js/bootstrap-autocomplete.min.js')"/> - </xsl:attribute> - </xsl:element> - <xsl:element name="script"> - <xsl:attribute name="src"> - <xsl:value-of select="concat($basepath,'webinterface/${BUILD_NUMBER}/js/bootstrap-select.js')"/> - </xsl:attribute> - </xsl:element> - <xsl:element name="script"> - <xsl:attribute name="src"> - <xsl:value-of select="concat($basepath,'webinterface/${BUILD_NUMBER}/js/state-machine.js')"/> - </xsl:attribute> - </xsl:element> - <xsl:element name="script"> - <xsl:attribute name="src"> - <xsl:value-of select="concat($basepath,'webinterface/${BUILD_NUMBER}/js/showdown.js')"/> - </xsl:attribute> - </xsl:element> - <xsl:element name="script"> - <xsl:attribute name="src"> - <xsl:value-of select="concat($basepath,'webinterface/${BUILD_NUMBER}/js/dropzone.js')"/> - </xsl:attribute> - </xsl:element> - <xsl:element name="script"> - <xsl:attribute name="src"> - <xsl:value-of select="concat($basepath,'webinterface/${BUILD_NUMBER}/js/loglevel.js')"/> - </xsl:attribute> - </xsl:element> - <xsl:element name="script"> - <xsl:attribute name="src"> - <xsl:value-of select="concat($basepath,'webinterface/${BUILD_NUMBER}/js/plotly.js')"/> - </xsl:attribute> - </xsl:element> - <xsl:element name="script"> - <xsl:attribute name="src"> - <xsl:value-of select="concat($basepath,'webinterface/${BUILD_NUMBER}/js/webcaosdb.js')"/> - </xsl:attribute> - </xsl:element> - <script> - $(document).ready(() => paging.initPaging(window.location.href, <xsl:value-of select="/Response/@count"/> )); - </script> - <xsl:element name="script"> - <xsl:attribute name="src"> - <xsl:value-of select="concat($basepath,'webinterface/${BUILD_NUMBER}/js/caosdb.js')"/> - </xsl:attribute> - </xsl:element> - <xsl:element name="script"> - <xsl:attribute name="src"> - <xsl:value-of select="concat($basepath,'webinterface/${BUILD_NUMBER}/js/form_elements.js')"/> - </xsl:attribute> - </xsl:element> - <xsl:element name="script"> - <xsl:attribute name="src"> - <xsl:value-of select="concat($basepath,'webinterface/${BUILD_NUMBER}/js/autocomplete.js')"/> - </xsl:attribute> - </xsl:element> - <xsl:element name="script"> - <xsl:attribute name="src"> - <xsl:value-of select="concat($basepath,'webinterface/${BUILD_NUMBER}/js/preview.js')"/> - </xsl:attribute> - </xsl:element> - <xsl:element name="script"> - <xsl:attribute name="src"> - <xsl:value-of select="concat($basepath,'webinterface/${BUILD_NUMBER}/js/ext_references.js')"/> - </xsl:attribute> - </xsl:element> - <xsl:element name="script"> - <xsl:attribute name="src"> - <xsl:value-of select="concat($basepath,'webinterface/${BUILD_NUMBER}/js/ext_xls_download.js')"/> - </xsl:attribute> - </xsl:element> - <xsl:element name="script"> - <xsl:attribute name="src"> - <xsl:value-of select="concat($basepath,'webinterface/${BUILD_NUMBER}/js/query_shortcuts.js')"/> - </xsl:attribute> - </xsl:element> - <xsl:element name="script"> - <xsl:attribute name="src"> - <xsl:value-of select="concat($basepath,'webinterface/${BUILD_NUMBER}/js/ext_jupyterdrag.js')"/> - </xsl:attribute> - </xsl:element> - <xsl:element name="script"> - <xsl:attribute name="src"> - <xsl:value-of select="concat($basepath,'webinterface/${BUILD_NUMBER}/js/annotation.js')"/> - </xsl:attribute> - </xsl:element> - <xsl:element name="script"> - <xsl:attribute name="src"> - <xsl:value-of select="concat($basepath,'webinterface/${BUILD_NUMBER}/js/edit_mode.js')"/> - </xsl:attribute> - </xsl:element> - <xsl:element name="script"> - <xsl:attribute name="src"> - <xsl:value-of select="concat($basepath,'webinterface/${BUILD_NUMBER}/js/leaflet.js')"/> - </xsl:attribute> - </xsl:element> - <xsl:element name="script"> - <xsl:attribute name="src"> - <xsl:value-of select="concat($basepath,'webinterface/${BUILD_NUMBER}/js/leaflet-graticule.js')"/> - </xsl:attribute> - </xsl:element> - <xsl:element name="script"> - <xsl:attribute name="src"> - <xsl:value-of select="concat($basepath,'webinterface/${BUILD_NUMBER}/js/leaflet-latlng-graticule.js')"/> - </xsl:attribute> - </xsl:element> - <xsl:element name="script"> - <xsl:attribute name="src"> - <xsl:value-of select="concat($basepath,'webinterface/${BUILD_NUMBER}/js/leaflet-coordinates.js')"/> + <xsl:value-of select="concat($basepath,'webinterface/${BUILD_NUMBER}/webcaosdb.dist.js')"/> </xsl:attribute> </xsl:element> - <xsl:element name="script"> - <xsl:attribute name="src"> - <xsl:value-of select="concat($basepath,'webinterface/${BUILD_NUMBER}/js/proj4.js')"/> - </xsl:attribute> - </xsl:element> - <xsl:element name="script"> - <xsl:attribute name="src"> - <xsl:value-of select="concat($basepath,'webinterface/${BUILD_NUMBER}/js/proj4leaflet.js')"/> - </xsl:attribute> - </xsl:element> - <xsl:element name="script"> - <xsl:attribute name="src"> - <xsl:value-of select="concat($basepath,'webinterface/${BUILD_NUMBER}/js/ext_map.js')"/> - </xsl:attribute> - </xsl:element> - <xsl:element name="script"> - <xsl:attribute name="src"> - <xsl:value-of select="concat($basepath,'webinterface/${BUILD_NUMBER}/js/tour.js')"/> - </xsl:attribute> - </xsl:element> - <xsl:element name="script"> - <xsl:attribute name="src"> - <xsl:value-of select="concat($basepath,'webinterface/${BUILD_NUMBER}/js/ext_bottom_line.js')"/> - </xsl:attribute> - </xsl:element> - <xsl:element name="script"> - <xsl:attribute name="src"> - <xsl:value-of select="concat($basepath,'webinterface/${BUILD_NUMBER}/js/ext_revisions.js')"/> - </xsl:attribute> - </xsl:element> - <xsl:element name="script"> - <xsl:attribute name="src"> - <xsl:value-of select="concat($basepath,'webinterface/${BUILD_NUMBER}/js/ext_sss_markdown.js')"/> - </xsl:attribute> - </xsl:element> - <xsl:element name="script"> - <xsl:attribute name="src"> - <xsl:value-of select="concat($basepath,'webinterface/${BUILD_NUMBER}/js/ext_trigger_crawler_form.js')"/> - </xsl:attribute> - </xsl:element> - <!--JS_EXTENSIONS--> </xsl:template> <xsl:template name="caosdb-data-container"> - <div class="container caosdb-f-main"> - <div class="row caosdb-v-main-col"> - <div class="panel-group caosdb-f-main-entities"> + <div class="container d-flex flex-column-reverse flex-lg-row caosdb-f-main"> + <div class="flex-grow-1 caosdb-f-main-entities"> <xsl:call-template name="paging-panel"/> <xsl:apply-templates select="/Response/UserInfo"/> <xsl:apply-templates mode="top-level-data" select="/Response/*"/> @@ -268,18 +113,16 @@ <xsl:if test="not(/Response/Query/Selection)"> <xsl:apply-templates mode="entities" select="/Response/*"/> </xsl:if> - <xsl:if test="count(/Response/*)<2 and not(/Response/Error|/Response/Info|/Response/Warning)"> - <xsl:call-template name="welcome"/> - </xsl:if> <xsl:call-template name="paging-panel"/> </div> - </div> - <div class="panel panel-warning caosdb-f-edit caosdb-v-edit-panel caosdb-v-edit-panel hidden"> - <div class="panel-heading"> - <h3 class="panel-title">Edit Mode Toolbox</h3> + <div class="caosdb-f-edit ms-2"> + <div class="card caosdb-v-edit-panel"> + <div class="card-header"> + <span class="card-title">Edit Mode Toolbox</span> + </div> + <div class="caosdb-f-edit-panel-body"></div> + </div> </div> - <div class="caosdb-f-edit-panel-body panel-body"></div> - </div> </div> </xsl:template> <xsl:template match="*" mode="entities"/> diff --git a/src/core/xsl/messages.xsl b/src/core/xsl/messages.xsl index 392d6e37e51e7426feafff8726382abb4012376b..42035c9942652c78714b46e0d99b2b8138b0eb3c 100644 --- a/src/core/xsl/messages.xsl +++ b/src/core/xsl/messages.xsl @@ -26,10 +26,9 @@ <xsl:template match="Error|Warning|Info"> <xsl:param name="class"/> <div> - <xsl:attribute name="class">alert - <xsl:value-of select="$class"/> alert-dismissable fade in</xsl:attribute> - <a class="close" data-dismiss="alert" href="#"> - <xsl:value-of select="$close-char"/> + <xsl:attribute name="class">alert caosdb-v-server-message + <xsl:value-of select="$class"/> alert-dismissable</xsl:attribute> + <a class="btn-close me-3" data-bs-dismiss="alert" href="#"> </a> <strong> <xsl:value-of select="name()"/> @@ -42,13 +41,13 @@ </xsl:template> <xsl:template match="script" mode="entities"> <div class="container" id="caosdb-container-script"> - <div class="panel panel-default"> - <div class="panel-heading"> + <div class="card"> + <div class="card-header"> <div class="row"> <div class="col-sm-8" id="caosdb-caption-script"> Output of the Script </div> - <div class="col-sm-4 text-right"> + <div class="col-sm-4 text-end"> Code: <span id="caosdb-return-code"><xsl:value-of select="@code"/></span></div> </div> </div> @@ -58,16 +57,16 @@ </div> </xsl:template> <xsl:template match="stderr"> - <div class="panel panel-default" id="caosdb-container-stderr"> - <div class="panel-heading" id="caosdb-caption-stderr">Errors:</div> + <div class="card" id="caosdb-container-stderr"> + <div class="card-header" id="caosdb-caption-stderr">Errors:</div> <div class="alert" id="caosdb-stderr"> <xsl:value-of select="text()"/> </div> </div> </xsl:template> <xsl:template match="stdout"> - <div class="panel panel-default" id="caosdb-container-stdout"> - <div class="panel-heading" id="caosdb-caption-stdout">Standard Messages:</div> + <div class="card" id="caosdb-container-stdout"> + <div class="card-header" id="caosdb-caption-stdout">Standard Messages:</div> <div id="caosdb-stdout"> <xsl:value-of select="text()"/> </div> diff --git a/src/core/xsl/navbar.xsl b/src/core/xsl/navbar.xsl index 907f27e512250cf5fde0ff2fe93fe85f800428a6..ee4df81be60558e6b6aa2e558096d7420636349f 100644 --- a/src/core/xsl/navbar.xsl +++ b/src/core/xsl/navbar.xsl @@ -30,7 +30,7 @@ <xsl:param name="display"/> <xsl:param name="paging" select="'0L10'"/> <li> - <a> + <a class="dropdown-item"> <xsl:attribute name="href"> <xsl:value-of select="concat($entitypath, '?all=', normalize-space($entity))"/> <xsl:if test="$paging"> @@ -42,104 +42,116 @@ </li> </xsl:template> <xsl:template name="caosdb-top-navbar"> - <!-- Some general settings first, current context should be the <body> node. --> - <xsl:if test="count(/Response/*)<2 and not(/Response/Error|/Response/Info|/Response/Warning)"> - <xsl:attribute name="class">caosdb-welcome</xsl:attribute> + <xsl:if test="/Response/@realm='${BUILD_MODULE_USER_MANAGEMENT_CHANGE_OWN_PASSWORD_REALM}'"> + <xsl:call-template name="change-password-modal"> + <xsl:with-param name="realm"><xsl:value-of select="/Response/@realm"/></xsl:with-param> + <xsl:with-param name="username"><xsl:value-of select="/Response/@username"/></xsl:with-param> + </xsl:call-template> </xsl:if> - <!-- Now the header follows. --> - <nav class="navbar navbar-default navbar-fixed-top"> + <nav class="navbar navbar-expand-lg navbar-light bg-light sticky-top mb-2 flex-column"> + <noscript>Please enable JavaScript!</noscript> <div class="container-fluid"> - <div class="navbar-header"> - <button class="navbar-toggle" data-target="#top-navbar" data-toggle="collapse" type="button"> - <span class="icon-bar"></span> - <span class="icon-bar"></span> - <span class="icon-bar"></span> - </button> - <a class="navbar-brand" href="/"> - <xsl:element name="img"> - <xsl:if test="'${BUILD_NAVBAR_BRAND_NAME}' != ''"> - <xsl:attribute name="class">caosdb-logo</xsl:attribute> - </xsl:if> - <xsl:attribute name="src"> - <xsl:value-of select="concat($basepath,'webinterface/${BUILD_NUMBER}/${BUILD_NAVBAR_LOGO}')"/> - </xsl:attribute> - </xsl:element> - ${BUILD_NAVBAR_BRAND_NAME} - </a> - </div> + <a class="navbar-brand" href="/"> + <xsl:element name="img"> + <xsl:if test="'${BUILD_NAVBAR_BRAND_NAME}' != ''"> + <xsl:attribute name="class">caosdb-logo</xsl:attribute> + </xsl:if> + <xsl:attribute name="src"> + <xsl:value-of select="concat($basepath,'webinterface/${BUILD_NUMBER}/${BUILD_NAVBAR_LOGO}')"/> + </xsl:attribute> + </xsl:element> + ${BUILD_NAVBAR_BRAND_NAME} + </a> + <button class="navbar-toggler" data-bs-target="#top-navbar" data-bs-toggle="collapse" type="button" aria-controls="top-navbar" aria-expanded="false" aria-label="Toggle navigation"> + <span class="navbar-toggler-icon"></span> + </button> <div class="collapse navbar-collapse" id="top-navbar"> - <xsl:if test="/Response/UserInfo"> - <ul class="nav navbar-nav caosdb-navbar"> - <li class="dropdown" id="caosdb-navbar-entities"> - <a class="dropdown-toggle" data-toggle="dropdown" href="#"> - Entities - <span class="caret"></span></a> - <ul class="dropdown-menu"> - <li class="dropdown-header">Retrieve all:</li> - <xsl:call-template name="make-retrieve-all-link"> - <xsl:with-param name="entity"> - Entity - </xsl:with-param> - <xsl:with-param name="display"> - Entities - </xsl:with-param> - </xsl:call-template> - <xsl:call-template name="make-retrieve-all-link"> - <xsl:with-param name="entity"> - Record - </xsl:with-param> - <xsl:with-param name="display"> - Records - </xsl:with-param> - </xsl:call-template> - <xsl:call-template name="make-retrieve-all-link"> - <xsl:with-param name="entity"> - RecordType - </xsl:with-param> - <xsl:with-param name="display"> - RecordTypes - </xsl:with-param> - </xsl:call-template> - <xsl:call-template name="make-retrieve-all-link"> - <xsl:with-param name="entity"> - Property - </xsl:with-param> - <xsl:with-param name="display"> - Properties - </xsl:with-param> - </xsl:call-template> - <xsl:call-template name="make-retrieve-all-link"> - <xsl:with-param name="entity"> - File - </xsl:with-param> - <xsl:with-param name="display"> - Files - </xsl:with-param> - </xsl:call-template> - </ul> - </li> - <li id="caosdb-navbar-filesystem"> + <ul class="navbar-nav caosdb-navbar me-auto"> + <li class="nav-item dropdown" id="caosdb-navbar-entities"> + <a class="nav-link dropdown-toggle" role="button" id="navbarEntitiesMenuLink" data-bs-toggle="dropdown" aria-expanded="false" href="#"> + Entities + </a> + <ul class="dropdown-menu dropdown-menu-light" aria-labelledby="navbarEntitiesMenuLink"> + <li class="dropdown-header">Retrieve all:</li> + <xsl:call-template name="make-retrieve-all-link"> + <xsl:with-param name="entity"> + Entity + </xsl:with-param> + <xsl:with-param name="display"> + Entities + </xsl:with-param> + </xsl:call-template> + <xsl:call-template name="make-retrieve-all-link"> + <xsl:with-param name="entity"> + Record + </xsl:with-param> + <xsl:with-param name="display"> + Records + </xsl:with-param> + </xsl:call-template> + <xsl:call-template name="make-retrieve-all-link"> + <xsl:with-param name="entity"> + RecordType + </xsl:with-param> + <xsl:with-param name="display"> + RecordTypes + </xsl:with-param> + </xsl:call-template> + <xsl:call-template name="make-retrieve-all-link"> + <xsl:with-param name="entity"> + Property + </xsl:with-param> + <xsl:with-param name="display"> + Properties + </xsl:with-param> + </xsl:call-template> + <xsl:call-template name="make-retrieve-all-link"> + <xsl:with-param name="entity"> + File + </xsl:with-param> + <xsl:with-param name="display"> + Files + </xsl:with-param> + </xsl:call-template> + </ul> + </li> + <li id="caosdb-navbar-filesystem" class="nav-item"> + <a class="nav-link" role="button"> <xsl:call-template name="make-filesystem-link"> <xsl:with-param name="href" select="'/'"/> <xsl:with-param name="display" select="'File System'"/> </xsl:call-template> - </li> - <li id="caosdb-navbar-query"> - <button class="navbar-btn btn btn-link" data-target="#caosdb-query-panel" data-toggle="collapse"> - Query - </button> - </li> - </ul> - </xsl:if> - <ul class="nav navbar-nav navbar-right"> - <xsl:call-template name="caosdb-user-menu"/> + </a> + </li> + <li id="caosdb-navbar-query" class="nav-item"> + <a class="nav-link" role="button" data-bs-target="#caosdb-query-panel-collapsible" data-bs-toggle="collapse"> + Query + </a> + </li> + </ul> + <ul class="navbar-nav"> + <li class="nav-item dropdown"> + <a class="nav-link dropdown-toggle" role="button" id="navbarBookmarkMenuLink" data-bs-toggle="dropdown" aria-expanded="false" href="#"> + <span id="caosdb-f-bookmarks-collection-counter" class="badge bg-secondary">0</span> + Bookmarks + </a> + <ul class="dropdown-menu dropdown-menu-light" aria-labelledby="navbarBookmarkMenuLink"> + <li class="disabled" id="caosdb-f-bookmarks-collection-link" + title="Show all bookmarked entities."> + <a class="dropdown-item">Show all</a></li> + <li class="disabled" id="caosdb-f-bookmarks-export-link" + title="Export all bookmarks to a file. The exported file is a spread sheet with columns for the id, the version, the complete URI of the bookmarked entities and the path, if the entity is a file."> + <a class="dropdown-item">Export to file</a></li> + <li class="disabled" id="caosdb-f-bookmarks-clear" + title="Empty the list of bookmarks."> + <a class="dropdown-item">Clear</a></li> + </ul> + </li> + <xsl:call-template name="caosdb-user-menu"/> </ul> - </div> - <!-- query panel --> - <div class="collapse" id="caosdb-query-panel"> - <xsl:call-template name="caosdb-query-panel"/> - </div> </div> + </div> + <!-- global messages --> <xsl:apply-templates select="/Response/Error"> <xsl:with-param name="class" select="'alert-danger'"/> </xsl:apply-templates> @@ -149,8 +161,13 @@ <xsl:apply-templates select="/Response/Info"> <xsl:with-param name="class" select="'alert-info'"/> </xsl:apply-templates> + <!-- query panel --> + <div class="collapse" id="caosdb-query-panel-collapsible" style="width: 100%"> + <div class="container py-2 py-sm-3 py-lg-4 py-xl-5 flex-column caosdb-query-panel" id="caosdb-query-panel"> + <xsl:call-template name="caosdb-query-panel"/> + </div> + </div> </nav> - <div class="container" id="subnav"/> </xsl:template> <xsl:template match="Role" name="caosdb-user-roles"> <div class="caosdb-user-role"> @@ -168,54 +185,100 @@ <xsl:apply-templates select="Roles/Role"/> </div> </xsl:template> + <xsl:template name="change-password-modal"> + <xsl:param name="realm"/> + <xsl:param name="username"/> + <div id="caosdb-f-change-password-form" class="modal fade" role="dialog"> + <div class="modal-dialog"> + <form class="modal-content" method="PUT"> + <input type="hidden" name="realm"><xsl:attribute name="value"><xsl:value-of select="$realm"/></xsl:attribute></input> + <input type="hidden" name="username"><xsl:attribute name="value"><xsl:value-of select="$username"/></xsl:attribute></input> + <div class="modal-header"> + <h4 class="modal-title">Set a new password</h4> + </div> + <div class="modal-body"> + <div class="form-group"> + <label>New Password + <input class="form-control" type="password" name="password" required="required"> + <xsl:attribute name="pattern">(?=.*[_\W])(?=.*\d)(?=.*[a-z])(?=.*[A-Z]).{8,}</xsl:attribute> + <xsl:attribute name="title">The new password must contain at least 8 characters, an uppercase letter (A-Z), a lowercase letter (a-z), a number (0-9), and a special character (!#$%'*+,-./:;?^_{|}~)</xsl:attribute> + </input> + </label> + </div> + <div class="form-group"> + <label>Repeat + <input class="form-control" type="password" name="password2" required="required"/> + </label> + </div> + <div class="checkbox"> + <label> + <input type="checkbox"/> + Show password + </label> + </div> + </div> + <div class="modal-footer"> + <button type="reset" class="btn btn-default" >Cancel</button> + <button type="submit" class="btn btn-default" >Submit</button> + </div> + </form> + </div> + </div> + </xsl:template> <xsl:template name="caosdb-user-menu"> <xsl:choose> <xsl:when test="/Response/@username"> - <li class="dropdown" id="user-menu"> - <a class="dropdown-toggle" data-toggle="dropdown" href="#"> + <li class="nav-item dropdown my-auto" id="user-menu"> + <a class="nav-link dropdown-toggle" role="button" data-bs-toggle="dropdown" href="#"> <xsl:value-of select="concat(/Response/@username,' ')"/> - <span class="glyphicon glyphicon-user"/> + <i class="bi-person-fill"></i> <span class="caret"></span> </a> - <ul class="dropdown-menu"> + <ul class="dropdown-menu dropdown-menu-light"> + <xsl:if test="/Response/@realm='${BUILD_MODULE_USER_MANAGEMENT_CHANGE_OWN_PASSWORD_REALM}'"> + <li> + <a title="Change your password." href="#" data-toggle="modal" data-target="#caosdb-f-change-password-form">Change Password</a> + </li> + </xsl:if> <li> - <a title="Click to logout."> + <a class="dropdown-item" title="Click to logout."> <xsl:attribute name="href"> <xsl:value-of select="concat($basepath, 'logout')"/> </xsl:attribute> - Logout - <span class="glyphicon glyphicon-log-out"/></a> + Logout <i class="bi-box-arrow-right"></i></a> </li> </ul> </li> </xsl:when> <xsl:otherwise> - <li id="user-menu"> - <form class="navbar-form" method="POST"> + <li id="user-menu" class="nav-item my-auto"> + <form id="caosdb-f-login-form" class="d-none" method="POST"> <xsl:attribute name="action"> <xsl:value-of select="concat($basepath, 'login')"/> </xsl:attribute> + <div class="row"> + <div class="col"> <input class="form-control" id="username" name="username" placeholder="username" type="text"/> + </div> + <div class="col"> <input class="form-control" id="password" name="password" placeholder="password" type="password"/> - <button class="btn btn-default" type="submit"> - <span class="glyphicon glyphicon-log-in"></span> - Login - </button> + </div> + <div class="col-auto"> + <button class="btn btn-primary" type="submit">Login</button> + </div> + </div> + </form> + <form class="my-auto"> + <button style="margin-right: 15px" class="btn btn-secondary navbar-btn d-inline-block" id="caosdb-f-login-show-button" type="button">Login</button> </form> </li> </xsl:otherwise> </xsl:choose> </xsl:template> <xsl:template name="paging-panel"> - <div class="container caosdb-paging-panel" style="display: none"> - <ul class="pager"> - <li class="previous"> - <a class="caosdb-prev-button">Previous Page</a> - </li> - <li class="next"> - <a class="caosdb-next-button">Next Page</a> - </li> - </ul> + <div class="caosdb-f-paging-panel mb-2"> + <a type="button" class="caosdb-prev-button btn btn-light">Previous Page</a> + <a type="button" class="caosdb-next-button btn btn-light">Next Page</a> </div> </xsl:template> </xsl:stylesheet> diff --git a/src/core/xsl/parent.xsl b/src/core/xsl/parent.xsl index 12345d584248333a373f7150a19b44a8b542be29..9dd032b3a9d238e943f3be5e4f0e81a7c6b0d6e7 100644 --- a/src/core/xsl/parent.xsl +++ b/src/core/xsl/parent.xsl @@ -27,18 +27,18 @@ <xsl:apply-templates select="./RecordType"/> </xsl:template> <xsl:template match="RecordType"> - <span class="caosdb-parent-item small"> - <xsl:attribute name="id"> - <xsl:value-of select="generate-id()"/> - </xsl:attribute> - <span class="caosdb-f-parent-actions-panel"> - </span> - <a class="caosdb-parent-name"> - <xsl:attribute name="href"> - <xsl:value-of select="concat($entitypath, @id)"/> - </xsl:attribute> - <xsl:value-of select="@name"/> - </a> + <span class="badge caosdb-parent-item me-1"> + <xsl:attribute name="id"> + <xsl:value-of select="generate-id()"/> + </xsl:attribute> + <span class="caosdb-f-parent-actions-panel"> + </span> + <a class="caosdb-parent-name"> + <xsl:attribute name="href"> + <xsl:value-of select="concat($entitypath, @id)"/> + </xsl:attribute> + <xsl:value-of select="@name"/> + </a> </span> </xsl:template> </xsl:stylesheet> diff --git a/src/core/xsl/query.xsl b/src/core/xsl/query.xsl index be49ed7d889e920cfee87e723c4c5c3b8efb27b2..2b647c07bebe7f7cd72198baf27e45318e25a18e 100644 --- a/src/core/xsl/query.xsl +++ b/src/core/xsl/query.xsl @@ -29,7 +29,7 @@ <xsl:apply-templates select="ParsingError"/> </xsl:template> <xsl:template match="ParseTree/ParsingError" mode="query-results"> - <div class="panel-body"> + <div class="card-body"> <div class="caosdb-overflow-box"> <div class="caosdb-overflow-content"> <span>ParseTree:</span> @@ -50,8 +50,8 @@ </div> </xsl:template> <xsl:template match="Query" mode="query-results"> - <div class="panel panel-default caosdb-query-response"> - <div class="panel-heading caosdb-query-response-heading"> + <div class="card caosdb-query-response mb-2"> + <div class="card-header caosdb-query-response-heading"> <div class="row"> <div class="col-sm-10 caosdb-overflow-box"> <div class="caosdb-overflow-content"> @@ -59,7 +59,7 @@ <xsl:value-of select="@string"/> </div> </div> - <div class="col-sm-2 text-right"> + <div class="col-sm-2 text-end"> <span>Results: </span> <span class="caosdb-query-response-results"> <xsl:value-of select="@results"/> @@ -68,7 +68,7 @@ </div> </div> <xsl:if test="@results=0"> - <div class="panel panel-default caosdb-no-results"> + <div class="card caosdb-no-results"> <div class="alert alert-warning" role="alert"> There were no results for this query. </div> @@ -81,36 +81,41 @@ </xsl:if> </xsl:template> <xsl:template match="Selection" mode="select-table"> - <div class="panel panel-default caosdb-select-table"> - <div class="panel-heading"> + <div class="card caosdb-select-table"> + <div class="card-header"> <div class="container-fluid panel-container"> <div class="col-xs-6"> <h5>Table of selected fields</h5> </div> - <div class="col-xs-6 text-right"> + <div class="col-xs-6 text-end"> <!-- Trigger the modal with a button --> - <button class="btn btn-info btn-sm" data-target="#downloadModal" data-toggle="modal" type="button">Download this table</button> + <button class="btn btn-info btn-sm caosdb-v-btn-select" data-bs-target="#downloadModal" data-bs-toggle="modal" type="button">Export</button> <!-- Modal --> <div class="modal fade text-left" id="downloadModal" role="dialog"> <div class="modal-dialog"> <!-- Modal content--> <div class="modal-content"> - <div class="modal-header"> - <button class="close" data-dismiss="modal" type="button">×</button> - <h4 class="modal-title">Download this table</h4> + <div class="modal-header align-middle"> + <h4 class="modal-title">Download this table</h4> + <button class="btn-close" data-bs-dismiss="modal" type="button"></button> </div> - <div class="modal-body"> + <div class="modal-body text-start"> <p> <a id="caosdb-f-query-select-data-tsv" onclick="downloadTSV(this)" href="#selected_data.tsv" download="selected_data.tsv"> - Download TSV File + Download table as TSV File </a> - <span class="checkbox" style="margin-top: 0; display: inline; position: absolute; right: 10px"><label><input type="checkbox" name="raw" id="caosdb-table-export-raw-flag-tsv" title="Export raw entity ids instead of the visible page content."/>raw</label></span> + <span class="form-check" style="margin-top: 0; display: inline; position: absolute; right: 1rem"><label><input type="checkbox" class="me-1" name="raw" id="caosdb-table-export-raw-flag-tsv" title="Export raw entity ids instead of the visible page content."/>raw</label></span> </p> <p> <a class="caosdb-v-query-select-data-xsl" onclick="downloadXLS(this)" href="#selected_data.xsl" download=""> - Download XLS File + Download table as XLS File + </a> + <span class="form-check" style="margin-top: 0; display: inline; position: absolute; right: 1rem"><label><input type="checkbox" class="me-1" name="raw" id="caosdb-table-export-raw-flag-xls" title="Export raw entity ids instead of the visible page content."/>raw</label></span> + </p> + <p> + <a id="caosdb-f-query-select-files" onclick="ext_file_download.download_files(this)" href="#selected_data.tsv" download="files.zip" title="Collects file entities listed in the table in a zip file. If the entity belonging to a row is a file entity, it will be included."> + Download files referenced in the table </a> - <span class="checkbox" style="margin-top: 0; display: inline; position: absolute; right: 10px"><label><input type="checkbox" name="raw" id="caosdb-table-export-raw-flag-xls" title="Export raw entity ids instead of the visible page content."/>raw</label></span> </p> <hr/> <p> @@ -123,7 +128,13 @@ </p> </div> <div class="modal-footer"> - <button class="btn btn-default" data-dismiss="modal" type="button">Close</button> + <div class="row" style="margin:0px"> + <div class="col-xs-6 caosdb-f-modal-footer-left"> + </div> + <div class="col-xs-6"> + <button class="btn btn-secondary" data-bs-dismiss="modal" type="button">Close</button> + </div> + </div> </div> </div> </div> @@ -131,59 +142,72 @@ </div> </div> </div> - <div class="caosdb-select-table-actions-panel text-right btn-group-xs"></div> - <div class="panel-body"> - <div class="table-responsive"> - <table class="table table-hover"> - <thead> - <tr> - <th></th> - <xsl:for-each select="Selector[@name!='id']"> - <th> - <xsl:value-of select="@name"/> - </th> - </xsl:for-each> - </tr> - </thead> - <tbody> - <xsl:for-each select="/Response/*[@id]"> - <xsl:call-template name="select-table-row"> - <xsl:with-param name="entity-id" select="@id"/> - </xsl:call-template> + <div class="caosdb-select-table-actions-panel text-end btn-group-sm"></div> + <div class="card-body"> + <table class="table ttable-responsive able-hover"> + <thead> + <tr> + <th></th> + <xsl:for-each select="Selector"> + <th> + <xsl:value-of select="@name"/> + </th> </xsl:for-each> - </tbody> - </table> - </div> + </tr> + </thead> + <tbody> + <xsl:for-each select="/Response/*[@id]"> + <xsl:call-template name="select-table-row"> + <xsl:with-param name="entity-id" select="@id"/> + <xsl:with-param name="version-id" select="Version/@id"/> + <xsl:with-param name="ishead" select="Version/@head"/> + </xsl:call-template> + </xsl:for-each> + </tbody> + </table> </div> </div> </xsl:template> <xsl:template name="entity-link"> <xsl:param name="entity-id"/> - <a class="btn btn-default btn-sm caosdb-select-id"> + <xsl:param name="version-id"/> + <xsl:param name="ishead"/> + <a class="btn btn-secondary btn-sm caosdb-select-id" title="Go to this entity."> <xsl:attribute name="href"> <xsl:value-of select="concat($entitypath, $entity-id)"/> + <xsl:if test="$version-id and not($ishead)"> + <xsl:value-of select="concat('@', $version-id)"/> + </xsl:if> </xsl:attribute> <!-- <xsl:value-of select="$entity-id" /> --> <span class="caosdb-select-id-target"> - <span class="glyphicon glyphicon-new-window"/> + <i class="bi bi-box-arrow-up-right"></i> </span> </a> </xsl:template> <xsl:template name="select-table-row"> <xsl:param name="entity-id"/> + <xsl:param name="version-id"/> + <xsl:param name="ishead"/> <tr> <xsl:attribute name="data-entity-id"> <xsl:value-of select="$entity-id"/> </xsl:attribute> + <xsl:attribute name="data-version-id"> + <xsl:value-of select="$version-id"/> + </xsl:attribute> <td> <xsl:call-template name="entity-link"> <xsl:with-param name="entity-id" select="$entity-id"/> + <xsl:with-param name="version-id" select="$version-id"/> + <xsl:with-param name="ishead" select="$ishead"/> </xsl:call-template> </td> <xsl:for-each select="/Response/Query/Selection/Selector"> <xsl:call-template name="select-table-cell"> <xsl:with-param name="entity-id" select="$entity-id"/> + <xsl:with-param name="version-id" select="$version-id"/> <xsl:with-param name="field-name" select="translate(@name, $uppercase, $lowercase)"/> </xsl:call-template> </xsl:for-each> @@ -192,13 +216,15 @@ <xsl:template name="select-table-cell"> <xsl:param name="entity-id"/> + <xsl:param name="version-id"/> <xsl:param name="field-name"/> <td class="caosdb-f-entity-property"> <xsl:attribute name="data-property-name"> <xsl:value-of select="$field-name"/> </xsl:attribute> <div class="caosdb-f-property-value caosdb-v-property-value"> - <xsl:apply-templates select="/Response/*[@id=$entity-id]" mode="walk-select-segments"> + <xsl:apply-templates select="/Response/*[@id=$entity-id and Version/@id=$version-id]" mode="walk-select-segments"> + <!--<xsl:apply-templates select="/Response/*[@id=$entity-id]" mode="walk-select-segments">--> <xsl:with-param name="first-segment"> <xsl:value-of select="substring-before(concat($field-name, '.'), '.')"/> </xsl:with-param> @@ -212,9 +238,30 @@ <xsl:template match="Property" mode="walk-select-segments"> <!-- handle properties --> + <xsl:param name="first-segment"/> <xsl:param name="next-segments"/> <xsl:choose> + <xsl:when test="@*[translate($first-segment, $uppercase, $lowercase)=name()]"> + <!--handle attributes--> + <xsl:call-template name="single-value"> + <xsl:with-param name="value"> + <xsl:value-of select="@*[translate(name(), $uppercase, $lowercase)=$first-segment]"/> + </xsl:with-param> + <xsl:with-param name="reference" select="'false'"/> + <xsl:with-param name="boolean" select="'false'"/> + </xsl:call-template> + </xsl:when> + + <xsl:when test="translate($first-segment, $uppercase, $lowercase)='version'"> + <!--handle version--> + <xsl:call-template name="single-value"> + <xsl:with-param name="value" select="Version/@id"/> + <xsl:with-param name="reference" select="'false'"/> + <xsl:with-param name="boolean" select="'false'"/> + </xsl:call-template> + </xsl:when> + <xsl:when test="$next-segments='value'"> <!--handle value--> <xsl:apply-templates mode="property-value" select="."/> @@ -223,9 +270,9 @@ <xsl:when test="translate($next-segments, $uppercase, $lowercase)='unit'"> <!--handle unit--> <xsl:call-template name="single-value"> - <xsl:with-param name="value"> - <xsl:value-of select="@unit"/> - </xsl:with-param> + <xsl:with-param name="value" select="@unit"/> + <xsl:with-param name="reference" select="'false'"/> + <xsl:with-param name="boolean" select="'false'"/> </xsl:call-template> </xsl:when> @@ -260,15 +307,24 @@ <xsl:with-param name="value"> <xsl:value-of select="@*[translate(name(), $uppercase, $lowercase)=$first-segment]"/> </xsl:with-param> + <xsl:with-param name="reference" select="'false'"/> + <xsl:with-param name="boolean" select="'false'"/> + </xsl:call-template> + </xsl:when> + + <xsl:when test="translate($first-segment, $uppercase, $lowercase)='version'"> + <!--handle version--> + <xsl:call-template name="single-value"> + <xsl:with-param name="value" select="Version/@id"/> + <xsl:with-param name="reference" select="'false'"/> + <xsl:with-param name="boolean" select="'false'"/> </xsl:call-template> </xsl:when> <xsl:when test="$next-segments"> <!-- when there is a next-segmenst --> <xsl:apply-templates select="Property[translate(@name, $uppercase, $lowercase)=$first-segment]" mode="walk-select-segments"> - <xsl:with-param name="next-segments"> - <xsl:value-of select="$next-segments"/> - </xsl:with-param> + <xsl:with-param name="next-segments" select="$next-segments"/> </xsl:apply-templates> </xsl:when> @@ -282,21 +338,17 @@ <xsl:template name="caosdb-query-panel"> <!-- query panel, this is the area which contains the query form and other related stuff (e.g. query short cuts). --> - <div class="container caosdb-query-panel"> - <form class="panel" id="caosdb-query-form" method="GET"> + <form class="card caosdb-query-form" id="caosdb-query-form" method="GET"> <xsl:attribute name="action"> <xsl:value-of select="$entitypath"/> </xsl:attribute> <input id="caosdb-query-paging-input" name="P" type="hidden" value="0L10"/> <div class="input-group"> <input class="form-control" id="caosdb-query-textarea" name="query" placeholder="E.g. 'FIND Experiment'" rows="1" style="resize: vertical;" type="text"></input> - <span class="input-group-addon btn btn-default caosdb-search-btn"> - <a href="#" title="Click to execute the query."> - <span class="glyphicon glyphicon-search"></span> + <a class="btn btn-secondary caosdb-search-btn" href="#" title="Click to execute the query."> + <i class="bi-search"></i> </a> - </span> </div> </form> - </div> </xsl:template> </xsl:stylesheet> diff --git a/src/core/xsl/welcome.xsl b/src/core/xsl/welcome.xsl index 88b2a6e733b6f0726cae02a83c6e88579fdd1918..249c2f8e873e83dc4db3132fc22152211fd87965 100644 --- a/src/core/xsl/welcome.xsl +++ b/src/core/xsl/welcome.xsl @@ -25,7 +25,7 @@ <xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform"> <xsl:output method="html"/> <xsl:template name="welcome"> - <div class="jumbotron caosdb-f-welcome-panel"> + <div class="caosdb-v-welcome-panel bg-light"> <h1>Welcome</h1> <p>This is CaosDB.</p> <p>This is the default welcome message. If you are an administrator you can override it. Just copy <code>src/core/xsl/welcome.xsl</code> to <code>src/ext/xsl/welcome.xsl</code> and change this content. Then run <code>make</code> again in CaosdDB's web interface's root directory.</p> diff --git a/src/doc/Makefile b/src/doc/Makefile new file mode 100644 index 0000000000000000000000000000000000000000..f3519f277badaf083c7f3512c64b18911ddf1f11 --- /dev/null +++ b/src/doc/Makefile @@ -0,0 +1,51 @@ +# ** header v3.0 +# This file is a part of the CaosDB Project. +# +# Copyright (C) 2020 IndiScale GmbH <info@indiscale.com> +# Copyright (C) 2020 Daniel Hornung <d.hornung@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/>. +# +# ** end header + +# This Makefile is a wrapper for sphinx scripts. +# +# It is based upon the autocreated makefile for Sphinx documentation. + +# You can set these variables from the command line, and also +# from the environment for the first two. +SPHINXOPTS ?= -a +SPHINXBUILD ?= sphinx-build +# SPHINXAPIDOC ?= javasphinx-apidoc +SOURCEDIR = . +BUILDDIR = ../../build/doc + +# npm is not always in the global PATH +NPM_PATH = $(shell npm bin) +NPM_PREFIX = $(shell npm prefix) + +.PHONY: doc-help Makefile api + +# Put it first so that "make" without argument is like "make help". +doc-help: + @$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) + +# Catch-all target: route all unknown targets to Sphinx using the new +# "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS). +%: Makefile api + PATH=$(NPM_PATH):$$PATH $(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) +# sphinx-build -M html . ../../build/doc + +api: + PATH=$(NPM_PATH):$$PATH jsdoc -t $(NPM_PREFIX)/node_modules/jsdoc-sphinx/template -d $@ -r "../../src/core" diff --git a/src/doc/administration/comments.rst b/src/doc/administration/comments.rst new file mode 100644 index 0000000000000000000000000000000000000000..f44561d12c247ae5c1c42d9ac1c912d13ab768e2 --- /dev/null +++ b/src/doc/administration/comments.rst @@ -0,0 +1,85 @@ +The comments feature of the caosdb webui +======================================== + +WebUI contains a feature that allows users to add comments to existing +records. + +The feature is not enabled by default. + +You can manually activate it using the following steps: - Add a new +RecordType (e.g. using the Edit Mode) called “Annotation†- Add a new +RecordType called “CommentAnnotation†with parent “Annotation†- Add a +new TEXT Property called “comment†- Add a new REFERENCE Property called +“annotationOf†+ +or using the following XML: + +.. code:: xml + + <Property id="-1" name="comment" description="A comment on something." datatype="TEXT"> + </Property> + + <Property id="-2" name="annotationOf" description="The core property of the [Annotation] denoting which entity the annotation is annotating." datatype="REFERENCE"> + </Property> + + <RecordType id="-3" name="Annotation" description="Annotations annotate other entities in order to represent information about these entities without changing them. Mostly this will be comments by users on these entities or flags used by third party programs."> + <Property id="-2" name="annotationOf" description="The core property of the [Annotation] denoting which entity the annotation is annotating." datatype="REFERENCE" importance="OBLIGATORY"> + </Property> + </RecordType> + + <RecordType name="CommentAnnotation" description="CommentAnnotations represent user comments on other entities. As they are entities themselves they can be 'responded' by just annotating them with another CommentAnnotation."> + <Parent id="-3" name="Annotation" description="Annotations annotate other entities in order to represent information about these entities without changing them. Mostly this will be comments by users on these entities or flags used by third party programs." /> + <Property id="-1" name="comment" description="A comment on something." datatype="TEXT" importance="OBLIGATORY"> + </Property> + </RecordType> + +Additionally, on some servers the comment button might be disabled using +CSS. + +E.g. on the demo server you would have to comment out the following +lines in ``demoserver.css``: + +.. code:: css + + .caosdb-new-comment-button { + visibility: hidden; + } + +Using the YAML-Datamodel-Interface +---------------------------------- + +It’s even easier to add the model using the yaml interface. Use the +following yaml file: + +.. code:: yaml + + + Annotation: + description: Annotations annotate other entities in order to represent information about these entities without changing them. Mostly this will be comments by users on these entities or flags used by third party programs. + obligatory_properties: + annotationOf: + description: The core property of the [Annotation] denoting which entity the annotation is annotating. + datatype: REFERENCE + + CommentAnnotation: + description: CommentAnnotations represent user comments on other entities. As they are entities themselves they can be 'responded' by just annotating them with another CommentAnnotation. + inherit_from_obligatory: + - Annotation + obligatory_properties: + comment: + description: A comment on something. + datatype: TEXT + +Save this file under “datamodel.yamlâ€. + +Make sure you have installed caosdb-models. + +Then sync the model: + +.. code:: python + + import caosdb as db + from caosmodels.parser import parse_model_from_yaml + + model = parse_model_from_yaml("datamodel.yaml") + model.sync_data_model(noquestion=True) diff --git a/src/doc/administration/index.rst b/src/doc/administration/index.rst new file mode 100644 index 0000000000000000000000000000000000000000..e04e1dea523f1d9d8aa83231429a9347ecbfb4de --- /dev/null +++ b/src/doc/administration/index.rst @@ -0,0 +1,10 @@ +Administration +============== + +.. toctree:: + :maxdepth: 2 + :caption: Contents: + :hidden: + + comments + static-snapshots diff --git a/src/doc/administration/static-snapshots.md b/src/doc/administration/static-snapshots.md new file mode 100644 index 0000000000000000000000000000000000000000..b7f292ad37d4a35f63aed406600190e988bf84a6 --- /dev/null +++ b/src/doc/administration/static-snapshots.md @@ -0,0 +1,50 @@ +# Creating Static WebUI Snapshots + +It can be helpful to generate static snapshots of WebUI contents, e.g. for reviewing layouts or for presentation purposes. This is possible with a little bit of effort. Excitingly not only the layout can be exported, but also a lot of the javascript functionality can be maintained in the static pages. + +**NOTE: This manual page is currently work in progress.** + +## Create the static webui folder in the docker container + +We need a static version of the caosdb-webui. In principle it can be simply copied from e.g. a running docker container or from the public-directory. As it contains self-referencing (cyclic) symlinks a little bit of care has to be taken. + +### Using Docker + +Login to the caosdb/linkahead docker container as root: +```bash +docker exec -u 0 -ti linkahead /bin/bash +``` + +We need to be root (`-u 0`) in order to be able to create a copy of caosdb-webui within the container. + +Create the copy using `cp` and the option for following symlinks `-L`: + +```bash +cp -L git/caosdb-server/caosdb-webui/public/ webui-copy +``` + +It will warn you that two symlinks (which are cyclic) cannot be created. That's fine, we will create these two symlinks later. + +``` +cp: cannot copy cyclic symbolic link 'git/caosdb-server/caosdb-webui/public/1602145811' <- The number here is a "unique" build number +cp: cannot copy cyclic symbolic link 'git/caosdb-server/caosdb-webui/public/webinterface' +``` + +**Please copy the build number somewhere, or make sure your terminal history does not get wiped.** + +Copy webui-copy from the docker container to the location where you want to store the snapshots: +`docker cp linkahead:/opt/caosdb/webui-copy/ .` + +Create the two missing symlinks in webui-copy/public: +``` +ln -s webui-copy/public webui-copy/public/1602145811 +ln -s webui-copy/public webui-copy/public/webinterface +``` + +You can now use the included xslt stylesheet to convert xml files to html using: +```bash +xsltproc webui-copy/public/webcaosdb.xsl test.xml > test.html +``` + +As the generated html file still contains invalid references to `/webinterface/1602145811` +you have to replace all occurences of `/webinterface` with webui-copy/public`. diff --git a/src/doc/concepts.rst b/src/doc/concepts.rst new file mode 100644 index 0000000000000000000000000000000000000000..23e5fc4f6ddb666757fb9c79e192e07ffed8fb44 --- /dev/null +++ b/src/doc/concepts.rst @@ -0,0 +1,6 @@ +======================== +The concepts of pycaosdb +======================== + +Some text... + diff --git a/src/doc/conf.py b/src/doc/conf.py new file mode 100644 index 0000000000000000000000000000000000000000..8e627dd0c26f9d760c549abfab4cbc824baa9912 --- /dev/null +++ b/src/doc/conf.py @@ -0,0 +1,212 @@ +# -*- coding: utf-8 -*- +# +# Configuration file for the Sphinx documentation builder. +# +# This file only contains a selection of the most common options. For a full +# list see the documentation: +# https://www.sphinx-doc.org/en/master/usage/configuration.html + +# -- Path setup -------------------------------------------------------------- + +# If extensions (or modules to document with autodoc) are in another directory, +# add these directories to sys.path here. If the directory is relative to the +# documentation root, use os.path.abspath to make it absolute, like shown here. +# +# import os +# import sys +# sys.path.insert(0, os.path.abspath('../caosdb')) + + +# -- Project information ----------------------------------------------------- + +import sphinx_rtd_theme + +project = 'caosdb-webui' +copyright = '2020, IndiScale GmbH' +author = 'Daniel Hornung' + +# The short X.Y version +version = '0.X.Y' +# The full version, including alpha/beta/rc tags +release = '0.x.y-beta-rc2' + + +# -- General configuration --------------------------------------------------- + +# If your documentation needs a minimal Sphinx version, state it here. +# +# needs_sphinx = '1.0' + +# Add any Sphinx extension module names here, as strings. They can be +# extensions coming with Sphinx (named 'sphinx.ext.*') or your custom +# ones. +extensions = [ +# 'sphinx_js', + 'sphinx.ext.todo', + "sphinx.ext.autodoc", +# 'autoapi.extension', + "recommonmark", # For markdown files. + "sphinx_rtd_theme", + 'sphinx.ext.intersphinx', + 'sphinx.ext.mathjax', + 'sphinx.ext.ifconfig', + # 'sphinx.ext.napoleon', # For Google style docstrings +] + +# Add any paths that contain templates here, relative to this directory. +templates_path = ['_templates'] + +# The suffix(es) of source filenames. +# You can specify multiple suffix as a list of string: +# +# source_suffix = ['.rst', '.md'] +source_suffix = '.rst' + +# The master toctree document. +master_doc = 'index' + +# The language for content autogenerated by Sphinx. Refer to documentation +# for a list of supported languages. +# +# This is also used if you do content translation via gettext catalogs. +# Usually you set "language" from the command line for these cases. +language = None + +# List of patterns, relative to source directory, that match files and +# directories to ignore when looking for source files. +# This pattern also affects html_static_path and html_extra_path. +exclude_patterns = [] + +# The name of the Pygments (syntax highlighting) style to use. +pygments_style = None + + +# -- Options for HTML output ------------------------------------------------- + +# The theme to use for HTML and HTML Help pages. See the documentation for +# a list of builtin themes. +# +html_theme = "sphinx_rtd_theme" + +# Theme options are theme-specific and customize the look and feel of a theme +# further. For a list of options available for each theme, see the +# documentation. +# +# html_theme_options = {} + +# Add any paths that contain custom static files (such as style sheets) here, +# relative to this directory. They are copied after the builtin static files, +# so a file named "default.css" will overwrite the builtin "default.css". +# html_static_path = ['_static'] + +# Custom sidebar templates, must be a dictionary that maps document names +# to template names. +# +# The default sidebars (for documents that don't match any pattern) are +# defined by theme itself. Builtin themes are using these templates by +# default: ``['localtoc.html', 'relations.html', 'sourcelink.html', +# 'searchbox.html']``. +# +# html_sidebars = {} + + +# -- Options for HTMLHelp output --------------------------------------------- + +# Output file base name for HTML help builder. +htmlhelp_basename = 'caosdb-webuidoc' + + +# -- Options for LaTeX output ------------------------------------------------ + +latex_elements = { + # The paper size ('letterpaper' or 'a4paper'). + # + # 'papersize': 'letterpaper', + + # The font size ('10pt', '11pt' or '12pt'). + # + # 'pointsize': '10pt', + + # Additional stuff for the LaTeX preamble. + # + # 'preamble': '', + + # Latex figure (float) alignment + # + # 'figure_align': 'htbp', +} + +# Grouping the document tree into LaTeX files. List of tuples +# (source start file, target name, title, +# author, documentclass [howto, manual, or own class]). +latex_documents = [ + (master_doc, 'caosdb-webui.tex', 'caosdb-webui Documentation', + 'IndiScale GmbH', 'manual'), +] + + +# -- Options for manual page output ------------------------------------------ + +# One entry per manual page. List of tuples +# (source start file, name, description, authors, manual section). +man_pages = [ + (master_doc, 'caosdb-webui', 'caosdb-webui Documentation', + [author], 1) +] + + +# -- Options for Texinfo output ---------------------------------------------- + +# Grouping the document tree into Texinfo files. List of tuples +# (source start file, target name, title, author, +# dir menu entry, description, category) +texinfo_documents = [ + (master_doc, 'caosdb-webui', 'caosdb-webui Documentation', + author, 'caosdb-webui', 'One line description of project.', + 'Miscellaneous'), +] + + +# -- Options for Epub output ------------------------------------------------- + +# Bibliographic Dublin Core info. +epub_title = project + +# The unique identifier of the text. This can be a ISBN number +# or the project homepage. +# +# epub_identifier = '' + +# A unique identification for the text. +# +# epub_uid = '' + +# A list of files that should not be packed into the epub file. +epub_exclude_files = ['search.html'] + + +# -- Extension configuration ------------------------------------------------- + +# -- Options for sphinx-js --------------------------------------------------- +# See also https://pypi.org/project/sphinx-js/ + +js_source_path = '../core/js/' +primary_domain = 'js' # Not strictly necessary? + +# -- Options for intersphinx extension --------------------------------------- + +# Example configuration for intersphinx: refer to the Python standard library. +intersphinx_mapping = {'https://docs.python.org/': None} + +# TODO Which options do we want? +autodoc_default_options = { + 'members': None, + 'undoc-members': None, +} + +# -- Options for sphinx-autoapi ---------------------------------------------- +# See also https://pypi.org/project/sphinx-js/ + +autoapi_type = 'javascript' +autoapi_dirs = ['../core/js/'] +autoapi_add_toctree_entry = False diff --git a/src/doc/extension.rst b/src/doc/extension.rst new file mode 100644 index 0000000000000000000000000000000000000000..18fd0f25a8ce75cd19edfa845b06f5c45bd3fd20 --- /dev/null +++ b/src/doc/extension.rst @@ -0,0 +1,13 @@ + +Extending the CaosDB Web Interface +================================== + +Here we collect information on how to extend the web interface as a developer. + +.. toctree:: + :maxdepth: 1 + :glob: + + extension/* + + diff --git a/src/doc/extension/forms.rst b/src/doc/extension/forms.rst new file mode 100644 index 0000000000000000000000000000000000000000..1bced612b5f9517c5ec149871cbf53321b4671d4 --- /dev/null +++ b/src/doc/extension/forms.rst @@ -0,0 +1,80 @@ + +Creating forms for the CaosDB Web Interface +=========================================== + +The ``form_elements`` module provides a library for generating forms from simple config objects. The forms are styled for the seamless integration into the CaosDB web interface and are especially useful for calling server side scripts. + +See also the :doc:`API documentation <../api/module-form_elements>` + +Examples +-------- + +Generating a generic form +^^^^^^^^^^^^^^^^^^^^^^^^^ + +The following code snippet adds a form to the body of the HTML document. + +.. code-block:: javascript + + function my_special_submit_handler (form) { + // handle form submision + }; + const config = { + name: "my_form", + fields: [ + { type: "reference_drop_down", name: "experiment_id", label: "Experiment", query: "FIND Record Experiment", required: true }, + { type: "integer", name: "number", label: "A Number", required: true }, + { type: "date", name: "date", label: "A Date", required: false }, + { type: "text", name: "comment", label: "A Comment", required: false }, + ], + submit: my_special_submit_handler + }; + const form = form_elements.make_form(config); + $("body").append(form); + +The form has four fields: + + 1. A drop-down menu which contains all Records of type "Experiment" as options, + 2. an integer field, labeled "A Number", + 3. a date field, labeled "A Date", and + 4. a text field, labeled "A Comment". + +The first two fields are required and the form cannot be submitted without it. The latter are optional. + +On submission, the function ``my_special_submit_handler`` is being called with the form element as only parameter. + +As the generated form is a plain HTML form, the javascript form API can be used. However, there are special methods in the ``form_elements`` module e.g. :doc:`get_fields <../api/module-form_elements>` which are especially designed to interact with the forms generated by the ``make_form`` factory. + +Calling a server-side script +^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +If you intend to call a server-side script, the config has to be changed a litte bit and the script calling is done by the ``form_elements`` module. There is no need to define the submit_hander anymore. Instead, just name the script which is to be called. + +.. code-block:: javascript + + const config = { + script: "process.py", + fields: [ + { type: "reference_drop_down", name: "experiment_id", label: "Experiment", query: "FIND Record Experiment", required: true }, + { type: "integer", name: "number", label: "A Number", required: true }, + { type: "date", name: "date", label: "A Date", required: false }, + { type: "text", name: "comment", label: "A Comment", required: false }, + ], + }; + const form = form_elements.make_form(config); + $("body").append(form); + +On submission, the form data will be send as a json file to the script and passed as the first parameter. The call would look like ``./process.py form.json`` and the file would contain, for example, + +.. code-block:: json + + { + "experiment_id": "234234", + "number": "400", + "date": "2020-12-24", + "comment": "This is a comment", + } + +For more and advanced options for the form see the :doc:`API documentation <../api/module-form_elements>` + + diff --git a/src/doc/extension/module.md b/src/doc/extension/module.md new file mode 100644 index 0000000000000000000000000000000000000000..d4f9b56db508d7c3be9dc0a52953de5c82bf310d --- /dev/null +++ b/src/doc/extension/module.md @@ -0,0 +1,72 @@ +# How to add a module to CaosDB WebUI +The CaosDB WebUI is organized in modules which can easily be added and on a module basis enabled or disabled. + +There are a few steps necessary to create a new module. + +## Create the module file +Create a new file in `src/core/js` starting with `ext_`. E.g. `ext_flight_preview.js`. This file should define one function that wraps every thing and which is enabled at the bottom of the file: + +```js +/* + * ** header with license infoc + * ... + */ + +'use strict'; + +/** + * description of the module ... + * + * @module ext_flight_preview + * @version 0.1 + * + * @requires somelibrary + * (pass the dependencies as arguments) + */ +var ext_flight_preview = function (somelibrary) { + + var init = function (toolbox) { + /* initialization of the module */ + } + + /** + * doc string + */ + var some_function = function (arg1, arg2) { + } + + /* the main function must return the initialization of the module */ + return { + init: init, + }; +//pass the dependencies as arguments here as well +}(somelibrary); + +// this will be replaced by require.js in the future. +$(document).ready(function() { + // use a variable starting with `BUILD_MODULE_` to enable your module + // the build variable has to be enabled in the `build.properties.d/` directory. + // Otherwise the module will not be activated. + if ("${BUILD_MODULE_EXT_BOTTOM_LINE}" === "ENABLED") { + caosdb_modules.register(ext_flight_preview); + } +}); +``` +## Update xml +Add a section to `src/core/xsl/main.xsl` to include your new file. + +```xsl +<xsl:element name="script"> + <xsl:attribute name="src"> + <xsl:value-of select="concat($basepath,'webinterface/${BUILD_NUMBER}/js/ext_sss_markdown.js')"/> + </xsl:attribute> +</xsl:element> +``` + +## Add to index.html in test +If you have unittests (and you should), you need to add a line in : +`test/core/index.html`. + +## Update the changelog + +## Create a merge request \ No newline at end of file diff --git a/src/doc/extension/xslt-debugging.md b/src/doc/extension/xslt-debugging.md new file mode 100644 index 0000000000000000000000000000000000000000..80443dd8656da5645a5315f83f5ce26dfbbebce0 --- /dev/null +++ b/src/doc/extension/xslt-debugging.md @@ -0,0 +1,33 @@ +# XSLT Debugging + +The CaosDB WebUI uses [XSLT](https://en.wikipedia.org/wiki/XSLT) to transform the servers response into a web page. +In the webui-repository these XSLT stylesheets can be found in `src/core/` and `src/core/xsl`. + +The XSLT stylesheet is typically interpreted on the client side, e.g. in Mozilla Firefox. Error output of the browser regarding XSLT problems are typically hard to debug. For example, Firefox typically does not print detailed information about the location of an exception in the sourcecode. + +So what options do we have to debug xslt stylesheets? + +* So called "printf-style" debugging +* Using a different xslt processor + +I found this thread on Stack Overflow very helpful: +https://stackoverflow.com/questions/218522/tools-for-debugging-xslt + +# "printf-style" debugging + +As mentioned in the Stack Overflow thread referenced above, `<xsl:message>` can be used to output debugging messages during XSLT processing. + +# Using different XSLT processors + +## xsltproc from libxslt + +`xsltproc` is a tool from libxslt that allows transforming XML using XSLT stylesheets on the command line. It is called using: +```bash +xsltproc <stylesheet> <xmlfile> +``` + +So a possible workflow for debugging an xslt script could be: +* Save the test response from the server as `test.xml`. +* Run `make` in repository `caosdb-webui` +* Go to folder `public` in `caosdb-webui` +* Run: `xsltproc webcaosdb.xsl test.xml` diff --git a/src/doc/genindex.rst b/src/doc/genindex.rst new file mode 100644 index 0000000000000000000000000000000000000000..48ab71fd283bb48564ac30e4c69e62bbd463cd77 --- /dev/null +++ b/src/doc/genindex.rst @@ -0,0 +1,4 @@ +.. This file is a placeholder and will be replaced. + +Index +===== diff --git a/src/doc/getting_started.md b/src/doc/getting_started.md new file mode 120000 index 0000000000000000000000000000000000000000..88332e357f5e06f3de522768ccdcd9e513c15f62 --- /dev/null +++ b/src/doc/getting_started.md @@ -0,0 +1 @@ +../../README_SETUP.md \ No newline at end of file diff --git a/src/doc/index.rst b/src/doc/index.rst new file mode 100644 index 0000000000000000000000000000000000000000..24c394349a045bf276c4252a2fde47feae6f533c --- /dev/null +++ b/src/doc/index.rst @@ -0,0 +1,26 @@ + +Welcome to the documentation of CaosDB's web UI! +================================================ + +.. toctree:: + :maxdepth: 2 + :caption: Contents: + :hidden: + + Getting started <getting_started> + Tutorials <tutorials/index> + Concepts <concepts> + administration/index.rst + Extending the UI <extension> + API <api/index> + + +This documentation helps you to :doc:`get started<getting_started>`, explains the most important +:doc:`concepts<concepts>` and offers a range of :doc:`tutorials<tutorials>`. + + +Indices and tables +================== + +* :ref:`genindex` +* :ref:`search` diff --git a/src/doc/tutorials/change-entity.png b/src/doc/tutorials/change-entity.png new file mode 100644 index 0000000000000000000000000000000000000000..ddae63fa841976709b2dcf98389e578d79998f03 Binary files /dev/null and b/src/doc/tutorials/change-entity.png differ diff --git a/src/doc/tutorials/delete-entity-button.png b/src/doc/tutorials/delete-entity-button.png new file mode 100644 index 0000000000000000000000000000000000000000..c118b8217a63d00eeee6469731d8561810a8d7d4 Binary files /dev/null and b/src/doc/tutorials/delete-entity-button.png differ diff --git a/src/doc/tutorials/edit-entity-button.png b/src/doc/tutorials/edit-entity-button.png new file mode 100644 index 0000000000000000000000000000000000000000..79003121df69cc615890589941079e766f970775 Binary files /dev/null and b/src/doc/tutorials/edit-entity-button.png differ diff --git a/src/doc/tutorials/edit-mode-button.png b/src/doc/tutorials/edit-mode-button.png new file mode 100644 index 0000000000000000000000000000000000000000..28dd167a5887be45d6c41581a4645e796c3b6328 Binary files /dev/null and b/src/doc/tutorials/edit-mode-button.png differ diff --git a/src/doc/tutorials/edit-mode-toolbox.png b/src/doc/tutorials/edit-mode-toolbox.png new file mode 100644 index 0000000000000000000000000000000000000000..e291c43a77e41adfa9f2b821d90f9982449c5f9f Binary files /dev/null and b/src/doc/tutorials/edit-mode-toolbox.png differ diff --git a/src/doc/tutorials/edit_mode.rst b/src/doc/tutorials/edit_mode.rst new file mode 100644 index 0000000000000000000000000000000000000000..01502f55d5d76959a17bf74c71632d0d902f625e --- /dev/null +++ b/src/doc/tutorials/edit_mode.rst @@ -0,0 +1,193 @@ +The Edit Mode +============= + +Entities in CaosDB can be changed, created, and deleted using the +``Edit Mode``. In the following chapter, you'll learn how. You should +be fairly familiar with the concepts of Records, RecordTypes and +Properties in CaosDB. If you have doubts, please have a look at the +`data model documentation +<https://docs.indiscale.com/caosdb-server/Data-Model.html>`_. + +In usual setups of CaosDB, you have to log in to use the edit +mode. Afterwards, you can access it by clicking on the button in the +to panel as shown below: + +.. image:: edit-mode-button.png + :width: 480 + :alt: Edit mode button + +After entering the edit mode, the button changes its text to ``Leave +Edit Mode``. Unsurprisingly, clicking here terminates the edit mode. + +.. note:: + + The edit mode is only available if you have sufficient + privileges. You can only create/edit/delete entities if your user + is allowed to do that. User and group permissions can be configured + in detail as explained in the `server documentation + <https://docs.indiscale.com/caosdb-server/Permissions.html>`_. + +When you have entered the edit mode, you'll see the edit mode toolbox +appearing on the right hand side of your screen: + +.. image:: edit-mode-toolbox.png + :width: 240 + :alt: Edit mode toolbox + +You'll learn more about its contents in the following sections. Right +now it only contains the options for creating a new RecordType or +Property which we'll explain in :ref:`new_recordtypes_properties`. + +.. _change_existing: + +Changing an existing Entity +--------------------------- + +We'll start by changing and updating existing entities. First, find +the entity you want to change and enter the edit mode. You'll see an +edit button in the top right of the entity card: + +.. image:: edit-entity-button.png + :width: 240 + :alt: Edit entity button + +After clicking on this button, the edit menu for this entity is opened +as shown below for a guitar Record from the `demo +<https://demo.indiscale.com>`_. You'll also note that the edit mode +toolbox changes its contents: It now harbours two lists of the +existing Properties and RecordTypes. + +.. image:: change-entity.png + :width: 720 + :alt: Changing an existing Record + +Property values can be changed in the entity card directly; additional +parents can be added by dragging them from the list of RecordTypes to +the corresponding area at the top of the Record. Similarly, Properties +can be added by dragging Properties (or RecordTypes) from the list in the edit mode toolbox +to the corresponding area at the bottom of the Record. Properties and +parents can be removed from the entity by clicking on the trash-can +symbol. Not that a Record must always have at least one parent. + +Changes will be applied after clicking on ``Save`` or can be discarded +entirely by clicking ``Cancel``. Existing Properties and RecordTypes +can be edited in the same way. Note that when changing a RecordType, +the properties don't have values. + + +Creating a new Record +--------------------- + +If you want to create a new Record of a given RecordType, visit that +RecordType and enter the edit mode (if your new Record will have more +than one parent, visit any one of them - you can add the others +later). A new Record is then created by clicking on the ``+Record`` +button in the top right of the RecordType: + +.. image:: new-record.png + :width: 240 + :alt: New record button + +Clicking here opens an entity card with an edit menu for the new +Record similar to the one discussed in :ref:`change_existing`. In +here, you can enter the name and the description of your new Record, +assign values to its properties, and add further parents or properties +from the corresponding lists in the edit mode toolbox. The new Record +is inserted by clicking ``Save``. + +.. _new_recordtypes_properties: + +Creating new RecordTypes and Properties +--------------------------------------- + +You can extend the data model of your CaosDB by creating new +RecordTypes and Properties directly from the WebUI. This is done by +clicking on the corresponding buttons in the edit mode toolbox: + +.. image:: edit-mode-toolbox.png + :width: 240 + :alt: Edit mode toolbox + +When creating a new RecordType, a RecordType card is added to the +entity panel, similar to the new Record explained above: + +.. image:: new-recordtype.png + :width: 720 + :alt: Create a new RecordType + +As above, you can enter a name and a description. You can add parents +and properties by selecting them from the lists in the edit mode +toolbox and dragging them to the respective areas in the new +RecordType. Note that in contrast to Records, the properties of +RecordTypes do not have values. + +When creating a new property name and description can be entered as +above. In addition, the `datatype` can be selected from the CaosDB +datatypes ``TEXT``, ``DOUBLE``, ``INTEGER``, ``DATETIME``, +``BOOLEAN``, ``FILE``, and ``REFERENCE``. See `here +<https://docs.indiscale.com/caosdb-server/specification/Datatype.html>`_ +for more information on the datatypes. You can also choose whether the +new property should have a single value or a list of values. + +.. image:: new-property.png + :width: 720 + :alt: Create a new Property + +When creating a property with datatype ``INTEGER`` or ``DOUBLE``, +i.e., a number, you may enter a unit in an additional input field if +applicable. In case of a ``REFERENCE`` property, you may specify the +RecordType that all referenced Records must have. In the above example +a ``REFERENCE`` property is created which may only have violins as +values. Again, the new entity is created by clicking on ``save``. + +.. note:: + + After having created a new RecordType or Property, you may have to + leave and re-enter the edit mode for the new entity to appear in + the lists of Properties or RecordTypes in the edit mode toolbox. + +Deleting an Entity +------------------ + +Entities can also be deleted by clicking on the delete button in the +top right of the entity: + +.. image:: delete-entity-button.png + :width: 240 + :alt: Delete entity button + +After clicking on ``delete`` you'll be asked for confirmation. Note that +entities cannot be deleted if they are needed by other entities, e.g., +as a reference. + + +Uploading files +--------------- + +In case of properties with data type ``FILE``, you can use the edit +mode to upload a corresponding file directly. When editing the Record +which will have the file to be uploaded as the value of the +corresponding property (named ``SourceFile`` in the example below), +add the property as described above if it isn't present already. Next +to the dropdown menu, in which you can choose from existing files, you +find an uploaded button: + +.. image:: file-upload.png + :width: 720 + :alt: File upload button + +Click on it to open an upload dialogue in which you can choose the +file that you want to upload. The files uploaded this way will be +stored within ``/uploaded.by/<REALM>/<USER>/``. + +The same is true for properties with data type ``REFERENCE``, too. In +that case, the Record of the file that is uploaded will be assigned the +RecordType of value of the original reference property. + +.. warning:: + + Until `this bug + <https://gitlab.indiscale.com/caosdb/src/caosdb-webui/-/issues/200>`_ + has been fixed, the upload button is broken and does not open the + upload dialogue. + diff --git a/src/doc/tutorials/file-upload.png b/src/doc/tutorials/file-upload.png new file mode 100644 index 0000000000000000000000000000000000000000..85c9224a6453cc219e7510b13fb1c5869f7c01c6 Binary files /dev/null and b/src/doc/tutorials/file-upload.png differ diff --git a/src/doc/tutorials/first_steps.rst b/src/doc/tutorials/first_steps.rst new file mode 100644 index 0000000000000000000000000000000000000000..48126b2aab6175da557919133493585eda59bbfa --- /dev/null +++ b/src/doc/tutorials/first_steps.rst @@ -0,0 +1,87 @@ +First Steps +=========== + +Before using or even manipulating data stored in CaosDB, it is important to +understand the way data is structured. Here, we will briefly look at this +structure. You can find more details here_. In CaosDB data is stored in objects called +`Records`. A `Record` can have multiple `Properties`, like numbers, text or references +to other `Records`. `RecordTypes` are kind of blue prints for `Records` and +provide a structure to the data. Let's look at an example: + +.. image:: model.svg + +.. The image is not good yet. Children should have properties of parents. + +This illustrates a simple data model used in the `demo instance`_ provided by `IndiScale`_. +It shows that the `RecordType` Analysis has among others the `Properties` +`quality_factor`, a number, and `date`, you guessed it... a date. The `Property` +`MusicalInstrumet` illustrates that a `Record` that has `Analysis` as a parent +`RecordType` should reference a `Record` that has the `MusicalInstrumet` `RecordType` as a parent. + +We recommend that you connect to the demo instance in order to try out the following +examples (see :doc:`Getting Started secton</getting_started>`.). However, you +can also translate the examples to the data model that you have at hand. + + + +Main Menu (WIP) +--------------- + + +.. note:: + By default only 10 Entities are shown on one page. You can get to + other pages with the “Next Page†and “Previous Page†buttons. + +:math:`\Rightarrow` What are the differences between the options of the +“Entities†menu? + +Entities, Records, Properties…What? + + +- semantic data modeling + +- entries in LinkAhead are like Objects + +- RecordType: blue print for data + +- Record: actual data + + +See also the +`wiki <https://gitlab.com/caosdb/caosdb/wikis/Concepts/Data%20Model>`__ +or the `paper <https://www.mdpi.com/2306-5729/4/2/83>`__ + +|image| + +References in two directions + +- | References in LinkAhead are directed: + | A Record A references another Record B + +- The referencing Record A has a corresponding Property. + +- The referenced Record B does not. + +- In order to get referencing Records in the Web Interface, click on the following button + (or “Backref†on older systems). + +|image1| + +File System +----------- + +- Clicking on “File System†in the main menu allows you to browse files + that LinkAhead knows about. + +- Typically, most files will be mounted from some file server. + +.. note:: You will not find any Records in this view (that are not Files). + + + +.. _here: https://gitlabio.something +.. _`demo instance`: https://demo.indiscale.com +.. _`IndiScale`: https://indiscale.com +.. |image| image:: model.svg +.. |image1| image:: References_button.png + :width: 4em diff --git a/src/doc/tutorials/index.rst b/src/doc/tutorials/index.rst new file mode 100644 index 0000000000000000000000000000000000000000..b01e45e3d574cd675b8410da1dbd1d79cb8e20c0 --- /dev/null +++ b/src/doc/tutorials/index.rst @@ -0,0 +1,14 @@ + +CaosDB Web Interface Tutorials +============================== + +This chapter contains the following tutorials: + +.. toctree:: + :maxdepth: 2 + :glob: + + first_steps + query + edit_mode + * diff --git a/src/doc/tutorials/model.svg b/src/doc/tutorials/model.svg new file mode 100644 index 0000000000000000000000000000000000000000..2602cb43f15976305d48e6f2d5efeb3821e1d669 --- /dev/null +++ b/src/doc/tutorials/model.svg @@ -0,0 +1,632 @@ +<?xml version="1.0" encoding="UTF-8" standalone="no"?> +<svg + xmlns:dc="http://purl.org/dc/elements/1.1/" + xmlns:cc="http://creativecommons.org/ns#" + xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#" + xmlns:svg="http://www.w3.org/2000/svg" + xmlns="http://www.w3.org/2000/svg" + xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd" + xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape" + contentScriptType="application/ecmascript" + contentStyleType="text/css" + height="502" + preserveAspectRatio="none" + version="1.1" + viewBox="0 0 407 502" + width="407" + zoomAndPan="magnify" + id="svg233" + sodipodi:docname="model.svg" + inkscape:version="0.92.4 5da689c313, 2019-01-14"> + <metadata + id="metadata237"> + <rdf:RDF> + <cc:Work + rdf:about=""> + <dc:format>image/svg+xml</dc:format> + <dc:type + rdf:resource="http://purl.org/dc/dcmitype/StillImage" /> + <dc:title></dc:title> + </cc:Work> + </rdf:RDF> + </metadata> + <sodipodi:namedview + pagecolor="#ffffff" + bordercolor="#666666" + borderopacity="1" + objecttolerance="10" + gridtolerance="10" + guidetolerance="10" + inkscape:pageopacity="0" + inkscape:pageshadow="2" + inkscape:window-width="1920" + inkscape:window-height="1043" + id="namedview235" + showgrid="false" + fit-margin-top="0" + fit-margin-left="0" + fit-margin-right="0" + fit-margin-bottom="0" + inkscape:zoom="1.1817368" + inkscape:cx="112.55875" + inkscape:cy="257" + inkscape:window-x="1920" + inkscape:window-y="0" + inkscape:window-maximized="1" + inkscape:current-layer="svg233" /> + <defs + id="defs11"> + <filter + height="3" + id="f64vrt8w3qxjw" + width="3" + x="-1" + y="-1"> + <feGaussianBlur + result="blurOut" + stdDeviation="2.0" + id="feGaussianBlur2" /> + <feColorMatrix + in="blurOut" + result="blurOut2" + type="matrix" + values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 .4 0" + id="feColorMatrix4" /> + <feOffset + dx="4.0" + dy="4.0" + in="blurOut2" + result="blurOut3" + id="feOffset6" /> + <feBlend + in="SourceGraphic" + in2="blurOut3" + mode="normal" + id="feBlend8" /> + </filter> + </defs> + <rect + style="fill:#ffffff;fill-opacity:1;stroke:none;stroke-width:1.13385832;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" + id="rect4867" + width="407" + height="502" + x="0" + y="0" /> + <polygon + id="polygon13" + style="fill:#dddddd;stroke:#000000;stroke-width:1.5;filter:url(#f64vrt8w3qxjw)" + points="533.5,526 126.5,526 126.5,24 236.5,24 243.5,46.2969 533.5,46.2969 " + transform="translate(-126.5,-24)" /> + <line + id="line15" + y2="22.296902" + y1="22.296902" + x2="117" + x1="0" + style="stroke:#000000;stroke-width:1.5" /> + <text + style="font-weight:bold;font-size:14px;font-family:sans-serif;fill:#000000" + id="text17" + y="38.995098" + x="130.5" + textLength="104" + lengthAdjust="spacingAndGlyphs" + font-weight="bold" + font-size="14" + transform="translate(-126.5,-24)">RecordTypes</text> + <text + style="font-size:11px;font-family:sans-serif;fill:#000000" + id="text42" + y="144.7104" + x="461" + textLength="0" + lengthAdjust="spacingAndGlyphs" + font-size="11" + transform="translate(-126.5,-24)" /> + <rect + y="411" + x="16" + width="116" + style="fill:#fefece;stroke:#a80036;stroke-width:1.5;filter:url(#f64vrt8w3qxjw)" + id="Manufacturer" + height="60.804699" /> + <circle + r="11" + id="ellipse47" + style="fill:#ff1111;stroke:#a80036;stroke-width:1" + cy="427" + cx="31" /> + <path + inkscape:connector-curvature="0" + id="path49" + d="m 33.9688,432.6406 q -0.5782,0.2969 -1.2188,0.4375 -0.6406,0.1563 -1.3437,0.1563 -2.5,0 -3.8282,-1.6406 -1.3125,-1.6563 -1.3125,-4.7813 0,-3.125 1.3125,-4.7812 1.3282,-1.6563 3.8282,-1.6563 0.7031,0 1.3437,0.1563 0.6563,0.1562 1.2188,0.4531 v 2.7187 q -0.625,-0.5781 -1.2188,-0.8437 -0.5937,-0.2813 -1.2187,-0.2813 -1.3438,0 -2.0313,1.0782 -0.6875,1.0625 -0.6875,3.1562 0,2.0938 0.6875,3.1719 0.6875,1.0625 2.0313,1.0625 0.625,0 1.2187,-0.2656 0.5938,-0.2813 1.2188,-0.8594 z" /> + <text + style="font-size:12px;font-family:sans-serif;fill:#000000" + id="text51" + y="455.1543" + x="171.5" + textLength="84" + lengthAdjust="spacingAndGlyphs" + font-size="12" + transform="translate(-126.5,-24)">Manufacturer</text> + <line + id="line53" + y2="443" + y1="443" + x2="131" + x1="17" + style="stroke:#a80036;stroke-width:1.5" /> + <text + style="font-size:11px;font-family:sans-serif;fill:#000000" + id="text55" + y="481.21039" + x="152.5" + textLength="0" + lengthAdjust="spacingAndGlyphs" + font-size="11" + transform="translate(-126.5,-24)" /> + <line + id="line57" + y2="463.80469" + y1="463.80469" + x2="131" + x1="17" + style="stroke:#a80036;stroke-width:1;stroke-dasharray:1, 2" /> + <rect + y="235" + x="16" + width="174" + style="fill:#fefece;stroke:#a80036;stroke-width:1.5;filter:url(#f64vrt8w3qxjw)" + id="MusicalInstrument" + height="101.6211" /> + <circle + r="11" + id="ellipse60" + style="fill:#ff1111;stroke:#a80036;stroke-width:1" + cy="251" + cx="43.600006" /> + <path + inkscape:connector-curvature="0" + id="path62" + d="m 46.5688,256.6406 q -0.5782,0.2969 -1.2188,0.4375 -0.6406,0.1563 -1.3437,0.1563 -2.5,0 -3.8282,-1.6406 -1.3125,-1.6563 -1.3125,-4.7813 0,-3.125 1.3125,-4.7812 1.3282,-1.6563 3.8282,-1.6563 0.7031,0 1.3437,0.1563 0.6563,0.1562 1.2188,0.4531 v 2.7187 q -0.625,-0.5781 -1.2188,-0.8437 -0.5937,-0.2813 -1.2187,-0.2813 -1.3438,0 -2.0313,1.0782 -0.6875,1.0625 -0.6875,3.1562 0,2.0938 0.6875,3.1719 0.6875,1.0625 2.0313,1.0625 0.625,0 1.2187,-0.2656 0.5938,-0.2813 1.2188,-0.8594 z" /> + <text + style="font-size:12px;font-family:sans-serif;fill:#000000" + id="text64" + y="279.1543" + x="186.89999" + textLength="114" + lengthAdjust="spacingAndGlyphs" + font-size="12" + transform="translate(-126.5,-24)">MusicalInstrument</text> + <line + id="line66" + y2="267" + y1="267" + x2="189" + x1="17" + style="stroke:#a80036;stroke-width:1.5" /> + <line + id="line68" + y2="281.40231" + y1="281.40231" + x2="73.5" + x1="17" + style="stroke:#a80036;stroke-width:1.5" /> + <text + style="font-size:11px;font-family:sans-serif;fill:#000000" + id="text70" + y="308.71039" + x="200" + textLength="59" + lengthAdjust="spacingAndGlyphs" + font-size="11" + transform="translate(-126.5,-24)">Properties</text> + <line + id="line72" + y2="281.40231" + y1="281.40231" + x2="189" + x1="132.5" + style="stroke:#a80036;stroke-width:1.5" /> + <text + style="font-size:11px;font-family:sans-serif;fill:#000000" + id="text74" + y="341.2222" + x="148.5" + textLength="86" + lengthAdjust="spacingAndGlyphs" + font-size="11" + transform="translate(-126.5,-24)">price (DOUBLE)</text> + <text + style="font-size:11px;font-family:sans-serif;fill:#000000" + id="text76" + y="354.02689" + x="148.5" + textLength="162" + lengthAdjust="spacingAndGlyphs" + font-size="11" + transform="translate(-126.5,-24)">Manufacturer (Manufacturer)</text> + <line + id="line78" + y2="300.60941" + y1="300.60941" + x2="62" + x1="17" + style="stroke:#a80036;stroke-width:1;stroke-dasharray:1, 2" /> + <text + style="font-size:11px;font-family:sans-serif;fill:#000000" + id="text80" + y="327.91751" + x="188.5" + textLength="82" + lengthAdjust="spacingAndGlyphs" + font-size="11" + transform="translate(-126.5,-24)">recommended</text> + <line + id="line82" + y2="300.60941" + y1="300.60941" + x2="189" + x1="144" + style="stroke:#a80036;stroke-width:1;stroke-dasharray:1, 2" /> + <rect + y="411" + x="167.5" + width="65" + style="fill:#fefece;stroke:#a80036;stroke-width:1.5;filter:url(#f64vrt8w3qxjw)" + id="Violin" + height="60.804699" /> + <circle + r="11" + id="ellipse85" + style="fill:#ff1111;stroke:#a80036;stroke-width:1" + cy="427" + cx="182.5" /> + <path + inkscape:connector-curvature="0" + id="path87" + d="m 185.4688,432.6406 q -0.5782,0.2969 -1.2188,0.4375 -0.6406,0.1563 -1.3437,0.1563 -2.5,0 -3.8282,-1.6406 -1.3125,-1.6563 -1.3125,-4.7813 0,-3.125 1.3125,-4.7812 1.3282,-1.6563 3.8282,-1.6563 0.7031,0 1.3437,0.1563 0.6563,0.1562 1.2188,0.4531 v 2.7187 q -0.625,-0.5781 -1.2188,-0.8437 -0.5937,-0.2813 -1.2187,-0.2813 -1.3438,0 -2.0313,1.0782 -0.6875,1.0625 -0.6875,3.1562 0,2.0938 0.6875,3.1719 0.6875,1.0625 2.0313,1.0625 0.625,0 1.2187,-0.2656 0.5938,-0.2813 1.2188,-0.8594 z" /> + <text + style="font-size:12px;font-family:sans-serif;fill:#000000" + id="text89" + y="455.1543" + x="323" + textLength="33" + lengthAdjust="spacingAndGlyphs" + font-size="12" + transform="translate(-126.5,-24)">Violin</text> + <line + id="line91" + y2="443" + y1="443" + x2="231.5" + x1="168.5" + style="stroke:#a80036;stroke-width:1.5" /> + <text + style="font-size:11px;font-family:sans-serif;fill:#000000" + id="text93" + y="481.21039" + x="304" + textLength="0" + lengthAdjust="spacingAndGlyphs" + font-size="11" + transform="translate(-126.5,-24)" /> + <line + id="line95" + y2="463.80469" + y1="463.80469" + x2="231.5" + x1="168.5" + style="stroke:#a80036;stroke-width:1;stroke-dasharray:1, 2" /> + <rect + y="397" + x="267.5" + width="119" + style="fill:#fefece;stroke:#a80036;stroke-width:1.5;filter:url(#f64vrt8w3qxjw)" + id="Guitar" + height="88.816399" /> + <circle + r="11" + id="ellipse98" + style="fill:#ff1111;stroke:#a80036;stroke-width:1" + cy="413" + cx="304.54999" /> + <path + inkscape:connector-curvature="0" + id="path100" + d="m 307.5188,418.6406 q -0.5782,0.2969 -1.2188,0.4375 -0.6406,0.1563 -1.3437,0.1563 -2.5,0 -3.8282,-1.6406 -1.3125,-1.6563 -1.3125,-4.7813 0,-3.125 1.3125,-4.7812 1.3282,-1.6563 3.8282,-1.6563 0.7031,0 1.3437,0.1563 0.6563,0.1562 1.2188,0.4531 v 2.7187 q -0.625,-0.5781 -1.2188,-0.8437 -0.5937,-0.2813 -1.2187,-0.2813 -1.3438,0 -2.0313,1.0782 -0.6875,1.0625 -0.6875,3.1562 0,2.0938 0.6875,3.1719 0.6875,1.0625 2.0313,1.0625 0.625,0 1.2187,-0.2656 0.5938,-0.2813 1.2188,-0.8594 z" /> + <text + style="font-size:12px;font-family:sans-serif;fill:#000000" + id="text102" + y="441.1543" + x="449.95001" + textLength="38" + lengthAdjust="spacingAndGlyphs" + font-size="12" + transform="translate(-126.5,-24)">Guitar</text> + <line + id="line104" + y2="429" + y1="429" + x2="385.5" + x1="268.5" + style="stroke:#a80036;stroke-width:1.5" /> + <line + id="line106" + y2="443.40231" + y1="443.40231" + x2="297.5" + x1="268.5" + style="stroke:#a80036;stroke-width:1.5" /> + <text + style="font-size:11px;font-family:sans-serif;fill:#000000" + id="text108" + y="470.71039" + x="424" + textLength="59" + lengthAdjust="spacingAndGlyphs" + font-size="11" + transform="translate(-126.5,-24)">Properties</text> + <line + id="line110" + y2="443.40231" + y1="443.40231" + x2="385.5" + x1="356.5" + style="stroke:#a80036;stroke-width:1.5" /> + <text + style="font-size:11px;font-family:sans-serif;fill:#000000" + id="text112" + y="503.2222" + x="400" + textLength="107" + lengthAdjust="spacingAndGlyphs" + font-size="11" + transform="translate(-126.5,-24)">electric (BOOLEAN)</text> + <line + id="line114" + y2="462.60941" + y1="462.60941" + x2="286" + x1="268.5" + style="stroke:#a80036;stroke-width:1;stroke-dasharray:1, 2" /> + <text + style="font-size:11px;font-family:sans-serif;fill:#000000" + id="text116" + y="489.91751" + x="412.5" + textLength="82" + lengthAdjust="spacingAndGlyphs" + font-size="11" + transform="translate(-126.5,-24)">recommended</text> + <line + id="line118" + y2="462.60941" + y1="462.60941" + x2="385.5" + x1="368" + style="stroke:#a80036;stroke-width:1;stroke-dasharray:1, 2" /> + <rect + y="255.5" + x="225.5" + width="165" + style="fill:#fefece;stroke:#a80036;stroke-width:1.5;filter:url(#f64vrt8w3qxjw)" + id="SoundQualityAnalyzer" + height="60.804699" /> + <circle + r="11" + id="ellipse121" + style="fill:#ff1111;stroke:#a80036;stroke-width:1" + cy="271.5" + cx="240.5" /> + <path + inkscape:connector-curvature="0" + id="path123" + d="m 243.4688,277.1406 q -0.5782,0.2969 -1.2188,0.4375 -0.6406,0.1563 -1.3437,0.1563 -2.5,0 -3.8282,-1.6406 -1.3125,-1.6563 -1.3125,-4.7813 0,-3.125 1.3125,-4.7812 1.3282,-1.6563 3.8282,-1.6563 0.7031,0 1.3437,0.1563 0.6563,0.1562 1.2188,0.4531 v 2.7187 q -0.625,-0.5781 -1.2188,-0.8437 -0.5937,-0.2813 -1.2187,-0.2813 -1.3438,0 -2.0313,1.0782 -0.6875,1.0625 -0.6875,3.1562 0,2.0938 0.6875,3.1719 0.6875,1.0625 2.0313,1.0625 0.625,0 1.2187,-0.2656 0.5938,-0.2813 1.2188,-0.8594 z" /> + <text + style="font-size:12px;font-family:sans-serif;fill:#000000" + id="text125" + y="299.6543" + x="381" + textLength="133" + lengthAdjust="spacingAndGlyphs" + font-size="12" + transform="translate(-126.5,-24)">SoundQualityAnalyzer</text> + <line + id="line127" + y2="287.5" + y1="287.5" + x2="389.5" + x1="226.5" + style="stroke:#a80036;stroke-width:1.5" /> + <text + style="font-size:11px;font-family:sans-serif;fill:#000000" + id="text129" + y="325.71039" + x="362" + textLength="0" + lengthAdjust="spacingAndGlyphs" + font-size="11" + transform="translate(-126.5,-24)" /> + <line + id="line131" + y2="308.30469" + y1="308.30469" + x2="389.5" + x1="226.5" + style="stroke:#a80036;stroke-width:1;stroke-dasharray:1, 2" /> + <rect + y="35" + x="20" + width="268" + style="fill:#fefece;stroke:#a80036;stroke-width:1.5;filter:url(#f64vrt8w3qxjw)" + id="Analysis" + height="140.0352" /> + <circle + r="11" + id="ellipse134" + style="fill:#ff1111;stroke:#a80036;stroke-width:1" + cy="51" + cx="124.75" /> + <path + inkscape:connector-curvature="0" + id="path136" + d="m 127.7188,56.6406 q -0.5782,0.2969 -1.2188,0.4375 -0.6406,0.1563 -1.3437,0.1563 -2.5,0 -3.8282,-1.6406 -1.3125,-1.6563 -1.3125,-4.7813 0,-3.125 1.3125,-4.7812 1.3282,-1.6563 3.8282,-1.6563 0.7031,0 1.3437,0.1563 0.6563,0.1562 1.2188,0.4531 v 2.7187 q -0.625,-0.5781 -1.2188,-0.8437 -0.5937,-0.2813 -1.2187,-0.2813 -1.3438,0 -2.0313,1.0782 -0.6875,1.0625 -0.6875,3.1562 0,2.0938 0.6875,3.1719 0.6875,1.0625 2.0313,1.0625 0.625,0 1.2187,-0.2656 0.5938,-0.2813 1.2188,-0.8594 z" /> + <text + style="font-size:12px;font-family:sans-serif;fill:#000000" + id="text138" + y="79.154297" + x="271.75" + textLength="50" + lengthAdjust="spacingAndGlyphs" + font-size="12" + transform="translate(-126.5,-24)">Analysis</text> + <line + id="line140" + y2="67" + y1="67" + x2="287" + x1="21" + style="stroke:#a80036;stroke-width:1.5" /> + <line + id="line142" + y2="81.402298" + y1="81.402298" + x2="124.5" + x1="21" + style="stroke:#a80036;stroke-width:1.5" /> + <text + style="font-size:11px;font-family:sans-serif;fill:#000000" + id="text144" + y="108.7104" + x="251" + textLength="59" + lengthAdjust="spacingAndGlyphs" + font-size="11" + transform="translate(-126.5,-24)">Properties</text> + <line + id="line146" + y2="81.402298" + y1="81.402298" + x2="287" + x1="183.5" + style="stroke:#a80036;stroke-width:1.5" /> + <text + style="font-size:11px;font-family:sans-serif;fill:#000000" + id="text148" + y="141.2222" + x="152.5" + textLength="134" + lengthAdjust="spacingAndGlyphs" + font-size="11" + transform="translate(-126.5,-24)">quality_factor (DOUBLE)</text> + <text + style="font-size:11px;font-family:sans-serif;fill:#000000" + id="text150" + y="154.0269" + x="152.5" + textLength="92" + lengthAdjust="spacingAndGlyphs" + font-size="11" + transform="translate(-126.5,-24)">date (DATETIME)</text> + <text + style="font-size:11px;font-family:sans-serif;fill:#000000" + id="text152" + y="166.8315" + x="152.5" + textLength="111" + lengthAdjust="spacingAndGlyphs" + font-size="11" + transform="translate(-126.5,-24)">report (REFERENCE)</text> + <text + style="font-size:11px;font-family:sans-serif;fill:#000000" + id="text154" + y="179.6362" + x="152.5" + textLength="256" + lengthAdjust="spacingAndGlyphs" + font-size="11" + transform="translate(-126.5,-24)">SoundQualityAnalyzer (SoundQualityAnalyzer)</text> + <text + style="font-size:11px;font-family:sans-serif;fill:#000000" + id="text156" + y="192.4409" + x="152.5" + textLength="220" + lengthAdjust="spacingAndGlyphs" + font-size="11" + transform="translate(-126.5,-24)">MusicalInstrument (MusicalInstrument)</text> + <line + id="line158" + y2="100.6094" + y1="100.6094" + x2="113" + x1="21" + style="stroke:#a80036;stroke-width:1;stroke-dasharray:1, 2" /> + <text + style="font-size:11px;font-family:sans-serif;fill:#000000" + id="text160" + y="127.9175" + x="239.5" + textLength="82" + lengthAdjust="spacingAndGlyphs" + font-size="11" + transform="translate(-126.5,-24)">recommended</text> + <line + id="line162" + y2="100.6094" + y1="100.6094" + x2="287" + x1="195" + style="stroke:#a80036;stroke-width:1;stroke-dasharray:1, 2" /> + <path + inkscape:connector-curvature="0" + style="fill:none;stroke:#a80036;stroke-width:1" + id="MusicalInstrument-Violin" + d="m 145.51,354.27 c 12.48,19.76 25.51,40.37 35.69,56.48" /> + <polygon + id="polygon211" + style="fill:none;stroke:#a80036;stroke-width:1" + points="261.26,361.26 277.86,374.43 266.03,381.91 " + transform="translate(-126.5,-24)" /> + <path + inkscape:connector-curvature="0" + style="fill:none;stroke:#a80036;stroke-width:1" + id="MusicalInstrument-Guitar" + d="m 192.64,348.42 c 25.04,17.17 51.64,35.39 74.51,51.06" /> + <polygon + id="polygon214" + style="fill:none;stroke:#a80036;stroke-width:1" + points="302.54,361.05 322.99,366.58 315.08,378.13 " + transform="translate(-126.5,-24)" /> + <path + inkscape:connector-curvature="0" + style="fill:none;stroke:#a80036;stroke-width:1" + id="MusicalInstrument-Manufacturer" + d="m 91.08,350.09 c -3.97,21 -8.2,43.41 -11.46,60.66" /> + <polygon + id="polygon217" + style="fill:#a80036;stroke:#a80036;stroke-width:1" + points="220,361.26 214.9551,366.4126 217.771,373.0512 222.8159,367.8986 " + transform="translate(-126.5,-24)" /> + <path + inkscape:connector-curvature="0" + style="fill:none;stroke:#a80036;stroke-width:1" + id="Analysis-SoundQualityAnalyzer" + d="m 222.3,185.39 c 21.35,24.82 43.61,50.69 60.09,69.84" /> + <polygon + id="polygon220" + style="fill:#a80036;stroke:#a80036;stroke-width:1" + points="340.04,199.21 340.9231,206.3668 347.869,208.3043 346.9859,201.1475 " + transform="translate(-126.5,-24)" /> + <path + inkscape:connector-curvature="0" + style="fill:none;stroke:#a80036;stroke-width:1" + id="Analysis-MusicalInstrument" + d="m 130.66,187.93 c -4.56,16 -9.21,32.33 -13.37,46.92" /> + <polygon + id="polygon223" + style="fill:#a80036;stroke:#a80036;stroke-width:1" + points="260.78,199.21 255.287,203.8819 257.4868,210.7493 262.9798,206.0774 " + transform="translate(-126.5,-24)" /> +</svg> diff --git a/src/doc/tutorials/new-property.png b/src/doc/tutorials/new-property.png new file mode 100644 index 0000000000000000000000000000000000000000..af438007278b2ddf30fae4f56fb803bfda06577f Binary files /dev/null and b/src/doc/tutorials/new-property.png differ diff --git a/src/doc/tutorials/new-record.png b/src/doc/tutorials/new-record.png new file mode 100644 index 0000000000000000000000000000000000000000..0b2ac31afa78ca2aa20ee8f5f83f9a04ce39be30 Binary files /dev/null and b/src/doc/tutorials/new-record.png differ diff --git a/src/doc/tutorials/new-recordtype.png b/src/doc/tutorials/new-recordtype.png new file mode 100644 index 0000000000000000000000000000000000000000..9519a9ada3b034c01308ac4a4c350954d9d32d65 Binary files /dev/null and b/src/doc/tutorials/new-recordtype.png differ diff --git a/src/doc/tutorials/query.rst b/src/doc/tutorials/query.rst new file mode 100644 index 0000000000000000000000000000000000000000..29d998cc1dbce5c5e17ae4cf9f60176b9f88861a --- /dev/null +++ b/src/doc/tutorials/query.rst @@ -0,0 +1,143 @@ + +Querying CaosDB +=============== + +You should have the web interface of a CaosDB instance at hand. If you do not +have one, you can visit https://demo.indiscale.com + +Introduction +------------ + +The semantic data model of CaosDB allows efficient data access. The +CaosDB Query Language (CQL) is used to search data. Queries can be entered in +the webinterface under the respective menu entry. + +Let's start with a simple one:: + + FIND RECORD MusicalInstrument + +Most queries simply start with the ``FIND`` keyword and describe what we are +looking for behind that. The ``RECORD`` keyword denotes that we are only looking +for Records (and not Files, Properties or RecordTypes). Finally, we provided +a RecordType name: MusicalInstrument. This means that we will get all Records +that have this RecordType as parent. Try it out! + +Let's look at:: + + FIND Guitar + +When we leave out the ``RECORD`` keyword, we will get every entity that is a +Guitar. When you submit this query you should find also a RecordType Guitar +in the results. Using ``FIND RecordType Guitar`` would restrict the result to +only that RecordType. + +Note, that you cannot only provide RecordType names after the ``FIND``, but names +in general: ``FIND RECORD Nice Guitar``. This will give you a Record with the +name "Nice Guitar" (if one exists... and there should be one in the demo instance). + +While it does not matter whether you use capital letters or not, the names have to +be exact. There are two features that make it easy to use names for querying +in spite of this: +- You can use "*" to match any string. E.g. ``FIND RECORD Nice*`` +- After typing three letters, names that start with those three are +suggested by the auto completion. + +.. note:: + + Train yourself by trying to guess what the result will be before + actually executing the query. + + +Searching Data Using Properties +-------------------------------- + +Looking for entities with certain names or such that have certain parents is +nice. However, the queries become really useful if we can impose further conditions +on the results. Let's start with an example again:: + + FIND Guitar with price > 10000 + +This should list expensive guitars where are in the demo instance. Thus, +we are using a property (the price) of the Guitar Records to restrict the +result set. In general this looks like:: + + FIND <Name> <Property Filter> + +Typically, the filter has the form ``<Property> <Operator> <Value>``, +for example ``length >= 0.7mm``. +There are many filters available. You can check the specification for a comprehensive description of +those. Here, we will only look at the most common examples. + + +If you only want to assure that Records have a certain Property, without imposing +constrains on the value, you can use:: + + FIND RECORD MusicalInstrument WITH Manufacturer + + +Similarly, to what we saw above when using incomplete names, you can use a "*" +to match parts of text properties:: + + FIND RECORD WITH serialNumber like KN* + +There is large number of operators that can be used together with dates or +timestamps. One of the most useful is probably:: + + FIND RECORD WITH date in 2019 + +A lot of valuable information is often stored in the relations among data, i.e. in +the references of entities. So how can we use those?:: + + FIND RECORD WHICH REFERENCES A Guitar + +This should be pretty self explanatory. And it is also possible to check for +references in the other direction:: + + FIND RECORD WHICH IS REFERENCED BY A Analysis + +You can also simply provide the ID of the entity:: + + FIND RECORD WHICH IS REFERENCED BY 123`` + + +Using Multiple Filters +---------------------- + +Often, one condition is not sufficient. Thus multiple filters/conditions can be combined. +This can for example be done using the following structure:: + + FIND <Name> <Property Filter> (AND|OR) <Property Filter> + +An example would be:: + + FIND Guitar WITH price>48 AND electric=TRUE + +Furthermore, reference conditions can be nested:: + + FIND <Name> WHICH REFERENCES <Name> WHICH REFERENCES <Name> + + +For example:: + + FIND Manufacturer WHICH IS REFERENCED BY Guitar WHICH IS REFERENCED BY Analysis + + +Restricting Result Information +------------------------------ + +Using ``COUNT`` instead of ``FIND`` will only return the number of +entities in the result set. + +.. note:: This is often useful when experimenting with queries. + +Using ``SELECT ... FROM`` instead of ``FIND`` returns specific +information in a table. A comma separated list of Property names can be provided behind the +``SELECT`` keyword:: + + SELECT price, electric FROM Guitar + +Or:: + + SELECT quality_factor, report, date FROM Analysis WHICH REFERENCES A Guitar WITH electric=TRUE + + diff --git a/src/ext/js/fileupload.js b/src/ext/js/fileupload.js index fac11efadc3d1b6d364333d68b27904c37c3aa42..4e9f7cd02a4f3c5e63080f1e520c10a9328bfe22 100644 --- a/src/ext/js/fileupload.js +++ b/src/ext/js/fileupload.js @@ -28,14 +28,14 @@ var fileupload = new function() { const _modal_str = ` <div class="modal fade" tabindex="-1" role="dialog"> <div class="modal-dialog modal-lg" role="document"> <div class="modal-content"> <div class="modal-header"> <button type="button" - class="close" data-dismiss="modal">×</button> <h4 + class="btn-close" data-bs-dismiss="modal">×</button> <h4 class="modal-title">File Upload</h4> </div> <div class="modal-body"> <form action="/Entity/" class="dropzone dz-clickable" > <label>path</label><input id="upload-path" type="text" value="/"/> <div class="dz-message"> Drag'n'drop files to this area or click to upload. </div> </form> </div> - <div class="modal-footer"> <button type="button" class="btn btn-default + <div class="modal-footer"> <button type="button" class="btn btn-secondary caosdb-f-file-upload-submit-button">Ok</button> <button type="button" - class="btn btn-default" data-dismiss="modal">Close</button> </div> </div> + class="btn btn-secondary" data-bs-dismiss="modal">Close</button> </div> </div> </div> </div>`; /** Create a dropzone.js form for the file upload. @@ -93,7 +93,7 @@ var fileupload = new function() { formData.append("FileRepresentation", request); - // add the success and error handlers which put the + // add the success and error handlers xhr.addEventListener("load", success_handler); xhr.addEventListener("error", error_handler); }); @@ -114,8 +114,21 @@ var fileupload = new function() { return modal[0]; } - this.create_error_handler = function() { - return globalError; + this.create_error_handler = function(property) { + var input = $(property).find(".caosdb-f-property-value input"); + return function(event, error, xhr) { + if (xhr.status == "401") { + // add error message + input.after(`<div class="alert alert-danger alert-dismissible" + role="alert"> + <button type="button" class="btn-close" data-bs-dismiss="alert" + aria-label="Close"><span aria-hidden="true">×</span></button> + <strong>Error!</strong> You are not logged in!.</div>`); + } else { + globalError(event, error, xhr); + } + + } } /** Create a success handler function for the server's response which @@ -174,13 +187,13 @@ var fileupload = new function() { // add success message input.after(`<div class="alert alert-success alert-dismissible" role="alert"> - <button type="button" class="close" data-dismiss="alert" + <button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"><span aria-hidden="true">×</span></button> <strong>Success!</strong> The file <code class="caosdb-f-file-upload-file-name">` + getEntityName(entity) + `</code> has been uploaded.</div>`); - input.after(`<a class="btn btn-default btn-sm" + input.after(`<a class="btn btn-secondary btn-sm" href="` + connection.getEntityUri([getEntityId(entity)]) + `" target= "_blank">` + getEntityName(entity) + `</a>`); @@ -193,7 +206,7 @@ var fileupload = new function() { * @return {HTMLElement} a button element. */ this.create_small_icon_button = function() { - var button = $('<button class="caosdb-f-file-upload-button btn btn-link navbar-btn" ><span class="glyphicon glyphicon-upload" aria-hidden="true"></span></button>'); + var button = $('<button class="caosdb-f-file-upload-button btn btn-link navbar-btn" ><i aria-hidden="true" class="bi-upload"></button>'); return button[0]; }; @@ -235,8 +248,6 @@ var fileupload = new function() { } this.init = function() { - fileupload.debug("init"); - // add global listener for start_edit event document.body.addEventListener(edit_mode.start_edit.type, function(e) { $(e.target).find(".caosdb-properties .caosdb-f-entity-property").each(function(idx) { @@ -284,7 +295,7 @@ var fileupload = new function() { var default_path = this.get_default_path(); var button = this.create_small_icon_button(); var success_handler = this.create_success_handler(target); - var error_handler = this.create_error_handler(); + var error_handler = this.create_error_handler(target); var edit_menu = $(target).find(".caosdb-f-property-value")[0]; var dropzone_config = { diff --git a/src/linkahead_icon_512.png b/src/linkahead_icon_512.png new file mode 100644 index 0000000000000000000000000000000000000000..84d1b5ebd715b55418444787c82689b28d917e24 Binary files /dev/null and b/src/linkahead_icon_512.png differ diff --git a/src/server_side_scripting/ext_file_download/zip_files.py b/src/server_side_scripting/ext_file_download/zip_files.py new file mode 100755 index 0000000000000000000000000000000000000000..65f27c9d901a6790456b8addb636bd55b7fe0c7e --- /dev/null +++ b/src/server_side_scripting/ext_file_download/zip_files.py @@ -0,0 +1,112 @@ +#!/usr/bin/env python3 +# ** header v3.0 +# This file is a part of the CaosDB Project. +# +# Copyright (C) 2019 IndiScale GmbH +# +# 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/>. +# +# ** end header + +"""Creates a zip file from multiple file entities. """ + +import argparse +import datetime +import io +import logging +import os +import sys +from tempfile import NamedTemporaryFile +from zipfile import ZipFile + +import caosdb as db +import pandas as pd +from caosadvancedtools.serverside import helper +from caosdb import CaosDBException, ConsistencyError, EntityDoesNotExistError + + +def _parse_arguments(): + """Parses the command line arguments. + + Takes into account defaults from the environment (where known). + """ + parser = argparse.ArgumentParser(description='__doc__') + parser.add_argument('-a', '--auth-token', required=False, + help=("An authentication token. If not provided caosdb" + " pylib will search for other methods of " + "authentication if necessary.")) + parser.add_argument('ids', help="list of entity ids.") + parser.add_argument('table', help="tsv table to be saved (as string).") + + return parser.parse_args() + + +def collect_files_in_zip(ids, table): + # File output + now = datetime.datetime.now() + zip_name = "files.{time}.zip".format( + time=now.strftime("%Y-%m-%dT%H_%M_%S")) + zip_display_path, zip_internal_path = helper.get_shared_filename(zip_name) + with ZipFile(zip_internal_path, 'w') as zf: + nc = helper.NameCollector() + + # add the table which has been genereated by the webui table exporter + with NamedTemporaryFile(delete=False) as table_file: + # the file has been transmitted as string and has to be written to + # a file first. + table_file.write(table.encode()) + zf.write(table_file.name, "selected_table.tsv") + + # download and add all files + for file_id in ids: + try: + tmp = db.execute_query("FIND {a:} WITH ID={a:}".format( + a=file_id), + unique=True) + except EntityDoesNotExistError as e: + # TODO + # Current behavior: script terminates with error if just one + # file cannot be retrieved. + # Desired behavior: The script should go on with the other + # ids, but the user should be informed about the missing files. + # How should we do this? + logger = logging.getLogger("caosadvancedtools") + logger.error("Did not find Entity with ID={}.".format( + file_id)) + + raise e + savename = nc.get_unique_savename(os.path.basename(tmp.path)) + val_file = helper.get_file_via_download( + tmp, logger=logging.getLogger("caosadvancedtools")) + + zf.write(val_file, savename) + + return zip_display_path + + +def main(): + args = _parse_arguments() + + if hasattr(args, "auth_token") and args.auth_token: + db.configure_connection(auth_token=args.auth_token) + + id_list = [int(el) for el in args.ids.split(",")] + + zip_file = collect_files_in_zip(id_list, args.table) + + print(zip_file) + + +if __name__ == "__main__": + main() diff --git a/src/server_side_scripting/ext_table_preview/pandas_table_preview.py b/src/server_side_scripting/ext_table_preview/pandas_table_preview.py new file mode 100755 index 0000000000000000000000000000000000000000..c0659d9b1839c43e0629a878d792c414577ea344 --- /dev/null +++ b/src/server_side_scripting/ext_table_preview/pandas_table_preview.py @@ -0,0 +1,145 @@ +#!/usr/bin/env python3 +# encoding: utf-8 +# +# ** header v3.0 +# This file is a part of the CaosDB Project. +# +# Copyright (C) 2020 IndiScale GmbH <info@indiscale.com> +# Copyright (C) 2020 Henrik tom Wörden <h.tomwoerden@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/>. +# +# ** end header +# + +""" +This script tries to read typical table data files (.csv etc.) with pandas and +creates a html (partial) representation of the table. +""" + +import logging +import os +import sys +from datetime import datetime + +import caosdb as db +import pandas as pd +from caosadvancedtools.serverside.helper import get_argument_parser +from caosadvancedtools.serverside.logging import configure_server_side_logging + +MAXIMUMFILESIZE = 1e8 +VALID_ENDINGS = [".csv", ".tsv", ".xls", ".xlsx"] + + +def get_file(eid): + """ retrieves the file entity from caosdb """ + try: + fi = db.File(id=eid) + fi.retrieve() + except db.exceptions.EntityDoesNotExistError: + print("Cannot create preview for Entity with ID={}, because it seems" + "not to exist.".format(eid), file=sys.stderr) + sys.exit(1) + + return fi + + +def size_is_ok(fi): + """ show previews only for files that are not too large """ + + return fi.size <= MAXIMUMFILESIZE + + +def get_ending(fipath): + """ return which of the valid endings (tsv etc.) is the one present""" + + for end in VALID_ENDINGS: + if fipath.lower().endswith(end): + return end + + return None + + +def ending_is_valid(fipath): + """ return whether the ending indicates a file type that can be treated""" + + return get_ending(fipath) is not None + + +def read_file(fipath, ftype): + """ tries to read the provided file """ + + try: + if ftype in [".xls", ".xlsx"]: + df = pd.read_excel(fipath) + elif ftype == ".tsv": + df = pd.read_csv(fipath, sep="\t", comment="#") + elif ftype == ".csv": + df = pd.read_csv(fipath, comment="#") + else: + print("File type unknown: {}".format(ftype)) + raise RuntimeError("") + except Exception: + raise ValueError() + + return df + + +def create_table_preview(fi): + if not ending_is_valid(fi.path): + print("Cannot create preview for Entity with ID={}, because download" + "failed.".format(entity_id), file=sys.stderr) + sys.exit(5) + + ending = get_ending(fi.path) + + if not size_is_ok(fi): + print("Skipped creating a preview for Entity with ID={}, because the" + "file is large!".format(entity_id), file=sys.stderr) + sys.exit(2) + + try: + tmpfile = fi.download() + except Exception: + print("Cannot create preview for Entity with ID={}, because download" + "failed.".format(entity_id), file=sys.stderr) + + sys.exit(3) + + try: + df = read_file(tmpfile, ending) + except ValueError: + print("Cannot read File Entity with ID={}.".format(entity_id), + file=sys.stderr) + sys.exit(4) + + print(df.to_html(max_cols=10, max_rows=10)) + + +if __name__ == "__main__": + conlogger = logging.getLogger("connection") + conlogger.setLevel(level=logging.ERROR) + + parser = get_argument_parser() + args = parser.parse_args() + + debug_file = configure_server_side_logging() + logger = logging.getLogger("caosadvancedtools") + + db.configure_connection(auth_token=args.auth_token) + entity_id = args.filename + + fi = get_file(entity_id) + + create_table_preview(fi) diff --git a/test/core/html/form_elements_example_1.html b/test/core/html/form_elements_example_1.html index 9977e5d91c02cc0e4dd08e9f0939c85f98c6c1be..27da98afdaffd44ac6560d72724e5e40d50655f0 100644 --- a/test/core/html/form_elements_example_1.html +++ b/test/core/html/form_elements_example_1.html @@ -3,8 +3,8 @@ .caosdb-f-property-single-raw-value or introduce new .caosdb-v-property-text-value --> <form action="#" class="form-horizontal" method="post" name="sample_creation.py"> - <div class="form-group caosdb-f-field caosdb-f-entity-property caosdb-f-form-field-required caosdb-f-form-field-cached" data-field-name="ice_core" data-groups="(part1)"> - <label class="control-label col-sm-3" data-property-name="ice_core" for="ice_core">Ice Core</label> + <div class="form-control caosdb-f-field caosdb-f-entity-property caosdb-f-form-field-required caosdb-f-form-field-cached" data-field-name="ice_core" data-groups="(part1)"> + <label class="col-form-label col-sm-3" data-property-name="ice_core" for="ice_core">Ice Core</label> <div class="col-sm-9"> <div class="dropdown bootstrap-select form-control bs3"> <select class="selectpicker form-control" name="ice_core" tabindex="-98" title="Nothing selected"> @@ -16,7 +16,7 @@ <option value="6347">EGRIP18</option> <option value="6348">EGRIP19</option> </select> - <button aria-expanded="false" class="btn dropdown-toggle btn-default" data-toggle="dropdown" role="button" title="EGRIP15" type="button"> + <button aria-expanded="false" class="btn dropdown-toggle btn-secondary" data-toggle="dropdown" role="button" title="EGRIP15" type="button"> <div class="filter-option"> <div class="filter-option-inner"> <div class="filter-option-inner-inner">EGRIP15</div> @@ -64,8 +64,8 @@ </div> </div> </div> - <div class="form-group caosdb-f-field caosdb-f-entity-property" data-field-name="orig_sample_type" data-groups="(part1)"> - <label class="control-label col-sm-3" data-property-name="orig_sample_type" for="orig_sample_type">Original Sample Type</label> + <div class="form-control caosdb-f-field caosdb-f-entity-property" data-field-name="orig_sample_type" data-groups="(part1)"> + <label class="col-form-label col-sm-3" data-property-name="orig_sample_type" for="orig_sample_type">Original Sample Type</label> <div class="col-sm-9"> <div class="dropdown bootstrap-select form-control bs3"> <select class="selectpicker form-control" name="orig_sample_type" tabindex="-98" title="Nothing selected"> @@ -78,7 +78,7 @@ <option value="6338">PP_Sample</option> <option value="6340">LASM_Sample</option> </select> - <button aria-expanded="false" class="btn dropdown-toggle btn-default" data-toggle="dropdown" role="button" title="IceSample" type="button"> + <button aria-expanded="false" class="btn dropdown-toggle btn-secondary" data-toggle="dropdown" role="button" title="IceSample" type="button"> <div class="filter-option"> <div class="filter-option-inner"> <div class="filter-option-inner-inner">IceSample</div> @@ -131,8 +131,8 @@ </div> </div> </div> - <div class="form-group caosdb-f-field caosdb-f-entity-property caosdb-f-field-disabled" data-field-name="logging_protocol" data-groups="(part2)" style="display: none;"> - <label class="control-label col-sm-3" data-property-name="logging_protocol" for="logging_protocol">Logging Protocol</label> + <div class="form-control caosdb-f-field caosdb-f-entity-property caosdb-f-field-disabled" data-field-name="logging_protocol" data-groups="(part2)" style="display: none;"> + <label class="col-form-label col-sm-3" data-property-name="logging_protocol" for="logging_protocol">Logging Protocol</label> <div class="col-sm-9"> <div class="dropdown bootstrap-select form-control bs3"> <select class="selectpicker form-control" name="logging_protocol" tabindex="-98" title="Nothing selected"> @@ -140,7 +140,7 @@ <option disabled="disabled" selected="selected" style="display: none" value=""></option> <option value="6350">/logging_protocol_2019-06-15.pdf</option> </select> - <button class="btn dropdown-toggle btn-default bs-placeholder" data-toggle="dropdown" role="button" title="Nothing selected" type="button"> + <button class="btn dropdown-toggle btn-secondary bs-placeholder" data-toggle="dropdown" role="button" title="Nothing selected" type="button"> <div class="filter-option"> <div class="filter-option-inner"> <div class="filter-option-inner-inner">Nothing selected</div> @@ -157,8 +157,8 @@ </div> </div> </div> - <div class="form-group caosdb-f-field caosdb-f-entity-property" data-field-name="cutting_protocol" data-groups="(part3)" style=""> - <label class="control-label col-sm-3" data-property-name="cutting_protocol" for="cutting_protocol">Cutting Protocol</label> + <div class="form-control caosdb-f-field caosdb-f-entity-property" data-field-name="cutting_protocol" data-groups="(part3)" style=""> + <label class="col-form-label col-sm-3" data-property-name="cutting_protocol" for="cutting_protocol">Cutting Protocol</label> <div class="col-sm-9"> <div class="dropdown bootstrap-select form-control bs3"> <select class="selectpicker form-control" name="cutting_protocol" tabindex="-98" title="Nothing selected"> @@ -166,7 +166,7 @@ <option disabled="disabled" selected="selected" style="display: none" value=""></option> <option value="6349">/cutting_protocol_2019-08-15.pdf</option> </select> - <button aria-expanded="false" class="btn dropdown-toggle btn-default" data-toggle="dropdown" role="button" title="/cutting_protocol_2019-08-15.pdf" type="button"> + <button aria-expanded="false" class="btn dropdown-toggle btn-secondary" data-toggle="dropdown" role="button" title="/cutting_protocol_2019-08-15.pdf" type="button"> <div class="filter-option"> <div class="filter-option-inner"> <div class="filter-option-inner-inner">/cutting_protocol_2019-08-15.pdf</div> @@ -194,29 +194,29 @@ </div> </div> </div> - <div class="form-group caosdb-f-field caosdb-f-entity-property caosdb-f-form-field-required" data-field-name="cutting_date" data-groups="(part2)(part3)" style=""> - <label class="control-label col-sm-3" data-property-name="cutting_date" for="cutting_date">Cutting Date</label> + <div class="form-control caosdb-f-field caosdb-f-entity-property caosdb-f-form-field-required" data-field-name="cutting_date" data-groups="(part2)(part3)" style=""> + <label class="col-form-label col-sm-3" data-property-name="cutting_date" for="cutting_date">Cutting Date</label> <div class="caosdb-f-property-value col-sm-9"> <input class="form-control caosdb-property-text-value" name="cutting_date" type="date"/> </div> </div> - <div class="form-group caosdb-f-field caosdb-f-entity-property" data-field-name="bag_numbers" data-groups="(part2)(part3)" style=""> - <label class="control-label col-sm-3" data-property-name="bag_numbers" for="bag_numbers">Bag Numbers</label> + <div class="form-control caosdb-f-field caosdb-f-entity-property" data-field-name="bag_numbers" data-groups="(part2)(part3)" style=""> + <label class="col-form-label col-sm-3" data-property-name="bag_numbers" for="bag_numbers">Bag Numbers</label> <div class="caosdb-f-field caosdb-f-entity-property" data-field-name="bag_numbers_from"> - <label class="control-label col-sm-1" data-property-name="bag_numbers_from" for="bag_numbers_from">from</label> + <label class="col-form-label col-sm-1" data-property-name="bag_numbers_from" for="bag_numbers_from">from</label> <div class="caosdb-f-property-value col-sm-3"> <input class="form-control caosdb-property-text-value" name="bag_numbers_from" step="1" type="number"/> </div> </div> <div class="caosdb-f-field caosdb-f-entity-property" data-field-name="bag_numbers_to"> - <label class="control-label col-sm-1 col-sm-offset-1" data-property-name="bag_numbers_to" for="bag_numbers_to">to</label> + <label class="col-form-label col-sm-1 col-sm-offset-1" data-property-name="bag_numbers_to" for="bag_numbers_to">to</label> <div class="caosdb-f-property-value col-sm-3"> <input class="form-control caosdb-property-text-value" name="bag_numbers_to" step="1" type="number"/> </div> </div> </div> - <div class="form-group caosdb-f-field caosdb-f-entity-property" data-field-name="box_of_new_samples" data-groups="(part2)(part3)" style=""> - <label class="control-label col-sm-3" data-property-name="box_of_new_samples" for="box_of_new_samples">Box of New Samples</label> + <div class="form-control caosdb-f-field caosdb-f-entity-property" data-field-name="box_of_new_samples" data-groups="(part2)(part3)" style=""> + <label class="col-form-label col-sm-3" data-property-name="box_of_new_samples" for="box_of_new_samples">Box of New Samples</label> <div class="col-sm-9"> <div class="dropdown bootstrap-select form-control bs3"> <select class="selectpicker form-control" name="box_of_new_samples" tabindex="-98" title="Nothing selected"> @@ -230,7 +230,7 @@ <option value="2123">1112</option> <option value="4434">6053</option> </select> - <button aria-expanded="false" class="btn dropdown-toggle btn-default" data-toggle="dropdown" role="button" title="0062" type="button"> + <button aria-expanded="false" class="btn dropdown-toggle btn-secondary" data-toggle="dropdown" role="button" title="0062" type="button"> <div class="filter-option"> <div class="filter-option-inner"> <div class="filter-option-inner-inner">0062</div> @@ -288,8 +288,8 @@ </div> </div> </div> - <div class="form-group caosdb-f-field caosdb-f-entity-property" data-field-name="new_subsamples_selector" data-groups="(part3)" style=""> - <label class="control-label col-sm-3" data-property-name="new_subsamples_selector" for="new_subsamples_selector">New Subsamples</label> + <div class="form-control caosdb-f-field caosdb-f-entity-property" data-field-name="new_subsamples_selector" data-groups="(part3)" style=""> + <label class="col-form-label col-sm-3" data-property-name="new_subsamples_selector" for="new_subsamples_selector">New Subsamples</label> <div class="col-sm-9"> <div class="dropdown bootstrap-select show-tick form-control bs3"> <select class="selectpicker form-control" multiple="multiple" name="new_subsamples_selector" tabindex="-98" title="Nothing selected"> @@ -297,7 +297,7 @@ <option value="6338">PP_Sample</option> <option value="6340">LASM_Sample</option> </select> - <button aria-expanded="false" class="btn dropdown-toggle btn-default" data-toggle="dropdown" role="button" title="Subsample, PP_Sample" type="button"> + <button aria-expanded="false" class="btn dropdown-toggle btn-secondary" data-toggle="dropdown" role="button" title="Subsample, PP_Sample" type="button"> <div class="filter-option"> <div class="filter-option-inner"> <div class="filter-option-inner-inner">Subsample, PP_Sample</div> @@ -336,25 +336,25 @@ <div class="col-sm-9 col-sm-offset-3 row" style="background-color: rgb(255, 255, 255);"> <fieldset class="form-inline caosdb-f-form-elements-subform" data-subform-name="new_subsamples" id="new_subsamples_6335" style="padding-left: 15px; padding-right: 15px;"> <legend>Subsample</legend> - <div class="form-group caosdb-f-field caosdb-f-entity-property" data-field-name="type"> + <div class="form-control caosdb-f-field caosdb-f-entity-property" data-field-name="type"> <div class="caosdb-f-property-value col-sm-9"> <input class="form-control caosdb-property-text-value" name="type" type="hidden" value="6335"/> </div> </div> - <div class="form-group caosdb-f-field caosdb-f-entity-property" data-field-name="width" style="margin-left: 15px; margin-right: 15px;"> - <label class="control-label" data-property-name="width" for="width">width (cm)</label> + <div class="form-control caosdb-f-field caosdb-f-entity-property" data-field-name="width" style="margin-left: 15px; margin-right: 15px;"> + <label class="col-form-label" data-property-name="width" for="width">width (cm)</label> <div class="caosdb-f-property-value"> <input class="form-control caosdb-property-text-value" name="width" step="any" type="number"/> </div> </div> - <div class="form-group caosdb-f-field caosdb-f-entity-property" data-field-name="height" style="margin-left: 15px; margin-right: 15px;"> - <label class="control-label" data-property-name="height" for="height">height (cm)</label> + <div class="form-control caosdb-f-field caosdb-f-entity-property" data-field-name="height" style="margin-left: 15px; margin-right: 15px;"> + <label class="col-form-label" data-property-name="height" for="height">height (cm)</label> <div class="caosdb-f-property-value"> <input class="form-control caosdb-property-text-value" name="height" step="any" type="number"/> </div> </div> - <div class="form-group caosdb-f-field caosdb-f-entity-property" data-field-name="rectangular" style="margin-left: 15px; margin-right: 15px;"> - <label class="control-label" data-property-name="rectangular" for="rectangular">rectangular</label> + <div class="form-control caosdb-f-field caosdb-f-entity-property" data-field-name="rectangular" style="margin-left: 15px; margin-right: 15px;"> + <label class="col-form-label" data-property-name="rectangular" for="rectangular">rectangular</label> <div class="caosdb-f-property-value"> <input class="caosdb-property-text-value" name="rectangular" type="checkbox"/> </div> @@ -362,25 +362,25 @@ </fieldset> <fieldset class="form-inline caosdb-f-form-elements-subform" data-subform-name="new_subsamples" id="new_subsamples_6338" style="padding-left: 15px; padding-right: 15px;"> <legend>PP_Sample</legend> - <div class="form-group caosdb-f-field caosdb-f-entity-property" data-field-name="type"> + <div class="form-control caosdb-f-field caosdb-f-entity-property" data-field-name="type"> <div class="caosdb-f-property-value col-sm-9"> <input class="form-control caosdb-property-text-value" name="type" type="hidden" value="6338"/> </div> </div> - <div class="form-group caosdb-f-field caosdb-f-entity-property" data-field-name="width" style="margin-left: 15px; margin-right: 15px;"> - <label class="control-label" data-property-name="width" for="width">width (cm)</label> + <div class="form-control caosdb-f-field caosdb-f-entity-property" data-field-name="width" style="margin-left: 15px; margin-right: 15px;"> + <label class="col-form-label" data-property-name="width" for="width">width (cm)</label> <div class="caosdb-f-property-value"> <input class="form-control caosdb-property-text-value" name="width" step="any" type="number"/> </div> </div> - <div class="form-group caosdb-f-field caosdb-f-entity-property" data-field-name="height" style="margin-left: 15px; margin-right: 15px;"> - <label class="control-label" data-property-name="height" for="height">height (cm)</label> + <div class="form-control caosdb-f-field caosdb-f-entity-property" data-field-name="height" style="margin-left: 15px; margin-right: 15px;"> + <label class="col-form-label" data-property-name="height" for="height">height (cm)</label> <div class="caosdb-f-property-value"> <input class="form-control caosdb-property-text-value" name="height" step="any" type="number"/> </div> </div> - <div class="form-group caosdb-f-field caosdb-f-entity-property" data-field-name="rectangular" style="margin-left: 15px; margin-right: 15px;"> - <label class="control-label" data-property-name="rectangular" for="rectangular">rectangular</label> + <div class="form-control caosdb-f-field caosdb-f-entity-property" data-field-name="rectangular" style="margin-left: 15px; margin-right: 15px;"> + <label class="col-form-label" data-property-name="rectangular" for="rectangular">rectangular</label> <div class="caosdb-f-property-value"> <input class="caosdb-property-text-value" name="rectangular" type="checkbox"/> </div> @@ -388,8 +388,8 @@ </fieldset> </div> <div class="text-right caosdb-f-form-elements-footer"> - <button class="caosdb-f-form-elements-submit-button btn btn-primary btn-default" type="submit">Submit</button> - <button class="caosdb-f-form-elements-cancel-button btn btn-primary btn-default" type="button">Cancel</button> + <button class="caosdb-f-form-elements-submit-button btn btn-primary btn-secondary" type="submit">Submit</button> + <button class="caosdb-f-form-elements-cancel-button btn btn-primary btn-secondary" type="button">Cancel</button> </div> </form> </div> diff --git a/test/core/index.html b/test/core/index.html index 50d8cbef8003d6fb9ab383f94405bcfa07270774..834684b2e737be2e5f241eed3faaea73b3c4020c 100644 --- a/test/core/index.html +++ b/test/core/index.html @@ -22,72 +22,19 @@ * ** end header --> <html> -<head> - <meta charset="utf-8"/> - <title>WebCaosDB Unit Tests</title> - <link rel="stylesheet" href="css/qunit.css"/> - <link rel="stylesheet" href="css/webcaosdb.css"/> - <link rel="stylesheet" href="css/leaflet.css"/> -</head> -<body> - <div id="qunit"></div> - <div id="qunit-fixture"></div> - <script src="js/jquery.js"></script> - <script src="js/loglevel.js"></script> - <script src="js/bootstrap.js"></script> - <script src="js/bootstrap-select.js"></script> - <script src="js/bootstrap-autocomplete.min.js"></script> - <script src="js/webcaosdb.js"></script> - <script src="js/plotly.js"></script> - <script> - caosdb_modules.auto_init = false; - log.setLevel("trace"); - </script> - <script src="js/caosdb.js"></script> - <script src="js/state-machine.js"></script> - <script src="js/showdown.js"></script> - <script src="js/qunit.js"></script> - <script src="js/dropzone.js"></script> - <script src="js/setup.js"></script> - <script src="js/preview.js"></script> - <script src="js/annotation.js"></script> - <script src="js/edit_mode.js"></script> - <script src="js/query_shortcuts.js"></script> - <script src="js/ext_references.js"></script> - <script src="js/ext_xls_download.js"></script> - <script src="js/form_elements.js"></script> - <script src="js/tour.js"></script> - <script src="js/leaflet.js"></script> - <script src="js/leaflet-graticule.js"></script> - <script src="js/leaflet-latlng-graticule.js"></script> - <script src="js/leaflet-coordinates.js"></script> - <script src="js/proj4.js"></script> - <script src="js/proj4leaflet.js"></script> - <script src="js/ext_map.js"></script> - <script src="js/ext_bottom_line.js"></script> - <script src="js/ext_revisions.js"></script> - <script src="js/autocomplete.js"></script> - <script src="js/ext_sss_markdown.js"></script> - <script src="js/ext_trigger_crawler_form.js"></script> - <!--EXTENSIONS--> - <script src="js/modules/webcaosdb.js.js"></script> - <script src="js/modules/caosdb.js.js"></script> - <script src="js/modules/common.xsl.js"></script> - <script src="js/modules/entity.xsl.js"></script> - <script src="js/modules/welcome.xsl.js"></script> - <script src="js/modules/query.xsl.js"></script> - <script src="js/modules/annotation.xsl.js"></script> - <script src="js/modules/navbar.xsl.js"></script> - <script src="js/modules/edit_mode.js.js"></script> - <script src="js/modules/ext_xls_download.js.js"></script> - <script src="js/modules/query_shortcuts.js.js"></script> - <script src="js/modules/form_elements.js.js"></script> - <script src="js/modules/ext_references.js.js"></script> - <script src="js/modules/ext_map.js.js"></script> - <script src="js/modules/ext_bottom_line.js.js"></script> - <script src="js/modules/ext_revisions.js.js"></script> - <script src="js/modules/autocomplete.js.js"></script> - <script src="js/modules/ext_sss_markdown.js.js"></script> - <script src="js/modules/ext_trigger_crawler_form.js.js"></script> -</body> + <head> + <meta charset="utf-8" /> + <title>WebCaosDB Unit Tests</title> + <link rel="stylesheet" href="css/qunit.css" /> + <link rel="stylesheet" href="css/webcaosdb.css" /> + <link rel="stylesheet" href="css/leaflet.css" /> + </head> + <body> + <div id="qunit"></div> + <div id="qunit-fixture"></div> + <script> + var _caosdb_modules_auto_init = false; + </script> + <script src="webcaosdb.dist.js"></script> + </body> </html> diff --git a/test/core/js/modules/annotation.xsl.js b/test/core/js/modules/annotation.xsl.js index 20815816d5d1de75c4efeb11117f839bacd4ca1d..3640d93c0e1d33c638c92c9e2830c77c0e44ff40 100644 --- a/test/core/js/modules/annotation.xsl.js +++ b/test/core/js/modules/annotation.xsl.js @@ -35,7 +35,7 @@ QUnit.module("annotation.xsl", { this.testCases = []; - this.testCases[0] = '<Response><Record><Property name="annotationOf"/><History transaction="INSERT" datetime="2015-12-24T20:15:00" username="someuser"/><Property name="comment">This is a comment</Property></Record><Record><Property name="annotationOf"/></Record></Response>'; + this.testCases[0] = '<Response><Record><Property name="annotationOf"/><Version head="true" date="2015-12-24T20:15:00" username="someuser"/><Property name="comment">This is a comment</Property></Record><Record><Property name="annotationOf"/></Record></Response>'; assert.ok(str2xml(this.testCases[0])); } }); @@ -63,10 +63,10 @@ QUnit.test("Record rule returns li elements", function(assert){ var media = annos[0].children[0]; assert.equal(media.tagName, "DIV", "is DIV"); - assert.equal(media.className, "media", "className is media"); + assert.equal(media.className, "d-flex", "className is d-flex"); assert.equal(media.children.length, 2, "media has two children"); - assert.equal(media.children[0].className, "media-left"); - assert.equal(media.children[1].className, "media-body"); + assert.equal(media.children[0].className, "d-shrink-0"); + assert.equal(media.children[1].className, "flex-grow-1 ms-3"); }); @@ -75,12 +75,12 @@ QUnit.test("History element", function(assert){ var xml = str2xml(xml_str); var html = xslt(xml, this.annotationXSL); - var mediaBody = html.firstChild.getElementsByClassName("media-body")[0]; + var mediaBody = html.firstChild.getElementsByClassName("caosdb-f-comment-body")[0]; assert.ok(mediaBody, "media-body is there"); assert.ok(mediaBody.children.length>0,"media-body has children"); - var mediaHeading = mediaBody.getElementsByClassName("media-heading")[0]; + + var mediaHeading = html.firstChild.getElementsByClassName("caosdb-f-comment-header")[0]; assert.ok(mediaHeading, "media-heading is there"); - assert.equal(mediaHeading.parentNode, mediaBody, "media-heading is child of media-body"); assert.ok(xml2str(mediaHeading).indexOf("someuser")!==-1, "username is there"); assert.ok(xml2str(mediaHeading).indexOf("2015-12-24T20:15:00")!==-1, "datetime is there"); @@ -92,7 +92,7 @@ QUnit.test("Comment text", function(assert){ var html = xslt(xml, this.annotationXSL); - var mediaBody = html.firstChild.getElementsByClassName("media-body")[0]; + var mediaBody = html.firstChild.getElementsByClassName("caosdb-f-comment-body")[0]; assert.ok(mediaBody, "media-body is there"); assert.ok(mediaBody.children.length>0,"media-body has children"); var commentText = mediaBody.getElementsByClassName("caosdb-comment-annotation-text")[0]; diff --git a/test/core/js/modules/autocomplete.js.js b/test/core/js/modules/autocomplete.js.js deleted file mode 100644 index b75acdcad8b08e548423513ccdf78d7ca0ea7359..0000000000000000000000000000000000000000 --- a/test/core/js/modules/autocomplete.js.js +++ /dev/null @@ -1,71 +0,0 @@ -/* - * ** header v3.0 - * This file is a part of the CaosDB Project. - * - * Copyright (C) 2019 IndiScale GmbH - * - * 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/>. - * - * ** end header - */ - -'use strict'; - -QUnit.module("autocomplete.js", { - before: function (assert){ - autocomplete.retrieve_names = async function () { - return ['IceCore', 'Bag', 'IceSample', 'IceCream', 'Palette']; - } - } -}); - -QUnit.test("availability", function(assert) { - //assert.ok(bootstrap..init, "init available"); - assert.equal(autocomplete.version, "0.1", "test version"); - assert.ok(autocomplete.init, "init available"); -}); - - - -QUnit.test("filter", function(assert) { - assert.equal(autocomplete.filter('IceCore','Ice'), true, 'test filter') - assert.equal(autocomplete.filter('IceCore','iCe'), true, 'test filter') - assert.equal(autocomplete.filter('IceCore','Core'), false, 'test filter') - assert.equal(autocomplete.filter('Bag','Ice'), false, 'test filter') -}); - -QUnit.test("search", async function(assert) { - - var done = assert.async(2); - var gcallback = function(expresults){ - return function (results) { - assert.propEqual( - results, - expresults, - "test list filter"); - done(); - }; - }; - - await autocomplete.search("Ice", gcallback( - ['IceCore', 'IceSample', 'IceCream'] - )); - - await autocomplete.search("Core", gcallback([])); -}); - -QUnit.test("class", function(assert) { - assert.ok(autocomplete.toggle_completion , "toggle available"); - assert.ok(autocomplete.toggle_completion() , "toggle runs"); -}); diff --git a/test/core/js/modules/caosdb.js.js b/test/core/js/modules/caosdb.js.js index 76141117ffc845917c0e5ff50a165e7718663a31..8df5e2f9c2b933cd7b678286295961f2c73d7113 100644 --- a/test/core/js/modules/caosdb.js.js +++ b/test/core/js/modules/caosdb.js.js @@ -17,7 +17,7 @@ QUnit.module("caosdb.js", { }, err => {console.log(err);}); }, - + before: function(assert) { var done = assert.async(3); this.setTestDocument("x", done, ` @@ -133,18 +133,7 @@ QUnit.test("available", function(assert) { * Test whether properties are parsed correctly from the document tree. */ QUnit.test("getProperties", function(assert) { - try { - ps = getProperties(); - } - catch (e) { - assert.equal(e.message, "element is undefined"); - } - try { - ps = getProperties(undefined); - } - catch (e) { - assert.equal(e.message, "element is undefined"); - } + assert.throws(getProperties, "undefined element throws"); assert.equal(this.x.length, 4); @@ -332,59 +321,55 @@ QUnit.test("headingAttributes", function(assert) { * @author Alexander Schlemmer * Test replication of entities. */ -QUnit.test("replicationOfEntities", function(assert) { - var done = assert.async(); +QUnit.test("replicationOfEntities", async function(assert) { - var reptest = function(ent, respxml) { + var reptest = async function(k, ent, respxml) { var oldprops = getProperties(ent); var oldpars = getParents(ent); var doc = createResponse( createEntityXML(getEntityRole(ent), getEntityName(ent), getEntityID(ent), getProperties(ent), getParents(ent))); - assert.equal(xml2str(doc), respxml); + assert.equal(xml2str(doc).replace(/\s/g, ""), respxml.replace(/\s/g, "")); doc = createResponse( createEntityXML(getEntityRole(ent), getEntityName(ent), getEntityID(ent), getProperties(ent), getParents(ent), true)); - transformation.transformEntities(doc).then (x => { - ps = getProperties(x[0]); - pars = getParents(x[0]); - - assert.equal(getEntityRole(ent), getEntityRole(x[0])); - assert.equal(getEntityName(ent), getEntityName(x[0])); - assert.equal(getEntityID(ent), getEntityID(x[0])); - assert.equal(ps.length, oldprops.length); - for (var i=0; i<ps.length; i++) { - assert.equal(ps[i].name, oldprops[i].name); - assert.deepEqual(ps[i].value, oldprops[i].value); - assert.equal(ps[i].datatype, oldprops[i].datatype); - assert.equal(ps[i].list, oldprops[i].list); - assert.equal(ps[i].reference, oldprops[i].reference); - } - assert.equal(pars.length, oldpars.length); - for (var i=0; i<pars.length; i++) { - assert.equal(pars[i].name, oldpars[i].name); - assert.equal(pars[i].id, oldpars[i].id); - } - funj += 1; - // console.log(funj, maxfunccall); - if (funj == maxfunccall) { - done(); - } - }); + var k_2 = k; + var doc2 = str2xml(xml2str(doc)); + var x = await transformation.transformEntities(doc); + ps = getProperties(x[0]); + pars = getParents(x[0]); + + assert.equal(getEntityRole(ent), getEntityRole(x[0])); + assert.equal(getEntityName(ent), getEntityName(x[0])); + assert.equal(getEntityID(ent), getEntityID(x[0])); + assert.equal(ps.length, oldprops.length); + for (var i=0; i<ps.length; i++) { + assert.equal(ps[i].name, oldprops[i].name); + assert.deepEqual(ps[i].value, oldprops[i].value); + assert.equal(ps[i].datatype, oldprops[i].datatype); + assert.equal(ps[i].list, oldprops[i].list); + assert.equal(ps[i].reference, oldprops[i].reference); + } + assert.equal(pars.length, oldpars.length); + for (var i=0; i<pars.length; i++) { + assert.equal(pars[i].name, oldpars[i].name); + assert.equal(pars[i].id, oldpars[i].id); + } }; var respxmls = [ '<Response><Record name="nameofrecord"><Parent name="bla"/><Property name="A">245</Property></Record></Response>', '<Response><Record><Parent name="bla"/></Record></Response>', '<Response><Record id="17" name="nameofrec"><Parent id="244" name="bla"/><Parent id="217" name="bla2"/><Property name="B">245</Property><Property name="A">245.0</Property><Property name="A">245</Property></Record></Response>', - '<Response><Record><Parent name="bla"/><Property name="B">245</Property><Property name="A">245</Property><Property name="A"><Value>245</Value></Property><Property name="A"/><Property name="A"><Value>245</Value><Value>245</Value></Property><Property name="A"><Value>245</Value><Value>247</Value><Value>299</Value></Property></Record></Response>']; + `<Response> + <Record> + <Parent name="bla"/> + <Property name="B">245</Property><Property name="A">245</Property><Property name="A"><Value>245</Value></Property><Property name="A"/><Property name="A"><Value>245</Value><Value>245</Value></Property><Property name="A"><Value>245</Value><Value>247</Value><Value>299</Value></Property></Record></Response>`]; - var funj = 0; - var maxfunccall = this.x.length; - for (var i=0; i<this.x.length; i++) { - reptest(this.x[i], respxmls[i]); + for (var i=3; i<this.x.length; i++) { + var _ = await reptest(i, this.x[i], respxmls[i]); } }); @@ -474,3 +459,110 @@ QUnit.test("unset_entity_references", function(assert) { assert.equal(getProperties(r)[0].reference, true); } }); + + +QUnit.test("_constructXpaths", function (assert) { + assert.propEqual( + _constructXpaths([["id"], ["longitude"], ["latitude"]]), + ["@id", "Property[@name='longitude']", "Property[@name='latitude']"] + ); + assert.propEqual( + _constructXpaths([["Geo Location", "longitude"], ["latitude"]]), + ["Property[@name='Geo Location']//Property[@name='longitude']", "Property[@name='latitude']"] + ); + assert.propEqual( + _constructXpaths([["", "longitude"], ["latitude"]]), + ["Property//Property[@name='longitude']", "Property[@name='latitude']"] + ); + assert.propEqual( + _constructXpaths([["", "Geo Location", "", "longitude"]]), + ["Property//Property[@name='Geo Location']//Property//Property[@name='longitude']"] + ); +}); + + +QUnit.test("getPropertyValues", function (assert) { + const test_response = str2xml(` +<Response srid="851d063d-121b-4b67-98d4-6e5ad4d31e72" timestamp="1606214042274" baseuri="https://localhost:10443" count="8"> + <Query string="select Campaign.responsible.firstname from icecore" results="8"> + <ParseTree>(cq select (prop_sel (prop_subsel (selector_txt C a m p a i g n) . (prop_subsel (selector_txt r e s p o n s i b l e) . (prop_subsel (selector_txt f i r s t n a m e ))))) from (entity icecore) <EOF>)</ParseTree> + <Role/> + <Entity>icecore</Entity> + <Selection> + <Selector name="Campaign.responsible.firstname"/> + </Selection> + </Query> + <Record id="6525" name="Test_IceCore_1"> + <Permissions/> + <Property datatype="Campaign" id="6430" name="Campaign"> + <Record id="6516" name="Test-2020_Camp1"> + <Permissions/> + <Property datatype="REFERENCE" id="168" name="responsible"> + <Record id="6515" name="Test_Scientist"> + <Permissions/> + <Property datatype="DOUBLE" id="151" name="latitude" importance="FIX"> + 1.34 + <Permissions/> + </Property> + <Property datatype="DOUBLE" id="151" name="longitude" importance="FIX"> + 2 + <Permissions/> + </Property> + </Record> + <Permissions/> + </Property> + </Record> + <Permissions/> + </Property> + </Record> + <Record id="6526" name="Test_IceCore_2"> + <Permissions/> + <Property datatype="Campaign" id="6430" name="Campaign"> + <Record id="6516" name="Test-2020_Camp1"> + <Permissions/> + <Property datatype="REFERENCE" id="168" name="responsible"> + <Record id="6515" name="Test_Scientist"> + <Permissions/> + <Property datatype="DOUBLE" id="151" name="latitude" importance="FIX"> + 3 + <Permissions/> + </Property> + <Property datatype="DOUBLE" id="151" name="longitude" importance="FIX"> + 4.8345 + <Permissions/> + </Property> + </Record> + <Permissions/> + </Property> + </Record> + <Permissions/> + </Property> + </Record> +</Response>`); + + assert.propEqual( + getPropertyValues(test_response, [["id"], ["", "latitude"],["", "longitude"]]), + [["6525" ,"1.34", "2"], ["6526", "3", "4.8345"]]); +}); + +// Test for bug 103 +// If role is File when creating XML for entities, checksum, path and size must be given. +QUnit.test("unset_file_attributes", function(assert) { + // This should run: + var res1 = createEntityXML("Record", "test", 103, {}, {}); + assert.equal(xml2str(res1), "<Record id=\"103\" name=\"test\"/>"); + // This must throw an exception: + assert.throws(function () { + createEntityXML("File", "test", 103, {}, {}); + }); + // This should produce a valid XML. + var res2 = createEntityXML("File", "test", 103, {}, {}, + false, undefined, undefined, undefined, + "testfile.txt", "blablabla", 0); + assert.equal(xml2str(res2), "<File id=\"103\" name=\"test\" path=\"testfile.txt\" checksum=\"blablabla\" size=\"0\"/>"); + + var res3 = createFileXML("test", 103, {}, + "testfile.txt", "blablabla", 0, + undefined); + assert.equal(xml2str(res3), "<File id=\"103\" name=\"test\" path=\"testfile.txt\" checksum=\"blablabla\" size=\"0\"/>"); +}); diff --git a/test/core/js/modules/common.xsl.js b/test/core/js/modules/common.xsl.js index d77135d6d1e4e1f77c669a937425ec8c9934ad28..16f84cafc3bdb0b48a42cc10a1cdc03445f48541 100644 --- a/test/core/js/modules/common.xsl.js +++ b/test/core/js/modules/common.xsl.js @@ -53,6 +53,17 @@ QUnit.test("trim", function(assert) { assert.equal(trimmed.firstChild.textContent, 'test\n\ttest\n test', "trimmed"); }); +QUnit.test("remove_leading_ws", function(assert) { + var inject = '<xsl:template match="root"><xsl:call-template name="remove_leading_ws"><xsl:with-param name="str" select="text()"/></xsl:call-template></xsl:template>'; + console.log(inject); + var xsl = injectTemplate(this.commonXSL, inject); + var xml_str = '<root> \n \t \n abcd</root>'; + var xml = str2xml(xml_str); + console.log(xml); + var trimmed = xslt(xml, xsl); + console.log(trimmed); + assert.equal(trimmed.firstChild.textContent, 'abcd', "leading white spaces removed"); +}); QUnit.test("reverse", function(assert) { var inject = '<xsl:template match="root"><xsl:call-template name="reverse"><xsl:with-param name="str" select="text()"/></xsl:call-template></xsl:template>'; diff --git a/test/core/js/modules/edit_mode.js.js b/test/core/js/modules/edit_mode.js.js index ae11b04a380f162018de70a53409e34b4e6990c6..905b57ef0947f652e5d4959acd802244f0d4998d 100644 --- a/test/core/js/modules/edit_mode.js.js +++ b/test/core/js/modules/edit_mode.js.js @@ -93,9 +93,9 @@ QUnit.test("add_new_property", function (assert) { var done = assert.async(2); // test case setup - var entity = $("<div><div class='caosdb-properties' /></div>")[0]; + var entity = $(`<div><ul class='caosdb-properties'/><li class="caosdb-f-entity-property"><ol><li>value1</li></ol></li><li class="caosdb-f-edit-mode-property-dropzone"></li></ul>`)[0]; $(document.body).append(entity); - var new_prop = $("<div id='test_new_prop'/>")[0]; + var new_prop = $("<div class='test_new_prop'/>")[0]; // test bad cases assert_throws(assert, () => { @@ -113,8 +113,8 @@ QUnit.test("add_new_property", function (assert) { // test good cases - assert.equal($(entity).find("#test_new_prop").length, 0, "no property"); - entity.addEventListener("caosdb.edit_mode.property_added", function (e) { + assert.equal($(entity).find(".test_new_prop").length, 0, "no property"); + entity.addEventListener(edit_mode.property_added.type, function (e) { assert.ok(e.target === new_prop, "event fired on newprop"); assert.ok(this === entity, "event detected on entity"); done(); @@ -124,7 +124,7 @@ QUnit.test("add_new_property", function (assert) { "make_property_editable_cb called"); done(); }); - assert.equal($(entity).find("#test_new_prop").length, 1, "one property"); + assert.equal($(entity).find(".test_new_prop").length, 1, "one property"); // event has been fired and property has been added. $(entity).remove(); @@ -255,7 +255,7 @@ QUnit.test("make_datatype_input", function (assert) { const no_dt_input = edit_mode.make_datatype_input(undefined); no_dt_input.addEventListener("caosdb.field.ready", function (e) { var obj = form_elements - .form_to_object($(form_wrapper).append(no_dt_input)[0]); + .form_to_object($(form_wrapper).append(no_dt_input)[0])[0]; assert.propEqual(obj, { "atomic_datatype": "TEXT", "reference_scope": null, @@ -266,7 +266,7 @@ QUnit.test("make_datatype_input", function (assert) { const text_dt_input = edit_mode.make_datatype_input("TEXT"); text_dt_input.addEventListener("caosdb.field.ready", function (e) { var obj = form_elements - .form_to_object($(form_wrapper).append(text_dt_input)[0]); + .form_to_object($(form_wrapper).append(text_dt_input)[0])[0]; assert.propEqual(obj, { "atomic_datatype": "TEXT", "reference_scope": null, @@ -277,7 +277,7 @@ QUnit.test("make_datatype_input", function (assert) { const ref_dt_input = edit_mode.make_datatype_input("REFERENCE"); ref_dt_input.addEventListener("caosdb.field.ready", function (e) { var obj = form_elements - .form_to_object($(form_wrapper).append(ref_dt_input)[0]); + .form_to_object($(form_wrapper).append(ref_dt_input)[0])[0]; assert.propEqual(obj, { "atomic_datatype": "REFERENCE", "reference_scope": null, @@ -288,7 +288,7 @@ QUnit.test("make_datatype_input", function (assert) { const file_dt_input = edit_mode.make_datatype_input("FILE"); file_dt_input.addEventListener("caosdb.field.ready", function (e) { var obj = form_elements - .form_to_object($(form_wrapper).append(file_dt_input)[0]); + .form_to_object($(form_wrapper).append(file_dt_input)[0])[0]; assert.propEqual(obj, { "atomic_datatype": "FILE", "reference_scope": null, @@ -299,7 +299,7 @@ QUnit.test("make_datatype_input", function (assert) { const person_dt_input = edit_mode.make_datatype_input("Person"); person_dt_input.addEventListener("caosdb.field.ready", function (e) { var obj = form_elements - .form_to_object($(form_wrapper).append(person_dt_input)[0]); + .form_to_object($(form_wrapper).append(person_dt_input)[0])[0]; assert.propEqual(obj, { "atomic_datatype": "REFERENCE", "reference_scope": "Person", @@ -310,7 +310,7 @@ QUnit.test("make_datatype_input", function (assert) { const list_text_dt_input = edit_mode.make_datatype_input("LIST<TEXT>"); list_text_dt_input.addEventListener("caosdb.field.ready", function (e) { var obj = form_elements - .form_to_object($(form_wrapper).append(list_text_dt_input)[0]); + .form_to_object($(form_wrapper).append(list_text_dt_input)[0])[0]; assert.propEqual(obj, { "atomic_datatype": "TEXT", "reference_scope": null, @@ -322,7 +322,7 @@ QUnit.test("make_datatype_input", function (assert) { const list_ref_dt_input = edit_mode.make_datatype_input("LIST<REFERENCE>"); list_ref_dt_input.addEventListener("caosdb.field.ready", function (e) { var obj = form_elements - .form_to_object($(form_wrapper).append(list_ref_dt_input)[0]); + .form_to_object($(form_wrapper).append(list_ref_dt_input)[0])[0]; assert.propEqual(obj, { "atomic_datatype": "REFERENCE", "reference_scope": null, @@ -334,7 +334,7 @@ QUnit.test("make_datatype_input", function (assert) { const list_file_dt_input = edit_mode.make_datatype_input("LIST<FILE>"); list_file_dt_input.addEventListener("caosdb.field.ready", function (e) { var obj = form_elements - .form_to_object($(form_wrapper).append(list_file_dt_input)[0]); + .form_to_object($(form_wrapper).append(list_file_dt_input)[0])[0]; assert.propEqual(obj, { "atomic_datatype": "FILE", "reference_scope": null, @@ -346,7 +346,7 @@ QUnit.test("make_datatype_input", function (assert) { const list_per_dt_input = edit_mode.make_datatype_input("LIST<Person>"); list_per_dt_input.addEventListener("caosdb.field.ready", function (e) { var obj = form_elements - .form_to_object($(form_wrapper).append(list_per_dt_input)[0]); + .form_to_object($(form_wrapper).append(list_per_dt_input)[0])[0]; assert.propEqual(obj, { "atomic_datatype": "REFERENCE", "reference_scope": "Person", @@ -486,12 +486,9 @@ QUnit.test("remove_delete_button", function (assert) { { - const sleep = function sleep(ms) { - return new Promise(resolve => setTimeout(resolve, ms)); - } const datamodel = ` -<div><div class=\"btn-group-vertical\"><button type=\"button\" class=\"btn btn-default caosdb-f-edit-panel-new-button new-property\">Create new Property</button><button type=\"button\" class=\"btn btn-default caosdb-f-edit-panel-new-button new-recordtype\">Create new RecordType</button></div><div title=\"Drag and drop Properties from this panel to the Entities on the left.\" class=\"panel panel-default\"><div class=\"panel-heading\"><h5>Existing Properties</h5></div><div class=\"panel-body\"><div class=\"input-group\" style=\"width: 100%;\"><input class=\"form-control\" placeholder=\"filter...\" title=\"Type a name (full or partial).\" oninput=\"edit_mode.filter('properties');\" id=\"caosdb-f-filter-properties\" type=\"text\" /><span class=\"input-group-btn\"><button class=\"btn btn-default caosdb-f-edit-panel-new-button new-property caosdb-f-hide-on-empty-input\" title=\"Create this Property.\"><span class=\"glyphicon glyphicon-plus\"></span></button></span></div><ul class=\"caosdb-v-edit-list\"><li class=\"caosdb-f-edit-drag list-group-item caosdb-v-edit-drag\" id=\"caosdb-f-edit-p-20\">name</li><li class=\"caosdb-f-edit-drag list-group-item caosdb-v-edit-drag\" id=\"caosdb-f-edit-p-21\">unit</li><li class=\"caosdb-f-edit-drag list-group-item caosdb-v-edit-drag\" id=\"caosdb-f-edit-p-24\">description</li></ul></div></div><div title=\"Drag and drop RecordTypes from this panel to the Entities on the left.\" class=\"panel panel-default\"><div class=\"panel-heading\"><h5>Existing RecordTypes</h5></div><div class=\"panel-body\"><div class=\"input-group\" style=\"width: 100%;\"><input class=\"form-control\" placeholder=\"filter...\" title=\"Type a name (full or partial).\" oninput=\"edit_mode.filter('recordtypes');\" id=\"caosdb-f-filter-recordtypes\" type=\"text\" /><span class=\"input-group-btn\"><button class=\"btn btn-default caosdb-f-edit-panel-new-button new-recordtype caosdb-f-hide-on-empty-input\" title=\"Create this RecordType\"><span class=\"glyphicon glyphicon-plus\"></span></button></span></div><ul class=\"caosdb-v-edit-list\"><li class=\"caosdb-f-edit-drag list-group-item caosdb-v-edit-drag\" id=\"caosdb-f-edit-rt-30992\">Test</li><li class=\"caosdb-f-edit-drag list-group-item caosdb-v-edit-drag\" id=\"caosdb-f-edit-rt-31015\">Test2</li></ul></div></div></div>`; +<div><div class=\"btn-group-vertical\"><button type=\"button\" class=\"btn btn-secondary caosdb-f-edit-panel-new-button new-property\">Create new Property</button><button type=\"button\" class=\"btn btn-secondary caosdb-f-edit-panel-new-button new-recordtype\">Create new RecordType</button></div><div title=\"Drag and drop Properties from this panel to the Entities on the left.\" class=\"card\"><div class=\"card-header\"><h5>Existing Properties</h5></div><div class=\"card-body\"><div class=\"input-group\" style=\"width: 100%;\"><input class=\"form-control\" placeholder=\"filter...\" title=\"Type a name (full or partial).\" oninput=\"edit_mode.filter('properties');\" id=\"caosdb-f-filter-properties\" type=\"text\" /><span class=\"input-group-btn\"><button class=\"btn btn-secondary caosdb-f-edit-panel-new-button new-property caosdb-f-hide-on-empty-input\" title=\"Create this Property.\"><span class=\"glyphicon glyphicon-plus\"></span></button></span></div><ul class=\"caosdb-v-edit-list\"><li class=\"caosdb-f-edit-drag list-group-item caosdb-v-edit-drag\" id=\"caosdb-f-edit-p-20\">name</li><li class=\"caosdb-f-edit-drag list-group-item caosdb-v-edit-drag\" id=\"caosdb-f-edit-p-21\">unit</li><li class=\"caosdb-f-edit-drag list-group-item caosdb-v-edit-drag\" id=\"caosdb-f-edit-p-24\">description</li></ul></div></div><div title=\"Drag and drop RecordTypes from this panel to the Entities on the left.\" class=\"card\"><div class=\"card-header\"><h5>Existing RecordTypes</h5></div><div class=\"card-body\"><div class=\"input-group\" style=\"width: 100%;\"><input class=\"form-control\" placeholder=\"filter...\" title=\"Type a name (full or partial).\" oninput=\"edit_mode.filter('recordtypes');\" id=\"caosdb-f-filter-recordtypes\" type=\"text\" /><span class=\"input-group-btn\"><button class=\"btn btn-secondary caosdb-f-edit-panel-new-button new-recordtype caosdb-f-hide-on-empty-input\" title=\"Create this RecordType\"><span class=\"glyphicon glyphicon-plus\"></span></button></span></div><ul class=\"caosdb-v-edit-list\"><li class=\"caosdb-f-edit-drag list-group-item caosdb-v-edit-drag\" id=\"caosdb-f-edit-rt-30992\">Test</li><li class=\"caosdb-f-edit-drag list-group-item caosdb-v-edit-drag\" id=\"caosdb-f-edit-rt-31015\">Test2</li></ul></div></div></div>`; edit_mode.query = async function(q) { return []; diff --git a/test/core/js/modules/entity.xsl.js b/test/core/js/modules/entity.xsl.js index c607ee28bf7888f94e3086abf8e033e1532d0d09..21ce97ac3536b325ba77d6bc2d482b926619ae5d 100644 --- a/test/core/js/modules/entity.xsl.js +++ b/test/core/js/modules/entity.xsl.js @@ -55,27 +55,12 @@ QUnit.test("Property names are not links anymore", function(assert) { var xml_str = '<Property name="pname" id="2345" datatype="TEXT">pvalue</Property>'; var xml = str2xml(xml_str); var html = xslt(xml, xsl); - assert.equal(html.firstElementChild.getElementsByClassName("caosdb-property-name")[0].outerHTML, '<strong class=\"caosdb-property-name\">pname</strong>', "link there"); + assert.equal(html.firstElementChild.getElementsByClassName("caosdb-property-name")[0].outerHTML, '<span class=\"caosdb-property-name\">pname</span>', "link there"); }); -// QUnit.test("parent name is bold link", function(assert) { -// // make this xsl sheet accessible -// let html = applyTemplates(str2xml('<Parent name="TestParent" id="1234" description="DESC"/>'), this.entityXSL, 'entity-body'); -// assert.ok(html); - -// var name_e = html.firstElementChild.getElementsByClassName("caosdb-parent-name")[0]; -// assert.ok(name_e, "element is there"); -// assert.equal(name_e.tagName, "A", "is link"); -// assert.equal(name_e.getAttribute("href"), "/entitypath/1234", "href location"); -// assert.equal(window.getComputedStyle(name_e)["font-weight"], "700", "font is bold"); -// }); - QUnit.test("TestRecordType data type is recognized as a reference", function(assert) { - // inject an entrance rule - var xsl = getXSLScriptClone(this.entityXSL); - var entry_t = xsl.createElement("xsl:template"); - xsl.firstElementChild.appendChild(entry_t); - entry_t.outerHTML = '<xsl:template match="/"><xsl:apply-templates select="Property" mode="property-value"/></xsl:template>'; + var tmpl = '<xsl:template match="/"><xsl:apply-templates select="Property" mode="property-value"/></xsl:template>'; + var xsl = injectTemplate(this.entityXSL, tmpl); var xml_str = '<Property name="TestProperty" id="1234" description="DESC" type="TestRecordType">5678</Property>'; var xml = str2xml(xml_str); @@ -264,6 +249,67 @@ QUnit.test("data-version-successor attribute", function(assert) { assert.equal($(html).find("div.caosdb-entity-panel[data-version-successor]").length, 0, "data-version-successor attribute not present"); }); +QUnit.test("version full history", function (assert) { + var xmlstr = ` + <Response username="user1" realm="Realm1" srid="31ce8ea1-6c9b-4a82-82ec-9f6f3edd2622" timestamp="1606225647516" baseuri="https://localhost:10443" count="1"> + <Record id="3373" name="TestRecord1-10thVersion" description="This is the 10th version."> + <Permissions> + <Permission name="RETRIEVE:HISTORY" /> + </Permissions> + <Version id="vid6" username="user1" realm="Realm1" date="date6" completeHistory="true"> + <Predecessor id="vid5" username="user1" realm="Realm1" date="date5"> + <Predecessor id="vid4" username="user1" realm="Realm1" date="date4"> + <Predecessor id="vid3" username="user1" realm="Realm1" date="date3"> + <Predecessor id="vid2" username="user1" realm="Realm1" date="date2"> + <Predecessor id="vid1" username="user1" realm="Realm1" date="date1" /> + </Predecessor> + </Predecessor> + </Predecessor> + </Predecessor> + <Successor id="vid7" username="user1" realm="Realm1" date="date7"> + <Successor id="vid8" username="user1" realm="Realm1" date="date8"> + <Successor id="vid9" username="user1" realm="Realm1" date="date9"> + <Successor id="vid10" username="user1" realm="Realm1" date="date10" /> + </Successor> + </Successor> + </Successor> + </Version> + <Parent id="3372" name="TestRT" /> + </Record> +</Response> +`; + var xml = str2xml(xmlstr); + var html = applyTemplates(xml, this.entityXSL, "entities", "*"); + var version_info = $(html).find(".caosdb-f-entity-version-info"); + var table_elem = $(version_info).find("table"); + + var TAB = "%09", NEWL = "%0A", usr = "user1@Realm1", + path = "/entitypath/3373"; + var export_table = `data:text/csv;charset=utf-8,Entity ID${TAB}Version ID${TAB}Date${TAB}User${TAB}URI${NEWL}3373${TAB}vid10${TAB}date10${TAB}${usr}${TAB}${path}@vid10${NEWL}3373${TAB}vid9${TAB}date9${TAB}${usr}${TAB}${path}@vid9${NEWL}3373${TAB}vid8${TAB}date8${TAB}${usr}${TAB}${path}@vid8${NEWL}3373${TAB}vid7${TAB}date7${TAB}${usr}${TAB}${path}@vid7${NEWL}3373${TAB}vid6${TAB}date6${TAB}${usr}${TAB}${path}@vid6${NEWL}3373${TAB}vid5${TAB}date5${TAB}${usr}${TAB}${path}@vid5${NEWL}3373${TAB}vid4${TAB}date4${TAB}${usr}${TAB}${path}@vid4${NEWL}3373${TAB}vid3${TAB}date3${TAB}${usr}${TAB}${path}@vid3${NEWL}3373${TAB}vid2${TAB}date2${TAB}${usr}${TAB}${path}@vid2${NEWL}3373${TAB}vid1${TAB}date1${TAB}${usr}${TAB}${path}@vid1` + assert.equal(version_info.length, 1); + assert.equal(table_elem.length, 1); + assert.equal(version_history.get_history_tsv(table_elem[0]), export_table); +}); + +QUnit.test("Transforming abstract properties", function (assert) { + var xmlstr = `<Property id="3842" name="reftotestrt" datatype="TestRT"> + <Version id="04ad505da057603a9177a1fcf6c9efd5f3690fe4" date="2020-11-23T10:38:02.936+0100" /> + </Property>`; + var xml = str2xml(xmlstr); + var html = applyTemplates(xml, this.entityXSL, "entity-body", "Property"); + var prop = getPropertyFromElement(html.firstElementChild); + assert.propEqual(prop, { + "datatype": "TestRT", + "html": {}, + "id": "3842", + "list": false, + "name": "reftotestrt", + "reference": true, + "unit": undefined, + "value": ""} + ); +}); + /* MISC FUNCTIONS */ function applyTemplates(xml, xsl, mode, select = "*") { let entryRule = '<xsl:template priority="9" match="/"><xsl:apply-templates select="' + select + '" mode="' + mode + '"/></xsl:template>'; @@ -271,14 +317,19 @@ function applyTemplates(xml, xsl, mode, select = "*") { return xslt(xml, modXsl); } -function callTemplate(xsl, template, params) { - let entryRuleStart = '<xsl:template priority="9" match="/"><xsl:call-template name="' + template + '">'; - let entryRuleEnd = '</xsl:call-template></xsl:template>'; +function callTemplate(xsl, template, params, wrap_call) { + let entryRuleStart = '<xsl:call-template name="' + template + '">'; + let entryRuleEnd = '</xsl:call-template>'; var entryRule = entryRuleStart; for (name in params) { entryRule += '<xsl:with-param name="' + name + '"><xsl:value-of select="\'' + params[name] + '\'"/></xsl:with-param>'; } entryRule += entryRuleEnd; + if (typeof wrap_call == "function") { + entryRule = wrap_call(entryRule); + } + entryRule = '<xsl:template xmlns="http://www.w3.org/1999/xhtml" priority="9" match="/">' + + entryRule + '</xsl:template>'; let modXsl = injectTemplate(xsl, entryRule); return xslt(str2xml('<root/>'), modXsl); } diff --git a/test/core/js/modules/ext_autocomplete.js.js b/test/core/js/modules/ext_autocomplete.js.js new file mode 100644 index 0000000000000000000000000000000000000000..e8776f945b7bb46a0d431eb2d0ac0f7fe21419fc --- /dev/null +++ b/test/core/js/modules/ext_autocomplete.js.js @@ -0,0 +1,72 @@ +/* + * ** header v3.0 + * This file is a part of the CaosDB Project. + * + * Copyright (C) 2019 IndiScale GmbH + * + * 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/>. + * + * ** end header + */ + +'use strict'; + +QUnit.module("ext_autocomplete.js", { + before: function (assert){ + ext_autocomplete.retrieve_names = async function () { + return ['IceCore', 'Bag', 'IceSample', 'IceCream', 'Palette']; + } + ext_autocomplete.init(); + + } +}); + +QUnit.test("availability", function(assert) { + //assert.ok(bootstrap..init, "init available"); + assert.equal(ext_autocomplete.version, "0.1", "test version"); + assert.ok(ext_autocomplete.init, "init available"); +}); + + + +QUnit.test("starts_with_filter", function(assert) { + assert.equal(ext_autocomplete.starts_with_filter('IceCore','Ice'), true, 'test filter') + assert.equal(ext_autocomplete.starts_with_filter('IceCore','iCe'), true, 'test filter') + assert.equal(ext_autocomplete.starts_with_filter('IceCore','Core'), false, 'test filter') + assert.equal(ext_autocomplete.starts_with_filter('Bag','Ice'), false, 'test filter') +}); + +QUnit.test("search", async function(assert) { + + var done = assert.async(2); + var gcallback = function(expresults){ + return function (results) { + assert.propEqual( + results, + expresults, + "test list filter"); + done(); + }; + }; + await ext_autocomplete.search("Ice", + gcallback( ['IceCore', 'IceSample', 'IceCream']) + ); + + await ext_autocomplete.search("Core", gcallback([])); +}); + +QUnit.test("class", function(assert) { + assert.ok(ext_autocomplete.switch_on_completion , "toggle available"); + assert.ok(ext_autocomplete.switch_on_completion() , "toggle runs"); +}); diff --git a/test/core/js/modules/ext_bookmarks.js.js b/test/core/js/modules/ext_bookmarks.js.js new file mode 100644 index 0000000000000000000000000000000000000000..831df74231e479d5b4550524f6bc0da617c98fb3 --- /dev/null +++ b/test/core/js/modules/ext_bookmarks.js.js @@ -0,0 +1,181 @@ +/* + * ** header v3.0 + * This file is a part of the CaosDB Project. + * + * Copyright (C) 2020 IndiScale GmbH <info@indiscale.com> + * Copyright (C) 2020 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/>. + * + * ** end header + */ + +'use strict'; + +QUnit.module("ext_bookmarks.js", { + before: function (assert) { + // setup before module + ext_bookmarks.set_collection_id("test"); + // otherwise tests would collide with actual bookmarks + }, + beforeEach: function (assert) { + // setup before each test + }, + afterEach: function (assert) { + // teardown after each test + ext_bookmarks.clear_bookmark_storage(); + connection._init(); + }, + after: function (assert) { + // teardown after module + } +}); + +QUnit.test("parse_uri", function(assert) { + assert.equal(typeof ext_bookmarks.parse_uri(""), "undefined"); + assert.equal(typeof ext_bookmarks.parse_uri("asdf"), "undefined"); + assert.equal(typeof ext_bookmarks.parse_uri("https://localhost:1234/Entity/sada?sadfasd#sdfgdsf"), "undefined"); + + assert.propEqual(ext_bookmarks.parse_uri("safgsa/123&456&789?sadfasdf#_bm_1"), + {bookmarks: ["123", "456", "789"], collection_id: "1"}); +}); + +QUnit.test("get_bookmarks, clear_bookmark_storage", function(assert) { + assert.propEqual(ext_bookmarks.get_bookmarks(), []); + + ext_bookmarks.bookmark_storage[ext_bookmarks.get_key("sdfg")] = "3456" + assert.propEqual(ext_bookmarks.get_bookmarks(), ["3456"]); + + ext_bookmarks.clear_bookmark_storage(); + assert.propEqual(ext_bookmarks.get_bookmarks(), []); +}); + +QUnit.test("get_export_table", async function (assert) { + connection.get = (id) => `<root><Response><File id="${id}" path="testpath_${id.split("/")[1]}"><Version id="abcHead"/></File></Response></root>`; + const TAB = "%09"; + const NEWL = "%0A"; + const context_root = connection.getBasePath() + "Entity/"; + var table = await ext_bookmarks.get_export_table( + ["123@ver1", "456@ver2", "789@ver3", "101112", "@131415"]); + assert.equal(table, + `data:text/csv;charset=utf-8,ID${TAB}Version${TAB}URI${TAB}Path${TAB}Name${TAB}RecordType${NEWL}123${TAB}ver1${TAB}${context_root}123@ver1${TAB}testpath_123@ver1${TAB}${TAB}${NEWL}456${TAB}ver2${TAB}${context_root}456@ver2${TAB}testpath_456@ver2${TAB}${TAB}${NEWL}789${TAB}ver3${TAB}${context_root}789@ver3${TAB}testpath_789@ver3${TAB}${TAB}${NEWL}101112${TAB}abcHead${TAB}${context_root}101112@abcHead${TAB}testpath_101112${TAB}${TAB}${NEWL}${TAB}131415${TAB}${context_root}@131415${TAB}testpath_@131415${TAB}${TAB}`); + +}); + +QUnit.test("update_clear_button", function (assert) { + const clear_button = $(`<div id="caosdb-f-bookmarks-clear"/>`); + $("body").append(clear_button); + + assert.notOk(clear_button.is(".disabled")); + ext_bookmarks.update_clear_button([]); + assert.ok(clear_button.is(".disabled")); + + ext_bookmarks.update_clear_button(["asdf"]); + assert.notOk(clear_button.is(".disabled")); + + ext_bookmarks.update_clear_button(["asdf"]); + assert.notOk(clear_button.is(".disabled")); + + ext_bookmarks.update_clear_button([]); + assert.ok(clear_button.is(".disabled")); + + clear_button.remove(); +}); + +QUnit.test("update_export_link", function (assert) { + const export_link = $(`<div id="caosdb-f-bookmarks-export-link"/>`); + $("body").append(export_link); + + assert.notOk(export_link.is(".disabled")); + ext_bookmarks.update_export_link([]); + assert.ok(export_link.is(".disabled")); + + ext_bookmarks.update_export_link(["asdf"]); + assert.notOk(export_link.is(".disabled")); + + ext_bookmarks.update_export_link(["asdf"]); + assert.notOk(export_link.is(".disabled")); + + ext_bookmarks.update_export_link([]); + assert.ok(export_link.is(".disabled")); + + export_link.remove(); +}); + +QUnit.test("update_collection_link", function (assert) { + const collection_link = $( + `<div id="caosdb-f-bookmarks-collection-link"><a/></div>`); + const a = collection_link.find("a")[0]; + $("body").append(collection_link); + + assert.notOk(collection_link.is(".disabled")); + assert.notOk(a.href); + + ext_bookmarks.update_collection_link([]); + assert.ok(collection_link.is(".disabled")); + assert.notOk(a.href); + + ext_bookmarks.update_collection_link(["asdf"]); + assert.notOk(collection_link.is(".disabled")); + assert.equal(a.href, ext_bookmarks.get_collection_link(["asdf"])); + + ext_bookmarks.update_collection_link(["asdf", "sdfg"]); + assert.notOk(collection_link.is(".disabled")); + assert.equal(a.href, ext_bookmarks.get_collection_link(["asdf", "sdfg"])); + + ext_bookmarks.update_collection_link([]); + assert.ok(collection_link.is(".disabled")); + assert.notOk(a.href); + + collection_link.remove(); +}); + +QUnit.test("bookmark buttons", function (assert) { + const inactive_button = $(`<div data-bmval="id1"/>`); + const active_button = $(`<div class="active" data-bmval="id2"/>`); + const broken_button = $(`<div data-bmval=""/>`); + const non_button = $(`<div data-bla="sadf"/>)`); + const outside_button = $(`<div data-bmval="id3"/>`); + const inside_buttons = $("<div/>").append([inactive_button, active_button, + broken_button, non_button]); + + // get_bookmark_buttons + assert.equal(ext_bookmarks.get_bookmark_buttons("body").length, 0); + + $("body").append([outside_button, inside_buttons]); + + assert.equal(ext_bookmarks.get_bookmark_buttons("body").length, 4, "all but no_button"); + assert.equal(ext_bookmarks.get_bookmark_buttons(inside_buttons).length, 3, "all but non_button and outside_button"); + + // get_value + assert.equal(ext_bookmarks.get_value(inactive_button), "id1"); + assert.notOk(ext_bookmarks.get_value(non_button)); + assert.notOk(ext_bookmarks.get_value(broken_button)); + + // init_button + assert.ok(active_button.is(".active")); + ext_bookmarks.init_button(active_button); + assert.notOk(active_button.is(".active")); + + ext_bookmarks.bookmark_storage[ext_bookmarks.get_key("id1")] = "id1" + assert.notOk(inactive_button.is(".active")); + ext_bookmarks.init_button(inactive_button); + assert.ok(inactive_button.is(".active")); + + ext_bookmarks.clear_bookmark_storage(); + assert.notOk(inactive_button.is(".active"), "clear_bookmark_storage removes active class"); + + inside_buttons.remove(); + outside_button.remove(); +}); diff --git a/test/core/js/modules/ext_bottom_line.js b/test/core/js/modules/ext_bottom_line.js.js similarity index 88% rename from test/core/js/modules/ext_bottom_line.js rename to test/core/js/modules/ext_bottom_line.js.js index 21c92167271f87554a9af881554d33db686d9194..48dc64f231c1dc5929eece1f44756d60cb17c0a1 100644 --- a/test/core/js/modules/ext_bottom_line.js +++ b/test/core/js/modules/ext_bottom_line.js.js @@ -24,10 +24,6 @@ var ext_bottom_line_test_suite = function ($, ext_bottom_line, QUnit) { - const sleep = (ms) => { - return new Promise(res => setTimeout(res, ms)) - } - var test_config = { "version": 0.1, "fallback": "blablabla", "creators": [ @@ -45,7 +41,7 @@ var ext_bottom_line_test_suite = function ($, ext_bottom_line, QUnit) { }, { "id": "test.success-2", "is_applicable": "(entity) => getParents(entity).map(par => par.name).includes('TestPreviewRecordType') && getEntityName(entity) !== 'TestPreviewRecord-fall-back'", - "create": "(entity) => { return plotly_preview.create_plot([{x: [1,2,3,4,5], y: [1,2,4,8,16]}], { 'xaxis': {'title': 'time [samples]'}}); }" + "create": "(entity) => { return plotly_preview.create_plot([{x: [1,2,3,4,5], y: [1,2,4,8,16]}], { 'xaxis': {'title': 'time [samples]'}}, {displaylogo: false}); }" } ] }; @@ -67,7 +63,7 @@ var ext_bottom_line_test_suite = function ($, ext_bottom_line, QUnit) { }); QUnit.test("_creators", function (assert) { - assert.equal(ext_bottom_line._creators.length, 7, "seven creators"); + assert.equal(ext_bottom_line._creators.length, 9, "nine creators, 5 default ones, 4 from these tests."); }); QUnit.test("add_preview_container", function(assert) { @@ -105,7 +101,7 @@ var ext_bottom_line_test_suite = function ($, ext_bottom_line, QUnit) { assert.equal(container.text(), "blablabla"); break; case "error": - assert.equal(container.text(), "An error occured while loading this preview"); + assert.equal(container.text(), "An error occured while loading this preview.Test Error"); break; case "load-forever": assert.equal(container.text(), "Please wait..."); @@ -121,4 +117,12 @@ var ext_bottom_line_test_suite = function ($, ext_bottom_line, QUnit) { }); + QUnit.test("tiff converter", async function(assert) { + let entity_xml = `<Response><File path="../pics/saturn.tif"/></Response>`; + const entity = (await transformation.transformEntities(str2xml(entity_xml)))[0]; + const tiff_preview = await ext_bottom_line._creators.filter((c) => c.id == "_default_creators.tiff_images")[0].create(entity); + + assert.equal($(tiff_preview).find("img").attr("src").slice(0,21), "data:image/png;base64", "decoded tiff to png"); + }); + }($, ext_bottom_line, QUnit); diff --git a/test/core/js/modules/ext_file_download.js.js b/test/core/js/modules/ext_file_download.js.js new file mode 100644 index 0000000000000000000000000000000000000000..1103f61304c56259cc1a54a94c0c6b6b201d2aa5 --- /dev/null +++ b/test/core/js/modules/ext_file_download.js.js @@ -0,0 +1,65 @@ +/* + * ** header v3.0 + * This file is a part of the CaosDB Project. + * + * Copyright (C) 2020 IndiScale GmbH <info@indiscale.com> + * Copyright (C) 2020 Henrik tom Wörden <h.tomwoerden@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/>. + * + * ** end header + */ + +'use strict'; + +QUnit.module("ext_file_download.js", { + before: function (assert) { + // setup before module + }, + beforeEach: function (assert) { + // setup before each test + }, + afterEach: function (assert) { + // teardown after each test + }, + after: function (assert) { + // teardown after module + } +}); + +QUnit.test("chunk_list ", function(assert) { + const li = [1,2,3,4,5,6,7]; + const res = ext_file_download.chunk_list(li, 3); + assert.equal(res.length, 3, "number of parts"); + assert.propEqual(res[2], [7], "number of parts"); +}); + +QUnit.test("collect_ids ", function (assert) { + const line = id => $(`<tr data-entity-id="${id}"/>`); + const prop_val = x => $(`<div class="caosdb-f-property-value"/>`); + const single_val =x => $(`<div class="caosdb-f-property-single-raw-value caosdb-id">${x}</div>`); + + + const line1 = line("34"); + line1.append([prop_val().append(single_val("5")),prop_val().append(single_val("6"))]) + $("body").append([line1]); + + const res = ext_file_download.collect_ids() + assert.ok(res.indexOf("5") > -1, "missing id"); + assert.ok(res.indexOf("6") > -1, "missing id"); + assert.ok(res.indexOf("34") > -1, "missing id"); + + line1.remove(); + +}); diff --git a/test/core/js/modules/ext_map.js.js b/test/core/js/modules/ext_map.js.js index 9d068ad1c688c7a8643c18846103ea33a10c8874..9b6b01022d8106153d50eaa906a0ce33803a8dc3 100644 --- a/test/core/js/modules/ext_map.js.js +++ b/test/core/js/modules/ext_map.js.js @@ -23,10 +23,13 @@ 'use strict'; QUnit.module("ext_map.js", { - before: function(assert) { + before: function (assert) { var lat = "latitude"; var lng = "longitude"; - this.datamodel = { lat: lat, lng: lng }; + this.datamodel = { + lat: lat, + lng: lng + }; this.test_map_entity = ` <div class="caosdb-entity-panel caosdb-properties"> <div class="caosdb-id">1234</div> @@ -42,22 +45,22 @@ QUnit.module("ext_map.js", { </div> </div>`; }, - beforeEach: function(assert) { + beforeEach: function (assert) { sessionStorage.removeItem("caosdb_map.view"); } }); -QUnit.test("availability", function(assert) { - assert.equal(caosdb_map.version, "0.3", "test version"); +QUnit.test("availability", function (assert) { + assert.equal(caosdb_map.version, "0.4", "test version"); assert.ok(caosdb_map.init, "init available"); }); -QUnit.test("default config", function(assert) { +QUnit.test("default config", function (assert) { assert.ok(caosdb_map._default_config); assert.equal(caosdb_map._default_config.version, caosdb_map.version, "version"); }); -QUnit.test("load_config", async function(assert) { +QUnit.test("load_config", async function (assert) { assert.ok(caosdb_map.load_config, "available"); var config = await caosdb_map.load_config("non_existing.json"); assert.ok(config, "returns something"); @@ -65,39 +68,43 @@ QUnit.test("load_config", async function(assert) { assert.equal(config.views[0].id, "UNCONFIGURED", "view has id 'UNCONFIGURED'."); }); -QUnit.test("check_config", function(assert) { +QUnit.test("check_config", function (assert) { assert.ok(caosdb_map.check_config(caosdb_map._default_config), "default config ok"); - assert.throws(()=>caosdb_map.check_config({"version": "wrong version",}), /The version of the configuration does not match the version of this implementation of the caosdb_map module. Should be '.*', was 'wrong version'./, "wrong version"); + assert.throws(() => caosdb_map.check_config({ + "version": "wrong version", + }), /The version of the configuration does not match the version of this implementation of the caosdb_map module. Should be '.*', was 'wrong version'./, "wrong version"); }); -QUnit.test("check dependencies", function(assert) { +QUnit.test("check dependencies", function (assert) { assert.ok(caosdb_map.check_dependencies, "available"); - assert.propEqual(caosdb_map.dependencies, ["log", {"L": ["latlngGraticule", "Proj"]}, "navbar", "caosdb_utils"]); + assert.propEqual(caosdb_map.dependencies, ["log", { + "L": ["latlngGraticule", "Proj"] + }, "navbar", "caosdb_utils"]); assert.ok(caosdb_map.check_dependencies(), "deps available"); }); -QUnit.test("create_toggle_map_button", function(assert) { +QUnit.test("create_toggle_map_button", function (assert) { assert.ok(caosdb_map.create_toggle_map_button, "available"); var button = caosdb_map.create_toggle_map_button(); - assert.equal(button.tagName, "BUTTON", "is button"); - assert.ok($(button).hasClass("caosdb-f-toggle-map-button"),"has caosdb-f-toggle-map-button class"); + assert.equal(button.tagName, "A", "is A"); + assert.ok($(button).hasClass("caosdb-f-toggle-map-button"), "has caosdb-f-toggle-map-button class"); assert.equal($(button).text(), "Map", "button says 'Map'"); // set other content: button = caosdb_map.create_toggle_map_button("Karte"); - assert.equal(button.tagName, "BUTTON", "is button"); - assert.ok($(button).hasClass("caosdb-f-toggle-map-button"),"has caosdb-f-toggle-map-button class"); + assert.equal(button.tagName, "A", "is A"); + assert.ok($(button).hasClass("caosdb-f-toggle-map-button"), "has caosdb-f-toggle-map-button class"); assert.equal($(button).text(), "Karte", "button says 'Karte'"); }); -QUnit.test("bind_toggle_map", function(assert) { +QUnit.test("bind_toggle_map", function (assert) { let button = $("<button/>")[0]; let done = assert.async(); assert.ok(caosdb_map.bind_toggle_map, "available"); - assert.throws(()=>caosdb_map.bind_toggle_map(button), /parameter 'toggle_cb'.* was undefined/, "no function throws"); - assert.throws(()=>caosdb_map.bind_toggle_map("test", ()=>{}), /parameter 'button'.* was string/, "string button throws"); + assert.throws(() => caosdb_map.bind_toggle_map(button), /parameter 'toggle_cb'.* was undefined/, "no function throws"); + assert.throws(() => caosdb_map.bind_toggle_map("test", () => {}), /parameter 'button'.* was string/, "string button throws"); assert.equal(caosdb_map.bind_toggle_map(button, done), button, "call returns button"); // button click calls 'done' @@ -105,12 +112,12 @@ QUnit.test("bind_toggle_map", function(assert) { }); -QUnit.test("create_map", function(assert) { +QUnit.test("create_map", function (assert) { assert.equal(typeof caosdb_map.create_map_view, "function", "function available"); }); -QUnit.test("create_map_panel", function(assert) { +QUnit.test("create_map_panel", function (assert) { assert.ok(caosdb_map.create_map_panel, "available"); let panel = caosdb_map.create_map_panel(); assert.equal(panel.tagName, "DIV", "is div"); @@ -118,33 +125,35 @@ QUnit.test("create_map_panel", function(assert) { assert.ok($(panel).hasClass("container"), "has class container"); }); -QUnit.test("create_map_view", function(assert) { - var view_config = $.extend(true, {}, caosdb_map._unconfigured_views[0], - {"select": true, "view_change": true}); +QUnit.test("create_map_view", function (assert) { + var view_config = $.extend(true, {}, caosdb_map._unconfigured_views[0], { + "select": true, + "view_change": true + }); var map_panel = $("<div/>"); var map = caosdb_map.create_map_view(map_panel[0], view_config); - console.log(map_panel[0]); assert.ok(map instanceof L.Map, "map instance created"); - assert.ok(map_panel.hasClass("leaflet-container"), "map_panel has .leaflet-container child"); + assert.equal($(map_panel).find(".leaflet-container").length, 1, "map_panel has .leaflet-container child"); assert.notOk(map._crs, "no special crs"); map.remove(); + map_panel = $("<div/>"); // test with pre-defined crs view_config["crs"] = "Simple"; map = caosdb_map.create_map_view(map_panel[0], view_config); - console.log(map_panel[0]); assert.ok(map instanceof L.Map, "map instance created"); - assert.ok(map_panel.hasClass("leaflet-container"), "map_panel has .leaflet-container child"); + assert.equal($(map_panel).find(".leaflet-container").length, 1, "map_panel has .leaflet-container child"); assert.equal(map._crs, L.CRS.Simple, "map has SIMPLE crs"); map.remove(); + map_panel = $("<div/>"); // test with special crs: - view_config["crs"] = { + view_config["crs"] = { "code": "EPSG:3995", "proj4def": "+proj=stere +lat_0=90 +lat_ts=71 +lon_0=0 +k=1 +x_0=0 +y_0=0 +datum=WGS84 +units=m +no_defs", "options": { @@ -156,14 +165,14 @@ QUnit.test("create_map_view", function(assert) { console.log(map_panel[0]); assert.ok(map instanceof L.Map, "map instance created"); - assert.ok(map_panel.hasClass("leaflet-container"), "map_panel has .leaflet-container child"); + assert.equal($(map_panel).find(".leaflet-container").length, 1, "map_panel has .leaflet-container child"); assert.ok(map._crs instanceof L.Proj.CRS, "map has special crs"); map.remove(); }); -QUnit.test("get_map_entities", function(assert) { +QUnit.test("get_map_entities", function (assert) { var datamodel = this.datamodel; var container = $('<div/>').append(this.test_map_entity); var map_objects = caosdb_map.get_map_entities(container[0], datamodel); @@ -171,12 +180,12 @@ QUnit.test("get_map_entities", function(assert) { }); -QUnit.test("create_entitiy_markers", function(assert) { +QUnit.test("create_entity_markers", function (assert) { var datamodel = this.datamodel; var entities = $(this.test_map_entity).toArray(); // w/o popup - var markers = caosdb_map.create_entitiy_markers(entities, datamodel); + var markers = caosdb_map.create_entity_markers(entities, datamodel); assert.equal(markers.length, 1, "has one marker"); assert.ok(markers[0] instanceof L.Marker, "is marker"); var latlng = markers[0]._latlng; @@ -185,30 +194,32 @@ QUnit.test("create_entitiy_markers", function(assert) { assert.notOk(markers[0].getPopup(), "no popup"); // with popup - var markers = caosdb_map.create_entitiy_markers(entities, datamodel, ()=>"popup"); + var markers = caosdb_map.create_entity_markers(entities, datamodel, () => "popup"); assert.ok(markers[0].getPopup(), "has popup"); }); -QUnit.test("_add_current_page_entities", function(assert) { +QUnit.test("_add_current_page_entities", async function (assert) { var datamodel = this.datamodel; var layerGroup = L.layerGroup(); var container = $('<div class="caosdb-f-main-entities"/>').append(this.test_map_entity); $("body").append(container); assert.equal(layerGroup.getLayers().length, 0, "no layer"); - var cpe = caosdb_map._get_current_page_entities(datamodel, undefined, undefined, undefined, undefined); + var cpe = await caosdb_map._generic_get_current_page_entities(datamodel, undefined, undefined, undefined, undefined, undefined); assert.equal(cpe.length, 1, "has one entity"); container.remove(); }); -QUnit.test("make_layer_chooser_html", function(assert) { - var test_conf = { "id": "test_id", +QUnit.test("make_layer_chooser_html", function (assert) { + var test_conf = { + "id": "test_id", "name": "test name", "description": "test description", - "icon": { "html": "<span>ICON</span>", + "icon": { + "html": "<span>ICON</span>", }, }; @@ -217,19 +228,139 @@ QUnit.test("make_layer_chooser_html", function(assert) { assert.equal($(layer_chooser).attr("title"), "test description", "description set as title"); }); -QUnit.test("init_entity_layer", function(assert) { - var done = assert.async(); - var test_conf = { "id": "test_id", +QUnit.test("_init_single_entity_layer", function (assert) { + var test_conf = { + "id": "test_id", "name": "test name", "description": "test description", - "get_entities": async function() {done(); return []}, - "icon": { "html": "<span>ICON</span>", + "icon": { + "html": "<span>ICON</span>", }, } - var entityLayer= caosdb_map.init_entity_layer(test_conf); + var entityLayer = caosdb_map._init_single_entity_layer(test_conf); assert.equal(entityLayer.id, test_conf.id, "id"); assert.equal(entityLayer.active, true, "is active"); assert.ok(entityLayer.chooser_html instanceof HTMLElement, "chooser_html is HTMLElement"); - assert.equal(entityLayer.layer_group.getLayers().length, 0 , "empty layergroup"); + assert.equal(entityLayer.layer_group.getLayers().length, 0, "empty layergroup"); +}); + +QUnit.test("_get_with_POV ", function (assert) { + assert.equal(caosdb_map._get_with_POV( + []), "", "no POV"); + assert.equal(caosdb_map._get_with_POV( + ["lol"]), " WITH lol ", "single POV"); + assert.equal(caosdb_map._get_with_POV( + ["lol", "hi"]), " WITH lol WITH hi ", "with two POV"); +}); + + +QUnit.test("_get_select_with_path ", function (assert) { + assert.throws(() => caosdb_map._get_select_with_path(), /Supply the datamodel./, "missing datamodel"); + assert.throws(() => caosdb_map._get_select_with_path(this.datamodel, []), /Supply at least a RecordType./, "missing value"); + assert.equal(caosdb_map._get_select_with_path( + this.datamodel, + ["RealRT"]), "SELECT parent,latitude,longitude FROM ENTITY RealRT WITH latitude AND longitude ", "RT only"); + assert.equal(caosdb_map._get_select_with_path( + this.datamodel, + ["RealRT", "prop1"]), "SELECT parent,prop1.latitude,prop1.longitude FROM ENTITY RealRT WITH prop1 WITH latitude AND longitude ", "RT with one prop"); + assert.equal(caosdb_map._get_select_with_path( + this.datamodel, + ["RealRT", "prop1", "prop2"]), "SELECT parent,prop1.prop2.latitude,prop1.prop2.longitude FROM ENTITY RealRT WITH prop1 WITH prop2 WITH latitude AND longitude ", "RT with two props"); +}); + + +QUnit.test("_get_leaf_prop", async function (assert) { + const test_response = str2xml(` +<Response srid="851d063d-121b-4b67-98d4-6e5ad4d31e72" timestamp="1606214042274" baseuri="https://localhost:10443" count="8"> + <Query string="select Campaign.responsible.firstname from icecore" results="8"> + <ParseTree>(cq select (prop_sel (prop_subsel (selector_txt C a m p a i g n) . (prop_subsel (selector_txt r e s p o n s i b l e) . (prop_subsel (selector_txt f i r s t n a m e ))))) from (entity icecore) <EOF>)</ParseTree> + <Role/> + <Entity>icecore</Entity> + <Selection> + <Selector name="Campaign.responsible.firstname"/> + </Selection> + </Query> + <Record id="6525" name="Test_IceCore_1"> + <Permissions/> + <Property datatype="Campaign" id="6430" name="Campaign"> + <Record id="6516" name="Test-2020_Camp1"> + <Permissions/> + <Property datatype="REFERENCE" id="168" name="responsible"> + <Record id="6515" name="Test_Scientist"> + <Permissions/> + <Property datatype="DOUBLE" id="151" name="latitude" importance="FIX"> + 1.34 + <Permissions/> + </Property> + <Property datatype="DOUBLE" id="151" name="longitude" importance="FIX"> + 2 + <Permissions/> + </Property> + </Record> + <Permissions/> + </Property> + </Record> + <Permissions/> + </Property> + </Record> + <Record id="6526" name="Test_IceCore_2"> + <Permissions/> + <Property datatype="Campaign" id="6430" name="Campaign"> + <Record id="6516" name="Test-2020_Camp1"> + <Permissions/> + <Property datatype="REFERENCE" id="168" name="responsible"> + <Record id="6515" name="Test_Scientist"> + <Permissions/> + <Property datatype="DOUBLE" id="151" name="latitude" importance="FIX"> + 3 + <Permissions/> + </Property> + <Property datatype="DOUBLE" id="151" name="longitude" importance="FIX"> + 4.8345 + <Permissions/> + </Property> + </Record> + <Permissions/> + </Property> + </Record> + <Permissions/> + </Property> + </Record> +</Response>`); + var leaves = caosdb_map._get_leaf_prop(test_response, 2, this.datamodel) + + assert.equal(Object.keys(leaves).length, 2, "number of records"); + assert.notEqual(typeof leaves["6525"], "undefined", "has entity id"); + assert.deepEqual(leaves["6525"], ["1.34", "2"]); + assert.deepEqual(leaves["6526"], ["3", "4.8345"], "long/lat in second rec"); + + assert.equal( + caosdb_map._get_toplvl_rec_with_id(test_response, "6526")["id"], + "6526", + "number of records"); + + caosdb_map._set_subprops_at_top( + test_response, 2, this.datamodel, { + "6526": [1.234, 5.67] + }) + assert.equal($(test_response).find(`[name='longitude']`).length, + 4, + "number lng props"); + assert.equal($(test_response).find(`[name='latitude']`).length, + 4, + "number lat props"); + // after transforming, the long/lat props should be accessible + var html_ents = await transformation.transformEntities(test_response); + assert.equal( + getProperty(html_ents[0], "longitude"), + "2", + "longitude of first rec"); + +}); + +QUnit.test("_get_id_POV", function (assert) { + assert.equal(caosdb_map._get_id_POV([]), "WITH ", "no POV"); + assert.equal(caosdb_map._get_id_POV([5]), "WITH id=5", "one id"); + assert.equal(caosdb_map._get_id_POV([5, 6]), "WITH id=5 or id=6", "two ids"); }); diff --git a/test/core/js/modules/ext_references.js.js b/test/core/js/modules/ext_references.js.js index 38d61a9ac18d1bccaddd5cc7cdad1d1c9d3b5498..43cc1ddd742d6702232b740bbfd96411f41b08f5 100644 --- a/test/core/js/modules/ext_references.js.js +++ b/test/core/js/modules/ext_references.js.js @@ -73,6 +73,7 @@ QUnit.module("ext_references.js", { }, beforeEach: function(assert) { + resolve_references.getParents = getParents; resolve_references.test_resolver = this.test_resolver(true); resolve_references.is_in_viewport_horizontally = () => true; resolve_references.is_in_viewport_vertically = () => true; @@ -87,6 +88,21 @@ QUnit.test("init", function(assert){ assert.ok(resolve_references.init); }); +QUnit.test("is_child", function(assert){ + resolve_references.getParents = (entity) => [{"name": "par1"}, {"name": "par2"}]; + const ent = transformation.transformEntities(str2xml( + `<Response> + <Record id="50023"> + <Parent name="TestReferenced"/> + </Record> + </Response>` + )); + + assert.ok(resolve_references.is_child(ent, "par1")); + assert.ok(resolve_references.is_child(ent, "par2")); + assert.ok(!resolve_references.is_child(ent, "par3")); +}); + QUnit.test("get_person_str", function(assert){ assert.ok(resolve_references.get_person_str); }); diff --git a/test/core/js/modules/ext_revisions.js.js b/test/core/js/modules/ext_revisions.js.js deleted file mode 100644 index e90fd7c97851e5f054690cb0d376be5e14d826e4..0000000000000000000000000000000000000000 --- a/test/core/js/modules/ext_revisions.js.js +++ /dev/null @@ -1,121 +0,0 @@ -/* - * ** header v3.0 - * This file is a part of the CaosDB Project. - * - * Copyright (C) 2020 IndiScale GmbH <info@indiscale.com> - * Copyright (C) 2020 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/>. - * - * ** end header - */ - -'use strict'; - -var ext_revisions_test_suite = function ($, ext_revisions, QUnit, edit_mode) { - - var datamodel = ext_revisions._datamodel; - - QUnit.module("ext_revisions.js", { - before: function (assert) { - // setup before module - this.original_update_entity = edit_mode.update_entity; - this.original_insert = transaction.insertEntitiesXml; - this.original_retrieve = transaction.retrieveEntityById; - this.original_query = query; - ext_revisions._logger.setLevel("trace"); - }, - beforeEach: function (assert) { - // setup before each test - datamodel.obsolete = "UNITTESTObsolete"; - datamodel.revisionOf = "UNITTESTRevisionOf"; - }, - afterEach: function (assert) { - // teardown after each test - query = this.original_query; - edit_mode.update_entity = this.original_update_entity; - transaction.insertEntitiesXml = this.original_insert; - transaction.retrieveEntityById = this.original_retrieve; - }, - after: function (assert) { - // teardown after module - } - }); - - QUnit.test("_make_revision_of_property", async function(assert) { - var p = await ext_revisions._make_revision_of_property("1234"); - var editfield = $(p).find(".caosdb-property-edit-value"); - var value = $(editfield).find("select").first()[0].selectedOptions[0].value; - assert.ok($(p).hasClass("caosdb-f-entity-property"), "is property"); - assert.equal(value, "1234", "has value 1234"); - assert.equal(getPropertyName(p), datamodel.revisionOf, "has revisionOf name"); - assert.equal(getPropertyDatatype(p), datamodel.obsolete, "has Obsolete datatype"); - }); - - /** - * This is a rather complete test, not a unit test. - */ - QUnit.test("update calls update_entity through proxy", async function (assert) { - var done = assert.async(3); - var done_query = assert.async(2); - var ent_element = $('<div data-entity-id="15"><div class="caosdb-properties"/></div>')[0]; - - // mock server responses to several requests... - var retrieve_fun = async function(id) { - assert.equal(id, "15", "retrieve id 15"); - done(); - return $(`<Record id="15"><Parent name="ORIG_PARENT"/></Record>`)[0]; - } - var insert_fun = async function(xml) { - var rec = xml.firstElementChild.firstElementChild; - assert.equal(rec.id, "-1", "insert with tmp id"); - assert.equal($(rec).find("Parent").attr("name"), datamodel.obsolete, "Obsolete Parent"); - xml.firstElementChild.firstElementChild.id = "2345"; - console.log(xml2str(xml)); - done(); - return xml; - }; - var update_fun = async function(ent_element) { - var prop = edit_mode.getProperties(ent_element)[0]; - assert.equal(prop.name, datamodel.revisionOf, "has revisionOf"); - assert.equal(prop.value, "2345", "revisionOf 2345"); - done(); - }; - var query_fun = async function(query) { - assert.ok(query.startsWith("FIND") && ( query.endsWith(datamodel.obsolete) || query.endsWith(datamodel.revisionOf)), query); - done_query(); // called twice - return [$(`<div data-entity-name="${datamodel.revisionOf}" data-caosdb-id="3456"/>`)[0]]; - } - - // injecting the server mock-up responses. - transaction.retrieveEntityById = retrieve_fun; - transaction.insertEntitiesXml = insert_fun; - edit_mode.update_entity = update_fun; - query = query_fun; - - - // actual tests - assert.equal(update_fun, edit_mode.update_entity, "before init, the edit_mode.update_entity function has not been overridden."); - - // call init which checks the datamodel and overwrites the - // edit_mode.update_entity function. - await ext_revisions.init(); - assert.notEqual(update_fun, edit_mode.update_entity, "after init, the edit_mode.update_entity hab been overriden with a proxy calling the update_fun and the original function."); - - // call edit_mode.update_entity which calls the insert_fun and the - // update_fun - await edit_mode.update_entity(ent_element); - }); - -}($, ext_revisions, QUnit, edit_mode); diff --git a/test/core/js/modules/ext_xls_download.js.js b/test/core/js/modules/ext_xls_download.js.js index 4f9dc6c59b156f1f2265acb4b315887536667194..74cdb244dcf0f5c2238c8d2503a3194580c35d90 100644 --- a/test/core/js/modules/ext_xls_download.js.js +++ b/test/core/js/modules/ext_xls_download.js.js @@ -32,7 +32,7 @@ * @return {HTMLElement} DIV.caosdb-query-response */ transformation.transformSelectTable = async function _tST (xml) { - var root_template = '<xsl:template match="/"><div class="root"><xsl:apply-templates select="Response/Query" mode="query-results"/></div></xsl:template>'; + var root_template = '<xsl:template xmlns="http://www.w3.org/1999/xhtml" match="/"><div class="root"><xsl:apply-templates select="Response/Query" mode="query-results"/></div></xsl:template>'; var queryXsl = await transformation.retrieveXsltScript("query.xsl"); var entityXsl = await transformation.retrieveEntityXsl(root_template); insertParam(entityXsl, "uppercase", 'abcdefghijklmnopqrstuvwxyz'); @@ -64,43 +64,39 @@ QUnit.module("ext_xls_download", { }); -{ - const sleep = function sleep(ms) { - return new Promise(resolve => setTimeout(resolve, ms)); - } - QUnit.test("call downloadXLS", async function(assert) { - var done = assert.async(2); +QUnit.test("call downloadXLS", async function(assert) { + var done = assert.async(2); - // mock server response (successful) - connection.runScript = async function(exec, param){ - assert.equal(exec, "xls_from_csv.py", "call xls_from_csv.py"); - done(); - return str2xml('<response><script code="0" /><stdout>bla</stdout></response>'); - } + // mock server response (successful) + connection.runScript = async function(exec, param){ + assert.equal(exec, "xls_from_csv.py", "call xls_from_csv.py"); + done(); + return str2xml('<response><script code="0" /><stdout>bla</stdout></response>'); + } - caosdb_table_export._go_to_script_results = function(xls_link, filename) { - xls_link.setAttribute( - "href", - location.protocol + "//" +location.host + "/Shared/" + filename); - assert.equal(filename, "bla", "filename correct"); - done(); - } + caosdb_table_export.go_to_script_results = function(filename) { + assert.equal(filename, "bla", "filename correct"); + done(); + } - var tsv_data = $('<a id="caosdb-f-query-select-data-tsv" />'); - $(document.body).append(tsv_data); + var tsv_data = $('<a id="caosdb-f-query-select-data-tsv" />'); + var modal = $('<div id="downloadModal"><div>'); + $(document.body).append([tsv_data, modal]); - var xsl_link = $("<a/>"); - downloadXLS(xsl_link[0]); + var xsl_link = $("<a/>"); + downloadXLS(xsl_link[0]); - await sleep(500); + await sleep(500); - assert.ok(xsl_link.attr("href").endsWith("Shared/bla"), xsl_link.attr("href") + " ends with Shared/bla"); + tsv_data.remove(); + modal.remove(); +}); - tsv_data.remove(); - }); -} +QUnit.test("_clean_cell", function(assert) { + assert.equal(caosdb_table_export._clean_cell("\n\t\n\t"), " ", "No valid content"); +}); QUnit.test("_get_property_value", function(assert) { var f = caosdb_table_export._get_property_value; @@ -114,11 +110,17 @@ QUnit.test("_get_property_value", function(assert) { QUnit.test("_get_tsv_string", function(assert) { const table = this.test_case_1; const entities = $(table).find("tbody tr").toArray(); - assert.equal(entities.length, 2, "two example entities"); + assert.equal(entities.length, 3, "three example entities"); - var f = caosdb_table_export._get_tsv_string + var f = caosdb_table_export._create_tsv_string var tsv_string = f(entities, ["Bag", "Number"], true); - assert.equal(tsv_string, "data:text/csv;charset=utf-8,ID%09Bag%09Number%0A242%096366, 6406, 6407, 6408, 6409, 6410, 6411, 6412, 6413%090284%0A2112%09%091101", "tsv generated"); + var prefix = "data:text/csv;charset=utf-8," + assert.equal(tsv_string, + "ID\tVersion\tBag\tNumber\n242\tabc123\t6366, 6406, 6407, 6408, 6409, 6410, 6411, 6412, 6413\t02 8 4aaa a\n2112\tabc124\t\t1101\n2112\tabc125\t\t1102", "tsv generated"); + tsv_string = caosdb_table_export._encode_tsv_string(tsv_string); + assert.equal(tsv_string.slice(0,prefix.length), prefix); + assert.equal(decodeURIComponent(tsv_string.slice(prefix.length, tsv_string.length)), + "ID\tVersion\tBag\tNumber\n242\tabc123\t6366, 6406, 6407, 6408, 6409, 6410, 6411, 6412, 6413\t02 8 4aaa a\n2112\tabc124\t\t1101\n2112\tabc125\t\t1102", "tsv generated"); }); QUnit.test("_get_property_value", function (assert) { diff --git a/test/core/js/modules/form_elements.js.js b/test/core/js/modules/form_elements.js.js index 7f5930cd4253f408edf785045db10290697cd6c3..f93fde0db2d69156312b2a34c3748112619b68eb 100644 --- a/test/core/js/modules/form_elements.js.js +++ b/test/core/js/modules/form_elements.js.js @@ -23,7 +23,7 @@ 'use strict'; QUnit.module("form_elements.js", { - before: function(assert) { + before: function (assert) { markdown.init(); var entities = [ $('<div><div class="caosdb-id" data-entity-name="name12">id12</div></div>')[0], @@ -32,27 +32,27 @@ QUnit.module("form_elements.js", { $('<div><div class="caosdb-id" data-entity-name="name15">id15</div></div>')[0], ]; - form_elements._query = async function(query) { + form_elements._query = async function (query) { return entities; }; - this.get_example_1 = async function() { + this.get_example_1 = async function () { return $(await $.ajax("html/form_elements_example_1.html"))[0]; }; }, - after: function(assert) { + after: function (assert) { form_elements._init_functions(); } }); -QUnit.test("availability", function(assert) { +QUnit.test("availability", function (assert) { assert.equal(form_elements.version, "0.1", "test version"); assert.ok(form_elements.init, "init available"); assert.ok(form_elements.version, "version available"); }); -QUnit.test("make_reference_option", function(assert) { +QUnit.test("make_reference_option", function (assert) { assert.equal(typeof form_elements.make_reference_option, "function", "function available"); - assert.throws(()=>form_elements.make_reference_option(), /is expected to be a string/, "noargs throws"); + assert.throws(() => form_elements.make_reference_option(), /is expected to be a string/, "noargs throws"); var option = form_elements.make_reference_option("id15"); assert.equal($(option).val(), "id15", "value"); assert.equal($(option).text(), "id15", "text"); @@ -62,14 +62,17 @@ QUnit.test("make_reference_option", function(assert) { }); -QUnit.test("make_reference_select", async function(assert) { +QUnit.test("make_reference_select", async function (assert) { assert.equal(typeof form_elements.make_reference_select, "function", "function available"); - //assert.throws(()=> unasync(form_elements.make_reference_select), /param `entities` is expected to be an array/, "undefined entities throws"); - //assert.throws(()=> unasync(form_elements.make_reference_select, "test"), /param `entities` is expected to be an array/, "string entities throws"); - var select = await form_elements.make_reference_select([ - {dataset: {entityId : "id17"}}, - {dataset: {entityId : "id18"}}, - ]); + var select = await form_elements.make_reference_select([{ + dataset: { + entityId: "id17" + } + }, { + dataset: { + entityId: "id18" + } + }, ]); assert.ok($(select).hasClass("selectpicker"), "selectpicker class from bootstrap-select"); assert.notOk($(select).val(), "unselected"); $(select).val(["id18"]); @@ -86,7 +89,7 @@ QUnit.test("make_reference_select", async function(assert) { }); -QUnit.test("make_script_form", async function(assert) { +QUnit.test("make_script_form", async function (assert) { assert.equal(typeof form_elements.make_script_form, "function", "function available"); // TODO @@ -96,12 +99,31 @@ QUnit.test("make_script_form", async function(assert) { var done = assert.async(3); var config = { - groups: [ - { name: "group1", fields: ["date"], enabled: false }, - ], - fields: [ - {type: "date", name: "baldate"}, - ], + groups: [{ + name: "group1", + fields: ["date"], + enabled: false + }, ], + fields: [{ + type: "date", + name: "baldate" + }, { + type: "select", + name: "Sex", + label: "Sex", + value: "female", + required: true, + options: [{ + value: "female", + label: "female" + }, { + value: "diverse", + label: "diverse" + }, { + value: "male", + label: "male" + }] + }], }; var script_form = await form_elements.make_script_form(config, "test_script"); @@ -115,24 +137,29 @@ QUnit.test("make_script_form", async function(assert) { assert.equal(cancel_button.length, 1, "has cancel button"); var field = $(script_form).find(".caosdb-f-field"); - assert.equal(field.length, 1, "has one field"); + assert.equal(field.length, 2, "has two field"); assert.equal(field.find("input[type='date']").length, 1, "has date input"); + assert.equal(field.find("select").length, 1, "has select input"); - script_form.addEventListener("caosdb.form.cancel", function(e) { + script_form.addEventListener("caosdb.form.cancel", function (e) { done(); }, true); cancel_button.click(); - script_form.addEventListener("caosdb.form.error", function(e) { + script_form.addEventListener("caosdb.form.error", function (e) { assert.equal($(script_form).find(".caosdb-f-form-elements-message-error").length, 2, "error message there (call and stderr)"); done(); script_form.remove(); }); - form_elements._run_script = async function(script, params) { + form_elements._run_script = async function (script, params) { done(); - return {code: "1", stderr: "Autsch!", call: "none"}; + return { + code: "1", + stderr: "Autsch!", + call: "none" + }; }; assert.equal($(script_form).find(".caosdb-f-form-error-message").length, 0, "no error message"); @@ -144,7 +171,7 @@ QUnit.test("make_script_form", async function(assert) { }); -QUnit.test("make_date_input", function(assert) { +QUnit.test("make_date_input", function (assert) { assert.equal(typeof form_elements.make_date_input, "function", "function available"); var config = { @@ -164,7 +191,7 @@ QUnit.test("make_date_input", function(assert) { }); -QUnit.test("make_range_input", async function(assert) { +QUnit.test("make_range_input", async function (assert) { assert.equal(typeof form_elements.make_range_input, "function", "function available"); var config = { @@ -192,20 +219,27 @@ QUnit.test("make_range_input", async function(assert) { }); -QUnit.test("make_form_field", async function(assert) { +QUnit.test("make_form_field", async function (assert) { assert.equal(typeof form_elements.make_form_field, "function", "function available"); - var cached = false; - for ( var t of ["date", "range", "reference_drop_down", "subform", "checkbox", "double", "integer"] ) { + var cached = false; + for (var t of ["date", "range", "reference_drop_down", "subform", "checkbox", "double", "integer"]) { cached = !cached; var config = { - help: {title: "HELP", content: "help me, help me, help me-e-e!"}, + help: { + title: "HELP", + content: "help me, help me, help me-e-e!" + }, type: t, cached: cached, name: "a name", label: "a label", - from: {name: "from_bla"}, - to: {name: "to_bla"}, + from: { + name: "from_bla" + }, + to: { + name: "to_bla" + }, query: "FIND something", make_desc: getEntityName, fields: [], @@ -220,8 +254,8 @@ QUnit.test("make_form_field", async function(assert) { }); -QUnit.test("make_subform", async function(assert) { - assert.equal(typeof form_elements.make_reference_drop_down, "function", "function available"); +QUnit.test("make_subform", async function (assert) { + assert.equal(typeof form_elements.make_subform, "function", "function available"); const config = { type: "subform", @@ -239,7 +273,7 @@ QUnit.test("make_subform", async function(assert) { }); -QUnit.test("make_reference_drop_down", async function(assert) { +QUnit.test("make_reference_drop_down", async function (assert) { assert.equal(typeof form_elements.make_reference_drop_down, "function", "function available"); var config = { @@ -257,7 +291,7 @@ QUnit.test("make_reference_drop_down", async function(assert) { assert.equal(label.text(), "IceCore", "label has text"); }); -QUnit.test("make_checkbox_input", function(assert) { +QUnit.test("make_checkbox_input", function (assert) { assert.equal(typeof form_elements.make_checkbox_input, "function", "function available"); @@ -274,7 +308,7 @@ QUnit.test("make_checkbox_input", function(assert) { assert.equal(input.attr("name"), "approved", "input has name"); assert.ok(input.is(":checked"), "input is checked"); - var obj = form_elements.form_to_object(field); + var obj = form_elements.form_to_object(field)[0]; assert.equal(obj["approved"], "yes!!!", "checked value"); @@ -286,36 +320,72 @@ QUnit.test("make_checkbox_input", function(assert) { assert.equal(input.attr("name"), "approved", "input has name"); assert.notOk(input.is(":checked"), "input is not checked"); - obj = form_elements.form_to_object(field); + obj = form_elements.form_to_object(field)[0]; assert.equal(typeof obj["approved"], "undefined", "no checked value"); }); -QUnit.test("form_to_object", async function(assert) { +QUnit.test("form_to_object", async function (assert) { assert.equal(typeof form_elements.form_to_object, "function", "function available"); var config = { - fields: [ - { type: "date", name: "the-date" }, - { type: "reference_drop_down", name: "icecore", query: "FIND Record IceCore"}, - { type: "range", name: "the-range", from: {name: "fromblla"}, to: {name: "toblla"}}, - { type: "subform", name: "subform1", fields: [ - { type: "date", name: "the-other-date", }, - { type: "checkbox", name: "rectangular", }, - ],}, - ], + fields: [{ + type: "date", + name: "the-date" + }, { + type: "reference_drop_down", + name: "icecore", + query: "FIND Record IceCore" + }, { + type: "range", + name: "the-range", + from: { + name: "fromblla" + }, + to: { + name: "toblla" + } + }, { + type: "subform", + name: "subform1", + fields: [{ + type: "date", + name: "the-other-date", + }, { + type: "checkbox", + name: "rectangular", + }, ], + }, { + type: "select", + required: true, + cached: true, + name: "sex", + label: "Sex", + value: "d", + options: [{ + "value": "f", + "label": "female" + }, { + "value": "d", + "label": "diverse" + }, { + "value": "m", + "label": "male" + }, ], + }, ], }; var form = await form_elements.make_script_form(config, "bla"); - var json = form_elements.form_to_object(form); + var json = form_elements.form_to_object(form)[0]; assert.equal(typeof json["cancel"], "undefined", "cancel button not serialized"); assert.equal(json["the-date"], "", "date"); assert.equal(json["icecore"], null, "reference_drop_down"); assert.equal(json["fromblla"], "", "range from"); assert.equal(json["toblla"], "", "range to"); + assert.equal(json["sex"], "d", "select"); assert.equal(typeof json["the-other-date"], "undefined", "subform element not on root level"); var subform = json["subform1"]; @@ -327,10 +397,13 @@ QUnit.test("form_to_object", async function(assert) { }); -QUnit.test("make_double_input", function(assert) { +QUnit.test("make_double_input", function (assert) { assert.equal(typeof form_elements.make_double_input, "function", "function available"); - var config = {type: "double", name: "d"}; + var config = { + type: "double", + name: "d" + }; var input = $(form_elements.make_double_input(config)).find("input"); assert.ok(input.is("[type='number'][step='any']"), "double input"); @@ -340,10 +413,13 @@ QUnit.test("make_double_input", function(assert) { }); -QUnit.test("make_integer_input", function(assert) { +QUnit.test("make_integer_input", function (assert) { assert.equal(typeof form_elements.make_integer_input, "function", "function available"); - var config = {type: "integer", name: "i"}; + var config = { + type: "integer", + name: "i" + }; var input = $(form_elements.make_integer_input(config)).find("input"); assert.ok(input.is("[type='number'][step='1']"), "integer input"); @@ -352,21 +428,26 @@ QUnit.test("make_integer_input", function(assert) { assert.equal(input.val("abc").val(), "", "abc not valid"); }); -QUnit.test("make_form", function(assert) { +QUnit.test("make_form", function (assert) { assert.equal(typeof form_elements.make_form, "function", "function available"); - var form1 = form_elements.make_form({fields: []}); + var form1 = form_elements.make_form({ + fields: [] + }); assert.equal(form1.tagName, "DIV", "wrapper is div"); assert.ok($(form1).hasClass("caosdb-f-form-wrapper"), "div has caosdb-f-form-wrapper class"); assert.equal($(form1).find(".h3").length, 0, "no header"); - var form2 = form_elements.make_form({fields: [], header: "bla"}); + var form2 = form_elements.make_form({ + fields: [], + header: "bla" + }); assert.equal(form2.tagName, "DIV", "wrapper is div"); assert.equal($(form2).find(".h3").length, 1, "one header"); assert.equal($(form2).find(".h3").text(), "bla", "header text set"); }); -QUnit.test("enable/disable_group", function(assert) { +QUnit.test("enable/disable_group", function (assert) { assert.equal(typeof form_elements.disable_group, "function", "function available"); assert.equal(typeof form_elements.enable_group, "function", "function available"); @@ -413,11 +494,11 @@ QUnit.test("enable/disable_group", function(assert) { }); -QUnit.test("parse_script_result", function(assert) { +QUnit.test("parse_script_result", function (assert) { assert.equal(typeof form_elements.parse_script_result, "function", "function available"); var result = str2xml( -`<?xml version="1.0" encoding="UTF-8"?> + `<?xml version="1.0" encoding="UTF-8"?> <?xml-stylesheet type="text/xsl" href="https://localhost:10443/webinterface/webcaosdb.xsl" ?> <Response username="admin" realm="PAM" srid="256c14970dac2b2b5649973d52e4c06a" timestamp="1570785591824" baseuri="https://localhost:10443"> <UserInfo username="admin" realm="PAM"> @@ -443,15 +524,15 @@ QUnit.test("parse_script_result", function(assert) { }); -QUnit.test("disable_name", function(assert) { +QUnit.test("disable_name", function (assert) { assert.equal(typeof form_elements.disable_name, "function", "function available"); }); -QUnit.test("enable_name", function(assert) { +QUnit.test("enable_name", function (assert) { assert.equal(typeof form_elements.enable_name, "function", "function available"); }); -QUnit.test("add_field_to_group", function(assert) { +QUnit.test("add_field_to_group", function (assert) { assert.equal(typeof form_elements.add_field_to_group, "function", "function available"); var field = $(form_elements._make_field_wrapper("field1"))[0]; @@ -462,7 +543,7 @@ QUnit.test("add_field_to_group", function(assert) { }); -QUnit.test("cache_form", async function(assert) { +QUnit.test("cache_form", async function (assert) { var form = await this.get_example_1(); assert.equal($(form).find("form").length, 1, "example form available"); @@ -476,7 +557,7 @@ QUnit.test("cache_form", async function(assert) { }); -QUnit.test("load_cached", async function(assert) { +QUnit.test("load_cached", async function (assert) { var done = assert.async(); var form = await this.get_example_1(); assert.equal($(form).find("form").length, 1, "example form available"); @@ -498,7 +579,7 @@ QUnit.test("load_cached", async function(assert) { }); -QUnit.test("field_ready", function(assert) { +QUnit.test("field_ready", function (assert) { var done = assert.async(3); var field1 = $('<div id="f1"><div class="caosdb-f-field-not-ready"/></div>')[0]; var field2 = $('<div id="f2" class="caosdb-f-field-not-ready"/>')[0]; @@ -541,3 +622,167 @@ QUnit.test("field_ready", function(assert) { }); }); +QUnit.test("make_alert - cancel", async function (assert) { + var cancel_callback = assert.async() + var _alert = form_elements.make_alert({ + message: "message", + proceed_callback: () => { + assert.ok(false, "this should not be called"); + }, + cancel_callback: cancel_callback, + }); + $("body").append(_alert); + + assert.equal($("body").find(".caosdb-f-form-elements-alert").length, 1, "alert is there"); + $(_alert).find("button.caosdb-f-btn-alert-cancel")[0].click(); + await sleep(500); + + assert.equal($("body").find(".caosdb-f-form-elements-alert").length, 0, "has been removed"); + + +}); + +QUnit.test("make_alert - proceed", async function (assert) { + var proceed_callback = assert.async(); + var _alert = form_elements.make_alert({ + message: "message", + proceed_callback: proceed_callback, + }); + assert.equal($(_alert).find("[type='checkbox']").length, 0, "no remember checkbox"); + + $("body").append(_alert); + assert.equal($("body").find(".caosdb-f-form-elements-alert").length, 1, "alert is there"); + $(_alert).find("button.caosdb-f-btn-alert-proceed")[0].click(); + await sleep(500); + + assert.equal($("body").find(".caosdb-f-form-elements-alert").length, 0, "has been removed"); + +}); + +QUnit.test("make_alert - remember", async function (assert) { + form_elements._set_alert_decision("unittests", ""); + + var proceed_callback = assert.async(3); + var _alert = form_elements.make_alert({ + message: "message", + proceed_callback: proceed_callback, + remember_my_decision_id: "unittests", + }); + assert.equal($(_alert).find("[type='checkbox']").length, 1, "has remember checkbox"); + + // append for the first time + $("body").append(_alert); + assert.equal($("body").find(".caosdb-f-form-elements-alert").length, 1, "alert is there"); + + // click proceed without "don't ask me again". + $(_alert).find("button.caosdb-f-btn-alert-proceed")[0].click(); + await sleep(500); + + form_elements._set_alert_decision("unittests", ""); + + assert.equal($("body").find(".caosdb-f-form-elements-alert").length, 0, "has been removed"); + + // append 2nd time + _alert = form_elements.make_alert({ + message: "message", + proceed_callback: proceed_callback, + remember_my_decision_id: "unittests", + }); + assert.equal($(_alert).find("[type='checkbox']").length, 1, "has remember checkbox"); + $("body").append(_alert); + assert.equal($("body").find(".caosdb-f-form-elements-alert").length, 1, "alert is there"); + $(_alert).find("[type='checkbox']").prop("checked", true); + + $(_alert).find("button.caosdb-f-btn-alert-proceed")[0].click(); + await sleep(500); + form_elements._set_alert_decision("unittests", "proceed"); + + // try 3rd time + _alert = form_elements.make_alert({ + message: "message", + proceed_callback: proceed_callback, + remember_my_decision_id: "unittests", + }); + assert.equal(typeof _alert, "undefined", "alert was not created, proceed callback was called third time"); +}); + +QUnit.test("make_select_input", function (assert) { + const config = { + name: "sex", + label: "Sex", + multiple: true, + options: [{ + "value": "f", + "label": "female" + }, { + "value": "d", + "label": "diverse" + }, { + "value": "m", + "label": "male" + }, ], + } + const select = $(form_elements.make_select_input(config)); + assert.equal(select.find("select").length, 1, "select input there"); + assert.equal(select.find("select").attr("name"), "sex", "has select with correct name"); + assert.equal(select.find("select option").length, 3, "three options there"); +}); + +QUnit.test("select_input caching", async function (assert) { + const config = { + "name": "test-form", + "fields": [{ + type: "select", + required: true, + cached: true, + name: "sex", + label: "Sex", + options: [{ + "value": "f", + "label": "female" + }, { + "value": "d", + "label": "diverse" + }, { + "value": "m", + "label": "male" + }, ], + }, ], + } + const form_wrapper = $(form_elements.make_form(config)); + await sleep(200); + const form = form_wrapper.find("form"); + assert.equal(form.find("select").length, 1); + + + // write to cache + const cache = {}; + const field = $(form_elements.get_fields(form[0], "sex")); + field.find("select").val("f"); + assert.equal(form_elements.get_cache_value(field[0]), "f", "initial value set"); + assert.equal(form_elements.get_cache_key(form[0], field[0]), "form_elements.cache.test-form.sex", "cache key correct"); + + form_elements.cache_form(cache, form[0]); + assert.equal(cache[form_elements.get_cache_key(form[0], field[0])], "f"); + + // read from cache and set the value + field.find("select").val("m"); + assert.equal(form_elements.get_cache_value(field[0]), "m", "different value set"); + + form_elements.load_cached(cache, form[0]); + await sleep(200); + assert.equal(form_elements.get_cache_value(field[0]), "f", "value back to value from cache"); +}); + +QUnit.test("make_file_input", function (assert) { + const config = { + name: "some_file", + multiple: true, + accept: ".tsv, .csv", + } + const file_input = $(form_elements.make_file_input(config)); + assert.equal(file_input.find(":input").length, 1, "file input there"); + assert.equal(file_input.find(":input").attr("name"), "some_file", "has file input with correct name"); + assert.ok(file_input.find(":input").prop("multiple"), "is multiple"); + assert.equal(file_input.find(":input").attr("accept"), ".tsv, .csv", "accept there"); +}); diff --git a/test/core/js/modules/navbar.xsl.js b/test/core/js/modules/navbar.xsl.js index 0b43cae70cbd4694290885560c69594a5d4a555e..78113ce2bd7c9ada61205ae5fc3b2e098ee92e8f 100644 --- a/test/core/js/modules/navbar.xsl.js +++ b/test/core/js/modules/navbar.xsl.js @@ -57,25 +57,3 @@ QUnit.test("create navbar", function(assert){ assert.ok(html, "html ok"); assert.equal(html.firstChild.tagName, "NAV", "is nav element"); }); - -/* MISC FUNCTIONS */ -function getXSLScriptClone(source){ - return str2xml(xml2str(source)) -} - -function injectTemplate(orig_xsl, template){ - var xsl = getXSLScriptClone(orig_xsl); - var entry_t = xsl.createElement("xsl:template"); - xsl.firstElementChild.appendChild(entry_t); - entry_t.outerHTML = template; - return xsl; -} - -function insertParam(xsl, name, value=null){ - var param = xsl.createElement("xsl:param"); - param.setAttribute("name", name); - if (value != null) { - param.setAttribute("select", "'"+value+"'"); - } - xsl.firstElementChild.append(param); -} diff --git a/test/core/js/modules/query.xsl.js b/test/core/js/modules/query.xsl.js index 6c371281e7d4b01db0050a00111064d1d3c488b0..371b51598918e2fd6bb5d94ca13a94337ccd322e 100644 --- a/test/core/js/modules/query.xsl.js +++ b/test/core/js/modules/query.xsl.js @@ -52,7 +52,7 @@ QUnit.test("basic properties of select-table feature", function(assert) { var xml = str2xml("<Selection/>"); var html = xslt(xml, xsl); assert.equal(html.firstElementChild.tagName, "DIV", "first child is div."); - assert.equal(html.firstElementChild.className, "panel panel-default caosdb-select-table", "first child has class caosdb-select-table."); + assert.equal(html.firstElementChild.className, "card caosdb-select-table", "first child has class caosdb-select-table."); }); /* Test table of empty result sets of queries */ @@ -122,7 +122,7 @@ QUnit.test("Query tag is transformed via xslt", function(assert) { let html = applyTemplates(str2xml('<Query/>'), this.queryXSL, 'query-results'); //var html = xslt(xml, xsl); assert.equal(html.firstElementChild.tagName, "DIV", "first child is div."); - assert.equal(html.firstElementChild.className, "panel panel-default caosdb-query-response", "first child has class caosdb-query-reponse."); + assert.equal(html.firstElementChild.className, "card caosdb-query-response mb-2", "first child has class caosdb-query-reponse."); }); QUnit.test("xsl defines id 'caosdb-query-form'", function(assert) { @@ -141,14 +141,12 @@ QUnit.test("xsl script's 'caosdb-query-form' has a hidden input, with name=P and }); -QUnit.test("Query is available, contained by a div.", function(assert) { - var cont = getQueryFormContainer(this.queryXSL); - assert.equal(cont.tagName, "DIV", "contained by a div"); - assert.equal(cont.className, "container caosdb-query-panel", "container has classname 'container caosdb-query-panel'"); - assert.equal(cont.firstElementChild.tagName, "FORM", "form element is available"); - assert.equal(cont.firstElementChild.className, "panel", "FORM has class 'panel'"); - assert.equal(cont.firstElementChild.id, "caosdb-query-form", "FORM has id 'caosdb-query-form'"); +QUnit.test("Query is available", function(assert) { + var cont = getQueryForm(this.queryXSL); + assert.equal(cont.tagName, "FORM", "contained by a div"); + assert.equal(cont.className, "card caosdb-query-form", "container has classname 'card caosdb-query-form'"); }); + QUnit.test("Query is send with a paging of 0L10 by default", function(assert) { var form_e = getQueryForm(this.queryXSL); @@ -172,21 +170,21 @@ QUnit.test("template entity-link", function(assert){ }); QUnit.test("template select-table-row ", function(assert){ - let row = callTemplate(this.queryXSL, "select-table-row", {"entity-id": "sdfg"}, str2xml('<Response>')); - assert.equal(row.firstElementChild.tagName, "TR", "tagName = TR"); - assert.equal(row.firstElementChild.firstElementChild.tagName, "TD", "tagName = TD"); - assert.equal(row.firstElementChild.firstElementChild.firstElementChild.tagName, "A", "tagName = A"); + let row = callTemplate(this.queryXSL, "select-table-row", {"entity-id": "sdfg", "version-id": "dsfg", "ishead": "true"}, (x) => `<table><tbody>${x}</tbody></table>`); + var next = row.firstElementChild; + assert.equal(next.tagName, "TABLE", "tagName = TABLE"); + next = next.firstElementChild; + assert.equal(next.tagName, "TBODY", "tagName = TBODY"); + next = next.firstElementChild; + assert.equal(next.tagName, "TR", "tagName = TR"); + next = next.firstElementChild; + assert.equal(next.tagName, "TD", "tagName = TD"); + next = next.firstElementChild; + assert.equal(next.tagName, "A", "tagName = A"); }); /* MISC FUNCTIONS */ function getQueryForm(queryXSL) { - var cont = getQueryFormContainer(queryXSL); - return cont.getElementsByTagName("form")[0]; -} - -function getQueryFormContainer(queryXSL) { - var xsl = injectTemplate(queryXSL, '<xsl:template match="/"><xsl:call-template name="caosdb-query-panel"/></xsl:template>"') - var xml = str2xml("<root/>"); - var html = xslt(xml, xsl); + var html = callTemplate(queryXSL, "caosdb-query-panel", {}); return html.firstElementChild; } diff --git a/test/core/js/modules/query_shortcuts.js.js b/test/core/js/modules/query_shortcuts.js.js index f088b729001daac8a42ed276507e2370f3e08679..3798c5fe81ea6e860cd176797a9a6959a9a64895 100644 --- a/test/core/js/modules/query_shortcuts.js.js +++ b/test/core/js/modules/query_shortcuts.js.js @@ -141,7 +141,9 @@ QUnit.test("init_delete_shortcut_form", function(assert) { assert.equal(panel.find(".caosdb-f-form-wrapper").length, 1, "panel has form after"); // test cancel button - panel[0].addEventListener("caosdb.form.cancel", function(e) { + var done = assert.async(); + panel[0].addEventListener("caosdb.form.cancel", async function(e) { + await sleep(200); assert.equal(panel.find("form").length, 0, "form is gone"); done(); }, true); @@ -187,23 +189,17 @@ QUnit.test("make_delete_form", function(assert) { } var form = query_shortcuts.make_delete_form(panel[0], delete_callback); + $('body').append(form); - // wait for form - form.addEventListener("caosdb.form.ready", function(e) { - - assert.equal($(form).hasClass("caosdb-f-form-wrapper"), true, "is form"); - assert.equal($(form).find("input[type='checkbox']").length, 3, "three checkboxes (for three user-defined shortcuts)"); - - // check two - $(form).find(":checkbox[name='id28']").click(); - $(form).find(":checkbox[name='id29']").click(); - $(form).find("[type='submit']").click(); - - $(form).find("button.caosdb-f-form-elements-cancel-button").click(); + assert.equal($(form).hasClass("caosdb-f-form-wrapper"), true, "is form"); + assert.equal($(form).find("input[type='checkbox']").length, 3, "three checkboxes (for three user-defined shortcuts)"); - }, true); + // check two + $(form).find(":checkbox[name='id28']").click(); + $(form).find(":checkbox[name='id29']").click(); + $(form).find("[type='submit']").click(); - $('body').append(form); + $(form).find("button.caosdb-f-form-elements-cancel-button").click(); }); @@ -228,31 +224,24 @@ QUnit.test("transform_entities", async function(assert) { }); QUnit.test("make_create_form", function(assert) { - - var done = assert.async(); var panel = $('<div/>'); var form = query_shortcuts.make_create_form(panel[0], () => {}); assert.ok($(form).hasClass("caosdb-f-form-wrapper"), "form created"); $('body').append(form); - form.addEventListener(form_elements.form_ready_event.type, function(e) { - $(form).find(":input[name='templateDescription']").val("NEW DESC"); - $(form).find(":input[name='Query']").val("NEW QUERY"); + $(form).find(":input[name='templateDescription']").val("NEW DESC"); + $(form).find(":input[name='Query']").val("NEW QUERY"); - var entity = getEntityXML(form); - assert.equal(xml2str(entity), "<Record><Parent name=\"UserTemplate\"/><Property name=\"templateDescription\">NEW DESC</Property><Property name=\"Query\">NEW QUERY</Property></Record>", "entity extracted"); + var entity = getEntityXML(form); + assert.equal(xml2str(entity), "<Record><Parent name=\"UserTemplate\"/><Property name=\"templateDescription\">NEW DESC</Property><Property name=\"Query\">NEW QUERY</Property></Record>", "entity extracted"); - form_elements.dismiss_form(form); - done(); + form_elements.dismiss_form(form); - }, true); }); QUnit.test("make_update_form", function(assert) { - - var done = assert.async(); var panel = $('<div/>'); var header = $('<span class="h3">Shortcuts</span>'); var userTemplate1 = query_shortcuts.generate_user_shortcut("the_description", "FIND nothing", "id28"); @@ -271,16 +260,12 @@ QUnit.test("make_update_form", function(assert) { $('body').append(form); - form.addEventListener(form_elements.form_ready_event.type, function(e) { - $(form).find(":input[name='templateDescription']").val("NEW DESC"); - $(form).find(":input[name='Query']").val("NEW QUERY"); - - var entity = getEntityXML(form); - assert.equal(xml2str(entity), "<Record id=\"id28\"><Parent name=\"UserTemplate\"/><Property name=\"templateDescription\">NEW DESC</Property><Property name=\"Query\">NEW QUERY</Property></Record>", "entity extracted"); + $(form).find(":input[name='templateDescription']").val("NEW DESC"); + $(form).find(":input[name='Query']").val("NEW QUERY"); - form_elements.dismiss_form(form); - done(); + var entity = getEntityXML(form); + assert.equal(xml2str(entity), "<Record id=\"id28\"><Parent name=\"UserTemplate\"/><Property name=\"templateDescription\">NEW DESC</Property><Property name=\"Query\">NEW QUERY</Property></Record>", "entity extracted"); - }, true); + form_elements.dismiss_form(form); }); diff --git a/test/core/js/modules/webcaosdb.js.js b/test/core/js/modules/webcaosdb.js.js index 239758161b8419a8a5e15799c67c909977f1fbf0..52bf4ada52d2ce59b59d8615c89ca5796343622b 100644 --- a/test/core/js/modules/webcaosdb.js.js +++ b/test/core/js/modules/webcaosdb.js.js @@ -27,19 +27,19 @@ /* SETUP general module */ QUnit.module("webcaosdb.js", { - before: function(assert) { + before: function (assert) { markdown.init(); connection._init(); }, - after: function(assert) { + after: function (assert) { connection._init(); }, }); /* TESTS */ -QUnit.test("xslt", function(assert) { +QUnit.test("xslt", 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>'; + 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>content</newroot></xsl:template></xsl:stylesheet>'; xml = str2xml(xml_str); xsl = str2xml(xsl_str); broken_xsl = str2xml('<blabla/>'); @@ -69,7 +69,29 @@ QUnit.test("xslt", function(assert) { }, "nu ll xsl throws exc."); }); -QUnit.test("getEntityId", function(assert) { +QUnit.test("markdown.textTohtml", function (assert) { + const str = `\# header\n\#\# another header\nparagraph`; + assert.equal(markdown.textToHtml(str), "<h1 id=\"header\">header</h1>\n<h2 id=\"anotherheader\">another header</h2>\n<p>paragraph</p>"); + +}); + +QUnit.test("injectTemplate", async function (assert) { + const xml_str = '<root/>'; + const xsl_str = '<xsl:stylesheet xmlns:xsl="http://www.w3.org/1999/XSL/Transform" version="1.0"><xsl:output method="html" /></xsl:stylesheet>'; + const xml = str2xml(xml_str); + const xsl = str2xml(xsl_str); + + const result_xsl = injectTemplate(xsl, '<xsl:template xmlns="http://www.w3.org/1999/xhtml" match="root"><newroot>content</newroot></xsl:template>'); + + assert.equal(xml2str(result_xsl), "<xsl:stylesheet xmlns:xsl=\"http://www.w3.org/1999/XSL/Transform\" version=\"1.0\"><xsl:output method=\"html\"/><xsl:template xmlns=\"http://www.w3.org/1999/xhtml\" match=\"root\"><newroot>content</newroot></xsl:template></xsl:stylesheet>"); + var result_xml = xslt(xml, result_xsl); + assert.equal(xml2str(result_xml), "<newroot xmlns=\"http://www.w3.org/1999/xhtml\">content</newroot>"); + + result_xml = await asyncXslt(xml, result_xsl); + 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]; @@ -91,7 +113,7 @@ QUnit.test("getEntityId", function(assert) { assert.equal("1234", getEntityId(okElem), "ID found"); }); -QUnit.test("asyncXslt", function(assert) { +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>'; xml = str2xml(xml_str); @@ -110,22 +132,22 @@ QUnit.test("asyncXslt", function(assert) { // broken xsl throws exception asyncXslt(xml, broken_xsl).catch((error) => { - assert.equal(/^\[Exception.*\]$/.test(error.toString()), true, "broken xsl thros exc."); + assert.equal(/^XSL Transformation.*$/.test(error.message), true, "broken xsl thros exc."); done(); }); }); -QUnit.test("xml2str", function(assert) { +QUnit.test("xml2str", function (assert) { xml = str2xml('<root/>'); assert.equal(xml2str(xml), '<root/>'); }); -QUnit.test("str2xml", function(assert) { +QUnit.test("str2xml", function (assert) { xml = str2xml('<root/>'); assert.ok(xml); // make sure this is a document: - assert.equal(xml.contentType, "text/xml", "has contentType=text/xml"); + assert.ok(xml.contentType.endsWith("/xml"), "has contentType=*/xml"); assert.ok(xml.documentElement, "has documentElement"); assert.equal(xml.documentElement.outerHTML, '<root/>', "has outerHTML"); @@ -133,11 +155,11 @@ QUnit.test("str2xml", function(assert) { // valid. }); -QUnit.test("postXml", function(assert) { +QUnit.test("postXml", function (assert) { assert.ok(postXml, "function exists."); }); -QUnit.test("createErrorNotification", function(assert) { +QUnit.test("createErrorNotification", function (assert) { assert.ok(createErrorNotification, "function available"); let err = createErrorNotification("test"); assert.ok($(err).hasClass(preview.classNameErrorNotification), "has class caosdb-preview-error-notification"); @@ -145,37 +167,37 @@ QUnit.test("createErrorNotification", function(assert) { /* MODULE connection */ QUnit.module("webcaosdb.js - connection", { - before: function(assert) { + before: function (assert) { assert.ok(connection, "connection module is defined"); } }); -QUnit.test("get", function(assert) { +QUnit.test("get", function (assert) { assert.expect(4); assert.ok(connection.get, "function available"); let done = assert.async(2); - connection.get("webinterface/${BUILD_NUMBER}/xsl/entity.xsl").then(function(resolve) { + connection.get("webinterface/${BUILD_NUMBER}/xsl/entity.xsl").then(function (resolve) { assert.equal(resolve.toString(), "[object XMLDocument]", "entity.xsl returned."); done(); }); - connection.get("webinterface/non-existent").then((resolve) => resolve, function(error) { - assert.equal(error.toString().split(" - ",1)[0], "Error: GET webinterface/non-existent returned with HTTP status 404", "404 error thrown"); + connection.get("webinterface/non-existent").then((resolve) => resolve, function (error) { + assert.equal(error.toString().split(" - ", 1)[0], "Error: GET webinterface/non-existent returned with HTTP status 404", "404 error thrown"); done(); }); }); /* MODULE transformation */ QUnit.module("webcaosdb.js - transformation", { - before: function(assert) { + before: function (assert) { assert.ok(transformation, "transformation module is defined"); } }); -QUnit.test("removePermissions", function(assert) { +QUnit.test("removePermissions", function (assert) { assert.ok(transformation.removePermissions, "function available"); }); -QUnit.test("retrieveXsltScript", function(assert) { +QUnit.test("retrieveXsltScript", function (assert) { assert.ok(transformation.retrieveXsltScript, "function available"); let done = assert.async(2); transformation.retrieveXsltScript("entity.xsl").then(xsl => { @@ -188,7 +210,7 @@ QUnit.test("retrieveXsltScript", function(assert) { }); }); -QUnit.test("retrieveEntityXsl", function(assert) { +QUnit.test("retrieveEntityXsl", function (assert) { assert.ok(transformation.retrieveEntityXsl, "function available"); let done = assert.async(); transformation.retrieveEntityXsl().then(xsl => { @@ -199,7 +221,7 @@ QUnit.test("retrieveEntityXsl", function(assert) { }); }); -QUnit.test("transformEntities", function(assert) { +QUnit.test("transformEntities", function (assert) { assert.ok(transformation.transformEntities, "function available"); let done = assert.async(); let xml = str2xml('<Response><Record id="142"><Warning description="asdf"/></Record></Response>'); @@ -212,23 +234,23 @@ QUnit.test("transformEntities", function(assert) { }); }); -QUnit.test("mergeXsltScripts", function(assert) { +QUnit.test("mergeXsltScripts", function (assert) { assert.ok(transformation.mergeXsltScripts, 'function available.'); - let xslMainStr = '<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform"/>'; + let xslMainStr = '<xsl:stylesheet xmlns:xsl="http://www.w3.org/1999/XSL/Transform" version="1.0"/>'; assert.equal(xml2str(transformation.mergeXsltScripts(str2xml(xslMainStr), [])), xslMainStr, 'no includes returns same as xslMain.'); - let xslIncludeStr = '<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform"><xsl:template name="bla"/></xsl:stylesheet>' + let xslIncludeStr = '<xsl:stylesheet xmlns:xsl="http://www.w3.org/1999/XSL/Transform" version="1.0"><xsl:template name="bla"/></xsl:stylesheet>' let xslInclude = str2xml(xslIncludeStr); assert.ok($(transformation.mergeXsltScripts(str2xml(xslMainStr), [xslInclude])).find("[name='bla']")[0], 'template bla is there.'); }); /* MODULE transaction */ QUnit.module("webcaosdb.js - transaction", { - before: function(assert) { + before: function (assert) { assert.ok(transaction, "transaction module is defined"); }, }); -QUnit.test("generateEntitiesUri", function(assert) { +QUnit.test("generateEntitiesUri", function (assert) { assert.ok(transaction.generateEntitiesUri, "function available"); assert.throws(() => { @@ -240,10 +262,10 @@ QUnit.test("generateEntitiesUri", function(assert) { assert.equal(transaction.generateEntitiesUri(["asdf", "qwer"]), "Entity/asdf&qwer", "works"); }); -QUnit.test("updateEntitiesXml", function(assert) { +QUnit.test("updateEntitiesXml", function (assert) { assert.ok(transaction.updateEntitiesXml, "function available"); var done = assert.async(); - connection.put = function(uri, data) { + connection.put = function (uri, data) { assert.equal(uri, 'Entity/', "updateEntitiesXml calls connection.put"); assert.equal(xml2str(data), '<Update/>'); done(); @@ -251,20 +273,20 @@ QUnit.test("updateEntitiesXml", function(assert) { transaction.updateEntitiesXml(str2xml('<Update/>')); }); -QUnit.test("retrieveEntitiesById", function(assert) { +QUnit.test("retrieveEntitiesById", function (assert) { assert.ok(transaction.retrieveEntitiesById, "function available"); var done = assert.async(); - connection.get = function(uri) { + connection.get = function (uri) { assert.equal(uri, 'Entity/1234&2345', "retrieveEntitiesById calls connection.get"); done(); }; transaction.retrieveEntitiesById(["1234", "2345"]); }); -QUnit.test("retrieveEntityById", function(assert) { +QUnit.test("retrieveEntityById", function (assert) { assert.ok(transaction.retrieveEntityById, "function available"); var done = assert.async(); - connection.get = function(uri) { + connection.get = function (uri) { assert.equal(uri, 'Entity/1234', "retrieveEntityById calls connection.get"); return new Promise((ok, fail) => { setTimeout(() => ok(str2xml('<Response><Entity id="1234" name="new"/></Response>')), 200); @@ -277,29 +299,29 @@ QUnit.test("retrieveEntityById", function(assert) { /* MODULE transaction.update */ QUnit.module("webcaosdb.js - transaction.update", { - before: function(assert) { + before: function (assert) { assert.ok(transaction.update, "transaction.update module is defined"); } }); -QUnit.test("createWaitRetrieveNotification", function(assert) { +QUnit.test("createWaitRetrieveNotification", function (assert) { assert.ok(transaction.update.createWaitRetrieveNotification(), 'function available and returns non-null'); }); -QUnit.test("createWaitUpdateNotification", function(assert) { +QUnit.test("createWaitUpdateNotification", function (assert) { assert.ok(transaction.update.createWaitUpdateNotification(), 'function available and returns non-null'); }); -QUnit.test("createUpdateForm", function(assert) { +QUnit.test("createUpdateForm", function (assert) { let done = assert.async(); let cuf = transaction.update.createUpdateForm; assert.ok(cuf, "function available"); - assert.throws(() => cuf(null, function(xml) {}), "null entityXmlStr throws"); + assert.throws(() => cuf(null, function (xml) {}), "null entityXmlStr throws"); assert.throws(() => cuf("", null), "null putCallback throws"); assert.throws(() => cuf("", ""), "non-function putCallback throws"); - assert.throws(() => cuf("", function() {}), "putCallback function without parameters throws"); + assert.throws(() => cuf("", function () {}), "putCallback function without parameters throws"); - let form = cuf("<root/>", function(xml) { + let form = cuf("<root/>", function (xml) { assert.equal(xml, '<newroot/>', "modified xml is submitted."); done(); }); @@ -321,10 +343,10 @@ QUnit.test("createUpdateForm", function(assert) { //$(document.body).append(form); }); -QUnit.test("createUpdateEntityHeading", function(assert) { +QUnit.test("createUpdateEntityHeading", function (assert) { let cueh = transaction.update.createUpdateEntityHeading; assert.ok(cueh, "function available"); - let eh = $('<div class="panel-heading"><div class="1strow"/><div class="2ndrow"/></div>')[0]; + let eh = $('<div class="card-header"><div class="1strow"></div><div class="2ndrow"></div></div>')[0]; assert.equal($(eh).children('.1strow').length, 1, "eh has 1st row"); assert.equal($(eh).children('.2ndrow').length, 1, "eh has 2nd row"); let uh = cueh(eh); @@ -332,20 +354,20 @@ QUnit.test("createUpdateEntityHeading", function(assert) { assert.equal($(uh).children('.1strow').length, 1, "uh has 1st row"); }); -QUnit.test("createUpdateEntityPanel", function(assert) { +QUnit.test("createUpdateEntityPanel", function (assert) { let cued = transaction.update.createUpdateEntityPanel; assert.ok(cued, "function available"); let div = $(cued($('<div id="headingid">heading</div>'))); - assert.ok(div.hasClass("panel"), "panel has class panel."); + assert.ok(div.hasClass("card"), "card has class card."); assert.equal(div.children(":first-child")[0].id, "headingid", "heading is first child element"); }); -QUnit.test("updateSingleEntity - success", function(assert) { +QUnit.test("updateSingleEntity - success", function (assert) { let done = assert.async(); let use = transaction.update.updateSingleEntity; assert.ok(use, "function available"); - let entityPanel = $('<div class="panel panel-default caosdb-entity-panel"><div class="panel-heading caosdb-entity-panel-heading"><div>heading<div class="caosdb-id">1234</div></div><div>other stuff in the heading</div></div>body</div>')[0]; - connection.get = function(uri) { + let entityPanel = $('<div class="card caosdb-entity-panel"><div class="card-header caosdb-entity-panel-heading"><div>heading<div class="caosdb-id">1234</div></div><div>other stuff in the heading</div></div>body</div>')[0]; + connection.get = function (uri) { assert.equal(uri, 'Entity/1234', 'get was called with correct uri'); return new Promise((ok, fail) => { setTimeout(() => { @@ -362,7 +384,7 @@ QUnit.test("updateSingleEntity - success", function(assert) { setTimeout(() => { connection._init(); - connection.put = function(uri, xml) { + connection.put = function (uri, xml) { assert.equal(xml2str(xml), '<Update><Entity id="1234" name="new"/></Update>', "put was called with correct xml"); return new Promise((ok, fail) => { setTimeout(() => ok(str2xml('<Response><Record id="1234" name="new"></Record></Response>')), 200); @@ -380,18 +402,18 @@ QUnit.test("updateSingleEntity - success", function(assert) { - app.onEnterFinal = function(e) { + app.onEnterFinal = function (e) { done(); $(entityPanel).remove(); }; }); -QUnit.test("updateSingleEntity - with errors in the server's response", function(assert) { +QUnit.test("updateSingleEntity - with errors in the server's response", function (assert) { let done = assert.async(); let use = transaction.update.updateSingleEntity; - let entityPanel = $('<div class="panel panel-default caosdb-entity-panel"><div class="panel-heading caosdb-entity-panel-heading"><div>heading<div class="caosdb-id">1234</div></div><div>other stuff in the heading</div></div>body</div>')[0]; - connection.get = function(uri) { + let entityPanel = $('<div class="card caosdb-entity-panel"><div class="card-header caosdb-entity-panel-heading"><div>heading<div class="caosdb-id">1234</div></div><div>other stuff in the heading</div></div>body</div>')[0]; + connection.get = function (uri) { return new Promise((ok, fail) => { setTimeout(() => { ok(str2xml('<Response><Entity id="1234" name="old"/></Response>')); @@ -405,7 +427,7 @@ QUnit.test("updateSingleEntity - with errors in the server's response", function // submit form -> server response contains error tag. setTimeout(() => { connection._init(); - connection.put = function(uri, xml) { + connection.put = function (uri, xml) { return new Promise((ok, fail) => { setTimeout(() => ok(str2xml('<Response><Record id="1234" name="new"><Error description="This is an error."/></Record></Response>')), 200); }); @@ -413,12 +435,12 @@ QUnit.test("updateSingleEntity - with errors in the server's response", function $(document.body).find('form.' + transaction.classNameUpdateForm).submit(); }, 400); - app.onLeaveWaitPutEntity = function(e) { + app.onLeaveWaitPutEntity = function (e) { assert.equal(e.transition, "openForm", "app returns to form again due to errors."); - assert.equal($(app.updatePanel).find('.panel-heading .' + preview.classNameErrorNotification).length, 0, "has no error notification before the response is processed."); + assert.equal($(app.updatePanel).find('.card-header .' + preview.classNameErrorNotification).length, 0, "has no error notification before the response is processed."); setTimeout(() => { - assert.equal($(app.updatePanel).find('.panel-heading .' + preview.classNameErrorNotification).length, 1, "has an error notification after the response is processed."); + assert.equal($(app.updatePanel).find('.card-header .' + preview.classNameErrorNotification).length, 1, "has an error notification after the response is processed."); $(app.updatePanel).remove(); entityPanel.remove(); done(); @@ -427,17 +449,17 @@ QUnit.test("updateSingleEntity - with errors in the server's response", function }); -QUnit.test("createErrorInUpdatedEntityNotification", function(assert) { +QUnit.test("createErrorInUpdatedEntityNotification", function (assert) { assert.ok(transaction.update.createErrorInUpdatedEntityNotification, "function available."); }); -QUnit.test("addErrorNotification", function(assert) { +QUnit.test("addErrorNotification", function (assert) { assert.ok(transaction.update.addErrorNotification, "function available"); }); /* MODULE preview */ QUnit.module("webcaosdb.js - preview", { - before: function(assert) { + before: function (assert) { // load xmlTestCase var done = assert.async(2); var qunit_obj = this; @@ -445,13 +467,13 @@ QUnit.module("webcaosdb.js - preview", { cache: true, dataType: 'xml', url: "xml/test_case_preview_entities.xml", - }).done(function(data, textStatus, jdXHR) { + }).done(function (data, textStatus, jdXHR) { qunit_obj.testXml = data; - }).always(function() { + }).always(function () { done(); }); // load entity.xsl - preview.getEntityXsl("../").then(function(data) { + preview.getEntityXsl("../").then(function (data) { insertParam(data, "entitypath", "/entitypath/"); insertParam(data, "filesystempath", "/filesystempath/"); qunit_obj.entityXSL = injectTemplate(data, '<xsl:template match="/"><root><xsl:apply-templates select="/Response/*" mode="top-level-data"/></root></xsl:template>'); @@ -460,22 +482,31 @@ QUnit.module("webcaosdb.js - preview", { assert.ok(preview, "preview module is defined"); }, - afterEach: function(assert) { + afterEach: function (assert) { connection._init(); } }); -QUnit.test("halfArray", function(assert){ +QUnit.test("halfArray", function (assert) { assert.ok(preview.halfArray, "function available"); assert.throws(() => { preview.halfArray([1]); }, "length < 2 throws.") - assert.deepEqual(preview.halfArray([1,2]), [[1],[2]]); - assert.deepEqual(preview.halfArray([1,2,3]), [[1],[2,3]]); - assert.deepEqual(preview.halfArray([1,2,3,4]), [[1,2],[3,4]]); -}); - -QUnit.test("xslt file preview", function(assert) { + assert.deepEqual(preview.halfArray([1, 2]), [ + [1], + [2] + ]); + assert.deepEqual(preview.halfArray([1, 2, 3]), [ + [1], + [2, 3] + ]); + assert.deepEqual(preview.halfArray([1, 2, 3, 4]), [ + [1, 2], + [3, 4] + ]); +}); + +QUnit.test("xslt file preview", function (assert) { let done = assert.async(); let entityXSL = this.entityXSL; $.ajax({ @@ -493,7 +524,7 @@ QUnit.test("xslt file preview", function(assert) { }); }); -QUnit.test("createShowPreviewButton", function(assert) { +QUnit.test("createShowPreviewButton", function (assert) { assert.ok(preview.createShowPreviewButton, "function available"); let showPreviewButton = preview.createShowPreviewButton(); assert.ok(showPreviewButton, "not null"); @@ -501,7 +532,7 @@ QUnit.test("createShowPreviewButton", function(assert) { assert.ok($(showPreviewButton).hasClass("caosdb-show-preview-button"), "has class caosdb-show-preview-button"); }); -QUnit.test("createHidePreviewButton", function(assert) { +QUnit.test("createHidePreviewButton", function (assert) { assert.ok(preview.createHidePreviewButton, "function available"); let hidePreviewButton = preview.createHidePreviewButton(); assert.ok(hidePreviewButton, "not null"); @@ -509,7 +540,7 @@ QUnit.test("createHidePreviewButton", function(assert) { assert.ok($(hidePreviewButton).hasClass("caosdb-hide-preview-button"), "has class 'caosdb-hide-preview-button'"); }); -QUnit.test("addHidePreviewButton", function(assert) { +QUnit.test("addHidePreviewButton", function (assert) { assert.ok(preview.addHidePreviewButton, "function available"); let okTestElem = $('<div><div class="caosdb-f-property-value"></div></div>')[0] let notOkTestElem = $('<div></div>')[0] @@ -534,7 +565,7 @@ QUnit.test("addHidePreviewButton", function(assert) { assert.equal(okTestElem.firstChild.childNodes.length, 1, "after: test div has new child"); }); -QUnit.test("addShowPreviewButton", function(assert) { +QUnit.test("addShowPreviewButton", function (assert) { assert.ok(preview.addShowPreviewButton, "function available"); let okTestElem = $('<div><div class="caosdb-f-property-value"></div></div>')[0] let notOkTestElem = $('<div></div>')[0] @@ -551,15 +582,12 @@ QUnit.test("addShowPreviewButton", function(assert) { assert.throws(() => { preview.addShowPreviewButton(null, $('<div/>')[0]); }, "null ref_property_elem parameter throws."); - assert.throws(() => { - preview.addShowPreviewButton(notOkTestElem, $('<div/>')[0]); - }, "ref_property_elem w/o caosdb-value-list throws."); assert.equal(okTestElem.firstChild.childNodes.length, 0, "before: test div has no children"); assert.equal(okTestElem, preview.addShowPreviewButton(okTestElem, preview.createShowPreviewButton()), "returns the first parameter"); assert.equal(okTestElem.firstChild.childNodes.length, 1, "after: test div has new child"); }); -QUnit.test("addWaitingNotification", function(assert) { +QUnit.test("addWaitingNotification", function (assert) { assert.ok(preview.addWaitingNotification, "function available"); let testWaiting = $('<div>Waiting!</div>')[0]; let okTestElem = $('<div><div class="caosdb-preview-notification-area"></div></div>')[0]; @@ -586,7 +614,7 @@ QUnit.test("addWaitingNotification", function(assert) { assert.equal(okTestElem.firstChild.childNodes.length, 1, "after: test div has new child"); }); -QUnit.test("addErrorNotification", function(assert) { +QUnit.test("addErrorNotification", function (assert) { assert.ok(preview.addErrorNotification, "function available"); let testError = $('<div>Error!</div>')[0]; let okTestElem = $('<div><div class="caosdb-preview-notification-area"></div></div>')[0]; @@ -613,14 +641,14 @@ QUnit.test("addErrorNotification", function(assert) { assert.equal(okTestElem.firstChild.childNodes.length, 1, "after: test div has new child"); }); -QUnit.test("createWaitingNotification", function(assert) { +QUnit.test("createWaitingNotification", function (assert) { assert.ok(preview.createWaitingNotification, "function available"); let welem = preview.createWaitingNotification(); assert.ok(welem, "not null"); assert.ok($(welem).hasClass("caosdb-preview-waiting-notification"), "element has class 'caosdb-preview-waiting-notification'"); }), - QUnit.test("createNotificationArea", function(assert) { + QUnit.test("createNotificationArea", function (assert) { assert.ok(preview.createNotificationArea, "function available"); let narea = preview.createNotificationArea(); assert.ok(narea, "not null"); @@ -628,7 +656,7 @@ QUnit.test("createWaitingNotification", function(assert) { assert.ok($(narea).hasClass("caosdb-preview-notification-area"), "has class caosdb-preview-notification-area"); }); -QUnit.test("getHidePreviewButton", function(assert) { +QUnit.test("getHidePreviewButton", function (assert) { assert.ok(preview.getHidePreviewButton, "function available"); let okElem = $('<div><button class="caosdb-hide-preview-button">click</button></div>')[0]; let notOkElem = $('<div></div>')[0]; @@ -644,7 +672,7 @@ QUnit.test("getHidePreviewButton", function(assert) { assert.equal(preview.getHidePreviewButton(okElem), okElem.firstChild, "button found"); }); -QUnit.test("getRefLinksContainer", function(assert) { +QUnit.test("getRefLinksContainer", function (assert) { assert.ok(preview.getRefLinksContainer, "function available"); // TODO: references or lists of references should have a special class, not just // caosdb-value-list. -> entity.xsl @@ -664,7 +692,7 @@ QUnit.test("getRefLinksContainer", function(assert) { assert.equal(preview.getRefLinksContainer(okSingle), okSingle.firstChild.firstChild, "single link found"); }); -QUnit.test("getPreviewCarousel", function(assert) { +QUnit.test("getPreviewCarousel", function (assert) { assert.ok(preview.getPreviewCarousel, "function available"); let okElem = $('<div><div class="' + preview.classNamePreview + '"></div></div>')[0]; let notOkElem = $('<div></div>')[0]; @@ -680,7 +708,7 @@ QUnit.test("getPreviewCarousel", function(assert) { assert.equal(preview.getPreviewCarousel(okElem), okElem.firstChild, "carousel found"); }); -QUnit.test("getShowPreviewButton", function(assert) { +QUnit.test("getShowPreviewButton", function (assert) { assert.ok(preview.getShowPreviewButton, "function available"); let okElem = $('<div><button class="caosdb-show-preview-button">click</button></div>')[0]; let notOkElem = $('<div></div>')[0]; @@ -696,7 +724,7 @@ QUnit.test("getShowPreviewButton", function(assert) { assert.equal(preview.getShowPreviewButton(okElem), okElem.firstChild, "button found"); }); -QUnit.test("removeAllErrorNotifications", function(assert) { +QUnit.test("removeAllErrorNotifications", function (assert) { assert.ok(preview.removeAllErrorNotifications, "function available"); let okElem = $('<div><div class="caosdb-preview-error-notification">Error1</div>' + '<div class="caosdb-preview-error-notification">Error2</div>' + @@ -718,7 +746,7 @@ QUnit.test("removeAllErrorNotifications", function(assert) { assert.equal(emptyElem, preview.removeAllErrorNotifications(emptyElem), "empty elem works"); }); -QUnit.test("removeAllWaitingNotifications", function(assert) { +QUnit.test("removeAllWaitingNotifications", function (assert) { assert.ok(removeAllWaitingNotifications, "function available"); let okElem = $('<div><div class="caosdb-preview-waiting-notification">Waiting1</div>' + '<div class="caosdb-preview-waiting-notification">Waiting2</div>' + @@ -740,28 +768,28 @@ QUnit.test("removeAllWaitingNotifications", function(assert) { assert.equal(emptyElem, removeAllWaitingNotifications(emptyElem), "empty elem works"); }); -QUnit.test("getActiveSlideItemIndex", function(assert) { +QUnit.test("getActiveSlideItemIndex", function (assert) { assert.ok(preview.getActiveSlideItemIndex, "function available"); let okElem0 = $('<div><div class="carousel-inner">' + - '<div class="item active"></div>' // index 0 + '<div class="carousel-item active"></div>' // index 0 + - '<div class="item"></div>' + - '<div class="item"></div>' + + '<div class="carousel-item"></div>' + + '<div class="carousel-item"></div>' + '</div></div>')[0]; let okElem1 = $('<div><div class="carousel-inner">' + - '<div class="item"></div>' + - '<div class="item active"></div>' // index 1 + '<div class="carousel-item"></div>' + + '<div class="carousel-item active"></div>' // index 1 + - '<div class="item"></div>' + + '<div class="carousel-item"></div>' + '</div></div>')[0]; let okElem2 = $('<div><div class="carousel-inner">' + - '<div class="item"></div>' + - '<div class="item"></div>' + - '<div class="item active"></div>' // index 2 + '<div class="carousel-item"></div>' + + '<div class="carousel-item"></div>' + + '<div class="carousel-item active"></div>' // index 2 + '</div></div>')[0]; let noInner = $('<div></div>')[0]; - let noActive = $('<div><div class="carousel-inner"><div class="item"></div></div></div>')[0]; + let noActive = $('<div><div class="carousel-inner"><div class="carousel-item"></div></div></div>')[0]; assert.throws(() => { preview.getActiveSlideItemIndex() @@ -781,7 +809,7 @@ QUnit.test("getActiveSlideItemIndex", function(assert) { assert.equal(2, preview.getActiveSlideItemIndex(okElem2)); }); -QUnit.test("getEntityByIdVersion", function(assert) { +QUnit.test("getEntityByIdVersion", function (assert) { assert.ok(preview.getEntityByIdVersion, "function available"); let e1 = $('<div><div class="caosdb-id">1</div></div>')[0]; let e2 = $('<div><div class="caosdb-id">2</div></div>')[0]; @@ -806,7 +834,7 @@ QUnit.test("getEntityByIdVersion", function(assert) { assert.equal(null, preview.getEntityByIdVersion(es, "3"), "find 3 -> null"); }); -QUnit.test("createEmptyInner", function(assert) { +QUnit.test("createEmptyInner", function (assert) { assert.ok(preview.createEmptyInner, "function available"); assert.throws(() => { @@ -824,14 +852,14 @@ QUnit.test("createEmptyInner", function(assert) { let inner = preview.createEmptyInner(3); assert.equal(inner.children.length, 3, "three items"); - assert.equal(inner.children[0].className, "item active", "first item is active"); - assert.equal(inner.children[1].className, "item", "second item is not active"); - assert.equal(inner.children[2].className, "item", "third item is not active"); + assert.equal(inner.children[0].className, "carousel-item active", "first item is active"); + assert.equal(inner.children[1].className, "carousel-item", "second item is not active"); + assert.equal(inner.children[2].className, "carousel-item", "third item is not active"); }); -QUnit.test("createCarouselNav", function(assert) { +QUnit.test("createCarouselNav", function (assert) { assert.ok(preview.createCarouselNav, "function available"); - let refLinks = $('<div style="display: none;" class="caosdb-value-list"><a><span class="caosdb-id">1234</span></a><a><span class="caosdb-id">2345</span></a><a><span class="caosdb-id">3456</span></a><a><span class="caosdb-id">4567</span></a></div>')[0]; + let refLinks = $('<div style="display: none;" class="caosdb-value-list"><a class="caosdb-f-reference-value"><span class="caosdb-id">1234</span></a><a class="caosdb-f-reference-value"><span class="caosdb-id">2345</span></a><a class="caosdb-f-reference-value"><span class="caosdb-id">3456</span></a><a class="caosdb-f-reference-value"><span class="caosdb-id">4567</span></a></div>')[0]; assert.throws(() => { preview.createCarouselNav(); }, "no param throws"); @@ -844,16 +872,16 @@ QUnit.test("createCarouselNav", function(assert) { let nav = preview.createCarouselNav(refLinks, "cid"); assert.equal(nav.className, "caosdb-preview-carousel-nav", "caosdb-carousel-nav"); - assert.ok($(nav).find('[data-slide="prev"][href="#cid"]')[0], "has prev button"); - assert.ok($(nav).find('[data-slide="next"][href="#cid"]')[0], "has next button"); + assert.ok($(nav).find('[data-bs-slide="prev"][href="#cid"]')[0], "has prev button"); + assert.ok($(nav).find('[data-bs-slide="next"][href="#cid"]')[0], "has next button"); let selectors = preview.getRefLinksContainer(nav); assert.equal(selectors.children.length, 4, '4 selctor buttons'); $(document.body).append(nav); assert.equal($(selectors).is(':hidden'), false, "selectors not hidden."); $(nav).remove(); $(selectors).find('a').each((index, button) => { - assert.equal(button.getAttribute("data-slide-to"), index, "buttons have correct data-slide-to attribute"); - assert.equal(button.getAttribute("data-target"), "#cid", "buttons have correct data-target attribute"); + assert.equal(button.getAttribute("data-bs-slide-to"), index, "buttons have correct data-bs-slide-to attribute"); + assert.equal(button.getAttribute("data-bs-target"), "#cid", "buttons have correct data-bs-target attribute"); assert.notOk(button.getAttribute("href"), "button dont have href"); }); assert.equal($(selectors).find('a:first').hasClass('active'), true, "first button is active"); @@ -861,18 +889,18 @@ QUnit.test("createCarouselNav", function(assert) { }); { - let refLinks = $('<div class="caosdb-value-list"><a><span class="caosdb-id">1234</span></a><a><span class="caosdb-id">2345</span></a><a><span class="caosdb-id">3456</span></a><a><span class="caosdb-id">4567</span></a></div>')[0]; - let e1 = $('<div><div class="caosdb-id">1234</div></div>')[0]; - let e2 = $('<div><div class="caosdb-id">2345</div></div>')[0]; - let e3 = $('<div><div class="caosdb-id">3456</div></div>')[0]; - let e4 = $('<div><div class="caosdb-id">4567</div></div>')[0]; + let refLinks = $('<div class="caosdb-value-list"><a class="caosdb-f-reference-value"><span class="caosdb-id">1234</span></a><a class="caosdb-f-reference-value"><span class="caosdb-id">2345</span></a><a class="caosdb-f-reference-value"><span class="caosdb-id">3456</span></a><a class="caosdb-f-reference-value"><span class="caosdb-id">4567</span></a></div>')[0]; + let e1 = $('<a class="caosdb-f-reference-value"><div class="caosdb-id">1234</div></a>')[0]; + let e2 = $('<a class="caosdb-f-reference-value"><div class="caosdb-id">2345</div></a>')[0]; + let e3 = $('<a class="caosdb-f-reference-value"><div class="caosdb-id">3456</div></a>')[0]; + let e4 = $('<a class="caosdb-f-reference-value"><div class="caosdb-id">4567</div></a>')[0]; let entities = [e1, e3, e4, e2]; let carousel = preview.createPreviewCarousel(entities, refLinks); let correct_order_id = ["1234", "2345", "3456", "4567"]; let preview3Links = preview.createPreview(entities, refLinks); let preview1Link = preview.createPreview([e1], refLinks.children[0]); - QUnit.test("createPreviewCarousel", function(assert) { + QUnit.test("createPreviewCarousel", function (assert) { assert.ok(preview.createPreviewCarousel, "function available"); @@ -892,19 +920,19 @@ 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('.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"); - assert.equal($(carousel).attr("data-interval"), "false", "no auto-sliding"); + assert.equal($(carousel).attr("data-bs-interval"), "false", "no auto-sliding"); }); - QUnit.test("getSelectorButtons", function(assert) { + QUnit.test("getSelectorButtons", function (assert) { assert.ok(preview.getSelectorButtons, "function available"); - assert.equal(preview.getSelectorButtons($(carousel).find('.' + preview.classNamePreviewCarouselNav)[0])[0].getAttribute('data-slide-to'), "0", "found selector button"); + assert.equal(preview.getSelectorButtons($(carousel).find('.' + preview.classNamePreviewCarouselNav)[0])[0].getAttribute('data-bs-slide-to'), "0", "found selector button"); }); - QUnit.test("setActiveSlideItemSelector", function(assert) { + QUnit.test("setActiveSlideItemSelector", function (assert) { assert.ok(preview.setActiveSlideItemSelector, "function available"); assert.throws(() => { @@ -929,24 +957,24 @@ QUnit.test("createCarouselNav", function(assert) { assert.equal(preview.setActiveSlideItemSelector(carousel, 1), carousel, "returns carousel"); for (let i = 0; i < correct_order_id.length; i++) { preview.setActiveSlideItemSelector(carousel, i); - assert.equal($($(carousel).find('[data-slide-to]')[i]).hasClass("active"), true, "button " + i + " is active"); - assert.equal($(carousel).find('.item.active').length, 1, "and none else"); + assert.equal($($(carousel).find('[data-bs-slide-to]')[i]).hasClass("active"), true, "button " + i + " is active"); + assert.equal($(carousel).find('.carousel-item.active').length, 1, "and none else"); } }); - QUnit.test("triggerUpdateActiveSlideItemSelector", function(assert) { + QUnit.test("triggerUpdateActiveSlideItemSelector", function (assert) { assert.ok(preview.triggerUpdateActiveSlideItemSelector, "function available"); preview.setActiveSlideItemSelector(carousel, 1); assert.equal(preview.getActiveSlideItemIndex(carousel), 0, "before: active item is 0"); - assert.equal($(carousel).find('.' + preview.classNamePreviewCarouselNav).find('.active')[0].getAttribute('data-slide-to'), 1, 'before: active selector is 1.'); + assert.equal($(carousel).find('.' + preview.classNamePreviewCarouselNav).find('.active')[0].getAttribute('data-bs-slide-to'), 1, 'before: active selector is 1.'); $(carousel).on('slid.bs.carousel', preview.triggerUpdateActiveSlideItemSelector); $(carousel).trigger('slid.bs.carousel'); assert.equal(preview.getActiveSlideItemIndex(carousel), 0, "after: active item is 0"); - assert.equal($(carousel).find('.' + preview.classNamePreviewCarouselNav).find('.active')[0].getAttribute('data-slide-to'), 0, 'after: active selector is 0.'); + assert.equal($(carousel).find('.' + preview.classNamePreviewCarouselNav).find('.active')[0].getAttribute('data-bs-slide-to'), 0, 'after: active selector is 0.'); }); - QUnit.test("createPreview", function(assert) { + QUnit.test("createPreview", function (assert) { assert.ok($(preview3Links).hasClass(preview.classNamePreview), "3 links class name."); assert.ok($(preview1Link).hasClass(preview.classNamePreview), "1 links class name."); assert.ok($(preview1Link).hasClass(preview.classNamePreview), "1 links returns entity element"); @@ -957,11 +985,7 @@ QUnit.test("createCarouselNav", function(assert) { let original_get = connection.get; ref_property_elem.find('div').append(refLinks); - const sleep = function sleep(ms) { - return new Promise(resolve => setTimeout(resolve, ms)); - } - - QUnit.test("initProperty", async function(assert) { + QUnit.test("initProperty", async function (assert) { var done = assert.async(2); assert.ok(preview.initProperty, "function available"); @@ -972,7 +996,7 @@ QUnit.test("createCarouselNav", function(assert) { assert.equal(showPreviewButton.length, 1, 'one show preview button.'); // test case for error in get method - connection.get = async function(uri) { + connection.get = async function (uri) { assert.equal(uri, "Entity/1234&2345&3456&4567", "get called with correct uri"); await sleep(1000); done(); @@ -989,7 +1013,7 @@ QUnit.test("createCarouselNav", function(assert) { assert.equal(app.state, 'showLinks', 'after reset in showLinks state'); // test case for mockup-preview data - connection.get = async function(uri) { + connection.get = async function (uri) { if (uri.match(/webinterface/g)) { return await original_get(uri); } @@ -1012,7 +1036,7 @@ QUnit.test("createCarouselNav", function(assert) { hidePreviewButton.click(); assert.equal(app.state, 'showLinks', 'again in showLinks state.'); - connection.get = function(uri) { + connection.get = function (uri) { assert.ok(null, 'get was called: ' + uri); } showPreviewButton.click(); @@ -1027,39 +1051,41 @@ QUnit.test("createCarouselNav", function(assert) { }; -QUnit.test("preparePreviewEntity", function(assert){ +QUnit.test("preparePreviewEntity", function (assert) { assert.ok(preview.preparePreviewEntity, "function available"); - let e = $('<div><div class="label caosdb-id">1234</div></div>')[0]; + let e = $('<div><div class="caosdb-v-entity-header-buttons-list"><div class="caosdb-f-reference-value"><a class="caosdb-id">1234</a></div></div></div>')[0]; let prepared = preview.preparePreviewEntity(e); - assert.equal($(prepared).find('a.caosdb-id')[0].href, connection.getBasePath() + "Entity/1234", "link is correct."); + assert.equal($(prepared).find("a[title='Load this entity in a new window.']")[0].href, connection.getBasePath() + "Entity/1234", "link is correct."); }); -QUnit.test("getEntityRef", function(assert) { +QUnit.test("getEntityRef", function (assert) { assert.ok(preview.getEntityRef, 'function available'); - var html = $('<div><div class="caosdb-id">sdfg</div></div>')[0]; + var html = $('<div class="caosdb-f-reference-value"><div class="caosdb-id">sdfg</div></div>')[0]; assert.equal(preview.getEntityRef(html), "sdfg", "id extracted"); - html = $('<div><div class="caosdb-id"></div></div>')[0]; + html = $('<div class="caosdb-f-reference-value"><div class="caosdb-id"></div></div>')[0]; assert.equal(preview.getEntityRef(html), "", "empty string extracted"); - html = $('<div></div>')[0]; - assert.throws(()=>{preview.getEntityRef(html);}, "missing .caosdb-id throws"); + html = $('<div class="caosdb-f-reference-value"></div>')[0]; + assert.throws(() => { + preview.getEntityRef(html); + }, "missing .caosdb-id throws"); }); -QUnit.test("getAllEntityRefs", function(assert) { +QUnit.test("getAllEntityRefs", function (assert) { assert.ok(preview.getAllEntityRefs, 'function available'); assert.throws(preview.getAllEntityRefs, "null param throws"); // overwrite called methods const oldGetReferenceLinks = preview.getReferenceLinks; - preview.getReferenceLinks = function(links) { + preview.getReferenceLinks = function (links) { assert.propEqual(links, ["bla"], "array is passed to getReferenceLinks"); return links; } const oldGetEntityRef = preview.getEntityRef; - preview.getEntityRef = function(link) { + preview.getEntityRef = function (link) { assert.equal(link, "bla", "array elements are passed to getEntityRef"); return "asdf"; } @@ -1071,10 +1097,10 @@ QUnit.test("getAllEntityRefs", function(assert) { preview.getReferenceLinks = oldGetReferenceLinks; }); -QUnit.test("retrievePreviewEntities", function(assert) { +QUnit.test("retrievePreviewEntities", function (assert) { let done = assert.async(3); - connection.get = function(url){ - if(url.length>15) { + connection.get = function (url) { + if (url.length > 15) { assert.equal(url, "Entity/1&2&3&4&5", "All five entities are to be retrieved."); done(); throw new Error("UriTooLongException") @@ -1085,19 +1111,22 @@ QUnit.test("retrievePreviewEntities", function(assert) { } } assert.ok(preview.retrievePreviewEntities, "function available"); - preview.retrievePreviewEntities([1,2,3,4,5]).catch(err=>{assert.equal(err.message, "Terminate this test!", "The url had been split up.");done();}); + preview.retrievePreviewEntities([1, 2, 3, 4, 5]).catch(err => { + assert.equal(err.message, "Terminate this test!", "The url had been split up."); + done(); + }); }); -QUnit.test("transformXmlToPreviews", function(assert) { +QUnit.test("transformXmlToPreviews", function (assert) { assert.ok(preview.transformXmlToPreviews, "function available"); assert.ok(this.entityXSL, "xsl there"); assert.ok(this.testXml, "xml there"); let done = assert.async(); - let asyncTestCase = function(resolve) { + let asyncTestCase = function (resolve) { done(); }; - let asyncErr = function(error) { + let asyncErr = function (error) { console.log(error); done(); done(); @@ -1105,22 +1134,22 @@ QUnit.test("transformXmlToPreviews", function(assert) { preview.transformXmlToPreviews(this.testXml, this.entityXSL).then(asyncTestCase).catch(asyncErr); }); -QUnit.test("init", function(assert) { +QUnit.test("init", function (assert) { assert.ok(preview.init, "function available"); }); -QUnit.test("initEntity", function(assert) { +QUnit.test("initEntity", function (assert) { assert.ok(preview.initEntity, "function available"); }); /* MODULE queryForm */ QUnit.module("webcaosdb.js - queryForm", { - before: function(assert) { + before: function (assert) { assert.ok(queryForm, "queryForm is defined"); } }); -QUnit.test("removePagingField", function(assert) { +QUnit.test("removePagingField", function (assert) { assert.ok(queryForm.removePagingField, "function available."); assert.throws(() => queryForm.removePagingField(), "null param throws."); let form = $('<form><input name="P"></form>')[0]; @@ -1130,7 +1159,7 @@ QUnit.test("removePagingField", function(assert) { }); -QUnit.test("isSelectQuery", function(assert) { +QUnit.test("isSelectQuery", function (assert) { assert.ok(queryForm.isSelectQuery, "function available."); assert.throws(() => queryForm.isSelectQuery(), "null param throws."); assert.equal(queryForm.isSelectQuery("SELECT asdf"), true); @@ -1141,11 +1170,11 @@ QUnit.test("isSelectQuery", function(assert) { assert.equal(queryForm.isSelectQuery("SEL ECT"), false); }); -QUnit.test("init", function(assert) { +QUnit.test("init", function (assert) { assert.ok(queryForm.init, "init available"); }); -QUnit.test("restoreLastQuery", function(assert) { +QUnit.test("restoreLastQuery", function (assert) { assert.ok(queryForm.restoreLastQuery, "available"); let form = document.createElement("form"); @@ -1164,10 +1193,12 @@ QUnit.test("restoreLastQuery", function(assert) { assert.equal(form.query.value, "this is the old query", "after2: field is not empty"); }); -QUnit.test("bindOnClick", function(assert) { +QUnit.test("bindOnClick", function (assert) { assert.ok(queryForm.bindOnClick, "available"); var done = assert.async(2); - queryForm.redirect = function(a, b) {done();}; + queryForm.redirect = function (a, b) { + done(); + }; let form = document.createElement("form"); let submitButton = $("<input type=\"submit\">"); @@ -1179,9 +1210,9 @@ QUnit.test("bindOnClick", function(assert) { assert.throws(() => queryForm.bindOnClick(null, (set) => undefined), "null form throws exc."); assert.throws(() => queryForm.bindOnClick("asdf", (set) => undefined), "string form throws exc."); - let storage = function() { + let storage = function () { let x = undefined; - return function(set) { + return function (set) { if (set) { x = set; } @@ -1213,10 +1244,10 @@ QUnit.test("bindOnClick", function(assert) { /* MODULE paging */ QUnit.module("webcaosdb.js - paging", { - before: function(assert) {} + before: function (assert) {} }); -QUnit.test("initPaging", function(assert) { +QUnit.test("initPaging", function (assert) { let initPaging = paging.initPaging; let getPageHref = paging.getPageHref; assert.ok(initPaging, "function exists."); @@ -1230,137 +1261,10 @@ QUnit.test("initPaging", function(assert) { assert.equal(initPaging(getPageHref(window.location.href, "0L10"), 1234), true, "1234 returns true."); assert.equal(initPaging(getPageHref(window.location.href, "0L10"), '1234'), true, "'1234' returns true."); - // test effectiveness - let $pagingPanel = $('<div>', { - "class": "caosdb-paging-panel" - }); - let $prevButton = $('<a>', { - "class": "caosdb-prev-button" - }); - let $nextButton = $('<a>', { - "class": "caosdb-next-button" - }); +}); + - $pagingPanel.append($prevButton).append($nextButton); - $(document.body).append($pagingPanel); - - $prevButton.hide(); - $nextButton.hide(); - $pagingPanel.hide(); - - - // no paging at all: - let hidden_prev = $('.caosdb-prev-button').css("display") == "none"; - let hidden_next = $('.caosdb-next-button').css("display") == "none"; - let hidden_panel = $('.caosdb-paging-panel').css("display") == "none"; - - initPaging(window.location.href); - hidden_prev = hidden_prev && $('.caosdb-prev-button').css("display") == "none"; - hidden_next = hidden_next && $('.caosdb-next-button').css("display") == "none"; - hidden_panel = hidden_panel && $('.caosdb-paging-panel').css("display") == "none"; - - initPaging(window.location.href, null); - hidden_prev = hidden_prev && $('.caosdb-prev-button').css("display") == "none"; - hidden_next = hidden_next && $('.caosdb-next-button').css("display") == "none"; - hidden_panel = hidden_panel && $('.caosdb-paging-panel').css("display") == "none"; - - initPaging(null, null); - hidden_prev = hidden_prev && $('.caosdb-prev-button').css("display") == "none"; - hidden_next = hidden_next && $('.caosdb-next-button').css("display") == "none"; - hidden_panel = hidden_panel && $('.caosdb-paging-panel').css("display") == "none"; - - initPaging(window.location.href, "100"); - hidden_prev = hidden_prev && $('.caosdb-prev-button').css("display") == "none"; - hidden_next = hidden_next && $('.caosdb-next-button').css("display") == "none"; - hidden_panel = hidden_panel && $('.caosdb-paging-panel').css("display") == "none"; - - initPaging(window.location.href, 100); - hidden_prev = hidden_prev && $('.caosdb-prev-button').css("display") == "none"; - hidden_next = hidden_next && $('.caosdb-next-button').css("display") == "none"; - hidden_panel = hidden_panel && $('.caosdb-paging-panel').css("display") == "none"; - - initPaging(getPageHref(window.location.href, "0L100"), "100"); - hidden_prev = hidden_prev && $('.caosdb-prev-button').css("display") == "none"; - hidden_next = hidden_next && $('.caosdb-next-button').css("display") == "none"; - hidden_panel = hidden_panel && $('.caosdb-paging-panel').css("display") == "none"; - - assert.equal(hidden_prev, true, "prev button has display=none"); - assert.equal(hidden_next, true, "next button has display=none"); - assert.equal(hidden_panel, true, "paging panel has display=none"); - - // show next button - initPaging(getPageHref(window.location.href, "0L10"), 100); - hidden_prev = $('.caosdb-prev-button').css("display") == "none"; - hidden_panel = $('.caosdb-paging-panel').css("display") != "block"; - hidden_next = $('.caosdb-next-button').css("display") != "inline"; - let nextHrefOk = $('.caosdb-next-button').attr('href') == getPageHref(window.location.href, "10L10"); - - initPaging(getPageHref(window.location.href, "0L10"), "100"); - hidden_prev = hidden_prev && $('.caosdb-prev-button').css("display") == "none"; - hidden_panel = hidden_panel || $('.caosdb-paging-panel').css("display") != "block"; - hidden_next = hidden_next || $('.caosdb-next-button').css("display") != "inline"; - nextHrefOk = nextHrefOk && $('.caosdb-next-button').attr('href') == getPageHref(window.location.href, "10L10"); - - initPaging(getPageHref(window.location.href, "0L99"), "100"); - hidden_prev = hidden_prev && $('.caosdb-prev-button').css("display") == "none"; - hidden_panel = hidden_panel || $('.caosdb-paging-panel').css("display") != "block"; - hidden_next = hidden_next || $('.caosdb-next-button').css("display") != "inline"; - nextHrefOk = nextHrefOk && $('.caosdb-next-button').attr('href') == getPageHref(window.location.href, "99L99"); - - assert.equal(hidden_prev, true, "prev button has display=none"); - assert.equal(hidden_next, false, "next button has display=inline"); - assert.equal(hidden_panel, false, "paging panel has display=block"); - assert.equal(nextHrefOk, true, "next buttons href is ok"); - - // show prev button - initPaging(getPageHref(window.location.href, "10L100"), 100); - hidden_prev = $('.caosdb-prev-button').css("display") != "inline"; - hidden_panel = $('.caosdb-paging-panel').css("display") != "block"; - hidden_next = $('.caosdb-next-button').css("display") == "none"; - let prevHrefOk = $('.caosdb-prev-button').attr('href') == getPageHref(window.location.href, "0L100"); - - initPaging(getPageHref(window.location.href, "1L100"), 100); - hidden_prev = hidden_prev || $('.caosdb-prev-button').css("display") != "inline"; - hidden_panel = hidden_panel || $('.caosdb-paging-panel').css("display") != "block"; - hidden_next = hidden_next && $('.caosdb-next-button').css("display") == "none"; - prevHrefOk = prevHrefOk && $('.caosdb-prev-button').attr('href') == getPageHref(window.location.href, "0L100"); - - initPaging(getPageHref(window.location.href, "20L10"), 100); - hidden_prev = hidden_prev || $('.caosdb-prev-button').css("display") != "inline"; - hidden_panel = hidden_panel || $('.caosdb-paging-panel').css("display") != "block"; - prevHrefOk = prevHrefOk && $('.caosdb-prev-button').attr('href') == getPageHref(window.location.href, "10L10"); - - assert.equal(hidden_prev, false, "prev button has display=inline"); - assert.equal(hidden_next, true, "next button has display=none"); - assert.equal(hidden_panel, false, "paging panel has display=block"); - assert.equal(prevHrefOk, true, "prev buttons href is ok"); - - // show both - initPaging(getPageHref(window.location.href, "10L10"), 100); - hidden_prev = $('.caosdb-prev-button').css("display") != "inline"; - hidden_panel = $('.caosdb-paging-panel').css("display") != "block"; - hidden_next = $('.caosdb-next-button').css("display") != "inline"; - nextHrefOk = nextHrefOk && $('.caosdb-next-button').attr('href') == getPageHref(window.location.href, "20L10"); - prevHrefOk = prevHrefOk && $('.caosdb-prev-button').attr('href') == getPageHref(window.location.href, "0L10"); - - initPaging(getPageHref(window.location.href, "1L100"), 200); - hidden_prev = hidden_prev || $('.caosdb-prev-button').css("display") != "inline"; - hidden_panel = hidden_panel || $('.caosdb-paging-panel').css("display") != "block"; - hidden_next = hidden_next || $('.caosdb-next-button').css("display") != "inline"; - nextHrefOk = nextHrefOk && $('.caosdb-next-button').attr('href') == getPageHref(window.location.href, "101L100"); - prevHrefOk = prevHrefOk && $('.caosdb-prev-button').attr('href') == getPageHref(window.location.href, "0L100"); - - assert.equal(hidden_prev, false, "prev button has display=inline"); - assert.equal(hidden_next, false, "next button has display=inline"); - assert.equal(hidden_panel, false, "paging panel has display=block"); - assert.equal(prevHrefOk, true, "prev buttons href is ok"); - assert.equal(nextHrefOk, true, "next buttons href is ok"); - - document.body.removeChild($pagingPanel[0]); -}); - - -QUnit.test("getNextPage", function(assert) { +QUnit.test("getNextPage", function (assert) { let getNextPage = paging.getNextPage; assert.ok(getNextPage, "function exists."); assert.throws(() => { @@ -1392,7 +1296,7 @@ QUnit.test("getNextPage", function(assert) { }); -QUnit.test("getPrevPage", function(assert) { +QUnit.test("getPrevPage", function (assert) { let getPrevPage = paging.getPrevPage; assert.ok(getPrevPage, "function exists."); assert.throws(() => { @@ -1413,7 +1317,7 @@ QUnit.test("getPrevPage", function(assert) { assert.equal(getPrevPage("23L11"), "12L11", "Index 5 to index 0."); }); -QUnit.test("getPSegmentFromUri", function(assert) { +QUnit.test("getPSegmentFromUri", function (assert) { let getPSegmentFromUri = paging.getPSegmentFromUri; assert.ok(getPSegmentFromUri, "function exists."); assert.throws(() => { @@ -1431,7 +1335,7 @@ QUnit.test("getPSegmentFromUri", function(assert) { assert.equal(getPSegmentFromUri("https://example:1234/blabla?asdf&P=0L10&asdfasdf"), "0L10", "asdf?asdf&P=0L10&asdfasdf -> P=0L10"); }); -QUnit.test("getPageHref", function(assert) { +QUnit.test("getPageHref", function (assert) { let getPageHref = paging.getPageHref; assert.ok(getPageHref, "function exists."); assert.throws(() => { @@ -1447,20 +1351,20 @@ QUnit.test("getPageHref", function(assert) { /* MODULE annotation */ QUnit.module("webcaosdb.js - annotation", { - before: function(assert) { + before: function (assert) { markdown.init(); // overwrite (we don't actually want to send any post requests) - annotation.postCommentXml = function(xml) { + annotation.postCommentXml = function (xml) { return new Promise(resolve => setTimeout(resolve, 1000, str2xml("<Response/>"))); } } }); -QUnit.test("loadAnnotationXsl", function(assert) { +QUnit.test("loadAnnotationXsl", function (assert) { assert.ok(annotation.loadAnnotationXsl, "function exists"); }); -QUnit.test("getAnnotationsForEntity", function(assert) { +QUnit.test("getAnnotationsForEntity", function (assert) { let xsl_str = '<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform"><xsl:output method="html" /><xsl:template match="annotation"><div><xsl:value-of select="@id"/></div></xsl:template><xsl:template match="Response"><root><xsl:apply-templates select="annotation"/></root></xsl:template></xsl:stylesheet>'; let xslPromise = str2xml(xsl_str); let xml_str = '<Response><annotation id="1"/><annotation id="2"/><annotation id="3"/></Response>'; @@ -1470,7 +1374,7 @@ QUnit.test("getAnnotationsForEntity", function(assert) { }; let done = assert.async(); - let asyncTestCase = function(result) { + let asyncTestCase = function (result) { assert.equal(result.length, 3, "3 divs"); assert.equal(result[0].tagName, "DIV", "is DIV"); assert.equal(result[0].childNodes[0].nodeValue, "1", "test is '1'"); @@ -1488,7 +1392,7 @@ QUnit.test("getAnnotationsForEntity", function(assert) { }); QUnit.test("async/await behavior", (assert) => { - let af = async function() { + let af = async function () { return await "returnval"; }; @@ -1502,7 +1406,7 @@ QUnit.test("async/await behavior", (assert) => { done(); }); - let er = async function() { + let er = async function () { throw "asyncerror"; }; @@ -1512,30 +1416,30 @@ QUnit.test("async/await behavior", (assert) => { }); }); -QUnit.test("convertNewCommentForm", function(assert) { +QUnit.test("convertNewCommentForm", function (assert) { assert.ok(annotation.convertNewCommentForm, "function exists."); assert.equal(xml2str(annotation.convertNewCommentForm(annotation.createNewCommentForm(2345))), "<Insert><Record><Parent name=\"CommentAnnotation\"/><Property name=\"comment\"/><Property name=\"annotationOf\">2345</Property></Record></Insert>", "conversion ok."); }); -QUnit.test("convertNewCommentResponse", function(assert) { +QUnit.test("convertNewCommentResponse", function (assert) { let convertNewAnnotationResponse = annotation.convertNewCommentResponse; assert.ok(convertNewAnnotationResponse, "function exists."); let done = assert.async(); - let testResponse = '<Response><Record><Property name="annotationOf"/><History transaction="INSERT" datetime="2015-12-24T20:15:00" username="someuser"/><Property name="comment">This is a comment</Property></Record></Response>'; - let expectedResult = "<li xmlns=\"http://www.w3.org/1999/xhtml\" class=\"list-group-item markdowned\"><div class=\"media\"><div class=\"media-left\"><h3>»</h3></div><div class=\"media-body\"><h4 class=\"media-heading\">someuser<small><i> posted on 2015-12-24T20:15:00</i></small></h4><p class=\"caosdb-comment-annotation-text\"><p>This is a comment</p></p></div></div></li>"; - convertNewAnnotationResponse(str2xml(testResponse), annotation.loadAnnotationXsl("../../")).then(function(result) { + let testResponse = '<Response><Record><Property name="annotationOf"/><Version head="true" date="2015-12-24T20:15:00" username="someuser"/><Property name="comment">This is a comment</Property></Record></Response>'; + let expectedResult = "<li xmlns=\"http://www.w3.org/1999/xhtml\" class=\"list-group-item markdowned\"><div class=\"d-flex\"><div class=\"d-shrink-0\">»</div><div class=\"flex-grow-1 ms-3\"><div class=\"caosdb-f-comment-header\">someuser<small><i> posted on 2015-12-24T20:15:00</i></small></div><div class=\"caosdb-f-comment-body\"><small><p class=\"caosdb-comment-annotation-text\"><p>This is a comment</p></p></small></div></div></div></li>"; + convertNewAnnotationResponse(str2xml(testResponse), annotation.loadAnnotationXsl("../../")).then(function (result) { assert.equal(result.length, 1, "one element returned."); - assert.equal(xml2str(result[0]), expectedResult, "result converted correctly"); + assert.equal(xml2str(result[0]).replace(/\n/g, ""), expectedResult, "result converted correctly"); done(); - }, function(error) { + }, function (error) { console.log(error); assert.ok(false, "see console.log"); done(); }); }); -QUnit.test("getEntityId", function(assert) { +QUnit.test("getEntityId", function (assert) { let annotationSection = $('<div data-entity-id="dfgh"/>')[0]; assert.ok(annotation.getEntityId, "function exists."); assert.equal(annotation.getEntityId($('<div/>')[0]), null, "no data-entity-id attribute returns null"); @@ -1543,7 +1447,7 @@ QUnit.test("getEntityId", function(assert) { assert.equal(annotation.getEntityId(), null, "no param returns null."); }); -QUnit.test("createNewCommentForm", function(assert) { +QUnit.test("createNewCommentForm", function (assert) { let createNewCommentForm = annotation.createNewCommentForm; assert.ok(createNewCommentForm, "function exists."); assert.equal(createNewCommentForm(1234).tagName, "FORM", "returns form"); @@ -1555,7 +1459,7 @@ QUnit.test("createNewCommentForm", function(assert) { assert.equal($(createNewCommentForm(1234)).find("button[type='asdf']")[0], null, "no asdf button"); }); -QUnit.test("getNewCommentButton", function(assert) { +QUnit.test("getNewCommentButton", function (assert) { assert.ok(annotation.getNewCommentButton, "function exists."); assert.equal(annotation.getNewCommentButton($('<div/>')[0]), null, "not present"); assert.equal(annotation.getNewCommentButton($('<div><button class="otherclass"/></div>')[0]), null, "not present"); @@ -1564,19 +1468,19 @@ QUnit.test("getNewCommentButton", function(assert) { assert.equal(annotation.getNewCommentButton(null), null, "null parameter"); }); -QUnit.test("createPleaseWaitNotification", function(assert) { +QUnit.test("createPleaseWaitNotification", function (assert) { assert.ok(annotation.createPleaseWaitNotification, "function exists."); assert.ok($(annotation.createPleaseWaitNotification()).hasClass("caosdb-please-wait-notification"), "has class caosdb-please-wait-notification"); }); -QUnit.test("getNewCommentForm", function(assert) { +QUnit.test("getNewCommentForm", function (assert) { let annotationSection = $('<div><form id="sdfg" class="caosdb-new-comment-form"></form></div>')[0]; assert.ok(annotation.getNewCommentForm, "function exists"); assert.equal(annotation.getNewCommentForm(annotationSection).id, "sdfg", "NewCommentForm found."); assert.equal(annotation.getNewCommentForm(), null, "no param returns null"); }); -QUnit.test("validateNewCommentForm", function(assert) { +QUnit.test("validateNewCommentForm", function (assert) { assert.ok(annotation.validateNewCommentForm, "function exists."); let entityId = "asdf"; let form = annotation.createNewCommentForm(entityId); @@ -1588,17 +1492,17 @@ QUnit.test("validateNewCommentForm", function(assert) { assert.equal(annotation.validateNewCommentForm(form), true, "long enough returns true"); }); -QUnit.test("getPleaseWaitNotification", function(assert) { +QUnit.test("getPleaseWaitNotification", function (assert) { assert.ok(annotation.getPleaseWaitNotification, "function exists"); assert.equal(annotation.getPleaseWaitNotification(), null, "no param returns null"); assert.equal(annotation.getPleaseWaitNotification($('<div><div class="blablabla" id="asdf"></div></div>')[0]), null, "does not exist"); assert.equal(annotation.getPleaseWaitNotification($('<div><div class="caosdb-please-wait-notification" id="asdf"></div></div>')[0]).id, "asdf", "found."); }); -QUnit.test("NewCommentApp exception", function(assert) { +QUnit.test("NewCommentApp exception", function (assert) { try { var original = annotation.createNewCommentForm; - annotation.createNewCommentForm = function() { + annotation.createNewCommentForm = function () { throw new TypeError("This is really bad!"); } @@ -1617,7 +1521,7 @@ QUnit.test("NewCommentApp exception", function(assert) { } }); -QUnit.test("convertNewCommentResponse error", function(assert) { +QUnit.test("convertNewCommentResponse error", function (assert) { let errorStr = '<Response username="tf" realm="PAM" srid="dc1df091045eca7bd6940b88aa6db5b6" timestamp="1499814014684" baseuri="https://baal:8444/mpidsserver" count="1">\ <Error code="12" description="One or more entities are not qualified. None of them have been inserted/updated/deleted." />\ <Record>\ @@ -1640,26 +1544,26 @@ QUnit.test("convertNewCommentResponse error", function(assert) { </Response>'; let done = assert.async(); - let expectedResult = "<divxmlns=\"http://www.w3.org/1999/xhtml\"class=\"alertalert-dangercaosdb-new-comment-erroralert-dismissablemarkdowned\"><buttonclass=\"close\"data-dismiss=\"alert\"aria-label=\"close\">×</button><strong>Error!</strong>Thiscommenthasnotbeeninserted.<pclass=\"small\"><pre><code><record><errorcode=\"114\"description=\"Entityhasunqualifiedproperties.\"></error><warningcode=\"0\"description=\"Entityhasnoname.\"></warning><parentname=\"CommentAnnotation\"><errorcode=\"101\"description=\"Entitydoesnotexist.\"></error></parent><propertyname=\"comment\"importance=\"FIX\">sdfasdfasdf<errorcode=\"101\"description=\"Entitydoesnotexist.\"></error><errorcode=\"110\"description=\"Propertyhasnodatatype.\"></error></property><propertyname=\"annotationOf\"importance=\"FIX\">20<errorcode=\"101\"description=\"Entitydoesnotexist.\"></error><errorcode=\"110\"description=\"Propertyhasnodatatype.\"></error></property></record></code></pre></p></div>"; - annotation.convertNewCommentResponse(str2xml(errorStr), annotation.loadAnnotationXsl("../../")).then(function(result) { + let expectedResult = "<divxmlns=\"http://www.w3.org/1999/xhtml\"class=\"alertalert-dangercaosdb-new-comment-erroralert-dismissablemarkdowned\"><buttonclass=\"btn-close\"data-bs-dismiss=\"alert\"aria-label=\"close\">×</button><strong>Error!</strong>Thiscommenthasnotbeeninserted.<pclass=\"small\"><pre><code><record><errorcode=\"114\"description=\"Entityhasunqualifiedproperties.\"></error><warningcode=\"0\"description=\"Entityhasnoname.\"></warning><parentname=\"CommentAnnotation\"><errorcode=\"101\"description=\"Entitydoesnotexist.\"></error></parent><propertyname=\"comment\"importance=\"FIX\">sdfasdfasdf<errorcode=\"101\"description=\"Entitydoesnotexist.\"></error><errorcode=\"110\"description=\"Propertyhasnodatatype.\"></error></property><propertyname=\"annotationOf\"importance=\"FIX\">20<errorcode=\"101\"description=\"Entitydoesnotexist.\"></error><errorcode=\"110\"description=\"Propertyhasnodatatype.\"></error></property></record></code></pre></p></div>"; + annotation.convertNewCommentResponse(str2xml(errorStr), annotation.loadAnnotationXsl("../../")).then(function (result) { assert.equal(xml2str(result[0]).replace(/[\t\n\ ]/g, ""), expectedResult.replace(/[\t\n\ ]/g, ""), "transformed into an error div."); done(); - }, function(error) { + }, function (error) { console.log(error); assert.ok(false, "see console.log"); done(); }); }) -QUnit.test("NewCommentApp convertNewCommentResponse", function(assert) { +QUnit.test("NewCommentApp convertNewCommentResponse", function (assert) { var done = assert.async(2); var original = annotation.convertNewCommentResponse; - annotation.convertNewCommentResponse = function(xmlPromise, xslPromise) { + annotation.convertNewCommentResponse = function (xmlPromise, xslPromise) { done(1); // was called; return original(xmlPromise, xslPromise); } let originalPost = annotation.postCommentXml; - annotation.postCommentXml = function(xml) { + annotation.postCommentXml = function (xml) { let testResponse = '<Response><Record><Property name="annotationOf"/><History transaction="INSERT" datetime="2015-12-24T20:15:00" username="someuser"/><Property name="comment">This is a comment</Property></Record></Response>'; return new Promise(resolve => setTimeout(resolve, 1000, str2xml(testResponse))); } @@ -1691,13 +1595,13 @@ QUnit.test("NewCommentApp convertNewCommentResponse", function(assert) { annotation.postCommentXml = originalPost; }); -QUnit.test("NewCommentApp convertNewCommentForm/postCommentXml", function(assert) { +QUnit.test("NewCommentApp convertNewCommentForm/postCommentXml", function (assert) { let done = assert.async(2); // do not actually post the comment, just wait 1 second and return // something. let originalPost = annotation.postCommentXml; - annotation.postCommentXml = function(xml) { + annotation.postCommentXml = function (xml) { assert.equal(xml2str(xml), "<Insert><Record><Parent name=\"CommentAnnotation\"/><Property name=\"comment\">This is a new comment qwerasdf.</Property><Property name=\"annotationOf\">tzui</Property></Record></Insert>", "the conversion was sucessful"); done(2); // postCommentXml was called return new Promise(resolve => setTimeout(resolve, 1000, str2xml("<Response/>"))); @@ -1714,7 +1618,7 @@ QUnit.test("NewCommentApp convertNewCommentForm/postCommentXml", function(assert form.newComment.value = "This is a new comment qwerasdf."; let originalConvert = annotation.convertNewCommentForm; - annotation.convertNewCommentForm = function(sendform) { + annotation.convertNewCommentForm = function (sendform) { assert.ok(sendform == form, "form is still the same"); done(1); // convertNewCommentForm was called return originalConvert(sendform); @@ -1729,7 +1633,7 @@ QUnit.test("NewCommentApp convertNewCommentForm/postCommentXml", function(assert annotation.postCommentXml = originalPost; }); -QUnit.test("NewCommentApp waitingNotification", function(assert) { +QUnit.test("NewCommentApp waitingNotification", function (assert) { // prepare app let annotationSection = $('<div><button class="caosdb-new-comment-button"></button></div>')[0]; let app = annotation.initNewCommentApp(annotationSection); @@ -1746,7 +1650,7 @@ QUnit.test("NewCommentApp waitingNotification", function(assert) { }); -QUnit.test("NewCommentApp form.onsubmit", function(assert) { +QUnit.test("NewCommentApp form.onsubmit", function (assert) { let done = assert.async(2); let annotationSection = $('<div><button class="caosdb-new-comment-button"></button></div>')[0]; @@ -1761,7 +1665,7 @@ QUnit.test("NewCommentApp form.onsubmit", function(assert) { let submitButton = annotation.getSubmitNewCommentButton(annotationSection); // test with empty form -> rejected - app.observe("onBeforeTransition", function(e) { + app.observe("onBeforeTransition", function (e) { done("1&2"); }); @@ -1778,14 +1682,14 @@ QUnit.test("NewCommentApp form.onsubmit", function(assert) { $(annotationSection).remove(); }); -QUnit.test("NewCommentApp form.onreset", function(assert) { +QUnit.test("NewCommentApp form.onreset", function (assert) { let annotationSection = $('<div><button class="caosdb-new-comment-button"></button></div>')[0]; let app = annotation.initNewCommentApp(annotationSection); app.openForm(); let done = assert.async(); assert.equal(annotation.getNewCommentForm(annotationSection).className, "caosdb-new-comment-form", "form is there"); - app.observe("onBeforeCancelForm", function(e) { + app.observe("onBeforeCancelForm", function (e) { assert.equal(e.from, "write", "leaving write state"); done(); }); @@ -1803,7 +1707,7 @@ QUnit.test("NewCommentApp form.onreset", function(assert) { $(annotationSection).remove(); }); -QUnit.test("getCancelNewCommentButton", function(assert) { +QUnit.test("getCancelNewCommentButton", function (assert) { let annotationSection = $('<div><form class="caosdb-new-comment-form"><button id="fghj" type="reset"/></form></div>')[0]; assert.ok(annotation.getCancelNewCommentButton, "function exists."); assert.equal(annotation.getCancelNewCommentButton(), null, "no param returns null"); @@ -1812,7 +1716,7 @@ QUnit.test("getCancelNewCommentButton", function(assert) { assert.equal(annotation.getCancelNewCommentButton($('<div><form class="caosdb-new-comment-form"><button type="submit"/></form></div>')[0]), null, "button does not exist"); }); -QUnit.test("getSubmitNewCommentButton", function(assert) { +QUnit.test("getSubmitNewCommentButton", function (assert) { let annotationSection = $('<div><form class="caosdb-new-comment-form"><button id="fghj" type="submit"/></form></div>')[0]; assert.ok(annotation.getSubmitNewCommentButton, "function exists."); assert.equal(annotation.getSubmitNewCommentButton(), null, "no param returns null"); @@ -1821,7 +1725,7 @@ QUnit.test("getSubmitNewCommentButton", function(assert) { assert.equal(annotation.getSubmitNewCommentButton($('<div><form class="caosdb-new-comment-form"><button type="reset"/></form></div>')[0]), null, "button does not exist"); }); -QUnit.test("NewCommentApp newCommentButton.onclick", function(assert) { +QUnit.test("NewCommentApp newCommentButton.onclick", function (assert) { let annotationSection = $('<div><button class="caosdb-new-comment-button"></button></div>')[0]; let app = annotation.initNewCommentApp(annotationSection); let button = annotation.getNewCommentButton(annotationSection); @@ -1839,7 +1743,7 @@ QUnit.test("NewCommentApp newCommentButton.onclick", function(assert) { assert.equal(annotation.getNewCommentForm(annotationSection).parentNode.className, "list-group-item", "form is wrapped into list-group-item"); }); -QUnit.test("NewCommentApp transitions", function(assert) { +QUnit.test("NewCommentApp transitions", function (assert) { assert.throws(annotation.initNewCommentApp, "null parameter throws exc."); let annotationSection = $('<div><button class="caosdb-new-comment-button"></button></div>')[0]; @@ -1865,7 +1769,7 @@ QUnit.test("NewCommentApp transitions", function(assert) { assert.equal(app.state, "read", "reset -> state read"); }); -QUnit.test("annotation module", function(assert) { +QUnit.test("annotation module", function (assert) { assert.ok(annotation, "module exists."); assert.ok(annotation.createNewCommentForm, "createNewCommentForm exists."); assert.ok(annotation.initNewCommentApp, "initNewCommentApp exists."); @@ -1885,25 +1789,30 @@ QUnit.module("webcaosdb.js - navbar", { }, }); -QUnit.test("get_navbar", function(assert) { +QUnit.test("get_navbar", function (assert) { assert.equal(navbar.get_navbar().className, "caosdb-navbar"); }); -QUnit.test("add_button wrong parameters", function(assert) { - assert.throws(()=>{navbar.add_button(undefined)}, /button is expected/, "undefined throws"); - assert.throws(()=>{navbar.add_button({"test": "an object"})}, "object throws"); - assert.throws(()=>{navbar.add_button(["array of strings"])}, "array of string throws"); +QUnit.test("add_button wrong parameters", function (assert) { + assert.throws(() => { + navbar.add_button(undefined) + }, /button is expected/, "undefined throws"); + assert.throws(() => { + navbar.add_button({ + "test": "an object" + }) + }, "object throws"); + assert.throws(() => { + navbar.add_button(["array of strings"]) + }, "array of string throws"); }); -QUnit.test("test button classes", function(assert) { +QUnit.test("test button classes", function (assert) { var result = $(navbar.add_button("TestButton")).children().first() - assert.ok(result.hasClass("navbar-btn"), "has class navbar-btn"); - assert.ok(result.hasClass("btn"), "has class btn"); - assert.ok(result.hasClass("btn-link"), "has class btn-link"); assert.equal(result.text(), "TestButton", "text is correct"); }); -QUnit.test("add_tool", function(assert) { +QUnit.test("add_tool", function (assert) { assert.equal($(".caosdb-f-navbar-toolbox").length, 0, "no toolbox"); navbar.add_tool("TestButton", "TestMenu"); @@ -1921,7 +1830,7 @@ QUnit.test("add_tool", function(assert) { assert.notOk(toolbox.siblings("a.dropdown-toggle").hasClass("navbar-btn")); }); -QUnit.test("toolbox example", function(assert) { +QUnit.test("toolbox example", function (assert) { // this is a kind of integration test and it uses the toolbox_example // module from toolbox_example.js. That example is also usefull for manual // testing. @@ -1939,3 +1848,128 @@ QUnit.test("toolbox example", function(assert) { assert.equal($('.caosdb-f-navbar-toolbox[data-toolbox-name="Tools"]').length, 1, "one 'Tools' toolbox"); assert.equal($('.caosdb-f-navbar-toolbox[data-toolbox-name="Tools"] button').length, 3, "three 'Tools' buttons"); }); + +QUnit.module("webcaosdb.js - version_history", { + before: function (assert) { + connection._init(); + }, + after: function (assert) { + connection._init(); + }, +}); + +QUnit.test("available", function (assert) { + assert.equal(typeof version_history.init, "function"); + assert.equal(typeof version_history.get_history_tsv, "function"); + assert.equal(typeof version_history.init_export_history_buttons, "function"); + assert.equal(typeof version_history.init_load_history_buttons, "function"); + assert.equal(typeof version_history.retrieve_history, "function"); +}) + +QUnit.test("init_load_history_buttons and init_load_history_buttons", async function (assert) { + var xml_str = `<Response username="user1" realm="Realm1" srid="bc2f8f6b-71d6-49ca-890c-eebea3e38e18" timestamp="1606253365632" baseuri="https://localhost:10443" count="1"> + <UserInfo username="user1" realm="Realm1"> + <Roles> + <Role>role1</Role> + </Roles> + </UserInfo> + <Record id="8610" name="TestRecord1-6thVersion" description="This is the 6th version."> + <Permissions> + <Permission name="RETRIEVE:HISTORY" /> + </Permissions> + <Version id="efa5ac7126c722b3f43284e150d070d6deac0ba6"> + <Predecessor id="f09114b227d88f23d4e23645ae471d688b1e82f7" /> + <Successor id="5759d2bccec3662424db5bb005acea4456a299ef" /> + </Version> + <Parent id="8609" name="TestRT" /> + </Record> +</Response> +`; + var done = assert.async(2); + var xml = str2xml(xml_str); + version_history._get = async function (entity) { + assert.equal(entity, "Entity/8610@efa5ac7126c722b3f43284e150d070d6deac0ba6?H"); + done(); + $(xml).find("Version").attr("completeHistory", "true"); + return xml; + } + var html = await transformation.transformEntities(xml); + var load_button = $(html).find(".caosdb-f-entity-version-load-history-btn"); + $("body").append(html); + + assert.notOk(load_button.is(":visible"), "load_button hidden"); + load_button.click(); // nothing happens + + version_history.init_load_history_buttons(); + assert.ok(load_button.is(":visible"), "load_button is not hidden anymore"); + + // load_button triggers retrieval of history + load_button.click(); + await sleep(500); + + var gone_button = $(html).find(".caosdb-f-entity-version-load-history-btn"); + assert.equal(gone_button.length, 0, "button is gone"); + + export_button = $(html).find(".caosdb-f-entity-version-export-history-btn"); + assert.ok(export_button.is(":visible"), "export_button is visible"); + + version_history._download_tsv = function (tsv) { + assert.equal(tsv.indexOf("data:text/csv;charset=utf-8,Entity ID%09"), 0); + done(); + } + export_button.click(); + + $(html).remove(); +}); + + +/* SETUP tests for user_management */ +QUnit.module("webcaosdb.js - user_management", { + afterEach: function (assert) { + $(user_management.get_change_password_form()).remove(); + }, +}); + + +QUnit.test("get_change_password_form", async function (assert) { + assert.equal(typeof user_management.get_change_password_form(), "undefined", "no password form"); + + const modal = callTemplate( + (await transformation.retrieveXsltScript("navbar.xsl")), + "change-password-modal", { + "username": "testuser", + "realm": "testrealm" + }); + + $("body").append(modal); + + assert.ok(user_management.get_change_password_form(), "found password form"); +}); + +QUnit.test("submit new password", async function (assert) { + const modal = $(callTemplate( + (await transformation.retrieveXsltScript("navbar.xsl")), + "change-password-modal", { + "username": "testuser", + "realm": "testrealm" + }).firstElementChild); + + $("body").append(modal); + + user_management.init(); + var done = assert.async(); + user_management.set_new_password = async (realm, user, password) => { + assert.equal(realm, "testrealm", "realm correct"); + assert.equal(user, "testuser", "user correct"); + assert.equal(password, "newtestpassword1A!", "password correct"); + done(); + } + + const form = modal.find("form"); + assert.ok(form, "form there"); + + form[0]["password"].value = "newtestpassword1A!"; + form[0]["password2"].value = "newtestpassword1A!"; + + form.find(":submit").click(); +}); diff --git a/test/core/js/modules/welcome.xsl.js b/test/core/js/modules/welcome.xsl.js index 39fb9fd0edd9e4950e9bbad3189b2aeed4ed7a7c..de59eedca9bbacdbc4c797f9bfd51d11bb332132 100644 --- a/test/core/js/modules/welcome.xsl.js +++ b/test/core/js/modules/welcome.xsl.js @@ -46,10 +46,10 @@ QUnit.test("availability", function(assert) { assert.ok(this.welcomeXSL); }) -QUnit.test("welcome template produces .caosdb-f-welcome-panel", function(assert) { +QUnit.test("welcome template produces .caosdb-v-welcome-panel", function(assert) { var xsl = injectTemplate(this.welcomeXSL, '<xsl:template match="/"><xsl:call-template name="welcome"/></xsl:template>'); var xml_str = '<root>'; var xml = str2xml(xml_str); var html = xslt(xml, xsl); - assert.ok($(html.firstElementChild).hasClass("caosdb-f-welcome-panel"), "has class .caosdb-f-welcome-panel"); + assert.ok($(html.firstElementChild).hasClass("caosdb-v-welcome-panel"), "has class .caosdb-v-welcome-panel"); }); diff --git a/test/core/js/setup.js b/test/core/js/setup.js index 9894827988999e89a6388206f10bdd815098e81e..fac4fd78d0c867cf213917b16d874513d1871a09 100644 --- a/test/core/js/setup.js +++ b/test/core/js/setup.js @@ -46,3 +46,6 @@ QUnit.done(function( details ) { $.post("/done", report); }); +const sleep = function sleep(ms) { + return new Promise(resolve => setTimeout(resolve, ms)); +} diff --git a/test/core/pics/saturn.tif b/test/core/pics/saturn.tif new file mode 100644 index 0000000000000000000000000000000000000000..f226ebab7fca3c9caa306f78d80eaa7b4068f994 Binary files /dev/null and b/test/core/pics/saturn.tif differ diff --git a/test/core/xml/table_export/test_case_select_table_1.xml b/test/core/xml/table_export/test_case_select_table_1.xml index 12f26180b89bdf3f2ba3280d4546783f54bfa8ea..be07fd42df8bc1275ab16802c0e82c19ece417ad 100644 --- a/test/core/xml/table_export/test_case_select_table_1.xml +++ b/test/core/xml/table_export/test_case_select_table_1.xml @@ -9,8 +9,9 @@ </Selection> </Query> <Record id="242"> + <Version id="abc123" head="true"/> <Property id="117" name="Number" datatype="TEXT" importance="FIX"> - 0284 + 02	8


4aaa	a </Property> <Property id="104" name="Bag" datatype="Bag" importance="FIX"> 6366 @@ -35,10 +36,19 @@ </Property> </Record> <Record id="2112"> + <Version id="abc124" head="true"/> <Property id="117" name="Number" datatype="TEXT" importance="FIX"> 1101 </Property> <Property id="104" name="Bag" datatype="LIST<Bag>" importance="FIX"> </Property> </Record> + <Record id="2112"> + <Version id="abc125" head="true"/> + <Property id="117" name="Number" datatype="TEXT" importance="FIX"> + 1102 + </Property> + <Property id="104" name="Bag" datatype="LIST<Bag>" importance="FIX"> + </Property> + </Record> </Response> diff --git a/test/docker/Dockerfile b/test/docker/Dockerfile index a19843fb3de11bbedf1c01f71619470bcc99f75c..92889dc526229475463ddcdfe6a2080a669b9d25 100644 --- a/test/docker/Dockerfile +++ b/test/docker/Dockerfile @@ -1,7 +1,22 @@ -FROM debian:latest -RUN apt-get update && \ - apt-get install firefox-esr gettext-base pylint3 python3-pip \ - python3-httpbin git curl x11-apps xvfb unzip -y -RUN git clone -b dev https://gitlab.com/caosdb/caosdb-pylib.git && \ - cd caosdb-pylib && pip3 install . +FROM debian:10 +ADD node_gpg.asc /etc/apt/ +RUN apt-get update \ + && apt-get install -y gnupg ca-certificates\ + && apt-key add /etc/apt/node_gpg.asc \ + && echo "deb http://deb.debian.org/debian buster-backports main" >> /etc/apt/sources.list \ + && echo "deb https://deb.nodesource.com/node_14.x buster main" >> /etc/apt/sources.list \ + && apt-get update \ + && apt-get install -y \ + firefox-esr gettext-base python3-pip \ + python3-httpbin git curl x11-apps xvfb unzip \ + nodejs # Don't install `npm` (Debian), it conflicts with the `nodejs` (Node) package \ + && apt-get install -f +RUN pip3 install pylint pytest +RUN pip3 install caosdb==0.5.1 +RUN pip3 install pandas xlrd==1.2.0 +RUN pip3 install git+https://gitlab.com/caosdb/caosdb-advanced-user-tools.git@dev +# For automatic documentation +#RUN npm install -g jsdoc +#RUN npm install -g jsdoc-sphinx +RUN pip3 install sphinx-js sphinx-autoapi recommonmark sphinx-rtd-theme diff --git a/test/docker/node_gpg.asc b/test/docker/node_gpg.asc new file mode 100644 index 0000000000000000000000000000000000000000..ed458a24e9132e0f22b6081d5e2b68926d00c725 --- /dev/null +++ b/test/docker/node_gpg.asc @@ -0,0 +1,54 @@ +Downloaded on 2020-12-18 + +-----BEGIN PGP PUBLIC KEY BLOCK----- +Version: GnuPG v1 +Comment: GPGTools - https://gpgtools.org + +mQINBFObJLYBEADkFW8HMjsoYRJQ4nCYC/6Eh0yLWHWfCh+/9ZSIj4w/pOe2V6V+ +W6DHY3kK3a+2bxrax9EqKe7uxkSKf95gfns+I9+R+RJfRpb1qvljURr54y35IZgs +fMG22Np+TmM2RLgdFCZa18h0+RbH9i0b+ZrB9XPZmLb/h9ou7SowGqQ3wwOtT3Vy +qmif0A2GCcjFTqWW6TXaY8eZJ9BCEqW3k/0Cjw7K/mSy/utxYiUIvZNKgaG/P8U7 +89QyvxeRxAf93YFAVzMXhoKxu12IuH4VnSwAfb8gQyxKRyiGOUwk0YoBPpqRnMmD +Dl7SdmY3oQHEJzBelTMjTM8AjbB9mWoPBX5G8t4u47/FZ6PgdfmRg9hsKXhkLJc7 +C1btblOHNgDx19fzASWX+xOjZiKpP6MkEEzq1bilUFul6RDtxkTWsTa5TGixgCB/ +G2fK8I9JL/yQhDc6OGY9mjPOxMb5PgUlT8ox3v8wt25erWj9z30QoEBwfSg4tzLc +Jq6N/iepQemNfo6Is+TG+JzI6vhXjlsBm/Xmz0ZiFPPObAH/vGCY5I6886vXQ7ft +qWHYHT8jz/R4tigMGC+tvZ/kcmYBsLCCI5uSEP6JJRQQhHrCvOX0UaytItfsQfLm +EYRd2F72o1yGh3yvWWfDIBXRmaBuIGXGpajC0JyBGSOWb9UxMNZY/2LJEwARAQAB +tB9Ob2RlU291cmNlIDxncGdAbm9kZXNvdXJjZS5jb20+iQI4BBMBAgAiBQJTmyS2 +AhsDBgsJCAcDAgYVCAIJCgsEFgIDAQIeAQIXgAAKCRAWVaCraFdigHTmD/9OKhUy +jJ+h8gMRg6ri5EQxOExccSRU0i7UHktecSs0DVC4lZG9AOzBe+Q36cym5Z1di6JQ +kHl69q3zBdV3KTW+H1pdmnZlebYGz8paG9iQ/wS9gpnSeEyx0Enyi167Bzm0O4A1 +GK0prkLnz/yROHHEfHjsTgMvFwAnf9uaxwWgE1d1RitIWgJpAnp1DZ5O0uVlsPPm +XAhuBJ32mU8S5BezPTuJJICwBlLYECGb1Y65Cil4OALU7T7sbUqfLCuaRKxuPtcU +VnJ6/qiyPygvKZWhV6Od0Yxlyed1kftMJyYoL8kPHfeHJ+vIyt0s7cropfiwXoka +1iJB5nKyt/eqMnPQ9aRpqkm9ABS/r7AauMA/9RALudQRHBdWIzfIg0Mlqb52yyTI +IgQJHNGNX1T3z1XgZhI+Vi8SLFFSh8x9FeUZC6YJu0VXXj5iz+eZmk/nYjUt4Mtc +pVsVYIB7oIDIbImODm8ggsgrIzqxOzQVP1zsCGek5U6QFc9GYrQ+Wv3/fG8hfkDn +xXLww0OGaEQxfodm8cLFZ5b8JaG3+Yxfe7JkNclwvRimvlAjqIiW5OK0vvfHco+Y +gANhQrlMnTx//IdZssaxvYytSHpPZTYw+qPEjbBJOLpoLrz8ZafN1uekpAqQjffI +AOqW9SdIzq/kSHgl0bzWbPJPw86XzzftewjKNbkCDQRTmyS2ARAAxSSdQi+WpPQZ +fOflkx9sYJa0cWzLl2w++FQnZ1Pn5F09D/kPMNh4qOsyvXWlekaV/SseDZtVziHJ +Km6V8TBG3flmFlC3DWQfNNFwn5+pWSB8WHG4bTA5RyYEEYfpbekMtdoWW/Ro8Kmh +41nuxZDSuBJhDeFIp0ccnN2Lp1o6XfIeDYPegyEPSSZqrudfqLrSZhStDlJgXjea +JjW6UP6txPtYaaila9/Hn6vF87AQ5bR2dEWB/xRJzgNwRiax7KSU0xca6xAuf+TD +xCjZ5pp2JwdCjquXLTmUnbIZ9LGV54UZ/MeiG8yVu6pxbiGnXo4Ekbk6xgi1ewLi +vGmz4QRfVklV0dba3Zj0fRozfZ22qUHxCfDM7ad0eBXMFmHiN8hg3IUHTO+UdlX/ +aH3gADFAvSVDv0v8t6dGc6XE9Dr7mGEFnQMHO4zhM1HaS2Nh0TiL2tFLttLbfG5o +QlxCfXX9/nasj3K9qnlEg9G3+4T7lpdPmZRRe1O8cHCI5imVg6cLIiBLPO16e0fK +yHIgYswLdrJFfaHNYM/SWJxHpX795zn+iCwyvZSlLfH9mlegOeVmj9cyhN/VOmS3 +QRhlYXoA2z7WZTNoC6iAIlyIpMTcZr+ntaGVtFOLS6fwdBqDXjmSQu66mDKwU5Ek +fNlbyrpzZMyFCDWEYo4AIR/18aGZBYUAEQEAAYkCHwQYAQIACQUCU5sktgIbDAAK +CRAWVaCraFdigIPQEACcYh8rR19wMZZ/hgYv5so6Y1HcJNARuzmffQKozS/rxqec +0xM3wceL1AIMuGhlXFeGd0wRv/RVzeZjnTGwhN1DnCDy1I66hUTgehONsfVanuP1 +PZKoL38EAxsMzdYgkYH6T9a4wJH/IPt+uuFTFFy3o8TKMvKaJk98+Jsp2X/QuNxh +qpcIGaVbtQ1bn7m+k5Qe/fz+bFuUeXPivafLLlGc6KbdgMvSW9EVMO7yBy/2JE15 +ZJgl7lXKLQ31VQPAHT3an5IV2C/ie12eEqZWlnCiHV/wT+zhOkSpWdrheWfBT+ac +hR4jDH80AS3F8jo3byQATJb3RoCYUCVc3u1ouhNZa5yLgYZ/iZkpk5gKjxHPudFb +DdWjbGflN9k17VCf4Z9yAb9QMqHzHwIGXrb7ryFcuROMCLLVUp07PrTrRxnO9A/4 +xxECi0l/BzNxeU1gK88hEaNjIfviPR/h6Gq6KOcNKZ8rVFdwFpjbvwHMQBWhrqfu +G3KaePvbnObKHXpfIKoAM7X2qfO+IFnLGTPyhFTcrl6vZBTMZTfZiC1XDQLuGUnd +sckuXINIU3DFWzZGr0QrqkuE/jyr7FXeUJj9B7cLo+s/TXo+RaVfi3kOc9BoxIvy +/qiNGs/TKy2/Ujqp/affmIMoMXSozKmga81JSwkADO1JMgUy6dApXz9kP4EE3g== +=CLGF +-----END PGP PUBLIC KEY BLOCK----- diff --git a/test/server_side_scripting/ext_table_preview/data/bad.xls b/test/server_side_scripting/ext_table_preview/data/bad.xls new file mode 100644 index 0000000000000000000000000000000000000000..1f31bf2754258e3d07f88fd1e6bdee4d7b11bee1 Binary files /dev/null and b/test/server_side_scripting/ext_table_preview/data/bad.xls differ diff --git a/test/server_side_scripting/ext_table_preview/data/bad.xlsx b/test/server_side_scripting/ext_table_preview/data/bad.xlsx new file mode 100644 index 0000000000000000000000000000000000000000..1f31bf2754258e3d07f88fd1e6bdee4d7b11bee1 Binary files /dev/null and b/test/server_side_scripting/ext_table_preview/data/bad.xlsx differ diff --git a/test/server_side_scripting/ext_table_preview/data/server_error.csv b/test/server_side_scripting/ext_table_preview/data/server_error.csv new file mode 100644 index 0000000000000000000000000000000000000000..3e770df012f65d73ce4721a5f65d7e3f39959519 --- /dev/null +++ b/test/server_side_scripting/ext_table_preview/data/server_error.csv @@ -0,0 +1 @@ +Hi, this line contains a unicode backspace. This causes a server error, when pandas_table_preview.py's output is serialized into XML. \ No newline at end of file diff --git a/test/server_side_scripting/ext_table_preview/data/test.csv b/test/server_side_scripting/ext_table_preview/data/test.csv new file mode 100644 index 0000000000000000000000000000000000000000..7c9bfd1354393439f551021cfe340577433ce2aa --- /dev/null +++ b/test/server_side_scripting/ext_table_preview/data/test.csv @@ -0,0 +1,12 @@ +# test header +# two lines +A1,B1,C1,D1,E1,F1,G1,H1,I1,J1,K1,L1,M1,N1,O1,P1,Q1,R1,S1,T1 +A2,B2,C2,D2,E2,F2,G2,H2,I2,J2,K2,L2,M2,N2,O2,P2,Q2,R2,S2,T2 +A3,B3,csvfile,D3,E3,F3,G3,H3,I3,J3,K3,L3,M3,N3,O3,P3,Q3,R3,S3,T3 +A5,B5,C5,D5,E5,F5,G5,H5,I5,J5,K5,L5,M5,N5,O5,P5,Q5,R5,S5,T5 +A6,B6,C6,D6,E6,F6,G6,H6,I6,J6,K6,L6,M6,N6,O6,P6,Q6,R6,S6,T6 +A7,B7,csvfile,D7,E7,F7,G7,H7,I7,J7,K7,L7,M7,N7,O7,P7,Q7,R7,S7,T7 +A8,B8,C8,D8,E8,F8,G8,H8,I8,J8,K8,L8,M8,N8,O8,P8,Q8,R8,S8,T8 +A9,B9,C9,D9,E9,F9,G9,H9,I9,J9,K9,L9,M9,N9,O9,P9,Q9,R9,S9,T9 +A10,B10,C10,D10,E10,F10,G10,H10,I10,J10,K10,L10,M10,N10,O10,P10,Q10,R10,S10,T10 +A11,B11,C11,D11,E11,F11,G11,H11,I11,J11,K11,L11,M11,N11,O11,P11,Q11,R11,S11,T11 diff --git a/test/server_side_scripting/ext_table_preview/data/test.tsv b/test/server_side_scripting/ext_table_preview/data/test.tsv new file mode 100644 index 0000000000000000000000000000000000000000..863f692bf64e7dcabf74703587a93e37adf27e67 --- /dev/null +++ b/test/server_side_scripting/ext_table_preview/data/test.tsv @@ -0,0 +1,12 @@ +# test header +# two lines +A1 B1 C1 D1 E1 F1 G1 H1 I1 J1 K1 L1 M1 N1 O1 P1 Q1 R1 S1 T1 +A2 B2 C2 D2 E2 F2 G2 H2 I2 J2 K2 L2 M2 N2 O2 P2 Q2 R2 S2 T2 +A3 B3 csvfile D3 E3 F3 G3 H3 I3 J3 K3 L3 M3 N3 O3 P3 Q3 R3 S3 T3 +A5 B5 C5 D5 E5 F5 G5 H5 I5 J5 K5 L5 M5 N5 O5 P5 Q5 R5 S5 T5 +A6 B6 C6 D6 E6 F6 G6 H6 I6 J6 K6 L6 M6 N6 O6 P6 Q6 R6 S6 T6 +A7 B7 tsvfile D7 E7 F7 G7 H7 I7 J7 K7 L7 M7 N7 O7 P7 Q7 R7 S7 T7 +A8 B8 C8 D8 E8 F8 G8 H8 I8 J8 K8 L8 M8 N8 O8 P8 Q8 R8 S8 T8 +A9 B9 C9 D9 E9 F9 G9 H9 I9 J9 K9 L9 M9 N9 O9 P9 Q9 R9 S9 T9 +A10 B10 C10 D10 E10 F10 G10 H10 I10 J10 K10 L10 M10 N10 O10 P10 Q10 R10 S10 T10 +A11 B11 C11 D11 E11 F11 G11 H11 I11 J11 K11 L11 M11 N11 O11 P11 Q11 R11 S11 T11 diff --git a/test/server_side_scripting/ext_table_preview/data/test.xls b/test/server_side_scripting/ext_table_preview/data/test.xls new file mode 100644 index 0000000000000000000000000000000000000000..a355756b9ab72f9035246c5303800a2076d9bfc0 Binary files /dev/null and b/test/server_side_scripting/ext_table_preview/data/test.xls differ diff --git a/test/server_side_scripting/ext_table_preview/data/test.xlsx b/test/server_side_scripting/ext_table_preview/data/test.xlsx new file mode 100644 index 0000000000000000000000000000000000000000..bc291f1aa86cd6d550320f07a7ce69cf813b8116 Binary files /dev/null and b/test/server_side_scripting/ext_table_preview/data/test.xlsx differ diff --git a/test/server_side_scripting/ext_table_preview/data/xss_attack.csv b/test/server_side_scripting/ext_table_preview/data/xss_attack.csv new file mode 100644 index 0000000000000000000000000000000000000000..e7d43505aef42c397f1859805bc87aab8b6da1a2 --- /dev/null +++ b/test/server_side_scripting/ext_table_preview/data/xss_attack.csv @@ -0,0 +1,8 @@ +# as it seems all these characters are escaped correctly. +"","%3C","<","<","<","<","<","<","<","<" +"<","<","<","<","<","<","<","<","<","<" +"<","<","<","<","<","<","<","<","<","<" +"<","<","<","<","<","<","<","<","<","<" +"<","<","<","<","<","<","<","<","<","<" +"<","<","<","<","<","<","<","<","<","<" +"<","<","<","<","<","<","\x3c","\x3C","\u003c","\u003C" diff --git a/test/server_side_scripting/ext_table_preview/pandas_table_preview.py b/test/server_side_scripting/ext_table_preview/pandas_table_preview.py new file mode 120000 index 0000000000000000000000000000000000000000..f24b3901ce8610fd02fc28468b747b7171834307 --- /dev/null +++ b/test/server_side_scripting/ext_table_preview/pandas_table_preview.py @@ -0,0 +1 @@ +../../../src/server_side_scripting/ext_table_preview/pandas_table_preview.py \ No newline at end of file diff --git a/test/server_side_scripting/ext_table_preview/requirements.txt b/test/server_side_scripting/ext_table_preview/requirements.txt new file mode 100644 index 0000000000000000000000000000000000000000..4628529ba9dce50a08d574e21d3b4a71b50af2b1 --- /dev/null +++ b/test/server_side_scripting/ext_table_preview/requirements.txt @@ -0,0 +1,3 @@ +caosdb +caosadvancedtools +pandas diff --git a/test/server_side_scripting/ext_table_preview/test_pandas_table_preview.py b/test/server_side_scripting/ext_table_preview/test_pandas_table_preview.py new file mode 100644 index 0000000000000000000000000000000000000000..c7c0cd3bc22c62ad4f1a214d4ed777718cdbf74a --- /dev/null +++ b/test/server_side_scripting/ext_table_preview/test_pandas_table_preview.py @@ -0,0 +1,75 @@ +#!/usr/bin/env python3 +# encoding: utf-8 +# +# ** header v3.0 +# This file is a part of the CaosDB Project. +# +# Copyright (C) 2020 IndiScale GmbH <info@indiscale.com> +# Copyright (C) 2020 Henrik tom Wörden <h.tomwoerden@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/>. +# +# ** end header +# + +import os +import unittest + +import caosdb as db +from caosdb.common.models import _parse_single_xml_element +from lxml import etree +from pandas_table_preview import (MAXIMUMFILESIZE, create_table_preview, + ending_is_valid, read_file, size_is_ok) + + +class PreviewTest(unittest.TestCase): + def test_file_ending(self): + self.assertFalse(ending_is_valid("/this/is/no/xls.lol")) + self.assertFalse(ending_is_valid("xls.lol")) + self.assertFalse(ending_is_valid("ag.xls.lol")) + assert ending_is_valid("/this/is/a/lol.xls") + assert ending_is_valid("/this/is/a/lol.csv") + assert ending_is_valid("/this/is/a/lol.cSv") + assert ending_is_valid("/this/is/a/lol.CSV") + assert ending_is_valid("lol.CSV") + + def test_file_size(self): + entity_xml = ('<File id="1234" name="SomeFile" ' + 'path="/this/path.tsv" size="{size}"></File>') + small = _parse_single_xml_element( + etree.fromstring(entity_xml.format(size="20000"))) + + assert size_is_ok(small) + large = _parse_single_xml_element( + etree.fromstring(entity_xml.format( + size=str(int(MAXIMUMFILESIZE+1))))) + assert not size_is_ok(large) + + def test_output(self): + files = [os.path.join(os.path.dirname(__file__), "data", f) + for f in ["test.csv", "test.tsv", "test.xls", "test.xlsx"]] + + for fi in files: + table = read_file(fi, ftype="."+fi.split(".")[-1]) + searchkey = fi.split(".")[-1]+"file" + print(table) + assert (table == searchkey).any(axis=None) + + badfiles = [os.path.join(os.path.dirname(__file__), "data", f) + for f in ["bad.xls", "bad.xlsx"]] + + for bfi in badfiles: + print("bfi: ", bfi) + self.assertRaises(ValueError, read_file, + bfi, "."+bfi.split(".")[-1])