diff --git a/.eslintrc b/.eslintrc index 70df80114f..f69d51124f 100644 --- a/.eslintrc +++ b/.eslintrc @@ -22,9 +22,14 @@ "ecmaFeatures": { "modules": true }, - "project": "./tsconfig.json" + "project": "tsconfig.json" }, - "plugins": ["@typescript-eslint", "import", "simple-import-sort", "typesafe"], + "plugins": [ + "@typescript-eslint", + "import", + "simple-import-sort", + "typesafe" + ], "extends": ["plugin:@typescript-eslint/recommended"], "rules": { // Rules for auto sort of imports diff --git a/jest.config.js b/jest.config.js index b3d4e4c550..e913bc9c03 100644 --- a/jest.config.js +++ b/jest.config.js @@ -4,7 +4,11 @@ module.exports = { modulePaths: [''], testEnvironment: 'node', globalSetup: '/tests/jest-global-setup.js', - testPathIgnorePatterns: ['/dist/', '/node_modules/'], + testPathIgnorePatterns: [ + '/dist/', + '/node_modules/', + '/src/public', + ], collectCoverageFrom: ['./src/**/*.{ts,js}', '!**/__tests__/**'], coveragePathIgnorePatterns: ['./node_modules/', './tests'], coverageReporters: ['lcov', 'text'], diff --git a/package-lock.json b/package-lock.json index d5efea59a9..049012921e 100644 --- a/package-lock.json +++ b/package-lock.json @@ -4613,6 +4613,12 @@ "@types/node": "*" } }, + "@types/spark-md5": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/@types/spark-md5/-/spark-md5-3.0.2.tgz", + "integrity": "sha512-82E/lVRaqelV9qmRzzJ1PKTpyrpnT7mwdneKNJB9hUtypZDMggloDfFUCIqRRx3lYRxteCwXSq9c+W71Vf0QnQ==", + "dev": true + }, "@types/stack-utils": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/@types/stack-utils/-/stack-utils-2.0.0.tgz", @@ -8224,11 +8230,6 @@ "randomfill": "^1.0.3" } }, - "crypto-js": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/crypto-js/-/crypto-js-4.0.0.tgz", - "integrity": "sha512-bzHZN8Pn+gS7DQA6n+iUmBfl0hO5DJq++QP3U6uTucDtk/0iGpXd/Gg7CGR0p8tJhofJyaKoWBuJI4eAO00BBg==" - }, "crypto-md5": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/crypto-md5/-/crypto-md5-1.0.0.tgz", @@ -13974,6 +13975,15 @@ "@types/node": "*" } }, + "jest-mock-axios": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/jest-mock-axios/-/jest-mock-axios-4.3.0.tgz", + "integrity": "sha512-9391Cui3dHLP41/W41SXdoWsdBN/tsAewLOdzChPMp85u7cCeGnwygt+w1muGVyw2ZiEBZBsPmdhAM7SBx5vDA==", + "dev": true, + "requires": { + "synchronous-promise": "^2.0.15" + } + }, "jest-pnp-resolver": { "version": "1.2.2", "resolved": "https://registry.npmjs.org/jest-pnp-resolver/-/jest-pnp-resolver-1.2.2.tgz", @@ -20679,6 +20689,11 @@ "integrity": "sha1-PpNdfd1zYxuXZZlW1VEo6HtQhKM=", "dev": true }, + "spark-md5": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/spark-md5/-/spark-md5-3.0.1.tgz", + "integrity": "sha512-0tF3AGSD1ppQeuffsLDIOWlKUd3lS92tFxcsrh5Pe3ZphhnoK+oXIBTzOAThZCiuINZLvpiLH/1VS1/ANEJVig==" + }, "sparse-bitfield": { "version": "3.0.3", "resolved": "https://registry.npmjs.org/sparse-bitfield/-/sparse-bitfield-3.0.3.tgz", @@ -21581,6 +21596,12 @@ "integrity": "sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw==", "dev": true }, + "synchronous-promise": { + "version": "2.0.15", + "resolved": "https://registry.npmjs.org/synchronous-promise/-/synchronous-promise-2.0.15.tgz", + "integrity": "sha512-k8uzYIkIVwmT+TcglpdN50pS2y1BDcUnBPK9iJeGu0Pl1lOI8pD6wtzgw91Pjpe+RxtTncw32tLxs/R0yNL2Mg==", + "dev": true + }, "table": { "version": "6.0.7", "resolved": "https://registry.npmjs.org/table/-/table-6.0.7.tgz", diff --git a/package.json b/package.json index 98bf9647ec..72ffc2decb 100644 --- a/package.json +++ b/package.json @@ -104,7 +104,6 @@ "convict": "^6.0.0", "convict-format-with-validator": "^6.0.0", "cookie-parser": "~1.4.0", - "crypto-js": "^4.0.0", "css-toggle-switch": "^4.1.0", "csv-string": "^4.0.1", "dedent-js": "~1.0.1", @@ -148,6 +147,7 @@ "selectize": "0.12.6", "slick-carousel": "1.8.1", "sortablejs": "~1.13.0", + "spark-md5": "^3.0.1", "text-encoding": "^0.7.0", "toastr": "^2.1.4", "triple-beam": "^1.3.0", @@ -193,6 +193,7 @@ "@types/opossum": "^4.1.1", "@types/promise-retry": "^1.1.3", "@types/puppeteer-core": "^5.4.0", + "@types/spark-md5": "^3.0.2", "@types/supertest": "^2.0.10", "@types/triple-beam": "^1.3.2", "@types/uid-generator": "^2.0.2", @@ -228,6 +229,7 @@ "jasmine-sinon": "^0.4.0", "jasmine-spec-reporter": "^6.0.0", "jest": "^26.6.3", + "jest-mock-axios": "^4.3.0", "lint-staged": "^10.5.4", "maildev": "^1.1.0", "mini-css-extract-plugin": "^0.5.0", diff --git a/src/public/.eslintrc b/src/public/.eslintrc index 52ced10dec..029f038482 100644 --- a/src/public/.eslintrc +++ b/src/public/.eslintrc @@ -4,26 +4,103 @@ "commonjs": true, "jquery": true }, - "extends": ["plugin:angular/johnpapa"], - "globals": { - "angular": true - }, "parserOptions": { "ecmaVersion": 2018 }, - "rules": { - "angular/controller-name": 1, - "angular/controller-as-route": 1, - "angular/controller-as": 1, - "angular/window-service": 1, - "angular/module-getter": 1, - "angular/no-run-logic": 1, - "angular/module-setter": 1, - "angular/file-name": "off", - "angular/function-type": 2, - "angular/document-service": 1, - "angular/timeout-service": 1, - "angular/interval-service": 1, - "angular/no-service-method": 0 - } + "overrides": [ + { + "files": ["*.js"], + "extends": ["plugin:angular/johnpapa"], + "globals": { + "angular": true + }, + "rules": { + "angular/controller-name": 1, + "angular/controller-as-route": 1, + "angular/controller-as": 1, + "angular/window-service": 1, + "angular/module-getter": 1, + "angular/no-run-logic": 1, + "angular/module-setter": 1, + "angular/file-name": "off", + "angular/function-type": 2, + "angular/document-service": 1, + "angular/timeout-service": 1, + "angular/interval-service": 1, + "angular/no-service-method": 0 + } + }, + { + "files": ["*.ts"], + "parser": "@typescript-eslint/parser", + "parserOptions": { + "sourceType": "module", + "ecmaFeatures": { + "modules": true + }, + "project": "tsconfig.json" + }, + "plugins": [ + "@typescript-eslint", + "import", + "simple-import-sort", + "typesafe" + ], + "extends": ["plugin:@typescript-eslint/recommended"], + "rules": { + // Rules for auto sort of imports + "simple-import-sort/imports": [ + "error", + { + "groups": [ + // Side effect imports. + ["^\\u0000"], + // Packages. + // Things that start with a letter (or digit or underscore), or + // `@` followed by a letter. + ["^@?\\w"], + // Root imports + ["^(src)(/.*|$)"], + ["^(tests)(/.*|$)"], + // Parent imports. Put `..` last. + ["^\\.\\.(?!/?$)", "^\\.\\./?$"], + // Other relative imports. Put same-folder imports and `.` last. + ["^\\./(?=.*/)(?!/?$)", "^\\.(?!/?$)", "^\\./?$"] + ] + } + ], + "sort-imports": "off", + "import/order": "off", + "import/first": "error", + "import/newline-after-import": "error", + "import/no-duplicates": "error", + "@typescript-eslint/no-floating-promises": 2, + "@typescript-eslint/no-unused-vars": 2, + "typesafe/no-throw-sync-func": "error" + } + }, + { + "files": ["*.test.ts"], + "extends": ["plugin:jest/recommended"], + "rules": { + "typesafe/no-await-without-trycatch": 0 + } + }, + { + "files": ["*.ts", "*.js"], + "excludedFiles": ["**/*.test.ts", "**/.test.js"], + "rules": { + "typesafe/no-await-without-trycatch": "warn" + } + }, + { + "files": ["**/__mocks__/*.js"], + "parserOptions": { + "sourceType": "module", + "ecmaFeatures": { + "modules": true + } + } + } + ] } diff --git a/src/public/main.js b/src/public/main.js index 6957b55d6c..17b3cb1465 100644 --- a/src/public/main.js +++ b/src/public/main.js @@ -260,7 +260,6 @@ require('./modules/forms/config/forms.client.routes.js') require('./modules/forms/services/form-feedback.client.factory.js') require('./modules/forms/services/form-fields.client.service.js') require('./modules/forms/services/form-factory.client.service.js') -require('./modules/forms/services/file-handler.client.service.js') require('./modules/forms/services/form-api.client.factory.js') require('./modules/forms/services/form-error.client.factory.js') require('./modules/forms/services/spcp-session.client.factory.js') diff --git a/src/public/modules/forms/admin/controllers/edit-fields-modal.client.controller.js b/src/public/modules/forms/admin/controllers/edit-fields-modal.client.controller.js index ce19de3197..9ea4350b31 100644 --- a/src/public/modules/forms/admin/controllers/edit-fields-modal.client.controller.js +++ b/src/public/modules/forms/admin/controllers/edit-fields-modal.client.controller.js @@ -1,5 +1,6 @@ 'use strict' +const axios = require('axios').default const values = require('lodash/values') const { @@ -7,6 +8,9 @@ const { VALID_UPLOAD_FILE_TYPES, MAX_UPLOAD_FILE_SIZE, } = require('shared/constants') +const { uploadImage } = require('../../../../services/FileHandlerService') + +const CancelToken = axios.CancelToken const DATE_VALIDATION_OPTIONS = { disallowPast: 'Disallow past dates', @@ -26,7 +30,6 @@ angular 'Rating', 'Attachment', 'FormFields', - 'FileHandler', '$q', 'Betas', 'Auth', @@ -42,12 +45,12 @@ function EditFieldsModalController( Rating, Attachment, FormFields, - FileHandler, $q, Betas, Auth, $state, ) { + let source const vm = this // Copy so as to not touch the original @@ -522,7 +525,13 @@ function EditFieldsModalController( vm.beforeResizing = () => { vm.uploading = true - vm.shouldCancelUpload = $q.defer() // Will cancel the upload on resolve + vm.cancelUpload() + } + + vm.cancelUpload = () => { + if (source) { + source.cancel() + } } /** @@ -551,12 +560,16 @@ function EditFieldsModalController( } } else if (image) { vm.uploadError = null - - FileHandler.uploadImage( - image, - vm.myform._id, - vm.shouldCancelUpload.promise, - ) + source = CancelToken.source() + + return $q + .when( + uploadImage({ + image, + formId: vm.myform._id, + cancelToken: source.token, + }), + ) .then((result) => { field.url = result.url field.fileMd5Hash = result.fileMd5Hash @@ -568,12 +581,12 @@ function EditFieldsModalController( // On error, we explicitly clear the files stored in the model, as the library does not always automatically do this field.uploadedFile = '' - if (uploadError.xhrStatus === 'abort') { + if (axios.isCancel(uploadError)) { vm.uploadError = `Upload cancelled. Please try again!` - return + } else { + console.error(uploadError) + vm.uploadError = 'Upload error. Please try again!' } - console.error(uploadError) - vm.uploadError = 'Upload error. Please try again!' }) .finally(() => { vm.uploading = false diff --git a/src/public/modules/forms/admin/controllers/edit-start-page-modal.client.controller.js b/src/public/modules/forms/admin/controllers/edit-start-page-modal.client.controller.js index f2dc16644b..e41ed436e5 100644 --- a/src/public/modules/forms/admin/controllers/edit-start-page-modal.client.controller.js +++ b/src/public/modules/forms/admin/controllers/edit-start-page-modal.client.controller.js @@ -1,18 +1,21 @@ 'use strict' +const axios = require('axios').default const { MAX_UPLOAD_FILE_SIZE, VALID_UPLOAD_FILE_TYPES, } = require('shared/constants') +const { uploadLogo } = require('../../../../services/FileHandlerService') const { FormLogoState } = require('../../../../../types') const { getFormLogo } = require('../../helpers/logo') +const CancelToken = axios.CancelToken + angular .module('forms') .controller('EditStartPageController', [ '$uibModalInstance', 'ColorThemes', - 'FileHandler', '$q', 'myform', 'updateField', @@ -22,11 +25,11 @@ angular function EditStartPageController( $uibModalInstance, ColorThemes, - FileHandler, $q, myform, updateField, ) { + let source const vm = this vm.maxLogoSize = MAX_UPLOAD_FILE_SIZE @@ -81,7 +84,13 @@ function EditStartPageController( */ vm.beforeResizing = () => { vm.uploading = true - vm.shouldCancelUpload = $q.defer() // Will cancel the upload on resolve + vm.cancelUpload() + } + + vm.cancelUpload = () => { + if (source) { + source.cancel() + } } /** @@ -110,8 +119,15 @@ function EditStartPageController( } } else if (logo) { vm.uploadError = null - - FileHandler.uploadLogo(logo, vm.myform._id, vm.shouldCancelUpload.promise) + source = CancelToken.source() + + $q.when( + uploadLogo({ + image: logo, + formId: vm.myform._id, + cancelToken: source.token, + }), + ) .then((result) => { vm.myform.startPage.logo = { state: FormLogoState.Custom, @@ -127,12 +143,12 @@ function EditStartPageController( // On error, we explicitly clear the files stored in the model, as the library does not always automatically do this vm.uploaded.file = '' - if (uploadError.xhrStatus === 'abort') { + if (axios.isCancel(uploadError)) { vm.uploadError = `Upload cancelled. Please try again!` - return + } else { + console.error(uploadError) + vm.uploadError = 'Upload error. Please try again!' } - console.error(uploadError) - vm.uploadError = 'Upload error. Please try again!' vm.removeLogo() }) .finally(() => { diff --git a/src/public/modules/forms/admin/views/edit-fields.client.modal.html b/src/public/modules/forms/admin/views/edit-fields.client.modal.html index 44d2ca348c..a59c499cd5 100644 --- a/src/public/modules/forms/admin/views/edit-fields.client.modal.html +++ b/src/public/modules/forms/admin/views/edit-fields.client.modal.html @@ -83,7 +83,7 @@