diff --git a/.gitignore b/.gitignore
index 1acc0456f..0a8d77212 100755
--- a/.gitignore
+++ b/.gitignore
@@ -8,6 +8,7 @@ npm-debug.log
public/
sass-cache
settings.json
+tmp-uploads
# ignore everything in the package directory, but not govuk-prototype-kit.config.json, package.json and README.md
package/*
diff --git a/app/routes/upload.js b/app/routes/upload.js
new file mode 100644
index 000000000..699548217
--- /dev/null
+++ b/app/routes/upload.js
@@ -0,0 +1,205 @@
+const express = require('express');
+const router = express.Router();
+const multer = require('multer');
+const cache = require( '../../lib/cache' );
+
+function getErrorMessage(item) {
+ var message = '';
+ if(item.error.code == 'FILE_TYPE') {
+ message += item.file.originalname + ' must be a png or gif';
+ } else if(item.error.code == 'LIMIT_FILE_SIZE') {
+ message += item.file.originalname + ' must be smaller than 2mb';
+ }
+ return message;
+}
+
+function getUploadedFiles( req, res, next ){
+
+ if( !req.session.uploadId ){
+ req.session.uploadId = req.sessionID;
+ }
+
+ let files = cache.get( req.session.uploadId );
+
+ if( !files ){
+ files = [];
+ cache.set( req.session.uploadId, files );
+ }
+
+ req.uploadedFiles = files;
+ next();
+}
+
+////////////////////////////////////////////////////////////////////////////////////////
+// NO JAVASCRIPT
+////////////////////////////////////////////////////////////////////////////////////////
+
+const upload = multer( {
+ dest: './public/uploads',
+ limits: { fileSize: 2000000 },
+ fileFilter: function( req, file, cb ){
+ let ok = false;
+
+ if(!req.rejectedFiles) {
+ req.rejectedFiles = [];
+ }
+
+ if( file.mimetype !== 'image/png' && file.mimetype !== 'image/gif' && file.mimetype !== 'image/jpg' && file.mimetype !== 'image/jpeg') {
+ cb(null, false);
+ req.rejectedFiles.push({
+ file: file,
+ error: {
+ code: 'FILE_TYPE'
+ }
+ });
+ } else {
+ cb(null, true);
+ }
+ }
+} ).array('documents', 10);
+
+
+
+router.get('/components/multi-file-upload', getUploadedFiles, function( req, res ){
+
+ const { uploadedFiles } = req;
+
+ var pageObject = {
+ uploadedFiles: [],
+ errorMessage: null,
+ errorSummary: {
+ items: []
+ }
+ };
+
+ // 1. UPLOADED FILES
+
+ if(uploadedFiles.length) {
+ uploadedFiles.forEach(function(file) {
+ var o = file;
+ o.message = {
+ html: ` ${file.originalname} has been uploaded`
+ };
+
+ o.filePath = file.path;
+ o.originalFileName = file.originalname;
+ o.fileName = file.filename;
+ o.deleteButton = {
+ text: 'Delete'
+ };
+ pageObject.uploadedFiles.push(o);
+ });
+ }
+
+ // 2. REJECTED FILES
+
+ if(req.session.rejectedFiles && req.session.rejectedFiles.length) {
+ var errorMessage = '';
+ req.session.rejectedFiles.forEach(function(item) {
+ errorMessage += getErrorMessage(item);
+ errorMessage += '
';
+ });
+
+ req.session.rejectedFiles.forEach(function(item) {
+ pageObject.errorSummary.items.push({
+ text: getErrorMessage(item),
+ href: '#documents'
+ });
+ });
+
+ pageObject.errorMessage = {
+ html: errorMessage
+ };
+ }
+
+ req.session.rejectedFiles = null;
+
+ res.render( 'components/multi-file-upload/index.html', pageObject );
+});
+
+function removeFileFromFileList(fileList, filename) {
+
+ const index = fileList.findIndex( (item ) => item.filename === filename );
+ if( index >= 0 ){
+ fileList.splice( index, 1 );
+ }
+}
+
+router.post('/components/multi-file-upload', getUploadedFiles, function( req, res ){
+ upload(req, res, function(err) {
+ if(err) {
+ // console.log(err);
+ }
+
+ req.uploadedFiles.push(...req.files);
+
+ if(req.body.delete) {
+ removeFileFromFileList(req.uploadedFiles, req.body.delete);
+ }
+
+ // no concat because errors are discarded after use anyway
+ req.session.rejectedFiles = req.rejectedFiles;
+
+ res.redirect('/components/multi-file-upload');
+ });
+} );
+
+////////////////////////////////////////////////////////////////////////////////////////
+// AJAX
+////////////////////////////////////////////////////////////////////////////////////////
+
+const uploadAjax = multer( {
+ dest: './public/uploads',
+ limits: { fileSize: 2000000 },
+ fileFilter: function( req, file, cb ){
+ let ok = false;
+ if( file.mimetype !== 'image/png' && file.mimetype !== 'image/gif' && file.mimetype !== 'image/jpg' && file.mimetype !== 'image/jpeg'){
+ return cb({
+ code: 'FILE_TYPE',
+ field: 'documents',
+ file: file
+ }, false);
+ } else {
+ return cb(null, true);
+ }
+ }
+} ).single('documents');
+
+router.post('/ajax-upload', getUploadedFiles, function( req, res ){
+
+ uploadAjax(req, res, function(error, val1, val2) {
+ if(error) {
+ if(error.code == 'FILE_TYPE') {
+ error.message = error.file.originalname + ' must be a png or gif';
+ } else if(error.code == 'LIMIT_FILE_SIZE') {
+ // error.message = error.file.originalname + ' must be smaller than 2mb';
+ error.message = 'The file must be smaller than 2mb';
+ }
+
+ var response = {
+ error: error,
+ file: error.file || { filename: 'filename', originalname: 'originalname' }
+ };
+
+ res.json(response);
+ } else {
+
+ req.uploadedFiles.push(req.file);
+
+ res.json({
+ file: req.file,
+ success: {
+ messageHtml: ` ${req.file.originalname} has been uploaded`,
+ messageText: `${req.file.originalname} has been uploaded`
+ }
+ });
+ }
+ } );
+} );
+
+router.post('/ajax-delete', getUploadedFiles, function( req, res ){
+ removeFileFromFileList(req.uploadedFiles, req.body.delete);
+ res.json({});
+});
+
+module.exports = router;
\ No newline at end of file
diff --git a/app/views/components/multi-file-upload/index.html b/app/views/components/multi-file-upload/index.html
new file mode 100644
index 000000000..10ee8d924
--- /dev/null
+++ b/app/views/components/multi-file-upload/index.html
@@ -0,0 +1,80 @@
+{% extends "layouts/base.html" %}
+
+{% from "govuk/components/file-upload/macro.njk" import govukFileUpload %}
+
+{% block body %}
+
{{ mojSearch({
- classes: 'moj-search--ondark moj-search--toggle moj-hidden',
+ classes: 'moj-search--ondark moj-search--toggle moj-js-hidden',
input: {
id: 'search2',
name: 'search2'
diff --git a/app/views/includes/scripts.html b/app/views/includes/scripts.html
index acfe37b42..c9ea1717d 100755
--- a/app/views/includes/scripts.html
+++ b/app/views/includes/scripts.html
@@ -4,6 +4,7 @@
+
diff --git a/app/views/index.html b/app/views/index.html
index 006c8f2fd..636460178 100755
--- a/app/views/index.html
+++ b/app/views/index.html
@@ -12,6 +12,7 @@
MOJ Frontend
Form validator
Menu
Messages
+
Multi file upload
Multi-select
Header
Identity bar
diff --git a/app/views/layouts/base.html b/app/views/layouts/base.html
index 277c47bb9..32ad60fd3 100755
--- a/app/views/layouts/base.html
+++ b/app/views/layouts/base.html
@@ -29,6 +29,7 @@
{%- from "moj/components/identity-bar/macro.njk" import mojIdentityBar %}
{%- from "moj/components/menu/macro.njk" import mojMenu %}
{%- from "moj/components/messages/macro.njk" import mojMessages %}
+{%- from "moj/components/multi-file-upload/macro.njk" import mojMultiFileUpload %}
{%- from "moj/components/notification-badge/macro.njk" import mojNotificationBadge %}
{%- from "moj/components/organisation-switcher/macro.njk" import mojOrganisationSwitcher %}
{%- from "moj/components/pagination/macro.njk" import mojPagination %}
diff --git a/lib/cache.js b/lib/cache.js
new file mode 100644
index 000000000..1b30eac23
--- /dev/null
+++ b/lib/cache.js
@@ -0,0 +1,9 @@
+const cache = {};
+
+module.exports = {
+ get: ( key ) => cache[ key ],
+ set: ( key, value ) => {
+ cache[ key ] = value
+ },
+ delete: ( key ) => delete cache[ key ],
+};
\ No newline at end of file
diff --git a/package-lock.json b/package-lock.json
index 76a66c2b6..f80ac27df 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -1,6 +1,6 @@
{
"name": "@ministryofjustice/frontend",
- "version": "0.0.7-alpha",
+ "version": "0.0.9-alpha",
"lockfileVersion": 1,
"requires": true,
"dependencies": {
@@ -185,6 +185,11 @@
"buffer-equal": "^1.0.0"
}
},
+ "append-field": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/append-field/-/append-field-1.0.0.tgz",
+ "integrity": "sha1-HjRA6RXwsSA9I3SOeO3XubW0PlY="
+ },
"aproba": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/aproba/-/aproba-1.2.0.tgz",
@@ -872,22 +877,27 @@
"integrity": "sha512-FG+nFEZChJrbQ9tIccIfZJBz3J7mLrAhxakAbnrJWn8d7aKOC+LWifa0G+p4ZqKp4y13T7juYvdhq9NzKdsrjw=="
},
"body-parser": {
- "version": "1.18.3",
- "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.18.3.tgz",
- "integrity": "sha1-WykhmP/dVTs6DyDe0FkrlWlVyLQ=",
+ "version": "1.19.0",
+ "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.19.0.tgz",
+ "integrity": "sha512-dhEPs72UPbDnAQJ9ZKMNTP6ptJaionhP5cBb541nXPlW60Jepo9RV/a4fX4XWW9CuFNK22krhrj1+rgzifNCsw==",
"requires": {
- "bytes": "3.0.0",
+ "bytes": "3.1.0",
"content-type": "~1.0.4",
"debug": "2.6.9",
"depd": "~1.1.2",
- "http-errors": "~1.6.3",
- "iconv-lite": "0.4.23",
+ "http-errors": "1.7.2",
+ "iconv-lite": "0.4.24",
"on-finished": "~2.3.0",
- "qs": "6.5.2",
- "raw-body": "2.3.3",
- "type-is": "~1.6.16"
+ "qs": "6.7.0",
+ "raw-body": "2.4.0",
+ "type-is": "~1.6.17"
},
"dependencies": {
+ "bytes": {
+ "version": "3.1.0",
+ "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.0.tgz",
+ "integrity": "sha512-zauLjrfCG+xvoyaqLoV8bLVXXNGC4JqlxFCutSDWA6fJrTo2ZuvLYTqZ7aHBLZSMOopbzwv8f+wZcVzfVTI2Dg=="
+ },
"debug": {
"version": "2.6.9",
"resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz",
@@ -896,10 +906,73 @@
"ms": "2.0.0"
}
},
+ "http-errors": {
+ "version": "1.7.2",
+ "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-1.7.2.tgz",
+ "integrity": "sha512-uUQBt3H/cSIVfch6i1EuPNy/YsRSOUBXTVfZ+yR7Zjez3qjBz6i9+i4zjNaoqcoFVI4lQJ5plg63TvGfRSDCRg==",
+ "requires": {
+ "depd": "~1.1.2",
+ "inherits": "2.0.3",
+ "setprototypeof": "1.1.1",
+ "statuses": ">= 1.5.0 < 2",
+ "toidentifier": "1.0.0"
+ }
+ },
+ "iconv-lite": {
+ "version": "0.4.24",
+ "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz",
+ "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==",
+ "requires": {
+ "safer-buffer": ">= 2.1.2 < 3"
+ }
+ },
+ "mime-db": {
+ "version": "1.40.0",
+ "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.40.0.tgz",
+ "integrity": "sha512-jYdeOMPy9vnxEqFRRo6ZvTZ8d9oPb+k18PKoYNYUe2stVEBPPwsln/qWzdbmaIvnhZ9v2P+CuecK+fpUfsV2mA=="
+ },
+ "mime-types": {
+ "version": "2.1.24",
+ "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.24.tgz",
+ "integrity": "sha512-WaFHS3MCl5fapm3oLxU4eYDw77IQM2ACcxQ9RIxfaC3ooc6PFuBMGZZsYpvoXS5D5QTWPieo1jjLdAm3TBP3cQ==",
+ "requires": {
+ "mime-db": "1.40.0"
+ }
+ },
"qs": {
- "version": "6.5.2",
- "resolved": "https://registry.npmjs.org/qs/-/qs-6.5.2.tgz",
- "integrity": "sha512-N5ZAX4/LxJmF+7wN74pUD6qAh9/wnvdQcjq9TZjevvXzSUo7bfmw91saqMjzGS2xq91/odN2dW/WOl7qQHNDGA=="
+ "version": "6.7.0",
+ "resolved": "https://registry.npmjs.org/qs/-/qs-6.7.0.tgz",
+ "integrity": "sha512-VCdBRNFTX1fyE7Nb6FYoURo/SPe62QCaAyzJvUjwRaIsc+NePBEniHlvxFmmX56+HZphIGtV0XeCirBtpDrTyQ=="
+ },
+ "raw-body": {
+ "version": "2.4.0",
+ "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.4.0.tgz",
+ "integrity": "sha512-4Oz8DUIwdvoa5qMJelxipzi/iJIi40O5cGV1wNYp5hvZP8ZN0T+jiNkL0QepXs+EsQ9XJ8ipEDoiH70ySUJP3Q==",
+ "requires": {
+ "bytes": "3.1.0",
+ "http-errors": "1.7.2",
+ "iconv-lite": "0.4.24",
+ "unpipe": "1.0.0"
+ }
+ },
+ "setprototypeof": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.1.1.tgz",
+ "integrity": "sha512-JvdAWfbXeIGaZ9cILp38HntZSFSo3mWg6xGcJJsd+d4aRMOqauag1C63dJfDw7OaMYwEbHMOxEZ1lqVRYP2OAw=="
+ },
+ "statuses": {
+ "version": "1.5.0",
+ "resolved": "https://registry.npmjs.org/statuses/-/statuses-1.5.0.tgz",
+ "integrity": "sha1-Fhx9rBd2Wf2YEfQ3cfqZOBR4Yow="
+ },
+ "type-is": {
+ "version": "1.6.18",
+ "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz",
+ "integrity": "sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==",
+ "requires": {
+ "media-typer": "0.3.0",
+ "mime-types": "~2.1.24"
+ }
}
}
},
@@ -1159,6 +1232,38 @@
"resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.1.tgz",
"integrity": "sha512-MQcXEUbCKtEo7bhqEs6560Hyd4XaovZlO/k9V3hjVUF/zwW7KBVdSK4gIt/bzwS9MbR5qob+F5jusZsb0YQK2A=="
},
+ "busboy": {
+ "version": "0.2.14",
+ "resolved": "https://registry.npmjs.org/busboy/-/busboy-0.2.14.tgz",
+ "integrity": "sha1-bCpiLvz0fFe7vh4qnDetNseSVFM=",
+ "requires": {
+ "dicer": "0.2.5",
+ "readable-stream": "1.1.x"
+ },
+ "dependencies": {
+ "isarray": {
+ "version": "0.0.1",
+ "resolved": "https://registry.npmjs.org/isarray/-/isarray-0.0.1.tgz",
+ "integrity": "sha1-ihis/Kmo9Bd+Cav8YDiTmwXR7t8="
+ },
+ "readable-stream": {
+ "version": "1.1.14",
+ "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-1.1.14.tgz",
+ "integrity": "sha1-fPTFTvZI44EwhMY23SB54WbAgdk=",
+ "requires": {
+ "core-util-is": "~1.0.0",
+ "inherits": "~2.0.1",
+ "isarray": "0.0.1",
+ "string_decoder": "~0.10.x"
+ }
+ },
+ "string_decoder": {
+ "version": "0.10.31",
+ "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-0.10.31.tgz",
+ "integrity": "sha1-YuIDvEF2bGwoyfyEMB2rHFMQ+pQ="
+ }
+ }
+ },
"bytes": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/bytes/-/bytes-3.0.0.tgz",
@@ -2018,6 +2123,38 @@
"resolved": "https://registry.npmjs.org/dev-ip/-/dev-ip-1.0.1.tgz",
"integrity": "sha1-p2o+0YVb56ASu4rBbLgPPADcKPA="
},
+ "dicer": {
+ "version": "0.2.5",
+ "resolved": "https://registry.npmjs.org/dicer/-/dicer-0.2.5.tgz",
+ "integrity": "sha1-WZbAhrszIYyBLAkL3cCc0S+stw8=",
+ "requires": {
+ "readable-stream": "1.1.x",
+ "streamsearch": "0.1.2"
+ },
+ "dependencies": {
+ "isarray": {
+ "version": "0.0.1",
+ "resolved": "https://registry.npmjs.org/isarray/-/isarray-0.0.1.tgz",
+ "integrity": "sha1-ihis/Kmo9Bd+Cav8YDiTmwXR7t8="
+ },
+ "readable-stream": {
+ "version": "1.1.14",
+ "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-1.1.14.tgz",
+ "integrity": "sha1-fPTFTvZI44EwhMY23SB54WbAgdk=",
+ "requires": {
+ "core-util-is": "~1.0.0",
+ "inherits": "~2.0.1",
+ "isarray": "0.0.1",
+ "string_decoder": "~0.10.x"
+ }
+ },
+ "string_decoder": {
+ "version": "0.10.31",
+ "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-0.10.31.tgz",
+ "integrity": "sha1-YuIDvEF2bGwoyfyEMB2rHFMQ+pQ="
+ }
+ }
+ },
"dir-glob": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/dir-glob/-/dir-glob-2.0.0.tgz",
@@ -2507,6 +2644,23 @@
"vary": "~1.1.2"
},
"dependencies": {
+ "body-parser": {
+ "version": "1.18.3",
+ "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.18.3.tgz",
+ "integrity": "sha1-WykhmP/dVTs6DyDe0FkrlWlVyLQ=",
+ "requires": {
+ "bytes": "3.0.0",
+ "content-type": "~1.0.4",
+ "debug": "2.6.9",
+ "depd": "~1.1.2",
+ "http-errors": "~1.6.3",
+ "iconv-lite": "0.4.23",
+ "on-finished": "~2.3.0",
+ "qs": "6.5.2",
+ "raw-body": "2.3.3",
+ "type-is": "~1.6.16"
+ }
+ },
"debug": {
"version": "2.6.9",
"resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz",
@@ -2541,6 +2695,41 @@
}
}
},
+ "express-session": {
+ "version": "1.16.2",
+ "resolved": "https://registry.npmjs.org/express-session/-/express-session-1.16.2.tgz",
+ "integrity": "sha512-oy0sRsdw6n93E9wpCNWKRnSsxYnSDX9Dnr9mhZgqUEEorzcq5nshGYSZ4ZReHFhKQ80WI5iVUUSPW7u3GaKauw==",
+ "requires": {
+ "cookie": "0.3.1",
+ "cookie-signature": "1.0.6",
+ "debug": "2.6.9",
+ "depd": "~2.0.0",
+ "on-headers": "~1.0.2",
+ "parseurl": "~1.3.3",
+ "safe-buffer": "5.1.2",
+ "uid-safe": "~2.1.5"
+ },
+ "dependencies": {
+ "debug": {
+ "version": "2.6.9",
+ "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz",
+ "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==",
+ "requires": {
+ "ms": "2.0.0"
+ }
+ },
+ "depd": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz",
+ "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw=="
+ },
+ "parseurl": {
+ "version": "1.3.3",
+ "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz",
+ "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ=="
+ }
+ }
+ },
"ext-list": {
"version": "2.2.2",
"resolved": "https://registry.npmjs.org/ext-list/-/ext-list-2.2.2.tgz",
@@ -5431,6 +5620,21 @@
"resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz",
"integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g="
},
+ "multer": {
+ "version": "1.4.2",
+ "resolved": "https://registry.npmjs.org/multer/-/multer-1.4.2.tgz",
+ "integrity": "sha512-xY8pX7V+ybyUpbYMxtjM9KAiD9ixtg5/JkeKUTD6xilfDv0vzzOFcCp4Ljb1UU3tSOM3VTZtKo63OmzOrGi3Cg==",
+ "requires": {
+ "append-field": "^1.0.0",
+ "busboy": "^0.2.11",
+ "concat-stream": "^1.5.2",
+ "mkdirp": "^0.5.1",
+ "object-assign": "^4.1.1",
+ "on-finished": "^2.3.0",
+ "type-is": "^1.6.4",
+ "xtend": "^4.0.0"
+ }
+ },
"multipipe": {
"version": "0.1.2",
"resolved": "https://registry.npmjs.org/multipipe/-/multipipe-0.1.2.tgz",
@@ -5933,6 +6137,11 @@
"ee-first": "1.1.1"
}
},
+ "on-headers": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/on-headers/-/on-headers-1.0.2.tgz",
+ "integrity": "sha512-pZAE+FJLoyITytdqK0U5s+FIpjN0JP3OzFi/u8Rx+EV5/W+JTWGXG8xFzevE7AjBfDqHv/8vL8qQsIhHnqRkrA=="
+ },
"once": {
"version": "1.4.0",
"resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz",
@@ -6459,6 +6668,11 @@
"strict-uri-encode": "^1.0.0"
}
},
+ "random-bytes": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/random-bytes/-/random-bytes-1.0.0.tgz",
+ "integrity": "sha1-T2ih3Arli9P7lYSMMDJNt11kNgs="
+ },
"range-parser": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.0.tgz",
@@ -7476,6 +7690,11 @@
"limiter": "^1.0.5"
}
},
+ "streamsearch": {
+ "version": "0.1.2",
+ "resolved": "https://registry.npmjs.org/streamsearch/-/streamsearch-0.1.2.tgz",
+ "integrity": "sha1-gIudDlb8Jz2Am6VzOOkpkZoanxo="
+ },
"strict-uri-encode": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/strict-uri-encode/-/strict-uri-encode-1.1.0.tgz",
@@ -7790,6 +8009,11 @@
"through2": "^2.0.3"
}
},
+ "toidentifier": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.0.tgz",
+ "integrity": "sha512-yaOH/Pk/VEhBWWTlhI+qXxDFXlejDGcQipMlyxda9nthulaxLZUNcUqFxokp0vcYnvteJln5FNQDRrxj3YcbVw=="
+ },
"touch": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/touch/-/touch-3.1.0.tgz",
@@ -7873,6 +8097,14 @@
"resolved": "https://registry.npmjs.org/ua-parser-js/-/ua-parser-js-0.7.17.tgz",
"integrity": "sha512-uRdSdu1oA1rncCQL7sCj8vSyZkgtL7faaw9Tc9rZ3mGgraQ7+Pdx7w5mnOSF3gw9ZNG6oc+KXfkon3bKuROm0g=="
},
+ "uid-safe": {
+ "version": "2.1.5",
+ "resolved": "https://registry.npmjs.org/uid-safe/-/uid-safe-2.1.5.tgz",
+ "integrity": "sha512-KPHm4VL5dDXKz01UuEd88Df+KzynaohSL9fBh096KWAxSKZQDI2uBrVqtvRM4rwrIrRRKsdLNML/lnaaVSRioA==",
+ "requires": {
+ "random-bytes": "~1.0.0"
+ }
+ },
"ultron": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/ultron/-/ultron-1.1.1.tgz",
diff --git a/package.json b/package.json
index 7c689aee0..222b5cc9b 100755
--- a/package.json
+++ b/package.json
@@ -19,10 +19,12 @@
"homepage": "https://github.com/ministryofjustice/mojdt-frontend#readme",
"dependencies": {
"basic-auth": "^2.0.1",
+ "body-parser": "^1.19.0",
"browser-sync": "^2.26.7",
"del": "^3.0.0",
"dotenv": "^6.2.0",
"express": "^4.16.3",
+ "express-session": "^1.16.2",
"govuk-frontend": "^3.0.0",
"gulp": "^4.0.0",
"gulp-autoprefixer": "^6.0.0",
@@ -32,6 +34,7 @@
"gulp-nodemon": "^2.4.2",
"gulp-sass": "^4.0.2",
"moment": "^2.22.2",
+ "multer": "^1.4.2",
"nunjucks": "^3.2.0",
"portscanner": "^2.2.0",
"prompt": "^1.0.0",
diff --git a/package/govuk-prototype-kit.config.json b/package/govuk-prototype-kit.config.json
index 0f2c495f1..87208bf3f 100755
--- a/package/govuk-prototype-kit.config.json
+++ b/package/govuk-prototype-kit.config.json
@@ -1 +1,6 @@
-{"nunjucksPaths": ["/","/components"],"scripts": ["/all.js"],"assets": ["/assets"],"sass": ["/all.scss"]}
+{
+ "nunjucksPaths": ["/moj","/moj/components"],
+ "scripts": ["/moj/all.js"],
+ "assets": ["/moj/assets"],
+ "sass": ["/moj/all.scss"]
+}
diff --git a/server.js b/server.js
index f7c5828cf..baec2ae0d 100755
--- a/server.js
+++ b/server.js
@@ -7,6 +7,11 @@ const browserSync = require('browser-sync');
const dotenv = require('dotenv');
const express = require('express');
const nunjucks = require('nunjucks');
+const sessionInMemory = require('express-session');
+const bodyParser = require('body-parser');
+let sessionOptions = {
+ secret: 'moj-frontend'
+};
// Run before other code to make sure variables from .env are available
dotenv.config();
@@ -14,6 +19,7 @@ dotenv.config();
// Routing
const routes = require('./app/routes');
const autoRoutes = require('./app/routes/auto');
+const uploadRoutes = require('./app/routes/upload');
// Local dependencies
const utils = require('./lib/utils.js');
@@ -41,6 +47,8 @@ const appViews = [
// Application
const app = express();
+app.use(bodyParser.urlencoded({ extended: false }));
+
// Find a free port and start the server
utils.findAvailablePort(app, (port) => {
console.log('Listening on port ' + port + ' url: http://localhost:' + port);
@@ -121,20 +129,18 @@ app.use('/public', express.static(path.join(__dirname, '/public')));
app.use('/assets', express.static(path.join(__dirname, '/node_modules/govuk-frontend/govuk/assets')));
app.use('/assets', express.static(path.join(__dirname, 'src', 'moj', 'assets')));
+app.use(sessionInMemory(Object.assign(sessionOptions, {
+ name: 'moj-frontend',
+ resave: false,
+ saveUninitialized: false
+})));
+
// Use routes
app.use(routes);
+app.use(uploadRoutes);
app.use(autoRoutes); // must be the last one
-// Start app
-// app.listen(port, (err) => {
-
-// if (err) {
-// throw err;
-// } else {
-// console.log('Listening on port 3000 url: http://localhost:3000');
-// }
-// });
const nodeModulesExists = fs.existsSync(path.join(__dirname, '/node_modules'));
if (!nodeModulesExists) {
diff --git a/src/moj/components/_all.scss b/src/moj/components/_all.scss
index ddf9e50aa..bda1cddd8 100755
--- a/src/moj/components/_all.scss
+++ b/src/moj/components/_all.scss
@@ -7,6 +7,7 @@
@import "identity-bar/identity-bar";
@import "menu/menu";
@import "messages/messages";
+@import "multi-file-upload/multi-file-upload";
@import "multi-select/multi-select";
@import "notification-badge/notification-badge";
@import "organisation-switcher/organisation-switcher";
diff --git a/src/moj/components/filter-toggle-button/filter-toggle-button.js b/src/moj/components/filter-toggle-button/filter-toggle-button.js
index d4df0b43d..9b4678976 100644
--- a/src/moj/components/filter-toggle-button/filter-toggle-button.js
+++ b/src/moj/components/filter-toggle-button/filter-toggle-button.js
@@ -61,13 +61,13 @@ MOJFrontend.FilterToggleButton.prototype.removeCloseButton = function() {
MOJFrontend.FilterToggleButton.prototype.hideMenu = function() {
this.menuButton.attr('aria-expanded', 'false');
- this.options.filter.container.addClass('moj-hidden');
+ this.options.filter.container.addClass('moj-js-hidden');
this.menuButton.text(this.options.toggleButton.showText);
};
MOJFrontend.FilterToggleButton.prototype.showMenu = function() {
this.menuButton.attr('aria-expanded', 'true');
- this.options.filter.container.removeClass('moj-hidden');
+ this.options.filter.container.removeClass('moj-js-hidden');
this.menuButton.text(this.options.toggleButton.hideText);
};
diff --git a/src/moj/components/multi-file-upload/README.md b/src/moj/components/multi-file-upload/README.md
new file mode 100644
index 000000000..f801cc729
--- /dev/null
+++ b/src/moj/components/multi-file-upload/README.md
@@ -0,0 +1 @@
+# Multi file upload
\ No newline at end of file
diff --git a/src/moj/components/multi-file-upload/_multi-file-upload.scss b/src/moj/components/multi-file-upload/_multi-file-upload.scss
new file mode 100644
index 000000000..c29e84d5c
--- /dev/null
+++ b/src/moj/components/multi-file-upload/_multi-file-upload.scss
@@ -0,0 +1,66 @@
+.moj-multi-file-upload {
+ margin-bottom: 40px;
+}
+
+.moj-multi-file-upload--enhanced .moj-multi-file-upload__button {
+ display: none;
+}
+
+.moj-multi-file-upload__dropzone {
+ outline: 3px dashed govuk-colour('black');
+ display: flex;
+ text-align: center;
+ padding: govuk-spacing(9) govuk-spacing(3);
+ transition: outline-offset .1s ease-in-out, background-color .1s linear;
+}
+
+.moj-multi-file-upload__dropzone label {
+ margin-bottom: 0;
+ display: inline-block;
+ width: auto;
+}
+
+.moj-multi-file-upload__dropzone p {
+ margin-bottom: 0;
+ margin-right: 10px;
+ padding-top: 7px;
+}
+
+.moj-multi-file-upload__dropzone [type=file] {
+ position: absolute;
+ left: -9999em;
+}
+
+.moj-multi-file-upload--dragover {
+ background: #b1b4b6;
+ outline-color: #6f777b;
+}
+
+.moj-multi-file-upload--focused {
+ background-color: $govuk-focus-colour;
+ color: $govuk-focus-text-colour;
+ box-shadow: 0 -2px $govuk-focus-colour, 0 4px $govuk-focus-text-colour;
+ outline: none;
+}
+
+.moj-multi-file-upload__error {
+ color: govuk-colour('red');
+ font-weight: bold;
+}
+
+.moj-multi-file-upload__success {
+ color: govuk-colour('green');
+ font-weight: bold;
+}
+
+.moj-multi-file-upload__error svg {
+ fill: currentColor;
+ float: left;
+ margin-right: govuk-spacing(2);
+}
+
+.moj-multi-file-upload__success svg {
+ fill: currentColor;
+ float: left;
+ margin-right: govuk-spacing(2);
+}
\ No newline at end of file
diff --git a/src/moj/components/multi-file-upload/macro.njk b/src/moj/components/multi-file-upload/macro.njk
new file mode 100644
index 000000000..3b9c6d458
--- /dev/null
+++ b/src/moj/components/multi-file-upload/macro.njk
@@ -0,0 +1,3 @@
+{% macro mojMultiFileUpload(params) %}
+ {%- include "./template.njk" -%}
+{% endmacro %}
\ No newline at end of file
diff --git a/src/moj/components/multi-file-upload/multi-file-upload.js b/src/moj/components/multi-file-upload/multi-file-upload.js
new file mode 100644
index 000000000..4471d8244
--- /dev/null
+++ b/src/moj/components/multi-file-upload/multi-file-upload.js
@@ -0,0 +1,173 @@
+if(MOJFrontend.dragAndDropSupported() && MOJFrontend.formDataSupported() && MOJFrontend.fileApiSupported()) {
+ MOJFrontend.MultiFileUpload = function(params) {
+ this.defaultParams = {
+ uploadStatusText: 'Uploading files, please wait',
+ dropzoneHintText: 'Drag and drop files here or',
+ dropzoneButtonText: 'Choose files'
+ };
+
+ $.extend(params, this.defaultParams);
+
+ this.params = params;
+ this.params.container.addClass('moj-multi-file-upload--enhanced');
+
+ this.feedbackContainer = this.params.container.find('.moj-multi-file__uploaded-files');
+ this.setupFileInput();
+ this.setupDropzone();
+ this.setupLabel();
+ this.setupStatusBox();
+ this.params.container.on('click', '.moj-multi-file-upload__delete', $.proxy(this, 'onFileDeleteClick'));
+ };
+
+ MOJFrontend.MultiFileUpload.prototype.setupDropzone = function() {
+ this.fileInput.wrap('
');
+ this.dropzone = this.params.container.find('.moj-multi-file-upload__dropzone');
+ this.dropzone.on('dragover', $.proxy(this, 'onDragOver'));
+ this.dropzone.on('dragleave', $.proxy(this, 'onDragLeave'));
+ this.dropzone.on('drop', $.proxy(this, 'onDrop'));
+ };
+
+ MOJFrontend.MultiFileUpload.prototype.setupLabel = function() {
+ this.label = $('
');
+ this.dropzone.append('
' + this.params.dropzoneHintText + '
');
+ this.dropzone.append(this.label);
+ };
+
+ MOJFrontend.MultiFileUpload.prototype.setupFileInput = function() {
+ this.fileInput = this.params.container.find('.moj-multi-file-upload__input');
+ this.fileInput.on('change', $.proxy(this, 'onFileChange'));
+ this.fileInput.on('focus', $.proxy(this, 'onFileFocus'));
+ this.fileInput.on('blur', $.proxy(this, 'onFileBlur'));
+ };
+
+ MOJFrontend.MultiFileUpload.prototype.setupStatusBox = function() {
+ this.status = $('
');
+ this.dropzone.append(this.status);
+ };
+
+ MOJFrontend.MultiFileUpload.prototype.onDragOver = function(e) {
+ e.preventDefault();
+ this.dropzone.addClass('moj-multi-file-upload--dragover');
+ };
+
+ MOJFrontend.MultiFileUpload.prototype.onDragLeave = function() {
+ this.dropzone.removeClass('moj-multi-file-upload--dragover');
+ };
+
+ MOJFrontend.MultiFileUpload.prototype.onDrop = function(e) {
+ e.preventDefault();
+ this.dropzone.removeClass('moj-multi-file-upload--dragover');
+ this.feedbackContainer.removeClass('moj-hidden');
+ this.status.html(this.params.uploadStatusText);
+ this.uploadFiles(e.originalEvent.dataTransfer.files);
+ };
+
+ MOJFrontend.MultiFileUpload.prototype.uploadFiles = function(files) {
+ for(var i = 0; i < files.length; i++) {
+ this.uploadFile(files[i]);
+ }
+ };
+
+ MOJFrontend.MultiFileUpload.prototype.onFileChange = function(e) {
+ this.feedbackContainer.removeClass('moj-hidden');
+ this.status.html(this.params.uploadStatusText);
+ this.uploadFiles(e.currentTarget.files);
+ this.fileInput.replaceWith($(e.currentTarget).val('').clone(true));
+ this.setupFileInput();
+ this.fileInput.focus();
+ };
+
+ MOJFrontend.MultiFileUpload.prototype.onFileFocus = function(e) {
+ this.label.addClass('moj-multi-file-upload--focused');
+ };
+
+ MOJFrontend.MultiFileUpload.prototype.onFileBlur = function(e) {
+ this.label.removeClass('moj-multi-file-upload--focused');
+ };
+
+ MOJFrontend.MultiFileUpload.prototype.getSuccessHtml = function(success) {
+ return '
' + success.messageHtml + '';
+ };
+
+ MOJFrontend.MultiFileUpload.prototype.getErrorHtml = function(error) {
+ return '
'+ error.message +'';
+ };
+
+ MOJFrontend.MultiFileUpload.prototype.getFileRowHtml = function(file) {
+ var html = '';
+ html += '
';
+ html += '
';
+ html += ''+file.name+'';
+ html += '0%';
+ html += ' ';
+ html += ' ';
+ html += '';
+ return html;
+ };
+
+ MOJFrontend.MultiFileUpload.prototype.getDeleteButtonHtml = function(file) {
+ var html = '
';
+ return html;
+ };
+
+ MOJFrontend.MultiFileUpload.prototype.uploadFile = function(file) {
+ var formData = new FormData();
+ formData.append('documents', file);
+ var item = $(this.getFileRowHtml(formData.get('documents')));
+ this.feedbackContainer.find('.moj-multi-file-upload__list').append(item);
+
+ $.ajax({
+ url: this.params.uploadUrl,
+ type: 'post',
+ data: formData,
+ processData: false,
+ contentType: false,
+ success: $.proxy(function(response){
+ if(response.error) {
+ item.find('.moj-multi-file-upload__message').html(this.getErrorHtml(response.error));
+ this.status.html(response.error.message);
+ } else {
+ item.find('.moj-multi-file-upload__message').html(this.getSuccessHtml(response.success));
+ this.status.html(response.success.messageText);
+ }
+ item.find('.moj-multi-file-upload__actions').append(this.getDeleteButtonHtml(response.file));
+ }, this),
+ xhr: function() {
+ var xhr = new XMLHttpRequest();
+ xhr.upload.addEventListener('progress', function(e) {
+ if (e.lengthComputable) {
+ var percentComplete = e.loaded / e.total;
+ percentComplete = parseInt(percentComplete * 100, 10);
+ item.find('.moj-multi-file-upload__progress').text(' ' + percentComplete + '%');
+ }
+ }, false);
+ return xhr;
+ }
+ });
+ };
+
+ MOJFrontend.MultiFileUpload.prototype.onFileDeleteClick = function(e) {
+ e.preventDefault(); // if user refreshes page and then deletes
+ var button = $(e.currentTarget);
+ var data = {};
+ data[button[0].name] = button[0].value;
+ $.ajax({
+ url: this.params.deleteUrl,
+ type: 'post',
+ dataType: 'json',
+ data: data,
+ success: $.proxy(function(response){
+ if(response.error) {
+ // handle error
+ } else {
+ button.parents('.moj-multi-file-upload__row').remove();
+ if(this.feedbackContainer.find('.moj-multi-file-upload__row').length === 0) {
+ this.feedbackContainer.addClass('moj-hidden');
+ }
+ }
+ }, this)
+ });
+ };
+}
\ No newline at end of file
diff --git a/src/moj/components/multi-file-upload/template.njk b/src/moj/components/multi-file-upload/template.njk
new file mode 100644
index 000000000..4339e846a
--- /dev/null
+++ b/src/moj/components/multi-file-upload/template.njk
@@ -0,0 +1,25 @@
+
+
+
{{ params.uploadedFiles.heading.html | safe if params.uploadedFiles.heading.html else params.uploadedFiles.heading.text }}
+
+ {% if params.uploadedFiles.items and params.uploadedFiles.items.length > 0 %}
+ {% for item in params.uploadedFiles.items %}
+
+
+ {{ item.message.html | safe if item.message.html else item.message.text }}
+
+
+
+
+
+ {% endfor %}
+ {% endif %}
+
+
+
+
+ {{ params.uploadHtml | safe }}
+
+
\ No newline at end of file
diff --git a/src/moj/components/search-toggle/search-toggle.js b/src/moj/components/search-toggle/search-toggle.js
index 2261a569f..e86a12ae3 100644
--- a/src/moj/components/search-toggle/search-toggle.js
+++ b/src/moj/components/search-toggle/search-toggle.js
@@ -8,10 +8,10 @@ MOJFrontend.SearchToggle = function(options) {
MOJFrontend.SearchToggle.prototype.onToggleButtonClick = function() {
if(this.toggleButton.attr('aria-expanded') == 'false') {
this.toggleButton.attr('aria-expanded', 'true');
- this.options.search.container.removeClass('moj-hidden');
+ this.options.search.container.removeClass('moj-js-hidden');
this.options.search.container.find('input').first().focus();
} else {
- this.options.search.container.addClass('moj-hidden');
+ this.options.search.container.addClass('moj-js-hidden');
this.toggleButton.attr('aria-expanded', 'false');
}
};
diff --git a/src/moj/helpers.js b/src/moj/helpers.js
index 64a289726..cd2a965a4 100755
--- a/src/moj/helpers.js
+++ b/src/moj/helpers.js
@@ -24,4 +24,19 @@ MOJFrontend.addAttributeValue = function(el, attr, value) {
el.setAttribute(attr, el.getAttribute(attr) + ' ' + value);
}
}
+};
+
+MOJFrontend.dragAndDropSupported = function() {
+ var div = document.createElement('div');
+ return typeof div.ondrop != 'undefined';
+};
+
+MOJFrontend.formDataSupported = function() {
+ return typeof FormData == 'function';
+};
+
+MOJFrontend.fileApiSupported = function() {
+ var input = document.createElement('input');
+ input.type = 'file';
+ return typeof input.files != 'undefined';
};
\ No newline at end of file
diff --git a/src/moj/utilities/_hidden.scss b/src/moj/utilities/_hidden.scss
index 547a8a2d9..2251d6124 100755
--- a/src/moj/utilities/_hidden.scss
+++ b/src/moj/utilities/_hidden.scss
@@ -1,3 +1,7 @@
-.js-enabled .moj-hidden {
+.js-enabled .moj-js-hidden {
+ @include moj-hidden();
+}
+
+.moj-hidden {
@include moj-hidden();
}
\ No newline at end of file