Merge branch 'docker-improvements' into travis-test
This commit is contained in:
commit
427309dd0a
93 changed files with 3035 additions and 542 deletions
|
@ -6,8 +6,12 @@
|
||||||
"mixins": true,
|
"mixins": true,
|
||||||
"grid": true,
|
"grid": true,
|
||||||
"forms": true,
|
"forms": true,
|
||||||
"responsive-utilities":true
|
"input-groups": true,
|
||||||
|
"responsive-utilities":true,
|
||||||
|
"panels": true,
|
||||||
|
"type": true
|
||||||
},
|
},
|
||||||
|
|
||||||
"scripts": false
|
"scripts": false,
|
||||||
|
"styleNamespace": ".tw-bs"
|
||||||
}
|
}
|
|
@ -14,9 +14,9 @@ class Houdini::V1::API < Grape::API
|
||||||
mount Houdini::V1::Nonprofit => '/nonprofit'
|
mount Houdini::V1::Nonprofit => '/nonprofit'
|
||||||
# Additional mounts are added via generators above this line
|
# Additional mounts are added via generators above this line
|
||||||
# DON'T REMOVE THIS OR THE PREVIOUS LINES!!!
|
# 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 \
|
add_swagger_documentation \
|
||||||
host: "#{uriForHost.host}#{Settings.cdn.port ? ":#{Settings.cdn.port}" : ""}",
|
host: "#{uri_for_host.host}#{uri_for_host.port ? ":#{uri_for_host.port}" : ""}",
|
||||||
schemes: [uriForHost.scheme],
|
schemes: [uri_for_host.scheme],
|
||||||
base_path: '/api/v1'
|
base_path: '/api/v1'
|
||||||
end
|
end
|
|
@ -22,10 +22,13 @@ module Nonprofits
|
||||||
name_description = params[:year]
|
name_description = params[:year]
|
||||||
elsif (params[:start])
|
elsif (params[:start])
|
||||||
name_description = "from-#{params[:start]}"
|
name_description = "from-#{params[:start]}"
|
||||||
|
if (params[:end])
|
||||||
|
name_description += "-to-#{params[:end]}"
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
filename = "end-of-year-report-#{name_description}.csv"
|
filename = "aggregate-report-#{name_description}.csv"
|
||||||
data = QuerySupporters.year_aggregate_report(params[:nonprofit_id], {:year => params[:year], :start => params[:start]})
|
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)
|
send_data(Format::Csv.from_array(data), filename: filename)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
5
app/models/email_list.rb
Normal file
5
app/models/email_list.rb
Normal file
|
@ -0,0 +1,5 @@
|
||||||
|
class EmailList < ActiveRecord::Base
|
||||||
|
attr_accessible :list_name, :mailchimp_list_id, :nonprofit, :tag_master
|
||||||
|
belongs_to :nonprofit
|
||||||
|
belongs_to :tag_master
|
||||||
|
end
|
|
@ -8,6 +8,7 @@ class TagJoin < ActiveRecord::Base
|
||||||
validates :tag_master, presence: true
|
validates :tag_master, presence: true
|
||||||
|
|
||||||
belongs_to :tag_master
|
belongs_to :tag_master
|
||||||
|
belongs_to :supporter
|
||||||
|
|
||||||
def name; self.tag_master.name; end
|
def name; self.tag_master.name; end
|
||||||
|
|
||||||
|
|
|
@ -12,6 +12,7 @@ class TagMaster < ActiveRecord::Base
|
||||||
|
|
||||||
belongs_to :nonprofit
|
belongs_to :nonprofit
|
||||||
has_many :tag_joins, dependent: :destroy
|
has_many :tag_joins, dependent: :destroy
|
||||||
|
has_one :email_list
|
||||||
|
|
||||||
scope :not_deleted, ->{where(deleted: [nil,false])}
|
scope :not_deleted, ->{where(deleted: [nil,false])}
|
||||||
|
|
||||||
|
|
|
@ -22,4 +22,10 @@
|
||||||
<br>
|
<br>
|
||||||
<%= render 'donation_mailer/donation_payment_table', donation: @donation, charge: @donation.charges.last %>
|
<%= render 'donation_mailer/donation_payment_table', donation: @donation, charge: @donation.charges.last %>
|
||||||
|
|
||||||
|
<% if @donation.recurring_donation %>
|
||||||
|
<p>
|
||||||
|
<%= 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}))%>
|
||||||
|
</p>
|
||||||
|
<% end %>
|
||||||
|
|
||||||
<%= render 'emails/powered_by' %>
|
<%= render 'emails/powered_by' %>
|
||||||
|
|
|
@ -1,97 +1,9 @@
|
||||||
<%- # License: AGPL-3.0-or-later WITH Web-Template-Output-Additional-Permission-3.0-or-later -%>
|
<%- # License: AGPL-3.0-or-later WITH Web-Template-Output-Additional-Permission-3.0-or-later -%>
|
||||||
<!-- partial: donations/edit_modal -->
|
<!-- partial: donations/edit_modal -->
|
||||||
|
|
||||||
<div class="modal" id='editDonationModal'>
|
|
||||||
<!--= scope 'payment_details.data' -->
|
|
||||||
|
|
||||||
<%= render 'common/modal_header', title: 'Edit Donation' %>
|
|
||||||
|
|
||||||
<div class='modal-body'>
|
|
||||||
<%= render 'payment_info' %>
|
|
||||||
|
|
||||||
<form class='u-marginTop--20' parsley-validate>
|
|
||||||
<!--= on 'submit' (update_donation form_object) -->
|
|
||||||
|
|
||||||
|
|
||||||
<div>
|
<div id='EditPaymentPaneElement'/>
|
||||||
<!--= show_if (eq this.kind 'OffsitePayment') -->
|
|
||||||
<div class='layout--two'>
|
|
||||||
<fieldset>
|
|
||||||
<label>Gross Amount<br><small>This amount should be more than zero</small></label>
|
|
||||||
<input required type='number' min='0.01' name='gross_amount' step='0.01'>
|
|
||||||
<!--= set_value (remove_commas (cents_to_dollars this.gross_amount -->
|
|
||||||
</fieldset>
|
|
||||||
<fieldset>
|
|
||||||
<label>Processing Fees <small>(optional)</small><br><small>This amount should be 0 or negative</small></label>
|
|
||||||
<input required type='number' name='fee_total' max='0' step='0.01'>
|
|
||||||
<!--= set_value (remove_commas (cents_to_dollars this.fee_total -->
|
|
||||||
</fieldset>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<fieldset>
|
|
||||||
<label>Date</label>
|
|
||||||
<input required type='text' name='date'>
|
|
||||||
<!--= set_value (readable_date_time this.date) -->
|
|
||||||
</fieldset>
|
|
||||||
|
|
||||||
<fieldset>
|
|
||||||
<label>Check or Payment Number/ID</label>
|
|
||||||
<input type='text' name='check_number'>
|
|
||||||
<!--= set_value (this.offsite_payment.check_number) -->
|
|
||||||
</fieldset>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<fieldset>
|
|
||||||
<label>Campaign</label>
|
|
||||||
<select name='campaign_id'>
|
|
||||||
<option>
|
|
||||||
<!--= repeat campaigns.data -->
|
|
||||||
<!--= set_attr_if (eq payment_details.data.donation.campaign.name this.name) 'selected' true -->
|
|
||||||
<!--= set_value this.id -->
|
|
||||||
<!--= put this.name -->
|
|
||||||
</option>
|
|
||||||
</select>
|
|
||||||
</fieldset>
|
|
||||||
|
|
||||||
<fieldset>
|
|
||||||
<label>Event</label>
|
|
||||||
<select name='event_id'>
|
|
||||||
<option>
|
|
||||||
<!--= repeat events.data -->
|
|
||||||
<!--= set_attr_if (eq payment_details.data.donation.event.name this.name) 'selected' true -->
|
|
||||||
<!--= set_value this.id -->
|
|
||||||
<!--= put this.name -->
|
|
||||||
</option>
|
|
||||||
</select>
|
|
||||||
</fieldset>
|
|
||||||
|
|
||||||
<input type='hidden' name='id'>
|
|
||||||
<!--= set_value this.donation.id -->
|
|
||||||
|
|
||||||
<div class='layout--two'>
|
|
||||||
<fieldset>
|
|
||||||
<label>Dedication <small> (optional)</small></label>
|
|
||||||
<textarea rows='3' name='dedication' placeholder='Dedication'></textarea>
|
|
||||||
<!--= set_value this.donation.dedication -->
|
|
||||||
</fieldset>
|
|
||||||
|
|
||||||
<fieldset>
|
|
||||||
<label>Designation<small> (optional)</small></label>
|
|
||||||
<textarea rows='3' name='designation' placeholder='Designation'></textarea>
|
|
||||||
<!--= set_value this.donation.designation -->
|
|
||||||
</fieldset>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<fieldset>
|
|
||||||
<label>Notes <small> (optional) </small> </label>
|
|
||||||
<textarea name='comment' rows='3' placeholder='Notes'></textarea>
|
|
||||||
<!--= set_value this.donation.comment -->
|
|
||||||
</fieldset>
|
|
||||||
|
|
||||||
<%= render 'components/forms/submit_button', button_text: 'Save', loading_text: 'Updating...' %>
|
|
||||||
|
|
||||||
</form>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- end partial: donations/edit_modal -->
|
<!-- end partial: donations/edit_modal -->
|
||||||
|
|
|
@ -1,7 +1,6 @@
|
||||||
<%- # License: AGPL-3.0-or-later WITH Web-Template-Output-Additional-Permission-3.0-or-later -%>
|
<%- # License: AGPL-3.0-or-later WITH Web-Template-Output-Additional-Permission-3.0-or-later -%>
|
||||||
|
|
||||||
<!-- partial nonprofits/payments/_side_panel -->
|
<!-- partial nonprofits/payments/_side_panel -->
|
||||||
|
|
||||||
<div class='sidePanel'>
|
<div class='sidePanel'>
|
||||||
<!--= add_class_if loading 'u-halfOpacity' -->
|
<!--= add_class_if loading 'u-halfOpacity' -->
|
||||||
|
|
||||||
|
@ -25,7 +24,7 @@
|
||||||
<div class='u-marginTop--20'>
|
<div class='u-marginTop--20'>
|
||||||
<a class="button--tiny edit">
|
<a class="button--tiny edit">
|
||||||
<!--= show_if (all (payment_details.data.donation) (not payment_details.data.dispute)) -->
|
<!--= show_if (all (payment_details.data.donation) (not payment_details.data.dispute)) -->
|
||||||
<!--= on 'click' (open_modal 'editDonationModal') -->
|
<!--= on 'click' (open_donation_modal payment_details) -->
|
||||||
<i class='fa fa-pencil'></i> Edit Donation
|
<i class='fa fa-pencil'></i> Edit Donation
|
||||||
</a>
|
</a>
|
||||||
|
|
||||||
|
|
|
@ -4,13 +4,42 @@
|
||||||
|
|
||||||
<% content_for :stylesheets do %>
|
<% content_for :stylesheets do %>
|
||||||
<%= stylesheet_link_tag 'nonprofits/payments/index/page' %>
|
<%= stylesheet_link_tag 'nonprofits/payments/index/page' %>
|
||||||
|
<%= IncludeAsset.css '/client/css/bootstrap.css' %>
|
||||||
<% end %>
|
<% end %>
|
||||||
|
|
||||||
<% content_for :javascripts do %>
|
<% content_for :javascripts do %>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
appl.def('has_bank', <%= !!@nonprofit.bank_account %>)
|
appl.def('has_bank', <%= !!@nonprofit.bank_account %>)
|
||||||
</script>
|
</script>
|
||||||
<%= IncludeAsset.js '/client/js/nonprofits/payments/index/page.js' %>
|
<%= 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' %>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
appl.def('open_donation_modal', function(payment_details) {
|
||||||
|
|
||||||
|
function SetupLoadReactEditPaymentPane(modalActive){
|
||||||
|
LoadReactEditPaymentPane(document.getElementById('EditPaymentPaneElement'),
|
||||||
|
payment_details.data,
|
||||||
|
appl.campaigns.data,
|
||||||
|
appl.events.data,
|
||||||
|
() => {SetupLoadReactEditPaymentPane(false)},
|
||||||
|
modalActive,
|
||||||
|
appl.start_loading,
|
||||||
|
appl.update_donation__success,
|
||||||
|
ENV.nonprofitTimezone)
|
||||||
|
}
|
||||||
|
|
||||||
|
SetupLoadReactEditPaymentPane(true)
|
||||||
|
|
||||||
|
|
||||||
|
return appl
|
||||||
|
})
|
||||||
|
|
||||||
|
</script>
|
||||||
<% end %>
|
<% end %>
|
||||||
|
|
||||||
<%= render '/components/trial_bar' if QueryBillingSubscriptions.currently_in_trial?(@nonprofit.id) %>
|
<%= render '/components/trial_bar' if QueryBillingSubscriptions.currently_in_trial?(@nonprofit.id) %>
|
||||||
|
|
|
@ -19,13 +19,13 @@ child :donation, object_root: false do
|
||||||
|
|
||||||
|
|
||||||
child :campaign, object_root: false do
|
child :campaign, object_root: false do
|
||||||
attributes :name, :url
|
attributes :name, :url, :id
|
||||||
end
|
end
|
||||||
|
|
||||||
node(:campaign_gift){|d| {name: d.campaign_gifts.any? ? d.campaign_gifts.last.campaign_gift_option.name : nil}}
|
node(:campaign_gift){|d| {name: d.campaign_gifts.any? ? d.campaign_gifts.last.campaign_gift_option.name : nil}}
|
||||||
|
|
||||||
child :event, object_root: false do
|
child :event, object_root: false do
|
||||||
attributes :name, :url
|
attributes :name, :url, :id
|
||||||
end
|
end
|
||||||
|
|
||||||
child :recurring_donation, object_root: false do
|
child :recurring_donation, object_root: false do
|
||||||
|
@ -49,7 +49,7 @@ end
|
||||||
node(:ticket) do |payment|
|
node(:ticket) do |payment|
|
||||||
event = GetData.obj(payment.tickets.last, :event)
|
event = GetData.obj(payment.tickets.last, :event)
|
||||||
h = {
|
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(", "),
|
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(", ")
|
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
|
attributes :name, :email, :city, :state_code, :address, :zip_code, :phone, :id, :country
|
||||||
|
|
||||||
end
|
end
|
||||||
|
|
||||||
|
child :nonprofit do
|
||||||
|
attributes :id
|
||||||
|
end
|
||||||
|
|
|
@ -90,7 +90,7 @@
|
||||||
<%= render 'contact' %>
|
<%= render 'contact' %>
|
||||||
|
|
||||||
<div class='pastelBox--grey'>
|
<div class='pastelBox--grey'>
|
||||||
<header>Promote this nonprofit</header>
|
<header><%= t("organization_page.promote") %></header>
|
||||||
<div class='pastelBox-body'>
|
<div class='pastelBox-body'>
|
||||||
<%= render 'common/social_buttons' %>
|
<%= render 'common/social_buttons' %>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -2,10 +2,42 @@
|
||||||
<% content_for(:dont_load_optimizely) {'true'} %>
|
<% content_for(:dont_load_optimizely) {'true'} %>
|
||||||
<% content_for(:footer_hidden) {'hidden'} %>
|
<% content_for(:footer_hidden) {'hidden'} %>
|
||||||
|
|
||||||
<%= content_for(:stylesheets) {stylesheet_link_tag 'nonprofits/supporters/index/page'} %>
|
<% content_for(:stylesheets) do %>
|
||||||
|
<%= stylesheet_link_tag 'nonprofits/supporters/index/page' %>
|
||||||
|
<%= IncludeAsset.css '/client/css/bootstrap.css' %>
|
||||||
|
<% end %>
|
||||||
<%= content_for :javascripts do %>
|
<%= content_for :javascripts do %>
|
||||||
<%= IncludeAsset.js '/client/js/nonprofits/supporters/index/page.js' %>
|
<%= IncludeAsset.js '/client/js/nonprofits/supporters/index/page.js' %>
|
||||||
<%= render 'common/froala' %>
|
<%= render 'common/froala' %>
|
||||||
|
<%= IncludeAsset.js '/app/react.js' %>
|
||||||
|
<%= IncludeAsset.js '/app/react-dom.js' %>
|
||||||
|
<%= IncludeAsset.js '/app/vendor.js' %>
|
||||||
|
<%= IncludeAsset.js '/app/create_new_offsite_payment_panex.js' %>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
appl.def('open_donation_modal', function(supporter_id, donation_finish_successful_state_fn) {
|
||||||
|
$('.modal').removeClass('inView')
|
||||||
|
|
||||||
|
function SetupLoadCreateOffsiteDonationPane(modalActive){
|
||||||
|
LoadReactCreateOffsiteDonationPane(document.getElementById('react-vdom-hack'),
|
||||||
|
appl.campaigns.data,
|
||||||
|
appl.events.data,
|
||||||
|
app.nonprofit_id,
|
||||||
|
supporter_id,
|
||||||
|
() => {}, //appl.start_loading,
|
||||||
|
donation_finish_successful_state_fn, //appl.update_donation__success,
|
||||||
|
() => {SetupLoadCreateOffsiteDonationPane(false)},
|
||||||
|
modalActive,
|
||||||
|
ENV.nonprofitTimezone)
|
||||||
|
}
|
||||||
|
|
||||||
|
SetupLoadCreateOffsiteDonationPane(true)
|
||||||
|
|
||||||
|
|
||||||
|
return appl
|
||||||
|
})
|
||||||
|
|
||||||
|
</script>
|
||||||
<% end %>
|
<% end %>
|
||||||
|
|
||||||
<%= render '/components/trial_bar' if nonprofit_in_trial?(@nonprofit.id) %>
|
<%= render '/components/trial_bar' if nonprofit_in_trial?(@nonprofit.id) %>
|
||||||
|
@ -58,3 +90,5 @@
|
||||||
<%= render 'donations/new_offline_modal' %>
|
<%= render 'donations/new_offline_modal' %>
|
||||||
|
|
||||||
<div id='js-vdomParty'></div>
|
<div id='js-vdomParty'></div>
|
||||||
|
|
||||||
|
<div id="react-vdom-hack"></div>
|
||||||
|
|
|
@ -1,3 +0,0 @@
|
||||||
// License: LGPL-3.0-or-later
|
|
||||||
require('../../common/vendor/masonry')
|
|
||||||
require('../../common/onboard')
|
|
|
@ -21,7 +21,7 @@ function view(state) {
|
||||||
, type: 'radio'
|
, type: 'radio'
|
||||||
, id: radioId1
|
, id: radioId1
|
||||||
, value: 'honor'
|
, value: 'honor'
|
||||||
, selected: data.dedication_type === 'honor'
|
, checked: !data.dedication_type || data.dedication_type === 'honor'
|
||||||
}})
|
}})
|
||||||
, h('label', {props: {htmlFor: radioId1}}, I18n.t('nonprofits.donate.dedication.in_honor_label'))
|
, h('label', {props: {htmlFor: radioId1}}, I18n.t('nonprofits.donate.dedication.in_honor_label'))
|
||||||
])
|
])
|
||||||
|
@ -31,7 +31,7 @@ function view(state) {
|
||||||
, type: 'radio'
|
, type: 'radio'
|
||||||
, value: 'memory'
|
, value: 'memory'
|
||||||
, id: radioId2
|
, id: radioId2
|
||||||
, selected: data.dedication_type === 'memory'
|
, checked: data.dedication_type === 'memory'
|
||||||
}})
|
}})
|
||||||
, h('label', {props: {htmlFor: radioId2}}, I18n.t('nonprofits.donate.dedication.in_memory_label'))
|
, h('label', {props: {htmlFor: radioId2}}, I18n.t('nonprofits.donate.dedication.in_memory_label'))
|
||||||
])
|
])
|
||||||
|
|
|
@ -14,7 +14,7 @@ function view(state) {
|
||||||
h('a.button--small.facebook.u-width--full.share-button', {
|
h('a.button--small.facebook.u-width--full.share-button', {
|
||||||
props: {
|
props: {
|
||||||
target: '_blank'
|
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')}`] )
|
}, [h('i.fa.fa-facebook-square'), ` ${I18n.t('nonprofits.donate.followup.share.facebook')}`] )
|
||||||
])
|
])
|
||||||
|
|
|
@ -166,7 +166,7 @@ function view(state) {
|
||||||
])
|
])
|
||||||
, weekly
|
, weekly
|
||||||
, dedic && (dedic.first_name || dedic.last_name)
|
, dedic && (dedic.first_name || dedic.last_name)
|
||||||
? h('p.u-centered', `${dedic.dedication_type === 'memory' ? I18n.t('nonprofits.donate.dedication.in_memory_label') : I18n.t('nonprofits.donate.dedication.in_honor_label')} ` + `${dedic.first_name} ${dedic.last_name}`)
|
? h('p.u-centered', `${dedic.dedication_type === 'memory' ? I18n.t('nonprofits.donate.dedication.in_memory_label') : I18n.t('nonprofits.donate.dedication.in_honor_label')} ` + `${dedic.first_name || ''} ${dedic.last_name || ''}`)
|
||||||
: ''
|
: ''
|
||||||
, paymentTabs(state)
|
, paymentTabs(state)
|
||||||
])
|
])
|
||||||
|
|
|
@ -127,7 +127,9 @@ const setDonationDedication = (don, dedication) => {
|
||||||
, JSON.stringify({
|
, JSON.stringify({
|
||||||
supporter_id: dedication.supporter.id
|
supporter_id: dedication.supporter.id
|
||||||
, name: dedication.supporter.name
|
, name: dedication.supporter.name
|
||||||
, contact: R.join(" - ", [dedication.supporter.email, dedication.supporter.phone, dedication.supporter.address])
|
, contact: {email: dedication.supporter.email,
|
||||||
|
phone: dedication.supporter.phone,
|
||||||
|
address: dedication.supporter.address}
|
||||||
, note: dedication.note
|
, note: dedication.note
|
||||||
, type: dedication.type
|
, type: dedication.type
|
||||||
})
|
})
|
||||||
|
|
|
@ -89,27 +89,15 @@ appl.def('get_payment_purchase_object', function(payment) {
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
appl.def('update_donation', function(donation) {
|
appl.def('start_loading', function(){
|
||||||
if(!donation) return
|
|
||||||
appl.def('loading', true)
|
appl.def('loading', true)
|
||||||
donation.gross_amount = format.dollarsToCents(donation.gross_amount)
|
})
|
||||||
donation.fee_total = format.dollarsToCents(donation.fee_total)
|
|
||||||
var formattedDate = appl.readable_date_time_to_iso(donation.date)
|
appl.def('update_donation__success', function() {
|
||||||
if(formattedDate && formattedDate != "Invalid date") {
|
appl.ajax_payment_details.fetch(appl.payment_details.data.id)
|
||||||
donation.date = formattedDate
|
appl.def('loading', false)
|
||||||
} else {
|
// appl.close_modal()
|
||||||
appl.notify('Please enter a valid date')
|
appl.notify('Donation successfully updated!')
|
||||||
appl.def('loading', false)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
request.put('/nonprofits/' + app.nonprofit_id + '/donations/' + donation.id)
|
|
||||||
.send({donation: donation})
|
|
||||||
.end(function(err, resp) {
|
|
||||||
appl.ajax_payment_details.fetch(appl.payment_details.data.id)
|
|
||||||
appl.def('loading', false)
|
|
||||||
appl.close_modal()
|
|
||||||
appl.notify('Donation successfully updated!')
|
|
||||||
})
|
|
||||||
})
|
})
|
||||||
|
|
||||||
appl.def('delete_offline_donation', function() {
|
appl.def('delete_offline_donation', function() {
|
||||||
|
@ -161,14 +149,17 @@ appl.def('format_dedication', function(dedic, node) {
|
||||||
var json
|
var json
|
||||||
try { json = JSON.parse(dedic) } catch(e) {}
|
try { json = JSON.parse(dedic) } catch(e) {}
|
||||||
if(json) {
|
if(json) {
|
||||||
|
let supporter_link = (json.supporter_id && json.supporter_id != '') ?
|
||||||
|
`<a href='/nonprofits/${app.nonprofit_id}/supporters?sid=${json.supporter_id}'>${json.name}</a>` :
|
||||||
|
json.name
|
||||||
inner = `
|
inner = `
|
||||||
Donation made in ${dedic.type || 'honor'} of
|
Donation made in ${json.type || 'honor'} of
|
||||||
<a href='/nonprofits/${app.nonprofit_id}/supporters?sid=${json.supporter_id}'>${json.name}</a>.
|
${supporter_link}.
|
||||||
${json.note ? `<br>Note: <em>${json.note}</em>.` : ''}
|
${json.note ? `<br>Note: <em>${json.note}</em>.` : ''}
|
||||||
`
|
`
|
||||||
} else {
|
} else {
|
||||||
// Print plaintext dedication
|
// Print plaintext dedication
|
||||||
inner = dedic
|
inner = ''
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
td.innerHTML = inner
|
td.innerHTML = inner
|
||||||
|
|
|
@ -31,6 +31,7 @@ const init = _ => {
|
||||||
, newNote$: flyd.stream()
|
, newNote$: flyd.stream()
|
||||||
, editNote$: flyd.stream()
|
, editNote$: flyd.stream()
|
||||||
, deleteNote$: flyd.stream()
|
, deleteNote$: flyd.stream()
|
||||||
|
, newDonation$: flyd.stream()
|
||||||
}
|
}
|
||||||
|
|
||||||
const supporterID$ = R.compose(
|
const supporterID$ = R.compose(
|
||||||
|
@ -95,7 +96,6 @@ const init = _ => {
|
||||||
, flyd.map(()=> 'replyGmailModal', state.threadId$)
|
, flyd.map(()=> 'replyGmailModal', state.threadId$)
|
||||||
, flyd.map(()=> 'newSupporterNoteModal', state.editNoteData$)
|
, flyd.map(()=> 'newSupporterNoteModal', state.editNoteData$)
|
||||||
, flyd.map(()=> null, state.gmail.sendResponse$)
|
, flyd.map(()=> null, state.gmail.sendResponse$)
|
||||||
, flyd.map(()=> null, state.offsiteDonationForm.saved$)
|
|
||||||
, flyd.map(()=> null, state.supporterNoteForm.saved$)
|
, flyd.map(()=> null, state.supporterNoteForm.saved$)
|
||||||
])
|
])
|
||||||
|
|
||||||
|
@ -143,7 +143,7 @@ const view = state => {
|
||||||
, activities.view(state)
|
, activities.view(state)
|
||||||
, composeModal(state)
|
, composeModal(state)
|
||||||
, notification.view(state.notification)
|
, notification.view(state.notification)
|
||||||
, offsiteDonationForm.view(R.merge(state.offsiteDonationForm, {modalID$: state.modalID$}))
|
, offsiteDonationForm.view(R.merge(state.offsiteDonationForm))
|
||||||
, supporterNoteForm.view(R.merge(state.supporterNoteForm, {modalID$: state.modalID$}))
|
, supporterNoteForm.view(R.merge(state.supporterNoteForm, {modalID$: state.modalID$}))
|
||||||
, replyModal(state)
|
, replyModal(state)
|
||||||
, confirm.view(state.confirmDelete, 'Are you sure you want to delete this note?')
|
, confirm.view(state.confirmDelete, 'Are you sure you want to delete this note?')
|
||||||
|
|
|
@ -8,168 +8,26 @@ const format = require('../../../../common/format')
|
||||||
const moment = require('moment')
|
const moment = require('moment')
|
||||||
const request = require('../../../../common/request')
|
const request = require('../../../../common/request')
|
||||||
const serialize = require('form-serialize')
|
const serialize = require('form-serialize')
|
||||||
const flyd_filter = require('flyd/module/filter')
|
|
||||||
const flyd_flatMap = require('flyd/module/flatmap')
|
const flyd_flatMap = require('flyd/module/flatmap')
|
||||||
const flyd_mergeAll = require('flyd/module/mergeall')
|
const flyd_mergeAll = require('flyd/module/mergeall')
|
||||||
|
|
||||||
var rootUrl = `/nonprofits/${app.nonprofit_id}`
|
|
||||||
|
|
||||||
const getFundraisers = type => {
|
|
||||||
var response$ = request({
|
|
||||||
method: 'get'
|
|
||||||
, path: `${rootUrl}/${type}/name_and_id`
|
|
||||||
}).load
|
|
||||||
|
|
||||||
var body$ = R.compose(
|
|
||||||
flyd.map(x => x.body)
|
|
||||||
, flyd_filter(x => x.status === 200 || x.status === 304)
|
|
||||||
)(response$)
|
|
||||||
|
|
||||||
return flyd.merge(flyd.stream([]), body$)
|
|
||||||
}
|
|
||||||
|
|
||||||
function init(parentState) {
|
function init(parentState) {
|
||||||
var state = {
|
var state = {
|
||||||
submit$: flyd.stream()
|
submit$: flyd.stream()
|
||||||
, supporter$: parentState.supporter$
|
, supporter$: parentState.supporter$
|
||||||
, campaigns$: getFundraisers('campaigns')
|
, saved$: flyd.stream()
|
||||||
, events$: getFundraisers('events')
|
|
||||||
}
|
}
|
||||||
const resp$ = flyd_flatMap(
|
|
||||||
form => request({
|
|
||||||
method: 'post'
|
|
||||||
, path: `${rootUrl}/donations/create_offsite`
|
|
||||||
, send: {donation: setDefaults(serialize(form, {hash: true}))}
|
|
||||||
}).load
|
|
||||||
, state.submit$ )
|
|
||||||
state.saved$ = flyd_filter(resp => resp.status === 200, resp$)
|
|
||||||
state.error$ = flyd_filter(resp => resp.status !== 200, resp$)
|
|
||||||
state.loading$ = flyd_mergeAll([
|
|
||||||
flyd.map(()=> true, state.submit$)
|
|
||||||
, flyd.map(() => false, resp$)
|
|
||||||
])
|
|
||||||
return state
|
return state
|
||||||
}
|
}
|
||||||
|
|
||||||
const setDefaults = formData =>
|
|
||||||
R.evolve({
|
|
||||||
amount: format.dollarsToCents
|
|
||||||
, date: d => moment(d).format("YYYY-MM-DD")
|
|
||||||
}, formData)
|
|
||||||
|
|
||||||
function view(state) {
|
function view(state) {
|
||||||
var body = form(state)
|
|
||||||
|
return h('div', {id$: 'offsite_donation_form_modal'})
|
||||||
return h('div', [
|
|
||||||
modal({
|
|
||||||
id$: state.modalID$
|
|
||||||
, thisID: 'newOffsiteDonationModal'
|
|
||||||
, title: 'New Offsite Contribution'
|
|
||||||
, body
|
|
||||||
})
|
|
||||||
])
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const form = state => {
|
|
||||||
return h('form', {
|
|
||||||
on: {submit: ev => {ev.preventDefault(); state.submit$(ev.currentTarget)}}
|
|
||||||
}, [
|
|
||||||
h('input', {
|
|
||||||
props: {
|
|
||||||
type: 'hidden'
|
|
||||||
, name: 'nonprofit_id'
|
|
||||||
, value: app.nonprofit_id
|
|
||||||
}
|
|
||||||
})
|
|
||||||
, h('input', {
|
|
||||||
props: {
|
|
||||||
type: 'hidden'
|
|
||||||
, name: 'supporter_id'
|
|
||||||
, value: state.supporter$().id
|
|
||||||
}
|
|
||||||
})
|
|
||||||
, h('div.layout--four', [
|
|
||||||
h('fieldset', [
|
|
||||||
h('label', 'Amount')
|
|
||||||
, h('div.prepend--dollar', [
|
|
||||||
h('input', {
|
|
||||||
props: {
|
|
||||||
name: 'amount'
|
|
||||||
, step: 'any'
|
|
||||||
, type: 'number'
|
|
||||||
, min: 0
|
|
||||||
, required: true
|
|
||||||
}
|
|
||||||
})
|
|
||||||
])
|
|
||||||
])
|
|
||||||
, h('fieldset', [
|
|
||||||
h('label', 'Date')
|
|
||||||
, h('input', {
|
|
||||||
props: {
|
|
||||||
id: 'js-offsiteDonationDate'
|
|
||||||
, name: 'date'
|
|
||||||
, type: 'text'
|
|
||||||
, placeholder: 'MM/DD/YYYY'
|
|
||||||
}
|
|
||||||
})
|
|
||||||
])
|
|
||||||
, h('fieldset', [
|
|
||||||
h('label', 'Type')
|
|
||||||
, h('select', {props: {name: 'offsite_payment[kind]'}}, [
|
|
||||||
h('option', {props: {selected: true, value: 'check'}}, 'Check')
|
|
||||||
, h('option', {props: {value: 'cash'}}, 'Cash')
|
|
||||||
, h('option', {props: {value: ''}}, 'Other')
|
|
||||||
])
|
|
||||||
])
|
|
||||||
, h('fieldset', [
|
|
||||||
h('label', 'Check Number')
|
|
||||||
, h('input', {
|
|
||||||
props: {
|
|
||||||
name: 'offsite_payment[check_number]'
|
|
||||||
, type: 'text'
|
|
||||||
, placeholder: '1234'
|
|
||||||
}
|
|
||||||
})
|
|
||||||
])
|
|
||||||
])
|
|
||||||
, h('div.layout--two', [
|
|
||||||
h('fieldset', [
|
|
||||||
h('label', ['Towards an Event', h('small', ' (optional) ')])
|
|
||||||
, fundraiserSelects('event', state.events$())
|
|
||||||
])
|
|
||||||
, h('fieldset', [
|
|
||||||
h('label', ['Towards a Campaign ', h('small', ' (optional) ')])
|
|
||||||
, fundraiserSelects('campaign', state.campaigns$())
|
|
||||||
])
|
|
||||||
])
|
|
||||||
, h('div.layout--two', [
|
|
||||||
h('fieldset', [
|
|
||||||
h('label', ['In Memory/Honor Of ', h('small', ' (optional) ')])
|
|
||||||
, h('textarea', {props: {rows: 3, name: 'dedication', placeholder: 'In Memory/Honor Of'}})
|
|
||||||
])
|
|
||||||
, h('fieldset', [
|
|
||||||
h('label', ['Designation ', h('small', ' (optional) ')])
|
|
||||||
, h('textarea', {props: {rows: 3, name: 'designation', placeholder: 'Designation'}})
|
|
||||||
])
|
|
||||||
])
|
|
||||||
, h('fieldset', [
|
|
||||||
h('label', ['Notes ', h('small', ' (optional) ')])
|
|
||||||
, h('textarea', {props: {rows: 3, name: 'comment', placeholder: 'Notes'}})
|
|
||||||
])
|
|
||||||
, h('div.u-centered', [
|
|
||||||
button({loading$: state.loading$, error$: state.error$})
|
|
||||||
])
|
|
||||||
])
|
|
||||||
}
|
|
||||||
|
|
||||||
const fundraiserSelects = (type, arr) =>
|
|
||||||
h('select', {props: {name: `${type}_id`}}
|
|
||||||
, R.concat(
|
|
||||||
[h('option', {props: {value: ''}}, 'Select One')]
|
|
||||||
, R.map(x => h('option', {props: {value: x.id}}, x.name), arr)
|
|
||||||
)
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
module.exports = {init, view}
|
module.exports = {init, view}
|
||||||
|
|
|
@ -5,6 +5,7 @@ const flyd = require('flyd')
|
||||||
const h = require('snabbdom/h')
|
const h = require('snabbdom/h')
|
||||||
flyd.mergeAll = require('flyd/module/mergeall')
|
flyd.mergeAll = require('flyd/module/mergeall')
|
||||||
|
|
||||||
|
|
||||||
const button = (text, stream) =>
|
const button = (text, stream) =>
|
||||||
h('button.button--tiny.u-marginRight--10', {on: {click: stream}}
|
h('button.button--tiny.u-marginRight--10', {on: {click: stream}}
|
||||||
, [h('i.fa.fa-plus.u-marginRight--5') , text ])
|
, [h('i.fa.fa-plus.u-marginRight--5') , text ])
|
||||||
|
@ -13,8 +14,12 @@ const view = state =>
|
||||||
h('section.timeline-actions.u-padding--10', [
|
h('section.timeline-actions.u-padding--10', [
|
||||||
button('Note', state.newNote$)
|
button('Note', state.newNote$)
|
||||||
, button('Email', state.clickComposing$)
|
, button('Email', state.clickComposing$)
|
||||||
, button('Donation', [state.modalID$, 'newOffsiteDonationModal'])
|
, button('Donation', () => appl.open_donation_modal(state.supporter$().id,
|
||||||
])
|
() => {state.offsiteDonationForm.saved$(Math.random())}
|
||||||
|
)
|
||||||
|
)
|
||||||
|
]
|
||||||
|
)
|
||||||
|
|
||||||
module.exports = {view}
|
module.exports = {view}
|
||||||
|
|
||||||
|
|
|
@ -1,3 +0,0 @@
|
||||||
// License: LGPL-3.0-or-later
|
|
||||||
require('../common/onboard')
|
|
||||||
|
|
|
@ -1,3 +0,0 @@
|
||||||
// License: LGPL-3.0-or-later
|
|
||||||
require('../common/onboard')
|
|
||||||
|
|
|
@ -27,3 +27,6 @@ intntl:
|
||||||
symbol: "€"
|
symbol: "€"
|
||||||
abbv: "eur"
|
abbv: "eur"
|
||||||
format: "%n%u"
|
format: "%n%u"
|
||||||
|
|
||||||
|
api_domain:
|
||||||
|
url: "http://localhost:5000"
|
||||||
|
|
|
@ -284,6 +284,11 @@ Config.schema do
|
||||||
required(:url).filled?(:str)
|
required(:url).filled?(:str)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
# the domain for your api. Usually will be your CDN.url
|
||||||
|
optional(:api_domain).schema do
|
||||||
|
required(:url).filled?(:str)
|
||||||
|
end
|
||||||
|
|
||||||
end
|
end
|
||||||
|
|
||||||
Settings.reload!
|
Settings.reload!
|
||||||
|
|
|
@ -10,6 +10,8 @@ en:
|
||||||
body: 'Comment content'
|
body: 'Comment content'
|
||||||
organization:
|
organization:
|
||||||
name: "Organisation"
|
name: "Organisation"
|
||||||
|
organization_page:
|
||||||
|
promote: "Promote this organization"
|
||||||
donation:
|
donation:
|
||||||
amount: "Total Amount"
|
amount: "Total Amount"
|
||||||
date: "Transaction Date"
|
date: "Transaction Date"
|
||||||
|
@ -29,6 +31,7 @@ en:
|
||||||
transfer_label_html: "<strong>Donation %{nonprofit_statement}</strong>."
|
transfer_label_html: "<strong>Donation %{nonprofit_statement}</strong>."
|
||||||
oneoff_donation_html: "Your donation towards <strong>%{nonprofit_name}</strong> was successful!"
|
oneoff_donation_html: "Your donation towards <strong>%{nonprofit_name}</strong> was successful!"
|
||||||
recurring_donation_html: "Your recurring donation towards <strong>%{nonprofit_name}</strong>, started on %{start_date}, has been successfully paid."
|
recurring_donation_html: "Your recurring donation towards <strong>%{nonprofit_name}</strong>, 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: <a href=\"%{management_url}\">%{management_url}</a>"
|
||||||
donor_direct_debit_notification:
|
donor_direct_debit_notification:
|
||||||
subject: "Donation receipt for %{nonprofit_name}"
|
subject: "Donation receipt for %{nonprofit_name}"
|
||||||
transfer_info_html: "This transfer will appear on your bank statement as %{label}"
|
transfer_info_html: "This transfer will appear on your bank statement as %{label}"
|
||||||
|
|
|
@ -73,3 +73,6 @@ source_tokens:
|
||||||
|
|
||||||
nonprofits_must_be_vetted: false
|
nonprofits_must_be_vetted: false
|
||||||
|
|
||||||
|
api_domain:
|
||||||
|
url: "http://localhost:5000"
|
||||||
|
|
||||||
|
|
30
db/migrate/20181002160627_correct_dedications.rb
Normal file
30
db/migrate/20181002160627_correct_dedications.rb
Normal file
|
@ -0,0 +1,30 @@
|
||||||
|
class CorrectDedications < ActiveRecord::Migration
|
||||||
|
def up
|
||||||
|
execute <<-SQL
|
||||||
|
create or replace function is_valid_json(p_json text)
|
||||||
|
returns boolean
|
||||||
|
as
|
||||||
|
$$
|
||||||
|
begin
|
||||||
|
return (p_json::json is not null);
|
||||||
|
exception
|
||||||
|
when others then
|
||||||
|
return false;
|
||||||
|
end;
|
||||||
|
$$
|
||||||
|
language plpgsql
|
||||||
|
immutable;
|
||||||
|
SQL
|
||||||
|
|
||||||
|
dedications = MaintainDedications.retrieve_non_json_dedications
|
||||||
|
|
||||||
|
MaintainDedications.create_json_dedications_from_plain_text(dedications)
|
||||||
|
|
||||||
|
dedications = MaintainDedications.retrieve_json_dedications
|
||||||
|
MaintainDedications.add_honor_to_any_json_dedications_without_type(dedications)
|
||||||
|
end
|
||||||
|
|
||||||
|
def down
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
52
db/migrate/20181003212559_correct_dedication_contacts.rb
Normal file
52
db/migrate/20181003212559_correct_dedication_contacts.rb
Normal file
|
@ -0,0 +1,52 @@
|
||||||
|
class CorrectDedicationContacts < ActiveRecord::Migration
|
||||||
|
def up
|
||||||
|
json_dedications = Qx.select('id', 'dedication').from(:donations)
|
||||||
|
.where("dedication IS NOT NULL AND dedication != ''")
|
||||||
|
.and_where("is_valid_json(dedication)").ex
|
||||||
|
parsed_dedications = json_dedications.map{|i| {id: i['id'], dedication: JSON.parse(i['dedication'])}}
|
||||||
|
with_contact_to_correct = parsed_dedications.select {|i| !i[:dedication]['contact'].blank? && i[:dedication]['contact'].is_a?(String)}
|
||||||
|
really_icky_dedications, easy_to_split_strings = with_contact_to_correct.partition{|i| i[:dedication]['contact'].split(" - ").count > 3}
|
||||||
|
|
||||||
|
easy_to_split_strings.map do |i|
|
||||||
|
split_contact = i[:dedication]['contact'].split(' - ')
|
||||||
|
i[:dedication]['contact'] = {
|
||||||
|
email: split_contact[0],
|
||||||
|
phone:split_contact[1],
|
||||||
|
address: split_contact[2],
|
||||||
|
}
|
||||||
|
puts i
|
||||||
|
i
|
||||||
|
end.each_with_index do |i, index|
|
||||||
|
unless (i[:id])
|
||||||
|
raise Error("Item at index #{index} is invalid. Object:#{i}")
|
||||||
|
end
|
||||||
|
Qx.update(:donations).where("id = $id", id:i[:id]).set(dedication: JSON.generate(i[:dedication])).ex
|
||||||
|
end
|
||||||
|
|
||||||
|
puts "Corrected #{easy_to_split_strings.count} records."
|
||||||
|
|
||||||
|
puts ""
|
||||||
|
puts ""
|
||||||
|
puts "You must manually fix the following dedications: "
|
||||||
|
really_icky_dedications.each do |i|
|
||||||
|
puts i
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def down
|
||||||
|
json_dedications = Qx.select('id', 'dedication').from(:donations)
|
||||||
|
.where("dedication IS NOT NULL AND dedication != ''")
|
||||||
|
.and_where("is_valid_json(dedication)").ex
|
||||||
|
|
||||||
|
parsed_dedications = json_dedications.map{|i| {'id' => i['id'], 'dedication' => JSON.parse(i['dedication'])}}
|
||||||
|
|
||||||
|
with_contact_to_correct = parsed_dedications.select {|i| i['dedication']['contact'].is_a?(Hash)}
|
||||||
|
|
||||||
|
puts "#{with_contact_to_correct.count} to revert"
|
||||||
|
with_contact_to_correct.each do |i|
|
||||||
|
contact_string = "#{i['dedication']['contact']['email']} - #{i['dedication']['contact']['phone']} - #{i['dedication']['contact']['address']}"
|
||||||
|
i['dedication']['contact'] = contact_string
|
||||||
|
Qx.update(:donations).where("id = $id", id:i['id']).set(dedication: JSON.generate(i['dedication'])).ex
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
|
@ -57,6 +57,22 @@ CREATE EXTENSION IF NOT EXISTS "uuid-ossp" WITH SCHEMA public;
|
||||||
COMMENT ON EXTENSION "uuid-ossp" IS 'generate universally unique identifiers (UUIDs)';
|
COMMENT ON EXTENSION "uuid-ossp" IS 'generate universally unique identifiers (UUIDs)';
|
||||||
|
|
||||||
|
|
||||||
|
--
|
||||||
|
-- Name: is_valid_json(text); Type: FUNCTION; Schema: public; Owner: -
|
||||||
|
--
|
||||||
|
|
||||||
|
CREATE FUNCTION public.is_valid_json(p_json text) RETURNS boolean
|
||||||
|
LANGUAGE plpgsql IMMUTABLE
|
||||||
|
AS $$
|
||||||
|
begin
|
||||||
|
return (p_json::json is not null);
|
||||||
|
exception
|
||||||
|
when others then
|
||||||
|
return false;
|
||||||
|
end;
|
||||||
|
$$;
|
||||||
|
|
||||||
|
|
||||||
--
|
--
|
||||||
-- Name: update_supporter_assoc_search_vectors(); Type: FUNCTION; Schema: public; Owner: -
|
-- Name: update_supporter_assoc_search_vectors(); Type: FUNCTION; Schema: public; Owner: -
|
||||||
--
|
--
|
||||||
|
@ -4327,3 +4343,7 @@ INSERT INTO schema_migrations (version) VALUES ('20180713215825');
|
||||||
|
|
||||||
INSERT INTO schema_migrations (version) VALUES ('20180713220028');
|
INSERT INTO schema_migrations (version) VALUES ('20180713220028');
|
||||||
|
|
||||||
|
INSERT INTO schema_migrations (version) VALUES ('20181002160627');
|
||||||
|
|
||||||
|
INSERT INTO schema_migrations (version) VALUES ('20181003212559');
|
||||||
|
|
||||||
|
|
|
@ -13,4 +13,5 @@ WORKDIR /myapp
|
||||||
COPY Gemfile /myapp/Gemfile
|
COPY Gemfile /myapp/Gemfile
|
||||||
COPY Gemfile.lock /myapp/Gemfile.lock
|
COPY Gemfile.lock /myapp/Gemfile.lock
|
||||||
RUN bundle install
|
RUN bundle install
|
||||||
CMD
|
CMD rake db:create db:structure:load test:prepare && rake spec && npx jest && npm run build-all
|
||||||
|
|
||||||
|
|
33
javascripts/app/create_new_offsite_payment_pane.tsx
Normal file
33
javascripts/app/create_new_offsite_payment_pane.tsx
Normal file
|
@ -0,0 +1,33 @@
|
||||||
|
// 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 CreateOffsitePaymentPane from "../src/components/create_offsite_payment_pane/CreateOffsitePaymentPane"
|
||||||
|
|
||||||
|
import * as ReactDOM from 'react-dom'
|
||||||
|
import * as React from 'react'
|
||||||
|
|
||||||
|
export interface FundraiserInfo {
|
||||||
|
id: number
|
||||||
|
name: string
|
||||||
|
}
|
||||||
|
|
||||||
|
function LoadReactPage(element:HTMLElement, events: FundraiserInfo[],
|
||||||
|
campaigns: FundraiserInfo[],
|
||||||
|
nonprofitId: number,
|
||||||
|
supporterId:number,
|
||||||
|
preupdateDonationAction:() => void,
|
||||||
|
postUpdateSuccess: () => void,
|
||||||
|
|
||||||
|
//from ModalProps
|
||||||
|
onClose: () => void,
|
||||||
|
modalActive: boolean,
|
||||||
|
nonprofitTimezone?: string) {
|
||||||
|
ReactDOM.render(<Root><CreateOffsitePaymentPane campaigns={campaigns}
|
||||||
|
events={events} onClose={onClose}
|
||||||
|
modalActive={modalActive} nonprofitTimezone={nonprofitTimezone}
|
||||||
|
postUpdateSuccess={postUpdateSuccess}
|
||||||
|
preupdateDonationAction={preupdateDonationAction} nonprofitId={nonprofitId} supporterId={supporterId}/></Root>, element)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
(window as any).LoadReactCreateOffsiteDonationPane = LoadReactPage
|
27
javascripts/app/edit_payment_pane.tsx
Normal file
27
javascripts/app/edit_payment_pane.tsx
Normal file
|
@ -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(<Root><EditPaymentPane data={data} campaigns={campaigns}
|
||||||
|
events={events} onClose={onClose}
|
||||||
|
modalActive={modalActive} nonprofitTimezone={nonprofitTimezone}
|
||||||
|
postUpdateSuccess={postUpdateSuccess}
|
||||||
|
preupdateDonationAction={preupdateDonationAction}
|
||||||
|
/></Root>, element)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
(window as any).LoadReactEditPaymentPane = LoadReactPage
|
28
javascripts/src/components/common/Modal.spec.tsx
Normal file
28
javascripts/src/components/common/Modal.spec.tsx
Normal file
|
@ -0,0 +1,28 @@
|
||||||
|
// License: LGPL-3.0-or-later
|
||||||
|
import * as React from 'react';
|
||||||
|
import 'jest';
|
||||||
|
import Modal, {ModalProps} from './Modal'
|
||||||
|
import {shallow} from "enzyme";
|
||||||
|
import {toJS} from "mobx";
|
||||||
|
import toJson from "enzyme-to-json";
|
||||||
|
|
||||||
|
describe('Modal', () => {
|
||||||
|
test('nothing displayed if inactive', () => {
|
||||||
|
let modal = shallow(<Modal childGenerator={() => <div/>}/>)
|
||||||
|
|
||||||
|
expect(toJson(modal)).toMatchSnapshot()
|
||||||
|
})
|
||||||
|
|
||||||
|
test('active modal displays', () => {
|
||||||
|
let onCloseWasCalled = false
|
||||||
|
let modal = shallow(<Modal titleText={"title text"}
|
||||||
|
focusDialog={true}
|
||||||
|
modalActive={true}
|
||||||
|
onClose={() => { onCloseWasCalled = true}}
|
||||||
|
childGenerator={() => <div/>}/>)
|
||||||
|
expect(toJson(modal)).toMatchSnapshot()
|
||||||
|
let modalComponent = modal.instance() as React.Component<ModalProps, {}> //casting to modal didn't work for reasons?
|
||||||
|
modalComponent.props.onClose()
|
||||||
|
expect(onCloseWasCalled).toBeTruthy()
|
||||||
|
})
|
||||||
|
})
|
41
javascripts/src/components/common/Modal.tsx
Normal file
41
javascripts/src/components/common/Modal.tsx
Normal file
|
@ -0,0 +1,41 @@
|
||||||
|
// License: LGPL-3.0-or-later
|
||||||
|
import * as React from 'react';
|
||||||
|
import { observer } from 'mobx-react';
|
||||||
|
import AriaModal = require('react-aria-modal');
|
||||||
|
|
||||||
|
export interface ModalProps
|
||||||
|
{
|
||||||
|
onClose?: () => void // if you want your modal to close, this needs to set modalActive to false
|
||||||
|
modalActive?: boolean
|
||||||
|
titleText?: string
|
||||||
|
focusDialog?:boolean
|
||||||
|
dialogStyle?:any
|
||||||
|
childGenerator:() => any
|
||||||
|
}
|
||||||
|
|
||||||
|
class Modal extends React.Component<ModalProps, {}> {
|
||||||
|
|
||||||
|
static defaultProps = {
|
||||||
|
dialogStyle: {minWidth:'768px'}
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
const modal = this.props.modalActive ?
|
||||||
|
<AriaModal mounted={this.props.modalActive} titleText={this.props.titleText} focusDialog={this.props.focusDialog}
|
||||||
|
onExit={this.props.onClose} dialogStyle={this.props.dialogStyle}>
|
||||||
|
<header className='modal-header'>
|
||||||
|
<h4 className='modal-header-title'>{this.props.titleText}</h4>
|
||||||
|
</header>
|
||||||
|
<div className="modal-body">
|
||||||
|
{this.props.childGenerator()}
|
||||||
|
</div>
|
||||||
|
</AriaModal> : false;
|
||||||
|
|
||||||
|
return modal
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default observer(Modal)
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -30,7 +30,7 @@ export default class StandardFieldComponent extends React.Component<StandardFiel
|
||||||
let stickyErrorDiv = this.props.inStickyError ? <div className="help-block" role="alert">{stickyErrorMessage}</div> : ""
|
let stickyErrorDiv = this.props.inStickyError ? <div className="help-block" role="alert">{stickyErrorMessage}</div> : ""
|
||||||
|
|
||||||
return <div>
|
return <div>
|
||||||
{this.renderChildren()}
|
{this.props.children}
|
||||||
{errorDiv}
|
{errorDiv}
|
||||||
{stickyErrorDiv }
|
{stickyErrorDiv }
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -0,0 +1,32 @@
|
||||||
|
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||||
|
|
||||||
|
exports[`Modal active modal displays 1`] = `
|
||||||
|
<Displaced
|
||||||
|
dialogStyle={
|
||||||
|
Object {
|
||||||
|
"minWidth": "768px",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
focusDialog={true}
|
||||||
|
mounted={true}
|
||||||
|
onExit={[Function]}
|
||||||
|
titleText="title text"
|
||||||
|
>
|
||||||
|
<header
|
||||||
|
className="modal-header"
|
||||||
|
>
|
||||||
|
<h4
|
||||||
|
className="modal-header-title"
|
||||||
|
>
|
||||||
|
title text
|
||||||
|
</h4>
|
||||||
|
</header>
|
||||||
|
<div
|
||||||
|
className="modal-body"
|
||||||
|
>
|
||||||
|
<div />
|
||||||
|
</div>
|
||||||
|
</Displaced>
|
||||||
|
`;
|
||||||
|
|
||||||
|
exports[`Modal nothing displayed if inactive 1`] = `""`;
|
|
@ -2,10 +2,7 @@
|
||||||
|
|
||||||
exports[`StandardFieldComponent sets error message properly 1`] = `
|
exports[`StandardFieldComponent sets error message properly 1`] = `
|
||||||
<div>
|
<div>
|
||||||
<input
|
<input />
|
||||||
className="form-control"
|
|
||||||
key=".0"
|
|
||||||
/>
|
|
||||||
<div
|
<div
|
||||||
className="help-block"
|
className="help-block"
|
||||||
role="alert"
|
role="alert"
|
||||||
|
@ -17,10 +14,7 @@ exports[`StandardFieldComponent sets error message properly 1`] = `
|
||||||
|
|
||||||
exports[`StandardFieldComponent works with a child 1`] = `
|
exports[`StandardFieldComponent works with a child 1`] = `
|
||||||
<div>
|
<div>
|
||||||
<input
|
<input />
|
||||||
className="form-control"
|
|
||||||
key=".0"
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
`;
|
`;
|
||||||
|
|
||||||
|
|
|
@ -3,18 +3,69 @@ import * as React from 'react';
|
||||||
import {observer} from "mobx-react";
|
import {observer} from "mobx-react";
|
||||||
import {Field} from "../../../../types/mobx-react-form";
|
import {Field} from "../../../../types/mobx-react-form";
|
||||||
import LabeledFieldComponent from "./LabeledFieldComponent";
|
import LabeledFieldComponent from "./LabeledFieldComponent";
|
||||||
import {injectIntl, InjectedIntl} from 'react-intl';
|
|
||||||
import {HoudiniField} from "../../lib/houdini_form";
|
import {HoudiniField} from "../../lib/houdini_form";
|
||||||
import ReactInput from "./form/ReactInput";
|
import ReactInput from "./form/ReactInput";
|
||||||
|
import ReactSelect from './form/ReactSelect';
|
||||||
|
import ReactTextarea from "./form/ReactTextarea";
|
||||||
|
import ReactMaskedInput from "./form/ReactMaskedInput";
|
||||||
|
import createNumberMask from "../../lib/createNumberMask";
|
||||||
|
|
||||||
|
|
||||||
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
|
let field = props.field as HoudiniField
|
||||||
return <LabeledFieldComponent
|
return <LabeledFieldComponent
|
||||||
inputId={props.field.id} labelText={field.label} inError={field.hasError} error={field.error}
|
inputId={props.field.id} labelText={field.label} inError={field.hasError} error={field.error}
|
||||||
inStickyError={field.hasServerError} stickyError={field.serverError}
|
inStickyError={field.hasServerError} stickyError={field.serverError}
|
||||||
className={props.wrapperClassName} >
|
className={props.wrapperClassName} >
|
||||||
|
<ReactInput field={field} label={props.label} placeholder={props.placeholder} className={`form-control ${props.inputClassNames || ''}`}/>
|
||||||
<ReactInput field={field} label={props.label} placeholder={props.placeholder} className="form-control"/>
|
|
||||||
</LabeledFieldComponent>
|
</LabeledFieldComponent>
|
||||||
})
|
})
|
||||||
|
|
||||||
|
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 <LabeledFieldComponent
|
||||||
|
inputId={props.field.id} labelText={field.label} inError={field.hasError} error={field.error}
|
||||||
|
inStickyError={field.hasServerError} stickyError={field.serverError}
|
||||||
|
className={props.wrapperClassName} >
|
||||||
|
|
||||||
|
<ReactSelect field={field} label={props.label} placeholder={props.placeholder} className={`form-control ${props.inputClassNames}`} options={props.options}/>
|
||||||
|
|
||||||
|
</LabeledFieldComponent>
|
||||||
|
})
|
||||||
|
|
||||||
|
export const TextareaField = observer((props:{field:Field, placeholder?:string, label?:string, wrapperClassName?:string, inputClassNames?:string, rows?:number}) =>{
|
||||||
|
let field = props.field as HoudiniField
|
||||||
|
return <LabeledFieldComponent
|
||||||
|
inputId={props.field.id} labelText={field.label} inError={field.hasError} error={field.error}
|
||||||
|
inStickyError={field.hasServerError} stickyError={field.serverError}
|
||||||
|
className={props.wrapperClassName} >
|
||||||
|
|
||||||
|
<ReactTextarea field={field} label={props.label} placeholder={props.placeholder} className={`form-control ${props.inputClassNames}`} rows={props.rows}/>
|
||||||
|
|
||||||
|
</LabeledFieldComponent>
|
||||||
|
})
|
||||||
|
|
||||||
|
export const CurrencyField = observer((props:{field:Field,placeholder?:string, label?:string, currencySymbol?:string, wrapperClassName?:string, inputClassNames?:string, mustBeNegative?:boolean, allowNegative?:boolean}) => {
|
||||||
|
let field = props.field as HoudiniField
|
||||||
|
let currencySymbol = props.mustBeNegative ? "-$" : "$"
|
||||||
|
let allowNegative = props.allowNegative || !props.mustBeNegative
|
||||||
|
return <LabeledFieldComponent
|
||||||
|
inputId={props.field.id} labelText={field.label} inError={field.hasError} error={field.error}
|
||||||
|
inStickyError={field.hasServerError} stickyError={field.serverError}
|
||||||
|
className={props.wrapperClassName} >
|
||||||
|
|
||||||
|
<ReactMaskedInput field={field} label={props.label} placeholder={props.placeholder}
|
||||||
|
className={`form-control ${props.inputClassNames}`} guide={true}
|
||||||
|
mask={createNumberMask({allowDecimal:true,
|
||||||
|
requireDecimal:true,
|
||||||
|
prefix:currencySymbol,
|
||||||
|
allowNegative:allowNegative,
|
||||||
|
fixedDecimalScale:true
|
||||||
|
})}
|
||||||
|
showMask={true} placeholderChar={'0'}
|
||||||
|
/>
|
||||||
|
|
||||||
|
</LabeledFieldComponent>
|
||||||
|
|
||||||
|
|
||||||
|
});
|
|
@ -6,35 +6,36 @@ import {Form} from "mobx-react-form";
|
||||||
import {mount} from 'enzyme';
|
import {mount} from 'enzyme';
|
||||||
import {toJS, observable, action, runInAction} from 'mobx';
|
import {toJS, observable, action, runInAction} from 'mobx';
|
||||||
import {observer} from 'mobx-react';
|
import {observer} from 'mobx-react';
|
||||||
import {InputHTMLAttributes} from 'react';
|
|
||||||
import {ReactForm} from "./ReactForm";
|
import {ReactForm} from "./ReactForm";
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@observer
|
@observer
|
||||||
class TestChange extends React.Component{
|
class TestChange extends React.Component {
|
||||||
@observable
|
@observable
|
||||||
remove:boolean
|
remove: boolean
|
||||||
@observable
|
@observable
|
||||||
form: Form
|
form: Form
|
||||||
|
|
||||||
@action.bound
|
@action.bound
|
||||||
componentWillMount(){
|
componentWillMount() {
|
||||||
this.form = new Form({fields:[{
|
this.form = new Form({
|
||||||
name: 'name',
|
fields: [{
|
||||||
extra: null}
|
name: 'name',
|
||||||
]})
|
extra: null
|
||||||
|
}
|
||||||
|
]
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@action.bound
|
@action.bound
|
||||||
onClick(){
|
onClick() {
|
||||||
this.remove = true
|
this.remove = true
|
||||||
}
|
}
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
let reactInput = !this.remove ? <ReactInput field={this.form.$('name')} label={'label1'} placeholder={"holder"}>
|
let reactInput = !this.remove ? <ReactInput field={this.form.$('name')} label={'label1'} placeholder={"holder"}>
|
||||||
{this.props.children}
|
|
||||||
</ReactInput> : undefined
|
</ReactInput> : undefined
|
||||||
|
|
||||||
return <ReactForm form={this.form}>
|
return <ReactForm form={this.form}>
|
||||||
|
@ -45,18 +46,6 @@ class TestChange extends React.Component{
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
class WrappedInput extends React.Component<InputHTMLAttributes<HTMLInputElement>>{
|
|
||||||
|
|
||||||
render(){
|
|
||||||
let notChildren = {...this.props}
|
|
||||||
delete notChildren.children
|
|
||||||
return <div>
|
|
||||||
<input {...notChildren} />
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
describe('ReactInput', () => {
|
describe('ReactInput', () => {
|
||||||
|
|
||||||
let form: Form
|
let form: Form
|
||||||
|
@ -71,94 +60,48 @@ describe('ReactInput', () => {
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
describe('no children passed in', () => {
|
test('gets added properly', () => {
|
||||||
test('gets added properly', () => {
|
let res = mount(<ReactForm form={form}>
|
||||||
let res = mount(<ReactForm form={form}>
|
<ReactInput field={form.$('name')} label={"label"}
|
||||||
<ReactInput field={form.$('name')} label={"label"}
|
placeholder={"holder"} value={'snapshot'} aria-required={true}/>
|
||||||
placeholder={"holder"} value={'snapshot'} aria-required={true}/>
|
|
||||||
|
|
||||||
</ReactForm>)
|
</ReactForm>)
|
||||||
|
|
||||||
|
|
||||||
//Did the attributes settings work as expected back to the objects
|
//Did the attributes settings work as expected back to the objects
|
||||||
expect(form.$('name').label).toEqual('label')
|
expect(form.$('name').label).toEqual('label')
|
||||||
expect(form.$('name').placeholder).toEqual('holder')
|
expect(form.$('name').placeholder).toEqual('holder')
|
||||||
expect(form.$('name').value).toEqual('')
|
expect(form.$('name').value).toEqual('')
|
||||||
|
|
||||||
//is the aria attribute passted through to the input
|
//is the aria attribute passted through to the input
|
||||||
let input = res.find('input')
|
let input = res.find('input')
|
||||||
expect(input.prop('aria-required')).toEqual(true)
|
expect(input.prop('aria-required')).toEqual(true)
|
||||||
|
|
||||||
|
|
||||||
// is the input properly bound?
|
// is the input properly bound?
|
||||||
input.simulate('change', {target: { value: 'something' } })
|
input.simulate('change', {target: {value: 'something'}})
|
||||||
expect(form.$('name').value).toEqual('something')
|
expect(form.$('name').value).toEqual('something')
|
||||||
})
|
|
||||||
|
|
||||||
test('gets removed properly', () => {
|
|
||||||
|
|
||||||
let res = mount(<TestChange/>)
|
|
||||||
|
|
||||||
// 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')
|
|
||||||
})
|
|
||||||
})
|
})
|
||||||
|
|
||||||
describe('children passed in', () => {
|
test('gets removed properly', () => {
|
||||||
test('gets added properly', () => {
|
|
||||||
let res = mount(<ReactForm form={form}>
|
|
||||||
<ReactInput field={form.$('name')} label={"label"}
|
|
||||||
placeholder={"holder"} value={'snapshot'} aria-required={true}>
|
|
||||||
<WrappedInput/>
|
|
||||||
</ReactInput>
|
|
||||||
|
|
||||||
</ReactForm>)
|
let res = mount(<TestChange/>)
|
||||||
|
|
||||||
//Did the attributes settings work as expected back to the objects
|
// The two casts are needed because Typescript was going blowing up without the 'any' first.
|
||||||
expect(form.$('name').label).toEqual('label')
|
// Why was it? *shrugs*
|
||||||
expect(form.$('name').placeholder).toEqual('holder')
|
let f = res.find('ReactForm').instance() as any as ReactForm
|
||||||
expect(form.$('name').value).toEqual('')
|
expect(f.form.size).toEqual(1)
|
||||||
|
|
||||||
//is the aria attribute passted through to the input
|
res.find('input').simulate('change', {target: {value: 'something'}})
|
||||||
let input = res.find('input')
|
|
||||||
expect(input.prop('aria-required')).toEqual(true)
|
|
||||||
|
|
||||||
|
expect(f.form.$('name').value).toEqual('something')
|
||||||
|
|
||||||
// is the input properly bound?
|
res.find('button').simulate('click')
|
||||||
input.simulate('change', {target: { value: 'something' } })
|
expect(f.form.size).toEqual(1)
|
||||||
expect(form.$('name').value).toEqual('something')
|
|
||||||
})
|
|
||||||
|
|
||||||
test('gets removed properly', () => {
|
expect(toJS(res.find('form'))).toMatchSnapshot()
|
||||||
|
|
||||||
let res = mount(<TestChange>
|
|
||||||
<WrappedInput/>
|
|
||||||
</TestChange>)
|
|
||||||
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(f.form.$('name').label).toEqual('label1')
|
||||||
|
expect(f.form.$('name').placeholder).toEqual('holder')
|
||||||
})
|
})
|
||||||
})
|
})
|
|
@ -5,24 +5,18 @@ import {InjectedIntlProps, injectIntl} from 'react-intl';
|
||||||
import {Field} from "mobx-react-form";
|
import {Field} from "mobx-react-form";
|
||||||
import {observable, action, toJS, runInAction} from 'mobx';
|
import {observable, action, toJS, runInAction} from 'mobx';
|
||||||
import {InputHTMLAttributes} from 'react';
|
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<HTMLInputElement>
|
||||||
|
|
||||||
export interface ReactInputProps
|
class ReactInput extends React.Component<InputTypes, {}> {
|
||||||
{
|
|
||||||
field:Field
|
|
||||||
label?:string
|
|
||||||
placeholder?:string
|
|
||||||
}
|
|
||||||
|
|
||||||
function castToNullIfUndef(i:any){
|
constructor(props:InputTypes){
|
||||||
return i === undefined ? null : i
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
class ReactInput extends React.Component<ReactInputProps & InputHTMLAttributes<HTMLInputElement>, {}> {
|
|
||||||
|
|
||||||
constructor(props:ReactInputProps){
|
|
||||||
super(props)
|
super(props)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -43,7 +37,7 @@ class ReactInput extends React.Component<ReactInputProps & InputHTMLAttributes<H
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
componentDidUpdate(prevProps: Readonly<ReactInputProps>, prevState: Readonly<{}>): void {
|
componentDidUpdate(prevProps: Readonly<InputTypes>, prevState: Readonly<{}>): void {
|
||||||
this.updateProps()
|
this.updateProps()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -53,18 +47,9 @@ class ReactInput extends React.Component<ReactInputProps & InputHTMLAttributes<H
|
||||||
this.field.set('placeholder', castToNullIfUndef(this.props.placeholder))
|
this.field.set('placeholder', castToNullIfUndef(this.props.placeholder))
|
||||||
}
|
}
|
||||||
|
|
||||||
@action.bound
|
|
||||||
renderChildren(){
|
|
||||||
let ourProps = this.winnowProps()
|
|
||||||
let elem = React.cloneElement(this.props.children as React.ReactElement<any>,
|
|
||||||
{...ourProps, ...this.field.bind() })
|
|
||||||
return elem
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
///Removes the properties we don't want to put into the input element
|
///Removes the properties we don't want to put into the input element
|
||||||
@action.bound
|
@action.bound
|
||||||
winnowProps(): ReactInputProps & InputHTMLAttributes<HTMLInputElement> {
|
winnowProps(): InputTypes {
|
||||||
let ourProps = {...this.props}
|
let ourProps = {...this.props}
|
||||||
delete ourProps.field
|
delete ourProps.field
|
||||||
delete ourProps.value
|
delete ourProps.value
|
||||||
|
@ -73,14 +58,7 @@ class ReactInput extends React.Component<ReactInputProps & InputHTMLAttributes<H
|
||||||
}
|
}
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
|
|
||||||
if (this.props.children)
|
|
||||||
{
|
|
||||||
return this.renderChildren()
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
return <input {...this.winnowProps()} {...this.field.bind()}/>
|
return <input {...this.winnowProps()} {...this.field.bind()}/>
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
81
javascripts/src/components/common/form/ReactMaskedInput.tsx
Normal file
81
javascripts/src/components/common/form/ReactMaskedInput.tsx
Normal file
|
@ -0,0 +1,81 @@
|
||||||
|
// License: LGPL-3.0-or-later
|
||||||
|
import * as React from 'react';
|
||||||
|
import { observer } from 'mobx-react';
|
||||||
|
import {ReactInputProps} from "./react_input_props";
|
||||||
|
import {InputHTMLAttributes} from "react";
|
||||||
|
import {action, observable} from "mobx";
|
||||||
|
import {Field} from "mobx-react-form";
|
||||||
|
import {castToNullIfUndef} from "../../../lib/utils";
|
||||||
|
import MaskedInput, {maskArray} from "react-text-mask";
|
||||||
|
|
||||||
|
type InputTypes = ReactInputProps &
|
||||||
|
InputHTMLAttributes<HTMLInputElement> & {
|
||||||
|
mask?: maskArray | ((value: string) => maskArray);
|
||||||
|
|
||||||
|
guide?: boolean;
|
||||||
|
|
||||||
|
placeholderChar?: string;
|
||||||
|
|
||||||
|
keepCharPositions?: boolean;
|
||||||
|
|
||||||
|
pipe?: (
|
||||||
|
conformedValue: string,
|
||||||
|
config: any
|
||||||
|
) => false | string | { value: string; indexesOfPipedChars: number[] };
|
||||||
|
|
||||||
|
showMask?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
class ReactMaskedInput extends React.Component<InputTypes, {}> {
|
||||||
|
|
||||||
|
constructor(props:InputTypes){
|
||||||
|
super(props)
|
||||||
|
}
|
||||||
|
|
||||||
|
@observable
|
||||||
|
field:Field
|
||||||
|
|
||||||
|
|
||||||
|
@action.bound
|
||||||
|
componentWillMount(){
|
||||||
|
|
||||||
|
this.field = this.props.field
|
||||||
|
|
||||||
|
|
||||||
|
this.updateProps()
|
||||||
|
}
|
||||||
|
|
||||||
|
componentWillUnmount(){
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
componentDidUpdate(prevProps: Readonly<InputTypes>, 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 <MaskedInput {...this.winnowProps()} {...this.field.bind()}/>
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default observer(ReactMaskedInput)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
112
javascripts/src/components/common/form/ReactSelect.spec.tsx
Normal file
112
javascripts/src/components/common/form/ReactSelect.spec.tsx
Normal file
|
@ -0,0 +1,112 @@
|
||||||
|
// License: LGPL-3.0-or-later
|
||||||
|
import * as React from 'react';
|
||||||
|
import 'jest';
|
||||||
|
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 ? <ReactSelect field={this.form.$('name')} label={'label1'} placeholder={"holder"} options={[{id: null, name:null},
|
||||||
|
{id: 'something', name: "Something"},
|
||||||
|
{id: 'another_value', name: "aonther value"}
|
||||||
|
]}>
|
||||||
|
|
||||||
|
</ReactSelect> : undefined
|
||||||
|
|
||||||
|
return <ReactForm form={this.form}>
|
||||||
|
|
||||||
|
{reactInput}
|
||||||
|
<button onClick={() => this.onClick()}/>
|
||||||
|
</ReactForm>
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
describe('ReactSelect', () => {
|
||||||
|
let form: Form
|
||||||
|
beforeEach(() => {
|
||||||
|
form = new Form({
|
||||||
|
fields: [
|
||||||
|
{
|
||||||
|
name: 'name',
|
||||||
|
extra: null
|
||||||
|
}
|
||||||
|
]
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
test('gets added properly', () => {
|
||||||
|
let res = mount(<ReactForm form={form}>
|
||||||
|
<ReactSelect field={form.$('name')} label={"label"}
|
||||||
|
placeholder={"holder"} value={'snapshot'} aria-required={true}options={[{id: null, name:null},
|
||||||
|
{id: 'something', name: "Something"},
|
||||||
|
{id: 'another_value', name: "aonther value"}
|
||||||
|
]}/>
|
||||||
|
|
||||||
|
</ReactForm>)
|
||||||
|
|
||||||
|
|
||||||
|
//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('select')
|
||||||
|
let options = input.find('option')
|
||||||
|
expect(options.getElements().length).toBe(3)
|
||||||
|
|
||||||
|
|
||||||
|
// is the input properly bound?
|
||||||
|
input.simulate('change', {target: {value: 'something'}})
|
||||||
|
expect(form.$('name').value).toEqual('something')
|
||||||
|
})
|
||||||
|
|
||||||
|
test('gets removed properly', () => {
|
||||||
|
|
||||||
|
let res = mount(<TestChange/>)
|
||||||
|
|
||||||
|
// 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('select').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')
|
||||||
|
})
|
||||||
|
})
|
75
javascripts/src/components/common/form/ReactSelect.tsx
Normal file
75
javascripts/src/components/common/form/ReactSelect.tsx
Normal file
|
@ -0,0 +1,75 @@
|
||||||
|
// 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<HTMLSelectElement>
|
||||||
|
|
||||||
|
class ReactSelect extends React.Component<InputTypes, {}> {
|
||||||
|
|
||||||
|
constructor(props:InputTypes){
|
||||||
|
super(props)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@action.bound
|
||||||
|
componentWillMount(){
|
||||||
|
|
||||||
|
|
||||||
|
this.updateProps()
|
||||||
|
}
|
||||||
|
|
||||||
|
componentWillUnmount(){
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
componentDidUpdate(prevProps: Readonly<InputTypes >, prevState: Readonly<{}>): void {
|
||||||
|
this.updateProps()
|
||||||
|
}
|
||||||
|
|
||||||
|
@action.bound
|
||||||
|
updateProps() {
|
||||||
|
this.props.field.set('label', castToNullIfUndef(this.props.label))
|
||||||
|
this.props.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 <select {...this.winnowProps()} {...this.props.field.bind()}>
|
||||||
|
{ this.props.options ? this.props.options.map(option =>
|
||||||
|
<option key={option.id} value={option.id}>{option.name}</option>
|
||||||
|
) : this.props.children
|
||||||
|
}
|
||||||
|
</select>
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default observer(ReactSelect)
|
||||||
|
|
||||||
|
|
||||||
|
|
102
javascripts/src/components/common/form/ReactTextarea.spec.tsx
Normal file
102
javascripts/src/components/common/form/ReactTextarea.spec.tsx
Normal file
|
@ -0,0 +1,102 @@
|
||||||
|
// License: LGPL-3.0-or-later
|
||||||
|
import * as React from 'react';
|
||||||
|
import 'jest';
|
||||||
|
import {Form} from "mobx-react-form";
|
||||||
|
import {ReactForm} from "./ReactForm";
|
||||||
|
import {action, observable, toJS} from 'mobx';
|
||||||
|
import ReactTextarea from './ReactTextarea';
|
||||||
|
import {observer} from 'mobx-react';
|
||||||
|
import {mount} from 'enzyme';
|
||||||
|
|
||||||
|
|
||||||
|
@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 ? <ReactTextarea field={this.form.$('name')} label={'label1'} placeholder={"holder"} rows={3}>
|
||||||
|
|
||||||
|
</ReactTextarea> : undefined
|
||||||
|
|
||||||
|
return <ReactForm form={this.form}>
|
||||||
|
|
||||||
|
{reactInput}
|
||||||
|
<button onClick={() => this.onClick()}/>
|
||||||
|
</ReactForm>
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
describe('ReactTextarea', () => {
|
||||||
|
let form: Form
|
||||||
|
beforeEach(() => {
|
||||||
|
form = new Form({
|
||||||
|
fields: [
|
||||||
|
{
|
||||||
|
name: 'name',
|
||||||
|
extra: null
|
||||||
|
}
|
||||||
|
]
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
test('gets added properly', () => {
|
||||||
|
let res = mount(<ReactForm form={form}>
|
||||||
|
<ReactTextarea field={form.$('name')} label={"label"}
|
||||||
|
placeholder={"holder"} value={'snapshot'} aria-required={true} rows={3}/>
|
||||||
|
|
||||||
|
</ReactForm>)
|
||||||
|
|
||||||
|
|
||||||
|
//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('textarea')
|
||||||
|
expect(input.prop('aria-required')).toEqual(true)
|
||||||
|
expect(input.prop('rows')).toEqual(3)
|
||||||
|
|
||||||
|
|
||||||
|
// is the input properly bound?
|
||||||
|
input.simulate('change', {target: {value: 'something'}})
|
||||||
|
expect(form.$('name').value).toEqual('something')
|
||||||
|
})
|
||||||
|
|
||||||
|
test('gets removed properly', () => {
|
||||||
|
|
||||||
|
let res = mount(<TestChange/>)
|
||||||
|
|
||||||
|
// 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('textarea').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')
|
||||||
|
})
|
||||||
|
})
|
65
javascripts/src/components/common/form/ReactTextarea.tsx
Normal file
65
javascripts/src/components/common/form/ReactTextarea.tsx
Normal file
|
@ -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<HTMLTextAreaElement>
|
||||||
|
|
||||||
|
|
||||||
|
class ReactTextarea extends React.Component<InputTypes, {}> {
|
||||||
|
|
||||||
|
constructor(props:InputTypes){
|
||||||
|
super(props)
|
||||||
|
}
|
||||||
|
|
||||||
|
@observable
|
||||||
|
field:Field
|
||||||
|
|
||||||
|
|
||||||
|
@action.bound
|
||||||
|
componentWillMount(){
|
||||||
|
|
||||||
|
this.field = this.props.field
|
||||||
|
|
||||||
|
|
||||||
|
this.updateProps()
|
||||||
|
}
|
||||||
|
|
||||||
|
componentWillUnmount(){
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
componentDidUpdate(prevProps: Readonly<InputTypes>, 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 <textarea {...this.winnowProps()} {...this.field.bind()}/>
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default observer(ReactTextarea)
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||||
|
|
||||||
exports[`ReactInput no children passed in gets removed properly 1`] = `
|
exports[`ReactInput gets removed properly 1`] = `
|
||||||
<form>
|
<form>
|
||||||
<button
|
<button
|
||||||
onClick={[Function]}
|
onClick={[Function]}
|
||||||
|
|
|
@ -0,0 +1,9 @@
|
||||||
|
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||||
|
|
||||||
|
exports[`ReactSelect gets removed properly 1`] = `
|
||||||
|
<form>
|
||||||
|
<button
|
||||||
|
onClick={[Function]}
|
||||||
|
/>
|
||||||
|
</form>
|
||||||
|
`;
|
|
@ -0,0 +1,9 @@
|
||||||
|
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||||
|
|
||||||
|
exports[`ReactTextarea gets removed properly 1`] = `
|
||||||
|
<form>
|
||||||
|
<button
|
||||||
|
onClick={[Function]}
|
||||||
|
/>
|
||||||
|
</form>
|
||||||
|
`;
|
|
@ -0,0 +1,9 @@
|
||||||
|
// License: LGPL-3.0-or-later
|
||||||
|
import {Field} from "mobx-react-form";
|
||||||
|
|
||||||
|
export interface ReactInputProps
|
||||||
|
{
|
||||||
|
field:Field
|
||||||
|
label?:string
|
||||||
|
placeholder?:string
|
||||||
|
}
|
|
@ -4,10 +4,10 @@ import {observer} from "mobx-react";
|
||||||
import * as _ from 'lodash'
|
import * as _ from 'lodash'
|
||||||
|
|
||||||
export const TwoColumnFields = observer((props:{children:Array<React.ReactElement<any>>}) => {
|
export const TwoColumnFields = observer((props:{children:Array<React.ReactElement<any>>}) => {
|
||||||
return <div className="clearfix">
|
return <div className="row">
|
||||||
{
|
{
|
||||||
_.take(props.children, 2).map((i:React.ReactElement<any>) => {
|
_.take(props.children, 2).map((i:React.ReactElement<any>) => {
|
||||||
let className = "col-left-6"
|
let className = "col-sm-6"
|
||||||
if (_.last(props.children) !== i){
|
if (_.last(props.children) !== i){
|
||||||
className += " u-paddingRight--10"
|
className += " u-paddingRight--10"
|
||||||
}
|
}
|
||||||
|
@ -21,10 +21,10 @@ export const TwoColumnFields = observer((props:{children:Array<React.ReactElemen
|
||||||
})
|
})
|
||||||
|
|
||||||
export const ThreeColumnFields = observer((props:{children:React.ReactElement<any>[]}) => {
|
export const ThreeColumnFields = observer((props:{children:React.ReactElement<any>[]}) => {
|
||||||
return <div className="clearfix">
|
return <div className="row">
|
||||||
{
|
{
|
||||||
_.take(props.children, 3).map((i:React.ReactElement<any>) => {
|
_.take(props.children, 3).map((i:React.ReactElement<any>) => {
|
||||||
let className = "col-left-4"
|
let className = "col-sm-4"
|
||||||
if (_.last(props.children) !== i){
|
if (_.last(props.children) !== i){
|
||||||
className += " u-paddingRight--10"
|
className += " u-paddingRight--10"
|
||||||
}
|
}
|
||||||
|
|
|
@ -9,7 +9,7 @@ import {TabList} from "./TabList";
|
||||||
import {TabPanel} from "./TabPanel";
|
import {TabPanel} from "./TabPanel";
|
||||||
import {Tab} from "./Tab";
|
import {Tab} from "./Tab";
|
||||||
import toJson from 'enzyme-to-json';
|
import toJson from 'enzyme-to-json';
|
||||||
import {UniqueIdMock} from "../../tests/unique_id_mock";
|
import {UniqueIdMock} from "../../test/unique_id_mock";
|
||||||
import {mountForMobx, runTestsOnConditions, TriggerAndAction} from '../../test/react_test_helpers';
|
import {mountForMobx, runTestsOnConditions, TriggerAndAction} from '../../test/react_test_helpers';
|
||||||
|
|
||||||
let uniqueIdMock = new UniqueIdMock();
|
let uniqueIdMock = new UniqueIdMock();
|
||||||
|
|
|
@ -9,7 +9,7 @@ import {ReactWrapper} from 'enzyme';
|
||||||
import {WizardPanel} from "./WizardPanel";
|
import {WizardPanel} from "./WizardPanel";
|
||||||
|
|
||||||
import {mountForMobxWithIntl, runTestsOnConditions, TriggerAndAction} from "../test/react_test_helpers";
|
import {mountForMobxWithIntl, runTestsOnConditions, TriggerAndAction} from "../test/react_test_helpers";
|
||||||
import {UniqueIdMock} from "../tests/unique_id_mock";
|
import {UniqueIdMock} from "../test/unique_id_mock";
|
||||||
|
|
||||||
let uniqueIdMock = new UniqueIdMock();
|
let uniqueIdMock = new UniqueIdMock();
|
||||||
class MockableTabPanelState extends WizardTabPanelState
|
class MockableTabPanelState extends WizardTabPanelState
|
||||||
|
|
|
@ -3,7 +3,7 @@ import * as React from 'react';
|
||||||
import 'jest';
|
import 'jest';
|
||||||
import {AbstractWizardState, AbstractWizardTabPanelState} from './abstract_wizard_state'
|
import {AbstractWizardState, AbstractWizardTabPanelState} from './abstract_wizard_state'
|
||||||
import {observable, action, computed} from 'mobx'
|
import {observable, action, computed} from 'mobx'
|
||||||
import {UniqueIdMock} from "../tests/unique_id_mock";
|
import {UniqueIdMock} from "../test/unique_id_mock";
|
||||||
|
|
||||||
let uniqueIdMock = new UniqueIdMock();
|
let uniqueIdMock = new UniqueIdMock();
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,287 @@
|
||||||
|
// License: LGPL-3.0-or-later
|
||||||
|
import * as React from 'react';
|
||||||
|
import { observer } from 'mobx-react';
|
||||||
|
import {InjectedIntlProps, injectIntl} from 'react-intl';
|
||||||
|
import Modal from "../common/Modal";
|
||||||
|
import { FundraiserInfo} from "../edit_payment_pane/EditPaymentPane";
|
||||||
|
import {HoudiniForm} from "../../lib/houdini_form";
|
||||||
|
import {BasicField, CurrencyField, SelectField, TextareaField} from "../common/fields";
|
||||||
|
import ProgressableButton from "../common/ProgressableButton";
|
||||||
|
import {action, computed} from "mobx";
|
||||||
|
import {NonprofitTimezonedDates} from "../../lib/date";
|
||||||
|
import {Field, FieldDefinition} from "../../../../types/mobx-react-form";
|
||||||
|
import {createFieldDefinition} from "../../lib/mobx_utils";
|
||||||
|
import {centsToDollars, dollarsToCents} from "../../lib/format";
|
||||||
|
import {Validations} from "../../lib/vjf_rules";
|
||||||
|
import {serializeDedication} from "../../lib/dedication";
|
||||||
|
import {ApiManager} from "../../lib/api_manager";
|
||||||
|
import * as CustomAPIS from "../../lib/apis";
|
||||||
|
import {CSRFInterceptor} from "../../lib/csrf_interceptor";
|
||||||
|
import {CreateOffsiteDonation, CreateOffsiteDonationModel} from "../../lib/api/create_offsite_donation";
|
||||||
|
import blacklist = require("validator/lib/blacklist");
|
||||||
|
import * as _ from 'lodash'
|
||||||
|
import moment = require('moment');
|
||||||
|
import { castToUndefinedIfBlank } from '../../lib/utils';
|
||||||
|
import ReactInput from "../common/form/ReactInput";
|
||||||
|
|
||||||
|
export interface CreateOffsitePaymentPaneProps
|
||||||
|
{
|
||||||
|
events: FundraiserInfo[]
|
||||||
|
campaigns: FundraiserInfo[]
|
||||||
|
nonprofitId: number
|
||||||
|
supporterId:number
|
||||||
|
nonprofitTimezone?: string
|
||||||
|
preupdateDonationAction:() => void
|
||||||
|
postUpdateSuccess: () => void
|
||||||
|
|
||||||
|
//from ModalProps
|
||||||
|
onClose: () => void
|
||||||
|
modalActive: boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface FundraiserInfo {
|
||||||
|
id: number
|
||||||
|
name: string
|
||||||
|
}
|
||||||
|
|
||||||
|
class CreateOffsitePaymentPaneForm extends HoudiniForm {
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
class CreateNewOffsitePaymentPane extends React.Component<CreateOffsitePaymentPaneProps & InjectedIntlProps, {}> {
|
||||||
|
|
||||||
|
constructor(props: CreateOffsitePaymentPaneProps & InjectedIntlProps) {
|
||||||
|
super(props);
|
||||||
|
this.postOffsiteDonation = new ApiManager(CustomAPIS.APIS as Array<any>, CSRFInterceptor).get(CreateOffsiteDonation)
|
||||||
|
|
||||||
|
this.loadFormFromData()
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@computed
|
||||||
|
get nonprofitTimezonedDates():NonprofitTimezonedDates {
|
||||||
|
return new NonprofitTimezonedDates(this.props.nonprofitTimezone)
|
||||||
|
}
|
||||||
|
|
||||||
|
postOffsiteDonation : CreateOffsiteDonation
|
||||||
|
|
||||||
|
@action.bound
|
||||||
|
async createOffsiteDonation() {
|
||||||
|
if (this.props.preupdateDonationAction) {
|
||||||
|
this.props.preupdateDonationAction()
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
let postData:CreateOffsiteDonationModel = {
|
||||||
|
nonprofit_id:this.props.nonprofitId,
|
||||||
|
supporter_id:this.props.supporterId,
|
||||||
|
amount: dollarsToCents(this.form.$('gross_amount').get('value')),
|
||||||
|
designation: this.form.$('designation').value,
|
||||||
|
comment: this.form.$('comment').value,
|
||||||
|
campaign_id: this.form.$('campaign').get('value'),
|
||||||
|
event_id: this.form.$('event').get('value'),
|
||||||
|
date: this.form.$('date').get('value')
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.form.$('dedication.type').get('value') != '') {
|
||||||
|
const nameToValueForContact = ['full_address', 'phone', 'email'].map((i) => {
|
||||||
|
return {
|
||||||
|
name: i, value: this.form.$(`dedication.${i}`).get('value')
|
||||||
|
}
|
||||||
|
});
|
||||||
|
const contact = _.some(nameToValueForContact, (i) => i.value && i.value != "") ?
|
||||||
|
_.reduce(nameToValueForContact, (result: any, i) => {
|
||||||
|
result[i.name] = i.value;
|
||||||
|
return result;
|
||||||
|
}, {}) : undefined;
|
||||||
|
|
||||||
|
postData.dedication = serializeDedication({
|
||||||
|
type: this.form.$('dedication.type').get('value'),
|
||||||
|
supporter_id: this.form.$('dedication.supporter_id').get('value'),
|
||||||
|
name: this.form.$('dedication.name').get('value'),
|
||||||
|
contact: contact,
|
||||||
|
note: this.form.$('dedication.note').get('value')
|
||||||
|
})
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
postData.dedication = ""
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.form.has('check_number'))
|
||||||
|
{
|
||||||
|
postData.offsite_payment = postData.offsite_payment || {}
|
||||||
|
postData.offsite_payment.check_number = this.form.$('check_number').value
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
await this.postOffsiteDonation.postDonation(postData, this.props.nonprofitId)
|
||||||
|
|
||||||
|
|
||||||
|
if(this.props.postUpdateSuccess){
|
||||||
|
try {
|
||||||
|
this.props.postUpdateSuccess()
|
||||||
|
}
|
||||||
|
catch {}
|
||||||
|
}
|
||||||
|
|
||||||
|
this.props.onClose()
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
@action.bound
|
||||||
|
loadFormFromData() {
|
||||||
|
|
||||||
|
let params: {[name:string]:FieldDefinition} = {
|
||||||
|
'event': {name: 'event', label: 'Event',
|
||||||
|
output: (id:string) => castToUndefinedIfBlank(id)},
|
||||||
|
'campaign': {name: 'campaign', label: 'Campaign',
|
||||||
|
output: (id:string) => castToUndefinedIfBlank(id)},
|
||||||
|
'gross_amount': createFieldDefinition({name: 'gross_amount',
|
||||||
|
label: 'Gross Amount',
|
||||||
|
input: (amount:number) => centsToDollars(amount),
|
||||||
|
output: (dollarString:string) => parseFloat(blacklist(dollarString, '$,')),
|
||||||
|
value: 0
|
||||||
|
}),
|
||||||
|
// 'fee_total': createFieldDefinition({name: 'fee_total', label: 'Fees',
|
||||||
|
// input: (amount:number) => centsToDollars(amount),
|
||||||
|
// output: (dollarString:string) => parseFloat(blacklist(dollarString, '$,')),
|
||||||
|
// value: 0
|
||||||
|
// }),
|
||||||
|
'date': createFieldDefinition({name: 'date', label: 'Date',
|
||||||
|
input: (isoTime:string) => this.nonprofitTimezonedDates.readable_date(isoTime),
|
||||||
|
output:(date:string) => this.nonprofitTimezonedDates.readable_date_time_to_iso(date),
|
||||||
|
value: moment.utc().toISOString()
|
||||||
|
}),
|
||||||
|
'dedication': {name: 'dedication', label: 'Dedication', fields: [
|
||||||
|
createFieldDefinition({name:'type', label: 'Dedication Type'}),
|
||||||
|
createFieldDefinition({name: 'supporter_id', type: 'hidden'}),
|
||||||
|
createFieldDefinition({name:'name', label:'Person dedicated for'}),
|
||||||
|
createFieldDefinition({name: 'full_address', label: 'Full address'}),
|
||||||
|
createFieldDefinition({name: 'phone', label: 'Phone'}),
|
||||||
|
createFieldDefinition({name: 'email', label: 'Email'}),
|
||||||
|
createFieldDefinition({name: 'note', label: 'Dedication Note', type: 'textarea'})
|
||||||
|
]},
|
||||||
|
'designation': {name: 'designation', label: 'Designation'},
|
||||||
|
'comment': {name: 'comment', label: 'Note'}
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
params.check_number = {name: 'check_number', label: 'Check Number'}
|
||||||
|
|
||||||
|
params.date.validators = [Validations.isDate('MM/DD/YYYY')]
|
||||||
|
|
||||||
|
params.gross_amount.validators = [Validations.isGreaterThanOrEqualTo(0.01)];
|
||||||
|
// params.fee_total.validators = [Validations.optional(Validations.isLessThanOrEqualTo(0))];
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
return new CreateOffsitePaymentPaneForm({fields: _.values(params)}, {
|
||||||
|
hooks: {
|
||||||
|
onSuccess: async (e: Field) => {
|
||||||
|
await this.createOffsiteDonation()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
@computed get form(): CreateOffsitePaymentPaneForm {
|
||||||
|
//add this.props because we need to reload on prop change
|
||||||
|
return this.props && this.loadFormFromData()
|
||||||
|
}
|
||||||
|
|
||||||
|
@computed get dateFormatter(): NonprofitTimezonedDates {
|
||||||
|
return new NonprofitTimezonedDates(this.props.nonprofitTimezone)
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
this.form.values()
|
||||||
|
const modal =
|
||||||
|
<Modal modalActive={this.props.modalActive} titleText={'Create Offsite Donation'} focusDialog={true}
|
||||||
|
onClose={this.props.onClose} dialogStyle={{minWidth:'768px'}} childGenerator={() => {
|
||||||
|
return <div className={"tw-bs"}>
|
||||||
|
<form className='u-marginTop--20'>
|
||||||
|
|
||||||
|
<CurrencyField field={this.form.$('gross_amount')} label={"Gross Amount"} currencySymbol={"$"}/>
|
||||||
|
{/* <CurrencyField field={this.form.$('fee_total')} label={"Processing Fees"} mustBeNegative={true}/> */}
|
||||||
|
|
||||||
|
<BasicField field={this.form.$('date')} label={"Date"} />
|
||||||
|
<SelectField field={this.form.$('campaign')}
|
||||||
|
label={"Campaign"}
|
||||||
|
options={this.props.campaigns}/>
|
||||||
|
|
||||||
|
|
||||||
|
<SelectField field={this.form.$('event')}
|
||||||
|
label={"Event"}
|
||||||
|
options={this.props.events} />
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
<TextareaField field={this.form.$('designation')} label={"Designation"} rows={3} />
|
||||||
|
<div className="panel panel-default">
|
||||||
|
<div className="panel-heading"><label>Dedication <small> (optional)</small></label></div>
|
||||||
|
<div className="panel-body">
|
||||||
|
<SelectField field={this.form.$('dedication.type')} label={"Dedication Type"}
|
||||||
|
options={[{id: null, name: ''}, {id: 'honor', name: 'In honor of'}, {
|
||||||
|
id: 'memory',
|
||||||
|
name: 'In memory of'
|
||||||
|
}]}/>
|
||||||
|
|
||||||
|
{this.form.$('dedication.type').get('value') != '' ? <div>
|
||||||
|
<div className={"panel panel-default"}>
|
||||||
|
<div className="panel-heading"><label>Dedicated to:</label></div>
|
||||||
|
<div className={'panel-body'}>
|
||||||
|
<table className='table--small u-marginBottom--10'>
|
||||||
|
<tbody>
|
||||||
|
<tr>
|
||||||
|
<th>Name</th>
|
||||||
|
<td><ReactInput field={this.form.$('dedication.name')} label={'Name'} className={"form-control"}/></td>
|
||||||
|
</tr>
|
||||||
|
|
||||||
|
<tr>
|
||||||
|
<th>Full Address
|
||||||
|
</th>
|
||||||
|
<td><ReactInput field={this.form.$('dedication.full_address')} className={"form-control"}/>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<th>Phone Number
|
||||||
|
</th>
|
||||||
|
<td><ReactInput field={this.form.$('dedication.phone')} className={"form-control"}/>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<th>Email Address
|
||||||
|
</th>
|
||||||
|
<td><ReactInput field={this.form.$('dedication.email')} className={"form-control"}/>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<TextareaField rows={3} placeholder={"Dedication"} field={this.form.$('dedication.note')}
|
||||||
|
label={"Dedication Note"}/></div>
|
||||||
|
: undefined}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
|
||||||
|
<TextareaField field={this.form.$('comment')} label={"Notes"} rows={3} />
|
||||||
|
|
||||||
|
|
||||||
|
<ProgressableButton buttonText={'Save'}
|
||||||
|
buttonTextOnProgress={'Updating...'} className={'button'}
|
||||||
|
inProgress={this.form.submitting}
|
||||||
|
disabled={!this.form.isValid}
|
||||||
|
disableOnProgress={true} onClick={this.form.onSubmit}/>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
}} />
|
||||||
|
return <div>{modal}</div>;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default injectIntl(observer(CreateNewOffsitePaymentPane))
|
||||||
|
|
||||||
|
|
||||||
|
|
495
javascripts/src/components/edit_payment_pane/EditPaymentPane.tsx
Normal file
495
javascripts/src/components/edit_payment_pane/EditPaymentPane.tsx
Normal file
|
@ -0,0 +1,495 @@
|
||||||
|
// License: LGPL-3.0-or-later
|
||||||
|
import * as React from 'react';
|
||||||
|
import {observer} from 'mobx-react';
|
||||||
|
import {InjectedIntlProps, injectIntl} from 'react-intl';
|
||||||
|
import {action, computed} from "mobx";
|
||||||
|
import {FieldDefinition} from "mobx-react-form";
|
||||||
|
import {HoudiniForm} from "../../lib/houdini_form";
|
||||||
|
import ProgressableButton from "../common/ProgressableButton";
|
||||||
|
import {centsToDollars, dollarsToCents, readableInterval, readableKind} from "../../lib/format";
|
||||||
|
import {NonprofitTimezonedDates} from '../../lib/date';
|
||||||
|
import {UpdateDonationModel, PutDonation} from "../../lib/api/put_donation";
|
||||||
|
import {ApiManager} from "../../lib/api_manager";
|
||||||
|
import * as CustomAPIS from "../../lib/apis";
|
||||||
|
import {CSRFInterceptor} from "../../lib/csrf_interceptor";
|
||||||
|
import {BasicField, CurrencyField, SelectField, TextareaField} from '../common/fields';
|
||||||
|
import {TwoColumnFields} from "../common/layout";
|
||||||
|
import {Validations} from "../../lib/vjf_rules";
|
||||||
|
import _ = require("lodash");
|
||||||
|
import {Dedication, parseDedication, serializeDedication} from '../../lib/dedication';
|
||||||
|
import blacklist = require("validator/lib/blacklist");
|
||||||
|
import {createFieldDefinition} from "../../lib/mobx_utils";
|
||||||
|
import Modal from "../common/Modal";
|
||||||
|
import ReactInput from "../common/form/ReactInput";
|
||||||
|
|
||||||
|
|
||||||
|
interface Charge {
|
||||||
|
status: string
|
||||||
|
}
|
||||||
|
|
||||||
|
interface RecurringDonation {
|
||||||
|
interval?: number
|
||||||
|
time_unit?: string
|
||||||
|
created_at: string
|
||||||
|
}
|
||||||
|
|
||||||
|
interface Donation {
|
||||||
|
designation?: string
|
||||||
|
comment?: string
|
||||||
|
event?: { id: number }
|
||||||
|
campaign?: { id: number }
|
||||||
|
dedication?: string
|
||||||
|
recurring_donation?: RecurringDonation
|
||||||
|
id: number
|
||||||
|
}
|
||||||
|
|
||||||
|
interface PaymentData {
|
||||||
|
gross_amount: number
|
||||||
|
fee_total: number
|
||||||
|
date: string
|
||||||
|
offsite_payment: OffsitePayment
|
||||||
|
donation: Donation
|
||||||
|
kind: string
|
||||||
|
id: string
|
||||||
|
refund_total: number
|
||||||
|
net_amount: number
|
||||||
|
origin_url?: string
|
||||||
|
charge?: Charge,
|
||||||
|
nonprofit: { id: number }
|
||||||
|
}
|
||||||
|
|
||||||
|
interface OffsitePayment {
|
||||||
|
check_number: string
|
||||||
|
kind: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface EditPaymentPaneProps {
|
||||||
|
data: PaymentData
|
||||||
|
events: FundraiserInfo[]
|
||||||
|
campaigns: FundraiserInfo[]
|
||||||
|
|
||||||
|
nonprofitTimezone?: string
|
||||||
|
preupdateDonationAction: () => void
|
||||||
|
postUpdateSuccess: () => void
|
||||||
|
|
||||||
|
//from ModalProps
|
||||||
|
onClose: () => void
|
||||||
|
modalActive: boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface FundraiserInfo {
|
||||||
|
id: number
|
||||||
|
name: string
|
||||||
|
}
|
||||||
|
|
||||||
|
class EditPaymentPaneForm extends HoudiniForm {
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@observer
|
||||||
|
class EditPaymentPane extends React.Component<EditPaymentPaneProps & InjectedIntlProps, {}> {
|
||||||
|
|
||||||
|
constructor(props: EditPaymentPaneProps & InjectedIntlProps) {
|
||||||
|
super(props);
|
||||||
|
this.putDonation = new ApiManager(CustomAPIS.APIS as Array<any>, CSRFInterceptor).get(PutDonation);
|
||||||
|
|
||||||
|
this.loadFormFromData()
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
@computed
|
||||||
|
get nonprofitTimezonedDates(): NonprofitTimezonedDates {
|
||||||
|
return new NonprofitTimezonedDates(this.props.nonprofitTimezone)
|
||||||
|
}
|
||||||
|
|
||||||
|
@computed
|
||||||
|
get dedication(): Dedication | null {
|
||||||
|
return parseDedication(this.props.data && this.props.data.donation && this.props.data.donation.dedication)
|
||||||
|
}
|
||||||
|
|
||||||
|
putDonation: PutDonation;
|
||||||
|
|
||||||
|
@action.bound
|
||||||
|
async updateDonation() {
|
||||||
|
if (this.props.preupdateDonationAction) {
|
||||||
|
this.props.preupdateDonationAction()
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
let updateData: UpdateDonationModel = {
|
||||||
|
id: Number(this.props.data.donation.id),
|
||||||
|
donation: {
|
||||||
|
designation: this.form.$('designation').value,
|
||||||
|
comment: this.form.$('comment').value,
|
||||||
|
campaign_id: this.form.$('campaign').value,
|
||||||
|
event_id: this.form.$('event').value,
|
||||||
|
gross_amount: dollarsToCents(this.form.$('gross_amount').get('value')),
|
||||||
|
fee_total: dollarsToCents(this.form.$('fee_total').get('value')),
|
||||||
|
|
||||||
|
date: this.form.$('date').get('value')
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
if (this.form.$('dedication.type').get('value') != '') {
|
||||||
|
const nameToValueForContact = ['full_address', 'phone', 'email'].map((i) => {
|
||||||
|
return {
|
||||||
|
name: i, value: this.form.$(`dedication.${i}`).get('value')
|
||||||
|
}
|
||||||
|
});
|
||||||
|
const contact = _.some(nameToValueForContact, (i) => i.value && i.value != "") ?
|
||||||
|
_.reduce(nameToValueForContact, (result: any, i) => {
|
||||||
|
result[i.name] = i.value;
|
||||||
|
return result;
|
||||||
|
}, {}) : undefined;
|
||||||
|
|
||||||
|
updateData.donation.dedication = serializeDedication({
|
||||||
|
type: this.form.$('dedication.type').get('value'),
|
||||||
|
supporter_id: this.form.$('dedication.supporter_id').get('value'),
|
||||||
|
name: this.form.$('dedication.name').get('value'),
|
||||||
|
contact: contact,
|
||||||
|
note: this.form.$('dedication.note').get('value')
|
||||||
|
});
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
updateData.donation.dedication = "";
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.form.has('check_number')) {
|
||||||
|
updateData.donation.check_number = this.form.$('check_number').value
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
await this.putDonation.putDonation(updateData, this.props.data.nonprofit.id);
|
||||||
|
|
||||||
|
|
||||||
|
if (this.props.postUpdateSuccess) {
|
||||||
|
try {
|
||||||
|
this.props.postUpdateSuccess()
|
||||||
|
}
|
||||||
|
catch {
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
this.props.onClose()
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@action.bound
|
||||||
|
loadFormFromData() {
|
||||||
|
const eventId = this.props.data.donation.event && this.props.data.donation.event.id;
|
||||||
|
const campaignId = this.props.data.donation.campaign && this.props.data.donation.campaign.id;
|
||||||
|
let params: { [name: string]: FieldDefinition } = {
|
||||||
|
'event': {name: 'event', label: 'Event', value: eventId},
|
||||||
|
'campaign': {name: 'campaign', label: 'Campaign', value: campaignId},
|
||||||
|
'gross_amount': createFieldDefinition({
|
||||||
|
name: 'gross_amount', label: 'Gross Amount', value: this.props.data.gross_amount,
|
||||||
|
input: (amount: number) => centsToDollars(amount),
|
||||||
|
output: (dollarString: string) => parseFloat(blacklist(dollarString, '$,'))
|
||||||
|
}),
|
||||||
|
'fee_total': createFieldDefinition({
|
||||||
|
name: 'fee_total', label: 'Fees', value: this.props.data.fee_total,
|
||||||
|
input: (amount: number) => centsToDollars(amount),
|
||||||
|
output: (dollarString: string) => parseFloat(blacklist(dollarString, '$,'))
|
||||||
|
}),
|
||||||
|
'date': createFieldDefinition({
|
||||||
|
name: 'date', label: 'Date',
|
||||||
|
value: this.props.data.date,
|
||||||
|
input: (isoTime: string) => this.nonprofitTimezonedDates.readable_date(isoTime),
|
||||||
|
output: (date: string) => this.nonprofitTimezonedDates.readable_date_time_to_iso(date)
|
||||||
|
}),
|
||||||
|
'dedication': {
|
||||||
|
name: 'dedication', label: 'Dedication', fields: [
|
||||||
|
createFieldDefinition({name: 'type', label: 'Dedication Type', value: this.dedication && this.dedication.type}),
|
||||||
|
createFieldDefinition({name: 'supporter_id', type: 'hidden', value: this.dedication && this.dedication.supporter_id}),
|
||||||
|
createFieldDefinition({name: 'name', label: 'Person dedicated for', value: this.dedication && this.dedication.name}),
|
||||||
|
createFieldDefinition({name: 'full_address', label: 'Full address', value: this.dedication && this.dedication.contact && this.dedication.contact.address}),
|
||||||
|
createFieldDefinition({name: 'phone', label: 'Phone', value: this.dedication && this.dedication.contact && this.dedication.contact.phone}),
|
||||||
|
createFieldDefinition({name: 'email', label: 'email', value: this.dedication && this.dedication.contact && this.dedication.contact.email}),
|
||||||
|
createFieldDefinition({name: 'note', value: this.dedication && this.dedication.note})
|
||||||
|
]
|
||||||
|
},
|
||||||
|
'designation': {name: 'designation', label: 'Designation', value: this.props.data.donation.designation},
|
||||||
|
'comment': {name: 'comment', label: 'Note', value: this.props.data.donation.comment}
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
if (this.props.data.kind == 'OffsitePayment') {
|
||||||
|
params.check_number = {
|
||||||
|
name: 'check_number',
|
||||||
|
label: 'Check Number',
|
||||||
|
value: this.props.data.offsite_payment.check_number
|
||||||
|
};
|
||||||
|
|
||||||
|
params.date.validators = [Validations.isDate('MM/DD/YYYY')];
|
||||||
|
|
||||||
|
params.gross_amount.validators = [Validations.isGreaterThanOrEqualTo(0.01)];
|
||||||
|
params.fee_total.validators = [Validations.optional(Validations.isLessThanOrEqualTo(0))];
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
return new EditPaymentPaneForm({fields: _.values(params)}, {
|
||||||
|
hooks: {
|
||||||
|
onSuccess: async () => {
|
||||||
|
await this.updateDonation()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
@computed get form(): EditPaymentPaneForm {
|
||||||
|
//add this.props because we need to reload on prop change
|
||||||
|
return this.props && this.loadFormFromData()
|
||||||
|
}
|
||||||
|
|
||||||
|
@computed get dateFormatter(): NonprofitTimezonedDates {
|
||||||
|
return new NonprofitTimezonedDates(this.props.nonprofitTimezone)
|
||||||
|
}
|
||||||
|
|
||||||
|
@action.bound
|
||||||
|
innerRender() {
|
||||||
|
let rd = this.props.data && this.props.data.donation && this.props.data.donation.recurring_donation;
|
||||||
|
let initialTable = <table className='table--small u-marginBottom--10'>
|
||||||
|
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>Payment Info</th>
|
||||||
|
<th/>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
|
||||||
|
<tbody>
|
||||||
|
<tr>
|
||||||
|
<td>Date</td>
|
||||||
|
<td>
|
||||||
|
{this.dateFormatter.readable_date(this.props.data.date)}
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
|
||||||
|
<tr>
|
||||||
|
<td>Type</td>
|
||||||
|
<td>
|
||||||
|
{readableKind(this.props.data.kind)}
|
||||||
|
|
||||||
|
{
|
||||||
|
this.props.data.offsite_payment && this.props.data.offsite_payment && this.props.data.offsite_payment.kind ?
|
||||||
|
|
||||||
|
<span>
|
||||||
|
|
||||||
|
({this.props.data.offsite_payment.kind})
|
||||||
|
</span> : undefined
|
||||||
|
|
||||||
|
}
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
|
||||||
|
{
|
||||||
|
this.props.data.kind === 'RecurringDonation' ?
|
||||||
|
<tr>
|
||||||
|
<td>Recurring</td>
|
||||||
|
<td>
|
||||||
|
{rd ? readableInterval(rd.interval, rd.time_unit) : false}
|
||||||
|
since
|
||||||
|
{rd ? this.dateFormatter.readable_date(rd.created_at) : false}
|
||||||
|
</td>
|
||||||
|
</tr> : false
|
||||||
|
}
|
||||||
|
|
||||||
|
<tr className='test-grossAmount'>
|
||||||
|
<td>Gross Amount</td>
|
||||||
|
<td>
|
||||||
|
${centsToDollars(this.props.data.gross_amount)}
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
|
||||||
|
<tr>
|
||||||
|
<td>Processing Fees</td>
|
||||||
|
<td>
|
||||||
|
${centsToDollars(this.props.data.fee_total)}
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
|
||||||
|
{
|
||||||
|
this.props.data.refund_total && this.props.data.refund_total > 0 ?
|
||||||
|
<tr>
|
||||||
|
|
||||||
|
<td>Total Refunds</td>
|
||||||
|
<td>
|
||||||
|
${centsToDollars(this.props.data.fee_total)}
|
||||||
|
</td>
|
||||||
|
</tr> : false
|
||||||
|
}
|
||||||
|
<tr>
|
||||||
|
<td>Net Amount</td>
|
||||||
|
<td>
|
||||||
|
${centsToDollars(this.props.data.net_amount)}
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
|
||||||
|
{
|
||||||
|
this.props.data.origin_url ?
|
||||||
|
<tr>
|
||||||
|
|
||||||
|
<td>Origin</td>
|
||||||
|
<td>
|
||||||
|
<a target='_blank' href={this.props.data.origin_url}>
|
||||||
|
{this.props.data.origin_url}
|
||||||
|
</a>
|
||||||
|
</td>
|
||||||
|
</tr> : false
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
this.props.data.charge ?
|
||||||
|
<tr>
|
||||||
|
|
||||||
|
<td>Status</td>
|
||||||
|
<td>{this.props.data.charge.status}</td>
|
||||||
|
</tr> : false
|
||||||
|
}
|
||||||
|
|
||||||
|
{this.props.data.offsite_payment && this.props.data.offsite_payment.check_number ?
|
||||||
|
<tr>
|
||||||
|
|
||||||
|
<td>Check #</td>
|
||||||
|
<td>
|
||||||
|
{this.props.data.offsite_payment.check_number}
|
||||||
|
</td>
|
||||||
|
</tr> : false
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
<tr>
|
||||||
|
<td>ID</td>
|
||||||
|
<td>{this.props.data.id}</td>
|
||||||
|
</tr>
|
||||||
|
|
||||||
|
</tbody>
|
||||||
|
</table>;
|
||||||
|
|
||||||
|
|
||||||
|
let checkNumber = this.props.data.offsite_payment && this.props.data.offsite_payment.check_number ?
|
||||||
|
<BasicField field={this.form.$('check_number')} label={"Check or Payment Number/ID"}/> : false;
|
||||||
|
|
||||||
|
let offsitePayment = this.props.data.kind === "OffsitePayment" ? (<div>
|
||||||
|
|
||||||
|
<TwoColumnFields>
|
||||||
|
<CurrencyField field={this.form.$('gross_amount')} label={"Gross Amount"} currencySymbol={"$"}/>
|
||||||
|
<CurrencyField field={this.form.$('fee_total')} label={"Processing Fees"} mustBeNegative={true}/>
|
||||||
|
|
||||||
|
</TwoColumnFields>
|
||||||
|
|
||||||
|
<BasicField field={this.form.$('date')} label={"Date"}/>
|
||||||
|
|
||||||
|
|
||||||
|
{checkNumber}
|
||||||
|
</div>) : undefined;
|
||||||
|
return <div className={"tw-bs"}>
|
||||||
|
<div>
|
||||||
|
{initialTable}
|
||||||
|
|
||||||
|
<form className='u-marginTop--20'>
|
||||||
|
|
||||||
|
{offsitePayment}
|
||||||
|
|
||||||
|
|
||||||
|
<SelectField field={this.form.$('campaign')}
|
||||||
|
label={"Campaign"}
|
||||||
|
options={this.props.campaigns}/>
|
||||||
|
|
||||||
|
|
||||||
|
<SelectField field={this.form.$('event')}
|
||||||
|
label={"Event"}
|
||||||
|
options={this.props.events}/>
|
||||||
|
|
||||||
|
|
||||||
|
<TextareaField field={this.form.$('designation')} label={"Designation"} rows={3}/>
|
||||||
|
|
||||||
|
<div className="panel panel-default">
|
||||||
|
<div className="panel-heading"><label>Dedication</label></div>
|
||||||
|
<div className="panel-body">
|
||||||
|
<SelectField field={this.form.$('dedication.type')} label={"Dedication Type"}
|
||||||
|
options={[{id: null, name: ''}, {id: 'honor', name: 'In honor of'}, {
|
||||||
|
id: 'memory',
|
||||||
|
name: 'In memory of'
|
||||||
|
}]}/>
|
||||||
|
|
||||||
|
{this.form.$('dedication.type').get('value') != '' ? <div>
|
||||||
|
<div className={"panel panel-default"}>
|
||||||
|
<div className="panel-heading"><label>Dedicated to:</label></div>
|
||||||
|
<div className={'panel-body'}>
|
||||||
|
<table className='table--small u-marginBottom--10'>
|
||||||
|
<tbody>
|
||||||
|
<tr>
|
||||||
|
<th>Name</th>
|
||||||
|
<td><ReactInput field={this.form.$('dedication.name')} label={'Name'} className={"form-control"}/></td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<th> Supporter
|
||||||
|
ID
|
||||||
|
</th>
|
||||||
|
<td>{this.dedication.supporter_id}<input {...this.form.$('dedication.supporter_id').bind()}/>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
|
||||||
|
<tr>
|
||||||
|
<th>Full Address
|
||||||
|
</th>
|
||||||
|
<td><ReactInput field={this.form.$('dedication.full_address')} className={"form-control"}/>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<th>Phone Number
|
||||||
|
</th>
|
||||||
|
<td><ReactInput field={this.form.$('dedication.phone')} className={"form-control"}/>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<th>Email Address
|
||||||
|
</th>
|
||||||
|
<td><ReactInput field={this.form.$('dedication.email')} className={"form-control"}/>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<TextareaField rows={3} placeholder={"Dedication"} field={this.form.$('dedication.note')}
|
||||||
|
label={"Dedication Note"}/></div>
|
||||||
|
: undefined}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
|
||||||
|
<TextareaField field={this.form.$('comment')} label={"Notes"} rows={3}/>
|
||||||
|
|
||||||
|
|
||||||
|
<ProgressableButton buttonText={'Save'}
|
||||||
|
buttonTextOnProgress={'Updating...'} className={'button'}
|
||||||
|
inProgress={this.form.submitting}
|
||||||
|
disabled={!this.form.isValid}
|
||||||
|
disableOnProgress={true} onClick={this.form.onSubmit}/>
|
||||||
|
|
||||||
|
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
|
||||||
|
//force it to check the form values so this updates
|
||||||
|
this.form.values()
|
||||||
|
|
||||||
|
const modal =
|
||||||
|
<Modal modalActive={this.props.modalActive} titleText={'Edit Donation'} focusDialog={true}
|
||||||
|
onClose={this.props.onClose} dialogStyle={{minWidth: '768px'}} childGenerator={() => this.innerRender()}/>
|
||||||
|
|
||||||
|
return modal;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default injectIntl(observer(EditPaymentPane))
|
|
@ -51,34 +51,44 @@ class NonprofitInfoForm extends React.Component<NonprofitInfoFormProps & Injecte
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
return <fieldset >
|
return <fieldset >
|
||||||
|
|
||||||
<BasicField field={this.props.form.$("organization_name")}
|
<BasicField field={this.props.form.$("organization_name")}
|
||||||
label={this.props.intl.formatMessage({id:'registration.wizard.nonprofit.name.label' })}
|
label={this.props.intl.formatMessage({id:'registration.wizard.nonprofit.name.label' })}
|
||||||
placeholder={this.props.intl.formatMessage({id: 'registration.wizard.nonprofit.name.placeholder'})}
|
placeholder={this.props.intl.formatMessage({id: 'registration.wizard.nonprofit.name.placeholder'})}
|
||||||
|
inputClassNames={"input-lg"}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<BasicField field={this.props.form.$('website')}
|
<BasicField field={this.props.form.$('website')}
|
||||||
label={this.props.intl.formatMessage({id: 'registration.wizard.nonprofit.website.label'})}
|
label={this.props.intl.formatMessage({id: 'registration.wizard.nonprofit.website.label'})}
|
||||||
placeholder={this.props.intl.formatMessage({id: 'registration.wizard.nonprofit.website.placeholder'})}/>
|
placeholder={this.props.intl.formatMessage({id: 'registration.wizard.nonprofit.website.placeholder'})}
|
||||||
|
inputClassNames={"input-lg"}/>
|
||||||
|
|
||||||
<TwoColumnFields>
|
<TwoColumnFields>
|
||||||
<BasicField field={this.props.form.$('org_email')}
|
<BasicField field={this.props.form.$('org_email')}
|
||||||
label={this.props.intl.formatMessage({id: 'registration.wizard.nonprofit.email.label'})}
|
label={this.props.intl.formatMessage({id: 'registration.wizard.nonprofit.email.label'})}
|
||||||
placeholder={this.props.intl.formatMessage({id: 'registration.wizard.nonprofit.email.placeholder'})}/>
|
placeholder={this.props.intl.formatMessage({id: 'registration.wizard.nonprofit.email.placeholder'})}
|
||||||
|
inputClassNames={"input-lg"}/>
|
||||||
<BasicField field={this.props.form.$('org_phone')}
|
<BasicField field={this.props.form.$('org_phone')}
|
||||||
label={this.props.intl.formatMessage({id: 'registration.wizard.nonprofit.phone.label'})}
|
label={this.props.intl.formatMessage({id: 'registration.wizard.nonprofit.phone.label'})}
|
||||||
placeholder={this.props.intl.formatMessage({id: 'registration.wizard.nonprofit.phone.placeholder'})}/>
|
placeholder={this.props.intl.formatMessage({id: 'registration.wizard.nonprofit.phone.placeholder'})}
|
||||||
|
inputClassNames={"input-lg"}/>
|
||||||
</TwoColumnFields>
|
</TwoColumnFields>
|
||||||
|
|
||||||
<ThreeColumnFields>
|
<ThreeColumnFields>
|
||||||
<BasicField field={this.props.form.$('city')}
|
<BasicField field={this.props.form.$('city')}
|
||||||
label={this.props.intl.formatMessage({id: 'registration.wizard.nonprofit.city.label'})}
|
label={this.props.intl.formatMessage({id: 'registration.wizard.nonprofit.city.label'})}
|
||||||
placeholder={this.props.intl.formatMessage({id: 'registration.wizard.nonprofit.city.placeholder'})}/>
|
placeholder={this.props.intl.formatMessage({id: 'registration.wizard.nonprofit.city.placeholder'})}
|
||||||
|
inputClassNames={"input-lg"}/>
|
||||||
<BasicField field={this.props.form.$('state')}
|
<BasicField field={this.props.form.$('state')}
|
||||||
label={this.props.intl.formatMessage({id: 'registration.wizard.nonprofit.state.label'})}
|
label={this.props.intl.formatMessage({id: 'registration.wizard.nonprofit.state.label'})}
|
||||||
placeholder={this.props.intl.formatMessage({id: 'registration.wizard.nonprofit.state.placeholder'})}/>
|
placeholder={this.props.intl.formatMessage({id: 'registration.wizard.nonprofit.state.placeholder'})}
|
||||||
|
inputClassNames={"input-lg"}/>
|
||||||
<BasicField field={this.props.form.$('zip')}
|
<BasicField field={this.props.form.$('zip')}
|
||||||
label={this.props.intl.formatMessage({id: 'registration.wizard.nonprofit.zip.label' })}
|
label={this.props.intl.formatMessage({id: 'registration.wizard.nonprofit.zip.label' })}
|
||||||
placeholder={this.props.intl.formatMessage({id: 'registration.wizard.nonprofit.zip.placeholder'})}/>
|
placeholder={this.props.intl.formatMessage({id: 'registration.wizard.nonprofit.zip.placeholder'})}
|
||||||
|
inputClassNames={"input-lg"}/>
|
||||||
</ThreeColumnFields>
|
</ThreeColumnFields>
|
||||||
|
|
||||||
<ProgressableButton onClick={this.props.form.onSubmit} className="button" disabled={!this.props.form.isValid} buttonText={this.props.intl.formatMessage({id: this.props.buttonText})}
|
<ProgressableButton onClick={this.props.form.onSubmit} className="button" disabled={!this.props.form.isValid} buttonText={this.props.intl.formatMessage({id: this.props.buttonText})}
|
||||||
inProgress={this.props.form.submitting || this.props.form.container().submitting} disableOnProgress={true}/>
|
inProgress={this.props.form.submitting || this.props.form.container().submitting} disableOnProgress={true}/>
|
||||||
</fieldset>
|
</fieldset>
|
||||||
|
|
|
@ -16,7 +16,7 @@ class RegistrationPage extends React.Component<RegistrationPageProps & InjectedI
|
||||||
|
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
return <div className="container"><h1><FormattedMessage id="registration.get_started.header"/></h1><p><FormattedMessage id="registration.get_started.description"/></p><RegistrationWizard/></div>
|
return <div className="tw-bs"><div className="container"><h1><FormattedMessage id="registration.get_started.header"/></h1><p><FormattedMessage id="registration.get_started.description"/></p><RegistrationWizard/></div></div>
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -47,18 +47,22 @@ class UserInfoForm extends React.Component<UserInfoFormProps & InjectedIntlProps
|
||||||
<BasicField field={this.props.form.$("name")}
|
<BasicField field={this.props.form.$("name")}
|
||||||
label={
|
label={
|
||||||
this.props.intl.formatMessage({id: "registration.wizard.contact.name.label"})}
|
this.props.intl.formatMessage({id: "registration.wizard.contact.name.label"})}
|
||||||
placeholder={this.props.intl.formatMessage({id: "registration.wizard.contact.name.placeholder"})}/>
|
placeholder={this.props.intl.formatMessage({id: "registration.wizard.contact.name.placeholder"})}
|
||||||
|
inputClassNames={"input-lg"}/>
|
||||||
<BasicField field={this.props.form.$('email')}
|
<BasicField field={this.props.form.$('email')}
|
||||||
label={this.props.intl.formatMessage({id: "registration.wizard.contact.email.label"})}
|
label={this.props.intl.formatMessage({id: "registration.wizard.contact.email.label"})}
|
||||||
placeholder={this.props.intl.formatMessage({id: "registration.wizard.contact.email.placeholder"})}
|
placeholder={this.props.intl.formatMessage({id: "registration.wizard.contact.email.placeholder"})}
|
||||||
|
inputClassNames={"input-lg"}
|
||||||
/>
|
/>
|
||||||
</TwoColumnFields>
|
</TwoColumnFields>
|
||||||
|
|
||||||
<BasicField field={this.props.form.$('password')}
|
<BasicField field={this.props.form.$('password')}
|
||||||
label={this.props.intl.formatMessage({id:'registration.wizard.contact.password.label'})}
|
label={this.props.intl.formatMessage({id:'registration.wizard.contact.password.label'})}
|
||||||
|
inputClassNames={"input-lg"}
|
||||||
/>
|
/>
|
||||||
<BasicField field={this.props.form.$('password_confirmation')}
|
<BasicField field={this.props.form.$('password_confirmation')}
|
||||||
label={this.props.intl.formatMessage({id:'registration.wizard.contact.password_confirmation.label'})}
|
label={this.props.intl.formatMessage({id:'registration.wizard.contact.password_confirmation.label'})}
|
||||||
|
inputClassNames={"input-lg"}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -105,9 +105,9 @@ class InnerSessionLoginForm extends React.Component<SessionLoginFormProps & Inje
|
||||||
|
|
||||||
return <form onSubmit={this.form.onSubmit}>
|
return <form onSubmit={this.form.onSubmit}>
|
||||||
<BasicField field={this.form.$('email')}
|
<BasicField field={this.form.$('email')}
|
||||||
label={this.props.intl.formatMessage({id: 'login.email'})}/>
|
label={this.props.intl.formatMessage({id: 'login.email'})} inputClassNames={"input-lg"}/>
|
||||||
<BasicField field={this.form.$('password')}
|
<BasicField field={this.form.$('password')}
|
||||||
label={this.props.intl.formatMessage({id: 'login.password'})}/>
|
label={this.props.intl.formatMessage({id: 'login.password'})} inputClassNames={"input-lg"}/>
|
||||||
{errorDiv}
|
{errorDiv}
|
||||||
<div className={'form-group'}>
|
<div className={'form-group'}>
|
||||||
<ProgressableButton onClick={this.form.onSubmit} className="button" disabled={!this.form.isValid || this.form.submitting} inProgress={this.form.submitting}
|
<ProgressableButton onClick={this.form.onSubmit} className="button" disabled={!this.form.isValid || this.form.submitting} inProgress={this.form.submitting}
|
||||||
|
|
|
@ -11,11 +11,11 @@ export interface SessionLoginPageProps
|
||||||
|
|
||||||
class SessionLoginPage extends React.Component<SessionLoginPageProps & InjectedIntlProps, {}> {
|
class SessionLoginPage extends React.Component<SessionLoginPageProps & InjectedIntlProps, {}> {
|
||||||
render() {
|
render() {
|
||||||
return <div className="container"><div className="row"><div className={'col-sm-6'}>
|
return <div className="tw-bs"><div className="container"><div className="row"><div className={'col-sm-6'}>
|
||||||
<h1><FormattedMessage id="login.header"/></h1>
|
<h1><FormattedMessage id="login.header"/></h1>
|
||||||
<SessionLoginForm buttonText="login.login" buttonTextOnProgress="login.logging_in"/>
|
<SessionLoginForm buttonText="login.login" buttonTextOnProgress="login.logging_in"/>
|
||||||
</div></div>
|
</div></div>
|
||||||
</div>;
|
</div></div>;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
99
javascripts/src/lib/api/create_offsite_donation.ts
Normal file
99
javascripts/src/lib/api/create_offsite_donation.ts
Normal file
|
@ -0,0 +1,99 @@
|
||||||
|
// License: LGPL-3.0-or-later
|
||||||
|
import * as $ from 'jquery';
|
||||||
|
import {Configuration} from "../../../api/configuration";
|
||||||
|
|
||||||
|
export class CreateOffsiteDonation {
|
||||||
|
protected basePath = '/';
|
||||||
|
public defaultHeaders: Array<string> = [];
|
||||||
|
public defaultExtraJQueryAjaxSettings?: JQueryAjaxSettings = null;
|
||||||
|
public configuration: Configuration = new Configuration();
|
||||||
|
|
||||||
|
constructor(basePath?: string, configuration?: Configuration, defaultExtraJQueryAjaxSettings?: JQueryAjaxSettings) {
|
||||||
|
if (basePath) {
|
||||||
|
this.basePath = basePath;
|
||||||
|
}
|
||||||
|
if (configuration) {
|
||||||
|
this.configuration = configuration;
|
||||||
|
}
|
||||||
|
if (defaultExtraJQueryAjaxSettings) {
|
||||||
|
this.defaultExtraJQueryAjaxSettings = defaultExtraJQueryAjaxSettings;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public postDonation(donation: CreateOffsiteDonationModel, nonprofitId: number, extraJQueryAjaxSettings?: JQueryAjaxSettings): Promise<any> {
|
||||||
|
let localVarPath = `${this.basePath}nonprofits/${nonprofitId}/donations/create_offsite`;
|
||||||
|
|
||||||
|
let queryParameters: any = {};
|
||||||
|
let headerParams: any = {};
|
||||||
|
// verify required parameter 'nonprofit' is not null or undefined
|
||||||
|
if (donation === null || donation === undefined) {
|
||||||
|
throw new Error('Required parameter nonprofit was null or undefined when calling postNonprofit.');
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
localVarPath = localVarPath + "?" + $.param(queryParameters);
|
||||||
|
// to determine the Content-Type header
|
||||||
|
let consumes: string[] = [
|
||||||
|
'application/json'
|
||||||
|
];
|
||||||
|
|
||||||
|
// to determine the Accept header
|
||||||
|
let produces: string[] = [
|
||||||
|
'application/json'
|
||||||
|
];
|
||||||
|
|
||||||
|
|
||||||
|
headerParams['Content-Type'] = 'application/json';
|
||||||
|
|
||||||
|
let requestOptions: JQueryAjaxSettings = {
|
||||||
|
url: localVarPath,
|
||||||
|
type: 'POST',
|
||||||
|
headers: headerParams,
|
||||||
|
processData: false
|
||||||
|
};
|
||||||
|
|
||||||
|
requestOptions.data = JSON.stringify({donation:donation});
|
||||||
|
if (headerParams['Content-Type']) {
|
||||||
|
requestOptions.contentType = headerParams['Content-Type'];
|
||||||
|
}
|
||||||
|
|
||||||
|
if (extraJQueryAjaxSettings) {
|
||||||
|
requestOptions = (<any>Object).assign(requestOptions, extraJQueryAjaxSettings);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.defaultExtraJQueryAjaxSettings) {
|
||||||
|
requestOptions = (<any>Object).assign(requestOptions, this.defaultExtraJQueryAjaxSettings);
|
||||||
|
}
|
||||||
|
|
||||||
|
let dfd = $.Deferred();
|
||||||
|
$.ajax(requestOptions).then(
|
||||||
|
(data: any, textStatus: string, jqXHR: JQueryXHR) =>
|
||||||
|
dfd.resolve(jqXHR, data),
|
||||||
|
(xhr: JQueryXHR, textStatus: string, errorThrown: string) => {
|
||||||
|
|
||||||
|
|
||||||
|
dfd.reject(xhr.responseJSON)
|
||||||
|
|
||||||
|
}
|
||||||
|
);
|
||||||
|
return dfd.promise();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
export interface CreateOffsiteDonationModel {
|
||||||
|
designation?:string
|
||||||
|
dedication?:string
|
||||||
|
comment?:string
|
||||||
|
|
||||||
|
|
||||||
|
amount?: number
|
||||||
|
supporter_id:number
|
||||||
|
nonprofit_id:number
|
||||||
|
date?:string
|
||||||
|
|
||||||
|
campaign_id?:string
|
||||||
|
event_id?: string
|
||||||
|
offsite_payment?:{check_number?: string}
|
||||||
|
}
|
||||||
|
|
100
javascripts/src/lib/api/put_donation.ts
Normal file
100
javascripts/src/lib/api/put_donation.ts
Normal file
|
@ -0,0 +1,100 @@
|
||||||
|
// License: LGPL-3.0-or-later
|
||||||
|
import * as $ from 'jquery';
|
||||||
|
import {Configuration} from "../../../api/configuration";
|
||||||
|
|
||||||
|
export class PutDonation {
|
||||||
|
protected basePath = '/';
|
||||||
|
public defaultHeaders: Array<string> = [];
|
||||||
|
public defaultExtraJQueryAjaxSettings?: JQueryAjaxSettings = null;
|
||||||
|
public configuration: Configuration = new Configuration();
|
||||||
|
|
||||||
|
constructor(basePath?: string, configuration?: Configuration, defaultExtraJQueryAjaxSettings?: JQueryAjaxSettings) {
|
||||||
|
if (basePath) {
|
||||||
|
this.basePath = basePath;
|
||||||
|
}
|
||||||
|
if (configuration) {
|
||||||
|
this.configuration = configuration;
|
||||||
|
}
|
||||||
|
if (defaultExtraJQueryAjaxSettings) {
|
||||||
|
this.defaultExtraJQueryAjaxSettings = defaultExtraJQueryAjaxSettings;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public putDonation(donation: UpdateDonationModel, nonprofitId: number, extraJQueryAjaxSettings?: JQueryAjaxSettings): Promise<any> {
|
||||||
|
let localVarPath = `${this.basePath}nonprofits/${nonprofitId}/donations/${donation.id}`;
|
||||||
|
|
||||||
|
let queryParameters: any = {};
|
||||||
|
let headerParams: any = {};
|
||||||
|
// verify required parameter 'nonprofit' is not null or undefined
|
||||||
|
if (donation === null || donation === undefined) {
|
||||||
|
throw new Error('Required parameter nonprofit was null or undefined when calling postNonprofit.');
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
localVarPath = localVarPath + "?" + $.param(queryParameters);
|
||||||
|
// to determine the Content-Type header
|
||||||
|
let consumes: string[] = [
|
||||||
|
'application/json'
|
||||||
|
];
|
||||||
|
|
||||||
|
// to determine the Accept header
|
||||||
|
let produces: string[] = [
|
||||||
|
'application/json'
|
||||||
|
];
|
||||||
|
|
||||||
|
|
||||||
|
headerParams['Content-Type'] = 'application/json';
|
||||||
|
|
||||||
|
let requestOptions: JQueryAjaxSettings = {
|
||||||
|
url: localVarPath,
|
||||||
|
type: 'PUT',
|
||||||
|
headers: headerParams,
|
||||||
|
processData: false
|
||||||
|
};
|
||||||
|
|
||||||
|
requestOptions.data = JSON.stringify({donation:donation.donation});
|
||||||
|
if (headerParams['Content-Type']) {
|
||||||
|
requestOptions.contentType = headerParams['Content-Type'];
|
||||||
|
}
|
||||||
|
|
||||||
|
if (extraJQueryAjaxSettings) {
|
||||||
|
requestOptions = (<any>Object).assign(requestOptions, extraJQueryAjaxSettings);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.defaultExtraJQueryAjaxSettings) {
|
||||||
|
requestOptions = (<any>Object).assign(requestOptions, this.defaultExtraJQueryAjaxSettings);
|
||||||
|
}
|
||||||
|
|
||||||
|
let dfd = $.Deferred();
|
||||||
|
$.ajax(requestOptions).then(
|
||||||
|
(data: any, textStatus: string, jqXHR: JQueryXHR) =>
|
||||||
|
dfd.resolve(jqXHR, data),
|
||||||
|
(xhr: JQueryXHR, textStatus: string, errorThrown: string) => {
|
||||||
|
|
||||||
|
|
||||||
|
dfd.reject(xhr.responseJSON)
|
||||||
|
|
||||||
|
}
|
||||||
|
);
|
||||||
|
return dfd.promise();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
interface UpdateDonationModelData {
|
||||||
|
designation?:string
|
||||||
|
dedication?:string
|
||||||
|
comment?:string
|
||||||
|
campaign_id:string
|
||||||
|
event_id: string
|
||||||
|
|
||||||
|
gross_amount?: number
|
||||||
|
fee_total?: number
|
||||||
|
check_number?:string
|
||||||
|
date?:string
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface UpdateDonationModel {
|
||||||
|
id:number
|
||||||
|
donation: UpdateDonationModelData
|
||||||
|
}
|
|
@ -1,4 +1,6 @@
|
||||||
// License: LGPL-3.0-or-later
|
// License: LGPL-3.0-or-later
|
||||||
import {WebUserSignInOut} from "./api/sign_in";
|
import {WebUserSignInOut} from "./api/sign_in";
|
||||||
|
import {PutDonation} from './api/put_donation';
|
||||||
|
import {CreateOffsiteDonation} from "./api/create_offsite_donation";
|
||||||
|
|
||||||
export const APIS = [WebUserSignInOut]
|
export const APIS = [WebUserSignInOut, PutDonation, CreateOffsiteDonation]
|
236
javascripts/src/lib/createNumberMask.spec.ts
Normal file
236
javascripts/src/lib/createNumberMask.spec.ts
Normal file
|
@ -0,0 +1,236 @@
|
||||||
|
//from: https://github.com/text-mask/text-mask/pull/760/commits/f66c81b62c4894b7da43862bee5943f659dc7537
|
||||||
|
// under: Unlicense
|
||||||
|
import createNumberMask from './createNumberMask'
|
||||||
|
describe('createNumberMask', () => {
|
||||||
|
it('can returns a configured currency mask', () => {
|
||||||
|
let numberMask = createNumberMask()
|
||||||
|
|
||||||
|
expect(typeof numberMask).toBe('function')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('takes a prefix', () => {
|
||||||
|
let numberMask = createNumberMask({prefix: '$'})
|
||||||
|
|
||||||
|
expect(numberMask('12')).toEqual(['$', /\d/, /\d/])
|
||||||
|
})
|
||||||
|
|
||||||
|
it('takes a suffix', () => {
|
||||||
|
let numberMask = createNumberMask({suffix: ' $', prefix: ''})
|
||||||
|
|
||||||
|
expect(numberMask('12')).toEqual([/\d/, /\d/, ' ', '$'])
|
||||||
|
})
|
||||||
|
|
||||||
|
it('works when the prefix contains numbers', () => {
|
||||||
|
let numberMask = createNumberMask({prefix: 'm2 '})
|
||||||
|
|
||||||
|
expect(numberMask('m2 1')).toEqual(['m', '2', ' ', /\d/])
|
||||||
|
})
|
||||||
|
|
||||||
|
it('works when the suffix contains numbers', () => {
|
||||||
|
let numberMask = createNumberMask({prefix: '', suffix: ' m2'})
|
||||||
|
|
||||||
|
expect(numberMask('1 m2')).toEqual([/\d/, ' ', 'm', '2'])
|
||||||
|
})
|
||||||
|
|
||||||
|
it('works when there is a decimal and the suffix contains numbers', () => {
|
||||||
|
let numberMask = createNumberMask({prefix: '', suffix: ' m2', allowDecimal: true})
|
||||||
|
|
||||||
|
expect(numberMask('1.2 m2')).toEqual([/\d/, '[]', '.', '[]', /\d/, ' ', 'm', '2'])
|
||||||
|
})
|
||||||
|
|
||||||
|
it('can be configured to add a thousands separator or not', () => {
|
||||||
|
let numberMaskWithoutThousandsSeparator = createNumberMask({includeThousandsSeparator: false})
|
||||||
|
expect(numberMaskWithoutThousandsSeparator('1000')).toEqual(['$', /\d/, /\d/, /\d/, /\d/])
|
||||||
|
|
||||||
|
let numberMaskWithThousandsSeparator = createNumberMask()
|
||||||
|
expect(numberMaskWithThousandsSeparator('1000')).toEqual(['$', /\d/, ',', /\d/, /\d/, /\d/])
|
||||||
|
})
|
||||||
|
|
||||||
|
it('can be configured with a custom character for the thousands separator', () => {
|
||||||
|
let numberMask = createNumberMask({thousandsSeparatorSymbol: '.'})
|
||||||
|
|
||||||
|
expect(numberMask('1000')).toEqual(['$', /\d/, '.', /\d/, /\d/, /\d/])
|
||||||
|
})
|
||||||
|
|
||||||
|
it('can be configured to accept a fraction and returns the fraction separator with caret traps', () => {
|
||||||
|
let numberMask = createNumberMask({allowDecimal: true})
|
||||||
|
|
||||||
|
expect(numberMask('1000.')).toEqual(['$', /\d/, ',', /\d/, /\d/, /\d/, '[]', '.', '[]'])
|
||||||
|
})
|
||||||
|
|
||||||
|
it('rejects fractions by default', () => {
|
||||||
|
let numberMask = createNumberMask()
|
||||||
|
|
||||||
|
expect(numberMask('1000.')).toEqual(['$', /\d/, ',', /\d/, /\d/, /\d/])
|
||||||
|
})
|
||||||
|
|
||||||
|
it('can be configured with a custom character for the fraction separator', () => {
|
||||||
|
let numberMask = createNumberMask({
|
||||||
|
allowDecimal: true,
|
||||||
|
decimalSymbol: ',',
|
||||||
|
thousandsSeparatorSymbol: '.'
|
||||||
|
})
|
||||||
|
|
||||||
|
expect(numberMask('1000,')).toEqual(['$', /\d/, '.', /\d/, /\d/, /\d/, '[]', ',', '[]'])
|
||||||
|
})
|
||||||
|
|
||||||
|
it('can limit the length of the fraction', () => {
|
||||||
|
let numberMask = createNumberMask({allowDecimal: true, decimalLimit: 2})
|
||||||
|
|
||||||
|
expect(numberMask('1000.3823')).toEqual(['$', /\d/, ',', /\d/, /\d/, /\d/, '[]', '.', '[]', /\d/, /\d/])
|
||||||
|
})
|
||||||
|
|
||||||
|
it('can require a fraction', () => {
|
||||||
|
let numberMask = createNumberMask({requireDecimal: true})
|
||||||
|
|
||||||
|
expect(numberMask('1000')).toEqual(['$', /\d/, ',', /\d/, /\d/, /\d/, '[]', '.', '[]'])
|
||||||
|
})
|
||||||
|
|
||||||
|
it('accepts negative integers', function() {
|
||||||
|
let numberMask = createNumberMask({allowNegative: true})
|
||||||
|
expect(numberMask('-$12')).toEqual([/-/, '$', /\d/, /\d/])
|
||||||
|
})
|
||||||
|
|
||||||
|
it('ignores multiple minus signs', function() {
|
||||||
|
let numberMask = createNumberMask({allowNegative: true})
|
||||||
|
expect(numberMask('--$12')).toEqual([/-/, '$', /\d/, /\d/])
|
||||||
|
})
|
||||||
|
|
||||||
|
it('adds a digit placeholder if the input is nothing but a minus sign in order to attract the caret', () => {
|
||||||
|
let numberMask = createNumberMask({allowNegative: true})
|
||||||
|
expect(numberMask('-')).toEqual([/-/, '$', /\d/])
|
||||||
|
})
|
||||||
|
|
||||||
|
it('starts with dot should be considered as decimal input', () => {
|
||||||
|
let numberMask = createNumberMask({prefix: '$', allowDecimal: true})
|
||||||
|
expect(numberMask('.')).toEqual(['$', '0', '.', /\d/])
|
||||||
|
|
||||||
|
numberMask = createNumberMask({prefix: '#', allowDecimal: true})
|
||||||
|
expect(numberMask('.')).toEqual(['#', '0', '.', /\d/])
|
||||||
|
|
||||||
|
numberMask = createNumberMask({prefix: '', allowDecimal: true})
|
||||||
|
expect(numberMask('.')).toEqual(['0', '.', /\d/])
|
||||||
|
|
||||||
|
numberMask = createNumberMask({allowDecimal: false})
|
||||||
|
expect(numberMask('.')).toEqual(['$'])
|
||||||
|
|
||||||
|
numberMask = createNumberMask({prefix: '', suffix: '$', allowDecimal: true})
|
||||||
|
expect(numberMask('.')).toEqual(['0', '.', /\d/, '$'])
|
||||||
|
})
|
||||||
|
|
||||||
|
it('can allow leading zeroes', function() {
|
||||||
|
let numberMask = createNumberMask({allowLeadingZeroes: true})
|
||||||
|
expect(numberMask('012')).toEqual(['$', /\d/, /\d/, /\d/])
|
||||||
|
})
|
||||||
|
|
||||||
|
it('works with large numbers when leading zeroes is false', function() {
|
||||||
|
let numberMask = createNumberMask({allowLeadingZeroes: false})
|
||||||
|
expect(numberMask('111111111111111111111111')).toEqual([
|
||||||
|
'$', /\d/, /\d/, /\d/, ',', /\d/, /\d/, /\d/, ',', /\d/, /\d/, /\d/, ',', /\d/, /\d/, /\d/, ',',
|
||||||
|
/\d/, /\d/, /\d/, ',', /\d/, /\d/, /\d/, ',', /\d/, /\d/, /\d/, ',', /\d/, /\d/, /\d/
|
||||||
|
])
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('integer limiting', () => {
|
||||||
|
it('can limit the length of the integer part', () => {
|
||||||
|
let numberMask = createNumberMask({integerLimit: 3})
|
||||||
|
expect(numberMask('1999')).toEqual(['$', /\d/, /\d/, /\d/])
|
||||||
|
})
|
||||||
|
|
||||||
|
it('works when there is a prefix', () => {
|
||||||
|
let numberMask = createNumberMask({integerLimit: 3, prefix: '$'})
|
||||||
|
expect(numberMask('$1999')).toEqual(['$', /\d/, /\d/, /\d/])
|
||||||
|
})
|
||||||
|
|
||||||
|
it('works when there is a thousands separator', () => {
|
||||||
|
expect(createNumberMask({integerLimit: 4, prefix: ''})('1,9995'))
|
||||||
|
.toEqual([/\d/, ',', /\d/, /\d/, /\d/])
|
||||||
|
|
||||||
|
expect(createNumberMask({integerLimit: 7, prefix: ''})('1,000,0001'))
|
||||||
|
.toEqual([/\d/, ',', /\d/, /\d/, /\d/, ',', /\d/, /\d/, /\d/])
|
||||||
|
})
|
||||||
|
|
||||||
|
it('works when there is a decimal and a prefix', () => {
|
||||||
|
let numberMask = createNumberMask({integerLimit: 3, allowDecimal: true})
|
||||||
|
expect(numberMask('$199.34')).toEqual(['$', /\d/, /\d/, /\d/, '[]', '.', '[]', /\d/, /\d/])
|
||||||
|
})
|
||||||
|
|
||||||
|
it('works when there is a decimal and no prefix', () => {
|
||||||
|
let numberMask = createNumberMask({integerLimit: 3, allowDecimal: true, prefix: ''})
|
||||||
|
expect(numberMask('199.34')).toEqual([/\d/, /\d/, /\d/, '[]', '.', '[]', /\d/, /\d/])
|
||||||
|
})
|
||||||
|
|
||||||
|
it('works when thousandsSeparatorSymbol is a period', () => {
|
||||||
|
let numberMask = createNumberMask({
|
||||||
|
prefix: '',
|
||||||
|
thousandsSeparatorSymbol: '.',
|
||||||
|
decimalSymbol: ',',
|
||||||
|
allowDecimal: true,
|
||||||
|
requireDecimal: true,
|
||||||
|
integerLimit: 5,
|
||||||
|
decimalLimit: 3,
|
||||||
|
})
|
||||||
|
expect(numberMask('1234567890,12345678'))
|
||||||
|
.toEqual([/\d/, /\d/, '.', /\d/, /\d/, /\d/, '[]', ',', '[]', /\d/, /\d/, /\d/])
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('numberMask default behavior', () => {
|
||||||
|
let numberMask:ReturnType<typeof createNumberMask> = null
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
numberMask = createNumberMask()
|
||||||
|
})
|
||||||
|
|
||||||
|
it('returns a mask that has the same number of digits as the given number', () => {
|
||||||
|
expect(numberMask('20382')).toEqual(['$', /\d/, /\d/, ',', /\d/, /\d/, /\d/])
|
||||||
|
})
|
||||||
|
|
||||||
|
it('uses the dollar symbol as the default prefix', () => {
|
||||||
|
expect(numberMask('1')).toEqual(['$', /\d/])
|
||||||
|
})
|
||||||
|
|
||||||
|
it('adds no suffix by default', () => {
|
||||||
|
expect(numberMask('1')).toEqual(['$', /\d/])
|
||||||
|
})
|
||||||
|
|
||||||
|
it('returns a mask that appends the currency symbol', () => {
|
||||||
|
expect(numberMask('1')).toEqual(['$', /\d/])
|
||||||
|
})
|
||||||
|
|
||||||
|
it('adds adds a comma after a thousand', () => {
|
||||||
|
expect(numberMask('1000')).toEqual(['$', /\d/, ',', /\d/, /\d/, /\d/])
|
||||||
|
})
|
||||||
|
|
||||||
|
it('adds as many commas as needed', () => {
|
||||||
|
expect(numberMask('23984209342084'))
|
||||||
|
.toEqual(
|
||||||
|
['$', /\d/, /\d/, ',', /\d/, /\d/, /\d/, ',', /\d/, /\d/, /\d/, ',', /\d/, /\d/, /\d/, ',', /\d/, /\d/, /\d/]
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('accepts any string and strips out any non-digit characters', () => {
|
||||||
|
expect(numberMask('h4x0r sp43k')).toEqual(['$', /\d/, ',', /\d/, /\d/, /\d/])
|
||||||
|
})
|
||||||
|
|
||||||
|
it('does not allow leading zeroes', function() {
|
||||||
|
let numberMask = createNumberMask()
|
||||||
|
expect(numberMask('012')).toEqual(['$', /\d/, /\d/])
|
||||||
|
})
|
||||||
|
|
||||||
|
it('allows one leading zero followed by a fraction', function() {
|
||||||
|
let numberMask = createNumberMask({allowDecimal: true})
|
||||||
|
expect(numberMask('0.12')).toEqual(['$', /\d/, '[]', '.', '[]', /\d/, /\d/])
|
||||||
|
})
|
||||||
|
|
||||||
|
it('ignores fixedDecimalScale when requireDecimal:false', () => {
|
||||||
|
let numberMask = createNumberMask({allowDecimal: true, fixedDecimalScale:true})
|
||||||
|
expect(numberMask('0.1')).toEqual(['$', /\d/, '[]', '.', '[]', /\d/])
|
||||||
|
})
|
||||||
|
|
||||||
|
it('fixedDecimalScale expands decimal', () => {
|
||||||
|
let numberMask = createNumberMask({requireDecimal: true, fixedDecimalScale:true})
|
||||||
|
expect(numberMask('0.1')).toEqual(['$', /\d/, '[]', '.', '[]', /\d/, /\d/])
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
183
javascripts/src/lib/createNumberMask.ts
Normal file
183
javascripts/src/lib/createNumberMask.ts
Normal file
|
@ -0,0 +1,183 @@
|
||||||
|
// License: Unlicense
|
||||||
|
// from: https://github.com/text-mask/text-mask/pull/760/commits/f66c81b62c4894b7da43862bee5943f659dc7537
|
||||||
|
|
||||||
|
const dollarSign = '$'
|
||||||
|
const emptyString = ''
|
||||||
|
const comma = ','
|
||||||
|
const period = '.'
|
||||||
|
const minus = '-'
|
||||||
|
const minusRegExp = /-/
|
||||||
|
const nonDigitsRegExp = /\D+/g
|
||||||
|
const number = 'number'
|
||||||
|
const digitRegExp = /\d/
|
||||||
|
const caretTrap = '[]'
|
||||||
|
|
||||||
|
|
||||||
|
interface NumberMaskProps {
|
||||||
|
prefix?: string
|
||||||
|
suffix?: string
|
||||||
|
includeThousandsSeparator?: boolean
|
||||||
|
thousandsSeparatorSymbol?: string
|
||||||
|
allowDecimal?:boolean
|
||||||
|
decimalSymbol?: string
|
||||||
|
decimalLimit?: number
|
||||||
|
integerLimit?: number
|
||||||
|
requireDecimal?: boolean
|
||||||
|
allowNegative?: boolean
|
||||||
|
allowLeadingZeroes?: boolean
|
||||||
|
fixedDecimalScale?:boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function createNumberMask({
|
||||||
|
prefix = dollarSign,
|
||||||
|
suffix = emptyString,
|
||||||
|
includeThousandsSeparator = true,
|
||||||
|
thousandsSeparatorSymbol = comma,
|
||||||
|
allowDecimal = false,
|
||||||
|
decimalSymbol = period,
|
||||||
|
decimalLimit = 2,
|
||||||
|
requireDecimal = false,
|
||||||
|
allowNegative = false,
|
||||||
|
allowLeadingZeroes = false,
|
||||||
|
fixedDecimalScale = false,
|
||||||
|
integerLimit = null
|
||||||
|
}:NumberMaskProps = {}) {
|
||||||
|
const prefixLength = prefix && prefix.length || 0
|
||||||
|
const suffixLength = suffix && suffix.length || 0
|
||||||
|
const thousandsSeparatorSymbolLength = thousandsSeparatorSymbol && thousandsSeparatorSymbol.length || 0
|
||||||
|
|
||||||
|
function numberMask(rawValue = emptyString) {
|
||||||
|
const rawValueLength = rawValue.length
|
||||||
|
|
||||||
|
if (
|
||||||
|
rawValue === emptyString ||
|
||||||
|
(rawValue[0] === prefix[0] && rawValueLength === 1)
|
||||||
|
) {
|
||||||
|
return stringMaskArray(prefix.split(emptyString)).concat([digitRegExp]).concat(suffix.split(emptyString))
|
||||||
|
} else if (
|
||||||
|
rawValue === decimalSymbol &&
|
||||||
|
allowDecimal
|
||||||
|
) {
|
||||||
|
return stringMaskArray(prefix.split(emptyString)).concat(['0', decimalSymbol, digitRegExp]).concat(suffix.split(emptyString))
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
const isNegative = ((rawValue[0] === minus) && allowNegative)
|
||||||
|
//If negative remove "-" sign
|
||||||
|
if (isNegative) {
|
||||||
|
rawValue = rawValue.toString().substr(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
const indexOfLastDecimal = rawValue.lastIndexOf(decimalSymbol)
|
||||||
|
const hasDecimal = indexOfLastDecimal !== -1
|
||||||
|
|
||||||
|
let integer
|
||||||
|
let fraction
|
||||||
|
let mask
|
||||||
|
|
||||||
|
// remove the suffix
|
||||||
|
if (rawValue.slice(suffixLength * -1) === suffix) {
|
||||||
|
rawValue = rawValue.slice(0, suffixLength * -1)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (hasDecimal && (allowDecimal || requireDecimal)) {
|
||||||
|
integer = rawValue.slice(rawValue.slice(0, prefixLength) === prefix ? prefixLength : 0, indexOfLastDecimal)
|
||||||
|
|
||||||
|
fraction = rawValue.slice(indexOfLastDecimal + 1, rawValueLength)
|
||||||
|
fraction = convertToMask(fraction.replace(nonDigitsRegExp, emptyString))
|
||||||
|
} else {
|
||||||
|
if (rawValue.slice(0, prefixLength) === prefix) {
|
||||||
|
integer = rawValue.slice(prefixLength)
|
||||||
|
} else {
|
||||||
|
integer = rawValue
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (integerLimit && typeof integerLimit === number) {
|
||||||
|
const thousandsSeparatorRegex = thousandsSeparatorSymbol === '.' ? '[.]' : `${thousandsSeparatorSymbol}`
|
||||||
|
const numberOfThousandSeparators = (integer.match(new RegExp(thousandsSeparatorRegex, 'g')) || []).length
|
||||||
|
|
||||||
|
integer = integer.slice(0, integerLimit + (numberOfThousandSeparators * thousandsSeparatorSymbolLength))
|
||||||
|
}
|
||||||
|
|
||||||
|
integer = integer.replace(nonDigitsRegExp, emptyString)
|
||||||
|
|
||||||
|
if (!allowLeadingZeroes) {
|
||||||
|
integer = integer.replace(/^0+(0$|[^0])/, '$1')
|
||||||
|
}
|
||||||
|
|
||||||
|
integer = (includeThousandsSeparator) ? addThousandsSeparator(integer, thousandsSeparatorSymbol) : integer
|
||||||
|
|
||||||
|
mask = convertToMask(integer)
|
||||||
|
|
||||||
|
if ((hasDecimal && allowDecimal) || requireDecimal === true) {
|
||||||
|
if (rawValue[indexOfLastDecimal - 1] !== decimalSymbol) {
|
||||||
|
mask.push(caretTrap)
|
||||||
|
}
|
||||||
|
|
||||||
|
mask.push(decimalSymbol, caretTrap)
|
||||||
|
|
||||||
|
if (fraction) {
|
||||||
|
if (typeof decimalLimit === number) {
|
||||||
|
fraction = fraction.slice(0, decimalLimit)
|
||||||
|
}
|
||||||
|
mask = mask.concat(fraction)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (requireDecimal === true) {
|
||||||
|
if (fixedDecimalScale === true) {
|
||||||
|
const decimalLimitRemaining = fraction ? decimalLimit - fraction.length : decimalLimit
|
||||||
|
for (var i = 0; i < decimalLimitRemaining; i++) {
|
||||||
|
mask.push(digitRegExp)
|
||||||
|
}
|
||||||
|
} else if (rawValue[indexOfLastDecimal - 1] === decimalSymbol) {
|
||||||
|
mask.push(digitRegExp)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (prefixLength > 0) {
|
||||||
|
let res:(string|RegExp)[] = prefix.split(emptyString)
|
||||||
|
mask = res.concat(mask)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isNegative) {
|
||||||
|
// If user is entering a negative number, add a mask placeholder spot to attract the caret to it.
|
||||||
|
if (mask.length === prefixLength) {
|
||||||
|
mask.push(digitRegExp)
|
||||||
|
}
|
||||||
|
|
||||||
|
mask = stringMaskArray([minusRegExp]).concat(mask)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (suffix.length > 0) {
|
||||||
|
mask = mask.concat(suffix.split(emptyString))
|
||||||
|
}
|
||||||
|
|
||||||
|
return mask
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
return numberMask
|
||||||
|
}
|
||||||
|
|
||||||
|
function stringMaskArray(i:(string|(string|RegExp)[])):(string|RegExp)[] {
|
||||||
|
if (typeof i === 'string'){
|
||||||
|
return [i]
|
||||||
|
}
|
||||||
|
return i
|
||||||
|
}
|
||||||
|
|
||||||
|
function convertToMask(strNumber:string):(string|RegExp)[] {
|
||||||
|
return strNumber
|
||||||
|
.split(emptyString)
|
||||||
|
.map((char) => digitRegExp.test(char) ? digitRegExp : char)
|
||||||
|
}
|
||||||
|
|
||||||
|
// http://stackoverflow.com/a/10899795/604296
|
||||||
|
function addThousandsSeparator(n:string, thousandsSeparatorSymbol:string) {
|
||||||
|
return n.replace(/\B(?=(\d{3})+(?!\d))/g, thousandsSeparatorSymbol)
|
||||||
|
}
|
63
javascripts/src/lib/date.ts
Normal file
63
javascripts/src/lib/date.ts
Normal file
|
@ -0,0 +1,63 @@
|
||||||
|
// License: LGPL-3.0-or-later
|
||||||
|
import * as moment from 'moment';
|
||||||
|
import 'moment-timezone'
|
||||||
|
|
||||||
|
function momentTz(date:string, timezone:string='UTC'):moment.Moment {
|
||||||
|
return moment.tz(date, "YYYY-MM-DD HH:mm:ss", 'UTC').tz(timezone)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Return a date in the format MM/DD/YY for a given date string or moment obj
|
||||||
|
export function readable_date(date?:string, timezone:string='UTC'):string {
|
||||||
|
if(!date) return
|
||||||
|
return momentTz(date,timezone).format("MM/DD/YYYY")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Given a created_at string (eg. Charge.last.created_at.to_s), convert it to a readable date-time string
|
||||||
|
export function readable_date_time(date?:string, timezone:string='UTC'):string {
|
||||||
|
if(!date) return
|
||||||
|
return momentTz(date,timezone).format("MM/DD/YYYY H:mma z")
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// converts the return value of readable_date_time to it's ISO equivalent
|
||||||
|
export function readable_date_time_to_iso(date?:string, timezone:string='UTC') {
|
||||||
|
if(!date) return
|
||||||
|
return moment.tz(date, 'MM/DD/YYYY H:mma z', timezone)
|
||||||
|
.tz('UTC')
|
||||||
|
.toISOString()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get the month number (eg 01,02...) for the given date string (or moment obj)
|
||||||
|
export function get_month(date:string|moment.Moment) {
|
||||||
|
var monthNum = moment(date).month()
|
||||||
|
return moment().month(monthNum).format('MMM')
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get the year (eg 2017) for the given date string (or moment obj)
|
||||||
|
export function get_year(date:string|moment.Moment) {
|
||||||
|
return moment(date).year()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get the day (number in the month) for the given date string (or moment obj)
|
||||||
|
export function get_day(date:string|moment.Moment) {
|
||||||
|
return moment(date).date()
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
export class NonprofitTimezonedDates {
|
||||||
|
constructor(readonly nonprofitTimezone:string){
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
readable_date(date?:string):string{
|
||||||
|
return readable_date(date, this.nonprofitTimezone || 'UTC')
|
||||||
|
}
|
||||||
|
|
||||||
|
readable_date_time(date?:string):string {
|
||||||
|
return readable_date_time(date, this.nonprofitTimezone || 'UTC')
|
||||||
|
}
|
||||||
|
|
||||||
|
readable_date_time_to_iso(date?:string):string {
|
||||||
|
return readable_date_time_to_iso(date, this.nonprofitTimezone || 'UTC')
|
||||||
|
}
|
||||||
|
}
|
26
javascripts/src/lib/dedication.ts
Normal file
26
javascripts/src/lib/dedication.ts
Normal file
|
@ -0,0 +1,26 @@
|
||||||
|
// License: LGPL-3.0-or-later
|
||||||
|
export interface Dedication {
|
||||||
|
type?:'honor'|'memory',
|
||||||
|
supporter_id?: number,
|
||||||
|
name?:string
|
||||||
|
contact?: {
|
||||||
|
email?: string,
|
||||||
|
phone?:string
|
||||||
|
address?:string
|
||||||
|
}
|
||||||
|
note?:string
|
||||||
|
}
|
||||||
|
|
||||||
|
export function parseDedication(dedication?:string) : Dedication {
|
||||||
|
if (!dedication || dedication == "")
|
||||||
|
return {}
|
||||||
|
return JSON.parse(dedication)
|
||||||
|
}
|
||||||
|
|
||||||
|
export function serializeDedication(dedication:Dedication) : string {
|
||||||
|
return JSON.stringify(dedication)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
15
javascripts/src/lib/deprecated_format.ts
Normal file
15
javascripts/src/lib/deprecated_format.ts
Normal file
|
@ -0,0 +1,15 @@
|
||||||
|
// License: LGPL-3.0-or-later
|
||||||
|
|
||||||
|
export function pluralize(quantity:number, plural_word:string) : string{
|
||||||
|
var str = String(quantity) + ' '
|
||||||
|
if(quantity !== 1) return str+plural_word
|
||||||
|
else return str + to_singular(plural_word)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
export function to_singular(plural_word:string) : string {
|
||||||
|
return plural_word
|
||||||
|
.replace(/ies$/, 'y')
|
||||||
|
.replace(/oes$/, 'o')
|
||||||
|
.replace(/s$/, '')
|
||||||
|
}
|
22
javascripts/src/lib/format.spec.ts
Normal file
22
javascripts/src/lib/format.spec.ts
Normal file
|
@ -0,0 +1,22 @@
|
||||||
|
// License: LGPL-3.0-or-later
|
||||||
|
import * as Format from './format'
|
||||||
|
import 'jest';
|
||||||
|
|
||||||
|
|
||||||
|
describe('Format.dollarsToCents', () => {
|
||||||
|
|
||||||
|
const expectedAmount = 120
|
||||||
|
|
||||||
|
test("accepts negative amounts",() =>
|
||||||
|
expect(Format.dollarsToCents("-1.20")).toBe(expectedAmount* -1)
|
||||||
|
)
|
||||||
|
|
||||||
|
test("strips commas and dollar signs",() =>
|
||||||
|
expect(Format.dollarsToCents("$1,20")).toBe(expectedAmount* 100)
|
||||||
|
)
|
||||||
|
|
||||||
|
test("properly handles slightly shorter than normal decimals",() =>
|
||||||
|
expect(Format.dollarsToCents("$1.2")).toBe(expectedAmount)
|
||||||
|
)
|
||||||
|
|
||||||
|
})
|
57
javascripts/src/lib/format.ts
Normal file
57
javascripts/src/lib/format.ts
Normal file
|
@ -0,0 +1,57 @@
|
||||||
|
// License: LGPL-3.0-or-later
|
||||||
|
|
||||||
|
import * as deprecated_format from './deprecated_format'
|
||||||
|
|
||||||
|
export function centsToDollars(cents:string|number|undefined, options:{noCents?:boolean}={}):string {
|
||||||
|
if(cents === undefined) return '0'
|
||||||
|
let centsAsNumber:number = undefined
|
||||||
|
if (typeof cents === 'string')
|
||||||
|
{
|
||||||
|
centsAsNumber = Number(cents)
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
centsAsNumber = cents
|
||||||
|
}
|
||||||
|
return numberWithCommas((centsAsNumber / 100.0).toFixed(options.noCents ? 0 : 2).toString()).replace(/\.00$/,'')
|
||||||
|
}
|
||||||
|
|
||||||
|
export function dollarsToCents(dollars:string) {
|
||||||
|
//strips
|
||||||
|
dollars = dollars.toString().replace(/[$,]/g, '')
|
||||||
|
if(dollars.match(/^-?\d+\.\d$/)) {
|
||||||
|
// could we use toFixed instead? Probably but this is straightforward.
|
||||||
|
dollars = dollars + "0"
|
||||||
|
}
|
||||||
|
if(!dollars.match(/^-?\d+(\.\d\d)?$/)) throw "Invalid dollar amount: " + dollars
|
||||||
|
return Math.round(Number(dollars) * 100)
|
||||||
|
}
|
||||||
|
|
||||||
|
export function numberWithCommas(n:string|number):string {
|
||||||
|
return String(n).replace(/\B(?=(\d{3})+(?!\d))/g, ",")
|
||||||
|
}
|
||||||
|
|
||||||
|
export function camelToWords(str:string, os?:any) {
|
||||||
|
if(!str) return str
|
||||||
|
return str.replace(/([A-Z])/g, " $1")
|
||||||
|
}
|
||||||
|
|
||||||
|
export function readableKind(kind:string) {
|
||||||
|
if (kind === "Donation") return "One-Time Donation"
|
||||||
|
else if (kind === "OffsitePayment") return "Offsite Donation"
|
||||||
|
else if (kind === "Ticket") return "Ticket Purchase"
|
||||||
|
else return camelToWords(kind)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
export function readableInterval(interval:number, time_unit:string) {
|
||||||
|
if(interval === 1) return time_unit + 'ly'
|
||||||
|
if(interval === 4 && time_unit === 'year') return 'quarterly'
|
||||||
|
if(interval === 2 && time_unit === 'year') return 'biannually'
|
||||||
|
if(interval === 2 && time_unit === 'week') return 'biweekly'
|
||||||
|
if(interval === 2 && time_unit === 'month') return 'bimonthly'
|
||||||
|
else return 'every ' + deprecated_format.pluralize(Number(interval), time_unit + 's')
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
5
javascripts/src/lib/mobx_utils.ts
Normal file
5
javascripts/src/lib/mobx_utils.ts
Normal file
|
@ -0,0 +1,5 @@
|
||||||
|
import {FieldDefinition} from "mobx-react-form";
|
||||||
|
|
||||||
|
export function createFieldDefinition<TInOut>(fieldDef:FieldDefinition<TInOut>) : FieldDefinition<TInOut> {
|
||||||
|
return fieldDef;
|
||||||
|
}
|
17
javascripts/src/lib/utils.ts
Normal file
17
javascripts/src/lib/utils.ts
Normal file
|
@ -0,0 +1,17 @@
|
||||||
|
// License: LGPL-3.0-or-later
|
||||||
|
export function castToNullIfUndef<T>(i:T): T | null{
|
||||||
|
return i === undefined ? null : i
|
||||||
|
}
|
||||||
|
|
||||||
|
export function isBlank(i:null|undefined|string) : boolean {
|
||||||
|
return i === null || i === undefined || i === '';
|
||||||
|
}
|
||||||
|
|
||||||
|
export function isFilled(i:null|undefined|string) : boolean {
|
||||||
|
return !isBlank(i)
|
||||||
|
}
|
||||||
|
|
||||||
|
export function castToUndefinedIfBlank(i:null|undefined|string) :
|
||||||
|
string | undefined {
|
||||||
|
return isBlank(i) ? undefined : i;
|
||||||
|
}
|
|
@ -1,6 +1,7 @@
|
||||||
// License: LGPL-3.0-or-later
|
// License: LGPL-3.0-or-later
|
||||||
import * as Regex from './regex'
|
import * as Regex from './regex'
|
||||||
import {Field, Form} from "mobx-react-form";
|
import {Field, Form} from "mobx-react-form";
|
||||||
|
import moment = require("moment");
|
||||||
|
|
||||||
|
|
||||||
interface ValidationInput {
|
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.get('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 = field.get('value')
|
||||||
|
return [
|
||||||
|
(flip ? -1 * float : float) <= value,
|
||||||
|
`${field.label} must be no more than ${value}`
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
static optional(validation:Validation) : Validation {
|
static optional(validation:Validation) : Validation {
|
||||||
return ({field, form, validator}:ValidationInput) => {
|
return ({field, form, validator}:ValidationInput) => {
|
||||||
if (!field.value || validator.isEmpty(field.value)){
|
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}`
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
|
@ -154,4 +154,70 @@ module Mailchimp
|
||||||
.execute.map{|h| h['mailchimp_list_id']}
|
.execute.map{|h| h['mailchimp_list_id']}
|
||||||
end
|
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?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
|
end
|
||||||
|
|
44
lib/maintain/maintain_dedications.rb
Normal file
44
lib/maintain/maintain_dedications.rb
Normal file
|
@ -0,0 +1,44 @@
|
||||||
|
# License: AGPL-3.0-or-later WITH Web-Template-Output-Additional-Permission-3.0-or-later
|
||||||
|
module MaintainDedications
|
||||||
|
def self.retrieve_json_dedications
|
||||||
|
return Qx.select('id', 'dedication').from(:donations)
|
||||||
|
.where("is_valid_json(dedication)").ex
|
||||||
|
|
||||||
|
end
|
||||||
|
|
||||||
|
def self.retrieve_non_json_dedications(include_blank=false)
|
||||||
|
temp = Qx.select('id', 'dedication').from(:donations)
|
||||||
|
temp = temp.where("dedication IS NOT NULL AND dedication != ''") unless include_blank
|
||||||
|
temp = temp.and_where("NOT is_valid_json(dedication)")
|
||||||
|
return temp.ex
|
||||||
|
end
|
||||||
|
|
||||||
|
def self.create_json_dedications_from_plain_text(dedications)
|
||||||
|
dedications.map do |i|
|
||||||
|
output = {id: i['id']}
|
||||||
|
if i['dedication'] =~ /(((in (loving )?)?memory of|in memorium)\:? )(.+)/i
|
||||||
|
output[:dedication] = JSON.generate({type: 'memory', note: $+ })
|
||||||
|
elsif i['dedication'] =~ /((in honor of|honor of)\:? )(.+)/i
|
||||||
|
output[:dedication] = JSON.generate({type: 'honor', note: $+ })
|
||||||
|
else
|
||||||
|
output[:dedication] = JSON.generate({type: 'honor', note: i['dedication'] })
|
||||||
|
end
|
||||||
|
output
|
||||||
|
end.each do |i|
|
||||||
|
Qx.update(:donations).where('id = $id', {id: i[:id]}).set({dedication: i[:dedication]}).ex
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def self.add_honor_to_any_json_dedications_without_type(json_dedications)
|
||||||
|
json_dedications.map{|i| {'id' => i['id'], 'dedication' => JSON::parse(i['dedication']) }}
|
||||||
|
.select{|i| !%w(honor memory).include?(i['dedication']['type'])}
|
||||||
|
.map{|i| i['dedication']['type'] = 'honor'; i }
|
||||||
|
.each do |i|
|
||||||
|
Qx.update(:donations).where('id = $id', id: i['id'])
|
||||||
|
.set(dedication: JSON.generate(i['dedication'])).ex
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
end
|
|
@ -335,6 +335,11 @@ module QueryPayments
|
||||||
}
|
}
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
||||||
|
def self.get_dedication_or_empty(*path)
|
||||||
|
"json_extract_path_text(coalesce(nullif(trim(both from donations.dedication), ''), '{}')::json, #{path.map{|i| "'#{i}'"}.join(',')})"
|
||||||
|
end
|
||||||
|
|
||||||
def self.export_selects
|
def self.export_selects
|
||||||
["to_char(payments.date::timestamptz, 'YYYY-MM-DD HH24:MI:SS TZ') AS date",
|
["to_char(payments.date::timestamptz, 'YYYY-MM-DD HH24:MI:SS TZ') AS date",
|
||||||
'(payments.gross_amount / 100.0)::money::text AS gross_amount',
|
'(payments.gross_amount / 100.0)::money::text AS gross_amount',
|
||||||
|
@ -344,7 +349,13 @@ module QueryPayments
|
||||||
.concat(QuerySupporters.supporter_export_selections)
|
.concat(QuerySupporters.supporter_export_selections)
|
||||||
.concat([
|
.concat([
|
||||||
"coalesce(donations.designation, 'None') AS designation",
|
"coalesce(donations.designation, 'None') AS designation",
|
||||||
'donations.dedication AS "Honorarium/Memorium"',
|
"#{get_dedication_or_empty('type')}::text AS \"Dedication Type\"",
|
||||||
|
"#{get_dedication_or_empty('name')}::text AS \"Dedicated To: Name\"",
|
||||||
|
"#{get_dedication_or_empty('supporter_id')}::text AS \"Dedicated To: Supporter ID\"",
|
||||||
|
"#{get_dedication_or_empty('contact', 'email')}::text AS \"Dedicated To: Email\"",
|
||||||
|
"#{get_dedication_or_empty('contact', "phone")}::text AS \"Dedicated To: Phone\"",
|
||||||
|
"#{get_dedication_or_empty( "contact", "address")}::text AS \"Dedicated To: Address\"",
|
||||||
|
"#{get_dedication_or_empty( "note")}::text AS \"Dedicated To: Note\"",
|
||||||
'donations.anonymous',
|
'donations.anonymous',
|
||||||
'donations.comment',
|
'donations.comment',
|
||||||
"coalesce(nullif(campaigns_for_export.name, ''), 'None') AS campaign",
|
"coalesce(nullif(campaigns_for_export.name, ''), 'None') AS campaign",
|
||||||
|
|
|
@ -630,16 +630,13 @@ UNION DISTINCT
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
if (time_range_params[:start])
|
if (time_range_params[:start])
|
||||||
wip = time_range_params[:start].is_a?(DateTime) ? time_range_params[:start] : nil
|
start = parse_convert_datetime(time_range_params[:start])
|
||||||
if (wip.nil? && time_range_params[:start].is_a?(Date))
|
if (time_range_params[:end])
|
||||||
wip = time_range_params[:start].to_datetime
|
end_datetime = parse_convert_datetime(time_range_params[:end])
|
||||||
end
|
|
||||||
if(wip.nil? && time_range_params[:start].is_a?(String))
|
|
||||||
wip = DateTime.parse(time_range_params[:start])
|
|
||||||
end
|
end
|
||||||
|
|
||||||
unless wip.nil?
|
unless start.nil?
|
||||||
return wip, wip + 1.year
|
return start, end_datetime ? end_datetime : start + 1.year
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
raise ArgumentError.new("no valid time range provided")
|
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 = Supporter.where('supporters.nonprofit_id = ?', npo_id).includes(:recurring_donations)
|
||||||
supporters.select{|s| s.recurring_donations.select{|rd| rd.active }.length > 1}
|
supporters.select{|s| s.recurring_donations.select{|rd| rd.active }.length > 1}
|
||||||
end
|
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
|
end
|
||||||
|
|
||||||
|
|
|
@ -91,7 +91,7 @@ module UpdateDonation
|
||||||
# edits_to_payments
|
# edits_to_payments
|
||||||
if is_offsite
|
if is_offsite
|
||||||
#if offline, set date, gross_amount, fee_total, net_amount
|
#if offline, set date, gross_amount, fee_total, net_amount
|
||||||
existing_payment.towards = data[:dedication] if data[:dedication]
|
existing_payment.towards = data[:designation] if data[:designation]
|
||||||
existing_payment.date = data[:date] if data[:date]
|
existing_payment.date = data[:date] if data[:date]
|
||||||
existing_payment.gross_amount = data[:gross_amount] if data[:gross_amount]
|
existing_payment.gross_amount = data[:gross_amount] if data[:gross_amount]
|
||||||
existing_payment.fee_total = data[:fee_total] if data[:fee_total]
|
existing_payment.fee_total = data[:fee_total] if data[:fee_total]
|
||||||
|
|
|
@ -9,7 +9,7 @@ module UpdateTickets
|
||||||
bid_id: {is_integer: true},
|
bid_id: {is_integer: true},
|
||||||
#note: nothing to check?
|
#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
|
edited = true
|
||||||
end
|
end
|
||||||
|
|
||||||
if data[:checked_in]
|
unless data[:checked_in].nil?
|
||||||
entities[:ticket_id].checked_in = data[:checked_in]
|
entities[:ticket_id].checked_in = data[:checked_in]
|
||||||
edited = true
|
edited = true
|
||||||
end
|
end
|
||||||
|
|
143
package-lock.json
generated
143
package-lock.json
generated
|
@ -141,6 +141,15 @@
|
||||||
"integrity": "sha512-WD2vUOKfBBVHxWUV9iMR9RMfpuf8HquxWeAq2yqGVL7Nc4JW2+sQama0pREMqzNI3Tutj0PyxYUJwuoxxvX+xA==",
|
"integrity": "sha512-WD2vUOKfBBVHxWUV9iMR9RMfpuf8HquxWeAq2yqGVL7Nc4JW2+sQama0pREMqzNI3Tutj0PyxYUJwuoxxvX+xA==",
|
||||||
"dev": true
|
"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==",
|
||||||
|
"dev": true,
|
||||||
|
"requires": {
|
||||||
|
"moment": ">=2.14.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"@types/node": {
|
"@types/node": {
|
||||||
"version": "10.0.6",
|
"version": "10.0.6",
|
||||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-10.0.6.tgz",
|
"resolved": "https://registry.npmjs.org/@types/node/-/node-10.0.6.tgz",
|
||||||
|
@ -190,6 +199,15 @@
|
||||||
"@types/react": "*"
|
"@types/react": "*"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"@types/react-text-mask": {
|
||||||
|
"version": "5.4.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/react-text-mask/-/react-text-mask-5.4.2.tgz",
|
||||||
|
"integrity": "sha512-R4h07wAeOPh6xc6E9qYPMgYpeuUJ1w3rs3oWwp9oWIVPtnAGxPGDuCPEs2+ynlP5syeM7heZeZeM8saAHRgENA==",
|
||||||
|
"dev": true,
|
||||||
|
"requires": {
|
||||||
|
"@types/react": "*"
|
||||||
|
}
|
||||||
|
},
|
||||||
"@types/sinon": {
|
"@types/sinon": {
|
||||||
"version": "4.3.3",
|
"version": "4.3.3",
|
||||||
"resolved": "https://registry.npmjs.org/@types/sinon/-/sinon-4.3.3.tgz",
|
"resolved": "https://registry.npmjs.org/@types/sinon/-/sinon-4.3.3.tgz",
|
||||||
|
@ -1843,9 +1861,8 @@
|
||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
"bootstrap-loader": {
|
"bootstrap-loader": {
|
||||||
"version": "2.2.0",
|
"version": "github:houdiniproject/bootstrap-loader#53cdc907485ba21c72470f2d8fb7011c616c823b",
|
||||||
"resolved": "https://registry.npmjs.org/bootstrap-loader/-/bootstrap-loader-2.2.0.tgz",
|
"from": "github:houdiniproject/bootstrap-loader#compiled_namespaced",
|
||||||
"integrity": "sha512-LG8/klminqsCCtPDDCMSCA50LdzmoRvC7JpvJAFFeqWAbSfSY0hZkPUEk5X4wygf33JuFGyiJ7CH/KVnT65I6A==",
|
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"requires": {
|
"requires": {
|
||||||
"chalk": "^1.1.3",
|
"chalk": "^1.1.3",
|
||||||
|
@ -4764,6 +4781,23 @@
|
||||||
"resolved": "https://registry.npmjs.org/focus-group/-/focus-group-0.3.1.tgz",
|
"resolved": "https://registry.npmjs.org/focus-group/-/focus-group-0.3.1.tgz",
|
||||||
"integrity": "sha1-4PMu2GsNq91v/OvfiY7LMuR/7c4="
|
"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": {
|
"for-in": {
|
||||||
"version": "1.0.2",
|
"version": "1.0.2",
|
||||||
"resolved": "https://registry.npmjs.org/for-in/-/for-in-1.0.2.tgz",
|
"resolved": "https://registry.npmjs.org/for-in/-/for-in-1.0.2.tgz",
|
||||||
|
@ -4908,14 +4942,12 @@
|
||||||
"balanced-match": {
|
"balanced-match": {
|
||||||
"version": "1.0.0",
|
"version": "1.0.0",
|
||||||
"bundled": true,
|
"bundled": true,
|
||||||
"dev": true,
|
"dev": true
|
||||||
"optional": true
|
|
||||||
},
|
},
|
||||||
"brace-expansion": {
|
"brace-expansion": {
|
||||||
"version": "1.1.11",
|
"version": "1.1.11",
|
||||||
"bundled": true,
|
"bundled": true,
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"optional": true,
|
|
||||||
"requires": {
|
"requires": {
|
||||||
"balanced-match": "^1.0.0",
|
"balanced-match": "^1.0.0",
|
||||||
"concat-map": "0.0.1"
|
"concat-map": "0.0.1"
|
||||||
|
@ -4930,20 +4962,17 @@
|
||||||
"code-point-at": {
|
"code-point-at": {
|
||||||
"version": "1.1.0",
|
"version": "1.1.0",
|
||||||
"bundled": true,
|
"bundled": true,
|
||||||
"dev": true,
|
"dev": true
|
||||||
"optional": true
|
|
||||||
},
|
},
|
||||||
"concat-map": {
|
"concat-map": {
|
||||||
"version": "0.0.1",
|
"version": "0.0.1",
|
||||||
"bundled": true,
|
"bundled": true,
|
||||||
"dev": true,
|
"dev": true
|
||||||
"optional": true
|
|
||||||
},
|
},
|
||||||
"console-control-strings": {
|
"console-control-strings": {
|
||||||
"version": "1.1.0",
|
"version": "1.1.0",
|
||||||
"bundled": true,
|
"bundled": true,
|
||||||
"dev": true,
|
"dev": true
|
||||||
"optional": true
|
|
||||||
},
|
},
|
||||||
"core-util-is": {
|
"core-util-is": {
|
||||||
"version": "1.0.2",
|
"version": "1.0.2",
|
||||||
|
@ -5060,8 +5089,7 @@
|
||||||
"inherits": {
|
"inherits": {
|
||||||
"version": "2.0.3",
|
"version": "2.0.3",
|
||||||
"bundled": true,
|
"bundled": true,
|
||||||
"dev": true,
|
"dev": true
|
||||||
"optional": true
|
|
||||||
},
|
},
|
||||||
"ini": {
|
"ini": {
|
||||||
"version": "1.3.5",
|
"version": "1.3.5",
|
||||||
|
@ -5073,7 +5101,6 @@
|
||||||
"version": "1.0.0",
|
"version": "1.0.0",
|
||||||
"bundled": true,
|
"bundled": true,
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"optional": true,
|
|
||||||
"requires": {
|
"requires": {
|
||||||
"number-is-nan": "^1.0.0"
|
"number-is-nan": "^1.0.0"
|
||||||
}
|
}
|
||||||
|
@ -5088,7 +5115,6 @@
|
||||||
"version": "3.0.4",
|
"version": "3.0.4",
|
||||||
"bundled": true,
|
"bundled": true,
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"optional": true,
|
|
||||||
"requires": {
|
"requires": {
|
||||||
"brace-expansion": "^1.1.7"
|
"brace-expansion": "^1.1.7"
|
||||||
}
|
}
|
||||||
|
@ -5096,14 +5122,12 @@
|
||||||
"minimist": {
|
"minimist": {
|
||||||
"version": "0.0.8",
|
"version": "0.0.8",
|
||||||
"bundled": true,
|
"bundled": true,
|
||||||
"dev": true,
|
"dev": true
|
||||||
"optional": true
|
|
||||||
},
|
},
|
||||||
"minipass": {
|
"minipass": {
|
||||||
"version": "2.2.4",
|
"version": "2.2.4",
|
||||||
"bundled": true,
|
"bundled": true,
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"optional": true,
|
|
||||||
"requires": {
|
"requires": {
|
||||||
"safe-buffer": "^5.1.1",
|
"safe-buffer": "^5.1.1",
|
||||||
"yallist": "^3.0.0"
|
"yallist": "^3.0.0"
|
||||||
|
@ -5122,7 +5146,6 @@
|
||||||
"version": "0.5.1",
|
"version": "0.5.1",
|
||||||
"bundled": true,
|
"bundled": true,
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"optional": true,
|
|
||||||
"requires": {
|
"requires": {
|
||||||
"minimist": "0.0.8"
|
"minimist": "0.0.8"
|
||||||
}
|
}
|
||||||
|
@ -5203,8 +5226,7 @@
|
||||||
"number-is-nan": {
|
"number-is-nan": {
|
||||||
"version": "1.0.1",
|
"version": "1.0.1",
|
||||||
"bundled": true,
|
"bundled": true,
|
||||||
"dev": true,
|
"dev": true
|
||||||
"optional": true
|
|
||||||
},
|
},
|
||||||
"object-assign": {
|
"object-assign": {
|
||||||
"version": "4.1.1",
|
"version": "4.1.1",
|
||||||
|
@ -5216,7 +5238,6 @@
|
||||||
"version": "1.4.0",
|
"version": "1.4.0",
|
||||||
"bundled": true,
|
"bundled": true,
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"optional": true,
|
|
||||||
"requires": {
|
"requires": {
|
||||||
"wrappy": "1"
|
"wrappy": "1"
|
||||||
}
|
}
|
||||||
|
@ -5338,7 +5359,6 @@
|
||||||
"version": "1.0.2",
|
"version": "1.0.2",
|
||||||
"bundled": true,
|
"bundled": true,
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"optional": true,
|
|
||||||
"requires": {
|
"requires": {
|
||||||
"code-point-at": "^1.0.0",
|
"code-point-at": "^1.0.0",
|
||||||
"is-fullwidth-code-point": "^1.0.0",
|
"is-fullwidth-code-point": "^1.0.0",
|
||||||
|
@ -9190,9 +9210,9 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"moment": {
|
"moment": {
|
||||||
"version": "2.9.0",
|
"version": "2.22.2",
|
||||||
"resolved": "https://registry.npmjs.org/moment/-/moment-2.9.0.tgz",
|
"resolved": "https://registry.npmjs.org/moment/-/moment-2.22.2.tgz",
|
||||||
"integrity": "sha1-d+wRdfopT0JifxDI5t5jAsA29tU="
|
"integrity": "sha1-PCV/mDn8DpP/UxSWMiOeuQeD/2Y="
|
||||||
},
|
},
|
||||||
"moment-range": {
|
"moment-range": {
|
||||||
"version": "2.2.0",
|
"version": "2.2.0",
|
||||||
|
@ -9200,11 +9220,11 @@
|
||||||
"integrity": "sha1-sCV1d4pKxQpld3k59cXXV/g/zYU="
|
"integrity": "sha1-sCV1d4pKxQpld3k59cXXV/g/zYU="
|
||||||
},
|
},
|
||||||
"moment-timezone": {
|
"moment-timezone": {
|
||||||
"version": "0.4.1",
|
"version": "0.5.21",
|
||||||
"resolved": "https://registry.npmjs.org/moment-timezone/-/moment-timezone-0.4.1.tgz",
|
"resolved": "https://registry.npmjs.org/moment-timezone/-/moment-timezone-0.5.21.tgz",
|
||||||
"integrity": "sha1-gfWYw61eIs2teWtn7NjYjQ9bqgY=",
|
"integrity": "sha512-j96bAh4otsgj3lKydm3K7kdtA3iKf2m6MY2iSYCzCm5a1zmHo1g+aK3068dDEeocLZQIS9kU8bsdQHLqEvgW0A==",
|
||||||
"requires": {
|
"requires": {
|
||||||
"moment": ">= 2.6.0"
|
"moment": ">= 2.9.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"moo": {
|
"moo": {
|
||||||
|
@ -11809,6 +11829,16 @@
|
||||||
"prop-types": "^15.6.0"
|
"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": {
|
"react-autocomplete": {
|
||||||
"version": "1.8.1",
|
"version": "1.8.1",
|
||||||
"resolved": "https://registry.npmjs.org/react-autocomplete/-/react-autocomplete-1.8.1.tgz",
|
"resolved": "https://registry.npmjs.org/react-autocomplete/-/react-autocomplete-1.8.1.tgz",
|
||||||
|
@ -11818,6 +11848,11 @@
|
||||||
"prop-types": "^15.5.10"
|
"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": {
|
"react-dom": {
|
||||||
"version": "16.3.2",
|
"version": "16.3.2",
|
||||||
"resolved": "https://registry.npmjs.org/react-dom/-/react-dom-16.3.2.tgz",
|
"resolved": "https://registry.npmjs.org/react-dom/-/react-dom-16.3.2.tgz",
|
||||||
|
@ -11841,9 +11876,10 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"react-is": {
|
"react-is": {
|
||||||
"version": "16.3.2",
|
"version": "16.5.2",
|
||||||
"resolved": "https://registry.npmjs.org/react-is/-/react-is-16.3.2.tgz",
|
"resolved": "https://registry.npmjs.org/react-is/-/react-is-16.5.2.tgz",
|
||||||
"integrity": "sha512-ybEM7YOr4yBgFd6w8dJqwxegqZGJNBZl6U27HnGKuTZmDvVrD5quWOK/wAnMywiZzW+Qsk+l4X2c70+thp/A8Q=="
|
"integrity": "sha512-hSl7E6l25GTjNEZATqZIuWOgSnpXb3kD0DVCujmg46K5zLxsbiKaaT6VO9slkSBDPZfYs30lwfJwbOFOnoEnKQ==",
|
||||||
|
"dev": true
|
||||||
},
|
},
|
||||||
"react-lifecycles-compat": {
|
"react-lifecycles-compat": {
|
||||||
"version": "3.0.4",
|
"version": "3.0.4",
|
||||||
|
@ -11863,14 +11899,27 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"react-test-renderer": {
|
"react-test-renderer": {
|
||||||
"version": "16.3.2",
|
"version": "16.5.2",
|
||||||
"resolved": "https://registry.npmjs.org/react-test-renderer/-/react-test-renderer-16.3.2.tgz",
|
"resolved": "https://registry.npmjs.org/react-test-renderer/-/react-test-renderer-16.5.2.tgz",
|
||||||
"integrity": "sha512-lL8WHIpCTMdSe+CRkt0rfMxBkJFyhVrpdQ54BaJRIrXf9aVmbeHbRA8GFRpTvohPN5tPzMabmrzW2PUfWCfWwQ==",
|
"integrity": "sha512-AGbJYbCVx1J6jdUgI4s0hNp+9LxlgzKvXl0ROA3DHTrtjAr00Po1RhDZ/eAq2VC/ww8AHgpDXULh5V2rhEqqJg==",
|
||||||
|
"dev": true,
|
||||||
"requires": {
|
"requires": {
|
||||||
"fbjs": "^0.8.16",
|
|
||||||
"object-assign": "^4.1.1",
|
"object-assign": "^4.1.1",
|
||||||
"prop-types": "^15.6.0",
|
"prop-types": "^15.6.2",
|
||||||
"react-is": "^16.3.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": {
|
"react-text-mask": {
|
||||||
|
@ -12693,6 +12742,15 @@
|
||||||
"resolved": "https://registry.npmjs.org/sax/-/sax-1.1.5.tgz",
|
"resolved": "https://registry.npmjs.org/sax/-/sax-1.1.5.tgz",
|
||||||
"integrity": "sha1-HaUKjQDN7NWUBWWfX/hTSf53N0M="
|
"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": {
|
"schema-utils": {
|
||||||
"version": "0.3.0",
|
"version": "0.3.0",
|
||||||
"resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-0.3.0.tgz",
|
"resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-0.3.0.tgz",
|
||||||
|
@ -13683,6 +13741,11 @@
|
||||||
"acorn-node": "^1.2.0"
|
"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": {
|
"tapable": {
|
||||||
"version": "0.2.8",
|
"version": "0.2.8",
|
||||||
"resolved": "https://registry.npmjs.org/tapable/-/tapable-0.2.8.tgz",
|
"resolved": "https://registry.npmjs.org/tapable/-/tapable-0.2.8.tgz",
|
||||||
|
|
10
package.json
10
package.json
|
@ -24,11 +24,13 @@
|
||||||
"@types/jquery": "^3.3.1",
|
"@types/jquery": "^3.3.1",
|
||||||
"@types/jsdom": "^11.0.4",
|
"@types/jsdom": "^11.0.4",
|
||||||
"@types/lodash": "^4.14.106",
|
"@types/lodash": "^4.14.106",
|
||||||
|
"@types/moment-timezone": "^0.5.9",
|
||||||
"@types/prop-types": "^15.5.5",
|
"@types/prop-types": "^15.5.5",
|
||||||
"@types/react": "^16.1.0",
|
"@types/react": "^16.1.0",
|
||||||
"@types/react-dom": "^16.0.5",
|
"@types/react-dom": "^16.0.5",
|
||||||
"@types/react-intl": "^2.3.7",
|
"@types/react-intl": "^2.3.7",
|
||||||
"@types/react-test-renderer": "^16.0.1",
|
"@types/react-test-renderer": "^16.0.1",
|
||||||
|
"@types/react-text-mask": "^5.4.2",
|
||||||
"@types/sinon": "^4.3.3",
|
"@types/sinon": "^4.3.3",
|
||||||
"@types/validator": "^9.4.1",
|
"@types/validator": "^9.4.1",
|
||||||
"babel-core": "^6.26.0",
|
"babel-core": "^6.26.0",
|
||||||
|
@ -36,7 +38,7 @@
|
||||||
"babel-preset-env": "^1.6.1",
|
"babel-preset-env": "^1.6.1",
|
||||||
"babel-preset-es2015": "^6.24.1",
|
"babel-preset-es2015": "^6.24.1",
|
||||||
"bootstrap": "^3.3.7",
|
"bootstrap": "^3.3.7",
|
||||||
"bootstrap-loader": "^2.2.0",
|
"bootstrap-loader": "github:houdiniproject/bootstrap-loader#compiled_namespaced",
|
||||||
"bootstrap-sass": "^3.3.7",
|
"bootstrap-sass": "^3.3.7",
|
||||||
"browserify": "13.0.1",
|
"browserify": "13.0.1",
|
||||||
"browserify-incremental": "3.1.1",
|
"browserify-incremental": "3.1.1",
|
||||||
|
@ -117,9 +119,9 @@
|
||||||
"mobx-react-devtools": "^5.0.1",
|
"mobx-react-devtools": "^5.0.1",
|
||||||
"mobx-react-form": "github:houdiniproject/mobx-react-form#our_fix",
|
"mobx-react-form": "github:houdiniproject/mobx-react-form#our_fix",
|
||||||
"mobx-utils": "^5.0.1",
|
"mobx-utils": "^5.0.1",
|
||||||
"moment": "2.9.0",
|
"moment": "^2.22.2",
|
||||||
"moment-range": "2.2.0",
|
"moment-range": "2.2.0",
|
||||||
"moment-timezone": "0.4.1",
|
"moment-timezone": "^0.5.21",
|
||||||
"no-scroll": "^2.1.0",
|
"no-scroll": "^2.1.0",
|
||||||
"parsleyjs": "2.0.7",
|
"parsleyjs": "2.0.7",
|
||||||
"percent": "1.1.1",
|
"percent": "1.1.1",
|
||||||
|
@ -130,10 +132,10 @@
|
||||||
"quill": "^1.3.6",
|
"quill": "^1.3.6",
|
||||||
"ramda": "^0.21.0",
|
"ramda": "^0.21.0",
|
||||||
"react": "^16.2.0",
|
"react": "^16.2.0",
|
||||||
|
"react-aria-modal": "^3.0.0",
|
||||||
"react-autocomplete": "^1.8.1",
|
"react-autocomplete": "^1.8.1",
|
||||||
"react-dom": "^16.3.1",
|
"react-dom": "^16.3.1",
|
||||||
"react-intl": "^2.4.0",
|
"react-intl": "^2.4.0",
|
||||||
"react-test-renderer": "^16.3.1",
|
|
||||||
"react-text-mask": "^5.3.0",
|
"react-text-mask": "^5.3.0",
|
||||||
"shuffle-array": "1.0.1",
|
"shuffle-array": "1.0.1",
|
||||||
"snabbdom": "0.3.0",
|
"snabbdom": "0.3.0",
|
||||||
|
|
5
spec/factories/email_lists.rb
Normal file
5
spec/factories/email_lists.rb
Normal file
|
@ -0,0 +1,5 @@
|
||||||
|
FactoryBot.define do
|
||||||
|
factory :email_list do
|
||||||
|
|
||||||
|
end
|
||||||
|
end
|
47
spec/lib/mailchimp_spec.rb
Normal file
47
spec/lib/mailchimp_spec.rb
Normal file
|
@ -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
|
|
@ -263,7 +263,7 @@ describe UpdateDonation do
|
||||||
|
|
||||||
|
|
||||||
expected_donation = donation.attributes.merge({
|
expected_donation = donation.attributes.merge({
|
||||||
date: new_date.to_time_in_current_zone,
|
date: new_date,
|
||||||
amount: new_amount,
|
amount: new_amount,
|
||||||
|
|
||||||
designation: new_designation,
|
designation: new_designation,
|
||||||
|
@ -279,7 +279,7 @@ describe UpdateDonation do
|
||||||
donation.reload
|
donation.reload
|
||||||
expect(donation.attributes).to eq expected_donation
|
expect(donation.attributes).to eq expected_donation
|
||||||
|
|
||||||
expected_p1 = payment.attributes.merge({towards: new_dedication, updated_at: Time.now, date: new_date, gross_amount: new_amount, fee_total: new_fee, net_amount: new_amount-new_fee}).with_indifferent_access
|
expected_p1 = payment.attributes.merge({towards: new_designation, updated_at: Time.now, date: new_date, gross_amount: new_amount, fee_total: new_fee, net_amount: new_amount-new_fee}).with_indifferent_access
|
||||||
payment.reload
|
payment.reload
|
||||||
expect(payment.attributes).to eq expected_p1
|
expect(payment.attributes).to eq expected_p1
|
||||||
|
|
||||||
|
@ -367,7 +367,7 @@ describe UpdateDonation do
|
||||||
donation.reload
|
donation.reload
|
||||||
expect(donation.attributes).to eq expected_donation
|
expect(donation.attributes).to eq expected_donation
|
||||||
|
|
||||||
expected_p1 = payment.attributes.merge({towards: '', updated_at: Time.now, date: new_date, gross_amount: new_amount, fee_total: new_fee, net_amount: new_amount-new_fee}).with_indifferent_access
|
expected_p1 = payment.attributes.merge({towards: '', updated_at: Time.now, date: new_date.to_time_in_current_zone, gross_amount: new_amount, fee_total: new_fee, net_amount: new_amount-new_fee}).with_indifferent_access
|
||||||
payment.reload
|
payment.reload
|
||||||
expect(payment.attributes).to eq expected_p1
|
expect(payment.attributes).to eq expected_p1
|
||||||
|
|
||||||
|
|
|
@ -188,6 +188,15 @@ describe UpdateTickets do
|
||||||
expect(ticket.attributes).to eq expected
|
expect(ticket.attributes).to eq expected
|
||||||
end
|
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
|
it 'success editing token' do
|
||||||
result = UpdateTickets.update(basic_valid_ticket_input.merge(token:source_token.token))
|
result = UpdateTickets.update(basic_valid_ticket_input.merge(token:source_token.token))
|
||||||
expected = general_expected.merge(source_token_id: source_token.id)
|
expected = general_expected.merge(source_token_id: source_token.id)
|
||||||
|
|
|
@ -1,7 +1,13 @@
|
||||||
# License: AGPL-3.0-or-later WITH Web-Template-Output-Additional-Permission-3.0-or-later
|
# License: AGPL-3.0-or-later WITH Web-Template-Output-Additional-Permission-3.0-or-later
|
||||||
module MockHelpers
|
module MockHelpers
|
||||||
def self.payment_export_headers()
|
def self.payment_export_headers()
|
||||||
["Date",'Gross Amount', 'Fee Total', 'Net Amount','Type', 'Last Name', 'First Name','Full Name', 'Organization', 'Email', 'Phone', 'Address', 'City', 'State', 'Postal Code', 'Country', 'Anonymous?', 'Supporter Id', 'Designation', 'Honorarium/Memorium', 'Anonymous','Comment','Campaign', 'Campaign Gift Level', 'Event Name', 'Payment', 'Check Number', 'Donation Note']
|
["Date",'Gross Amount', 'Fee Total', 'Net Amount','Type', 'Last Name', 'First Name','Full Name', 'Organization', 'Email', 'Phone', 'Address', 'City', 'State', 'Postal Code', 'Country', 'Anonymous?', 'Supporter Id', 'Designation', "Dedication Type",
|
||||||
|
"Dedicated To: Name",
|
||||||
|
"Dedicated To: Supporter Id",
|
||||||
|
"Dedicated To: Email",
|
||||||
|
"Dedicated To: Phone",
|
||||||
|
"Dedicated To: Address",
|
||||||
|
"Dedicated To: Note", 'Anonymous','Comment','Campaign', 'Campaign Gift Level', 'Event Name', 'Payment', 'Check Number', 'Donation Note']
|
||||||
end
|
end
|
||||||
|
|
||||||
def self.recurring_donation_export_headers()
|
def self.recurring_donation_export_headers()
|
||||||
|
|
7
types/mobx-react-form/index.d.ts
vendored
7
types/mobx-react-form/index.d.ts
vendored
|
@ -1,3 +1,5 @@
|
||||||
|
import { values } from "mobx";
|
||||||
|
|
||||||
// License: LGPL-3.0-or-later
|
// License: LGPL-3.0-or-later
|
||||||
|
|
||||||
interface ValidationInput {
|
interface ValidationInput {
|
||||||
|
@ -171,7 +173,7 @@ interface FieldHandlers {
|
||||||
onError?(e:Field):any
|
onError?(e:Field):any
|
||||||
}
|
}
|
||||||
|
|
||||||
interface FieldDefinition {
|
interface FieldDefinition<TInputType=any> {
|
||||||
name: string
|
name: string
|
||||||
key?: string
|
key?: string
|
||||||
label?: string
|
label?: string
|
||||||
|
@ -190,6 +192,8 @@ interface FieldDefinition {
|
||||||
rules?: string
|
rules?: string
|
||||||
id?:string,
|
id?:string,
|
||||||
validators?: Validation | Array<Validation>
|
validators?: Validation | Array<Validation>
|
||||||
|
input?: (input:TInputType) => string
|
||||||
|
output?: (value:string) => TInputType
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@ -313,6 +317,7 @@ export class Form implements Base {
|
||||||
readonly isValid :boolean;
|
readonly isValid :boolean;
|
||||||
readonly size:number
|
readonly size:number
|
||||||
|
|
||||||
|
values(): {[fields:string] : ValuesResponse|string}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
36
types/react-aria-modal/index.d.ts
vendored
Normal file
36
types/react-aria-modal/index.d.ts
vendored
Normal file
|
@ -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<ModalProps, {}>{}
|
||||||
|
|
||||||
|
export = Modal
|
20
types/react-text-mask/index.d.ts
vendored
20
types/react-text-mask/index.d.ts
vendored
|
@ -1,20 +0,0 @@
|
||||||
// License: LGPL-3.0-or-later
|
|
||||||
import {Component} from 'react'
|
|
||||||
|
|
||||||
|
|
||||||
export interface MaskedInputProps
|
|
||||||
{
|
|
||||||
mask: Array<any>|Function|Boolean | {mask: Array<any> | Function, pipe: Function}
|
|
||||||
|
|
||||||
guide?: Boolean
|
|
||||||
value?: String| Number,
|
|
||||||
pipe?: Function,
|
|
||||||
placeholderChar?: String,
|
|
||||||
keepCharPositions?: Boolean,
|
|
||||||
showMask?: Boolean,
|
|
||||||
[additionalProps: string] : any
|
|
||||||
}
|
|
||||||
|
|
||||||
export class MaskedInput extends Component<MaskedInputProps, {}> {
|
|
||||||
|
|
||||||
}
|
|
2
types/text-mask/index.d.ts
vendored
2
types/text-mask/index.d.ts
vendored
|
@ -1,2 +0,0 @@
|
||||||
// License: LGPL-3.0-or-later
|
|
||||||
declare module "text-mask"
|
|
Loading…
Reference in a new issue