diff --git a/.gitignore b/.gitignore index f69db87ad5a5226535559b6965e771d975ded103..d2bd7089a35ed5496464734e2026397e3a802baa 100644 --- a/.gitignore +++ b/.gitignore @@ -4,6 +4,9 @@ .* !/.git* +# backup files +*~ + # extracted libraries /libs/** !/libs/*.zip @@ -26,3 +29,4 @@ xerr.log conf/ext test/ext src/ext +*~ diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index bcbcfcda9df7fa823ecd1454990bb8d6ff28ff79..6817020fcabf4b7551e264709fc89cc487e4f0a0 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -23,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 @@ -73,9 +71,8 @@ trigger_build: 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" @@ -88,6 +85,9 @@ build-testenv: 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 @@ -102,13 +102,13 @@ build-testenv: # stage: deploy # Special job for serving a static website. See https://docs.gitlab.com/ee/ci/yaml/README.html#pages -pages: - tags: [ docker ] +pages_prepare: &pages_prepare + tags: [ cached-dind ] stage: deploy only: - - dev + refs: + - /^release-.*$/i script: - # TODO is this a good location here? - npm install jsdoc - npm install jsdoc-sphinx - echo "Deploying" @@ -117,3 +117,8 @@ pages: artifacts: paths: - public +pages: + <<: *pages_prepare + only: + refs: + - main diff --git a/.gitlab/issue_templates/default.md b/.gitlab/issue_templates/default.md new file mode 100644 index 0000000000000000000000000000000000000000..aa1a65aca363b87aff50280e1a86824009d2098b --- /dev/null +++ b/.gitlab/issue_templates/default.md @@ -0,0 +1,28 @@ +## Summary + +*Please give a short summary of what the issue is.* + +## Expected Behavior + +*What did you expect how the software should behave?* + +## Actual Behavior + +*What did the software actually do?* + +## Steps to Reproduce the Problem + +*Please describe, step by step, how others can reproduce the problem. Please try these steps for yourself on a clean system.* + +1. +2. +3. + +## Specifications + +- Version: *Which version of this software?* +- Platform: *Which operating system, which other relevant software versions?* + +## Possible fixes + +*Do you have ideas how the issue can be resolved?* diff --git a/CHANGELOG.md b/CHANGELOG.md index 358beda6169249d903268d310159d8fb0e8f60f6..f0f46cddce0ffcd2450c3ec8bcc5b6693b2161c5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,10 +1,206 @@ # Changelog All notable changes to this project will be documented in this file. -The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), +The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). -## [Unreleased] +## [Unpublished] + +### Added (for new features, dependecies etc.) + +### Changed (for changes in existing functionality) + +### Deprecated (for soon-to-be removed features) + +### Removed (for now removed features) + +### Fixed (for any bug fixes) + +### Security (in case of vulnerabilities) + +### Documentation (for notable additions or changes of the documentation) + +## [0.4.2] - 2021-12-06 + +### Added (for new features, dependecies etc.) + +* Documentation link in standard footer +* Build properties for footer elements: + * `BUILD_FOOTER_CONTACT_HREF` + * `BUILD_FOOTER_IMPRINT_HREF` + * `BUILD_FOOTER_DOCS_HREF` + * `BUILD_FOOTER_SOURCES_HREF` + * `BUILD_FOOTER_LICENCE_HREF` + See `build.properties.d/00_default.properties` for more information +* Start editing an entity/creating a new record directly by adding an `#edit` or + `#new_record` URI fragment, respectively. +* Optional WYSIWYG editor with markdown output for text properties. Controled by + the `BUILD_MODULE_EXT_EDITMODE_WYSIWYG_TEXT` build variable which is set do + `DISABLED` by default. + - Added button to version history panel that allows restoring old versions + +### Changed (for changes in existing functionality) + +* Default footer elements contain invalid links for imprint, contact, and data-policy now + +### Deprecated (for soon-to-be removed features) + +### Removed (for now removed features) + +* Build property `BUILD_CUSTOM_IMPRINT`. Please use BUILD_FOOTER_IMPRINT_HREF + and link a document instead. You can always put a html page to + `src/ext/html/` and link to that. + +### Fixed (for any bug fixes) +- #156 +- #251 + +### Security (in case of vulnerabilities) + +### Documentation (for notable additions or changes of the documentation) + + +## [0.4.1] - 2021-11-04 + +### Added (for new features, dependecies etc.) + +* `form_panel` module for conveniently creating a panel for web forms. +* `restore_old_version` function to base functionality (caosdb.js) +* buttons to the version history modal that allow restoring older versions + +### Changed (for changes in existing functionality) + +* Default footer elements contain invalid links for imprint, contact, and data-policy now + +### Deprecated (for soon-to-be removed features) + +### Removed (for now removed features) + +* Build property `BUILD_CUSTOM_IMPRINT`. Please use BUILD_FOOTER_IMPRINT_HREF + and link a document instead. You can always put a html page to + `src/ext/html/` and link to that. + +### Fixed (for any bug fixes) + +* Auto-completion and edit_mode can handle entity names with empty spaces better now +* [#251](https://gitlab.indiscale.com/caosdb/src/caosdb-webui/-/issues/251) - Data loss when editing Entities with URL-like properties + +### Security (in case of vulnerabilities) + +### Documentation (for notable additions or changes of the documentation) + +## [0.4.0] - 2021-10-28 + +### Added (for new features, dependecies etc.) + +* Module `ext_qrcode` which generates a QR Code for an entity (pointing to the + the head or the exact version). +* Optional functionality to bookmark all query results. Note that too many + bookmarks will result in the URI being too lang and bookmarks will have to be + cleared manually. + +### Changed (for changes in existing functionality) + +### Deprecated (for soon-to-be removed features) + +### Removed (for now removed features) + +* `getEntityId`, a former duplicate of `getEntityID` which must be used instead. + +### Fixed (for any bug fixes) + +### Security (in case of vulnerabilities) + +### Documentation (for notable additions or changes of the documentation) + +## [v0.4.0-rc1] - 2021-06-16 + +### Added (for new features, dependecies etc.) + +- `ext_applicable` module for building fancy features which append + functionality to entities (EXPERIMENTAL). +- `ext_cosmetics` module which converts http(s) uris in property values into + clickable links (with tests) +- 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 +- Documentation on how to customize reference resolving + +### Changed (for changes in existing functionality) + +- Updated from bootstrap 3 to bootstrap 5. This is a major change which will + possibly break existing custom implementations (e.g. a custom welcome page) + since a lot of classes where renamed by bootstrap an other classes have been + dropped entirely (e.g. "jumbotron"). Please have a look at + * https://getbootstrap.com/docs/5.0/migration/ + * https://getbootstrap.com/docs/4.6/migration/ +- Moved the resolving of references to Person Records to separate + example which can be disabled + +### Deprecated (for soon-to-be removed features) + +- css-class `.caosdb-property-text-value`. Please use + `.caosdb-f-property-text-value` or `.caosdb-v-property-text-value` instead. + +### Removed (for now removed features) + +* `#subnav` element from navbar which was previously used for spacing +* `caosdb.form.ready` event + +### Fixed + +- #212 - form_elements: Drop-down menu shows wrong value after clicking "None" +- #202 - Make filter fields in edit mode toolbox visible +- #117 - Reload data model after adding an RT or a Property +- #214 - Paging panel is hidden. +- Displaying issues with long lists in property values +- An issue whereby a grey container would appear above the map when + changing the map view. +- #200 - Re-enabled the file-upload button + +### Security (in case of vulnerabilities) + +## [v0.3.1] - 2021-06-15 + +This is the last Bootstrap-3 compatible release. + +### Added (for new features, dependecies etc.) + +* Displaying and interacting with the entity state. +* Change password functionality for users of the internal user source. Disable + with `BUILD_MODULE_USER_MANAGEMENT=DISABLED` and set the user realm with + `BUILD_MODULE_USER_MANAGEMENT_CHANGE_OWN_PASSWORD_REALM=CaosDB`. +* 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. + +### 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) + +* Any bootstrap-3 dependencies. Please prepare upgrading to bootstrap-5 with the next release. + +### 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/` + +### Fixed + +* #156 - Edit mode for Safari 11 +* #160 - Entity preview for Safari 11 +* Several minor cosmetic flaws +* Fixed edit mode for Safari 11. + +### Security (in case of vulnerabilities) + +## [v0.3.0] - 2021-02-10 ### Added (for new features, dependecies etc.) @@ -34,17 +230,20 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - enabled and enhanced autocompletion * Login form is hidden behind another button. -### Deprecated (for soon-to-be removed features) +### 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) @@ -56,7 +255,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Changed (for changes in existing functionality) -### Deprecated (for soon-to-be removed features) +### Deprecated (for soon-to-be removed features) ### Removed (for now removed features) @@ -94,7 +293,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 See the doc string of that method for more information. - added a layout argument to the create_plot function of ext_bottom_line -### Deprecated (for soon-to-be removed features) +### Deprecated (for soon-to-be removed features) * css class `caosdb-property-text-value` is deprecated because different functionality interpreted it differently and most of the uses of this class @@ -184,20 +383,20 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 * updated QUnit test framework to 2.9.2 -### Deprecated (for soon-to-be removed features) +### Deprecated (for soon-to-be removed features) * Image Preview in the FileSystem. The functionality is to be replaced by real thumbnails, which cover also non-image data-formats. The thumbnails resource is part of the new file system API of the CaosDB Server which is currently under development. -### Removed (for now removed features) +### Removed (for now removed features) * Removed non-informative tests for webcaosdb.css * Hard-coded image and video preview in the entity panel. The preview of images and videos is controlled by the `ext_bottom_line` module now. -### Fixed (for any bug fixes) +### Fixed (for any bug fixes) * #95 - Edit Mode removes property values of reference properties when server response for possible reference targets is empty. diff --git a/DEPENDENCIES.md b/DEPENDENCIES.md new file mode 100644 index 0000000000000000000000000000000000000000..f3e1d1706dd8dd9bdbdab16377c2c75ed0299f7f --- /dev/null +++ b/DEPENDENCIES.md @@ -0,0 +1,35 @@ +* CaosDB Server 0.5.x +* Make 4.2.0 + +# Java Script Libraries (included in this repository) +* bootstrap-5.0.1 +* bootstrap-autocomplete-2.3.5 +* bootstrap-icons-1.4.1 +* bootstrap-select-1.14.0-beta2 +* dropzone-5.5.0 +* javascript-state-machine-master +* jquery-3.6.0.min.js +* loglevel-1.6.4 +* qrcode-1.4.4 +* showdown-1.8.6 +* plotly.js-1.52.2 +* UTIF-8205c1f + +## For the map + +* leaflet-1.5.1 +* Leaflet.Coordinates-0.1.5 +* leaflet.latlng-graticule-20191007 +* L.Graticule.js https://github.com/turban/Leaflet.Graticule/blob/e9146fbea59ce1b0ada4ea2a012087f9a1a12473/L.Graticule.js +* proj4js-2.5.0 +* Proj4Leaflet-1.0.1 + +## For CKEditor (WYSIWYG editor in edit mode) + +* we're using a custom-built ckeditor 31.0.0 from + https://ckeditor.com/ckeditor-5/online-builder/ with a customized set of + editor plugins. Please refer to the `package.json` within + `libs/ckeditor...zip` for a full list of said plugins. + +## For testing +* qunit-2.9.2 diff --git a/Makefile b/Makefile index 1f182e7864a2252e4c1263cde7fa4238d31a4269..5cca686319c617006007e4884cfb269796ac7af4 100644 --- a/Makefile +++ b/Makefile @@ -43,7 +43,8 @@ LIBS_DIR = $(abspath libs) TEST_CORE_DIR = $(abspath test/core/) TEST_EXT_DIR = $(abspath test/ext) TEST_SSS_DIR =$(abspath test/server_side_scripting) -LIBS = fonts css/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 js/pako.js js/utif.js +LIBS = fonts css/fonts css/bootstrap.css css/bootstrap-icons.css js/state-machine.js js/jquery.js js/showdown.js js/dropzone.js css/dropzone.css js/loglevel.js js/leaflet.js css/leaflet.css css/images js/leaflet-latlng-graticule.js js/proj4.js js/proj4leaflet.js js/leaflet-coordinates.js css/leaflet-coordinates.css js/leaflet-graticule.js js/bootstrap-select.js css/bootstrap-select.css js/bootstrap-autocomplete.min.js js/plotly.js js/pako.js js/utif.js js/bootstrap.js js/qrcode.js js/ckeditor.js + TEST_LIBS = $(LIBS) js/qunit.js css/qunit.css $(subst $(TEST_CORE_DIR)/,,$(shell find $(TEST_CORE_DIR)/)) @@ -52,9 +53,9 @@ LIBS_SUBDIRS = $(addprefix $(LIBS_DIR)/, js css fonts) ALL: install -install: clean install-sss 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)) merge_js build_properties merge_xsl -test: clean install-sss 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)) merge_js build_properties merge_xsl @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; \ @@ -67,8 +68,12 @@ test: clean install-sss cp-src cp-ext cp-ext-test cp-conf $(addprefix $(PUBLIC_D merge_xsl: misc/merge_xsl.sh +merge_js: + for f in ${BUILDFILELIST} ; do source "$$f" ; done ; \ + JS_DIST_BUNDLE=$${JS_DIST_BUNDLE} AUTO_DISCOVER_MODULES=$$AUTO_DISCOVER_MODULES misc/merge_js.sh $${MODULE_DEPENDENCIES[*]} + EXCLUDE_EXPR = %~ %.backup -BUILDFILELIST = $(filter-out $(EXCLUDE_EXPR),$(wildcard build.properties.d/*)) +BUILDFILELIST = $(sort $(filter-out $(EXCLUDE_EXPR),$(wildcard build.properties.d/*))) build_properties: @set -a -e ; \ pushd build.properties.files ; \ @@ -91,24 +96,24 @@ build_properties: @ln -s $(PUBLIC_DIR) $(PUBLIC_DIR)/$(BUILD_NUMBER) @ln -s $(PUBLIC_DIR) $(PUBLIC_DIR)/webinterface -PORT = 8000 +TEST_PORT ?= 8000 TIMEOUT = 60 run-test-server: test - $(MISC_DIR)/unit_test_http_server.py $(PORT) $(TIMEOUT) False $(PUBLIC_DIR) + $(MISC_DIR)/unit_test_http_server.py $(TEST_PORT) $(TIMEOUT) False $(PUBLIC_DIR) keep-test-server: test - $(MISC_DIR)/unit_test_http_server.py $(PORT) -1 True $(PUBLIC_DIR) + $(MISC_DIR)/unit_test_http_server.py $(TEST_PORT) -1 True $(PUBLIC_DIR) run-qunit: test $(foreach exec, firefox Xvfb xwd,\ $(if $(shell which $(exec)),echo "found $(exec)",$(error "No $(exec) in PATH"))) # start server in background - $(MISC_DIR)/unit_test_http_server.py $(PORT) $(TIMEOUT) False $(PUBLIC_DIR) & + $(MISC_DIR)/unit_test_http_server.py $(TEST_PORT) $(TIMEOUT) False $(PUBLIC_DIR) & # start firefox with virtual xserver Xvfb :1 -screen 0 1024x768x24 & - DISPLAY=:1 firefox "http://localhost:$(PORT)/?hidepassed" & + DISPLAY=:1 firefox "http://localhost:$(TEST_PORT)/?hidepassed" & while [ 1 -eq 1 ]; do \ sleep 5 ; \ @@ -171,6 +176,9 @@ cp-ext: for f in $(wildcard $(SRC_EXT_DIR)/xsl/*) ; do \ echo "y" | $(CMD_COPY_EXT_FILES) "$$(realpath "$$f")" $(PUBLIC_DIR)/xsl/ ; \ done + for f in $(wildcard $(SRC_EXT_DIR)/include/*) ; do \ + echo "y" | $(CMD_COPY_EXT_FILES) "$$(realpath "$$f")" "$(PUBLIC_DIR)/$$(basename "$$f")" ; \ + done cp-ext-test: for f in $(wildcard $(TEST_EXT_DIR)/js/*) ; do \ @@ -211,22 +219,25 @@ $(PUBLIC_DIR)/%: $(TEST_EXT_DIR)/% cp -r $< $@ $(LIBS_DIR)/fonts: unzip - ln -s $(LIBS_DIR)/bootstrap-3.4.1-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.4.1-dist/js/bootstrap.min.js $@ + ln -s $(LIBS_DIR)/bootstrap-5.0.1-dist/js/bootstrap.bundle.min.js $@ $(LIBS_DIR)/css/bootstrap.css: unzip $(LIBS_DIR)/css - ln -s $(LIBS_DIR)/bootstrap-3.4.1-dist/css/bootstrap.min.css $@ + ln -s $(LIBS_DIR)/bootstrap-5.0.1-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.5.1/jquery-3.5.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 $@ @@ -261,6 +272,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 $@ @@ -288,21 +302,33 @@ $(LIBS_DIR)/js/pako.js: unzip $(LIBS_DIR)/js $(LIBS_DIR)/js/utif.js: unzip $(LIBS_DIR)/js ln -s $(LIBS_DIR)/UTIF-8205c1f/UTIF.js $@ +$(LIBS_DIR)/js/qrcode.js: unzip $(LIBS_DIR)/js + ln -s $(LIBS_DIR)/qrcode-1.4.4/qrcode.min.js $@ + +$(LIBS_DIR)/js/ckeditor.js: unzip $(LIBS_DIR)/js + ln -s $(LIBS_DIR)/ckeditor5-31.0.0-k356w86hp13l/build/ckeditor.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 ?= pylint diff --git a/README.md b/README.md index 3f144a30731dab4f425f3f8978248f730a4c392c..dca645c4244573ae31c7e0072208b53a47cec1d2 100644 --- a/README.md +++ b/README.md @@ -1,27 +1,49 @@ -<!--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** and join the + CaosDB community on + [#caosdb:matrix.org](https://matrix.to/#/!unwwlTfOznjEnMMXxf:matrix.org). + +## 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/RELEASE_GUIDELINES.md b/RELEASE_GUIDELINES.md index ce69c0fc24e2ea754589ffe4dd4b8bf2a2fadfaf..9dd648c235258b052332212bd906525e87863629 100644 --- a/RELEASE_GUIDELINES.md +++ b/RELEASE_GUIDELINES.md @@ -1,4 +1,4 @@ -# Release Guidelines for the CaosDB MySQL Backend +# Release Guidelines for the CaosDB Web Interface This document specifies release guidelines in addition to the generel release guidelines of the CaosDB Project @@ -8,7 +8,7 @@ guidelines of the CaosDB Project * All tests are passing. * FEATURES.md is up-to-date and a public API is being declared in that document. -* CHANGELOG.md is up-to-date. +* CHANGELOG.md is up-to-date (insert version number and remove unpublished) * DEPENDENCIES.md is up-to-date. ## Steps @@ -18,10 +18,12 @@ 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. + +7. Prepare CHANGELOG for next release cycle. diff --git a/build.properties.d/00_default.properties b/build.properties.d/00_default.properties index cb0a89ce6d5cf991de67b9063caaf4349e34b59d..535a6c846a5c39c48937dee43f087501f43285ae 100644 --- a/build.properties.d/00_default.properties +++ b/build.properties.d/00_default.properties @@ -42,7 +42,6 @@ # Modules enabled/disabled by default ############################################################################## 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 @@ -50,6 +49,18 @@ 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_EXT_ADD_QUERY_TO_BOOKMARKS=DISABLED +BUILD_MODULE_EXT_ANNOTATION=ENABLED +BUILD_MODULE_EXT_COSMETICS_LINKIFY=DISABLED +BUILD_MODULE_EXT_QRCODE=ENABLED + +BUILD_MODULE_USER_MANAGEMENT=ENABLED +BUILD_MODULE_USER_MANAGEMENT_CHANGE_OWN_PASSWORD_REALM=CaosDB + +BUILD_MODULE_EXT_RESOLVE_REFERENCES=ENABLED +BUILD_EXT_REFERENCES_CUSTOM_RESOLVER=person_reference + +BUILD_MODULE_EXT_EDITMODE_WYSIWYG_TEXT=DISABLED ############################################################################## # Navbar properties @@ -68,7 +79,24 @@ BUILD_FAVICON=pics/caosdb_logo_42.png ############################################################################## # Link to the data policy statement document. -BUILD_FOOTER_DATA_POLICY_HREF=https://missing-domain.com/missing-page +BUILD_FOOTER_DATA_POLICY_HREF="Please configure me!" + +# Contact mail or link to contact information for the responsible administrator of this caosdb server instance. +BUILD_FOOTER_CONTACT_HREF="Please configure me!" +#BUILD_FOOTER_CONTACT_HREF=mailto:info@indiscale.com + +# Link to imprint for this caosdb server instance. +BUILD_FOOTER_IMPRINT_HREF="Please configure me!" +#BUILD_FOOTER_IMPRINT_HREF=https://www.indiscale.com/imprint/ + +# Link to docs (should rarely be changed (maybe for additional docs)) +BUILD_FOOTER_DOCS_HREF="https://docs.indiscale.com" + +# Link to sources (should almost never be changed (maybe for additional sources)) +BUILD_FOOTER_SOURCES_HREF="https://gitlab.com/caosdb" + +# Link to license (should almost never be changed (maybe for other languages)) +BUILD_FOOTER_LICENCE_HREF="https://www.gnu.org/licenses/agpl-3.0.en.html" # Custom footer elements can be placed here (will be placed inside a <div> # element). @@ -81,9 +109,72 @@ BUILD_FOOTER_CUSTOM_ELEMENT_ONE= # ${BUILD_NUMBER}. BUILD_FOOTER_CUSTOM_ELEMENT_TWO= # BUILD_FOOTER_CUSTOM_ELEMENT_TWO=$(cat footer_element_2.html) -BUILD_CUSTOM_IMPRINT='<p> Put an imprint note here </p>' ############################################################################## # ext_trigger_crawler_form properties ############################################################################## BUILD_MODULE_EXT_TRIGGER_CRAWLER_FORM_TOOLBOX="Tools" + +############################################################################## +# Build a dist file containing all JS code from the files in the +# MODULE_DEPENDENCIES array. +############################################################################## +JS_DIST_BUNDLE=TRUE +############################################################################## +# TRUE means that all javascript sources which are no mentioned in the +# MODULE_DEPENDENCIES array will be added in no particular order into the +# build. If you need to guarantee a specific order (in which the are loaded or +# appear in the dit file) you need to add them to the MODULE_DEPENDENCIES. +############################################################################## +AUTO_DISCOVER_MODULES=TRUE +############################################################################## +# Module dependencies +# Override or extend to specify the order of js files in the resulting +# bundled js file +############################################################################## +MODULE_DEPENDENCIES=( + jquery.js + bootstrap.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_applicable.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 + ext_cosmetics.js + qrcode.js + ext_qrcode.js + form_panel.js + ckeditor.js + ext_editmode_wysiwyg_text.js +) diff --git a/doc/QueryShortcuts/doc.pdf b/doc/QueryShortcuts/doc.pdf deleted file mode 100644 index 0f7e46bb78b4f8c6cf7a30216cb9f507be747ad4..0000000000000000000000000000000000000000 Binary files a/doc/QueryShortcuts/doc.pdf and /dev/null differ diff --git a/doc/QueryShortcuts/doc.tex b/doc/QueryShortcuts/doc.tex deleted file mode 100644 index 4cd78391d1cda808dc9c332328d3a984ae5926d8..0000000000000000000000000000000000000000 --- a/doc/QueryShortcuts/doc.tex +++ /dev/null @@ -1,192 +0,0 @@ -\documentclass{article} -% General document formatting -\usepackage[margin=0.7in]{geometry} -\usepackage[parfill]{parskip} -\usepackage[utf8]{inputenc} -\usepackage{graphicx} - -% Related to math -\usepackage{amsmath,amssymb,amsfonts,amsthm} -\title{Documentation Query Shortcuts} - -\begin{document} -\maketitle -\section{Introduction}\label{introduction} - -The WebUI supports the creation of query shortcuts which appear below -the normal query input field. These shortcuts facilitate looking for -data as query strings which are used frequently. They can be stored and -reused. - -\begin{figure}[h] -\centering -\includegraphics[width=.8\textwidth]{shortcut_toolbox.png} -\caption{The Shortcuts in the Query Panel; Note the Toolbox for in the top -right} -\end{figure} - -There are two ways to integrate query templates into the WebUI: - -\begin{itemize} -\item - Global shortcuts are integrated by the webmaster only. They are - defined and stored in a\\ - \texttt{./conf/ext/json/globale\_query\_shortcuts.json} in the root - directory of the webui. -\item - User-defined templates can be defined by users and are only visible - for the user who created them. In this sense, user-defined shortcuts - are also private, whereas global shortcuts are always publicly - visible. -\end{itemize} - -\section{User-defined Query Shortcuts}\label{user-defined-query-shortcuts} - -\subsection{Create a New Shortcut}\label{create-a-new-shortcut} - -New Query Shortcuts can be generated by any authenticated user with -sufficient write permissions. - -In the web interface, click \texttt{Query}. In the \texttt{Shortcuts} -section, click the wrench (on the right side). - -In the drop-down menu, click \texttt{Create}. - -It now opens a form with two input fields, \texttt{Description} and -\texttt{Query}. -\begin{figure}[h] -\centering -\includegraphics[width=.6\textwidth]{create_shortcut.png} -\caption{The view to create a new shortcut} -\end{figure} - -See \ref{basic-shortcut} and -\ref{advanced-shortcut} for further -explanation of the components of a Query Shortcut. - -Edit the fields and click \texttt{Submit} for the creation of the new -shortcut or click \texttt{Cancel} to cancel the process. - -The new shortcut is shown in the shortcuts section. -\begin{figure}[h] -\centering -\includegraphics[width=.6\textwidth]{create_success.png} -\caption{The view when creation was successful} -\end{figure} - -\subsection{Change an Existing Shortcut}\label{change-an-existing-shortcut} - -Existing Query Shortcuts which are visible in your shortcuts section can -be edited directly in the shortcuts section. - -In the web interface, click \texttt{Query}. In the \texttt{Shortcuts} -section, click the wrench (on the right side). - -In the drop-down menu, click \texttt{Edit}. - -\begin{figure}[h] -\centering -\includegraphics[width=.6\textwidth]{choose_edit.png} -\caption{Choosing which shortcut to edit} -\end{figure} -Every editable shortcut (note: global shortcuts are not editable in the -webinterface at all) will receive a new button \texttt{Edit} - -Click \texttt{Edit} of the shortcut that is to be changed. - -It now opens a form with two input fields, \texttt{Description} and -\texttt{Query}, pre-filled. - -See \ref{basic-shortcut} and -\ref{advanced-shortcut} for further -explanation of the components of a Query Shortcut. - -Edit the fields and click \texttt{Submit} for the creation of the new -shortcut or click \texttt{Cancel} to cancel the process. - -The updated shortcut is shown in the shortcuts section. - -See the - -\subsection{Delete an Existing Shortcut}\label{delete-an-existing-shortcut} - -Existing Query Shortcut which are visible in your shortcuts section can -be edited directly in the shortcuts section. - -In the web interface, click \texttt{Query}. In the \texttt{Shortcuts} -section, click the wrench (on the right side). - -In the drop-down menu, click \texttt{Delete}. -\begin{figure}[h] -\centering -\includegraphics[width=.6\textwidth]{delete_shortcuts.png} -\caption{Choosing which shortcuts to delete} -\end{figure} - -Every user-defined shortcut (note: global shortcuts are not deletable in -the webinterface at all) will receive checkbox and \texttt{Delete} and -\texttt{Cancel} buttons appear at the bottom of the shortcuts section. - -Check all shortcuts which are to be deleted and click \texttt{Delete} or -click \texttt{Cancel} to cancel the deletion. - -All deleted shortcuts are marked as deleted afterwards and will not -appear again in the shortcuts section after reload. - -\subsection{Basic Shortcut}\label{basic-shortcut} - -The \texttt{Description} is a verbose definition of the query, -e.g.~``Search for experiments and return a table.''. It will be the text -that is visible in the shortcuts section. - -The \texttt{Query} is the query that will be executed with the shortcut. -It adheres to the definition of the CaosDB Query Language (CQL). - -The corresponding query of our example is -\texttt{SELECT\ date,\ name\ FROM\ Experiment}. - -\subsection{Advanced Shortcut}\label{advanced-shortcut} - -The basic shortcut does not allow for any parameterization. It is just a -plain string or like a bookmark. - -Advanced shortcuts use a special syntax, where text placeholders are -used to define parameters of the shortcut. The parameters can be set by -the user at the time of the execution. An example can best illustrate -what that means: - -Suppose you want to search for experiments by their year. The query for -that would be -\texttt{SELECT\ date,\ name\ FROM\ Experiment\ WITH\ date\ IN\ 2018}. - -Now, the actual year in the query can be made editable by replacing the -year \texttt{2018} with \texttt{\{year\}}. - -The \texttt{Description} now must also contain this placeholder -\texttt{\{year\}}, e.g.~``Search for experiements conducted in year -\{year\}''. When the shortcut is displayed in the shortcuts section -below the query input field, the placeholder is replaced by a text input -field and the user can insert a year and execute the shortcut with the -year being inserted into the query. - -\subsubsection{Placeholders}\label{placeholders} - -The placeholders have simple rules. A placeholder always starts and ends -with curly brackets, like in the example \texttt{\{year\}}. The text -inside the brackets (the placeholder's \emph{id}) may contain any -combination of alphanumeric signs (0-9,a-z,A-Z). The use of special -characters like colons, commas or the like is discouraged. They are -reserved for future extensions of the placeholders. Apart from that, you -are free to choose any placeholder \emph{id} that seems suitable for -you. - -Both components of the query shortcut (description and query) must -contain the same set of placeholders, otherwise the query shortcuts -might not work as intended. If there is a \texttt{\{year\}} in the -query, there must be a \texttt{\{year\}} in the description. - -Each placeholder \emph{id} must occur only once in both components -- if -you need to use two years in your shortcut you have to use -\texttt{\{year1\}} and \texttt{\{year2\}} or any other combinations of -placeholder \emph{ids}. -\end{document} diff --git a/libs/bootstrap-3.4.1-dist.zip b/libs/bootstrap-3.4.1-dist.zip deleted file mode 100644 index 9002b8521706bc582546f41635da3437edf20c3c..0000000000000000000000000000000000000000 Binary files a/libs/bootstrap-3.4.1-dist.zip and /dev/null differ diff --git a/libs/bootstrap-5.0.1-dist.zip b/libs/bootstrap-5.0.1-dist.zip new file mode 100644 index 0000000000000000000000000000000000000000..196a8212a471170f120de352455dbcf9f3466639 Binary files /dev/null and b/libs/bootstrap-5.0.1-dist.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/ckeditor5-31.0.0-k356w86hp13l.zip b/libs/ckeditor5-31.0.0-k356w86hp13l.zip new file mode 100644 index 0000000000000000000000000000000000000000..71523482a2bdfa7c0a9776eeed7aa7be010e053b Binary files /dev/null and b/libs/ckeditor5-31.0.0-k356w86hp13l.zip differ diff --git a/libs/jquery-3.5.1.zip b/libs/jquery-3.5.1.zip deleted file mode 100644 index c554bbf416786da9c18bef62061cba932646f996..0000000000000000000000000000000000000000 Binary files a/libs/jquery-3.5.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/qrcode-1.4.4.zip b/libs/qrcode-1.4.4.zip new file mode 100644 index 0000000000000000000000000000000000000000..ed1e0f854985cfcec8cd4f419fe27c195f39c22c Binary files /dev/null and b/libs/qrcode-1.4.4.zip differ diff --git a/misc/entity_state_test_data.py b/misc/entity_state_test_data.py new file mode 100755 index 0000000000000000000000000000000000000000..400b73749011834fb5390be2e00eebdde1a91062 --- /dev/null +++ b/misc/entity_state_test_data.py @@ -0,0 +1,210 @@ +#!/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", description="Edit this entity. The changes are not publicly available until this entity will have been reviewed and published.").add_parent( + "Transition").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/ext_cosmetics_test_data.py b/misc/ext_cosmetics_test_data.py new file mode 100755 index 0000000000000000000000000000000000000000..786f46c2d6bcd1d55488a15e2c4f50085f331950 --- /dev/null +++ b/misc/ext_cosmetics_test_data.py @@ -0,0 +1,67 @@ +#!/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 caosdb as db + +# clean +old = db.execute_query("FIND Test*") +if len(old): + old.delete() + +# data model +datamodel = db.Container() +datamodel.extend([ + db.Property("TestProp", datatype=db.TEXT), + db.RecordType("TestRecordType"), +]) + +datamodel.insert() + + +# test data +testdata = db.Container() + +test_cases = [ + "no link", + "https://example.com", + "https://example.com and http://example.com", + "this is text https://example.com", + "this is text https://example.com and this as well", + "this is text https://example.com and another linke https://example.com", + "this is text https://example.com and another linke https://example.com and more text", + ("this is a lot of text with links in it 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. Excepteur sint occaecat cupidatat non " + "proident, sunt in culpa qui officia deserunt mollit anim id est " + "laborum.https://example.com and another linke https://example.com and " + "more text and here comes a very long link: " + "https://example.com/this/has/certainly/more/than/40/characters/just/count/if/you/dont/believe/it.html"), +] +for test_case in test_cases: + testdata.append(db.Record().add_parent("TestRecordType").add_property("TestProp", + test_case)) +testdata.insert() 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..506a65866db42c329b4e19c197842f04bdfc4aca --- /dev/null +++ b/misc/merge_js.sh @@ -0,0 +1,127 @@ +#!/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=$@ +PUBLIC_JS_DIR=public/js/ +DIST_BUNDLE=webcaosdb.dist.js +DIST_BUNDLE_TARGET=public/${DIST_BUNDLE} +JSHEADER_TARGET=public/xsl/jsheader.xsl +ALL_SOURCES=() + +_create_jsheader () { + _JS_INCLUDE= + if [ "$JS_DIST_BUNDLE" == "TRUE" ] ; then + _SIZE=$(( $(wc -c ${DIST_BUNDLE_TARGET} | awk '{print $1}')/1024)) + echo "including ${DIST_BUNDLE} (${_SIZE}kB) into ${JSHEADER_TARGET}" + _JS_INCLUDE=" + <xsl:element name=\"script\"> + <xsl:attribute name=\"src\"> + <xsl:value-of select=\"concat(\$basepath,'webinterface/\${BUILD_NUMBER}/${DIST_BUNDLE}')\"/> + </xsl:attribute> + </xsl:element> + " + [[ -f "public/index.html" ]] && sed -i "s|^\(.*JS_INCLUDE.*\)$| <script src=\"${DIST_BUNDLE}\"><\/script>\n\1|g" public/index.html ; + else + _ALL_SOURCES=$@ + echo "${_ALL_SOURCES}" + for _SOURCE in ${_ALL_SOURCES[@]} ; do + _SIZE=$(( $(wc -c ${_SOURCE} | awk '{print $1}')/1024)) + _SOURCE=js/${_SOURCE/${PUBLIC_JS_DIR}/} + echo "including ${_SOURCE} (${_SIZE}kB) into ${JSHEADER_TARGET}" + _JS_INCLUDE="${_JS_INCLUDE} + <xsl:element name=\"script\"> + <xsl:attribute name=\"src\"> + <xsl:value-of select=\"concat(\$basepath,'webinterface/\${BUILD_NUMBER}/${_SOURCE}')\"/> + </xsl:attribute> + </xsl:element> + " + sed -i "s|^\(.*JS_INCLUDE.*\)$| <script src=\"${_SOURCE}\"><\/script>\n\1|g" public/index.html ; + done + fi + + + echo "<?xml version=\"1.0\" encoding=\"UTF-8\"?> +<!-- THIS FILE IS AUTO-GENERATED BY THE merge_js.sh SCRIPT --> +<xsl:stylesheet version=\"1.0\" xmlns:xsl=\"http://www.w3.org/1999/XSL/Transform\"> + <xsl:output method=\"html\"/> + <xsl:template name=\"caosdb-head-js\"> + <script> + window.sessionStorage.caosdbBasePath = \"<xsl:value-of select=\"\$basepath\"/>\"; + </script> + ${_JS_INCLUDE} + </xsl:template> +</xsl:stylesheet>" > ${JSHEADER_TARGET} +} + +function _merge () { + if [ "$JS_DIST_BUNDLE" != "TRUE" ] ; then + return 0 + fi + _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 $DIST_BUNDLE_TARGET || true +touch $DIST_BUNDLE_TARGET + +for _SOURCE in ${CORE_MODULES[@]} ; do + [[ ! " ${ALL_SOURCES[@]} " =~ " ${_SOURCE} " ]] && ALL_SOURCES+=(${PUBLIC_JS_DIR}${_SOURCE}) + _merge "core" "${PUBLIC_JS_DIR}${_SOURCE}" $DIST_BUNDLE_TARGET +done + +if [ "$AUTO_DISCOVER_MODULES" == "TRUE" ] ; then + # load other js files but exclude any subdirectory + for _SOURCE in $(find ${PUBLIC_JS_DIR}* -prune -iname "*.js") ; do + [[ ! " ${ALL_SOURCES[@]} " =~ " ${_SOURCE} " ]] && ALL_SOURCES+=(${_SOURCE}) + _merge "extension" ${_SOURCE} $DIST_BUNDLE_TARGET + done +fi + +# for `make test` +for _SOURCE in $(find ${PUBLIC_JS_DIR} -ipath "${PUBLIC_JS_DIR}modules/*.js") ; do + [[ ! " ${ALL_SOURCES[@]} " =~ " ${_SOURCE} " ]] && ALL_SOURCES+=(${_SOURCE}) + _merge "extension" ${_SOURCE} $DIST_BUNDLE_TARGET +done + +_create_jsheader ${ALL_SOURCES[@]} diff --git a/misc/merge_xsl.sh b/misc/merge_xsl.sh index 07cc6d7ce8dc690eaa58ad8a42e6c883e5eda149..abf95444de1ca550f5cd66ae1f02e2f0587503f0 100755 --- a/misc/merge_xsl.sh +++ b/misc/merge_xsl.sh @@ -30,7 +30,7 @@ SOURCE_DIR=public/ -MERGE="xsl/footer.xsl xsl/filesystem.xsl xsl/entity.xsl xsl/query.xsl xsl/messages.xsl xsl/navbar.xsl xsl/main.xsl xsl/welcome.xsl xsl/common.xsl" +MERGE="xsl/jsheader.xsl xsl/footer.xsl xsl/filesystem.xsl xsl/entity.xsl xsl/query.xsl xsl/messages.xsl xsl/navbar.xsl xsl/main.xsl xsl/welcome.xsl xsl/common.xsl" TARGET=public/webcaosdb.xsl 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/unit_test_http_server.py b/misc/unit_test_http_server.py index 68e6a7434a584a26a242e5891329f3fb6c6d159f..56205df8d34491b865698742f6b24b1679894df8 100755 --- a/misc/unit_test_http_server.py +++ b/misc/unit_test_http_server.py @@ -153,5 +153,5 @@ class UnitTestHTTPServer(HTTPServer): os._exit(self._exit_code)#pylint: disable=protected-access -UnitTestHTTPServer(server_address=('127.0.0.1', int(sys.argv[1])), +UnitTestHTTPServer(server_address=('0.0.0.0', int(sys.argv[1])), timeout=float(sys.argv[2]), ignore_done=(sys.argv[3] == "True")).start() 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/src/core/css/tour.css b/src/core/css/tour.css index 3bb471369b698981f49858b8127652e957a11f8d..d772c79e8e9684690796b8acbd35b2116adf6f9b 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,101 @@ 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; +} + +.caosdb-v-tour-toc-header { + margin-left: 0.75rem; +} + +/* 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 69a700376423a44bcb28a9920f1f3d15ef9a3b90..b76a5fe9ecc9f6d866dc159429ef7401ac3ddd5d 100644 --- a/src/core/css/webcaosdb.css +++ b/src/core/css/webcaosdb.css @@ -22,11 +22,34 @@ */ @CHARSET "UTF-8"; + +main { +} + body { - display: flex; - flex-direction: column; + background-color: lightgrey; +} + +.background { + background-color: white; + 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; @@ -56,17 +79,19 @@ tbody:not(:hover) tr .caosdb-v-entity-version-hint-cur { color: unset; } -#top-navbar>ul>li>a { - margin: 8px 0px; - padding: 6px 12px; - display: inline-block; +.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; - position: relative; top: 3px; } @@ -106,20 +131,12 @@ tbody:not(:hover) tr .caosdb-v-entity-version-hint-cur { 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 { @@ -142,7 +159,8 @@ button.caosdb-v-entity-version-button { /* DEPRECATED css class .caosdb-property-text-value - Use * .caosdb-f-property-single-raw-value or introduce new * .caosdb-v-property-text-value */ -.caosdb-property-text-value { +.caosdb-property-text-value, +.caosdb-v-property-text-value { white-space: pre-line; } @@ -166,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 { @@ -193,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 { @@ -212,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 { @@ -257,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 */ @@ -278,7 +295,7 @@ h5 { } .caosdb-value-list > .btn-group, .caosdb-value-list > ol { - display: inline-block; + display: inline-block; float: none; white-space: nowrap; margin-bottom: 0px; @@ -286,32 +303,37 @@ h5 { } .caosdb-value-list > .list-inline > li { - border-left-width: 0px; + background-color: white; + padding: 0.2rem 0.5rem; + margin: auto 0; + border: 1px solid #212529; + border-left-width: 0px; + font-size: 0.875rem; } .caosdb-value-list > .list-inline > li:first-child { - border-radius: 4px 0px 0px 4px; - border-left-width: 1px; + border-radius: 0.2rem 0px 0px 0.2rem; + border-left-width: 1px; } .caosdb-value-list > .list-inline > li:last-child { - border-radius: 0px 4px 4px 0px; + border-radius: 0px 0.2rem 0.2rem 0px; } /* 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; } @@ -320,16 +342,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; } @@ -359,25 +379,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; } @@ -408,33 +414,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 { @@ -453,13 +448,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 { @@ -468,44 +463,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; @@ -530,7 +491,10 @@ h5 { } .caosdb-logo { + margin-top: auto; + margin-bottom: auto; margin-right: 8px; + max-height: 30px; } .caosdb-comment-action-item { @@ -543,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; } @@ -584,7 +518,14 @@ 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; + border-radius: none; + border-left: none; + border-right: none; + } @keyframes appear { @@ -618,16 +559,215 @@ 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-footer-element { - margin: 1em; +.caosdb-v-edit-mode-highlight { + color: #333; + background-color: #d3fdd3; + border: 2px solid #d3fdd3; +} + +.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; +} + +.caosdb-v-field .bootstrap-select button { + border: 1px solid #ced4da; + background-color: #FFF; +} + +.caosdb-v-field > div { + margin-top: auto; + margin-bottom: auto; +} + +.caosdb-v-property-href-value { + text-decoration: none; + font-weight: 600; + font-size: 0.875rem; + color: #5E6762; +} diff --git a/src/core/css/webcaosdb.less b/src/core/css/webcaosdb.less deleted file mode 100644 index 5ecb7fdf28a4cf3e5f93c78863faaef40e34e385..0000000000000000000000000000000000000000 --- a/src/core/css/webcaosdb.less +++ /dev/null @@ -1,401 +0,0 @@ -/* - * ** header v3.0 - * This file is a part of the CaosDB Project. - * - * Copyright (C) 2018 Research Group Biomedical Physics, - * Max-Planck-Institute for Dynamics and Self-Organization Göttingen - * - * 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 - */ - -@radius_normal: 4px; -@margin_normal: 8px; -@border1: 1px solid #e7e7e7; - -.caosdb-v-show-only-child { - display: none; -} - -.caosdb-v-show-only-child:only-child { - display: initial; -} - -.caosdb-comment-annotation-text h1 { - font-size: 24px; -} - -.caosdb-comment-annotation-text h2 { - font-size: 20px; -} - -.caosdb-comment-annotation-text h3 { - font-size: 18px; -} - -.caosdb-comment-annotation-text h4 { - font-size: 16px; -} - -.caosdb-show-preview-button, .caosdb-hide-preview-button { - position: absolute; - top: 5px; - left: -5px; -} - -.caosdb-entity-heading-attr { - overflow-x: auto; -} - -a.label.caosdb-id-button:hover { - background-color: #5E6762; -} - -.caosdb-entity-preview .caosdb-entity-panel-body { - overflow-y: auto; - max-height: 250px; -} - -.caosdb-preview-carousel-nav > .caosdb-value-list > .btn-group > .btn:first-child { - margin-left: 34px; -} -.caosdb-preview-carousel-nav > .caosdb-value-list > .btn-group > .btn:last-child { - margin-right: 34px; -} - -.caosdb-preview-carousel-nav { - position: relative; -} -.caosdb-query-panel { - padding-top: 20px; - padding-bottom: 20px; -} - -.navbar-default .btn-link:hover { - text-decoration: none; -} - -.navbar-default .btn-link:focus { - text-decoration: none; -} - -.caosdb-property-name { - font-weight: bold; - margin-left: @margin-normal; -} - -.caosdb-square { - position: relative; - overflow: hidden; -} - -.caosdb-square::before { - content: ""; - display: block; - padding-top: 100%; -} - -.caosdb-square-content { - position: absolute; - top: 0px; - right: 0px; - bottom: 0px; - left: 0px; -} - -.caosdb-parent-name { - font-weight: bold; - margin-right: 4px; -} - -/* lists of values */ -/* INLINE (with default scroll-bar) */ -.caosdb-value-list { - overflow-x: auto; -} - -.caosdb-value-list > .btn-group > .btn { - float: none; -} -.caosdb-value-list > .btn-group, -.caosdb-value-list > ol { - display: inline-block; - float: none; - white-space: nowrap; - margin-bottom: 0px; - margin-left: 0px; -} - -.caosdb-value-list > .list-inline > li { - border-left-width: 0px; -} - -.caosdb-value-list > .list-inline > li:first-child { - border-radius: @radius_normal 0px 0px @radius_normal; - border-left-width: 1px; -} - -.caosdb-value-list > .list-inline > li:last-child { - border-radius: 0px @radius_normal @radius_normal 0px; -} - -/* single boolean values */ -.caosdb-boolean-true { - font-weight: bold; - font-size: 90%; - border: 1px solid #bbb; - padding: 2px 8px; - border-radius: 8px; -} - -.caosdb-boolean-false { - .caosdb-boolean-true(); -} - -.caosdb-entity-panel-heading { - border-bottom-left-radius: 3px; - border-bottom-right-radius: 3px; -} - -.caosdb-entity-panel-body { - padding: 0px 15px; -} - -.caosdb-entity-panel-body > :first-child { - margin-top: 15px; -} - -.caosdb-entity-heading-attr-name { - color: #6c6c6c; - font-size: 90%; - margin-right: 0.3em; -} - -.caosdb-subproperty-divider { - margin: 4px 0px; - border-top: 1px solid #dddddd -} - -.caosdb-prop-label { - background-color: #f5f5f5; - font-weight: bold; - padding: 8px 15px; -} - -.caosdb-prop-value { - background-color: #ffffff; - padding: 8px 15px; -} - -.caosdb-prop-list-group .row { - margin-left: 0px; - margin-right: 0px; -} - -.caosdb-prop-list-group>.list-group-item { - padding: 0px -} - -.caosdb-prop-list-group>.list-group-item:first-child .caosdb-prop-label - { - border-top-left-radius: @radius_normal; - border-top-right-radius: @radius_normal; -} - -.caosdb-prop-list-group>.list-group-item:first-child .caosdb-prop-value - { - border-top-right-radius: @radius_normal; -} - -.caosdb-prop-list-group>.list-group-item:last-child .caosdb-prop-label { - border-bottom-left-radius: @radius_normal; -} - -.caosdb-prop-list-group>.list-group-item:last-child .caosdb-prop-value { - border-bottom-left-radius: @radius_normal; - border-bottom-right-radius: @radius_normal; -} - -.caosdb-label-record { - background-color: #F92108; - margin-right: @margin-normal; -} - -.caosdb-label-recordtype { - background-color: #00A32E; - margin-right: @margin-normal; -} - -.caosdb-label-property { - background-color: #496DAB; - margin-right: @margin-normal; -} - -.caosdb-label-file { - background-color: #C92E86; - margin-right: @margin-normal; -} - -.label.caosdb-id-button { - background-color: #4E5752; -} - - -.caosdb-properties-heading { - padding-top: 2px; - padding-bottom: 2px; - background-color: #f5f5f5; - color: #7c7c7c -} - -.caosdb-parents-heading { - .caosdb-properties-heading(); -} - -.caosdb-comments-heading { - .caosdb-properties-heading(); -} - -.caosdb-parent-item { - padding-left: 40px; - text-indent: -40px -} - -.caosdb-unit { - color: #8c8c8c; - font-size: 80%; - 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: @margin-normal; -} - -.caosdb-fs-dir:hover>.glyphicon::before { - content: "\e118"; -} - -.caosdb-fs-file>.glyphicon::before { - content: "\e022"; -} - -.caosdb-fs-file>.glyphicon { - margin-right: @margin-normal; -} - -.caosdb-fs-file:hover>.glyphicon::before { - content: "\e025"; -} - -.caosdb-fs-btn-file { - padding: 0px; - background-color: transparent; - border: 0px; -} - -.caosdb-fs-btn-file:hover .caosdb-label-file { - background-color: #F96EB6; -} - -.caosdb-fs-btn-file:hover .caosdb-label-id { - background-color: #6E8782; -} - -.back-to-top { - cursor: pointer; - position: fixed; - bottom: 10px; - right: 15px; - display: none; - z-index: 1050; -} - -.caosdb-logo { - margin-right: @margin-normal; -} - -.caosdb-comment-action-item { - padding-left: 4px; - padding-right: 4px; - border-left: 1px solid #7c7c7c; -} - -.caosdb-comment-action>.caosdb-comment-action-item:first-child { - border-left: 0px solid #7c7c7c; -} - -.caosdb-pagination { - margin: 5px 15px; -} - -.caosdb-pagination-navbar { - padding-bottom: 5px; - position: fixed; - bottom: 0px; - width: 100%; - border: 0px; - border-top: @border1; - z-index: 1000; - position: fixed; -} - -.caosdb-heading { - color: #5e5e5e; - background-color: #f8f8f8; - border-bottom: @border1; -} - -.caosdb-heading>.container { - padding: 20px 0px; -} - -.spinning { - animation: spin 2s linear infinite; -} - -@keyframes spin { - 0% { transform: rotate(0deg); } - 100% { transform: rotate(360deg); } -} - -.flipped-horiz-icon { - transform: scaleX(-1); -} - -.spacer { - margin-left: 12px; -} - -input[type="file"] { - display: none; -} diff --git a/src/core/js/annotation.js b/src/core/js/annotation.js index 685930da76d73980985210484a60f51690e45a46..42b4217cf2a7c2cd1214d0384ba09a23e2cddaf3 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/caosdb.js b/src/core/js/caosdb.js index d673c9198cef6d41bfd19c5d5f618e13d20f0a02..37e006f720c896ead3327d1102eaf50344873113 100644 --- a/src/core/js/caosdb.js +++ b/src/core/js/caosdb.js @@ -634,7 +634,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); @@ -807,7 +807,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; @@ -821,15 +821,12 @@ 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 */ - valueelement.innerHTML = "<span class='caosdb-property-text-value'>" + property.value + "</span>"; + valueelement.innerHTML = "<span class='caosdb-f-property-single-raw-value caosdb-f-property-text-value caosdb-v-property-text-value'>" + property.value + "</span>"; } } @@ -966,23 +963,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); } @@ -1084,7 +1073,7 @@ function createFileXML(name, id, parents, * Update, Response, Delete. * * @param {string} root - The name of the newly created document root node. - * @param {(Document|XMLDocumentFragment)} xmls The xml documents. + * @param {Document[]|XMLDocumentFragment[]} xmls The xml documents. * @return {Document} A new xml document. */ function wrapXML(root, xmls) { @@ -1168,22 +1157,6 @@ async function retrieve_dragged_property(id) { return transformation.transformProperty(entities); } -/** - * Retrieve all properties and record types and convert them into - * a web page using a specialized XSLT named entity palette. - * @return An array of entities. - */ -async function retrieve_data_model() { - // TODO possibly allow single query - let props = await connection.get("Entity/?query=FIND Property"); - let rts = await connection.get("Entity/?query=FIND RecordType"); - for (var p of props.children[0].children) { - rts.children[0].appendChild(p.cloneNode()); - } - return transformation.transformEntityPalette(rts); -} - - /** * Update an entity using its xml representation. * @param xml The xml of the entity which will be automatically wrapped with an Update. @@ -1193,6 +1166,36 @@ async function update(xml) { return await transaction.updateEntitiesXml(wrapped); } + +/** + * Restore an old version of an entity using an xml representation. + * First, the old version is retrieved and the current version is set to the + * old one. + * @param versionid The version id (e.g. 123@abbabbaeff23322) of the version of + * the entity which shall be restored. + */ +async function restore_old_version(versionid){ + // retrieve entity + var ent = await transaction.retrieveEntityById(versionid); + if (ent === undefined){ + throw new Error(`Entity with version id ${versionid} could not be retrieved.`); + } + // remove unwanted tags (Version and Permissions) + ent.getElementsByTagName("Version")[0].remove(); + var permissions = ent.getElementsByTagName("Permissions"); + for (let i = permissions.length-1; i >=0 ; i--) { + permissions[i].remove(); + } + + // use XML to update entity/restore old version + const doc = _createDocument("Request"); + doc.firstElementChild.appendChild(ent); + reps = await transaction.updateEntitiesXml(doc); + if (reps.getElementsByTagName("Error").length>0) { + throw new Error(`Could not restore the Entity to the version ${versionid}.`); + } +} + /** * Insert an entity in xml representation. * diff --git a/src/core/js/edit_mode.js b/src/core/js/edit_mode.js index 9df7505f1e81fd0234aaa4461444535846921885..87e588aab11a681e49ff4e6596a81f4037628316 100644 --- a/src/core/js/edit_mode.js +++ b/src/core/js/edit_mode.js @@ -5,6 +5,8 @@ * Copyright (C) 2018 Research Group Biomedical Physics, * Max-Planck-Institute for Dynamics and Self-Organization Göttingen * Copyright (C) 2019 Henrik tom Wörden + * Copyright (C) 2019-2021 IndiScale GmbH <info@indiscale.com> + * Copyright (C) 2019-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 @@ -27,7 +29,7 @@ /** * Edit mode module */ -var edit_mode = new function() { +var edit_mode = new function () { var logger = log.getLogger("edit_mode"); @@ -63,30 +65,85 @@ var edit_mode = new function() { */ this.property_data_type_changed = new Event("caosdb.edit_mode.property_data_type_changed"); - this.init = function() { + /** + * 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.has_edit_fragment = function () { + const fragment = window.location.hash.substr(1); + return fragment === "edit"; + } + + this.has_new_record_fragment = function () { + const fragment = window.location.hash.substr(1); + return fragment === "new_record"; + } + + this._init = function () { + var target = $("#top-navbar").find("ul").first(); + this.add_edit_mode_button(target, edit_mode.toggle_edit_mode); + + var after_setup_callback = () => {} // do nothing + if (this.has_edit_fragment()) { + // find first entity + const first_entity = $(".caosdb-entity-panel")[0]; + if (first_entity) { + window.localStorage["edit_mode"] = true; + after_setup_callback = () => { + logger.debug("Edit this entity after #edit in the uri", first_entity); + edit_mode.edit(first_entity); + } + } + } else if (this.has_new_record_fragment()) { + for (let entity of $(".caosdb-entity-panel")) { + // find first record type + if (getEntityRole(entity) === "RecordType") { + window.localStorage["edit_mode"] = true; + const new_record = edit_mode.create_new_record(getEntityID(entity)); + after_setup_callback = () => { + logger.debug("Create a new record after #new_record in the uri", entity); + new_record.then((new_record) => { + edit_mode.app.newEntity(new_record); + }, edit_mode.handle_error); + } + break; + } + } + } + + // intialize the edit mode panel and add all necessary buttons if the edit mode is + if (this.is_edit_mode()) { + + edit_mode.enter_edit_mode().then(after_setup_callback); + 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"); + + // add drag-n-drop listener (needed for the edit_mode toolbox). + edit_mode.init_dragable(); + } + - this.dragstart = function(e) { + this.dragstart = function (e) { e.dataTransfer.setData("text/plain", e.target.id); } - this.dragleave = function(e) { + this.dragleave = function (e) { edit_mode.unhighlight(); } - this.dragover = function(e) { + this.dragover = function (e) { e.preventDefault(); e.dataTransfer.dropEffect = "copy"; edit_mode.highlight(this); @@ -99,41 +156,54 @@ var edit_mode = new function() { * @param new_prop * @param make_property_editable_cb */ - this.add_new_property = function(entity, new_prop, make_property_editable_cb = edit_mode.make_property_editable) { + this.add_new_property = function (entity, new_prop, make_property_editable_cb = edit_mode.make_property_editable) { if (typeof entity === "undefined" || !(entity instanceof HTMLElement)) { throw new TypeError("entity must instantiate HTMLElement"); } 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]; @@ -144,34 +214,28 @@ 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); + this.property_drop_listener = function (e) { + edit_mode._drop_listener.call(this, e, edit_mode.add_dropped_property); } - this.parent_drop_listener = function(e) { + this.parent_drop_listener = function (e) { edit_mode._drop_listener.call(this, e, edit_mode.add_dropped_parent); } - this._drop_listener = function(e, add_cb) { + this._drop_listener = function (e, add_cb) { e.preventDefault(); edit_mode.unhighlight(); const app = edit_mode.app; const state = app.state; - if(state === "initial"){ + if (state === "initial") { var entity = $(this).parent(); app.startEdit(entity[0]); } else if (state !== "changed") { @@ -184,7 +248,7 @@ var edit_mode = new function() { // Dropping a RecordType in the heading will add it as a parent. // This is done by this function. - this.parent_drop = function(e) { + this.parent_drop = function (e) { logger.assert(edit_mode.app.state === "changed", "state should be changed. Current state: ", edit_mode.app.state, edit_mode.app, e); e.preventDefault(); edit_mode.unhighlight(); @@ -192,7 +256,7 @@ var edit_mode = new function() { } - this.set_entity_dropable = function(entity, dragover, dragleave, parent_drop, property_drop) { + this.set_entity_dropable = function (entity, dragover, dragleave, parent_drop, property_drop) { if (getEntityRole(entity) === "Property") { // currently no parents and subproperties for properties. return; @@ -212,7 +276,7 @@ var edit_mode = new function() { } - this.unset_entity_dropable = function(entity, dragover, dragleave, parent_drop, property_drop) { + this.unset_entity_dropable = function (entity, dragover, dragleave, parent_drop, property_drop) { var rts = entity.getElementsByClassName("caosdb-entity-panel-body"); for (var rel of rts) { rel.removeEventListener("dragleave", dragleave); @@ -227,11 +291,11 @@ var edit_mode = new function() { } } - this.remove_save_button = function(ent) { + this.remove_save_button = function (ent) { $(ent).find('.caosdb-f-entity-save-button').remove(); } - this.add_save_button = function(ent, callback) { + this.add_save_button = function (ent, callback) { var save_btn = $('<button class="btn btn-link caosdb-update-entity-button caosdb-f-entity-save-button">Save</button>'); $(ent).find(".caosdb-f-edit-mode-entity-actions-panel").append(save_btn); @@ -266,7 +330,7 @@ var edit_mode = new function() { * last trash button for the last parent if the header belongs to a * record). */ - this.add_parent_delete_buttons = function(header) { + this.add_parent_delete_buttons = function (header) { $(header).find(".caosdb-f-parent-trash-button").remove(); var parents = $(header).find(".caosdb-parent-item"); if ((parents.length > 1) || getEntityRole(header) != "Record") { @@ -282,7 +346,7 @@ var edit_mode = new function() { * Append a trash button with class "caosdb-f-parent-trash-button", bind a * remove on the deletable, and bind a callback function to click. */ - this.add_parent_trash_button = function(appendable, deletable, callback = undefined) { + this.add_parent_trash_button = function (appendable, deletable, callback = undefined) { edit_mode.add_trash_button(appendable, deletable, "caosdb-f-parent-trash-button", callback); } @@ -290,7 +354,7 @@ var edit_mode = new function() { * Append a trash button with class "caosdb-f-property-trash-button" and * bind a remove on the deletable. */ - this.add_property_trash_button = function(appendable, deletable) { + this.add_property_trash_button = function (appendable, deletable) { edit_mode.add_trash_button(appendable, deletable, "caosdb-f-property-trash-button", undefined, "Remove this property"); } @@ -306,9 +370,9 @@ var edit_mode = new function() { * @param {string} [title] - optional title for the button element. * @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>'); - if(title) { + this.add_trash_button = function (appendable, deletable, className, callback = undefined, title = undefined) { + 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); } $(appendable).append(button); @@ -353,7 +417,11 @@ var edit_mode = new function() { atomic = "REFERENCE"; } - return { "atomic_datatype": atomic, "reference_scope": ref, "is_list": is_list}; + return { + "atomic_datatype": atomic, + "reference_scope": ref, + "is_list": is_list + }; } /** @@ -394,10 +462,10 @@ var edit_mode = new function() { * of entities in HTML representation. * @returns {XMLDocument} the unprocessed server response. */ - this.insert_entity = async function(ent_elements) { + this.insert_entity = async function (ent_elements) { ent_elements = caosdb_utils.assert_array(ent_elements, "param `ent_elements`", true); var xmls = []; - for ( const ent_element of ent_elements ) { + for (const ent_element of ent_elements) { xmls.push(edit_mode.form_to_xml(ent_element)); } return await insert(xmls); @@ -410,8 +478,8 @@ var edit_mode = new function() { * @returns {Document|DocumentFragment} - An xml document containing the * entity in XML representation. */ - this.form_to_xml = function(entity_form) { - const obj = form_elements.form_to_object($(entity_form).find("form")[0]); + this.form_to_xml = function (entity_form) { + 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; @@ -420,9 +488,6 @@ var edit_mode = new function() { 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( entityRole, @@ -441,11 +506,11 @@ var edit_mode = new function() { /** * TODO merge with getPropertyFromElement in caosdb.js */ - this.getPropertyFromElement = function(element) { + this.getPropertyFromElement = function (element) { var editfield = $(element).find(".caosdb-f-property-value"); var property = getPropertyFromElement(element); - var _parse_single_datetime = function(field) { + var _parse_single_datetime = function (field) { let time = $(field).find(":input[type='time']").val() let date = $(field).find(":input[type='date']").val(); if (time) { @@ -458,7 +523,7 @@ var edit_mode = new function() { // LISTs need to be handled here if (property.list == true) { property.value = []; - if (["TEXT","DOUBLE","INTEGER"].includes(property.listDatatype)) { + if (["TEXT", "DOUBLE", "INTEGER"].includes(property.listDatatype)) { // LOOP over elements of editfield.find(":input") for (var singleelement of $(editfield).find(":not(.caosdb-f-list-item-button):input:not(.caosdb-unit)")) { property.value.push($(singleelement).val()); @@ -476,7 +541,7 @@ var edit_mode = new function() { throw ("This property's data type is not supported by the webui. Please issue a feature request for support for `" + property.datatype + "`."); } } else { - if (["TEXT","DOUBLE","INTEGER"].includes(property.datatype)) { + if (["TEXT", "DOUBLE", "INTEGER"].includes(property.datatype)) { property.value = editfield.find(":input:not(.caosdb-unit)").val(); } else if (property.datatype == "DATETIME") { property.value = _parse_single_datetime(editfield); @@ -498,7 +563,7 @@ var edit_mode = new function() { * * ent_element : {HTMLElement} entity in view mode */ - this.getProperties = function(ent_element) { + this.getProperties = function (ent_element) { const properties = []; if (ent_element) { const prop_elements = getPropertyElements(ent_element); @@ -512,27 +577,30 @@ var edit_mode = new function() { } - this.update_entity = async function(ent_element) { + this.update_entity = async function (ent_element) { var xml = edit_mode.form_to_xml(ent_element); return await edit_mode.update(xml); } 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>'); + this.add_edit_mode_button = function (target, toggle_function) { + 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(); } } @@ -540,21 +608,19 @@ var edit_mode = new function() { * To be overridden by an instance of `leave_edit_mode_template` during the * `enter_edit_mode()` execution. */ - this.leave_edit_mode = function() {} + this.leave_edit_mode = function () {} - this.enter_edit_mode = function(editApp = undefined) { + /** + * Initializes the edit mode and loads the tool box. + */ + 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 { $(".caosdb-f-btn-toggle-edit-mode").text("Leave Edit Mode"); - edit_mode.init_tool_box(model); + edit_mode.init_tool_box(); var nextEditApp = editApp; if (typeof nextEditApp == "undefined") { @@ -562,26 +628,74 @@ var edit_mode = new function() { } edit_mode.init_new_buttons(nextEditApp); - edit_mode.leave_edit_mode = function() { + edit_mode.leave_edit_mode = function () { edit_mode.leave_edit_mode_template(nextEditApp); }; return nextEditApp; - }, edit_mode.handle_error); + } catch (err) { + edit_mode.handle_error(err); + } + } + + /** + * (Re-)load the toolbox, i.e. retrieve Properties and RecordTypes + */ + this.load_edit_mode_toolbox = async function () {} + + /** + * Retrieve all properties and record types and convert them into + * a web page using a specialized XSLT named entity palette. + * + * @return {Promise<XMLDocument>} An array of entities in xml representation. + */ + this.retrieve_data_model = async function () { + const props_prom = connection.get("Entity/?query=FIND Property"); + const rts_prom = connection.get("Entity/?query=FIND RecordType"); + + const [props, rts] = await Promise.all([props_prom, rts_prom]); + + // add all properties to rts + for (var p of props.children[0].children) { + rts.documentElement.appendChild(p.cloneNode()); + } + return rts; + } + + /** + * @param {Promise<XMLElement[]>} xml - an array of XMLElements. + * @return {Promise<HTMLDocument>} an HTMLElements. + */ + this.transform_entity_palette = async function _tME(xml) { + var xsl = transformation.retrieveXsltScript("entity_palette.xsl"); + let html = await asyncXslt(xml, xsl); + return html; } - this.retrieve_data_model = retrieve_data_model; + /** + * Remove all warnings and set the HTML of the toolbox panel to + * the model given by model. + */ + this.init_tool_box = async function () { - this.init_tool_box = function(model) { + // remove previously added model + $(".caosdb-f-edit-mode-existing").remove() - var editPanel = edit_mode.get_edit_panel(); - removeAllWaitingNotifications(editPanel); + const editPanel = $(edit_mode.get_edit_panel()); + removeAllWaitingNotifications(editPanel[0]); + editPanel.append(createWaitingNotification("Please wait.")); + const model = await edit_mode.transform_entity_palette(edit_mode.retrieve_data_model()); - editPanel.innerHTML = xml2str(model); - edit_mode.init_dragable(); + removeAllWaitingNotifications(editPanel[0]); + editPanel.children()[0].appendChild(model); + if (edit_mode.app && edit_mode.app.entity && edit_mode.app.entity.parentElement && (edit_mode.app.state === "changed")) { + // an entity is being editted + $(".caosdb-f-edit-mode-existing").toggleClass("d-none", false); + $(".caosdb-f-edit-mode-create-buttons").toggleClass("d-none", true); + } } @@ -592,9 +706,9 @@ var edit_mode = new function() { * * @param {HTMLElement} entity */ - this.init_actions_panels = function(entity) { + this.init_actions_panels = function (entity) { this.reset_actions_panels([entity]); - $(entity).find(".caosdb-entity-actions-panel").each(function(index) { + $(entity).find(".caosdb-entity-actions-panel").each(function (index) { var clone = $(this).clone(true)[0]; $(clone).removeClass("caosdb-entity-actions-panel").addClass("caosdb-f-edit-mode-entity-actions-panel").insertBefore(this); $(clone).children().remove(); @@ -607,19 +721,27 @@ var edit_mode = new function() { * * @param {HTMLElements[]} array of entities in HTML representation. */ - this.reset_actions_panels = function(entities) { + this.reset_actions_panels = function (entities) { $(entities).find(".caosdb-f-edit-mode-entity-actions-panel").remove(); $(entities).find(".caosdb-entity-actions-panel").show(); } - this.make_header_editable = function(entity) { + this.make_header_editable = function (entity) { var header = $(entity).find('.caosdb-entity-panel-heading'); var roleElem = $(header).find('.caosdb-f-entity-role'); 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"); @@ -650,16 +772,17 @@ var edit_mode = new function() { 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]); } - this.isListDatatype = function(datatype) { + this.isListDatatype = function (datatype) { return (typeof datatype !== 'undefined' && datatype.substring(0, 5) == "LIST<"); } - this.unListDatatype = function(datatype) { + this.unListDatatype = function (datatype) { return datatype.substring(5, datatype.length - 1); } @@ -678,7 +801,7 @@ var edit_mode = new function() { * * @param {HTMLElement} form - The form containing the input fields. */ - this.make_datatype_input_logic = function(form) { + this.make_datatype_input_logic = function (form) { const datatype = form_elements.get_fields(form, "atomic_datatype"); $(datatype).find("select").change(function () { @@ -722,38 +845,38 @@ var edit_mode = new function() { * @param {string} unit - the initial value of the input element. * @returns {HTMLElement} - a labeled form field. */ - this.make_unit_input = function(unit) { + this.make_unit_input = function (unit) { const unit_input = $(form_elements - .make_text_input({ - name: "unit", - label: "unit", - value: unit, - })); - unit_input.toggleClass("form-group", true); + .make_text_input({ + name: "unit", + label: "unit", + value: unit, + })); + 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]; } this._known_atomic_datatypes = [ - "TEXT", - "DOUBLE", - "INTEGER", - "DATETIME", - "BOOLEAN", - "FILE", - "REFERENCE", - ]; + "TEXT", + "DOUBLE", + "INTEGER", + "DATETIME", + "BOOLEAN", + "FILE", + "REFERENCE", + ]; /** * 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} */ - this.make_datatype_input = function(datatype) { + this.make_datatype_input = function (datatype) { var _datatype = datatype || "TEXT"; // split/convert datatype string into more practical variables. @@ -790,7 +913,7 @@ var edit_mode = new function() { make_value: getEntityName, }; const ref_selector = form_elements - .make_reference_drop_down(reference_config); + .make_reference_drop_down(reference_config); // generate the checkbox ([ ] list) @@ -800,25 +923,24 @@ var edit_mode = new function() { checked: is_list, } const list_checkbox = form_elements - .make_checkbox_input(list_checkbox_config); + .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); return form_group[0]; } - 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]; + this.make_input = function (label, value) { + 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) { + this.smooth_replace = function (from, to) { $(to).hide(); $(from).fadeOut(); $(from).after(to); @@ -827,22 +949,22 @@ var edit_mode = new function() { } /** - * Create an input element for a single property's value. - * - * @param {object} property - entity property object. - * @param {HTMLElements[]} [options] - an array of OPTION elements - * which represent possible candidates for reference values. The - * options will be appended to the SELECT input. This parameter is - * optional and only used for reference properties. This parameter - * might as well be a Promise for such an array. - */ - this.createElementForProperty = function(property, options) { + * Create an input element for a single property's value. + * + * @param {object} property - entity property object. + * @param {HTMLElements[]} [options] - an array of OPTION elements + * which represent possible candidates for reference values. The + * options will be appended to the SELECT input. This parameter is + * optional and only used for reference properties. This parameter + * might as well be a Promise for such an array. + */ + 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) { + if (property.value) { dateandtime = caosdb2InputDate(property.value); } let date = dateandtime[0]; @@ -879,10 +1001,10 @@ var edit_mode = new function() { * @param {object} property - a property object. * @returns {HTMLElement} a SPAN element. */ - this.generate_list_item_control_panel = function(property, options) { + 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>'); - $(deleteButton).click(function() { + 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; $(this.parentElement.parentElement).remove(); @@ -891,8 +1013,8 @@ 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>'); - $(insertButton).click(function() { + 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 = { list: false, @@ -912,14 +1034,16 @@ 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]; } /** * */ - this.create_unit_field = function(unit) { + this.create_unit_field = function (unit) { return $(`<input class='caosdb-unit' title='unit' style='width: 60px;' placeholder='unit' value='${unit||""}' type='text'></input>`)[0]; } @@ -934,10 +1058,10 @@ var edit_mode = new function() { * @param {object} property - a property object. * @return {HTMLElement[]} */ - this.create_value_inputs = function(property) { + this.create_value_inputs = function (property) { logger.trace("enter create_value_inputs", arguments); - var result = [ ]; + var result = []; if (!property.list) { var options = property.reference ? edit_mode.retrieve_datatype_list(property.datatype) : undefined; result.push(edit_mode.createElementForProperty(property, options)); @@ -953,7 +1077,7 @@ var edit_mode = new function() { result.push(edit_mode.create_unit_field(property.unit)); } - for (var i=0; i<property.value.length; i++) { + for (var i = 0; i < property.value.length; i++) { // TODO MERGE WITH OTHER PLACES WHERE THIS STRING APPEARS var proptemp = { list: false, @@ -971,8 +1095,8 @@ 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>'); - $(insertButton).click(function() { + 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 = { list: false, @@ -994,7 +1118,8 @@ var edit_mode = new function() { result = result.concat([inputs[0], $("<span>Insert element at the end of the list: </span>") - .append(insertButton)[0]]); + .append(insertButton)[0] + ]); } return result; @@ -1020,11 +1145,11 @@ var edit_mode = new function() { * * @see {@link _toggle_list_property} which is the most prominent callee. */ - this._toggle_list_property_object = function(element, toList) { + this._toggle_list_property_object = function (element, toList) { const property = edit_mode.getPropertyFromElement(element); // property.list XAND toList - if(property.list ? toList : !toList) { + if (property.list ? toList : !toList) { // already in desired state. return undefined; } @@ -1037,7 +1162,7 @@ var edit_mode = new function() { if (!toList && $.isArray(property.value) && property.value.length < 2) { property.value = property.value[0] || ""; } else if (toList && !$.isArray(property.value)) { - property.value = [ property.value ] + property.value = [property.value] } else { throw new Error(`Could not toggle to list=${toList} with value=${property.value}.`); } @@ -1057,12 +1182,12 @@ var edit_mode = new function() { * @see {@link _toggle_list_property_object} which does the actual * conversion work. */ - this._toggle_list_property = function(element, toList) { + this._toggle_list_property = function (element, toList) { logger.trace("enter _toggle_list_property", arguments); let property = edit_mode._toggle_list_property_object(element, toList); - if(!property) { + if (!property) { // already in desired state. return; } @@ -1081,7 +1206,7 @@ var edit_mode = new function() { } - this.change_property_data_type = function(element, datatype) { + this.change_property_data_type = function (element, datatype) { $(element).find(".caosdb-property-datatype").text(datatype); element.dispatchEvent(edit_mode.property_data_type_changed); } @@ -1095,9 +1220,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); @@ -1113,7 +1238,7 @@ var edit_mode = new function() { // disable checkbox when list has more than 1 element let disabled = editfield.find("li").length > 1; checkbox.prop("disabled", disabled); - const listDatatype = list ? datatype.substring(5, datatype.length-1) : datatype; + const listDatatype = list ? datatype.substring(5, datatype.length - 1) : datatype; if (disabled) { checkbox.attr("title", `You must remove all elements of the list but one by clicking the trash buttons (to the left) before you can toggle the LIST<${listDatatype}>`); } else { @@ -1124,23 +1249,23 @@ var edit_mode = new function() { // list has more than one element. editfield[0].addEventListener( edit_mode.list_value_input_added.type, (e) => { - let disabled = editfield.find("li").length > 1; - checkbox.prop("disabled", disabled); - if (disabled) { - checkbox.attr("title", `You must remove all elements of the list but one by clicking the trash buttons (to the left) before you can toggle the LIST<${listDatatype}>`); - } - }, true); + let disabled = editfield.find("li").length > 1; + checkbox.prop("disabled", disabled); + if (disabled) { + checkbox.attr("title", `You must remove all elements of the list but one by clicking the trash buttons (to the left) before you can toggle the LIST<${listDatatype}>`); + } + }, true); // enable the checkbox when elements removed to the list and the number // of elements is smaller than 2. editfield[0].addEventListener( edit_mode.list_value_input_removed.type, (e) => { - let disabled = editfield.find("li").length > 1; - checkbox.prop("disabled", disabled); - if (!disabled) { - checkbox.attr("title", `Toggle LIST<${listDatatype}> data type of this property.`); - } - }, true); + let disabled = editfield.find("li").length > 1; + checkbox.prop("disabled", disabled); + if (!disabled) { + checkbox.attr("title", `Toggle LIST<${listDatatype}> data type of this property.`); + } + }, true); } /** @@ -1159,16 +1284,21 @@ var edit_mode = new function() { * * @param {HTMLElement} element - entity property in HTML representation. */ - this.make_property_editable = function(element) { + 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-6") + .removeClass("col-md-8") + .addClass("col-sm-4") + .addClass("col-md-6") + .addClass("caosdb-v-property-value-inputs") + .after(`<div class="col-sm-2 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); @@ -1176,10 +1306,17 @@ var edit_mode = new function() { edit_mode.add_toggle_list_checkbox(element, property.list, property.datatype); // TRASH BUTTON - edit_mode.add_property_trash_button($(element).find(".caosdb-property-edit")[0],element); + edit_mode.add_property_trash_button($(element).find(".caosdb-property-edit")[0], element); } - this.create_new_record = async function(recordtype_id, name = undefined) { + + /** + * Create a new Record using an existing RecordType. + * The RecordType can be specified using recordtype_id. + * + * Currently name is ignored. TODO: check whether that behavior is intended. + */ + this.create_new_record = async function (recordtype_id, name = undefined) { var rt = await retrieve(recordtype_id); var newrecord = createEntityXML("Record", undefined, undefined, getProperties(rt[0]), @@ -1193,20 +1330,26 @@ var edit_mode = new function() { return x[0]; } - - this.init_dragable = function() { - var props = document.getElementsByClassName("caosdb-f-edit-drag"); - for (var pel of props) { - pel.addEventListener("dragstart", edit_mode.dragstart); - pel.setAttribute("draggable", true); - } + /** + * All draggable elements (that are those which have a css class + * caosdb-f-edit-drag are added the correct drag listener and the attribute draggable. + * + * This is now done using bubbling so that the listeners are also dynamically updated. + */ + this.init_dragable = function () { + // Bubbling: Add the listener to the document and check whether the class is present. + document.addEventListener("dragstart", function (event) { + if (event.target.classList.contains("caosdb-f-edit-drag")) { + edit_mode.dragstart(event); + } + }); } - /* + /** * This function treats the deletion of entities, i.e. when the "delete" * button is clicked. */ - this.delete_action = async function(entity) { + this.delete_action = async function (entity) { var app = edit_mode.app; // show waiting notification @@ -1250,33 +1393,19 @@ var edit_mode = new function() { } - this.disable_new_buttons = function() { + this.disable_new_buttons = function () { var new_buttons = $('.caosdb-f-edit-panel-new-button'); new_buttons.attr("disabled", true); } - this.enable_new_buttons = function() { + this.enable_new_buttons = function () { var new_buttons = $('.caosdb-f-edit-panel-new-button'); new_buttons.attr("disabled", false); } - this.init_new_buttons = function(app) { + this.init_new_buttons = function (app) { var new_buttons = $('.caosdb-f-edit-panel-new-button'); - // Show a button "+" to create a new property when filter results in empty list. - new_buttons.filter('.caosdb-f-hide-on-empty-input').parent().each(function(index) { - var button = $(this); - button.hide(); - var input = button.parent().find("input"); - input.on("input", function(e) { - if (input.val() == '') { - button.fadeOut(); - } else { - button.fadeIn(); - } - }); - }); - // handler for new property button // calls newEntity transition of state machine new_buttons.filter('.new-property').click(() => { @@ -1327,7 +1456,7 @@ var edit_mode = new function() { * * `finish` is triggered automatically when leaving the edit mode and does a little cleanup. */ - this.init_edit_app = function() { + this.init_edit_app = function () { var app = new StateMachine({ @@ -1367,9 +1496,9 @@ var edit_mode = new function() { }); - var init_drag_n_drop = function() { + var init_drag_n_drop = function () { const state = app.state; - $('.caosdb-entity-panel').each(function(index) { + $('.caosdb-entity-panel').each(function (index) { let entity = this; if ($(entity).is("[data-version-successor]")) { // not the latest version -> ignore @@ -1392,22 +1521,25 @@ var edit_mode = new function() { } // Define the error handler for the state machine. - app.errorHandler = function(fn) { + app.errorHandler = function (fn) { try { fn(); } catch (e) { edit_mode.handle_error(e); } }; - app.onAfterTransition = function(e) { + app.onAfterTransition = function (e) { init_drag_n_drop(); } - app.onEnterInitial = async function(e) { + 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 edit_mode.enable_new_buttons(); - $('.caosdb-entity-panel').each(function(index) { + $('.caosdb-entity-panel').each(function (index) { let entity = this; if ($(entity).is("[data-version-successor]")) { // not the latest version -> ignore @@ -1437,9 +1569,9 @@ var edit_mode = new function() { }); }); }; - app.onLeaveInitial = function(e) { + app.onLeaveInitial = function (e) { app.errorHandler(() => { - $('.caosdb-entity-panel').each(function(index) { + $('.caosdb-entity-panel').each(function (index) { // add the save button an so on edit_mode.remove_start_edit_button(this); edit_mode.remove_new_record_button(this); @@ -1448,7 +1580,7 @@ var edit_mode = new function() { }); edit_mode.disable_new_buttons(); }; - app.onBeforeStartEdit = function(e, entity) { + app.onBeforeStartEdit = function (e, entity) { edit_mode.unhighlight(); app.old = entity; app.entity = $(entity).clone(true)[0]; @@ -1461,11 +1593,11 @@ var edit_mode = new function() { edit_mode.freeze_but(app.entity); }; - app.onBeforeCancel = function(e) { + app.onBeforeCancel = function (e) { edit_mode.smooth_replace(app.entity, app.old); edit_mode.unfreeze(); }; - app.onUpdate = function(e, entity) { + app.onUpdate = function (e, entity) { edit_mode.update_entity(entity).then(response => { return transformation.transformEntities(response); }, edit_mode.handle_error).then(entities => { @@ -1474,7 +1606,12 @@ var edit_mode = new function() { app.showResults(); }, edit_mode.handle_error); }; - app.onEnterChanged = function(e) { + 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); @@ -1487,15 +1624,18 @@ 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) { + app.onEnterWait = function (e) { edit_mode.smooth_replace(app.entity, app.waiting); } - app.onLeaveWait = function(e) { + app.onLeaveWait = function (e) { edit_mode.smooth_replace(app.waiting, app.entity); } - app.onBeforeNewEntity = function(e, entity) { + app.onBeforeNewEntity = function (e, entity) { if (typeof entity == "undefined") { throw new TypeError("entity is undefined"); } @@ -1523,7 +1663,7 @@ var edit_mode = new function() { app.old = $('<div/>')[0]; } - app.onInsert = function(e, entity) { + app.onInsert = function (e, entity) { edit_mode.insert_entity(entity).then(response => { return transformation.transformEntities(response); }, edit_mode.handle_error).then(entities => { @@ -1536,7 +1676,7 @@ var edit_mode = new function() { app.showResults(); }, edit_mode.handle_error); } - app.onFinish = function(e) { + app.onFinish = function (e) { // this transition is triggerd on leaving the edit mode. edit_mode.unhighlight(); if (app.old) { @@ -1544,7 +1684,7 @@ var edit_mode = new function() { } edit_mode.unfreeze(); } - app.onShowResults = function(e) { + app.onShowResults = function (e) { // this transition is triggered by the response of the server on a // previous insert/update request. @@ -1556,6 +1696,7 @@ var edit_mode = new function() { app.entity.dispatchEvent(edit_mode.end_edit); resolve_references.init(); preview.init(); + edit_mode.init_tool_box(); } app.waiting = createWaitingNotification("Please wait."); $(app.waiting).hide(); @@ -1568,26 +1709,38 @@ var edit_mode = new function() { return app; } - this.has_errors = function(entity) { + this.has_errors = function (entity) { return $(entity).find(".alert.alert-danger").length > 0; } - this.freeze_but = function(element) { - $('.caosdb-f-main-entities').children().each(function(index) { + this.freeze_but = function (element) { + $('.caosdb-f-main-entities').children().each(function (index) { if (element != this) { edit_mode.freeze_entity(this); } }); } - this.unfreeze = function() { - $('.caosdb-f-main-entities').children().each(function(index) { + 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); }); } - this._create_reference_options = function(entities) { + this._create_reference_options = function (entities) { var results = []; for (var i = 0; i < entities.length; i++) { @@ -1623,7 +1776,7 @@ var edit_mode = new function() { this.fill_reference_drop_down = async function (drop_down, options) { var resolved_options = await options; var old = $(drop_down).find("option.caosdb-f-option-default"); - if(old.length == 0) { + if (old.length == 0) { // no option selected, append all $(drop_down).append($(resolved_options).clone()); return; @@ -1650,8 +1803,8 @@ var edit_mode = new function() { * @returns {HTMLElement[]} array of OPTION element, representing an entity * which can be referenced by the property. */ - this.retrieve_datatype_list = async function(datatype) { - var find_entity = ["FILE", "REFERENCE"].includes(datatype) ? "" : datatype; + this.retrieve_datatype_list = async function (datatype) { + var find_entity = ["FILE", "REFERENCE"].includes(datatype) ? "" : `"${datatype}"`; var entities = datatype !== "FILE" ? await edit_mode.query(`FIND Record ${find_entity}`) : []; var files = await edit_mode.query(`FIND File ${find_entity}`); @@ -1664,44 +1817,51 @@ var edit_mode = new function() { } - this.highlight = function(entity) { - $(entity).addClass("caosdb-v-edit-mode-highlight").css("background-color", "lightgreen"); + this.highlight = function (entity) { + $(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", ""); + this.unhighlight = function () { + $('.caosdb-v-edit-mode-highlight') + .removeClass("caosdb-v-edit-mode-highlight"); } - this.handle_error = function(err) { + this.handle_error = function (err) { globalError(err); } - this.get_edit_panel = function() { + this.get_edit_panel = function () { return $('.caosdb-f-edit-panel-body')[0]; } - this.add_wait_datamodel_info = function() { - $(this.get_edit_panel()).append(createWaitingNotification("Please wait.")); + this.toggle_edit_panel = function () { + //$(".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_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"); + 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) { + this.leave_edit_mode_template = function (app) { app.finish(); $(".caosdb-f-btn-toggle-edit-mode").text("Edit Mode"); edit_mode.reset_actions_panels($(".caosdb-f-main").toArray()); window.localStorage.removeItem("edit_mode"); } - this.is_edit_mode = function() { + this.is_edit_mode = function () { return window.localStorage.edit_mode !== undefined; } - this.add_cancel_button = function(ent, callback) { + this.add_cancel_button = function (ent, callback) { var cancel_btn = $('<button class="btn btn-link caosdb-update-entity-button caosdb-f-entity-cancel-button">Cancel</button>'); $(ent).find(".caosdb-f-edit-mode-entity-actions-panel").append(cancel_btn); @@ -1709,33 +1869,33 @@ var edit_mode = new function() { $(cancel_btn).click(callback); } - this.create_new_entity = async function(role) { + this.create_new_entity = async function (role) { var empty_entity = str2xml('<Response><' + role + '/></Response>'); return (await transformation.transformEntities(empty_entity))[0]; } - this.remove_cancel_button = function(entity) { + this.remove_cancel_button = function (entity) { $(entity).find('.caosdb-f-entity-cancel-button').remove() } /** * entity : HTMLElement */ - this.freeze_entity = function(entity) { + this.freeze_entity = function (entity) { $(entity).css("pointer-events", "none").css("filter", "blur(10px)"); } /** * entity : HTMLElement */ - this.unfreeze_entity = function(entity) { + this.unfreeze_entity = function (entity) { $(entity).css("pointer-events", "").css("filter", ""); } /** * this function is called in entity_palette.xsl */ - this.filter = function(ent_type) { + this.filter = function (ent_type) { var text = $("#caosdb-f-filter-" + ent_type).val(); if (ent_type == "properties") { var short_type = "p"; @@ -1766,8 +1926,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; + } - this.add_start_edit_button = function(entity, callback) { + /** + * 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>'); @@ -1776,12 +1996,15 @@ var edit_mode = new function() { $(button).click(callback); } - this.remove_start_edit_button = function(entity) { + this.remove_start_edit_button = function (entity) { $(entity).find(".caosdb-f-entity-start-edit-button").remove(); } - this.add_new_record_button = function(entity, callback) { + 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>'); @@ -1790,11 +2013,14 @@ var edit_mode = new function() { $(button).click(callback); } - this.remove_new_record_button = function(entity) { + this.remove_new_record_button = function (entity) { $(entity).find(".caosdb-f-entity-new-record-button").remove(); } - this.add_delete_button = function(entity, callback) { + 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>'); @@ -1803,26 +2029,26 @@ var edit_mode = new function() { $(button).click(() => { // 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", + 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-right"); + $(_alert).addClass("text-end"); $(entity).find(".caosdb-f-edit-mode-entity-actions-panel").after(_alert).hide(); }); } - this.remove_delete_button = function(entity) { + this.remove_delete_button = function (entity) { $(entity).find(".caosdb-f-entity-delete-button").remove(); } @@ -1834,6 +2060,6 @@ var edit_mode = new function() { /** * Add the extensions to the webui. */ -$(document).ready(function() { +$(document).ready(function () { edit_mode.init(); }); diff --git a/src/core/js/ext_applicable.js b/src/core/js/ext_applicable.js new file mode 100644 index 0000000000000000000000000000000000000000..6e4ae94f742e00e46f6cff609f4217100c9f4292 --- /dev/null +++ b/src/core/js/ext_applicable.js @@ -0,0 +1,342 @@ +/* + * ** 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'; + +/** + * Useful helpers for is_applicable functionality. + */ +var _helpers = function(getEntityPath) { + + /** + * Check if an entity has a path attribute and one of a set of extensions. + * + * Note: the array of extensions must contain only lower-case strings. + * + * @param {HTMLElement} entity + * @param {string[]} extensions - an array of file extesions, e.g. `jpg`. + * @return {boolean} true iff the entity has a path with one of the + * extensionss. + */ + const path_has_file_extension = function(entity, extensions) { + const path = getEntityPath(entity); + if (path) { + for (var ext of extensions) { + if (path.toLowerCase().endsWith(ext)) { + return true; + } + } + } + return false; + } + + return { + path_has_file_extension: path_has_file_extension + }; +}(getEntityPath) + +var ext_applicable = function($, logger, is_in_view_port, load_config, getEntityPath, connection, helpers, createWaitingNotification) { + + const version = "0.1"; + + /** + * Run through all creators and call the "create" function of the first + * creator which returns true from is_applicable(entity). + * + * @param {HTMLElement} entity + * @param {Creator[]} creators - list of creators + * @return {HTMLElement|String} content - the result of the first matching + * creator. + */ + const root_creator = async function (entity, creators) { + for (let c of creators) { + var is_applicable = false; + try { + is_applicable = await c.is_applicable(entity); + } catch (err) { + logger.error(`error in is_applicable function of creator`, c, err); + continue; + } + if (is_applicable) { + const content = await c.create(entity); + return content; + } + } + return undefined; + } + + var _set_content = function (container, content) { + const _container = $(container); + _container.empty(); + + if (content) { + _container.append(content); + } + } + + /** + * Replace the old content (if any) of the container by the new the content + * (created by the creators). + * + * Dispatch contentReadyEvent or noContentEvent if given. + * + * @param {HTMLElement} container + * @param {HTMLElement|string} [content] + * @param {Event} [contentReadyEvent] + * @param {Event} [noContentEvent] + */ + const set_content = async function (container, content, contentReadyEvent, noContentEvent) { + try { + const wait = createWaitingNotification("Please wait..."); + _set_content(container, wait); + const result = await content; + _set_content(container, result); + if (result && contentReadyEvent) { + container.dispatchEvent(contentReadyEvent); + } else if (!result && noContentEvent) { + container.dispatchEvent(noContentEvent); + } + + } catch (err) { + logger.error(err); + const err_msg = "An error occured while loading this content."; + _set_content(container, err_msg); + } + } + + + /** + * @param {HTMLElement} entity + * @param {get_container_cb} get_container_cb + * @param {Creator[]} creators + * @param {Event} [contentReadyEvent] + * @param {Event} [noContentEvent] + */ + const _root_handler = function (entity, get_container_cb, creators, contentReadyEvent, noContentEvent) { + const _container = get_container_cb(entity); + if (_container) { + const content = root_creator(entity, creators); + set_content(_container, content, contentReadyEvent, noContentEvent); + } + } + + + /** + * The root handler trigger call the root_handler callback on every + * .caosdb-entity-panel and every .caosdb-entity-preview in the viewport. + * + * @param {function} root_handler - the root handler callback. + */ + var root_handler_trigger = function(root_handler) { + var entities = $(".caosdb-entity-panel,.caosdb-entity-preview"); + for (let entity of entities) { + + // TODO viewport + 1000 px for earlier loading + if (is_in_view_port(entity)) { + root_handler(entity); + } + } + } + + + /** + * Initialize the scroll watcher which listens on the scroll event of the + * window and triggers the root handler with a delay after the last + * scroll event. + * + * @param {integer} delay - timeout in milliseconds after the last scroll + * event. After this timeout the trigger is called. + * @param {function} trigger - the root handler callback which is called. + */ + var init_watcher = function(delay, trigger) { + var scroll_timeout = undefined; + $(window).scroll(() => { + if (scroll_timeout) { + clearTimeout(scroll_timeout); + } + scroll_timeout = setTimeout(trigger, delay); + }); + + var preview_timeout = undefined; + + // init watcher on newly loaded entity previews. + window.addEventListener( + preview.previewReadyEvent.type, + () => { + if (preview_timeout) { + clearTimeout(preview_timeout); + } + preview_timeout = setTimeout(trigger, delay); + }, + true); + + // trigger for the first time + trigger(); + }; + + /** + * @callback get_container_cb + * @param {HTMLElement} entity - the entity for which this callback shall + * return the is_applicable container. + * @returns {HTMLElement} child of entity which is a container for the + * is_applicable_app + */ + + /** + * @type {IsApplicableApp} + * @property {IsApplicableConfig} config + * @property {Creator[]} creators + * @property {function} root_handler + */ + + /** + * @type {IsApplicableConfig} + * @property {string|HTMLElement|function} fallback - Fallback content or + * callback 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. + */ + + /** + * make a fallback creator + * + * @return {Creator} + */ + const _make_fallback_creator = function(fallback) { + if (fallback) { + return { + id: "_generated_fallback_creator", + is_applicable: (entity) => true, // always applicable + create: typeof fallback === "function" ? fallback : (entity) => fallback, + }; + } + return undefined; + } + + const _make_creator = function (c) { + return { + id: c.id, + is_applicable: typeof c.is_applicable === "function" ? + c.is_applicable : eval(c.is_applicable), + create: typeof c.create === "function" ? c.create : eval(c.create) + }; + } + + + /** + * @param {IsApplicableConfig} + * @return {Creator[]} creators + */ + const _make_creators = function(config) { + const creators = []; + for (let c of config.creators) { + creators.push(_make_creator(c)); + } + const fallback_creator = _make_fallback_creator(config.fallback); + if (fallback_creator) { + creators.push(fallback_creator); + } + return 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. + */ + + /** + * @param {string} app_name - the name of this app. + * @param {get_container_cb} get_container_cb + * @return {get_container_cb} wrapped function which also checks if the + * container has already been filled with the created content. + */ + const _make_get_container_wrapper = function (get_container_cb, app_name) { + const _wrapper = function (entity) { + const container = get_container_cb(entity); + const app_done = $(container).data(app_name); + if(!app_done) { + // mark container as used + $(container).data(app_name, "done"); + return container + } + // don't return the container if already used by this app + return undefined; + } + return _wrapper; + } + + /** + * @param {string} that_version - a version string. + * @throws {Error} if that_version doesn't match this modules version. + */ + const _check_version = function(that_version) { + if(that_version != version) { + throw new Error(`Wrong version in config. Was '${that_version}', should be '${version}'.`); + } + } + + /** + * @param {string} app_name + * @param {IsApplicableConfig} config + * @param {get_container_cb} get_container + * @param {Event} [contentReadyEvent] + * @param {Event} [noContentEvent] + * returns {IsApplicableApp} + */ + const create_is_applicable_app = function(app_name, config, get_container, contentReadyEvent, noContentEvent) { + logger.debug("create_is_applicable_app", config, get_container); + _check_version(config["version"]) + const creators = _make_creators(config) + const get_container_wrapper = _make_get_container_wrapper(get_container, app_name); + + const root_handler = (entity) => _root_handler(entity, get_container_wrapper, creators, contentReadyEvent, noContentEvent); + + if (config.init_watcher) { + init_watcher(config.delay || 500, () => {root_handler_trigger(root_handler);}); + } + return { + config: config, + creators: creators, + root_handler: root_handler, + } + } + + return { + create_is_applicable_app: create_is_applicable_app, + root_handler_trigger: root_handler_trigger, + init_watcher: init_watcher, + helpers: helpers, + version: version, + }; + +}($, log.getLogger("ext_applicable"), resolve_references.is_in_viewport_vertically, load_config, getEntityPath, connection, _helpers, createWaitingNotification); diff --git a/src/core/js/ext_autocomplete.js b/src/core/js/ext_autocomplete.js index 9d99241485245ca6232a7626c04f7998579174e8..2f6fa0dde729907f7248bd05367f26633872f39e 100644 --- a/src/core/js/ext_autocomplete.js +++ b/src/core/js/ext_autocomplete.js @@ -42,12 +42,47 @@ var ext_autocomplete = new function () { "WHICH", "WITH", "CREATED BY", + "CREATED BY ME", + "CREATED AT", "CREATED ON", + "CREATED IN", + "CREATED BEFORE", + "CREATED UNTIL", + "CREATED AFTER", + "CREATED SINCE", "SOMEONE", "STORED AT", "HAS A PROPERTY", "HAS BEEN", "ANY VERSION OF", + "FROM", + "INSERTED AT", + "INSERTED ON", + "INSERTED IN", + "INSERTED BY", + "INSERTED BY ME", + "INSERTED BEFORE", + "INSERTED UNTIL", + "INSERTED AFTER", + "INSERTED SINCE", + "UPDATED AT", + "UPDATED ON", + "UPDATED IN", + "UPDATED BY", + "UPDATED BY ME", + "UPDATED BEFORE", + "UPDATED UNTIL", + "UPDATED AFTER", + "UPDATED SINCE", + "SINCE", + "BEFORE", + "ON", + "IN", + "AFTER", + "UNTIL", + "AT", + "BY", + "BY ME", ]; this.version = "0.1"; @@ -123,8 +158,16 @@ var ext_autocomplete = new function () { var start = newValue.slice(0, beginning_of_word + 1); var end = origJQElement[0].value.slice(cursorpos); var result = resultsFromServer.map(x => { + var x_quoted = x; + if (x.indexOf(" ") > -1) { + if(x.indexOf("\"") > -1) { + x_quoted = `'${x}'`; + } else { + x_quoted = `"${x}"`; + } + } return { - text: start + x + end, + text: start + x_quoted + end, html: x } }); @@ -145,7 +188,10 @@ var ext_autocomplete = new function () { searchPost: this.searchPost, }, noResultsText: 'No autocompletion suggestions', - bootstrapVersion: "3", + bootstrapVersion: "4", + /* this should be updated to 5 when + bootstrap-autocomplete releases official support for bootstrap 5. + Until then: let's hope the best.*/ }); diff --git a/src/core/js/ext_bookmarks.js b/src/core/js/ext_bookmarks.js index d99ff362d8a26ee4818a76a624c80f55f757c419..d9da463699ff7021b9f94a877aa3d9379dffb5dc 100644 --- a/src/core/js/ext_bookmarks.js +++ b/src/core/js/ext_bookmarks.js @@ -2,18 +2,19 @@ * ** header v3.0 * This file is a part of the CaosDB Project. * - * Copyright (C) 2020 IndiScale GmbH <info@indiscale.com> + * Copyright (C) 2020,2021 IndiScale GmbH <info@indiscale.com> * Copyright (C) 2020 Timm Fitschen <t.fitschen@indiscale.com> + * Copyright (C) 2021 Florian Spreckelsen <f.spreckelsen@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 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. + * 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/>. @@ -231,7 +232,7 @@ var ext_bookmarks = function ($, logger, config) { */ 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); + 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 : ""); @@ -489,10 +490,8 @@ var ext_bookmarks = function ($, logger, config) { counter = 0; update_collection([]); - // reset all buttons - get_bookmark_buttons().forEach((x) => { - set_button_state(x, false); - }); + // re-init to reset all buttons + init(); const storage_key_prefix = get_collection_prefix() remove_from_storage_by_prefix(storage_key_prefix); @@ -566,6 +565,78 @@ var ext_bookmarks = function ($, logger, config) { collection_id = id; } + /** + * Add a button to add all query results to bookmarks. + */ + const add_add_query_results_button = function () { + const row_id = "caosdb-f-add-query-to-bookmarks-row" + // do nothing if already existing + if ($("#" + row_id).length > 0) { + return; + } + + // do nothing if no results + if ($(".caosdb-query-response-results").text().trim() == "0") { + return; + } + + const button_html = $(`<div class="text-end" id=${row_id}> + <button class="btn btn-link" onclick="ext_bookmarks.add_query_results_to_bookmarks();">Bookmark all query results</button> +</div>`)[0]; + + // Add to query results box + $(".caosdb-query-response-heading").append(button_html); + } + + /** + * Execute select query and add all new ids to bookmarks. + */ + const add_query_results_to_bookmarks = async function () { + + const query_string = get_query_from_response(); + const waiting_notification = createWaitingNotification( + "Adding results to bookmarks. Please wait and do not reload the page."); + const bookmarks_row = $("#caosdb-f-add-query-to-bookmarks-row"); + bookmarks_row.find("button").hide(); + bookmarks_row.append(waiting_notification); + const resp = await query(query_string); + for (const eid of resp) { + bookmark_storage.setItem(get_key(getEntityID(eid)), getEntityID(eid)); + } + // re-init for correct display of counter and entities on page + init(); + removeAllWaitingNotifications(bookmarks_row); + bookmarks_row.find("button").prop("disabled", true).show(); + } + + /** + * Transform a given query it to a "SELECT ID FROM ..." query. + * + * @param {string} query_string + */ + const get_select_id_query_string = function (query_string) { + const test_string = query_string.toLowerCase(); + const select_string = "SELECT ID FROM "; + + // Will only be called on valid query results, so don't have to check + // for invalid query strings. + if (test_string.startsWith("find") || test_string.startsWith("count")) { + return select_string + query_string.slice(query_string.indexOf(" ") + 1); + } + if (test_string.startsWith("select")) { + return select_string + query_string.slice(test_string.indexOf("from ") + 5); + } + } + + /** + * Return the SELECT query created from the contents of the query response field + */ + const get_query_from_response = function () { + + const orig_query = $(".caosdb-f-query-response-string")[0].innerText; + return get_select_id_query_string(orig_query.trim()); + } + /** * Initialize this module. */ @@ -591,6 +662,10 @@ var ext_bookmarks = function ($, logger, config) { init_bookmark_buttons(e.target); }, true); } + + if ("${BUILD_MODULE_EXT_ADD_QUERY_TO_BOOKMARKS}" == "ENABLED") { + add_add_query_results_button(); + } } /** @@ -625,7 +700,6 @@ var ext_bookmarks = function ($, logger, config) { 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) { @@ -652,6 +726,9 @@ var ext_bookmarks = function ($, logger, config) { get_bookmark_buttons: get_bookmark_buttons, init_button: init_button, get_bookmark_data: get_bookmark_data, + get_select_id_query_string: get_select_id_query_string, + get_query_from_response: get_query_from_response, + add_query_results_to_bookmarks: add_query_results_to_bookmarks, } }; @@ -666,10 +743,10 @@ $(document).ready(function () { // 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]) || ""; - } + const entity = $(`[data-bmval='${id}']`); + if (entity.length > 0) { + return getEntityPath(entity[0]) || ""; + } } return $(await transaction.retrieveEntityById(id)).attr("path"); } @@ -681,20 +758,20 @@ $(document).ready(function () { const get_name = async function (id) { if (id.indexOf("@") > -1) { - const entity = $(`[data-bmval='${id}']`); - if (entity.length > 0) { - return getEntityName(entity[0]) || ""; - } + 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 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")) @@ -727,4 +804,4 @@ $(document).ready(function () { ext_bookmarks = ext_bookmarks($, log.getLogger("ext_bookmarks"), config); caosdb_modules.register(ext_bookmarks); } -}); +}); \ No newline at end of file diff --git a/src/core/js/ext_bottom_line.js b/src/core/js/ext_bottom_line.js index 51ca8ac976314bcbe45d338e11cd2c6321f8afe2..2f40cd5c822c7153ef9fe8c40e7b06ffe70a3996 100644 --- a/src/core/js/ext_bottom_line.js +++ b/src/core/js/ext_bottom_line.js @@ -257,6 +257,7 @@ var ext_bottom_line = function($, logger, is_in_view_port, load_config, getEntit 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"; @@ -303,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.")); @@ -399,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(); @@ -432,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); diff --git a/src/core/js/ext_cosmetics.js b/src/core/js/ext_cosmetics.js index 3913a37b6645e88d3a357b9c4c124cf296ccfcad..77556437394df6a6763661ce5c0d5001f68ce61a 100644 --- a/src/core/js/ext_cosmetics.js +++ b/src/core/js/ext_cosmetics.js @@ -1,26 +1,83 @@ -var cosmetics = new function() { - this.init = function() { - this.linkify(); - } +/* + * This file is a part of the CaosDB Project. + * + * Copyright (C) 2021 IndiScale GmbH <info@indiscale.com> + * Copyright (C) 2021 Timm Fitschen <t.fitschen@indiscale.com> + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see <https://www.gnu.org/licenses/>. + */ + +"use strict"; + +/** + * Cosmetics module is a collection of small look-and-feel tweaks for the + * caosdb webui. + * + * @author Timm Fitschen + */ +var cosmetics = new function () { + + /** + * Cut-off length of links. When linkify processes the links any href + * longer than this will be cut off at character 25 and "[...]" will be + * appended for the link text. + */ + var _link_cut_off_length = 40; + + var _linkify = function () { + $('.caosdb-f-property-text-value').each(function (index) { + if (/https?:\/\//.test(this.innerText)) { + var result = this.innerText.replace(/https?:\/\/[^\s]*/g, function (href, index) { + var link_text = href; + if (_link_cut_off_length > 4 && link_text.length > _link_cut_off_length) { + link_text = link_text.substring(0, _link_cut_off_length - 5) + "[...]"; + } + + return `<a title="Open ${href} in a new tab." target="_blank" class="caosdb-v-property-href-value" href="${href}">${link_text} <i class="bi bi-box-arrow-up-right"></i></a>`; + }); - this.linkify = function() { - /* DEPRECATED css class .caosdb-property-text-value - Use - * .caosdb-f-property-single-raw-value or introduce new - * .caosdb-v-property-text-value */ - $('.caosdb-property-text-value').each(function(index) { - if (/^https?:\/\//.test(this.innerText)) { - var uri = this.innerText; - var text = uri - - $(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).hide(); + $(this).after(result); } }); } + + /** + * Convert any substring of a text-value beginning with 'http(s)://' into a + * link. + * + * A listener detects edit-mode changes and previews + */ + var linkify = function () { + _linkify(); + + // edit-mode-listener + document.body.addEventListener(edit_mode.end_edit.type, _linkify, true); + // preview listener + document.body.addEventListener(preview.previewReadyEvent.type, _linkify, true); + } + + this.init = function () { + this.linkify = linkify; + if ("${BUILD_MODULE_EXT_COSMETICS_LINKIFY}" == "ENABLED") { + linkify(); + } + } + } -$(document).ready(function() { - cosmetics.init(); -}); +$(document).ready(function () { + caosdb_modules.register(cosmetics); +}); \ No newline at end of file diff --git a/src/core/js/ext_editmode_wysiwyg_text.js b/src/core/js/ext_editmode_wysiwyg_text.js new file mode 100644 index 0000000000000000000000000000000000000000..f784dbd5d998ffeb95b4d594c907ff725441eadd --- /dev/null +++ b/src/core/js/ext_editmode_wysiwyg_text.js @@ -0,0 +1,123 @@ +/* + * This file is a part of the CaosDB Project. + * + * Copyright (C) 2021 IndiScale GmbH <info@indiscale.com> + * Copyright (C) 2021 Florian Spreckelsen <f.spreckelsen@indiscale.com> + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see <https://www.gnu.org/licenses/>. + */ + +"use strict"; + +/** + * Replaces textareas in the edit mode by a wysiwyg editor + * + * @module ext_editmode_wysiwyg_text + * @version 0.1 + * + * @param jQuery - well-known library. + * @param log - singleton from loglevel library or javascript console. + * @param {class} ClassicEditor - ClassicEditor class from ckEditor + * @param {module} edit_mode - caosdb's edit-mode module + * @param {function} getPropertyElements - caosdb's function to extract the + * property HTML elements from an entity HTML element + * @param {function} getPropertyDatatype - caosdb's function to extract the + * data type from a property HTML element + * @param {function} getPropertyName - caosdb's function to extract the + * name from a property HTML element + */ +var ext_editmode_wysiwyg_text = function ($, logger, ClassicEditor, edit_mode, getPropertyElements, getPropertyDatatype, getPropertyName) { + + var insertEditorInProperty = async function (prop) { + if (!(getPropertyDatatype(prop) === 'TEXT')) { + // Ignore anything that isn't a list property, even LIST<TEXT> + return; + } + + try { + const editor = await ClassicEditor + .create(prop.querySelector('textarea'), { + // use all plugins since we built the editor dependency to + // contain only those we need. + plugins: ClassicEditor.builtinPlugins, + // Markdown needs a header row so enforce this + table: { + defaultHeadings: { + rows: 1, + columns: 0 + }, + }, + }) + logger.debug('Initialized editor for ' + getPropertyName(prop)); + // Manually implement saving the data since edit mode is not + // a form to be submitted. + editor.model.document.on("change:data", (e) => { + editor.updateSourceElement(); + }); + } catch (error) { + logger.error(error.stack); + } + } + + var replaceTextAreas = function (entity) { + const properties = getPropertyElements(entity); + for (let prop of properties) { + // TODO(fspreck): This will be replaced by a whitelist of properties + // in the future. + if (getPropertyName(prop)) { + insertEditorInProperty(prop); + } + } + } + + var init = function () { + if (edit_mode.app && edit_mode.app.entity) { + // replace text areas if we're already in the edit mode and all + // events did fire already. + replaceTextAreas(edit_mode.app.entity); + } + + // Insert an editor into all TEXT properties of the entity which is + // being edited. + document.body.addEventListener(edit_mode.start_edit.type, (e) => { + logger.debug('Replacing text areas ...'); + ext_editmode_wysiwyg_text.replaceTextAreas(e.target); + }, true); + + // Listen to added properties and replace the textarea if necessary + document.body.addEventListener(edit_mode.property_added.type, (e) => { + logger.debug('Replacing textarea in ' + getPropertyName(e.target)); + ext_editmode_wysiwyg_text.insertEditorInProperty(e.target); + }, true) + + // Listen to properties, the data type of which has changed. Mainly + // because of change from list to scalar and vice versa. + document.body.addEventListener(edit_mode.property_data_type_changed.type, (e) => { + logger.debug('Re-rendering ' + getPropertyName(e.target)); + ext_editmode_wysiwyg_text.insertEditorInProperty(e.target); + }, true); + }; + + return { + init: init, + replaceTextAreas: replaceTextAreas, + insertEditorInProperty: insertEditorInProperty, + }; +}($, log.getLogger("ext_editmode_wysiwyg_text"), ClassicEditor, edit_mode, getPropertyElements, getPropertyDatatype, getPropertyName); + +$(document).ready(() => { + if ("${BUILD_MODULE_EXT_EDITMODE_WYSIWYG_TEXT}" == "ENABLED") { + caosdb_modules.register(ext_editmode_wysiwyg_text); + } +}); 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_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 d71a1d1aa2168a8afe788b3f254523c4953ddedb..c20cdefc2e9de73a21db81e3a7d5ebacebe73416 100644 --- a/src/core/js/ext_map.js +++ b/src/core/js/ext_map.js @@ -49,6 +49,13 @@ var caosdb_map = new function () { }, "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, @@ -121,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, @@ -133,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 @@ -151,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 @@ -174,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> @@ -205,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}. */ @@ -438,7 +445,7 @@ var caosdb_map = new function () { } else { pov = caosdb_map._get_id_POV(ids); } - return `SELECT ${selector}${datamodel.lat},${selector}${datamodel.lng} FROM ENTITY ${recordtype} ${pov} `; + return `SELECT parent,${selector}${datamodel.lat},${selector}${datamodel.lng} FROM ENTITY ${recordtype} ${pov} `; } @@ -628,7 +635,7 @@ var caosdb_map = new function () { "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>', + html: '<i class="bi-geo-alt-fill" style="font-size: 20px; color: #00F;"></i>', iconAnchor: [10, 19], className: "", }, @@ -650,7 +657,7 @@ var caosdb_map = new function () { "name": "All entities", "description": "Show all entities with coordinates.", "icon": { - html: '<span style="color: #F00; font-size: 20px" class="glyphicon glyphicon-map-marker"></span>', + html: '<i class="bi-geo-alt-fill" style="font-size: 20px; color: #F00;"></i>', iconAnchor: [10, 19], className: "", }, @@ -727,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"); @@ -742,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]; } @@ -797,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); @@ -867,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. */ @@ -956,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"); @@ -978,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(); } @@ -1065,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); @@ -1243,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 @@ -1426,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({ @@ -1451,14 +1470,14 @@ var caosdb_map = new function () { */ this.make_entity_name_label = function (entity) { const name = getEntityName(entity); - const id = getEntityId(entity); + const id = getEntityID(entity); const entity_on_page = $(`#${id}`).length > 0; const href = entity_on_page ? `#${id}` : connection.getBasePath() + `Entity/${id}` 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 @@ -1665,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 @@ -1675,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); @@ -1981,7 +2000,7 @@ entity.`; 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", ( @@ -2229,4 +2248,4 @@ entity.`; $(document).ready(function () { caosdb_modules.register(caosdb_map); -}); \ No newline at end of file +}); diff --git a/src/core/js/ext_qrcode.js b/src/core/js/ext_qrcode.js new file mode 100644 index 0000000000000000000000000000000000000000..d075ef884a89d407cb1e79b98f2045c6d4e25a26 --- /dev/null +++ b/src/core/js/ext_qrcode.js @@ -0,0 +1,201 @@ +/* + * This file is a part of the CaosDB Project. + * + * Copyright (C) 2021 IndiScale GmbH <info@indiscale.com> + * Copyright (C) 2021 Timm Fitschen <t.fitschen@indiscale.com> + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see <https://www.gnu.org/licenses/>. + */ + +"use strict"; + +/** + * Adds QR-Code generation to entities. + * + * @author Timm Fitschen + */ +var ext_qrcode = function ($, connection, getEntityVersion, getEntityID, QRCode, logger) { + + const _buttons_list_class = "caosdb-v-entity-header-buttons-list"; + const _qrcode_button_class = "caosdb-f-entity-qrcode-button"; + const _qrcode_canvas_container = "caosdb-f-entity-qrcode"; + const _qrcode_link_container = "caosdb-f-entity-qrcode-link"; + const _qrcode_icon = `<i class="bi bi-upc"></i>`; + + /** + * Create a new QR Code and a caption with a link, either linking to the + * entity head or to the exact version of the entity, based on the selected + * radio buttons and insert it into the modal. + * + * @param {HTMLElement} modal + * @param {string} entity_id + * @param {string} entity_version + */ + var update_qrcode = function (modal, entity_id, entity_version) { + modal = $(modal); + const uri = modal.find("input[name=entity-qrcode-versioned]:checked").val(); + var display_version = ""; + if (uri.indexOf("@") > -1) { + display_version = `@${entity_version.substring(0,8)}`; + } + const description = `Entity <a href="${uri}">${entity_id}${display_version}</a>`; + modal.find(`.${_qrcode_canvas_container}`).empty(); + modal.find(`.${_qrcode_link_container}`).empty().append(description); + QRCode.toCanvas(uri, { + "scale": 6 + }).then((canvas) => { + modal.find(`.${_qrcode_canvas_container}`).empty().append(canvas); + }).catch(logger.error); + } + + /** + * Create modal which shows the QR Code and a form where the user can choose + * whether the QR Code links to the entity head or the exact version of the + * entity. + * + * @param {string} modal_id - id of the resulting HTMLElement + * @param {string} entity_id + * @param {string} entity_version + * @return {HTMLElement} the resulting modal. + */ + var create_qrcode_modal = function (modal_id, entity_id, entity_version) { + const uri = `${connection.getEntityUri([entity_id])}`; + const short_version = entity_version.substring(0, 8); + const modal = $(`<div class="modal fade" id="${modal_id}" tabindex="-1" aria-labelledby="${modal_id}-label" aria-hidden="true"> + <div class="modal-dialog"> + <div class="modal-content"> + <div class="modal-header"> + <h4 class="modal-title" id="${modal_id}-label">QR Code</h5> + <button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button> + </div> + <div class="modal-body text-center"> + <div class="${_qrcode_canvas_container}"></div> + <div class="${_qrcode_link_container}"></div> + </div> + <div class="modal-footer justify-content-start"> + <form> + <div class="form-check"> + <label class="form-check-label"> + <input value="${uri}" class="form-check-input" type="radio" name="entity-qrcode-versioned" checked> + Link to this entity. + </label> + </div> + <div class="form-check"> + <label class="form-check-label" for="flexRadioDefault1"> + <input value="${uri}@${entity_version}" class="form-check-input" type="radio" name="entity-qrcode-versioned"> + Link to this exact version of this entity. + </label> + </div> + </form> + </div> + </div> + </div> + </div>`); + modal.find("form").change(() => { + update_qrcode(modal, entity_id, entity_version); + }); + return modal[0]; + } + + /** + * Click handler of the QR Code button. The click event opens a modal showing + * the QR Code and a form where the user can choose whether the QR Code links + * to the entity head or the exact version of the entity. + * + * @param {string} entity_id + * @param {string} entity_version + */ + var qrcode_button_click_handler = function (entity_id, entity_version) { + const modal_id = `qrcode-modal-${entity_id}-${entity_version}`; + var modal_element = document.getElementById(modal_id); + if (modal_element) { + // toggle modal + const modal = bootstrap.Modal.getInstance(modal_element); + modal.toggle(); + } else { + modal_element = create_qrcode_modal(modal_id, entity_id, entity_version); + update_qrcode(modal_element, entity_id, entity_version); + $("body").append(modal_element); + const options = {}; + const modal = new bootstrap.Modal(modal_element, options); + modal.show(); + } + } + + /** + * Create a button which opens the QR Code modal on click. + * + * @param {string} entity_id + * @param {string} entity_version + * @return {HTMLElement} the newly created button. + */ + var create_qrcode_button = function (entity_id, entity_version) { + const button = $(`<button title="Create QR Code" type="button" class="${_qrcode_button_class} caosdb-v-entity-qrcode-button btn">${_qrcode_icon}</button>`); + button.click(() => { + qrcode_button_click_handler(entity_id, entity_version); + }); + return button[0]; + } + + /** + * Add a qrcode button to a given entity. + * @param {HTMLElement} entity + */ + var add_qrcode_to_entity = function (entity) { + const entity_id = getEntityID(entity); + const entity_version = getEntityVersion(entity); + + $(entity).find(`.${_buttons_list_class}`).append(create_qrcode_button(entity_id, entity_version)); + } + + var remove_qrcode_button = function (entity) { + $(entity).find(`.${_buttons_list_class} .${_qrcode_button_class}`).remove(); + } + + var _init = function () { + for (let entity of $(".caosdb-entity-panel")) { + remove_qrcode_button(entity); + add_qrcode_to_entity(entity); + } + } + + /** + * Initialize this module and append a QR Code button to all entities panels on the page. + * + * Removes all respective buttons if present before adding a new one. + */ + var init = function () { + _init(); + + // edit-mode-listener + document.body.addEventListener(edit_mode.end_edit.type, _init, true); + }; + + return { + update_qrcode: update_qrcode, + add_qrcode_to_entity: add_qrcode_to_entity, + remove_qrcode_button: remove_qrcode_button, + create_qrcode_button: create_qrcode_button, + create_qrcode_modal: create_qrcode_modal, + qrcode_button_click_handler: qrcode_button_click_handler, + init: init + }; + +}($, connection, getEntityVersion, getEntityID, QRCode, console); + +$(document).ready(function () { + if ("${BUILD_MODULE_EXT_QRCODE}" == "ENABLED") { + caosdb_modules.register(ext_qrcode); + } +}); \ No newline at end of file diff --git a/src/core/js/ext_references.js b/src/core/js/ext_references.js index 7cd597e128e8c09da9134f42f542898fb84a4e53..fe4d618c752490400e501116470cce0f28a909ad 100644 --- a/src/core/js/ext_references.js +++ b/src/core/js/ext_references.js @@ -43,13 +43,13 @@ var isOutOfViewport = function (elem) { out.top = bounding.top < 0; out.left = bounding.left < 0; out.bottom = bounding.bottom > (window.innerHeight || - document.documentElement.clientHeight); + document.documentElement.clientHeight); out.right = bounding.right > - (window.innerWidth || document.documentElement.clientWidth); + (window.innerWidth || document.documentElement.clientWidth); out.any = - out.top || out.left || out.bottom || out.right; + out.top || out.left || out.bottom || out.right; out.all = out.top && - out.left && out.bottom && out.right; + out.left && out.bottom && out.right; return out; }; @@ -90,56 +90,56 @@ var reference_list_summary = new function () { * array. */ this.simplify_integer_numbers = function (array) { - logger.trace("enter simplify_integer_numbers", array); - var set = Array.from(new Set(array)); - - if (set.length === 0) { - return "" - } else if (set.length === 1) { - return `${set[0]}`; - } - - // sort numerically - set.sort((a, b) => a - b); - - if (set.length === 2) { - return `${set[0]}, ${set[1]}`; - } - - - var ret = `${set[0]}`; - var last = undefined; - // set[0]; - - // e.g. [1,2,3,4,5,8,9,10]; - for (const next of set) { - // append '-' to summarize consecutive numbers - if (next - last === 1 && !ret.endsWith("-")) { - ret += "-"; - } - - if (next - last > 1) { - - if (ret.endsWith("-")) { - // close previous interval and start new - ret += `${last}, ${next}`; - } else { - // no previous interval, start interval. - ret += `, ${next}`; - } - } else if (next === set[set.length - 1]) { - // finish interval if next is last item - ret += next; - break; - } + logger.trace("enter simplify_integer_numbers", array); + var set = Array.from(new Set(array)); + + if (set.length === 0) { + return "" + } else if (set.length === 1) { + return `${set[0]}`; + } + + // sort numerically + set.sort((a, b) => a - b); + + if (set.length === 2) { + return `${set[0]}, ${set[1]}`; + } + + + var ret = `${set[0]}`; + var last = undefined; + // set[0]; + + // e.g. [1,2,3,4,5,8,9,10]; + for (const next of set) { + // append '-' to summarize consecutive numbers + if (next - last === 1 && !ret.endsWith("-")) { + ret += "-"; + } + + if (next - last > 1) { + + if (ret.endsWith("-")) { + // close previous interval and start new + ret += `${last}, ${next}`; + } else { + // no previous interval, start interval. + ret += `, ${next}`; + } + } else if (next === set[set.length - 1]) { + // finish interval if next is last item + ret += next; + break; + } - last = next; + last = next; - } + } - // e.g. "1-5, 8-10" - return ret; + // e.g. "1-5, 8-10" + return ret; } /** @@ -158,19 +158,19 @@ var reference_list_summary = new function () { * @return {HTMLElement|string} generated summary */ this.generate = function (ref_infos, summary_container) { - logger.trace("enter generate", ref_infos); - if (ref_infos.length > 0 && - typeof ref_infos[0].callback === "function") { - const summary = - ref_infos[0].callback(ref_infos); - if (summary && summary_container) { - $(summary_container).append(summary); - } - logger.trace("leave generate", summary); - return summary; - } - logger.trace("leave generate, return undefined"); - return undefined; + logger.trace("enter generate", ref_infos); + if (ref_infos.length > 0 && + typeof ref_infos[0].callback === "function") { + const summary = + ref_infos[0].callback(ref_infos); + if (summary && summary_container) { + $(summary_container).append(summary); + } + logger.trace("leave generate", summary); + return summary; + } + logger.trace("leave generate, return undefined"); + return undefined; } } @@ -205,12 +205,12 @@ var resolve_references = new function () { * last scroll event. */ var scroll_listener = () => { - if (_scroll_timeout) { - clearTimeout(_scroll_timeout); - } - _scroll_timeout = setTimeout(function () { - resolve_references.update_visible_references(); - }, 500); + if (_scroll_timeout) { + clearTimeout(_scroll_timeout); + } + _scroll_timeout = setTimeout(function () { + resolve_references.update_visible_references(); + }, 500); }; @@ -220,15 +220,15 @@ var resolve_references = new function () { * visible references. */ this.init = function () { - if ("${BUILD_MODULE_EXT_RESOLVE_REFERENCES}" === "ENABLED") { - scroll_listener(); + if ("${BUILD_MODULE_EXT_RESOLVE_REFERENCES}" === "ENABLED") { + scroll_listener(); - // mainly for vertical scrolling - $(window).scroll(scroll_listener); + // mainly for vertical scrolling + $(window).scroll(scroll_listener); - // for horizontal scrolling. - $(".caosdb-value-list").scroll(scroll_listener); - } + // for horizontal scrolling. + $(".caosdb-value-list").scroll(scroll_listener); + } } /** @@ -241,9 +241,9 @@ var resolve_references = new function () { * */ this.is_in_viewport_vertically = function (elem) { - var out = - isOutOfViewport(elem); - return !(out.top || out.bottom); + var out = + isOutOfViewport(elem); + return !(out.top || out.bottom); } /** Check if an element is inside of the viewport on the horizontal axis. @@ -257,35 +257,19 @@ var resolve_references = new function () { * */ this.is_in_viewport_horizontally = function (elem) { - var scrollbox = elem.parentElement.parentElement; - // Check this condition only if the grand parent is a list and return true - // otherwise. - if (scrollbox.classList.contains("caosdb-value-list") == - true) { - var boundel = elem.getBoundingClientRect(); - var boundscroll = scrollbox.getBoundingClientRect(); - var leftcrit = boundel.right > boundscroll.left; - var rightcrit = boundel.left < boundscroll.right; - return leftcrit && rightcrit; - } else { - return true; - } - } - - - /** - * Return the name of a person as firstname + lastname - */ - this.get_person_str = function (el) { - var valpr = getProperties(el); - if (valpr == undefined) { - return; - } - return valpr.filter(valprel => - valprel.name.toLowerCase() == "firstname")[0].value + - " " + - valpr.filter(valprel => valprel.name.toLowerCase() == - "lastname")[0].value; + var scrollbox = elem.parentElement.parentElement; + // Check this condition only if the grand parent is a list and return true + // otherwise. + if (scrollbox.classList.contains("caosdb-value-list") == + true) { + var boundel = elem.getBoundingClientRect(); + var boundscroll = scrollbox.getBoundingClientRect(); + var leftcrit = boundel.right > boundscroll.left; + var rightcrit = boundel.left < boundscroll.right; + return leftcrit && rightcrit; + } else { + return true; + } } @@ -296,13 +280,13 @@ var resolve_references = new function () { * {string} par - parent name. @return {boolean} */ this.is_child = function (entity, par) { - var pars = resolve_references.getParents(entity); - for (const thispar of pars) { - if (thispar.name === par) { - return true; - } - } - return false; + var pars = resolve_references.getParents(entity); + for (const thispar of pars) { + if (thispar.name === par) { + return true; + } + } + return false; } /** @@ -318,46 +302,56 @@ var resolve_references = new function () { /** * Return a reference_info for an entity. * + * You may add your own custom resolver by specifying a JS module + * via the `BUILD_EXT_REFERENCES_CUSTOM_RESOLVER` build + * variable. The custom resolver has to be a JS module (typically + * located at caosdb-webui/src/ext/js), the name of which is given + * as the value of `BUILD_EXT_REFERENCES_CUSTOM_RESOLVER`. It has + * to provide a `resolve` function that takes the entity id to be + * resolved as a string and returns a `reference_info` object with + * the resolved custom reference as a `text` property. + * + * See caosdb-webui/src/ext/js/person_reference_resolver.js for an + * example. + * * TODO refactor to be configurable. @async @param {string} id - the id of * the entity which is to be resolved. @return {reference_info} */ this.resolve_reference = async function (id) { - const custom_reference_resolver = window["${BUILD_EXT_REFERENCES_CUSTOM_RESOLVER}"]; - if (custom_reference_resolver && typeof custom_reference_resolver.resolve === "function") { - // try custom_reference_resolver and fall-back to standard implementation - var ret = await custom_reference_resolver.resolve(id); - if (ret) { - return ret; - } - } - - const entity = (await resolve_references.retrieve(id))[0]; - - // TODO handle multiple parents - const par = resolve_references.getParents(entity)[0] || {}; - - var ret = { - "text": id - }; - if (getEntityHeadingAttribute(entity, "path") !== - undefined || par.name == "Image") { - // show file name - var pths = getEntityHeadingAttribute(entity, "path") - .split("/"); - ret["text"] = pths[pths.length - 1]; - } else if (par.name === "Person") { - ret["text"] = this.get_person_str(entity); - } else if (par.name === "TestReferenced" && typeof resolve_references.test_resolver === "function") { - // this is a test case, initialized by the test suite. - ret = resolve_references.test_resolver(entity); - } else { - var name = getEntityName(entity); - if (typeof name !== "undefined" && name.length > 0) { - ret["text"] = name; - } - } - + const custom_reference_resolver = window["${BUILD_EXT_REFERENCES_CUSTOM_RESOLVER}"]; + if (custom_reference_resolver && typeof custom_reference_resolver.resolve === "function") { + // try custom_reference_resolver and fall-back to standard implementation + var ret = await custom_reference_resolver.resolve(id); + if (ret) { return ret; + } + } + + const entity = (await resolve_references.retrieve(id))[0]; + + // TODO handle multiple parents + const par = resolve_references.getParents(entity)[0] || {}; + + var ret = { + "text": id + }; + if (getEntityHeadingAttribute(entity, "path") !== + undefined || par.name == "Image") { + // show file name + var pths = getEntityHeadingAttribute(entity, "path") + .split("/"); + ret["text"] = pths[pths.length - 1]; + } else if (par.name === "TestReferenced" && typeof resolve_references.test_resolver === "function") { + // this is a test case, initialized by the test suite. + ret = resolve_references.test_resolver(entity); + } else { + var name = getEntityName(entity); + if (typeof name !== "undefined" && name.length > 0) { + ret["text"] = name; + } + } + + return ret; } @@ -372,12 +366,12 @@ var resolve_references = new function () { * @return {HTMLElement} the new/existing target element. */ this.add_target = function (element) { - if(element.getElementsByClassName(this._target_class).length > 0){ - return element.getElementsByClassName(this._target_class); - } else { - return $(`<span class="${this._target_class}"/>`) - .appendTo(element)[0]; - } + if(element.getElementsByClassName(this._target_class).length > 0){ + return element.getElementsByClassName(this._target_class); + } else { + return $(`<span class="${this._target_class}"/>`) + .appendTo(element)[0]; + } } /** @@ -388,14 +382,14 @@ var resolve_references = new function () { * @return {reference_info} the resolved reference information */ this.update_single_resolvable_reference = async function (rs) { - $(rs).find(".caosdb-id-button").hide(); - const target = resolve_references.add_target(rs); - const id = getEntityID(rs); - target.textContent = id; - const resolved_entity_info = ( - await resolve_references.resolve_reference(id)); - target.textContent = resolved_entity_info.text; - return resolved_entity_info; + $(rs).find(".caosdb-id-button").hide(); + const target = resolve_references.add_target(rs); + const id = getEntityID(rs); + target.textContent = id; + const resolved_entity_info = ( + await resolve_references.resolve_reference(id)); + target.textContent = resolved_entity_info.text; + return resolved_entity_info; } @@ -411,10 +405,10 @@ var resolve_references = new function () { * @return {HTMLElement} a summary field. */ this.add_summary_field = function (list_values) { - const summary = $( - `<div class="${resolve_references._summary_class}"/>`); - $(list_values).prepend(summary); - return summary[0]; + const summary = $( + `<div class="${resolve_references._summary_class}"/>`); + $(list_values).prepend(summary); + return summary[0]; } this._summary_class = "caosdb-resolve-reference-summary"; @@ -426,9 +420,9 @@ var resolve_references = new function () { this._unresolved_class_name = "caosdb-resolvable-reference"; this.get_resolvable_properties = function (container) { - const _unresolved_class_name = this._unresolved_class_name; - return $(container).find(".caosdb-f-property-value").has( - `.${_unresolved_class_name}`).toArray(); + const _unresolved_class_name = this._unresolved_class_name; + return $(container).find(".caosdb-f-property-value").has( + `.${_unresolved_class_name}`).toArray(); } @@ -442,115 +436,115 @@ var resolve_references = new function () { * @param {HTMLElement} container */ this.update_visible_references = async function (container) { - const property_values = resolve_references - .get_resolvable_properties(container || document.body); - - const _unresolved_class_name = resolve_references - ._unresolved_class_name; - - // 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) { - 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); - - for (var i = 0; i < lists.length; i++) { - const list = lists[i]; - if (resolve_references - .is_in_viewport_vertically(list)) { - const rs = $(list).find( - `.${_unresolved_class_name}`) - .toggleClass(_unresolved_class_name, false); - - // First resolve only one reference. If the `ref_info` - // indicates that a summary is to be generated from the - // list of references, retrieve all other other - // references. Otherwise retrieve only those which are - // visible in the viewport horizontally and trigger the - // retrieval of the others when they are scrolled into - // the view port. - const first_ref_info = await resolve_references - .update_single_resolvable_reference(rs[0]); - - first_ref_info["index"] = 0; - - if (typeof first_ref_info.callback === "function") { - // there is a callback function, hence we need to - // generate a summary. - logger.debug("loading all references for summary", - rs); - const summary_field = resolve_references - .add_summary_field(property_value); - - // collect ref infos for the summary - const ref_infos = [first_ref_info]; - for (var j = 1; j < rs.length; j++) { - const ref_info = resolve_references - .update_single_resolvable_reference(rs[j]); - ref_info["index"] = j; - ref_infos.push(ref_info); - } - - // wait for resolution of references, - // then generate the summary, - // dispatch event when ready. - Promise.all(ref_infos) - .then(_ref_infos => {reference_list_summary - .generate(_ref_infos, summary_field);}) - .then(() => { - summary_field.dispatchEvent( - resolve_references - .summary_ready_event - );}) - .catch((err) => { - logger.error(err); - }) - - } else { - // no summary to be generated - - logger.debug("lazy loading references", rs); - for (var j = 1; j < rs.length; j++) { - // mark others to be loaded later and only if - // visible - $(rs[j]).toggleClass(_unresolved_class_name, true); - } - } - } - } - } - - // Load all remaining references. These are single reference values - // and those references from lists which are left for lazy loading. - 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]) && - resolve_references.is_in_viewport_horizontally( - rs[i])) { - logger.debug("processing single references", rs); - $(rs[i]).toggleClass(_unresolved_class_name, false); - - // discard return value as it is not needed for any summary - // generation as above. - resolve_references.update_single_resolvable_reference(rs[i]); - } - } + const property_values = resolve_references + .get_resolvable_properties(container || document.body); + + const _unresolved_class_name = resolve_references + ._unresolved_class_name; + + // 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) { + 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); + + for (var i = 0; i < lists.length; i++) { + const list = lists[i]; + if (resolve_references + .is_in_viewport_vertically(list)) { + const rs = $(list).find( + `.${_unresolved_class_name}`) + .toggleClass(_unresolved_class_name, false); + + // First resolve only one reference. If the `ref_info` + // indicates that a summary is to be generated from the + // list of references, retrieve all other other + // references. Otherwise retrieve only those which are + // visible in the viewport horizontally and trigger the + // retrieval of the others when they are scrolled into + // the view port. + const first_ref_info = await resolve_references + .update_single_resolvable_reference(rs[0]); + + first_ref_info["index"] = 0; + + if (typeof first_ref_info.callback === "function") { + // there is a callback function, hence we need to + // generate a summary. + logger.debug("loading all references for summary", + rs); + const summary_field = resolve_references + .add_summary_field(property_value); + + // collect ref infos for the summary + const ref_infos = [first_ref_info]; + for (var j = 1; j < rs.length; j++) { + const ref_info = resolve_references + .update_single_resolvable_reference(rs[j]); + ref_info["index"] = j; + ref_infos.push(ref_info); + } + + // wait for resolution of references, + // then generate the summary, + // dispatch event when ready. + Promise.all(ref_infos) + .then(_ref_infos => {reference_list_summary + .generate(_ref_infos, summary_field);}) + .then(() => { + summary_field.dispatchEvent( + resolve_references + .summary_ready_event + );}) + .catch((err) => { + logger.error(err); + }) + + } else { + // no summary to be generated + + logger.debug("lazy loading references", rs); + for (var j = 1; j < rs.length; j++) { + // mark others to be loaded later and only if + // visible + $(rs[j]).toggleClass(_unresolved_class_name, true); + } + } } } + } + + // Load all remaining references. These are single reference values + // and those references from lists which are left for lazy loading. + 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]) && + resolve_references.is_in_viewport_horizontally( + rs[i])) { + logger.debug("processing single references", rs); + $(rs[i]).toggleClass(_unresolved_class_name, false); + + // discard return value as it is not needed for any summary + // generation as above. + resolve_references.update_single_resolvable_reference(rs[i]); + } + } + } + } } $(document).ready(function () { if ("${BUILD_MODULE_EXT_RESOLVE_REFERENCES}" === "ENABLED") { - caosdb_modules.register(resolve_references); + caosdb_modules.register(resolve_references); } }); 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 index 1d9da6fa9334a52de5eb39a66b585c7eb67cd120..708d4da4b69602b4824b46e2aac082e4037b3102 100644 --- a/src/core/js/ext_table_preview.js +++ b/src/core/js/ext_table_preview.js @@ -55,8 +55,8 @@ var ext_table_preview = function ($, logger, connection, getEntityPath, getEnti } else { const tablecontent = script_result.getElementsByTagName("stdout")[0]; const unformatted = markdown.textToHtml(tablecontent.textContent) - const formatted = $('<div class="table-responsive"/>').append(unformatted); - formatted.find("table").addClass("table table-bordered table-condensed").removeAttr("border"); + const formatted = $('<div/>').append(unformatted); + formatted.find("table").addClass("table table-responsive table-bordered table-sm").removeAttr("border"); return formatted[0]; } } catch (err) { 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 ebeb2ac94ca7bd84cc41ffef7bf3d1abc1982875..5ea29ba38698b7c14ad4329201b2f3104dfe57ab 100644 --- a/src/core/js/ext_xls_download.js +++ b/src/core/js/ext_xls_download.js @@ -62,7 +62,7 @@ var caosdb_table_export = new function () { } /** - * In order to create a valid tsv table, characters that have a special + * 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 @@ -71,7 +71,7 @@ var caosdb_table_export = new function () { * @return {string} cleaned up content */ this._clean_cell = function(raw) { - return raw.replaceAll("\t"," ").replaceAll("\n"," ").replaceAll("\r"," ").replaceAll("\x1E"," ").replaceAll("\x15"," ") + return raw.replace(/\t/g," ").replace(/\n/g," ").replace(/\r/g," ").replace(/\x1E/g," ").replace(/\x15/g," ") } /** @@ -83,7 +83,7 @@ var caosdb_table_export = new 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); + .filter(e => e.length > 0 && e.toLowerCase() != "id" && e.toLowerCase() != "version"); } /** @@ -124,7 +124,7 @@ var caosdb_table_export = new function () { */ this._create_tsv_string = function (entities, columns, raw) { logger.trace("enter _create_tsv_string ", entities, columns); - var header = "ID\t" + columns.join("\t") + "\n" + 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")); @@ -146,7 +146,7 @@ var caosdb_table_export = new function () { * @return {string[]} */ this._get_entity_row = function (entity, columns, raw) { - var cells = [getEntityID(entity)]; + var cells = [getEntityID(entity), getEntityVersion(entity)]; var properties = getProperties(entity); for (const column of columns) { diff --git a/src/ext/js/fileupload.js b/src/core/js/fileupload.js similarity index 83% rename from src/ext/js/fileupload.js rename to src/core/js/fileupload.js index 1c069f2a8fdded69a2b7cc93f27601226e1d149c..8a988e86a6a9b5bbb39f39a37d63b32b144c748d 100644 --- a/src/ext/js/fileupload.js +++ b/src/core/js/fileupload.js @@ -25,18 +25,31 @@ var fileupload = new function() { // TODO * action to config * upload-path id -> class * message configurable // * style path input - 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="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 - caosdb-f-file-upload-submit-button">Ok</button> <button type="button" - class="btn btn-default" data-dismiss="modal">Close</button> </div> </div> - </div> </div>`; + 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"> + <h4 class="modal-title">File Upload</h4> + <button type="button" class="btn-close" data-bs-dismiss="modal"> + </button> + </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-secondary caosdb-f-file-upload-submit-button">Ok</button> + <button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Close</button> + </div> + </div> + </div> +</div>`; /** Create a dropzone.js form for the file upload. */ @@ -114,8 +127,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"></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 @@ -130,7 +156,7 @@ var fileupload = new function() { // get property-value input element (in case of FILE property) var input = $(property).find(".caosdb-f-property-value input"); var set_value = function(entity) { - input.val(getEntityId(entity)); + input.val(getEntityID(entity)); } if (input.length == 0) { @@ -174,14 +200,14 @@ 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" - aria-label="Close"><span aria-hidden="true">×</span></button> + <button type="button" class="btn-close" data-bs-dismiss="alert" + aria-label="Close"></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" - href="` + connection.getEntityUri([getEntityId(entity)]) + `" target= "_blank">` + + input.after(`<a class="btn btn-secondary btn-sm" + href="` + connection.getEntityUri([getEntityID(entity)]) + `" target= "_blank">` + getEntityName(entity) + `</a>`); }; @@ -193,7 +219,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]; }; @@ -282,7 +308,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 = { @@ -295,7 +321,7 @@ var fileupload = new function() { error_handler, atom_par); var toggle_function = function() { - $(modal).modal() + $(modal).modal("toggle"); }; this.add_file_upload_button(edit_menu, button, toggle_function); diff --git a/src/core/js/footer.js b/src/core/js/footer.js deleted file mode 100644 index c48c5cf19c405aea326b36aba2868008466a8329..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 something - */ -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 d4cd4234a28953140fcc1f62104e2c43a3460cdb..193235a2f8a799c07ccc893742d5df9a7d0fa7d1 100644 --- a/src/core/js/form_elements.js +++ b/src/core/js/form_elements.js @@ -84,28 +84,57 @@ var form_elements = new function () { /** * The configuration for double, integer, date input elements. * - * There are specializations of this configuration object. See - * {@link ReferenceDropDownConfig} + * 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 - * @see {@link ReferenceDropDownConfig} + * @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"); + /** + * Event. On form cancel. + */ this.cancel_form_event = new Event("caosdb.form.cancel"); + /** + * Event. On form submit. + */ this.submit_form_event = new Event("caosdb.form.submit"); - this.form_ready_event = new Event("caosdb.form.ready"); + /** + * Event. On field change. + */ this.field_changed_event = new Event("caosdb.field.changed"); + /** + * Event. On field enabled. + */ this.field_enabled_event = new Event("caosdb.field.enabled"); + /** + * Event. On field disabled. + */ this.field_disabled_event = new Event("caosdb.field.disabled"); + /** + * Event. On field ready (e.g. for reference drop downs) + */ this.field_ready_event = new Event("caosdb.field.ready"); + /** + * Event. On field error (e.g. for reference drop downs) + */ this.field_error_event = new Event("caosdb.field.error"); + /** + * Event. Form submitted successfully. + */ this.form_success_event = new Event("caosdb.form.success"); + /** + * Event. Error after form was submitted. + */ this.form_error_event = new Event("caosdb.form.error"); @@ -208,7 +237,7 @@ var form_elements = new 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`"); + "config param `proceed_callback`"); // define some defaults. const title = config.title ? `<h4>${config.title}</h4>` : ""; @@ -219,40 +248,43 @@ var form_elements = new function () { // 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; - } + 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; + } } // create the alert const _alert = $(`<div class="alert alert-${severity} - alert-dismissible fade in caosdb-f-form-elements-alert" role="alert">${title} + 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="checkbox"><label> + 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); } // create buttons ... - const cancel_button = config.cancel_button || $(`<button type="button" class="btn btn-default caosdb-f-btn-alert-cancel">${cancel_text}</button>`); + 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(() => { - $(_alert).alert('close'); + var alert = bootstrap.Alert.getInstance(_alert[0]); + alert.close() if (typeof config.cancel_callback == "function") { config.cancel_callback(); } @@ -263,10 +295,13 @@ var form_elements = new function () { form_elements._set_alert_decision(config.remember_my_decision_id, "proceed"); } - $(_alert).alert('close'); + var alert = bootstrap.Alert.getInstance(_alert[0]); + alert.close() config.proceed_callback(); }); + new bootstrap.Alert(_alert[0]); + return _alert[0]; } @@ -293,12 +328,23 @@ var form_elements = new function () { if (typeof desc == "undefined") { desc = entity_id; } - var opt_str = '<option value="' + entity_id + '">' + desc + + 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. */ @@ -320,10 +366,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", @@ -333,12 +380,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); @@ -351,6 +393,30 @@ var form_elements = new function () { return ret[0]; } + /** + * 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 @@ -369,10 +435,9 @@ var form_elements = new function () { * defined by `label`. If the `label` property is undefined, the `name` * is shown instead. * - * The ReferenceDropDownConfig is a specialisation of a - * {@link FieldConfig}. - * * @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 @@ -386,8 +451,6 @@ 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. - * - * @see {@link FieldConfig} */ this._query = async function (q) { @@ -396,8 +459,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", @@ -406,6 +484,16 @@ 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); @@ -422,13 +510,13 @@ var form_elements = new function () { */ /** - * Bla, TODO + * 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) { - 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; @@ -456,15 +544,15 @@ var form_elements = new function () { 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"/>'); + let loading = $(createWaitingNotification("loading...")) + .addClass("caosdb-f-field-not-ready"); + let input_col = $('<div class="caosdb-f-property-value 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, + entities, config.make_desc, config.make_value, config.name, config.multiple, config.value)); - select.attr("name", config.name); loading.remove(); input_col.append(select); form_elements.init_select_picker(ret[0], config.value); @@ -514,8 +602,8 @@ var form_elements = new function () { 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 class="btn-group btn-group-sm d-grid"> + <button type="button" class="actions-btn bs-deselect-all btn btn-light">None</button> </div> </div>`) .hide(); @@ -537,9 +625,11 @@ var form_elements = new function () { actions_box .find(".bs-deselect-all") .click((e) => { - select.val(null) - .selectpicker("render") - .parent().toggleClass("open", false); + select + .selectpicker("val", null); + select + .selectpicker("render"); + select.dropdown("hide"); select[0].dispatchEvent(form_elements.field_changed_event); }); } @@ -574,16 +664,27 @@ var form_elements = new function () { } /** - * generate a java script object representation of a form + * 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. * - * @function + * 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`"); - const _to_json = (element, data) => { - this.logger.trace("enter element_to_json", element, data); + 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 @@ -595,7 +696,7 @@ var form_elements = new function () { if (is_subform) { const subform = $(child).data("subform-name"); // recursive - var subform_obj = _to_json(child, {}); + var subform_obj = _to_json(child, {}, files)[0]; if (typeof data[subform] === "undefined") { data[subform] = subform_obj; } else if (Array.isArray(data[subform])) { @@ -606,7 +707,30 @@ var form_elements = new function () { } else if (name && name !== "") { // input elements const not_checkbox = !$(child).is(":checkbox"); - if (not_checkbox || $(child).is(":checked")) { + 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") { @@ -621,15 +745,15 @@ var form_elements = new function () { } } else if (child.children.length > 0) { // recursive - _to_json(child, data); + _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, {}); + const ret = _to_json(form, {}, []); this.logger.trace("leave form_to_json", ret); return ret; } @@ -649,11 +773,19 @@ var form_elements = new function () { } /** - * TODO make syncronous + * 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 = async function (config) { + 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`"); @@ -661,6 +793,8 @@ var form_elements = new function () { 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") { @@ -670,12 +804,13 @@ var form_elements = new function () { } else if (type === "integer") { field = this.make_integer_input(config); } else if (type === "range") { - field = await this.make_range_input(config); + 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") { - // TODO handle cache and required for subforms - return await this.make_subform(config); + return this.make_subform(config); } else { throw new TypeError("undefined field type `" + type + "`"); } @@ -695,13 +830,13 @@ var form_elements = new function () { 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"/>') + 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-content", config); + help_button.attr("data-bs-content", config); help_button.popover(); } else { help_button.popover(config); @@ -732,31 +867,18 @@ var form_elements = new function () { 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); + form_elements.logger.trace("cancel form", e); wrapper.remove(); }; - wrapper.append(loading); - - Promise.resolve(form).then(form => { - // form ready - loading.remove(); - wrapper.append(form); - wrapper[0].dispatchEvent(this.form_ready_event); + wrapper[0].addEventListener(this.cancel_form_event.type, cancel, true); - }).catch(err => { - logger.error("form loading error", err); - loading.remove(); - wrapper.append(err); - }); - wrapper[0].addEventListener(this.cancel_form_event.type, cancel, true); + var header = this.make_heading(config); + wrapper.append(header); + wrapper.append(form); return wrapper[0]; } @@ -782,9 +904,9 @@ var form_elements = new function () { /** * Create a form. * - * The returned element is a container which will eventually contain a HTML - * form element. The container emits a {@link form_ready_event} when the - * form is ready. + * 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} @@ -802,9 +924,20 @@ var form_elements = new function () { } /** - * TODO make syncronous + * @typedef {object} SubFormConfig + * + * @augments FieldConfig + * @property {FieldConfig[]} fields - array of fields. The order is the + * order in which they appear in the resulting subform. + */ + + /** + * Return a new subform. + * + * @param {SubFormConfig} config - the configuration of the subform. + * @return {HTMLElement} */ - this.make_subform = async function (config) { + 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`"); @@ -815,7 +948,7 @@ var form_elements = new function () { for (let field of config.fields) { this.logger.trace("add subform field", field); - let elem = await this.make_form_field(field); + let elem = this.make_form_field(field); form.append(elem); } @@ -865,6 +998,7 @@ var form_elements = new function () { 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); } @@ -872,6 +1006,7 @@ var form_elements = new function () { 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); } @@ -885,7 +1020,7 @@ var form_elements = new function () { this.disable_fields($(form).find(".caosdb-f-field[data-field-name='" + name + "']").toArray()); } - this.make_script_form = async function (config, script) { + this.make_script_form = function (config, script) { this.logger.trace("enter make_script_form"); const submit_callback = async function (form) { @@ -914,7 +1049,7 @@ var form_elements = new function () { name: script, submit: submit_callback }, config); - return await this.make_generic_form(new_config); + return this.make_generic_form(new_config); } /** @@ -924,9 +1059,9 @@ var form_elements = new function () { * The `config.fields` array may contain `form_elements.field_config` * objects or HTMLElements. * - * TODO + * @return {HTMLElement} */ - this.make_generic_form = async function (config) { + this.make_generic_form = function (config) { this.logger.trace("enter make_generic_form"); caosdb_utils.assert_type(config, "object", "param `config`"); @@ -946,7 +1081,7 @@ var form_elements = new function () { if (field instanceof HTMLElement) { form.append(field); } else { - let elem = await this.make_form_field(field); + let elem = this.make_form_field(field); form.append(elem); } } @@ -1093,7 +1228,7 @@ var form_elements = new function () { } this.make_footer = function () { - return $('<div class="text-right caosdb-f-form-elements-footer"/>') + return $('<div class="text-end caosdb-f-form-elements-footer"/>') .css({ "margin": "20px", }).append(this.make_required_marker()) @@ -1125,11 +1260,24 @@ var form_elements = new function () { } /** - * TODO make syncronous + * @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 = async function (config) { + this.make_range_input = function (config) { - // TODO + // 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 @@ -1144,8 +1292,11 @@ var form_elements = new function () { 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 from_input = this.make_form_field(from_config); + $(from_input).toggleClass("form-control", false); + + const to_input = this.make_form_field(to_config); + $(to_input).toggleClass("form-control", false); const ret = $(this._make_field_wrapper(config.name)); if (config.label) { @@ -1156,12 +1307,8 @@ var form_elements = new function () { ret.append(to_input); // 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"); + $(from_input).toggleClass("col-sm-4", true); + $(to_input).toggleClass("col-sm-4", true); return ret[0]; } @@ -1177,14 +1324,25 @@ var form_elements = new function () { */ 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 $('<div class="row caosdb-f-field caosdb-v-field" data-field-name="' + name + '" />')[0]; } + /** + * Return a new date field. + * + * @param {FieldConfig} config + * @return {HTMLElement} + */ 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); } @@ -1195,14 +1353,20 @@ var form_elements = new function () { * * `config.type` is set to "number" and overrides any other type. * - * @param {form_elements.input_config} config. + * @param {FieldConfig} config. * @returns {HTMLElement} a double form field. */ this.make_double_input = function (config) { - var clone = $.extend({}, config, { + const _config = $.extend({}, config, { type: "number" }); - var ret = $(this._make_input(clone)) + 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]; } @@ -1213,7 +1377,7 @@ var form_elements = new function () { * * `config.type` is set to "number" and overrides any other type. * - * @param {form_elements.input_config} config. + * @param {FieldConfig} config. * @returns {HTMLElement} an integer form field. */ this.make_integer_input = function (config) { @@ -1222,6 +1386,79 @@ var form_elements = new function () { 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" + }); + if (config.accept) { + $(ret) + .find(":input") + .attr("accept", config.accept); + } + + return ret; + } + + /** + * @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 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; + } + /** * Return a checkbox input field. @@ -1340,25 +1577,29 @@ var form_elements = new function () { * * @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) { + 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; - let input = $('<input class="form-control caosdb-f-property-single-raw-value" type="' + type + - '" name="' + name + - '" />'); - input.change(function () { + 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); + input_col.append(_input); if (value) { - input.val(value); + _input.val(value); } return ret.append(label, input_col)[0]; } @@ -1379,7 +1620,7 @@ var form_elements = new function () { let label = config.label; return label ? '<label for="' + name + '" data-property-name="' + name + - '" class="control-label col-sm-3">' + label + + '" class="col-form-label col-sm-3">' + label + '</label>' : ""; } diff --git a/src/core/js/form_panel.js b/src/core/js/form_panel.js new file mode 100644 index 0000000000000000000000000000000000000000..9728a4ccea54c36d85399a3148373b7372108db0 --- /dev/null +++ b/src/core/js/form_panel.js @@ -0,0 +1,94 @@ +/* + * ** header v3.0 + * This file is a part of the CaosDB Project. + * + * Copyright (C) 2021 IndiScale GmbH <info@indiscale.com> + * Copyright (C) 2021 Timm Fitschen <t.fitschen@indiscale.com> + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see <https://www.gnu.org/licenses/>. + * + * ** end header + */ + +'use strict'; + +/** + * form_panel module for creating a panel below the navbar where forms can be + * placed. + */ +var form_panel = new function () { + const logger = log.getLogger("form_panel"); + this.version = "0.1"; + this.dependencies = ["log", "caosdb_utils", "markdown", "bootstrap"]; + + /** + * Return a the panel which shall contain the form. + * + * Side-effects: + * 1. Creates the form panel if it does not exist. + * 2. Removes the welcome panel if present. + */ + this.get_form_panel = function (panel_id, title) { + // remove welcome + $(".caosdb-f-welcome-panel").remove(); + $(".caosdb-v-welcome-panel").remove(); + + var existing = $("#" + panel_id); + if (existing.length > 0) { + return existing[0]; + } + const panel = $('<div id="' + panel_id + '" class="caosdb-f-form-panel bg-light container mb-1"/>'); + const header = $('<h2 class="text-center">' + title + '</h2>'); + panel.append(header); + + // add to main panel + $('nav').after(panel); + + return panel[0]; + }; + + /** + * Remove the form panel from the DOM tree. + */ + this.destroy_form_panel = function (panel) { + $(panel).remove(); + }; + + /** + * Creates a callback function that toggles the form panel which + */ + this.create_show_form_callback = function (panel_id, title, form_config) { + return (e) => { + logger.trace("enter show_form_panel", e); + + const panel = $(form_panel.get_form_panel(panel_id, title)); + if (panel.find("form").length === 0) { + const form = form_elements.make_form(form_config); + panel.append(form); + $(form).find(".selectpicker").selectpicker(); + + form.addEventListener("caosdb.form.cancel", + (e) => form_panel.destroy_form_panel(panel), + true + ); + } + } + }; + + this.init = function () { + } +} + +$(document).ready(function () { + caosdb_modules.register(form_panel); +}); diff --git a/src/core/js/preview.js b/src/core/js/preview.js index b85f7b56dc13ff94aa2c3de90eb1b464d7f1e9d6..c5b7752b5070796ab01fb3d007065d2a4fc6b425 100644 --- a/src/core/js/preview.js +++ b/src/core/js/preview.js @@ -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."); } @@ -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..a0a7f877555dc6e2c2617892540c900be12a6a19 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,22 +931,6 @@ var query_shortcuts = new function() { this._cache_visibility_key = "caosdb.query-shortcuts-panel.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 this._updateEntities = update; this._insertEntities = insert; @@ -1103,12 +1059,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 55337a36a97d6a7ad42aadfe673e429d6d942a0a..03cc7bfbb399e898a88cb99dc0a05cc90289f96a 100644 --- a/src/core/js/webcaosdb.js +++ b/src/core/js/webcaosdb.js @@ -116,21 +116,12 @@ 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(); - if ($(menu).is("ul.caosdb-navbar")) { - // special styling for buttons which are added directly to the - // navbar - $(button_elem) - .toggleClass("navbar-btn", true) - .toggleClass("btn", true) - .toggleClass("btn-link", true); - } - logger.debug("add", wrapper, "to", menu); $(menu).append(wrapper); @@ -143,7 +134,7 @@ 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(); @@ -151,7 +142,7 @@ this.navbar = new function () { /** - * Initialize the hiding/showing of the input form. + * Initialize the hiding/showing of the login form. * * If the viewport is xs (width <= 768px) the login form is hidden in the * a menu anyways. @@ -164,7 +155,7 @@ this.navbar = new function () { * 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 10 seconds. + * reestablished and the form hides after 20 seconds. */ this.init_login_show_button = function () { const form = $("#caosdb-f-login-form"); @@ -173,32 +164,36 @@ this.navbar = new function () { // show form and hide the show_button const _in = () => { - // xs means viewport <= 768px - form.removeClass("visible-xs-inline-block"); - show_button.addClass("hidden"); + // xs means viewport <= 768px + 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 - form.addClass("visible-xs-inline-block"); - show_button.removeClass("hidden"); + // xs means viewport <= 768px + 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) + timeout = setTimeout(_out, 10000) }); form.find("input,button").on("blur", () => { if (timeout) { // cancel existing timeout (e.g. from showing) clearTimeout(timeout); } - // hide after 10 seconds if nothing happens - timeout = setTimeout(_out,10000) + // hide after 20 seconds if nothing happens + timeout = setTimeout(_out, 20000) }); - form.find("input,button").on("change", () => { + form.find("input,button").on("input", () => { // something happens! if (timeout) { clearTimeout(timeout); @@ -217,9 +212,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 @@ -312,6 +307,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); @@ -344,8 +346,8 @@ this.caosdb_utils = new function () { * @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); + 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)) @@ -357,6 +359,8 @@ this.caosdb_utils = new function () { * connection module contains all ajax calls. */ this.connection = new function () { + const logger = log.getLogger("connection"); + this._init = function () { /** * Send a get request. @@ -374,7 +378,7 @@ this.connection = new function () { if (error.status == 414) { throw new Error("UriTooLongException for GET " + uri); } else if (error.status == 0) { - console.log(error); + logger.error(error); } else if (error.status != null) { throw new Error("GET " + uri + " returned with HTTP status " + error.status + " - " + error.statusText); } else { @@ -398,7 +402,7 @@ this.connection = new function () { }); } catch (error) { if (error.status == 0) { - console.log(error); + logger.error(error); } else if (error.status != null) { throw new Error("PUT " + uri + " returned with HTTP status " + error.status + " - " + error.statusText); } else { @@ -433,7 +437,7 @@ this.connection = new function () { }); } catch (error) { if (error.status == 0) { - console.log(error); + logger.error(error); } else if (error.status != null) { throw new Error( "POST scripting returned with HTTP status " + error.status + @@ -459,7 +463,7 @@ this.connection = new function () { }); } catch (error) { if (error.status == 0) { - console.log(error); + logger.error(error); } else if (error.status != null) { throw new Error("POST " + uri + " returned with HTTP status " + error.status + " - " + error.statusText); } else { @@ -484,7 +488,7 @@ this.connection = new function () { }); } catch (error) { if (error.status == 0) { - console.log(error); + logger.error(error); } else if (error.status != null) { throw new Error("DELETE " + "Entity/" + idline + " returned with HTTP status " + error.status + " - " + error.statusText); } else { @@ -586,7 +590,6 @@ this.transformation = new function () { var xsl = await transformation.retrieveXsltScript("property.xsl"); insertParam(xsl, "filesystempath", connection.getBasePath() + "FileSystem/"); insertParam(xsl, "entitypath", connection.getBasePath() + "Entity/"); - insertParam(xsl, "close-char", '×'); var entityXsl = await transformation.retrieveXsltScript('entity.xsl'); var messageXsl = await transformation.retrieveXsltScript('messages.xsl'); var commonXsl = await transformation.retrieveXsltScript('common.xsl'); @@ -594,15 +597,6 @@ this.transformation = new function () { let html = await asyncXslt(xml, xslt); return html; } - /** - * @param {XMLDocument} xml - * @return {HTMLElement[]} an array of HTMLElements. - */ - this.transformEntityPalette = async function _tME(xml) { - var xsl = await transformation.retrieveXsltScript("entity_palette.xsl"); - let html = await asyncXslt(xml, xsl); - return html; - } /** * Retrieve the entity.xsl script and modify it such that we can use it @@ -615,14 +609,13 @@ 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/child::*" 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'); var xslt = transformation.mergeXsltScripts(entityXsl, [errorXsl, commonXsl]); insertParam(xslt, "filesystempath", connection.getBasePath() + "FileSystem/"); insertParam(xslt, "entitypath", connection.getBasePath() + "Entity/"); - insertParam(xslt, "close-char", '×'); xslt = injectTemplate(xslt, _root); return xslt; } @@ -746,11 +739,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); @@ -821,7 +814,7 @@ this.transaction = new function () { $(updatePanel).insertBefore(entity); // create and add waiting notification updatePanel.appendChild(transaction.update.createWaitRetrieveNotification()); - let entityId = getEntityId(entity); + let entityId = getEntityID(entity); transaction.update.retrieveOldEntityXmlString(entityId).then(xmlstr => { app.openForm(xmlstr); }, err => { @@ -868,7 +861,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. @@ -895,10 +888,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; } @@ -938,7 +931,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]; }; @@ -978,7 +971,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(); @@ -994,6 +987,9 @@ this.transaction = new function () { */ var version_history = new function () { + const logger = log.getLogger("version_history"); + this.logger = logger; + this._get = connection.get; /** * Retrieve the version history of an entity and return a table with the @@ -1002,7 +998,7 @@ var version_history = new function () { * @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) { + this.retrieve_history = async function (entity) { const xml = this._get(transaction .generateEntitiesUri([entity]) + "?H"); const html = (await transformation.transformEntities(xml))[0]; @@ -1044,6 +1040,7 @@ var version_history = new function () { .retrieve_history(entity_id_version); sparse.replaceWith(history_table); version_history.init_export_history_buttons(entity); + version_history.init_restore_version_buttons(entity); }); } } @@ -1059,8 +1056,8 @@ var version_history = new function () { 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); + const cells = $(row).find(".export-data").toArray().map(x => x.textContent); + rows.push(cells); } return caosdb_utils.create_tsv_table(rows); } @@ -1079,9 +1076,9 @@ var version_history = new function () { this.init_export_history_buttons = function (entity) { entity = entity || $(".caosdb-entity-panel"); for (let version_info of $(entity) - .find(".caosdb-f-entity-version-info")) { + .find(".caosdb-f-entity-version-info")) { $(version_info).find(".caosdb-f-entity-version-export-history-btn") - .click(async () => { + .click(() => { const html_table = $(version_info).find("table")[0]; const history_tsv = this.get_history_tsv(html_table); version_history._download_tsv(history_tsv); @@ -1089,7 +1086,73 @@ var version_history = new function () { } } - this._download_tsv = function(tsv_link) { + /** + * Initialize the restore old version buttons of `entity`. + * + * The buttons are only visible when the user is allowed to update the + * entity. + * + * The causes a retrieve of the specified version of the entity and then an + * update that restores that version. + * + * @param {HTMLElement} [entity] - if undefined, the export buttons of all + * page entities are initialized. + */ + this.init_restore_version_buttons = function (entity) { + var entities = [entity] || $(".caosdb-entity-panel"); + + for (let _entity of entities) { + // initialize buttons only if the user is allowed to update the entity + if (hasEntityPermission(_entity, "UPDATE:*") || hasEntityPermission(_entity, "UPDATE:DESCRIPTION")) { + for (let version_info of + $(_entity).find(".caosdb-f-entity-version-info")) { + // find the restore button + $(version_info).find(".caosdb-f-entity-version-restore-btn") + .toggleClass("d-none", false) // show button + .click(async (eve) => { + // the version id is stored in the restore button's + // data-version-id attribute + const versionid = eve.delegateTarget.getAttribute("data-version-id") + const reload = () => { + window.location.reload(); + } + const _alert = form_elements.make_alert({ + title: "Warning", + message: "You are going to restore this version of the entity.", + proceed_callback: async () => { + try { + await restore_old_version(versionid); + $(_alert).remove(); + // reload after sucessful update + $(version_info).find(".modal-body").prepend( + $(`<div class="alert alert-success" role="alert">Restore successful! <p>You are being forwarded to the latest version of this entity or you can click <a href="#" onclick="window.location.reload()">here</a>.</p></div>`)); + setTimeout(reload, 5000); + } catch (e) { + logger.error(e); + // print errors in an alert div + $(version_info).find(".modal-body").prepend( + $(`<div class="alert alert-danger alert-dismissible " role="alert"> <button class="btn-close" data-bs-dismiss="alert" aria-label="close"></button> Restore failed! <p>${e.message}</p></div>`)); + + } + }, + cancel_callback: () => { + // do nothing + $(_alert).remove(); + $(version_info).find("table").show(); + }, + proceed_text: "Yes, restore!", + remember_my_decision_id: "restore_entity", + }); + + $(version_info).find("table").after(_alert).hide(); + $(_alert).addClass("text-end"); + }); + } + } + } + } + + this._download_tsv = function (tsv_link) { window.location.href = tsv_link; } @@ -1097,6 +1160,7 @@ var version_history = new function () { this.init = function () { this.init_load_history_buttons(); this.init_export_history_buttons(); + this.init_restore_version_buttons(); } } @@ -1134,27 +1198,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. @@ -1275,6 +1334,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 () { @@ -1320,7 +1386,7 @@ var queryForm = new function () { var submithandler = function () { // store current query var queryField = form.query; - var value = queryField.value.toUpperCase(); + var value = queryField.value.toUpperCase().trim(); if (typeof value == "undefined" || value.length == 0) { return; } @@ -1334,12 +1400,12 @@ var queryForm = new function () { paging = form.P.value } - queryForm.redirect(queryField.value, paging); + queryForm.redirect(queryField.value.trim(), paging); }; $("#caosdb-query-textarea").on("keydown", (e) => { // prevent submit on enter - if(e.originalEvent.which == 13) { + if (e.originalEvent.which == 13) { e.originalEvent.preventDefault(); } }) @@ -1491,9 +1557,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(); @@ -1514,10 +1580,12 @@ function createErrorNotification(msg) { * Create a waiting notification with a informative message for the waiting user. * * @param {String} info, a message for the user + * @param {String} id, optional, the id of the message div. Default is empty * @return {HTMLElement} A div with class `caosdb-preview-waiting-notification`. */ -function createWaitingNotification(info) { - return $('<div class="' + globalClassNames.WaitingNotification + '">' + info + '</div>')[0]; +function createWaitingNotification(info, id) { + id = id ? `id="${id}"` : ""; + return $(`<div class="${globalClassNames.WaitingNotification}" ${id}>${info}</div>`)[0]; } /** @@ -1527,23 +1595,10 @@ function createWaitingNotification(info) { * @return {HTMLElement} The parameter `elem`. */ function removeAllWaitingNotifications(elem) { - $(elem.getElementsByClassName(globalClassNames.WaitingNotification)).remove(); + $(elem).find(`.${globalClassNames.WaitingNotification}`).remove(); return elem; } -/** - * Extract the ID of an entity by parsing the textContent of the first occuring element with - * class `caosdb-id`. - * - * @param {HTMLElement} entity - * @returns {Number} ID of entity. - */ -function getEntityId(entity) { - let id = Number.parseInt(entity.getElementsByClassName("caosdb-id")[0].textContent); - if (isNaN(id)) throw new Error("id was NaN"); - return id; -} - // TODO remove and use connection.post /** * Post an xml document to basepath/Entity @@ -1661,16 +1716,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; } /** @@ -1681,13 +1745,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; } @@ -1703,18 +1772,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 FIXME - */ + +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 TODO + * Initialize all the submodules. */ function initOnDocumentReady() { + paging.init(); hintMessages.init(); // init query form @@ -1725,20 +1924,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); + } } @@ -1797,4 +2000,4 @@ class _CaosDBModules { var caosdb_modules = new _CaosDBModules() -$(document).ready(initOnDocumentReady); +$(document).ready(initOnDocumentReady); \ No newline at end of file diff --git a/src/core/webcaosdb.xsl b/src/core/webcaosdb.xsl index cb1ad2966cb2006767309da96151196102c3af57..9a0d6769f1901e860c7a2318fce28957d6461496 100644 --- a/src/core/webcaosdb.xsl +++ b/src/core/webcaosdb.xsl @@ -27,6 +27,7 @@ <xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform"> <xsl:output method="html" /> + <xsl:include href="xsl/jsheader.xsl" /> <xsl:include href="xsl/main.xsl" /> <xsl:include href="xsl/navbar.xsl" /> <xsl:include href="xsl/messages.xsl" /> @@ -37,6 +38,17 @@ <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> + </div> + </xsl:template> + <xsl:template match="/"> <html lang="en"> <head> @@ -54,9 +66,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 cac9e87ea40bf6a687a19a1942b5534276c8faf7..d8ba00a9810e9861bfba741055ea113c7f2b88bc 100644 --- a/src/core/xsl/entity.xsl +++ b/src/core/xsl/entity.xsl @@ -25,40 +25,46 @@ <xsl:output method="html"/> <!-- These little colored Rs, RTs, Ps, and Fs which hilite the beginning of a new entity. --> + <xsl:template match="Entity" mode="entity-heading-label"> + <span class="badge caosdb-f-entity-role caosdb-label-entity me-1" + title="This is an entity. The role is not specified.">E</span> + </xsl:template> <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 +74,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,15 +95,15 @@ </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> </div> </xsl:template> <!-- Main entry for ENTITIES --> - <xsl:template match="Property|Record|RecordType|File" mode="entities"> - <div class="panel panel-default caosdb-entity-panel"> + <xsl:template match="Property|Record|RecordType|File|Response/Entity" mode="entities"> + <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,23 +111,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()"/> @@ -141,23 +152,23 @@ </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 btn-link caosdb-v-bookmark-button"> + <button class="btn caosdb-v-bookmark-button"> <xsl:attribute name="data-bmval"> <xsl:value-of select="@id"/> <xsl:if test="Version/Successor"> @@ -165,22 +176,27 @@ <xsl:value-of select="concat('@', Version/@id)"/> </xsl:if> </xsl:attribute> - <span class="glyphicon glyphicon-bookmark"/> + <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"> @@ -196,9 +212,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> @@ -231,47 +244,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"> @@ -283,7 +287,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> @@ -309,7 +313,7 @@ <!-- DEPRECATED css class .caosdb-property-text-value - Use .caosdb-f-property-single-raw-value or introduce new .caosdb-v-property-text-value --> - <xsl:value-of select="'caosdb-f-property-single-raw-value caosdb-property-text-value'"/> + <xsl:value-of select="'caosdb-f-property-single-raw-value caosdb-property-text-value caosdb-f-property-text-value caosdb-v-property-text-value'"/> </xsl:attribute> <xsl:call-template name="trim"> <xsl:with-param name="str"> @@ -324,7 +328,7 @@ <!-- DEPRECATED css class .caosdb-property-text-value - Use .caosdb-f-property-single-raw-value or introduce new .caosdb-v-property-text-value --> - <span class="caosdb-f-property-single-raw-value caosdb-property-text-value"/> + <span class="caosdb-f-property-single-raw-value caosdb-property-text-value caosdb-f-property-text-value caosdb-v-property-text-value"/> </xsl:otherwise> </xsl:choose> </xsl:template> @@ -368,7 +372,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'"/> @@ -455,43 +459,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"> @@ -504,22 +492,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"> @@ -527,7 +594,7 @@ <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 --> @@ -540,23 +607,24 @@ <div> <xsl:attribute name="class"> modal-header + 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="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> + <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> <!-- modal-header end --> <div class="caosdb-f-entity-version-history"> @@ -576,18 +644,21 @@ <div class="modal-body"> <table class="table table-hover"> <thead> - <tr><div class="export-data">Entity ID</div><th/> + <tr> + <th></th> + <th class="invisible"><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> - <div class="export-data">URI</div> + <th class="invisible"><div class="export-data">URI</div></th> + <th></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> - <div class="export-data"><xsl:value-of select="$entityId"/></div> + <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> @@ -595,7 +666,14 @@ </td><td class="export-data"> <xsl:value-of select="@username"/>@<xsl:value-of select="@realm"/> </td> - <div class="export-data"><xsl:value-of select="concat($entitypath, $entityId, '@', @id)"/></div> + <td class="invisible"><div class="export-data"><xsl:value-of select="concat($entitypath, $entityId, '@', @id)"/></div></td> + <td> + <xsl:if test="not(@head='true')"> + <button type="button" class="caosdb-f-entity-version-restore-btn btn btn-secondary d-none" title="Restore this version of the entity."> + <xsl:attribute name="data-version-id"><xsl:value-of select="$entityId"/>@<xsl:value-of select="@id"/></xsl:attribute> + <i class="bi-arrow-counterclockwise"></i></button> + </xsl:if> + </td> </tr> <xsl:apply-templates mode="entity-version-modal-predecessor" select="Predecessor"> <xsl:with-param name="entityId" select="$entityId"/> @@ -604,7 +682,7 @@ </table> </div> <div class="modal-footer"> - <button type="button" class="caosdb-f-entity-version-export-history-btn btn btn-default">Export history</button> + <button type="button" class="caosdb-f-entity-version-export-history-btn btn btn-secondary" title="Export this history table as a CSV file.">Export history</button> </div> </xsl:template> @@ -640,7 +718,7 @@ </table> </div> <div class="modal-footer"> - <button type="button" style="display: none" class="caosdb-f-entity-version-load-history-btn btn btn-default">Load full history</button> + <button type="button" style="display: none" class="caosdb-f-entity-version-load-history-btn btn btn-secondary">Load full history</button> </div> </xsl:template> @@ -648,7 +726,7 @@ <!-- a versions'id (abbreviated) --> <xsl:attribute name="title">Full Version ID: <xsl:value-of select="."/></xsl:attribute> <xsl:value-of select="substring(.,1,8)"/> - <div class="export-data"><xsl:value-of select="."/></div> + <td class="invisible"><div class="export-data"><xsl:value-of select="."/></div></td> </xsl:template> <xsl:template match="@date" mode="entity-version-date"> @@ -657,7 +735,7 @@ <xsl:value-of select="substring(.,0,11)"/> <xsl:value-of select="' '"/> <xsl:value-of select="substring(.,12,8)"/> - <div class="export-data"><xsl:value-of select="."/></div> + <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"> @@ -665,7 +743,7 @@ <xsl:param name="entityId"/> <xsl:param name="hint"/> <tr> - <div class="export-data"><xsl:value-of select="$entityId"/></div> + <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"> @@ -676,7 +754,15 @@ </td><td class="export-data"> <xsl:value-of select="@username"/>@<xsl:value-of select="@realm"/> </td> - <div class="export-data"><xsl:value-of select="concat($entitypath, $entityId, '@', @id)"/></div> + <td class="invisible"><div class="export-data"><xsl:value-of select="concat($entitypath, $entityId, '@', @id)"/></div></td> + <td> + <!-- include button if it is not head, i.e. Predecessors are always old and Successors if they do have a Successor Member --> + <xsl:if test="(name()='Predecessor' or Successor)"> + <button type="button" class="caosdb-f-entity-version-restore-btn btn btn-secondary d-none" title="Restore this version of the entity."> + <xsl:attribute name="data-version-id"><xsl:value-of select="$entityId"/>@<xsl:value-of select="@id"/></xsl:attribute> + <i class="bi-arrow-counterclockwise"></i></button> + </xsl:if> + </td> </tr> </xsl:template> @@ -723,7 +809,7 @@ <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> diff --git a/src/core/xsl/entity_palette.xsl b/src/core/xsl/entity_palette.xsl index 961a51dc51584c3fe87496e54f9837fcec9de0b0..9d3a13f1c96a7ce864554b78b9d9e0dbebfb3c5f 100644 --- a/src/core/xsl/entity_palette.xsl +++ b/src/core/xsl/entity_palette.xsl @@ -3,47 +3,37 @@ <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> - <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"> + <span class="card-title">Existing Properties</span> </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> </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"> + <span class="card-title">Existing RecordTypes</span> </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> </div> - <ul class="caosdb-v-edit-list"> + <ul class="caosdb-v-edit-list list-group"> <xsl:apply-templates select="./RecordType"/> </ul> - </div> </div> + </div> </xsl:template> <xsl:template match="RecordType"> <xsl:if test="string-length(@name)>0"> - <li class="caosdb-f-edit-drag list-group-item caosdb-v-edit-drag"> + <li draggable="true" class="caosdb-f-edit-drag list-group-item caosdb-v-edit-drag"> <xsl:attribute name="id">caosdb-f-edit-rt-<xsl:value-of select="@id"/></xsl:attribute> <xsl:value-of select="@name"/> </li> @@ -62,7 +52,7 @@ <!-- ignore unit property --> </xsl:when> <xsl:otherwise> - <li class="caosdb-f-edit-drag list-group-item caosdb-v-edit-drag"> + <li draggable="true" class="caosdb-f-edit-drag list-group-item caosdb-v-edit-drag"> <xsl:attribute name="id">caosdb-f-edit-p-<xsl:value-of select="@id"/></xsl:attribute> <xsl:value-of select="@name"/> </li> 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..ce491222ca8e018ced18167bdfc9e934c37fa4d8 100644 --- a/src/core/xsl/footer.xsl +++ b/src/core/xsl/footer.xsl @@ -26,20 +26,26 @@ <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="${BUILD_FOOTER_CONTACT_HREF}">Contact</a> + <span class="caosdb-bulletsep d-none d-md-inline">•</span> + <a href="${BUILD_FOOTER_IMPRINT_HREF}">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="${BUILD_FOOTER_LICENCE_HREF}" target="_blank">License (AGPL-v3)</a> + <span class="caosdb-bulletsep d-none d-md-inline">•</span> + <a href="${BUILD_FOOTER_SOURCES_HREF}" target="_blank">Sources</a> + <span class="caosdb-bulletsep d-none d-md-inline">•</span> + <a href="${BUILD_FOOTER_DOCS_HREF}" target="_blank">Documentation</a> </div> </xsl:template> </xsl:stylesheet> diff --git a/src/core/xsl/main.xsl b/src/core/xsl/main.xsl index e414f9af4147714c5f6ff8a03b70309a02ec1446..1a0bdb812afb18ad6ab291acb975de994c5d3e47 100644 --- a/src/core/xsl/main.xsl +++ b/src/core/xsl/main.xsl @@ -85,207 +85,17 @@ <xsl:value-of select="concat($basepath,'webinterface/${BUILD_NUMBER}/css/bootstrap-select.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> - <xsl:element name="script"> - <xsl:attribute name="src"> - <xsl:value-of select="concat($basepath,'webinterface/${BUILD_NUMBER}/js/pako.js')"/> - </xsl:attribute> - </xsl:element> - <xsl:element name="script"> - <xsl:attribute name="src"> - <xsl:value-of select="concat($basepath,'webinterface/${BUILD_NUMBER}/js/utif.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/ext_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_table_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_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/ext_file_download.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: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> - <xsl:element name="script"> - <xsl:attribute name="src"> - <xsl:value-of select="concat($basepath,'webinterface/${BUILD_NUMBER}/js/ext_bookmarks.js')"/> + <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> - <!--JS_EXTENSIONS--> + <!--CSS_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/*"/> @@ -293,23 +103,27 @@ <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 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> + </div> + </div> </div> - <div class="caosdb-f-edit-panel-body panel-body"></div> - </div> </div> </xsl:template> <xsl:template match="*" mode="entities"/> <xsl:template match="*" mode="top-level-data"/> - <xsl:variable name="close-char" select="'×'"/> <!-- assure that this uri ends with a '/' --> <xsl:template name="uri_ends_with_slash"> <xsl:param name="uri"/> 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 6ce69e638efda10dbb95a066e8b59508854a4435..529945d7c37a95a0687473dce34f0269e2942c92 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,121 +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"> - <li class="dropdown"> - <a class="dropdown-toggle" data-toggle="dropdown" href="#"> - <span id="caosdb-f-bookmarks-collection-counter" class="badge">0</span> + </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 - <span class="caret"></span></a> - <ul class="dropdown-menu"> + </a> + <ul class="dropdown-menu dropdown-menu-end dropdown-menu-light" aria-labelledby="navbarBookmarkMenuLink"> <li class="disabled" id="caosdb-f-bookmarks-collection-link" title="Show all bookmarked entities."> - <a>Show all</a></li> + <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>Export to file</a></li> + <a class="dropdown-item">Export to file</a></li> <li class="disabled" id="caosdb-f-bookmarks-clear" title="Empty the list of bookmarks."> - <a>Clear</a></li> + <a class="dropdown-item">Clear</a></li> </ul> </li> - <xsl:call-template name="caosdb-user-menu"/> + <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> @@ -166,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"> @@ -185,52 +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-end dropdown-menu-light"> + <xsl:if test="/Response/@realm='${BUILD_MODULE_USER_MANAGEMENT_CHANGE_OWN_PASSWORD_REALM}'"> + <li> + <a class="dropdown-item" title="Change your password." href="#" data-bs-toggle="modal" data-bs-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 id="caosdb-f-login-form" class="navbar-form visible-xs-inline-block" 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"/> + </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> - <button style="margin-right: 15px" class="btn btn-default navbar-btn hidden-xs" id="caosdb-f-login-show-button" type="button">Login</button> </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 ca1884aea16d59c7df92de304d3154698dc471bf..702a390f28ada96e140f40f10218b1740ab10700 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,16 +50,18 @@ </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"> <span>Query: </span> - <xsl:value-of select="@string"/> + <span class="caosdb-f-query-response-string"> + <xsl:value-of select="@string"/> + </span> </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 +70,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 +83,36 @@ </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 caosdb-v-btn-select" data-target="#downloadModal" data-toggle="modal" type="button">Export</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 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 table as XLS 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-xls" 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-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."> @@ -132,7 +134,7 @@ <div class="col-xs-6 caosdb-f-modal-footer-left"> </div> <div class="col-xs-6"> - <button class="btn btn-default" data-dismiss="modal" type="button">Close</button> + <button class="btn btn-secondary" data-bs-dismiss="modal" type="button">Close</button> </div> </div> </div> @@ -142,59 +144,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> @@ -203,29 +218,65 @@ <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:with-param name="first-segment"> - <xsl:value-of select="substring-before(concat($field-name, '.'), '.')"/> - </xsl:with-param> - <xsl:with-param name="next-segments"> - <xsl:value-of select="substring-after($field-name, '.')"/> - </xsl:with-param> - </xsl:apply-templates> + <xsl:choose> + <xsl:when test="$version-id!=''"> + <xsl:apply-templates select="/Response/*[@id=$entity-id and Version/@id=$version-id]" mode="walk-select-segments"> + <xsl:with-param name="first-segment"> + <xsl:value-of select="substring-before(concat($field-name, '.'), '.')"/> + </xsl:with-param> + <xsl:with-param name="next-segments"> + <xsl:value-of select="substring-after($field-name, '.')"/> + </xsl:with-param> + </xsl:apply-templates> + </xsl:when> + <xsl:otherwise> + <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> + <xsl:with-param name="next-segments"> + <xsl:value-of select="substring-after($field-name, '.')"/> + </xsl:with-param> + </xsl:apply-templates> + </xsl:otherwise> + </xsl:choose> </div> </td> </xsl:template> <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="."/> @@ -234,9 +285,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> @@ -271,15 +322,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> @@ -293,21 +353,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..c3c80222492abaf243cbf227b143a9c13546a3f1 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 container"> <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/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/extension/forms.rst b/src/doc/extension/forms.rst index 1bced612b5f9517c5ec149871cbf53321b4671d4..e1891b8d7e571c4a273cec98fcc39e50398936f0 100644 --- a/src/doc/extension/forms.rst +++ b/src/doc/extension/forms.rst @@ -45,6 +45,36 @@ On submission, the function ``my_special_submit_handler`` is being called with t 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. + +Placing the form in a panel below the navbar +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +There are functions in the `form_panel` module to make it easy to place forms at the typical location: +below the navbar. The following shows how the config (see above) is passed to +`init_show_form_panel_button` a direct call to `make_form` is no longer necessary. + +.. code-block:: javascript + + const title = "Upload CSV File"; // title of the form and text in the toolbox + const panel_id = "csv_upload_form_panel"; + + /** + * Add a button to the navbar, saying "Upload CSV File" which opens a + * form for file upload. + */ + const init_show_form_panel_button = function () { + navbar.add_tool(title, tool_box, { + callback: form_panel.create_show_form_callback( + panel_id, + title, + csv_form_config) + }); + }; + + const init = function () { + init_show_form_panel_button(); + } + Calling a server-side script ^^^^^^^^^^^^^^^^^^^^^^^^^^^^ diff --git a/doc/QueryShortcuts/choose_edit.png b/src/doc/extension/images/choose_edit.png similarity index 100% rename from doc/QueryShortcuts/choose_edit.png rename to src/doc/extension/images/choose_edit.png diff --git a/doc/QueryShortcuts/create_shortcut.png b/src/doc/extension/images/create_shortcut.png similarity index 100% rename from doc/QueryShortcuts/create_shortcut.png rename to src/doc/extension/images/create_shortcut.png diff --git a/doc/QueryShortcuts/create_success.png b/src/doc/extension/images/create_success.png similarity index 100% rename from doc/QueryShortcuts/create_success.png rename to src/doc/extension/images/create_success.png diff --git a/doc/QueryShortcuts/delete_shortcuts.png b/src/doc/extension/images/delete_shortcuts.png similarity index 100% rename from doc/QueryShortcuts/delete_shortcuts.png rename to src/doc/extension/images/delete_shortcuts.png diff --git a/doc/QueryShortcuts/delete_success.png b/src/doc/extension/images/delete_success.png similarity index 100% rename from doc/QueryShortcuts/delete_success.png rename to src/doc/extension/images/delete_success.png diff --git a/doc/QueryShortcuts/edit_shortcut.png b/src/doc/extension/images/edit_shortcut.png similarity index 100% rename from doc/QueryShortcuts/edit_shortcut.png rename to src/doc/extension/images/edit_shortcut.png diff --git a/doc/QueryShortcuts/edit_success.png b/src/doc/extension/images/edit_success.png similarity index 100% rename from doc/QueryShortcuts/edit_success.png rename to src/doc/extension/images/edit_success.png diff --git a/doc/QueryShortcuts/shortcut_toolbox.png b/src/doc/extension/images/shortcut_toolbox.png similarity index 100% rename from doc/QueryShortcuts/shortcut_toolbox.png rename to src/doc/extension/images/shortcut_toolbox.png diff --git a/src/doc/extension/module.md b/src/doc/extension/module.md new file mode 100644 index 0000000000000000000000000000000000000000..207d3fff9d013cda657ef8c01f3cca5988622f4a --- /dev/null +++ b/src/doc/extension/module.md @@ -0,0 +1,92 @@ +# 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 for each new module. We have the convention, that extensions +which are optional and should stay that way and also custom extensions for +special purposes to name the file 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) + */ +const ext_flight_preview = function (libA, libB) { + + const init = function () { + /* initialization of the module */ + } + + /* doc string */ + const 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 +}(libA, libB); + +// 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); + } +}); +``` + +## Install the module + +The new new file should be placed in `src/core/js` if it is intended to be merged into the main repository eventually. For development purposes and for custom extensions which are not to be published you may place it in `src/ext/js`. + +Everything inside `src/core/js` and `src/ext/js` will eventually being loaded. +So, if there are no other modules which depend on this particular new module, +you are done. + +Otherwise, when we need to configure the order in which the +module is being loaded. + + +### Dependency order + +#### For Upstream Code + +For modules which are about to be merged into the main or dev branch of this +repository, add the module's file to `build.properties.d/00_default.properties` +at the right location in the list of module files (Array +`MODULE_DEPENDENCIES`). The list defines the order in which module files are +being loaded. + +#### For Custom Extensions + +For modules which will not be published and merged with the main repository you +may append all your module files in the desired order to the +`MODULE_DEPENDENCIES` array in a new `*.properties` file (e.g. +`build.properties.d/99_local_stuff`): + + MODULE_DEPENDENCIES+=(libA.js libB.js ext_flight_preview.js) + +In this example, `libA.js`, `libB.js` and `ext_flight_preview.js` are custom modules developed for this particular CaosDB webui instance. diff --git a/src/doc/extension/query_templates.rst b/src/doc/extension/query_templates.rst new file mode 100644 index 0000000000000000000000000000000000000000..015d26a21fe20bc09dc97db9c7a3e7a1ca58a0b3 --- /dev/null +++ b/src/doc/extension/query_templates.rst @@ -0,0 +1,216 @@ +Introduction +============ + +The WebUI supports the creation of query shortcuts which appear below +the normal query input field. These shortcuts facilitate looking for +data as query strings which are used frequently. They can be stored and +reused. + +.. figure:: images/shortcut_toolbox.png + :alt: The Shortcuts in the Query Panel; Note the Toolbox for in the + top right + + The Shortcuts in the Query Panel; Note the Toolbox for in the top + right + +There are two ways to integrate query templates into the WebUI: + +- | Global shortcuts are integrated by the webmaster only. They are + defined and stored in a + | ``./conf/ext/json/global_query_shortcuts.json`` in the root + directory of the webui. (See the example below.) + +- User-defined templates can be defined by users and are only visible + for the user who created them. In this sense, user-defined shortcuts + are also private, whereas global shortcuts are always publicly + visible. + +User-defined Query Shortcuts +============================ + +Create a New Shortcut +--------------------- + +New Query Shortcuts can be generated by any authenticated user with +sufficient write permissions. + +In the web interface, click ``Query``. In the ``Shortcuts`` section, +click the wrench (on the right side). + +In the drop-down menu, click ``Create``. + +It now opens a form with two input fields, ``Description`` and +``Query``. + +.. figure:: images/create_shortcut.png + :alt: The view to create a new shortcut + + The view to create a new shortcut + +See `2.4 <#basic-shortcut>`__ and `2.5 <#advanced-shortcut>`__ for +further explanation of the components of a Query Shortcut. + +Edit the fields and click ``Submit`` for the creation of the new +shortcut or click ``Cancel`` to cancel the process. + +The new shortcut is shown in the shortcuts section. + +.. figure:: images/create_success.png + :alt: The view when creation was successful + + The view when creation was successful + +Change an Existing Shortcut +--------------------------- + +Existing Query Shortcuts which are visible in your shortcuts section can +be edited directly in the shortcuts section. + +In the web interface, click ``Query``. In the ``Shortcuts`` section, +click the wrench (on the right side). + +In the drop-down menu, click ``Edit``. + +.. figure:: images/choose_edit.png + :alt: Choosing which shortcut to edit + + Choosing which shortcut to edit + +Every editable shortcut (note: global shortcuts are not editable in the +webinterface at all) will receive a new button ``Edit`` + +Click ``Edit`` of the shortcut that is to be changed. + +It now opens a form with two input fields, ``Description`` and +``Query``, pre-filled. + +See `2.4 <#basic-shortcut>`__ and `2.5 <#advanced-shortcut>`__ for +further explanation of the components of a Query Shortcut. + +Edit the fields and click ``Submit`` for the creation of the new +shortcut or click ``Cancel`` to cancel the process. + +The updated shortcut is shown in the shortcuts section. + +See the + +Delete an Existing Shortcut +--------------------------- + +Existing Query Shortcut which are visible in your shortcuts section can +be edited directly in the shortcuts section. + +In the web interface, click ``Query``. In the ``Shortcuts`` section, +click the wrench (on the right side). + +In the drop-down menu, click ``Delete``. + +.. figure:: images/delete_shortcuts.png + :alt: Choosing which shortcuts to delete + + Choosing which shortcuts to delete + +Every user-defined shortcut (note: global shortcuts are not deletable in +the webinterface at all) will receive checkbox and ``Delete`` and +``Cancel`` buttons appear at the bottom of the shortcuts section. + +Check all shortcuts which are to be deleted and click ``Delete`` or +click ``Cancel`` to cancel the deletion. + +All deleted shortcuts are marked as deleted afterwards and will not +appear again in the shortcuts section after reload. + +Basic Shortcut +-------------- + +The ``Description`` is a verbose definition of the query, e.g. “Search +for experiments and return a table.â€. It will be the text that is +visible in the shortcuts section. + +The ``Query`` is the query that will be executed with the shortcut. It +adheres to the definition of the CaosDB Query Language (CQL). + +The corresponding query of our example is +``SELECT date, name FROM Experiment``. + +Advanced Shortcut +----------------- + +The basic shortcut does not allow for any parameterization. It is just a +plain string or like a bookmark. + +Advanced shortcuts use a special syntax, where text placeholders are +used to define parameters of the shortcut. The parameters can be set by +the user at the time of the execution. An example can best illustrate +what that means: + +Suppose you want to search for experiments by their year. The query for +that would be ``SELECT date, name FROM Experiment WITH date IN 2018``. + +Now, the actual year in the query can be made editable by replacing the +year ``2018`` with ``{year}``. + +The ``Description`` now must also contain this placeholder ``{year}``, +e.g. “Search for experiements conducted in year {year}â€. When the +shortcut is displayed in the shortcuts section below the query input +field, the placeholder is replaced by a text input field and the user +can insert a year and execute the shortcut with the year being inserted +into the query. + +Placeholders +~~~~~~~~~~~~ + +The placeholders have simple rules. A placeholder always starts and ends +with curly brackets, like in the example ``{year}``. The text inside the +brackets (the placeholder’s *id*) may contain any combination of +alphanumeric signs (0-9,a-z,A-Z). The use of special characters like +colons, commas or the like is discouraged. They are reserved for future +extensions of the placeholders. Apart from that, you are free to choose +any placeholder *id* that seems suitable for you. + +Both components of the query shortcut (description and query) must +contain the same set of placeholders, otherwise the query shortcuts +might not work as intended. If there is a ``{year}`` in the query, there +must be a ``{year}`` in the description. + +Each placeholder *id* must occur only once in both components – if you +need to use two years in your shortcut you have to use ``{year1}`` and +``{year2}`` or any other combinations of placeholder *ids*. + +Example for global_query_shortcuts.json +--------------------------------------- + +The following example for the file global_query_shortcuts.json would create two global query shortcuts for finding experiments. The second example includes a variable for specifying the year. + +.. code-block:: json + + [ + { + "description": "Show a list of all Experiments", + "query": "FIND Record Experiment" + }, + { + "description": "Show a table of Experiments for year: {year}", + "query": "SELECT date, project, identifier FROM Record Experiment with date in {year}" + }, + ] + +Data Model for User Query Templates +----------------------------------- + +The current default data model for CaosDB does not include the RecordTypes which are needed for the user query templates. See https://gitlab.indiscale.com/caosdb/src/caosdb-webui/-/issues/104 for details. + +The solution is to create the RecordTypes, e.g. using the Python interface, as follows: + +.. code-block:: python + + datamodel = caosdb.Container() + datamodel.extend([ + caosdb.Property("Query", datatype=caosdb.TEXT), + caosdb.Property("templateDescription", datatype=caosdb.TEXT), + caosdb.RecordType( + "UserTemplate" + ).add_property("Query", importance=caosdb.OBLIGATORY + ).add_property("templateDescription", importance=caosdb.OBLIGATORY), + ]) + datamodel.insert() diff --git a/src/doc/extension/references.rst b/src/doc/extension/references.rst new file mode 100644 index 0000000000000000000000000000000000000000..63c551612e5e9d807846595b6c5e458bc5096615 --- /dev/null +++ b/src/doc/extension/references.rst @@ -0,0 +1,38 @@ +Customizing the display of referenced entities +============================================= + +CaosDB WebUI supports the customized display of referenced entities +using the :doc:`ext_references <../api/module-resolve_references>` +module. The ``BUILD_MODULE_EXT_RESOLVE_REFERENCES`` build variable has +to be set to ``ENABLED`` (see :doc:`/getting_started`) in order to use +this module. + +You may then define your own JavaScript module to define how +references to specific Records should be resolved. The module has to +be located at a directory which is known to CaosDB WebUI; we recommend +``caosdb-webui/src/ext/js``. Set the value of the +``BUILD_EXT_REFERENCES_CUSTOM_RESOLVER`` build variable to the name of +this module. The module has to have a ``resolve`` function which takes +an entity id as its only parameter and returns a ``reference_info`` +object with the resolved custom reference as a ``text`` property. So +the basic structure of the module should look like + +.. code-block:: javascript + + var my_reference_resolver = new function () { + // Has to be called ``resolve`` and has to take exactly one + // string parameter: the id of the referenced entity. + this.resolve = async function (id) { + /* + * find the string that the reference should be resolved to, + * e.g., from the value of the entity's properties. + */ + return {"text": new_reference_text} + } + } + +An example is located in +``caosdb-webui/src/ext/js/person_reference_resolver.js``. It resolves +any reference to a ``Person`` Record to the value of its ``firstname`` +and ``lastname`` properties separated by a space and is active by +default. 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/index.rst b/src/doc/index.rst index 107c9052fd6cdafecd201eb17118d8e56f3da440..24c394349a045bf276c4252a2fde47feae6f533c 100644 --- a/src/doc/index.rst +++ b/src/doc/index.rst @@ -10,6 +10,7 @@ Welcome to the documentation of CaosDB's web UI! Getting started <getting_started> Tutorials <tutorials/index> Concepts <concepts> + administration/index.rst Extending the UI <extension> API <api/index> 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..3e7d9ffb4472ab8ff0ec9a4539ab9ab11563ffd2 --- /dev/null +++ b/src/doc/tutorials/edit_mode.rst @@ -0,0 +1,185 @@ +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. 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/index.rst b/src/doc/tutorials/index.rst index dbfeafed2a09d587ebd538a62ed9002943b52aa0..b01e45e3d574cd675b8410da1dbd1d79cb8e20c0 100644 --- a/src/doc/tutorials/index.rst +++ b/src/doc/tutorials/index.rst @@ -8,4 +8,7 @@ This chapter contains the following tutorials: :maxdepth: 2 :glob: + first_steps + query + edit_mode * 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/ext/js/person_reference_resover.js b/src/ext/js/person_reference_resover.js new file mode 100644 index 0000000000000000000000000000000000000000..393557354904787f04472585bca0883d64200d86 --- /dev/null +++ b/src/ext/js/person_reference_resover.js @@ -0,0 +1,65 @@ +/* + * ** header v3.0 + * This file is a part of the CaosDB Project. + * + * Copyright (C) 2021 IndiScale GmbH (info@indiscale.com) + * Copyright (C) 2021 Florian Spreckelsen (f.spreckelsen@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 + */ + +/** + * @module person_reference + * + * Replace the reference to a Person Record by the values of that + * Record's firstname and lastname properties. + * + * TODO: Make name(s) of person RecordType(s) and names of firstname + * and lastname properties configurable. + */ +var person_reference = new function () { + + var logger = log.getLogger("person_reference"); + + const lastname_prop_name = "lastname" + const firstname_prop_name = "firstname" + const person_rt_name = "Person" + + /** + * Return the name of a person as firstname + lastname + */ + this.get_person_str = function (el) { + var valpr = getProperties(el); + if (valpr == undefined) { + return; + } + return valpr.filter(valprel => + valprel.name.toLowerCase().trim() == + firstname_prop_name.toLowerCase())[0].value + + " " + + valpr.filter(valprel => valprel.name.toLowerCase().trim() == + lastname_prop_name.toLowerCase())[0].value; + } + + this.resolve = async function (id) { + + const entity = (await resolve_references.retrieve(id))[0]; + + if (resolve_references.is_child(entity, person_rt_name)) { + return {"text": person_reference.get_person_str(entity)}; + } + } +} 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/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 ea7b63b9e37943f0bdd4e32499c6c1ea9310a618..0dfea85580fe64d0f7a4676f1d919ec5662dee09 100644 --- a/test/core/index.html +++ b/test/core/index.html @@ -22,79 +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/utif.js"></script> - <script src="js/pako.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_file_download.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_table_preview.js"></script> - <script src="js/ext_bottom_line.js"></script> - <script src="js/ext_revisions.js"></script> - <script src="js/ext_autocomplete.js"></script> - <script src="js/ext_sss_markdown.js"></script> - <script src="js/ext_trigger_crawler_form.js"></script> - <script src="js/ext_bookmarks.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/ext_file_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/ext_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> - <script src="js/modules/ext_bookmarks.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> + <!--JS_INCLUDE--> + </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/caosdb.js.js b/test/core/js/modules/caosdb.js.js index 20dc4b4eee0116fecf57b0a178f622c4385f08b2..8df5e2f9c2b933cd7b678286295961f2c73d7113 100644 --- a/test/core/js/modules/caosdb.js.js +++ b/test/core/js/modules/caosdb.js.js @@ -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]); } }); 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..ae1a51d837348ba0ba9c31f48a28a69ef2c9ad7b 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", @@ -400,8 +400,28 @@ QUnit.test("unfreeze", function (assert) { assert.ok(edit_mode.unfreeze); }); -QUnit.test("retrieve_datatype_list", function (assert) { +QUnit.test("retrieve_datatype_list", async function (assert) { assert.ok(edit_mode.retrieve_datatype_list); + var query_done; + + edit_mode.query = function (query) { + var re = /^FIND (Record|File) "IceCore"$/g; + assert.ok(query.match(re), `${query} should match ${re}`); + query_done(); + return []; + } + query_done = assert.async(2); + await edit_mode.retrieve_datatype_list("IceCore"); + + + edit_mode.query = function (query) { + var re = /^FIND (Record|File) "Ice Core"$/g; + assert.ok(query.match(re), `${query} should match ${re}`); + query_done(); + return []; + } + query_done = assert.async(2); + await edit_mode.retrieve_datatype_list("Ice Core"); }); QUnit.test("highlight", function (assert) { @@ -420,10 +440,6 @@ QUnit.test("get_edit_panel", function (assert) { assert.ok(edit_mode.get_edit_panel); }); -QUnit.test("add_wait_datamodel_info", function (assert) { - assert.ok(edit_mode.add_wait_datamodel_info); -}); - QUnit.test("toggle_edit_panel", function (assert) { assert.ok(edit_mode.toggle_edit_panel); }); @@ -485,100 +501,90 @@ 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>`; - - edit_mode.query = async function(q) { - return []; - } - - QUnit.test("test case 1 - insert property", async function (assert) { +edit_mode.query = async function(q) { + return []; +} - // here lives the test tool box - const test_tool_box = $('<div class="caosdb-f-edit-panel-body" />'); +QUnit.test("test case 1 - insert property", async function (assert) { - // here live the entities - const main_panel = $('<div class="caosdb-f-main-entities"/>'); - assert.equal($(".caosdb-f-main-entities").length, 0); + // here lives the test tool box + const test_tool_box = $('<div class="caosdb-f-edit-panel-body" > <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>'); - $(document.body).append(test_tool_box).append(main_panel); + // here live the entities + const main_panel = $('<div class="caosdb-f-main-entities"/>'); + assert.equal($(".caosdb-f-main-entities").length, 0); + $(document.body).append(test_tool_box).append(main_panel); - // ENTER EDIT MODE - assert.equal(edit_mode.is_edit_mode(), false, "edit_mode should not be active"); - // fake server response - edit_mode.retrieve_data_model = async function () { - return str2xml(datamodel); - } - var app = await edit_mode.enter_edit_mode(); - assert.equal(edit_mode.is_edit_mode(), true, "now, edit_mode should be active"); + // ENTER EDIT MODE + assert.equal(edit_mode.is_edit_mode(), false, "edit_mode should not be active"); + // fake server response + edit_mode.retrieve_data_model = async function () { + return str2xml("<Response/>"); + } + var app = await edit_mode.enter_edit_mode(); + assert.equal(edit_mode.is_edit_mode(), true, "now, edit_mode should be active"); - // NEW PROPERTY - assert.equal($(".caosdb-f-edit-panel-new-button.new-property").length, 2, "two new-property buttons should be present"); - assert.equal($(".caosdb-entity-panel").length, 0, "no entities"); - assert.equal(app.state, "initial", "initial state"); - // click on "new property" - $(".caosdb-f-edit-panel-new-button.new-property").first().click(); - while (app.state === "initial") { - await sleep(500); - } + // NEW PROPERTY + assert.equal($(".caosdb-f-edit-panel-new-button.new-property").length, 1, "one new-property button should be present"); + assert.equal($(".caosdb-entity-panel").length, 0, "no entities"); + assert.equal(app.state, "initial", "initial state"); + // click on "new property" + $(".caosdb-f-edit-panel-new-button.new-property").first().click(); - // EDIT PROPERTY - assert.equal(app.state, "changed", "changed state"); - var entity = $(".caosdb-entity-panel"); - assert.equal(entity.length, 1, "entity added"); - // set name - $(".caosdb-entity-panel .caosdb-f-entity-name").val("TestProperty"); - - // SAVE - var save_button = $(".caosdb-f-entity-save-button"); - assert.equal(save_button.length, 1, "save button available"); - // fake server response - connection.post = async function (uri, data) { - await sleep(500); - assert.equal(xml2str(data), "<Request><Property name=\"TestProperty\" datatype=\"TEXT\"/></Request>"); - assert.equal(app.state, "wait", "in wait state"); - return str2xml("<Response><Property id=\"newId\" name=\"TestProperty\" datatype=\"TEXT\"/></Response>"); - } - // click save button - var updated_entity = main_panel.find(".caosdb-entity-panel .caosdb-id:contains('newId')"); - assert.equal(updated_entity.length, 0, "entity with id not yet in main panel"); - save_button.click(); + while (app.state === "initial") { + await sleep(500); + } - while (app.state === "changed" || app.state === "wait") { - await sleep(500); - } + // EDIT PROPERTY + assert.equal(app.state, "changed", "changed state"); + var entity = $(".caosdb-entity-panel"); + assert.equal(entity.length, 1, "entity added"); + // set name + $(".caosdb-entity-panel .caosdb-f-entity-name").val("TestProperty"); + + // SAVE + var save_button = $(".caosdb-f-entity-save-button"); + assert.equal(save_button.length, 1, "save button available"); + // fake server response + connection.post = async function (uri, data) { + await sleep(500); + assert.equal(xml2str(data), "<Request><Property name=\"TestProperty\" datatype=\"TEXT\"/></Request>"); + assert.equal(app.state, "wait", "in wait state"); + return str2xml("<Response><Property id=\"newId\" name=\"TestProperty\" datatype=\"TEXT\"/></Response>"); + } + // click save button + var updated_entity = main_panel.find(".caosdb-entity-panel .caosdb-id:contains('newId')"); + assert.equal(updated_entity.length, 0, "entity with id not yet in main panel"); + save_button.click(); - // SEE RESPONSE - assert.equal(app.state, "initial", "initial state"); + while (app.state === "changed" || app.state === "wait") { + await sleep(500); + } - var response = $("#newId"); - assert.equal(response.length, 1, "entity added"); + // SEE RESPONSE + assert.equal(app.state, "initial", "initial state"); - // entity has been added to main panel - updated_entity = main_panel.find(".caosdb-entity-panel .caosdb-id:contains('newId')"); - assert.equal(updated_entity.length, 1, "entity with new id now in main panel"); + var response = $("#newId"); + assert.equal(response.length, 1, "entity added"); - // tests for closed issue https://gitlab.com/caosdb/caosdb-webui/issues/47 - assert.equal(main_panel.find(".caosdb-entity-panel .caosdb-entity-actions-panel").length, 1, "general actions panel there"); - assert.equal(main_panel.find(".caosdb-entity-panel .caosdb-f-edit-mode-entity-actions-panel").length, 1, "edit_mode actions panel there (BUG caosdb-webui#47)"); + // entity has been added to main panel + updated_entity = main_panel.find(".caosdb-entity-panel .caosdb-id:contains('newId')"); + assert.equal(updated_entity.length, 1, "entity with new id now in main panel"); - main_panel.remove(); - test_tool_box.remove(); + // tests for closed issue https://gitlab.com/caosdb/caosdb-webui/issues/47 + assert.equal(main_panel.find(".caosdb-entity-panel .caosdb-entity-actions-panel").length, 1, "general actions panel there"); + assert.equal(main_panel.find(".caosdb-entity-panel .caosdb-f-edit-mode-entity-actions-panel").length, 1, "edit_mode actions panel there (BUG caosdb-webui#47)"); - edit_mode.leave_edit_mode(); - assert.equal(edit_mode.is_edit_mode(), false, "edit_mode should not be active"); + main_panel.remove(); + test_tool_box.remove(); - }); + edit_mode.leave_edit_mode(); + assert.equal(edit_mode.is_edit_mode(), false, "edit_mode should not be active"); -} +}); var transformProperty = async function (xml_str) { diff --git a/test/core/js/modules/entity.xsl.js b/test/core/js/modules/entity.xsl.js index 59a2f8a6f9a03cf4990fe11bfd751d437a3ffc3e..e5ff1e8700b8349cddbdda0a0b52ffef47e4e75f 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); @@ -169,7 +154,7 @@ QUnit.test("single-value template with reference property.", function(assert) { 'value': '', 'reference': 'true', 'boolean': 'false' - })), "<span xmlns=\"http://www.w3.org/1999/xhtml\" class=\"caosdb-f-property-single-raw-value caosdb-property-text-value\"></span>", "empty value produces empty span."); + })), "<span xmlns=\"http://www.w3.org/1999/xhtml\" class=\"caosdb-f-property-single-raw-value caosdb-property-text-value caosdb-f-property-text-value caosdb-v-property-text-value\"></span>", "empty value produces empty span."); let link = callTemplate(this.entityXSL, 'single-value', { 'value': '1234', 'reference': 'true', @@ -332,14 +317,20 @@ 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, root) { + 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); + root = root || '<root/>'; + return xslt(str2xml(root), modXsl); } diff --git a/test/core/js/modules/ext_applicable.js.js b/test/core/js/modules/ext_applicable.js.js new file mode 100644 index 0000000000000000000000000000000000000000..596f41c9c9a80e47f4a8ebd8268194af4eba1236 --- /dev/null +++ b/test/core/js/modules/ext_applicable.js.js @@ -0,0 +1,30 @@ +/* + * ** 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_applicable", {}); + +QUnit.test("availability", function(assert) { + assert.equal(ext_applicable.version, "0.1"); +}); diff --git a/test/core/js/modules/ext_autocomplete.js.js b/test/core/js/modules/ext_autocomplete.js.js index e8776f945b7bb46a0d431eb2d0ac0f7fe21419fc..96cab766fb848b74b04677f9b3312b574b9a3844 100644 --- a/test/core/js/modules/ext_autocomplete.js.js +++ b/test/core/js/modules/ext_autocomplete.js.js @@ -25,7 +25,7 @@ QUnit.module("ext_autocomplete.js", { before: function (assert){ ext_autocomplete.retrieve_names = async function () { - return ['IceCore', 'Bag', 'IceSample', 'IceCream', 'Palette']; + return ['IceCore', 'Bag', 'IceSample', 'IceCream', 'Palette', 'Ice Core']; } ext_autocomplete.init(); @@ -60,12 +60,34 @@ QUnit.test("search", async function(assert) { }; }; await ext_autocomplete.search("Ice", - gcallback( ['IceCore', 'IceSample', 'IceCream']) + gcallback( ['IceCore', 'IceSample', 'IceCream', 'Ice Core']) ); await ext_autocomplete.search("Core", gcallback([])); }); +QUnit.test("searchPost", async function(assert) { + const resultsFromServer = ["Ice Core", "IceCore"]; + const origJQElement = [{ + selectionEnd: 8, + value: "FIND Ice WHERE", + }]; + + const expected = [ + { + "html": "Ice Core", + "text": "FIND \"Ice Core\" WHERE" + }, + { + "html": "IceCore", + "text": "FIND IceCore WHERE" + } + ]; + + const result = ext_autocomplete.searchPost(resultsFromServer, origJQElement); + assert.propEqual(result, expected); +}); + 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 index 831df74231e479d5b4550524f6bc0da617c98fb3..8130fe422a5bd124ef1c6767708b036e2e904f53 100644 --- a/test/core/js/modules/ext_bookmarks.js.js +++ b/test/core/js/modules/ext_bookmarks.js.js @@ -42,16 +42,18 @@ QUnit.module("ext_bookmarks.js", { } }); -QUnit.test("parse_uri", function(assert) { +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"}); + 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) { +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" @@ -67,9 +69,9 @@ QUnit.test("get_export_table", async function (assert) { 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"]); + ["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}`); + `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}`); }); @@ -115,7 +117,7 @@ QUnit.test("update_export_link", function (assert) { QUnit.test("update_collection_link", function (assert) { const collection_link = $( - `<div id="caosdb-f-bookmarks-collection-link"><a/></div>`); + `<div id="caosdb-f-bookmarks-collection-link"><a/></div>`); const a = collection_link.find("a")[0]; $("body").append(collection_link); @@ -148,7 +150,8 @@ QUnit.test("bookmark buttons", function (assert) { 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]); + broken_button, non_button + ]); // get_bookmark_buttons assert.equal(ext_bookmarks.get_bookmark_buttons("body").length, 0); @@ -174,8 +177,51 @@ QUnit.test("bookmark buttons", function (assert) { 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(); }); + +QUnit.test("select-query transformation", function (assert) { + assert.equal( + ext_bookmarks.get_select_id_query_string("FIND analysis"), + "SELECT ID FROM analysis"); + assert.equal( + ext_bookmarks.get_select_id_query_string( + "FIND RECORD analysis WHICH HAS A date > 2012"), + "SELECT ID FROM RECORD analysis WHICH HAS A date > 2012"); + assert.equal( + ext_bookmarks.get_select_id_query_string( + "SELECT name, date FROM analysis"), + "SELECT ID FROM analysis"); + assert.equal( + ext_bookmarks.get_select_id_query_string("COUNT analysis"), + "SELECT ID FROM analysis"); + assert.equal( + ext_bookmarks.get_select_id_query_string("fInD analysis"), + "SELECT ID FROM analysis"); +}); + +QUnit.test("select-query extraction", function (assert) { + // Use response field copied from demo + const response_field = $(`<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"> + <span>Query: </span> + <span class = "caosdb-f-query-response-string">SELECT name, id FROM RECORD MusicalAnalysis</span> + </div> + </div> + <div class="col-sm-2 text-end"> + <span>Results: </span> + <span class="caosdb-query-response-results">3</span> + </div> + </div> + </div> +</div>`); + $("body").append(response_field); + + assert.equal(ext_bookmarks.get_query_from_response(), + "SELECT ID FROM RECORD MusicalAnalysis"); +}); \ No newline at end of file diff --git a/test/core/js/modules/ext_bottom_line.js.js b/test/core/js/modules/ext_bottom_line.js.js index 610d95d1b00b3a8bf220a6d39e12ed71ecaa9090..48dc64f231c1dc5929eece1f44756d60cb17c0a1 100644 --- a/test/core/js/modules/ext_bottom_line.js.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": [ diff --git a/test/core/js/modules/ext_cosmetics.js.js b/test/core/js/modules/ext_cosmetics.js.js new file mode 100644 index 0000000000000000000000000000000000000000..969c8297b8b5cf85d0a668d7c30e8b0f45e34d4d --- /dev/null +++ b/test/core/js/modules/ext_cosmetics.js.js @@ -0,0 +1,94 @@ +/* + * This file is a part of the CaosDB Project. + * + * Copyright (C) 2021 IndiScale GmbH <info@indiscale.com> + * Copyright (C) 2021 Timm Fitschen <t.fitschen@indiscale.com> + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see <https://www.gnu.org/licenses/>. + */ + +'use strict'; + +QUnit.module("ext_cosmetics.js", { + before: function (assert) { + cosmetics.init(); + // 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("linkify - https", function (assert) { + assert.ok(cosmetics.linkify, "linkify available"); + var test_cases = [ + ["https://link", 1], + ["this is other text https://link", 1], + ["https://link this is other text", 1], + ["this is other text https://link and this as well", 1], + ["this is other text https://link", 1], + ["this is other text https://link and here comes another link https://link and more text", 2], + ]; + + for (let test_case of test_cases) { + const container = $('<div></div>'); + $(document.body).append(container); + const text_value = $(`<div class="caosdb-f-property-text-value">${test_case[0]}</div>`); + container.append(text_value); + assert.equal($(container).find("a[href='https://link']").length, 0, "no link present"); + cosmetics.linkify(); + assert.equal($(container).find("a[href='https://link']").length, test_case[1], "link is present"); + container.remove(); + } +}); + +QUnit.test("linkify - http", function (assert) { + var test_cases = [ + ["http://link", 1], + ["this is other text http://link", 1], + ["http://link this is other text", 1], + ["this is other text http://link and this as well", 1], + ["this is other text http://link", 1], + ["this is other text http://link and here comes another link http://link and more text", 2], + ]; + for (let test_case of test_cases) { + const container = $('<div></div>'); + $(document.body).append(container); + const text_value = $(`<div class="caosdb-f-property-text-value">${test_case[0]}</div>`); + $(container).append(text_value); + assert.equal($(container).find("a[href='http://link']").length, 0, "no link present"); + cosmetics.linkify(); + assert.equal($(container).find("a[href='http://link']").length, test_case[1], "link is present"); + container.remove(); + } +}); + +QUnit.test("linkify cut-off (40)", function (assert) { + const container = $('<div></div>'); + $(document.body).append(container); + const test_case = "here is some text https://this.is.a.link/with/more/than/40/characters/ this is more text"; + const text_value = $(`<div class="caosdb-f-property-text-value">${test_case}</div>`); + $(container).append(text_value); + assert.equal($(container).find("a").length, 0, "no link present"); + cosmetics.linkify(); + assert.equal($(container).find("a").length, 1, "link is present"); + assert.equal($(container).find("a").text(), "https://this.is.a.link/with/more/th[...] ", "link text has been cut off"); + container.remove(); +}); diff --git a/test/core/js/modules/ext_map.js.js b/test/core/js/modules/ext_map.js.js index d14d7ebfdbb9b86645adce613a1df3f484c5e09c..9b6b01022d8106153d50eaa906a0ce33803a8dc3 100644 --- a/test/core/js/modules/ext_map.js.js +++ b/test/core/js/modules/ext_map.js.js @@ -86,13 +86,13 @@ QUnit.test("check dependencies", 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.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.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'"); @@ -134,23 +134,23 @@ QUnit.test("create_map_view", function (assert) { 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"] = { @@ -165,7 +165,7 @@ 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(); @@ -260,13 +260,13 @@ QUnit.test("_get_select_with_path ", function (assert) { 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 latitude,longitude FROM ENTITY RealRT WITH latitude AND longitude ", "RT only"); + ["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 prop1.latitude,prop1.longitude FROM ENTITY RealRT WITH prop1 WITH latitude AND longitude ", "RT with one prop"); + ["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 prop1.prop2.latitude,prop1.prop2.longitude FROM ENTITY RealRT WITH prop1 WITH prop2 WITH latitude AND longitude ", "RT with two props"); + ["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"); }); diff --git a/test/core/js/modules/ext_qrcode.js.js b/test/core/js/modules/ext_qrcode.js.js new file mode 100644 index 0000000000000000000000000000000000000000..d4d505913035d17d14cb7b110e8dc67b0a018a44 --- /dev/null +++ b/test/core/js/modules/ext_qrcode.js.js @@ -0,0 +1,101 @@ +/* + * This file is a part of the CaosDB Project. + * + * Copyright (C) 2021 IndiScale GmbH <info@indiscale.com> + * Copyright (C) 2021 Timm Fitschen <t.fitschen@indiscale.com> + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see <https://www.gnu.org/licenses/>. + */ + +'use strict'; + +QUnit.module("ext_qrcode.js", { + before: function (assert) { + // setup before module + }, + beforeEach: function (assert) { + // setup before each test + $(document.body).append('<div data-entity-id="eid123" data-version-id="vid234" id="ext-qrcode-test-entity" class="caosdb-entity-panel"><div class="caosdb-v-entity-header-buttons-list"></div></div>'); + }, + afterEach: function (assert) { + // teardown after each test + const modal = bootstrap.Modal.getInstance($(".modal")[0]); + if (modal) modal.dispose(); + $("#ext-qrcode-test-entity").remove(); + $(".modal").remove(); + }, + after: function (assert) { + // teardown after module + } +}); + +QUnit.test("init", function (assert) { + assert.ok(ext_qrcode.init, "init available"); + assert.equal($(".caosdb-f-entity-qrcode-button").length, 0, "no button before."); + ext_qrcode.init(); + assert.equal($(".caosdb-f-entity-qrcode-button").length, 1, "button has been added."); + ext_qrcode.init(); + assert.equal($(".caosdb-f-entity-qrcode-button").length, 1, "still only one button."); + + ext_qrcode.remove_qrcode_button($("#ext-qrcode-test-entity")[0]); + assert.equal($(".caosdb-f-entity-qrcode-button").length, 0, "no button after removal."); +}); + +QUnit.test("create_qrcode_button", function (assert) { + assert.equal(ext_qrcode.create_qrcode_button("entityid", "versionid").tagName, "BUTTON", "create_qrcode_button creates a button"); +}); + + +QUnit.test("qrcode_button_click_handler", function (assert) { + var done = assert.async(); + assert.equal($("#qrcode-modal-entityid-versionid").length, 0, "no modal before first click"); + ext_qrcode.qrcode_button_click_handler("entityid", "versionid") + $("#qrcode-modal-entityid-versionid").on("shown.bs.modal", done); + assert.equal($("#qrcode-modal-entityid-versionid").length, 1, "first click added the modal"); +}); + +QUnit.test("update_qrcode", async function (assert) { + // create modal + const entity_id = "eid456"; + const entity_version = "vid3564"; + const modal_id = `qrcode-modal-${entity_id}-${entity_version}`; + const modal_element = ext_qrcode.create_qrcode_modal(modal_id, entity_id, entity_version); + $(document.body).append(modal_element); + + assert.equal($("#" + modal_id).find(".caosdb-f-entity-qrcode *").length, 0, "no qrcode."); + assert.equal($("#" + modal_id).find(".caosdb-f-entity-qrcode-link *").length, 0, "no link."); + + // update adds qrcode + ext_qrcode.update_qrcode(modal_element, entity_id, entity_version); + + assert.equal($("#" + modal_id).find(".caosdb-f-entity-qrcode-link a")[0].href, connection.getEntityUri([entity_id]), "link points to entity head."); + // wait until qrcode is ready + await sleep(500); + assert.equal($("#" + modal_id).find(".caosdb-f-entity-qrcode canvas").length, 1, "qrcode is there."); + + $("#" + modal_id).find("canvas").remove(); + assert.equal($("#" + modal_id).find(".caosdb-f-entity-qrcode canvas").length, 0, "removed qrcode canvas for next test."); + // select radio button for link to exact version: check both... + $("#" + modal_id).find("input[name=entity-qrcode-versioned]").prop("checked", true); + // ...then uncheck first + $("#" + modal_id).find("input[name=entity-qrcode-versioned]").first().prop("checked", false); + $("#" + modal_id).find("form").trigger("change"); + + // check: uri has changed + assert.equal($("#" + modal_id).find(".caosdb-f-entity-qrcode-link a")[0].href, connection.getEntityUri([entity_id]) + "@" + entity_version, "link changed to versioned entity."); + // wait until qrcode is ready + await sleep(500); + assert.equal($("#" + modal_id).find(".caosdb-f-entity-qrcode canvas").length, 1, "qrcode there again."); + +}); \ No newline at end of file diff --git a/test/core/js/modules/ext_references.js.js b/test/core/js/modules/ext_references.js.js index 43cc1ddd742d6702232b740bbfd96411f41b08f5..54e06d33d5f1c33781efe11802a7fbfc5ba44d89 100644 --- a/test/core/js/modules/ext_references.js.js +++ b/test/core/js/modules/ext_references.js.js @@ -104,7 +104,7 @@ QUnit.test("is_child", function(assert){ }); QUnit.test("get_person_str", function(assert){ - assert.ok(resolve_references.get_person_str); + assert.ok(person_reference.get_person_str); }); QUnit.test("update_visible_references_without_summary", async function(assert){ 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 997a89ec21d4a22f49746cbda1c46bb56268f80d..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,40 +64,35 @@ 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(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" />'); - var modal = $('<div id="downloadModal"><div>'); - $(document.body).append([tsv_data, modal]); + 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); - tsv_data.remove(); - modal.remove(); - }); -} + tsv_data.remove(); + modal.remove(); +}); QUnit.test("_clean_cell", function(assert) { assert.equal(caosdb_table_export._clean_cell("\n\t\n\t"), " ", "No valid content"); @@ -115,17 +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._create_tsv_string + var f = caosdb_table_export._create_tsv_string var tsv_string = f(entities, ["Bag", "Number"], true); var prefix = "data:text/csv;charset=utf-8," - assert.equal(tsv_string, - "ID\tBag\tNumber\n242\t6366, 6406, 6407, 6408, 6409, 6410, 6411, 6412, 6413\t02 8 4aaa a\n2112\t\t1101", "tsv generated"); + 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\tBag\tNumber\n242\t6366, 6406, 6407, 6408, 6409, 6410, 6411, 6412, 6413\t02 8 4aaa a\n2112\t\t1101", "tsv generated"); + 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 f8bf1ed1ac1495a8a3688aeb7b0cce387c8b69fb..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,16 +622,13 @@ QUnit.test("field_ready", function(assert) { }); }); -{ -const sleep = (ms) => { - return new Promise(res => setTimeout(res, ms)) -} - -QUnit.test("make_alert - cancel", async 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");}, + proceed_callback: () => { + assert.ok(false, "this should not be called"); + }, cancel_callback: cancel_callback, }); $("body").append(_alert); @@ -564,7 +642,7 @@ QUnit.test("make_alert - cancel", async function(assert) { }); -QUnit.test("make_alert - proceed", async function(assert) { +QUnit.test("make_alert - proceed", async function (assert) { var proceed_callback = assert.async(); var _alert = form_elements.make_alert({ message: "message", @@ -581,7 +659,7 @@ QUnit.test("make_alert - proceed", async function(assert) { }); -QUnit.test("make_alert - remember", async function(assert) { +QUnit.test("make_alert - remember", async function (assert) { form_elements._set_alert_decision("unittests", ""); var proceed_callback = assert.async(3); @@ -628,4 +706,83 @@ QUnit.test("make_alert - remember", async function(assert) { 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/form_panel.js.js b/test/core/js/modules/form_panel.js.js new file mode 100644 index 0000000000000000000000000000000000000000..bc8343d4a65233e025039e7476861fb998c2abbc --- /dev/null +++ b/test/core/js/modules/form_panel.js.js @@ -0,0 +1,63 @@ +/* + * ** header v3.0 + * This file is a part of the CaosDB Project. + * + * Copyright (C) 2021 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("form_panel.js", { + before: function (assert) { + + }, + after: function (assert) { + } +}); + +QUnit.test("availability", function (assert) { + assert.ok(form_panel.init, "init available"); + assert.ok(form_panel.create_show_form_callback , "version available"); +}); + +QUnit.test("create_show_form_callback ", function (assert) { + const title = "Upload CSV File"; // title of the form and text in the toolbox + const panel_id = "csv_upload_form_panel"; + const server_side_script = "csv_script.py"; + const tool_box = "Tools"; // Name of the drop-down menu where the button is added in the navbar + const help_text = "something"; + const accepted_files_formats = [ ".csv", "text/tsv", ] // Mime types and file endings. + + const csv_form_config = { + script: server_side_script, + fields: [{ + type: "file", + name: "csv_file", + label: "CSV File", // label of the file selector in the form + required: true, + cached: false, + accept: accepted_files_formats.join(","), + help: help_text, + }, ], + }; + cb = form_panel.create_show_form_callback( panel_id, title, csv_form_config); + assert.equal(typeof cb, "function", "function created"); + cb() +}); + + 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..e644a674b6a8e58fd7c1395d1e337d5416c2bc5e 100644 --- a/test/core/js/modules/query.xsl.js +++ b/test/core/js/modules/query.xsl.js @@ -28,19 +28,25 @@ QUnit.module("query.xsl", { // load query.xsl var done = assert.async(); var qunit_obj = this; - $.ajax({ - cache: true, - dataType: 'xml', - url: "xsl/query.xsl", - }).done(function(data, textStatus, jdXHR) { - insertParam(data, "entitypath", "/entitypath/"); - qunit_obj.queryXSL = data; - }).always(function() { + _retrieveQueryXSL().then(function(xsl) { + qunit_obj.queryXSL = xsl; done(); }); } }); +async function _retrieveQueryXSL() { + var queryXsl = await transformation.retrieveXsltScript("query.xsl"); + var entityXsl = await transformation.retrieveXsltScript("entity.xsl"); + var commonXsl = await transformation.retrieveXsltScript("common.xsl"); + var xsl = transformation.mergeXsltScripts(entityXsl, [commonXsl, queryXsl]); + insertParam(xsl, "entitypath", "/entitypath/"); + insertParam(xsl, "filesystempath", "/filesystempath/"); + insertParam(xsl, "lowercase", "abcdefghijklmnopqrstuvwxyz"); + insertParam(xsl, "uppercase", "ABCDEFGHIJKLMNOPQRSTUVWXYZ"); + return xsl; +} + /* TESTS */ QUnit.test("availability", function(assert) { assert.ok(this.queryXSL); @@ -52,7 +58,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 +128,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 +147,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 +176,50 @@ 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"); +}); + +QUnit.test("template select-table-cell (with version) ", function(assert){ + let cell = callTemplate(this.queryXSL, "select-table-cell", {"version-id": "vid-2345", "entity-id": "eid-1234", "field-name": "name"}, (x) => `<table><tbody><tr>${x}</tr></tbody></table>`,`<Response><Entity id="eid-1234" name="the-name"><Version id="vid-2345"/></Entity></Response>`); + var next = cell.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.textContent; + assert.equal(next, "the-name", "name = the-name"); +}); + + +QUnit.test("template select-table-cell (id only) ", function(assert){ + let cell = callTemplate(this.queryXSL, "select-table-cell", {"entity-id": "eid-1234", "field-name": "id"}, (x) => `<table><tbody><tr>${x}</tr></tbody></table>`,`<Response><Entity id="eid-1234"/></Response>`); + var next = cell.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.textContent; + assert.equal(next, "eid-1234", "id = eid-1234"); }); /* 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 7b3f7abf404f261668c18690df337b73794dd8bd..5f45c32adb9d171c5d362467d0bba0840f02836f 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,29 +69,29 @@ QUnit.test("xslt", function(assert) { }, "nu ll xsl throws exc."); }); -QUnit.test("getEntityId", function(assert) { - assert.ok(getEntityId, "function available"); - let okElem = $('<div><div class="caosdb-id">1234</div></div>')[0]; - let notOkElem = $('<div><div class="caosdb-id">asdf</div></div>')[0]; - let emptyElem = $('<div></div>')[0]; +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>"); - assert.throws(() => { - getEntityId(); - }, "no parameter throws"); - assert.throws(() => { - getEntityId(null); - }, "null parameter throws"); - assert.throws(() => { - getEntityId(notOkElem); - }, "on-integer ID throws"); - assert.throws(() => { - getEntityId(empty); - }, "empty elem throws"); +}); + +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("1234", getEntityId(okElem), "ID found"); + 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("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 +110,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 +133,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 +145,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 +188,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 +199,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 +212,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 +240,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 +251,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 +277,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 +321,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><div class="2ndrow"></div></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 +332,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 +362,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 +380,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 +405,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 +413,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 +427,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 +445,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 +460,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 +502,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 +510,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 +518,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 +543,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 +560,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 +592,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 +619,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 +634,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 +650,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 +670,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 +686,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,19 +702,13 @@ 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>' + '<div class="caosdb-preview-waiting-notification">Please wait!</div></div>')[0]; let emptyElem = $('<div></div>')[0]; - assert.throws(() => { - preview.removeAllErrorNotifications(); - }, "no parameter throws"); - assert.throws(() => { - preview.removeAllErrorNotifications(null); - }, "null parameter throws"); assert.equal(okElem.childNodes.length, 3, "before: three children"); assert.equal(okElem, preview.removeAllErrorNotifications(okElem), "return first parameter"); @@ -718,20 +718,13 @@ 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>' + '<div class="caosdb-preview-error-notification">Error!</div></div>')[0]; let emptyElem = $('<div></div>')[0]; - assert.throws(() => { - removeAllWaitingNotifications(); - }, "no parameter throws"); - assert.throws(() => { - removeAllWaitingNotifications(null); - }, "null parameter throws"); - assert.equal(okElem.childNodes.length, 3, "before: three children"); assert.equal(okElem, removeAllWaitingNotifications(okElem), "return first parameter"); assert.equal(okElem.childNodes.length, 1, "after: one child"); @@ -740,28 +733,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 +774,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 +799,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 +817,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 +837,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 +854,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 +885,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 +922,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,7 +950,7 @@ QUnit.test("createCarouselNav", function(assert) { let original_get = connection.get; ref_property_elem.find('div').append(refLinks); - QUnit.test("initProperty", async function(assert) { + QUnit.test("initProperty", async function (assert) { var done = assert.async(2); assert.ok(preview.initProperty, "function available"); @@ -968,7 +961,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(); @@ -985,7 +978,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); } @@ -1008,7 +1001,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(); @@ -1023,39 +1016,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"; } @@ -1067,10 +1062,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") @@ -1081,19 +1076,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(); @@ -1101,22 +1099,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]; @@ -1126,7 +1124,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); @@ -1137,11 +1135,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"); @@ -1160,10 +1158,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\">"); @@ -1175,9 +1175,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; } @@ -1209,10 +1209,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."); @@ -1226,137 +1226,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(() => { @@ -1388,7 +1261,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(() => { @@ -1409,7 +1282,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(() => { @@ -1427,7 +1300,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(() => { @@ -1443,20 +1316,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>'; @@ -1466,7 +1339,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'"); @@ -1484,7 +1357,7 @@ QUnit.test("getAnnotationsForEntity", function(assert) { }); QUnit.test("async/await behavior", (assert) => { - let af = async function() { + let af = async function () { return await "returnval"; }; @@ -1498,7 +1371,7 @@ QUnit.test("async/await behavior", (assert) => { done(); }); - let er = async function() { + let er = async function () { throw "asyncerror"; }; @@ -1508,30 +1381,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"); @@ -1539,7 +1412,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"); @@ -1551,7 +1424,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"); @@ -1560,19 +1433,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); @@ -1584,17 +1457,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!"); } @@ -1613,7 +1486,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>\ @@ -1636,26 +1509,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))); } @@ -1687,13 +1560,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/>"))); @@ -1710,7 +1583,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); @@ -1725,7 +1598,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); @@ -1742,7 +1615,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]; @@ -1757,7 +1630,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"); }); @@ -1774,14 +1647,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(); }); @@ -1799,7 +1672,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"); @@ -1808,7 +1681,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"); @@ -1817,7 +1690,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); @@ -1835,7 +1708,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]; @@ -1861,7 +1734,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."); @@ -1881,25 +1754,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"); @@ -1917,7 +1795,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. @@ -1937,10 +1815,10 @@ QUnit.test("toolbox example", function(assert) { }); QUnit.module("webcaosdb.js - version_history", { - before: function(assert) { + before: function (assert) { connection._init(); }, - after: function(assert) { + after: function (assert) { connection._init(); }, }); @@ -1949,6 +1827,7 @@ 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_restore_version_buttons, "function"); assert.equal(typeof version_history.init_load_history_buttons, "function"); assert.equal(typeof version_history.retrieve_history, "function"); }) @@ -1992,7 +1871,7 @@ QUnit.test("init_load_history_buttons and init_load_history_buttons", async func // load_button triggers retrieval of history load_button.click(); - await sleep(200); + await sleep(500); var gone_button = $(html).find(".caosdb-f-entity-version-load-history-btn"); assert.equal(gone_button.length, 0, "button is gone"); @@ -2009,6 +1888,124 @@ QUnit.test("init_load_history_buttons and init_load_history_buttons", async func $(html).remove(); }); -const sleep = function sleep(ms) { - return new Promise(resolve => setTimeout(resolve, ms)); -} +QUnit.test("available", function (assert) { + assert.equal(typeof restore_old_version, "function"); +}) + +QUnit.test("init_restore_version_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" /> + <Permission name="UPDATE:*" /> + </Permissions> + <Version id="efa5ac7126c722b3f43284e150d070d6deac0ba6" > + <Predecessor id="f09114b227d88f23d4e23645ae471d688b1e82f7" /> + <Successor id="5759d2bccec3662424db5bb005acea4456a299ef" /> + </Version> + <Parent id="8609" name="TestRT" /> + </Record> +</Response> +`; + var done = assert.async(1); + 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"); + + //console.log(xml2str(restore_button[0])); + //assert.ok(restore_button.hasClass("d-none"), "restore_button is hidden"); + + + // load_button triggers retrieval of history + load_button.click(); + await sleep(500); + + //console.log(xml2str(restore_button[0])); + //version_history.init_restore_version_buttons(); + + var restore_button = $("body").find(".caosdb-f-entity-version-restore-btn"); + assert.ok(!restore_button.hasClass("d-none"), "restore_button is not hidden anymore"); + + // restore_button triggers retrieval of history + localStorage["form_elements.alert_decision.restore_entity"] = "proceed"; + restore_button.first().click(); + localStorage.removeItem("form_elements.alert_decision.restore_entity"); + await sleep(500); + + // restore is not possible in the unit test + alertdiv = $(html).find(".alert-danger"); + assert.equal(alertdiv.length, 1, "on alert div"); + assert.ok(alertdiv.text().indexOf("Restore failed") > 0, "Restore failed"); + + $(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/xml/table_export/test_case_select_table_1.xml b/test/core/xml/table_export/test_case_select_table_1.xml index ae0a856f106557be3712f303b06a99f6220ef827..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,6 +9,7 @@ </Selection> </Query> <Record id="242"> + <Version id="abc123" head="true"/> <Property id="117" name="Number" datatype="TEXT" importance="FIX"> 02	8


4aaa	a </Property> @@ -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 1dab16688e35c165abcf398f758ab47f4037bad6..2880d864bc7df86bf7cade4d6c232b387e513bfd 100644 --- a/test/docker/Dockerfile +++ b/test/docker/Dockerfile @@ -7,14 +7,16 @@ RUN apt-get update \ && 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 pylint3 python3-pip \ + firefox-esr gettext-base python3-pip \ python3-httpbin git curl x11-apps xvfb unzip \ + libhdf5-dev \ + pkgconf \ 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 -RUN pip3 install pandas xlrd==1.2.0 +RUN pip3 install caosdb>=0.5.2 +RUN pip3 install pandas RUN pip3 install git+https://gitlab.com/caosdb/caosdb-advanced-user-tools.git@dev # For automatic documentation #RUN npm install -g jsdoc diff --git a/test/server_side_scripting/ext_table_preview/data/bad.csv b/test/server_side_scripting/ext_table_preview/data/bad.csv deleted file mode 100644 index d29a9312a387186beb8bf4f77a8ec0e4b0ab80fa..0000000000000000000000000000000000000000 Binary files a/test/server_side_scripting/ext_table_preview/data/bad.csv and /dev/null differ diff --git a/test/server_side_scripting/ext_table_preview/data/bad.tsv b/test/server_side_scripting/ext_table_preview/data/bad.tsv deleted file mode 100644 index d29a9312a387186beb8bf4f77a8ec0e4b0ab80fa..0000000000000000000000000000000000000000 Binary files a/test/server_side_scripting/ext_table_preview/data/bad.tsv and /dev/null differ 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 index 00d1c7f38746abe437abc76cd51b29600adcd049..c7c0cd3bc22c62ad4f1a214d4ed777718cdbf74a 100644 --- 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 @@ -67,8 +67,9 @@ class PreviewTest(unittest.TestCase): assert (table == searchkey).any(axis=None) badfiles = [os.path.join(os.path.dirname(__file__), "data", f) - for f in ["bad.csv", "bad.tsv", "bad.xls", "bad.xlsx"]] + for f in ["bad.xls", "bad.xlsx"]] for bfi in badfiles: + print("bfi: ", bfi) self.assertRaises(ValueError, read_file, bfi, "."+bfi.split(".")[-1])