From faf1d7d84a12206c1ceab528499e88e030f39d35 Mon Sep 17 00:00:00 2001 From: Eric Schultz Date: Thu, 6 Sep 2018 12:03:42 -0500 Subject: [PATCH 01/47] Add api_domain to configuration --- app/api/houdini/v1/api.rb | 6 +++--- config/default_organization.yml | 3 +++ config/environment.rb | 5 +++++ config/settings.yml | 3 +++ 4 files changed, 14 insertions(+), 3 deletions(-) diff --git a/app/api/houdini/v1/api.rb b/app/api/houdini/v1/api.rb index 0940fac6..d0e3a921 100644 --- a/app/api/houdini/v1/api.rb +++ b/app/api/houdini/v1/api.rb @@ -14,9 +14,9 @@ class Houdini::V1::API < Grape::API mount Houdini::V1::Nonprofit => '/nonprofit' # Additional mounts are added via generators above this line # DON'T REMOVE THIS OR THE PREVIOUS LINES!!! - uriForHost = URI.parse(Settings.cdn.url) + uri_for_host = URI.parse(Settings.api_domain&.url || Settings.cdn.url) add_swagger_documentation \ - host: "#{uriForHost.host}#{Settings.cdn.port ? ":#{Settings.cdn.port}" : ""}", - schemes: [uriForHost.scheme], + host: "#{uri_for_host.host}#{uri_for_host.port ? ":#{uri_for_host.port}" : ""}", + schemes: [uri_for_host.scheme], base_path: '/api/v1' end \ No newline at end of file diff --git a/config/default_organization.yml b/config/default_organization.yml index e24b45a8..155cc596 100644 --- a/config/default_organization.yml +++ b/config/default_organization.yml @@ -27,3 +27,6 @@ intntl: symbol: "€" abbv: "eur" format: "%n%u" + +api_domain: + url: "http://localhost:5000" diff --git a/config/environment.rb b/config/environment.rb index 31707397..74e95206 100755 --- a/config/environment.rb +++ b/config/environment.rb @@ -284,6 +284,11 @@ Config.schema do required(:url).filled?(:str) end + # the domain for your api. Usually will be your CDN.url + optional(:api_domain).schema do + required(:url).filled?(:str) + end + end Settings.reload! diff --git a/config/settings.yml b/config/settings.yml index 560c403a..3d74efd0 100644 --- a/config/settings.yml +++ b/config/settings.yml @@ -73,3 +73,6 @@ source_tokens: nonprofits_must_be_vetted: false +api_domain: + url: "http://localhost:5000" + From 7fdba9745be550ed133733d7212cb310a2c5de33 Mon Sep 17 00:00:00 2001 From: Eric Schultz Date: Thu, 13 Sep 2018 16:12:22 -0500 Subject: [PATCH 02/47] Fix typo preventing Facebook shares from working --- client/js/nonprofits/donate/followup-step.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/client/js/nonprofits/donate/followup-step.js b/client/js/nonprofits/donate/followup-step.js index 77c0b650..32f52df3 100644 --- a/client/js/nonprofits/donate/followup-step.js +++ b/client/js/nonprofits/donate/followup-step.js @@ -14,7 +14,7 @@ function view(state) { h('a.button--small.facebook.u-width--full.share-button', { props: { target: '_blank' - , href: 'https://www.facebook.com/dialog/feed?app_id='+app.facebook_app_id +"display=popup&caption=" + (app.campaign.name || app.nonprofit.name) + "&link="+window.location.href + , href: 'https://www.facebook.com/dialog/feed?app_id='+app.facebook_app_id +"&display=popup&caption=" + (app.campaign.name || app.nonprofit.name) + "&link="+window.location.href } }, [h('i.fa.fa-facebook-square'), ` ${I18n.t('nonprofits.donate.followup.share.facebook')}`] ) ]) From 22895e9550c442ec37c134cb134ee101fc8d7dbd Mon Sep 17 00:00:00 2001 From: Eric Schultz Date: Mon, 1 Oct 2018 14:42:41 -0500 Subject: [PATCH 03/47] Update moment --- package-lock.json | 50 +++++++++++++++++++++-------------------------- package.json | 5 +++-- 2 files changed, 25 insertions(+), 30 deletions(-) diff --git a/package-lock.json b/package-lock.json index 86571daa..0558b509 100644 --- a/package-lock.json +++ b/package-lock.json @@ -141,6 +141,14 @@ "integrity": "sha512-WD2vUOKfBBVHxWUV9iMR9RMfpuf8HquxWeAq2yqGVL7Nc4JW2+sQama0pREMqzNI3Tutj0PyxYUJwuoxxvX+xA==", "dev": true }, + "@types/moment-timezone": { + "version": "0.5.9", + "resolved": "https://registry.npmjs.org/@types/moment-timezone/-/moment-timezone-0.5.9.tgz", + "integrity": "sha512-tBf1QR8xAayQfI1xD+SMSNDMxi+aCYKEhjgVXTZt3sgxS2XusNX3jM6jJbFoY/ar1CK/PaYJoPkWs/mwcwgOqw==", + "requires": { + "moment": ">=2.14.0" + } + }, "@types/node": { "version": "10.0.6", "resolved": "https://registry.npmjs.org/@types/node/-/node-10.0.6.tgz", @@ -4908,14 +4916,12 @@ "balanced-match": { "version": "1.0.0", "bundled": true, - "dev": true, - "optional": true + "dev": true }, "brace-expansion": { "version": "1.1.11", "bundled": true, "dev": true, - "optional": true, "requires": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" @@ -4930,20 +4936,17 @@ "code-point-at": { "version": "1.1.0", "bundled": true, - "dev": true, - "optional": true + "dev": true }, "concat-map": { "version": "0.0.1", "bundled": true, - "dev": true, - "optional": true + "dev": true }, "console-control-strings": { "version": "1.1.0", "bundled": true, - "dev": true, - "optional": true + "dev": true }, "core-util-is": { "version": "1.0.2", @@ -5060,8 +5063,7 @@ "inherits": { "version": "2.0.3", "bundled": true, - "dev": true, - "optional": true + "dev": true }, "ini": { "version": "1.3.5", @@ -5073,7 +5075,6 @@ "version": "1.0.0", "bundled": true, "dev": true, - "optional": true, "requires": { "number-is-nan": "^1.0.0" } @@ -5088,7 +5089,6 @@ "version": "3.0.4", "bundled": true, "dev": true, - "optional": true, "requires": { "brace-expansion": "^1.1.7" } @@ -5096,14 +5096,12 @@ "minimist": { "version": "0.0.8", "bundled": true, - "dev": true, - "optional": true + "dev": true }, "minipass": { "version": "2.2.4", "bundled": true, "dev": true, - "optional": true, "requires": { "safe-buffer": "^5.1.1", "yallist": "^3.0.0" @@ -5122,7 +5120,6 @@ "version": "0.5.1", "bundled": true, "dev": true, - "optional": true, "requires": { "minimist": "0.0.8" } @@ -5203,8 +5200,7 @@ "number-is-nan": { "version": "1.0.1", "bundled": true, - "dev": true, - "optional": true + "dev": true }, "object-assign": { "version": "4.1.1", @@ -5216,7 +5212,6 @@ "version": "1.4.0", "bundled": true, "dev": true, - "optional": true, "requires": { "wrappy": "1" } @@ -5338,7 +5333,6 @@ "version": "1.0.2", "bundled": true, "dev": true, - "optional": true, "requires": { "code-point-at": "^1.0.0", "is-fullwidth-code-point": "^1.0.0", @@ -9190,9 +9184,9 @@ } }, "moment": { - "version": "2.9.0", - "resolved": "https://registry.npmjs.org/moment/-/moment-2.9.0.tgz", - "integrity": "sha1-d+wRdfopT0JifxDI5t5jAsA29tU=" + "version": "2.22.2", + "resolved": "https://registry.npmjs.org/moment/-/moment-2.22.2.tgz", + "integrity": "sha1-PCV/mDn8DpP/UxSWMiOeuQeD/2Y=" }, "moment-range": { "version": "2.2.0", @@ -9200,11 +9194,11 @@ "integrity": "sha1-sCV1d4pKxQpld3k59cXXV/g/zYU=" }, "moment-timezone": { - "version": "0.4.1", - "resolved": "https://registry.npmjs.org/moment-timezone/-/moment-timezone-0.4.1.tgz", - "integrity": "sha1-gfWYw61eIs2teWtn7NjYjQ9bqgY=", + "version": "0.5.21", + "resolved": "https://registry.npmjs.org/moment-timezone/-/moment-timezone-0.5.21.tgz", + "integrity": "sha512-j96bAh4otsgj3lKydm3K7kdtA3iKf2m6MY2iSYCzCm5a1zmHo1g+aK3068dDEeocLZQIS9kU8bsdQHLqEvgW0A==", "requires": { - "moment": ">= 2.6.0" + "moment": ">= 2.9.0" } }, "moo": { diff --git a/package.json b/package.json index 79a70d9e..ac02ad05 100644 --- a/package.json +++ b/package.json @@ -80,6 +80,7 @@ "webpack-sweet-entry": "^1.1.4" }, "dependencies": { + "@types/moment-timezone": "^0.5.9", "attr-binder": "0.3.1", "aws-sdk": "2.2.39", "chart.js": "2.1.4", @@ -117,9 +118,9 @@ "mobx-react-devtools": "^5.0.1", "mobx-react-form": "github:houdiniproject/mobx-react-form#our_fix", "mobx-utils": "^5.0.1", - "moment": "2.9.0", + "moment": "^2.22.2", "moment-range": "2.2.0", - "moment-timezone": "0.4.1", + "moment-timezone": "^0.5.21", "no-scroll": "^2.1.0", "parsleyjs": "2.0.7", "percent": "1.1.1", From 3f34f4c4a27ef3f816177ee805727878b43e7159 Mon Sep 17 00:00:00 2001 From: Eric Schultz Date: Mon, 1 Oct 2018 14:49:04 -0500 Subject: [PATCH 04/47] Correct dev or prod state of NPM dependencies --- package-lock.json | 42 +++++++++++++++++++++++++++++++++--------- package.json | 3 +-- 2 files changed, 34 insertions(+), 11 deletions(-) diff --git a/package-lock.json b/package-lock.json index 0558b509..bd328b16 100644 --- a/package-lock.json +++ b/package-lock.json @@ -145,6 +145,7 @@ "version": "0.5.9", "resolved": "https://registry.npmjs.org/@types/moment-timezone/-/moment-timezone-0.5.9.tgz", "integrity": "sha512-tBf1QR8xAayQfI1xD+SMSNDMxi+aCYKEhjgVXTZt3sgxS2XusNX3jM6jJbFoY/ar1CK/PaYJoPkWs/mwcwgOqw==", + "dev": true, "requires": { "moment": ">=2.14.0" } @@ -11835,9 +11836,10 @@ } }, "react-is": { - "version": "16.3.2", - "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.3.2.tgz", - "integrity": "sha512-ybEM7YOr4yBgFd6w8dJqwxegqZGJNBZl6U27HnGKuTZmDvVrD5quWOK/wAnMywiZzW+Qsk+l4X2c70+thp/A8Q==" + "version": "16.5.2", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.5.2.tgz", + "integrity": "sha512-hSl7E6l25GTjNEZATqZIuWOgSnpXb3kD0DVCujmg46K5zLxsbiKaaT6VO9slkSBDPZfYs30lwfJwbOFOnoEnKQ==", + "dev": true }, "react-lifecycles-compat": { "version": "3.0.4", @@ -11857,14 +11859,27 @@ } }, "react-test-renderer": { - "version": "16.3.2", - "resolved": "https://registry.npmjs.org/react-test-renderer/-/react-test-renderer-16.3.2.tgz", - "integrity": "sha512-lL8WHIpCTMdSe+CRkt0rfMxBkJFyhVrpdQ54BaJRIrXf9aVmbeHbRA8GFRpTvohPN5tPzMabmrzW2PUfWCfWwQ==", + "version": "16.5.2", + "resolved": "https://registry.npmjs.org/react-test-renderer/-/react-test-renderer-16.5.2.tgz", + "integrity": "sha512-AGbJYbCVx1J6jdUgI4s0hNp+9LxlgzKvXl0ROA3DHTrtjAr00Po1RhDZ/eAq2VC/ww8AHgpDXULh5V2rhEqqJg==", + "dev": true, "requires": { - "fbjs": "^0.8.16", "object-assign": "^4.1.1", - "prop-types": "^15.6.0", - "react-is": "^16.3.2" + "prop-types": "^15.6.2", + "react-is": "^16.5.2", + "schedule": "^0.5.0" + }, + "dependencies": { + "prop-types": { + "version": "15.6.2", + "resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.6.2.tgz", + "integrity": "sha512-3pboPvLiWD7dkI3qf3KbUe6hKFKa52w+AE0VCqECtf+QHAKgOL37tTaNCnuX1nAAQ4ZhyP+kYVKf8rLmJ/feDQ==", + "dev": true, + "requires": { + "loose-envify": "^1.3.1", + "object-assign": "^4.1.1" + } + } } }, "react-text-mask": { @@ -12687,6 +12702,15 @@ "resolved": "https://registry.npmjs.org/sax/-/sax-1.1.5.tgz", "integrity": "sha1-HaUKjQDN7NWUBWWfX/hTSf53N0M=" }, + "schedule": { + "version": "0.5.0", + "resolved": "https://registry.npmjs.org/schedule/-/schedule-0.5.0.tgz", + "integrity": "sha512-HUcJicG5Ou8xfR//c2rPT0lPIRR09vVvN81T9fqfVgBmhERUbDEQoYKjpBxbueJnCPpSu2ujXzOnRQt6x9o/jw==", + "dev": true, + "requires": { + "object-assign": "^4.1.1" + } + }, "schema-utils": { "version": "0.3.0", "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-0.3.0.tgz", diff --git a/package.json b/package.json index ac02ad05..9007b729 100644 --- a/package.json +++ b/package.json @@ -24,6 +24,7 @@ "@types/jquery": "^3.3.1", "@types/jsdom": "^11.0.4", "@types/lodash": "^4.14.106", + "@types/moment-timezone": "^0.5.9", "@types/prop-types": "^15.5.5", "@types/react": "^16.1.0", "@types/react-dom": "^16.0.5", @@ -80,7 +81,6 @@ "webpack-sweet-entry": "^1.1.4" }, "dependencies": { - "@types/moment-timezone": "^0.5.9", "attr-binder": "0.3.1", "aws-sdk": "2.2.39", "chart.js": "2.1.4", @@ -134,7 +134,6 @@ "react-autocomplete": "^1.8.1", "react-dom": "^16.3.1", "react-intl": "^2.4.0", - "react-test-renderer": "^16.3.1", "react-text-mask": "^5.3.0", "shuffle-array": "1.0.1", "snabbdom": "0.3.0", From a282e12c646a11cbd2c904c0733d444ef0880306 Mon Sep 17 00:00:00 2001 From: Eric Schultz Date: Mon, 1 Oct 2018 14:51:51 -0500 Subject: [PATCH 05/47] Add react-aria-modal --- package-lock.json | 37 +++++++++++++++++++++++++++++++++++++ package.json | 1 + types/react-aria-modal | 36 ++++++++++++++++++++++++++++++++++++ 3 files changed, 74 insertions(+) create mode 100644 types/react-aria-modal diff --git a/package-lock.json b/package-lock.json index bd328b16..e61dca29 100644 --- a/package-lock.json +++ b/package-lock.json @@ -4773,6 +4773,23 @@ "resolved": "https://registry.npmjs.org/focus-group/-/focus-group-0.3.1.tgz", "integrity": "sha1-4PMu2GsNq91v/OvfiY7LMuR/7c4=" }, + "focus-trap": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/focus-trap/-/focus-trap-3.0.0.tgz", + "integrity": "sha512-jTFblf0tLWbleGjj2JZsAKbgtZTdL1uC48L8FcmSDl4c2vDoU4NycN1kgV5vJhuq1mxNFkw7uWZ1JAGlINWvyw==", + "requires": { + "tabbable": "^3.1.0", + "xtend": "^4.0.1" + } + }, + "focus-trap-react": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/focus-trap-react/-/focus-trap-react-4.0.1.tgz", + "integrity": "sha512-UUZKVEn5cFbF6yUnW7lbXNW0iqN617ShSqYKgxctUvWw1wuylLtyVmC0RmPQNnJ/U+zoKc/djb0tZMs0uN/0QQ==", + "requires": { + "focus-trap": "^3.0.0" + } + }, "for-in": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/for-in/-/for-in-1.0.2.tgz", @@ -11804,6 +11821,16 @@ "prop-types": "^15.6.0" } }, + "react-aria-modal": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/react-aria-modal/-/react-aria-modal-3.0.0.tgz", + "integrity": "sha512-qudZTYYkrahJIPXssuI/MYQiskkIg4pW+3eVI1mPCn3XbNQ2eun7/3ghVV4IPYTSXERRX9LVQbbfmoFu2Ie9Gg==", + "requires": { + "focus-trap-react": "^4.0.0", + "no-scroll": "^2.1.1", + "react-displace": "^2.3.0" + } + }, "react-autocomplete": { "version": "1.8.1", "resolved": "https://registry.npmjs.org/react-autocomplete/-/react-autocomplete-1.8.1.tgz", @@ -11813,6 +11840,11 @@ "prop-types": "^15.5.10" } }, + "react-displace": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/react-displace/-/react-displace-2.3.0.tgz", + "integrity": "sha512-T8g/lyn3IX8kxLO4k4vJ/oIO9G72pRTc9GYslqKsfPcN4gY5+FYR5OHxeTH1skPjVylJrveGE3OC2qCt3BuHeA==" + }, "react-dom": { "version": "16.3.2", "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-16.3.2.tgz", @@ -13701,6 +13733,11 @@ "acorn-node": "^1.2.0" } }, + "tabbable": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/tabbable/-/tabbable-3.1.1.tgz", + "integrity": "sha512-583MHIOwictf7+zbxqO/L5fBqMN6Li4SJ1XTKQA9WzHRA7c2BB+D+Ny7Y6kGqU2u+rHK59+oRzrBvMU53aZz+A==" + }, "tapable": { "version": "0.2.8", "resolved": "https://registry.npmjs.org/tapable/-/tapable-0.2.8.tgz", diff --git a/package.json b/package.json index 9007b729..e0087b25 100644 --- a/package.json +++ b/package.json @@ -131,6 +131,7 @@ "quill": "^1.3.6", "ramda": "^0.21.0", "react": "^16.2.0", + "react-aria-modal": "^3.0.0", "react-autocomplete": "^1.8.1", "react-dom": "^16.3.1", "react-intl": "^2.4.0", diff --git a/types/react-aria-modal b/types/react-aria-modal new file mode 100644 index 00000000..aefd3d0d --- /dev/null +++ b/types/react-aria-modal @@ -0,0 +1,36 @@ +// License: LGPL-3.0-or-later + +import {Component} from "react"; + +interface ModalProps { + underlayProps?: any + dialogId?:string + underlayClickExits?: boolean + escapeExits?:boolean + onEnter?:() => void + titleText?:string + titleId?:string + + applicationNode?:Node + getApplicationNode?:() => Node + onExit?:() => void + alert?: boolean + includeDefaultStyles?:boolean + dialogClass?:string + dialogStyle?:any + focusDialog?:boolean + initialFocus?:string + mounted?:boolean + underlayStyle?:any + underlayClass?:any + underlayClickExits?:boolean + underlayColor?:string|false + verticallyCenter?:boolean + focusTrapPaused?:boolean + focusTrapOptions?:any + scrollDisabled?:boolean +} + +class Modal extends Component{} + +export = Modal \ No newline at end of file From 2b5b4b40c11b4d010d227770ad7a1720453e00ad Mon Sep 17 00:00:00 2001 From: Eric Schultz Date: Mon, 1 Oct 2018 14:55:05 -0500 Subject: [PATCH 06/47] Improve types of mobx-react-form --- types/mobx-react-form/index.d.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/types/mobx-react-form/index.d.ts b/types/mobx-react-form/index.d.ts index 87b45a4c..b14c7a35 100644 --- a/types/mobx-react-form/index.d.ts +++ b/types/mobx-react-form/index.d.ts @@ -171,7 +171,7 @@ interface FieldHandlers { onError?(e:Field):any } -interface FieldDefinition { +interface FieldDefinition { name: string key?: string label?: string @@ -190,6 +190,8 @@ interface FieldDefinition { rules?: string id?:string, validators?: Validation | Array + input?: (input:TInputType) => string + output?: (value:string) => TInputType } From 03e48bfa15c21d15bb72840fc2d02acd0b0c9e34 Mon Sep 17 00:00:00 2001 From: Eric Schultz Date: Mon, 1 Oct 2018 14:59:13 -0500 Subject: [PATCH 07/47] Use a version of bootstrap-loader that can wrap bootstrap styles --- package-lock.json | 5 ++--- package.json | 2 +- 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/package-lock.json b/package-lock.json index e61dca29..40a1af07 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1852,9 +1852,8 @@ "dev": true }, "bootstrap-loader": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/bootstrap-loader/-/bootstrap-loader-2.2.0.tgz", - "integrity": "sha512-LG8/klminqsCCtPDDCMSCA50LdzmoRvC7JpvJAFFeqWAbSfSY0hZkPUEk5X4wygf33JuFGyiJ7CH/KVnT65I6A==", + "version": "github:houdiniproject/bootstrap-loader#53cdc907485ba21c72470f2d8fb7011c616c823b", + "from": "github:houdiniproject/bootstrap-loader#compiled_namespaced", "dev": true, "requires": { "chalk": "^1.1.3", diff --git a/package.json b/package.json index e0087b25..eb7f2b84 100644 --- a/package.json +++ b/package.json @@ -37,7 +37,7 @@ "babel-preset-env": "^1.6.1", "babel-preset-es2015": "^6.24.1", "bootstrap": "^3.3.7", - "bootstrap-loader": "^2.2.0", + "bootstrap-loader": "github:houdiniproject/bootstrap-loader#compiled_namespaced", "bootstrap-sass": "^3.3.7", "browserify": "13.0.1", "browserify-incremental": "3.1.1", From 1f4314a31e9d66b166594deb6783ae3e89085732 Mon Sep 17 00:00:00 2001 From: Eric Schultz Date: Mon, 1 Oct 2018 15:02:03 -0500 Subject: [PATCH 08/47] Improve bootstrap usage --- .bootstraprc | 7 +++++-- javascripts/src/components/common/layout.tsx | 8 ++++---- 2 files changed, 9 insertions(+), 6 deletions(-) diff --git a/.bootstraprc b/.bootstraprc index df9bc9bb..74f080b1 100644 --- a/.bootstraprc +++ b/.bootstraprc @@ -6,8 +6,11 @@ "mixins": true, "grid": true, "forms": true, - "responsive-utilities":true + "responsive-utilities":true, + "panels": true, + "type": true }, - "scripts": false + "scripts": false, + "styleNamespace": ".tw-bs" } \ No newline at end of file diff --git a/javascripts/src/components/common/layout.tsx b/javascripts/src/components/common/layout.tsx index 4e30056f..808933da 100644 --- a/javascripts/src/components/common/layout.tsx +++ b/javascripts/src/components/common/layout.tsx @@ -4,10 +4,10 @@ import {observer} from "mobx-react"; import * as _ from 'lodash' export const TwoColumnFields = observer((props:{children:Array>}) => { - return
+ return
{ _.take(props.children, 2).map((i:React.ReactElement) => { - let className = "col-left-6" + let className = "col-sm-6" if (_.last(props.children) !== i){ className += " u-paddingRight--10" } @@ -21,10 +21,10 @@ export const TwoColumnFields = observer((props:{children:Array[]}) => { - return
+ return
{ _.take(props.children, 3).map((i:React.ReactElement) => { - let className = "col-left-4" + let className = "col-sm-4" if (_.last(props.children) !== i){ className += " u-paddingRight--10" } From 60760fbb8d23898965a0f2745d8363215709f533 Mon Sep 17 00:00:00 2001 From: Eric Schultz Date: Mon, 1 Oct 2018 15:04:04 -0500 Subject: [PATCH 09/47] Fix bootstrap and make inputs larger --- .../registration_page/NonprofitInfoForm.tsx | 22 ++++++++++++++----- .../registration_page/RegistrationPage.tsx | 2 +- .../registration_page/UserInfoForm.tsx | 6 ++++- .../session_login_page/SessionLoginForm.tsx | 4 ++-- .../session_login_page/SessionLoginPage.tsx | 4 ++-- 5 files changed, 26 insertions(+), 12 deletions(-) diff --git a/javascripts/src/components/registration_page/NonprofitInfoForm.tsx b/javascripts/src/components/registration_page/NonprofitInfoForm.tsx index 1299920c..b82bc1bc 100644 --- a/javascripts/src/components/registration_page/NonprofitInfoForm.tsx +++ b/javascripts/src/components/registration_page/NonprofitInfoForm.tsx @@ -51,34 +51,44 @@ class NonprofitInfoForm extends React.Component + + placeholder={this.props.intl.formatMessage({id: 'registration.wizard.nonprofit.website.placeholder'})} + inputClassNames={"input-lg"}/> + + placeholder={this.props.intl.formatMessage({id: 'registration.wizard.nonprofit.email.placeholder'})} + inputClassNames={"input-lg"}/> + placeholder={this.props.intl.formatMessage({id: 'registration.wizard.nonprofit.phone.placeholder'})} + inputClassNames={"input-lg"}/> + placeholder={this.props.intl.formatMessage({id: 'registration.wizard.nonprofit.city.placeholder'})} + inputClassNames={"input-lg"}/> + placeholder={this.props.intl.formatMessage({id: 'registration.wizard.nonprofit.state.placeholder'})} + inputClassNames={"input-lg"}/> + placeholder={this.props.intl.formatMessage({id: 'registration.wizard.nonprofit.zip.placeholder'})} + inputClassNames={"input-lg"}/> + diff --git a/javascripts/src/components/registration_page/RegistrationPage.tsx b/javascripts/src/components/registration_page/RegistrationPage.tsx index 04878004..c14d5ae6 100644 --- a/javascripts/src/components/registration_page/RegistrationPage.tsx +++ b/javascripts/src/components/registration_page/RegistrationPage.tsx @@ -16,7 +16,7 @@ class RegistrationPage extends React.Component

+ return

} } diff --git a/javascripts/src/components/registration_page/UserInfoForm.tsx b/javascripts/src/components/registration_page/UserInfoForm.tsx index 637d65db..f85961ac 100644 --- a/javascripts/src/components/registration_page/UserInfoForm.tsx +++ b/javascripts/src/components/registration_page/UserInfoForm.tsx @@ -47,18 +47,22 @@ class UserInfoForm extends React.Component + placeholder={this.props.intl.formatMessage({id: "registration.wizard.contact.name.placeholder"})} + inputClassNames={"input-lg"}/> diff --git a/javascripts/src/components/session_login_page/SessionLoginForm.tsx b/javascripts/src/components/session_login_page/SessionLoginForm.tsx index 0f201733..3d7d3ca8 100644 --- a/javascripts/src/components/session_login_page/SessionLoginForm.tsx +++ b/javascripts/src/components/session_login_page/SessionLoginForm.tsx @@ -105,9 +105,9 @@ class InnerSessionLoginForm extends React.Component + label={this.props.intl.formatMessage({id: 'login.email'})} inputClassNames={"input-lg"}/> + label={this.props.intl.formatMessage({id: 'login.password'})} inputClassNames={"input-lg"}/> {errorDiv}
{ render() { - return
+ return

-
; +
; } } From 0bae015a013e1b3cbce897e8ecf1fd4611ec6646 Mon Sep 17 00:00:00 2001 From: Eric Schultz Date: Mon, 1 Oct 2018 15:09:10 -0500 Subject: [PATCH 10/47] Add new validators --- javascripts/src/lib/vjf_rules.ts | 40 ++++++++++++++++++++++++++++++++ 1 file changed, 40 insertions(+) diff --git a/javascripts/src/lib/vjf_rules.ts b/javascripts/src/lib/vjf_rules.ts index 795e5468..ad40e006 100644 --- a/javascripts/src/lib/vjf_rules.ts +++ b/javascripts/src/lib/vjf_rules.ts @@ -1,6 +1,7 @@ // License: LGPL-3.0-or-later import * as Regex from './regex' import {Field, Form} from "mobx-react-form"; +import moment = require("moment"); interface ValidationInput { @@ -42,6 +43,35 @@ export class Validations { ] } + static isNumber({field, validator}:ValidationInput):StringBoolTuple { + return [ + !isNaN(parseFloat(field.value)), + `${field.label} must be a number` + ] + } + + + static isGreaterThanOrEqualTo(value:number) : ({field, validator}:ValidationInput) => StringBoolTuple + { + return ({field, validator}:ValidationInput) => { + return [ + parseFloat(field.value) >= value, + `${field.label} must be at least ${value}` + ] + } + } + + static isLessThanOrEqualTo(value:number, flip:boolean=false) : ({field, validator}:ValidationInput) => StringBoolTuple + { + return ({field, validator}:ValidationInput) => { + let float = parseFloat(field.value) + return [ + (flip ? -1 * float : float) <= value, + `${field.label} must be no more than ${value}` + ] + } + } + static optional(validation:Validation) : Validation { return ({field, form, validator}:ValidationInput) => { if (!field.value || validator.isEmpty(field.value)){ @@ -53,5 +83,15 @@ export class Validations { }; } + static isDate(format:string): ({field, validator}:ValidationInput) => StringBoolTuple { + return ({field, validator}:ValidationInput) => { + let m = moment(field.value, format, true); + return [ + m.isValid(), + `${field.label} must be a date with format: ${format}` + ] + } + } + } \ No newline at end of file From 313bb5f45f1832d00b16d38d9224416f17ce7397 Mon Sep 17 00:00:00 2001 From: Eric Schultz Date: Tue, 9 Oct 2018 11:49:52 -0500 Subject: [PATCH 11/47] Remove no longer needed page.js --- client/js/explore/index/page.js | 3 --- client/js/onboard/page.js | 3 --- client/js/pricing/page.js | 3 --- 3 files changed, 9 deletions(-) delete mode 100644 client/js/explore/index/page.js delete mode 100644 client/js/onboard/page.js delete mode 100644 client/js/pricing/page.js diff --git a/client/js/explore/index/page.js b/client/js/explore/index/page.js deleted file mode 100644 index a937b58a..00000000 --- a/client/js/explore/index/page.js +++ /dev/null @@ -1,3 +0,0 @@ -// License: LGPL-3.0-or-later -require('../../common/vendor/masonry') -require('../../common/onboard') diff --git a/client/js/onboard/page.js b/client/js/onboard/page.js deleted file mode 100644 index 7b854e1b..00000000 --- a/client/js/onboard/page.js +++ /dev/null @@ -1,3 +0,0 @@ -// License: LGPL-3.0-or-later -require('../common/onboard') - diff --git a/client/js/pricing/page.js b/client/js/pricing/page.js deleted file mode 100644 index 7b854e1b..00000000 --- a/client/js/pricing/page.js +++ /dev/null @@ -1,3 +0,0 @@ -// License: LGPL-3.0-or-later -require('../common/onboard') - From 6607f1e9e7b2532bcf8c0e4538bccc7b2c1d19a3 Mon Sep 17 00:00:00 2001 From: Eric Schultz Date: Tue, 9 Oct 2018 11:50:51 -0500 Subject: [PATCH 12/47] Correct missing inputClassName property --- javascripts/src/components/common/fields.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/javascripts/src/components/common/fields.tsx b/javascripts/src/components/common/fields.tsx index 998fe103..84e8d55f 100644 --- a/javascripts/src/components/common/fields.tsx +++ b/javascripts/src/components/common/fields.tsx @@ -8,13 +8,13 @@ import {HoudiniField} from "../../lib/houdini_form"; import ReactInput from "./form/ReactInput"; -export const BasicField = observer((props:{field:Field, placeholder?:string, label?:string, wrapperClassName?:string}) =>{ +export const BasicField = observer((props:{field:Field, placeholder?:string, label?:string, wrapperClassName?:string, inputClassNames?:string}) =>{ let field = props.field as HoudiniField return - + }) \ No newline at end of file From d0bda57f6917ddbd7b8c71a524d95855657db622 Mon Sep 17 00:00:00 2001 From: Eric Schultz Date: Tue, 9 Oct 2018 13:34:57 -0500 Subject: [PATCH 13/47] Add in recurring_donation cancel/modify link --- .../donation_mailer/donor_payment_notification.html.erb | 6 ++++++ config/locales/en.yml | 1 + 2 files changed, 7 insertions(+) diff --git a/app/views/donation_mailer/donor_payment_notification.html.erb b/app/views/donation_mailer/donor_payment_notification.html.erb index 25c46d46..0a4e3d04 100644 --- a/app/views/donation_mailer/donor_payment_notification.html.erb +++ b/app/views/donation_mailer/donor_payment_notification.html.erb @@ -22,4 +22,10 @@
<%= render 'donation_mailer/donation_payment_table', donation: @donation, charge: @donation.charges.last %> +<% if @donation.recurring_donation %> +

+ <%= t('mailer.donations.donor_receipt.recurring_donation_cancel_modify_html', management_url: edit_recurring_donation_url(@donation.recurring_donation, {t: @donation.recurring_donation.edit_token}))%> +

+<% end %> + <%= render 'emails/powered_by' %> diff --git a/config/locales/en.yml b/config/locales/en.yml index 903bffd6..ae806a88 100644 --- a/config/locales/en.yml +++ b/config/locales/en.yml @@ -29,6 +29,7 @@ en: transfer_label_html: "Donation %{nonprofit_statement}." oneoff_donation_html: "Your donation towards %{nonprofit_name} was successful!" recurring_donation_html: "Your recurring donation towards %{nonprofit_name}, started on %{start_date}, has been successfully paid." + recurring_donation_cancel_modify_html: "If you need to update your card or cancel your recurring donation, please follow this link: %{management_url}" donor_direct_debit_notification: subject: "Donation receipt for %{nonprofit_name}" transfer_info_html: "This transfer will appear on your bank statement as %{label}" From fd5b1a68efa53dd55a6cf0125ae5ab740855b785 Mon Sep 17 00:00:00 2001 From: Eric Schultz Date: Wed, 10 Oct 2018 13:44:30 -0500 Subject: [PATCH 14/47] Correct language on org page to say and localize it --- app/views/nonprofits/show.html.erb | 2 +- config/locales/en.yml | 2 ++ 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/app/views/nonprofits/show.html.erb b/app/views/nonprofits/show.html.erb index f6fb010a..99f417c6 100755 --- a/app/views/nonprofits/show.html.erb +++ b/app/views/nonprofits/show.html.erb @@ -90,7 +90,7 @@ <%= render 'contact' %>
-
Promote this nonprofit
+
<%= t("organization_page.promote") %>
<%= render 'common/social_buttons' %>
diff --git a/config/locales/en.yml b/config/locales/en.yml index ae806a88..a745be9f 100644 --- a/config/locales/en.yml +++ b/config/locales/en.yml @@ -10,6 +10,8 @@ en: body: 'Comment content' organization: name: "Organisation" + organization_page: + promote: "Promote this organization" donation: amount: "Total Amount" date: "Transaction Date" From 9d4925dc784229fe5af10feed95e8803f2b9e571 Mon Sep 17 00:00:00 2001 From: Eric Schultz Date: Wed, 24 Oct 2018 11:36:02 -0500 Subject: [PATCH 15/47] Can hard sync a mailchimp list --- app/models/email_list.rb | 5 ++++ app/models/tag_join.rb | 1 + app/models/tag_master.rb | 1 + lib/mailchimp.rb | 56 +++++++++++++++++++++++++++++++++++ spec/factories/email_lists.rb | 5 ++++ spec/lib/mailchimp_spec.rb | 47 +++++++++++++++++++++++++++++ 6 files changed, 115 insertions(+) create mode 100644 app/models/email_list.rb create mode 100644 spec/factories/email_lists.rb create mode 100644 spec/lib/mailchimp_spec.rb diff --git a/app/models/email_list.rb b/app/models/email_list.rb new file mode 100644 index 00000000..1d148f22 --- /dev/null +++ b/app/models/email_list.rb @@ -0,0 +1,5 @@ +class EmailList < ActiveRecord::Base + attr_accessible :list_name, :mailchimp_list_id + belongs_to :nonprofit + belongs_to :tag_master +end diff --git a/app/models/tag_join.rb b/app/models/tag_join.rb index bcaad420..5f0e402a 100644 --- a/app/models/tag_join.rb +++ b/app/models/tag_join.rb @@ -8,6 +8,7 @@ class TagJoin < ActiveRecord::Base validates :tag_master, presence: true belongs_to :tag_master + belongs_to :supporter def name; self.tag_master.name; end diff --git a/app/models/tag_master.rb b/app/models/tag_master.rb index 70272a56..6e9f0ffd 100644 --- a/app/models/tag_master.rb +++ b/app/models/tag_master.rb @@ -12,6 +12,7 @@ class TagMaster < ActiveRecord::Base belongs_to :nonprofit has_many :tag_joins, dependent: :destroy + has_one :email_list scope :not_deleted, ->{where(deleted: [nil,false])} diff --git a/lib/mailchimp.rb b/lib/mailchimp.rb index f8f2c129..03460de7 100644 --- a/lib/mailchimp.rb +++ b/lib/mailchimp.rb @@ -154,4 +154,60 @@ module Mailchimp .execute.map{|h| h['mailchimp_list_id']} end + + # @param [Nonprofit] nonprofit + def self.hard_sync_lists(nonprofit) + return if !nonprofit + + nonprofit.tag_masters.not_deleted.each do |i| + if (i.email_list) + hard_sync_list(i.email_list) + end + end + end + + # @param [EmailList] email_list + def self.hard_sync_list(email_list) + ops = generate_batch_ops_for_hard_sync(email_list) + perform_batch_operations(email_list.nonprofit.id, ops) + + end + + def self.generate_batch_ops_for_hard_sync(email_list) + #get the subscribers from mailchimp + mailchimp_subscribers = get_list_mailchimp_subscribers(email_list) + #get our subscribers + our_supporters = email_list.tag_master.tag_joins.map{|i| i.supporter} + + #split them as follows: + # on both lists, on our list, on the mailchimp list + in_both, in_mailchimp_only = mailchimp_subscribers.partition do |mc_sub| + our_supporters.any?{|s| s.email.downcase == mc_sub[:email_address].downcase} + end + + _, in_our_side_only = our_supporters.partition do |s| + mailchimp_subscribers.any?{|mc_sub| s.email.downcase == mc_sub[:email_address].downcase} + end + + # if on our list, add to mailchimp + output = in_our_side_only.map{|i| + {method: 'POST', path: "lists/#{email_list.mailchimp_list_id}/members", body: {email_address: i.email, status: 'subscribed'}.to_json} + } + + # if on mailchimp list, delete from mailchimp + output = output.concat(in_mailchimp_only.map{|i| {method: 'DELETE', path: "lists/#{email_list.mailchimp_list_id}/members/#{i[:id]}"}}) + + return output + end + + def self.get_list_mailchimp_subscribers(email_list) + mailchimp_token = get_mailchimp_token(email_list.tag_master.nonprofit.id) + uri = base_uri(mailchimp_token) + result = get(uri + "/lists/#{email_list.mailchimp_list_id}/members", { + basic_auth: {username: "CommitChange", password: mailchimp_token}, + headers: {'Content-Type' => 'application/json'}}) + members = result['members'].map do |i| + {id: i['id'], email_address: i['email_address']} + end.to_a + end end diff --git a/spec/factories/email_lists.rb b/spec/factories/email_lists.rb new file mode 100644 index 00000000..677617be --- /dev/null +++ b/spec/factories/email_lists.rb @@ -0,0 +1,5 @@ +FactoryBot.define do + factory :email_list do + + end +end diff --git a/spec/lib/mailchimp_spec.rb b/spec/lib/mailchimp_spec.rb new file mode 100644 index 00000000..f710e70b --- /dev/null +++ b/spec/lib/mailchimp_spec.rb @@ -0,0 +1,47 @@ +# License: AGPL-3.0-or-later WITH Web-Template-Output-Additional-Permission-3.0-or-later +require 'rails_helper' + +describe Mailchimp do + describe '.hard_sync_list' do + let(:ret_val) { [{id: 'on_both', email_address: 'on_both@email.com'}, + {id: 'on_mailchimp', email_address: 'on_mailchimp@email.com'}]} + + let(:np) { force_create(:nonprofit)} + let(:tag_master) {force_create(:tag_master, nonprofit: np)} + let(:email_list) {force_create(:email_list, mailchimp_list_id: 'list_id', tag_master: tag_master, nonprofit:np, list_name: "temp")} + let(:supporter_on_both) { force_create(:supporter, nonprofit:np, email: 'on_BOTH@email.com')} + let(:supporter_on_local) { force_create(:supporter, nonprofit:np, email: 'on_local@email.com')} + let(:tag_join) {force_create(:tag_join, tag_master: tag_master, supporter: supporter_on_both)} + + let(:tag_join2) {force_create(:tag_join, tag_master: tag_master, supporter: supporter_on_local)} + + + it 'excepts when excepting' do + expect(Mailchimp).to receive(:get_list_mailchimp_subscribers).with(email_list).and_raise + + expect{ Mailchimp.generate_batch_ops_for_hard_sync(email_list)}.to raise_error + end + + it 'passes' do + tag_join + tag_join2 + email_list + + expect(Mailchimp).to receive(:get_list_mailchimp_subscribers).with(email_list).and_return(ret_val) + + result = Mailchimp.generate_batch_ops_for_hard_sync(email_list) + + expect(result).to contain_exactly( + { + method: 'POST', + path: 'lists/list_id/members', + body: {email_address: supporter_on_local.email, status: 'subscribed'}.to_json + }, + { + method: 'DELETE', + path: 'lists/list_id/members/on_mailchimp' + }) + end + end + +end From 0ca77c5c72f154fe6410433455080ca22d045d78 Mon Sep 17 00:00:00 2001 From: Eric Schultz Date: Wed, 17 Oct 2018 20:47:26 -0500 Subject: [PATCH 16/47] Fix some bugs for finding list subscribers and setting atributes on EmailList --- app/models/email_list.rb | 2 +- lib/mailchimp.rb | 12 +++++++++++- 2 files changed, 12 insertions(+), 2 deletions(-) diff --git a/app/models/email_list.rb b/app/models/email_list.rb index 1d148f22..c866eeaf 100644 --- a/app/models/email_list.rb +++ b/app/models/email_list.rb @@ -1,5 +1,5 @@ class EmailList < ActiveRecord::Base - attr_accessible :list_name, :mailchimp_list_id + attr_accessible :list_name, :mailchimp_list_id, :nonprofit, :tag_master belongs_to :nonprofit belongs_to :tag_master end diff --git a/lib/mailchimp.rb b/lib/mailchimp.rb index 03460de7..31f06bc4 100644 --- a/lib/mailchimp.rb +++ b/lib/mailchimp.rb @@ -203,11 +203,21 @@ module Mailchimp def self.get_list_mailchimp_subscribers(email_list) mailchimp_token = get_mailchimp_token(email_list.tag_master.nonprofit.id) uri = base_uri(mailchimp_token) - result = get(uri + "/lists/#{email_list.mailchimp_list_id}/members", { + result = get(uri + "/lists/#{email_list.mailchimp_list_id}/members?count=1000000000", { basic_auth: {username: "CommitChange", password: mailchimp_token}, headers: {'Content-Type' => 'application/json'}}) members = result['members'].map do |i| {id: i['id'], email_address: i['email_address']} end.to_a end + + def self.get_email_lists(nonprofit) + mailchimp_token = get_mailchimp_token(nonprofit.id) + uri = base_uri(mailchimp_token) + result = get(uri + "/lists", { + basic_auth: {username: "CommitChange", password: mailchimp_token}, + headers: {'Content-Type' => 'application/json'}}) + result['lists'] + + end end From efbe2a0a8012d0a6cb36c821f7bc97aba4b01c68 Mon Sep 17 00:00:00 2001 From: Eric Schultz Date: Tue, 23 Oct 2018 10:43:05 -0500 Subject: [PATCH 17/47] Provide editable range to the end-of-year report --- .../nonprofits/reports_controller.rb | 7 ++++-- lib/query/query_supporters.rb | 25 +++++++++++++------ 2 files changed, 22 insertions(+), 10 deletions(-) diff --git a/app/controllers/nonprofits/reports_controller.rb b/app/controllers/nonprofits/reports_controller.rb index c9f63bc6..fd7e0346 100644 --- a/app/controllers/nonprofits/reports_controller.rb +++ b/app/controllers/nonprofits/reports_controller.rb @@ -22,10 +22,13 @@ module Nonprofits name_description = params[:year] elsif (params[:start]) name_description = "from-#{params[:start]}" + if (params[:end]) + name_description += "-to-#{params[:end]}" + end end - filename = "end-of-year-report-#{name_description}.csv" - data = QuerySupporters.year_aggregate_report(params[:nonprofit_id], {:year => params[:year], :start => params[:start]}) + filename = "aggregate-report-#{name_description}.csv" + data = QuerySupporters.year_aggregate_report(params[:nonprofit_id], {:year => params[:year], :start => params[:start], :end => params[:end]}) send_data(Format::Csv.from_array(data), filename: filename) end end diff --git a/lib/query/query_supporters.rb b/lib/query/query_supporters.rb index 27eeaf3d..7459787b 100644 --- a/lib/query/query_supporters.rb +++ b/lib/query/query_supporters.rb @@ -630,16 +630,13 @@ UNION DISTINCT end end if (time_range_params[:start]) - wip = time_range_params[:start].is_a?(DateTime) ? time_range_params[:start] : nil - if (wip.nil? && time_range_params[:start].is_a?(Date)) - wip = time_range_params[:start].to_datetime - end - if(wip.nil? && time_range_params[:start].is_a?(String)) - wip = DateTime.parse(time_range_params[:start]) + start = parse_convert_datetime(time_range_params[:start]) + if (time_range_params[:end]) + end_datetime = parse_convert_datetime(time_range_params[:end]) end - unless wip.nil? - return wip, wip + 1.year + unless start.nil? + return start, end_datetime ? end_datetime : start + 1.year end end raise ArgumentError.new("no valid time range provided") @@ -672,5 +669,17 @@ UNION DISTINCT supporters = Supporter.where('supporters.nonprofit_id = ?', npo_id).includes(:recurring_donations) supporters.select{|s| s.recurring_donations.select{|rd| rd.active }.length > 1} end + + def self.parse_convert_datetime(date) + if (date.is_a?(DateTime)) + return date + end + if (date.is_a?(Date)) + return date.to_datetime + end + if(date.is_a?(String)) + return DateTime.parse(date) + end + end end From e4161531aaf78376387624390db37437dfa2d0d8 Mon Sep 17 00:00:00 2001 From: Eric Schultz Date: Tue, 23 Oct 2018 10:43:58 -0500 Subject: [PATCH 18/47] Fix bug where ticket check-ins don't work properly. Closes #101 --- lib/update/update_tickets.rb | 4 ++-- spec/lib/update/update_tickets_spec.rb | 9 +++++++++ 2 files changed, 11 insertions(+), 2 deletions(-) diff --git a/lib/update/update_tickets.rb b/lib/update/update_tickets.rb index 0b77b7be..393ba2a9 100644 --- a/lib/update/update_tickets.rb +++ b/lib/update/update_tickets.rb @@ -9,7 +9,7 @@ module UpdateTickets bid_id: {is_integer: true}, #note: nothing to check? - checked_in: {included_in: ['true', 'false']} + checked_in: {included_in: ['true', 'false', true, false]} }) @@ -40,7 +40,7 @@ module UpdateTickets edited = true end - if data[:checked_in] + unless data[:checked_in].nil? entities[:ticket_id].checked_in = data[:checked_in] edited = true end diff --git a/spec/lib/update/update_tickets_spec.rb b/spec/lib/update/update_tickets_spec.rb index 86533030..7c8df4e1 100644 --- a/spec/lib/update/update_tickets_spec.rb +++ b/spec/lib/update/update_tickets_spec.rb @@ -188,6 +188,15 @@ describe UpdateTickets do expect(ticket.attributes).to eq expected end + it 'success editing checked_in as a boolean' do + result = UpdateTickets.update(basic_valid_ticket_input.merge(checked_in:true)) + expected = general_expected.merge(checked_in: true) + + expect(result.attributes).to eq expected + ticket.reload + expect(ticket.attributes).to eq expected + end + it 'success editing token' do result = UpdateTickets.update(basic_valid_ticket_input.merge(token:source_token.token)) expected = general_expected.merge(source_token_id: source_token.id) From 62f6228f81c5077dd22a24952f5f9a0ffd16d668 Mon Sep 17 00:00:00 2001 From: Eric Schultz Date: Mon, 1 Oct 2018 15:27:58 -0500 Subject: [PATCH 19/47] Add new form components --- .../common/StandardFieldComponent.tsx | 2 +- javascripts/src/components/common/fields.tsx | 45 ++++++++++- .../src/components/common/form/ReactInput.tsx | 42 +++------- .../common/form/ReactSelect.spec.tsx | 10 +++ .../components/common/form/ReactSelect.tsx | 80 +++++++++++++++++++ .../common/form/ReactTextarea.spec.tsx | 10 +++ .../components/common/form/ReactTextarea.tsx | 65 +++++++++++++++ .../common/form/react_input_props.ts | 8 ++ 8 files changed, 227 insertions(+), 35 deletions(-) create mode 100644 javascripts/src/components/common/form/ReactSelect.spec.tsx create mode 100644 javascripts/src/components/common/form/ReactSelect.tsx create mode 100644 javascripts/src/components/common/form/ReactTextarea.spec.tsx create mode 100644 javascripts/src/components/common/form/ReactTextarea.tsx create mode 100644 javascripts/src/components/common/form/react_input_props.ts diff --git a/javascripts/src/components/common/StandardFieldComponent.tsx b/javascripts/src/components/common/StandardFieldComponent.tsx index 48c23aaf..14380c43 100644 --- a/javascripts/src/components/common/StandardFieldComponent.tsx +++ b/javascripts/src/components/common/StandardFieldComponent.tsx @@ -30,7 +30,7 @@ export default class StandardFieldComponent extends React.Component{stickyErrorMessage}
: "" return
- {this.renderChildren()} + {this.props.children} {errorDiv} {stickyErrorDiv }
diff --git a/javascripts/src/components/common/fields.tsx b/javascripts/src/components/common/fields.tsx index 84e8d55f..459f4155 100644 --- a/javascripts/src/components/common/fields.tsx +++ b/javascripts/src/components/common/fields.tsx @@ -6,6 +6,8 @@ import LabeledFieldComponent from "./LabeledFieldComponent"; import {injectIntl, InjectedIntl} from 'react-intl'; import {HoudiniField} from "../../lib/houdini_form"; import ReactInput from "./form/ReactInput"; +import ReactSelect from './form/ReactSelect'; +import ReactTextarea from "./form/ReactTextarea"; export const BasicField = observer((props:{field:Field, placeholder?:string, label?:string, wrapperClassName?:string, inputClassNames?:string}) =>{ @@ -14,7 +16,46 @@ export const BasicField = observer((props:{field:Field, placeholder?:string, lab inputId={props.field.id} labelText={field.label} inError={field.hasError} error={field.error} inStickyError={field.hasServerError} stickyError={field.serverError} className={props.wrapperClassName} > - -}) \ No newline at end of file +}) + +export const SelectField = observer((props:{field:Field, placeholder?:string, label?:string, wrapperClassName?:string, inputClassNames?:string, options?:Array<{id:any, name:string}>}) =>{ + let field = props.field as HoudiniField + return + + + + +}) + +export const TextareaField = observer((props:{field:Field, placeholder?:string, label?:string, wrapperClassName?:string, inputClassNames?:string, rows?:number}) =>{ + let field = props.field as HoudiniField + return + + + + +}) + +export const CurrencyField = observer((props:{field:Field,placeholder?:string, label?:string, currencySymbol?:string, wrapperClassName?:string, inputClassNames?:string}) => { + let field = props.field as HoudiniField + let currencySymbolId = props.field.id + "_____currency_symbol" + return +
+ {props.currencySymbol} + +
+
+ + +}); \ No newline at end of file diff --git a/javascripts/src/components/common/form/ReactInput.tsx b/javascripts/src/components/common/form/ReactInput.tsx index d63d756c..3df05eae 100644 --- a/javascripts/src/components/common/form/ReactInput.tsx +++ b/javascripts/src/components/common/form/ReactInput.tsx @@ -5,24 +5,18 @@ import {InjectedIntlProps, injectIntl} from 'react-intl'; import {Field} from "mobx-react-form"; import {observable, action, toJS, runInAction} from 'mobx'; import {InputHTMLAttributes} from 'react'; +import {ReactInputProps} from "./react_input_props"; +import {SelectHTMLAttributes} from "react"; +import {ReactSelectProps} from "./ReactSelect"; +import {castToNullIfUndef} from "../../../lib/utils"; +type InputTypes = ReactInputProps & + InputHTMLAttributes -export interface ReactInputProps -{ - field:Field - label?:string - placeholder?:string -} +class ReactInput extends React.Component { -function castToNullIfUndef(i:any){ - return i === undefined ? null : i -} - - -class ReactInput extends React.Component, {}> { - - constructor(props:ReactInputProps){ + constructor(props:InputTypes){ super(props) } @@ -43,7 +37,7 @@ class ReactInput extends React.Component, prevState: Readonly<{}>): void { + componentDidUpdate(prevProps: Readonly, prevState: Readonly<{}>): void { this.updateProps() } @@ -53,18 +47,9 @@ class ReactInput extends React.Component, - {...ourProps, ...this.field.bind() }) - return elem - - } - ///Removes the properties we don't want to put into the input element @action.bound - winnowProps(): ReactInputProps & InputHTMLAttributes { + winnowProps(): InputTypes { let ourProps = {...this.props} delete ourProps.field delete ourProps.value @@ -73,14 +58,7 @@ class ReactInput extends React.Component - } } } diff --git a/javascripts/src/components/common/form/ReactSelect.spec.tsx b/javascripts/src/components/common/form/ReactSelect.spec.tsx new file mode 100644 index 00000000..7cb9e7b5 --- /dev/null +++ b/javascripts/src/components/common/form/ReactSelect.spec.tsx @@ -0,0 +1,10 @@ +// License: LGPL-3.0-or-later +import * as React from 'react'; +import 'jest'; +import ReactSelect from './ReactSelect' + +describe('ReactSelect', () => { + test('your test here', () => { + expect(false).toBe(true) + }) +}) \ No newline at end of file diff --git a/javascripts/src/components/common/form/ReactSelect.tsx b/javascripts/src/components/common/form/ReactSelect.tsx new file mode 100644 index 00000000..a6d71056 --- /dev/null +++ b/javascripts/src/components/common/form/ReactSelect.tsx @@ -0,0 +1,80 @@ +// License: LGPL-3.0-or-later +import * as React from 'react'; +import { observer } from 'mobx-react'; +import {InjectedIntlProps, injectIntl} from 'react-intl'; +import {Field} from "../../../../../types/mobx-react-form"; +import {InputHTMLAttributes} from "react"; +import {action, observable} from "mobx"; +import {SelectHTMLAttributes} from "react"; +import {ReactInputProps} from "./react_input_props"; +import {castToNullIfUndef} from "../../../lib/utils"; + + +export interface ReactSelectProps extends ReactInputProps +{ + options?:Array<{id:any, name:string}> +} + +type InputTypes = ReactSelectProps & SelectHTMLAttributes + +class ReactSelect extends React.Component { + + constructor(props:InputTypes){ + super(props) + } + + @observable + field:Field + + + @action.bound + componentWillMount(){ + + this.field = this.props.field + + + this.updateProps() + } + + componentWillUnmount(){ + } + + + componentDidUpdate(prevProps: Readonly, prevState: Readonly<{}>): void { + this.updateProps() + } + + @action.bound + updateProps() { + this.field.set('label', castToNullIfUndef(this.props.label)) + this.field.set('placeholder', castToNullIfUndef(this.props.placeholder)) + } + + + ///Removes the properties we don't want to put into the input element + @action.bound + winnowProps(): InputTypes { + let ourProps = {...this.props} + delete ourProps.field + delete ourProps.value + delete ourProps.options + return ourProps + + } + + render() { + + return + + } +} + +export default observer(ReactSelect) + + + diff --git a/javascripts/src/components/common/form/ReactTextarea.spec.tsx b/javascripts/src/components/common/form/ReactTextarea.spec.tsx new file mode 100644 index 00000000..7cb9e7b5 --- /dev/null +++ b/javascripts/src/components/common/form/ReactTextarea.spec.tsx @@ -0,0 +1,10 @@ +// License: LGPL-3.0-or-later +import * as React from 'react'; +import 'jest'; +import ReactSelect from './ReactSelect' + +describe('ReactSelect', () => { + test('your test here', () => { + expect(false).toBe(true) + }) +}) \ No newline at end of file diff --git a/javascripts/src/components/common/form/ReactTextarea.tsx b/javascripts/src/components/common/form/ReactTextarea.tsx new file mode 100644 index 00000000..a948a2af --- /dev/null +++ b/javascripts/src/components/common/form/ReactTextarea.tsx @@ -0,0 +1,65 @@ +// License: LGPL-3.0-or-later +import * as React from 'react'; +import { observer } from 'mobx-react'; +import {InjectedIntlProps, injectIntl} from 'react-intl'; +import {Field} from "../../../../../types/mobx-react-form"; +import {InputHTMLAttributes, ReactText, TextareaHTMLAttributes} from "react"; +import {action, observable} from "mobx"; +import {ReactInputProps} from "./react_input_props"; +import {castToNullIfUndef} from "../../../lib/utils"; + +type InputTypes = ReactInputProps & TextareaHTMLAttributes + + +class ReactTextarea extends React.Component { + + constructor(props:InputTypes){ + super(props) + } + + @observable + field:Field + + + @action.bound + componentWillMount(){ + + this.field = this.props.field + + + this.updateProps() + } + + componentWillUnmount(){ + } + + + componentDidUpdate(prevProps: Readonly, prevState: Readonly<{}>): void { + this.updateProps() + } + + @action.bound + updateProps() { + this.field.set('label', castToNullIfUndef(this.props.label)) + this.field.set('placeholder', castToNullIfUndef(this.props.placeholder)) + } + + ///Removes the properties we don't want to put into the input element + @action.bound + winnowProps(): InputTypes { + let ourProps = {...this.props} + delete ourProps.field + delete ourProps.value + return ourProps + + } + + render() { + return - - - -
- - - -
-
- -
- - - -
- - <%= render 'components/forms/submit_button', button_text: 'Save', loading_text: 'Updating...' %> - - -
-
diff --git a/app/views/nonprofits/payments/_side_panel.html.erb b/app/views/nonprofits/payments/_side_panel.html.erb index f6bdbbd4..875de66e 100644 --- a/app/views/nonprofits/payments/_side_panel.html.erb +++ b/app/views/nonprofits/payments/_side_panel.html.erb @@ -1,7 +1,6 @@ <%- # License: AGPL-3.0-or-later WITH Web-Template-Output-Additional-Permission-3.0-or-later -%> -
@@ -25,7 +24,7 @@
- + Edit Donation diff --git a/app/views/nonprofits/payments/index.html.erb b/app/views/nonprofits/payments/index.html.erb index ebf54a76..d95f1d36 100644 --- a/app/views/nonprofits/payments/index.html.erb +++ b/app/views/nonprofits/payments/index.html.erb @@ -4,13 +4,43 @@ <% content_for :stylesheets do %> <%= stylesheet_link_tag 'nonprofits/payments/index/page' %> + <%= IncludeAsset.css '/client/css/bootstrap.css' %> <% end %> <% content_for :javascripts do %> + <%= IncludeAsset.js '/client/js/nonprofits/payments/index/page.js' %> + <%= IncludeAsset.js '/app/react.js' %> + <%= IncludeAsset.js '/app/react-dom.js' %> + <%= IncludeAsset.js '/app/vendor.js' %> + <%= IncludeAsset.js '/app/edit_payment_panex.js' %> + + <% end %> <%= render '/components/trial_bar' if QueryBillingSubscriptions.currently_in_trial?(@nonprofit.id) %> diff --git a/app/views/nonprofits/payments/show.rabl b/app/views/nonprofits/payments/show.rabl index 5419f918..62561f57 100644 --- a/app/views/nonprofits/payments/show.rabl +++ b/app/views/nonprofits/payments/show.rabl @@ -19,13 +19,13 @@ child :donation, object_root: false do child :campaign, object_root: false do - attributes :name, :url + attributes :name, :url, :id end node(:campaign_gift){|d| {name: d.campaign_gifts.any? ? d.campaign_gifts.last.campaign_gift_option.name : nil}} child :event, object_root: false do - attributes :name, :url + attributes :name, :url, :id end child :recurring_donation, object_root: false do @@ -49,7 +49,7 @@ end node(:ticket) do |payment| event = GetData.obj(payment.tickets.last, :event) h = { - event: {name: GetData.obj(event, :name), url: GetData.obj(event, :url)}, + event: {name: GetData.obj(event, :name), url: GetData.obj(event, :url), id: GetData.obj(event, :id)}, levels: payment.tickets.map{|t| "#{GetData.chain(t.ticket_level, :name)} (#{t.quantity}x)"}.join(", "), discount: payment.tickets.map{|t| t.event_discount ? "#{t.event_discount.name} (#{t.event_discount.percent}%)" : nil}.compact.join(", ") } @@ -68,3 +68,7 @@ child :supporter do attributes :name, :email, :city, :state_code, :address, :zip_code, :phone, :id, :country end + +child :nonprofit do + attributes :id +end diff --git a/client/js/nonprofits/payments/index/payment_details.js b/client/js/nonprofits/payments/index/payment_details.js index 8c416680..486b4ce9 100644 --- a/client/js/nonprofits/payments/index/payment_details.js +++ b/client/js/nonprofits/payments/index/payment_details.js @@ -112,6 +112,16 @@ appl.def('update_donation', function(donation) { }) }) +appl.def('start_loading', function(){ + appl.def('loading', true) +}) + +appl.def('update_donation__success', function() { + appl.ajax_payment_details.fetch(appl.payment_details.data.id) + appl.def('loading', false) + appl.notify('Donation successfully updated!') +}) + appl.def('delete_offline_donation', function() { var payment = appl.payment_details.data request diff --git a/javascripts/app/edit_payment_pane.tsx b/javascripts/app/edit_payment_pane.tsx new file mode 100644 index 00000000..6f9a4d81 --- /dev/null +++ b/javascripts/app/edit_payment_pane.tsx @@ -0,0 +1,27 @@ +// License: LGPL-3.0-or-later +// require a root component here. This will be treated as the root of a webpack package +import Root from "../src/components/common/Root" +import EditPaymentPane, {FundraiserInfo} from "../src/components/edit_payment_pane/EditPaymentPane" + +import * as ReactDOM from 'react-dom' +import * as React from 'react' + +function LoadReactPage(element:HTMLElement, data:any, campaigns:FundraiserInfo[], + events:FundraiserInfo[], + onClose:() => void, + modalActive:boolean, + preupdateDonationAction: () => void, + postUpdateSuccess: () => void, + nonprofitTimezone?:string + + ) { + ReactDOM.render(, element) +} + + +(window as any).LoadReactEditPaymentPane = LoadReactPage \ No newline at end of file diff --git a/javascripts/src/components/common/__snapshots__/StandardFieldComponent.spec.tsx.snap b/javascripts/src/components/common/__snapshots__/StandardFieldComponent.spec.tsx.snap index eaae44af..acfd395c 100644 --- a/javascripts/src/components/common/__snapshots__/StandardFieldComponent.spec.tsx.snap +++ b/javascripts/src/components/common/__snapshots__/StandardFieldComponent.spec.tsx.snap @@ -2,10 +2,7 @@ exports[`StandardFieldComponent sets error message properly 1`] = `
- +
- +
`; diff --git a/javascripts/src/components/common/form/ReactInput.spec.tsx b/javascripts/src/components/common/form/ReactInput.spec.tsx index b2a030fe..19b7bcdf 100644 --- a/javascripts/src/components/common/form/ReactInput.spec.tsx +++ b/javascripts/src/components/common/form/ReactInput.spec.tsx @@ -6,35 +6,36 @@ import {Form} from "mobx-react-form"; import {mount} from 'enzyme'; import {toJS, observable, action, runInAction} from 'mobx'; import {observer} from 'mobx-react'; -import {InputHTMLAttributes} from 'react'; import {ReactForm} from "./ReactForm"; - @observer -class TestChange extends React.Component{ +class TestChange extends React.Component { @observable - remove:boolean + remove: boolean @observable form: Form @action.bound - componentWillMount(){ - this.form = new Form({fields:[{ - name: 'name', - extra: null} - ]}) + componentWillMount() { + this.form = new Form({ + fields: [{ + name: 'name', + extra: null + } + ] + }) } - @action.bound - onClick(){ + onClick() { this.remove = true } + render() { - let reactInput = !this.remove ? - {this.props.children} + let reactInput = !this.remove ? + : undefined return @@ -45,18 +46,6 @@ class TestChange extends React.Component{ } } - -class WrappedInput extends React.Component>{ - - render(){ - let notChildren = {...this.props} - delete notChildren.children - return
- -
- } -} - describe('ReactInput', () => { let form: Form @@ -71,94 +60,48 @@ describe('ReactInput', () => { }) }) - describe('no children passed in', () => { - test('gets added properly', () => { - let res = mount( - + test('gets added properly', () => { + let res = mount( + - ) + ) - //Did the attributes settings work as expected back to the objects - expect(form.$('name').label).toEqual('label') - expect(form.$('name').placeholder).toEqual('holder') - expect(form.$('name').value).toEqual('') + //Did the attributes settings work as expected back to the objects + expect(form.$('name').label).toEqual('label') + expect(form.$('name').placeholder).toEqual('holder') + expect(form.$('name').value).toEqual('') - //is the aria attribute passted through to the input - let input = res.find('input') - expect(input.prop('aria-required')).toEqual(true) + //is the aria attribute passted through to the input + let input = res.find('input') + expect(input.prop('aria-required')).toEqual(true) - // is the input properly bound? - input.simulate('change', {target: { value: 'something' } }) - expect(form.$('name').value).toEqual('something') - }) - - test('gets removed properly', () => { - - let res = mount() - - // The two casts are needed because Typescript was going blowing up without the 'any' first. - // Why was it? *shrugs* - let f = res.find('ReactForm').instance() as any as ReactForm - expect(f.form.size).toEqual(1) - - res.find('input').simulate('change', {target: { value: 'something' } }) - - expect(f.form.$('name').value).toEqual('something') - - res.find('button').simulate('click') - expect(f.form.size).toEqual(1) - - expect(toJS(res.find('form'))).toMatchSnapshot() - - expect(f.form.$('name').label).toEqual('label1') - expect(f.form.$('name').placeholder).toEqual('holder') - }) + // is the input properly bound? + input.simulate('change', {target: {value: 'something'}}) + expect(form.$('name').value).toEqual('something') }) - describe('children passed in', () => { - test('gets added properly', () => { - let res = mount( - - - + test('gets removed properly', () => { - ) + let res = mount() - //Did the attributes settings work as expected back to the objects - expect(form.$('name').label).toEqual('label') - expect(form.$('name').placeholder).toEqual('holder') - expect(form.$('name').value).toEqual('') + // The two casts are needed because Typescript was going blowing up without the 'any' first. + // Why was it? *shrugs* + let f = res.find('ReactForm').instance() as any as ReactForm + expect(f.form.size).toEqual(1) - //is the aria attribute passted through to the input - let input = res.find('input') - expect(input.prop('aria-required')).toEqual(true) + res.find('input').simulate('change', {target: {value: 'something'}}) + expect(f.form.$('name').value).toEqual('something') - // is the input properly bound? - input.simulate('change', {target: { value: 'something' } }) - expect(form.$('name').value).toEqual('something') - }) + res.find('button').simulate('click') + expect(f.form.size).toEqual(1) - test('gets removed properly', () => { - - let res = mount( - - ) - let f = res.find('ReactForm').instance() as any as ReactForm - res.find('input').simulate('change', {target: { value: 'something' } }) - - expect(f.form.$('name').value).toEqual('something') - expect(f.form.size).toEqual(1) - res.find('button').simulate('click') - expect(f.form.size).toEqual(1) - - expect(f.form.$('name').label).toEqual('label1') - expect(f.form.$('name').placeholder).toEqual('holder') - }) + expect(toJS(res.find('form'))).toMatchSnapshot() + expect(f.form.$('name').label).toEqual('label1') + expect(f.form.$('name').placeholder).toEqual('holder') }) }) \ No newline at end of file diff --git a/javascripts/src/components/common/form/ReactSelect.spec.tsx b/javascripts/src/components/common/form/ReactSelect.spec.tsx index 7cb9e7b5..825392f0 100644 --- a/javascripts/src/components/common/form/ReactSelect.spec.tsx +++ b/javascripts/src/components/common/form/ReactSelect.spec.tsx @@ -1,10 +1,112 @@ // License: LGPL-3.0-or-later import * as React from 'react'; import 'jest'; -import ReactSelect from './ReactSelect' +import {Form} from "mobx-react-form"; +import ReactInput from "./ReactInput"; +import {ReactForm} from "./ReactForm"; +import {action, observable, toJS} from 'mobx'; +import ReactTextarea from './ReactTextarea'; +import {observer} from 'mobx-react'; +import {mount} from 'enzyme'; +import ReactSelect from './ReactSelect'; + + +@observer +class TestChange extends React.Component{ + @observable + remove:boolean + @observable + form: Form + + @action.bound + componentWillMount(){ + this.form = new Form({fields:[{ + name: 'name', + extra: null} + ]}) + } + + + + @action.bound + onClick(){ + this.remove = true + } + render() { + let reactInput = !this.remove ? + + : undefined + + return + + {reactInput} +