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,
 | 
			
		||||
    "grid": 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'
 | 
			
		||||
	# Additional mounts are added via generators above this line
 | 
			
		||||
  # DON'T REMOVE THIS OR THE PREVIOUS LINES!!!
 | 
			
		||||
	uriForHost = URI.parse(Settings.cdn.url)
 | 
			
		||||
	uri_for_host = URI.parse(Settings.api_domain&.url || Settings.cdn.url)
 | 
			
		||||
	add_swagger_documentation \
 | 
			
		||||
		host: "#{uriForHost.host}#{Settings.cdn.port ? ":#{Settings.cdn.port}" : ""}",
 | 
			
		||||
		schemes: [uriForHost.scheme],
 | 
			
		||||
		host: "#{uri_for_host.host}#{uri_for_host.port ? ":#{uri_for_host.port}" : ""}",
 | 
			
		||||
		schemes: [uri_for_host.scheme],
 | 
			
		||||
		base_path: '/api/v1'
 | 
			
		||||
end
 | 
			
		||||
| 
						 | 
				
			
			@ -22,10 +22,13 @@ module Nonprofits
 | 
			
		|||
            name_description = params[:year]
 | 
			
		||||
          elsif (params[:start])
 | 
			
		||||
            name_description = "from-#{params[:start]}"
 | 
			
		||||
            if (params[:end])
 | 
			
		||||
              name_description += "-to-#{params[:end]}"
 | 
			
		||||
            end
 | 
			
		||||
          end
 | 
			
		||||
 | 
			
		||||
          filename = "end-of-year-report-#{name_description}.csv"
 | 
			
		||||
          data = QuerySupporters.year_aggregate_report(params[:nonprofit_id], {:year => params[:year], :start => params[:start]})
 | 
			
		||||
          filename = "aggregate-report-#{name_description}.csv"
 | 
			
		||||
          data = QuerySupporters.year_aggregate_report(params[:nonprofit_id], {:year => params[:year], :start => params[:start], :end => params[:end]})
 | 
			
		||||
          send_data(Format::Csv.from_array(data), filename: filename)
 | 
			
		||||
        end
 | 
			
		||||
      end
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
							
								
								
									
										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
 | 
			
		||||
 | 
			
		||||
	belongs_to :tag_master
 | 
			
		||||
	belongs_to :supporter
 | 
			
		||||
 | 
			
		||||
	def name; self.tag_master.name; end
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -12,6 +12,7 @@ class TagMaster < ActiveRecord::Base
 | 
			
		|||
 | 
			
		||||
	belongs_to :nonprofit
 | 
			
		||||
	has_many :tag_joins, dependent: :destroy
 | 
			
		||||
	has_one :email_list
 | 
			
		||||
 | 
			
		||||
	scope :not_deleted, ->{where(deleted: [nil,false])}
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -22,4 +22,10 @@
 | 
			
		|||
<br>
 | 
			
		||||
<%= 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' %>
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -1,97 +1,9 @@
 | 
			
		|||
<%- # License: AGPL-3.0-or-later WITH Web-Template-Output-Additional-Permission-3.0-or-later -%>
 | 
			
		||||
<!-- 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>
 | 
			
		||||
      <!--= 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>
 | 
			
		||||
      <div id='EditPaymentPaneElement'/>
 | 
			
		||||
 | 
			
		||||
      <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 -->
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -1,7 +1,6 @@
 | 
			
		|||
<%- # License: AGPL-3.0-or-later WITH Web-Template-Output-Additional-Permission-3.0-or-later -%>
 | 
			
		||||
 | 
			
		||||
<!-- partial nonprofits/payments/_side_panel -->
 | 
			
		||||
 | 
			
		||||
<div class='sidePanel'>
 | 
			
		||||
	<!--= add_class_if loading 'u-halfOpacity' -->
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -25,7 +24,7 @@
 | 
			
		|||
		<div class='u-marginTop--20'>
 | 
			
		||||
      <a class="button--tiny edit">
 | 
			
		||||
        <!--= 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
 | 
			
		||||
      </a>
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -4,13 +4,42 @@
 | 
			
		|||
 | 
			
		||||
<% content_for :stylesheets do %>
 | 
			
		||||
			<%= stylesheet_link_tag 'nonprofits/payments/index/page' %>
 | 
			
		||||
      <%= IncludeAsset.css '/client/css/bootstrap.css' %>
 | 
			
		||||
<% end %>
 | 
			
		||||
 | 
			
		||||
<% content_for :javascripts do %>
 | 
			
		||||
 | 
			
		||||
	<script>
 | 
			
		||||
		appl.def('has_bank', <%= !!@nonprofit.bank_account %>)
 | 
			
		||||
	</script>
 | 
			
		||||
	<%= 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 %>
 | 
			
		||||
 | 
			
		||||
<%= 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
 | 
			
		||||
  	attributes :name, :url
 | 
			
		||||
  	attributes :name, :url, :id
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  node(:campaign_gift){|d| {name: d.campaign_gifts.any? ? d.campaign_gifts.last.campaign_gift_option.name : nil}}
 | 
			
		||||
 | 
			
		||||
  child :event, object_root: false do
 | 
			
		||||
    attributes :name, :url
 | 
			
		||||
    attributes :name, :url, :id
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
	child :recurring_donation, object_root: false do
 | 
			
		||||
| 
						 | 
				
			
			@ -49,7 +49,7 @@ end
 | 
			
		|||
node(:ticket) do |payment|
 | 
			
		||||
  event = GetData.obj(payment.tickets.last, :event)
 | 
			
		||||
  h = {
 | 
			
		||||
    event: {name: GetData.obj(event, :name), url: GetData.obj(event, :url)},
 | 
			
		||||
    event: {name: GetData.obj(event, :name), url: GetData.obj(event, :url), id: GetData.obj(event, :id)},
 | 
			
		||||
    levels: payment.tickets.map{|t| "#{GetData.chain(t.ticket_level, :name)} (#{t.quantity}x)"}.join(", "),
 | 
			
		||||
    discount: payment.tickets.map{|t| t.event_discount ? "#{t.event_discount.name} (#{t.event_discount.percent}%)" : nil}.compact.join(", ")
 | 
			
		||||
  }
 | 
			
		||||
| 
						 | 
				
			
			@ -68,3 +68,7 @@ child :supporter do
 | 
			
		|||
	attributes :name, :email, :city, :state_code, :address, :zip_code, :phone, :id, :country
 | 
			
		||||
 | 
			
		||||
end
 | 
			
		||||
 | 
			
		||||
child :nonprofit do
 | 
			
		||||
    attributes :id
 | 
			
		||||
end
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -90,7 +90,7 @@
 | 
			
		|||
		<%= render 'contact' %>
 | 
			
		||||
 | 
			
		||||
		<div class='pastelBox--grey'>
 | 
			
		||||
			<header>Promote this nonprofit</header>
 | 
			
		||||
			<header><%= t("organization_page.promote") %></header>
 | 
			
		||||
			<div class='pastelBox-body'>
 | 
			
		||||
				<%= render 'common/social_buttons' %>
 | 
			
		||||
			</div>
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -2,10 +2,42 @@
 | 
			
		|||
<% content_for(:dont_load_optimizely) {'true'} %>
 | 
			
		||||
<% 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 %>
 | 
			
		||||
	<%= IncludeAsset.js '/client/js/nonprofits/supporters/index/page.js' %>
 | 
			
		||||
	<%= 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 %>
 | 
			
		||||
 | 
			
		||||
<%= render '/components/trial_bar' if nonprofit_in_trial?(@nonprofit.id) %>
 | 
			
		||||
| 
						 | 
				
			
			@ -58,3 +90,5 @@
 | 
			
		|||
<%= render 'donations/new_offline_modal' %>
 | 
			
		||||
 | 
			
		||||
<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'
 | 
			
		||||
      , id: radioId1
 | 
			
		||||
      , 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'))
 | 
			
		||||
    ])
 | 
			
		||||
| 
						 | 
				
			
			@ -31,7 +31,7 @@ function view(state) {
 | 
			
		|||
      , type: 'radio'
 | 
			
		||||
      , value: 'memory'
 | 
			
		||||
      , 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'))
 | 
			
		||||
    ])
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -14,7 +14,7 @@ function view(state) {
 | 
			
		|||
      h('a.button--small.facebook.u-width--full.share-button', {
 | 
			
		||||
        props: {
 | 
			
		||||
          target: '_blank'
 | 
			
		||||
        , href: 'https://www.facebook.com/dialog/feed?app_id='+app.facebook_app_id +"display=popup&caption=" + (app.campaign.name || app.nonprofit.name) + "&link="+window.location.href
 | 
			
		||||
        , href: 'https://www.facebook.com/dialog/feed?app_id='+app.facebook_app_id +"&display=popup&caption=" + (app.campaign.name || app.nonprofit.name) + "&link="+window.location.href
 | 
			
		||||
        }
 | 
			
		||||
      }, [h('i.fa.fa-facebook-square'), ` ${I18n.t('nonprofits.donate.followup.share.facebook')}`] )
 | 
			
		||||
    ])
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -166,7 +166,7 @@ function view(state) {
 | 
			
		|||
    ])
 | 
			
		||||
  , weekly
 | 
			
		||||
  , 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)
 | 
			
		||||
  ])
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -127,7 +127,9 @@ const setDonationDedication = (don, dedication) => {
 | 
			
		|||
  , JSON.stringify({
 | 
			
		||||
      supporter_id: dedication.supporter.id
 | 
			
		||||
    , 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
 | 
			
		||||
    , type: dedication.type
 | 
			
		||||
    })
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -89,27 +89,15 @@ appl.def('get_payment_purchase_object', function(payment) {
 | 
			
		|||
	}
 | 
			
		||||
})
 | 
			
		||||
 | 
			
		||||
appl.def('update_donation', function(donation) {
 | 
			
		||||
	if(!donation) return
 | 
			
		||||
appl.def('start_loading', function(){
 | 
			
		||||
  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) 
 | 
			
		||||
  if(formattedDate && formattedDate != "Invalid date") {
 | 
			
		||||
    donation.date = formattedDate 
 | 
			
		||||
  } else {
 | 
			
		||||
    appl.notify('Please enter a valid date')
 | 
			
		||||
    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('update_donation__success', function() {
 | 
			
		||||
  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() {
 | 
			
		||||
| 
						 | 
				
			
			@ -161,14 +149,17 @@ appl.def('format_dedication', function(dedic, node) {
 | 
			
		|||
    var json
 | 
			
		||||
    try { json = JSON.parse(dedic) } catch(e) {}
 | 
			
		||||
    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 = `
 | 
			
		||||
        Donation made in ${dedic.type || 'honor'} of 
 | 
			
		||||
        <a href='/nonprofits/${app.nonprofit_id}/supporters?sid=${json.supporter_id}'>${json.name}</a>.
 | 
			
		||||
        Donation made in ${json.type || 'honor'} of 
 | 
			
		||||
        ${supporter_link}.
 | 
			
		||||
        ${json.note ? `<br>Note: <em>${json.note}</em>.` : ''}
 | 
			
		||||
      `
 | 
			
		||||
    } else {
 | 
			
		||||
      // Print plaintext dedication
 | 
			
		||||
      inner = dedic
 | 
			
		||||
      inner = ''
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
  td.innerHTML = inner
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -31,6 +31,7 @@ const init = _ => {
 | 
			
		|||
  , newNote$: flyd.stream()
 | 
			
		||||
  , editNote$: flyd.stream()
 | 
			
		||||
  , deleteNote$: flyd.stream()
 | 
			
		||||
  , newDonation$: flyd.stream()
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  const supporterID$ = R.compose(
 | 
			
		||||
| 
						 | 
				
			
			@ -95,7 +96,6 @@ const init = _ => {
 | 
			
		|||
  , flyd.map(()=> 'replyGmailModal', state.threadId$)
 | 
			
		||||
  , flyd.map(()=> 'newSupporterNoteModal', state.editNoteData$)
 | 
			
		||||
  , flyd.map(()=> null, state.gmail.sendResponse$)
 | 
			
		||||
  , flyd.map(()=> null, state.offsiteDonationForm.saved$)
 | 
			
		||||
  , flyd.map(()=> null, state.supporterNoteForm.saved$)
 | 
			
		||||
  ])
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -143,7 +143,7 @@ const view = state => {
 | 
			
		|||
  , activities.view(state)
 | 
			
		||||
  , composeModal(state)
 | 
			
		||||
  , 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$}))
 | 
			
		||||
  , replyModal(state)
 | 
			
		||||
  , 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 request = require('../../../../common/request')
 | 
			
		||||
const serialize = require('form-serialize')
 | 
			
		||||
const flyd_filter = require('flyd/module/filter')
 | 
			
		||||
 | 
			
		||||
const flyd_flatMap = require('flyd/module/flatmap')
 | 
			
		||||
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) {
 | 
			
		||||
  var state = {
 | 
			
		||||
    submit$: flyd.stream()
 | 
			
		||||
  , supporter$: parentState.supporter$
 | 
			
		||||
  , campaigns$: getFundraisers('campaigns')
 | 
			
		||||
  , events$: getFundraisers('events')
 | 
			
		||||
  , saved$: flyd.stream()
 | 
			
		||||
  }
 | 
			
		||||
  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
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
const setDefaults = formData =>
 | 
			
		||||
  R.evolve({
 | 
			
		||||
    amount: format.dollarsToCents
 | 
			
		||||
  , date: d => moment(d).format("YYYY-MM-DD")
 | 
			
		||||
  }, formData)
 | 
			
		||||
 | 
			
		||||
function view(state) {
 | 
			
		||||
  var body = form(state)
 | 
			
		||||
 | 
			
		||||
  return h('div', [
 | 
			
		||||
    modal({
 | 
			
		||||
      id$: state.modalID$
 | 
			
		||||
    , thisID: 'newOffsiteDonationModal'
 | 
			
		||||
    , title: 'New Offsite Contribution'
 | 
			
		||||
    , body
 | 
			
		||||
    })
 | 
			
		||||
  ])
 | 
			
		||||
  
 | 
			
		||||
  return h('div', {id$: 'offsite_donation_form_modal'})
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
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}
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -5,6 +5,7 @@ const flyd = require('flyd')
 | 
			
		|||
const h = require('snabbdom/h')
 | 
			
		||||
flyd.mergeAll = require('flyd/module/mergeall')
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
const button = (text, stream) =>
 | 
			
		||||
  h('button.button--tiny.u-marginRight--10', {on: {click: stream}}
 | 
			
		||||
  , [h('i.fa.fa-plus.u-marginRight--5') , text ])
 | 
			
		||||
| 
						 | 
				
			
			@ -13,8 +14,12 @@ const view = state =>
 | 
			
		|||
  h('section.timeline-actions.u-padding--10', [
 | 
			
		||||
    button('Note', state.newNote$)
 | 
			
		||||
  , 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}
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -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: "€"
 | 
			
		||||
    abbv: "eur"
 | 
			
		||||
    format: "%n%u"
 | 
			
		||||
 | 
			
		||||
api_domain:
 | 
			
		||||
  url: "http://localhost:5000"
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -284,6 +284,11 @@ Config.schema do
 | 
			
		|||
    required(:url).filled?(:str)
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  # the domain for your api. Usually will be your CDN.url
 | 
			
		||||
  optional(:api_domain).schema do
 | 
			
		||||
    required(:url).filled?(:str)
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
end
 | 
			
		||||
 | 
			
		||||
Settings.reload!
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -10,6 +10,8 @@ en:
 | 
			
		|||
        body: 'Comment content'
 | 
			
		||||
  organization:
 | 
			
		||||
    name: "Organisation"
 | 
			
		||||
  organization_page:
 | 
			
		||||
    promote: "Promote this organization"
 | 
			
		||||
  donation:
 | 
			
		||||
   amount: "Total Amount"
 | 
			
		||||
   date: "Transaction Date"
 | 
			
		||||
| 
						 | 
				
			
			@ -29,6 +31,7 @@ en:
 | 
			
		|||
        transfer_label_html: "<strong>Donation %{nonprofit_statement}</strong>."
 | 
			
		||||
        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_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:
 | 
			
		||||
        subject: "Donation receipt for %{nonprofit_name}"
 | 
			
		||||
        transfer_info_html: "This transfer will appear on your bank statement as %{label}"
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -73,3 +73,6 @@ source_tokens:
 | 
			
		|||
 | 
			
		||||
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)';
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
--
 | 
			
		||||
-- 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: -
 | 
			
		||||
--
 | 
			
		||||
| 
						 | 
				
			
			@ -4327,3 +4343,7 @@ INSERT INTO schema_migrations (version) VALUES ('20180713215825');
 | 
			
		|||
 | 
			
		||||
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.lock /myapp/Gemfile.lock
 | 
			
		||||
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> : ""
 | 
			
		||||
 | 
			
		||||
    return <div>
 | 
			
		||||
        {this.renderChildren()}
 | 
			
		||||
        {this.props.children}
 | 
			
		||||
        {errorDiv}
 | 
			
		||||
        {stickyErrorDiv }
 | 
			
		||||
      </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`] = `
 | 
			
		||||
<div>
 | 
			
		||||
  <input
 | 
			
		||||
    className="form-control"
 | 
			
		||||
    key=".0"
 | 
			
		||||
  />
 | 
			
		||||
  <input />
 | 
			
		||||
  <div
 | 
			
		||||
    className="help-block"
 | 
			
		||||
    role="alert"
 | 
			
		||||
| 
						 | 
				
			
			@ -17,10 +14,7 @@ exports[`StandardFieldComponent sets error message properly 1`] = `
 | 
			
		|||
 | 
			
		||||
exports[`StandardFieldComponent works with a child 1`] = `
 | 
			
		||||
<div>
 | 
			
		||||
  <input
 | 
			
		||||
    className="form-control"
 | 
			
		||||
    key=".0"
 | 
			
		||||
  />
 | 
			
		||||
  <input />
 | 
			
		||||
</div>
 | 
			
		||||
`;
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -3,18 +3,69 @@ import * as React from 'react';
 | 
			
		|||
import {observer} from "mobx-react";
 | 
			
		||||
import {Field} from "../../../../types/mobx-react-form";
 | 
			
		||||
import LabeledFieldComponent from "./LabeledFieldComponent";
 | 
			
		||||
import {injectIntl, InjectedIntl} from 'react-intl';
 | 
			
		||||
import {HoudiniField} from "../../lib/houdini_form";
 | 
			
		||||
import ReactInput from "./form/ReactInput";
 | 
			
		||||
import ReactSelect from './form/ReactSelect';
 | 
			
		||||
import ReactTextarea from "./form/ReactTextarea";
 | 
			
		||||
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
 | 
			
		||||
    return <LabeledFieldComponent
 | 
			
		||||
        inputId={props.field.id} labelText={field.label} inError={field.hasError} error={field.error}
 | 
			
		||||
        inStickyError={field.hasServerError} stickyError={field.serverError}
 | 
			
		||||
        className={props.wrapperClassName} >
 | 
			
		||||
 | 
			
		||||
        <ReactInput field={field} label={props.label} placeholder={props.placeholder} className="form-control"/>
 | 
			
		||||
        <ReactInput field={field} label={props.label} placeholder={props.placeholder} className={`form-control ${props.inputClassNames || ''}`}/>
 | 
			
		||||
    </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 {toJS, observable, action, runInAction} from 'mobx';
 | 
			
		||||
import {observer} from 'mobx-react';
 | 
			
		||||
import {InputHTMLAttributes} from 'react';
 | 
			
		||||
import {ReactForm} from "./ReactForm";
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@observer
 | 
			
		||||
class TestChange extends React.Component{
 | 
			
		||||
class TestChange extends React.Component {
 | 
			
		||||
  @observable
 | 
			
		||||
  remove:boolean
 | 
			
		||||
  remove: boolean
 | 
			
		||||
  @observable
 | 
			
		||||
  form: Form
 | 
			
		||||
 | 
			
		||||
  @action.bound
 | 
			
		||||
  componentWillMount(){
 | 
			
		||||
    this.form = new Form({fields:[{
 | 
			
		||||
    name: 'name',
 | 
			
		||||
      extra: null}
 | 
			
		||||
    ]})
 | 
			
		||||
  componentWillMount() {
 | 
			
		||||
    this.form = new Form({
 | 
			
		||||
      fields: [{
 | 
			
		||||
        name: 'name',
 | 
			
		||||
        extra: null
 | 
			
		||||
      }
 | 
			
		||||
      ]
 | 
			
		||||
    })
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
  @action.bound
 | 
			
		||||
  onClick(){
 | 
			
		||||
  onClick() {
 | 
			
		||||
    this.remove = true
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  render() {
 | 
			
		||||
    let reactInput = !this.remove ?  <ReactInput field={this.form.$('name')} label={'label1'} placeholder={"holder"}>
 | 
			
		||||
      {this.props.children}
 | 
			
		||||
    let reactInput = !this.remove ? <ReactInput field={this.form.$('name')} label={'label1'} placeholder={"holder"}>
 | 
			
		||||
 | 
			
		||||
    </ReactInput> : undefined
 | 
			
		||||
 | 
			
		||||
    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', () => {
 | 
			
		||||
 | 
			
		||||
  let form: Form
 | 
			
		||||
| 
						 | 
				
			
			@ -71,94 +60,48 @@ describe('ReactInput', () => {
 | 
			
		|||
    })
 | 
			
		||||
  })
 | 
			
		||||
 | 
			
		||||
  describe('no children passed in', () => {
 | 
			
		||||
    test('gets added properly', () => {
 | 
			
		||||
      let res = mount(<ReactForm form={form}>
 | 
			
		||||
          <ReactInput field={form.$('name')} label={"label"}
 | 
			
		||||
                      placeholder={"holder"} value={'snapshot'} aria-required={true}/>
 | 
			
		||||
  test('gets added properly', () => {
 | 
			
		||||
    let res = mount(<ReactForm form={form}>
 | 
			
		||||
      <ReactInput field={form.$('name')} label={"label"}
 | 
			
		||||
                  placeholder={"holder"} value={'snapshot'} aria-required={true}/>
 | 
			
		||||
 | 
			
		||||
        </ReactForm>)
 | 
			
		||||
    </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('')
 | 
			
		||||
    //Did the attributes settings work as expected back to the objects
 | 
			
		||||
    expect(form.$('name').label).toEqual('label')
 | 
			
		||||
    expect(form.$('name').placeholder).toEqual('holder')
 | 
			
		||||
    expect(form.$('name').value).toEqual('')
 | 
			
		||||
 | 
			
		||||
      //is the aria attribute passted through to the input
 | 
			
		||||
      let input = res.find('input')
 | 
			
		||||
      expect(input.prop('aria-required')).toEqual(true)
 | 
			
		||||
    //is the aria attribute passted through to the input
 | 
			
		||||
    let input = res.find('input')
 | 
			
		||||
    expect(input.prop('aria-required')).toEqual(true)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
      // is the input properly bound?
 | 
			
		||||
      input.simulate('change',  {target: { value: 'something' } })
 | 
			
		||||
      expect(form.$('name').value).toEqual('something')
 | 
			
		||||
    })
 | 
			
		||||
 | 
			
		||||
    test('gets removed properly', () => {
 | 
			
		||||
 | 
			
		||||
      let res = mount(<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')
 | 
			
		||||
    })
 | 
			
		||||
    // is the input properly bound?
 | 
			
		||||
    input.simulate('change', {target: {value: 'something'}})
 | 
			
		||||
    expect(form.$('name').value).toEqual('something')
 | 
			
		||||
  })
 | 
			
		||||
 | 
			
		||||
  describe('children passed in', () => {
 | 
			
		||||
    test('gets added properly', () => {
 | 
			
		||||
      let res = mount(<ReactForm form={form}>
 | 
			
		||||
        <ReactInput field={form.$('name')} label={"label"}
 | 
			
		||||
                    placeholder={"holder"} value={'snapshot'} aria-required={true}>
 | 
			
		||||
          <WrappedInput/>
 | 
			
		||||
        </ReactInput>
 | 
			
		||||
  test('gets removed properly', () => {
 | 
			
		||||
 | 
			
		||||
      </ReactForm>)
 | 
			
		||||
    let res = mount(<TestChange/>)
 | 
			
		||||
 | 
			
		||||
      //Did the attributes settings work as expected back to the objects
 | 
			
		||||
      expect(form.$('name').label).toEqual('label')
 | 
			
		||||
      expect(form.$('name').placeholder).toEqual('holder')
 | 
			
		||||
      expect(form.$('name').value).toEqual('')
 | 
			
		||||
    // The two casts are needed because Typescript was going blowing up without the 'any' first.
 | 
			
		||||
    // Why was it? *shrugs*
 | 
			
		||||
    let f = res.find('ReactForm').instance() as any as ReactForm
 | 
			
		||||
    expect(f.form.size).toEqual(1)
 | 
			
		||||
 | 
			
		||||
      //is the aria attribute passted through to the input
 | 
			
		||||
      let input = res.find('input')
 | 
			
		||||
      expect(input.prop('aria-required')).toEqual(true)
 | 
			
		||||
    res.find('input').simulate('change', {target: {value: 'something'}})
 | 
			
		||||
 | 
			
		||||
    expect(f.form.$('name').value).toEqual('something')
 | 
			
		||||
 | 
			
		||||
      // is the input properly bound?
 | 
			
		||||
      input.simulate('change',  {target: { value: 'something' } })
 | 
			
		||||
      expect(form.$('name').value).toEqual('something')
 | 
			
		||||
    })
 | 
			
		||||
    res.find('button').simulate('click')
 | 
			
		||||
    expect(f.form.size).toEqual(1)
 | 
			
		||||
 | 
			
		||||
    test('gets removed properly', () => {
 | 
			
		||||
 | 
			
		||||
      let res = mount(<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(toJS(res.find('form'))).toMatchSnapshot()
 | 
			
		||||
 | 
			
		||||
    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 {observable, action, toJS, runInAction} from 'mobx';
 | 
			
		||||
import {InputHTMLAttributes} from 'react';
 | 
			
		||||
import {ReactInputProps} from "./react_input_props";
 | 
			
		||||
import {SelectHTMLAttributes} from "react";
 | 
			
		||||
import {ReactSelectProps} from "./ReactSelect";
 | 
			
		||||
import {castToNullIfUndef} from "../../../lib/utils";
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
type InputTypes = ReactInputProps &
 | 
			
		||||
  InputHTMLAttributes<HTMLInputElement>
 | 
			
		||||
 | 
			
		||||
export interface ReactInputProps
 | 
			
		||||
{
 | 
			
		||||
  field:Field
 | 
			
		||||
  label?:string
 | 
			
		||||
  placeholder?:string
 | 
			
		||||
}
 | 
			
		||||
class ReactInput extends React.Component<InputTypes, {}> {
 | 
			
		||||
 | 
			
		||||
function castToNullIfUndef(i:any){
 | 
			
		||||
  return i === undefined ? null : i
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class ReactInput extends React.Component<ReactInputProps & InputHTMLAttributes<HTMLInputElement>, {}> {
 | 
			
		||||
 | 
			
		||||
  constructor(props:ReactInputProps){
 | 
			
		||||
  constructor(props:InputTypes){
 | 
			
		||||
    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()
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -53,18 +47,9 @@ class ReactInput extends React.Component<ReactInputProps & InputHTMLAttributes<H
 | 
			
		|||
      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
 | 
			
		||||
  @action.bound
 | 
			
		||||
  winnowProps(): ReactInputProps & InputHTMLAttributes<HTMLInputElement> {
 | 
			
		||||
  winnowProps(): InputTypes {
 | 
			
		||||
    let ourProps = {...this.props}
 | 
			
		||||
    delete ourProps.field
 | 
			
		||||
    delete ourProps.value
 | 
			
		||||
| 
						 | 
				
			
			@ -73,14 +58,7 @@ class ReactInput extends React.Component<ReactInputProps & InputHTMLAttributes<H
 | 
			
		|||
  }
 | 
			
		||||
 | 
			
		||||
  render() {
 | 
			
		||||
 | 
			
		||||
    if (this.props.children)
 | 
			
		||||
    {
 | 
			
		||||
      return this.renderChildren()
 | 
			
		||||
    }
 | 
			
		||||
    else {
 | 
			
		||||
      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
 | 
			
		||||
 | 
			
		||||
exports[`ReactInput no children passed in gets removed properly 1`] = `
 | 
			
		||||
exports[`ReactInput gets removed properly 1`] = `
 | 
			
		||||
<form>
 | 
			
		||||
  <button
 | 
			
		||||
    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'
 | 
			
		||||
 | 
			
		||||
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>) => {
 | 
			
		||||
                let className = "col-left-6"
 | 
			
		||||
                let className = "col-sm-6"
 | 
			
		||||
                if (_.last(props.children) !== i){
 | 
			
		||||
                    className += " u-paddingRight--10"
 | 
			
		||||
                }
 | 
			
		||||
| 
						 | 
				
			
			@ -21,10 +21,10 @@ export const TwoColumnFields = observer((props:{children:Array<React.ReactElemen
 | 
			
		|||
})
 | 
			
		||||
 | 
			
		||||
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>) => {
 | 
			
		||||
                let className = "col-left-4"
 | 
			
		||||
                let className = "col-sm-4"
 | 
			
		||||
                if (_.last(props.children) !== i){
 | 
			
		||||
                    className += " u-paddingRight--10"
 | 
			
		||||
                }
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -9,7 +9,7 @@ import {TabList} from "./TabList";
 | 
			
		|||
import {TabPanel} from "./TabPanel";
 | 
			
		||||
import {Tab} from "./Tab";
 | 
			
		||||
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';
 | 
			
		||||
 | 
			
		||||
let uniqueIdMock = new UniqueIdMock();
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -9,7 +9,7 @@ import {ReactWrapper} from 'enzyme';
 | 
			
		|||
import {WizardPanel} from "./WizardPanel";
 | 
			
		||||
 | 
			
		||||
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();
 | 
			
		||||
class MockableTabPanelState extends WizardTabPanelState
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -3,7 +3,7 @@ import * as React from 'react';
 | 
			
		|||
import 'jest';
 | 
			
		||||
import {AbstractWizardState, AbstractWizardTabPanelState} from './abstract_wizard_state'
 | 
			
		||||
import {observable, action, computed} from 'mobx'
 | 
			
		||||
import {UniqueIdMock} from "../tests/unique_id_mock";
 | 
			
		||||
import {UniqueIdMock} from "../test/unique_id_mock";
 | 
			
		||||
 | 
			
		||||
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() {
 | 
			
		||||
     return <fieldset >
 | 
			
		||||
 | 
			
		||||
       <BasicField field={this.props.form.$("organization_name")}
 | 
			
		||||
                   label={this.props.intl.formatMessage({id:'registration.wizard.nonprofit.name.label' })}
 | 
			
		||||
                   placeholder={this.props.intl.formatMessage({id: 'registration.wizard.nonprofit.name.placeholder'})}
 | 
			
		||||
                   inputClassNames={"input-lg"}
 | 
			
		||||
       />
 | 
			
		||||
 | 
			
		||||
       <BasicField field={this.props.form.$('website')}
 | 
			
		||||
                   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>
 | 
			
		||||
         <BasicField field={this.props.form.$('org_email')}
 | 
			
		||||
                     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')}
 | 
			
		||||
                     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>
 | 
			
		||||
 | 
			
		||||
       <ThreeColumnFields>
 | 
			
		||||
         <BasicField field={this.props.form.$('city')}
 | 
			
		||||
                     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')}
 | 
			
		||||
                     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')}
 | 
			
		||||
                     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>
 | 
			
		||||
 | 
			
		||||
       <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}/>
 | 
			
		||||
     </fieldset>
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -16,7 +16,7 @@ class RegistrationPage extends React.Component<RegistrationPageProps & InjectedI
 | 
			
		|||
 | 
			
		||||
 | 
			
		||||
  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")}
 | 
			
		||||
            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')}
 | 
			
		||||
          label={this.props.intl.formatMessage({id: "registration.wizard.contact.email.label"})}
 | 
			
		||||
          placeholder={this.props.intl.formatMessage({id: "registration.wizard.contact.email.placeholder"})}
 | 
			
		||||
                    inputClassNames={"input-lg"}
 | 
			
		||||
        />
 | 
			
		||||
      </TwoColumnFields>
 | 
			
		||||
 | 
			
		||||
      <BasicField field={this.props.form.$('password')}
 | 
			
		||||
                  label={this.props.intl.formatMessage({id:'registration.wizard.contact.password.label'})}
 | 
			
		||||
                  inputClassNames={"input-lg"}
 | 
			
		||||
                  />
 | 
			
		||||
      <BasicField field={this.props.form.$('password_confirmation')}
 | 
			
		||||
                  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}>
 | 
			
		||||
      <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')}
 | 
			
		||||
                  label={this.props.intl.formatMessage({id: 'login.password'})}/>
 | 
			
		||||
                  label={this.props.intl.formatMessage({id: 'login.password'})} inputClassNames={"input-lg"}/>
 | 
			
		||||
      {errorDiv}
 | 
			
		||||
      <div className={'form-group'}>
 | 
			
		||||
        <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, {}> {
 | 
			
		||||
  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>
 | 
			
		||||
       <SessionLoginForm buttonText="login.login" buttonTextOnProgress="login.logging_in"/>
 | 
			
		||||
     </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
 | 
			
		||||
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
 | 
			
		||||
import * as Regex from './regex'
 | 
			
		||||
import {Field, Form} from "mobx-react-form";
 | 
			
		||||
import moment = require("moment");
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
interface ValidationInput {
 | 
			
		||||
| 
						 | 
				
			
			@ -42,6 +43,35 @@ export class Validations  {
 | 
			
		|||
    ]
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  static isNumber({field, validator}:ValidationInput):StringBoolTuple {
 | 
			
		||||
    return [
 | 
			
		||||
      !isNaN(parseFloat(field.value)),
 | 
			
		||||
      `${field.label} must be a number`
 | 
			
		||||
    ]
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
  static isGreaterThanOrEqualTo(value:number) : ({field, validator}:ValidationInput) => StringBoolTuple
 | 
			
		||||
  {
 | 
			
		||||
    return ({field, validator}:ValidationInput) => {
 | 
			
		||||
      return [
 | 
			
		||||
        parseFloat(field.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 {
 | 
			
		||||
    return ({field, form, validator}:ValidationInput) => {
 | 
			
		||||
      if (!field.value || validator.isEmpty(field.value)){
 | 
			
		||||
| 
						 | 
				
			
			@ -53,5 +83,15 @@ export class Validations  {
 | 
			
		|||
    };
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  static isDate(format:string): ({field, validator}:ValidationInput) => StringBoolTuple  {
 | 
			
		||||
    return ({field, validator}:ValidationInput) => {
 | 
			
		||||
      let m = moment(field.value, format, true);
 | 
			
		||||
      return [
 | 
			
		||||
        m.isValid(),
 | 
			
		||||
        `${field.label} must be a date with format: ${format}`
 | 
			
		||||
      ]
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -154,4 +154,70 @@ module Mailchimp
 | 
			
		|||
      .execute.map{|h| h['mailchimp_list_id']}
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
  # @param [Nonprofit] nonprofit
 | 
			
		||||
  def self.hard_sync_lists(nonprofit)
 | 
			
		||||
    return if !nonprofit
 | 
			
		||||
 | 
			
		||||
    nonprofit.tag_masters.not_deleted.each do |i|
 | 
			
		||||
      if (i.email_list)
 | 
			
		||||
        hard_sync_list(i.email_list)
 | 
			
		||||
      end
 | 
			
		||||
    end
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  # @param [EmailList] email_list
 | 
			
		||||
  def self.hard_sync_list(email_list)
 | 
			
		||||
    ops = generate_batch_ops_for_hard_sync(email_list)
 | 
			
		||||
    perform_batch_operations(email_list.nonprofit.id, ops)
 | 
			
		||||
    
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  def self.generate_batch_ops_for_hard_sync(email_list)
 | 
			
		||||
    #get the subscribers from mailchimp
 | 
			
		||||
    mailchimp_subscribers = get_list_mailchimp_subscribers(email_list)
 | 
			
		||||
    #get our subscribers
 | 
			
		||||
    our_supporters = email_list.tag_master.tag_joins.map{|i| i.supporter}
 | 
			
		||||
 | 
			
		||||
    #split them as follows:
 | 
			
		||||
    # on both lists, on our list, on the mailchimp list
 | 
			
		||||
    in_both, in_mailchimp_only = mailchimp_subscribers.partition do |mc_sub|
 | 
			
		||||
      our_supporters.any?{|s| s.email.downcase == mc_sub[:email_address].downcase}
 | 
			
		||||
    end
 | 
			
		||||
 
 | 
			
		||||
    _, in_our_side_only = our_supporters.partition do |s|
 | 
			
		||||
      mailchimp_subscribers.any?{|mc_sub| s.email.downcase == mc_sub[:email_address].downcase}
 | 
			
		||||
    end
 | 
			
		||||
 | 
			
		||||
    # if on our list, add to mailchimp
 | 
			
		||||
    output = in_our_side_only.map{|i|
 | 
			
		||||
      {method: 'POST', path: "lists/#{email_list.mailchimp_list_id}/members", body: {email_address: i.email, status: 'subscribed'}.to_json}
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    # if on mailchimp list, delete from mailchimp
 | 
			
		||||
    output = output.concat(in_mailchimp_only.map{|i| {method: 'DELETE', path: "lists/#{email_list.mailchimp_list_id}/members/#{i[:id]}"}})
 | 
			
		||||
 | 
			
		||||
    return output
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  def self.get_list_mailchimp_subscribers(email_list)
 | 
			
		||||
    mailchimp_token = get_mailchimp_token(email_list.tag_master.nonprofit.id)
 | 
			
		||||
    uri = base_uri(mailchimp_token)
 | 
			
		||||
    result = get(uri + "/lists/#{email_list.mailchimp_list_id}/members?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
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
							
								
								
									
										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
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
  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
 | 
			
		||||
    ["to_char(payments.date::timestamptz, 'YYYY-MM-DD HH24:MI:SS TZ') AS date",
 | 
			
		||||
     '(payments.gross_amount / 100.0)::money::text AS gross_amount',
 | 
			
		||||
| 
						 | 
				
			
			@ -344,7 +349,13 @@ module QueryPayments
 | 
			
		|||
    .concat(QuerySupporters.supporter_export_selections)
 | 
			
		||||
    .concat([
 | 
			
		||||
     "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.comment',
 | 
			
		||||
     "coalesce(nullif(campaigns_for_export.name, ''), 'None') AS campaign",
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -630,16 +630,13 @@ UNION DISTINCT
 | 
			
		|||
        end
 | 
			
		||||
      end
 | 
			
		||||
      if (time_range_params[:start])
 | 
			
		||||
        wip = time_range_params[:start].is_a?(DateTime) ? time_range_params[:start] : nil
 | 
			
		||||
        if (wip.nil? && time_range_params[:start].is_a?(Date))
 | 
			
		||||
          wip = time_range_params[:start].to_datetime
 | 
			
		||||
        end
 | 
			
		||||
        if(wip.nil? && time_range_params[:start].is_a?(String))
 | 
			
		||||
          wip = DateTime.parse(time_range_params[:start])
 | 
			
		||||
        start = parse_convert_datetime(time_range_params[:start])
 | 
			
		||||
        if (time_range_params[:end])
 | 
			
		||||
          end_datetime = parse_convert_datetime(time_range_params[:end])
 | 
			
		||||
        end
 | 
			
		||||
 | 
			
		||||
        unless wip.nil?
 | 
			
		||||
          return wip, wip + 1.year
 | 
			
		||||
        unless start.nil?
 | 
			
		||||
          return start, end_datetime ? end_datetime : start + 1.year
 | 
			
		||||
        end
 | 
			
		||||
      end
 | 
			
		||||
      raise ArgumentError.new("no valid time range provided")
 | 
			
		||||
| 
						 | 
				
			
			@ -672,5 +669,17 @@ UNION DISTINCT
 | 
			
		|||
    supporters = Supporter.where('supporters.nonprofit_id = ?', npo_id).includes(:recurring_donations)
 | 
			
		||||
    supporters.select{|s| s.recurring_donations.select{|rd| rd.active }.length > 1}
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  def self.parse_convert_datetime(date)
 | 
			
		||||
    if (date.is_a?(DateTime))
 | 
			
		||||
      return date
 | 
			
		||||
    end
 | 
			
		||||
    if (date.is_a?(Date))
 | 
			
		||||
      return date.to_datetime
 | 
			
		||||
    end
 | 
			
		||||
    if(date.is_a?(String))
 | 
			
		||||
      return DateTime.parse(date)
 | 
			
		||||
    end
 | 
			
		||||
  end
 | 
			
		||||
end
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -91,7 +91,7 @@ module UpdateDonation
 | 
			
		|||
      # edits_to_payments
 | 
			
		||||
      if is_offsite
 | 
			
		||||
        #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.gross_amount = data[:gross_amount] if data[:gross_amount]
 | 
			
		||||
        existing_payment.fee_total = data[:fee_total] if data[:fee_total]
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -9,7 +9,7 @@ module UpdateTickets
 | 
			
		|||
        bid_id: {is_integer: true},
 | 
			
		||||
        #note: nothing to check?
 | 
			
		||||
 | 
			
		||||
        checked_in: {included_in: ['true', 'false']}
 | 
			
		||||
        checked_in: {included_in: ['true', 'false', true, false]}
 | 
			
		||||
 | 
			
		||||
    })
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -40,7 +40,7 @@ module UpdateTickets
 | 
			
		|||
      edited = true
 | 
			
		||||
    end
 | 
			
		||||
 | 
			
		||||
    if data[:checked_in]
 | 
			
		||||
    unless data[:checked_in].nil?
 | 
			
		||||
      entities[:ticket_id].checked_in = data[:checked_in]
 | 
			
		||||
      edited = true
 | 
			
		||||
    end
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
							
								
								
									
										143
									
								
								package-lock.json
									
										
									
										generated
									
									
									
								
							
							
						
						
									
										143
									
								
								package-lock.json
									
										
									
										generated
									
									
									
								
							| 
						 | 
				
			
			@ -141,6 +141,15 @@
 | 
			
		|||
      "integrity": "sha512-WD2vUOKfBBVHxWUV9iMR9RMfpuf8HquxWeAq2yqGVL7Nc4JW2+sQama0pREMqzNI3Tutj0PyxYUJwuoxxvX+xA==",
 | 
			
		||||
      "dev": true
 | 
			
		||||
    },
 | 
			
		||||
    "@types/moment-timezone": {
 | 
			
		||||
      "version": "0.5.9",
 | 
			
		||||
      "resolved": "https://registry.npmjs.org/@types/moment-timezone/-/moment-timezone-0.5.9.tgz",
 | 
			
		||||
      "integrity": "sha512-tBf1QR8xAayQfI1xD+SMSNDMxi+aCYKEhjgVXTZt3sgxS2XusNX3jM6jJbFoY/ar1CK/PaYJoPkWs/mwcwgOqw==",
 | 
			
		||||
      "dev": true,
 | 
			
		||||
      "requires": {
 | 
			
		||||
        "moment": ">=2.14.0"
 | 
			
		||||
      }
 | 
			
		||||
    },
 | 
			
		||||
    "@types/node": {
 | 
			
		||||
      "version": "10.0.6",
 | 
			
		||||
      "resolved": "https://registry.npmjs.org/@types/node/-/node-10.0.6.tgz",
 | 
			
		||||
| 
						 | 
				
			
			@ -190,6 +199,15 @@
 | 
			
		|||
        "@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": {
 | 
			
		||||
      "version": "4.3.3",
 | 
			
		||||
      "resolved": "https://registry.npmjs.org/@types/sinon/-/sinon-4.3.3.tgz",
 | 
			
		||||
| 
						 | 
				
			
			@ -1843,9 +1861,8 @@
 | 
			
		|||
      "dev": true
 | 
			
		||||
    },
 | 
			
		||||
    "bootstrap-loader": {
 | 
			
		||||
      "version": "2.2.0",
 | 
			
		||||
      "resolved": "https://registry.npmjs.org/bootstrap-loader/-/bootstrap-loader-2.2.0.tgz",
 | 
			
		||||
      "integrity": "sha512-LG8/klminqsCCtPDDCMSCA50LdzmoRvC7JpvJAFFeqWAbSfSY0hZkPUEk5X4wygf33JuFGyiJ7CH/KVnT65I6A==",
 | 
			
		||||
      "version": "github:houdiniproject/bootstrap-loader#53cdc907485ba21c72470f2d8fb7011c616c823b",
 | 
			
		||||
      "from": "github:houdiniproject/bootstrap-loader#compiled_namespaced",
 | 
			
		||||
      "dev": true,
 | 
			
		||||
      "requires": {
 | 
			
		||||
        "chalk": "^1.1.3",
 | 
			
		||||
| 
						 | 
				
			
			@ -4764,6 +4781,23 @@
 | 
			
		|||
      "resolved": "https://registry.npmjs.org/focus-group/-/focus-group-0.3.1.tgz",
 | 
			
		||||
      "integrity": "sha1-4PMu2GsNq91v/OvfiY7LMuR/7c4="
 | 
			
		||||
    },
 | 
			
		||||
    "focus-trap": {
 | 
			
		||||
      "version": "3.0.0",
 | 
			
		||||
      "resolved": "https://registry.npmjs.org/focus-trap/-/focus-trap-3.0.0.tgz",
 | 
			
		||||
      "integrity": "sha512-jTFblf0tLWbleGjj2JZsAKbgtZTdL1uC48L8FcmSDl4c2vDoU4NycN1kgV5vJhuq1mxNFkw7uWZ1JAGlINWvyw==",
 | 
			
		||||
      "requires": {
 | 
			
		||||
        "tabbable": "^3.1.0",
 | 
			
		||||
        "xtend": "^4.0.1"
 | 
			
		||||
      }
 | 
			
		||||
    },
 | 
			
		||||
    "focus-trap-react": {
 | 
			
		||||
      "version": "4.0.1",
 | 
			
		||||
      "resolved": "https://registry.npmjs.org/focus-trap-react/-/focus-trap-react-4.0.1.tgz",
 | 
			
		||||
      "integrity": "sha512-UUZKVEn5cFbF6yUnW7lbXNW0iqN617ShSqYKgxctUvWw1wuylLtyVmC0RmPQNnJ/U+zoKc/djb0tZMs0uN/0QQ==",
 | 
			
		||||
      "requires": {
 | 
			
		||||
        "focus-trap": "^3.0.0"
 | 
			
		||||
      }
 | 
			
		||||
    },
 | 
			
		||||
    "for-in": {
 | 
			
		||||
      "version": "1.0.2",
 | 
			
		||||
      "resolved": "https://registry.npmjs.org/for-in/-/for-in-1.0.2.tgz",
 | 
			
		||||
| 
						 | 
				
			
			@ -4908,14 +4942,12 @@
 | 
			
		|||
        "balanced-match": {
 | 
			
		||||
          "version": "1.0.0",
 | 
			
		||||
          "bundled": true,
 | 
			
		||||
          "dev": true,
 | 
			
		||||
          "optional": true
 | 
			
		||||
          "dev": true
 | 
			
		||||
        },
 | 
			
		||||
        "brace-expansion": {
 | 
			
		||||
          "version": "1.1.11",
 | 
			
		||||
          "bundled": true,
 | 
			
		||||
          "dev": true,
 | 
			
		||||
          "optional": true,
 | 
			
		||||
          "requires": {
 | 
			
		||||
            "balanced-match": "^1.0.0",
 | 
			
		||||
            "concat-map": "0.0.1"
 | 
			
		||||
| 
						 | 
				
			
			@ -4930,20 +4962,17 @@
 | 
			
		|||
        "code-point-at": {
 | 
			
		||||
          "version": "1.1.0",
 | 
			
		||||
          "bundled": true,
 | 
			
		||||
          "dev": true,
 | 
			
		||||
          "optional": true
 | 
			
		||||
          "dev": true
 | 
			
		||||
        },
 | 
			
		||||
        "concat-map": {
 | 
			
		||||
          "version": "0.0.1",
 | 
			
		||||
          "bundled": true,
 | 
			
		||||
          "dev": true,
 | 
			
		||||
          "optional": true
 | 
			
		||||
          "dev": true
 | 
			
		||||
        },
 | 
			
		||||
        "console-control-strings": {
 | 
			
		||||
          "version": "1.1.0",
 | 
			
		||||
          "bundled": true,
 | 
			
		||||
          "dev": true,
 | 
			
		||||
          "optional": true
 | 
			
		||||
          "dev": true
 | 
			
		||||
        },
 | 
			
		||||
        "core-util-is": {
 | 
			
		||||
          "version": "1.0.2",
 | 
			
		||||
| 
						 | 
				
			
			@ -5060,8 +5089,7 @@
 | 
			
		|||
        "inherits": {
 | 
			
		||||
          "version": "2.0.3",
 | 
			
		||||
          "bundled": true,
 | 
			
		||||
          "dev": true,
 | 
			
		||||
          "optional": true
 | 
			
		||||
          "dev": true
 | 
			
		||||
        },
 | 
			
		||||
        "ini": {
 | 
			
		||||
          "version": "1.3.5",
 | 
			
		||||
| 
						 | 
				
			
			@ -5073,7 +5101,6 @@
 | 
			
		|||
          "version": "1.0.0",
 | 
			
		||||
          "bundled": true,
 | 
			
		||||
          "dev": true,
 | 
			
		||||
          "optional": true,
 | 
			
		||||
          "requires": {
 | 
			
		||||
            "number-is-nan": "^1.0.0"
 | 
			
		||||
          }
 | 
			
		||||
| 
						 | 
				
			
			@ -5088,7 +5115,6 @@
 | 
			
		|||
          "version": "3.0.4",
 | 
			
		||||
          "bundled": true,
 | 
			
		||||
          "dev": true,
 | 
			
		||||
          "optional": true,
 | 
			
		||||
          "requires": {
 | 
			
		||||
            "brace-expansion": "^1.1.7"
 | 
			
		||||
          }
 | 
			
		||||
| 
						 | 
				
			
			@ -5096,14 +5122,12 @@
 | 
			
		|||
        "minimist": {
 | 
			
		||||
          "version": "0.0.8",
 | 
			
		||||
          "bundled": true,
 | 
			
		||||
          "dev": true,
 | 
			
		||||
          "optional": true
 | 
			
		||||
          "dev": true
 | 
			
		||||
        },
 | 
			
		||||
        "minipass": {
 | 
			
		||||
          "version": "2.2.4",
 | 
			
		||||
          "bundled": true,
 | 
			
		||||
          "dev": true,
 | 
			
		||||
          "optional": true,
 | 
			
		||||
          "requires": {
 | 
			
		||||
            "safe-buffer": "^5.1.1",
 | 
			
		||||
            "yallist": "^3.0.0"
 | 
			
		||||
| 
						 | 
				
			
			@ -5122,7 +5146,6 @@
 | 
			
		|||
          "version": "0.5.1",
 | 
			
		||||
          "bundled": true,
 | 
			
		||||
          "dev": true,
 | 
			
		||||
          "optional": true,
 | 
			
		||||
          "requires": {
 | 
			
		||||
            "minimist": "0.0.8"
 | 
			
		||||
          }
 | 
			
		||||
| 
						 | 
				
			
			@ -5203,8 +5226,7 @@
 | 
			
		|||
        "number-is-nan": {
 | 
			
		||||
          "version": "1.0.1",
 | 
			
		||||
          "bundled": true,
 | 
			
		||||
          "dev": true,
 | 
			
		||||
          "optional": true
 | 
			
		||||
          "dev": true
 | 
			
		||||
        },
 | 
			
		||||
        "object-assign": {
 | 
			
		||||
          "version": "4.1.1",
 | 
			
		||||
| 
						 | 
				
			
			@ -5216,7 +5238,6 @@
 | 
			
		|||
          "version": "1.4.0",
 | 
			
		||||
          "bundled": true,
 | 
			
		||||
          "dev": true,
 | 
			
		||||
          "optional": true,
 | 
			
		||||
          "requires": {
 | 
			
		||||
            "wrappy": "1"
 | 
			
		||||
          }
 | 
			
		||||
| 
						 | 
				
			
			@ -5338,7 +5359,6 @@
 | 
			
		|||
          "version": "1.0.2",
 | 
			
		||||
          "bundled": true,
 | 
			
		||||
          "dev": true,
 | 
			
		||||
          "optional": true,
 | 
			
		||||
          "requires": {
 | 
			
		||||
            "code-point-at": "^1.0.0",
 | 
			
		||||
            "is-fullwidth-code-point": "^1.0.0",
 | 
			
		||||
| 
						 | 
				
			
			@ -9190,9 +9210,9 @@
 | 
			
		|||
      }
 | 
			
		||||
    },
 | 
			
		||||
    "moment": {
 | 
			
		||||
      "version": "2.9.0",
 | 
			
		||||
      "resolved": "https://registry.npmjs.org/moment/-/moment-2.9.0.tgz",
 | 
			
		||||
      "integrity": "sha1-d+wRdfopT0JifxDI5t5jAsA29tU="
 | 
			
		||||
      "version": "2.22.2",
 | 
			
		||||
      "resolved": "https://registry.npmjs.org/moment/-/moment-2.22.2.tgz",
 | 
			
		||||
      "integrity": "sha1-PCV/mDn8DpP/UxSWMiOeuQeD/2Y="
 | 
			
		||||
    },
 | 
			
		||||
    "moment-range": {
 | 
			
		||||
      "version": "2.2.0",
 | 
			
		||||
| 
						 | 
				
			
			@ -9200,11 +9220,11 @@
 | 
			
		|||
      "integrity": "sha1-sCV1d4pKxQpld3k59cXXV/g/zYU="
 | 
			
		||||
    },
 | 
			
		||||
    "moment-timezone": {
 | 
			
		||||
      "version": "0.4.1",
 | 
			
		||||
      "resolved": "https://registry.npmjs.org/moment-timezone/-/moment-timezone-0.4.1.tgz",
 | 
			
		||||
      "integrity": "sha1-gfWYw61eIs2teWtn7NjYjQ9bqgY=",
 | 
			
		||||
      "version": "0.5.21",
 | 
			
		||||
      "resolved": "https://registry.npmjs.org/moment-timezone/-/moment-timezone-0.5.21.tgz",
 | 
			
		||||
      "integrity": "sha512-j96bAh4otsgj3lKydm3K7kdtA3iKf2m6MY2iSYCzCm5a1zmHo1g+aK3068dDEeocLZQIS9kU8bsdQHLqEvgW0A==",
 | 
			
		||||
      "requires": {
 | 
			
		||||
        "moment": ">= 2.6.0"
 | 
			
		||||
        "moment": ">= 2.9.0"
 | 
			
		||||
      }
 | 
			
		||||
    },
 | 
			
		||||
    "moo": {
 | 
			
		||||
| 
						 | 
				
			
			@ -11809,6 +11829,16 @@
 | 
			
		|||
        "prop-types": "^15.6.0"
 | 
			
		||||
      }
 | 
			
		||||
    },
 | 
			
		||||
    "react-aria-modal": {
 | 
			
		||||
      "version": "3.0.0",
 | 
			
		||||
      "resolved": "https://registry.npmjs.org/react-aria-modal/-/react-aria-modal-3.0.0.tgz",
 | 
			
		||||
      "integrity": "sha512-qudZTYYkrahJIPXssuI/MYQiskkIg4pW+3eVI1mPCn3XbNQ2eun7/3ghVV4IPYTSXERRX9LVQbbfmoFu2Ie9Gg==",
 | 
			
		||||
      "requires": {
 | 
			
		||||
        "focus-trap-react": "^4.0.0",
 | 
			
		||||
        "no-scroll": "^2.1.1",
 | 
			
		||||
        "react-displace": "^2.3.0"
 | 
			
		||||
      }
 | 
			
		||||
    },
 | 
			
		||||
    "react-autocomplete": {
 | 
			
		||||
      "version": "1.8.1",
 | 
			
		||||
      "resolved": "https://registry.npmjs.org/react-autocomplete/-/react-autocomplete-1.8.1.tgz",
 | 
			
		||||
| 
						 | 
				
			
			@ -11818,6 +11848,11 @@
 | 
			
		|||
        "prop-types": "^15.5.10"
 | 
			
		||||
      }
 | 
			
		||||
    },
 | 
			
		||||
    "react-displace": {
 | 
			
		||||
      "version": "2.3.0",
 | 
			
		||||
      "resolved": "https://registry.npmjs.org/react-displace/-/react-displace-2.3.0.tgz",
 | 
			
		||||
      "integrity": "sha512-T8g/lyn3IX8kxLO4k4vJ/oIO9G72pRTc9GYslqKsfPcN4gY5+FYR5OHxeTH1skPjVylJrveGE3OC2qCt3BuHeA=="
 | 
			
		||||
    },
 | 
			
		||||
    "react-dom": {
 | 
			
		||||
      "version": "16.3.2",
 | 
			
		||||
      "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-16.3.2.tgz",
 | 
			
		||||
| 
						 | 
				
			
			@ -11841,9 +11876,10 @@
 | 
			
		|||
      }
 | 
			
		||||
    },
 | 
			
		||||
    "react-is": {
 | 
			
		||||
      "version": "16.3.2",
 | 
			
		||||
      "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.3.2.tgz",
 | 
			
		||||
      "integrity": "sha512-ybEM7YOr4yBgFd6w8dJqwxegqZGJNBZl6U27HnGKuTZmDvVrD5quWOK/wAnMywiZzW+Qsk+l4X2c70+thp/A8Q=="
 | 
			
		||||
      "version": "16.5.2",
 | 
			
		||||
      "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.5.2.tgz",
 | 
			
		||||
      "integrity": "sha512-hSl7E6l25GTjNEZATqZIuWOgSnpXb3kD0DVCujmg46K5zLxsbiKaaT6VO9slkSBDPZfYs30lwfJwbOFOnoEnKQ==",
 | 
			
		||||
      "dev": true
 | 
			
		||||
    },
 | 
			
		||||
    "react-lifecycles-compat": {
 | 
			
		||||
      "version": "3.0.4",
 | 
			
		||||
| 
						 | 
				
			
			@ -11863,14 +11899,27 @@
 | 
			
		|||
      }
 | 
			
		||||
    },
 | 
			
		||||
    "react-test-renderer": {
 | 
			
		||||
      "version": "16.3.2",
 | 
			
		||||
      "resolved": "https://registry.npmjs.org/react-test-renderer/-/react-test-renderer-16.3.2.tgz",
 | 
			
		||||
      "integrity": "sha512-lL8WHIpCTMdSe+CRkt0rfMxBkJFyhVrpdQ54BaJRIrXf9aVmbeHbRA8GFRpTvohPN5tPzMabmrzW2PUfWCfWwQ==",
 | 
			
		||||
      "version": "16.5.2",
 | 
			
		||||
      "resolved": "https://registry.npmjs.org/react-test-renderer/-/react-test-renderer-16.5.2.tgz",
 | 
			
		||||
      "integrity": "sha512-AGbJYbCVx1J6jdUgI4s0hNp+9LxlgzKvXl0ROA3DHTrtjAr00Po1RhDZ/eAq2VC/ww8AHgpDXULh5V2rhEqqJg==",
 | 
			
		||||
      "dev": true,
 | 
			
		||||
      "requires": {
 | 
			
		||||
        "fbjs": "^0.8.16",
 | 
			
		||||
        "object-assign": "^4.1.1",
 | 
			
		||||
        "prop-types": "^15.6.0",
 | 
			
		||||
        "react-is": "^16.3.2"
 | 
			
		||||
        "prop-types": "^15.6.2",
 | 
			
		||||
        "react-is": "^16.5.2",
 | 
			
		||||
        "schedule": "^0.5.0"
 | 
			
		||||
      },
 | 
			
		||||
      "dependencies": {
 | 
			
		||||
        "prop-types": {
 | 
			
		||||
          "version": "15.6.2",
 | 
			
		||||
          "resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.6.2.tgz",
 | 
			
		||||
          "integrity": "sha512-3pboPvLiWD7dkI3qf3KbUe6hKFKa52w+AE0VCqECtf+QHAKgOL37tTaNCnuX1nAAQ4ZhyP+kYVKf8rLmJ/feDQ==",
 | 
			
		||||
          "dev": true,
 | 
			
		||||
          "requires": {
 | 
			
		||||
            "loose-envify": "^1.3.1",
 | 
			
		||||
            "object-assign": "^4.1.1"
 | 
			
		||||
          }
 | 
			
		||||
        }
 | 
			
		||||
      }
 | 
			
		||||
    },
 | 
			
		||||
    "react-text-mask": {
 | 
			
		||||
| 
						 | 
				
			
			@ -12693,6 +12742,15 @@
 | 
			
		|||
      "resolved": "https://registry.npmjs.org/sax/-/sax-1.1.5.tgz",
 | 
			
		||||
      "integrity": "sha1-HaUKjQDN7NWUBWWfX/hTSf53N0M="
 | 
			
		||||
    },
 | 
			
		||||
    "schedule": {
 | 
			
		||||
      "version": "0.5.0",
 | 
			
		||||
      "resolved": "https://registry.npmjs.org/schedule/-/schedule-0.5.0.tgz",
 | 
			
		||||
      "integrity": "sha512-HUcJicG5Ou8xfR//c2rPT0lPIRR09vVvN81T9fqfVgBmhERUbDEQoYKjpBxbueJnCPpSu2ujXzOnRQt6x9o/jw==",
 | 
			
		||||
      "dev": true,
 | 
			
		||||
      "requires": {
 | 
			
		||||
        "object-assign": "^4.1.1"
 | 
			
		||||
      }
 | 
			
		||||
    },
 | 
			
		||||
    "schema-utils": {
 | 
			
		||||
      "version": "0.3.0",
 | 
			
		||||
      "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-0.3.0.tgz",
 | 
			
		||||
| 
						 | 
				
			
			@ -13683,6 +13741,11 @@
 | 
			
		|||
        "acorn-node": "^1.2.0"
 | 
			
		||||
      }
 | 
			
		||||
    },
 | 
			
		||||
    "tabbable": {
 | 
			
		||||
      "version": "3.1.1",
 | 
			
		||||
      "resolved": "https://registry.npmjs.org/tabbable/-/tabbable-3.1.1.tgz",
 | 
			
		||||
      "integrity": "sha512-583MHIOwictf7+zbxqO/L5fBqMN6Li4SJ1XTKQA9WzHRA7c2BB+D+Ny7Y6kGqU2u+rHK59+oRzrBvMU53aZz+A=="
 | 
			
		||||
    },
 | 
			
		||||
    "tapable": {
 | 
			
		||||
      "version": "0.2.8",
 | 
			
		||||
      "resolved": "https://registry.npmjs.org/tapable/-/tapable-0.2.8.tgz",
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
							
								
								
									
										10
									
								
								package.json
									
										
									
									
									
								
							
							
						
						
									
										10
									
								
								package.json
									
										
									
									
									
								
							| 
						 | 
				
			
			@ -24,11 +24,13 @@
 | 
			
		|||
    "@types/jquery": "^3.3.1",
 | 
			
		||||
    "@types/jsdom": "^11.0.4",
 | 
			
		||||
    "@types/lodash": "^4.14.106",
 | 
			
		||||
    "@types/moment-timezone": "^0.5.9",
 | 
			
		||||
    "@types/prop-types": "^15.5.5",
 | 
			
		||||
    "@types/react": "^16.1.0",
 | 
			
		||||
    "@types/react-dom": "^16.0.5",
 | 
			
		||||
    "@types/react-intl": "^2.3.7",
 | 
			
		||||
    "@types/react-test-renderer": "^16.0.1",
 | 
			
		||||
    "@types/react-text-mask": "^5.4.2",
 | 
			
		||||
    "@types/sinon": "^4.3.3",
 | 
			
		||||
    "@types/validator": "^9.4.1",
 | 
			
		||||
    "babel-core": "^6.26.0",
 | 
			
		||||
| 
						 | 
				
			
			@ -36,7 +38,7 @@
 | 
			
		|||
    "babel-preset-env": "^1.6.1",
 | 
			
		||||
    "babel-preset-es2015": "^6.24.1",
 | 
			
		||||
    "bootstrap": "^3.3.7",
 | 
			
		||||
    "bootstrap-loader": "^2.2.0",
 | 
			
		||||
    "bootstrap-loader": "github:houdiniproject/bootstrap-loader#compiled_namespaced",
 | 
			
		||||
    "bootstrap-sass": "^3.3.7",
 | 
			
		||||
    "browserify": "13.0.1",
 | 
			
		||||
    "browserify-incremental": "3.1.1",
 | 
			
		||||
| 
						 | 
				
			
			@ -117,9 +119,9 @@
 | 
			
		|||
    "mobx-react-devtools": "^5.0.1",
 | 
			
		||||
    "mobx-react-form": "github:houdiniproject/mobx-react-form#our_fix",
 | 
			
		||||
    "mobx-utils": "^5.0.1",
 | 
			
		||||
    "moment": "2.9.0",
 | 
			
		||||
    "moment": "^2.22.2",
 | 
			
		||||
    "moment-range": "2.2.0",
 | 
			
		||||
    "moment-timezone": "0.4.1",
 | 
			
		||||
    "moment-timezone": "^0.5.21",
 | 
			
		||||
    "no-scroll": "^2.1.0",
 | 
			
		||||
    "parsleyjs": "2.0.7",
 | 
			
		||||
    "percent": "1.1.1",
 | 
			
		||||
| 
						 | 
				
			
			@ -130,10 +132,10 @@
 | 
			
		|||
    "quill": "^1.3.6",
 | 
			
		||||
    "ramda": "^0.21.0",
 | 
			
		||||
    "react": "^16.2.0",
 | 
			
		||||
    "react-aria-modal": "^3.0.0",
 | 
			
		||||
    "react-autocomplete": "^1.8.1",
 | 
			
		||||
    "react-dom": "^16.3.1",
 | 
			
		||||
    "react-intl": "^2.4.0",
 | 
			
		||||
    "react-test-renderer": "^16.3.1",
 | 
			
		||||
    "react-text-mask": "^5.3.0",
 | 
			
		||||
    "shuffle-array": "1.0.1",
 | 
			
		||||
    "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({
 | 
			
		||||
              date: new_date.to_time_in_current_zone,
 | 
			
		||||
              date: new_date,
 | 
			
		||||
              amount: new_amount,
 | 
			
		||||
 | 
			
		||||
              designation: new_designation,
 | 
			
		||||
| 
						 | 
				
			
			@ -279,7 +279,7 @@ describe UpdateDonation do
 | 
			
		|||
          donation.reload
 | 
			
		||||
          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
 | 
			
		||||
          expect(payment.attributes).to eq expected_p1
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -367,7 +367,7 @@ describe UpdateDonation do
 | 
			
		|||
            donation.reload
 | 
			
		||||
            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
 | 
			
		||||
            expect(payment.attributes).to eq expected_p1
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -188,6 +188,15 @@ describe UpdateTickets do
 | 
			
		|||
      expect(ticket.attributes).to eq expected
 | 
			
		||||
    end
 | 
			
		||||
 | 
			
		||||
    it 'success editing checked_in as a boolean' do
 | 
			
		||||
      result = UpdateTickets.update(basic_valid_ticket_input.merge(checked_in:true))
 | 
			
		||||
      expected = general_expected.merge(checked_in: true)
 | 
			
		||||
 | 
			
		||||
      expect(result.attributes).to eq expected
 | 
			
		||||
      ticket.reload
 | 
			
		||||
      expect(ticket.attributes).to eq expected
 | 
			
		||||
    end
 | 
			
		||||
 | 
			
		||||
    it 'success editing token' do
 | 
			
		||||
      result = UpdateTickets.update(basic_valid_ticket_input.merge(token:source_token.token))
 | 
			
		||||
      expected = general_expected.merge(source_token_id: source_token.id)
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -1,7 +1,13 @@
 | 
			
		|||
# License: AGPL-3.0-or-later WITH Web-Template-Output-Additional-Permission-3.0-or-later
 | 
			
		||||
module MockHelpers
 | 
			
		||||
  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
 | 
			
		||||
 | 
			
		||||
  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
 | 
			
		||||
 | 
			
		||||
interface ValidationInput {
 | 
			
		||||
| 
						 | 
				
			
			@ -171,7 +173,7 @@ interface FieldHandlers {
 | 
			
		|||
    onError?(e:Field):any
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
interface FieldDefinition {
 | 
			
		||||
interface FieldDefinition<TInputType=any> {
 | 
			
		||||
    name: string
 | 
			
		||||
    key?: string
 | 
			
		||||
    label?: string
 | 
			
		||||
| 
						 | 
				
			
			@ -190,6 +192,8 @@ interface FieldDefinition {
 | 
			
		|||
    rules?: string
 | 
			
		||||
    id?:string,
 | 
			
		||||
    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 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…
	
	Add table
		
		Reference in a new issue