diff --git a/.babelrc b/.babelrc new file mode 100644 index 000000000..857e7ba32 --- /dev/null +++ b/.babelrc @@ -0,0 +1,9 @@ +{ + "presets": [ + ["@babel/preset-env", { "useBuiltIns": "usage" }] + ], + "plugins": [ + ["@babel/plugin-proposal-decorators", { "legacy": true }], + ["@babel/plugin-proposal-class-properties", { "loose": true }] + ] +} diff --git a/.directory b/.directory index 59c2c250d..9e958424d 100644 --- a/.directory +++ b/.directory @@ -1,5 +1,5 @@ [Dolphin] -Timestamp=2018,3,14,10,57,55 +Timestamp=2019,6,26,19,32,25 Version=4 [Settings] diff --git a/.eslintrc.json b/.eslintrc.json new file mode 100644 index 000000000..6fcef7c27 --- /dev/null +++ b/.eslintrc.json @@ -0,0 +1,28 @@ +{ + "env": { + "browser": true, + "es6": true, + "jasmine": true + }, + "extends": "eslint:recommended", + "globals": { + "Atomics": "readonly", + "SharedArrayBuffer": "readonly", + "flatpickr": "readonly", + "$": "readonly" + }, + "parser": "babel-eslint", + "parserOptions": { + "ecmaVersion": 2018, + "ecmaFeatures": { + "legacyDecorators": true + } + }, + "rules": { + "no-console": "off", + "no-extra-semi": "off", + "semi": ["error", "always"], + "comma-dangle": ["error", "always-multiline"], + "quotes": ["error", "single"] + } +} diff --git a/.gitignore b/.gitignore index b85a1c848..0fb3c32c0 100644 --- a/.gitignore +++ b/.gitignore @@ -1,6 +1,8 @@ dist* +static/bundles/ static/tmp/ static/combined/ +node_modules/ *.hi *.o *.sqlite3 @@ -31,4 +33,5 @@ src/Handler/Course.SnapCustom.hs .stack-work-* .directory tags -test.log \ No newline at end of file +test.log +*.dump-splices diff --git a/.vscode/tasks.json b/.vscode/tasks.json index 8b60430d0..4c18542ba 100644 --- a/.vscode/tasks.json +++ b/.vscode/tasks.json @@ -14,6 +14,7 @@ "reveal": "always", "focus": false, "panel": "dedicated", + "clear": true, "showReuseMessage": false } }, @@ -43,6 +44,31 @@ "panel": "dedicated", "showReuseMessage": false } + }, + { + "type": "npm", + "script": "yesod:lint", + "problemMatcher": [] + }, + { + "type": "npm", + "script": "yesod:start", + "problemMatcher": [] + }, + { + "type": "npm", + "script": "start", + "problemMatcher": [] + }, + { + "type": "npm", + "script": "frontend:lint", + "problemMatcher": [] + }, + { + "type": "npm", + "script": "lint", + "problemMatcher": [] } ] } \ No newline at end of file diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 000000000..6828319d7 --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,53 @@ +# Changelog + +All notable changes to this project will be documented in this file. See [standard-version](https://github.com/conventional-changelog/standard-version) for commit guidelines. + +## 1.0.0 (2019-07-03) + + +### Bug Fixes + +* **sheet corrector assigment:** minor bugfix ([749cd2f](https://gitlab.cip.ifi.lmu.de/jost/UniWorX/commit/749cd2f)) +* async table js util now knows current random css prefix ([cc90faf](https://gitlab.cip.ifi.lmu.de/jost/UniWorX/commit/cc90faf)) +* **correction assignment:** correcting lecturer's names are shown now ([16c556b](https://gitlab.cip.ifi.lmu.de/jost/UniWorX/commit/16c556b)) +* **corrector assignment:** sheet tabel mixed up columns sorted ([d07f53e](https://gitlab.cip.ifi.lmu.de/jost/UniWorX/commit/d07f53e)) +* **datepicker:** hide number input spinners in datepicker ([2073130](https://gitlab.cip.ifi.lmu.de/jost/UniWorX/commit/2073130)) +* **exam grading keys:** Fix spacing ([24aacef](https://gitlab.cip.ifi.lmu.de/jost/UniWorX/commit/24aacef)) +* **exams:** Fix registration ([1684da0](https://gitlab.cip.ifi.lmu.de/jost/UniWorX/commit/1684da0)) +* **fe:** style notifications acceptably for now ([fc80f08](https://gitlab.cip.ifi.lmu.de/jost/UniWorX/commit/fc80f08)) +* **fe-async-table:** Emulate no-js behaviour when handling pagesize ([28dcc8d](https://gitlab.cip.ifi.lmu.de/jost/UniWorX/commit/28dcc8d)) +* **fe-check-all:** use arrow fn to keep scope in event listeners ([09e681e](https://gitlab.cip.ifi.lmu.de/jost/UniWorX/commit/09e681e)) +* **fe-deflist:** avoid horizontal scroll on pages with deflist ([16d422d](https://gitlab.cip.ifi.lmu.de/jost/UniWorX/commit/16d422d)) +* **Help Widget, Corrector Assignment:** Modal Form closes in place; assign alerts ([89d5364](https://gitlab.cip.ifi.lmu.de/jost/UniWorX/commit/89d5364)), closes [#195](https://gitlab.cip.ifi.lmu.de/jost/UniWorX/issues/195) +* **info-lecturer:** Touch ups ([e1e26ab](https://gitlab.cip.ifi.lmu.de/jost/UniWorX/commit/e1e26ab)) +* **many occurrences throughout the project:** Fix typo: occurence -> occurrence everywhere ([96387cb](https://gitlab.cip.ifi.lmu.de/jost/UniWorX/commit/96387cb)) +* filter submission by not having corrector ([3bded50](https://gitlab.cip.ifi.lmu.de/jost/UniWorX/commit/3bded50)) +* minor heat correction for correction overview ([5546849](https://gitlab.cip.ifi.lmu.de/jost/UniWorX/commit/5546849)) +* **ratings:** disallow ratings for graded sheets without point value ([463b2b7](https://gitlab.cip.ifi.lmu.de/jost/UniWorX/commit/463b2b7)) +* **standard-version:** properly reset staging area before release ([5aa906e](https://gitlab.cip.ifi.lmu.de/jost/UniWorX/commit/5aa906e)) + + +### Features + +* **corrector-assignment:** show load/submission percentages ([228cd50](https://gitlab.cip.ifi.lmu.de/jost/UniWorX/commit/228cd50)) +* make pagesize changes load async ([6486120](https://gitlab.cip.ifi.lmu.de/jost/UniWorX/commit/6486120)) +* **development:** add commitlint to ensure proper commit msgs ([dd528c1](https://gitlab.cip.ifi.lmu.de/jost/UniWorX/commit/dd528c1)) +* **development:** add standard-version for automatic changelog generation ([c495ef5](https://gitlab.cip.ifi.lmu.de/jost/UniWorX/commit/c495ef5)) +* **exams:** CRU (no D) for exams ([67a50c9](https://gitlab.cip.ifi.lmu.de/jost/UniWorX/commit/67a50c9)) +* **exams:** exam registration ([99184ff](https://gitlab.cip.ifi.lmu.de/jost/UniWorX/commit/99184ff)) +* **exams:** Form validation ([6fb1399](https://gitlab.cip.ifi.lmu.de/jost/UniWorX/commit/6fb1399)) +* **fe-heatmap:** add css class heated for heatmap elements ([b09b876](https://gitlab.cip.ifi.lmu.de/jost/UniWorX/commit/b09b876)), closes [#405](https://gitlab.cip.ifi.lmu.de/jost/UniWorX/issues/405) +* **forms:** Introduce more convenient form validation ([f8d0b02](https://gitlab.cip.ifi.lmu.de/jost/UniWorX/commit/f8d0b02)) +* **standard-version:** allow adding additional changes to release ([7ed6fe4](https://gitlab.cip.ifi.lmu.de/jost/UniWorX/commit/7ed6fe4)) +* **standard-version:** complete release workflow ([605e62f](https://gitlab.cip.ifi.lmu.de/jost/UniWorX/commit/605e62f)) + + +### Tests + +* Does ist build with everything except for `makeClassy ''Entity`? Probably the functional dependency is to blame?! ([bb552c4](https://gitlab.cip.ifi.lmu.de/jost/UniWorX/commit/bb552c4)) +* removing makeCLassyFor maybe build works then? ([2550f74](https://gitlab.cip.ifi.lmu.de/jost/UniWorX/commit/2550f74)) + + +### BREAKING CHANGES + +* **standard-version:** Start of new versioning schema diff --git a/ChangeLog.md b/ChangeLog.md deleted file mode 100644 index f35e0e155..000000000 --- a/ChangeLog.md +++ /dev/null @@ -1,81 +0,0 @@ - * Version 27.03.2019 - - Kurse Veranstalter können nun mehrere Dozenten und Assistenten selbst eintragen - - Erfassung Studiengangsdaten - - * Version 20.03.2019 - - Kursanmeldung benötigen assoziertes Hauptfach (für Studierende mit mehreren Hauptfächern) - - * Version 30.01.2019 - - Designänderungen - - * Version 16.01.2019 - - Links für Bequemlichkeiten hinzugefügt (z.B. aktuelles Übungsblatt) - - Liste zugewiesener Abgaben lassen sich nun filtern - - Bugfix: Wenn zwischen Anzeige und Empfang eines Tabellen-Formulars Zeilen verschwinden wird nun eine sinnvolle Fehlermeldung angezeigt - - * Version 30.11.2018 - - Bugfix: Übungsblätter im "bestehen nach Punkten"-Modus werden wieder korrekt gespeichert - - * Version 29.11.2018 - - Bugfix: Formulare innerhalb von Tabellen funktionieren nun auch nach Javascript-Seitenwechsel oder Ändern der Sortierung - - * Version 09.11.2018 - - Bugfix: Zahlreiche Knöpfe/Formulare funktionieren wieder bei eingeschaltetem Javascript - - Verschiedene Verbesserungen für Korrektoren - - * Version 19.10.2018 - - Benutzer können sich in der Testphase komplett selbst löschen - - Hilfe Widget - - Benachrichtigungen per eMail für einige Ereignisse - - * Version 18.09.2018 - - Tooltips funktionieren auch ohne JavaScript - - Kurskürzel müssen nur innerhalb eines Instituts eindeutig sein - - User Data zeigt nun alle momentan gespeicherten Datensätze an - - Unterstützung von Tabellenzusammenfassungen, z.B. Punktsummen - - Intelligente Verteilung von Abgaben auf Korrektoren (z.B. bei Krankheit) - - Übungsblätter können Abgabe von Dateien verbieten und angeben ob ZIP-Archive entpackt werden sollen - - * Version 06.08.2018 - - Einführung einer Option, ob Dateien automatisch heruntergeladen werden sollen - - * Version 01.08.2018 - - Verbesserter Campus-Login - (Ersatz einer C-Bibliothek mit undokumentierter Abhängigkeit durch selbst entwickelten Haskell-Code erlaubt nun auch Umlaute.) - - * Version 31.07.2018 - - Viele Verbesserung zur Anzeige von Korrekturen - - Kursliste über alle Semester hinweg (Top-Level-Navigation "Kurse"), wird in Zukunft Filter/Suchfunktion erhalten - - * Version 10.07.2018 - - Bugfixes, wählbares Format für Datum - - * Version 03.07.2018 - - Willkommen bei Uni2work aka "You-need-to-work!" - diff --git a/README.md b/README.md new file mode 100644 index 000000000..f61775faa --- /dev/null +++ b/README.md @@ -0,0 +1,130 @@ +# "Quick Start" Guide + +The following description applies to Ubuntu and similar debian based Linux distributions. + +## Prerequisites +These are the things you need to do/install before you can get started working on Uni2work. + +### Clone repository +Clone this repository and navigate into it +```sh +$ git clone https://gitlab.cip.ifi.lmu.de/jost/UniWorX.git && cd UniWorX +``` + +### `LDAP` +LDAP is needed to handle logins. + +Install: +```sh +sudo apt-get install slapd ldap-utils +``` + +### `PostgreSQL` +PostgreSQL will serve as database for Uni2work. + +Install: +```sh +$ sudo apt-get install postgresql +``` + +Switch to user *postgres* (got created during installation): +```sh +$ sudo -i -u postgres +``` + +Add new database user *uniworx*: +```sh +$ createuser --interactive +``` + +You'll get a prompt: + +```sh +Enter name of role to add: uniworx +Shall the new role be a superuser? (y/n) [not exactly sure. Guess not?] +Password: uniworx +... +``` + +Create database *uniworx*: +```sh +$ psql -c 'create database uniworx owner uniworx' +$ psql -c 'create database uniworx_test owner uniworx' +``` + +After you added the database switch back to your own user with `Ctrl + D`. + +To properly access the database you now need to add a new linux user called *uniworx*. Enter "uniworx" as the password. +```sh +$ sudo adduser uniworx +``` + +### `Stack` +Stack is a toolbox for "Haskellers" to aid in developing Haskell projects. + +Install: +```sh +$ curl -sSL https://get.haskellstack.org/ | sh +``` + +Setup stack and install dependencies. This needs to be run from inside the directory you cloned this repository to: +```sh +$ stack setup +``` + +During this step or the next you might get an error that says something about missing C libraries for `ldap` and `lber`. You can install these using +```sh +$ sudo apt-get install libsasl2-dev libldap2-dev +``` + +If you get an error that says *You need to install postgresql-server-dev-X.Y for building a server-side extension or libpq-dev for building a client-side application.* +Go ahead and install `libpq-dev` with +```sh +$ sudo apt-get install libpq-dev +``` + +Other packages you might need to install during this process: +```sh +$ sudo apt-get install pkg-config +$ sudo apt-get install libsodium-dev +``` + +Build the app: +```sh +$ stack build +``` + +This might take a few minutes... if not hours... be prepared. + +install yesod: +```sh +$ stack install yesod-bin --install-ghc +``` + +### `Node` & `npm` +Node and Npm are needed to compile the frontend. + +Install: +```sh +$ curl -sL https://deb.nodesource.com/setup_12.x | sudo -E bash - +$ sudo apt-get install -y nodejs +``` + +### Add dummy data to the database +After building the app you can prepare the database and add some dummy data: +```sh +$ ./db.sh -f +``` + +## Run Uni2work +```sh +$ npm start +``` + +This will compile both frontend and backend and will start Uni2work in development mode (might take a few minutes the first time). It will keep running and will watch any file changes to automatically re-compile the application if necessary. + +If you followed the steps above you should now be able to visit http://localhost:3000 and login as one of the accounts from the Development-Logins dropdown. + +## Troubleshooting + +Please see the [wiki](https://gitlab.cip.ifi.lmu.de/jost/UniWorX/wikis/home) for more infos. diff --git a/assets/lmu/logo.svg b/assets/lmu/logo.svg new file mode 100644 index 000000000..6b72bb7b9 --- /dev/null +++ b/assets/lmu/logo.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/assets/lmu/sigillum.svg b/assets/lmu/sigillum.svg new file mode 100644 index 000000000..78538233a --- /dev/null +++ b/assets/lmu/sigillum.svg @@ -0,0 +1,19 @@ + + + + + + + image/svg+xml + + + + + + + + + + + + diff --git a/assets/logo-o2.svg b/assets/logo-o2.svg deleted file mode 100644 index 80620673b..000000000 --- a/assets/logo-o2.svg +++ /dev/null @@ -1,4 +0,0 @@ - - - - diff --git a/assets/logo.png b/assets/logo.png deleted file mode 100644 index 4ef03212e..000000000 Binary files a/assets/logo.png and /dev/null differ diff --git a/build.sh b/build.sh index 962ccc1ee..9b4f5a2e2 100755 --- a/build.sh +++ b/build.sh @@ -1,4 +1,4 @@ #!/usr/bin/env bash -exec -- stack build --fast --flag uniworx:-library-only --flag uniworx:dev +exec -- stack build --fast --flag uniworx:-library-only --flag uniworx:dev $@ echo Build task completed. diff --git a/commitlint.config.js b/commitlint.config.js new file mode 100644 index 000000000..28fe5c5bf --- /dev/null +++ b/commitlint.config.js @@ -0,0 +1 @@ +module.exports = {extends: ['@commitlint/config-conventional']} diff --git a/config/archive-types b/config/archive-types new file mode 100644 index 000000000..0599971bb --- /dev/null +++ b/config/archive-types @@ -0,0 +1,40 @@ +# Simple list of mime-types corresponding to archive-formats +# +# Comments are empty lines and any line for which the first non-whitespace symbol is ‘#’ +# +# Format is a single mime-type per line (may not contain whitespace) +# +# Largely copied from https://en.wikipedia.org/wiki/List_of_archive_formats + +application/x-archive +application/x-cpio +application/x-bcpio +application/x-shar +application/x-iso9660-image +application/x-sbx +application/x-tar +application/x-7z-compressed +application/x-ace-compressed +application/x-astrotite-afa +application/x-alz-compressed +application/vnd.android.package-archive +application/x-arj +application/x-b1 +application/vnd.ms-cab-compressed +application/x-cfs-compressed +application/x-dar +application/x-dgc-compressed +application/x-apple-diskimage +application/x-gca-compressed +application/java-archive +application/x-lzh +application/x-lzx +application/x-rar-compressed +application/x-stuffit +application/x-stuffitx +application/x-gtar +application/x-ms-wim +application/x-xar +application/zip +application/x-zoo +application/x-par2 \ No newline at end of file diff --git a/config/mimetypes b/config/mimetypes new file mode 100644 index 000000000..dd3fe4224 --- /dev/null +++ b/config/mimetypes @@ -0,0 +1,788 @@ +# Mapping of mime-types to file extensions +# +# Comments are empty lines and any line for which the first non-whitespace symbol is ‘#’ +# +# Format is a single mime-type per line (may not contain whitespace) followed by a whitespace separated list of zero or more file extension (without leading ‘.’) +# Any file extension may occur at most once within this file +# +# Extensions are compared case-insensitive (see `Data.Text.toLower`) + +application/andrew-inset ez +application/applixware aw +application/atom+xml atom +application/atomcat+xml atomcat +application/atomsvc+xml atomsvc +application/ccxml+xml ccxml +application/cdmi-capability cdmia +application/cdmi-container cdmic +application/cdmi-domain cdmid +application/cdmi-object cdmio +application/cdmi-queue cdmiq +application/cu-seeme cu +application/davmount+xml davmount +application/docbook+xml dbk +application/dssc+der dssc +application/dssc+xml xdssc +application/ecmascript ecma +application/emma+xml emma +application/epub+zip epub +application/exi exi +application/font-tdpfr pfr +application/font-woff woff +application/font-woff2 woff2 +application/futuresplash spl +application/gml+xml gml +application/gpx+xml gpx +application/gxf gxf +application/hyperstudio stk +application/inkml+xml inkml ink +application/ipfix ipfix +application/java-archive war jar ear +application/java-serialized-object ser +application/java-vm class +application/javascript js +application/json json +application/jsonml+json jsonml +application/lost+xml lostxml +application/mac-binhex40 hqx +application/mac-compactpro cpt +application/mads+xml mads +application/marc mrc +application/marcxml+xml mrcx +application/mathematica nb mb ma +application/mathml+xml mathml +application/mbox mbox +application/mediaservercontrol+xml mscml +application/metalink+xml metalink +application/metalink4+xml meta4 +application/mets+xml mets +application/mods+xml mods +application/mp21 mp21 m21 +application/mp4 mp4s +application/msword dot doc +application/mxf mxf +application/octet-stream so pkg msp msm mar lrf img elc dump dms distz dist deploy bpk bin +application/oda oda +application/oebps-package+xml opf +application/ogg ogx +application/omdoc+xml omdoc +application/onenote onetoc2 onetoc onetmp onepkg +application/oxps oxps +application/patch-ops-error+xml xer +application/pdf pdf +application/pgp-encrypted pgp +application/pgp-signature sig +application/pics-rules prf +application/pkcs10 p10 +application/pkcs7-mime p7m p7c +application/pkcs7-signature p7s +application/pkcs8 p8 +application/pkix-attr-cert ac +application/pkix-cert cer +application/pkix-crl crl +application/pkix-pkipath pkipath +application/pkixcmp pki +application/pls+xml pls +application/postscript ps eps ai +application/prs.cww cww +application/pskc+xml pskcxml +application/rdf+xml rdf +application/reginfo+xml rif +application/relax-ng-compact-syntax rnc +application/resource-lists+xml rl +application/resource-lists-diff+xml rld +application/rls-services+xml rs +application/rpki-ghostbusters gbr +application/rpki-manifest mft +application/rpki-roa roa +application/rsd+xml rsd +application/rss+xml rss +application/rtf rtf +application/sbml+xml sbml +application/scvp-cv-request scq +application/scvp-cv-response scs +application/scvp-vp-request spq +application/scvp-vp-response spp +application/sdp sdp +application/set-payment-initiation setpay +application/set-registration-initiation setreg +application/shf+xml shf +application/smil+xml smil smi +application/sparql-query rq +application/sparql-results+xml srx +application/srgs gram +application/srgs+xml grxml +application/sru+xml sru +application/ssdl+xml ssdl +application/ssml+xml ssml +application/tei+xml teicorpus tei +application/thraud+xml tfi +application/timestamped-data tsd +application/vnd.3gpp.pic-bw-large plb +application/vnd.3gpp.pic-bw-small psb +application/vnd.3gpp.pic-bw-var pvb +application/vnd.3gpp2.tcap tcap +application/vnd.3m.post-it-notes pwn +application/vnd.accpac.simply.aso aso +application/vnd.accpac.simply.imp imp +application/vnd.acucobol acu +application/vnd.acucorp atc acutc +application/vnd.adobe.air-application-installer-package+zip air +application/vnd.adobe.formscentral.fcdt fcdt +application/vnd.adobe.fxp fxpl fxp +application/vnd.adobe.xdp+xml xdp +application/vnd.adobe.xfdf xfdf +application/vnd.ahead.space ahead +application/vnd.airzip.filesecure.azf azf +application/vnd.airzip.filesecure.azs azs +application/vnd.amazon.ebook azw +application/vnd.americandynamics.acc acc +application/vnd.amiga.ami ami +application/vnd.android.package-archive apk +application/vnd.anser-web-certificate-issue-initiation cii +application/vnd.anser-web-funds-transfer-initiation fti +application/vnd.antix.game-component atx +application/vnd.apple.installer+xml mpkg +application/vnd.apple.mpegurl m3u8 +application/vnd.aristanetworks.swi swi +application/vnd.astraea-software.iota iota +application/vnd.audiograph aep +application/vnd.blueice.multipass mpm +application/vnd.bmi bmi +application/vnd.businessobjects rep +application/vnd.chemdraw+xml cdxml +application/vnd.chipnuts.karaoke-mmd mmd +application/vnd.cinderella cdy +application/vnd.claymore cla +application/vnd.cloanto.rp9 rp9 +application/vnd.clonk.c4group c4u c4p c4g c4f c4d +application/vnd.cluetrust.cartomobile-config c11amc +application/vnd.cluetrust.cartomobile-config-pkg c11amz +application/vnd.commonspace csp +application/vnd.contact.cmsg cdbcmsg +application/vnd.cosmocaller cmc +application/vnd.crick.clicker clkx +application/vnd.crick.clicker.keyboard clkk +application/vnd.crick.clicker.palette clkp +application/vnd.crick.clicker.template clkt +application/vnd.crick.clicker.wordbank clkw +application/vnd.criticaltools.wbs+xml wbs +application/vnd.ctc-posml pml +application/vnd.cups-ppd ppd +application/vnd.curl.car car +application/vnd.curl.pcurl pcurl +application/vnd.dart dart +application/vnd.data-vision.rdz rdz +application/vnd.dece.data uvvf uvvd uvf uvd +application/vnd.dece.ttml+xml uvvt uvt +application/vnd.dece.unspecified uvx uvvx +application/vnd.dece.zip uvz uvvz +application/vnd.denovo.fcselayout-link fe_launch +application/vnd.dna dna +application/vnd.dolby.mlp mlp +application/vnd.dpgraph dpg +application/vnd.dreamfactory dfac +application/vnd.ds-keypoint kpxx +application/vnd.dvb.ait ait +application/vnd.dvb.service svc +application/vnd.dynageo geo +application/vnd.ecowin.chart mag +application/vnd.enliven nml +application/vnd.epson.esf esf +application/vnd.epson.msf msf +application/vnd.epson.quickanime qam +application/vnd.epson.salt slt +application/vnd.epson.ssf ssf +application/vnd.eszigno3+xml et3 es3 +application/vnd.ezpix-album ez2 +application/vnd.ezpix-package ez3 +application/vnd.fdf fdf +application/vnd.fdsn.mseed mseed +application/vnd.fdsn.seed seed dataless +application/vnd.flographit gph +application/vnd.fluxtime.clip ftc +application/vnd.framemaker maker frame fm book +application/vnd.frogans.fnc fnc +application/vnd.frogans.ltf ltf +application/vnd.fsc.weblaunch fsc +application/vnd.fujitsu.oasys oas +application/vnd.fujitsu.oasys2 oa2 +application/vnd.fujitsu.oasys3 oa3 +application/vnd.fujitsu.oasysgp fg5 +application/vnd.fujitsu.oasysprs bh2 +application/vnd.fujixerox.ddd ddd +application/vnd.fujixerox.docuworks xdw +application/vnd.fujixerox.docuworks.binder xbd +application/vnd.fuzzysheet fzs +application/vnd.genomatix.tuxedo txd +application/vnd.geogebra.file ggb +application/vnd.geogebra.tool ggt +application/vnd.geometry-explorer gre gex +application/vnd.geonext gxt +application/vnd.geoplan g2w +application/vnd.geospace g3w +application/vnd.gmx gmx +application/vnd.google-earth.kml+xml kml +application/vnd.google-earth.kmz kmz +application/vnd.grafeq gqs gqf +application/vnd.groove-account gac +application/vnd.groove-help ghf +application/vnd.groove-identity-message gim +application/vnd.groove-injector grv +application/vnd.groove-tool-message gtm +application/vnd.groove-tool-template tpl +application/vnd.groove-vcard vcg +application/vnd.hal+xml hal +application/vnd.handheld-entertainment+xml zmm +application/vnd.hbci hbci +application/vnd.hhe.lesson-player les +application/vnd.hp-hpgl hpgl +application/vnd.hp-hpid hpid +application/vnd.hp-hps hps +application/vnd.hp-jlyt jlt +application/vnd.hp-pcl pcl +application/vnd.hp-pclxl pclxl +application/vnd.hydrostatix.sof-data sfd-hdstx +application/vnd.ibm.minipay mpy +application/vnd.ibm.modcap listafp list3820 afp +application/vnd.ibm.rights-management irm +application/vnd.ibm.secure-container sc +application/vnd.iccprofile icm icc +application/vnd.igloader igl +application/vnd.immervision-ivp ivp +application/vnd.immervision-ivu ivu +application/vnd.insors.igm igm +application/vnd.intercon.formnet xpx xpw +application/vnd.intergeo i2g +application/vnd.intu.qbo qbo +application/vnd.intu.qfx qfx +application/vnd.ipunplugged.rcprofile rcprofile +application/vnd.irepository.package+xml irp +application/vnd.is-xpr xpr +application/vnd.isac.fcs fcs +application/vnd.jam jam +application/vnd.jcp.javame.midlet-rms rms +application/vnd.jisp jisp +application/vnd.joost.joda-archive joda +application/vnd.kahootz ktz ktr +application/vnd.kde.karbon karbon +application/vnd.kde.kchart chrt +application/vnd.kde.kformula kfo +application/vnd.kde.kivio flw +application/vnd.kde.kontour kon +application/vnd.kde.kpresenter kpt kpr +application/vnd.kde.kspread ksp +application/vnd.kde.kword kwt kwd +application/vnd.kenameaapp htke +application/vnd.kidspiration kia +application/vnd.kinar knp kne +application/vnd.koan skt skp skm skd +application/vnd.kodak-descriptor sse +application/vnd.las.las+xml lasxml +application/vnd.llamagraphics.life-balance.desktop lbd +application/vnd.llamagraphics.life-balance.exchange+xml lbe +application/vnd.lotus-1-2-3 123 +application/vnd.lotus-approach apr +application/vnd.lotus-freelance pre +application/vnd.lotus-notes nsf +application/vnd.lotus-organizer org +application/vnd.lotus-screencam scm +application/vnd.lotus-wordpro lwp +application/vnd.macports.portpkg portpkg +application/vnd.mcd mcd +application/vnd.medcalcdata mc1 +application/vnd.mediastation.cdkey cdkey +application/vnd.mfer mwf +application/vnd.mfmp mfm +application/vnd.micrografx.flo flo +application/vnd.micrografx.igx igx +application/vnd.mif mif +application/vnd.mobius.daf daf +application/vnd.mobius.dis dis +application/vnd.mobius.mbk mbk +application/vnd.mobius.mqy mqy +application/vnd.mobius.msl msl +application/vnd.mobius.plc plc +application/vnd.mobius.txf txf +application/vnd.mophun.application mpn +application/vnd.mophun.certificate mpc +application/vnd.mozilla.xul+xml xul +application/vnd.ms-artgalry cil +application/vnd.ms-cab-compressed cab +application/vnd.ms-excel xlw xlt xls xlm xlc xla +application/vnd.ms-excel.addin.macroenabled.12 xlam +application/vnd.ms-excel.sheet.binary.macroenabled.12 xlsb +application/vnd.ms-excel.sheet.macroenabled.12 xlsm +application/vnd.ms-excel.template.macroenabled.12 xltm +application/vnd.ms-fontobject eot +application/vnd.ms-htmlhelp chm +application/vnd.ms-ims ims +application/vnd.ms-lrm lrm +application/vnd.ms-officetheme thmx +application/vnd.ms-pki.seccat cat +application/vnd.ms-pki.stl stl +application/vnd.ms-powerpoint ppt pps pot +application/vnd.ms-powerpoint.addin.macroenabled.12 ppam +application/vnd.ms-powerpoint.presentation.macroenabled.12 pptm +application/vnd.ms-powerpoint.slide.macroenabled.12 sldm +application/vnd.ms-powerpoint.slideshow.macroenabled.12 ppsm +application/vnd.ms-powerpoint.template.macroenabled.12 potm +application/vnd.ms-project mpt mpp +application/vnd.ms-word.document.macroenabled.12 docm +application/vnd.ms-word.template.macroenabled.12 dotm +application/vnd.ms-works wps wks wdb wcm +application/vnd.ms-wpl wpl +application/vnd.ms-xpsdocument xps +application/vnd.mseq mseq +application/vnd.musician mus +application/vnd.muvee.style msty +application/vnd.mynfc taglet +application/vnd.neurolanguage.nlu nlu +application/vnd.nitf ntf nitf +application/vnd.noblenet-directory nnd +application/vnd.noblenet-sealer nns +application/vnd.noblenet-web nnw +application/vnd.nokia.n-gage.data ngdat +application/vnd.nokia.n-gage.symbian.install n-gage +application/vnd.nokia.radio-preset rpst +application/vnd.nokia.radio-presets rpss +application/vnd.novadigm.edm edm +application/vnd.novadigm.edx edx +application/vnd.novadigm.ext ext +application/vnd.oasis.opendocument.chart odc +application/vnd.oasis.opendocument.chart-template otc +application/vnd.oasis.opendocument.database odb +application/vnd.oasis.opendocument.formula odf +application/vnd.oasis.opendocument.formula-template odft +application/vnd.oasis.opendocument.graphics odg +application/vnd.oasis.opendocument.graphics-template otg +application/vnd.oasis.opendocument.image odi +application/vnd.oasis.opendocument.image-template oti +application/vnd.oasis.opendocument.presentation odp +application/vnd.oasis.opendocument.presentation-template otp +application/vnd.oasis.opendocument.spreadsheet ods +application/vnd.oasis.opendocument.spreadsheet-template ots +application/vnd.oasis.opendocument.text odt +application/vnd.oasis.opendocument.text-master odm +application/vnd.oasis.opendocument.text-template ott +application/vnd.oasis.opendocument.text-web oth +application/vnd.olpc-sugar xo +application/vnd.oma.dd2+xml dd2 +application/vnd.openofficeorg.extension oxt +application/vnd.openxmlformats-officedocument.presentationml.presentation pptx +application/vnd.openxmlformats-officedocument.presentationml.slide sldx +application/vnd.openxmlformats-officedocument.presentationml.slideshow ppsx +application/vnd.openxmlformats-officedocument.presentationml.template potx +application/vnd.openxmlformats-officedocument.spreadsheetml.sheet xlsx +application/vnd.openxmlformats-officedocument.spreadsheetml.template xltx +application/vnd.openxmlformats-officedocument.wordprocessingml.document docx +application/vnd.openxmlformats-officedocument.wordprocessingml.template dotx +application/vnd.osgeo.mapguide.package mgp +application/vnd.osgi.dp dp +application/vnd.osgi.subsystem esa +application/vnd.palm pqa pdb oprc +application/vnd.pawaafile paw +application/vnd.pg.format str +application/vnd.pg.osasli ei6 +application/vnd.picsel efif +application/vnd.pmi.widget wg +application/vnd.pocketlearn plf +application/vnd.powerbuilder6 pbd +application/vnd.previewsystems.box box +application/vnd.proteus.magazine mgz +application/vnd.publishare-delta-tree qps +application/vnd.pvi.ptid1 ptid +application/vnd.quark.quarkxpress qxt qxl qxd qxb qwt qwd +application/vnd.realvnc.bed bed +application/vnd.recordare.musicxml mxl +application/vnd.recordare.musicxml+xml musicxml +application/vnd.rig.cryptonote cryptonote +application/vnd.rim.cod cod +application/vnd.rn-realmedia rm +application/vnd.rn-realmedia-vbr rmvb +application/vnd.route66.link66+xml link66 +application/vnd.sailingtracker.track st +application/vnd.seemail see +application/vnd.sema sema +application/vnd.semd semd +application/vnd.semf semf +application/vnd.shana.informed.formdata ifm +application/vnd.shana.informed.formtemplate itp +application/vnd.shana.informed.interchange iif +application/vnd.shana.informed.package ipk +application/vnd.simtech-mindmapper twds twd +application/vnd.smaf mmf +application/vnd.smart.teacher teacher +application/vnd.solent.sdkm+xml sdkm sdkd +application/vnd.spotfire.dxp dxp +application/vnd.spotfire.sfs sfs +application/vnd.stardivision.calc sdc +application/vnd.stardivision.draw sda +application/vnd.stardivision.impress sdd +application/vnd.stardivision.math smf +application/vnd.stardivision.writer vor sdw +application/vnd.stardivision.writer-global sgl +application/vnd.stepmania.package smzip +application/vnd.stepmania.stepchart sm +application/vnd.sun.xml.calc sxc +application/vnd.sun.xml.calc.template stc +application/vnd.sun.xml.draw sxd +application/vnd.sun.xml.draw.template std +application/vnd.sun.xml.impress sxi +application/vnd.sun.xml.impress.template sti +application/vnd.sun.xml.math sxm +application/vnd.sun.xml.writer sxw +application/vnd.sun.xml.writer.global sxg +application/vnd.sun.xml.writer.template stw +application/vnd.sus-calendar susp sus +application/vnd.svd svd +application/vnd.symbian.install sisx sis +application/vnd.syncml+xml xsm +application/vnd.syncml.dm+wbxml bdm +application/vnd.syncml.dm+xml xdm +application/vnd.tao.intent-module-archive tao +application/vnd.tcpdump.pcap pcap dmp cap +application/vnd.tmobile-livetv tmo +application/vnd.trid.tpt tpt +application/vnd.triscape.mxs mxs +application/vnd.trueapp tra +application/vnd.ufdl ufdl ufd +application/vnd.uiq.theme utz +application/vnd.umajin umj +application/vnd.unity unityweb +application/vnd.uoml+xml uoml +application/vnd.vcx vcx +application/vnd.visio vsw vst vss vsd +application/vnd.visionary vis +application/vnd.vsf vsf +application/vnd.wap.wbxml wbxml +application/vnd.wap.wmlc wmlc +application/vnd.wap.wmlscriptc wmlsc +application/vnd.webturbo wtb +application/vnd.wolfram.player nbp +application/vnd.wordperfect wpd +application/vnd.wqd wqd +application/vnd.wt.stf stf +application/vnd.xara xar +application/vnd.xfdl xfdl +application/vnd.yamaha.hv-dic hvd +application/vnd.yamaha.hv-script hvs +application/vnd.yamaha.hv-voice hvp +application/vnd.yamaha.openscoreformat osf +application/vnd.yamaha.openscoreformat.osfpvg+xml osfpvg +application/vnd.yamaha.smaf-audio saf +application/vnd.yamaha.smaf-phrase spf +application/vnd.yellowriver-custom-menu cmp +application/vnd.zul zirz zir +application/vnd.zzazz.deck+xml zaz +application/voicexml+xml vxml +application/widget wgt +application/winhlp hlp +application/wsdl+xml wsdl +application/wspolicy+xml wspolicy +application/x-7z-compressed 7z +application/x-abiword abw +application/x-ace-compressed ace +application/x-apple-diskimage dmg +application/x-authorware-bin x32 vox u32 aab +application/x-authorware-map aam +application/x-authorware-seg aas +application/x-bcpio bcpio +application/x-bittorrent torrent +application/x-blorb blorb blb +application/x-bzip bz2 bz +application/x-bzip-compressed-tar tbz tar.bz2 +application/x-bzip2 boz +application/x-cbr cbz cbt cbr cba cb7 +application/x-cdlink vcd +application/x-cfs-compressed cfs +application/x-chat chat +application/x-chess-pgn pgn +application/x-cocoa cco +application/x-conference nsc +application/x-cpio cpio +application/x-csh csh +application/x-debian-package udeb deb +application/x-dgc-compressed dgc +application/x-director w3d swa fgd dxr dir dcr cxt cst cct +application/x-doom wad +application/x-dtbncx+xml ncx +application/x-dtbook+xml dtb +application/x-dtbresource+xml res +application/x-dvi dvi +application/x-envoy evy +application/x-eva eva +application/x-font-bdf bdf +application/x-font-ghostscript gsf +application/x-font-linux-psf psf +application/x-font-otf otf +application/x-font-pcf pcf +application/x-font-snf snf +application/x-font-ttf ttf ttc +application/x-font-type1 pfm pfb pfa afm +application/x-freearc arc +application/x-gca-compressed gca +application/x-glulx ulx +application/x-gnumeric gnumeric +application/x-gramps-xml gramps +application/x-gtar gtar +application/x-gzip gz +application/x-hdf hdf +application/x-install-instructions install +application/x-iso9660-image iso +application/x-java-archive-diff jardiff +application/x-java-jnlp-file jnlp +application/x-latex latex +application/x-lzh-compressed lzh lha +application/x-makeself run +application/x-mie mie +application/x-mobipocket-ebook prc mobi +application/x-ms-application application +application/x-ms-shortcut lnk +application/x-ms-wmd wmd +application/x-ms-xbap xbap +application/x-msaccess mdb +application/x-msbinder obd +application/x-mscardfile crd +application/x-msclip clp +application/x-msdownload msi exe dll com bat +application/x-msmediaview mvb m14 m13 +application/x-msmetafile wmz wmf emz emf +application/x-msmoney mny +application/x-mspublisher pub +application/x-msschedule scd +application/x-msterminal trm +application/x-mswrite wri +application/x-netcdf nc cdf +application/x-ns-proxy-autoconfig pac +application/x-nzb nzb +application/x-perl pm pl +application/x-pkcs12 pfx p12 +application/x-pkcs7-certificates spc p7b +application/x-pkcs7-certreqresp p7r +application/x-rar-compressed rar +application/x-redhat-package-manager rpm +application/x-research-info-systems ris +application/x-sea sea +application/x-sh sh +application/x-shar shar +application/x-shockwave-flash swf +application/x-silverlight-app xap +application/x-sql sql +application/x-stuffit sit +application/x-stuffitx sitx +application/x-subrip srt +application/x-sv4cpio sv4cpio +application/x-sv4crc sv4crc +application/x-t3vm-image t3 +application/x-tads gam +application/x-tar tar +application/x-tcl tk tcl +application/x-tex tex +application/x-tex-tfm tfm +application/x-texinfo texinfo texi +application/x-tgif obj +application/x-tgz tgz tar.gz +application/x-ustar ustar +application/x-wais-source src +application/x-x509-ca-cert pem der crt +application/x-xfig fig +application/x-xliff+xml xlf +application/x-xpinstall xpi +application/x-xz xz +application/x-zmachine z8 z7 z6 z5 z4 z3 z2 z1 +application/xaml+xml xaml +application/xcap-diff+xml xdf +application/xenc+xml xenc +application/xhtml+xml xhtml xht +application/xml xsl +application/xml-dtd dtd +application/xop+xml xop +application/xproc+xml xpl +application/xslt+xml xslt +application/xspf+xml xspf +application/xv+xml xvml xvm xhvml mxml +application/yang yang +application/yin+xml yin +application/zip zip +audio/adpcm adp +audio/basic snd au +audio/midi rmi midi mid kar +audio/mp4 mp4a +audio/mpeg mpga mp3 mp2a mp2 m3a m2a +audio/ogg spx ogg oga +audio/s3m s3m +audio/silk sil +audio/vnd.dece.audio uvva uva +audio/vnd.digital-winds eol +audio/vnd.dra dra +audio/vnd.dts dts +audio/vnd.dts.hd dtshd +audio/vnd.lucent.voice lvp +audio/vnd.ms-playready.media.pya pya +audio/vnd.nuera.ecelp4800 ecelp4800 +audio/vnd.nuera.ecelp7470 ecelp7470 +audio/vnd.nuera.ecelp9600 ecelp9600 +audio/vnd.rip rip +audio/webm weba +audio/x-aac aac +audio/x-aiff aiff aifc aif +audio/x-caf caf +audio/x-flac flac +audio/x-m4a m4a +audio/x-matroska mka +audio/x-mpegurl m3u +audio/x-ms-wax wax +audio/x-ms-wma wma +audio/x-pn-realaudio ram ra +audio/x-pn-realaudio-plugin rmp +audio/x-wav wav +audio/xm xm +chemical/x-cdx cdx +chemical/x-cif cif +chemical/x-cmdf cmdf +chemical/x-cml cml +chemical/x-csml csml +chemical/x-xyz xyz +image/bmp bmp +image/cgm cgm +image/g3fax g3 +image/gif gif +image/ief ief +image/jpeg jpg jpeg jpe +image/ktx ktx +image/png png +image/prs.btif btif +image/sgi sgi +image/svg+xml svgz svg +image/tiff tiff tif +image/vnd.adobe.photoshop psd +image/vnd.dece.graphic uvvi uvvg uvi uvg +image/vnd.djvu djvu djv +image/vnd.dwg dwg +image/vnd.dxf dxf +image/vnd.fastbidsheet fbs +image/vnd.fpx fpx +image/vnd.fst fst +image/vnd.fujixerox.edmics-mmr mmr +image/vnd.fujixerox.edmics-rlc rlc +image/vnd.microsoft.icon ico +image/vnd.ms-modi mdi +image/vnd.ms-photo wdp +image/vnd.net-fpx npx +image/vnd.wap.wbmp wbmp +image/vnd.xiff xif +image/webp webp +image/x-3ds 3ds +image/x-cmu-raster ras +image/x-cmx cmx +image/x-freehand fhc fh7 fh5 fh4 fh +image/x-jng jng +image/x-mrsid-image sid +image/x-pcx pcx +image/x-pict pic pct +image/x-portable-anymap pnm +image/x-portable-bitmap pbm +image/x-portable-graymap pgm +image/x-portable-pixmap ppm +image/x-rgb rgb +image/x-tga tga +image/x-xbitmap xbm +image/x-xpixmap xpm +image/x-xwindowdump xwd +message/rfc822 mime eml +model/iges igs iges +model/mesh silo msh mesh +model/vnd.collada+xml dae +model/vnd.dwf dwf +model/vnd.gdl gdl +model/vnd.gtw gtw +model/vnd.mts mts +model/vnd.vtu vtu +model/vrml wrl vrml +model/x3d+binary x3dbz x3db +model/x3d+vrml x3dvz x3dv +model/x3d+xml x3dz x3d +text/cache-manifest manifest appcache +text/calendar ifb ics +text/css less css +text/csv csv +text/html shtml html htm +text/mathml mml +text/n3 n3 +text/plain txt text log list in hs def cxx cpp conf c asc +text/prs.lines.tag dsc +text/richtext rtx +text/sgml sgml sgm +text/tab-separated-values tsv +text/troff tr t roff ms me man +text/turtle ttl +text/uri-list urls uris uri +text/vcard vcard +text/vnd.curl curl +text/vnd.curl.dcurl dcurl +text/vnd.curl.mcurl mcurl +text/vnd.curl.scurl scurl +text/vnd.dvb.subtitle sub +text/vnd.fly fly +text/vnd.fmi.flexstor flx +text/vnd.graphviz gv +text/vnd.in3d.3dml 3dml +text/vnd.in3d.spot spot +text/vnd.sun.j2me.app-descriptor jad +text/vnd.wap.wml wml +text/vnd.wap.wmlscript wmls +text/x-asm s asm +text/x-c hh h dic cc +text/x-component htc +text/x-fortran for f90 f77 f +text/x-java-source java +text/x-nfo nfo +text/x-opml opml +text/x-pascal pas p +text/x-setext etx +text/x-sfv sfv +text/x-uuencode uu +text/x-vcalendar vcs +text/x-vcard vcf +text/xml xml +video/3gpp 3gpp 3gp +video/3gpp2 3g2 +video/h261 h261 +video/h263 h263 +video/h264 h264 +video/jpeg jpgv +video/jpm jpm jpgm +video/mj2 mjp2 mj2 +video/mp4 mpg4 mp4v mp4 +video/mpeg mpg mpeg mpe m2v m1v +video/ogg ogv +video/quicktime qt mov +video/vnd.dece.hd uvvh uvh +video/vnd.dece.mobile uvvm uvm +video/vnd.dece.pd uvvp uvp +video/vnd.dece.sd uvvs uvs +video/vnd.dece.video uvvv uvv +video/vnd.dvb.file dvb +video/vnd.fvt fvt +video/vnd.mpegurl mxu m4u +video/vnd.ms-playready.media.pyv pyv +video/vnd.uvvu.mp4 uvvu uvu +video/vnd.vivo viv +video/webm webm +video/x-f4v f4v +video/x-fli fli +video/x-flv flv +video/x-m4v m4v +video/x-matroska mkv mks mk3d +video/x-mng mng +video/x-ms-asf asx asf +video/x-ms-vob vob +video/x-ms-wm wm +video/x-ms-wmv wmv +video/x-ms-wmx wmx +video/x-ms-wvx wvx +video/x-msvideo avi +video/x-sgi-movie movie +video/x-smv smv +x-conference/x-cooltalk ice diff --git a/config/settings.yml b/config/settings.yml index 3211d42db..edd971e64 100644 --- a/config/settings.yml +++ b/config/settings.yml @@ -27,7 +27,17 @@ notification-rate-limit: 3600 notification-collate-delay: 300 notification-expiration: 259201 session-timeout: 7200 -maximum-content-length: 52428800 +jwt-expiration: 604800 +jwt-encoding: HS256 +maximum-content-length: "_env:MAX_UPLOAD_SIZE:134217728" +health-check-interval: + matching-cluster-config: "_env:HEALTHCHECK_INTERVAL_MATCHING_CLUSTER_CONFIG:600" + http-reachable: "_env:HEALTHCHECK_INTERVAL_HTTP_REACHABLE:600" + ldap-admins: "_env:HEALTHCHECK_INTERVAL_LDAP_ADMINS:600" + smtp-connect: "_env:HEALTHCHECK_INTERVAL_SMTP_CONNECT:600" + widget-memcached: "_env:HEALTHCHECK_INTERVAL_WIDGET_MEMCACHED:600" +health-check-delay-notify: "_env:HEALTHCHECK_DELAY_NOTIFY:true" +health-check-http: "_env:HEALTHCHECK_HTTP:true" # Can we assume, that we can reach ourselves under APPROOT via HTTP (reverse proxies or firewalls might prevent this)? log-settings: detailed: "_env:DETAILED_LOGGING:false" @@ -59,6 +69,8 @@ database: database: "_env:PGDATABASE:uniworx" poolsize: "_env:PGPOOLSIZE:10" +auto-db-migrate: '_env:AUTO_DB_MIGRATE:true' + ldap: host: "_env:LDAPHOST:" tls: "_env:LDAPTLS:" diff --git a/db.sh b/db.sh index b05463c3a..3d80bf68f 100755 --- a/db.sh +++ b/db.sh @@ -1,4 +1,6 @@ #!/usr/bin/env bash # Options: see /test/Database.hs (Main) +set -e + stack build --fast --flag uniworx:-library-only --flag uniworx:dev stack exec uniworxdb -- $@ diff --git a/static/js/polyfills/fetchPolyfill.js b/frontend/polyfills/fetch.js similarity index 100% rename from static/js/polyfills/fetchPolyfill.js rename to frontend/polyfills/fetch.js diff --git a/frontend/polyfills/main.js b/frontend/polyfills/main.js new file mode 100644 index 000000000..7e4c554ea --- /dev/null +++ b/frontend/polyfills/main.js @@ -0,0 +1,2 @@ +import './fetch'; +import './url-search-params'; diff --git a/static/js/polyfills/urlPolyfill.js b/frontend/polyfills/url-search-params.js similarity index 100% rename from static/js/polyfills/urlPolyfill.js rename to frontend/polyfills/url-search-params.js diff --git a/frontend/src/app.js b/frontend/src/app.js new file mode 100644 index 000000000..b2db86c31 --- /dev/null +++ b/frontend/src/app.js @@ -0,0 +1,28 @@ +import { HttpClient } from './services/http-client/http-client'; +import { HtmlHelpers } from './services/html-helpers/html-helpers'; +import { I18n } from './services/i18n/i18n'; +import { UtilRegistry } from './services/util-registry/util-registry'; +import { isValidUtility } from './core/utility'; + +export class App { + httpClient = new HttpClient(); + htmlHelpers = new HtmlHelpers(); + i18n = new I18n(); + utilRegistry = new UtilRegistry(); + + constructor() { + this.utilRegistry.setApp(this); + + document.addEventListener('DOMContentLoaded', () => this.utilRegistry.setupAll()); + } + + registerUtilities(utils) { + if (!Array.isArray(utils)) { + throw new Error('Utils are expected to be passed as array!'); + } + + utils.filter(isValidUtility).forEach((util) => { + this.utilRegistry.register(util); + }); + } +} diff --git a/frontend/src/app.spec.js b/frontend/src/app.spec.js new file mode 100644 index 000000000..247be9f00 --- /dev/null +++ b/frontend/src/app.spec.js @@ -0,0 +1,65 @@ +import { App } from './app'; +import { Utility } from './core/utility'; + +@Utility({ selector: 'util1' }) +class TestUtil1 { } + +@Utility({ selector: 'util2' }) +class TestUtil2 { } + +const TEST_UTILS = [ + TestUtil1, + TestUtil2, +]; + +describe('App', () => { + let app; + + beforeEach(() => { + app = new App(); + }); + + it('should create', () => { + expect(app).toBeTruthy(); + }); + + it('should setup all utlites when page is done loading', () => { + spyOn(app.utilRegistry, 'setupAll'); + document.dispatchEvent(new Event('DOMContentLoaded')); + expect(app.utilRegistry.setupAll).toHaveBeenCalled(); + }); + + describe('provides services', () => { + it('HttpClient as httpClient', () => { + expect(app.httpClient).toBeTruthy(); + }); + + it('HtmlHelpers as htmlHelpers', () => { + expect(app.htmlHelpers).toBeTruthy(); + }); + + it('I18n as i18n', () => { + expect(app.i18n).toBeTruthy(); + }); + + it('UtilRegistry as utilRegistry', () => { + expect(app.utilRegistry).toBeTruthy(); + }); + }); + + describe('registerUtilities()', () => { + it('should register the given utilities', () => { + spyOn(app.utilRegistry, 'register'); + app.registerUtilities(TEST_UTILS); + expect(app.utilRegistry.register.calls.count()).toBe(TEST_UTILS.length); + expect(app.utilRegistry.register.calls.argsFor(0)).toEqual([TEST_UTILS[0]]); + expect(app.utilRegistry.register.calls.argsFor(1)).toEqual([TEST_UTILS[1]]); + }); + + it('should throw an error if not passed an array of utilities', () => { + expect(() => { + app.registerUtilities({}); + }).toThrow(); + }); + }); +}); diff --git a/frontend/src/core/utility.js b/frontend/src/core/utility.js new file mode 100644 index 000000000..dba52923a --- /dev/null +++ b/frontend/src/core/utility.js @@ -0,0 +1,22 @@ +export function isValidUtility(utility) { + if (!utility) { + return false; + } + + if (!utility.selector) { + return false; + } + + return true; +}; + +export function Utility(metadata) { + if (!metadata.selector) { + throw new Error('Utility needs to have a selector!'); + } + + return function (target) { + target.selector = metadata.selector; + target.isUtility = true; + }; +}; diff --git a/frontend/src/main.js b/frontend/src/main.js new file mode 100644 index 000000000..aa509bdc3 --- /dev/null +++ b/frontend/src/main.js @@ -0,0 +1,24 @@ +import { App } from './app'; +import { Utils } from './utils/utils'; + +export const app = new App(); +app.registerUtilities(Utils); + +// attach the app to window to be able to get a hold of the +// app instance from the shakespearean templates +window.App = app; + +// dont know where to put this currently... +// interceptor to throw an error if an http response does not match the expected content-type +// function contentTypeInterceptor(response, options) { +// if (!options || !options.headers.get('Accept')) { +// return; +// } + +// const contentType = response.headers.get('content-type'); +// if (!contentType.match(options.accept)) { +// throw new Error('Server returned with '' + contentType + '' when '' + options.accept + '' was expected'); +// } +// } + +// window.HttpClient.addResponseInterceptor(contentTypeInterceptor); diff --git a/frontend/src/services/html-helpers/html-helpers.js b/frontend/src/services/html-helpers/html-helpers.js new file mode 100644 index 000000000..b8bf7771b --- /dev/null +++ b/frontend/src/services/html-helpers/html-helpers.js @@ -0,0 +1,42 @@ +export class HtmlHelpers { + + // `parseResponse` takes a raw HttpClient response and an options object. + // Returns an object with `element` being an contextual fragment of the + // HTML in the response and `ifPrefix` being the prefix that was used to + // 'unique-ify' the ids of the received HTML. + // Original Response IDs can optionally be kept by adding `keepIds: true` + // to the `options` object. + parseResponse(response, options = {}) { + return response.text() + .then( + (responseText) => { + const element = document.createElement('div'); + element.innerHTML = responseText; + let idPrefix = ''; + if (!options.keepIds) { + idPrefix = this._getIdPrefix(); + this._prefixIds(element, idPrefix); + } + return Promise.resolve({ idPrefix, element }); + }, + Promise.reject, + ).catch(console.error); + } + + _prefixIds(element, idPrefix) { + const idAttrs = ['id', 'for', 'data-conditional-input', 'data-modal-trigger']; + + idAttrs.forEach((attr) => { + Array.from(element.querySelectorAll('[' + attr + ']')).forEach((input) => { + const value = idPrefix + input.getAttribute(attr); + input.setAttribute(attr, value); + }); + }); + } + + _getIdPrefix() { + // leading 'r'(andom) to overcome the fact that IDs + // starting with a numeric value are not valid in CSS + return 'r' + Math.floor(Math.random() * 100000) + '__'; + } +} diff --git a/frontend/src/services/html-helpers/html-helpers.spec.js b/frontend/src/services/html-helpers/html-helpers.spec.js new file mode 100644 index 000000000..f092e17fa --- /dev/null +++ b/frontend/src/services/html-helpers/html-helpers.spec.js @@ -0,0 +1,56 @@ +import { HtmlHelpers } from './html-helpers'; + +describe('HtmlHelpers', () => { + let htmlHelpers; + + beforeEach(() => { + htmlHelpers = new HtmlHelpers(); + }); + + it('should create', () => { + expect(htmlHelpers).toBeTruthy(); + }); + + describe('parseResponse()', () => { + let fakeHttpResponse; + + beforeEach(() => { + fakeHttpResponse = { + text: () => Promise.resolve('
Test
'), + }; + }); + + it('should return a promise with idPrefix and element', (done) => { + htmlHelpers.parseResponse(fakeHttpResponse).then(result => { + expect(result.idPrefix).toBeDefined(); + expect(result.element).toBeDefined(); + expect(result.element.textContent).toMatch('Test'); + done(); + }); + }); + + it('should nudge IDs', (done) => { + htmlHelpers.parseResponse(fakeHttpResponse).then(result => { + expect(result.idPrefix).toBeDefined(); + expect(result.element).toBeDefined(); + const elementWithOrigId = result.element.querySelector('#test-div'); + expect(elementWithOrigId).toBeFalsy(); + const elementWithNudgedId = result.element.querySelector('#' + result.idPrefix + 'test-div'); + expect(elementWithNudgedId).toBeTruthy(); + done(); + }); + }); + + it('should not nudge IDs with option "keepIds"', (done) => { + const options = { keepIds: true }; + + htmlHelpers.parseResponse(fakeHttpResponse, options).then(result => { + expect(result.idPrefix).toBe(''); + expect(result.element).toBeDefined(); + const elementWithOrigId = result.element.querySelector('#test-div'); + expect(elementWithOrigId).toBeTruthy(); + done(); + }); + }); + }); +}); diff --git a/frontend/src/services/http-client/http-client.js b/frontend/src/services/http-client/http-client.js new file mode 100644 index 000000000..6ba2341f0 --- /dev/null +++ b/frontend/src/services/http-client/http-client.js @@ -0,0 +1,41 @@ +export class HttpClient { + + static ACCEPT = { + TEXT_HTML: 'text/html', + JSON: 'application/json', + }; + + _responseInterceptors = []; + + addResponseInterceptor(interceptor) { + if (typeof interceptor === 'function') { + this._responseInterceptors.push(interceptor); + } + } + + get(args) { + args.method = 'GET'; + return this._fetch(args); + } + + post(args) { + args.method = 'POST'; + return this._fetch(args); + } + + _fetch(options) { + const requestOptions = { + credentials: 'same-origin', + ...options, + }; + + return fetch(options.url, requestOptions) + .then( + (response) => { + this._responseInterceptors.forEach((interceptor) => interceptor(response, options)); + return Promise.resolve(response); + }, + Promise.reject, + ).catch(console.error); + } +} diff --git a/frontend/src/services/http-client/http-client.spec.js b/frontend/src/services/http-client/http-client.spec.js new file mode 100644 index 000000000..a0f76584d --- /dev/null +++ b/frontend/src/services/http-client/http-client.spec.js @@ -0,0 +1,116 @@ +import { HttpClient } from './http-client'; + +const TEST_URL = 'http://example.com'; +const FAKE_RESPONSE = { + data: 'data', +}; + +describe('HttpClient', () => { + let httpClient; + + beforeEach(() => { + httpClient = new HttpClient(); + + // setup and spy on fake fetch API + spyOn(window, 'fetch').and.returnValue(Promise.resolve(FAKE_RESPONSE)); + }); + + it('should create', () => { + expect(httpClient).toBeTruthy(); + }); + + describe('get()', () => { + let params; + + beforeEach(() => { + params = { + url: TEST_URL, + }; + }); + + it('should GET the given url', () => { + httpClient.get(params); + expect(window.fetch).toHaveBeenCalledWith(params.url, jasmine.objectContaining({ method: 'GET' })); + }); + + it('should return a promise', (done) => { + const result = httpClient.get(params); + result.then((response) => { + expect(response).toEqual(FAKE_RESPONSE); + done(); + }); + }); + }); + + describe('post()', () => { + let params; + + beforeEach(() => { + params = { + url: TEST_URL, + }; + }); + + it('should POST the given url', () => { + httpClient.post(params); + expect(window.fetch).toHaveBeenCalledWith(params.url, jasmine.objectContaining({ method: 'POST' })); + }); + + it('should return a promise', (done) => { + const result = httpClient.post(params); + result.then((response) => { + expect(response).toEqual(FAKE_RESPONSE); + done(); + }); + }); + }); + + describe('Response Interceptors', () => { + it('can be added', () => { + const interceptor = () => {}; + expect(httpClient._responseInterceptors.length).toBe(0); + httpClient.addResponseInterceptor(interceptor); + expect(httpClient._responseInterceptors.length).toBe(1); + httpClient.addResponseInterceptor(interceptor); + expect(httpClient._responseInterceptors.length).toBe(2); + }); + + describe('get called', () => { + let intercepted1; + let intercepted2; + const interceptors = { + interceptor1: () => intercepted1 = true, + interceptor2: () => intercepted2 = true, + }; + + beforeEach(() => { + intercepted1 = false; + intercepted2 = false; + spyOn(interceptors, 'interceptor1').and.callThrough(); + spyOn(interceptors, 'interceptor2').and.callThrough(); + httpClient.addResponseInterceptor(interceptors.interceptor1); + httpClient.addResponseInterceptor(interceptors.interceptor2); + }); + + it('for GET requests', (done) => { + httpClient.get({ url: TEST_URL }).then(() => { + expect(intercepted1).toBeTruthy(); + expect(intercepted2).toBeTruthy(); + expect(interceptors.interceptor1).toHaveBeenCalledWith(FAKE_RESPONSE, jasmine.any(Object)); + expect(interceptors.interceptor2).toHaveBeenCalledWith(FAKE_RESPONSE, jasmine.any(Object)); + done(); + }); + }); + + it('for POST requests', (done) => { + httpClient.post({ url: TEST_URL }).then(() => { + expect(intercepted1).toBeTruthy(); + expect(intercepted2).toBeTruthy(); + expect(interceptors.interceptor1).toHaveBeenCalledWith(FAKE_RESPONSE, jasmine.any(Object)); + expect(interceptors.interceptor2).toHaveBeenCalledWith(FAKE_RESPONSE, jasmine.any(Object)); + done(); + }); + }); + }); + }); +}); diff --git a/frontend/src/services/i18n/i18n.js b/frontend/src/services/i18n/i18n.js new file mode 100644 index 000000000..7061f6ba6 --- /dev/null +++ b/frontend/src/services/i18n/i18n.js @@ -0,0 +1,32 @@ + /** + * I18n + * + * This module stores and serves translated strings, according to the users language settings. + * + * Translations are stored in /messages/frontend/*.msg. + * + * To make additions to any of these files accessible to JavaScrip Utilities + * you need to add them to the respective *.msg file and to the list of FrontendMessages + * in /src/Utils/Frontend/I18n.hs. + * + */ + +export class I18n { + + translations = {}; + + add(id, translation) { + this.translations[id] = translation; + } + + addMany(manyTranslations) { + Object.keys(manyTranslations).forEach((key) => this.add(key, manyTranslations[key])); + } + + get(id) { + if (!this.translations[id]) { + throw new Error('I18N Error: Translation missing for »' + id + '«!'); + } + return this.translations[id]; + } +} diff --git a/frontend/src/services/i18n/i18n.spec.js b/frontend/src/services/i18n/i18n.spec.js new file mode 100644 index 000000000..1b4edf3c4 --- /dev/null +++ b/frontend/src/services/i18n/i18n.spec.js @@ -0,0 +1,51 @@ +import { I18n } from './i18n'; + +describe('I18n', () => { + let i18n; + + beforeEach(() => { + i18n = new I18n(); + }); + + // helper function + function expectTranslation(id, value) { + expect(i18n.translations[id]).toMatch(value); + } + + it('should create', () => { + expect(i18n).toBeTruthy(); + }); + + describe('add()', () => { + it('should add the translation', () => { + i18n.add('id1', 'translated-id1'); + expectTranslation('id1', 'translated-id1'); + }); + }); + + describe('addMany()', () => { + it('should add many translations', () => { + i18n.addMany({ + id1: 'translated-id1', + id2: 'translated-id2', + id3: 'translated-id3', + }); + expectTranslation('id1', 'translated-id1'); + expectTranslation('id2', 'translated-id2'); + expectTranslation('id3', 'translated-id3'); + }); + }); + + describe('get()', () => { + it('should return stored translations', () => { + i18n.translations.id1 = 'something'; + expect(i18n.get('id1')).toMatch('something'); + }); + + it('should throw error if translation is missing', () => { + expect(() => { + i18n.get('id1'); + }).toThrow(); + }); + }); +}); diff --git a/frontend/src/services/util-registry/util-registry.js b/frontend/src/services/util-registry/util-registry.js new file mode 100644 index 000000000..d96d7a4b3 --- /dev/null +++ b/frontend/src/services/util-registry/util-registry.js @@ -0,0 +1,123 @@ +const DEBUG_MODE = /localhost/.test(window.location.href) && 0; + +export class UtilRegistry { + + _registeredUtils = []; + _activeUtilInstances = []; + _appInstance; + + /** + * function registerUtil + * + * utils need to have at least these properties: + * name: string | utils name, e.g. 'example' + * selector: string | utils selector, e.g. '[uw-example]' + * setup: Function | utils setup function, see below + * + * setup function must return instance object with at least these properties: + * name: string | utils name + * element: HTMLElement | element the util is applied to + * destroy: Function | function to destroy the util and remove any listeners + * + * @param util Object Utility that should be added to the registry + */ + register(util) { + if (DEBUG_MODE > 2) { + console.log('registering util "' + util.name + '"'); + console.log({ util }); + } + this._registeredUtils.push(util); + } + + deregister(name, destroy) { + const utilIndex = this._findUtilIndex(name); + + if (utilIndex >= 0) { + if (destroy === true) { + this._destroyUtilInstances(name); + } + + this._registeredUtils.splice(utilIndex, 1); + } + } + + setApp(appInstance) { + this._appInstance = appInstance; + } + + setupAll(scope) { + if (DEBUG_MODE > 1) { + console.info('registered js utilities:'); + console.table(this._registeredUtils); + } + + this._registeredUtils.forEach((util) => this.setup(util, scope)); + } + + setup(util, scope = document.body) { + if (DEBUG_MODE > 2) { + console.log('setting up util', { util }); + } + + let instances = []; + + if (util) { + const elements = this._findUtilElements(util, scope); + + elements.forEach((element) => { + let utilInstance = null; + + try { + utilInstance = new util(element, this._appInstance); + } catch(err) { + if (DEBUG_MODE > 0) { + console.warn('Error while trying to initialize a utility!', { util , element, err }); + } + } + + if (utilInstance) { + if (DEBUG_MODE > 2) { + console.info('Got utility instance for utility "' + util.name + '"', { utilInstance }); + } + + instances.push(utilInstance); + } + }); + } + + this._activeUtilInstances.push(...instances); + return instances; + } + + find(name) { + return this._registeredUtils.find((util) => util.name === name); + } + + _findUtilElements(util, scope) { + if (scope && scope.matches(util.selector)) { + return [scope]; + } + return Array.from(scope.querySelectorAll(util.selector)); + } + + _findUtilIndex(name) { + return this._registeredUtils.findIndex((util) => util.name === name); + } + + _destroyUtilInstances(name) { + this._activeUtilInstances + .map((util, index) => ({ + util: util, + index: index, + })) + .filter((activeUtil) => activeUtil.util.name === name) + .forEach((activeUtil) => { + // destroy util instance + activeUtil.util.destroy(); + delete this._activeUtilInstances[activeUtil.index]; + }); + + // get rid of now empty array slots + this._activeUtilInstances = this._activeUtilInstances.filter((util) => !!util); + } +} diff --git a/frontend/src/services/util-registry/util-registry.spec.js b/frontend/src/services/util-registry/util-registry.spec.js new file mode 100644 index 000000000..5f29f6a3c --- /dev/null +++ b/frontend/src/services/util-registry/util-registry.spec.js @@ -0,0 +1,146 @@ +import { UtilRegistry } from './util-registry'; +import { Utility } from '../../core/utility'; + +describe('UtilRegistry', () => { + let utilRegistry; + + beforeEach(() => { + utilRegistry = new UtilRegistry(); + }); + + it('should create', () => { + expect(utilRegistry).toBeTruthy(); + }); + + describe('register()', () => { + it('should allow to add utilities', () => { + let foundUtil = utilRegistry.find(TestUtil1.name); + expect(foundUtil).toBeFalsy(); + + utilRegistry.register(TestUtil1); + + foundUtil = utilRegistry.find(TestUtil1.name); + expect(foundUtil).toEqual(TestUtil1); + }); + }); + + describe('deregister()', () => { + it('should remove util', () => { + // register util + utilRegistry.register(TestUtil1); + let foundUtil = utilRegistry.find(TestUtil1.name); + expect(foundUtil).toBeTruthy(); + + // deregister util + utilRegistry.deregister(TestUtil1.name); + foundUtil = utilRegistry.find(TestUtil1.name); + expect(foundUtil).toBeFalsy(); + }); + + it('should destroy util instances if requested', () => { + pending('TBD'); + }); + }); + + describe('setup()', () => { + + it('should catch errors thrown by the utility', () => { + expect(() => { + utilRegistry.setup(ThrowingUtil); + }).not.toThrow(); + }); + + describe('scope has no matching elements', () => { + it('should not construct an instance', () => { + const scope = document.createElement('div'); + const instances = utilRegistry.setup(TestUtil1, scope); + expect(instances.length).toBe(0); + }); + + it('should use fallback scope', () => { + const instances = utilRegistry.setup(TestUtil1); + expect(instances.length).toBe(0); + }); + }); + + describe('scope has matching elements', () => { + let testScope; + let testElement1; + let testElement2; + + beforeEach(() => { + testScope = document.createElement('div'); + testElement1 = document.createElement('div'); + testElement2 = document.createElement('div'); + testElement1.classList.add('util1'); + testElement2.classList.add('util1'); + testScope.appendChild(testElement1); + testScope.appendChild(testElement2); + }); + + it('should construct a utility instance', () => { + const setupUtilities = utilRegistry.setup(TestUtil1, testScope); + expect(setupUtilities).toBeTruthy(); + expect(setupUtilities[0]).toBeTruthy(); + }); + + it('should construct an instance for each matching element', () => { + const setupUtilities = utilRegistry.setup(TestUtil1, testScope); + expect(setupUtilities).toBeTruthy(); + expect(setupUtilities[0].element).toBe(testElement1); + expect(setupUtilities[1].element).toBe(testElement2); + }); + + it('should pass the app instance', () => { + const fakeApp = { }; + utilRegistry.setApp(fakeApp); + + const setupUtilities = utilRegistry.setup(TestUtil1, testScope); + expect(setupUtilities).toBeTruthy(); + expect(setupUtilities[0].app).toBe(fakeApp); + expect(setupUtilities[1].app).toBe(fakeApp); + }); + }); + }); + + describe('setupAll()', () => { + it('should setup all the utilities', () => { + spyOn(utilRegistry, 'setup'); + utilRegistry.register(TestUtil1); + utilRegistry.register(TestUtil2); + utilRegistry.setupAll(); + + expect(utilRegistry.setup.calls.count()).toBe(2); + expect(utilRegistry.setup.calls.argsFor(0)).toEqual([TestUtil1, undefined]); + expect(utilRegistry.setup.calls.argsFor(1)).toEqual([TestUtil2, undefined]); + }); + + it('should pass the given scope', () => { + spyOn(utilRegistry, 'setup'); + utilRegistry.register(TestUtil1); + const scope = document.createElement('div'); + utilRegistry.setupAll(scope); + + expect(utilRegistry.setup).toHaveBeenCalledWith(TestUtil1, scope); + }); + }); +}); + +// test utilities +@Utility({ selector: '.util1' }) +class TestUtil1 { + constructor(element, app) { + this.element = element; + this.app = app; + } +} + +@Utility({ selector: '#util2' }) +class TestUtil2 { } + +@Utility({ selector: '#throws' }) +class ThrowingUtil { + constructor() { + throw new Error(); + } + } diff --git a/frontend/src/utils/alerts/alerts.js b/frontend/src/utils/alerts/alerts.js new file mode 100644 index 000000000..e7e04ddbb --- /dev/null +++ b/frontend/src/utils/alerts/alerts.js @@ -0,0 +1,165 @@ +import { Utility } from '../../core/utility'; +import './alerts.scss'; + +const ALERTS_INITIALIZED_CLASS = 'alerts--initialized'; +const ALERTS_ELEVATED_CLASS = 'alerts--elevated'; +const ALERTS_TOGGLER_CLASS = 'alerts__toggler'; +const ALERTS_TOGGLER_VISIBLE_CLASS = 'alerts__toggler--visible'; +const ALERTS_TOGGLER_APPEAR_DELAY = 120; + +const ALERT_CLASS = 'alert'; +const ALERT_INITIALIZED_CLASS = 'alert--initialized'; +const ALERT_CLOSER_CLASS = 'alert__closer'; +const ALERT_ICON_CLASS = 'alert__icon'; +const ALERT_CONTENT_CLASS = 'alert__content'; +const ALERT_INVISIBLE_CLASS = 'alert--invisible'; +const ALERT_AUTO_HIDE_DELAY = 10; +const ALERT_AUTOCLOSING_MATCHER = '.alert-info, .alert-success'; + +@Utility({ + selector: '[uw-alerts]', +}) +export class Alerts { + _togglerCheckRequested = false; + _togglerElement; + _alertElements; + + _element; + _app; + + constructor(element, app) { + if (!element) { + throw new Error('Alerts util has to be called with an element!'); + } + + this._element = element; + this._app = app; + + if (this._element.classList.contains(ALERTS_INITIALIZED_CLASS)) { + return false; + } + + this._togglerElement = this._element.querySelector('.' + ALERTS_TOGGLER_CLASS); + this._alertElements = this._gatherAlertElements(); + + if (this._togglerElement) { + this._initToggler(); + } + + this._initAlerts(); + + // register http client interceptor to filter out Alerts Header + this._setupHttpInterceptor(); + + // mark initialized + this._element.classList.add(ALERTS_INITIALIZED_CLASS); + } + + destroy() { + console.log('TBD: Destroy Alert'); + } + + _gatherAlertElements() { + return Array.from(this._element.querySelectorAll('.' + ALERT_CLASS)).filter(function(alert) { + return !alert.classList.contains(ALERT_INITIALIZED_CLASS); + }); + } + + _initToggler() { + this._togglerElement.addEventListener('click', () => { + this._alertElements.forEach((alertEl) => this._toggleAlert(alertEl, true)); + this._togglerElement.classList.remove(ALERTS_TOGGLER_VISIBLE_CLASS); + }); + } + + _initAlerts() { + this._alertElements.forEach((alert) => this._initAlert(alert)); + } + + _initAlert(alertElement) { + let autoHideDelay = ALERT_AUTO_HIDE_DELAY; + if (alertElement.dataset.decay) { + autoHideDelay = parseInt(alertElement.dataset.decay, 10); + } + + const closeEl = alertElement.querySelector('.' + ALERT_CLOSER_CLASS); + closeEl.addEventListener('click', () => { + this._toggleAlert(alertElement); + }); + + if (autoHideDelay > 0 && alertElement.matches(ALERT_AUTOCLOSING_MATCHER)) { + window.setTimeout(() => this._toggleAlert(alertElement), autoHideDelay * 1000); + } + } + + _toggleAlert(alertEl, visible) { + alertEl.classList.toggle(ALERT_INVISIBLE_CLASS, !visible); + this._checkToggler(); + } + + _checkToggler() { + if (this._togglerCheckRequested) { + return; + } + + const alertsHidden = this._alertElements.reduce(function(acc, alert) { + return acc && alert.classList.contains(ALERT_INVISIBLE_CLASS); + }, true); + + window.setTimeout(() => { + this._togglerElement.classList.toggle(ALERTS_TOGGLER_VISIBLE_CLASS, alertsHidden); + this._togglerCheckRequested = false; + }, ALERTS_TOGGLER_APPEAR_DELAY); + } + + _setupHttpInterceptor() { + this._app.httpClient.addResponseInterceptor(this._responseInterceptor.bind(this)); + } + + _elevateAlerts() { + this._element.classList.add(ALERTS_ELEVATED_CLASS); + } + + _responseInterceptor = (response) => { + let alerts; + for (const header of response.headers) { + if (header[0] === 'alerts') { + const decodedHeader = decodeURIComponent(header[1]); + alerts = JSON.parse(decodedHeader); + break; + } + } + + if (alerts) { + alerts.forEach((alert) => { + const alertElement = this._createAlertElement(alert.status, alert.content); + this._element.appendChild(alertElement); + this._alertElements.push(alertElement); + this._initAlert(alertElement); + }); + + this._elevateAlerts(); + } + } + + _createAlertElement(type, content) { + const alertElement = document.createElement('div'); + alertElement.classList.add(ALERT_CLASS, 'alert-' + type); + + const alertCloser = document.createElement('div'); + alertCloser.classList.add(ALERT_CLOSER_CLASS); + + const alertIcon = document.createElement('div'); + alertIcon.classList.add(ALERT_ICON_CLASS); + + const alertContent = document.createElement('div'); + alertContent.classList.add(ALERT_CONTENT_CLASS); + alertContent.innerHTML = content; + + alertElement.appendChild(alertCloser); + alertElement.appendChild(alertIcon); + alertElement.appendChild(alertContent); + + return alertElement; + } +} diff --git a/frontend/src/utils/alerts/alerts.md b/frontend/src/utils/alerts/alerts.md new file mode 100644 index 000000000..e3b36feed --- /dev/null +++ b/frontend/src/utils/alerts/alerts.md @@ -0,0 +1,35 @@ +# Alerts + +Makes alerts interactive. + +## Attribute: `uw-alerts` + +## Types of alerts: +- `default`\ + Regular Info Alert + Disappears automatically after 30 seconds + Disappears after x seconds if explicitly specified via data-decay='x' + Can be told not to disappear with data-decay='0' + +- `success`\ + Currently no special visual appearance + Disappears automatically after 30 seconds + +- `warning`\ + Will be coloured warning-orange regardless of user's selected theme + Does not disappear + +- `error`\ + Will be coloured error-red regardless of user's selected theme + Does not disappear + +## Example usage: +```html +
+
+
+
+
+
+ This is some information +``` diff --git a/static/css/utils/alerts.scss b/frontend/src/utils/alerts/alerts.scss similarity index 85% rename from static/css/utils/alerts.scss rename to frontend/src/utils/alerts/alerts.scss index 3256eef96..d2faf1b22 100644 --- a/static/css/utils/alerts.scss +++ b/frontend/src/utils/alerts/alerts.scss @@ -1,23 +1,3 @@ -/* ALERTS */ -/** - .alert - Regular Info Alert - Disappears automatically after 30 seconds - Disappears after x seconds if explicitly specified via data-decay='x' - Can be told not to disappear with data-decay='0' - - .alert-success - Disappears automatically after 30 seconds - - .alert-warning - Does not disappear - Orange regardless of user's selected theme - - .alert-error - Does not disappear - Red regardless of user's selected theme - - */ .alerts { position: fixed; bottom: 0; @@ -40,7 +20,7 @@ &::before { content: '\f077'; position: absolute; - font-family: "Font Awesome 5 Free"; + font-family: 'Font Awesome 5 Free'; left: 50%; top: 0; height: 30px; @@ -54,6 +34,10 @@ } } +.alerts--elevated { + z-index: 1000; +} + .alerts__toggler--visible { top: -40px; opacity: 1; @@ -142,7 +126,7 @@ &::before { content: '\f05a'; position: absolute; - font-family: "Font Awesome 5 Free"; + font-family: 'Font Awesome 5 Free'; font-size: 24px; top: 50%; left: 50%; @@ -180,7 +164,7 @@ &::before { content: '\f00d'; position: absolute; - font-family: "Font Awesome 5 Free"; + font-family: 'Font Awesome 5 Free'; top: 50%; left: 50%; display: flex; diff --git a/frontend/src/utils/alerts/alerts.spec.js b/frontend/src/utils/alerts/alerts.spec.js new file mode 100644 index 000000000..0b4749e97 --- /dev/null +++ b/frontend/src/utils/alerts/alerts.spec.js @@ -0,0 +1,27 @@ +import { Alerts } from './alerts'; + +const MOCK_APP = { + httpClient: { + addResponseInterceptor: () => {}, + }, +}; + +describe('Alerts', () => { + + let alerts; + + beforeEach(() => { + const element = document.createElement('div'); + alerts = new Alerts(element, MOCK_APP); + }); + + it('should create', () => { + expect(alerts).toBeTruthy(); + }); + + it('should throw if called without an element', () => { + expect(() => { + new Alerts(); + }).toThrow(); + }); +}); diff --git a/frontend/src/utils/asidenav/asidenav.js b/frontend/src/utils/asidenav/asidenav.js new file mode 100644 index 000000000..2964e1617 --- /dev/null +++ b/frontend/src/utils/asidenav/asidenav.js @@ -0,0 +1,82 @@ +import { Utility } from '../../core/utility'; +import './asidenav.scss'; + +const FAVORITES_BTN_CLASS = 'navbar__list-item--favorite'; +const FAVORITES_BTN_ACTIVE_CLASS = 'navbar__list-item--active'; +const ASIDENAV_INITIALIZED_CLASS = 'asidenav--initialized'; +const ASIDENAV_EXPANDED_CLASS = 'main__aside--expanded'; +const ASIDENAV_LIST_ITEM_CLASS = 'asidenav__list-item'; +const ASIDENAV_SUBMENU_CLASS = 'asidenav__nested-list-wrapper'; + +@Utility({ + selector: '[uw-asidenav]', +}) +export class Asidenav { + + _element; + _asidenavSubmenus; + + constructor(element) { + if (!element) { + throw new Error('Asidenav utility cannot be setup without an element!'); + } + + this._element = element; + + if (this._element.classList.contains(ASIDENAV_INITIALIZED_CLASS)) { + return false; + } + + this._initFavoritesButton(); + this._initAsidenavSubmenus(); + + // mark initialized + this._element.classList.add(ASIDENAV_INITIALIZED_CLASS); + } + + destroy() { + this._asidenavSubmenus.forEach((union) => { + union.listItem.removeEventListener(union.hoverHandler); + }); + } + + _initFavoritesButton() { + const favoritesBtn = document.querySelector('.' + FAVORITES_BTN_CLASS); + if (favoritesBtn) { + favoritesBtn.addEventListener('click', (event) => { + favoritesBtn.classList.toggle(FAVORITES_BTN_ACTIVE_CLASS); + this._element.classList.toggle(ASIDENAV_EXPANDED_CLASS); + event.preventDefault(); + }, true); + } + } + + _initAsidenavSubmenus() { + this._asidenavSubmenus = Array.from(this._element.querySelectorAll('.' + ASIDENAV_LIST_ITEM_CLASS)) + .map(function(listItem) { + const submenu = listItem.querySelector('.' + ASIDENAV_SUBMENU_CLASS); + return { listItem, submenu }; + }).filter(function(union) { + return union.submenu !== null; + }); + + this._asidenavSubmenus.forEach((union) => { + union.hoverHandler = this._createMouseoverHandler(union); + union.listItem.addEventListener('mouseover', union.hoverHandler); + }); + } + + _createMouseoverHandler(union) { + return () => { + const rectListItem = union.listItem.getBoundingClientRect(); + const rectSubMenu = union.submenu.getBoundingClientRect(); + + union.submenu.style.left = (rectListItem.left + rectListItem.width) + 'px'; + if (window.innerHeight - rectListItem.top < rectSubMenu.height) { + union.submenu.style.top = (rectListItem.top + rectListItem.height - rectSubMenu.height) + 'px'; + } else { + union.submenu.style.top = rectListItem.top + 'px'; + } + }; + } +} diff --git a/frontend/src/utils/asidenav/asidenav.md b/frontend/src/utils/asidenav/asidenav.md new file mode 100644 index 000000000..0aa73b0f7 --- /dev/null +++ b/frontend/src/utils/asidenav/asidenav.md @@ -0,0 +1,21 @@ +# Asidenav + +Correctly positions hovered asidenav submenus and handles the favorites button on mobile + +## Attribute: `uw-asidenav` + +## Example usage: +```html +
+
+
+